diff --git a/.editorconfig b/.editorconfig index 527c2c27d0..97fb47d4a0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,3 +12,6 @@ indent_size = 2 [*.md] trim_trailing_whitespace = false + +[*.hbs] +insert_final_newline = false diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index ea69b83263..12ea82c4af 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -58,3 +58,16 @@ bbe5d8d5cf1220165842985c0e2cd4c454d501cd # DEV: Template colocation for sidebar files 95c7cdab941a56686ac5831d2a5c5eca38d780c5 + +# DEV: Apply prettier to hbs files +c8e2e37fa77d3c3c69c7572866017e9bb92befa3 + +# DEV: Apply syntax_tree to... +5a003715d366e1d871f9fcb0656dc9e23e9c2259 +64171730827c58df26a7ad75f0e58f17c2add118 +b0fda61a8e75c81e3458c8af9d2afe9d32183457 +cb932d6ee1b3b3571e4d4d9118635e2dbf58f0ef +0cf6421716d0908da57ad7743a2decb08588b48a +7c77cc6a580d7cb49f8c19ceee8cfdd08862259d +436b3b392b9c917510d4ff0d73a5167cd3eb936c +055310cea496519a996b9c3bf4dc7e716cfe62ba diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 14d4e59c5f..b2125eea16 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -68,6 +68,10 @@ jobs: if: ${{ !cancelled() }} run: bundle exec rubocop --parallel . + - name: syntax_tree + if: ${{ !cancelled() }} + run: bundle exec stree check Gemfile $(git ls-files '*.rb') $(git ls-files '*.rake') + - name: ESLint (core) if: ${{ !cancelled() }} run: yarn eslint app/assets/javascripts diff --git a/.rubocop.yml b/.rubocop.yml index b7edfe4894..4d4ad6d8d8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,5 +1,5 @@ inherit_gem: - rubocop-discourse: default.yml + rubocop-discourse: stree-compat.yml # Still work to do in ensuring we don't link old files Discourse/NoAddReferenceOrAliasesActiveRecordMigration: diff --git a/.streerc b/.streerc index 0bc4379d46..cc0be49453 100644 --- a/.streerc +++ b/.streerc @@ -1,2 +1,2 @@ --print-width=100 ---plugins=plugin/trailing_comma +--plugins=plugin/trailing_comma,disable_ternary diff --git a/Gemfile b/Gemfile index 8aef1556fa..ef77228f74 100644 --- a/Gemfile +++ b/Gemfile @@ -1,51 +1,51 @@ # frozen_string_literal: true -source 'https://rubygems.org' +source "https://rubygems.org" # if there is a super emergency and rubygems is playing up, try #source 'http://production.cf.rubygems.org' -gem 'bootsnap', require: false, platform: :mri +gem "bootsnap", require: false, platform: :mri def rails_master? - ENV["RAILS_MASTER"] == '1' + ENV["RAILS_MASTER"] == "1" end if rails_master? - gem 'arel', git: 'https://github.com/rails/arel.git' - gem 'rails', git: 'https://github.com/rails/rails.git' + gem "arel", git: "https://github.com/rails/arel.git" + gem "rails", git: "https://github.com/rails/rails.git" else # NOTE: Until rubygems gives us optional dependencies we are stuck with this needing to be explicit # this allows us to include the bits of rails we use without pieces we do not. # # To issue a rails update bump the version number here - rails_version = '7.0.3.1' - gem 'actionmailer', rails_version - gem 'actionpack', rails_version - gem 'actionview', rails_version - gem 'activemodel', rails_version - gem 'activerecord', rails_version - gem 'activesupport', rails_version - gem 'railties', rails_version - gem 'sprockets-rails' + rails_version = "7.0.3.1" + gem "actionmailer", rails_version + gem "actionpack", rails_version + gem "actionview", rails_version + gem "activemodel", rails_version + gem "activerecord", rails_version + gem "activesupport", rails_version + gem "railties", rails_version + gem "sprockets-rails" end -gem 'json' +gem "json" # TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals # This is a desired upgrade we should get to. -gem 'sprockets', '3.7.2' +gem "sprockets", "3.7.2" # this will eventually be added to rails, # allows us to precompile all our templates in the unicorn master -gem 'actionview_precompiler', require: false +gem "actionview_precompiler", require: false -gem 'discourse-seed-fu' +gem "discourse-seed-fu" -gem 'mail', git: 'https://github.com/discourse/mail.git' -gem 'mini_mime' -gem 'mini_suffix' +gem "mail", git: "https://github.com/discourse/mail.git" +gem "mini_mime" +gem "mini_suffix" -gem 'redis' +gem "redis" # This is explicitly used by Sidekiq and is an optional dependency. # We tell Sidekiq to use the namespace "sidekiq" which triggers this @@ -53,79 +53,79 @@ gem 'redis' # redis namespace support is optional # We already namespace stuff in DiscourseRedis, so we should consider # just using a single implementation in core vs having 2 namespace implementations -gem 'redis-namespace' +gem "redis-namespace" # NOTE: AM serializer gets a lot slower with recent updates # we used an old branch which is the fastest one out there # are long term goal here is to fork this gem so we have a # better maintained living fork -gem 'active_model_serializers', '~> 0.8.3' +gem "active_model_serializers", "~> 0.8.3" -gem 'http_accept_language', require: false +gem "http_accept_language", require: false -gem 'discourse-fonts', require: 'discourse_fonts' +gem "discourse-fonts", require: "discourse_fonts" -gem 'message_bus' +gem "message_bus" -gem 'rails_multisite' +gem "rails_multisite" -gem 'fast_xs', platform: :ruby +gem "fast_xs", platform: :ruby -gem 'xorcist' +gem "xorcist" -gem 'fastimage' +gem "fastimage" -gem 'aws-sdk-s3', require: false -gem 'aws-sdk-sns', require: false -gem 'excon', require: false -gem 'unf', require: false +gem "aws-sdk-s3", require: false +gem "aws-sdk-sns", require: false +gem "excon", require: false +gem "unf", require: false -gem 'email_reply_trimmer' +gem "email_reply_trimmer" -gem 'image_optim' -gem 'multi_json' -gem 'mustache' -gem 'nokogiri' -gem 'loofah' -gem 'css_parser', require: false +gem "image_optim" +gem "multi_json" +gem "mustache" +gem "nokogiri" +gem "loofah" +gem "css_parser", require: false -gem 'omniauth' -gem 'omniauth-facebook' -gem 'omniauth-twitter' -gem 'omniauth-github' +gem "omniauth" +gem "omniauth-facebook" +gem "omniauth-twitter" +gem "omniauth-github" -gem 'omniauth-oauth2', require: false +gem "omniauth-oauth2", require: false -gem 'omniauth-google-oauth2' +gem "omniauth-google-oauth2" # pending: https://github.com/ohler55/oj/issues/789 -gem 'oj', '3.13.14' +gem "oj", "3.13.14" -gem 'pg' -gem 'mini_sql' -gem 'pry-rails', require: false -gem 'pry-byebug', require: false -gem 'r2', require: false -gem 'rake' +gem "pg" +gem "mini_sql" +gem "pry-rails", require: false +gem "pry-byebug", require: false +gem "r2", require: false +gem "rake" -gem 'thor', require: false -gem 'diffy', require: false -gem 'rinku' -gem 'sidekiq' -gem 'mini_scheduler' +gem "thor", require: false +gem "diffy", require: false +gem "rinku" +gem "sidekiq" +gem "mini_scheduler" -gem 'execjs', require: false -gem 'mini_racer' +gem "execjs", require: false +gem "mini_racer" -gem 'highline', require: false +gem "highline", require: false -gem 'rack' +gem "rack" -gem 'rack-protection' # security -gem 'cbor', require: false -gem 'cose', require: false -gem 'addressable' -gem 'json_schemer' +gem "rack-protection" # security +gem "cbor", require: false +gem "cose", require: false +gem "addressable" +gem "json_schemer" gem "net-smtp", require: false gem "net-imap", require: false @@ -135,146 +135,152 @@ gem "digest", require: false # Gems used only for assets and not required in production environments by default. # Allow everywhere for now cause we are allowing asset debugging in production group :assets do - gem 'uglifier' + gem "uglifier" end group :test do - gem 'capybara', require: false - gem 'webmock', require: false - gem 'fakeweb', require: false - gem 'minitest', require: false - gem 'simplecov', require: false - gem 'selenium-webdriver', require: false + gem "capybara", require: false + gem "webmock", require: false + gem "fakeweb", require: false + gem "minitest", require: false + gem "simplecov", require: false + gem "selenium-webdriver", require: false gem "test-prof" - gem 'webdrivers', require: false + gem "webdrivers", require: false end group :test, :development do - gem 'rspec' - gem 'listen', require: false - gem 'certified', require: false - gem 'fabrication', require: false - gem 'mocha', require: false + gem "rspec" + gem "listen", require: false + gem "certified", require: false + gem "fabrication", require: false + gem "mocha", require: false - gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false + gem "rb-fsevent", require: RUBY_PLATFORM =~ /darwin/i ? "rb-fsevent" : false - gem 'rspec-rails' + gem "rspec-rails" - gem 'shoulda-matchers', require: false - gem 'rspec-html-matchers' - gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri - gem 'rubocop-discourse', require: false - gem 'parallel_tests' + gem "shoulda-matchers", require: false + gem "rspec-html-matchers" + gem "byebug", require: ENV["RM_INFO"].nil?, platform: :mri + gem "rubocop-discourse", require: false + gem "parallel_tests" - gem 'rswag-specs' + gem "rswag-specs" - gem 'annotate' + gem "annotate" + + gem "syntax_tree" + gem "syntax_tree-disable_ternary" end group :development do - gem 'ruby-prof', require: false, platform: :mri - gem 'bullet', require: !!ENV['BULLET'] - gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS'] - gem 'binding_of_caller' - gem 'yaml-lint' + gem "ruby-prof", require: false, platform: :mri + gem "bullet", require: !!ENV["BULLET"] + gem "better_errors", platform: :mri, require: !!ENV["BETTER_ERRORS"] + gem "binding_of_caller" + gem "yaml-lint" end if ENV["ALLOW_DEV_POPULATE"] == "1" - gem 'discourse_dev_assets' - gem 'faker', "~> 2.16" + gem "discourse_dev_assets" + gem "faker", "~> 2.16" else group :development, :test do - gem 'discourse_dev_assets' - gem 'faker', "~> 2.16" + gem "discourse_dev_assets" + gem "faker", "~> 2.16" end end # this is an optional gem, it provides a high performance replacement # to String#blank? a method that is called quite frequently in current # ActiveRecord, this may change in the future -gem 'fast_blank', platform: :ruby +gem "fast_blank", platform: :ruby # this provides a very efficient lru cache -gem 'lru_redux' +gem "lru_redux" -gem 'htmlentities', require: false +gem "htmlentities", require: false # IMPORTANT: mini profiler monkey patches, so it better be required last # If you want to amend mini profiler to do the monkey patches in the railties # we are open to it. by deferring require to the initializer we can configure discourse installs without it -gem 'rack-mini-profiler', require: ['enable_rails_patches'] +gem "rack-mini-profiler", require: ["enable_rails_patches"] -gem 'unicorn', require: false, platform: :ruby -gem 'puma', require: false -gem 'rbtrace', require: false, platform: :mri -gem 'gc_tracer', require: false, platform: :mri +gem "unicorn", require: false, platform: :ruby +gem "puma", require: false +gem "rbtrace", require: false, platform: :mri +gem "gc_tracer", require: false, platform: :mri # required for feed importing and embedding -gem 'ruby-readability', require: false +gem "ruby-readability", require: false # rss gem is a bundled gem from Ruby 3 onwards -gem 'rss', require: false +gem "rss", require: false -gem 'stackprof', require: false, platform: :mri -gem 'memory_profiler', require: false, platform: :mri +gem "stackprof", require: false, platform: :mri +gem "memory_profiler", require: false, platform: :mri -gem 'cppjieba_rb', require: false +gem "cppjieba_rb", require: false -gem 'lograge', require: false -gem 'logstash-event', require: false -gem 'logstash-logger', require: false -gem 'logster' +gem "lograge", require: false +gem "logstash-event", require: false +gem "logstash-logger", require: false +gem "logster" # NOTE: later versions of sassc are causing a segfault, possibly dependent on processer architecture # and until resolved should be locked at 2.0.1 -gem 'sassc', '2.0.1', require: false +gem "sassc", "2.0.1", require: false gem "sassc-rails" -gem 'rotp', require: false +gem "rotp", require: false -gem 'rqrcode' +gem "rqrcode" -gem 'rubyzip', require: false +gem "rubyzip", require: false -gem 'sshkey', require: false +gem "sshkey", require: false -gem 'rchardet', require: false -gem 'lz4-ruby', require: false, platform: :ruby +gem "rchardet", require: false +gem "lz4-ruby", require: false, platform: :ruby -gem 'sanitize' +gem "sanitize" if ENV["IMPORT"] == "1" - gem 'mysql2' - gem 'redcarpet' + gem "mysql2" + gem "redcarpet" # NOTE: in import mode the version of sqlite can matter a lot, so we stick it to a specific one - gem 'sqlite3', '~> 1.3', '>= 1.3.13' - gem 'ruby-bbcode-to-md', git: 'https://github.com/nlalonde/ruby-bbcode-to-md' - gem 'reverse_markdown' - gem 'tiny_tds' - gem 'csv' + gem "sqlite3", "~> 1.3", ">= 1.3.13" + gem "ruby-bbcode-to-md", git: "https://github.com/nlalonde/ruby-bbcode-to-md" + gem "reverse_markdown" + gem "tiny_tds" + gem "csv" - gem 'parallel', require: false + gem "parallel", require: false end # workaround for openssl 3.0, see # https://github.com/pushpad/web-push/pull/2 -gem 'web-push', require: false, git: 'https://github.com/xfalcox/web-push', branch: 'openssl-3-compat' -gem 'colored2', require: false -gem 'maxminddb' +gem "web-push", + require: false, + git: "https://github.com/xfalcox/web-push", + branch: "openssl-3-compat" +gem "colored2", require: false +gem "maxminddb" -gem 'rails_failover', require: false +gem "rails_failover", require: false -gem 'faraday' -gem 'faraday-retry' +gem "faraday" +gem "faraday-retry" # workaround for faraday-net_http, see # https://github.com/ruby/net-imap/issues/16#issuecomment-803086765 -gem 'net-http' +gem "net-http" # workaround for prometheus-client -gem 'webrick', require: false +gem "webrick", require: false # Workaround until Ruby ships with cgi version 0.3.6 or higher. gem "cgi", ">= 0.3.6", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6edfc5a9f3..06f20a01c2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -194,7 +194,7 @@ GEM libv8-node (16.10.0.0-x86_64-darwin) libv8-node (16.10.0.0-x86_64-darwin-19) libv8-node (16.10.0.0-x86_64-linux) - listen (3.7.1) + listen (3.8.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) literate_randomizer (0.4.0) @@ -215,7 +215,7 @@ GEM matrix (0.4.2) maxminddb (0.1.22) memory_profiler (1.0.1) - message_bus (4.3.0) + message_bus (4.3.1) rack (>= 1.1.3) method_source (1.0.0) mini_mime (1.1.2) @@ -301,8 +301,9 @@ GEM parser (3.2.0.0) ast (~> 2.4.1) pg (1.4.5) + prettier_print (1.2.0) progress (3.6.0) - pry (0.14.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) pry-byebug (3.10.1) @@ -371,13 +372,13 @@ GEM rspec-mocks (~> 3.12.0) rspec-core (3.12.0) rspec-support (~> 3.12.0) - rspec-expectations (3.12.1) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-html-matchers (0.10.0) nokogiri (~> 1) rspec (>= 3.0.0.a) - rspec-mocks (3.12.1) + rspec-mocks (3.12.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-rails (6.0.1) @@ -396,19 +397,19 @@ GEM json-schema (>= 2.2, < 4.0) railties (>= 3.1, < 7.1) rspec-core (>= 2.14) - rubocop (1.42.0) + rubocop (1.43.0) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.2.1) + parser (>= 3.2.0.0) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) rubocop-ast (>= 1.24.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 3.0) + unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.24.1) parser (>= 3.1.1.0) - rubocop-discourse (3.0.1) + rubocop-discourse (3.0.3) rubocop (>= 1.1.0) rubocop-rspec (>= 2.0.0) rubocop-rspec (2.16.0) @@ -460,6 +461,9 @@ GEM sprockets (>= 3.0.0) sshkey (2.0.0) stackprof (0.2.23) + syntax_tree (5.2.0) + prettier_print (>= 1.2.0) + syntax_tree-disable_ternary (1.0.0) test-prof (1.1.0) thor (1.2.1) tilt (2.0.11) @@ -627,6 +631,8 @@ DEPENDENCIES sprockets-rails sshkey stackprof + syntax_tree + syntax_tree-disable_ternary test-prof thor uglifier diff --git a/app/assets/javascripts/bootstrap-json/package.json b/app/assets/javascripts/bootstrap-json/package.json index ce989fc828..f35b3b0e82 100644 --- a/app/assets/javascripts/bootstrap-json/package.json +++ b/app/assets/javascripts/bootstrap-json/package.json @@ -24,7 +24,7 @@ "discourse-plugins": "1.0.0", "express": "^4.18.2", "html-entities": "^2.3.3", - "jsdom": "^20.0.3", + "jsdom": "^21.0.0", "node-fetch": "^3.3.0" } } diff --git a/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js b/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js index 10c66629d6..d3bc1378a7 100644 --- a/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js +++ b/app/assets/javascripts/discourse/app/components/sidebar/user/categories-section.js @@ -1,28 +1,48 @@ import { inject as service } from "@ember/service"; import { action } from "@ember/object"; -import Category from "discourse/models/category"; import { cached } from "@glimmer/tracking"; +import { debounce } from "discourse-common/utils/decorators"; +import Category from "discourse/models/category"; import SidebarCommonCategoriesSection from "discourse/components/sidebar/common/categories-section"; +export const REFRESH_COUNTS_APP_EVENT_NAME = + "sidebar:refresh-categories-section-counts"; + export default class SidebarUserCategoriesSection extends SidebarCommonCategoriesSection { @service router; @service currentUser; + @service appEvents; constructor() { super(...arguments); this.callbackId = this.topicTrackingState.onStateChange(() => { - this.sectionLinks.forEach((sectionLink) => { - sectionLink.refreshCounts(); - }); + this._refreshCounts(); }); + + this.appEvents.on(REFRESH_COUNTS_APP_EVENT_NAME, this, this._refreshCounts); } willDestroy() { super.willDestroy(...arguments); this.topicTrackingState.offStateChange(this.callbackId); + + this.appEvents.off( + REFRESH_COUNTS_APP_EVENT_NAME, + this, + this._refreshCounts + ); + } + + // TopicTrackingState changes or plugins can trigger this function so we debounce to ensure we're not refreshing + // unnecessarily. + @debounce(300) + _refreshCounts() { + this.sectionLinks.forEach((sectionLink) => { + sectionLink.refreshCounts(); + }); } @cached diff --git a/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js b/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js index f80b68bde1..f7cd1f6132 100644 --- a/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js +++ b/app/assets/javascripts/discourse/app/initializers/register-media-optimization-upload-processor.js @@ -6,8 +6,27 @@ export default { name: "register-media-optimization-upload-processor", initialize(container) { - let siteSettings = container.lookup("service:site-settings"); + const siteSettings = container.lookup("service:site-settings"); + const capabilities = container.lookup("capabilities:main"); + if (siteSettings.composer_media_optimization_image_enabled) { + // NOTE: There are various performance issues with the Canvas + // in iOS Safari that are causing crashes when processing images + // with spikes of over 100% CPU usage. The cause of this is unknown, + // but profiling points to CanvasRenderingContext2D.getImageData() + // and CanvasRenderingContext2D.drawImage(). + // + // Until Safari makes some progress with OffscreenCanvas or other + // alternatives we cannot support this workflow. + // + // TODO (martin): Revisit around 2022-06-01 to see the state of iOS Safari. + if ( + capabilities.isIOS && + !siteSettings.composer_ios_media_optimisation_image_enabled + ) { + return; + } + addComposerUploadPreProcessor( UppyMediaOptimization, ({ isMobileDevice }) => { diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index fe587a4335..0822df4fbe 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -104,6 +104,8 @@ import { downloadCalendar } from "discourse/lib/download-calendar"; import { consolePrefix } from "discourse/lib/source-identifier"; import { addSectionLink as addCustomCommunitySectionLink } from "discourse/lib/sidebar/custom-community-section-links"; import { addSidebarSection } from "discourse/lib/sidebar/custom-sections"; +import { registerCustomCountable as registerUserCategorySectionLinkCountable } from "discourse/lib/sidebar/user/categories-section/category-section-link"; +import { REFRESH_COUNTS_APP_EVENT_NAME as REFRESH_USER_SIDEBAR_CATEGORIES_SECTION_COUNTS_APP_EVENT_NAME } from "discourse/components/sidebar/user/categories-section"; import DiscourseURL from "discourse/lib/url"; import { registerNotificationTypeRenderer } from "discourse/lib/notification-types-manager"; import { registerUserMenuTab } from "discourse/lib/user-menu/tab"; @@ -1809,6 +1811,87 @@ class PluginApi { addCustomCommunitySectionLink(arg, secondary); } + /** + * EXPERIMENTAL. Do not use. + * Registers a new countable for section links under Sidebar Categories section on top of the default countables of + * unread topics count and new topics count. + * + * ``` + * api.registerUserCategorySectionLinkCountable({ + * badgeTextFunction: (count) => { + * return I18n.t("custom.open_count", count: count"); + * }, + * route: "discovery.openCategory", + * shouldRegister: ({ category } => { + * return category.custom_fields.enable_open_topics_count; + * }), + * refreshCountFunction: ({ _topicTrackingState, category } => { + * return category.open_topics_count; + * }), + * prioritizeDefaults: ({ currentUser, category } => { + * return category.custom_fields.show_open_topics_count_first; + * }) + * }) + * ``` + * + * @callback badgeTextFunction + * @param {Integer} count - The count as given by the `refreshCountFunction`. + * @returns {String} - Text for the badge displayed in the section link. + * + * @callback shouldRegister + * @param {Object} arg + * @param {Category} arg.category - The category model for the sidebar section link. + * @returns {Boolean} - Whether the countable should be registered for the sidebar section link. + * + * @callback refreshCountFunction + * @param {Object} arg + * @param {Category} arg.category - The category model for the sidebar section link. + * @returns {integer} - The value used to set the property for the count. + * + * @callback prioritizeOverDefaults + * @param {Object} arg + * @param {Category} arg.category - The category model for the sidebar section link. + * @param {User} arg.currentUser - The user model for the current user. + * @returns {boolean} - Whether the countable should be prioritized over the defaults. + * + * @param {Object} arg - An object + * @param {string} arg.badgeTextFunction - Function used to generate the text for the badge displayed in the section link. + * @param {string} arg.route - The Ember route name to generate the href attribute for the link. + * @param {Object=} arg.routeQuery - Object representing the query params that should be appended to the route generated. + * @param {shouldRegister} arg.shouldRegister - Function used to determine if the countable should be registered for the category. + * @param {refreshCountFunction} arg.refreshCountFunction - Function used to calculate the value used to set the property for the count whenever the sidebar section link refreshes. + * @param {prioritizeOverDefaults} args.prioritizeOverDefaults - Function used to determine whether the countable should be prioritized over the default countables of unread/new. + */ + registerUserCategorySectionLinkCountable({ + badgeTextFunction, + route, + routeQuery, + shouldRegister, + refreshCountFunction, + prioritizeOverDefaults, + }) { + registerUserCategorySectionLinkCountable({ + badgeTextFunction, + route, + routeQuery, + shouldRegister, + refreshCountFunction, + prioritizeOverDefaults, + }); + } + + /** + * EXPERIMENTAL. Do not use. + * Triggers a refresh of the counts for all category section links under the categories section for a logged in user. + */ + refreshUserSidebarCategoriesSectionCounts() { + const appEvents = this._lookupContainer("service:app-events"); + + appEvents?.trigger( + REFRESH_USER_SIDEBAR_CATEGORIES_SECTION_COUNTS_APP_EVENT_NAME + ); + } + /** * EXPERIMENTAL. Do not use. * Support for adding a Sidebar section by returning a class which extends from the BaseCustomSidebarSection diff --git a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js index e855089d0a..1427b50bdf 100644 --- a/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js +++ b/app/assets/javascripts/discourse/app/lib/sidebar/user/categories-section/category-section-link.js @@ -1,35 +1,121 @@ import I18n from "I18n"; import { tracked } from "@glimmer/tracking"; +import { get, set } from "@ember/object"; import { bind } from "discourse-common/utils/decorators"; import Category from "discourse/models/category"; import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; +const DEFAULT_COUNTABLES = [ + { + propertyName: "totalUnread", + badgeTextFunction: (count) => { + return I18n.t("sidebar.unread_count", { count }); + }, + route: "discovery.unreadCategory", + refreshCountFunction: ({ topicTrackingState, category }) => { + return topicTrackingState.countUnread({ + categoryId: category.id, + }); + }, + }, + { + propertyName: "totalNew", + badgeTextFunction: (count) => { + return I18n.t("sidebar.new_count", { count }); + }, + route: "discovery.newCategory", + refreshCountFunction: ({ topicTrackingState, category }) => { + return topicTrackingState.countNew({ + categoryId: category.id, + }); + }, + }, +]; + +const customCountables = []; + +export function registerCustomCountable({ + badgeTextFunction, + route, + routeQuery, + shouldRegister, + refreshCountFunction, + prioritizeOverDefaults, +}) { + const length = customCountables.length + 1; + + customCountables.push({ + propertyName: `customCountableProperty${length}`, + badgeTextFunction, + route, + routeQuery, + shouldRegister, + refreshCountFunction, + prioritizeOverDefaults, + }); +} + +export function resetCustomCountables() { + customCountables.length = 0; +} + export default class CategorySectionLink { - @tracked totalUnread = 0; - @tracked totalNew = 0; - @tracked hideCount = - this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION; + @tracked activeCountable; constructor({ category, topicTrackingState, currentUser }) { this.category = category; this.topicTrackingState = topicTrackingState; this.currentUser = currentUser; + this.countables = this.#countables(); + this.refreshCounts(); } + #countables() { + const countables = [...DEFAULT_COUNTABLES]; + + if (customCountables.length > 0) { + customCountables.forEach((customCountable) => { + if ( + !customCountable.shouldRegister || + customCountable.shouldRegister({ category: this.category }) + ) { + if ( + customCountable?.prioritizeOverDefaults({ + category: this.category, + currentUser: this.currentUser, + }) + ) { + countables.unshift(customCountable); + } else { + countables.push(customCountable); + } + } + }); + } + + return countables; + } + + get hideCount() { + return this.currentUser?.sidebarListDestination !== UNREAD_LIST_DESTINATION; + } + @bind refreshCounts() { - this.totalUnread = this.topicTrackingState.countUnread({ - categoryId: this.category.id, - }); + this.countables = this.#countables(); - if (this.totalUnread === 0) { - this.totalNew = this.topicTrackingState.countNew({ - categoryId: this.category.id, + this.activeCountable = this.countables.find((countable) => { + const count = countable.refreshCountFunction({ + topicTrackingState: this.topicTrackingState, + category: this.category, }); - } + + set(this, countable.propertyName, count); + return count > 0; + }); } get name() { @@ -74,29 +160,38 @@ export default class CategorySectionLink { if (this.hideCount) { return; } - if (this.totalUnread > 0) { - return I18n.t("sidebar.unread_count", { - count: this.totalUnread, - }); - } else if (this.totalNew > 0) { - return I18n.t("sidebar.new_count", { - count: this.totalNew, - }); + + const activeCountable = this.activeCountable; + + if (activeCountable) { + return activeCountable.badgeTextFunction( + get(this, activeCountable.propertyName) + ); } } get route() { if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) { - if (this.totalUnread > 0) { - return "discovery.unreadCategory"; - } - if (this.totalNew > 0) { - return "discovery.newCategory"; + const activeCountable = this.activeCountable; + + if (activeCountable) { + return activeCountable.route; } } + return "discovery.category"; } + get query() { + if (this.currentUser?.sidebarListDestination === UNREAD_LIST_DESTINATION) { + const activeCountable = this.activeCountable; + + if (activeCountable?.routeQuery) { + return activeCountable.routeQuery; + } + } + } + get suffixCSSClass() { return "unread"; } @@ -106,7 +201,7 @@ export default class CategorySectionLink { } get suffixValue() { - if (this.hideCount && (this.totalUnread || this.totalNew)) { + if (this.hideCount && this.activeCountable) { return "circle"; } } diff --git a/app/assets/javascripts/discourse/app/templates/preferences/tracking.hbs b/app/assets/javascripts/discourse/app/templates/preferences/tracking.hbs index 49e3f1f0c9..749fa16477 100644 --- a/app/assets/javascripts/discourse/app/templates/preferences/tracking.hbs +++ b/app/assets/javascripts/discourse/app/templates/preferences/tracking.hbs @@ -11,20 +11,24 @@
- +
+ +
- +
+ +
{{#if this.canSave}} diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 8a3ac2a158..c4aa0e7d36 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -78,7 +78,7 @@ "html-entities": "^2.3.3", "imports-loader": "^4.0.1", "js-yaml": "^4.1.0", - "jsdom": "^20.0.3", + "jsdom": "^21.0.0", "loader.js": "^4.7.0", "markdown-it": "^13.0.1", "message-bus-client": "^4.3.0", diff --git a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js index 489972f675..70f063461e 100644 --- a/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js +++ b/app/assets/javascripts/discourse/tests/acceptance/sidebar-plugin-api-test.js @@ -1,13 +1,17 @@ import { test } from "qunit"; import I18n from "I18n"; -import { click, visit } from "@ember/test-helpers"; +import { click, settled, visit } from "@ember/test-helpers"; import { acceptance, exists, query, queryAll, + updateCurrentUser, } from "discourse/tests/helpers/qunit-helpers"; import { withPluginApi } from "discourse/lib/plugin-api"; +import Site from "discourse/models/site"; +import { resetCustomCountables } from "discourse/lib/sidebar/user/categories-section/category-section-link"; +import { UNREAD_LIST_DESTINATION } from "discourse/controllers/preferences/sidebar"; import { bind } from "discourse-common/utils/decorators"; acceptance("Sidebar - Plugin API", function (needs) { @@ -629,4 +633,115 @@ acceptance("Sidebar - Plugin API", function (needs) { "does not display the section" ); }); + + test("Registering a custom countable for a section link in the user's sidebar categories section", async function (assert) { + try { + return await withPluginApi("1.6.0", async (api) => { + const categories = Site.current().categories; + const category1 = categories[0]; + const category2 = categories[1]; + + updateCurrentUser({ + sidebar_category_ids: [category1.id, category2.id], + }); + + // User has one unread topic + this.container.lookup("service:topic-tracking-state").loadStates([ + { + topic_id: 2, + highest_post_number: 12, + last_read_post_number: 11, + created_at: "2020-02-09T09:40:02.672Z", + category_id: category1.id, + notification_level: 2, + created_in_new_period: false, + treat_as_new_topic_start_date: "2022-05-09T03:17:34.286Z", + }, + ]); + + api.registerUserCategorySectionLinkCountable({ + badgeTextFunction: (count) => { + return `some custom ${count}`; + }, + route: "discovery.latestCategory", + routeQuery: { status: "open" }, + shouldRegister: ({ category }) => { + if (category.name === category1.name) { + return true; + } else if (category.name === category2.name) { + return false; + } + }, + refreshCountFunction: ({ category }) => { + return category.topic_count; + }, + prioritizeOverDefaults: ({ category }) => { + return category.topic_count > 1000; + }, + }); + + await visit("/"); + + assert.ok( + exists( + `.sidebar-section-link-${category1.name} .sidebar-section-link-suffix.unread` + ), + "the right suffix is displayed when custom countable is active" + ); + + assert.strictEqual( + query(`.sidebar-section-link-${category1.name}`).pathname, + `/c/${category1.name}/${category1.id}`, + "does not use route configured for custom countable when user has elected not to show any counts in sidebar" + ); + + assert.notOk( + exists( + `.sidebar-section-link-${category2.name} .sidebar-section-link-suffix.unread` + ), + "does not display suffix when custom countable is not registered" + ); + + updateCurrentUser({ + sidebar_list_destination: UNREAD_LIST_DESTINATION, + }); + + assert.strictEqual( + query( + `.sidebar-section-link-${category1.name} .sidebar-section-link-content-badge` + ).innerText.trim(), + I18n.t("sidebar.unread_count", { count: 1 }), + "displays the right badge text in section link when unread is present and custom countable is not prioritised over unread" + ); + + category1.set("topic_count", 2000); + + api.refreshUserSidebarCategoriesSectionCounts(); + + await settled(); + + assert.strictEqual( + query( + `.sidebar-section-link-${category1.name} .sidebar-section-link-content-badge` + ).innerText.trim(), + `some custom ${category1.topic_count}`, + "displays the right badge text in section link when unread is present but custom countable is prioritised over unread" + ); + + assert.strictEqual( + query(`.sidebar-section-link-${category1.name}`).pathname, + `/c/${category1.name}/${category1.id}/l/latest`, + "has the right pathname for section link" + ); + + assert.strictEqual( + query(`.sidebar-section-link-${category1.name}`).search, + "?status=open", + "has the right query params for section link" + ); + }); + } finally { + resetCustomCountables(); + } + }); }); diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index d62417e440..2b8a33d8d7 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -6267,10 +6267,10 @@ js-yaml@^4.0.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdom@^20.0.3: - version "20.0.3" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-20.0.3.tgz#886a41ba1d4726f67a8858028c99489fed6ad4db" - integrity sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ== +jsdom@^21.0.0: + version "21.0.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-21.0.0.tgz#33e22f2fc44286e50ac853c7b7656c8864a4ea45" + integrity sha512-AIw+3ZakSUtDYvhwPwWHiZsUi3zHugpMEKlNPaurviseYoBqo0zBd3zqoUi3LPCNtPFlEP8FiW9MqCZdjb2IYA== dependencies: abab "^2.0.6" acorn "^8.8.1" @@ -6881,9 +6881,9 @@ merge2@^1.2.3, merge2@^1.3.0: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== message-bus-client@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/message-bus-client/-/message-bus-client-4.3.0.tgz#03710b9acdbaa4c9117b2215b2dd8038a360c168" - integrity sha512-tJbSFz+NB8fBXn5OqGqisi881SUv1ypIoETHgyQHfg5NHFr15Y+BIx4gPqXhy5iG0EJlayZfcNyFxYfFfhyv+w== + version "4.3.1" + resolved "https://registry.yarnpkg.com/message-bus-client/-/message-bus-client-4.3.1.tgz#2107b569131b03d7277801cd3409059e48e9f25e" + integrity sha512-gPG8POalZrM6t9xZPIzER3uDCiAfdwMEjx6ulbYICqzJx0CpLSnZRXKuWvhds4dM3iZQZXpH37UCfYYNICKu5g== messageformat@0.1.5: version "0.1.5" diff --git a/app/assets/stylesheets/common/admin/emails.scss b/app/assets/stylesheets/common/admin/emails.scss index a1da84fd0a..4b41a09919 100644 --- a/app/assets/stylesheets/common/admin/emails.scss +++ b/app/assets/stylesheets/common/admin/emails.scss @@ -129,3 +129,31 @@ } } } + +//specific email admin modal styling +.admin-incoming-email-modal { + .modal-inner-container { + width: var(--modal-max-width); + } + .incoming-emails { + label { + float: none; + margin: 0; + width: unset; + } + .control-group { + textarea { + height: 200px; + } + &:last-of-type textarea { + height: 40px; + } + } + .controls { + margin: 0; + } + p { + margin: 5px 0; + } + } +} diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index 3f3f9e15a3..ba5d59879e 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -1,30 +1,28 @@ # frozen_string_literal: true class AboutController < ApplicationController - requires_login only: [:live_post_counts] skip_before_action :check_xhr, only: [:index] def index - return redirect_to path('/login') if SiteSetting.login_required? && current_user.nil? + return redirect_to path("/login") if SiteSetting.login_required? && current_user.nil? @about = About.new(current_user) @title = "#{I18n.t("js.about.simple_title")} - #{SiteSetting.title}" respond_to do |format| - format.html do - render :index - end - format.json do - render_json_dump(AboutSerializer.new(@about, scope: guardian)) - end + format.html { render :index } + format.json { render_json_dump(AboutSerializer.new(@about, scope: guardian)) } end end def live_post_counts - RateLimiter.new(current_user, "live_post_counts", 1, 10.minutes).performed! unless current_user.staff? + unless current_user.staff? + RateLimiter.new(current_user, "live_post_counts", 1, 10.minutes).performed! + end category_topic_ids = Category.pluck(:topic_id).compact! - public_topics = Topic.listable_topics.visible.secured(Guardian.new(nil)).where.not(id: category_topic_ids) + public_topics = + Topic.listable_topics.visible.secured(Guardian.new(nil)).where.not(id: category_topic_ids) stats = { public_topic_count: public_topics.count } stats[:public_post_count] = public_topics.sum(:posts_count) - stats[:public_topic_count] render json: stats diff --git a/app/controllers/admin/admin_controller.rb b/app/controllers/admin/admin_controller.rb index d9b1d705d9..2220a511e7 100644 --- a/app/controllers/admin/admin_controller.rb +++ b/app/controllers/admin/admin_controller.rb @@ -7,5 +7,4 @@ class Admin::AdminController < ApplicationController def index render body: nil end - end diff --git a/app/controllers/admin/api_controller.rb b/app/controllers/admin/api_controller.rb index 627c4c4009..49b8a70414 100644 --- a/app/controllers/admin/api_controller.rb +++ b/app/controllers/admin/api_controller.rb @@ -8,40 +8,40 @@ class Admin::ApiController < Admin::AdminController offset = (params[:offset] || 0).to_i limit = (params[:limit] || 50).to_i.clamp(1, 50) - keys = ApiKey - .where(hidden: false) - .includes(:user, :api_key_scopes) - # Sort revoked keys by revoked_at and active keys by created_at - .order("revoked_at DESC NULLS FIRST, created_at DESC") - .offset(offset) - .limit(limit) + keys = + ApiKey + .where(hidden: false) + .includes(:user, :api_key_scopes) + # Sort revoked keys by revoked_at and active keys by created_at + .order("revoked_at DESC NULLS FIRST, created_at DESC") + .offset(offset) + .limit(limit) - render_json_dump( - keys: serialize_data(keys, ApiKeySerializer), - offset: offset, - limit: limit - ) + render_json_dump(keys: serialize_data(keys, ApiKeySerializer), offset: offset, limit: limit) end def show api_key = ApiKey.includes(:api_key_scopes).find_by!(id: params[:id]) - render_serialized(api_key, ApiKeySerializer, root: 'key') + render_serialized(api_key, ApiKeySerializer, root: "key") end def scopes - scopes = ApiKeyScope.scope_mappings.reduce({}) do |memo, (resource, actions)| - memo.tap do |m| - m[resource] = actions.map do |k, v| - { - scope_id: "#{resource}:#{k}", - key: k, - name: k.to_s.gsub('_', ' '), - params: v[:params], - urls: v[:urls] - } + scopes = + ApiKeyScope + .scope_mappings + .reduce({}) do |memo, (resource, actions)| + memo.tap do |m| + m[resource] = actions.map do |k, v| + { + scope_id: "#{resource}:#{k}", + key: k, + name: k.to_s.gsub("_", " "), + params: v[:params], + urls: v[:urls], + } + end + end end - end - end render json: { scopes: scopes } end @@ -52,7 +52,7 @@ class Admin::ApiController < Admin::AdminController api_key.update!(update_params) log_api_key(api_key, UserHistory.actions[:api_key_update], changes: api_key.saved_changes) end - render_serialized(api_key, ApiKeySerializer, root: 'key') + render_serialized(api_key, ApiKeySerializer, root: "key") end def destroy @@ -76,7 +76,7 @@ class Admin::ApiController < Admin::AdminController api_key.save! log_api_key(api_key, UserHistory.actions[:api_key_create], changes: api_key.saved_changes) end - render_serialized(api_key, ApiKeySerializer, root: 'key') + render_serialized(api_key, ApiKeySerializer, root: "key") end def undo_revoke_key @@ -105,7 +105,7 @@ class Admin::ApiController < Admin::AdminController def build_scopes params.require(:key)[:scopes].to_a.map do |scope_params| - resource, action = scope_params[:scope_id].split(':') + resource, action = scope_params[:scope_id].split(":") mapping = ApiKeyScope.scope_mappings.dig(resource.to_sym, action.to_sym) raise Discourse::InvalidParameters if mapping.nil? # invalid mapping @@ -113,7 +113,7 @@ class Admin::ApiController < Admin::AdminController ApiKeyScope.new( resource: resource, action: action, - allowed_parameters: build_params(scope_params, mapping[:params]) + allowed_parameters: build_params(scope_params, mapping[:params]), ) end end @@ -121,11 +121,13 @@ class Admin::ApiController < Admin::AdminController def build_params(scope_params, params) return if params.nil? - scope_params.slice(*params).tap do |allowed_params| - allowed_params.each do |k, v| - v.blank? ? allowed_params.delete(k) : allowed_params[k] = v.split(',') + scope_params + .slice(*params) + .tap do |allowed_params| + allowed_params.each do |k, v| + v.blank? ? allowed_params.delete(k) : allowed_params[k] = v.split(",") + end end - end end def update_params @@ -146,5 +148,4 @@ class Admin::ApiController < Admin::AdminController def log_api_key_restore(*args) StaffActionLogger.new(current_user).log_api_key_restore(*args) end - end diff --git a/app/controllers/admin/backups_controller.rb b/app/controllers/admin/backups_controller.rb index 64dfeb0ee4..2660c9895e 100644 --- a/app/controllers/admin/backups_controller.rb +++ b/app/controllers/admin/backups_controller.rb @@ -7,7 +7,7 @@ class Admin::BackupsController < Admin::AdminController include ExternalUploadHelpers before_action :ensure_backups_enabled - skip_before_action :check_xhr, only: [:index, :show, :logs, :check_backup_chunk, :upload_backup_chunk] + skip_before_action :check_xhr, only: %i[index show logs check_backup_chunk upload_backup_chunk] def index respond_to do |format| @@ -62,7 +62,7 @@ class Admin::BackupsController < Admin::AdminController Jobs.enqueue( :download_backup_email, user_id: current_user.id, - backup_file_path: url_for(controller: 'backups', action: 'show') + backup_file_path: url_for(controller: "backups", action: "show"), ) render body: nil @@ -73,8 +73,8 @@ class Admin::BackupsController < Admin::AdminController def show if !EmailBackupToken.compare(current_user.id, params.fetch(:token)) - @error = I18n.t('download_backup_mailer.no_token') - return render layout: 'no_ember', status: 422, formats: [:html] + @error = I18n.t("download_backup_mailer.no_token") + return render layout: "no_ember", status: 422, formats: [:html] end store = BackupRestore::BackupStore.create @@ -86,7 +86,7 @@ class Admin::BackupsController < Admin::AdminController if store.remote? redirect_to backup.source else - headers['Content-Length'] = File.size(backup.source).to_s + headers["Content-Length"] = File.size(backup.source).to_s send_file backup.source end else @@ -170,9 +170,15 @@ class Admin::BackupsController < Admin::AdminController identifier = params.fetch(:resumableIdentifier) raise Discourse::InvalidParameters.new(:resumableIdentifier) unless valid_filename?(identifier) - return render status: 415, plain: I18n.t("backup.backup_file_should_be_tar_gz") unless valid_extension?(filename) - return render status: 415, plain: I18n.t("backup.not_enough_space_on_disk") unless has_enough_space_on_disk?(total_size) - return render status: 415, plain: I18n.t("backup.invalid_filename") unless valid_filename?(filename) + unless valid_extension?(filename) + return render status: 415, plain: I18n.t("backup.backup_file_should_be_tar_gz") + end + unless has_enough_space_on_disk?(total_size) + return render status: 415, plain: I18n.t("backup.not_enough_space_on_disk") + end + unless valid_filename?(filename) + return render status: 415, plain: I18n.t("backup.invalid_filename") + end file = params.fetch(:file) chunk_number = params.fetch(:resumableChunkNumber).to_i @@ -187,7 +193,13 @@ class Admin::BackupsController < Admin::AdminController uploaded_file_size = previous_chunk_number * chunk_size if uploaded_file_size + current_chunk_size >= total_size # merge all the chunks in a background thread - Jobs.enqueue_in(5.seconds, :backup_chunks_merger, filename: filename, identifier: identifier, chunks: chunk_number) + Jobs.enqueue_in( + 5.seconds, + :backup_chunks_merger, + filename: filename, + identifier: identifier, + chunks: chunk_number, + ) end render body: nil @@ -197,7 +209,9 @@ class Admin::BackupsController < Admin::AdminController params.require(:filename) filename = params.fetch(:filename) - return render_json_error(I18n.t("backup.backup_file_should_be_tar_gz")) unless valid_extension?(filename) + unless valid_extension?(filename) + return render_json_error(I18n.t("backup.backup_file_should_be_tar_gz")) + end return render_json_error(I18n.t("backup.invalid_filename")) unless valid_filename?(filename) store = BackupRestore::BackupStore.create @@ -236,8 +250,16 @@ class Admin::BackupsController < Admin::AdminController end def validate_before_create_multipart(file_name:, file_size:, upload_type:) - raise ExternalUploadHelpers::ExternalUploadValidationError.new(I18n.t("backup.backup_file_should_be_tar_gz")) unless valid_extension?(file_name) - raise ExternalUploadHelpers::ExternalUploadValidationError.new(I18n.t("backup.invalid_filename")) unless valid_filename?(file_name) + unless valid_extension?(file_name) + raise ExternalUploadHelpers::ExternalUploadValidationError.new( + I18n.t("backup.backup_file_should_be_tar_gz"), + ) + end + unless valid_filename?(file_name) + raise ExternalUploadHelpers::ExternalUploadValidationError.new( + I18n.t("backup.invalid_filename"), + ) + end end def self.serialize_upload(_upload) @@ -248,7 +270,11 @@ class Admin::BackupsController < Admin::AdminController begin yield rescue BackupRestore::BackupStore::StorageError => err - message = debug_upload_error(err, I18n.t("upload.create_multipart_failure", additional_detail: err.message)) + message = + debug_upload_error( + err, + I18n.t("upload.create_multipart_failure", additional_detail: err.message), + ) raise ExternalUploadHelpers::ExternalUploadValidationError.new(message) rescue BackupRestore::BackupStore::BackupFileExists raise ExternalUploadHelpers::ExternalUploadValidationError.new(I18n.t("backup.file_exists")) diff --git a/app/controllers/admin/badges_controller.rb b/app/controllers/admin/badges_controller.rb index 6d278679e5..4efb86cff8 100644 --- a/app/controllers/admin/badges_controller.rb +++ b/app/controllers/admin/badges_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'csv' +require "csv" class Admin::BadgesController < Admin::AdminController MAX_CSV_LINES = 50_000 @@ -10,25 +10,29 @@ class Admin::BadgesController < Admin::AdminController data = { badge_types: BadgeType.all.order(:id).to_a, badge_groupings: BadgeGrouping.all.order(:position).to_a, - badges: Badge.includes(:badge_grouping) - .includes(:badge_type, :image_upload) - .references(:badge_grouping) - .order('badge_groupings.position, badge_type_id, badges.name').to_a, + badges: + Badge + .includes(:badge_grouping) + .includes(:badge_type, :image_upload) + .references(:badge_grouping) + .order("badge_groupings.position, badge_type_id, badges.name") + .to_a, protected_system_fields: Badge.protected_system_fields, - triggers: Badge.trigger_hash + triggers: Badge.trigger_hash, } render_serialized(OpenStruct.new(data), AdminBadgesSerializer) end def preview - unless SiteSetting.enable_badge_sql - return render json: "preview not allowed", status: 403 - end + return render json: "preview not allowed", status: 403 unless SiteSetting.enable_badge_sql - render json: BadgeGranter.preview(params[:sql], - target_posts: params[:target_posts] == "true", - explain: params[:explain] == "true", - trigger: params[:trigger].to_i) + render json: + BadgeGranter.preview( + params[:sql], + target_posts: params[:target_posts] == "true", + explain: params[:explain] == "true", + trigger: params[:trigger].to_i, + ) end def new @@ -47,18 +51,21 @@ class Admin::BadgesController < Admin::AdminController if !badge.enabled? render_json_error( - I18n.t('badges.mass_award.errors.badge_disabled', badge_name: badge.display_name), - status: 422 + I18n.t("badges.mass_award.errors.badge_disabled", badge_name: badge.display_name), + status: 422, ) return end - replace_badge_owners = params[:replace_badge_owners] == 'true' - ensure_users_have_badge_once = params[:grant_existing_holders] != 'true' + replace_badge_owners = params[:replace_badge_owners] == "true" + ensure_users_have_badge_once = params[:grant_existing_holders] != "true" if !ensure_users_have_badge_once && !badge.multiple_grant? render_json_error( - I18n.t('badges.mass_award.errors.cant_grant_multiple_times', badge_name: badge.display_name), - status: 422 + I18n.t( + "badges.mass_award.errors.cant_grant_multiple_times", + badge_name: badge.display_name, + ), + status: 422, ) return end @@ -72,7 +79,7 @@ class Admin::BadgesController < Admin::AdminController line_number += 1 if line.present? - if line.include?('@') + if line.include?("@") emails << line else usernames << line @@ -80,26 +87,35 @@ class Admin::BadgesController < Admin::AdminController end if emails.size + usernames.size > MAX_CSV_LINES - return render_json_error I18n.t('badges.mass_award.errors.too_many_csv_entries', count: MAX_CSV_LINES), status: 400 + return( + render_json_error I18n.t( + "badges.mass_award.errors.too_many_csv_entries", + count: MAX_CSV_LINES, + ), + status: 400 + ) end end end BadgeGranter.revoke_all(badge) if replace_badge_owners - results = BadgeGranter.enqueue_mass_grant_for_users( - badge, - emails: emails, - usernames: usernames, - ensure_users_have_badge_once: ensure_users_have_badge_once - ) + results = + BadgeGranter.enqueue_mass_grant_for_users( + badge, + emails: emails, + usernames: usernames, + ensure_users_have_badge_once: ensure_users_have_badge_once, + ) render json: { - unmatched_entries: results[:unmatched_entries].first(100), - matched_users_count: results[:matched_users_count], - unmatched_entries_count: results[:unmatched_entries_count] - }, status: :ok + unmatched_entries: results[:unmatched_entries].first(100), + matched_users_count: results[:matched_users_count], + unmatched_entries_count: results[:unmatched_entries_count], + }, + status: :ok rescue CSV::MalformedCSVError - render_json_error I18n.t('badges.mass_award.errors.invalid_csv', line_number: line_number), status: 400 + render_json_error I18n.t("badges.mass_award.errors.invalid_csv", line_number: line_number), + status: 400 end def badge_types @@ -119,9 +135,7 @@ class Admin::BadgesController < Admin::AdminController group.save end - badge_groupings.each do |g| - g.destroy unless g.system? || ids.include?(g.id) - end + badge_groupings.each { |g| g.destroy unless g.system? || ids.include?(g.id) } badge_groupings = BadgeGrouping.all.order(:position).to_a render_serialized(badge_groupings, BadgeGroupingSerializer, root: "badge_groupings") @@ -173,21 +187,23 @@ class Admin::BadgesController < Admin::AdminController def update_badge_from_params(badge, opts = {}) errors = [] Badge.transaction do - allowed = Badge.column_names.map(&:to_sym) - allowed -= [:id, :created_at, :updated_at, :grant_count] + allowed = Badge.column_names.map(&:to_sym) + allowed -= %i[id created_at updated_at grant_count] allowed -= Badge.protected_system_fields if badge.system? allowed -= [:query] unless SiteSetting.enable_badge_sql params.permit(*allowed) - allowed.each do |key| - badge.public_send("#{key}=" , params[key]) if params[key] - end + allowed.each { |key| badge.public_send("#{key}=", params[key]) if params[key] } # Badge query contract checks begin if SiteSetting.enable_badge_sql - BadgeGranter.contract_checks!(badge.query, target_posts: badge.target_posts, trigger: badge.trigger) + BadgeGranter.contract_checks!( + badge.query, + target_posts: badge.target_posts, + trigger: badge.trigger, + ) end rescue => e errors << e.message @@ -203,7 +219,7 @@ class Admin::BadgesController < Admin::AdminController :bulk_user_title_update, new_title: badge.name, granted_badge_id: badge.id, - action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION + action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION, ) end diff --git a/app/controllers/admin/color_schemes_controller.rb b/app/controllers/admin/color_schemes_controller.rb index 4b6407158a..26a066477c 100644 --- a/app/controllers/admin/color_schemes_controller.rb +++ b/app/controllers/admin/color_schemes_controller.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true class Admin::ColorSchemesController < Admin::AdminController - - before_action :fetch_color_scheme, only: [:update, :destroy] + before_action :fetch_color_scheme, only: %i[update destroy] def index - render_serialized(ColorScheme.base_color_schemes + ColorScheme.order('id ASC').all.to_a, ColorSchemeSerializer) + render_serialized( + ColorScheme.base_color_schemes + ColorScheme.order("id ASC").all.to_a, + ColorSchemeSerializer, + ) end def create @@ -38,6 +40,8 @@ class Admin::ColorSchemesController < Admin::AdminController end def color_scheme_params - params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: [:name, :hex]])[:color_scheme] + params.permit(color_scheme: [:base_scheme_id, :name, :user_selectable, colors: %i[name hex]])[ + :color_scheme + ] end end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 99f9fcff89..d2ed26665d 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -11,9 +11,12 @@ class Admin::DashboardController < Admin::StaffController render json: data end - def moderation; end - def security; end - def reports; end + def moderation + end + def security + end + def reports + end def general render json: AdminDashboardGeneralData.fetch_cached_stats @@ -33,7 +36,7 @@ class Admin::DashboardController < Admin::StaffController data = { new_features: new_features, has_unseen_features: DiscourseUpdates.has_unseen_features?(current_user.id), - release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"] + release_notes_link: AdminDashboardGeneralData.fetch_cached_stats["release_notes_link"], } render json: data end diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index b56f10f9f6..b165827f4d 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::EmailController < Admin::AdminController - def index data = { delivery_method: delivery_method, settings: delivery_settings } render_json_dump(data) @@ -35,29 +34,22 @@ class Admin::EmailController < Admin::AdminController else email_logs.where( "replace(post_reply_keys.reply_key::VARCHAR, '-', '') ILIKE ?", - "%#{reply_key}%" + "%#{reply_key}%", ) end end email_logs = email_logs.to_a - tuples = email_logs.map do |email_log| - [email_log.post_id, email_log.user_id] - end + tuples = email_logs.map { |email_log| [email_log.post_id, email_log.user_id] } reply_keys = {} if tuples.present? PostReplyKey - .where( - "(post_id,user_id) IN (#{(['(?)'] * tuples.size).join(', ')})", - *tuples - ) + .where("(post_id,user_id) IN (#{(["(?)"] * tuples.size).join(", ")})", *tuples) .pluck(:post_id, :user_id, "reply_key::text") - .each do |post_id, user_id, key| - reply_keys[[post_id, user_id]] = key - end + .each { |post_id, user_id, key| reply_keys[[post_id, user_id]] = key } end render_serialized(email_logs, EmailLogSerializer, reply_keys: reply_keys) @@ -96,14 +88,10 @@ class Admin::EmailController < Admin::AdminController def advanced_test params.require(:email) - receiver = Email::Receiver.new(params['email']) + receiver = Email::Receiver.new(params["email"]) text, elided, format = receiver.select_body - render json: success_json.merge!( - text: text, - elided: elided, - format: format - ) + render json: success_json.merge!(text: text, elided: elided, format: format) end def send_digest @@ -112,9 +100,8 @@ class Admin::EmailController < Admin::AdminController params.require(:email) user = User.find_by_username(params[:username]) - message, skip_reason = UserNotifications.public_send(:digest, user, - since: params[:last_seen_at] - ) + message, skip_reason = + UserNotifications.public_send(:digest, user, since: params[:last_seen_at]) if message message.to = params[:email] @@ -134,9 +121,16 @@ class Admin::EmailController < Admin::AdminController params.require(:to) # These strings aren't localized; they are sent to an anonymous SMTP user. if !User.with_email(Email.downcase(params[:from])).exists? && !SiteSetting.enable_staged_users - render json: { reject: true, reason: "Mail from your address is not accepted. Do you have an account here?" } + render json: { + reject: true, + reason: "Mail from your address is not accepted. Do you have an account here?", + } elsif Email::Receiver.check_address(Email.downcase(params[:to])).nil? - render json: { reject: true, reason: "Mail to this address is not accepted. Check the address and try to send again?" } + render json: { + reject: true, + reason: + "Mail to this address is not accepted. Check the address and try to send again?", + } else render json: { reject: false } end @@ -157,10 +151,15 @@ class Admin::EmailController < Admin::AdminController retry_count = 0 begin - Jobs.enqueue(:process_email, mail: email_raw, retry_on_rate_limit: true, source: "handle_mail") + Jobs.enqueue( + :process_email, + mail: email_raw, + retry_on_rate_limit: true, + source: "handle_mail", + ) rescue JSON::GeneratorError, Encoding::UndefinedConversionError => e if retry_count == 0 - email_raw = email_raw.force_encoding('iso-8859-1').encode("UTF-8") + email_raw = email_raw.force_encoding("iso-8859-1").encode("UTF-8") retry_count += 1 retry else @@ -171,7 +170,8 @@ class Admin::EmailController < Admin::AdminController # TODO: 2022-05-01 Remove this route once all sites have migrated over # to using the new email_encoded param. if deprecated_email_param_used - render plain: "warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded email_encoded parameter instead. email has been received and is queued for processing" + render plain: + "warning: the email parameter is deprecated. all POST requests to this route should be sent with a base64 strict encoded email_encoded parameter instead. email has been received and is queued for processing" else render plain: "email has been received and is queued for processing" end @@ -204,15 +204,15 @@ class Admin::EmailController < Admin::AdminController end if incoming_email.nil? - email_local_part, email_domain = SiteSetting.notification_email.split('@') + email_local_part, email_domain = SiteSetting.notification_email.split("@") bounced_to_address = "#{email_local_part}+verp-#{email_log.bounce_key}@#{email_domain}" incoming_email = IncomingEmail.find_by(to_addresses: bounced_to_address) end # Temporary fix until all old format of emails has been purged via lib/email/cleaner.rb if incoming_email.nil? - email_local_part, email_domain = SiteSetting.reply_by_email_address.split('@') - subdomain, root_domain, extension = email_domain&.split('.') + email_local_part, email_domain = SiteSetting.reply_by_email_address.split("@") + subdomain, root_domain, extension = email_domain&.split(".") bounced_to_address = "#{subdomain}+verp-#{email_log.bounce_key}@#{root_domain}.#{extension}" incoming_email = IncomingEmail.find_by(to_addresses: bounced_to_address) end @@ -231,41 +231,61 @@ class Admin::EmailController < Admin::AdminController def filter_logs(logs, params) table_name = logs.table_name - logs = logs.includes(:user, post: :topic) - .references(:user) - .order(created_at: :desc) - .offset(params[:offset] || 0) - .limit(50) + logs = + logs + .includes(:user, post: :topic) + .references(:user) + .order(created_at: :desc) + .offset(params[:offset] || 0) + .limit(50) logs = logs.where("users.username ILIKE ?", "%#{params[:user]}%") if params[:user].present? - logs = logs.where("#{table_name}.to_address ILIKE ?", "%#{params[:address]}%") if params[:address].present? - logs = logs.where("#{table_name}.email_type ILIKE ?", "%#{params[:type]}%") if params[:type].present? + logs = logs.where("#{table_name}.to_address ILIKE ?", "%#{params[:address]}%") if params[ + :address + ].present? + logs = logs.where("#{table_name}.email_type ILIKE ?", "%#{params[:type]}%") if params[ + :type + ].present? if table_name == "email_logs" && params[:smtp_transaction_response].present? - logs = logs.where("#{table_name}.smtp_transaction_response ILIKE ?", "%#{params[:smtp_transaction_response]}%") + logs = + logs.where( + "#{table_name}.smtp_transaction_response ILIKE ?", + "%#{params[:smtp_transaction_response]}%", + ) end logs end def filter_incoming_emails(incoming_emails, params) - incoming_emails = incoming_emails.includes(:user, post: :topic) - .order(created_at: :desc) - .offset(params[:offset] || 0) - .limit(50) + incoming_emails = + incoming_emails + .includes(:user, post: :topic) + .order(created_at: :desc) + .offset(params[:offset] || 0) + .limit(50) - incoming_emails = incoming_emails.where("from_address ILIKE ?", "%#{params[:from]}%") if params[:from].present? - incoming_emails = incoming_emails.where("to_addresses ILIKE :to OR cc_addresses ILIKE :to", to: "%#{params[:to]}%") if params[:to].present? - incoming_emails = incoming_emails.where("subject ILIKE ?", "%#{params[:subject]}%") if params[:subject].present? - incoming_emails = incoming_emails.where("error ILIKE ?", "%#{params[:error]}%") if params[:error].present? + incoming_emails = incoming_emails.where("from_address ILIKE ?", "%#{params[:from]}%") if params[ + :from + ].present? + incoming_emails = + incoming_emails.where( + "to_addresses ILIKE :to OR cc_addresses ILIKE :to", + to: "%#{params[:to]}%", + ) if params[:to].present? + incoming_emails = incoming_emails.where("subject ILIKE ?", "%#{params[:subject]}%") if params[ + :subject + ].present? + incoming_emails = incoming_emails.where("error ILIKE ?", "%#{params[:error]}%") if params[ + :error + ].present? incoming_emails end def delivery_settings - action_mailer_settings - .reject { |k, _| k == :password } - .map { |k, v| { name: k, value: v } } + action_mailer_settings.reject { |k, _| k == :password }.map { |k, v| { name: k, value: v } } end def delivery_method diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index 48fffe9483..f1050e885c 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -1,70 +1,69 @@ # frozen_string_literal: true class Admin::EmailTemplatesController < Admin::AdminController - def self.email_keys - @email_keys ||= [ - "custom_invite_forum_mailer", - "custom_invite_mailer", - "invite_forum_mailer", - "invite_mailer", - "invite_password_instructions", - "new_version_mailer", - "new_version_mailer_with_notes", - "system_messages.backup_failed", - "system_messages.backup_succeeded", - "system_messages.bulk_invite_failed", - "system_messages.bulk_invite_succeeded", - "system_messages.csv_export_failed", - "system_messages.csv_export_succeeded", - "system_messages.download_remote_images_disabled", - "system_messages.email_error_notification", - "system_messages.email_reject_auto_generated", - "system_messages.email_reject_bad_destination_address", - "system_messages.email_reject_empty", - "system_messages.email_reject_invalid_access", - "system_messages.email_reject_parsing", - "system_messages.email_reject_reply_key", - "system_messages.email_reject_screened_email", - "system_messages.email_reject_topic_closed", - "system_messages.email_reject_topic_not_found", - "system_messages.email_reject_unrecognized_error", - "system_messages.email_reject_user_not_found", - "system_messages.email_revoked", - "system_messages.pending_users_reminder", - "system_messages.post_hidden", - "system_messages.post_hidden_again", - "system_messages.queued_posts_reminder", - "system_messages.restore_failed", - "system_messages.restore_succeeded", - "system_messages.silenced_by_staff", - "system_messages.spam_post_blocked", - "system_messages.too_many_spam_flags", - "system_messages.unsilenced", - "system_messages.user_automatically_silenced", - "system_messages.welcome_invite", - "system_messages.welcome_user", - "system_messages.welcome_staff", - "test_mailer", - "user_notifications.account_created", - "user_notifications.admin_login", - "user_notifications.confirm_new_email", - "user_notifications.email_login", - "user_notifications.forgot_password", - "user_notifications.notify_old_email", - "user_notifications.set_password", - "user_notifications.signup", - "user_notifications.signup_after_approval", - "user_notifications.suspicious_login", - "user_notifications.user_invited_to_private_message_pm", - "user_notifications.user_invited_to_private_message_pm_group", - "user_notifications.user_invited_to_topic", - "user_notifications.user_linked", - "user_notifications.user_mentioned", - "user_notifications.user_posted", - "user_notifications.user_posted_pm", - "user_notifications.user_quoted", - "user_notifications.user_replied", + @email_keys ||= %w[ + custom_invite_forum_mailer + custom_invite_mailer + invite_forum_mailer + invite_mailer + invite_password_instructions + new_version_mailer + new_version_mailer_with_notes + system_messages.backup_failed + system_messages.backup_succeeded + system_messages.bulk_invite_failed + system_messages.bulk_invite_succeeded + system_messages.csv_export_failed + system_messages.csv_export_succeeded + system_messages.download_remote_images_disabled + system_messages.email_error_notification + system_messages.email_reject_auto_generated + system_messages.email_reject_bad_destination_address + system_messages.email_reject_empty + system_messages.email_reject_invalid_access + system_messages.email_reject_parsing + system_messages.email_reject_reply_key + system_messages.email_reject_screened_email + system_messages.email_reject_topic_closed + system_messages.email_reject_topic_not_found + system_messages.email_reject_unrecognized_error + system_messages.email_reject_user_not_found + system_messages.email_revoked + system_messages.pending_users_reminder + system_messages.post_hidden + system_messages.post_hidden_again + system_messages.queued_posts_reminder + system_messages.restore_failed + system_messages.restore_succeeded + system_messages.silenced_by_staff + system_messages.spam_post_blocked + system_messages.too_many_spam_flags + system_messages.unsilenced + system_messages.user_automatically_silenced + system_messages.welcome_invite + system_messages.welcome_user + system_messages.welcome_staff + test_mailer + user_notifications.account_created + user_notifications.admin_login + user_notifications.confirm_new_email + user_notifications.email_login + user_notifications.forgot_password + user_notifications.notify_old_email + user_notifications.set_password + user_notifications.signup + user_notifications.signup_after_approval + user_notifications.suspicious_login + user_notifications.user_invited_to_private_message_pm + user_notifications.user_invited_to_private_message_pm_group + user_notifications.user_invited_to_topic + user_notifications.user_linked + user_notifications.user_mentioned + user_notifications.user_posted + user_notifications.user_posted_pm + user_notifications.user_quoted + user_notifications.user_replied ] end @@ -91,9 +90,18 @@ class Admin::EmailTemplatesController < Admin::AdminController log_site_text_change(subject_result) log_site_text_change(body_result) - render_serialized(key, AdminEmailTemplateSerializer, root: 'email_template', rest_serializer: true) + render_serialized( + key, + AdminEmailTemplateSerializer, + root: "email_template", + rest_serializer: true, + ) else - TranslationOverride.upsert!(I18n.locale, "#{key}.subject_template", subject_result[:old_value]) + TranslationOverride.upsert!( + I18n.locale, + "#{key}.subject_template", + subject_result[:old_value], + ) TranslationOverride.upsert!(I18n.locale, "#{key}.text_body_template", body_result[:old_value]) render_json_error(error_messages) @@ -105,11 +113,22 @@ class Admin::EmailTemplatesController < Admin::AdminController raise Discourse::NotFound unless self.class.email_keys.include?(params[:id]) revert_and_log("#{key}.subject_template", "#{key}.text_body_template") - render_serialized(key, AdminEmailTemplateSerializer, root: 'email_template', rest_serializer: true) + render_serialized( + key, + AdminEmailTemplateSerializer, + root: "email_template", + rest_serializer: true, + ) end def index - render_serialized(self.class.email_keys, AdminEmailTemplateSerializer, root: 'email_templates', rest_serializer: true, overridden_keys: overridden_keys) + render_serialized( + self.class.email_keys, + AdminEmailTemplateSerializer, + root: "email_templates", + rest_serializer: true, + overridden_keys: overridden_keys, + ) end private @@ -121,11 +140,7 @@ class Admin::EmailTemplatesController < Admin::AdminController translation_override = TranslationOverride.upsert!(I18n.locale, key, value) end - { - key: key, - old_value: old_value, - error_messages: translation_override&.errors&.full_messages - } + { key: key, old_value: old_value, error_messages: translation_override&.errors&.full_messages } end def revert_and_log(*keys) @@ -144,7 +159,10 @@ class Admin::EmailTemplatesController < Admin::AdminController def log_site_text_change(update_result) new_value = I18n.t(update_result[:key]) StaffActionLogger.new(current_user).log_site_text_change( - update_result[:key], new_value, update_result[:old_value]) + update_result[:key], + new_value, + update_result[:old_value], + ) end def format_error_message(update_result, attribute_key) diff --git a/app/controllers/admin/embeddable_hosts_controller.rb b/app/controllers/admin/embeddable_hosts_controller.rb index 765408039c..96e08759af 100644 --- a/app/controllers/admin/embeddable_hosts_controller.rb +++ b/app/controllers/admin/embeddable_hosts_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::EmbeddableHostsController < Admin::AdminController - def create save_host(EmbeddableHost.new, :create) end @@ -14,7 +13,10 @@ class Admin::EmbeddableHostsController < Admin::AdminController def destroy host = EmbeddableHost.where(id: params[:id]).first host.destroy - StaffActionLogger.new(current_user).log_embeddable_host(host, UserHistory.actions[:embeddable_host_destroy]) + StaffActionLogger.new(current_user).log_embeddable_host( + host, + UserHistory.actions[:embeddable_host_destroy], + ) render json: success_json end @@ -23,17 +25,25 @@ class Admin::EmbeddableHostsController < Admin::AdminController def save_host(host, action) host.host = params[:embeddable_host][:host] host.allowed_paths = params[:embeddable_host][:allowed_paths] - host.class_name = params[:embeddable_host][:class_name] + host.class_name = params[:embeddable_host][:class_name] host.category_id = params[:embeddable_host][:category_id] host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank? if host.save changes = host.saved_changes if action == :update - StaffActionLogger.new(current_user).log_embeddable_host(host, UserHistory.actions[:"embeddable_host_#{action}"], changes: changes) - render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true) + StaffActionLogger.new(current_user).log_embeddable_host( + host, + UserHistory.actions[:"embeddable_host_#{action}"], + changes: changes, + ) + render_serialized( + host, + EmbeddableHostSerializer, + root: "embeddable_host", + rest_serializer: true, + ) else render_json_error(host) end end - end diff --git a/app/controllers/admin/embedding_controller.rb b/app/controllers/admin/embedding_controller.rb index c4540edd80..51b0d48785 100644 --- a/app/controllers/admin/embedding_controller.rb +++ b/app/controllers/admin/embedding_controller.rb @@ -1,25 +1,22 @@ # frozen_string_literal: true class Admin::EmbeddingController < Admin::AdminController - before_action :fetch_embedding def show - render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + render_serialized(@embedding, EmbeddingSerializer, root: "embedding", rest_serializer: true) end def update if params[:embedding][:embed_by_username].blank? - return render_json_error(I18n.t('site_settings.embed_username_required')) + return render_json_error(I18n.t("site_settings.embed_username_required")) end - Embedding.settings.each do |s| - @embedding.public_send("#{s}=", params[:embedding][s]) - end + Embedding.settings.each { |s| @embedding.public_send("#{s}=", params[:embedding][s]) } if @embedding.save fetch_embedding - render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + render_serialized(@embedding, EmbeddingSerializer, root: "embedding", rest_serializer: true) else render_json_error(@embedding) end diff --git a/app/controllers/admin/emojis_controller.rb b/app/controllers/admin/emojis_controller.rb index 486a06c666..774506744d 100644 --- a/app/controllers/admin/emojis_controller.rb +++ b/app/controllers/admin/emojis_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::EmojisController < Admin::AdminController - def index render_serialized(Emoji.custom, EmojiSerializer, root: false) end @@ -17,15 +16,12 @@ class Admin::EmojisController < Admin::AdminController hijack do # fix the name name = File.basename(name, ".*") - name = name.gsub(/[^a-z0-9]+/i, '_') - .gsub(/_{2,}/, '_') - .downcase + name = name.gsub(/[^a-z0-9]+/i, "_").gsub(/_{2,}/, "_").downcase - upload = UploadCreator.new( - file.tempfile, - file.original_filename, - type: 'custom_emoji' - ).create_for(current_user.id) + upload = + UploadCreator.new(file.tempfile, file.original_filename, type: "custom_emoji").create_for( + current_user.id, + ) good = true @@ -61,5 +57,4 @@ class Admin::EmojisController < Admin::AdminController render json: success_json end - end diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 935931de22..4d9e581fd3 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -7,27 +7,19 @@ class Admin::GroupsController < Admin::StaffController attributes = group_params.to_h.except(:owner_usernames, :usernames) group = Group.new(attributes) - unless group_params[:allow_membership_requests] - group.membership_request_template = nil - end + group.membership_request_template = nil unless group_params[:allow_membership_requests] if group_params[:owner_usernames].present? - owner_ids = User.where( - username: group_params[:owner_usernames].split(",") - ).pluck(:id) + owner_ids = User.where(username: group_params[:owner_usernames].split(",")).pluck(:id) - owner_ids.each do |user_id| - group.group_users.build(user_id: user_id, owner: true) - end + owner_ids.each { |user_id| group.group_users.build(user_id: user_id, owner: true) } end if group_params[:usernames].present? user_ids = User.where(username: group_params[:usernames].split(",")).pluck(:id) user_ids -= owner_ids if owner_ids - user_ids.each do |user_id| - group.group_users.build(user_id: user_id) - end + user_ids.each { |user_id| group.group_users.build(user_id: user_id) } end if group.save @@ -140,45 +132,43 @@ class Admin::GroupsController < Admin::StaffController protected def can_not_modify_automatic - render_json_error(I18n.t('groups.errors.can_not_modify_automatic')) + render_json_error(I18n.t("groups.errors.can_not_modify_automatic")) end private def group_params - permitted = [ - :name, - :mentionable_level, - :messageable_level, - :visibility_level, - :members_visibility_level, - :automatic_membership_email_domains, - :title, - :primary_group, - :grant_trust_level, - :incoming_email, - :flair_icon, - :flair_upload_id, - :flair_bg_color, - :flair_color, - :bio_raw, - :public_admission, - :public_exit, - :allow_membership_requests, - :full_name, - :default_notification_level, - :membership_request_template, - :owner_usernames, - :usernames, - :publish_read_state, - :notify_users + permitted = %i[ + name + mentionable_level + messageable_level + visibility_level + members_visibility_level + automatic_membership_email_domains + title + primary_group + grant_trust_level + incoming_email + flair_icon + flair_upload_id + flair_bg_color + flair_color + bio_raw + public_admission + public_exit + allow_membership_requests + full_name + default_notification_level + membership_request_template + owner_usernames + usernames + publish_read_state + notify_users ] custom_fields = DiscoursePluginRegistry.editable_group_custom_fields permitted << { custom_fields: custom_fields } unless custom_fields.blank? - if guardian.can_associate_groups? - permitted << { associated_group_ids: [] } - end + permitted << { associated_group_ids: [] } if guardian.can_associate_groups? permitted = permitted | DiscoursePluginRegistry.group_params diff --git a/app/controllers/admin/impersonate_controller.rb b/app/controllers/admin/impersonate_controller.rb index 8045f20d1e..76df6b32d8 100644 --- a/app/controllers/admin/impersonate_controller.rb +++ b/app/controllers/admin/impersonate_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::ImpersonateController < Admin::AdminController - def create params.require(:username_or_email) @@ -18,5 +17,4 @@ class Admin::ImpersonateController < Admin::AdminController render body: nil end - end diff --git a/app/controllers/admin/permalinks_controller.rb b/app/controllers/admin/permalinks_controller.rb index 4e60a1ad01..54bea1f528 100644 --- a/app/controllers/admin/permalinks_controller.rb +++ b/app/controllers/admin/permalinks_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::PermalinksController < Admin::AdminController - before_action :fetch_permalink, only: [:destroy] def index @@ -20,7 +19,8 @@ class Admin::PermalinksController < Admin::AdminController params[:permalink_type_value] = Tag.find_by_name(params[:permalink_type_value])&.id end - permalink = Permalink.new(:url => params[:url], params[:permalink_type] => params[:permalink_type_value]) + permalink = + Permalink.new(:url => params[:url], params[:permalink_type] => params[:permalink_type_value]) if permalink.save render_serialized(permalink, PermalinkSerializer) else @@ -38,5 +38,4 @@ class Admin::PermalinksController < Admin::AdminController def fetch_permalink @permalink = Permalink.find(params[:id]) end - end diff --git a/app/controllers/admin/plugins_controller.rb b/app/controllers/admin/plugins_controller.rb index b9e4913cc9..202019844b 100644 --- a/app/controllers/admin/plugins_controller.rb +++ b/app/controllers/admin/plugins_controller.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class Admin::PluginsController < Admin::StaffController - def index - render_serialized(Discourse.visible_plugins, AdminPluginSerializer, root: 'plugins') + render_serialized(Discourse.visible_plugins, AdminPluginSerializer, root: "plugins") end - end diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index 90e7c4de1f..abc8b9e83e 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -2,24 +2,28 @@ class Admin::ReportsController < Admin::StaffController def index - reports_methods = ['page_view_total_reqs'] + - ApplicationRequest.req_types.keys - .select { |r| r =~ /^page_view_/ && r !~ /mobile/ } - .map { |r| r + "_reqs" } + - Report.singleton_methods.grep(/^report_(?!about|storage_stats)/) + reports_methods = + ["page_view_total_reqs"] + + ApplicationRequest + .req_types + .keys + .select { |r| r =~ /^page_view_/ && r !~ /mobile/ } + .map { |r| r + "_reqs" } + + Report.singleton_methods.grep(/^report_(?!about|storage_stats)/) - reports = reports_methods.map do |name| - type = name.to_s.gsub('report_', '') - description = I18n.t("reports.#{type}.description", default: '') - description_link = I18n.t("reports.#{type}.description_link", default: '') + reports = + reports_methods.map do |name| + type = name.to_s.gsub("report_", "") + description = I18n.t("reports.#{type}.description", default: "") + description_link = I18n.t("reports.#{type}.description_link", default: "") - { - type: type, - title: I18n.t("reports.#{type}.title"), - description: description.presence ? description : nil, - description_link: description_link.presence ? description_link : nil - } - end + { + type: type, + title: I18n.t("reports.#{type}.title"), + description: description.presence ? description : nil, + description_link: description_link.presence ? description_link : nil, + } + end render_json_dump(reports: reports.sort_by { |report| report[:title] }) end @@ -32,18 +36,14 @@ class Admin::ReportsController < Admin::StaffController args = parse_params(report_params) report = nil - if (report_params[:cache]) - report = Report.find_cached(report_type, args) - end + report = Report.find_cached(report_type, args) if (report_params[:cache]) if report reports << report else report = Report.find(report_type, args) - if (report_params[:cache]) && report - Report.cache(report) - end + Report.cache(report) if (report_params[:cache]) && report if report.blank? report = Report._get(report_type, args) @@ -66,22 +66,16 @@ class Admin::ReportsController < Admin::StaffController args = parse_params(params) report = nil - if (params[:cache]) - report = Report.find_cached(report_type, args) - end + report = Report.find_cached(report_type, args) if (params[:cache]) - if report - return render_json_dump(report: report) - end + return render_json_dump(report: report) if report hijack do report = Report.find(report_type, args) raise Discourse::NotFound if report.blank? - if (params[:cache]) - Report.cache(report) - end + Report.cache(report) if (params[:cache]) render_json_dump(report: report) end @@ -91,16 +85,28 @@ class Admin::ReportsController < Admin::StaffController def parse_params(report_params) begin - start_date = (report_params[:start_date].present? ? Time.parse(report_params[:start_date]).to_date : 1.days.ago).beginning_of_day - end_date = (report_params[:end_date].present? ? Time.parse(report_params[:end_date]).to_date : start_date + 30.days).end_of_day + start_date = + ( + if report_params[:start_date].present? + Time.parse(report_params[:start_date]).to_date + else + 1.days.ago + end + ).beginning_of_day + end_date = + ( + if report_params[:end_date].present? + Time.parse(report_params[:end_date]).to_date + else + start_date + 30.days + end + ).end_of_day rescue ArgumentError => e raise Discourse::InvalidParameters.new(e.message) end facets = nil - if Array === report_params[:facets] - facets = report_params[:facets].map { |s| s.to_s.to_sym } - end + facets = report_params[:facets].map { |s| s.to_s.to_sym } if Array === report_params[:facets] limit = nil if report_params.has_key?(:limit) && report_params[:limit].to_i > 0 @@ -108,16 +114,8 @@ class Admin::ReportsController < Admin::StaffController end filters = nil - if report_params.has_key?(:filters) - filters = report_params[:filters] - end + filters = report_params[:filters] if report_params.has_key?(:filters) - { - start_date: start_date, - end_date: end_date, - filters: filters, - facets: facets, - limit: limit - } + { start_date: start_date, end_date: end_date, filters: filters, facets: facets, limit: limit } end end diff --git a/app/controllers/admin/robots_txt_controller.rb b/app/controllers/admin/robots_txt_controller.rb index b269a6c9ec..d2912f0692 100644 --- a/app/controllers/admin/robots_txt_controller.rb +++ b/app/controllers/admin/robots_txt_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::RobotsTxtController < Admin::AdminController - def show render json: { robots_txt: current_robots_txt, overridden: @overridden } end diff --git a/app/controllers/admin/screened_emails_controller.rb b/app/controllers/admin/screened_emails_controller.rb index 0e2ccc9161..b8688a0998 100644 --- a/app/controllers/admin/screened_emails_controller.rb +++ b/app/controllers/admin/screened_emails_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true class Admin::ScreenedEmailsController < Admin::StaffController - def index - screened_emails = ScreenedEmail.limit(200).order('last_match_at desc').to_a + screened_emails = ScreenedEmail.limit(200).order("last_match_at desc").to_a render_serialized(screened_emails, ScreenedEmailSerializer) end @@ -12,5 +11,4 @@ class Admin::ScreenedEmailsController < Admin::StaffController screen.destroy! render json: success_json end - end diff --git a/app/controllers/admin/screened_ip_addresses_controller.rb b/app/controllers/admin/screened_ip_addresses_controller.rb index a270fb0193..27c50387d2 100644 --- a/app/controllers/admin/screened_ip_addresses_controller.rb +++ b/app/controllers/admin/screened_ip_addresses_controller.rb @@ -1,16 +1,19 @@ # frozen_string_literal: true class Admin::ScreenedIpAddressesController < Admin::StaffController - - before_action :fetch_screened_ip_address, only: [:update, :destroy] + before_action :fetch_screened_ip_address, only: %i[update destroy] def index filter = params[:filter] filter = IPAddr.handle_wildcards(filter) screened_ip_addresses = ScreenedIpAddress - screened_ip_addresses = screened_ip_addresses.where("cidr :filter >>= ip_address OR ip_address >>= cidr :filter", filter: filter) if filter.present? - screened_ip_addresses = screened_ip_addresses.limit(200).order('match_count desc') + screened_ip_addresses = + screened_ip_addresses.where( + "cidr :filter >>= ip_address OR ip_address >>= cidr :filter", + filter: filter, + ) if filter.present? + screened_ip_addresses = screened_ip_addresses.limit(200).order("match_count desc") begin screened_ip_addresses = screened_ip_addresses.to_a @@ -54,5 +57,4 @@ class Admin::ScreenedIpAddressesController < Admin::StaffController def fetch_screened_ip_address @screened_ip_address = ScreenedIpAddress.find(params[:id]) end - end diff --git a/app/controllers/admin/screened_urls_controller.rb b/app/controllers/admin/screened_urls_controller.rb index cedd7b0dfa..20cc8a2d6a 100644 --- a/app/controllers/admin/screened_urls_controller.rb +++ b/app/controllers/admin/screened_urls_controller.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true class Admin::ScreenedUrlsController < Admin::StaffController - def index - screened_urls = ScreenedUrl.select("domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at").group(:domain).order('last_match_at DESC').to_a + screened_urls = + ScreenedUrl + .select( + "domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at", + ) + .group(:domain) + .order("last_match_at DESC") + .to_a render_serialized(screened_urls, GroupedScreenedUrlSerializer) end - end diff --git a/app/controllers/admin/search_logs_controller.rb b/app/controllers/admin/search_logs_controller.rb index a36b77d2b7..bd15b9e381 100644 --- a/app/controllers/admin/search_logs_controller.rb +++ b/app/controllers/admin/search_logs_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Admin::SearchLogsController < Admin::StaffController - def index period = params[:period] || "all" search_type = params[:search_type] || "all" @@ -22,5 +21,4 @@ class Admin::SearchLogsController < Admin::StaffController details[:search_result] = serialize_data(result, GroupedSearchResultSerializer, result: result) render_json_dump(term: details) end - end diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb index 4db8b72e43..ad28e93af4 100644 --- a/app/controllers/admin/site_settings_controller.rb +++ b/app/controllers/admin/site_settings_controller.rb @@ -6,10 +6,7 @@ class Admin::SiteSettingsController < Admin::AdminController end def index - render_json_dump( - site_settings: SiteSetting.all_settings, - diags: SiteSetting.diags - ) + render_json_dump(site_settings: SiteSetting.all_settings, diags: SiteSetting.diags) end def update @@ -18,15 +15,17 @@ class Admin::SiteSettingsController < Admin::AdminController value = params[id] value.strip! if value.is_a?(String) - new_setting_name = SiteSettings::DeprecatedSettings::SETTINGS.find do |old_name, new_name, override, _| - if old_name == id - if !override - raise Discourse::InvalidParameters, "You cannot change this site setting because it is deprecated, use #{new_name} instead." - end + new_setting_name = + SiteSettings::DeprecatedSettings::SETTINGS.find do |old_name, new_name, override, _| + if old_name == id + if !override + raise Discourse::InvalidParameters, + "You cannot change this site setting because it is deprecated, use #{new_name} instead." + end - break new_name + break new_name + end end - end id = new_setting_name if new_setting_name @@ -36,9 +35,7 @@ class Admin::SiteSettingsController < Admin::AdminController value = Upload.get_from_urls(value.split("|")).to_a end - if SiteSetting.type_supervisor.get_type(id) == :upload - value = Upload.get_from_url(value) || "" - end + value = Upload.get_from_url(value) || "" if SiteSetting.type_supervisor.get_type(id) == :upload update_existing_users = params[:update_existing_user].present? previous_value = value_or_default(SiteSetting.public_send(id)) if update_existing_users @@ -68,22 +65,40 @@ class Admin::SiteSettingsController < Admin::AdminController notification_level = category_notification_level(id) categories_to_unwatch = previous_category_ids - new_category_ids - CategoryUser.where(category_id: categories_to_unwatch, notification_level: notification_level).delete_all + CategoryUser.where( + category_id: categories_to_unwatch, + notification_level: notification_level, + ).delete_all TopicUser .joins(:topic) - .where(notification_level: TopicUser.notification_levels[:watching], - notifications_reason_id: TopicUser.notification_reasons[:auto_watch_category], - topics: { category_id: categories_to_unwatch }) + .where( + notification_level: TopicUser.notification_levels[:watching], + notifications_reason_id: TopicUser.notification_reasons[:auto_watch_category], + topics: { + category_id: categories_to_unwatch, + }, + ) .update_all(notification_level: TopicUser.notification_levels[:regular]) (new_category_ids - previous_category_ids).each do |category_id| skip_user_ids = CategoryUser.where(category_id: category_id).pluck(:user_id) - User.real.where(staged: false).where.not(id: skip_user_ids).select(:id).find_in_batches do |users| - category_users = [] - users.each { |user| category_users << { category_id: category_id, user_id: user.id, notification_level: notification_level } } - CategoryUser.insert_all!(category_users) - end + User + .real + .where(staged: false) + .where.not(id: skip_user_ids) + .select(:id) + .find_in_batches do |users| + category_users = [] + users.each do |user| + category_users << { + category_id: category_id, + user_id: user.id, + notification_level: notification_level, + } + end + CategoryUser.insert_all!(category_users) + end end elsif id.start_with?("default_tags_") previous_tag_ids = Tag.where(name: previous_value.split("|")).pluck(:id) @@ -92,19 +107,40 @@ class Admin::SiteSettingsController < Admin::AdminController notification_level = tag_notification_level(id) - TagUser.where(tag_id: (previous_tag_ids - new_tag_ids), notification_level: notification_level).delete_all + TagUser.where( + tag_id: (previous_tag_ids - new_tag_ids), + notification_level: notification_level, + ).delete_all (new_tag_ids - previous_tag_ids).each do |tag_id| skip_user_ids = TagUser.where(tag_id: tag_id).pluck(:user_id) - User.real.where(staged: false).where.not(id: skip_user_ids).select(:id).find_in_batches do |users| - tag_users = [] - users.each { |user| tag_users << { tag_id: tag_id, user_id: user.id, notification_level: notification_level, created_at: now, updated_at: now } } - TagUser.insert_all!(tag_users) - end + User + .real + .where(staged: false) + .where.not(id: skip_user_ids) + .select(:id) + .find_in_batches do |users| + tag_users = [] + users.each do |user| + tag_users << { + tag_id: tag_id, + user_id: user.id, + notification_level: notification_level, + created_at: now, + updated_at: now, + } + end + TagUser.insert_all!(tag_users) + end end elsif is_sidebar_default_setting?(id) - Jobs.enqueue(:backfill_sidebar_site_settings, setting_name: id, previous_value: previous_value, new_value: new_value) + Jobs.enqueue( + :backfill_sidebar_site_settings, + setting_name: id, + previous_value: previous_value, + new_value: new_value, + ) end end @@ -135,15 +171,26 @@ class Admin::SiteSettingsController < Admin::AdminController notification_level = category_notification_level(id) - user_ids = CategoryUser.where(category_id: previous_category_ids - new_category_ids, notification_level: notification_level).distinct.pluck(:user_id) - user_ids += User - .real - .joins("CROSS JOIN categories c") - .joins("LEFT JOIN category_users cu ON users.id = cu.user_id AND c.id = cu.category_id") - .where(staged: false) - .where("c.id IN (?) AND cu.notification_level IS NULL", new_category_ids - previous_category_ids) - .distinct - .pluck("users.id") + user_ids = + CategoryUser + .where( + category_id: previous_category_ids - new_category_ids, + notification_level: notification_level, + ) + .distinct + .pluck(:user_id) + user_ids += + User + .real + .joins("CROSS JOIN categories c") + .joins("LEFT JOIN category_users cu ON users.id = cu.user_id AND c.id = cu.category_id") + .where(staged: false) + .where( + "c.id IN (?) AND cu.notification_level IS NULL", + new_category_ids - previous_category_ids, + ) + .distinct + .pluck("users.id") json[:user_count] = user_ids.uniq.count elsif id.start_with?("default_tags_") @@ -152,19 +199,28 @@ class Admin::SiteSettingsController < Admin::AdminController notification_level = tag_notification_level(id) - user_ids = TagUser.where(tag_id: previous_tag_ids - new_tag_ids, notification_level: notification_level).distinct.pluck(:user_id) - user_ids += User - .real - .joins("CROSS JOIN tags t") - .joins("LEFT JOIN tag_users tu ON users.id = tu.user_id AND t.id = tu.tag_id") - .where(staged: false) - .where("t.id IN (?) AND tu.notification_level IS NULL", new_tag_ids - previous_tag_ids) - .distinct - .pluck("users.id") + user_ids = + TagUser + .where(tag_id: previous_tag_ids - new_tag_ids, notification_level: notification_level) + .distinct + .pluck(:user_id) + user_ids += + User + .real + .joins("CROSS JOIN tags t") + .joins("LEFT JOIN tag_users tu ON users.id = tu.user_id AND t.id = tu.tag_id") + .where(staged: false) + .where("t.id IN (?) AND tu.notification_level IS NULL", new_tag_ids - previous_tag_ids) + .distinct + .pluck("users.id") json[:user_count] = user_ids.uniq.count elsif is_sidebar_default_setting?(id) - json[:user_count] = SidebarSiteSettingsBackfiller.new(id, previous_value: previous_value, new_value: new_value).number_of_users_to_backfill + json[:user_count] = SidebarSiteSettingsBackfiller.new( + id, + previous_value: previous_value, + new_value: new_value, + ).number_of_users_to_backfill end render json: json @@ -173,7 +229,7 @@ class Admin::SiteSettingsController < Admin::AdminController private def is_sidebar_default_setting?(setting_name) - %w{default_sidebar_categories default_sidebar_tags}.include?(setting_name.to_s) + %w[default_sidebar_categories default_sidebar_tags].include?(setting_name.to_s) end def user_options @@ -198,7 +254,7 @@ class Admin::SiteSettingsController < Admin::AdminController default_include_tl0_in_digests: "include_tl0_in_digests", default_text_size: "text_size_key", default_title_count_mode: "title_count_mode_key", - default_hide_profile_and_presence: "hide_profile_and_presence" + default_hide_profile_and_presence: "hide_profile_and_presence", } end diff --git a/app/controllers/admin/site_texts_controller.rb b/app/controllers/admin/site_texts_controller.rb index a3ed9f7d60..041c59128c 100644 --- a/app/controllers/admin/site_texts_controller.rb +++ b/app/controllers/admin/site_texts_controller.rb @@ -1,22 +1,25 @@ # frozen_string_literal: true class Admin::SiteTextsController < Admin::AdminController - def self.preferred_keys - ['system_messages.usage_tips.text_body_template', - 'education.new-topic', - 'education.new-reply', - 'login_required.welcome_message'] + %w[ + system_messages.usage_tips.text_body_template + education.new-topic + education.new-reply + login_required.welcome_message + ] end def self.restricted_keys - ['user_notifications.confirm_old_email.title', - 'user_notifications.confirm_old_email.subject_template', - 'user_notifications.confirm_old_email.text_body_template'] + %w[ + user_notifications.confirm_old_email.title + user_notifications.confirm_old_email.subject_template + user_notifications.confirm_old_email.text_body_template + ] end def index - overridden = params[:overridden] == 'true' + overridden = params[:overridden] == "true" extras = {} query = params[:q] || "" @@ -61,7 +64,7 @@ class Admin::SiteTextsController < Admin::AdminController render_serialized( results[first..last - 1], SiteTextSerializer, - root: 'site_texts', + root: "site_texts", rest_serializer: true, extras: extras, overridden_keys: overridden, @@ -71,7 +74,7 @@ class Admin::SiteTextsController < Admin::AdminController def show locale = fetch_locale(params[:locale]) site_text = find_site_text(locale) - render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true) + render_serialized(site_text, SiteTextSerializer, root: "site_text", rest_serializer: true) end def update @@ -92,14 +95,14 @@ class Admin::SiteTextsController < Admin::AdminController :bulk_user_title_update, new_title: value, granted_badge_id: system_badge_id, - action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION + action: Jobs::BulkUserTitleUpdate::UPDATE_ACTION, ) end - render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true) + render_serialized(site_text, SiteTextSerializer, root: "site_text", rest_serializer: true) else - render json: failed_json.merge( - message: translation_override.errors.full_messages.join("\n\n") - ), status: 422 + render json: + failed_json.merge(message: translation_override.errors.full_messages.join("\n\n")), + status: 422 end end @@ -118,31 +121,27 @@ class Admin::SiteTextsController < Admin::AdminController Jobs.enqueue( :bulk_user_title_update, granted_badge_id: system_badge_id, - action: Jobs::BulkUserTitleUpdate::RESET_ACTION + action: Jobs::BulkUserTitleUpdate::RESET_ACTION, ) end - render_serialized(site_text, SiteTextSerializer, root: 'site_text', rest_serializer: true) + render_serialized(site_text, SiteTextSerializer, root: "site_text", rest_serializer: true) end def get_reseed_options render_json_dump( categories: SeedData::Categories.with_default_locale.reseed_options, - topics: SeedData::Topics.with_default_locale.reseed_options + topics: SeedData::Topics.with_default_locale.reseed_options, ) end def reseed hijack do if params[:category_ids].present? - SeedData::Categories.with_default_locale.update( - site_setting_names: params[:category_ids] - ) + SeedData::Categories.with_default_locale.update(site_setting_names: params[:category_ids]) end if params[:topic_ids].present? - SeedData::Topics.with_default_locale.update( - site_setting_names: params[:topic_ids] - ) + SeedData::Topics.with_default_locale.update(site_setting_names: params[:topic_ids]) end render json: success_json @@ -152,8 +151,8 @@ class Admin::SiteTextsController < Admin::AdminController protected def is_badge_title?(id = "") - badge_parts = id.split('.') - badge_parts[0] == 'badges' && badge_parts[2] == 'name' + badge_parts = id.split(".") + badge_parts[0] == "badges" && badge_parts[2] == "name" end def record_for(key:, value: nil, locale:) @@ -165,10 +164,15 @@ class Admin::SiteTextsController < Admin::AdminController def find_site_text(locale) if self.class.restricted_keys.include?(params[:id]) - raise Discourse::InvalidAccess.new(nil, nil, custom_message: 'email_template_cant_be_modified') + raise Discourse::InvalidAccess.new( + nil, + nil, + custom_message: "email_template_cant_be_modified", + ) end - if I18n.exists?(params[:id], locale) || TranslationOverride.exists?(locale: locale, translation_key: params[:id]) + if I18n.exists?(params[:id], locale) || + TranslationOverride.exists?(locale: locale, translation_key: params[:id]) return record_for(key: params[:id], locale: locale) end @@ -182,9 +186,7 @@ class Admin::SiteTextsController < Admin::AdminController def find_translations(query, overridden, locale) translations = Hash.new { |hash, key| hash[key] = {} } - search_results = I18n.with_locale(locale) do - I18n.search(query, only_overridden: overridden) - end + search_results = I18n.with_locale(locale) { I18n.search(query, only_overridden: overridden) } search_results.each do |key, value| if PLURALIZED_REGEX.match(key) @@ -205,7 +207,9 @@ class Admin::SiteTextsController < Admin::AdminController plural_value = plural[1] results << record_for( - key: "#{key}.#{plural_key}", value: plural_value, locale: plural.last + key: "#{key}.#{plural_key}", + value: plural_value, + locale: plural.last, ) end else @@ -218,7 +222,7 @@ class Admin::SiteTextsController < Admin::AdminController def fix_plural_keys(key, value, locale) value = value.with_indifferent_access - plural_keys = I18n.with_locale(locale) { I18n.t('i18n.plural.keys') } + plural_keys = I18n.with_locale(locale) { I18n.t("i18n.plural.keys") } return value if value.keys.size == plural_keys.size && plural_keys.all? { |k| value.key?(k) } fallback_value = I18n.t(key, locale: :en, default: {}) diff --git a/app/controllers/admin/staff_action_logs_controller.rb b/app/controllers/admin/staff_action_logs_controller.rb index a01e38ca82..69687294ca 100644 --- a/app/controllers/admin/staff_action_logs_controller.rb +++ b/app/controllers/admin/staff_action_logs_controller.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true class Admin::StaffActionLogsController < Admin::StaffController - def index - filters = params.slice(*UserHistory.staff_filters + [:page, :limit]) + filters = params.slice(*UserHistory.staff_filters + %i[page limit]) page = (params[:page] || 0).to_i page_size = (params[:limit] || 200).to_i.clamp(1, 200) @@ -20,8 +19,8 @@ class Admin::StaffActionLogsController < Admin::StaffController total_rows_staff_action_logs: count, load_more_staff_action_logs: admin_staff_action_logs_path(load_more_params), extras: { - user_history_actions: staff_available_actions - } + user_history_actions: staff_available_actions, + }, ) end @@ -37,16 +36,10 @@ class Admin::StaffActionLogsController < Admin::StaffController output = +"

#{CGI.escapeHTML(cur&.dig("name").to_s)}

" - diff_fields["name"] = { - prev: prev&.dig("name").to_s, - cur: cur&.dig("name").to_s, - } + diff_fields["name"] = { prev: prev&.dig("name").to_s, cur: cur&.dig("name").to_s } - ["default", "user_selectable"].each do |f| - diff_fields[f] = { - prev: (!!prev&.dig(f)).to_s, - cur: (!!cur&.dig(f)).to_s - } + %w[default user_selectable].each do |f| + diff_fields[f] = { prev: (!!prev&.dig(f)).to_s, cur: (!!cur&.dig(f)).to_s } end diff_fields["color scheme"] = { @@ -54,10 +47,7 @@ class Admin::StaffActionLogsController < Admin::StaffController cur: cur&.dig("color_scheme", "name").to_s, } - diff_fields["included themes"] = { - prev: child_themes(prev), - cur: child_themes(cur) - } + diff_fields["included themes"] = { prev: child_themes(prev), cur: child_themes(cur) } load_diff(diff_fields, :cur, cur) load_diff(diff_fields, :prev, prev) @@ -94,10 +84,7 @@ class Admin::StaffActionLogsController < Admin::StaffController def staff_available_actions UserHistory.staff_actions.sort.map do |name| - { - id: name, - action_id: UserHistory.actions[name] || UserHistory.actions[:custom_staff], - } + { id: name, action_id: UserHistory.actions[name] || UserHistory.actions[:custom_staff] } end end end diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index a47f2016d7..bf8ebb27d9 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'base64' +require "base64" class Admin::ThemesController < Admin::AdminController - - skip_before_action :check_xhr, only: [:show, :preview, :export] + skip_before_action :check_xhr, only: %i[show preview export] before_action :ensure_admin def preview @@ -15,7 +14,6 @@ class Admin::ThemesController < Admin::AdminController end def upload_asset - ban_in_allowlist_mode! path = params[:file].path @@ -34,32 +32,30 @@ class Admin::ThemesController < Admin::AdminController end def generate_key_pair - require 'sshkey' + require "sshkey" k = SSHKey.generate Discourse.redis.setex("ssh_key_#{k.ssh_public_key}", 1.hour, k.private_key) render json: { public_key: k.ssh_public_key } end - THEME_CONTENT_TYPES ||= %w{ + THEME_CONTENT_TYPES ||= %w[ application/gzip application/x-gzip application/x-zip-compressed application/zip - } + ] def import @theme = nil if params[:theme] && params[:theme].content_type == "application/json" - ban_in_allowlist_mode! # .dcstyle.json import. Deprecated, but still available to allow conversion - json = JSON::parse(params[:theme].read) - theme = json['theme'] + json = JSON.parse(params[:theme].read) + theme = json["theme"] @theme = Theme.new(name: theme["name"], user_id: theme_user.id, auto_update: false) theme["theme_fields"]&.each do |field| - if field["raw_upload"] begin tmp = Tempfile.new @@ -79,7 +75,7 @@ class Admin::ThemesController < Admin::AdminController name: field["name"], value: field["value"], type_id: field["type_id"], - upload_id: field["upload_id"] + upload_id: field["upload_id"], ) end @@ -93,17 +89,22 @@ class Admin::ThemesController < Admin::AdminController begin guardian.ensure_allowed_theme_repo_import!(remote.strip) rescue Discourse::InvalidAccess - render_json_error I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip }), status: :forbidden + render_json_error I18n.t("themes.import_error.not_allowed_theme", { repo: remote.strip }), + status: :forbidden return end hijack do begin branch = params[:branch] ? params[:branch] : nil - private_key = params[:public_key] ? Discourse.redis.get("ssh_key_#{params[:public_key]}") : nil - return render_json_error I18n.t("themes.import_error.ssh_key_gone") if params[:public_key].present? && private_key.blank? + private_key = + params[:public_key] ? Discourse.redis.get("ssh_key_#{params[:public_key]}") : nil + if params[:public_key].present? && private_key.blank? + return render_json_error I18n.t("themes.import_error.ssh_key_gone") + end - @theme = RemoteTheme.import_theme(remote, theme_user, private_key: private_key, branch: branch) + @theme = + RemoteTheme.import_theme(remote, theme_user, private_key: private_key, branch: branch) render json: @theme, status: :created rescue RemoteTheme::ImportError => e if params[:force] @@ -125,8 +126,8 @@ class Admin::ThemesController < Admin::AdminController end end end - elsif params[:bundle] || (params[:theme] && THEME_CONTENT_TYPES.include?(params[:theme].content_type)) - + elsif params[:bundle] || + (params[:theme] && THEME_CONTENT_TYPES.include?(params[:theme].content_type)) ban_in_allowlist_mode! # params[:bundle] used by theme CLI. params[:theme] used by admin UI @@ -135,21 +136,23 @@ class Admin::ThemesController < Admin::AdminController update_components = params[:components] match_theme_by_name = !!params[:bundle] && !params.key?(:theme_id) # Old theme CLI behavior, match by name. Remove Jan 2020 begin - @theme = RemoteTheme.update_zipped_theme( - bundle.path, - bundle.original_filename, - match_theme: match_theme_by_name, - user: theme_user, - theme_id: theme_id, - update_components: update_components - ) + @theme = + RemoteTheme.update_zipped_theme( + bundle.path, + bundle.original_filename, + match_theme: match_theme_by_name, + user: theme_user, + theme_id: theme_id, + update_components: update_components, + ) log_theme_change(nil, @theme) render json: @theme, status: :created rescue RemoteTheme::ImportError => e render_json_error e.message end else - render_json_error I18n.t("themes.import_error.unknown_file_type"), status: :unprocessable_entity + render_json_error I18n.t("themes.import_error.unknown_file_type"), + status: :unprocessable_entity end end @@ -160,24 +163,25 @@ class Admin::ThemesController < Admin::AdminController payload = { themes: ActiveModel::ArraySerializer.new(@themes, each_serializer: ThemeSerializer), extras: { - color_schemes: ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer) - } + color_schemes: + ActiveModel::ArraySerializer.new(@color_schemes, each_serializer: ColorSchemeSerializer), + }, } - respond_to do |format| - format.json { render json: payload } - end + respond_to { |format| format.json { render json: payload } } end def create - ban_in_allowlist_mode! - @theme = Theme.new(name: theme_params[:name], - user_id: theme_user.id, - user_selectable: theme_params[:user_selectable] || false, - color_scheme_id: theme_params[:color_scheme_id], - component: [true, "true"].include?(theme_params[:component])) + @theme = + Theme.new( + name: theme_params[:name], + user_id: theme_user.id, + user_selectable: theme_params[:user_selectable] || false, + color_scheme_id: theme_params[:color_scheme_id], + component: [true, "true"].include?(theme_params[:component]), + ) set_fields respond_to do |format| @@ -199,10 +203,8 @@ class Admin::ThemesController < Admin::AdminController disables_component = [false, "false"].include?(theme_params[:enabled]) enables_component = [true, "true"].include?(theme_params[:enabled]) - [:name, :color_scheme_id, :user_selectable, :enabled, :auto_update].each do |field| - if theme_params.key?(field) - @theme.public_send("#{field}=", theme_params[field]) - end + %i[name color_scheme_id user_selectable enabled auto_update].each do |field| + @theme.public_send("#{field}=", theme_params[field]) if theme_params.key?(field) end if theme_params.key?(:child_theme_ids) @@ -218,13 +220,9 @@ class Admin::ThemesController < Admin::AdminController update_translations handle_switch - if params[:theme][:remote_check] - @theme.remote_theme.update_remote_version - end + @theme.remote_theme.update_remote_version if params[:theme][:remote_check] - if params[:theme][:remote_update] - @theme.remote_theme.update_from_remote - end + @theme.remote_theme.update_from_remote if params[:theme][:remote_update] respond_to do |format| if @theme.save @@ -245,7 +243,7 @@ class Admin::ThemesController < Admin::AdminController error = I18n.t("themes.bad_color_scheme") if @theme.errors[:color_scheme].present? error ||= I18n.t("themes.other_error") - render json: { errors: [ error ] }, status: :unprocessable_entity + render json: { errors: [error] }, status: :unprocessable_entity end end end @@ -260,9 +258,7 @@ class Admin::ThemesController < Admin::AdminController StaffActionLogger.new(current_user).log_theme_destroy(@theme) @theme.destroy - respond_to do |format| - format.json { head :no_content } - end + respond_to { |format| format.json { head :no_content } } end def show @@ -279,10 +275,10 @@ class Admin::ThemesController < Admin::AdminController exporter = ThemeStore::ZipExporter.new(@theme) file_path = exporter.package_filename - headers['Content-Length'] = File.size(file_path).to_s + headers["Content-Length"] = File.size(file_path).to_s send_data File.read(file_path), - filename: File.basename(file_path), - content_type: "application/zip" + filename: File.basename(file_path), + content_type: "application/zip" ensure exporter.cleanup! end @@ -330,9 +326,7 @@ class Admin::ThemesController < Admin::AdminController end end - Theme.where(id: expected).each do |theme| - @theme.add_relative_theme!(kind, theme) - end + Theme.where(id: expected).each { |theme| @theme.add_relative_theme!(kind, theme) } end def update_default_theme @@ -361,11 +355,13 @@ class Admin::ThemesController < Admin::AdminController :component, :enabled, :auto_update, - settings: {}, - translations: {}, - theme_fields: [:name, :target, :value, :upload_id, :type_id], + settings: { + }, + translations: { + }, + theme_fields: %i[name target value upload_id type_id], child_theme_ids: [], - parent_theme_ids: [] + parent_theme_ids: [], ) end end @@ -382,7 +378,7 @@ class Admin::ThemesController < Admin::AdminController name: field[:name], value: field[:value], type_id: field[:type_id], - upload_id: field[:upload_id] + upload_id: field[:upload_id], ) end end @@ -408,7 +404,12 @@ class Admin::ThemesController < Admin::AdminController end def log_theme_setting_change(setting_name, previous_value, new_value) - StaffActionLogger.new(current_user).log_theme_setting_change(setting_name, previous_value, new_value, @theme) + StaffActionLogger.new(current_user).log_theme_setting_change( + setting_name, + previous_value, + new_value, + @theme, + ) end def log_theme_component_disabled @@ -422,10 +423,14 @@ class Admin::ThemesController < Admin::AdminController def handle_switch param = theme_params[:component] if param.to_s == "false" && @theme.component? - raise Discourse::InvalidParameters.new(:component) if @theme.id == SiteSetting.default_theme_id + if @theme.id == SiteSetting.default_theme_id + raise Discourse::InvalidParameters.new(:component) + end @theme.switch_to_theme! elsif param.to_s == "true" && !@theme.component? - raise Discourse::InvalidParameters.new(:component) if @theme.id == SiteSetting.default_theme_id + if @theme.id == SiteSetting.default_theme_id + raise Discourse::InvalidParameters.new(:component) + end @theme.switch_to_component! end end diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb index 5d6cae243c..b244867b7a 100644 --- a/app/controllers/admin/user_fields_controller.rb +++ b/app/controllers/admin/user_fields_controller.rb @@ -1,9 +1,18 @@ # frozen_string_literal: true class Admin::UserFieldsController < Admin::AdminController - def self.columns - %i(name field_type editable description required show_on_profile show_on_user_card position searchable) + %i[ + name + field_type + editable + description + required + show_on_profile + show_on_user_card + position + searchable + ] end def create @@ -13,14 +22,12 @@ class Admin::UserFieldsController < Admin::AdminController field.required = params[:user_field][:required] == "true" update_options(field) - json_result(field, serializer: UserFieldSerializer) do - field.save - end + json_result(field, serializer: UserFieldSerializer) { field.save } end def index user_fields = UserField.all.includes(:user_field_options).order(:position) - render_serialized(user_fields, UserFieldSerializer, root: 'user_fields') + render_serialized(user_fields, UserFieldSerializer, root: "user_fields") end def update @@ -28,9 +35,7 @@ class Admin::UserFieldsController < Admin::AdminController field = UserField.where(id: params.require(:id)).first Admin::UserFieldsController.columns.each do |col| - unless field_params[col].nil? - field.public_send("#{col}=", field_params[col]) - end + field.public_send("#{col}=", field_params[col]) unless field_params[col].nil? end update_options(field) @@ -38,7 +43,7 @@ class Admin::UserFieldsController < Admin::AdminController if !field.show_on_profile && !field.show_on_user_card DirectoryColumn.where(user_field_id: field.id).destroy_all end - render_serialized(field, UserFieldSerializer, root: 'user_field') + render_serialized(field, UserFieldSerializer, root: "user_field") else render_json_error(field) end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 7d7526417d..45c8f9313c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -3,28 +3,31 @@ class Admin::UsersController < Admin::StaffController MAX_SIMILAR_USERS = 10 - before_action :fetch_user, only: [:suspend, - :unsuspend, - :log_out, - :revoke_admin, - :revoke_moderation, - :grant_moderation, - :approve, - :activate, - :deactivate, - :silence, - :unsilence, - :trust_level, - :trust_level_lock, - :add_group, - :remove_group, - :primary_group, - :anonymize, - :merge, - :reset_bounce_score, - :disable_second_factor, - :delete_posts_batch, - :sso_record] + before_action :fetch_user, + only: %i[ + suspend + unsuspend + log_out + revoke_admin + revoke_moderation + grant_moderation + approve + activate + deactivate + silence + unsilence + trust_level + trust_level_lock + add_group + remove_group + primary_group + anonymize + merge + reset_bounce_score + disable_second_factor + delete_posts_batch + sso_record + ] def index users = ::AdminUserIndexQuery.new(params).find_users @@ -42,9 +45,7 @@ class Admin::UsersController < Admin::StaffController @user = User.find_by(id: params[:id]) raise Discourse::NotFound unless @user - similar_users = User.real - .where.not(id: @user.id) - .where(ip_address: @user.ip_address) + similar_users = User.real.where.not(id: @user.id).where(ip_address: @user.ip_address) render_serialized( @user, @@ -64,7 +65,6 @@ class Admin::UsersController < Admin::StaffController # DELETE action to delete penalty history for a user def penalty_history - # We don't delete any history, we merely remove the action type # with a removed type. It can still be viewed in the logs but # will not affect TL3 promotions. @@ -87,16 +87,19 @@ class Admin::UsersController < Admin::StaffController DB.exec( sql, - UserHistory.actions.slice( - :silence_user, - :suspend_user, - :unsilence_user, - :unsuspend_user, - :removed_silence_user, - :removed_unsilence_user, - :removed_suspend_user, - :removed_unsuspend_user - ).merge(user_id: params[:user_id].to_i) + UserHistory + .actions + .slice( + :silence_user, + :suspend_user, + :unsilence_user, + :unsuspend_user, + :removed_silence_user, + :removed_unsilence_user, + :removed_suspend_user, + :removed_unsuspend_user, + ) + .merge(user_id: params[:user_id].to_i), ) render json: success_json @@ -107,14 +110,21 @@ class Admin::UsersController < Admin::StaffController if @user.suspended? suspend_record = @user.suspend_record - message = I18n.t("user.already_suspended", - staff: suspend_record.acting_user.username, - time_ago: FreedomPatches::Rails4.time_ago_in_words(suspend_record.created_at, true, scope: :'datetime.distance_in_words_verbose') - ) + message = + I18n.t( + "user.already_suspended", + staff: suspend_record.acting_user.username, + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + suspend_record.created_at, + true, + scope: :"datetime.distance_in_words_verbose", + ), + ) return render json: failed_json.merge(message: message), status: 409 end - params.require([:suspend_until, :reason]) + params.require(%i[suspend_until reason]) all_users = [@user] if Array === params[:other_user_ids] @@ -133,12 +143,13 @@ class Admin::UsersController < Admin::StaffController User.transaction do user.save! - user_history = StaffActionLogger.new(current_user).log_user_suspend( - user, - params[:reason], - message: message, - post_id: params[:post_id] - ) + user_history = + StaffActionLogger.new(current_user).log_user_suspend( + user, + params[:reason], + message: message, + post_id: params[:post_id], + ) end user.logged_out @@ -147,7 +158,7 @@ class Admin::UsersController < Admin::StaffController :critical_user_email, type: "account_suspended", user_id: user.id, - user_history_id: user_history.id + user_history_id: user_history.id, ) end @@ -159,7 +170,7 @@ class Admin::UsersController < Admin::StaffController user_history: user_history, post_id: params[:post_id], suspended_till: params[:suspend_until], - suspended_at: DateTime.now + suspended_at: DateTime.now, ) end @@ -171,8 +182,8 @@ class Admin::UsersController < Admin::StaffController full_suspend_reason: user_history.try(:details), suspended_till: @user.suspended_till, suspended_at: @user.suspended_at, - suspended_by: BasicUserSerializer.new(current_user, root: false).as_json - } + suspended_by: BasicUserSerializer.new(current_user, root: false).as_json, + }, ) end @@ -185,12 +196,7 @@ class Admin::UsersController < Admin::StaffController DiscourseEvent.trigger(:user_unsuspended, user: @user) - render_json_dump( - suspension: { - suspended_till: nil, - suspended_at: nil - } - ) + render_json_dump(suspension: { suspended_till: nil, suspended_at: nil }) end def log_out @@ -199,7 +205,7 @@ class Admin::UsersController < Admin::StaffController @user.logged_out render json: success_json else - render json: { error: I18n.t('admin_js.admin.users.id_not_found') }, status: 404 + render json: { error: I18n.t("admin_js.admin.users.id_not_found") }, status: 404 end end @@ -237,7 +243,7 @@ class Admin::UsersController < Admin::StaffController group = Group.find(params[:group_id].to_i) raise Discourse::NotFound unless group - return render_json_error(I18n.t('groups.errors.can_not_modify_automatic')) if group.automatic + return render_json_error(I18n.t("groups.errors.can_not_modify_automatic")) if group.automatic guardian.ensure_can_edit!(group) group.add(@user) @@ -250,7 +256,7 @@ class Admin::UsersController < Admin::StaffController group = Group.find(params[:group_id].to_i) raise Discourse::NotFound unless group - return render_json_error(I18n.t('groups.errors.can_not_modify_automatic')) if group.automatic + return render_json_error(I18n.t("groups.errors.can_not_modify_automatic")) if group.automatic guardian.ensure_can_edit!(group) if group.remove(@user) @@ -266,9 +272,7 @@ class Admin::UsersController < Admin::StaffController if group = Group.find(primary_group_id) guardian.ensure_can_change_primary_group!(@user, group) - if group.user_ids.include?(@user.id) - @user.primary_group_id = primary_group_id - end + @user.primary_group_id = primary_group_id if group.user_ids.include?(@user.id) end else @user.primary_group_id = nil @@ -304,9 +308,7 @@ class Admin::UsersController < Admin::StaffController guardian.ensure_can_change_trust_level!(@user) new_lock = params[:locked].to_s - unless new_lock =~ /true|false/ - return render_json_error I18n.t('errors.invalid_boolean') - end + return render_json_error I18n.t("errors.invalid_boolean") unless new_lock =~ /true|false/ @user.manual_locked_trust_level = (new_lock == "true") ? @user.trust_level : nil @user.save @@ -320,31 +322,38 @@ class Admin::UsersController < Admin::StaffController def approve guardian.ensure_can_approve!(@user) - reviewable = ReviewableUser.find_by(target: @user) || - Jobs::CreateUserReviewable.new.execute(user_id: @user.id).reviewable + reviewable = + ReviewableUser.find_by(target: @user) || + Jobs::CreateUserReviewable.new.execute(user_id: @user.id).reviewable reviewable.perform(current_user, :approve_user) render body: nil end def approve_bulk - Reviewable.bulk_perform_targets(current_user, :approve_user, 'ReviewableUser', params[:users]) + Reviewable.bulk_perform_targets(current_user, :approve_user, "ReviewableUser", params[:users]) render body: nil end def activate guardian.ensure_can_activate!(@user) # ensure there is an active email token - @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) if !@user.email_tokens.active.exists? + if !@user.email_tokens.active.exists? + @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) + end @user.activate - StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t('user.activated_by_staff')) + StaffActionLogger.new(current_user).log_user_activate(@user, I18n.t("user.activated_by_staff")) render json: success_json end def deactivate guardian.ensure_can_deactivate!(@user) @user.deactivate(current_user) - StaffActionLogger.new(current_user).log_user_deactivate(@user, I18n.t('user.deactivated_by_staff'), params.slice(:context)) + StaffActionLogger.new(current_user).log_user_deactivate( + @user, + I18n.t("user.deactivated_by_staff"), + params.slice(:context), + ) refresh_browser @user render json: success_json end @@ -354,10 +363,17 @@ class Admin::UsersController < Admin::StaffController if @user.silenced? silenced_record = @user.silenced_record - message = I18n.t("user.already_silenced", - staff: silenced_record.acting_user.username, - time_ago: FreedomPatches::Rails4.time_ago_in_words(silenced_record.created_at, true, scope: :'datetime.distance_in_words_verbose') - ) + message = + I18n.t( + "user.already_silenced", + staff: silenced_record.acting_user.username, + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + silenced_record.created_at, + true, + scope: :"datetime.distance_in_words_verbose", + ), + ) return render json: failed_json.merge(message: message), status: 409 end @@ -370,15 +386,16 @@ class Admin::UsersController < Admin::StaffController user_history = nil all_users.each do |user| - silencer = UserSilencer.new( - user, - current_user, - silenced_till: params[:silenced_till], - reason: params[:reason], - message_body: params[:message], - keep_posts: true, - post_id: params[:post_id] - ) + silencer = + UserSilencer.new( + user, + current_user, + silenced_till: params[:silenced_till], + reason: params[:reason], + message_body: params[:message], + keep_posts: true, + post_id: params[:post_id], + ) if silencer.silence user_history = silencer.user_history @@ -386,7 +403,7 @@ class Admin::UsersController < Admin::StaffController :critical_user_email, type: "account_silenced", user_id: user.id, - user_history_id: user_history.id + user_history_id: user_history.id, ) end end @@ -399,8 +416,8 @@ class Admin::UsersController < Admin::StaffController silence_reason: user_history.try(:details), silenced_till: @user.silenced_till, silenced_at: @user.silenced_at, - silenced_by: BasicUserSerializer.new(current_user, root: false).as_json - } + silenced_by: BasicUserSerializer.new(current_user, root: false).as_json, + }, ) end @@ -413,8 +430,8 @@ class Admin::UsersController < Admin::StaffController silenced: false, silence_reason: nil, silenced_till: nil, - silenced_at: nil - } + silenced_at: nil, + }, ) end @@ -428,11 +445,7 @@ class Admin::UsersController < Admin::StaffController user_security_key.destroy_all StaffActionLogger.new(current_user).log_disable_second_factor_auth(@user) - Jobs.enqueue( - :critical_user_email, - type: "account_second_factor_disabled", - user_id: @user.id - ) + Jobs.enqueue(:critical_user_email, type: "account_second_factor_disabled", user_id: @user.id) render json: success_json end @@ -442,7 +455,7 @@ class Admin::UsersController < Admin::StaffController guardian.ensure_can_delete_user!(user) options = params.slice(:context, :delete_as_spammer) - [:delete_posts, :block_email, :block_urls, :block_ip].each do |param_name| + %i[delete_posts block_email block_urls block_ip].each do |param_name| options[param_name] = ActiveModel::Type::Boolean.new.cast(params[param_name]) end options[:prepare_for_destroy] = true @@ -453,15 +466,21 @@ class Admin::UsersController < Admin::StaffController render json: { deleted: true } else render json: { - deleted: false, - user: AdminDetailedUserSerializer.new(user, root: false).as_json - } + deleted: false, + user: AdminDetailedUserSerializer.new(user, root: false).as_json, + } end rescue UserDestroyer::PostsExistError render json: { - deleted: false, - message: I18n.t("user.cannot_delete_has_posts", username: user.username, count: user.posts.joins(:topic).count), - }, status: 403 + deleted: false, + message: + I18n.t( + "user.cannot_delete_has_posts", + username: user.username, + count: user.posts.joins(:topic).count, + ), + }, + status: 403 end end end @@ -482,9 +501,16 @@ class Admin::UsersController < Admin::StaffController return render body: nil, status: 404 unless SiteSetting.enable_discourse_connect begin - sso = DiscourseConnect.parse("sso=#{params[:sso]}&sig=#{params[:sig]}", secure_session: secure_session) + sso = + DiscourseConnect.parse( + "sso=#{params[:sso]}&sig=#{params[:sig]}", + secure_session: secure_session, + ) rescue DiscourseConnect::ParseError - return render json: failed_json.merge(message: I18n.t("discourse_connect.login_error")), status: 422 + return( + render json: failed_json.merge(message: I18n.t("discourse_connect.login_error")), + status: 422 + ) end begin @@ -494,7 +520,8 @@ class Admin::UsersController < Admin::StaffController rescue ActiveRecord::RecordInvalid => ex render json: failed_json.merge(message: ex.message), status: 403 rescue DiscourseConnect::BlankExternalId => ex - render json: failed_json.merge(message: I18n.t('discourse_connect.blank_id_error')), status: 422 + render json: failed_json.merge(message: I18n.t("discourse_connect.blank_id_error")), + status: 422 end end @@ -510,12 +537,13 @@ class Admin::UsersController < Admin::StaffController block_urls: true, block_ip: true, delete_as_spammer: true, - context: I18n.t("user.destroy_reasons.same_ip_address", ip_address: params[:ip]) + context: I18n.t("user.destroy_reasons.same_ip_address", ip_address: params[:ip]), } - AdminUserIndexQuery.new(params).find_users(50).each do |user| - user_destroyer.destroy(user, options) - end + AdminUserIndexQuery + .new(params) + .find_users(50) + .each { |user| user_destroyer.destroy(user, options) } render json: success_json end @@ -536,7 +564,8 @@ class Admin::UsersController < Admin::StaffController if user = UserAnonymizer.new(@user, current_user, opts).make_anonymous render json: success_json.merge(username: user.username) else - render json: failed_json.merge(user: AdminDetailedUserSerializer.new(user, root: false).as_json) + render json: + failed_json.merge(user: AdminDetailedUserSerializer.new(user, root: false).as_json) end end @@ -547,7 +576,12 @@ class Admin::UsersController < Admin::StaffController guardian.ensure_can_merge_users!(@user, target_user) - Jobs.enqueue(:merge_user, user_id: @user.id, target_user_id: target_user.id, current_user_id: current_user.id) + Jobs.enqueue( + :merge_user, + user_id: @user.id, + target_user_id: target_user.id, + current_user_id: current_user.id, + ) render json: success_json end @@ -566,24 +600,25 @@ class Admin::UsersController < Admin::StaffController private def perform_post_action - return unless params[:post_id].present? && - params[:post_action].present? + return unless params[:post_id].present? && params[:post_action].present? if post = Post.where(id: params[:post_id]).first case params[:post_action] - when 'delete' + when "delete" PostDestroyer.new(current_user, post).destroy if guardian.can_delete_post_or_topic?(post) when "delete_replies" - PostDestroyer.delete_with_replies(current_user, post) if guardian.can_delete_post_or_topic?(post) - when 'edit' + if guardian.can_delete_post_or_topic?(post) + PostDestroyer.delete_with_replies(current_user, post) + end + when "edit" revisor = PostRevisor.new(post) # Take what the moderator edited in as gospel revisor.revise!( current_user, - { raw: params[:post_edit] }, + { raw: params[:post_edit] }, skip_validations: true, - skip_revision: true + skip_revision: true, ) end end @@ -597,5 +632,4 @@ class Admin::UsersController < Admin::StaffController def refresh_browser(user) MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] end - end diff --git a/app/controllers/admin/watched_words_controller.rb b/app/controllers/admin/watched_words_controller.rb index badfcd2173..2a5cbb496f 100644 --- a/app/controllers/admin/watched_words_controller.rb +++ b/app/controllers/admin/watched_words_controller.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -require 'csv' +require "csv" class Admin::WatchedWordsController < Admin::StaffController skip_before_action :check_xhr, only: [:download] def index watched_words = WatchedWord.by_action - watched_words = watched_words.where.not(action: WatchedWord.actions[:tag]) if !SiteSetting.tagging_enabled + watched_words = + watched_words.where.not(action: WatchedWord.actions[:tag]) if !SiteSetting.tagging_enabled render_json_dump WatchedWordListSerializer.new(watched_words, scope: guardian, root: false) end @@ -38,19 +39,20 @@ class Admin::WatchedWordsController < Admin::StaffController begin CSV.foreach(file.tempfile, encoding: "bom|utf-8") do |row| if row[0].present? && (!has_replacement || row[1].present?) - watched_word = WatchedWord.create_or_update_word( - word: row[0], - replacement: has_replacement ? row[1] : nil, - action_key: action_key, - case_sensitive: "true" == row[2]&.strip&.downcase - ) + watched_word = + WatchedWord.create_or_update_word( + word: row[0], + replacement: has_replacement ? row[1] : nil, + action_key: action_key, + case_sensitive: "true" == row[2]&.strip&.downcase, + ) if watched_word.valid? StaffActionLogger.new(current_user).log_watched_words_creation(watched_word) end end end - data = { url: '/ok' } + data = { url: "/ok" } rescue => e data = failed_json.merge(errors: [e.message]) end @@ -73,10 +75,10 @@ class Admin::WatchedWordsController < Admin::StaffController content = content.pluck(:word).join("\n") end - headers['Content-Length'] = content.bytesize.to_s + headers["Content-Length"] = content.bytesize.to_s send_data content, - filename: "#{Discourse.current_hostname}-watched-words-#{name}.csv", - content_type: "text/csv" + filename: "#{Discourse.current_hostname}-watched-words-#{name}.csv", + content_type: "text/csv" end def clear_all @@ -85,10 +87,12 @@ class Admin::WatchedWordsController < Admin::StaffController action = WatchedWord.actions[name] raise Discourse::NotFound if !action - WatchedWord.where(action: action).find_each do |watched_word| - watched_word.destroy! - StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word) - end + WatchedWord + .where(action: action) + .find_each do |watched_word| + watched_word.destroy! + StaffActionLogger.new(current_user).log_watched_words_deletion(watched_word) + end WordWatcher.clear_cache! render json: success_json end diff --git a/app/controllers/admin/web_hooks_controller.rb b/app/controllers/admin/web_hooks_controller.rb index cb747112bd..1ca6e81a26 100644 --- a/app/controllers/admin/web_hooks_controller.rb +++ b/app/controllers/admin/web_hooks_controller.rb @@ -1,17 +1,19 @@ # frozen_string_literal: true class Admin::WebHooksController < Admin::AdminController - before_action :fetch_web_hook, only: %i(show update destroy list_events bulk_events ping) + before_action :fetch_web_hook, only: %i[show update destroy list_events bulk_events ping] def index limit = 50 offset = params[:offset].to_i - web_hooks = WebHook.limit(limit) - .offset(offset) - .includes(:web_hook_event_types) - .includes(:categories) - .includes(:groups) + web_hooks = + WebHook + .limit(limit) + .offset(offset) + .includes(:web_hook_event_types) + .includes(:categories) + .includes(:groups) json = { web_hooks: serialize_data(web_hooks, AdminWebHookSerializer), @@ -19,29 +21,34 @@ class Admin::WebHooksController < Admin::AdminController event_types: WebHookEventType.active, default_event_types: WebHook.default_event_types, content_types: WebHook.content_types.map { |name, id| { id: id, name: name } }, - delivery_statuses: WebHook.last_delivery_statuses.map { |name, id| { id: id, name: name.to_s } }, + delivery_statuses: + WebHook.last_delivery_statuses.map { |name, id| { id: id, name: name.to_s } }, }, total_rows_web_hooks: WebHook.count, - load_more_web_hooks: admin_web_hooks_path(limit: limit, offset: offset + limit, format: :json) + load_more_web_hooks: + admin_web_hooks_path(limit: limit, offset: offset + limit, format: :json), } render json: MultiJson.dump(json), status: 200 end def show - render_serialized(@web_hook, AdminWebHookSerializer, root: 'web_hook') + render_serialized(@web_hook, AdminWebHookSerializer, root: "web_hook") end def edit - render_serialized(@web_hook, AdminWebHookSerializer, root: 'web_hook') + render_serialized(@web_hook, AdminWebHookSerializer, root: "web_hook") end def create web_hook = WebHook.new(web_hook_params) if web_hook.save - StaffActionLogger.new(current_user).log_web_hook(web_hook, UserHistory.actions[:web_hook_create]) - render_serialized(web_hook, AdminWebHookSerializer, root: 'web_hook') + StaffActionLogger.new(current_user).log_web_hook( + web_hook, + UserHistory.actions[:web_hook_create], + ) + render_serialized(web_hook, AdminWebHookSerializer, root: "web_hook") else render_json_error web_hook.errors.full_messages end @@ -49,8 +56,12 @@ class Admin::WebHooksController < Admin::AdminController def update if @web_hook.update(web_hook_params) - StaffActionLogger.new(current_user).log_web_hook(@web_hook, UserHistory.actions[:web_hook_update], changes: @web_hook.saved_changes) - render_serialized(@web_hook, AdminWebHookSerializer, root: 'web_hook') + StaffActionLogger.new(current_user).log_web_hook( + @web_hook, + UserHistory.actions[:web_hook_update], + changes: @web_hook.saved_changes, + ) + render_serialized(@web_hook, AdminWebHookSerializer, root: "web_hook") else render_json_error @web_hook.errors.full_messages end @@ -58,7 +69,10 @@ class Admin::WebHooksController < Admin::AdminController def destroy @web_hook.destroy! - StaffActionLogger.new(current_user).log_web_hook(@web_hook, UserHistory.actions[:web_hook_destroy]) + StaffActionLogger.new(current_user).log_web_hook( + @web_hook, + UserHistory.actions[:web_hook_destroy], + ) render json: success_json end @@ -67,12 +81,17 @@ class Admin::WebHooksController < Admin::AdminController offset = params[:offset].to_i json = { - web_hook_events: serialize_data(@web_hook.web_hook_events.limit(limit).offset(offset), AdminWebHookEventSerializer), + web_hook_events: + serialize_data( + @web_hook.web_hook_events.limit(limit).offset(offset), + AdminWebHookEventSerializer, + ), total_rows_web_hook_events: @web_hook.web_hook_events.count, - load_more_web_hook_events: web_hook_events_admin_api_index_path(limit: limit, offset: offset + limit, format: :json), + load_more_web_hook_events: + web_hook_events_admin_api_index_path(limit: limit, offset: offset + limit, format: :json), extras: { - web_hook_id: @web_hook.id - } + web_hook_id: @web_hook.id, + }, } render json: MultiJson.dump(json), status: 200 @@ -91,26 +110,37 @@ class Admin::WebHooksController < Admin::AdminController web_hook = web_hook_event.web_hook emitter = WebHookEmitter.new(web_hook, web_hook_event) emitter.emit!(headers: MultiJson.load(web_hook_event.headers), body: web_hook_event.payload) - render_serialized(web_hook_event, AdminWebHookEventSerializer, root: 'web_hook_event') + render_serialized(web_hook_event, AdminWebHookEventSerializer, root: "web_hook_event") else render json: failed_json end end def ping - Jobs.enqueue(:emit_web_hook_event, web_hook_id: @web_hook.id, event_type: 'ping', event_name: 'ping') + Jobs.enqueue( + :emit_web_hook_event, + web_hook_id: @web_hook.id, + event_type: "ping", + event_name: "ping", + ) render json: success_json end private def web_hook_params - params.require(:web_hook).permit(:payload_url, :content_type, :secret, - :wildcard_web_hook, :active, :verify_certificate, - web_hook_event_type_ids: [], - group_ids: [], - tag_names: [], - category_ids: []) + params.require(:web_hook).permit( + :payload_url, + :content_type, + :secret, + :wildcard_web_hook, + :active, + :verify_certificate, + web_hook_event_type_ids: [], + group_ids: [], + tag_names: [], + category_ids: [], + ) end def fetch_web_hook diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0ab26fef89..41bd4889bc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'current_user' +require "current_user" class ApplicationController < ActionController::Base include CurrentUser @@ -43,17 +43,18 @@ class ApplicationController < ActionController::Base before_action :block_if_requires_login before_action :preload_json before_action :check_xhr - after_action :add_readonly_header - after_action :perform_refresh_session - after_action :dont_cache_page - after_action :conditionally_allow_site_embedding - after_action :ensure_vary_header - after_action :add_noindex_header, if: -> { is_feed_request? || !SiteSetting.allow_index_in_robots_txt } - after_action :add_noindex_header_to_non_canonical, if: :spa_boot_request? + after_action :add_readonly_header + after_action :perform_refresh_session + after_action :dont_cache_page + after_action :conditionally_allow_site_embedding + after_action :ensure_vary_header + after_action :add_noindex_header, + if: -> { is_feed_request? || !SiteSetting.allow_index_in_robots_txt } + after_action :add_noindex_header_to_non_canonical, if: :spa_boot_request? around_action :link_preload, if: -> { spa_boot_request? && GlobalSetting.preload_link_header } - HONEYPOT_KEY ||= 'HONEYPOT_KEY' - CHALLENGE_KEY ||= 'CHALLENGE_KEY' + HONEYPOT_KEY ||= "HONEYPOT_KEY" + CHALLENGE_KEY ||= "CHALLENGE_KEY" layout :set_layout @@ -68,12 +69,12 @@ class ApplicationController < ActionController::Base def use_crawler_layout? @use_crawler_layout ||= - request.user_agent && - (request.media_type.blank? || request.media_type.include?('html')) && - !['json', 'rss'].include?(params[:format]) && - (has_escaped_fragment? || params.key?("print") || show_browser_update? || - CrawlerDetection.crawler?(request.user_agent, request.headers["HTTP_VIA"]) - ) + request.user_agent && (request.media_type.blank? || request.media_type.include?("html")) && + !%w[json rss].include?(params[:format]) && + ( + has_escaped_fragment? || params.key?("print") || show_browser_update? || + CrawlerDetection.crawler?(request.user_agent, request.headers["HTTP_VIA"]) + ) end def perform_refresh_session @@ -91,19 +92,16 @@ class ApplicationController < ActionController::Base response.cache_control[:no_cache] = true response.cache_control[:extras] = ["no-store"] end - if SiteSetting.login_required - response.headers['Discourse-No-Onebox'] = '1' - end + response.headers["Discourse-No-Onebox"] = "1" if SiteSetting.login_required end def conditionally_allow_site_embedding - if SiteSetting.allow_embedding_site_in_an_iframe - response.headers.delete('X-Frame-Options') - end + response.headers.delete("X-Frame-Options") if SiteSetting.allow_embedding_site_in_an_iframe end def ember_cli_required? - Rails.env.development? && ENV["ALLOW_EMBER_CLI_PROXY_BYPASS"] != "1" && request.headers['X-Discourse-Ember-CLI'] != 'true' + Rails.env.development? && ENV["ALLOW_EMBER_CLI_PROXY_BYPASS"] != "1" && + request.headers["X-Discourse-Ember-CLI"] != "true" end def application_layout @@ -118,14 +116,16 @@ class ApplicationController < ActionController::Base return "crawler" end - use_crawler_layout? ? 'crawler' : application_layout + use_crawler_layout? ? "crawler" : application_layout end - class RenderEmpty < StandardError; end - class PluginDisabled < StandardError; end + class RenderEmpty < StandardError + end + class PluginDisabled < StandardError + end rescue_from RenderEmpty do - with_resolved_locale { render 'default/empty' } + with_resolved_locale { render "default/empty" } end rescue_from ArgumentError do |e| @@ -147,10 +147,10 @@ class ApplicationController < ActionController::Base end rescue_from Discourse::SiteSettingMissing do |e| - render_json_error I18n.t('site_setting_missing', name: e.message), status: 500 + render_json_error I18n.t("site_setting_missing", name: e.message), status: 500 end - rescue_from ActionController::RoutingError, PluginDisabled do + rescue_from ActionController::RoutingError, PluginDisabled do rescue_discourse_actions(:not_found, 404) end @@ -180,21 +180,20 @@ class ApplicationController < ActionController::Base rescue_from RateLimiter::LimitExceeded do |e| retry_time_in_seconds = e&.available_in - response_headers = { - 'Retry-After': retry_time_in_seconds.to_s - } + response_headers = { "Retry-After": retry_time_in_seconds.to_s } - if e&.error_code - response_headers['Discourse-Rate-Limit-Error-Code'] = e.error_code - end + response_headers["Discourse-Rate-Limit-Error-Code"] = e.error_code if e&.error_code with_resolved_locale do render_json_error( e.description, type: :rate_limit, status: 429, - extras: { wait_seconds: retry_time_in_seconds, time_left: e&.time_left }, - headers: response_headers + extras: { + wait_seconds: retry_time_in_seconds, + time_left: e&.time_left, + }, + headers: response_headers, ) end end @@ -208,10 +207,7 @@ class ApplicationController < ActionController::Base end rescue_from Discourse::InvalidParameters do |e| - opts = { - custom_message: 'invalid_params', - custom_message_params: { message: e.message } - } + opts = { custom_message: "invalid_params", custom_message_params: { message: e.message } } if (request.format && request.format.json?) || request.xhr? || !request.get? rescue_discourse_actions(:invalid_parameters, 400, opts.merge(include_ember: true)) @@ -226,14 +222,12 @@ class ApplicationController < ActionController::Base e.status, check_permalinks: e.check_permalinks, original_path: e.original_path, - custom_message: e.custom_message + custom_message: e.custom_message, ) end rescue_from Discourse::InvalidAccess do |e| - if e.opts[:delete_cookie].present? - cookies.delete(e.opts[:delete_cookie]) - end + cookies.delete(e.opts[:delete_cookie]) if e.opts[:delete_cookie].present? rescue_discourse_actions( :invalid_access, @@ -241,7 +235,7 @@ class ApplicationController < ActionController::Base include_ember: true, custom_message: e.custom_message, custom_message_params: e.custom_message_params, - group: e.group + group: e.group, ) end @@ -249,20 +243,16 @@ class ApplicationController < ActionController::Base unless response_body respond_to do |format| format.json do - render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503 - end - format.html do - render status: 503, layout: 'no_ember', template: 'exceptions/read_only' + render_json_error I18n.t("read_only_mode_enabled"), type: :read_only, status: 503 end + format.html { render status: 503, layout: "no_ember", template: "exceptions/read_only" } end end end rescue_from SecondFactor::AuthManager::SecondFactorRequired do |e| if request.xhr? - render json: { - second_factor_challenge_nonce: e.nonce - }, status: 403 + render json: { second_factor_challenge_nonce: e.nonce }, status: 403 else redirect_to session_2fa_path(nonce: e.nonce) end @@ -274,7 +264,7 @@ class ApplicationController < ActionController::Base def redirect_with_client_support(url, options = {}) if request.xhr? - response.headers['Discourse-Xhr-Redirect'] = 'true' + response.headers["Discourse-Xhr-Redirect"] = "true" render plain: url else redirect_to url, options @@ -283,9 +273,9 @@ class ApplicationController < ActionController::Base def rescue_discourse_actions(type, status_code, opts = nil) opts ||= {} - show_json_errors = (request.format && request.format.json?) || - (request.xhr?) || - ((params[:external_id] || '').ends_with? '.json') + show_json_errors = + (request.format && request.format.json?) || (request.xhr?) || + ((params[:external_id] || "").ends_with? ".json") if type == :not_found && opts[:check_permalinks] url = opts[:original_path] || request.fullpath @@ -295,7 +285,9 @@ class ApplicationController < ActionController::Base # cause category / topic was deleted if permalink.present? && permalink.target_url # permalink present, redirect to that URL - redirect_with_client_support permalink.target_url, status: :moved_permanently, allow_other_host: true + redirect_with_client_support permalink.target_url, + status: :moved_permanently, + allow_other_host: true return end end @@ -321,11 +313,15 @@ class ApplicationController < ActionController::Base with_resolved_locale(check_current_user: false) do # Include error in HTML format for topics#show. - if (request.params[:controller] == 'topics' && request.params[:action] == 'show') || (request.params[:controller] == 'categories' && request.params[:action] == 'find_by_slug') + if (request.params[:controller] == "topics" && request.params[:action] == "show") || + ( + request.params[:controller] == "categories" && + request.params[:action] == "find_by_slug" + ) opts[:extras] = { - title: I18n.t('page_not_found.page_title'), + title: I18n.t("page_not_found.page_title"), html: build_not_found_page(error_page_opts), - group: error_page_opts[:group] + group: error_page_opts[:group], } end end @@ -340,7 +336,7 @@ class ApplicationController < ActionController::Base return render plain: message, status: status_code end with_resolved_locale do - error_page_opts[:layout] = (opts[:include_ember] && @preloaded) ? 'application' : 'no_ember' + error_page_opts[:layout] = (opts[:include_ember] && @preloaded) ? "application" : "no_ember" render html: build_not_found_page(error_page_opts) end end @@ -373,9 +369,8 @@ class ApplicationController < ActionController::Base def clear_notifications if current_user && !@readonly_mode - - cookie_notifications = cookies['cn'] - notifications = request.headers['Discourse-Clear-Notifications'] + cookie_notifications = cookies["cn"] + notifications = request.headers["Discourse-Clear-Notifications"] if cookie_notifications if notifications.present? @@ -392,22 +387,28 @@ class ApplicationController < ActionController::Base current_user.publish_notifications_state cookie_args = {} cookie_args[:path] = Discourse.base_path if Discourse.base_path.present? - cookies.delete('cn', cookie_args) + cookies.delete("cn", cookie_args) end end end def with_resolved_locale(check_current_user: true) - if check_current_user && (user = current_user rescue nil) + if check_current_user && + ( + user = + begin + current_user + rescue StandardError + nil + end + ) locale = user.effective_locale else locale = Discourse.anonymous_locale(request) locale ||= SiteSetting.default_locale end - if !I18n.locale_available?(locale) - locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE - end + locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE if !I18n.locale_available?(locale) I18n.ensure_all_loaded! I18n.with_locale(locale) { yield } @@ -458,7 +459,8 @@ class ApplicationController < ActionController::Base safe_mode = safe_mode.split(",") request.env[NO_THEMES] = safe_mode.include?(NO_THEMES) || safe_mode.include?(LEGACY_NO_THEMES) request.env[NO_PLUGINS] = safe_mode.include?(NO_PLUGINS) - request.env[NO_UNOFFICIAL_PLUGINS] = safe_mode.include?(NO_UNOFFICIAL_PLUGINS) || safe_mode.include?(LEGACY_NO_UNOFFICIAL_PLUGINS) + request.env[NO_UNOFFICIAL_PLUGINS] = safe_mode.include?(NO_UNOFFICIAL_PLUGINS) || + safe_mode.include?(LEGACY_NO_UNOFFICIAL_PLUGINS) end end @@ -471,8 +473,7 @@ class ApplicationController < ActionController::Base theme_id = nil if (preview_theme_id = request[:preview_theme_id]&.to_i) && - guardian.allow_themes?([preview_theme_id], include_preview: true) - + guardian.allow_themes?([preview_theme_id], include_preview: true) theme_id = preview_theme_id end @@ -491,7 +492,8 @@ class ApplicationController < ActionController::Base theme_id = ids.first if guardian.allow_themes?(ids) end - if theme_id.blank? && SiteSetting.default_theme_id != -1 && guardian.allow_themes?([SiteSetting.default_theme_id]) + if theme_id.blank? && SiteSetting.default_theme_id != -1 && + guardian.allow_themes?([SiteSetting.default_theme_id]) theme_id = SiteSetting.default_theme_id end @@ -533,13 +535,11 @@ class ApplicationController < ActionController::Base def render_json_dump(obj, opts = nil) opts ||= {} if opts[:rest_serializer] - obj['__rest_serializer'] = "1" - opts.each do |k, v| - obj[k] = v if k.to_s.start_with?("refresh_") - end + obj["__rest_serializer"] = "1" + opts.each { |k, v| obj[k] = v if k.to_s.start_with?("refresh_") } - obj['extras'] = opts[:extras] if opts[:extras] - obj['meta'] = opts[:meta] if opts[:meta] + obj["extras"] = opts[:extras] if opts[:extras] + obj["meta"] = opts[:meta] if opts[:meta] end render json: MultiJson.dump(obj), status: opts[:status] || 200 @@ -557,29 +557,33 @@ class ApplicationController < ActionController::Base def fetch_user_from_params(opts = nil, eager_load = []) opts ||= {} - user = if params[:username] - username_lower = params[:username].downcase.chomp('.json') + user = + if params[:username] + username_lower = params[:username].downcase.chomp(".json") - if current_user && current_user.username_lower == username_lower - current_user - else - find_opts = { username_lower: username_lower } - find_opts[:active] = true unless opts[:include_inactive] || current_user.try(:staff?) - result = User - (result = result.includes(*eager_load)) if !eager_load.empty? - result.find_by(find_opts) + if current_user && current_user.username_lower == username_lower + current_user + else + find_opts = { username_lower: username_lower } + find_opts[:active] = true unless opts[:include_inactive] || current_user.try(:staff?) + result = User + (result = result.includes(*eager_load)) if !eager_load.empty? + result.find_by(find_opts) + end + elsif params[:external_id] + external_id = params[:external_id].chomp(".json") + if provider_name = params[:external_provider] + raise Discourse::InvalidAccess unless guardian.is_admin? # external_id might be something sensitive + provider = Discourse.enabled_authenticators.find { |a| a.name == provider_name } + raise Discourse::NotFound if !provider&.is_managed? # Only managed authenticators use UserAssociatedAccount + UserAssociatedAccount.find_by( + provider_name: provider_name, + provider_uid: external_id, + )&.user + else + SingleSignOnRecord.find_by(external_id: external_id).try(:user) + end end - elsif params[:external_id] - external_id = params[:external_id].chomp('.json') - if provider_name = params[:external_provider] - raise Discourse::InvalidAccess unless guardian.is_admin? # external_id might be something sensitive - provider = Discourse.enabled_authenticators.find { |a| a.name == provider_name } - raise Discourse::NotFound if !provider&.is_managed? # Only managed authenticators use UserAssociatedAccount - UserAssociatedAccount.find_by(provider_name: provider_name, provider_uid: external_id)&.user - else - SingleSignOnRecord.find_by(external_id: external_id).try(:user) - end - end raise Discourse::NotFound if user.blank? guardian.ensure_can_see!(user) @@ -587,15 +591,17 @@ class ApplicationController < ActionController::Base end def post_ids_including_replies - post_ids = params[:post_ids].map(&:to_i) - post_ids |= PostReply.where(post_id: params[:reply_post_ids]).pluck(:reply_post_id) if params[:reply_post_ids] + post_ids = params[:post_ids].map(&:to_i) + post_ids |= PostReply.where(post_id: params[:reply_post_ids]).pluck(:reply_post_id) if params[ + :reply_post_ids + ] post_ids end def no_cookies # do your best to ensure response has no cookies # longer term we may want to push this into middleware - headers.delete 'Set-Cookie' + headers.delete "Set-Cookie" request.session_options[:skip] = true end @@ -615,9 +621,7 @@ class ApplicationController < ActionController::Base RateLimiter.new(nil, "second-factor-min-#{request.remote_ip}", 6, 1.minute).performed! - if user - RateLimiter.new(nil, "second-factor-min-#{user.username}", 6, 1.minute).performed! - end + RateLimiter.new(nil, "second-factor-min-#{user.username}", 6, 1.minute).performed! if user end private @@ -634,11 +638,17 @@ class ApplicationController < ActionController::Base end def preload_current_user_data - store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false))) - report = TopicTrackingState.report(current_user) - serializer = ActiveModel::ArraySerializer.new( - report, each_serializer: TopicTrackingStateSerializer, scope: guardian + store_preloaded( + "currentUser", + MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)), ) + report = TopicTrackingState.report(current_user) + serializer = + ActiveModel::ArraySerializer.new( + report, + each_serializer: TopicTrackingStateSerializer, + scope: guardian, + ) store_preloaded("topicTrackingStates", MultiJson.dump(serializer)) end @@ -648,20 +658,18 @@ class ApplicationController < ActionController::Base data = if @theme_id.present? { - top: Theme.lookup_field(@theme_id, target, "after_header"), - footer: Theme.lookup_field(@theme_id, target, "footer") + top: Theme.lookup_field(@theme_id, target, "after_header"), + footer: Theme.lookup_field(@theme_id, target, "footer"), } else {} end - if DiscoursePluginRegistry.custom_html - data.merge! DiscoursePluginRegistry.custom_html - end + data.merge! DiscoursePluginRegistry.custom_html if DiscoursePluginRegistry.custom_html DiscoursePluginRegistry.html_builders.each do |name, _| if name.start_with?("client:") - data[name.sub(/^client:/, '')] = DiscoursePluginRegistry.build_html(name, self) + data[name.sub(/^client:/, "")] = DiscoursePluginRegistry.build_html(name, self) end end @@ -703,7 +711,7 @@ class ApplicationController < ActionController::Base render( json: MultiJson.dump(create_errors_json(obj, opts)), - status: opts[:status] || status_code(obj) + status: opts[:status] || status_code(obj), ) end @@ -714,11 +722,11 @@ class ApplicationController < ActionController::Base end def success_json - { success: 'OK' } + { success: "OK" } end def failed_json - { failed: 'FAILED' } + { failed: "FAILED" } end def json_result(obj, opts = {}) @@ -727,17 +735,21 @@ class ApplicationController < ActionController::Base # If we were given a serializer, add the class to the json that comes back if opts[:serializer].present? - json[obj.class.name.underscore] = opts[:serializer].new(obj, scope: guardian).serializable_hash + json[obj.class.name.underscore] = opts[:serializer].new( + obj, + scope: guardian, + ).serializable_hash end render json: MultiJson.dump(json) else error_obj = nil if opts[:additional_errors] - error_target = opts[:additional_errors].find do |o| - target = obj.public_send(o) - target && target.errors.present? - end + error_target = + opts[:additional_errors].find do |o| + target = obj.public_send(o) + target && target.errors.present? + end error_obj = obj.public_send(error_target) if error_target end render_json_error(error_obj || obj) @@ -756,11 +768,15 @@ class ApplicationController < ActionController::Base def check_xhr # bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying return if !request.get? && (is_api? || is_user_api?) - raise ApplicationController::RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?) + unless ((request.format && request.format.json?) || request.xhr?) + raise ApplicationController::RenderEmpty.new + end end def apply_cdn_headers - Discourse.apply_cdn_headers(response.headers) if Discourse.is_cdn_request?(request.env, request.method) + if Discourse.is_cdn_request?(request.env, request.method) + Discourse.apply_cdn_headers(response.headers) + end end def self.requires_login(arg = {}) @@ -811,8 +827,9 @@ class ApplicationController < ActionController::Base if SiteSetting.auth_immediately && SiteSetting.enable_discourse_connect? # save original URL in a session so we can redirect after login session[:destination_url] = destination_url - redirect_to path('/session/sso') - elsif SiteSetting.auth_immediately && !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 && !cookies[:authentication_data] + redirect_to path("/session/sso") + elsif SiteSetting.auth_immediately && !SiteSetting.enable_local_logins && + Discourse.enabled_authenticators.length == 1 && !cookies[:authentication_data] # Only one authentication provider, direct straight to it. # If authentication_data is present, then we are halfway though registration. Don't redirect offsite cookies[:destination_url] = destination_url @@ -831,9 +848,7 @@ class ApplicationController < ActionController::Base # Redirects to provided URL scheme if # - request uses a valid public key and auth_redirect scheme # - one_time_password scope is allowed - if !current_user && - params.has_key?(:user_api_public_key) && - params.has_key?(:auth_redirect) + if !current_user && params.has_key?(:user_api_public_key) && params.has_key?(:auth_redirect) begin OpenSSL::PKey::RSA.new(params[:user_api_public_key]) rescue OpenSSL::PKey::RSAError @@ -872,26 +887,45 @@ class ApplicationController < ActionController::Base def should_enforce_2fa? disqualified_from_2fa_enforcement = request.format.json? || is_api? || current_user.anonymous? - enforcing_2fa = ((SiteSetting.enforce_second_factor == 'staff' && current_user.staff?) || SiteSetting.enforce_second_factor == 'all') - !disqualified_from_2fa_enforcement && enforcing_2fa && !current_user.has_any_second_factor_methods_enabled? + enforcing_2fa = + ( + (SiteSetting.enforce_second_factor == "staff" && current_user.staff?) || + SiteSetting.enforce_second_factor == "all" + ) + !disqualified_from_2fa_enforcement && enforcing_2fa && + !current_user.has_any_second_factor_methods_enabled? end def build_not_found_page(opts = {}) if SiteSetting.bootstrap_error_pages? preload_json - opts[:layout] = 'application' if opts[:layout] == 'no_ember' + opts[:layout] = "application" if opts[:layout] == "no_ember" end - @current_user = current_user rescue nil + @current_user = + begin + current_user + rescue StandardError + nil + end if !SiteSetting.login_required? || @current_user key = "page_not_found_topics:#{I18n.locale}" - @topics_partial = Discourse.cache.fetch(key, expires_in: 10.minutes) do - category_topic_ids = Category.pluck(:topic_id).compact - @top_viewed = TopicQuery.new(nil, except_topic_ids: category_topic_ids).list_top_for("monthly").topics.first(10) - @recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10) - render_to_string partial: '/exceptions/not_found_topics', formats: [:html] - end.html_safe + @topics_partial = + Discourse + .cache + .fetch(key, expires_in: 10.minutes) do + category_topic_ids = Category.pluck(:topic_id).compact + @top_viewed = + TopicQuery + .new(nil, except_topic_ids: category_topic_ids) + .list_top_for("monthly") + .topics + .first(10) + @recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10) + render_to_string partial: "/exceptions/not_found_topics", formats: [:html] + end + .html_safe end @container_class = "wrap not-found-container" @@ -902,13 +936,16 @@ class ApplicationController < ActionController::Base params[:slug] = params[:slug].first if params[:slug].kind_of?(Array) params[:id] = params[:id].first if params[:id].kind_of?(Array) - @slug = (params[:slug].presence || params[:id].presence || "").to_s.tr('-', ' ') + @slug = (params[:slug].presence || params[:id].presence || "").to_s.tr("-", " ") - render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: '/exceptions/not_found' + render_to_string status: opts[:status], + layout: opts[:layout], + formats: [:html], + template: "/exceptions/not_found" end def is_asset_path - request.env['DISCOURSE_IS_ASSET_PATH'] = 1 + request.env["DISCOURSE_IS_ASSET_PATH"] = 1 end def is_feed_request? @@ -916,19 +953,20 @@ class ApplicationController < ActionController::Base end def add_noindex_header - if request.get? && !response.headers['X-Robots-Tag'] + if request.get? && !response.headers["X-Robots-Tag"] if SiteSetting.allow_index_in_robots_txt - response.headers['X-Robots-Tag'] = 'noindex' + response.headers["X-Robots-Tag"] = "noindex" else - response.headers['X-Robots-Tag'] = 'noindex, nofollow' + response.headers["X-Robots-Tag"] = "noindex, nofollow" end end end def add_noindex_header_to_non_canonical canonical = (@canonical_url || @default_canonical) - if canonical.present? && canonical != request.url && !SiteSetting.allow_indexing_non_canonical_urls - response.headers['X-Robots-Tag'] ||= 'noindex' + if canonical.present? && canonical != request.url && + !SiteSetting.allow_indexing_non_canonical_urls + response.headers["X-Robots-Tag"] ||= "noindex" end end @@ -955,7 +993,7 @@ class ApplicationController < ActionController::Base # returns an array of integers given a param key # returns nil if key is not found - def param_to_integer_list(key, delimiter = ',') + def param_to_integer_list(key, delimiter = ",") case params[key] when String params[key].split(delimiter).map(&:to_i) @@ -978,20 +1016,19 @@ class ApplicationController < ActionController::Base user_agent = request.user_agent&.downcase return if user_agent.blank? - SiteSetting.slow_down_crawler_user_agents.downcase.split("|").each do |crawler| - if user_agent.include?(crawler) - key = "#{crawler}_crawler_rate_limit" - limiter = RateLimiter.new( - nil, - key, - 1, - SiteSetting.slow_down_crawler_rate, - error_code: key - ) - limiter.performed! - break + SiteSetting + .slow_down_crawler_user_agents + .downcase + .split("|") + .each do |crawler| + if user_agent.include?(crawler) + key = "#{crawler}_crawler_rate_limit" + limiter = + RateLimiter.new(nil, key, 1, SiteSetting.slow_down_crawler_rate, error_code: key) + limiter.performed! + break + end end - end end def run_second_factor!(action_class, action_data = nil) @@ -1000,9 +1037,8 @@ class ApplicationController < ActionController::Base yield(manager) if block_given? result = manager.run!(request, params, secure_session) - if !result.no_second_factors_enabled? && - !result.second_factor_auth_completed? && - !result.second_factor_auth_skipped? + if !result.no_second_factors_enabled? && !result.second_factor_auth_completed? && + !result.second_factor_auth_skipped? # should never happen, but I want to know if somehow it does! (osama) raise "2fa process ended up in a bad state!" end @@ -1013,7 +1049,7 @@ class ApplicationController < ActionController::Base def link_preload @links_to_preload = [] yield - response.headers['Link'] = @links_to_preload.join(', ') if !@links_to_preload.empty? + response.headers["Link"] = @links_to_preload.join(", ") if !@links_to_preload.empty? end def spa_boot_request? diff --git a/app/controllers/associated_groups_controller.rb b/app/controllers/associated_groups_controller.rb index cf82ae20c7..136a97e594 100644 --- a/app/controllers/associated_groups_controller.rb +++ b/app/controllers/associated_groups_controller.rb @@ -5,6 +5,6 @@ class AssociatedGroupsController < ApplicationController def index guardian.ensure_can_associate_groups! - render_serialized(AssociatedGroup.all, AssociatedGroupSerializer, root: 'associated_groups') + render_serialized(AssociatedGroup.all, AssociatedGroupSerializer, root: "associated_groups") end end diff --git a/app/controllers/badges_controller.rb b/app/controllers/badges_controller.rb index 3b90fce090..e505ea7c59 100644 --- a/app/controllers/badges_controller.rb +++ b/app/controllers/badges_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class BadgesController < ApplicationController - skip_before_action :check_xhr, only: [:index, :show] + skip_before_action :check_xhr, only: %i[index show] after_action :add_noindex_header def index @@ -16,18 +16,29 @@ class BadgesController < ApplicationController if (params[:only_listable] == "true") || !request.xhr? # NOTE: this is sorted client side if needed - badges = badges.includes(:badge_grouping) - .includes(:badge_type, :image_upload) - .where(enabled: true, listable: true) + badges = + badges + .includes(:badge_grouping) + .includes(:badge_type, :image_upload) + .where(enabled: true, listable: true) end badges = badges.to_a user_badges = nil if current_user - user_badges = Set.new(current_user.user_badges.select('distinct badge_id').pluck(:badge_id)) + user_badges = Set.new(current_user.user_badges.select("distinct badge_id").pluck(:badge_id)) end - serialized = MultiJson.dump(serialize_data(badges, BadgeIndexSerializer, root: "badges", user_badges: user_badges, include_long_description: true)) + serialized = + MultiJson.dump( + serialize_data( + badges, + BadgeIndexSerializer, + root: "badges", + user_badges: user_badges, + include_long_description: true, + ), + ) respond_to do |format| format.html do store_preloaded "badges", serialized @@ -42,27 +53,27 @@ class BadgesController < ApplicationController params.require(:id) @badge = Badge.enabled.find(params[:id]) - @rss_title = I18n.t('rss_description.badge', display_name: @badge.display_name, site_title: SiteSetting.title) + @rss_title = + I18n.t( + "rss_description.badge", + display_name: @badge.display_name, + site_title: SiteSetting.title, + ) @rss_link = "#{Discourse.base_url}/badges/#{@badge.id}/#{@badge.slug}" if current_user user_badge = UserBadge.find_by(user_id: current_user.id, badge_id: @badge.id) - if user_badge && user_badge.notification - user_badge.notification.update read: true - end - if user_badge - @badge.has_badge = true - end + user_badge.notification.update read: true if user_badge && user_badge.notification + @badge.has_badge = true if user_badge end - serialized = MultiJson.dump(serialize_data(@badge, BadgeSerializer, root: "badge", include_long_description: true)) + serialized = + MultiJson.dump( + serialize_data(@badge, BadgeSerializer, root: "badge", include_long_description: true), + ) respond_to do |format| - format.rss do - @rss_description = @badge.long_description - end - format.html do - store_preloaded "badge", serialized - end + format.rss { @rss_description = @badge.long_description } + format.html { store_preloaded "badge", serialized } format.json { render json: serialized } end end diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index bfeaa58a3b..a3e385e585 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -6,26 +6,34 @@ class BookmarksController < ApplicationController def create params.require(:bookmarkable_id) params.require(:bookmarkable_type) - params.permit(:bookmarkable_id, :bookmarkable_type, :name, :reminder_at, :auto_delete_preference) + params.permit( + :bookmarkable_id, + :bookmarkable_type, + :name, + :reminder_at, + :auto_delete_preference, + ) RateLimiter.new( - current_user, "create_bookmark", SiteSetting.max_bookmarks_per_day, 1.day.to_i + current_user, + "create_bookmark", + SiteSetting.max_bookmarks_per_day, + 1.day.to_i, ).performed! bookmark_manager = BookmarkManager.new(current_user) - bookmark = bookmark_manager.create_for( - bookmarkable_id: params[:bookmarkable_id], - bookmarkable_type: params[:bookmarkable_type], - name: params[:name], - reminder_at: params[:reminder_at], - options: { - auto_delete_preference: params[:auto_delete_preference] - } - ) + bookmark = + bookmark_manager.create_for( + bookmarkable_id: params[:bookmarkable_id], + bookmarkable_type: params[:bookmarkable_type], + name: params[:name], + reminder_at: params[:reminder_at], + options: { + auto_delete_preference: params[:auto_delete_preference], + }, + ) - if bookmark_manager.errors.empty? - return render json: success_json.merge(id: bookmark.id) - end + return render json: success_json.merge(id: bookmark.id) if bookmark_manager.errors.empty? render json: failed_json.merge(errors: bookmark_manager.errors.full_messages), status: 400 end @@ -33,7 +41,8 @@ class BookmarksController < ApplicationController def destroy params.require(:id) destroyed_bookmark = BookmarkManager.new(current_user).destroy(params[:id]) - render json: success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) + render json: + success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) end def update @@ -46,13 +55,11 @@ class BookmarksController < ApplicationController name: params[:name], reminder_at: params[:reminder_at], options: { - auto_delete_preference: params[:auto_delete_preference] - } + auto_delete_preference: params[:auto_delete_preference], + }, ) - if bookmark_manager.errors.empty? - return render json: success_json - end + return render json: success_json if bookmark_manager.errors.empty? render json: failed_json.merge(errors: bookmark_manager.errors.full_messages), status: 400 end @@ -63,9 +70,7 @@ class BookmarksController < ApplicationController bookmark_manager = BookmarkManager.new(current_user) bookmark_manager.toggle_pin(bookmark_id: params[:bookmark_id]) - if bookmark_manager.errors.empty? - return render json: success_json - end + return render json: success_json if bookmark_manager.errors.empty? render json: failed_json.merge(errors: bookmark_manager.errors.full_messages), status: 400 end diff --git a/app/controllers/bootstrap_controller.rb b/app/controllers/bootstrap_controller.rb index 8f7c4b4f62..48a31a6e16 100644 --- a/app/controllers/bootstrap_controller.rb +++ b/app/controllers/bootstrap_controller.rb @@ -37,35 +37,34 @@ class BootstrapController < ApplicationController assets_fake_request.env["QUERY_STRING"] = query end - Discourse.find_plugin_css_assets( - include_official: allow_plugins?, - include_unofficial: allow_third_party_plugins?, - mobile_view: mobile_view?, - desktop_view: !mobile_view?, - request: assets_fake_request - ).each do |file| - add_style(file, plugin: true) - end + Discourse + .find_plugin_css_assets( + include_official: allow_plugins?, + include_unofficial: allow_third_party_plugins?, + mobile_view: mobile_view?, + desktop_view: !mobile_view?, + request: assets_fake_request, + ) + .each { |file| add_style(file, plugin: true) } add_style(mobile_view? ? :mobile_theme : :desktop_theme) if theme_id.present? extra_locales = [] if ExtraLocalesController.client_overrides_exist? - extra_locales << ExtraLocalesController.url('overrides') + extra_locales << ExtraLocalesController.url("overrides") end - if staff? - extra_locales << ExtraLocalesController.url('admin') - end + extra_locales << ExtraLocalesController.url("admin") if staff? - if admin? - extra_locales << ExtraLocalesController.url('wizard') - end + extra_locales << ExtraLocalesController.url("wizard") if admin? - plugin_js = Discourse.find_plugin_js_assets( - include_official: allow_plugins?, - include_unofficial: allow_third_party_plugins?, - request: assets_fake_request - ).map { |f| script_asset_path(f) } + plugin_js = + Discourse + .find_plugin_js_assets( + include_official: allow_plugins?, + include_unofficial: allow_third_party_plugins?, + request: assets_fake_request, + ) + .map { |f| script_asset_path(f) } plugin_test_js = if Rails.env != "production" @@ -76,7 +75,7 @@ class BootstrapController < ApplicationController bootstrap = { theme_id: theme_id, - theme_color: "##{ColorScheme.hex_for_name('header_background', scheme_id)}", + theme_color: "##{ColorScheme.hex_for_name("header_background", scheme_id)}", title: SiteSetting.title, current_homepage: current_homepage, locale_script: locale, @@ -90,7 +89,7 @@ class BootstrapController < ApplicationController html_classes: html_classes, html_lang: html_lang, login_path: main_app.login_path, - authentication_data: authentication_data + authentication_data: authentication_data, } bootstrap[:extra_locales] = extra_locales if extra_locales.present? bootstrap[:csrf_token] = form_authenticity_token if current_user @@ -99,39 +98,44 @@ class BootstrapController < ApplicationController end def plugin_css_for_tests - urls = Discourse.find_plugin_css_assets( - include_disabled: true, - desktop_view: true, - ).map do |target| - details = Stylesheet::Manager.new().stylesheet_details(target, 'all') - details[0][:new_href] - end + urls = + Discourse + .find_plugin_css_assets(include_disabled: true, desktop_view: true) + .map do |target| + details = Stylesheet::Manager.new().stylesheet_details(target, "all") + details[0][:new_href] + end stylesheet = <<~CSS /* For use in tests only - `@import`s all plugin stylesheets */ - #{urls.map { |url| "@import \"#{url}\";" }.join("\n") } + #{urls.map { |url| "@import \"#{url}\";" }.join("\n")} CSS - render plain: stylesheet, content_type: 'text/css' + render plain: stylesheet, content_type: "text/css" end -private + private + def add_scheme(scheme_id, media, css_class) return if scheme_id.to_i == -1 - if style = Stylesheet::Manager.new(theme_id: theme_id).color_scheme_stylesheet_details(scheme_id, media) + if style = + Stylesheet::Manager.new(theme_id: theme_id).color_scheme_stylesheet_details( + scheme_id, + media, + ) @stylesheets << { href: style[:new_href], media: media, class: css_class } end end def add_style(target, opts = nil) - if styles = Stylesheet::Manager.new(theme_id: theme_id).stylesheet_details(target, 'all') + if styles = Stylesheet::Manager.new(theme_id: theme_id).stylesheet_details(target, "all") styles.each do |style| @stylesheets << { href: style[:new_href], - media: 'all', + media: "all", theme_id: style[:theme_id], - target: style[:target] + target: style[:target], }.merge(opts || {}) end end @@ -150,7 +154,11 @@ private end def add_plugin_html(html, key) - add_if_present(html, key, DiscoursePluginRegistry.build_html("server:#{key.to_s.dasherize}", self)) + add_if_present( + html, + key, + DiscoursePluginRegistry.build_html("server:#{key.to_s.dasherize}", self), + ) end def create_theme_html @@ -159,10 +167,14 @@ private theme_view = mobile_view? ? :mobile : :desktop - add_if_present(theme_html, :body_tag, Theme.lookup_field(theme_id, theme_view, 'body_tag')) - add_if_present(theme_html, :head_tag, Theme.lookup_field(theme_id, theme_view, 'head_tag')) - add_if_present(theme_html, :header, Theme.lookup_field(theme_id, theme_view, 'header')) - add_if_present(theme_html, :translations, Theme.lookup_field(theme_id, :translations, I18n.locale)) + add_if_present(theme_html, :body_tag, Theme.lookup_field(theme_id, theme_view, "body_tag")) + add_if_present(theme_html, :head_tag, Theme.lookup_field(theme_id, theme_view, "head_tag")) + add_if_present(theme_html, :header, Theme.lookup_field(theme_id, theme_view, "header")) + add_if_present( + theme_html, + :translations, + Theme.lookup_field(theme_id, :translations, I18n.locale), + ) add_if_present(theme_html, :js, Theme.lookup_field(theme_id, :extra_js, nil)) theme_html @@ -171,5 +183,4 @@ private def add_if_present(hash, key, val) hash[key] = val if val.present? end - end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index d6e1a6e2de..c907c3829c 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -3,11 +3,19 @@ class CategoriesController < ApplicationController include TopicQueryParams - requires_login except: [:index, :categories_and_latest, :categories_and_top, :show, :redirect, :find_by_slug, :visible_groups] + requires_login except: %i[ + index + categories_and_latest + categories_and_top + show + redirect + find_by_slug + visible_groups + ] - before_action :fetch_category, only: [:show, :update, :destroy, :visible_groups] - before_action :initialize_staff_action_logger, only: [:create, :update, :destroy] - skip_before_action :check_xhr, only: [:index, :categories_and_latest, :categories_and_top, :redirect] + before_action :fetch_category, only: %i[show update destroy visible_groups] + before_action :initialize_staff_action_logger, only: %i[create update destroy] + skip_before_action :check_xhr, only: %i[index categories_and_latest categories_and_top redirect] SYMMETRICAL_CATEGORIES_TO_TOPICS_FACTOR = 1.5 MIN_CATEGORIES_TOPICS = 5 @@ -22,17 +30,20 @@ class CategoriesController < ApplicationController @description = SiteSetting.site_description - parent_category = Category.find_by_slug(params[:parent_category_id]) || Category.find_by(id: params[:parent_category_id].to_i) + parent_category = + Category.find_by_slug(params[:parent_category_id]) || + Category.find_by(id: params[:parent_category_id].to_i) - include_subcategories = SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" || - params[:include_subcategories] == "true" + include_subcategories = + SiteSetting.desktop_category_page_style == "subcategories_with_featured_topics" || + params[:include_subcategories] == "true" category_options = { is_homepage: current_homepage == "categories", parent_category_id: params[:parent_category_id], include_topics: include_topics(parent_category), include_subcategories: include_subcategories, - tag: params[:tag] + tag: params[:tag], } @category_list = CategoryList.new(guardian, category_options) @@ -40,35 +51,38 @@ class CategoriesController < ApplicationController if category_options[:is_homepage] && SiteSetting.short_site_description.present? @title = "#{SiteSetting.title} - #{SiteSetting.short_site_description}" elsif !category_options[:is_homepage] - @title = "#{I18n.t('js.filters.categories.title')} - #{SiteSetting.title}" + @title = "#{I18n.t("js.filters.categories.title")} - #{SiteSetting.title}" end respond_to do |format| format.html do - store_preloaded(@category_list.preload_key, MultiJson.dump(CategoryListSerializer.new(@category_list, scope: guardian))) + store_preloaded( + @category_list.preload_key, + MultiJson.dump(CategoryListSerializer.new(@category_list, scope: guardian)), + ) style = SiteSetting.desktop_category_page_style - topic_options = { - per_page: CategoriesController.topics_per_page, - no_definitions: true, - } + topic_options = { per_page: CategoriesController.topics_per_page, no_definitions: true } if style == "categories_and_latest_topics_created_date" - topic_options[:order] = 'created' + topic_options[:order] = "created" @topic_list = TopicQuery.new(current_user, topic_options).list_latest @topic_list.more_topics_url = url_for(public_send("latest_path", sort: :created)) elsif style == "categories_and_latest_topics" @topic_list = TopicQuery.new(current_user, topic_options).list_latest @topic_list.more_topics_url = url_for(public_send("latest_path")) elsif style == "categories_and_top_topics" - @topic_list = TopicQuery.new(current_user, topic_options).list_top_for(SiteSetting.top_page_default_timeframe.to_sym) + @topic_list = + TopicQuery.new(current_user, topic_options).list_top_for( + SiteSetting.top_page_default_timeframe.to_sym, + ) @topic_list.more_topics_url = url_for(public_send("top_path")) end if @topic_list.present? && @topic_list.topics.present? store_preloaded( @topic_list.preload_key, - MultiJson.dump(TopicListSerializer.new(@topic_list, scope: guardian)) + MultiJson.dump(TopicListSerializer.new(@topic_list, scope: guardian)), ) end @@ -109,7 +123,9 @@ class CategoriesController < ApplicationController by_category = Hash[change_requests.map { |cat, pos| [Category.find(cat.to_i), pos] }] unless guardian.is_admin? - raise Discourse::InvalidAccess unless by_category.keys.all? { |c| guardian.can_see_category? c } + unless by_category.keys.all? { |c| guardian.can_see_category? c } + raise Discourse::InvalidAccess + end end by_category.each do |cat, pos| @@ -187,14 +203,12 @@ class CategoriesController < ApplicationController @category, old_category_params, old_permissions: old_permissions, - old_custom_fields: old_custom_fields + old_custom_fields: old_custom_fields, ) end end - if result - DiscourseEvent.trigger(:category_updated, cat) - end + DiscourseEvent.trigger(:category_updated, cat) if result result end @@ -207,7 +221,7 @@ class CategoriesController < ApplicationController custom_slug = params[:slug].to_s if custom_slug.blank? - error = @category.errors.full_message(:slug, I18n.t('errors.messages.blank')) + error = @category.errors.full_message(:slug, I18n.t("errors.messages.blank")) render_json_error(error) elsif @category.update(slug: custom_slug) render json: success_json @@ -221,7 +235,13 @@ class CategoriesController < ApplicationController notification_level = params[:notification_level].to_i CategoryUser.set_notification_level_for_category(current_user, notification_level, category_id) - render json: success_json.merge({ indirectly_muted_category_ids: CategoryUser.indirectly_muted_category_ids(current_user) }) + render json: + success_json.merge( + { + indirectly_muted_category_ids: + CategoryUser.indirectly_muted_category_ids(current_user), + }, + ) end def destroy @@ -237,34 +257,40 @@ class CategoriesController < ApplicationController def find_by_slug params.require(:category_slug) - @category = Category.find_by_slug_path(params[:category_slug].split('/')) + @category = Category.find_by_slug_path(params[:category_slug].split("/")) raise Discourse::NotFound unless @category.present? if !guardian.can_see?(@category) if SiteSetting.detailed_404 && group = @category.access_category_via_group raise Discourse::InvalidAccess.new( - 'not in group', - @category, - custom_message: 'not_in_group.title_category', - custom_message_params: { group: group.name }, - group: group - ) + "not in group", + @category, + custom_message: "not_in_group.title_category", + custom_message_params: { + group: group.name, + }, + group: group, + ) else raise Discourse::NotFound end end - @category.permission = CategoryGroup.permission_types[:full] if Category.topic_create_allowed(guardian).where(id: @category.id).exists? + @category.permission = CategoryGroup.permission_types[:full] if Category + .topic_create_allowed(guardian) + .where(id: @category.id) + .exists? render_serialized(@category, CategorySerializer) end def visible_groups @guardian.ensure_can_see!(@category) - groups = if !@category.groups.exists?(id: Group::AUTO_GROUPS[:everyone]) - @category.groups.merge(Group.visible_groups(current_user)).pluck("name") - end + groups = + if !@category.groups.exists?(id: Group::AUTO_GROUPS[:everyone]) + @category.groups.merge(Group.visible_groups(current_user)).pluck("name") + end render json: success_json.merge(groups: groups || []) end @@ -285,17 +311,14 @@ class CategoriesController < ApplicationController category_options = { is_homepage: current_homepage == "categories", parent_category_id: params[:parent_category_id], - include_topics: false + include_topics: false, } - topic_options = { - per_page: CategoriesController.topics_per_page, - no_definitions: true, - } + topic_options = { per_page: CategoriesController.topics_per_page, no_definitions: true } topic_options.merge!(build_topic_list_options) style = SiteSetting.desktop_category_page_style - topic_options[:order] = 'created' if style == "categories_and_latest_topics_created_date" + topic_options[:order] = "created" if style == "categories_and_latest_topics_created_date" result = CategoryAndTopicLists.new result.category_list = CategoryList.new(guardian, category_options) @@ -303,9 +326,10 @@ class CategoriesController < ApplicationController if topics_filter == :latest result.topic_list = TopicQuery.new(current_user, topic_options).list_latest elsif topics_filter == :top - result.topic_list = TopicQuery.new(current_user, topic_options).list_top_for( - SiteSetting.top_page_default_timeframe.to_sym - ) + result.topic_list = + TopicQuery.new(current_user, topic_options).list_top_for( + SiteSetting.top_page_default_timeframe.to_sym, + ) end render_serialized(result, CategoryAndTopicListsSerializer, root: false) @@ -316,88 +340,90 @@ class CategoriesController < ApplicationController end def required_create_params - required_param_keys.each do |key| - params.require(key) - end + required_param_keys.each { |key| params.require(key) } category_params end def category_params - @category_params ||= begin - if p = params[:permissions] - p.each do |k, v| - p[k] = v.to_i + @category_params ||= + begin + if p = params[:permissions] + p.each { |k, v| p[k] = v.to_i } end + + if SiteSetting.tagging_enabled + params[:allowed_tags] = params[:allowed_tags].presence || [] if params[:allowed_tags] + params[:allowed_tag_groups] = params[:allowed_tag_groups].presence || [] if params[ + :allowed_tag_groups + ] + params[:required_tag_groups] = params[:required_tag_groups].presence || [] if params[ + :required_tag_groups + ] + end + + if SiteSetting.enable_category_group_moderation? + params[:reviewable_by_group_id] = Group.where( + name: params[:reviewable_by_group_name], + ).pluck_first(:id) if params[:reviewable_by_group_name] + end + + result = + params.permit( + *required_param_keys, + :position, + :name, + :color, + :text_color, + :email_in, + :email_in_allow_strangers, + :mailinglist_mirror, + :all_topics_wiki, + :allow_unlimited_owner_edits_on_first_post, + :default_slow_mode_seconds, + :parent_category_id, + :auto_close_hours, + :auto_close_based_on_last_post, + :uploaded_logo_id, + :uploaded_logo_dark_id, + :uploaded_background_id, + :slug, + :allow_badges, + :topic_template, + :sort_order, + :sort_ascending, + :topic_featured_link_allowed, + :show_subcategory_list, + :num_featured_topics, + :default_view, + :subcategory_list_style, + :default_top_period, + :minimum_required_tags, + :navigate_to_first_post_after_read, + :search_priority, + :allow_global_tags, + :read_only_banner, + :default_list_filter, + :reviewable_by_group_id, + custom_fields: [custom_field_params], + permissions: [*p.try(:keys)], + allowed_tags: [], + allowed_tag_groups: [], + required_tag_groups: %i[name min_count], + ) + + if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array) + raise Discourse::InvalidParameters.new(:required_tag_groups) + end + + result end - - if SiteSetting.tagging_enabled - params[:allowed_tags] = params[:allowed_tags].presence || [] if params[:allowed_tags] - params[:allowed_tag_groups] = params[:allowed_tag_groups].presence || [] if params[:allowed_tag_groups] - params[:required_tag_groups] = params[:required_tag_groups].presence || [] if params[:required_tag_groups] - end - - if SiteSetting.enable_category_group_moderation? - params[:reviewable_by_group_id] = Group.where(name: params[:reviewable_by_group_name]).pluck_first(:id) if params[:reviewable_by_group_name] - end - - result = params.permit( - *required_param_keys, - :position, - :name, - :color, - :text_color, - :email_in, - :email_in_allow_strangers, - :mailinglist_mirror, - :all_topics_wiki, - :allow_unlimited_owner_edits_on_first_post, - :default_slow_mode_seconds, - :parent_category_id, - :auto_close_hours, - :auto_close_based_on_last_post, - :uploaded_logo_id, - :uploaded_logo_dark_id, - :uploaded_background_id, - :slug, - :allow_badges, - :topic_template, - :sort_order, - :sort_ascending, - :topic_featured_link_allowed, - :show_subcategory_list, - :num_featured_topics, - :default_view, - :subcategory_list_style, - :default_top_period, - :minimum_required_tags, - :navigate_to_first_post_after_read, - :search_priority, - :allow_global_tags, - :read_only_banner, - :default_list_filter, - :reviewable_by_group_id, - custom_fields: [custom_field_params], - permissions: [*p.try(:keys)], - allowed_tags: [], - allowed_tag_groups: [], - required_tag_groups: [:name, :min_count] - ) - - if result[:required_tag_groups] && !result[:required_tag_groups].is_a?(Array) - raise Discourse::InvalidParameters.new(:required_tag_groups) - end - - result - end end def custom_field_params keys = params[:custom_fields].try(:keys) return if keys.blank? - keys.map do |key| - params[:custom_fields][key].is_a?(Array) ? { key => [] } : key - end + keys.map { |key| params[:custom_fields][key].is_a?(Array) ? { key => [] } : key } end def fetch_category @@ -411,12 +437,9 @@ class CategoriesController < ApplicationController def include_topics(parent_category = nil) style = SiteSetting.desktop_category_page_style - view_context.mobile_view? || - params[:include_topics] || + view_context.mobile_view? || params[:include_topics] || (parent_category && parent_category.subcategory_list_includes_topics?) || - style == "categories_with_featured_topics" || - style == "subcategories_with_featured_topics" || - style == "categories_boxes_with_topics" || - style == "categories_with_top_topics" + style == "categories_with_featured_topics" || style == "subcategories_with_featured_topics" || + style == "categories_boxes_with_topics" || style == "categories_with_top_topics" end end diff --git a/app/controllers/clicks_controller.rb b/app/controllers/clicks_controller.rb index 5b932484e7..59dabcd121 100644 --- a/app/controllers/clicks_controller.rb +++ b/app/controllers/clicks_controller.rb @@ -4,17 +4,16 @@ class ClicksController < ApplicationController skip_before_action :check_xhr, :preload_json, :verify_authenticity_token def track - params.require([:url, :post_id, :topic_id]) + params.require(%i[url post_id topic_id]) TopicLinkClick.create_from( url: params[:url], post_id: params[:post_id], topic_id: params[:topic_id], ip: request.remote_ip, - user_id: current_user&.id + user_id: current_user&.id, ) render json: success_json end - end diff --git a/app/controllers/composer_controller.rb b/app/controllers/composer_controller.rb index fd4847e115..f558a890eb 100644 --- a/app/controllers/composer_controller.rb +++ b/app/controllers/composer_controller.rb @@ -13,12 +13,13 @@ class ComposerController < ApplicationController end # allowed_names is necessary just for new private messages. - @allowed_names = if params[:allowed_names].present? - raise Discourse::InvalidParameters(:allowed_names) if !params[:allowed_names].is_a?(Array) - params[:allowed_names] << current_user.username - else - [] - end + @allowed_names = + if params[:allowed_names].present? + raise Discourse.InvalidParameters(:allowed_names) if !params[:allowed_names].is_a?(Array) + params[:allowed_names] << current_user.username + else + [] + end user_reasons = {} group_reasons = {} @@ -33,64 +34,73 @@ class ComposerController < ApplicationController end if @topic && @names.include?(SiteSetting.here_mention) && guardian.can_mention_here? - here_count = PostAlerter.new.expand_here_mention(@topic.first_post, exclude_ids: [current_user.id]).size + here_count = + PostAlerter.new.expand_here_mention(@topic.first_post, exclude_ids: [current_user.id]).size end - serialized_groups = groups.values.reduce({}) do |hash, group| - serialized_group = { user_count: group.user_count } + serialized_groups = + groups + .values + .reduce({}) do |hash, group| + serialized_group = { user_count: group.user_count } - if group_reasons[group.name] == :not_allowed && - members_visible_group_ids.include?(group.id) && - (@topic&.private_message? || @allowed_names.present?) + if group_reasons[group.name] == :not_allowed && + members_visible_group_ids.include?(group.id) && + (@topic&.private_message? || @allowed_names.present?) + # Find users that are notified already because they have been invited + # directly or via a group + notified_count = + GroupUser + # invited directly + .where(user_id: topic_allowed_user_ids) + .or( + # invited via a group + GroupUser.where( + user_id: GroupUser.where(group_id: topic_allowed_group_ids).select(:user_id), + ), + ) + .where(group_id: group.id) + .select(:user_id) + .distinct + .count - # Find users that are notified already because they have been invited - # directly or via a group - notified_count = GroupUser - # invited directly - .where(user_id: topic_allowed_user_ids) - .or( - # invited via a group - GroupUser.where( - user_id: GroupUser.where(group_id: topic_allowed_group_ids).select(:user_id) - ) - ) - .where(group_id: group.id) - .select(:user_id).distinct.count + if notified_count > 0 + group_reasons[group.name] = :some_not_allowed + serialized_group[:notified_count] = notified_count + end + end - if notified_count > 0 - group_reasons[group.name] = :some_not_allowed - serialized_group[:notified_count] = notified_count + hash[group.name] = serialized_group + hash end - end - - hash[group.name] = serialized_group - hash - end render json: { - users: users.keys, - user_reasons: user_reasons, - groups: serialized_groups, - group_reasons: group_reasons, - here_count: here_count, - max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention, - } + users: users.keys, + user_reasons: user_reasons, + groups: serialized_groups, + group_reasons: group_reasons, + here_count: here_count, + max_users_notified_per_group_mention: SiteSetting.max_users_notified_per_group_mention, + } end private def user_reason(user) - reason = if @topic && !user.guardian.can_see?(@topic) - @topic.private_message? ? :private : :category - elsif @allowed_names.present? && !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) - # This would normally be handled by the previous if, but that does not work for new private messages. - :private - elsif topic_muted_by.include?(user.id) - :muted_topic - elsif @topic&.private_message? && !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) - # Admins can see the topic, but they will not be mentioned if they were not invited. - :not_allowed - end + reason = + if @topic && !user.guardian.can_see?(@topic) + @topic.private_message? ? :private : :category + elsif @allowed_names.present? && + !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) + # This would normally be handled by the previous if, but that does not work for new private messages. + :private + elsif topic_muted_by.include?(user.id) + :muted_topic + elsif @topic&.private_message? && + !is_user_allowed?(user, topic_allowed_user_ids, topic_allowed_group_ids) + # Admins can see the topic, but they will not be mentioned if they were not invited. + :not_allowed + end # Regular users can see only basic information why the users cannot see the topic. reason = nil if !guardian.is_staff? && reason != :private && reason != :category @@ -101,7 +111,8 @@ class ComposerController < ApplicationController def group_reason(group) if !mentionable_group_ids.include?(group.id) :not_mentionable - elsif (@topic&.private_message? || @allowed_names.present?) && !topic_allowed_group_ids.include?(group.id) + elsif (@topic&.private_message? || @allowed_names.present?) && + !topic_allowed_group_ids.include?(group.id) :not_allowed end end @@ -111,74 +122,57 @@ class ComposerController < ApplicationController end def users - @users ||= User - .not_staged - .where(username_lower: @names.map(&:downcase)) - .index_by(&:username_lower) + @users ||= + User.not_staged.where(username_lower: @names.map(&:downcase)).index_by(&:username_lower) end def groups - @groups ||= Group - .visible_groups(current_user) - .where('lower(name) IN (?)', @names.map(&:downcase)) - .index_by(&:name) + @groups ||= + Group + .visible_groups(current_user) + .where("lower(name) IN (?)", @names.map(&:downcase)) + .index_by(&:name) end def mentionable_group_ids - @mentionable_group_ids ||= Group - .mentionable(current_user, include_public: false) - .where(name: @names) - .pluck(:id) - .to_set + @mentionable_group_ids ||= + Group.mentionable(current_user, include_public: false).where(name: @names).pluck(:id).to_set end def members_visible_group_ids - @members_visible_group_ids ||= Group - .members_visible_groups(current_user) - .where(name: @names) - .pluck(:id) - .to_set + @members_visible_group_ids ||= + Group.members_visible_groups(current_user).where(name: @names).pluck(:id).to_set end def topic_muted_by - @topic_muted_by ||= if @topic.present? - TopicUser - .where(topic: @topic) - .where(user_id: users.values.map(&:id)) - .where(notification_level: TopicUser.notification_levels[:muted]) - .pluck(:user_id) - .to_set - else - Set.new - end + @topic_muted_by ||= + if @topic.present? + TopicUser + .where(topic: @topic) + .where(user_id: users.values.map(&:id)) + .where(notification_level: TopicUser.notification_levels[:muted]) + .pluck(:user_id) + .to_set + else + Set.new + end end def topic_allowed_user_ids - @topic_allowed_user_ids ||= if @allowed_names.present? - User - .where(username_lower: @allowed_names.map(&:downcase)) - .pluck(:id) - .to_set - elsif @topic&.private_message? - TopicAllowedUser - .where(topic: @topic) - .pluck(:user_id) - .to_set - end + @topic_allowed_user_ids ||= + if @allowed_names.present? + User.where(username_lower: @allowed_names.map(&:downcase)).pluck(:id).to_set + elsif @topic&.private_message? + TopicAllowedUser.where(topic: @topic).pluck(:user_id).to_set + end end def topic_allowed_group_ids - @topic_allowed_group_ids ||= if @allowed_names.present? - Group - .messageable(current_user) - .where(name: @allowed_names) - .pluck(:id) - .to_set - elsif @topic&.private_message? - TopicAllowedGroup - .where(topic: @topic) - .pluck(:group_id) - .to_set - end + @topic_allowed_group_ids ||= + if @allowed_names.present? + Group.messageable(current_user).where(name: @allowed_names).pluck(:id).to_set + elsif @topic&.private_message? + TopicAllowedGroup.where(topic: @topic).pluck(:group_id).to_set + end end end diff --git a/app/controllers/composer_messages_controller.rb b/app/controllers/composer_messages_controller.rb index b830301455..3785e25728 100644 --- a/app/controllers/composer_messages_controller.rb +++ b/app/controllers/composer_messages_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class ComposerMessagesController < ApplicationController - requires_login def index - finder = ComposerMessagesFinder.new(current_user, params.slice(:composer_action, :topic_id, :post_id)) + finder = + ComposerMessagesFinder.new(current_user, params.slice(:composer_action, :topic_id, :post_id)) json = { composer_messages: [finder.find].compact } if params[:topic_id].present? @@ -25,14 +25,24 @@ class ComposerMessagesController < ApplicationController warning_message = nil if user_count > 0 - message_locale = if user_count == 1 - "education.user_not_seen_in_a_while.single" - else - "education.user_not_seen_in_a_while.multiple" - end + message_locale = + if user_count == 1 + "education.user_not_seen_in_a_while.single" + else + "education.user_not_seen_in_a_while.multiple" + end end - json = { user_count: user_count, usernames: users, time_ago: FreedomPatches::Rails4.time_ago_in_words(SiteSetting.pm_warn_user_last_seen_months_ago.month.ago, true, scope: :'datetime.distance_in_words_verbose') } + json = { + user_count: user_count, + usernames: users, + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + SiteSetting.pm_warn_user_last_seen_months_ago.month.ago, + true, + scope: :"datetime.distance_in_words_verbose", + ), + } render_json_dump(json) end end diff --git a/app/controllers/csp_reports_controller.rb b/app/controllers/csp_reports_controller.rb index d18fd7d1c2..ac206da5b3 100644 --- a/app/controllers/csp_reports_controller.rb +++ b/app/controllers/csp_reports_controller.rb @@ -10,12 +10,11 @@ class CspReportsController < ApplicationController if report.blank? render_json_error("empty CSP report", status: 422) else - Logster.add_to_env(request.env, 'CSP Report', report) - Rails.logger.warn("CSP Violation: '#{report['blocked-uri']}' \n\n#{report['script-sample']}") + Logster.add_to_env(request.env, "CSP Report", report) + Rails.logger.warn("CSP Violation: '#{report["blocked-uri"]}' \n\n#{report["script-sample"]}") head :ok end - rescue JSON::ParserError render_json_error("invalid CSP report", status: 422) end @@ -25,20 +24,20 @@ class CspReportsController < ApplicationController def parse_report obj = JSON.parse(request.body.read) if Hash === obj - obj = obj['csp-report'] + obj = obj["csp-report"] if Hash === obj obj.slice( - 'blocked-uri', - 'disposition', - 'document-uri', - 'effective-directive', - 'original-policy', - 'referrer', - 'script-sample', - 'status-code', - 'violated-directive', - 'line-number', - 'source-file' + "blocked-uri", + "disposition", + "document-uri", + "effective-directive", + "original-policy", + "referrer", + "script-sample", + "status-code", + "violated-directive", + "line-number", + "source-file", ) end end diff --git a/app/controllers/directory_columns_controller.rb b/app/controllers/directory_columns_controller.rb index d11f5e30ff..572c7e92ad 100644 --- a/app/controllers/directory_columns_controller.rb +++ b/app/controllers/directory_columns_controller.rb @@ -3,6 +3,8 @@ class DirectoryColumnsController < ApplicationController def index directory_columns = DirectoryColumn.includes(:user_field).where(enabled: true).order(:position) - render_json_dump(directory_columns: serialize_data(directory_columns, DirectoryColumnSerializer)) + render_json_dump( + directory_columns: serialize_data(directory_columns, DirectoryColumnSerializer), + ) end end diff --git a/app/controllers/directory_items_controller.rb b/app/controllers/directory_items_controller.rb index 692088cdc5..15f1451d5a 100644 --- a/app/controllers/directory_items_controller.rb +++ b/app/controllers/directory_items_controller.rb @@ -4,7 +4,9 @@ class DirectoryItemsController < ApplicationController PAGE_SIZE = 50 def index - raise Discourse::InvalidAccess.new(:enable_user_directory) unless SiteSetting.enable_user_directory? + unless SiteSetting.enable_user_directory? + raise Discourse::InvalidAccess.new(:enable_user_directory) + end period = params.require(:period) period_type = DirectoryItem.period_types[period.to_sym] @@ -23,30 +25,36 @@ class DirectoryItemsController < ApplicationController end if params[:exclude_usernames] - result = result.references(:user).where.not(users: { username: params[:exclude_usernames].split(",") }) + result = + result + .references(:user) + .where.not(users: { username: params[:exclude_usernames].split(",") }) end order = params[:order] || DirectoryColumn.automatic_column_names.first - dir = params[:asc] ? 'ASC' : 'DESC' + dir = params[:asc] ? "ASC" : "DESC" active_directory_column_names = DirectoryColumn.active_column_names if active_directory_column_names.include?(order.to_sym) result = result.order("directory_items.#{order} #{dir}, directory_items.id") - elsif params[:order] === 'username' + elsif params[:order] === "username" result = result.order("users.#{order} #{dir}, directory_items.id") else # Ordering by user field value user_field = UserField.find_by(name: params[:order]) if user_field - result = result - .references(:user) - .joins("LEFT OUTER JOIN user_custom_fields ON user_custom_fields.user_id = users.id AND user_custom_fields.name = 'user_field_#{user_field.id}'") - .order("user_custom_fields.name = 'user_field_#{user_field.id}' ASC, user_custom_fields.value #{dir}") + result = + result + .references(:user) + .joins( + "LEFT OUTER JOIN user_custom_fields ON user_custom_fields.user_id = users.id AND user_custom_fields.name = 'user_field_#{user_field.id}'", + ) + .order( + "user_custom_fields.name = 'user_field_#{user_field.id}' ASC, user_custom_fields.value #{dir}", + ) end end - if period_type == DirectoryItem.period_types[:all] - result = result.includes(:user_stat) - end + result = result.includes(:user_stat) if period_type == DirectoryItem.period_types[:all] page = params[:page].to_i user_ids = nil @@ -54,12 +62,10 @@ class DirectoryItemsController < ApplicationController user_ids = UserSearch.new(params[:name], include_staged_users: true).search.pluck(:id) if user_ids.present? # Add the current user if we have at least one other match - if current_user && result.dup.where(user_id: user_ids).exists? - user_ids << current_user.id - end + user_ids << current_user.id if current_user && result.dup.where(user_id: user_ids).exists? result = result.where(user_id: user_ids) else - result = result.where('false') + result = result.where("false") end end @@ -68,7 +74,7 @@ class DirectoryItemsController < ApplicationController if user_id result = result.where(user_id: user_id) else - result = result.where('false') + result = result.where("false") end end @@ -82,7 +88,6 @@ class DirectoryItemsController < ApplicationController # Put yourself at the top of the first page if result.present? && current_user.present? && page == 0 && !params[:group].present? - position = result.index { |r| r.user_id == current_user.id } # Don't show the record unless you're not in the top positions already @@ -90,7 +95,6 @@ class DirectoryItemsController < ApplicationController your_item = DirectoryItem.where(period_type: period_type, user_id: current_user.id).first result.insert(0, your_item) if your_item end - end last_updated_at = DirectoryItem.last_updated_at(period_type) @@ -101,7 +105,9 @@ class DirectoryItemsController < ApplicationController user_field_ids = params[:user_field_ids]&.split("|")&.map(&:to_i) user_field_ids.each do |user_field_id| - serializer_opts[:user_custom_field_map]["#{User::USER_FIELD_PREFIX}#{user_field_id}"] = user_field_id + serializer_opts[:user_custom_field_map][ + "#{User::USER_FIELD_PREFIX}#{user_field_id}" + ] = user_field_id end end @@ -112,12 +118,13 @@ class DirectoryItemsController < ApplicationController serializer_opts[:attributes] = active_directory_column_names serialized = serialize_data(result, DirectoryItemSerializer, serializer_opts) - render_json_dump(directory_items: serialized, - meta: { - last_updated_at: last_updated_at, - total_rows_directory_items: result_count, - load_more_directory_items: load_more_directory_items_json - } - ) + render_json_dump( + directory_items: serialized, + meta: { + last_updated_at: last_updated_at, + total_rows_directory_items: result_count, + load_more_directory_items: load_more_directory_items_json, + }, + ) end end diff --git a/app/controllers/do_not_disturb_controller.rb b/app/controllers/do_not_disturb_controller.rb index db24c7167c..42d32c9b70 100644 --- a/app/controllers/do_not_disturb_controller.rb +++ b/app/controllers/do_not_disturb_controller.rb @@ -6,11 +6,23 @@ class DoNotDisturbController < ApplicationController def create raise Discourse::InvalidParameters.new(:duration) if params[:duration].blank? - duration_minutes = (Integer(params[:duration]) rescue false) + duration_minutes = + ( + begin + Integer(params[:duration]) + rescue StandardError + false + end + ) - ends_at = duration_minutes ? - ends_at_from_minutes(duration_minutes) : - ends_at_from_string(params[:duration]) + ends_at = + ( + if duration_minutes + ends_at_from_minutes(duration_minutes) + else + ends_at_from_string(params[:duration]) + end + ) new_timing = current_user.do_not_disturb_timings.new(starts_at: Time.zone.now, ends_at: ends_at) @@ -37,7 +49,7 @@ class DoNotDisturbController < ApplicationController end def ends_at_from_string(string) - if string == 'tomorrow' + if string == "tomorrow" Time.now.end_of_day.utc else raise Discourse::InvalidParameters.new(:duration) diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb index 86a5c25418..78d1ea4fad 100644 --- a/app/controllers/drafts_controller.rb +++ b/app/controllers/drafts_controller.rb @@ -9,15 +9,9 @@ class DraftsController < ApplicationController params.permit(:offset) params.permit(:limit) - stream = Draft.stream( - user: current_user, - offset: params[:offset], - limit: params[:limit] - ) + stream = Draft.stream(user: current_user, offset: params[:offset], limit: params[:limit]) - render json: { - drafts: stream ? serialize_data(stream, DraftSerializer) : [] - } + render json: { drafts: stream ? serialize_data(stream, DraftSerializer) : [] } end def show @@ -38,10 +32,9 @@ class DraftsController < ApplicationController params[:sequence].to_i, params[:data], params[:owner], - force_save: params[:force_save] + force_save: params[:force_save], ) rescue Draft::OutOfSequence - begin if !Draft.exists?(user_id: current_user.id, draft_key: params[:draft_key]) Draft.set( @@ -49,18 +42,17 @@ class DraftsController < ApplicationController params[:draft_key], DraftSequence.current(current_user, params[:draft_key]), params[:data], - params[:owner] + params[:owner], ) else raise Draft::OutOfSequence end - rescue Draft::OutOfSequence - render_json_error I18n.t('draft.sequence_conflict_error.title'), - status: 409, - extras: { - description: I18n.t('draft.sequence_conflict_error.description') - } + render_json_error I18n.t("draft.sequence_conflict_error.title"), + status: 409, + extras: { + description: I18n.t("draft.sequence_conflict_error.description"), + } return end end @@ -68,7 +60,7 @@ class DraftsController < ApplicationController json = success_json.merge(draft_sequence: sequence) begin - data = JSON::parse(params[:data]) + data = JSON.parse(params[:data]) rescue JSON::ParserError raise Discourse::InvalidParameters.new(:data) end @@ -76,7 +68,8 @@ class DraftsController < ApplicationController if data.present? # this is a bit of a kludge we need to remove (all the parsing) too many special cases here # we need to catch action edit and action editSharedDraft - if data["postId"].present? && data["originalText"].present? && data["action"].to_s.start_with?("edit") + if data["postId"].present? && data["originalText"].present? && + data["action"].to_s.start_with?("edit") post = Post.find_by(id: data["postId"]) if post && post.raw != data["originalText"] conflict_user = BasicUserSerializer.new(post.last_editor, root: false) diff --git a/app/controllers/edit_directory_columns_controller.rb b/app/controllers/edit_directory_columns_controller.rb index b40d13ce66..4b32ca9fb2 100644 --- a/app/controllers/edit_directory_columns_controller.rb +++ b/app/controllers/edit_directory_columns_controller.rb @@ -18,14 +18,20 @@ class EditDirectoryColumnsController < ApplicationController directory_column_params = params.permit(directory_columns: {}) directory_columns = DirectoryColumn.all - has_enabled_column = directory_column_params[:directory_columns].values.any? do |column_data| - column_data[:enabled].to_s == "true" + has_enabled_column = + directory_column_params[:directory_columns].values.any? do |column_data| + column_data[:enabled].to_s == "true" + end + unless has_enabled_column + raise Discourse::InvalidParameters, "Must have at least one column enabled" end - raise Discourse::InvalidParameters, "Must have at least one column enabled" unless has_enabled_column directory_column_params[:directory_columns].values.each do |column_data| existing_column = directory_columns.detect { |c| c.id == column_data[:id].to_i } - if (existing_column.enabled != column_data[:enabled] || existing_column.position != column_data[:position].to_i) + if ( + existing_column.enabled != column_data[:enabled] || + existing_column.position != column_data[:position].to_i + ) existing_column.update(enabled: column_data[:enabled], position: column_data[:position]) end end @@ -37,7 +43,8 @@ class EditDirectoryColumnsController < ApplicationController def ensure_user_fields_have_columns user_fields_without_column = - UserField.left_outer_joins(:directory_column) + UserField + .left_outer_joins(:directory_column) .where(directory_column: { user_field_id: nil }) .where("show_on_profile=? OR show_on_user_card=?", true, true) @@ -47,12 +54,14 @@ class EditDirectoryColumnsController < ApplicationController new_directory_column_attrs = [] user_fields_without_column.each do |user_field| - new_directory_column_attrs.push({ - user_field_id: user_field.id, - enabled: false, - type: DirectoryColumn.types[:user_field], - position: next_position - }) + new_directory_column_attrs.push( + { + user_field_id: user_field.id, + enabled: false, + type: DirectoryColumn.types[:user_field], + position: next_position, + }, + ) next_position += 1 end diff --git a/app/controllers/email_controller.rb b/app/controllers/email_controller.rb index b7fd4c8380..ed31702272 100644 --- a/app/controllers/email_controller.rb +++ b/app/controllers/email_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class EmailController < ApplicationController - layout 'no_ember' + layout "no_ember" skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required @@ -11,9 +11,7 @@ class EmailController < ApplicationController @key_owner_found = key&.user.present? if @found && @key_owner_found - UnsubscribeKey - .get_unsubscribe_strategy_for(key) - &.prepare_unsubscribe_options(self) + UnsubscribeKey.get_unsubscribe_strategy_for(key)&.prepare_unsubscribe_options(self) if current_user.present? && (@user != current_user) @different_user = @user.name @@ -28,17 +26,14 @@ class EmailController < ApplicationController key = UnsubscribeKey.find_by(key: params[:key]) raise Discourse::NotFound if key.nil? || key.user.nil? user = key.user - updated = UnsubscribeKey.get_unsubscribe_strategy_for(key) - &.unsubscribe(params) + updated = UnsubscribeKey.get_unsubscribe_strategy_for(key)&.unsubscribe(params) if updated cache_key = "unsub_#{SecureRandom.hex}" Discourse.cache.write cache_key, user.email, expires_in: 1.hour url = path("/email/unsubscribed?key=#{cache_key}") - if key.associated_topic - url += "&topic_id=#{key.associated_topic.id}" - end + url += "&topic_id=#{key.associated_topic.id}" if key.associated_topic redirect_to url else diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index 3951761c73..8ff1f870c0 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -5,10 +5,10 @@ class EmbedController < ApplicationController skip_before_action :check_xhr, :preload_json, :verify_authenticity_token - before_action :prepare_embeddable, except: [ :info ] - before_action :ensure_api_request, only: [ :info ] + before_action :prepare_embeddable, except: [:info] + before_action :ensure_api_request, only: [:info] - layout 'embed' + layout "embed" rescue_from Discourse::InvalidAccess do if current_user.try(:admin?) @@ -16,14 +16,14 @@ class EmbedController < ApplicationController @show_reason = true @hosts = EmbeddableHost.all end - render 'embed_error', status: 400 + render "embed_error", status: 400 end def topics discourse_expires_in 1.minute unless SiteSetting.embed_topics_list? - render 'embed_topics_error', status: 400 + render "embed_topics_error", status: 400 return end @@ -32,10 +32,12 @@ class EmbedController < ApplicationController end if @embed_class = params[:embed_class] - raise Discourse::InvalidParameters.new(:embed_class) unless @embed_class =~ /^[a-zA-Z0-9\-_]+$/ + unless @embed_class =~ /^[a-zA-Z0-9\-_]+$/ + raise Discourse::InvalidParameters.new(:embed_class) + end end - response.headers['X-Robots-Tag'] = 'noindex, indexifembedded' + response.headers["X-Robots-Tag"] = "noindex, indexifembedded" if params.has_key?(:template) && params[:template] == "complete" @template = "complete" @@ -46,8 +48,7 @@ class EmbedController < ApplicationController list_options = build_topic_list_options if params.has_key?(:per_page) - list_options[:per_page] = - [params[:per_page].to_i, SiteSetting.embed_topic_limit_per_page].min + list_options[:per_page] = [params[:per_page].to_i, SiteSetting.embed_topic_limit_per_page].min end if params[:allow_create] @@ -67,11 +68,12 @@ class EmbedController < ApplicationController valid_top_period = false end - @list = if valid_top_period - topic_query.list_top_for(top_period) - else - topic_query.list_latest - end + @list = + if valid_top_period + topic_query.list_top_for(top_period) + else + topic_query.list_latest + end end def comments @@ -80,7 +82,7 @@ class EmbedController < ApplicationController embed_topic_id = params[:topic_id]&.to_i unless embed_topic_id || EmbeddableHost.url_allowed?(embed_url) - raise Discourse::InvalidAccess.new('invalid embed host') + raise Discourse::InvalidAccess.new("invalid embed host") end topic_id = nil @@ -91,28 +93,33 @@ class EmbedController < ApplicationController end if topic_id - @topic_view = TopicView.new(topic_id, - current_user, - limit: SiteSetting.embed_post_limit, - only_regular: true, - exclude_first: true, - exclude_deleted_users: true, - exclude_hidden: true) + @topic_view = + TopicView.new( + topic_id, + current_user, + limit: SiteSetting.embed_post_limit, + only_regular: true, + exclude_first: true, + exclude_deleted_users: true, + exclude_hidden: true, + ) raise Discourse::NotFound if @topic_view.blank? @posts_left = 0 @second_post_url = "#{@topic_view.topic.url}/2" @reply_count = @topic_view.filtered_posts.count - 1 @reply_count = 0 if @reply_count < 0 - @posts_left = @reply_count - SiteSetting.embed_post_limit if @reply_count > SiteSetting.embed_post_limit + @posts_left = @reply_count - SiteSetting.embed_post_limit if @reply_count > + SiteSetting.embed_post_limit elsif embed_url.present? - Jobs.enqueue(:retrieve_topic, - user_id: current_user.try(:id), - embed_url: embed_url, - author_username: embed_username, - referer: request.env['HTTP_REFERER'] - ) - render 'loading' + Jobs.enqueue( + :retrieve_topic, + user_id: current_user.try(:id), + embed_url: embed_url, + author_username: embed_username, + referer: request.env["HTTP_REFERER"], + ) + render "loading" end discourse_expires_in 1.minute @@ -132,16 +139,16 @@ class EmbedController < ApplicationController by_url = {} if embed_urls.present? - urls = embed_urls.map { |u| u.sub(/#discourse-comments$/, '').sub(/\/$/, '') } + urls = embed_urls.map { |u| u.sub(/#discourse-comments$/, "").sub(%r{/$}, "") } topic_embeds = TopicEmbed.where(embed_url: urls).includes(:topic).references(:topic) topic_embeds.each do |te| url = te.embed_url url = "#{url}#discourse-comments" unless params[:embed_url].include?(url) if te.topic.present? - by_url[url] = I18n.t('embed.replies', count: te.topic.posts_count - 1) + by_url[url] = I18n.t("embed.replies", count: te.topic.posts_count - 1) else - by_url[url] = I18n.t('embed.replies', count: 0) + by_url[url] = I18n.t("embed.replies", count: 0) end end end @@ -152,16 +159,18 @@ class EmbedController < ApplicationController private def prepare_embeddable - response.headers.delete('X-Frame-Options') + response.headers.delete("X-Frame-Options") @embeddable_css_class = "" embeddable_host = EmbeddableHost.record_for_url(request.referer) - @embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present? + @embeddable_css_class = + " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && + embeddable_host.class_name.present? @data_referer = request.referer - @data_referer = '*' if SiteSetting.embed_any_origin? && @data_referer.blank? + @data_referer = "*" if SiteSetting.embed_any_origin? && @data_referer.blank? end def ensure_api_request - raise Discourse::InvalidAccess.new('api key not set') if !is_api? + raise Discourse::InvalidAccess.new("api key not set") if !is_api? end end diff --git a/app/controllers/exceptions_controller.rb b/app/controllers/exceptions_controller.rb index 2cbfeeed02..2b50d7c56c 100644 --- a/app/controllers/exceptions_controller.rb +++ b/app/controllers/exceptions_controller.rb @@ -12,5 +12,4 @@ class ExceptionsController < ApplicationController def not_found_body render html: build_not_found_page(status: 200) end - end diff --git a/app/controllers/export_csv_controller.rb b/app/controllers/export_csv_controller.rb index 93f16b91d1..833bd53b30 100644 --- a/app/controllers/export_csv_controller.rb +++ b/app/controllers/export_csv_controller.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true class ExportCsvController < ApplicationController - skip_before_action :preload_json, :check_xhr, only: [:show] def export_entity guardian.ensure_can_export_entity!(export_params[:entity]) - if export_params[:entity] == 'user_archive' + if export_params[:entity] == "user_archive" Jobs.enqueue(:export_user_archive, user_id: current_user.id, args: export_params[:args]) else - Jobs.enqueue(:export_csv_file, entity: export_params[:entity], user_id: current_user.id, args: export_params[:args]) + Jobs.enqueue( + :export_csv_file, + entity: export_params[:entity], + user_id: current_user.id, + args: export_params[:args], + ) end StaffActionLogger.new(current_user).log_entity_export(export_params[:entity]) render json: success_json @@ -21,9 +25,10 @@ class ExportCsvController < ApplicationController private def export_params - @_export_params ||= begin - params.require(:entity) - params.permit(:entity, args: Report::FILTERS).to_h - end + @_export_params ||= + begin + params.require(:entity) + params.permit(:entity, args: Report::FILTERS).to_h + end end end diff --git a/app/controllers/extra_locales_controller.rb b/app/controllers/extra_locales_controller.rb index 314e432900..63fdb5d81c 100644 --- a/app/controllers/extra_locales_controller.rb +++ b/app/controllers/extra_locales_controller.rb @@ -4,11 +4,11 @@ class ExtraLocalesController < ApplicationController layout :false skip_before_action :check_xhr, - :preload_json, - :redirect_to_login_if_required, - :verify_authenticity_token + :preload_json, + :redirect_to_login_if_required, + :verify_authenticity_token - OVERRIDES_BUNDLE ||= 'overrides' + OVERRIDES_BUNDLE ||= "overrides" MD5_HASH_LENGTH ||= 32 def show diff --git a/app/controllers/finish_installation_controller.rb b/app/controllers/finish_installation_controller.rb index e84252c65a..cdc6d8990f 100644 --- a/app/controllers/finish_installation_controller.rb +++ b/app/controllers/finish_installation_controller.rb @@ -2,9 +2,9 @@ class FinishInstallationController < ApplicationController skip_before_action :check_xhr, :preload_json, :redirect_to_login_if_required - layout 'finish_installation' + layout "finish_installation" - before_action :ensure_no_admins, except: ['confirm_email', 'resend_email'] + before_action :ensure_no_admins, except: %w[confirm_email resend_email] def index end @@ -61,7 +61,9 @@ class FinishInstallationController < ApplicationController end def find_allowed_emails - return [] unless GlobalSetting.respond_to?(:developer_emails) && GlobalSetting.developer_emails.present? + unless GlobalSetting.respond_to?(:developer_emails) && GlobalSetting.developer_emails.present? + return [] + end GlobalSetting.developer_emails.split(",").map(&:strip) end diff --git a/app/controllers/forums_controller.rb b/app/controllers/forums_controller.rb index ac172d54b3..61987c111b 100644 --- a/app/controllers/forums_controller.rb +++ b/app/controllers/forums_controller.rb @@ -6,7 +6,7 @@ class ForumsController < ActionController::Base include ReadOnlyMixin before_action :check_readonly_mode - after_action :add_readonly_header + after_action :add_readonly_header def status if params[:cluster] diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 830f3ad230..99f3634ee6 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,44 +1,38 @@ # frozen_string_literal: true class GroupsController < ApplicationController - requires_login only: [ - :set_notifications, - :mentionable, - :messageable, - :check_name, - :update, - :histories, - :request_membership, - :search, - :new, - :test_email_settings - ] + requires_login only: %i[ + set_notifications + mentionable + messageable + check_name + update + histories + request_membership + search + new + test_email_settings + ] - skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed] + skip_before_action :preload_json, :check_xhr, only: %i[posts_feed mentions_feed] skip_before_action :check_xhr, only: [:show] after_action :add_noindex_header TYPE_FILTERS = { - my: Proc.new { |groups, user| - raise Discourse::NotFound unless user - Group.member_of(groups, user) - }, - owner: Proc.new { |groups, user| - raise Discourse::NotFound unless user - Group.owner_of(groups, user) - }, - public: Proc.new { |groups| - groups.where(public_admission: true, automatic: false) - }, - close: Proc.new { |groups| - groups.where(public_admission: false, automatic: false) - }, - automatic: Proc.new { |groups| - groups.where(automatic: true) - }, - non_automatic: Proc.new { |groups| - groups.where(automatic: false) - } + my: + Proc.new do |groups, user| + raise Discourse::NotFound unless user + Group.member_of(groups, user) + end, + owner: + Proc.new do |groups, user| + raise Discourse::NotFound unless user + Group.owner_of(groups, user) + end, + public: Proc.new { |groups| groups.where(public_admission: true, automatic: false) }, + close: Proc.new { |groups| groups.where(public_admission: false, automatic: false) }, + automatic: Proc.new { |groups| groups.where(automatic: true) }, + non_automatic: Proc.new { |groups| groups.where(automatic: false) }, } ADD_MEMBERS_LIMIT = 1000 @@ -47,7 +41,7 @@ class GroupsController < ApplicationController raise Discourse::InvalidAccess.new(:enable_group_directory) end - order = %w{name user_count}.delete(params[:order]) + order = %w[name user_count].delete(params[:order]) dir = params[:asc].to_s == "true" ? "ASC" : "DESC" sort = order ? "#{order} #{dir}" : nil groups = Group.visible_groups(current_user, sort) @@ -56,7 +50,7 @@ class GroupsController < ApplicationController if (username = params[:username]).present? raise Discourse::NotFound unless user = User.find_by_username(username) groups = TYPE_FILTERS[:my].call(groups.members_visible_groups(current_user, sort), user) - type_filters = type_filters - [:my, :owner] + type_filters = type_filters - %i[my owner] end if (filter = params[:filter]).present? @@ -83,7 +77,7 @@ class GroupsController < ApplicationController user_group_ids = group_users.pluck(:group_id) owner_group_ids = group_users.where(owner: true).pluck(:group_id) else - type_filters = type_filters - [:my, :owner] + type_filters = type_filters - %i[my owner] end type_filters.delete(:non_automatic) @@ -96,22 +90,19 @@ class GroupsController < ApplicationController groups = groups.offset(page * page_size).limit(page_size) render_json_dump( - groups: serialize_data(groups, - BasicGroupSerializer, - user_group_ids: user_group_ids || [], - owner_group_ids: owner_group_ids || [] - ), + groups: + serialize_data( + groups, + BasicGroupSerializer, + user_group_ids: user_group_ids || [], + owner_group_ids: owner_group_ids || [], + ), extras: { - type_filters: type_filters + type_filters: type_filters, }, total_rows_groups: total, - load_more_groups: groups_path( - page: page + 1, - type: type, - order: order, - asc: params[:asc], - filter: filter - ) + load_more_groups: + groups_path(page: page + 1, type: type, order: order, asc: params[:asc], filter: filter), ) end @@ -122,21 +113,23 @@ class GroupsController < ApplicationController format.html do @title = group.full_name.present? ? group.full_name.capitalize : group.name @full_title = "#{@title} - #{SiteSetting.title}" - @description_meta = group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title + @description_meta = + group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title render :show end format.json do groups = Group.visible_groups(current_user) if !guardian.is_staff? - groups = groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators]) + groups = + groups.where("automatic IS FALSE OR groups.id = ?", Group::AUTO_GROUPS[:moderators]) end render_json_dump( group: serialize_data(group, GroupShowSerializer, root: nil), extras: { - visible_group_names: groups.pluck(:name) - } + visible_group_names: groups.pluck(:name), + }, ) end end @@ -161,7 +154,15 @@ class GroupsController < ApplicationController if params[:update_existing_users].blank? user_count = count_existing_users(group.group_users, notification_level, categories, tags) - return render status: 422, json: { user_count: user_count, errors: [I18n.t('invalid_params', message: :update_existing_users)] } if user_count > 0 + if user_count > 0 + return( + render status: 422, + json: { + user_count: user_count, + errors: [I18n.t("invalid_params", message: :update_existing_users)], + } + ) + end end end @@ -169,7 +170,9 @@ class GroupsController < ApplicationController GroupActionLogger.new(current_user, group).log_change_group_settings group.record_email_setting_changes!(current_user) group.expire_imap_mailbox_cache - update_existing_users(group.group_users, notification_level, categories, tags) if params[:update_existing_users] == "true" + if params[:update_existing_users] == "true" + update_existing_users(group.group_users, notification_level, categories, tags) + end AdminDashboardData.clear_found_problem("group_#{group.id}_email_credentials") # Redirect user to groups index page if they can no longer see the group @@ -185,10 +188,7 @@ class GroupsController < ApplicationController group = find_group(:group_id) guardian.ensure_can_see_group_members!(group) - posts = group.posts_for( - guardian, - params.permit(:before_post_id, :category_id) - ).limit(20) + posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20) render_serialized posts.to_a, GroupPostSerializer end @@ -196,37 +196,32 @@ class GroupsController < ApplicationController group = find_group(:group_id) guardian.ensure_can_see_group_members!(group) - @posts = group.posts_for( - guardian, - params.permit(:before_post_id, :category_id) - ).limit(50) - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}" + @posts = group.posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50) + @title = + "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}" @link = Discourse.base_url @description = I18n.t("rss_description.group_posts", group_name: group.name) - render 'posts/latest', formats: [:rss] + render "posts/latest", formats: [:rss] end def mentions raise Discourse::NotFound unless SiteSetting.enable_mentions? group = find_group(:group_id) - posts = group.mentioned_posts_for( - guardian, - params.permit(:before_post_id, :category_id) - ).limit(20) + posts = + group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(20) render_serialized posts.to_a, GroupPostSerializer end def mentions_feed raise Discourse::NotFound unless SiteSetting.enable_mentions? group = find_group(:group_id) - @posts = group.mentioned_posts_for( - guardian, - params.permit(:before_post_id, :category_id) - ).limit(50) - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}" + @posts = + group.mentioned_posts_for(guardian, params.permit(:before_post_id, :category_id)).limit(50) + @title = + "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}" @link = Discourse.base_url @description = I18n.t("rss_description.group_mentions", group_name: group.name) - render 'posts/latest', formats: [:rss] + render "posts/latest", formats: [:rss] end def members @@ -240,10 +235,14 @@ class GroupsController < ApplicationController raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000 raise Discourse::InvalidParameters.new(:offset) if offset < 0 - dir = (params[:asc] && params[:asc].present?) ? 'ASC' : 'DESC' + dir = (params[:asc] && params[:asc].present?) ? "ASC" : "DESC" if params[:desc] - Discourse.deprecate(":desc is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') - dir = (params[:desc] && params[:desc].present?) ? 'DESC' : 'ASC' + Discourse.deprecate( + ":desc is deprecated please use :asc instead", + output_in_test: true, + drop_from: "2.9.0", + ) + dir = (params[:desc] && params[:desc].present?) ? "DESC" : "ASC" end order = "NOT group_users.owner" @@ -254,7 +253,7 @@ class GroupsController < ApplicationController total = users.count if (filter = params[:filter]).present? - filter = filter.split(',') if filter.include?(',') + filter = filter.split(",") if filter.include?(",") if current_user&.admin users = users.filter_by_username_or_email(filter) @@ -263,26 +262,29 @@ class GroupsController < ApplicationController end end - users = users - .select("users.*, group_requests.reason, group_requests.created_at requested_at") - .order(params[:order] == 'requested_at' ? "group_requests.created_at #{dir}" : "") - .order(username_lower: dir) - .limit(limit) - .offset(offset) + users = + users + .select("users.*, group_requests.reason, group_requests.created_at requested_at") + .order(params[:order] == "requested_at" ? "group_requests.created_at #{dir}" : "") + .order(username_lower: dir) + .limit(limit) + .offset(offset) - return render json: { - members: serialize_data(users, GroupRequesterSerializer), - meta: { - total: total, - limit: limit, - offset: offset - } - } + return( + render json: { + members: serialize_data(users, GroupRequesterSerializer), + meta: { + total: total, + limit: limit, + offset: offset, + }, + } + ) end - if params[:order] && %w{last_posted_at last_seen_at}.include?(params[:order]) + if params[:order] && %w[last_posted_at last_seen_at].include?(params[:order]) order = "#{params[:order]} #{dir} NULLS LAST" - elsif params[:order] == 'added_at' + elsif params[:order] == "added_at" order = "group_users.created_at #{dir}" end @@ -290,7 +292,7 @@ class GroupsController < ApplicationController total = users.count if (filter = params[:filter]).present? - filter = filter.split(',') if filter.include?(',') + filter = filter.split(",") if filter.include?(",") if current_user&.admin users = users.filter_by_username_or_email(filter) @@ -299,25 +301,26 @@ class GroupsController < ApplicationController end end - users = users - .includes(:primary_group) - .includes(:user_option) - .select('users.*, group_users.created_at as added_at') - .order(order) - .order(username_lower: dir) + users = + users + .includes(:primary_group) + .includes(:user_option) + .select("users.*, group_users.created_at as added_at") + .order(order) + .order(username_lower: dir) members = users.limit(limit).offset(offset) - owners = users.where('group_users.owner') + owners = users.where("group_users.owner") render json: { - members: serialize_data(members, GroupUserSerializer), - owners: serialize_data(owners, GroupUserSerializer), - meta: { - total: total, - limit: limit, - offset: offset - } - } + members: serialize_data(members, GroupUserSerializer), + owners: serialize_data(owners, GroupUserSerializer), + meta: { + total: total, + limit: limit, + offset: offset, + }, + } end def add_members @@ -327,10 +330,12 @@ class GroupsController < ApplicationController users = users_from_params.to_a emails = [] if params[:emails] - params[:emails].split(",").each do |email| - existing_user = User.find_by_email(email) - existing_user.present? ? users.push(existing_user) : emails.push(email) - end + params[:emails] + .split(",") + .each do |email| + existing_user = User.find_by_email(email) + existing_user.present? ? users.push(existing_user) : emails.push(email) + end end guardian.ensure_can_invite_to_forum!([group]) if emails.present? @@ -340,43 +345,43 @@ class GroupsController < ApplicationController end if users.length > ADD_MEMBERS_LIMIT - return render_json_error( - I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT) + return( + render_json_error(I18n.t("groups.errors.adding_too_many_users", count: ADD_MEMBERS_LIMIT)) ) end usernames_already_in_group = group.users.where(id: users.map(&:id)).pluck(:username) - if usernames_already_in_group.present? && - usernames_already_in_group.length == users.length && - emails.blank? - render_json_error(I18n.t( - "groups.errors.member_already_exist", - username: usernames_already_in_group.sort.join(", "), - count: usernames_already_in_group.size - )) + if usernames_already_in_group.present? && usernames_already_in_group.length == users.length && + emails.blank? + render_json_error( + I18n.t( + "groups.errors.member_already_exist", + username: usernames_already_in_group.sort.join(", "), + count: usernames_already_in_group.size, + ), + ) else notify = params[:notify_users]&.to_s == "true" uniq_users = users.uniq - uniq_users.each do |user| - add_user_to_group(group, user, notify) - end + uniq_users.each { |user| add_user_to_group(group, user, notify) } emails.each do |email| begin Invite.generate(current_user, email: email, group_ids: [group.id]) rescue RateLimiter::LimitExceeded => e - return render_json_error(I18n.t( - "invite.rate_limit", - count: SiteSetting.max_invites_per_day, - time_left: e.time_left - )) + return( + render_json_error( + I18n.t( + "invite.rate_limit", + count: SiteSetting.max_invites_per_day, + time_left: e.time_left, + ), + ) + ) end end - render json: success_json.merge!( - usernames: uniq_users.map(&:username), - emails: emails - ) + render json: success_json.merge!(usernames: uniq_users.map(&:username), emails: emails) end end @@ -412,12 +417,13 @@ class GroupsController < ApplicationController end if params[:accept] - PostCreator.new(current_user, - title: I18n.t('groups.request_accepted_pm.title', group_name: group.name), - raw: I18n.t('groups.request_accepted_pm.body', group_name: group.name), + PostCreator.new( + current_user, + title: I18n.t("groups.request_accepted_pm.title", group_name: group.name), + raw: I18n.t("groups.request_accepted_pm.body", group_name: group.name), archetype: Archetype.private_message, target_usernames: user.username, - skip_validations: true + skip_validations: true, ).create! end @@ -460,9 +466,9 @@ class GroupsController < ApplicationController params[:user_emails] = params[:user_email] if params[:user_email].present? users = users_from_params - raise Discourse::InvalidParameters.new( - 'user_ids or usernames or user_emails must be present' - ) if users.empty? + if users.empty? + raise Discourse::InvalidParameters.new("user_ids or usernames or user_emails must be present") + end removed_users = [] skipped_users = [] @@ -480,10 +486,7 @@ class GroupsController < ApplicationController end end - render json: success_json.merge!( - usernames: removed_users, - skipped_usernames: skipped_users - ) + render json: success_json.merge!(usernames: removed_users, skipped_usernames: skipped_users) end def leave @@ -511,24 +514,35 @@ class GroupsController < ApplicationController begin GroupRequest.create!(group: group, user: current_user, reason: params[:reason]) rescue ActiveRecord::RecordNotUnique - return render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")), status: 409 + return( + render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")), + status: 409 + ) end usernames = [current_user.username].concat( - group.users.where('group_users.owner') + group + .users + .where("group_users.owner") .order("users.last_seen_at DESC") .limit(MAX_NOTIFIED_OWNERS) - .pluck("users.username") + .pluck("users.username"), ) - post = PostCreator.new(current_user, - title: I18n.t('groups.request_membership_pm.title', group_name: group.name), - raw: params[:reason], - archetype: Archetype.private_message, - target_usernames: usernames.join(','), - topic_opts: { custom_fields: { requested_group_id: group.id } }, - skip_validations: true - ).create! + post = + PostCreator.new( + current_user, + title: I18n.t("groups.request_membership_pm.title", group_name: group.name), + raw: params[:reason], + archetype: Archetype.private_message, + target_usernames: usernames.join(","), + topic_opts: { + custom_fields: { + requested_group_id: group.id, + }, + }, + skip_validations: true, + ).create! render json: success_json.merge(relative_url: post.topic.relative_url) end @@ -538,11 +552,10 @@ class GroupsController < ApplicationController notification_level = params.require(:notification_level) user_id = current_user.id - if guardian.is_staff? - user_id = params[:user_id] || user_id - end + user_id = params[:user_id] || user_id if guardian.is_staff? - GroupUser.where(group_id: group.id) + GroupUser + .where(group_id: group.id) .where(user_id: user_id) .update_all(notification_level: notification_level) @@ -556,29 +569,28 @@ class GroupsController < ApplicationController page_size = 25 offset = (params[:offset] && params[:offset].to_i) || 0 - group_histories = GroupHistory.with_filters(group, params[:filters]) - .limit(page_size) - .offset(offset * page_size) + group_histories = + GroupHistory.with_filters(group, params[:filters]).limit(page_size).offset(offset * page_size) render_json_dump( logs: serialize_data(group_histories, BasicGroupHistorySerializer), - all_loaded: group_histories.count < page_size + all_loaded: group_histories.count < page_size, ) end def search - groups = Group.visible_groups(current_user) - .where("groups.id <> ?", Group::AUTO_GROUPS[:everyone]) - .includes(:flair_upload) - .order(:name) + groups = + Group + .visible_groups(current_user) + .where("groups.id <> ?", Group::AUTO_GROUPS[:everyone]) + .includes(:flair_upload) + .order(:name) if (term = params[:term]).present? groups = groups.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{term}%") end - if params[:ignore_automatic].to_s == "true" - groups = groups.where(automatic: false) - end + groups = groups.where(automatic: false) if params[:ignore_automatic].to_s == "true" if Group.preloaded_custom_field_names.present? Group.preload_custom_fields(groups, Group.preloaded_custom_field_names) @@ -589,8 +601,14 @@ class GroupsController < ApplicationController def permissions group = find_group(:id) - category_groups = group.category_groups.select { |category_group| guardian.can_see_category?(category_group.category) } - render_serialized(category_groups.sort_by { |category_group| category_group.category.name }, CategoryGroupSerializer) + category_groups = + group.category_groups.select do |category_group| + guardian.can_see_category?(category_group.category) + end + render_serialized( + category_groups.sort_by { |category_group| category_group.category.name }, + CategoryGroupSerializer, + ) end def test_email_settings @@ -611,7 +629,7 @@ class GroupsController < ApplicationController enable_tls = settings[:ssl] == "true" email_host = params[:host] - if !["smtp", "imap"].include?(params[:protocol]) + if !%w[smtp imap].include?(params[:protocol]) raise Discourse::InvalidParameters.new("Valid protocols to test are smtp and imap") end @@ -622,20 +640,29 @@ class GroupsController < ApplicationController enable_starttls_auto = false settings.delete(:ssl) - final_settings = settings.merge(enable_tls: enable_tls, enable_starttls_auto: enable_starttls_auto) - .permit(:host, :port, :username, :password, :enable_tls, :enable_starttls_auto, :debug) - EmailSettingsValidator.validate_as_user(current_user, "smtp", **final_settings.to_h.symbolize_keys) + final_settings = + settings.merge( + enable_tls: enable_tls, + enable_starttls_auto: enable_starttls_auto, + ).permit(:host, :port, :username, :password, :enable_tls, :enable_starttls_auto, :debug) + EmailSettingsValidator.validate_as_user( + current_user, + "smtp", + **final_settings.to_h.symbolize_keys, + ) when "imap" - final_settings = settings.merge(ssl: enable_tls) - .permit(:host, :port, :username, :password, :ssl, :debug) - EmailSettingsValidator.validate_as_user(current_user, "imap", **final_settings.to_h.symbolize_keys) + final_settings = + settings.merge(ssl: enable_tls).permit(:host, :port, :username, :password, :ssl, :debug) + EmailSettingsValidator.validate_as_user( + current_user, + "imap", + **final_settings.to_h.symbolize_keys, + ) end render json: success_json rescue *EmailSettingsExceptionHandler::EXPECTED_EXCEPTIONS, StandardError => err - render_json_error( - EmailSettingsExceptionHandler.friendly_exception_message(err, email_host) - ) + render_json_error(EmailSettingsExceptionHandler.friendly_exception_message(err, email_host)) end end end @@ -653,7 +680,7 @@ class GroupsController < ApplicationController end def group_params(automatic: false) - attributes = %i{ + attributes = %i[ bio_raw default_notification_level messageable_level @@ -662,7 +689,7 @@ class GroupsController < ApplicationController flair_color flair_icon flair_upload_id - } + ] if automatic attributes.push(:visibility_level) @@ -673,7 +700,7 @@ class GroupsController < ApplicationController :full_name, :public_exit, :public_admission, - :membership_request_template + :membership_request_template, ) end @@ -703,7 +730,7 @@ class GroupsController < ApplicationController :grant_trust_level, :automatic_membership_email_domains, :publish_read_state, - :allow_unknown_sender_topic_replies + :allow_unknown_sender_topic_replies, ) custom_fields = DiscoursePluginRegistry.editable_group_custom_fields @@ -711,7 +738,7 @@ class GroupsController < ApplicationController end if !automatic || current_user.admin - [:muted, :regular, :tracking, :watching, :watching_first_post].each do |level| + %i[muted regular tracking watching watching_first_post].each do |level| attributes << { "#{level}_category_ids" => [] } attributes << { "#{level}_tags" => [] } end @@ -770,8 +797,10 @@ class GroupsController < ApplicationController end def user_default_notifications(group, params) - category_notifications = group.group_category_notification_defaults.pluck(:category_id, :notification_level).to_h - tag_notifications = group.group_tag_notification_defaults.pluck(:tag_id, :notification_level).to_h + category_notifications = + group.group_category_notification_defaults.pluck(:category_id, :notification_level).to_h + tag_notifications = + group.group_tag_notification_defaults.pluck(:tag_id, :notification_level).to_h categories = {} tags = {} @@ -782,10 +811,7 @@ class GroupsController < ApplicationController category_id = category_id.to_i old_value = category_notifications[category_id] - metadata = { - old_value: old_value, - new_value: value - } + metadata = { old_value: old_value, new_value: value } if old_value.blank? metadata[:action] = :create @@ -805,10 +831,7 @@ class GroupsController < ApplicationController tag_ids.each do |tag_id| old_value = tag_notifications[tag_id] - metadata = { - old_value: old_value, - new_value: value - } + metadata = { old_value: old_value, new_value: value } if old_value.blank? metadata[:action] = :create @@ -834,20 +857,18 @@ class GroupsController < ApplicationController notification_level = nil default_notification_level = params[:default_notification_level]&.to_i - if default_notification_level.present? && group.default_notification_level != default_notification_level + if default_notification_level.present? && + group.default_notification_level != default_notification_level notification_level = { old_value: group.default_notification_level, - new_value: default_notification_level + new_value: default_notification_level, } end [notification_level, categories, tags] end - %i{ - count - update - }.each do |action| + %i[count update].each do |action| define_method("#{action}_existing_users") do |group_users, notification_level, categories, tags| return 0 if notification_level.blank? && categories.blank? && tags.blank? @@ -865,7 +886,12 @@ class GroupsController < ApplicationController categories.each do |category_id, data| if data[:action] == :update || data[:action] == :delete - category_users = CategoryUser.where(category_id: category_id, notification_level: data[:old_value], user_id: group_users.select(:user_id)) + category_users = + CategoryUser.where( + category_id: category_id, + notification_level: data[:old_value], + user_id: group_users.select(:user_id), + ) if action == :update category_users.delete_all @@ -879,7 +905,12 @@ class GroupsController < ApplicationController tags.each do |tag_id, data| if data[:action] == :update || data[:action] == :delete - tag_users = TagUser.where(tag_id: tag_id, notification_level: data[:old_value], user_id: group_users.select(:user_id)) + tag_users = + TagUser.where( + tag_id: tag_id, + notification_level: data[:old_value], + user_id: group_users.select(:user_id), + ) if action == :update tag_users.delete_all @@ -892,47 +923,65 @@ class GroupsController < ApplicationController end if categories.present? || tags.present? - group_users.select(:id, :user_id).find_in_batches do |batch| - user_ids = batch.pluck(:user_id) + group_users + .select(:id, :user_id) + .find_in_batches do |batch| + user_ids = batch.pluck(:user_id) - categories.each do |category_id, data| - category_users = [] - existing_users = CategoryUser.where(category_id: category_id, user_id: user_ids).where("notification_level IS NOT NULL") - skip_user_ids = existing_users.pluck(:user_id) + categories.each do |category_id, data| + category_users = [] + existing_users = + CategoryUser.where(category_id: category_id, user_id: user_ids).where( + "notification_level IS NOT NULL", + ) + skip_user_ids = existing_users.pluck(:user_id) - batch.each do |group_user| - next if skip_user_ids.include?(group_user.user_id) - category_users << { category_id: category_id, user_id: group_user.user_id, notification_level: data[:new_value] } + batch.each do |group_user| + next if skip_user_ids.include?(group_user.user_id) + category_users << { + category_id: category_id, + user_id: group_user.user_id, + notification_level: data[:new_value], + } + end + + next if category_users.blank? + + if action == :update + CategoryUser.insert_all!(category_users) + else + ids += category_users.pluck(:user_id) + end end - next if category_users.blank? + tags.each do |tag_id, data| + tag_users = [] + existing_users = + TagUser.where(tag_id: tag_id, user_id: user_ids).where( + "notification_level IS NOT NULL", + ) + skip_user_ids = existing_users.pluck(:user_id) - if action == :update - CategoryUser.insert_all!(category_users) - else - ids += category_users.pluck(:user_id) + batch.each do |group_user| + next if skip_user_ids.include?(group_user.user_id) + tag_users << { + tag_id: tag_id, + user_id: group_user.user_id, + notification_level: data[:new_value], + created_at: Time.now, + updated_at: Time.now, + } + end + + next if tag_users.blank? + + if action == :update + TagUser.insert_all!(tag_users) + else + ids += tag_users.pluck(:user_id) + end end end - - tags.each do |tag_id, data| - tag_users = [] - existing_users = TagUser.where(tag_id: tag_id, user_id: user_ids).where("notification_level IS NOT NULL") - skip_user_ids = existing_users.pluck(:user_id) - - batch.each do |group_user| - next if skip_user_ids.include?(group_user.user_id) - tag_users << { tag_id: tag_id, user_id: group_user.user_id, notification_level: data[:new_value], created_at: Time.now, updated_at: Time.now } - end - - next if tag_users.blank? - - if action == :update - TagUser.insert_all!(tag_users) - else - ids += tag_users.pluck(:user_id) - end - end - end end ids.uniq.count diff --git a/app/controllers/highlight_js_controller.rb b/app/controllers/highlight_js_controller.rb index 5011fc9535..b77c14e82a 100644 --- a/app/controllers/highlight_js_controller.rb +++ b/app/controllers/highlight_js_controller.rb @@ -1,26 +1,26 @@ # frozen_string_literal: true class HighlightJsController < ApplicationController - skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show] + skip_before_action :preload_json, + :redirect_to_login_if_required, + :check_xhr, + :verify_authenticity_token, + only: [:show] before_action :apply_cdn_headers, only: [:show] def show - no_cookies RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do - current_version = HighlightJs.version(SiteSetting.highlighted_languages) - if current_version != params[:version] - return redirect_to path(HighlightJs.path) - end + return redirect_to path(HighlightJs.path) if current_version != params[:version] # note, this can be slightly optimised by caching the bundled file, it cuts down on N reads # our nginx config caches this so in practical terms it does not really matter and keeps # code simpler - languages = SiteSetting.highlighted_languages.split('|') + languages = SiteSetting.highlighted_languages.split("|") highlight_js = HighlightJs.bundle(languages) @@ -28,7 +28,7 @@ class HighlightJsController < ApplicationController response.headers["Content-Length"] = highlight_js.bytesize.to_s immutable_for 1.year - render plain: highlight_js, disposition: nil, content_type: 'application/javascript' + render plain: highlight_js, disposition: nil, content_type: "application/javascript" end end end diff --git a/app/controllers/inline_onebox_controller.rb b/app/controllers/inline_onebox_controller.rb index 871be8c308..3b3d61c55c 100644 --- a/app/controllers/inline_onebox_controller.rb +++ b/app/controllers/inline_onebox_controller.rb @@ -5,12 +5,13 @@ class InlineOneboxController < ApplicationController def show hijack do - oneboxes = InlineOneboxer.new( - params[:urls] || [], - user_id: current_user.id, - category_id: params[:category_id].to_i, - topic_id: params[:topic_id].to_i - ).process + oneboxes = + InlineOneboxer.new( + params[:urls] || [], + user_id: current_user.id, + category_id: params[:category_id].to_i, + topic_id: params[:topic_id].to_i, + ).process render json: { "inline-oneboxes" => oneboxes } end end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 9f9eb04190..a8483d2b46 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -1,17 +1,24 @@ # frozen_string_literal: true -require 'csv' +require "csv" class InvitesController < ApplicationController - - requires_login only: [:create, :retrieve, :destroy, :destroy_all_expired, :resend_invite, :resend_all_invites, :upload_csv] + requires_login only: %i[ + create + retrieve + destroy + destroy_all_expired + resend_invite + resend_all_invites + upload_csv + ] skip_before_action :check_xhr, except: [:perform_accept_invitation] skip_before_action :preload_json, except: [:show] skip_before_action :redirect_to_login_if_required - before_action :ensure_invites_allowed, only: [:show, :perform_accept_invitation] - before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation] + before_action :ensure_invites_allowed, only: %i[show perform_accept_invitation] + before_action :ensure_new_registrations_allowed, only: %i[show perform_accept_invitation] def show expires_now @@ -27,7 +34,7 @@ class InvitesController < ApplicationController end rescue RateLimiter::LimitExceeded => e flash.now[:error] = e.description - render layout: 'no_ember' + render layout: "no_ember" end def create @@ -45,24 +52,37 @@ class InvitesController < ApplicationController if !groups_can_see_topic?(groups, topic) editable_topic_groups = topic.category.groups.filter { |g| guardian.can_edit_group?(g) } - return render_json_error(I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", "))) + return( + render_json_error( + I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")), + ) + ) end - invite = Invite.generate(current_user, - email: params[:email], - domain: params[:domain], - skip_email: params[:skip_email], - invited_by: current_user, - custom_message: params[:custom_message], - max_redemptions_allowed: params[:max_redemptions_allowed], - topic_id: topic&.id, - group_ids: groups&.map(&:id), - expires_at: params[:expires_at], - invite_to_topic: params[:invite_to_topic] - ) + invite = + Invite.generate( + current_user, + email: params[:email], + domain: params[:domain], + skip_email: params[:skip_email], + invited_by: current_user, + custom_message: params[:custom_message], + max_redemptions_allowed: params[:max_redemptions_allowed], + topic_id: topic&.id, + group_ids: groups&.map(&:id), + expires_at: params[:expires_at], + invite_to_topic: params[:invite_to_topic], + ) if invite.present? - render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email), show_warnings: true) + render_serialized( + invite, + InviteSerializer, + scope: guardian, + root: nil, + show_emails: params.has_key?(:email), + show_warnings: true, + ) else render json: failed_json, status: 422 end @@ -81,7 +101,14 @@ class InvitesController < ApplicationController guardian.ensure_can_invite_to_forum!(nil) - render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email), show_warnings: true) + render_serialized( + invite, + InviteSerializer, + scope: guardian, + root: nil, + show_emails: params.has_key?(:email), + show_warnings: true, + ) end def update @@ -108,12 +135,19 @@ class InvitesController < ApplicationController if params.has_key?(:group_ids) || params.has_key?(:group_names) invite.invited_groups.destroy_all - groups.each { |group| invite.invited_groups.find_or_create_by!(group_id: group.id) } if groups.present? + if groups.present? + groups.each { |group| invite.invited_groups.find_or_create_by!(group_id: group.id) } + end end if !groups_can_see_topic?(invite.groups, invite.topics.first) - editable_topic_groups = invite.topics.first.category.groups.filter { |g| guardian.can_edit_group?(g) } - return render_json_error(I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", "))) + editable_topic_groups = + invite.topics.first.category.groups.filter { |g| guardian.can_edit_group?(g) } + return( + render_json_error( + I18n.t("invite.requires_groups", groups: editable_topic_groups.pluck(:name).join(", ")), + ) + ) end if params.has_key?(:email) @@ -121,20 +155,26 @@ class InvitesController < ApplicationController new_email = params[:email].presence if new_email - if Invite.where.not(id: invite.id).find_by(email: new_email.downcase, invited_by_id: current_user.id)&.redeemable? - return render_json_error( - I18n.t("invite.invite_exists", email: CGI.escapeHTML(new_email)), - status: 409 + if Invite + .where.not(id: invite.id) + .find_by(email: new_email.downcase, invited_by_id: current_user.id) + &.redeemable? + return( + render_json_error( + I18n.t("invite.invite_exists", email: CGI.escapeHTML(new_email)), + status: 409, + ) ) end end if old_email != new_email - invite.emailed_status = if new_email && !params[:skip_email] - Invite.emailed_status_types[:pending] - else - Invite.emailed_status_types[:not_required] - end + invite.emailed_status = + if new_email && !params[:skip_email] + Invite.emailed_status_types[:pending] + else + Invite.emailed_status_types[:not_required] + end end invite.domain = nil if invite.email.present? @@ -162,9 +202,13 @@ class InvitesController < ApplicationController end begin - invite.update!(params.permit(:email, :custom_message, :max_redemptions_allowed, :expires_at)) + invite.update!( + params.permit(:email, :custom_message, :max_redemptions_allowed, :expires_at), + ) rescue ActiveRecord::RecordInvalid => e - return render json: {}, status: 200 if SiteSetting.hide_email_address_taken? && e.record.email_already_exists? + if SiteSetting.hide_email_address_taken? && e.record.email_already_exists? + return render json: {}, status: 200 + end return render_json_error(e.record.errors.full_messages.first) end end @@ -174,7 +218,14 @@ class InvitesController < ApplicationController Jobs.enqueue(:invite_email, invite_id: invite.id, invite_to_topic: params[:invite_to_topic]) end - render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email), show_warnings: true) + render_serialized( + invite, + InviteSerializer, + scope: guardian, + root: nil, + show_emails: params.has_key?(:email), + show_warnings: true, + ) end def destroy @@ -192,17 +243,23 @@ class InvitesController < ApplicationController # via the SessionController#sso_login route def perform_accept_invitation params.require(:id) - params.permit(:email, :username, :name, :password, :timezone, :email_token, user_custom_fields: {}) + params.permit( + :email, + :username, + :name, + :password, + :timezone, + :email_token, + user_custom_fields: { + }, + ) invite = Invite.find_by(invite_key: params[:id]) redeeming_user = current_user if invite.present? begin - attrs = { - ip_address: request.remote_ip, - session: session - } + attrs = { ip_address: request.remote_ip, session: session } if redeeming_user attrs[:redeeming_user] = redeeming_user @@ -230,12 +287,10 @@ class InvitesController < ApplicationController end if user.blank? - return render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404 + return render json: failed_json.merge(message: I18n.t("invite.not_found_json")), status: 404 end - if !redeeming_user && user.active? && user.guardian.can_access_forum? - log_on_user(user) - end + log_on_user(user) if !redeeming_user && user.active? && user.guardian.can_access_forum? user.update_timezone_if_missing(params[:timezone]) post_process_invite(user) @@ -246,9 +301,7 @@ class InvitesController < ApplicationController if user.present? if user.active? && user.guardian.can_access_forum? - if redeeming_user - response[:message] = I18n.t("invite.existing_user_success") - end + response[:message] = I18n.t("invite.existing_user_success") if redeeming_user if user.guardian.can_see?(topic) response[:redirect_to] = path(topic.relative_url) @@ -257,20 +310,18 @@ class InvitesController < ApplicationController end else response[:message] = if user.active? - I18n.t('activation.approval_required') + I18n.t("activation.approval_required") else - I18n.t('invite.confirm_email') + I18n.t("invite.confirm_email") end - if user.guardian.can_see?(topic) - cookies[:destination_url] = path(topic.relative_url) - end + cookies[:destination_url] = path(topic.relative_url) if user.guardian.can_see?(topic) end end render json: success_json.merge(response) else - render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404 + render json: failed_json.merge(message: I18n.t("invite.not_found_json")), status: 404 end end @@ -279,7 +330,7 @@ class InvitesController < ApplicationController Invite .where(invited_by: current_user) - .where('expires_at < ?', Time.zone.now) + .where("expires_at < ?", Time.zone.now) .find_each { |invite| invite.trash!(current_user) } render json: success_json @@ -301,13 +352,20 @@ class InvitesController < ApplicationController guardian.ensure_can_resend_all_invites!(current_user) begin - RateLimiter.new(current_user, "bulk-reinvite-per-day", 1, 1.day, apply_limit_to_staff: true).performed! + RateLimiter.new( + current_user, + "bulk-reinvite-per-day", + 1, + 1.day, + apply_limit_to_staff: true, + ).performed! rescue RateLimiter::LimitExceeded return render_json_error(I18n.t("rate_limiter.slow_down")) end - Invite.pending(current_user) - .where('invites.email IS NOT NULL') + Invite + .pending(current_user) + .where("invites.email IS NOT NULL") .find_each { |invite| invite.resend_invite } render json: success_json @@ -326,17 +384,15 @@ class InvitesController < ApplicationController CSV.foreach(file.tempfile, encoding: "bom|utf-8") do |row| # Try to extract a CSV header, if it exists if csv_header.nil? - if row[0] == 'email' + if row[0] == "email" csv_header = row next else - csv_header = ["email", "groups", "topic_id"] + csv_header = %w[email groups topic_id] end end - if row[0].present? - invites.push(csv_header.zip(row).map.to_h.filter { |k, v| v.present? }) - end + invites.push(csv_header.zip(row).map.to_h.filter { |k, v| v.present? }) if row[0].present? break if invites.count >= SiteSetting.max_bulk_invites end @@ -345,7 +401,16 @@ class InvitesController < ApplicationController Jobs.enqueue(:bulk_invite, invites: invites, current_user_id: current_user.id) if invites.count >= SiteSetting.max_bulk_invites - render json: failed_json.merge(errors: [I18n.t("bulk_invite.max_rows", max_bulk_invites: SiteSetting.max_bulk_invites)]), status: 422 + render json: + failed_json.merge( + errors: [ + I18n.t( + "bulk_invite.max_rows", + max_bulk_invites: SiteSetting.max_bulk_invites, + ), + ], + ), + status: 422 else render json: success_json end @@ -375,9 +440,7 @@ class InvitesController < ApplicationController email_verified_by_link = invite.email_token.present? && params[:t] == invite.email_token - if email_verified_by_link - email = invite.email - end + email = invite.email if email_verified_by_link hidden_email = email != invite.email @@ -393,12 +456,10 @@ class InvitesController < ApplicationController hidden_email: hidden_email, username: username, is_invite_link: invite.is_invite_link?, - email_verified_by_link: email_verified_by_link + email_verified_by_link: email_verified_by_link, } - if different_external_email - info[:different_external_email] = true - end + info[:different_external_email] = true if different_external_email if staged_user = User.where(staged: true).with_email(invite.email).first info[:username] = staged_user.username @@ -417,36 +478,46 @@ class InvitesController < ApplicationController secure_session["invite-key"] = invite.invite_key - render layout: 'application' + render layout: "application" end def show_irredeemable_invite(invite) - flash.now[:error] = \ - if invite.blank? - I18n.t('invite.not_found', base_url: Discourse.base_url) - elsif invite.redeemed? - if invite.is_invite_link? - I18n.t('invite.not_found_template_link', site_name: SiteSetting.title, base_url: Discourse.base_url) - else - I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url) - end - elsif invite.expired? - I18n.t('invite.expired', base_url: Discourse.base_url) + flash.now[:error] = if invite.blank? + I18n.t("invite.not_found", base_url: Discourse.base_url) + elsif invite.redeemed? + if invite.is_invite_link? + I18n.t( + "invite.not_found_template_link", + site_name: SiteSetting.title, + base_url: Discourse.base_url, + ) + else + I18n.t( + "invite.not_found_template", + site_name: SiteSetting.title, + base_url: Discourse.base_url, + ) end + elsif invite.expired? + I18n.t("invite.expired", base_url: Discourse.base_url) + end - render layout: 'no_ember' + render layout: "no_ember" end def ensure_invites_allowed - if (!SiteSetting.enable_local_logins && Discourse.enabled_auth_providers.count == 0 && !SiteSetting.enable_discourse_connect) + if ( + !SiteSetting.enable_local_logins && Discourse.enabled_auth_providers.count == 0 && + !SiteSetting.enable_discourse_connect + ) raise Discourse::NotFound end end def ensure_new_registrations_allowed unless SiteSetting.allow_new_registrations - flash[:error] = I18n.t('login.new_registrations_disabled') - render layout: 'no_ember' + flash[:error] = I18n.t("login.new_registrations_disabled") + render layout: "no_ember" false end end @@ -461,13 +532,14 @@ class InvitesController < ApplicationController end def post_process_invite(user) - user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message + user.enqueue_welcome_message("welcome_invite") if user.send_welcome_message Group.refresh_automatic_groups!(:admins, :moderators, :staff) if user.staff? if user.has_password? if !user.active - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) EmailToken.enqueue_signup_email(email_token) end elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins @@ -478,17 +550,19 @@ class InvitesController < ApplicationController def create_topic_invite_notifications(invite, user) invite.topics.each do |topic| if user.guardian.can_see?(topic) - last_notification = user.notifications - .where(notification_type: Notification.types[:invited_to_topic]) - .where(topic_id: topic.id) - .where(post_number: 1) - .where('created_at > ?', 1.hour.ago) + last_notification = + user + .notifications + .where(notification_type: Notification.types[:invited_to_topic]) + .where(topic_id: topic.id) + .where(post_number: 1) + .where("created_at > ?", 1.hour.ago) if !last_notification.exists? topic.create_invite_notification!( user, Notification.types[:invited_to_topic], - invite.invited_by + invite.invited_by, ) end end diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 66d8adbcc3..6e34b86867 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -6,45 +6,47 @@ class ListController < ApplicationController skip_before_action :check_xhr - before_action :set_category, only: [ - :category_default, - # filtered topics lists - Discourse.filters.map { |f| :"category_#{f}" }, - Discourse.filters.map { |f| :"category_none_#{f}" }, - # top summaries - :category_top, - :category_none_top, - # top pages (ie. with a period) - TopTopic.periods.map { |p| :"category_top_#{p}" }, - TopTopic.periods.map { |p| :"category_none_top_#{p}" }, - # category feeds - :category_feed, - ].flatten + before_action :set_category, + only: [ + :category_default, + # filtered topics lists + Discourse.filters.map { |f| :"category_#{f}" }, + Discourse.filters.map { |f| :"category_none_#{f}" }, + # top summaries + :category_top, + :category_none_top, + # top pages (ie. with a period) + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + # category feeds + :category_feed, + ].flatten - before_action :ensure_logged_in, except: [ - :topics_by, - # anonymous filters - Discourse.anonymous_filters, - Discourse.anonymous_filters.map { |f| "#{f}_feed" }, - # anonymous categorized filters - :category_default, - Discourse.anonymous_filters.map { |f| :"category_#{f}" }, - Discourse.anonymous_filters.map { |f| :"category_none_#{f}" }, - # category feeds - :category_feed, - # user topics feed - :user_topics_feed, - # top summaries - :top, - :category_top, - :category_none_top, - # top pages (ie. with a period) - TopTopic.periods.map { |p| :"top_#{p}" }, - TopTopic.periods.map { |p| :"top_#{p}_feed" }, - TopTopic.periods.map { |p| :"category_top_#{p}" }, - TopTopic.periods.map { |p| :"category_none_top_#{p}" }, - :group_topics - ].flatten + before_action :ensure_logged_in, + except: [ + :topics_by, + # anonymous filters + Discourse.anonymous_filters, + Discourse.anonymous_filters.map { |f| "#{f}_feed" }, + # anonymous categorized filters + :category_default, + Discourse.anonymous_filters.map { |f| :"category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"category_none_#{f}" }, + # category feeds + :category_feed, + # user topics feed + :user_topics_feed, + # top summaries + :top, + :category_top, + :category_none_top, + # top pages (ie. with a period) + TopTopic.periods.map { |p| :"top_#{p}" }, + TopTopic.periods.map { |p| :"top_#{p}_feed" }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + :group_topics, + ].flatten # Create our filters Discourse.filters.each do |filter| @@ -52,7 +54,8 @@ class ListController < ApplicationController list_opts = build_topic_list_options list_opts.merge!(options) if options user = list_target_user - if params[:category].blank? && filter == :latest && !SiteSetting.show_category_definitions_in_topic_lists + if params[:category].blank? && filter == :latest && + !SiteSetting.show_category_definitions_in_topic_lists list_opts[:no_definitions] = true end @@ -61,17 +64,16 @@ class ListController < ApplicationController if guardian.can_create_shared_draft? && @category.present? if @category.id == SiteSetting.shared_drafts_category.to_i # On shared drafts, show the destination category - list.topics.each do |t| - t.includes_destination_category = t.shared_draft.present? - end + list.topics.each { |t| t.includes_destination_category = t.shared_draft.present? } else # When viewing a non-shared draft category, find topics whose # destination are this category - shared_drafts = TopicQuery.new( - user, - category: SiteSetting.shared_drafts_category, - destination_category_id: list_opts[:category] - ).list_latest + shared_drafts = + TopicQuery.new( + user, + category: SiteSetting.shared_drafts_category, + destination_category_id: list_opts[:category], + ).list_latest if shared_drafts.present? && shared_drafts.topics.present? list.shared_drafts = shared_drafts.topics @@ -90,12 +92,14 @@ class ListController < ApplicationController if (filter.to_s != current_homepage) && use_crawler_layout? filter_title = I18n.t("js.filters.#{filter.to_s}.title", count: 0) if list_opts[:category] && @category - @title = I18n.t('js.filters.with_category', filter: filter_title, category: @category.name) + @title = + I18n.t("js.filters.with_category", filter: filter_title, category: @category.name) else - @title = I18n.t('js.filters.with_topics', filter: filter_title) + @title = I18n.t("js.filters.with_topics", filter: filter_title) end @title << " - #{SiteSetting.title}" - elsif @category.blank? && (filter.to_s == current_homepage) && SiteSetting.short_site_description.present? + elsif @category.blank? && (filter.to_s == current_homepage) && + SiteSetting.short_site_description.present? @title = "#{SiteSetting.title} - #{SiteSetting.short_site_description}" end end @@ -116,14 +120,21 @@ class ListController < ApplicationController def category_default canonical_url "#{Discourse.base_url_no_prefix}#{@category.url}" view_method = @category.default_view - view_method = 'latest' unless %w(latest top).include?(view_method) + view_method = "latest" unless %w[latest top].include?(view_method) self.public_send(view_method, category: @category.id) end def topics_by list_opts = build_topic_list_options - target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts) }, [:user_stat, :user_option]) + target_user = + fetch_user_from_params( + { + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + }, + %i[user_stat user_option], + ) ensure_can_see_profile!(target_user) list = generate_list_for("topics_by", target_user, list_opts) @@ -152,14 +163,15 @@ class ListController < ApplicationController end def message_route(action) - target_user = fetch_user_from_params({ include_inactive: current_user.try(:staff?) }, [:user_stat, :user_option]) + target_user = + fetch_user_from_params( + { include_inactive: current_user.try(:staff?) }, + %i[user_stat user_option], + ) case action - when :private_messages_unread, - :private_messages_new, - :private_messages_group_new, + when :private_messages_unread, :private_messages_new, :private_messages_group_new, :private_messages_group_unread - raise Discourse::NotFound if target_user.id != current_user.id when :private_messages_tag raise Discourse::NotFound if !guardian.can_tag_pms? @@ -181,7 +193,7 @@ class ListController < ApplicationController respond_with_list(list) end - %i{ + %i[ private_messages private_messages_sent private_messages_unread @@ -193,14 +205,12 @@ class ListController < ApplicationController private_messages_group_archive private_messages_warnings private_messages_tag - }.each do |action| - generate_message_route(action) - end + ].each { |action| generate_message_route(action) } def latest_feed discourse_expires_in 1.minute - options = { order: 'created' }.merge(build_topic_list_options) + options = { order: "created" }.merge(build_topic_list_options) @title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}" @link = "#{Discourse.base_url}/latest" @@ -208,7 +218,7 @@ class ListController < ApplicationController @description = I18n.t("rss_description.latest") @topic_list = TopicQuery.new(nil, options).list_latest - render 'list', formats: [:rss] + render "list", formats: [:rss] end def top_feed @@ -223,7 +233,7 @@ class ListController < ApplicationController @topic_list = TopicQuery.new(nil).list_top_for(period) - render 'list', formats: [:rss] + render "list", formats: [:rss] end def category_feed @@ -233,10 +243,11 @@ class ListController < ApplicationController @title = "#{@category.name} - #{SiteSetting.title}" @link = "#{Discourse.base_url_no_prefix}#{@category.url}" @atom_link = "#{Discourse.base_url_no_prefix}#{@category.url}.rss" - @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" + @description = + "#{I18n.t("topics_in_category", category: @category.name)} #{@category.description}" @topic_list = TopicQuery.new(current_user).list_new_in_category(@category) - render 'list', formats: [:rss] + render "list", formats: [:rss] end def user_topics_feed @@ -244,22 +255,22 @@ class ListController < ApplicationController target_user = fetch_user_from_params ensure_can_see_profile!(target_user) - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.user_topics", username: target_user.username)}" + @title = + "#{SiteSetting.title} - #{I18n.t("rss_description.user_topics", username: target_user.username)}" @link = "#{target_user.full_url}/activity/topics" @atom_link = "#{target_user.full_url}/activity/topics.rss" @description = I18n.t("rss_description.user_topics", username: target_user.username) - @topic_list = TopicQuery - .new(nil, order: 'created') - .public_send("list_topics_by", target_user) + @topic_list = TopicQuery.new(nil, order: "created").public_send("list_topics_by", target_user) - render 'list', formats: [:rss] + render "list", formats: [:rss] end def top(options = nil) options ||= {} period = params[:period] - period ||= ListController.best_period_for(current_user.try(:previous_visit_at), options[:category]) + period ||= + ListController.best_period_for(current_user.try(:previous_visit_at), options[:category]) TopTopic.validate_period(period) public_send("top_#{period}", options) end @@ -299,10 +310,7 @@ class ListController < ApplicationController end define_method("category_none_top_#{period}") do - self.public_send("top_#{period}", - category: @category.id, - no_subcategories: true - ) + self.public_send("top_#{period}", category: @category.id, no_subcategories: true) end # rss feed @@ -315,7 +323,7 @@ class ListController < ApplicationController @atom_link = "#{Discourse.base_url}/top.rss?period=#{period}" @topic_list = TopicQuery.new(nil).list_top_for(period) - render 'list', formats: [:rss] + render "list", formats: [:rss] end end @@ -337,16 +345,17 @@ class ListController < ApplicationController private def page_params - route_params = { format: 'json' } + route_params = { format: "json" } if @category.present? slug_path = @category.slug_path - route_params[:category_slug_path_with_id] = - (slug_path + [@category.id.to_s]).join("/") + route_params[:category_slug_path_with_id] = (slug_path + [@category.id.to_s]).join("/") end - route_params[:username] = UrlHelper.encode_component(params[:username]) if params[:username].present? + route_params[:username] = UrlHelper.encode_component(params[:username]) if params[ + :username + ].present? route_params[:period] = params[:period] if params[:period].present? route_params end @@ -355,9 +364,7 @@ class ListController < ApplicationController category_slug_path_with_id = params.require(:category_slug_path_with_id) @category = Category.find_by_slug_path_with_id(category_slug_path_with_id) - if @category.nil? - raise Discourse::NotFound.new("category not found", check_permalinks: true) - end + raise Discourse::NotFound.new("category not found", check_permalinks: true) if @category.nil? params[:category] = @category.id.to_s @@ -385,13 +392,14 @@ class ListController < ApplicationController return redirect_to path(url), status: 301 end - @description_meta = if @category.uncategorized? - I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale) - elsif @category.description_text.present? - @category.description_text - else - SiteSetting.site_description - end + @description_meta = + if @category.uncategorized? + I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale) + elsif @category.description_text.present? + @category.description_text + else + SiteSetting.site_description + end if use_crawler_layout? @subcategories = @category.subcategories.select { |c| guardian.can_see?(c) } @@ -431,7 +439,7 @@ class ListController < ApplicationController opts.delete(:category) if page_params.include?(:category_slug_path_with_id) - url = public_send(method, opts.merge(page_params)).sub('.json?', '?') + url = public_send(method, opts.merge(page_params)).sub(".json?", "?") # Unicode usernames need to be encoded when calling Rails' path helper. However, it means that the already # encoded username are encoded again which we do not want. As such, we unencode the url once when unicode usernames @@ -446,16 +454,24 @@ class ListController < ApplicationController end def self.best_period_for(previous_visit_at, category_id = nil) - default_period = ((category_id && Category.where(id: category_id).pluck_first(:default_top_period)) || - SiteSetting.top_page_default_timeframe).to_sym + default_period = + ( + (category_id && Category.where(id: category_id).pluck_first(:default_top_period)) || + SiteSetting.top_page_default_timeframe + ).to_sym best_period_with_topics_for(previous_visit_at, category_id, default_period) || default_period end - def self.best_period_with_topics_for(previous_visit_at, category_id = nil, default_period = SiteSetting.top_page_default_timeframe) + def self.best_period_with_topics_for( + previous_visit_at, + category_id = nil, + default_period = SiteSetting.top_page_default_timeframe + ) best_periods_for(previous_visit_at, default_period.to_sym).find do |period| top_topics = TopTopic.where("#{period}_score > 0") - top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id + top_topics = + top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id top_topics = top_topics.limit(SiteSetting.topics_per_period_in_top_page) top_topics.count == SiteSetting.topics_per_period_in_top_page end @@ -465,13 +481,12 @@ class ListController < ApplicationController return [default_period, :all].uniq unless date periods = [] - periods << :daily if date > (1.week + 1.day).ago - periods << :weekly if date > (1.month + 1.week).ago - periods << :monthly if date > (3.months + 3.weeks).ago + periods << :daily if date > (1.week + 1.day).ago + periods << :weekly if date > (1.month + 1.week).ago + periods << :monthly if date > (3.months + 3.weeks).ago periods << :quarterly if date > (1.year + 1.month).ago - periods << :yearly if date > 3.years.ago + periods << :yearly if date > 3.years.ago periods << :all periods end - end diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index eff9af7ac5..4d75df06f0 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -6,7 +6,7 @@ class MetadataController < ApplicationController def manifest expires_in 1.minutes - render json: default_manifest.to_json, content_type: 'application/manifest+json' + render json: default_manifest.to_json, content_type: "application/manifest+json" end def opensearch @@ -17,13 +17,13 @@ class MetadataController < ApplicationController def app_association_android raise Discourse::NotFound unless SiteSetting.app_association_android.present? expires_in 1.minutes - render plain: SiteSetting.app_association_android, content_type: 'application/json' + render plain: SiteSetting.app_association_android, content_type: "application/json" end def app_association_ios raise Discourse::NotFound unless SiteSetting.app_association_ios.present? expires_in 1.minutes - render plain: SiteSetting.app_association_ios, content_type: 'application/json' + render plain: SiteSetting.app_association_ios, content_type: "application/json" end private @@ -32,56 +32,56 @@ class MetadataController < ApplicationController display = "standalone" if request.user_agent regex = Regexp.new(SiteSetting.pwa_display_browser_regex) - if regex.match(request.user_agent) - display = "browser" - end + display = "browser" if regex.match(request.user_agent) end scheme_id = view_context.scheme_id - primary_color = ColorScheme.hex_for_name('primary', scheme_id) - icon_url_base = UrlHelper.absolute("/svg-sprite/#{Discourse.current_hostname}/icon/#{primary_color}") + primary_color = ColorScheme.hex_for_name("primary", scheme_id) + icon_url_base = + UrlHelper.absolute("/svg-sprite/#{Discourse.current_hostname}/icon/#{primary_color}") manifest = { name: SiteSetting.title, - short_name: SiteSetting.short_title.presence || SiteSetting.title.truncate(12, separator: ' ', omission: ''), + short_name: + SiteSetting.short_title.presence || + SiteSetting.title.truncate(12, separator: " ", omission: ""), description: SiteSetting.site_description, display: display, - start_url: Discourse.base_path.present? ? "#{Discourse.base_path}/" : '.', - background_color: "##{ColorScheme.hex_for_name('secondary', scheme_id)}", - theme_color: "##{ColorScheme.hex_for_name('header_background', scheme_id)}", - icons: [ - ], + start_url: Discourse.base_path.present? ? "#{Discourse.base_path}/" : ".", + background_color: "##{ColorScheme.hex_for_name("secondary", scheme_id)}", + theme_color: "##{ColorScheme.hex_for_name("header_background", scheme_id)}", + icons: [], share_target: { action: "#{Discourse.base_path}/new-topic", method: "GET", enctype: "application/x-www-form-urlencoded", params: { title: "title", - text: "body" - } + text: "body", + }, }, shortcuts: [ { - name: I18n.t('js.topic.create_long'), - short_name: I18n.t('js.topic.create'), + name: I18n.t("js.topic.create_long"), + short_name: I18n.t("js.topic.create"), url: "#{Discourse.base_path}/new-topic", }, { - name: I18n.t('js.user.messages.inbox'), - short_name: I18n.t('js.user.messages.inbox'), + name: I18n.t("js.user.messages.inbox"), + short_name: I18n.t("js.user.messages.inbox"), url: "#{Discourse.base_path}/my/messages", }, { - name: I18n.t('js.user.bookmarks'), - short_name: I18n.t('js.user.bookmarks'), + name: I18n.t("js.user.bookmarks"), + short_name: I18n.t("js.user.bookmarks"), url: "#{Discourse.base_path}/my/activity/bookmarks", }, { - name: I18n.t('js.filters.top.title'), - short_name: I18n.t('js.filters.top.title'), + name: I18n.t("js.filters.top.title"), + short_name: I18n.t("js.filters.top.title"), url: "#{Discourse.base_path}/top", - } - ] + }, + ], } logo = SiteSetting.site_manifest_icon_url @@ -89,41 +89,40 @@ class MetadataController < ApplicationController icon_entry = { src: UrlHelper.absolute(logo), sizes: "512x512", - type: MiniMime.lookup_by_filename(logo)&.content_type || "image/png" + type: MiniMime.lookup_by_filename(logo)&.content_type || "image/png", } manifest[:icons] << icon_entry.dup icon_entry[:purpose] = "maskable" manifest[:icons] << icon_entry end - SiteSetting.manifest_screenshots.split('|').each do |image| - next unless Discourse.store.has_been_uploaded?(image) + SiteSetting + .manifest_screenshots + .split("|") + .each do |image| + next unless Discourse.store.has_been_uploaded?(image) - upload = Upload.find_by(sha1: Upload.extract_sha1(image)) - next if upload.nil? + upload = Upload.find_by(sha1: Upload.extract_sha1(image)) + next if upload.nil? - manifest[:screenshots] = [] if manifest.dig(:screenshots).nil? + manifest[:screenshots] = [] if manifest.dig(:screenshots).nil? - manifest[:screenshots] << { - src: UrlHelper.absolute(image), - sizes: "#{upload.width}x#{upload.height}", - type: "image/#{upload.extension}" - } - end + manifest[:screenshots] << { + src: UrlHelper.absolute(image), + sizes: "#{upload.width}x#{upload.height}", + type: "image/#{upload.extension}", + } + end - if current_user && current_user.trust_level >= 1 && SiteSetting.native_app_install_banner_android - manifest = manifest.merge( - prefer_related_applications: true, - related_applications: [ - { - platform: "play", - id: SiteSetting.android_app_id - } - ] - ) + if current_user && current_user.trust_level >= 1 && + SiteSetting.native_app_install_banner_android + manifest = + manifest.merge( + prefer_related_applications: true, + related_applications: [{ platform: "play", id: SiteSetting.android_app_id }], + ) end manifest end - end diff --git a/app/controllers/new_topic_controller.rb b/app/controllers/new_topic_controller.rb index f6b9017666..2de899377b 100644 --- a/app/controllers/new_topic_controller.rb +++ b/app/controllers/new_topic_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true class NewTopicController < ApplicationController - def index; end + def index + end end diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 2006ddeddd..f495551957 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class NotificationsController < ApplicationController - requires_login - before_action :ensure_admin, only: [:create, :update, :destroy] - before_action :set_notification, only: [:update, :destroy] + before_action :ensure_admin, only: %i[create update destroy] + before_action :set_notification, only: %i[update destroy] def index user = @@ -20,9 +19,8 @@ class NotificationsController < ApplicationController if notification_types = params[:filter_by_types]&.split(",").presence notification_types.map! do |type| - Notification.types[type.to_sym] || ( - raise Discourse::InvalidParameters.new("invalid notification type: #{type}") - ) + Notification.types[type.to_sym] || + (raise Discourse::InvalidParameters.new("invalid notification type: #{type}")) end end @@ -35,7 +33,8 @@ class NotificationsController < ApplicationController if SiteSetting.legacy_navigation_menu? notifications = Notification.recent_report(current_user, limit, notification_types) else - notifications = Notification.prioritized_list(current_user, count: limit, types: notification_types) + notifications = + Notification.prioritized_list(current_user, count: limit, types: notification_types) # notification_types is blank for the "all notifications" user menu tab include_reviewables = notification_types.blank? && guardian.can_see_review_queue? end @@ -47,7 +46,8 @@ class NotificationsController < ApplicationController end end - if !params.has_key?(:silent) && params[:bump_last_seen_reviewable] && !@readonly_mode && include_reviewables + if !params.has_key?(:silent) && params[:bump_last_seen_reviewable] && !@readonly_mode && + include_reviewables current_user_id = current_user.id Scheduler::Defer.later "bump last seen reviewable for user" do # we lookup current_user again in the background thread to avoid @@ -62,13 +62,13 @@ class NotificationsController < ApplicationController json = { notifications: serialize_data(notifications, NotificationSerializer), - seen_notification_id: current_user.seen_notification_id + seen_notification_id: current_user.seen_notification_id, } if include_reviewables json[:pending_reviewables] = Reviewable.basic_serializers_for_list( Reviewable.user_menu_list_for(current_user), - current_user + current_user, ).as_json end @@ -76,10 +76,8 @@ class NotificationsController < ApplicationController else offset = params[:offset].to_i - notifications = Notification.where(user_id: user.id) - .visible - .includes(:topic) - .order(created_at: :desc) + notifications = + Notification.where(user_id: user.id).visible.includes(:topic).order(created_at: :desc) notifications = notifications.where(read: true) if params[:filter] == "read" @@ -88,12 +86,14 @@ class NotificationsController < ApplicationController total_rows = notifications.dup.count notifications = notifications.offset(offset).limit(60) notifications = filter_inaccessible_notifications(notifications) - render_json_dump(notifications: serialize_data(notifications, NotificationSerializer), - total_rows_notifications: total_rows, - seen_notification_id: user.seen_notification_id, - load_more_notifications: notifications_path(username: user.username, offset: offset + 60, filter: params[:filter])) + render_json_dump( + notifications: serialize_data(notifications, NotificationSerializer), + total_rows_notifications: total_rows, + seen_notification_id: user.seen_notification_id, + load_more_notifications: + notifications_path(username: user.username, offset: offset + 60, filter: params[:filter]), + ) end - end def mark_read @@ -144,7 +144,15 @@ class NotificationsController < ApplicationController end def notification_params - params.permit(:notification_type, :user_id, :data, :read, :topic_id, :post_number, :post_action_id) + params.permit( + :notification_type, + :user_id, + :data, + :read, + :topic_id, + :post_number, + :post_action_id, + ) end def render_notification diff --git a/app/controllers/offline_controller.rb b/app/controllers/offline_controller.rb index 03990e8a5f..b826e3f30d 100644 --- a/app/controllers/offline_controller.rb +++ b/app/controllers/offline_controller.rb @@ -5,6 +5,6 @@ class OfflineController < ApplicationController skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required def index - render :offline, content_type: 'text/html' + render :offline, content_type: "text/html" end end diff --git a/app/controllers/onebox_controller.rb b/app/controllers/onebox_controller.rb index dfa81c5e80..5ccdcd0b9e 100644 --- a/app/controllers/onebox_controller.rb +++ b/app/controllers/onebox_controller.rb @@ -4,7 +4,7 @@ class OneboxController < ApplicationController requires_login def show - unless params[:refresh] == 'true' + unless params[:refresh] == "true" preview = Oneboxer.cached_preview(params[:url]) preview = preview.strip if preview.present? return render(plain: preview) if preview.present? @@ -16,7 +16,7 @@ class OneboxController < ApplicationController user_id = current_user.id category_id = params[:category_id].to_i topic_id = params[:topic_id].to_i - invalidate = params[:refresh] == 'true' + invalidate = params[:refresh] == "true" url = params[:url] return render(body: nil, status: 404) if Oneboxer.recently_failed?(url) @@ -24,12 +24,14 @@ class OneboxController < ApplicationController hijack(info: "#{url} topic_id: #{topic_id} user_id: #{user_id}") do Oneboxer.preview_onebox!(user_id) - preview = Oneboxer.preview(url, - invalidate_oneboxes: invalidate, - user_id: user_id, - category_id: category_id, - topic_id: topic_id - ) + preview = + Oneboxer.preview( + url, + invalidate_oneboxes: invalidate, + user_id: user_id, + category_id: category_id, + topic_id: topic_id, + ) preview = preview.strip if preview.present? @@ -43,5 +45,4 @@ class OneboxController < ApplicationController end end end - end diff --git a/app/controllers/permalinks_controller.rb b/app/controllers/permalinks_controller.rb index 33f8d04c40..ee34e6bfaa 100644 --- a/app/controllers/permalinks_controller.rb +++ b/app/controllers/permalinks_controller.rb @@ -33,12 +33,8 @@ class PermalinksController < ApplicationController render json: MultiJson.dump(data) rescue Discourse::NotFound - data = { - found: false, - html: build_not_found_page(status: 200), - } + data = { found: false, html: build_not_found_page(status: 200) } render json: MultiJson.dump(data) end end - end diff --git a/app/controllers/post_action_users_controller.rb b/app/controllers/post_action_users_controller.rb index 24cac61295..7b39cd2fab 100644 --- a/app/controllers/post_action_users_controller.rb +++ b/app/controllers/post_action_users_controller.rb @@ -23,11 +23,14 @@ class PostActionUsersController < ApplicationController unknown_user_ids.merge(result) end - post_actions = post.post_actions.where(post_action_type_id: post_action_type_id) - .includes(:user) - .offset(page * page_size) - .order('post_actions.created_at ASC') - .limit(page_size) + post_actions = + post + .post_actions + .where(post_action_type_id: post_action_type_id) + .includes(:user) + .offset(page * page_size) + .order("post_actions.created_at ASC") + .limit(page_size) if !guardian.can_see_post_actors?(post.topic, post_action_type_id) raise Discourse::InvalidAccess unless current_user @@ -38,16 +41,15 @@ class PostActionUsersController < ApplicationController total_count = post["#{action_type}_count"].to_i data = { - post_action_users: serialize_data( - post_actions.to_a, - PostActionUserSerializer, - unknown_user_ids: unknown_user_ids - ) + post_action_users: + serialize_data( + post_actions.to_a, + PostActionUserSerializer, + unknown_user_ids: unknown_user_ids, + ), } - if total_count > page_size - data[:total_rows_post_action_users] = total_count - end + data[:total_rows_post_action_users] = total_count if total_count > page_size render_json_dump(data) end diff --git a/app/controllers/post_actions_controller.rb b/app/controllers/post_actions_controller.rb index 9e75797044..966af4d4ba 100644 --- a/app/controllers/post_actions_controller.rb +++ b/app/controllers/post_actions_controller.rb @@ -9,16 +9,17 @@ class PostActionsController < ApplicationController def create raise Discourse::NotFound if @post.blank? - creator = PostActionCreator.new( - current_user, - @post, - @post_action_type_id, - is_warning: params[:is_warning], - message: params[:message], - take_action: params[:take_action] == 'true', - flag_topic: params[:flag_topic] == 'true', - queue_for_review: params[:queue_for_review] == 'true' - ) + creator = + PostActionCreator.new( + current_user, + @post, + @post_action_type_id, + is_warning: params[:is_warning], + message: params[:message], + take_action: params[:take_action] == "true", + flag_topic: params[:flag_topic] == "true", + queue_for_review: params[:queue_for_review] == "true", + ) result = creator.perform if result.failed? @@ -29,19 +30,20 @@ class PostActionsController < ApplicationController if @post_action_type_id == PostActionType.types[:like] limiter = result.post_action.post_action_rate_limiter - response.headers['Discourse-Actions-Remaining'] = limiter.remaining.to_s - response.headers['Discourse-Actions-Max'] = limiter.max.to_s + response.headers["Discourse-Actions-Remaining"] = limiter.remaining.to_s + response.headers["Discourse-Actions-Max"] = limiter.max.to_s end render_post_json(@post, add_raw: false) end end def destroy - result = PostActionDestroyer.new( - current_user, - Post.find_by(id: params[:id].to_i), - @post_action_type_id - ).perform + result = + PostActionDestroyer.new( + current_user, + Post.find_by(id: params[:id].to_i), + @post_action_type_id, + ).perform if result.failed? render_json_error(result) @@ -58,15 +60,16 @@ class PostActionsController < ApplicationController flag_topic = params[:flag_topic] flag_topic = flag_topic && (flag_topic == true || flag_topic == "true") - post_id = if flag_topic - begin - Topic.find(params[:id]).posts.first.id - rescue - raise Discourse::NotFound + post_id = + if flag_topic + begin + Topic.find(params[:id]).posts.first.id + rescue StandardError + raise Discourse::NotFound + end + else + params[:id] end - else - params[:id] - end finder = Post.where(id: post_id) diff --git a/app/controllers/post_readers_controller.rb b/app/controllers/post_readers_controller.rb index bc9c3a197b..a6ff0a1f5e 100644 --- a/app/controllers/post_readers_controller.rb +++ b/app/controllers/post_readers_controller.rb @@ -7,23 +7,30 @@ class PostReadersController < ApplicationController post = Post.includes(topic: %i[topic_allowed_groups topic_allowed_users]).find(params[:id]) ensure_can_see_readers!(post) - readers = User - .real - .where(staged: false) - .where.not(id: post.user_id) - .joins(:topic_users) - .where.not(topic_users: { last_read_post_number: nil }) - .where('topic_users.topic_id = ? AND topic_users.last_read_post_number >= ?', post.topic_id, post.post_number) + readers = + User + .real + .where(staged: false) + .where.not(id: post.user_id) + .joins(:topic_users) + .where.not(topic_users: { last_read_post_number: nil }) + .where( + "topic_users.topic_id = ? AND topic_users.last_read_post_number >= ?", + post.topic_id, + post.post_number, + ) - readers = readers.where('admin OR moderator') if post.whisper? + readers = readers.where("admin OR moderator") if post.whisper? - readers = readers.map do |r| - { - id: r.id, avatar_template: r.avatar_template, - username: r.username, - username_lower: r.username_lower - } - end + readers = + readers.map do |r| + { + id: r.id, + avatar_template: r.avatar_template, + username: r.username, + username_lower: r.username_lower, + } + end render_json_dump(post_readers: readers) end @@ -31,10 +38,17 @@ class PostReadersController < ApplicationController private def ensure_can_see_readers!(post) - show_readers = GroupUser - .where(user: current_user) - .joins(:group) - .where(groups: { id: post.topic.topic_allowed_groups.map(&:group_id), publish_read_state: true }).exists? + show_readers = + GroupUser + .where(user: current_user) + .joins(:group) + .where( + groups: { + id: post.topic.topic_allowed_groups.map(&:group_id), + publish_read_state: true, + }, + ) + .exists? raise Discourse::InvalidAccess unless show_readers end diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index c404cb6969..b5c9ad2e0e 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -5,31 +5,27 @@ class PostsController < ApplicationController # see https://github.com/rails/rails/issues/44867 self._flash_types -= [:notice] - requires_login except: [ - :show, - :replies, - :by_number, - :by_date, - :short_link, - :reply_history, - :reply_ids, - :revisions, - :latest_revision, - :expand_embed, - :markdown_id, - :markdown_num, - :cooked, - :latest, - :user_posts_feed - ] + requires_login except: %i[ + show + replies + by_number + by_date + short_link + reply_history + reply_ids + revisions + latest_revision + expand_embed + markdown_id + markdown_num + cooked + latest + user_posts_feed + ] - skip_before_action :preload_json, :check_xhr, only: [ - :markdown_id, - :markdown_num, - :short_link, - :latest, - :user_posts_feed - ] + skip_before_action :preload_json, + :check_xhr, + only: %i[markdown_id markdown_num short_link latest user_posts_feed] MARKDOWN_TOPIC_PAGE_SIZE ||= 100 @@ -42,13 +38,15 @@ class PostsController < ApplicationController post_revision = find_post_revision_from_topic_id render plain: post_revision.modifications[:raw].last elsif params[:post_number].present? - markdown Post.find_by(topic_id: params[:topic_id].to_i, post_number: params[:post_number].to_i) + markdown Post.find_by( + topic_id: params[:topic_id].to_i, + post_number: params[:post_number].to_i, + ) else opts = params.slice(:page) opts[:limit] = MARKDOWN_TOPIC_PAGE_SIZE topic_view = TopicView.new(params[:topic_id], current_user, opts) - content = topic_view.posts.map do |p| - <<~MD + content = topic_view.posts.map { |p| <<~MD } #{p.user.username} | #{p.updated_at} | ##{p.post_number} #{p.raw} @@ -56,7 +54,6 @@ class PostsController < ApplicationController ------------------------- MD - end render plain: content.join end end @@ -68,26 +65,30 @@ class PostsController < ApplicationController if params[:id] == "private_posts" raise Discourse::NotFound if current_user.nil? - posts = Post.private_posts - .order(created_at: :desc) - .where('posts.id <= ?', last_post_id) - .where('posts.id > ?', last_post_id - 50) - .includes(topic: :category) - .includes(user: [:primary_group, :flair_group]) - .includes(:reply_to_user) - .limit(50) + posts = + Post + .private_posts + .order(created_at: :desc) + .where("posts.id <= ?", last_post_id) + .where("posts.id > ?", last_post_id - 50) + .includes(topic: :category) + .includes(user: %i[primary_group flair_group]) + .includes(:reply_to_user) + .limit(50) rss_description = I18n.t("rss_description.private_posts") else - posts = Post.public_posts - .visible - .where(post_type: Post.types[:regular]) - .order(created_at: :desc) - .where('posts.id <= ?', last_post_id) - .where('posts.id > ?', last_post_id - 50) - .includes(topic: :category) - .includes(user: [:primary_group, :flair_group]) - .includes(:reply_to_user) - .limit(50) + posts = + Post + .public_posts + .visible + .where(post_type: Post.types[:regular]) + .order(created_at: :desc) + .where("posts.id <= ?", last_post_id) + .where("posts.id > ?", last_post_id - 50) + .includes(topic: :category) + .includes(user: %i[primary_group flair_group]) + .includes(:reply_to_user) + .limit(50) rss_description = I18n.t("rss_description.posts") @use_canonical = true end @@ -103,17 +104,20 @@ class PostsController < ApplicationController @title = "#{SiteSetting.title} - #{rss_description}" @link = Discourse.base_url @description = rss_description - render 'posts/latest', formats: [:rss] + render "posts/latest", formats: [:rss] end format.json do - render_json_dump(serialize_data(posts, - PostSerializer, - scope: guardian, - root: params[:id], - add_raw: true, - add_title: true, - all_post_actions: counts) - ) + render_json_dump( + serialize_data( + posts, + PostSerializer, + scope: guardian, + root: params[:id], + add_raw: true, + add_title: true, + all_post_actions: counts, + ), + ) end end end @@ -123,35 +127,33 @@ class PostsController < ApplicationController user = fetch_user_from_params raise Discourse::NotFound unless guardian.can_see_profile?(user) - posts = Post.public_posts - .visible - .where(user_id: user.id) - .where(post_type: Post.types[:regular]) - .order(created_at: :desc) - .includes(:user) - .includes(topic: :category) - .limit(50) + posts = + Post + .public_posts + .visible + .where(user_id: user.id) + .where(post_type: Post.types[:regular]) + .order(created_at: :desc) + .includes(:user) + .includes(topic: :category) + .limit(50) posts = posts.reject { |post| !guardian.can_see?(post) || post.topic.blank? } respond_to do |format| format.rss do @posts = posts - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.user_posts", username: user.username)}" + @title = + "#{SiteSetting.title} - #{I18n.t("rss_description.user_posts", username: user.username)}" @link = "#{user.full_url}/activity" @description = I18n.t("rss_description.user_posts", username: user.username) - render 'posts/latest', formats: [:rss] + render "posts/latest", formats: [:rss] end format.json do - render_json_dump(serialize_data(posts, - PostSerializer, - scope: guardian, - add_excerpt: true) - ) + render_json_dump(serialize_data(posts, PostSerializer, scope: guardian, add_excerpt: true)) end end - end def cooked @@ -173,7 +175,7 @@ class PostsController < ApplicationController # Stuff the user in the request object, because that's what IncomingLink wants if params[:user_id] user = User.find_by(id: params[:user_id].to_i) - request['u'] = user.username_lower if user + request["u"] = user.username_lower if user end guardian.ensure_can_see!(post) @@ -188,13 +190,14 @@ class PostsController < ApplicationController manager = NewPostManager.new(current_user, @manager_params) if is_api? - memoized_payload = DistributedMemoizer.memoize(signature_for(@manager_params), 120) do - result = manager.perform - MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false)) - end + memoized_payload = + DistributedMemoizer.memoize(signature_for(@manager_params), 120) do + result = manager.perform + MultiJson.dump(serialize_data(result, NewPostResultSerializer, root: false)) + end parsed_payload = JSON.parse(memoized_payload) - backwards_compatible_json(parsed_payload, parsed_payload['success']) + backwards_compatible_json(parsed_payload, parsed_payload["success"]) else result = manager.perform json = serialize_data(result, NewPostResultSerializer, root: false) @@ -213,27 +216,20 @@ class PostsController < ApplicationController post.image_sizes = params[:image_sizes] if params[:image_sizes].present? - if !guardian.public_send("can_edit?", post) && - post.user_id == current_user.id && - post.edit_time_limit_expired?(current_user) - - return render_json_error(I18n.t('too_late_to_edit')) + if !guardian.public_send("can_edit?", post) && post.user_id == current_user.id && + post.edit_time_limit_expired?(current_user) + return render_json_error(I18n.t("too_late_to_edit")) end guardian.ensure_can_edit!(post) - changes = { - raw: params[:post][:raw], - edit_reason: params[:post][:edit_reason] - } + changes = { raw: params[:post][:raw], edit_reason: params[:post][:edit_reason] } - Post.plugin_permitted_update_params.keys.each do |param| - changes[param] = params[:post][param] - end + Post.plugin_permitted_update_params.keys.each { |param| changes[param] = params[:post][param] } raw_old = params[:post][:raw_old] if raw_old.present? && raw_old != post.raw - return render_json_error(I18n.t('edit_conflict'), status: 409) + return render_json_error(I18n.t("edit_conflict"), status: 409) end # to stay consistent with the create api, we allow for title & category changes here @@ -246,7 +242,7 @@ class PostsController < ApplicationController if category || (changes[:category_id].to_i == 0) guardian.ensure_can_move_topic_to_category!(category) else - return render_json_error(I18n.t('category.errors.not_found')) + return render_json_error(I18n.t("category.errors.not_found")) end end end @@ -273,7 +269,11 @@ class PostsController < ApplicationController result = { post: post_serializer.as_json } if revisor.category_changed.present? - result[:category] = BasicCategorySerializer.new(revisor.category_changed, scope: guardian, root: false).as_json + result[:category] = BasicCategorySerializer.new( + revisor.category_changed, + scope: guardian, + root: false, + ).as_json end render_json_dump(result) @@ -303,11 +303,7 @@ class PostsController < ApplicationController user_custom_fields = User.custom_fields_for_ids(reply_history.pluck(:user_id), added_fields) end - render_serialized( - reply_history, - PostSerializer, - user_custom_fields: user_custom_fields - ) + render_serialized(reply_history, PostSerializer, user_custom_fields: user_custom_fields) end def reply_ids @@ -335,15 +331,25 @@ class PostsController < ApplicationController end unless guardian.can_moderate_topic?(post.topic) - RateLimiter.new(current_user, "delete_post_per_min", SiteSetting.max_post_deletions_per_minute, 1.minute).performed! - RateLimiter.new(current_user, "delete_post_per_day", SiteSetting.max_post_deletions_per_day, 1.day).performed! + RateLimiter.new( + current_user, + "delete_post_per_min", + SiteSetting.max_post_deletions_per_minute, + 1.minute, + ).performed! + RateLimiter.new( + current_user, + "delete_post_per_day", + SiteSetting.max_post_deletions_per_day, + 1.day, + ).performed! end PostDestroyer.new( current_user, post, context: params[:context], - force_destroy: force_destroy + force_destroy: force_destroy, ).destroy render body: nil @@ -351,8 +357,8 @@ class PostsController < ApplicationController def expand_embed render json: { cooked: TopicEmbed.expanded_for(find_post_from_params) } - rescue - render_json_error I18n.t('errors.embed.load_from_remote') + rescue StandardError + render_json_error I18n.t("errors.embed.load_from_remote") end def recover @@ -360,8 +366,18 @@ class PostsController < ApplicationController guardian.ensure_can_recover_post!(post) unless guardian.can_moderate_topic?(post.topic) - RateLimiter.new(current_user, "delete_post_per_min", SiteSetting.max_post_deletions_per_minute, 1.minute).performed! - RateLimiter.new(current_user, "delete_post_per_day", SiteSetting.max_post_deletions_per_day, 1.day).performed! + RateLimiter.new( + current_user, + "delete_post_per_min", + SiteSetting.max_post_deletions_per_minute, + 1.minute, + ).performed! + RateLimiter.new( + current_user, + "delete_post_per_day", + SiteSetting.max_post_deletions_per_day, + 1.day, + ).performed! end destroyer = PostDestroyer.new(current_user, post) @@ -383,7 +399,11 @@ class PostsController < ApplicationController Post.transaction do posts.each_with_index do |p, i| - PostDestroyer.new(current_user, p, defer_flags: !(agree_with_first_reply_flag && i == 0)).destroy + PostDestroyer.new( + current_user, + p, + defer_flags: !(agree_with_first_reply_flag && i == 0), + ).destroy end end @@ -418,7 +438,8 @@ class PostsController < ApplicationController raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions? post_revision = find_post_revision_from_params - post_revision_serializer = PostRevisionSerializer.new(post_revision, scope: guardian, root: false) + post_revision_serializer = + PostRevisionSerializer.new(post_revision, scope: guardian, root: false) render_json_dump(post_revision_serializer) end @@ -427,7 +448,8 @@ class PostsController < ApplicationController raise Discourse::NotFound if post.hidden && !guardian.can_view_hidden_post_revisions? post_revision = find_latest_post_revision_from_params - post_revision_serializer = PostRevisionSerializer.new(post_revision, scope: guardian, root: false) + post_revision_serializer = + PostRevisionSerializer.new(post_revision, scope: guardian, root: false) render_json_dump(post_revision_serializer) end @@ -473,17 +495,27 @@ class PostsController < ApplicationController post_revision.post = post guardian.ensure_can_see!(post_revision) guardian.ensure_can_edit!(post) - return render_json_error(I18n.t('revert_version_same')) if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? && post_revision.modifications["category_id"].blank? + if post_revision.modifications["raw"].blank? && post_revision.modifications["title"].blank? && + post_revision.modifications["category_id"].blank? + return render_json_error(I18n.t("revert_version_same")) + end topic = Topic.with_deleted.find(post.topic_id) changes = {} - changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications["raw"].present? && post_revision.modifications["raw"][0] != post.raw + changes[:raw] = post_revision.modifications["raw"][0] if post_revision.modifications[ + "raw" + ].present? && post_revision.modifications["raw"][0] != post.raw if post.is_first_post? - changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications["title"].present? && post_revision.modifications["title"][0] != topic.title - changes[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? && post_revision.modifications["category_id"][0] != topic.category.id + changes[:title] = post_revision.modifications["title"][0] if post_revision.modifications[ + "title" + ].present? && post_revision.modifications["title"][0] != topic.title + changes[:category_id] = post_revision.modifications["category_id"][ + 0 + ] if post_revision.modifications["category_id"].present? && + post_revision.modifications["category_id"][0] != topic.category.id end - return render_json_error(I18n.t('revert_version_same')) unless changes.length > 0 + return render_json_error(I18n.t("revert_version_same")) unless changes.length > 0 changes[:edit_reason] = "reverted to version ##{post_revision.number.to_i - 1}" revisor = PostRevisor.new(post, topic) @@ -500,8 +532,14 @@ class PostsController < ApplicationController result = { post: post_serializer.as_json } if post.is_first_post? - result[:topic] = BasicTopicSerializer.new(topic, scope: guardian, root: false).as_json if post_revision.modifications["title"].present? - result[:category_id] = post_revision.modifications["category_id"][0] if post_revision.modifications["category_id"].present? + result[:topic] = BasicTopicSerializer.new( + topic, + scope: guardian, + root: false, + ).as_json if post_revision.modifications["title"].present? + result[:category_id] = post_revision.modifications["category_id"][ + 0 + ] if post_revision.modifications["category_id"].present? end render_json_dump(result) @@ -524,7 +562,7 @@ class PostsController < ApplicationController post.custom_fields[Post::NOTICE] = { type: Post.notices[:custom], raw: params[:notice], - cooked: PrettyText.cook(params[:notice], features: { onebox: false }) + cooked: PrettyText.cook(params[:notice], features: { onebox: false }), } else post.custom_fields.delete(Post::NOTICE) @@ -535,7 +573,7 @@ class PostsController < ApplicationController StaffActionLogger.new(current_user).log_post_staff_note( post, old_value: old_notice&.[]("raw"), - new_value: params[:notice] + new_value: params[:notice], ) render body: nil @@ -544,14 +582,16 @@ class PostsController < ApplicationController def destroy_bookmark params.require(:post_id) - bookmark_id = Bookmark.where( - bookmarkable_id: params[:post_id], - bookmarkable_type: "Post", - user_id: current_user.id - ).pluck_first(:id) + bookmark_id = + Bookmark.where( + bookmarkable_id: params[:post_id], + bookmarkable_type: "Post", + user_id: current_user.id, + ).pluck_first(:id) destroyed_bookmark = BookmarkManager.new(current_user).destroy(bookmark_id) - render json: success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) + render json: + success_json.merge(BookmarkManager.bookmark_metadata(destroyed_bookmark, current_user)) end def wiki @@ -596,8 +636,9 @@ class PostsController < ApplicationController def flagged_posts Discourse.deprecate( - 'PostsController#flagged_posts is deprecated. Please use /review instead.', - since: '2.8.0.beta4', drop_from: '2.9' + "PostsController#flagged_posts is deprecated. Please use /review instead.", + since: "2.8.0.beta4", + drop_from: "2.9", ) params.permit(:offset, :limit) @@ -607,10 +648,14 @@ class PostsController < ApplicationController offset = [params[:offset].to_i, 0].max limit = [(params[:limit] || 60).to_i, 100].min - posts = user_posts(guardian, user.id, offset: offset, limit: limit) - .where(id: PostAction.where(post_action_type_id: PostActionType.notify_flag_type_ids) - .where(disagreed_at: nil) - .select(:post_id)) + posts = + user_posts(guardian, user.id, offset: offset, limit: limit).where( + id: + PostAction + .where(post_action_type_id: PostActionType.notify_flag_type_ids) + .where(disagreed_at: nil) + .select(:post_id), + ) render_serialized(posts, AdminUserActionSerializer) end @@ -633,7 +678,11 @@ class PostsController < ApplicationController user = fetch_user_from_params raise Discourse::NotFound unless guardian.can_edit_user?(user) - render_serialized(user.pending_posts.order(created_at: :desc), PendingPostSerializer, root: :pending_posts) + render_serialized( + user.pending_posts.order(created_at: :desc), + PendingPostSerializer, + root: :pending_posts, + ) end protected @@ -692,7 +741,8 @@ class PostsController < ApplicationController end def find_post_revision_from_topic_id - post = Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i) + post = + Post.find_by(topic_id: params[:topic_id].to_i, post_number: (params[:post_number] || 1).to_i) raise Discourse::NotFound unless guardian.can_see?(post) revision = params[:revision].to_i @@ -711,26 +761,26 @@ class PostsController < ApplicationController def user_posts(guardian, user_id, opts) # Topic.unscoped is necessary to remove the default deleted_at: nil scope - posts = Topic.unscoped do - Post.includes(:user, :topic, :deleted_by, :user_actions) - .where(user_id: user_id) - .with_deleted - .order(created_at: :desc) - end + posts = + Topic.unscoped do + Post + .includes(:user, :topic, :deleted_by, :user_actions) + .where(user_id: user_id) + .with_deleted + .order(created_at: :desc) + end if guardian.user.moderator? - # Awful hack, but you can't seem to remove the `default_scope` when joining # So instead I grab the topics separately topic_ids = posts.dup.pluck(:topic_id) - topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message') + topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: "private_message") topics = topics.secured(guardian) posts = posts.where(topic_id: topics.pluck(:id)) end - posts.offset(opts[:offset]) - .limit(opts[:limit]) + posts.offset(opts[:offset]).limit(opts[:limit]) end def create_params @@ -747,18 +797,18 @@ class PostsController < ApplicationController :typing_duration_msecs, :composer_open_duration_msecs, :visible, - :draft_key + :draft_key, ] Post.plugin_permitted_create_params.each do |key, value| if value[:plugin].enabled? - permitted << case value[:type] - when :string - key.to_sym - when :array - { key => [] } - when :hash - { key => {} } + permitted << case value[:type] + when :string + key.to_sym + when :array + { key => [] } + when :hash + { key => {} } end end end @@ -785,11 +835,14 @@ class PostsController < ApplicationController permitted << :external_id end - result = params.permit(*permitted).tap do |allowed| - allowed[:image_sizes] = params[:image_sizes] - # TODO this does not feel right, we should name what meta_data is allowed - allowed[:meta_data] = params[:meta_data] - end + result = + params + .permit(*permitted) + .tap do |allowed| + allowed[:image_sizes] = params[:image_sizes] + # TODO this does not feel right, we should name what meta_data is allowed + allowed[:meta_data] = params[:meta_data] + end # Staff are allowed to pass `is_warning` if current_user.staff? @@ -804,14 +857,20 @@ class PostsController < ApplicationController result[:no_bump] = true end - if params[:shared_draft] == 'true' + if params[:shared_draft] == "true" raise Discourse::InvalidParameters.new(:shared_draft) unless guardian.can_create_shared_draft? result[:shared_draft] = true end if params[:whisper] == "true" - raise Discourse::InvalidAccess.new("invalid_whisper_access", nil, custom_message: "invalid_whisper_access") unless guardian.can_create_whisper? + unless guardian.can_create_whisper? + raise Discourse::InvalidAccess.new( + "invalid_whisper_access", + nil, + custom_message: "invalid_whisper_access", + ) + end result[:post_type] = Post.types[:whisper] end @@ -827,14 +886,19 @@ class PostsController < ApplicationController result[:referrer] = request.env["HTTP_REFERER"] if recipients = result[:target_usernames] - Discourse.deprecate("`target_usernames` is deprecated, use `target_recipients` instead.", output_in_test: true, drop_from: '2.9.0') + Discourse.deprecate( + "`target_usernames` is deprecated, use `target_recipients` instead.", + output_in_test: true, + drop_from: "2.9.0", + ) else recipients = result[:target_recipients] end if recipients recipients = recipients.split(",").map(&:downcase) - groups = Group.messageable(current_user).where('lower(name) in (?)', recipients).pluck('lower(name)') + groups = + Group.messageable(current_user).where("lower(name) in (?)", recipients).pluck("lower(name)") recipients -= groups emails = recipients.select { |user| user.match(/@/) } recipients -= emails @@ -848,13 +912,14 @@ class PostsController < ApplicationController end def signature_for(args) - +"post##" << Digest::SHA1.hexdigest(args - .to_h - .to_a - .concat([["user", current_user.id]]) - .sort { |x, y| x[0] <=> y[0] }.join do |x, y| - "#{x}:#{y}" - end) + +"post##" << Digest::SHA1.hexdigest( + args + .to_h + .to_a + .concat([["user", current_user.id]]) + .sort { |x, y| x[0] <=> y[0] } + .join { |x, y| "#{x}:#{y}" }, + ) end def display_post(post) @@ -873,11 +938,13 @@ class PostsController < ApplicationController end def find_post_from_params_by_date - by_date_finder = TopicView.new(params[:topic_id], current_user) - .filtered_posts - .where("created_at >= ?", Time.zone.parse(params[:date])) - .order("created_at ASC") - .limit(1) + by_date_finder = + TopicView + .new(params[:topic_id], current_user) + .filtered_posts + .where("created_at >= ?", Time.zone.parse(params[:date])) + .order("created_at ASC") + .limit(1) find_post_using(by_date_finder) end @@ -892,15 +959,14 @@ class PostsController < ApplicationController post.topic = Topic.with_deleted.find_by(id: post.topic_id) if !post.topic || - ( - (post.deleted_at.present? || post.topic.deleted_at.present?) && - !guardian.can_moderate_topic?(post.topic) - ) + ( + (post.deleted_at.present? || post.topic.deleted_at.present?) && + !guardian.can_moderate_topic?(post.topic) + ) raise Discourse::NotFound end guardian.ensure_can_see!(post) post end - end diff --git a/app/controllers/presence_controller.rb b/app/controllers/presence_controller.rb index fe26e68fab..ad68f801c9 100644 --- a/app/controllers/presence_controller.rb +++ b/app/controllers/presence_controller.rb @@ -9,18 +9,23 @@ class PresenceController < ApplicationController def get names = params.require(:channels) - raise Discourse::InvalidParameters.new(:channels) if !(names.is_a?(Array) && names.all? { |n| n.is_a? String }) + if !(names.is_a?(Array) && names.all? { |n| n.is_a? String }) + raise Discourse::InvalidParameters.new(:channels) + end names.uniq! - raise Discourse::InvalidParameters.new("Too many channels") if names.length > MAX_CHANNELS_PER_REQUEST - - user_group_ids = if current_user - GroupUser.where(user_id: current_user.id).pluck("group_id") - else - [] + if names.length > MAX_CHANNELS_PER_REQUEST + raise Discourse::InvalidParameters.new("Too many channels") end + user_group_ids = + if current_user + GroupUser.where(user_id: current_user.id).pluck("group_id") + else + [] + end + result = {} names.each do |name| channel = PresenceChannel.new(name) @@ -38,19 +43,23 @@ class PresenceController < ApplicationController def update client_id = params[:client_id] - raise Discourse::InvalidParameters.new(:client_id) if !client_id.is_a?(String) || client_id.blank? + if !client_id.is_a?(String) || client_id.blank? + raise Discourse::InvalidParameters.new(:client_id) + end # JS client is designed to throttle to one request per second # When no changes are being made, it makes one request every 30 seconds RateLimiter.new(nil, "update-presence-#{current_user.id}", 20, 10.seconds).performed! present_channels = params[:present_channels] - if present_channels && !(present_channels.is_a?(Array) && present_channels.all? { |c| c.is_a? String }) + if present_channels && + !(present_channels.is_a?(Array) && present_channels.all? { |c| c.is_a? String }) raise Discourse::InvalidParameters.new(:present_channels) end leave_channels = params[:leave_channels] - if leave_channels && !(leave_channels.is_a?(Array) && leave_channels.all? { |c| c.is_a? String }) + if leave_channels && + !(leave_channels.is_a?(Array) && leave_channels.all? { |c| c.is_a? String }) raise Discourse::InvalidParameters.new(:leave_channels) end diff --git a/app/controllers/published_pages_controller.rb b/app/controllers/published_pages_controller.rb index abf27ccda5..328e2ba408 100644 --- a/app/controllers/published_pages_controller.rb +++ b/app/controllers/published_pages_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class PublishedPagesController < ApplicationController - skip_before_action :preload_json skip_before_action :check_xhr, :verify_authenticity_token, only: [:show] before_action :ensure_publish_enabled @@ -19,12 +18,14 @@ class PublishedPagesController < ApplicationController begin guardian.ensure_can_see!(pp.topic) rescue Discourse::InvalidAccess => e - return rescue_discourse_actions( - :invalid_access, - 403, - include_ember: false, - custom_message: e.custom_message, - group: e.group + return( + rescue_discourse_actions( + :invalid_access, + 403, + include_ember: false, + custom_message: e.custom_message, + group: e.group, + ) ) end end @@ -37,18 +38,19 @@ class PublishedPagesController < ApplicationController TopicViewItem.add(pp.topic.id, request.remote_ip, current_user ? current_user.id : nil) - @body_classes = Set.new([ - 'published-page', - params[:slug], - "topic-#{@topic.id}", - @topic.tags.pluck(:name) - ].flatten.compact) + @body_classes = + Set.new( + [ + "published-page", + params[:slug], + "topic-#{@topic.id}", + @topic.tags.pluck(:name), + ].flatten.compact, + ) - if @topic.category - @body_classes << @topic.category.slug - end + @body_classes << @topic.category.slug if @topic.category - render layout: 'publish' + render layout: "publish" end def details @@ -60,12 +62,13 @@ class PublishedPagesController < ApplicationController def upsert pp_params = params.require(:published_page) - result, pp = PublishedPage.publish!( - current_user, - fetch_topic, - pp_params[:slug].strip, - pp_params.permit(:public) - ) + result, pp = + PublishedPage.publish!( + current_user, + fetch_topic, + pp_params[:slug].strip, + pp_params.permit(:public), + ) json_result(pp, serializer: PublishedPageSerializer) { result } end @@ -85,7 +88,7 @@ class PublishedPagesController < ApplicationController end end -private + private def fetch_topic topic = Topic.find_by(id: params[:topic_id]) @@ -94,18 +97,13 @@ private end def ensure_publish_enabled - if !SiteSetting.enable_page_publishing? || SiteSetting.secure_uploads - raise Discourse::NotFound - end + raise Discourse::NotFound if !SiteSetting.enable_page_publishing? || SiteSetting.secure_uploads end def enforce_login_required! - if SiteSetting.login_required? && - !current_user && - !SiteSetting.show_published_pages_login_required? && - redirect_to_login + if SiteSetting.login_required? && !current_user && + !SiteSetting.show_published_pages_login_required? && redirect_to_login true end end - end diff --git a/app/controllers/push_notification_controller.rb b/app/controllers/push_notification_controller.rb index 0b0a75b39d..a1cd3b0765 100644 --- a/app/controllers/push_notification_controller.rb +++ b/app/controllers/push_notification_controller.rb @@ -18,6 +18,6 @@ class PushNotificationController < ApplicationController private def push_params - params.require(:subscription).permit(:endpoint, keys: [:p256dh, :auth]) + params.require(:subscription).permit(:endpoint, keys: %i[p256dh auth]) end end diff --git a/app/controllers/qunit_controller.rb b/app/controllers/qunit_controller.rb index 4ee3392d50..31c411688d 100644 --- a/app/controllers/qunit_controller.rb +++ b/app/controllers/qunit_controller.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class QunitController < ApplicationController - skip_before_action *%i{ - check_xhr - preload_json - redirect_to_login_if_required - } + skip_before_action *%i[check_xhr preload_json redirect_to_login_if_required] layout false def theme @@ -25,16 +21,20 @@ class QunitController < ApplicationController end if param_key && theme.blank? - return render plain: "Can't find theme with #{param_key} #{get_param(param_key).inspect}", status: :not_found + return( + render plain: "Can't find theme with #{param_key} #{get_param(param_key).inspect}", + status: :not_found + ) end if !param_key - @suggested_themes = Theme - .where( - id: ThemeField.where(target_id: Theme.targets[:tests_js]).distinct.pluck(:theme_id) - ) - .order(updated_at: :desc) - .pluck(:id, :name) + @suggested_themes = + Theme + .where( + id: ThemeField.where(target_id: Theme.targets[:tests_js]).distinct.pluck(:theme_id), + ) + .order(updated_at: :desc) + .pluck(:id, :name) return end diff --git a/app/controllers/reviewable_claimed_topics_controller.rb b/app/controllers/reviewable_claimed_topics_controller.rb index 2a6033118b..8d5cc4a25d 100644 --- a/app/controllers/reviewable_claimed_topics_controller.rb +++ b/app/controllers/reviewable_claimed_topics_controller.rb @@ -10,12 +10,10 @@ class ReviewableClaimedTopicsController < ApplicationController begin ReviewableClaimedTopic.create!(user_id: current_user.id, topic_id: topic.id) rescue ActiveRecord::RecordInvalid - return render_json_error(I18n.t('reviewables.conflict'), status: 409) + return render_json_error(I18n.t("reviewables.conflict"), status: 409) end - topic.reviewables.find_each do |reviewable| - reviewable.log_history(:claimed, current_user) - end + topic.reviewables.find_each { |reviewable| reviewable.log_history(:claimed, current_user) } notify_users(topic, current_user) render json: success_json @@ -27,9 +25,7 @@ class ReviewableClaimedTopicsController < ApplicationController guardian.ensure_can_claim_reviewable_topic!(topic) ReviewableClaimedTopic.where(topic_id: topic.id).delete_all - topic.reviewables.find_each do |reviewable| - reviewable.log_history(:unclaimed, current_user) - end + topic.reviewables.find_each { |reviewable| reviewable.log_history(:unclaimed, current_user) } notify_users(topic, nil) render json: success_json @@ -40,7 +36,8 @@ class ReviewableClaimedTopicsController < ApplicationController def notify_users(topic, claimed_by) group_ids = Set.new([Group::AUTO_GROUPS[:staff]]) - if SiteSetting.enable_category_group_moderation? && group_id = topic.category&.reviewable_by_group_id.presence + if SiteSetting.enable_category_group_moderation? && + group_id = topic.category&.reviewable_by_group_id.presence group_ids.add(group_id) end diff --git a/app/controllers/reviewables_controller.rb b/app/controllers/reviewables_controller.rb index 4d3ebf143a..61af8f0e3e 100644 --- a/app/controllers/reviewables_controller.rb +++ b/app/controllers/reviewables_controller.rb @@ -5,7 +5,7 @@ class ReviewablesController < ApplicationController PER_PAGE = 10 - before_action :version_required, only: [:update, :perform] + before_action :version_required, only: %i[update perform] before_action :ensure_can_see, except: [:destroy] def index @@ -15,20 +15,21 @@ class ReviewablesController < ApplicationController raise Discourse::InvalidParameters.new(:type) unless Reviewable.valid_type?(params[:type]) end - status = (params[:status] || 'pending').to_sym + status = (params[:status] || "pending").to_sym raise Discourse::InvalidParameters.new(:status) unless allowed_statuses.include?(status) topic_id = params[:topic_id] ? params[:topic_id].to_i : nil category_id = params[:category_id] ? params[:category_id].to_i : nil custom_keys = Reviewable.custom_filters.map(&:first) - additional_filters = JSON.parse(params.fetch(:additional_filters, {}), symbolize_names: true).slice(*custom_keys) + additional_filters = + JSON.parse(params.fetch(:additional_filters, {}), symbolize_names: true).slice(*custom_keys) filters = { ids: params[:ids], status: status, category_id: category_id, topic_id: topic_id, - additional_filters: additional_filters.reject { |_, v| v.blank? } + additional_filters: additional_filters.reject { |_, v| v.blank? }, } %i[priority username reviewed_by from_date to_date type sort_order].each do |filter_key| @@ -36,7 +37,8 @@ class ReviewablesController < ApplicationController end total_rows = Reviewable.list_for(current_user, **filters).count - reviewables = Reviewable.list_for(current_user, **filters.merge(limit: PER_PAGE, offset: offset)).to_a + reviewables = + Reviewable.list_for(current_user, **filters.merge(limit: PER_PAGE, offset: offset)).to_a claimed_topics = ReviewableClaimedTopic.claimed_hash(reviewables.map { |r| r.topic_id }.uniq) @@ -44,23 +46,25 @@ class ReviewablesController < ApplicationController # is mutated by the serializer and contains the side loaded records which must be merged in the end. hash = {} json = { - reviewables: reviewables.map! do |r| - result = r.serializer.new( - r, - root: nil, - hash: hash, - scope: guardian, - claimed_topics: claimed_topics - ).as_json - hash[:bundled_actions].uniq! - (hash['actions'] || []).uniq! - result - end, - meta: filters.merge( - total_rows_reviewables: total_rows, types: meta_types, reviewable_types: Reviewable.types, - reviewable_count: current_user.reviewable_count, - unseen_reviewable_count: Reviewable.unseen_reviewable_count(current_user) - ) + reviewables: + reviewables.map! do |r| + result = + r + .serializer + .new(r, root: nil, hash: hash, scope: guardian, claimed_topics: claimed_topics) + .as_json + hash[:bundled_actions].uniq! + (hash["actions"] || []).uniq! + result + end, + meta: + filters.merge( + total_rows_reviewables: total_rows, + types: meta_types, + reviewable_types: Reviewable.types, + reviewable_count: current_user.reviewable_count, + unseen_reviewable_count: Reviewable.unseen_reviewable_count(current_user), + ), } if (offset + PER_PAGE) < total_rows json[:meta][:load_more_reviewables] = review_path(filters.merge(offset: offset + PER_PAGE)) @@ -72,10 +76,11 @@ class ReviewablesController < ApplicationController def user_menu_list json = { - reviewables: Reviewable.basic_serializers_for_list( - Reviewable.user_menu_list_for(current_user), - current_user - ).as_json + reviewables: + Reviewable.basic_serializers_for_list( + Reviewable.user_menu_list_for(current_user), + current_user, + ).as_json, } render_json_dump(json, rest_serializer: true) end @@ -108,17 +113,17 @@ class ReviewablesController < ApplicationController meta[:unique_users] = users.size end - topics = Topic.where(id: topic_ids).order('reviewable_score DESC') + topics = Topic.where(id: topic_ids).order("reviewable_score DESC") render_serialized( topics, ReviewableTopicSerializer, - root: 'reviewable_topics', + root: "reviewable_topics", stats: stats, claimed_topics: ReviewableClaimedTopic.claimed_hash(topic_ids), rest_serializer: true, meta: { - types: meta_types - } + types: meta_types, + }, ) end @@ -129,7 +134,7 @@ class ReviewablesController < ApplicationController { reviewable: reviewable, scores: reviewable.explain_score }, ReviewableExplanationSerializer, rest_serializer: true, - root: 'reviewable_explanation' + root: "reviewable_explanation", ) end @@ -141,10 +146,10 @@ class ReviewablesController < ApplicationController reviewable.serializer, rest_serializer: true, claimed_topics: ReviewableClaimedTopic.claimed_hash([reviewable.topic_id]), - root: 'reviewable', + root: "reviewable", meta: { - types: meta_types - } + types: meta_types, + }, ) end @@ -186,7 +191,7 @@ class ReviewablesController < ApplicationController render_json_error(reviewable.errors) end rescue Reviewable::UpdateConflict - render_json_error(I18n.t('reviewables.conflict'), status: 409) + render_json_error(I18n.t("reviewables.conflict"), status: 409) end end @@ -201,23 +206,32 @@ class ReviewablesController < ApplicationController return render_json_error(error) end - args.merge!(reject_reason: params[:reject_reason], send_email: params[:send_email] != "false") if reviewable.type == 'ReviewableUser' - - plugin_params = DiscoursePluginRegistry.reviewable_params.select do |reviewable_param| - reviewable.type == reviewable_param[:type].to_s.classify + if reviewable.type == "ReviewableUser" + args.merge!( + reject_reason: params[:reject_reason], + send_email: params[:send_email] != "false", + ) end + + plugin_params = + DiscoursePluginRegistry.reviewable_params.select do |reviewable_param| + reviewable.type == reviewable_param[:type].to_s.classify + end args.merge!(params.slice(*plugin_params.map { |pp| pp[:param] }).permit!) result = reviewable.perform(current_user, params[:action_id].to_sym, args) rescue Reviewable::InvalidAction => e - if reviewable.type == 'ReviewableUser' && !reviewable.pending? && reviewable.target.blank? - raise Discourse::NotFound.new(e.message, custom_message: "reviewables.already_handled_and_user_not_exist") + if reviewable.type == "ReviewableUser" && !reviewable.pending? && reviewable.target.blank? + raise Discourse::NotFound.new( + e.message, + custom_message: "reviewables.already_handled_and_user_not_exist", + ) else # Consider InvalidAction an InvalidAccess raise Discourse::InvalidAccess.new(e.message) end rescue Reviewable::UpdateConflict - return render_json_error(I18n.t('reviewables.conflict'), status: 409) + return render_json_error(I18n.t("reviewables.conflict"), status: 409) end if result.success? @@ -230,7 +244,7 @@ class ReviewablesController < ApplicationController def settings raise Discourse::InvalidAccess.new unless current_user.admin? - post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order('id') + post_action_types = PostActionType.where(id: PostActionType.flag_types.values).order("id") if request.put? params[:reviewable_priorities].each do |id, priority| @@ -239,7 +253,7 @@ class ReviewablesController < ApplicationController # to calculate it a different way. PostActionType.where(id: id).update_all( reviewable_priority: priority.to_i, - score_bonus: priority.to_f + score_bonus: priority.to_f, ) end end @@ -249,7 +263,7 @@ class ReviewablesController < ApplicationController render_serialized(data, ReviewableSettingsSerializer, rest_serializer: true) end -protected + protected def claim_error?(reviewable) return if SiteSetting.reviewable_claiming == "disabled" || reviewable.topic_id.blank? @@ -257,9 +271,9 @@ protected claimed_by_id = ReviewableClaimedTopic.where(topic_id: reviewable.topic_id).pluck(:user_id)[0] if SiteSetting.reviewable_claiming == "required" && claimed_by_id.blank? - I18n.t('reviewables.must_claim') + I18n.t("reviewables.must_claim") elsif claimed_by_id.present? && claimed_by_id != current_user.id - I18n.t('reviewables.user_claimed') + I18n.t("reviewables.user_claimed") end end @@ -274,18 +288,11 @@ protected end def version_required - if params[:version].blank? - render_json_error(I18n.t('reviewables.missing_version'), status: 422) - end + render_json_error(I18n.t("reviewables.missing_version"), status: 422) if params[:version].blank? end def meta_types - { - created_by: 'user', - target_created_by: 'user', - reviewed_by: 'user', - claimed_by: 'user' - } + { created_by: "user", target_created_by: "user", reviewed_by: "user", claimed_by: "user" } end def ensure_can_see diff --git a/app/controllers/robots_txt_controller.rb b/app/controllers/robots_txt_controller.rb index 706a67be77..d9e4ea2a73 100644 --- a/app/controllers/robots_txt_controller.rb +++ b/app/controllers/robots_txt_controller.rb @@ -7,7 +7,7 @@ class RobotsTxtController < ApplicationController OVERRIDDEN_HEADER = "# This robots.txt file has been customized at /admin/customize/robots\n" # NOTE: order is important! - DISALLOWED_PATHS ||= %w{ + DISALLOWED_PATHS ||= %w[ /admin/ /auth/ /assets/browser-update*.js @@ -16,18 +16,9 @@ class RobotsTxtController < ApplicationController /user-api-key /*?api_key* /*?*api_key* - } + ] - DISALLOWED_WITH_HEADER_PATHS ||= %w{ - /badges - /u/ - /my - /search - /tag/*/l - /g - /t/*/*.rss - /c/*.rss - } + DISALLOWED_WITH_HEADER_PATHS ||= %w[/badges /u/ /my /search /tag/*/l /g /t/*/*.rss /c/*.rss] def index if (overridden = SiteSetting.overridden_robots_txt.dup).present? @@ -37,9 +28,9 @@ class RobotsTxtController < ApplicationController end if SiteSetting.allow_index_in_robots_txt? @robots_info = self.class.fetch_default_robots_info - render :index, content_type: 'text/plain' + render :index, content_type: "text/plain" else - render :no_index, content_type: 'text/plain' + render :no_index, content_type: "text/plain" end end @@ -56,32 +47,37 @@ class RobotsTxtController < ApplicationController def self.fetch_default_robots_info deny_paths_googlebot = DISALLOWED_PATHS.map { |p| Discourse.base_path + p } - deny_paths = deny_paths_googlebot + DISALLOWED_WITH_HEADER_PATHS.map { |p| Discourse.base_path + p } - deny_all = [ "#{Discourse.base_path}/" ] + deny_paths = + deny_paths_googlebot + DISALLOWED_WITH_HEADER_PATHS.map { |p| Discourse.base_path + p } + deny_all = ["#{Discourse.base_path}/"] result = { - header: "# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file", - agents: [] + header: + "# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file", + agents: [], } if SiteSetting.allowed_crawler_user_agents.present? - SiteSetting.allowed_crawler_user_agents.split('|').each do |agent| - paths = agent == "Googlebot" ? deny_paths_googlebot : deny_paths - result[:agents] << { name: agent, disallow: paths } - end - - result[:agents] << { name: '*', disallow: deny_all } - else - - if SiteSetting.blocked_crawler_user_agents.present? - SiteSetting.blocked_crawler_user_agents.split('|').each do |agent| - result[:agents] << { name: agent, disallow: deny_all } + SiteSetting + .allowed_crawler_user_agents + .split("|") + .each do |agent| + paths = agent == "Googlebot" ? deny_paths_googlebot : deny_paths + result[:agents] << { name: agent, disallow: paths } end + + result[:agents] << { name: "*", disallow: deny_all } + else + if SiteSetting.blocked_crawler_user_agents.present? + SiteSetting + .blocked_crawler_user_agents + .split("|") + .each { |agent| result[:agents] << { name: agent, disallow: deny_all } } end - result[:agents] << { name: '*', disallow: deny_paths } + result[:agents] << { name: "*", disallow: deny_paths } - result[:agents] << { name: 'Googlebot', disallow: deny_paths_googlebot } + result[:agents] << { name: "Googlebot", disallow: deny_paths_googlebot } end DiscourseEvent.trigger(:robots_info, result) diff --git a/app/controllers/safe_mode_controller.rb b/app/controllers/safe_mode_controller.rb index 3ffe891d17..53cc0cff32 100644 --- a/app/controllers/safe_mode_controller.rb +++ b/app/controllers/safe_mode_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class SafeModeController < ApplicationController - layout 'no_ember' + layout "no_ember" before_action :ensure_safe_mode_enabled before_action :force_safe_mode_for_route @@ -39,5 +39,4 @@ class SafeModeController < ApplicationController request.env[ApplicationController::NO_THEMES] = true request.env[ApplicationController::NO_PLUGINS] = true end - end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 95c49bd75d..f626e620f2 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class SearchController < ApplicationController - before_action :cancel_overloaded_search, only: [:query] skip_before_action :check_xhr, only: :show after_action :add_noindex_header def self.valid_context_types - %w{user topic category private_messages tag} + %w[user topic category private_messages tag] end def show @@ -16,12 +15,9 @@ class SearchController < ApplicationController # a q param has been given but it's not in the correct format # eg: ?q[foo]=bar - if params[:q].present? && !@search_term.present? - raise Discourse::InvalidParameters.new(:q) - end + raise Discourse::InvalidParameters.new(:q) if params[:q].present? && !@search_term.present? - if @search_term.present? && - @search_term.length < SiteSetting.min_search_term_length + if @search_term.present? && @search_term.length < SiteSetting.min_search_term_length raise Discourse::InvalidParameters.new(:q) end @@ -31,21 +27,17 @@ class SearchController < ApplicationController page = permitted_params[:page] # check for a malformed page parameter - if page && (!page.is_a?(String) || page.to_i.to_s != page) - raise Discourse::InvalidParameters - end + raise Discourse::InvalidParameters if page && (!page.is_a?(String) || page.to_i.to_s != page) rate_limit_errors = rate_limit_search discourse_expires_in 1.minute search_args = { - type_filter: 'topic', + type_filter: "topic", guardian: guardian, blurb_length: 300, - page: if page.to_i <= 10 - [page.to_i, 1].max - end + page: ([page.to_i, 1].max if page.to_i <= 10), } context, type = lookup_search_context @@ -59,19 +51,21 @@ class SearchController < ApplicationController search_args[:user_id] = current_user.id if current_user.present? if rate_limit_errors - result = Search::GroupedSearchResults.new( - type_filter: search_args[:type_filter], - term: @search_term, - search_context: context - ) + result = + Search::GroupedSearchResults.new( + type_filter: search_args[:type_filter], + term: @search_term, + search_context: context, + ) result.error = I18n.t("rate_limiter.slow_down") elsif site_overloaded? - result = Search::GroupedSearchResults.new( - type_filter: search_args[:type_filter], - term: @search_term, - search_context: context - ) + result = + Search::GroupedSearchResults.new( + type_filter: search_args[:type_filter], + term: @search_term, + search_context: context, + ) result.error = I18n.t("search.extreme_load_error") else @@ -83,12 +77,8 @@ class SearchController < ApplicationController serializer = serialize_data(result, GroupedSearchResultSerializer, result: result) respond_to do |format| - format.html do - store_preloaded("search", MultiJson.dump(serializer)) - end - format.json do - render_json_dump(serializer) - end + format.html { store_preloaded("search", MultiJson.dump(serializer)) } + format.json { render_json_dump(serializer) } end end @@ -105,8 +95,8 @@ class SearchController < ApplicationController search_args = { guardian: guardian } - search_args[:type_filter] = params[:type_filter] if params[:type_filter].present? - search_args[:search_for_id] = true if params[:search_for_id].present? + search_args[:type_filter] = params[:type_filter] if params[:type_filter].present? + search_args[:search_for_id] = true if params[:search_for_id].present? context, type = lookup_search_context @@ -118,22 +108,26 @@ class SearchController < ApplicationController search_args[:search_type] = :header search_args[:ip_address] = request.remote_ip search_args[:user_id] = current_user.id if current_user.present? - search_args[:restrict_to_archetype] = params[:restrict_to_archetype] if params[:restrict_to_archetype].present? + search_args[:restrict_to_archetype] = params[:restrict_to_archetype] if params[ + :restrict_to_archetype + ].present? if rate_limit_errors - result = Search::GroupedSearchResults.new( - type_filter: search_args[:type_filter], - term: params[:term], - search_context: context - ) + result = + Search::GroupedSearchResults.new( + type_filter: search_args[:type_filter], + term: params[:term], + search_context: context, + ) result.error = I18n.t("rate_limiter.slow_down") elsif site_overloaded? - result = GroupedSearchResults.new( - type_filter: search_args[:type_filter], - term: params[:term], - search_context: context - ) + result = + GroupedSearchResults.new( + type_filter: search_args[:type_filter], + term: params[:term], + search_context: context, + ) else search = Search.new(params[:term], search_args) result = search.execute(readonly_mode: @readonly_mode) @@ -163,7 +157,7 @@ class SearchController < ApplicationController SearchLog.where(attributes).update_all( search_result_type: SearchLog.search_result_types[search_result_type], - search_result_id: search_result_id + search_result_id: search_result_id, ) end @@ -173,7 +167,7 @@ class SearchController < ApplicationController protected def site_overloaded? - queue_time = request.env['REQUEST_QUEUE_SECONDS'] + queue_time = request.env["REQUEST_QUEUE_SECONDS"] if queue_time threshold = GlobalSetting.disable_search_queue_threshold.to_f threshold > 0 && queue_time > threshold @@ -185,10 +179,25 @@ class SearchController < ApplicationController def rate_limit_search begin if current_user.present? - RateLimiter.new(current_user, "search-min", SiteSetting.rate_limit_search_user, 1.minute).performed! + RateLimiter.new( + current_user, + "search-min", + SiteSetting.rate_limit_search_user, + 1.minute, + ).performed! else - RateLimiter.new(nil, "search-min-#{request.remote_ip}", SiteSetting.rate_limit_search_anon_user, 1.minute).performed! - RateLimiter.new(nil, "search-min-anon-global", SiteSetting.rate_limit_search_anon_global, 1.minute).performed! + RateLimiter.new( + nil, + "search-min-#{request.remote_ip}", + SiteSetting.rate_limit_search_anon_user, + 1.minute, + ).performed! + RateLimiter.new( + nil, + "search-min-anon-global", + SiteSetting.rate_limit_search_anon_global, + 1.minute, + ).performed! end rescue RateLimiter::LimitExceeded => e return e @@ -197,13 +206,10 @@ class SearchController < ApplicationController end def cancel_overloaded_search - if site_overloaded? - render_json_error I18n.t("search.extreme_load_error"), status: 409 - end + render_json_error I18n.t("search.extreme_load_error"), status: 409 if site_overloaded? end def lookup_search_context - return if params[:skip_context] == "true" search_context = params[:search_context] @@ -214,30 +220,29 @@ class SearchController < ApplicationController end if search_context.present? - raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type]) + unless SearchController.valid_context_types.include?(search_context[:type]) + raise Discourse::InvalidParameters.new(:search_context) + end raise Discourse::InvalidParameters.new(:search_context) if search_context[:id].blank? # A user is found by username context_obj = nil - if ['user', 'private_messages'].include? search_context[:type] + if %w[user private_messages].include? search_context[:type] context_obj = User.find_by(username_lower: search_context[:id].downcase) - elsif 'category' == search_context[:type] + elsif "category" == search_context[:type] context_obj = Category.find_by(id: search_context[:id].to_i) - elsif 'topic' == search_context[:type] + elsif "topic" == search_context[:type] context_obj = Topic.find_by(id: search_context[:id].to_i) - elsif 'tag' == search_context[:type] + elsif "tag" == search_context[:type] context_obj = Tag.where_name(search_context[:name]).first end type_filter = nil - if search_context[:type] == 'private_messages' - type_filter = 'private_messages' - end + type_filter = "private_messages" if search_context[:type] == "private_messages" guardian.ensure_can_see!(context_obj) [context_obj, type_filter] end end - end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index ecc653c88d..9c8216522c 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -1,14 +1,16 @@ # frozen_string_literal: true class SessionController < ApplicationController - before_action :check_local_login_allowed, only: %i(create forgot_password) - before_action :rate_limit_login, only: %i(create email_login) + before_action :check_local_login_allowed, only: %i[create forgot_password] + before_action :rate_limit_login, only: %i[create email_login] skip_before_action :redirect_to_login_if_required - skip_before_action :preload_json, :check_xhr, only: %i(sso sso_login sso_provider destroy one_time_password) + skip_before_action :preload_json, + :check_xhr, + only: %i[sso sso_login sso_provider destroy one_time_password] - skip_before_action :check_xhr, only: %i(second_factor_auth_show) + skip_before_action :check_xhr, only: %i[second_factor_auth_show] - requires_login only: [:second_factor_auth_show, :second_factor_auth_perform] + requires_login only: %i[second_factor_auth_show second_factor_auth_perform] allow_in_staff_writes_only_mode :create allow_in_staff_writes_only_mode :email_login @@ -23,10 +25,10 @@ class SessionController < ApplicationController raise Discourse::NotFound unless SiteSetting.enable_discourse_connect? destination_url = cookies[:destination_url] || session[:destination_url] - return_path = params[:return_path] || path('/') + return_path = params[:return_path] || path("/") - if destination_url && return_path == path('/') - uri = URI::parse(destination_url) + if destination_url && return_path == path("/") + uri = URI.parse(destination_url) return_path = "#{uri.path}#{uri.query ? "?#{uri.query}" : ""}" end @@ -41,11 +43,12 @@ class SessionController < ApplicationController def sso_provider(payload = nil, confirmed_2fa_during_login = false) raise Discourse::NotFound unless SiteSetting.enable_discourse_connect_provider - result = run_second_factor!( - SecondFactor::Actions::DiscourseConnectProvider, - payload: payload, - confirmed_2fa_during_login: confirmed_2fa_during_login - ) + result = + run_second_factor!( + SecondFactor::Actions::DiscourseConnectProvider, + payload: payload, + confirmed_2fa_during_login: confirmed_2fa_during_login, + ) if result.second_factor_auth_skipped? data = result.data @@ -57,7 +60,7 @@ class SessionController < ApplicationController if data[:no_current_user] cookies[:sso_payload] = payload || request.query_string - redirect_to path('/login') + redirect_to path("/login") return end @@ -93,12 +96,11 @@ class SessionController < ApplicationController skip_before_action :check_xhr, only: [:become] def become - raise Discourse::InvalidAccess if Rails.env.production? raise Discourse::ReadOnly if @readonly_mode - if ENV['DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE'] != "1" - render(content_type: 'text/plain', inline: <<~TEXT) + if ENV["DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE"] != "1" + render(content_type: "text/plain", inline: <<~TEXT) To enable impersonating any user without typing passwords set the following ENV var export DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE=1 @@ -132,7 +134,9 @@ class SessionController < ApplicationController begin sso = DiscourseConnect.parse(request.query_string, secure_session: secure_session) rescue DiscourseConnect::ParseError => e - connect_verbose_warn { "Verbose SSO log: Signature parse error\n\n#{e.message}\n\n#{sso&.diagnostics}" } + connect_verbose_warn do + "Verbose SSO log: Signature parse error\n\n#{e.message}\n\n#{sso&.diagnostics}" + end # Do NOT pass the error text to the client, it would give them the correct signature return render_sso_error(text: I18n.t("discourse_connect.login_error"), status: 422) @@ -144,7 +148,9 @@ class SessionController < ApplicationController end if ScreenedIpAddress.should_block?(request.remote_ip) - connect_verbose_warn { "Verbose SSO log: IP address is blocked #{request.remote_ip}\n\n#{sso.diagnostics}" } + connect_verbose_warn do + "Verbose SSO log: IP address is blocked #{request.remote_ip}\n\n#{sso.diagnostics}" + end return render_sso_error(text: I18n.t("discourse_connect.unknown_error"), status: 500) end @@ -163,9 +169,7 @@ class SessionController < ApplicationController end if SiteSetting.must_approve_users? && !user.approved? - if invite.present? && user.invited_user.blank? - redeem_invitation(invite, sso, user) - end + redeem_invitation(invite, sso, user) if invite.present? && user.invited_user.blank? if SiteSetting.discourse_connect_not_approved_url.present? redirect_to SiteSetting.discourse_connect_not_approved_url, allow_other_host: true @@ -174,9 +178,9 @@ class SessionController < ApplicationController end return - # we only want to redeem the invite if - # the user has not already redeemed an invite - # (covers the same SSO user visiting an invite link) + # we only want to redeem the invite if + # the user has not already redeemed an invite + # (covers the same SSO user visiting an invite link) elsif invite.present? && user.invited_user.blank? redeem_invitation(invite, sso, user) @@ -199,7 +203,7 @@ class SessionController < ApplicationController end # If it's not a relative URL check the host - if return_path !~ /^\/[^\/]/ + if return_path !~ %r{^/[^/]} begin uri = URI(return_path) if (uri.hostname == Discourse.current_hostname) @@ -207,7 +211,7 @@ class SessionController < ApplicationController elsif !SiteSetting.discourse_connect_allows_all_return_paths return_path = path("/") end - rescue + rescue StandardError return_path = path("/") end end @@ -215,16 +219,13 @@ class SessionController < ApplicationController # this can be done more surgically with a regex # but it the edge case of never supporting redirects back to # any url with `/session/sso` in it anywhere is reasonable - if return_path.include?(path("/session/sso")) - return_path = path("/") - end + return_path = path("/") if return_path.include?(path("/session/sso")) redirect_to return_path, allow_other_host: true else render_sso_error(text: I18n.t("discourse_connect.not_found"), status: 500) end rescue ActiveRecord::RecordInvalid => e - connect_verbose_warn { <<~TEXT } Verbose SSO log: Record was invalid: #{e.record.class.name} #{e.record.id} #{e.record.errors.to_h} @@ -243,7 +244,8 @@ class SessionController < ApplicationController if e.record.email.blank? text = I18n.t("discourse_connect.no_email") else - text = I18n.t("discourse_connect.email_error", email: ERB::Util.html_escape(e.record.email)) + text = + I18n.t("discourse_connect.email_error", email: ERB::Util.html_escape(e.record.email)) end end @@ -270,7 +272,9 @@ class SessionController < ApplicationController end def login_sso_user(sso, user) - connect_verbose_warn { "Verbose SSO log: User was logged on #{user.username}\n\n#{sso.diagnostics}" } + connect_verbose_warn do + "Verbose SSO log: User was logged on #{user.username}\n\n#{sso.diagnostics}" + end log_on_user(user) if user.id != current_user&.id end @@ -287,7 +291,6 @@ class SessionController < ApplicationController rate_limit_second_factor!(user) if user.present? - # If their password is correct unless user.confirm_password?(params[:password]) invalid_credentials @@ -313,9 +316,7 @@ class SessionController < ApplicationController end second_factor_auth_result = authenticate_second_factor(user) - if !second_factor_auth_result.ok - return render(json: @second_factor_failure_payload) - end + return render(json: @second_factor_failure_payload) if !second_factor_auth_result.ok if user.active && user.email_confirmed? login(user, second_factor_auth_result) @@ -332,33 +333,31 @@ class SessionController < ApplicationController check_local_login_allowed(user: user, check_login_via_email: true) if matched_token - response = { - can_login: true, - token: token, - token_email: matched_token.email - } + response = { can_login: true, token: token, token_email: matched_token.email } matched_user = matched_token.user if matched_user&.totp_enabled? response.merge!( second_factor_required: true, - backup_codes_enabled: matched_user&.backup_codes_enabled? + backup_codes_enabled: matched_user&.backup_codes_enabled?, ) end if matched_user&.security_keys_enabled? Webauthn.stage_challenge(matched_user, secure_session) response.merge!( - Webauthn.allowed_credentials(matched_user, secure_session).merge(security_key_required: true) + Webauthn.allowed_credentials(matched_user, secure_session).merge( + security_key_required: true, + ), ) end render json: response else render json: { - can_login: false, - error: I18n.t('email_login.invalid_token', base_url: Discourse.base_url) - } + can_login: false, + error: I18n.t("email_login.invalid_token", base_url: Discourse.base_url), + } end end @@ -388,7 +387,7 @@ class SessionController < ApplicationController end end - render json: { error: I18n.t('email_login.invalid_token', base_url: Discourse.base_url) } + render json: { error: I18n.t("email_login.invalid_token", base_url: Discourse.base_url) } end def one_time_password @@ -406,10 +405,10 @@ class SessionController < ApplicationController # Display the form end else - @error = I18n.t('user_api_key.invalid_token') + @error = I18n.t("user_api_key.invalid_token") end - render layout: 'no_ember', locals: { hide_auth_buttons: true } + render layout: "no_ember", locals: { hide_auth_buttons: true } end def second_factor_auth_show @@ -431,7 +430,7 @@ class SessionController < ApplicationController json.merge!( totp_enabled: user.totp_enabled?, backup_enabled: user.backup_codes_enabled?, - allowed_methods: challenge[:allowed_methods] + allowed_methods: challenge[:allowed_methods], ) if user.security_keys_enabled? Webauthn.stage_challenge(user, secure_session) @@ -440,9 +439,7 @@ class SessionController < ApplicationController else json[:security_keys_enabled] = false end - if challenge[:description] - json[:description] = challenge[:description] - end + json[:description] = challenge[:description] if challenge[:description] else json[:error] = I18n.t(error_key) end @@ -453,9 +450,7 @@ class SessionController < ApplicationController raise ApplicationController::RenderEmpty.new end - format.json do - render json: json, status: status_code - end + format.json { render json: json, status: status_code } end end @@ -472,11 +467,12 @@ class SessionController < ApplicationController end if error_key - json = failed_json.merge( - ok: false, - error: I18n.t(error_key), - reason: "challenge_not_found_or_expired" - ) + json = + failed_json.merge( + ok: false, + error: I18n.t(error_key), + reason: "challenge_not_found_or_expired", + ) render json: failed_json.merge(json), status: status_code return end @@ -505,21 +501,23 @@ class SessionController < ApplicationController challenge[:generated_at] += 1.minute.to_i secure_session["current_second_factor_auth_challenge"] = challenge.to_json else - error_json = second_factor_auth_result - .to_h - .deep_symbolize_keys - .slice(:ok, :error, :reason) - .merge(failed_json) + error_json = + second_factor_auth_result + .to_h + .deep_symbolize_keys + .slice(:ok, :error, :reason) + .merge(failed_json) render json: error_json, status: 400 return end end render json: { - ok: true, - callback_method: challenge[:callback_method], - callback_path: challenge[:callback_path], - redirect_url: challenge[:redirect_url] - }, status: 200 + ok: true, + callback_method: challenge[:callback_method], + callback_path: challenge[:callback_path], + redirect_url: challenge[:redirect_url], + }, + status: 200 end def forgot_password @@ -532,19 +530,33 @@ class SessionController < ApplicationController RateLimiter.new(nil, "forgot-password-hr-#{request.remote_ip}", 6, 1.hour).performed! RateLimiter.new(nil, "forgot-password-min-#{request.remote_ip}", 3, 1.minute).performed! - user = if SiteSetting.hide_email_address_taken && !current_user&.staff? - raise Discourse::InvalidParameters.new(:login) if !EmailAddressValidator.valid_value?(normalized_login_param) - User.real.where(staged: false).find_by_email(Email.downcase(normalized_login_param)) - else - User.real.where(staged: false).find_by_username_or_email(normalized_login_param) - end + user = + if SiteSetting.hide_email_address_taken && !current_user&.staff? + if !EmailAddressValidator.valid_value?(normalized_login_param) + raise Discourse::InvalidParameters.new(:login) + end + User.real.where(staged: false).find_by_email(Email.downcase(normalized_login_param)) + else + User.real.where(staged: false).find_by_username_or_email(normalized_login_param) + end if user RateLimiter.new(nil, "forgot-password-login-day-#{user.username}", 6, 1.day).performed! - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) - Jobs.enqueue(:critical_user_email, type: "forgot_password", user_id: user.id, email_token: email_token.token) + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) + Jobs.enqueue( + :critical_user_email, + type: "forgot_password", + user_id: user.id, + email_token: email_token.token, + ) else - RateLimiter.new(nil, "forgot-password-login-hour-#{normalized_login_param}", 5, 1.hour).performed! + RateLimiter.new( + nil, + "forgot-password-login-hour-#{normalized_login_param}", + 5, + 1.hour, + ).performed! end json = success_json @@ -566,7 +578,8 @@ class SessionController < ApplicationController redirect_url = params[:return_url].presence || SiteSetting.logout_redirect.presence sso = SiteSetting.enable_discourse_connect - only_one_authenticator = !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 + only_one_authenticator = + !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 if SiteSetting.login_required && (sso || only_one_authenticator) # In this situation visiting most URLs will start the auth process again # Go to the `/login` page to avoid an immediate redirect @@ -575,16 +588,19 @@ class SessionController < ApplicationController redirect_url ||= path("/") - event_data = { redirect_url: redirect_url, user: current_user, client_ip: request&.ip, user_agent: request&.user_agent } + event_data = { + redirect_url: redirect_url, + user: current_user, + client_ip: request&.ip, + user_agent: request&.user_agent, + } DiscourseEvent.trigger(:before_session_destroy, event_data) redirect_url = event_data[:redirect_url] reset_session log_off_user if request.xhr? - render json: { - redirect_url: redirect_url - } + render json: { redirect_url: redirect_url } else redirect_to redirect_url, allow_other_host: true end @@ -595,17 +611,17 @@ class SessionController < ApplicationController secure_session.set(CHALLENGE_KEY, challenge_value, expires: 1.hour) render json: { - value: honeypot_value, - challenge: challenge_value, - expires_in: SecureSession.expiry - } + value: honeypot_value, + challenge: challenge_value, + expires_in: SecureSession.expiry, + } end def scopes if is_api? key = request.env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY] api_key = ApiKey.active.with_key(key).first - render_serialized(api_key.api_key_scopes, ApiKeyScopeSerializer, root: 'scopes') + render_serialized(api_key.api_key_scopes, ApiKeyScopeSerializer, root: "scopes") else render body: nil, status: 404 end @@ -628,8 +644,7 @@ class SessionController < ApplicationController return if user&.admin? if (check_login_via_email && !SiteSetting.enable_local_logins_via_email) || - SiteSetting.enable_discourse_connect || - !SiteSetting.enable_local_logins + SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins raise Discourse::InvalidAccess, "SSO takes over local login or the local login is disallowed." end end @@ -637,9 +652,7 @@ class SessionController < ApplicationController private def connect_verbose_warn(&blk) - if SiteSetting.verbose_discourse_connect_logging - Rails.logger.warn(blk.call) - end + Rails.logger.warn(blk.call) if SiteSetting.verbose_discourse_connect_logging end def authenticate_second_factor(user) @@ -660,9 +673,7 @@ class SessionController < ApplicationController def login_error_check(user) return failed_to_login(user) if user.suspended? - if ScreenedIpAddress.should_block?(request.remote_ip) - return not_allowed_from_ip_address(user) - end + return not_allowed_from_ip_address(user) if ScreenedIpAddress.should_block?(request.remote_ip) if ScreenedIpAddress.block_admin_login?(user, request.remote_ip) admin_not_allowed_from_ip_address(user) @@ -684,11 +695,11 @@ class SessionController < ApplicationController def not_activated(user) session[ACTIVATE_USER_KEY] = user.id render json: { - error: I18n.t("login.not_activated"), - reason: 'not_activated', - sent_to_email: user.find_email || user.email, - current_email: user.email - } + error: I18n.t("login.not_activated"), + reason: "not_activated", + sent_to_email: user.find_email || user.email, + current_email: user.email, + } end def not_allowed_from_ip_address(user) @@ -700,10 +711,7 @@ class SessionController < ApplicationController end def failed_to_login(user) - { - error: user.suspended_message, - reason: 'suspended' - } + { error: user.suspended_message, reason: "suspended" } end def login(user, second_factor_auth_result) @@ -712,11 +720,11 @@ class SessionController < ApplicationController log_on_user(user) if payload = cookies.delete(:sso_payload) - confirmed_2fa_during_login = ( - second_factor_auth_result&.ok && - second_factor_auth_result.used_2fa_method.present? && - second_factor_auth_result.used_2fa_method != UserSecondFactor.methods[:backup_codes] - ) + confirmed_2fa_during_login = + ( + second_factor_auth_result&.ok && second_factor_auth_result.used_2fa_method.present? && + second_factor_auth_result.used_2fa_method != UserSecondFactor.methods[:backup_codes] + ) sso_provider(payload, confirmed_2fa_during_login) else render_serialized(user, UserSerializer) @@ -728,20 +736,20 @@ class SessionController < ApplicationController nil, "login-hr-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_hour, - 1.hour + 1.hour, ).performed! RateLimiter.new( nil, "login-min-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_minute, - 1.minute + 1.minute, ).performed! end def render_sso_error(status:, text:) @sso_error = text - render status: status, layout: 'no_ember' + render status: status, layout: "no_ember" end # extension to allow plugins to customize the SSO URL @@ -769,9 +777,15 @@ class SessionController < ApplicationController raise Invite::ValidationFailed.new(I18n.t("invite.not_matching_email")) end elsif invite.expired? - raise Invite::ValidationFailed.new(I18n.t('invite.expired', base_url: Discourse.base_url)) + raise Invite::ValidationFailed.new(I18n.t("invite.expired", base_url: Discourse.base_url)) elsif invite.redeemed? - raise Invite::ValidationFailed.new(I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)) + raise Invite::ValidationFailed.new( + I18n.t( + "invite.not_found_template", + site_name: SiteSetting.title, + base_url: Discourse.base_url, + ), + ) end invite @@ -785,11 +799,11 @@ class SessionController < ApplicationController ip_address: request.remote_ip, session: session, email: sso.email, - redeeming_user: redeeming_user + redeeming_user: redeeming_user, ).redeem secure_session["invite-key"] = nil - # note - more specific errors are handled in the sso_login method + # note - more specific errors are handled in the sso_login method rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e Rails.logger.warn("SSO invite redemption failed: #{e}") raise Invite::RedemptionFailed diff --git a/app/controllers/similar_topics_controller.rb b/app/controllers/similar_topics_controller.rb index e2f3314657..05a87e1419 100644 --- a/app/controllers/similar_topics_controller.rb +++ b/app/controllers/similar_topics_controller.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SimilarTopicsController < ApplicationController - class SimilarTopic def initialize(topic) @topic = topic @@ -26,5 +25,4 @@ class SimilarTopicsController < ApplicationController topics.map! { |t| SimilarTopic.new(t) } render_serialized(topics, SimilarTopicSerializer, root: :similar_topics, rest_serializer: true) end - end diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 734036cd69..a8cc7ea213 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -3,7 +3,7 @@ class SiteController < ApplicationController layout false skip_before_action :preload_json, :check_xhr - skip_before_action :redirect_to_login_if_required, only: ['basic_info', 'statistics'] + skip_before_action :redirect_to_login_if_required, only: %w[basic_info statistics] def site render json: Site.json_for(guardian) @@ -33,9 +33,9 @@ class SiteController < ApplicationController favicon_url: UrlHelper.absolute(SiteSetting.site_favicon_url), title: SiteSetting.title, description: SiteSetting.site_description, - header_primary_color: ColorScheme.hex_for_name('header_primary') || '333333', - header_background_color: ColorScheme.hex_for_name('header_background') || 'ffffff', - login_required: SiteSetting.login_required + header_primary_color: ColorScheme.hex_for_name("header_primary") || "333333", + header_background_color: ColorScheme.hex_for_name("header_background") || "ffffff", + login_required: SiteSetting.login_required, } if mobile_logo_url = SiteSetting.site_mobile_logo_url.presence @@ -49,7 +49,7 @@ class SiteController < ApplicationController end def statistics - return redirect_to path('/') unless SiteSetting.share_anonymized_statistics? + return redirect_to path("/") unless SiteSetting.share_anonymized_statistics? render json: About.fetch_cached_stats end end diff --git a/app/controllers/sitemap_controller.rb b/app/controllers/sitemap_controller.rb index 3ae7efcde0..30fed7190f 100644 --- a/app/controllers/sitemap_controller.rb +++ b/app/controllers/sitemap_controller.rb @@ -6,9 +6,7 @@ class SitemapController < ApplicationController before_action :check_sitemap_enabled def index - @sitemaps = Sitemap - .where(enabled: true) - .where.not(name: Sitemap::NEWS_SITEMAP_NAME) + @sitemaps = Sitemap.where(enabled: true).where.not(name: Sitemap::NEWS_SITEMAP_NAME) render :index end @@ -18,37 +16,46 @@ class SitemapController < ApplicationController sitemap = Sitemap.find_by(enabled: true, name: index.to_s) raise Discourse::NotFound if sitemap.nil? - @output = Rails.cache.fetch("sitemap/#{sitemap.name}/#{sitemap.max_page_size}", expires_in: 24.hours) do - @topics = sitemap.topics - render :page, content_type: 'text/xml; charset=UTF-8' - end + @output = + Rails + .cache + .fetch("sitemap/#{sitemap.name}/#{sitemap.max_page_size}", expires_in: 24.hours) do + @topics = sitemap.topics + render :page, content_type: "text/xml; charset=UTF-8" + end - render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + render plain: @output, content_type: "text/xml; charset=UTF-8" unless performed? end def recent sitemap = Sitemap.touch(Sitemap::RECENT_SITEMAP_NAME) - @output = Rails.cache.fetch("sitemap/recent/#{sitemap.last_posted_at.to_i}", expires_in: 1.hour) do - @topics = sitemap.topics - render :page, content_type: 'text/xml; charset=UTF-8' - end + @output = + Rails + .cache + .fetch("sitemap/recent/#{sitemap.last_posted_at.to_i}", expires_in: 1.hour) do + @topics = sitemap.topics + render :page, content_type: "text/xml; charset=UTF-8" + end - render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + render plain: @output, content_type: "text/xml; charset=UTF-8" unless performed? end def news sitemap = Sitemap.touch(Sitemap::NEWS_SITEMAP_NAME) - @output = Rails.cache.fetch("sitemap/news", expires_in: 5.minutes) do - dlocale = SiteSetting.default_locale.downcase - @locale = dlocale.gsub(/_.*/, '') - @locale = dlocale.sub('_', '-') if @locale === "zh" - @topics = sitemap.topics - render :news, content_type: 'text/xml; charset=UTF-8' - end + @output = + Rails + .cache + .fetch("sitemap/news", expires_in: 5.minutes) do + dlocale = SiteSetting.default_locale.downcase + @locale = dlocale.gsub(/_.*/, "") + @locale = dlocale.sub("_", "-") if @locale === "zh" + @topics = sitemap.topics + render :news, content_type: "text/xml; charset=UTF-8" + end - render plain: @output, content_type: 'text/xml; charset=UTF-8' unless performed? + render plain: @output, content_type: "text/xml; charset=UTF-8" unless performed? end private @@ -58,7 +65,7 @@ class SitemapController < ApplicationController end def build_sitemap_topic_url(slug, id, posts_count = nil) - base_url = [Discourse.base_url, 't', slug, id].join('/') + base_url = [Discourse.base_url, "t", slug, id].join("/") return base_url if posts_count.nil? page, mod = posts_count.divmod(TopicView.chunk_size) @@ -67,5 +74,4 @@ class SitemapController < ApplicationController page > 1 ? "#{base_url}?page=#{page}" : base_url end helper_method :build_sitemap_topic_url - end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index b0eaaa2f56..fde841a46e 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -2,26 +2,41 @@ class StaticController < ApplicationController skip_before_action :check_xhr, :redirect_to_login_if_required - skip_before_action :verify_authenticity_token, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] - skip_before_action :preload_json, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] - skip_before_action :handle_theme, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] + skip_before_action :verify_authenticity_token, + only: %i[brotli_asset cdn_asset enter favicon service_worker_asset] + skip_before_action :preload_json, + only: %i[brotli_asset cdn_asset enter favicon service_worker_asset] + skip_before_action :handle_theme, + only: %i[brotli_asset cdn_asset enter favicon service_worker_asset] - before_action :apply_cdn_headers, only: [:brotli_asset, :cdn_asset, :enter, :favicon, :service_worker_asset] + before_action :apply_cdn_headers, + only: %i[brotli_asset cdn_asset enter favicon service_worker_asset] - PAGES_WITH_EMAIL_PARAM = ['login', 'password_reset', 'signup'] - MODAL_PAGES = ['password_reset', 'signup'] + PAGES_WITH_EMAIL_PARAM = %w[login password_reset signup] + MODAL_PAGES = %w[password_reset signup] DEFAULT_PAGES = { - "faq" => { redirect: "faq_url", topic_id: "guidelines_topic_id" }, - "tos" => { redirect: "tos_url", topic_id: "tos_topic_id" }, - "privacy" => { redirect: "privacy_policy_url", topic_id: "privacy_topic_id" }, + "faq" => { + redirect: "faq_url", + topic_id: "guidelines_topic_id", + }, + "tos" => { + redirect: "tos_url", + topic_id: "tos_topic_id", + }, + "privacy" => { + redirect: "privacy_policy_url", + topic_id: "privacy_topic_id", + }, } CUSTOM_PAGES = {} # Add via `#add_topic_static_page` in plugin API def show - return redirect_to(path '/') if current_user && (params[:id] == 'login' || params[:id] == 'signup') + if current_user && (params[:id] == "login" || params[:id] == "signup") + return redirect_to(path "/") + end - if SiteSetting.login_required? && current_user.nil? && ['faq', 'guidelines'].include?(params[:id]) - return redirect_to path('/login') + if SiteSetting.login_required? && current_user.nil? && %w[faq guidelines].include?(params[:id]) + return redirect_to path("/login") end map = DEFAULT_PAGES.deep_merge(CUSTOM_PAGES) @@ -34,10 +49,10 @@ class StaticController < ApplicationController end # The /guidelines route ALWAYS shows our FAQ, ignoring the faq_url site setting. - @page = 'faq' if @page == 'guidelines' + @page = "faq" if @page == "guidelines" # Don't allow paths like ".." or "/" or anything hacky like that - @page = @page.gsub(/[^a-z0-9\_\-]/, '') + @page = @page.gsub(/[^a-z0-9\_\-]/, "") if map.has_key?(@page) topic_id = map[@page][:topic_id] @@ -46,11 +61,12 @@ class StaticController < ApplicationController @topic = Topic.find_by_id(SiteSetting.get(topic_id)) raise Discourse::NotFound unless @topic - title_prefix = if I18n.exists?("js.#{@page}") - I18n.t("js.#{@page}") - else - @topic.title - end + title_prefix = + if I18n.exists?("js.#{@page}") + I18n.t("js.#{@page}") + else + @topic.title + end @title = "#{title_prefix} - #{SiteSetting.title}" @body = @topic.posts.first.cooked @faq_overridden = !SiteSetting.faq_url.blank? @@ -104,10 +120,7 @@ class StaticController < ApplicationController forum_uri = URI(Discourse.base_url) uri = URI(redirect_location) - if uri.path.present? && - (uri.host.blank? || uri.host == forum_uri.host) && - uri.path !~ /\./ - + if uri.path.present? && (uri.host.blank? || uri.host == forum_uri.host) && uri.path !~ /\./ destination = "#{uri.path}#{uri.query ? "?#{uri.query}" : ""}" end rescue URI::Error @@ -135,31 +148,33 @@ class StaticController < ApplicationController is_asset_path hijack do - data = DistributedMemoizer.memoize("FAVICON#{SiteIconManager.favicon_url}", 60 * 30) do - favicon = SiteIconManager.favicon - next "" unless favicon + data = + DistributedMemoizer.memoize("FAVICON#{SiteIconManager.favicon_url}", 60 * 30) do + favicon = SiteIconManager.favicon + next "" unless favicon - if Discourse.store.external? - begin - file = FileHelper.download( - Discourse.store.cdn_url(favicon.url), - max_file_size: favicon.filesize, - tmp_file_name: FAVICON, - follow_redirect: true - ) + if Discourse.store.external? + begin + file = + FileHelper.download( + Discourse.store.cdn_url(favicon.url), + max_file_size: favicon.filesize, + tmp_file_name: FAVICON, + follow_redirect: true, + ) - file&.read || "" - rescue => e - AdminDashboardData.add_problem_message('dashboard.bad_favicon_url', 1800) - Rails.logger.debug("Failed to fetch favicon #{favicon.url}: #{e}\n#{e.backtrace}") - "" - ensure - file&.unlink + file&.read || "" + rescue => e + AdminDashboardData.add_problem_message("dashboard.bad_favicon_url", 1800) + Rails.logger.debug("Failed to fetch favicon #{favicon.url}: #{e}\n#{e.backtrace}") + "" + ensure + file&.unlink + end + else + File.read(Rails.root.join("public", favicon.url[1..-1])) end - else - File.read(Rails.root.join("public", favicon.url[1..-1])) end - end if data.bytesize == 0 @@default_favicon ||= File.read(Rails.root + "public/images/default-favicon.png") @@ -178,9 +193,7 @@ class StaticController < ApplicationController def brotli_asset is_asset_path - serve_asset(".br") do - response.headers["Content-Encoding"] = 'br' - end + serve_asset(".br") { response.headers["Content-Encoding"] = "br" } end def cdn_asset @@ -199,20 +212,22 @@ class StaticController < ApplicationController # However, ensure that these may be cached and served for longer on servers. immutable_for 1.year - if Rails.application.assets_manifest.assets['service-worker.js'] - path = File.expand_path(Rails.root + "public/assets/#{Rails.application.assets_manifest.assets['service-worker.js']}") + if Rails.application.assets_manifest.assets["service-worker.js"] + path = + File.expand_path( + Rails.root + + "public/assets/#{Rails.application.assets_manifest.assets["service-worker.js"]}", + ) response.headers["Last-Modified"] = File.ctime(path).httpdate end - content = Rails.application.assets_manifest.find_sources('service-worker.js').first + content = Rails.application.assets_manifest.find_sources("service-worker.js").first - base_url = File.dirname(helpers.script_asset_path('service-worker')) - content = content.sub( - /^\/\/# sourceMappingURL=(service-worker-.+\.map)$/ - ) { "//# sourceMappingURL=#{base_url}/#{Regexp.last_match(1)}" } - render( - plain: content, - content_type: 'application/javascript' - ) + base_url = File.dirname(helpers.script_asset_path("service-worker")) + content = + content.sub(%r{^//# sourceMappingURL=(service-worker-.+\.map)$}) do + "//# sourceMappingURL=#{base_url}/#{Regexp.last_match(1)}" + end + render(plain: content, content_type: "application/javascript") end end end @@ -220,7 +235,6 @@ class StaticController < ApplicationController protected def serve_asset(suffix = nil) - path = File.expand_path(Rails.root + "public/assets/#{params[:path]}#{suffix}") # SECURITY what if path has /../ @@ -254,12 +268,10 @@ class StaticController < ApplicationController immutable_for 1.year # disable NGINX mucking with transfer - request.env['sendfile.type'] = '' + request.env["sendfile.type"] = "" opts = { disposition: nil } opts[:type] = "application/javascript" if params[:path] =~ /\.js$/ send_file(path, opts) - end - end diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index 1f17ae6af9..153a2cccfd 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -12,7 +12,7 @@ class StepsController < ApplicationController updater.update if updater.success? - result = { success: 'OK' } + result = { success: "OK" } result[:refresh_required] = true if updater.refresh_required? render json: result else @@ -23,5 +23,4 @@ class StepsController < ApplicationController render json: { errors: errors }, status: 422 end end - end diff --git a/app/controllers/stylesheets_controller.rb b/app/controllers/stylesheets_controller.rb index ee78f30765..97edb80b57 100644 --- a/app/controllers/stylesheets_controller.rb +++ b/app/controllers/stylesheets_controller.rb @@ -1,9 +1,13 @@ # frozen_string_literal: true class StylesheetsController < ApplicationController - skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_source_map, :color_scheme] + skip_before_action :preload_json, + :redirect_to_login_if_required, + :check_xhr, + :verify_authenticity_token, + only: %i[show show_source_map color_scheme] - before_action :apply_cdn_headers, only: [:show, :show_source_map, :color_scheme] + before_action :apply_cdn_headers, only: %i[show show_source_map color_scheme] def show_source_map show_resource(source_map: true) @@ -20,14 +24,13 @@ class StylesheetsController < ApplicationController params.permit("theme_id") manager = Stylesheet::Manager.new(theme_id: params[:theme_id]) - stylesheet = manager.color_scheme_stylesheet_details(params[:id], 'all') + stylesheet = manager.color_scheme_stylesheet_details(params[:id], "all") render json: stylesheet end protected def show_resource(source_map: false) - extension = source_map ? ".css.map" : ".css" no_cookies @@ -47,7 +50,7 @@ class StylesheetsController < ApplicationController if digest query = query.where(digest: digest) else - query = query.order('id desc') + query = query.order("id desc") end # Security note, safe due to route constraint @@ -58,9 +61,7 @@ class StylesheetsController < ApplicationController stylesheet_time = query.pluck_first(:created_at) - if !stylesheet_time - handle_missing_cache(location, target, digest) - end + handle_missing_cache(location, target, digest) if !stylesheet_time if cache_time && stylesheet_time && stylesheet_time <= cache_time return render body: nil, status: 304 @@ -76,10 +77,10 @@ class StylesheetsController < ApplicationController end if Rails.env == "development" - response.headers['Last-Modified'] = Time.zone.now.httpdate + response.headers["Last-Modified"] = Time.zone.now.httpdate immutable_for(1.second) else - response.headers['Last-Modified'] = stylesheet_time.httpdate if stylesheet_time + response.headers["Last-Modified"] = stylesheet_time.httpdate if stylesheet_time immutable_for(1.year) end send_file(location, disposition: :inline) @@ -104,5 +105,4 @@ class StylesheetsController < ApplicationController rescue Errno::ENOENT end end - end diff --git a/app/controllers/svg_sprite_controller.rb b/app/controllers/svg_sprite_controller.rb index 5366654d9d..f073314e20 100644 --- a/app/controllers/svg_sprite_controller.rb +++ b/app/controllers/svg_sprite_controller.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true class SvgSpriteController < ApplicationController - skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :search, :svg_icon] + skip_before_action :preload_json, + :redirect_to_login_if_required, + :check_xhr, + :verify_authenticity_token, + only: %i[show search svg_icon] - before_action :apply_cdn_headers, only: [:show, :search, :svg_icon] + before_action :apply_cdn_headers, only: %i[show search svg_icon] - requires_login except: [:show, :svg_icon] + requires_login except: %i[show svg_icon] def show - no_cookies RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do @@ -24,20 +27,19 @@ class SvgSpriteController < ApplicationController response.headers["Content-Length"] = svg_sprite.bytesize.to_s immutable_for 1.year - render plain: svg_sprite, disposition: nil, content_type: 'application/javascript' + render plain: svg_sprite, disposition: nil, content_type: "application/javascript" end end def search RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do - keyword = params.require(:keyword) data = SvgSprite.search(keyword) if data.blank? render body: nil, status: 404 else - render plain: data.inspect, disposition: nil, content_type: 'text/plain' + render plain: data.inspect, disposition: nil, content_type: "text/plain" end end end @@ -65,14 +67,14 @@ class SvgSpriteController < ApplicationController else doc = Nokogiri.XML(icon) doc.at_xpath("symbol").name = "svg" - doc.at_xpath("svg")['xmlns'] = "http://www.w3.org/2000/svg" - doc.at_xpath("svg")['fill'] = adjust_hex(params[:color]) if params[:color] + doc.at_xpath("svg")["xmlns"] = "http://www.w3.org/2000/svg" + doc.at_xpath("svg")["fill"] = adjust_hex(params[:color]) if params[:color] response.headers["Last-Modified"] = 1.years.ago.httpdate response.headers["Content-Length"] = doc.to_s.bytesize.to_s immutable_for 1.day - render plain: doc, disposition: nil, content_type: 'image/svg+xml' + render plain: doc, disposition: nil, content_type: "image/svg+xml" end end end diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb index 9921d7af6d..fd6dae701d 100644 --- a/app/controllers/tag_groups_controller.rb +++ b/app/controllers/tag_groups_controller.rb @@ -4,12 +4,17 @@ class TagGroupsController < ApplicationController requires_login except: [:search] before_action :ensure_staff, except: [:search] - skip_before_action :check_xhr, only: [:index, :show, :new] - before_action :fetch_tag_group, only: [:show, :update, :destroy] + skip_before_action :check_xhr, only: %i[index show new] + before_action :fetch_tag_group, only: %i[show update destroy] def index - tag_groups = TagGroup.order('name ASC').includes(:parent_tag).preload(:tags).all - serializer = ActiveModel::ArraySerializer.new(tag_groups, each_serializer: TagGroupSerializer, root: 'tag_groups') + tag_groups = TagGroup.order("name ASC").includes(:parent_tag).preload(:tags).all + serializer = + ActiveModel::ArraySerializer.new( + tag_groups, + each_serializer: TagGroupSerializer, + root: "tag_groups", + ) respond_to do |format| format.html do store_preloaded "tagGroups", MultiJson.dump(serializer) @@ -31,8 +36,13 @@ class TagGroupsController < ApplicationController end def new - tag_groups = TagGroup.order('name ASC').includes(:parent_tag).preload(:tags).all - serializer = ActiveModel::ArraySerializer.new(tag_groups, each_serializer: TagGroupSerializer, root: 'tag_groups') + tag_groups = TagGroup.order("name ASC").includes(:parent_tag).preload(:tags).all + serializer = + ActiveModel::ArraySerializer.new( + tag_groups, + each_serializer: TagGroupSerializer, + root: "tag_groups", + ) store_preloaded "tagGroup", MultiJson.dump(serializer) render "default/empty" end @@ -63,19 +73,18 @@ class TagGroupsController < ApplicationController def search matches = TagGroup.includes(:tags).visible(guardian).all - if params[:q].present? - matches = matches.where('lower(name) ILIKE ?', "%#{params[:q].strip}%") - end + matches = matches.where("lower(name) ILIKE ?", "%#{params[:q].strip}%") if params[:q].present? if params[:names].present? - matches = matches.where('lower(NAME) in (?)', params[:names].map(&:downcase)) + matches = matches.where("lower(NAME) in (?)", params[:names].map(&:downcase)) end - matches = matches.order('name').limit(params[:limit] || 5) + matches = matches.order("name").limit(params[:limit] || 5) render json: { - results: matches.map { |x| { name: x.name, tag_names: x.tags.base_tags.pluck(:name).sort } } - } + results: + matches.map { |x| { name: x.name, tag_names: x.tags.base_tags.pluck(:name).sort } }, + } end private @@ -88,14 +97,8 @@ class TagGroupsController < ApplicationController tag_group = params.delete(:tag_group) params.merge!(tag_group.permit!) if tag_group - result = params.permit( - :id, - :name, - :one_per_topic, - tag_names: [], - parent_tag_name: [], - permissions: {} - ) + result = + params.permit(:id, :name, :one_per_topic, tag_names: [], parent_tag_name: [], permissions: {}) result[:tag_names] ||= [] result[:parent_tag_name] ||= [] diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 662d8da502..9b27b69e6a 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -5,29 +5,32 @@ class TagsController < ::ApplicationController include TopicQueryParams before_action :ensure_tags_enabled - before_action :ensure_visible, only: [:show, :info] + before_action :ensure_visible, only: %i[show info] def self.show_methods Discourse.anonymous_filters.map { |f| :"show_#{f}" } end - requires_login except: [ - :index, - :show, - :tag_feed, - :search, - :info, - *show_methods - ] + requires_login except: [:index, :show, :tag_feed, :search, :info, *show_methods] skip_before_action :check_xhr, only: [:tag_feed, :show, :index, *show_methods] - before_action :set_category, except: [:index, :update, :destroy, - :tag_feed, :search, :notifications, :update_notifications, :personal_messages, :info] + before_action :set_category, + except: %i[ + index + update + destroy + tag_feed + search + notifications + update_notifications + personal_messages + info + ] - before_action :fetch_tag, only: [:info, :create_synonyms, :destroy_synonym] + before_action :fetch_tag, only: %i[info create_synonyms destroy_synonym] - after_action :add_noindex_header, except: [:index, :show] + after_action :add_noindex_header, except: %i[index show] def index @description_meta = I18n.t("tags.title") @@ -39,9 +42,22 @@ class TagsController < ::ApplicationController ungrouped_tags = Tag.where("tags.id NOT IN (SELECT tag_id FROM tag_group_memberships)") ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags - grouped_tag_counts = TagGroup.visible(guardian).order('name ASC').includes(:tags).map do |tag_group| - { id: tag_group.id, name: tag_group.name, tags: self.class.tag_counts_json(tag_group.tags.where(target_tag_id: nil), show_pm_tags: guardian.can_tag_pms?) } - end + grouped_tag_counts = + TagGroup + .visible(guardian) + .order("name ASC") + .includes(:tags) + .map do |tag_group| + { + id: tag_group.id, + name: tag_group.name, + tags: + self.class.tag_counts_json( + tag_group.tags.where(target_tag_id: nil), + show_pm_tags: guardian.can_tag_pms?, + ), + } + end @tags = self.class.tag_counts_json(ungrouped_tags, show_pm_tags: guardian.can_tag_pms?) @extras = { tag_groups: grouped_tag_counts } @@ -49,41 +65,40 @@ class TagsController < ::ApplicationController tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0") unrestricted_tags = DiscourseTagging.filter_visible(tags.where(target_tag_id: nil), guardian) - categories = Category.where("id IN (SELECT category_id FROM category_tags)") - .where("id IN (?)", guardian.allowed_category_ids) - .includes(:tags) + categories = + Category + .where("id IN (SELECT category_id FROM category_tags)") + .where("id IN (?)", guardian.allowed_category_ids) + .includes(:tags) - category_tag_counts = categories.map do |c| - category_tags = self.class.tag_counts_json( - DiscourseTagging.filter_visible(c.tags.where(target_tag_id: nil), guardian) - ) - next if category_tags.empty? - { id: c.id, tags: category_tags } - end.compact + category_tag_counts = + categories + .map do |c| + category_tags = + self.class.tag_counts_json( + DiscourseTagging.filter_visible(c.tags.where(target_tag_id: nil), guardian), + ) + next if category_tags.empty? + { id: c.id, tags: category_tags } + end + .compact @tags = self.class.tag_counts_json(unrestricted_tags, show_pm_tags: guardian.can_tag_pms?) @extras = { categories: category_tag_counts } end respond_to do |format| + format.html { render :index } - format.html do - render :index - end - - format.json do - render json: { - tags: @tags, - extras: @extras - } - end + format.json { render json: { tags: @tags, extras: @extras } } end end Discourse.filters.each do |filter| define_method("show_#{filter}") do @tag_id = params[:tag_id].force_encoding("UTF-8") - @additional_tags = params[:additional_tag_ids].to_s.split('/').map { |t| t.force_encoding("UTF-8") } + @additional_tags = + params[:additional_tag_ids].to_s.split("/").map { |t| t.force_encoding("UTF-8") } list_opts = build_topic_list_options @list = nil @@ -101,14 +116,14 @@ class TagsController < ::ApplicationController @list.more_topics_url = construct_url_with(:next, list_opts) @list.prev_topics_url = construct_url_with(:prev, list_opts) @rss = "tag" - @description_meta = I18n.t("rss_by_tag", tag: tag_params.join(' & ')) + @description_meta = I18n.t("rss_by_tag", tag: tag_params.join(" & ")) @title = @description_meta canonical_params = params.slice(:category_slug_path_with_id, :tag_id) canonical_method = url_method(canonical_params) canonical_url "#{Discourse.base_url_no_prefix}#{public_send(canonical_method, *(canonical_params.values.map { |t| t.force_encoding("UTF-8") }))}" - if @list.topics.size == 0 && params[:tag_id] != 'none' && !Tag.where_name(@tag_id).exists? + if @list.topics.size == 0 && params[:tag_id] != "none" && !Tag.where_name(@tag_id).exists? raise Discourse::NotFound.new("tag not found", check_permalinks: true) else respond_with_list(@list) @@ -121,12 +136,7 @@ class TagsController < ::ApplicationController end def info - render_serialized( - @tag, - DetailedTagSerializer, - rest_serializer: true, - root: :tag_info - ) + render_serialized(@tag, DetailedTagSerializer, rest_serializer: true, root: :tag_info) end def update @@ -141,7 +151,11 @@ class TagsController < ::ApplicationController end tag.description = params[:tag][:description] if params[:tag]&.has_key?(:description) if tag.save - StaffActionLogger.new(current_user).log_custom('renamed_tag', previous_value: params[:tag_id], new_value: new_tag_name) + StaffActionLogger.new(current_user).log_custom( + "renamed_tag", + previous_value: params[:tag_id], + new_value: new_tag_name, + ) render json: { tag: { id: tag.name, description: tag.description } } else render_json_error tag.errors.full_messages @@ -149,7 +163,7 @@ class TagsController < ::ApplicationController end def upload - require 'csv' + require "csv" guardian.ensure_can_admin_tags! @@ -159,7 +173,9 @@ class TagsController < ::ApplicationController begin Tag.transaction do CSV.foreach(file.tempfile) do |row| - raise Discourse::InvalidParameters.new(I18n.t("tags.upload_row_too_long")) if row.length > 2 + if row.length > 2 + raise Discourse::InvalidParameters.new(I18n.t("tags.upload_row_too_long")) + end tag_name = DiscourseTagging.clean_tag(row[0]) tag_group_name = row[1] || nil @@ -167,7 +183,8 @@ class TagsController < ::ApplicationController tag = Tag.find_by_name(tag_name) || Tag.create!(name: tag_name) if tag_group_name - tag_group = TagGroup.find_by(name: tag_group_name) || TagGroup.create!(name: tag_group_name) + tag_group = + TagGroup.find_by(name: tag_group_name) || TagGroup.create!(name: tag_group_name) tag.tag_groups << tag_group unless tag.tag_groups.include?(tag_group) end end @@ -187,7 +204,7 @@ class TagsController < ::ApplicationController def destroy_unused guardian.ensure_can_admin_tags! tags = Tag.unused - StaffActionLogger.new(current_user).log_custom('deleted_unused_tags', tags: tags.pluck(:name)) + StaffActionLogger.new(current_user).log_custom("deleted_unused_tags", tags: tags.pluck(:name)) tags.destroy_all render json: success_json end @@ -200,7 +217,7 @@ class TagsController < ::ApplicationController TopicCustomField.transaction do tag.destroy - StaffActionLogger.new(current_user).log_custom('deleted_tag', subject: tag_name) + StaffActionLogger.new(current_user).log_custom("deleted_tag", subject: tag_name) end render json: success_json end @@ -218,7 +235,7 @@ class TagsController < ::ApplicationController latest_results = query.latest_results @topic_list = query.create_list(:by_tag, {}, latest_results) - render 'list/list', formats: [:rss] + render "list/list", formats: [:rss] end def search @@ -227,16 +244,14 @@ class TagsController < ::ApplicationController selected_tags: params[:selected_tags], limit: params[:limit], exclude_synonyms: params[:excludeSynonyms], - exclude_has_synonyms: params[:excludeHasSynonyms] + exclude_has_synonyms: params[:excludeHasSynonyms], } if filter_params[:limit] && filter_params[:limit].to_i < 0 raise Discourse::InvalidParameters.new(:limit) end - if params[:categoryId] - filter_params[:category] = Category.find_by_id(params[:categoryId]) - end + filter_params[:category] = Category.find_by_id(params[:categoryId]) if params[:categoryId] if !params[:q].blank? clean_name = DiscourseTagging.clean_tag(params[:q]) @@ -246,27 +261,35 @@ class TagsController < ::ApplicationController filter_params[:order_popularity] = true end - tags_with_counts, filter_result_context = DiscourseTagging.filter_allowed_tags( - guardian, - **filter_params, - with_context: true - ) + tags_with_counts, filter_result_context = + DiscourseTagging.filter_allowed_tags(guardian, **filter_params, with_context: true) tags = self.class.tag_counts_json(tags_with_counts, show_pm_tags: guardian.can_tag_pms?) json_response = { results: tags } - if clean_name && !tags.find { |h| h[:id].downcase == clean_name.downcase } && tag = Tag.where_name(clean_name).first + if clean_name && !tags.find { |h| h[:id].downcase == clean_name.downcase } && + tag = Tag.where_name(clean_name).first # filter_allowed_tags determined that the tag entered is not allowed json_response[:forbidden] = params[:q] if filter_params[:exclude_synonyms] && tag.synonym? - json_response[:forbidden_message] = I18n.t("tags.forbidden.synonym", tag_name: tag.target_tag.name) + json_response[:forbidden_message] = I18n.t( + "tags.forbidden.synonym", + tag_name: tag.target_tag.name, + ) elsif filter_params[:exclude_has_synonyms] && tag.synonyms.exists? - json_response[:forbidden_message] = I18n.t("tags.forbidden.has_synonyms", tag_name: tag.name) + json_response[:forbidden_message] = I18n.t( + "tags.forbidden.has_synonyms", + tag_name: tag.name, + ) else category_names = tag.categories.where(id: guardian.allowed_category_ids).pluck(:name) - category_names += Category.joins(tag_groups: :tags).where(id: guardian.allowed_category_ids, "tags.id": tag.id).pluck(:name) + category_names += + Category + .joins(tag_groups: :tags) + .where(id: guardian.allowed_category_ids, "tags.id": tag.id) + .pluck(:name) if category_names.present? category_names.uniq! @@ -274,10 +297,13 @@ class TagsController < ::ApplicationController "tags.forbidden.restricted_to", count: category_names.count, tag_name: tag.name, - category_names: category_names.join(", ") + category_names: category_names.join(", "), ) else - json_response[:forbidden_message] = I18n.t("tags.forbidden.in_this_category", tag_name: tag.name) + json_response[:forbidden_message] = I18n.t( + "tags.forbidden.in_this_category", + tag_name: tag.name, + ) end end end @@ -292,7 +318,9 @@ class TagsController < ::ApplicationController def notifications tag = Tag.where_name(params[:tag_id]).first raise Discourse::NotFound unless tag - level = tag.tag_users.where(user: current_user).first.try(:notification_level) || TagUser.notification_levels[:regular] + level = + tag.tag_users.where(user: current_user).first.try(:notification_level) || + TagUser.notification_levels[:regular] render json: { tag_notification: { id: tag.name, notification_level: level.to_i } } end @@ -318,9 +346,14 @@ class TagsController < ::ApplicationController guardian.ensure_can_admin_tags! value = DiscourseTagging.add_or_create_synonyms_by_name(@tag, params[:synonyms]) if value.is_a?(Array) - render json: failed_json.merge( - failed_tags: value.inject({}) { |h, t| h[t.name] = t.errors.full_messages.first; h } - ) + render json: + failed_json.merge( + failed_tags: + value.inject({}) do |h, t| + h[t.name] = t.errors.full_messages.first + h + end, + ) else render json: success_json end @@ -350,24 +383,29 @@ class TagsController < ::ApplicationController end def ensure_visible - raise Discourse::NotFound if DiscourseTagging.hidden_tag_names(guardian).include?(params[:tag_id]) + if DiscourseTagging.hidden_tag_names(guardian).include?(params[:tag_id]) + raise Discourse::NotFound + end end def self.tag_counts_json(tags, show_pm_tags: true) target_tags = Tag.where(id: tags.map(&:target_tag_id).compact.uniq).select(:id, :name) - tags.map do |t| - next if t.topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags + tags + .map do |t| + next if t.topic_count == 0 && t.pm_topic_count > 0 && !show_pm_tags - { - id: t.name, - text: t.name, - name: t.name, - description: t.description, - count: t.topic_count, - pm_count: show_pm_tags ? t.pm_topic_count : 0, - target_tag: t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil - } - end.compact + { + id: t.name, + text: t.name, + name: t.name, + description: t.description, + count: t.topic_count, + pm_count: show_pm_tags ? t.pm_topic_count : 0, + target_tag: + t.target_tag_id ? target_tags.find { |x| x.id == t.target_tag_id }&.name : nil, + } + end + .compact end def set_category @@ -383,7 +421,10 @@ class TagsController < ::ApplicationController if !@filter_on_category permalink = Permalink.find_by_url("c/#{params[:category_slug_path_with_id]}") if permalink.present? && permalink.category_id - return redirect_to "#{Discourse::base_path}/tags#{permalink.target_url}/#{params[:tag_id]}", status: :moved_permanently + return( + redirect_to "#{Discourse.base_path}/tags#{permalink.target_url}/#{params[:tag_id]}", + status: :moved_permanently + ) end end @@ -394,14 +435,15 @@ class TagsController < ::ApplicationController end def page_params - route_params = { format: 'json' } + route_params = { format: "json" } if @filter_on_category if request.path_parameters.include?(:category_slug_path_with_id) slug_path = @filter_on_category.slug_path - route_params[:category_slug_path_with_id] = - (slug_path + [@filter_on_category.id.to_s]).join("/") + route_params[:category_slug_path_with_id] = ( + slug_path + [@filter_on_category.id.to_s] + ).join("/") else route_params[:category] = @filter_on_category.slug_for_url end @@ -453,28 +495,30 @@ class TagsController < ::ApplicationController raise Discourse::NotFound end - url.sub('.json?', '?') + url.sub(".json?", "?") end def build_topic_list_options - options = super.merge( - page: params[:page], - topic_ids: param_to_integer_list(:topic_ids), - category: @filter_on_category ? @filter_on_category.id : params[:category], - order: params[:order], - ascending: params[:ascending], - min_posts: params[:min_posts], - max_posts: params[:max_posts], - status: params[:status], - filter: params[:filter], - state: params[:state], - search: params[:search], - q: params[:q] - ) - options[:no_subcategories] = true if params[:no_subcategories] == true || params[:no_subcategories] == 'true' + options = + super.merge( + page: params[:page], + topic_ids: param_to_integer_list(:topic_ids), + category: @filter_on_category ? @filter_on_category.id : params[:category], + order: params[:order], + ascending: params[:ascending], + min_posts: params[:min_posts], + max_posts: params[:max_posts], + status: params[:status], + filter: params[:filter], + state: params[:state], + search: params[:search], + q: params[:q], + ) + options[:no_subcategories] = true if params[:no_subcategories] == true || + params[:no_subcategories] == "true" options[:per_page] = params[:per_page].to_i.clamp(1, 30) if params[:per_page].present? - if params[:tag_id] == 'none' + if params[:tag_id] == "none" options.delete(:tags) options[:no_tags] = true else diff --git a/app/controllers/theme_javascripts_controller.rb b/app/controllers/theme_javascripts_controller.rb index 418a076b82..63d6484d71 100644 --- a/app/controllers/theme_javascripts_controller.rb +++ b/app/controllers/theme_javascripts_controller.rb @@ -9,10 +9,10 @@ class ThemeJavascriptsController < ApplicationController :preload_json, :redirect_to_login_if_required, :verify_authenticity_token, - only: [:show, :show_map, :show_tests] + only: %i[show show_map show_tests], ) - before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: [:show, :show_map, :show_tests] + before_action :is_asset_path, :no_cookies, :apply_cdn_headers, only: %i[show show_map show_tests] def show raise Discourse::NotFound unless last_modified.present? @@ -24,7 +24,8 @@ class ThemeJavascriptsController < ApplicationController write_if_not_cached(cache_file) do content, has_source_map = query.pluck_first(:content, "source_map IS NOT NULL") if has_source_map - content += "\n//# sourceMappingURL=#{params[:digest]}.map?__ws=#{Discourse.current_hostname}\n" + content += + "\n//# sourceMappingURL=#{params[:digest]}.map?__ws=#{Discourse.current_hostname}\n" end content end @@ -39,9 +40,7 @@ class ThemeJavascriptsController < ApplicationController # Security: safe due to route constraint cache_file = "#{DISK_CACHE_PATH}/#{params[:digest]}.map" - write_if_not_cached(cache_file) do - query.pluck_first(:source_map) - end + write_if_not_cached(cache_file) { query.pluck_first(:source_map) } serve_file(cache_file) end @@ -59,9 +58,7 @@ class ThemeJavascriptsController < ApplicationController @cache_file = "#{TESTS_DISK_CACHE_PATH}/#{digest}.js" return render body: nil, status: 304 if not_modified? - write_if_not_cached(@cache_file) do - content - end + write_if_not_cached(@cache_file) { content } serve_file @cache_file end @@ -73,13 +70,14 @@ class ThemeJavascriptsController < ApplicationController end def last_modified - @last_modified ||= begin - if params[:action].to_s == "show_tests" - File.exist?(@cache_file) ? File.ctime(@cache_file) : nil - else - query.pluck_first(:updated_at) + @last_modified ||= + begin + if params[:action].to_s == "show_tests" + File.exist?(@cache_file) ? File.ctime(@cache_file) : nil + else + query.pluck_first(:updated_at) + end end - end end def not_modified? @@ -95,10 +93,10 @@ class ThemeJavascriptsController < ApplicationController def set_cache_control_headers if Rails.env.development? - response.headers['Last-Modified'] = Time.zone.now.httpdate + response.headers["Last-Modified"] = Time.zone.now.httpdate immutable_for(1.second) else - response.headers['Last-Modified'] = last_modified.httpdate if last_modified + response.headers["Last-Modified"] = last_modified.httpdate if last_modified immutable_for(1.year) end end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index d2b3caeb24..4ad8b87911 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,40 +1,40 @@ # frozen_string_literal: true class TopicsController < ApplicationController - requires_login only: [ - :timings, - :destroy_timings, - :update, - :update_shared_draft, - :destroy, - :recover, - :status, - :invite, - :mute, - :unmute, - :set_notifications, - :move_posts, - :merge_topic, - :clear_pin, - :re_pin, - :status_update, - :timer, - :bulk, - :reset_new, - :change_post_owners, - :change_timestamps, - :archive_message, - :move_to_inbox, - :convert_topic, - :bookmark, - :publish, - :reset_bump_date, - :set_slow_mode - ] + requires_login only: %i[ + timings + destroy_timings + update + update_shared_draft + destroy + recover + status + invite + mute + unmute + set_notifications + move_posts + merge_topic + clear_pin + re_pin + status_update + timer + bulk + reset_new + change_post_owners + change_timestamps + archive_message + move_to_inbox + convert_topic + bookmark + publish + reset_bump_date + set_slow_mode + ] before_action :consider_user_for_promotion, only: :show - skip_before_action :check_xhr, only: [:show, :feed] + skip_before_action :check_xhr, only: %i[show feed] def id_for_slug topic = Topic.find_by_slug(params[:slug]) @@ -51,9 +51,7 @@ class TopicsController < ApplicationController end def show - if request.referer - flash["referer"] ||= request.referer[0..255] - end + flash["referer"] ||= request.referer[0..255] if request.referer # We'd like to migrate the wordpress feed to another url. This keeps up backwards compatibility with # existing installs. @@ -61,13 +59,27 @@ class TopicsController < ApplicationController # work around people somehow sending in arrays, # arrays are not supported - params[:page] = params[:page].to_i rescue 1 + params[:page] = begin + params[:page].to_i + rescue StandardError + 1 + end - opts = params.slice(:username_filters, :filter, :page, :post_number, :show_deleted, :replies_to_post_number, :filter_upwards_post_id, :filter_top_level_replies) + opts = + params.slice( + :username_filters, + :filter, + :page, + :post_number, + :show_deleted, + :replies_to_post_number, + :filter_upwards_post_id, + :filter_top_level_replies, + ) username_filters = opts[:username_filters] - opts[:print] = true if params[:print] == 'true' - opts[:username_filters] = username_filters.split(',') if username_filters.is_a?(String) + opts[:print] = true if params[:print] == "true" + opts[:username_filters] = username_filters.split(",") if username_filters.is_a?(String) # Special case: a slug with a number in front should look by slug first before looking # up that particular number @@ -79,7 +91,14 @@ class TopicsController < ApplicationController if opts[:print] raise Discourse::InvalidAccess unless SiteSetting.max_prints_per_hour_per_user > 0 begin - RateLimiter.new(current_user, "print-topic-per-hour", SiteSetting.max_prints_per_hour_per_user, 1.hour).performed! unless @guardian.is_admin? + unless @guardian.is_admin? + RateLimiter.new( + current_user, + "print-topic-per-hour", + SiteSetting.max_prints_per_hour_per_user, + 1.hour, + ).performed! + end rescue RateLimiter::LimitExceeded return render_json_error I18n.t("rate_limiter.slow_down") end @@ -100,37 +119,38 @@ class TopicsController < ApplicationController # If the user can't see the topic, clean up notifications for it. Notification.remove_for(current_user.id, params[:topic_id]) if current_user - deleted = guardian.can_see_topic?(ex.obj, false) || - (!guardian.can_see_topic?(ex.obj) && - ex.obj&.access_topic_via_group && - ex.obj.deleted_at) + deleted = + guardian.can_see_topic?(ex.obj, false) || + (!guardian.can_see_topic?(ex.obj) && ex.obj&.access_topic_via_group && ex.obj.deleted_at) if SiteSetting.detailed_404 if deleted raise Discourse::NotFound.new( - 'deleted topic', - custom_message: 'deleted_topic', - status: 410, - check_permalinks: true, - original_path: ex.obj.relative_url - ) + "deleted topic", + custom_message: "deleted_topic", + status: 410, + check_permalinks: true, + original_path: ex.obj.relative_url, + ) elsif !guardian.can_see_topic?(ex.obj) && group = ex.obj&.access_topic_via_group raise Discourse::InvalidAccess.new( - 'not in group', - ex.obj, - custom_message: 'not_in_group.title_topic', - custom_message_params: { group: group.name }, - group: group - ) + "not in group", + ex.obj, + custom_message: "not_in_group.title_topic", + custom_message_params: { + group: group.name, + }, + group: group, + ) end raise ex else raise Discourse::NotFound.new( - nil, - check_permalinks: deleted, - original_path: ex.obj.relative_url - ) + nil, + check_permalinks: deleted, + original_path: ex.obj.relative_url, + ) end end @@ -152,9 +172,7 @@ class TopicsController < ApplicationController @topic_view.draft = Draft.get(current_user, @topic_view.draft_key, @topic_view.draft_sequence) end - unless @topic_view.topic.visible - response.headers['X-Robots-Tag'] = 'noindex' - end + response.headers["X-Robots-Tag"] = "noindex" unless @topic_view.topic.visible canonical_url UrlHelper.absolute_without_cdn(@topic_view.canonical_path) @@ -162,7 +180,7 @@ class TopicsController < ApplicationController # we would like to give them a bit more signal about age of data if use_crawler_layout? if last_modified = @topic_view.posts&.map { |p| p.updated_at }&.max&.httpdate - response.headers['Last-Modified'] = last_modified + response.headers["Last-Modified"] = last_modified end end @@ -186,7 +204,13 @@ class TopicsController < ApplicationController def wordpress params.require(:best) params.require(:topic_id) - params.permit(:min_trust_level, :min_score, :min_replies, :bypass_trust_level_score, :only_moderator_liked) + params.permit( + :min_trust_level, + :min_score, + :min_replies, + :bypass_trust_level_score, + :only_moderator_liked, + ) opts = { best: params[:best].to_i, @@ -195,13 +219,14 @@ class TopicsController < ApplicationController min_replies: params[:min_replies].to_i, bypass_trust_level_score: params[:bypass_trust_level_score].to_i, # safe cause 0 means ignore only_moderator_liked: params[:only_moderator_liked].to_s == "true", - exclude_hidden: true + exclude_hidden: true, } @topic_view = TopicView.new(params[:topic_id], current_user, opts) discourse_expires_in 1.minute - wordpress_serializer = TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false) + wordpress_serializer = + TopicViewWordpressSerializer.new(@topic_view, scope: guardian, root: false) render_json_dump(wordpress_serializer) end @@ -214,7 +239,7 @@ class TopicsController < ApplicationController filter: params[:filter], skip_limit: true, asc: true, - skip_custom_fields: true + skip_custom_fields: true, } fetch_topic_view(options) @@ -243,8 +268,8 @@ class TopicsController < ApplicationController @topic_view, scope: guardian, root: false, - include_raw: !!params[:include_raw] - ) + include_raw: !!params[:include_raw], + ), ) end @@ -266,25 +291,27 @@ class TopicsController < ApplicationController @topic = Topic.with_deleted.where(id: params[:topic_id]).first guardian.ensure_can_see!(@topic) - @posts = Post.where(hidden: false, deleted_at: nil, topic_id: @topic.id) - .where('posts.id in (?)', post_ids) - .joins("LEFT JOIN users u on u.id = posts.user_id") - .pluck(:id, :cooked, :username, :action_code, :created_at) - .map do |post_id, cooked, username, action_code, created_at| - attrs = { - post_id: post_id, - username: username, - excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true), - } + @posts = + Post + .where(hidden: false, deleted_at: nil, topic_id: @topic.id) + .where("posts.id in (?)", post_ids) + .joins("LEFT JOIN users u on u.id = posts.user_id") + .pluck(:id, :cooked, :username, :action_code, :created_at) + .map do |post_id, cooked, username, action_code, created_at| + attrs = { + post_id: post_id, + username: username, + excerpt: PrettyText.excerpt(cooked, 800, keep_emoji_images: true), + } - if action_code - attrs[:action_code] = action_code - attrs[:created_at] = created_at + if action_code + attrs[:action_code] = action_code + attrs[:created_at] = created_at + end + + attrs end - attrs - end - render json: @posts.to_json end @@ -297,18 +324,14 @@ class TopicsController < ApplicationController PostTiming.destroy_for(current_user.id, [topic_id]) end - last_notification = Notification - .where( - user_id: current_user.id, - topic_id: topic_id - ) - .order(created_at: :desc) - .limit(1) - .first + last_notification = + Notification + .where(user_id: current_user.id, topic_id: topic_id) + .order(created_at: :desc) + .limit(1) + .first - if last_notification - last_notification.update!(read: false) - end + last_notification.update!(read: false) if last_notification render body: nil end @@ -321,9 +344,7 @@ class TopicsController < ApplicationController guardian.ensure_can_publish_topic!(topic, category) row_count = SharedDraft.where(topic_id: topic.id).update_all(category_id: category.id) - if row_count == 0 - SharedDraft.create(topic_id: topic.id, category_id: category.id) - end + SharedDraft.create(topic_id: topic.id, category_id: category.id) if row_count == 0 render json: success_json end @@ -342,15 +363,14 @@ class TopicsController < ApplicationController if category || (params[:category_id].to_i == 0) guardian.ensure_can_move_topic_to_category!(category) else - return render_json_error(I18n.t('category.errors.not_found')) + return render_json_error(I18n.t("category.errors.not_found")) end - if category && topic_tags = (params[:tags] || topic.tags.pluck(:name)).reject { |c| c.empty? } + if category && + topic_tags = (params[:tags] || topic.tags.pluck(:name)).reject { |c| c.empty? } if topic_tags.present? - allowed_tags = DiscourseTagging.filter_allowed_tags( - guardian, - category: category - ).map(&:name) + allowed_tags = + DiscourseTagging.filter_allowed_tags(guardian, category: category).map(&:name) invalid_tags = topic_tags - allowed_tags @@ -367,9 +387,13 @@ class TopicsController < ApplicationController if !invalid_tags.empty? if (invalid_tags & DiscourseTagging.hidden_tag_names).present? - return render_json_error(I18n.t('category.errors.disallowed_tags_generic')) + return render_json_error(I18n.t("category.errors.disallowed_tags_generic")) else - return render_json_error(I18n.t('category.errors.disallowed_topic_tags', tags: invalid_tags.join(", "))) + return( + render_json_error( + I18n.t("category.errors.disallowed_topic_tags", tags: invalid_tags.join(", ")), + ) + ) end end end @@ -379,9 +403,7 @@ class TopicsController < ApplicationController changes = {} - PostRevisor.tracked_topic_fields.each_key do |f| - changes[f] = params[f] if params.has_key?(f) - end + PostRevisor.tracked_topic_fields.each_key { |f| changes[f] = params[f] if params.has_key?(f) } changes.delete(:title) if topic.title == changes[:title] changes.delete(:category_id) if topic.category_id.to_i == changes[:category_id].to_i @@ -397,17 +419,16 @@ class TopicsController < ApplicationController bypass_bump = should_bypass_bump?(changes) first_post = topic.ordered_posts.first - success = PostRevisor.new(first_post, topic).revise!( - current_user, - changes, - validate_post: false, - bypass_bump: bypass_bump, - keep_existing_draft: params[:keep_existing_draft].to_s == "true" - ) + success = + PostRevisor.new(first_post, topic).revise!( + current_user, + changes, + validate_post: false, + bypass_bump: bypass_bump, + keep_existing_draft: params[:keep_existing_draft].to_s == "true", + ) - if !success && topic.errors.blank? - topic.errors.add(:base, :unable_to_update) - end + topic.errors.add(:base, :unable_to_update) if !success && topic.errors.blank? end # this is used to return the title to the client as it may have been changed by "TextCleaner" @@ -419,7 +440,12 @@ class TopicsController < ApplicationController topic = Topic.find_by(id: params[:topic_id]) guardian.ensure_can_edit_tags!(topic) - success = PostRevisor.new(topic.first_post, topic).revise!(current_user, { tags: params[:tags] }, validate_post: false) + success = + PostRevisor.new(topic.first_post, topic).revise!( + current_user, + { tags: params[:tags] }, + validate_post: false, + ) success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic) end @@ -431,10 +457,16 @@ class TopicsController < ApplicationController visible_topics = Topic.listable_topics.visible render json: { - pinned_in_category_count: visible_topics.where(category_id: category_id).where(pinned_globally: false).where.not(pinned_at: nil).count, - pinned_globally_count: visible_topics.where(pinned_globally: true).where.not(pinned_at: nil).count, - banner_count: Topic.listable_topics.where(archetype: Archetype.banner).count, - } + pinned_in_category_count: + visible_topics + .where(category_id: category_id) + .where(pinned_globally: false) + .where.not(pinned_at: nil) + .count, + pinned_globally_count: + visible_topics.where(pinned_globally: true).where.not(pinned_at: nil).count, + banner_count: Topic.listable_topics.where(archetype: Archetype.banner).count, + } end def status @@ -444,33 +476,33 @@ class TopicsController < ApplicationController status = params[:status] topic_id = params[:topic_id].to_i - enabled = params[:enabled] == 'true' + enabled = params[:enabled] == "true" check_for_status_presence(:status, status) @topic = Topic.find_by(id: topic_id) case status - when 'closed' + when "closed" guardian.ensure_can_close_topic!(@topic) - when 'archived' + when "archived" guardian.ensure_can_archive_topic!(@topic) - when 'visible' + when "visible" guardian.ensure_can_toggle_topic_visibility!(@topic) - when 'pinned' + when "pinned" guardian.ensure_can_pin_unpin_topic!(@topic) else guardian.ensure_can_moderate!(@topic) end - params[:until] === '' ? params[:until] = nil : params[:until] + params[:until] === "" ? params[:until] = nil : params[:until] @topic.update_status(status, enabled, current_user, until: params[:until]) - render json: success_json.merge!( - topic_status_update: TopicTimerSerializer.new( - TopicTimer.find_by(topic: @topic), root: false - ) - ) + render json: + success_json.merge!( + topic_status_update: + TopicTimerSerializer.new(TopicTimer.find_by(topic: @topic), root: false), + ) end def mute @@ -488,7 +520,7 @@ class TopicsController < ApplicationController status_type = begin TopicTimer.types.fetch(params[:status_type].to_sym) - rescue + rescue StandardError invalid_param(:status_type) end based_on_last_post = params[:based_on_last_post] @@ -497,37 +529,31 @@ class TopicsController < ApplicationController topic = Topic.find_by(id: params[:topic_id]) guardian.ensure_can_moderate!(topic) - if TopicTimer.destructive_types.values.include?(status_type) - guardian.ensure_can_delete!(topic) - end + guardian.ensure_can_delete!(topic) if TopicTimer.destructive_types.values.include?(status_type) - options = { - by_user: current_user, - based_on_last_post: based_on_last_post - } + options = { by_user: current_user, based_on_last_post: based_on_last_post } options.merge!(category_id: params[:category_id]) if !params[:category_id].blank? - options.merge!(duration_minutes: params[:duration_minutes].to_i) if params[:duration_minutes].present? + if params[:duration_minutes].present? + options.merge!(duration_minutes: params[:duration_minutes].to_i) + end options.merge!(duration: params[:duration].to_i) if params[:duration].present? begin - topic_timer = topic.set_or_create_timer( - status_type, - params[:time], - **options - ) + topic_timer = topic.set_or_create_timer(status_type, params[:time], **options) rescue ActiveRecord::RecordInvalid => e return render_json_error(e.message) end if topic.save - render json: success_json.merge!( - execute_at: topic_timer&.execute_at, - duration_minutes: topic_timer&.duration_minutes, - based_on_last_post: topic_timer&.based_on_last_post, - closed: topic.closed, - category_id: topic_timer&.category_id - ) + render json: + success_json.merge!( + execute_at: topic_timer&.execute_at, + duration_minutes: topic_timer&.duration_minutes, + based_on_last_post: topic_timer&.based_on_last_post, + closed: topic.closed, + category_id: topic_timer&.category_id, + ) else render_json_error(topic) end @@ -572,24 +598,16 @@ class TopicsController < ApplicationController group_ids = current_user.groups.pluck(:id) if group_ids.present? - allowed_groups = topic.allowed_groups - .where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id) + allowed_groups = + topic.allowed_groups.where("topic_allowed_groups.group_id IN (?)", group_ids).pluck(:id) allowed_groups.each do |id| if archive - GroupArchivedMessage.archive!( - id, - topic, - acting_user_id: current_user.id - ) + GroupArchivedMessage.archive!(id, topic, acting_user_id: current_user.id) group_id = id else - GroupArchivedMessage.move_to_inbox!( - id, - topic, - acting_user_id: current_user.id - ) + GroupArchivedMessage.move_to_inbox!(id, topic, acting_user_id: current_user.id) end end end @@ -616,9 +634,7 @@ class TopicsController < ApplicationController bookmark_manager = BookmarkManager.new(current_user) bookmark_manager.create_for(bookmarkable_id: topic.id, bookmarkable_type: "Topic") - if bookmark_manager.errors.any? - return render_json_error(bookmark_manager, status: 400) - end + return render_json_error(bookmark_manager, status: 400) if bookmark_manager.errors.any? render body: nil end @@ -639,7 +655,7 @@ class TopicsController < ApplicationController current_user, topic.ordered_posts.with_deleted.first, context: params[:context], - force_destroy: force_destroy + force_destroy: force_destroy, ).destroy render body: nil @@ -697,15 +713,20 @@ class TopicsController < ApplicationController raise Discourse::NotFound if !topic if !pm_has_slots?(topic) - return render_json_error( - I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients) + return( + render_json_error( + I18n.t( + "pm_reached_recipients_limit", + recipients_limit: SiteSetting.max_allowed_message_recipients, + ), + ) ) end if topic.private_message? guardian.ensure_can_invite_group_to_private_message!(group, topic) topic.invite_group(current_user, group) - render_json_dump BasicGroupSerializer.new(group, scope: guardian, root: 'group') + render_json_dump BasicGroupSerializer.new(group, scope: guardian, root: "group") else render json: failed_json, status: 422 end @@ -715,28 +736,31 @@ class TopicsController < ApplicationController topic = Topic.find_by(id: params[:topic_id]) raise Discourse::NotFound if !topic - if !topic.private_message? - return render_json_error(I18n.t("topic_invite.not_pm")) - end + return render_json_error(I18n.t("topic_invite.not_pm")) if !topic.private_message? if !pm_has_slots?(topic) - return render_json_error( - I18n.t("pm_reached_recipients_limit", recipients_limit: SiteSetting.max_allowed_message_recipients) + return( + render_json_error( + I18n.t( + "pm_reached_recipients_limit", + recipients_limit: SiteSetting.max_allowed_message_recipients, + ), + ) ) end guardian.ensure_can_invite_to!(topic) username_or_email = params[:user] ? fetch_username : fetch_email - group_ids = Group.lookup_groups( - group_ids: params[:group_ids], - group_names: params[:group_names] - ).pluck(:id) + group_ids = + Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names]).pluck( + :id, + ) begin if topic.invite(current_user, username_or_email, group_ids, params[:custom_message]) if user = User.find_by_username_or_email(username_or_email) - render_json_dump BasicUserSerializer.new(user, scope: guardian, root: 'user') + render_json_dump BasicUserSerializer.new(user, scope: guardian, root: "user") else render json: success_json end @@ -744,19 +768,16 @@ class TopicsController < ApplicationController json = failed_json unless topic.private_message? - group_names = topic.category - .visible_group_names(current_user) - .where(automatic: false) - .pluck(:name) - .join(", ") + group_names = + topic + .category + .visible_group_names(current_user) + .where(automatic: false) + .pluck(:name) + .join(", ") if group_names.present? - json.merge!(errors: [ - I18n.t( - "topic_invite.failed_to_invite", - group_names: group_names - ) - ]) + json.merge!(errors: [I18n.t("topic_invite.failed_to_invite", group_names: group_names)]) end end @@ -792,7 +813,8 @@ class TopicsController < ApplicationController if params[:archetype].present? args[:archetype] = params[:archetype] - args[:participants] = params[:participants] if params[:participants].present? && params[:archetype] == "private_message" + args[:participants] = params[:participants] if params[:participants].present? && + params[:archetype] == "private_message" end destination_topic = topic.move_posts(current_user, topic.posts.pluck(:id), args) @@ -814,8 +836,13 @@ class TopicsController < ApplicationController if params[:title].present? # when creating a new topic, ensure the 1st post is a regular post - if Post.where(topic: topic, id: post_ids).order(:post_number).pluck_first(:post_type) != Post.types[:regular] - return render_json_error("When moving posts to a new topic, the first post must be a regular post.") + if Post.where(topic: topic, id: post_ids).order(:post_number).pluck_first(:post_type) != + Post.types[:regular] + return( + render_json_error( + "When moving posts to a new topic, the first post must be a regular post.", + ) + ) end if params[:category_id].present? @@ -837,10 +864,12 @@ class TopicsController < ApplicationController guardian.ensure_can_change_post_owner! begin - PostOwnerChanger.new(post_ids: params[:post_ids].to_a, - topic_id: params[:topic_id].to_i, - new_owner: User.find_by(username: params[:username]), - acting_user: current_user).change_owner! + PostOwnerChanger.new( + post_ids: params[:post_ids].to_a, + topic_id: params[:topic_id].to_i, + new_owner: User.find_by(username: params[:username]), + acting_user: current_user, + ).change_owner! render json: success_json rescue ArgumentError render json: failed_json, status: 422 @@ -857,12 +886,13 @@ class TopicsController < ApplicationController previous_timestamp = topic.first_post.created_at begin - TopicTimestampChanger.new( - topic: topic, - timestamp: timestamp - ).change! + TopicTimestampChanger.new(topic: topic, timestamp: timestamp).change! - StaffActionLogger.new(current_user).log_topic_timestamps_changed(topic, Time.zone.at(timestamp), previous_timestamp) + StaffActionLogger.new(current_user).log_topic_timestamps_changed( + topic, + Time.zone.at(timestamp), + previous_timestamp, + ) render json: success_json rescue ActiveRecord::RecordInvalid, TopicTimestampChanger::InvalidTimestampError @@ -900,7 +930,7 @@ class TopicsController < ApplicationController topic_id, topic_time, timings.map { |post_number, t| [post_number.to_i, t.to_i] }, - mobile: view_context.mobile_view? + mobile: view_context.mobile_view?, ) render body: nil end @@ -914,43 +944,48 @@ class TopicsController < ApplicationController rescue Discourse::NotLoggedIn raise Discourse::NotFound rescue Discourse::InvalidAccess => ex - - deleted = guardian.can_see_topic?(ex.obj, false) || - (!guardian.can_see_topic?(ex.obj) && - ex.obj&.access_topic_via_group && - ex.obj.deleted_at) + deleted = + guardian.can_see_topic?(ex.obj, false) || + (!guardian.can_see_topic?(ex.obj) && ex.obj&.access_topic_via_group && ex.obj.deleted_at) raise Discourse::NotFound.new( - nil, - check_permalinks: deleted, - original_path: ex.obj.relative_url - ) + nil, + check_permalinks: deleted, + original_path: ex.obj.relative_url, + ) end discourse_expires_in 1.minute - response.headers['X-Robots-Tag'] = 'noindex, nofollow' - render 'topics/show', formats: [:rss] + response.headers["X-Robots-Tag"] = "noindex, nofollow" + render "topics/show", formats: [:rss] end def bulk if params[:topic_ids].present? unless Array === params[:topic_ids] - raise Discourse::InvalidParameters.new( - "Expecting topic_ids to contain a list of topic ids" - ) + raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids") end topic_ids = params[:topic_ids].map { |t| t.to_i } - elsif params[:filter] == 'unread' + elsif params[:filter] == "unread" topic_ids = bulk_unread_topic_ids else raise ActionController::ParameterMissing.new(:topic_ids) end - operation = params - .require(:operation) - .permit(:type, :group, :category_id, :notification_level_id, *DiscoursePluginRegistry.permitted_bulk_action_parameters, tags: []) - .to_h.symbolize_keys + operation = + params + .require(:operation) + .permit( + :type, + :group, + :category_id, + :notification_level_id, + *DiscoursePluginRegistry.permitted_bulk_action_parameters, + tags: [], + ) + .to_h + .symbolize_keys raise ActionController::ParameterMissing.new(:operation_type) if operation[:type].blank? operator = TopicsBulkAction.new(current_user, topic_ids, operation, group: operation[:group]) @@ -963,14 +998,14 @@ class TopicsController < ApplicationController if params[:topic_ids].present? unless Array === params[:topic_ids] - raise Discourse::InvalidParameters.new( - "Expecting topic_ids to contain a list of topic ids" - ) + raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids") end - topic_scope = topic_query - .private_messages_for(current_user, :all) - .where("topics.id IN (?)", params[:topic_ids].map(&:to_i)) + topic_scope = + topic_query.private_messages_for(current_user, :all).where( + "topics.id IN (?)", + params[:topic_ids].map(&:to_i), + ) else params.require(:inbox) inbox = params[:inbox].to_s @@ -978,11 +1013,8 @@ class TopicsController < ApplicationController topic_scope = topic_query.filter_private_message_new(current_user, filter) end - topic_ids = TopicsBulkAction.new( - current_user, - topic_scope.pluck(:id), - type: "dismiss_topics" - ).perform! + topic_ids = + TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform! render json: success_json.merge(topic_ids: topic_ids) end @@ -991,8 +1023,9 @@ class TopicsController < ApplicationController topic_scope = if params[:category_id].present? category_ids = [params[:category_id]] - if params[:include_subcategories] == 'true' - category_ids = category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id)) + if params[:include_subcategories] == "true" + category_ids = + category_ids.concat(Category.where(parent_category_id: params[:category_id]).pluck(:id)) end scope = Topic.where(category_id: category_ids) @@ -1012,16 +1045,15 @@ class TopicsController < ApplicationController if params[:topic_ids].present? unless Array === params[:topic_ids] - raise Discourse::InvalidParameters.new( - "Expecting topic_ids to contain a list of topic ids" - ) + raise Discourse::InvalidParameters.new("Expecting topic_ids to contain a list of topic ids") end topic_ids = params[:topic_ids].map { |t| t.to_i } topic_scope = topic_scope.where(id: topic_ids) end - dismissed_topic_ids = TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform! + dismissed_topic_ids = + TopicsBulkAction.new(current_user, topic_scope.pluck(:id), type: "dismiss_topics").perform! TopicTrackingState.publish_dismiss_new(current_user.id, topic_ids: dismissed_topic_ids) render body: nil @@ -1034,7 +1066,8 @@ class TopicsController < ApplicationController guardian.ensure_can_convert_topic!(topic) if params[:type] == "public" - converted_topic = topic.convert_to_public_topic(current_user, category_id: params[:category_id]) + converted_topic = + topic.convert_to_public_topic(current_user, category_id: params[:category_id]) else converted_topic = topic.convert_to_private_message(current_user) end @@ -1065,11 +1098,7 @@ class TopicsController < ApplicationController time = enabled && params[:enabled_until].present? ? params[:enabled_until] : nil - topic.set_or_create_timer( - slow_mode_type, - time, - by_user: timer&.user - ) + topic.set_or_create_timer(slow_mode_type, time, by_user: timer&.user) head :ok end @@ -1077,16 +1106,12 @@ class TopicsController < ApplicationController private def topic_params - params.permit( - :topic_id, - :topic_time, - timings: {} - ) + params.permit(:topic_id, :topic_time, timings: {}) end def fetch_topic_view(options) if (username_filters = params[:username_filters]).present? - options[:username_filters] = username_filters.split(',') + options[:username_filters] = username_filters.split(",") end @topic_view = TopicView.new(params[:topic_id], current_user, options) @@ -1132,7 +1157,7 @@ class TopicsController < ApplicationController url << ".json" if request.format.json? opts.each do |k, v| - s = url.include?('?') ? '&' : '?' + s = url.include?("?") ? "&" : "?" url << "#{s}#{k}=#{v}" end @@ -1140,7 +1165,7 @@ class TopicsController < ApplicationController end def track_visit_to_topic - topic_id = @topic_view.topic.id + topic_id = @topic_view.topic.id ip = request.remote_ip user_id = (current_user.id if current_user) track_visit = should_track_visit_to_topic? @@ -1152,8 +1177,8 @@ class TopicsController < ApplicationController current_user: current_user, topic_id: @topic_view.topic.id, post_number: @topic_view.current_post_number, - username: request['u'], - ip_address: request.remote_ip + username: request["u"], + ip_address: request.remote_ip, } # defer this way so we do not capture the whole controller # in the closure @@ -1181,32 +1206,33 @@ class TopicsController < ApplicationController end def perform_show_response - if request.head? head :ok return end - topic_view_serializer = TopicViewSerializer.new( - @topic_view, - scope: guardian, - root: false, - include_raw: !!params[:include_raw], - exclude_suggested_and_related: !!params[:replies_to_post_number] || !!params[:filter_upwards_post_id] || !!params[:filter_top_level_replies] - ) + topic_view_serializer = + TopicViewSerializer.new( + @topic_view, + scope: guardian, + root: false, + include_raw: !!params[:include_raw], + exclude_suggested_and_related: + !!params[:replies_to_post_number] || !!params[:filter_upwards_post_id] || + !!params[:filter_top_level_replies], + ) respond_to do |format| format.html do @tags = SiteSetting.tagging_enabled ? @topic_view.topic.tags : [] @breadcrumbs = helpers.categories_breadcrumb(@topic_view.topic) || [] - @description_meta = @topic_view.topic.excerpt.present? ? @topic_view.topic.excerpt : @topic_view.summary + @description_meta = + @topic_view.topic.excerpt.present? ? @topic_view.topic.excerpt : @topic_view.summary store_preloaded("topic_#{@topic_view.topic.id}", MultiJson.dump(topic_view_serializer)) render :show end - format.json do - render_json_dump(topic_view_serializer) - end + format.json { render_json_dump(topic_view_serializer) } end end @@ -1221,12 +1247,15 @@ class TopicsController < ApplicationController def move_posts_to_destination(topic) args = {} args[:title] = params[:title] if params[:title].present? - args[:destination_topic_id] = params[:destination_topic_id].to_i if params[:destination_topic_id].present? + args[:destination_topic_id] = params[:destination_topic_id].to_i if params[ + :destination_topic_id + ].present? args[:tags] = params[:tags] if params[:tags].present? if params[:archetype].present? args[:archetype] = params[:archetype] - args[:participants] = params[:participants] if params[:participants].present? && params[:archetype] == "private_message" + args[:participants] = params[:participants] if params[:participants].present? && + params[:archetype] == "private_message" else args[:category_id] = params[:category_id].to_i if params[:category_id].present? end @@ -1235,7 +1264,7 @@ class TopicsController < ApplicationController end def check_for_status_presence(key, attr) - invalid_param(key) unless %w(pinned pinned_globally visible closed archived).include?(attr) + invalid_param(key) unless %w[pinned pinned_globally visible closed archived].include?(attr) end def invalid_param(key) @@ -1264,7 +1293,11 @@ class TopicsController < ApplicationController topic_query.options[:limit] = false topics = topic_query.filter_private_messages_unread(current_user, filter) else - topics = TopicQuery.unread_filter(topic_query.joined_topic_user, whisperer: guardian.is_whisperer?).listable_topics + topics = + TopicQuery.unread_filter( + topic_query.joined_topic_user, + whisperer: guardian.is_whisperer?, + ).listable_topics topics = TopicQuery.tracked_filter(topics, current_user.id) if params[:tracked].to_s == "true" if params[:category_id] @@ -1274,7 +1307,7 @@ class TopicsController < ApplicationController category_id = :category_id SQL else - topics = topics.where('category_id = ?', params[:category_id]) + topics = topics.where("category_id = ?", params[:category_id]) end end diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index f1041f57bc..218b5ae88d 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -5,13 +5,18 @@ require "mini_mime" class UploadsController < ApplicationController include ExternalUploadHelpers - requires_login except: [:show, :show_short, :_show_secure_deprecated, :show_secure] + requires_login except: %i[show show_short _show_secure_deprecated show_secure] - skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required, only: [:show, :show_short, :_show_secure_deprecated, :show_secure] + skip_before_action :preload_json, + :check_xhr, + :redirect_to_login_if_required, + only: %i[show show_short _show_secure_deprecated show_secure] protect_from_forgery except: :show - before_action :is_asset_path, :apply_cdn_headers, only: [:show, :show_short, :_show_secure_deprecated, :show_secure] - before_action :external_store_check, only: [:_show_secure_deprecated, :show_secure] + before_action :is_asset_path, + :apply_cdn_headers, + only: %i[show show_short _show_secure_deprecated show_secure] + before_action :external_store_check, only: %i[_show_secure_deprecated show_secure] SECURE_REDIRECT_GRACE_SECONDS = 5 @@ -20,18 +25,21 @@ class UploadsController < ApplicationController me = current_user params.permit(:type, :upload_type) - if params[:type].blank? && params[:upload_type].blank? - raise Discourse::InvalidParameters - end + raise Discourse::InvalidParameters if params[:type].blank? && params[:upload_type].blank? # 50 characters ought to be enough for the upload type - type = (params[:upload_type].presence || params[:type].presence).parameterize(separator: "_")[0..50] + type = + (params[:upload_type].presence || params[:type].presence).parameterize(separator: "_")[0..50] - if type == "avatar" && !me.admin? && (SiteSetting.discourse_connect_overrides_avatar || !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, me)) + if type == "avatar" && !me.admin? && + ( + SiteSetting.discourse_connect_overrides_avatar || + !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, me) + ) return render json: failed_json, status: 422 end - url = params[:url] - file = params[:file] || params[:files]&.first + url = params[:url] + file = params[:file] || params[:files]&.first pasted = params[:pasted] == "true" for_private_message = params[:for_private_message] == "true" for_site_setting = params[:for_site_setting] == "true" @@ -42,17 +50,18 @@ class UploadsController < ApplicationController # longer term we may change this hijack do begin - info = UploadsController.create_upload( - current_user: me, - file: file, - url: url, - type: type, - for_private_message: for_private_message, - for_site_setting: for_site_setting, - pasted: pasted, - is_api: is_api, - retain_hours: retain_hours - ) + info = + UploadsController.create_upload( + current_user: me, + file: file, + url: url, + type: type, + for_private_message: for_private_message, + for_site_setting: for_site_setting, + pasted: pasted, + is_api: is_api, + retain_hours: retain_hours, + ) rescue => e render json: failed_json.merge(message: e.message&.split("\n")&.first), status: 422 else @@ -66,13 +75,11 @@ class UploadsController < ApplicationController uploads = [] if (params[:short_urls] && params[:short_urls].length > 0) - PrettyText::Helpers.lookup_upload_urls(params[:short_urls]).each do |short_url, paths| - uploads << { - short_url: short_url, - url: paths[:url], - short_path: paths[:short_path] - } - end + PrettyText::Helpers + .lookup_upload_urls(params[:short_urls]) + .each do |short_url, paths| + uploads << { short_url: short_url, url: paths[:url], short_path: paths[:short_path] } + end end render json: uploads.to_json @@ -87,7 +94,9 @@ class UploadsController < ApplicationController RailsMultisite::ConnectionManagement.with_connection(params[:site]) do |db| return render_404 if SiteSetting.prevent_anons_from_downloading_files && current_user.nil? - if upload = Upload.find_by(sha1: params[:sha]) || Upload.find_by(id: params[:id], url: request.env["PATH_INFO"]) + if upload = + Upload.find_by(sha1: params[:sha]) || + Upload.find_by(id: params[:id], url: request.env["PATH_INFO"]) unless Discourse.store.internal? local_store = FileStore::LocalStore.new return render_404 unless local_store.has_been_uploaded?(upload.url) @@ -104,21 +113,18 @@ class UploadsController < ApplicationController # do not serve uploads requested via XHR to prevent XSS return xhr_not_allowed if request.xhr? - if SiteSetting.prevent_anons_from_downloading_files && current_user.nil? - return render_404 - end + return render_404 if SiteSetting.prevent_anons_from_downloading_files && current_user.nil? sha1 = Upload.sha1_from_base62_encoded(params[:base62]) if upload = Upload.find_by(sha1: sha1) - if upload.secure? && SiteSetting.secure_uploads? - return handle_secure_upload_request(upload) - end + return handle_secure_upload_request(upload) if upload.secure? && SiteSetting.secure_uploads? if Discourse.store.internal? send_file_local_upload(upload) else - redirect_to Discourse.store.url_for(upload, force_download: force_download?), allow_other_host: true + redirect_to Discourse.store.url_for(upload, force_download: force_download?), + allow_other_host: true end else render_404 @@ -156,7 +162,8 @@ class UploadsController < ApplicationController # private, so we don't want to go to the CDN url just yet otherwise we # will get a 403. if the upload is not secure we assume the ACL is public signed_secure_url = Discourse.store.signed_url_for_path(path_with_ext) - redirect_to upload.secure? ? signed_secure_url : Discourse.store.cdn_url(upload.url), allow_other_host: true + redirect_to upload.secure? ? signed_secure_url : Discourse.store.cdn_url(upload.url), + allow_other_host: true end def handle_secure_upload_request(upload, path_with_ext = nil) @@ -167,20 +174,25 @@ class UploadsController < ApplicationController end # defaults to public: false, so only cached by the client browser - cache_seconds = SiteSetting.s3_presigned_get_url_expires_after_seconds - SECURE_REDIRECT_GRACE_SECONDS + cache_seconds = + SiteSetting.s3_presigned_get_url_expires_after_seconds - SECURE_REDIRECT_GRACE_SECONDS expires_in cache_seconds.seconds # url_for figures out the full URL, handling multisite DBs, # and will return a presigned URL for the upload if path_with_ext.blank? - return redirect_to Discourse.store.url_for(upload, force_download: force_download?), allow_other_host: true + return( + redirect_to Discourse.store.url_for(upload, force_download: force_download?), + allow_other_host: true + ) end redirect_to Discourse.store.signed_url_for_path( - path_with_ext, - expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, - force_download: force_download? - ), allow_other_host: true + path_with_ext, + expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, + force_download: force_download?, + ), + allow_other_host: true end def metadata @@ -189,11 +201,11 @@ class UploadsController < ApplicationController raise Discourse::NotFound unless upload render json: { - original_filename: upload.original_filename, - width: upload.width, - height: upload.height, - human_filesize: upload.human_filesize - } + original_filename: upload.original_filename, + width: upload.width, + height: upload.height, + human_filesize: upload.human_filesize, + } end protected @@ -207,17 +219,18 @@ class UploadsController < ApplicationController end def validate_file_size(file_name:, file_size:) - if file_size.zero? - raise ExternalUploadValidationError.new(I18n.t("upload.size_zero_failure")) - end + raise ExternalUploadValidationError.new(I18n.t("upload.size_zero_failure")) if file_size.zero? if file_size_too_big?(file_name, file_size) raise ExternalUploadValidationError.new( - I18n.t( - "upload.attachments.too_large_humanized", - max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_attachment_size_kb.kilobytes) - ) - ) + I18n.t( + "upload.attachments.too_large_humanized", + max_size: + ActiveSupport::NumberHelper.number_to_human_size( + SiteSetting.max_attachment_size_kb.kilobytes, + ), + ), + ) end end @@ -236,25 +249,34 @@ class UploadsController < ApplicationController serialized ||= (data || {}).as_json end - def self.create_upload(current_user:, - file:, - url:, - type:, - for_private_message:, - for_site_setting:, - pasted:, - is_api:, - retain_hours:) - + def self.create_upload( + current_user:, + file:, + url:, + type:, + for_private_message:, + for_site_setting:, + pasted:, + is_api:, + retain_hours: + ) if file.nil? if url.present? && is_api - maximum_upload_size = [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes - tempfile = FileHelper.download( - url, - follow_redirect: true, - max_file_size: maximum_upload_size, - tmp_file_name: "discourse-upload-#{type}" - ) rescue nil + maximum_upload_size = [ + SiteSetting.max_image_size_kb, + SiteSetting.max_attachment_size_kb, + ].max.kilobytes + tempfile = + begin + FileHelper.download( + url, + follow_redirect: true, + max_file_size: maximum_upload_size, + tmp_file_name: "discourse-upload-#{type}", + ) + rescue StandardError + nil + end filename = File.basename(URI.parse(url).path) end else @@ -288,13 +310,14 @@ class UploadsController < ApplicationController # as they may be further reduced in size by UploadCreator (at this point # they may have already been reduced in size by preprocessors) def file_size_too_big?(file_name, file_size) - !FileHelper.is_supported_image?(file_name) && file_size >= SiteSetting.max_attachment_size_kb.kilobytes + !FileHelper.is_supported_image?(file_name) && + file_size >= SiteSetting.max_attachment_size_kb.kilobytes end def send_file_local_upload(upload) opts = { filename: upload.original_filename, - content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type + content_type: MiniMime.lookup_by_filename(upload.original_filename)&.content_type, } if !FileHelper.is_inline_image?(upload.original_filename) @@ -313,7 +336,11 @@ class UploadsController < ApplicationController begin yield rescue Aws::S3::Errors::ServiceError => err - message = debug_upload_error(err, I18n.t("upload.create_multipart_failure", additional_detail: err.message)) + message = + debug_upload_error( + err, + I18n.t("upload.create_multipart_failure", additional_detail: err.message), + ) raise ExternalUploadHelpers::ExternalUploadValidationError.new(message) end end diff --git a/app/controllers/user_actions_controller.rb b/app/controllers/user_actions_controller.rb index 18efefa707..24326698bb 100644 --- a/app/controllers/user_actions_controller.rb +++ b/app/controllers/user_actions_controller.rb @@ -4,7 +4,11 @@ class UserActionsController < ApplicationController def index user_actions_params.require(:username) - user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts)) + user = + fetch_user_from_params( + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + ) offset = [0, user_actions_params[:offset].to_i].max action_types = (user_actions_params[:filter] || "").split(",").map(&:to_i) limit = user_actions_params.fetch(:limit, 30).to_i @@ -20,11 +24,11 @@ class UserActionsController < ApplicationController action_types: action_types, guardian: guardian, ignore_private_messages: params[:filter].blank?, - acting_username: params[:acting_username] + acting_username: params[:acting_username], } stream = UserAction.stream(opts).to_a - render_serialized(stream, UserActionSerializer, root: 'user_actions') + render_serialized(stream, UserActionSerializer, root: "user_actions") end def show diff --git a/app/controllers/user_api_keys_controller.rb b/app/controllers/user_api_keys_controller.rb index 0a34a33d65..2c38f7e8fd 100644 --- a/app/controllers/user_api_keys_controller.rb +++ b/app/controllers/user_api_keys_controller.rb @@ -1,17 +1,15 @@ # frozen_string_literal: true class UserApiKeysController < ApplicationController + layout "no_ember" - layout 'no_ember' - - requires_login only: [:create, :create_otp, :revoke, :undo_revoke] - skip_before_action :redirect_to_login_if_required, only: [:new, :otp] + requires_login only: %i[create create_otp revoke undo_revoke] + skip_before_action :redirect_to_login_if_required, only: %i[new otp] skip_before_action :check_xhr, :preload_json AUTH_API_VERSION ||= 4 def new - if request.head? head :ok, auth_api_version: AUTH_API_VERSION return @@ -24,9 +22,9 @@ class UserApiKeysController < ApplicationController cookies[:destination_url] = request.fullpath if SiteSetting.enable_discourse_connect? - redirect_to path('/session/sso') + redirect_to path("/session/sso") else - redirect_to path('/login') + redirect_to path("/login") end return end @@ -44,13 +42,11 @@ class UserApiKeysController < ApplicationController @push_url = params[:push_url] @localized_scopes = params[:scopes].split(",").map { |s| I18n.t("user_api_key.scopes.#{s}") } @scopes = params[:scopes] - rescue Discourse::InvalidAccess @generic_error = true end def create - require_params if params.key?(:auth_redirect) @@ -66,13 +62,14 @@ class UserApiKeysController < ApplicationController # destroy any old keys we had UserApiKey.where(user_id: current_user.id, client_id: params[:client_id]).destroy_all - key = UserApiKey.create!( - application_name: @application_name, - client_id: params[:client_id], - user_id: current_user.id, - push_url: params[:push_url], - scopes: scopes.map { |name| UserApiKeyScope.new(name: name) } - ) + key = + UserApiKey.create!( + application_name: @application_name, + client_id: params[:client_id], + user_id: current_user.id, + push_url: params[:push_url], + scopes: scopes.map { |name| UserApiKeyScope.new(name: name) }, + ) # we keep the payload short so it encrypts easily with public key # it is often restricted to 128 chars @@ -80,7 +77,7 @@ class UserApiKeysController < ApplicationController key: key.key, nonce: params[:nonce], push: key.has_push?, - api: AUTH_API_VERSION + api: AUTH_API_VERSION, }.to_json public_key = OpenSSL::PKey::RSA.new(params[:public_key]) @@ -94,8 +91,10 @@ class UserApiKeysController < ApplicationController if params[:auth_redirect] uri = URI.parse(params[:auth_redirect]) query_attributes = [uri.query, "payload=#{CGI.escape(@payload)}"] - query_attributes << "oneTimePassword=#{CGI.escape(otp_payload)}" if scopes.include?("one_time_password") - uri.query = query_attributes.compact.join('&') + if scopes.include?("one_time_password") + query_attributes << "oneTimePassword=#{CGI.escape(otp_payload)}" + end + uri.query = query_attributes.compact.join("&") redirect_to(uri.to_s, allow_other_host: true) else @@ -116,9 +115,9 @@ class UserApiKeysController < ApplicationController cookies[:destination_url] = request.fullpath if SiteSetting.enable_discourse_connect? - redirect_to path('/session/sso') + redirect_to path("/session/sso") else - redirect_to path('/login') + redirect_to path("/login") end return end @@ -144,7 +143,7 @@ class UserApiKeysController < ApplicationController def revoke revoke_key = find_key if params[:id] - if current_key = request.env['HTTP_USER_API_KEY'] + if current_key = request.env["HTTP_USER_API_KEY"] request_key = UserApiKey.with_key(current_key).first revoke_key ||= request_key end @@ -168,13 +167,7 @@ class UserApiKeysController < ApplicationController end def require_params - [ - :public_key, - :nonce, - :scopes, - :client_id, - :application_name - ].each { |p| params.require(p) } + %i[public_key nonce scopes client_id application_name].each { |p| params.require(p) } end def validate_params @@ -186,11 +179,7 @@ class UserApiKeysController < ApplicationController end def require_params_otp - [ - :public_key, - :auth_redirect, - :application_name - ].each { |p| params.require(p) } + %i[public_key auth_redirect application_name].each { |p| params.require(p) } end def meets_tl? @@ -198,7 +187,9 @@ class UserApiKeysController < ApplicationController end def one_time_password(public_key, username) - raise Discourse::InvalidAccess unless UserApiKey.allowed_scopes.superset?(Set.new(["one_time_password"])) + unless UserApiKey.allowed_scopes.superset?(Set.new(["one_time_password"])) + raise Discourse::InvalidAccess + end otp = SecureRandom.hex Discourse.redis.setex "otp_#{otp}", 10.minutes, username diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index 7b1ea6afbf..abf2408313 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true class UserAvatarsController < ApplicationController + skip_before_action :preload_json, + :redirect_to_login_if_required, + :check_xhr, + :verify_authenticity_token, + only: %i[show show_letter show_proxy_letter] - skip_before_action :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter, :show_proxy_letter] - - before_action :apply_cdn_headers, only: [:show, :show_letter, :show_proxy_letter] + before_action :apply_cdn_headers, only: %i[show show_letter show_proxy_letter] def refresh_gravatar user = User.find_by(username_lower: params[:username].downcase) @@ -15,17 +18,16 @@ class UserAvatarsController < ApplicationController user.create_user_avatar(user_id: user.id) unless user.user_avatar user.user_avatar.update_gravatar! - gravatar = if user.user_avatar.gravatar_upload_id - { - gravatar_upload_id: user.user_avatar.gravatar_upload_id, - gravatar_avatar_template: User.avatar_template(user.username, user.user_avatar.gravatar_upload_id) - } - else - { - gravatar_upload_id: nil, - gravatar_avatar_template: nil - } - end + gravatar = + if user.user_avatar.gravatar_upload_id + { + gravatar_upload_id: user.user_avatar.gravatar_upload_id, + gravatar_avatar_template: + User.avatar_template(user.username, user.user_avatar.gravatar_upload_id), + } + else + { gravatar_upload_id: nil, gravatar_avatar_template: nil } + end render json: gravatar end @@ -37,7 +39,7 @@ class UserAvatarsController < ApplicationController def show_proxy_letter is_asset_path - if SiteSetting.external_system_avatars_url !~ /^\/letter_avatar_proxy/ + if SiteSetting.external_system_avatars_url !~ %r{^/letter_avatar_proxy} raise Discourse::NotFound end @@ -48,7 +50,10 @@ class UserAvatarsController < ApplicationController hijack do begin - proxy_avatar("https://avatars.discourse-cdn.com/#{params[:version]}/letter/#{params[:letter]}/#{params[:color]}/#{params[:size]}.png", Time.new(1990, 01, 01)) + proxy_avatar( + "https://avatars.discourse-cdn.com/#{params[:version]}/letter/#{params[:letter]}/#{params[:color]}/#{params[:size]}.png", + Time.new(1990, 01, 01), + ) rescue OpenURI::HTTPError render_blank end @@ -81,16 +86,13 @@ class UserAvatarsController < ApplicationController # we need multisite support to keep a single origin pull for CDNs RailsMultisite::ConnectionManagement.with_hostname(params[:hostname]) do - hijack do - show_in_site(params[:hostname]) - end + hijack { show_in_site(params[:hostname]) } end end protected def show_in_site(hostname) - username = params[:username].to_s return render_blank unless user = User.find_by(username_lower: username.downcase) @@ -99,9 +101,7 @@ class UserAvatarsController < ApplicationController version = (version || OptimizedImage::VERSION).to_i # old versions simply get new avatar - if version > OptimizedImage::VERSION - return render_blank - end + return render_blank if version > OptimizedImage::VERSION upload_id = upload_id.to_i return render_blank unless upload_id > 0 @@ -111,7 +111,13 @@ class UserAvatarsController < ApplicationController if !Discourse.avatar_sizes.include?(size) && Discourse.store.external? closest = Discourse.avatar_sizes.to_a.min { |a, b| (size - a).abs <=> (size - b).abs } - avatar_url = UserAvatar.local_avatar_url(hostname, user.encoded_username(lower: true), upload_id, closest) + avatar_url = + UserAvatar.local_avatar_url( + hostname, + user.encoded_username(lower: true), + upload_id, + closest, + ) return redirect_to cdn_path(avatar_url), allow_other_host: true end @@ -119,7 +125,13 @@ class UserAvatarsController < ApplicationController upload ||= user.uploaded_avatar if user.uploaded_avatar_id == upload_id if user.uploaded_avatar && !upload - avatar_url = UserAvatar.local_avatar_url(hostname, user.encoded_username(lower: true), user.uploaded_avatar_id, size) + avatar_url = + UserAvatar.local_avatar_url( + hostname, + user.encoded_username(lower: true), + user.uploaded_avatar_id, + size, + ) return redirect_to cdn_path(avatar_url), allow_other_host: true elsif upload && optimized = get_optimized_image(upload, size) if optimized.local? @@ -151,10 +163,7 @@ class UserAvatarsController < ApplicationController PROXY_PATH = Rails.root + "tmp/avatar_proxy" def proxy_avatar(url, last_modified) - - if url[0..1] == "//" - url = (SiteSetting.force_https ? "https:" : "http:") + url - end + url = (SiteSetting.force_https ? "https:" : "http:") + url if url[0..1] == "//" sha = Digest::SHA1.hexdigest(url) filename = "#{sha}#{File.extname(url)}" @@ -162,13 +171,14 @@ class UserAvatarsController < ApplicationController unless File.exist? path FileUtils.mkdir_p PROXY_PATH - tmp = FileHelper.download( - url, - max_file_size: max_file_size, - tmp_file_name: filename, - follow_redirect: true, - read_timeout: 10 - ) + tmp = + FileHelper.download( + url, + max_file_size: max_file_size, + tmp_file_name: filename, + follow_redirect: true, + read_timeout: 10, + ) return render_blank if tmp.nil? @@ -206,5 +216,4 @@ class UserAvatarsController < ApplicationController upload.get_optimized_image(size, size) # TODO decide if we want to detach here end - end diff --git a/app/controllers/user_badges_controller.rb b/app/controllers/user_badges_controller.rb index d87504d9a2..1617f55516 100644 --- a/app/controllers/user_badges_controller.rb +++ b/app/controllers/user_badges_controller.rb @@ -6,11 +6,18 @@ class UserBadgesController < ApplicationController before_action :ensure_badges_enabled def index - params.permit [:granted_before, :offset, :username] + params.permit %i[granted_before offset username] badge = fetch_badge_from_params - user_badges = badge.user_badges.order('granted_at DESC, id DESC').limit(MAX_BADGES) - user_badges = user_badges.includes(:user, :granted_by, badge: :badge_type, post: :topic, user: [:primary_group, :flair_group]) + user_badges = badge.user_badges.order("granted_at DESC, id DESC").limit(MAX_BADGES) + user_badges = + user_badges.includes( + :user, + :granted_by, + badge: :badge_type, + post: :topic, + user: %i[primary_group flair_group], + ) grant_count = nil @@ -26,33 +33,40 @@ class UserBadgesController < ApplicationController user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact - user_badges = UserBadges.new(user_badges: user_badges, - username: params[:username], - grant_count: grant_count) + user_badges = + UserBadges.new( + user_badges: user_badges, + username: params[:username], + grant_count: grant_count, + ) render_serialized( user_badges, UserBadgesSerializer, root: :user_badge_info, include_long_description: true, - allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids) + allowed_user_badge_topic_ids: guardian.can_see_topic_ids(topic_ids: user_badges_topic_ids), ) end def username params.permit [:grouped] - user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts)) + user = + fetch_user_from_params( + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + ) raise Discourse::NotFound unless guardian.can_see_profile?(user) user_badges = user.user_badges - if params[:grouped] - user_badges = user_badges.group(:badge_id).select_for_grouping - end + user_badges = user_badges.group(:badge_id).select_for_grouping if params[:grouped] - user_badges = user_badges.includes(badge: [:badge_grouping, :badge_type, :image_upload]) - .includes(post: :topic) - .includes(:granted_by) + user_badges = + user_badges + .includes(badge: %i[badge_grouping badge_type image_upload]) + .includes(post: :topic) + .includes(:granted_by) user_badges_topic_ids = user_badges.map { |user_badge| user_badge.post&.topic_id }.compact @@ -68,16 +82,17 @@ class UserBadgesController < ApplicationController params.require(:username) user = fetch_user_from_params - unless can_assign_badge_to_user?(user) - return render json: failed_json, status: 403 - end + return render json: failed_json, status: 403 unless can_assign_badge_to_user?(user) badge = fetch_badge_from_params post_id = nil if params[:reason].present? unless is_badge_reason_valid? params[:reason] - return render json: failed_json.merge(message: I18n.t('invalid_grant_badge_reason_link')), status: 400 + return( + render json: failed_json.merge(message: I18n.t("invalid_grant_badge_reason_link")), + status: 400 + ) end if route = Discourse.route_for(params[:reason]) @@ -112,17 +127,17 @@ class UserBadgesController < ApplicationController user_badge = UserBadge.find(params[:user_badge_id]) user_badges = user_badge.user.user_badges - unless can_favorite_badge?(user_badge) - return render json: failed_json, status: 403 - end + return render json: failed_json, status: 403 unless can_favorite_badge?(user_badge) - if !user_badge.is_favorite && user_badges.select(:badge_id).distinct.where(is_favorite: true).count >= SiteSetting.max_favorite_badges + if !user_badge.is_favorite && + user_badges.select(:badge_id).distinct.where(is_favorite: true).count >= + SiteSetting.max_favorite_badges return render json: failed_json, status: 400 end - UserBadge - .where(user_id: user_badge.user_id, badge_id: user_badge.badge_id) - .update_all(is_favorite: !user_badge.is_favorite) + UserBadge.where(user_id: user_badge.user_id, badge_id: user_badge.badge_id).update_all( + is_favorite: !user_badge.is_favorite, + ) UserBadge.update_featured_ranks!(user_badge.user_id) end @@ -159,6 +174,6 @@ class UserBadgesController < ApplicationController def is_badge_reason_valid?(reason) route = Discourse.route_for(reason) - route && (route[:controller] == 'posts' || route[:controller] == 'topics') + route && (route[:controller] == "posts" || route[:controller] == "topics") end end diff --git a/app/controllers/users/associate_accounts_controller.rb b/app/controllers/users/associate_accounts_controller.rb index 9f12727b94..371cfbf58c 100644 --- a/app/controllers/users/associate_accounts_controller.rb +++ b/app/controllers/users/associate_accounts_controller.rb @@ -9,11 +9,11 @@ class Users::AssociateAccountsController < ApplicationController account_description = authenticator.description_for_auth_hash(auth_hash) existing_account_description = authenticator.description_for_user(current_user).presence render json: { - token: params[:token], - provider_name: auth_hash.provider, - account_description: account_description, - existing_account_description: existing_account_description - } + token: params[:token], + provider_name: auth_hash.provider, + account_description: account_description, + existing_account_description: existing_account_description, + } end def connect @@ -33,20 +33,23 @@ class Users::AssociateAccountsController < ApplicationController private def auth_hash - @auth_hash ||= begin - token = params[:token] - json = secure_session[self.class.key(token)] - raise Discourse::NotFound if json.nil? + @auth_hash ||= + begin + token = params[:token] + json = secure_session[self.class.key(token)] + raise Discourse::NotFound if json.nil? - OmniAuth::AuthHash.new(JSON.parse(json)) - end + OmniAuth::AuthHash.new(JSON.parse(json)) + end end def authenticator provider_name = auth_hash.provider authenticator = Discourse.enabled_authenticators.find { |a| a.name == provider_name } - raise Discourse::InvalidAccess.new(I18n.t('authenticator_not_found')) if authenticator.nil? - raise Discourse::InvalidAccess.new(I18n.t('authenticator_no_connect')) if !authenticator.can_connect_existing_user? + raise Discourse::InvalidAccess.new(I18n.t("authenticator_not_found")) if authenticator.nil? + if !authenticator.can_connect_existing_user? + raise Discourse::InvalidAccess.new(I18n.t("authenticator_no_connect")) + end authenticator end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb index 3634d2f4c0..673db129c4 100644 --- a/app/controllers/users/omniauth_callbacks_controller.rb +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -2,10 +2,9 @@ # frozen_string_literal: true class Users::OmniauthCallbacksController < ApplicationController - skip_before_action :redirect_to_login_if_required - layout 'no_ember' + layout "no_ember" # need to be able to call this skip_before_action :check_xhr @@ -40,7 +39,7 @@ class Users::OmniauthCallbacksController < ApplicationController DiscourseEvent.trigger(:after_auth, authenticator, @auth_result, session, cookies, request) end - preferred_origin = request.env['omniauth.origin'] + preferred_origin = request.env["omniauth.origin"] if session[:destination_url].present? preferred_origin = session[:destination_url] @@ -53,10 +52,11 @@ class Users::OmniauthCallbacksController < ApplicationController end if preferred_origin.present? - parsed = begin - URI.parse(preferred_origin) - rescue URI::Error - end + parsed = + begin + URI.parse(preferred_origin) + rescue URI::Error + end if valid_origin?(parsed) @origin = +"#{parsed.path}" @@ -64,9 +64,7 @@ class Users::OmniauthCallbacksController < ApplicationController end end - if @origin.blank? - @origin = Discourse.base_path("/") - end + @origin = Discourse.base_path("/") if @origin.blank? @auth_result.destination_url = @origin @auth_result.authenticator_name = authenticator.name @@ -81,16 +79,13 @@ class Users::OmniauthCallbacksController < ApplicationController client_hash = @auth_result.to_client_hash if authenticator.can_connect_existing_user? && - (SiteSetting.enable_local_logins || Discourse.enabled_authenticators.count > 1) + (SiteSetting.enable_local_logins || Discourse.enabled_authenticators.count > 1) # There is more than one login method, and users are allowed to manage associations themselves client_hash[:associate_url] = persist_auth_token(auth) end - cookies['_bypass_cache'] = true - cookies[:authentication_data] = { - value: client_hash.to_json, - path: Discourse.base_path("/") - } + cookies["_bypass_cache"] = true + cookies[:authentication_data] = { value: client_hash.to_json, path: Discourse.base_path("/") } redirect_to @origin end @@ -108,24 +103,24 @@ class Users::OmniauthCallbacksController < ApplicationController flash[:error] = I18n.t( "login.omniauth_error.#{error_key}", - default: I18n.t("login.omniauth_error.generic") + default: I18n.t("login.omniauth_error.generic"), ).html_safe - render 'failure' + render "failure" end def self.find_authenticator(name) Discourse.enabled_authenticators.each do |authenticator| return authenticator if authenticator.name == name end - raise Discourse::InvalidAccess.new(I18n.t('authenticator_not_found')) + raise Discourse::InvalidAccess.new(I18n.t("authenticator_not_found")) end protected def render_auth_result_failure flash[:error] = @auth_result.failed_reason.html_safe - render 'failure' + render "failure" end def complete_response_data @@ -160,13 +155,16 @@ class Users::OmniauthCallbacksController < ApplicationController user.update!(password: SecureRandom.hex) # Ensure there is an active email token - if !EmailToken.where(email: user.email, confirmed: true).exists? && !user.email_tokens.active.where(email: user.email).exists? + if !EmailToken.where(email: user.email, confirmed: true).exists? && + !user.email_tokens.active.where(email: user.email).exists? user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) end user.activate end - user.update!(registration_ip_address: request.remote_ip) if user.registration_ip_address.blank? + if user.registration_ip_address.blank? + user.update!(registration_ip_address: request.remote_ip) + end end if ScreenedIpAddress.should_block?(request.remote_ip) @@ -198,7 +196,9 @@ class Users::OmniauthCallbacksController < ApplicationController def persist_auth_token(auth) secret = SecureRandom.hex - secure_session.set "#{Users::AssociateAccountsController.key(secret)}", auth.to_json, expires: 10.minutes + secure_session.set "#{Users::AssociateAccountsController.key(secret)}", + auth.to_json, + expires: 10.minutes "#{Discourse.base_path}/associate/#{secret}" end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index f7debba784..23032ada60 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,29 +3,72 @@ class UsersController < ApplicationController skip_before_action :authorize_mini_profiler, only: [:avatar] - requires_login only: [ - :username, :update, :upload_user_image, - :pick_avatar, :destroy_user_image, :destroy, :check_emails, - :topic_tracking_state, :preferences, :create_second_factor_totp, - :enable_second_factor_totp, :disable_second_factor, :list_second_factors, - :update_second_factor, :create_second_factor_backup, :select_avatar, - :notification_level, :revoke_auth_token, :register_second_factor_security_key, - :create_second_factor_security_key, :feature_topic, :clear_featured_topic, - :bookmarks, :invited, :check_sso_email, :check_sso_payload, - :recent_searches, :reset_recent_searches, :user_menu_bookmarks, :user_menu_messages - ] + requires_login only: %i[ + username + update + upload_user_image + pick_avatar + destroy_user_image + destroy + check_emails + topic_tracking_state + preferences + create_second_factor_totp + enable_second_factor_totp + disable_second_factor + list_second_factors + update_second_factor + create_second_factor_backup + select_avatar + notification_level + revoke_auth_token + register_second_factor_security_key + create_second_factor_security_key + feature_topic + clear_featured_topic + bookmarks + invited + check_sso_email + check_sso_payload + recent_searches + reset_recent_searches + user_menu_bookmarks + user_menu_messages + ] - skip_before_action :check_xhr, only: [ - :show, :badges, :password_reset_show, :password_reset_update, :update, :account_created, - :activate_account, :perform_account_activation, :avatar, - :my_redirect, :toggle_anon, :admin_login, :confirm_admin, :email_login, :summary, - :feature_topic, :clear_featured_topic, :bookmarks, :user_menu_bookmarks, :user_menu_messages - ] + skip_before_action :check_xhr, + only: %i[ + show + badges + password_reset_show + password_reset_update + update + account_created + activate_account + perform_account_activation + avatar + my_redirect + toggle_anon + admin_login + confirm_admin + email_login + summary + feature_topic + clear_featured_topic + bookmarks + user_menu_bookmarks + user_menu_messages + ] - before_action :second_factor_check_confirmed_password, only: [ - :create_second_factor_totp, :enable_second_factor_totp, - :disable_second_factor, :update_second_factor, :create_second_factor_backup, - :register_second_factor_security_key, :create_second_factor_security_key + before_action :second_factor_check_confirmed_password, + only: %i[ + create_second_factor_totp + enable_second_factor_totp + disable_second_factor + update_second_factor + create_second_factor_backup + register_second_factor_security_key + create_second_factor_security_key ] before_action :respond_to_suspicious_request, only: [:create] @@ -34,22 +77,25 @@ class UsersController < ApplicationController # page is going to be empty, this means that server will see an invalid CSRF and blow the session # once that happens you can't log in with social skip_before_action :verify_authenticity_token, only: [:create] - skip_before_action :redirect_to_login_if_required, only: [:check_username, - :check_email, - :create, - :account_created, - :activate_account, - :perform_account_activation, - :send_activation_email, - :update_activation_email, - :password_reset_show, - :password_reset_update, - :confirm_email_token, - :email_login, - :admin_login, - :confirm_admin] + skip_before_action :redirect_to_login_if_required, + only: %i[ + check_username + check_email + create + account_created + activate_account + perform_account_activation + send_activation_email + update_activation_email + password_reset_show + password_reset_update + confirm_email_token + email_login + admin_login + confirm_admin + ] - after_action :add_noindex_header, only: [:show, :my_redirect] + after_action :add_noindex_header, only: %i[show my_redirect] allow_in_staff_writes_only_mode :admin_login allow_in_staff_writes_only_mode :email_login @@ -60,32 +106,34 @@ class UsersController < ApplicationController end def show(for_card: false) - return redirect_to path('/login') if SiteSetting.hide_user_profiles_from_public && !current_user + return redirect_to path("/login") if SiteSetting.hide_user_profiles_from_public && !current_user - @user = fetch_user_from_params( - include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts) - ) + @user = + fetch_user_from_params( + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + ) user_serializer = nil if guardian.can_see_profile?(@user) serializer_class = for_card ? UserCardSerializer : UserSerializer - user_serializer = serializer_class.new(@user, scope: guardian, root: 'user') + user_serializer = serializer_class.new(@user, scope: guardian, root: "user") topic_id = params[:include_post_count_for].to_i if topic_id != 0 && guardian.can_see?(Topic.find_by_id(topic_id)) - user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count } + user_serializer.topic_post_count = { + topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count, + } end else - user_serializer = HiddenProfileSerializer.new(@user, scope: guardian, root: 'user') + user_serializer = HiddenProfileSerializer.new(@user, scope: guardian, root: "user") end - if !params[:skip_track_visit] && (@user != current_user) - track_visit_to_user_profile - end + track_visit_to_user_profile if !params[:skip_track_visit] && (@user != current_user) # This is a hack to get around a Rails issue where values with periods aren't handled correctly # when used as part of a route. - if params[:external_id] && params[:external_id].ends_with?('.json') + if params[:external_id] && params[:external_id].ends_with?(".json") return render_json_dump(user_serializer) end @@ -96,9 +144,7 @@ class UsersController < ApplicationController render :show end - format.json do - render_json_dump(user_serializer) - end + format.json { render_json_dump(user_serializer) } end end @@ -108,25 +154,29 @@ class UsersController < ApplicationController # This route is not used in core, but is used by theme components (e.g. https://meta.discourse.org/t/144479) def cards - return redirect_to path('/login') if SiteSetting.hide_user_profiles_from_public && !current_user + return redirect_to path("/login") if SiteSetting.hide_user_profiles_from_public && !current_user user_ids = params.require(:user_ids).split(",").map(&:to_i) raise Discourse::InvalidParameters.new(:user_ids) if user_ids.length > 50 - users = User.where(id: user_ids).includes(:user_option, - :user_stat, - :default_featured_user_badges, - :user_profile, - :card_background_upload, - :primary_group, - :flair_group, - :primary_email, - :user_status - ) + users = + User.where(id: user_ids).includes( + :user_option, + :user_stat, + :default_featured_user_badges, + :user_profile, + :card_background_upload, + :primary_group, + :flair_group, + :primary_email, + :user_status, + ) users = users.filter { |u| guardian.can_see_profile?(u) } - preload_fields = User.allowed_user_custom_fields(guardian) + UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" } + preload_fields = + User.allowed_user_custom_fields(guardian) + + UserField.all.pluck(:id).map { |fid| "#{User::USER_FIELD_PREFIX}#{fid}" } User.preload_custom_fields(users, preload_fields) User.preload_recent_time_read(users) @@ -159,7 +209,9 @@ class UsersController < ApplicationController value = nil if value === "false" value = value[0...UserField.max_length] if value - return render_json_error(I18n.t("login.missing_user_field")) if value.blank? && field.required? + if value.blank? && field.required? + return render_json_error(I18n.t("login.missing_user_field")) + end attributes[:custom_fields]["#{User::USER_FIELD_PREFIX}#{field.id}"] = value end end @@ -168,8 +220,10 @@ class UsersController < ApplicationController attributes[:user_associated_accounts] = [] params[:external_ids].each do |provider_name, provider_uid| - if provider_name == 'discourse_connect' - raise Discourse::InvalidParameters.new(:external_ids) unless SiteSetting.enable_discourse_connect + if provider_name == "discourse_connect" + unless SiteSetting.enable_discourse_connect + raise Discourse::InvalidParameters.new(:external_ids) + end attributes[:discourse_connect] = { external_id: provider_uid } @@ -179,11 +233,18 @@ class UsersController < ApplicationController authenticator = Discourse.enabled_authenticators.find { |a| a.name == provider_name } raise Discourse::InvalidParameters.new(:external_ids) if !authenticator&.is_managed? - attributes[:user_associated_accounts] << { provider_name: provider_name, provider_uid: provider_uid } + attributes[:user_associated_accounts] << { + provider_name: provider_name, + provider_uid: provider_uid, + } end end - json_result(user, serializer: UserSerializer, additional_errors: [:user_profile, :user_option]) do |u| + json_result( + user, + serializer: UserSerializer, + additional_errors: %i[user_profile user_option], + ) do |u| updater = UserUpdater.new(current_user, user) updater.update(attributes.permit!) end @@ -192,7 +253,8 @@ class UsersController < ApplicationController def username params.require(:new_username) - if clashing_with_existing_route?(params[:new_username]) || User.reserved_username?(params[:new_username]) + if clashing_with_existing_route?(params[:new_username]) || + User.reserved_username?(params[:new_username]) return render_json_error(I18n.t("login.reserved_username")) end @@ -204,11 +266,11 @@ class UsersController < ApplicationController if result render json: { id: user.id, username: user.username } else - render_json_error(user.errors.full_messages.join(',')) + render_json_error(user.errors.full_messages.join(",")) end rescue Discourse::InvalidAccess if current_user&.staff? - render_json_error(I18n.t('errors.messages.auth_overrides_username')) + render_json_error(I18n.t("errors.messages.auth_overrides_username")) else render json: failed_json, status: 403 end @@ -226,11 +288,11 @@ class UsersController < ApplicationController unconfirmed_emails = user.unconfirmed_emails render json: { - email: email, - secondary_emails: secondary_emails, - unconfirmed_emails: unconfirmed_emails, - associated_accounts: user.associated_accounts - } + email: email, + secondary_emails: secondary_emails, + unconfirmed_emails: unconfirmed_emails, + associated_accounts: user.associated_accounts, + } rescue Discourse::InvalidAccess render json: failed_json, status: 403 end @@ -268,9 +330,7 @@ class UsersController < ApplicationController end def update_primary_email - if !SiteSetting.enable_secondary_emails - return render json: failed_json, status: 410 - end + return render json: failed_json, status: 410 if !SiteSetting.enable_secondary_emails params.require(:email) @@ -278,13 +338,13 @@ class UsersController < ApplicationController guardian.ensure_can_edit_email!(user) old_primary = user.primary_email - if old_primary.email == params[:email] - return render json: success_json - end + return render json: success_json if old_primary.email == params[:email] new_primary = user.user_emails.find_by(email: params[:email]) if new_primary.blank? - return render json: failed_json.merge(errors: [I18n.t("change_email.doesnt_exist")]), status: 428 + return( + render json: failed_json.merge(errors: [I18n.t("change_email.doesnt_exist")]), status: 428 + ) end User.transaction do @@ -303,9 +363,7 @@ class UsersController < ApplicationController end def destroy_email - if !SiteSetting.enable_secondary_emails - return render json: failed_json, status: 410 - end + return render json: failed_json, status: 410 if !SiteSetting.enable_secondary_emails params.require(:email) @@ -336,9 +394,12 @@ class UsersController < ApplicationController guardian.ensure_can_edit!(user) report = TopicTrackingState.report(user) - serializer = ActiveModel::ArraySerializer.new( - report, each_serializer: TopicTrackingStateSerializer, scope: guardian - ) + serializer = + ActiveModel::ArraySerializer.new( + report, + each_serializer: TopicTrackingStateSerializer, + scope: guardian, + ) render json: MultiJson.dump(serializer) end @@ -349,11 +410,12 @@ class UsersController < ApplicationController report = PrivateMessageTopicTrackingState.report(user) - serializer = ActiveModel::ArraySerializer.new( - report, - each_serializer: PrivateMessageTopicTrackingStateSerializer, - scope: guardian - ) + serializer = + ActiveModel::ArraySerializer.new( + report, + each_serializer: PrivateMessageTopicTrackingStateSerializer, + scope: guardian, + ) render json: MultiJson.dump(serializer) end @@ -373,28 +435,33 @@ class UsersController < ApplicationController log_params = { details: "title matching badge id #{user_badge.badge.id}", previous_value: previous_title, - new_value: user.title + new_value: user.title, } if current_user.staff? && current_user != user StaffActionLogger.new(current_user).log_title_change(user, log_params) else - UserHistory.create!(log_params.merge(target_user_id: user.id, action: UserHistory.actions[:change_title])) + UserHistory.create!( + log_params.merge(target_user_id: user.id, action: UserHistory.actions[:change_title]), + ) end else - user.title = '' + user.title = "" user.save! - log_params = { - previous_value: previous_title - } + log_params = { previous_value: previous_title } if current_user.staff? && current_user != user - StaffActionLogger - .new(current_user) - .log_title_revoke(user, log_params.merge(revoke_reason: 'user title was same as revoked badge name or custom badge name')) + StaffActionLogger.new(current_user).log_title_revoke( + user, + log_params.merge( + revoke_reason: "user title was same as revoked badge name or custom badge name", + ), + ) else - UserHistory.create!(log_params.merge(target_user_id: user.id, action: UserHistory.actions[:revoke_title])) + UserHistory.create!( + log_params.merge(target_user_id: user.id, action: UserHistory.actions[:revoke_title]), + ) end end @@ -406,7 +473,7 @@ class UsersController < ApplicationController end def my_redirect - raise Discourse::NotFound if params[:path] !~ /^[a-z_\-\/]+$/ + raise Discourse::NotFound if params[:path] !~ %r{^[a-z_\-/]+$} if current_user.blank? cookies[:destination_url] = path("/my/#{params[:path]}") @@ -421,10 +488,14 @@ class UsersController < ApplicationController end def summary - @user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts)) + @user = + fetch_user_from_params( + include_inactive: + current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts), + ) raise Discourse::NotFound unless guardian.can_see_profile?(@user) - response.headers['X-Robots-Tag'] = 'noindex' + response.headers["X-Robots-Tag"] = "noindex" respond_to do |format| format.html do @@ -432,11 +503,14 @@ class UsersController < ApplicationController render :show end format.json do - summary_json = Discourse.cache.fetch(summary_cache_key(@user), expires_in: 1.hour) do - summary = UserSummary.new(@user, guardian) - serializer = UserSummarySerializer.new(summary, scope: guardian) - MultiJson.dump(serializer) - end + summary_json = + Discourse + .cache + .fetch(summary_cache_key(@user), expires_in: 1.hour) do + summary = UserSummary.new(@user, guardian) + serializer = UserSummarySerializer.new(summary, scope: guardian) + MultiJson.dump(serializer) + end render json: summary_json end end @@ -445,24 +519,29 @@ class UsersController < ApplicationController def invited if guardian.can_invite_to_forum? filter = params[:filter] || "redeemed" - inviter = fetch_user_from_params(include_inactive: current_user.staff? || SiteSetting.show_inactive_accounts) + inviter = + fetch_user_from_params( + include_inactive: current_user.staff? || SiteSetting.show_inactive_accounts, + ) - invites = if filter == "pending" && guardian.can_see_invite_details?(inviter) - Invite.includes(:topics, :groups).pending(inviter) - elsif filter == "expired" - Invite.expired(inviter) - elsif filter == "redeemed" - Invite.redeemed_users(inviter) - else - Invite.none - end + invites = + if filter == "pending" && guardian.can_see_invite_details?(inviter) + Invite.includes(:topics, :groups).pending(inviter) + elsif filter == "expired" + Invite.expired(inviter) + elsif filter == "redeemed" + Invite.redeemed_users(inviter) + else + Invite.none + end invites = invites.offset(params[:offset].to_i || 0).limit(SiteSetting.invites_per_page) show_emails = guardian.can_see_invite_emails?(inviter) if params[:search].present? && invites.present? - filter_sql = '(LOWER(users.username) LIKE :filter)' - filter_sql = '(LOWER(invites.email) LIKE :filter) or (LOWER(users.username) LIKE :filter)' if show_emails + filter_sql = "(LOWER(users.username) LIKE :filter)" + filter_sql = + "(LOWER(invites.email) LIKE :filter) or (LOWER(users.username) LIKE :filter)" if show_emails invites = invites.where(filter_sql, filter: "%#{params[:search].downcase}%") end @@ -470,26 +549,30 @@ class UsersController < ApplicationController expired_count = Invite.expired(inviter).reorder(nil).count.to_i redeemed_count = Invite.redeemed_users(inviter).reorder(nil).count.to_i - render json: MultiJson.dump(InvitedSerializer.new( - OpenStruct.new( - invite_list: invites.to_a, - show_emails: show_emails, - inviter: inviter, - type: filter, - counts: { - pending: pending_count, - expired: expired_count, - redeemed: redeemed_count, - total: pending_count + expired_count - } - ), - scope: guardian, - root: false - )) + render json: + MultiJson.dump( + InvitedSerializer.new( + OpenStruct.new( + invite_list: invites.to_a, + show_emails: show_emails, + inviter: inviter, + type: filter, + counts: { + pending: pending_count, + expired: expired_count, + redeemed: redeemed_count, + total: pending_count + expired_count, + }, + ), + scope: guardian, + root: false, + ), + ) elsif current_user&.staff? - message = if SiteSetting.enable_discourse_connect - I18n.t("invite.disabled_errors.discourse_connect_enabled") - end + message = + if SiteSetting.enable_discourse_connect + I18n.t("invite.disabled_errors.discourse_connect_enabled") + end render_invite_error(message) else @@ -533,27 +616,25 @@ class UsersController < ApplicationController email = Email.downcase((params[:email] || "").strip) - if email.blank? || SiteSetting.hide_email_address_taken? - return render json: success_json - end + return render json: success_json if email.blank? || SiteSetting.hide_email_address_taken? if !EmailAddressValidator.valid_value?(email) - error = User.new.errors.full_message(:email, I18n.t(:'user.email.invalid')) + error = User.new.errors.full_message(:email, I18n.t(:"user.email.invalid")) return render json: failed_json.merge(errors: [error]) end if !EmailValidator.allowed?(email) - error = User.new.errors.full_message(:email, I18n.t(:'user.email.not_allowed')) + error = User.new.errors.full_message(:email, I18n.t(:"user.email.not_allowed")) return render json: failed_json.merge(errors: [error]) end if ScreenedEmail.should_block?(email) - error = User.new.errors.full_message(:email, I18n.t(:'user.email.blocked')) + error = User.new.errors.full_message(:email, I18n.t(:"user.email.blocked")) return render json: failed_json.merge(errors: [error]) end if User.where(staged: false).find_by_email(email).present? - error = User.new.errors.full_message(:email, I18n.t(:'errors.messages.taken')) + error = User.new.errors.full_message(:email, I18n.t(:"errors.messages.taken")) return render json: failed_json.merge(errors: [error]) end @@ -571,23 +652,21 @@ class UsersController < ApplicationController params.permit(:user_fields) params.permit(:external_ids) - unless SiteSetting.allow_new_registrations - return fail_with("login.new_registrations_disabled") - end + return fail_with("login.new_registrations_disabled") unless SiteSetting.allow_new_registrations if params[:password] && params[:password].length > User.max_password_length return fail_with("login.password_too_long") end - if params[:email].length > 254 + 1 + 253 - return fail_with("login.email_too_long") - end + return fail_with("login.email_too_long") if params[:email].length > 254 + 1 + 253 - if SiteSetting.require_invite_code && SiteSetting.invite_code.strip.downcase != params[:invite_code].strip.downcase + if SiteSetting.require_invite_code && + SiteSetting.invite_code.strip.downcase != params[:invite_code].strip.downcase return fail_with("login.wrong_invite_code") end - if clashing_with_existing_route?(params[:username]) || User.reserved_username?(params[:username]) + if clashing_with_existing_route?(params[:username]) || + User.reserved_username?(params[:username]) return fail_with("login.reserved_username") end @@ -636,14 +715,19 @@ class UsersController < ApplicationController authenticator = Discourse.enabled_authenticators.find { |a| a.name == provider_name } raise Discourse::InvalidParameters.new(:external_ids) if !authenticator&.is_managed? - association = UserAssociatedAccount.find_or_initialize_by(provider_name: provider_name, provider_uid: provider_uid) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: provider_name, + provider_uid: provider_uid, + ) associations << association end end authentication = UserAuthenticator.new(user, session) - if !authentication.has_authenticator? && !SiteSetting.enable_local_logins && !(current_user&.admin? && is_api?) + if !authentication.has_authenticator? && !SiteSetting.enable_local_logins && + !(current_user&.admin? && is_api?) return render body: nil, status: :forbidden end @@ -651,7 +735,7 @@ class UsersController < ApplicationController if authentication.email_valid? && !authentication.authenticated? # posted email is different that the already validated one? - return fail_with('login.incorrect_username_email_or_password') + return fail_with("login.incorrect_username_email_or_password") end activation = UserActivator.new(user, request, session, cookies) @@ -659,7 +743,8 @@ class UsersController < ApplicationController # just assign a password if we have an authenticator and no password # this is the case for Twitter - user.password = SecureRandom.hex if user.password.blank? && (authentication.has_authenticator? || associations.present?) + user.password = SecureRandom.hex if user.password.blank? && + (authentication.has_authenticator? || associations.present?) if user.save authentication.finish @@ -679,47 +764,36 @@ class UsersController < ApplicationController # add them to the review queue if they need to be approved user.activate if user.active? - render json: { - success: true, - active: user.active?, - message: activation.message, - }.merge(SiteSetting.hide_email_address_taken ? {} : { user_id: user.id }) - elsif SiteSetting.hide_email_address_taken && user.errors[:primary_email]&.include?(I18n.t('errors.messages.taken')) + render json: { success: true, active: user.active?, message: activation.message }.merge( + SiteSetting.hide_email_address_taken ? {} : { user_id: user.id }, + ) + elsif SiteSetting.hide_email_address_taken && + user.errors[:primary_email]&.include?(I18n.t("errors.messages.taken")) session["user_created_message"] = activation.success_message if existing_user = User.find_by_email(user.primary_email&.email) Jobs.enqueue(:critical_user_email, type: "account_exists", user_id: existing_user.id) end - render json: { - success: true, - active: false, - message: activation.success_message - } + render json: { success: true, active: false, message: activation.success_message } else errors = user.errors.to_hash errors[:email] = errors.delete(:primary_email) if errors[:primary_email] render json: { - success: false, - message: I18n.t( - 'login.errors', - errors: user.errors.full_messages.join("\n") - ), - errors: errors, - values: { - name: user.name, - username: user.username, - email: user.primary_email&.email - }, - is_developer: UsernameCheckerService.is_developer?(user.email) - } + success: false, + message: I18n.t("login.errors", errors: user.errors.full_messages.join("\n")), + errors: errors, + values: { + name: user.name, + username: user.username, + email: user.primary_email&.email, + }, + is_developer: UsernameCheckerService.is_developer?(user.email), + } end rescue ActiveRecord::StatementInvalid - render json: { - success: false, - message: I18n.t("login.something_already_taken") - } + render json: { success: false, message: I18n.t("login.something_already_taken") } end def password_reset_show @@ -735,21 +809,23 @@ class UsersController < ApplicationController second_factor_required: @user.totp_enabled?, security_key_required: @user.security_keys_enabled?, backup_enabled: @user.backup_codes_enabled?, - multiple_second_factor_methods: @user.has_multiple_second_factor_methods? + multiple_second_factor_methods: @user.has_multiple_second_factor_methods?, } end respond_to do |format| format.html do - return render 'password_reset', layout: 'no_ember' if @error + return render "password_reset", layout: "no_ember" if @error Webauthn.stage_challenge(@user, secure_session) store_preloaded( "password_reset", - MultiJson.dump(security_params.merge(Webauthn.allowed_credentials(@user, secure_session))) + MultiJson.dump( + security_params.merge(Webauthn.allowed_credentials(@user, secure_session)), + ), ) - render 'password_reset' + render "password_reset" end format.json do @@ -772,13 +848,20 @@ class UsersController < ApplicationController # a user from the token if @user if !secure_session["second-factor-#{token}"] - second_factor_authentication_result = @user.authenticate_second_factor(params, secure_session) + second_factor_authentication_result = + @user.authenticate_second_factor(params, secure_session) if !second_factor_authentication_result.ok - user_error_key = second_factor_authentication_result.reason == "invalid_security_key" ? :user_second_factors : :security_keys + user_error_key = + ( + if second_factor_authentication_result.reason == "invalid_security_key" + :user_second_factors + else + :security_keys + end + ) @user.errors.add(user_error_key, :invalid) @error = second_factor_authentication_result.error else - # this must be set because the first call we authenticate e.g. TOTP, and we do # not want to re-authenticate on the second call to change the password as this # will cause a TOTP error saying the code has already been used @@ -786,7 +869,8 @@ class UsersController < ApplicationController end end - if @invalid_password = params[:password].blank? || params[:password].size > User.max_password_length + if @invalid_password = + params[:password].blank? || params[:password].size > User.max_password_length @user.errors.add(:password, :invalid) end @@ -804,7 +888,7 @@ class UsersController < ApplicationController UserHistory.create!( target_user: @user, acting_user: @user, - action: UserHistory.actions[:change_password] + action: UserHistory.actions[:change_password], ) logon_after_password_reset end @@ -813,7 +897,7 @@ class UsersController < ApplicationController respond_to do |format| format.html do - return render 'password_reset', layout: 'no_ember' if @error + return render "password_reset", layout: "no_ember" if @error Webauthn.stage_challenge(@user, secure_session) @@ -823,32 +907,32 @@ class UsersController < ApplicationController second_factor_required: @user.totp_enabled?, security_key_required: @user.security_keys_enabled?, backup_enabled: @user.backup_codes_enabled?, - multiple_second_factor_methods: @user.has_multiple_second_factor_methods? + multiple_second_factor_methods: @user.has_multiple_second_factor_methods?, }.merge(Webauthn.allowed_credentials(@user, secure_session)) store_preloaded("password_reset", MultiJson.dump(security_params)) return redirect_to(wizard_path) if Wizard.user_requires_completion?(@user) - render 'password_reset' + render "password_reset" end format.json do if @error || @user&.errors&.any? render json: { - success: false, - message: @error, - errors: @user&.errors&.to_hash, - is_developer: UsernameCheckerService.is_developer?(@user&.email), - admin: @user&.admin? - } + success: false, + message: @error, + errors: @user&.errors&.to_hash, + is_developer: UsernameCheckerService.is_developer?(@user&.email), + admin: @user&.admin?, + } else render json: { - success: true, - message: @success, - requires_approval: !Guardian.new(@user).can_access_forum?, - redirect_to: Wizard.user_requires_completion?(@user) ? wizard_path : nil - } + success: true, + message: @success, + requires_approval: !Guardian.new(@user).can_access_forum?, + redirect_to: Wizard.user_requires_completion?(@user) ? wizard_path : nil, + } end end end @@ -865,10 +949,10 @@ class UsersController < ApplicationController if Guardian.new(@user).can_access_forum? # Log in the user log_on_user(@user) - 'password_reset.success' + "password_reset.success" else @requires_approval = true - 'password_reset.success_unapproved' + "password_reset.success_unapproved" end @success = I18n.t(message) @@ -882,22 +966,26 @@ class UsersController < ApplicationController RateLimiter.new(nil, "admin-login-min-#{request.remote_ip}", 3, 1.minute).performed! if user = User.with_email(params[:email]).admins.human_users.first - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) token_string = email_token.token - if params["use_safe_mode"] - token_string += "?safe_mode=no_plugins,no_themes" - end - Jobs.enqueue(:critical_user_email, type: "admin_login", user_id: user.id, email_token: token_string) + token_string += "?safe_mode=no_plugins,no_themes" if params["use_safe_mode"] + Jobs.enqueue( + :critical_user_email, + type: "admin_login", + user_id: user.id, + email_token: token_string, + ) @message = I18n.t("admin_login.success") else @message = I18n.t("admin_login.errors.unknown_email_address") end end - render layout: 'no_ember' + render layout: "no_ember" rescue RateLimiter::LimitExceeded @message = I18n.t("rate_limiter.slow_down") - render layout: 'no_ember' + render layout: "no_ember" end def email_login @@ -919,12 +1007,14 @@ class UsersController < ApplicationController if user_presence DiscourseEvent.trigger(:before_email_login, user) - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login]) - Jobs.enqueue(:critical_user_email, + Jobs.enqueue( + :critical_user_email, type: "email_login", user_id: user.id, - email_token: email_token.token + email_token: email_token.token, ) end end @@ -938,8 +1028,8 @@ class UsersController < ApplicationController end def toggle_anon - user = AnonymousShadowCreator.get_master(current_user) || - AnonymousShadowCreator.get(current_user) + user = + AnonymousShadowCreator.get_master(current_user) || AnonymousShadowCreator.get(current_user) if user log_on_user(user) @@ -956,12 +1046,12 @@ class UsersController < ApplicationController elsif destination_url = cookies.delete(:destination_url) return redirect_to(destination_url, allow_other_host: true) else - return redirect_to(path('/')) + return redirect_to(path("/")) end end @custom_body_class = "static-account-created" - @message = session['user_created_message'] || I18n.t('activation.missing_session') + @message = session["user_created_message"] || I18n.t("activation.missing_session") @account_created = { message: @message, show_controls: false } if session_user_id = session[SessionController::ACTIVATE_USER_KEY] @@ -983,7 +1073,7 @@ class UsersController < ApplicationController def activate_account expires_now - render layout: 'no_ember' + render layout: "no_ember" end def perform_account_activation @@ -992,7 +1082,7 @@ class UsersController < ApplicationController if @user = EmailToken.confirm(params[:token], scope: EmailToken.scopes[:signup]) # Log in the user unless they need to be approved if Guardian.new(@user).can_access_forum? - @user.enqueue_welcome_message('welcome_user') if @user.send_welcome_message + @user.enqueue_welcome_message("welcome_user") if @user.send_welcome_message log_on_user(@user) # invites#perform_accept_invitation already sets destination_url, but @@ -1002,39 +1092,44 @@ class UsersController < ApplicationController # the topic they were originally invited to. destination_url = cookies.delete(:destination_url) if destination_url.blank? - topic = Invite - .joins(:invited_users) - .find_by(invited_users: { user_id: @user.id }) - &.topics - &.first + topic = + Invite + .joins(:invited_users) + .find_by(invited_users: { user_id: @user.id }) + &.topics + &.first - if @user.guardian.can_see?(topic) - destination_url = path(topic.relative_url) - end + destination_url = path(topic.relative_url) if @user.guardian.can_see?(topic) end if Wizard.user_requires_completion?(@user) return redirect_to(wizard_path) elsif destination_url.present? return redirect_to(destination_url, allow_other_host: true) - elsif SiteSetting.enable_discourse_connect_provider && payload = cookies.delete(:sso_payload) + elsif SiteSetting.enable_discourse_connect_provider && + payload = cookies.delete(:sso_payload) return redirect_to(session_sso_provider_url + "?" + payload) end else @needs_approval = true end else - flash.now[:error] = I18n.t('activation.already_done') + flash.now[:error] = I18n.t("activation.already_done") end - render layout: 'no_ember' + render layout: "no_ember" end def update_activation_email RateLimiter.new(nil, "activate-edit-email-hr-#{request.remote_ip}", 5, 1.hour).performed! if params[:username].present? - RateLimiter.new(nil, "activate-edit-email-hr-username-#{params[:username]}", 5, 1.hour).performed! + RateLimiter.new( + nil, + "activate-edit-email-hr-username-#{params[:username]}", + 5, + 1.hour, + ).performed! @user = User.find_by_username_or_email(params[:username]) raise Discourse::InvalidAccess.new unless @user.present? raise Discourse::InvalidAccess.new unless @user.confirm_password?(params[:password]) @@ -1053,7 +1148,8 @@ class UsersController < ApplicationController primary_email.skip_validate_email = false if primary_email.save - @email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) + @email_token = + @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) EmailToken.enqueue_signup_email(@email_token, to_address: @user.email) render json: success_json else @@ -1070,9 +1166,7 @@ class UsersController < ApplicationController raise Discourse::InvalidAccess.new if SiteSetting.must_approve_users? - if params[:username].present? - @user = User.find_by_username_or_email(params[:username].to_s) - end + @user = User.find_by_username_or_email(params[:username].to_s) if params[:username].present? raise Discourse::NotFound unless @user @@ -1083,9 +1177,10 @@ class UsersController < ApplicationController session.delete(SessionController::ACTIVATE_USER_KEY) if @user.active && @user.email_confirmed? - render_json_error(I18n.t('activation.activated'), status: 409) + render_json_error(I18n.t("activation.activated"), status: 409) else - @email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) + @email_token = + @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:signup]) EmailToken.enqueue_signup_email(@email_token, to_address: @user.email) render body: nil end @@ -1104,12 +1199,14 @@ class UsersController < ApplicationController @groups = Group.where(name: group_names) if group_names.present? options = { - topic_allowed_users: topic_allowed_users, - searching_user: current_user, - groups: @groups + topic_allowed_users: topic_allowed_users, + searching_user: current_user, + groups: @groups, } - options[:include_staged_users] = !!ActiveModel::Type::Boolean.new.cast(params[:include_staged_users]) + options[:include_staged_users] = !!ActiveModel::Type::Boolean.new.cast( + params[:include_staged_users], + ) options[:last_seen_users] = !!ActiveModel::Type::Boolean.new.cast(params[:last_seen_users]) if params[:limit].present? options[:limit] = params[:limit].to_i @@ -1125,50 +1222,44 @@ class UsersController < ApplicationController # we do not want group results ever if term is blank groups = if term.present? && current_user - if params[:include_groups] == 'true' + if params[:include_groups] == "true" Group.visible_groups(current_user) - elsif params[:include_mentionable_groups] == 'true' + elsif params[:include_mentionable_groups] == "true" Group.mentionable(current_user) - elsif params[:include_messageable_groups] == 'true' + elsif params[:include_messageable_groups] == "true" Group.messageable(current_user) end end if groups - DiscoursePluginRegistry.groups_callback_for_users_search_controller_action.each do |param_name, block| - if params[param_name.to_s] - groups = block.call(groups, current_user) - end + DiscoursePluginRegistry + .groups_callback_for_users_search_controller_action + .each do |param_name, block| + groups = block.call(groups, current_user) if params[param_name.to_s] end groups = Group.search_groups(term, groups: groups, sort: :auto) - to_render[:groups] = groups.map do |m| - { name: m.name, full_name: m.full_name } - end + to_render[:groups] = groups.map { |m| { name: m.name, full_name: m.full_name } } end render json: to_render end - AVATAR_TYPES_WITH_UPLOAD ||= %w{uploaded custom gravatar} + AVATAR_TYPES_WITH_UPLOAD ||= %w[uploaded custom gravatar] def pick_avatar user = fetch_user_from_params guardian.ensure_can_edit!(user) - if SiteSetting.discourse_connect_overrides_avatar - return render json: failed_json, status: 422 - end + return render json: failed_json, status: 422 if SiteSetting.discourse_connect_overrides_avatar type = params[:type] - invalid_type = type.present? && !AVATAR_TYPES_WITH_UPLOAD.include?(type) && type != 'system' - if invalid_type - return render json: failed_json, status: 422 - end + invalid_type = type.present? && !AVATAR_TYPES_WITH_UPLOAD.include?(type) && type != "system" + return render json: failed_json, status: 422 if invalid_type - if type.blank? || type == 'system' + if type.blank? || type == "system" upload_id = nil elsif !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, user) return render json: failed_json, status: 422 @@ -1176,25 +1267,21 @@ class UsersController < ApplicationController upload_id = params[:upload_id] upload = Upload.find_by(id: upload_id) - if upload.nil? - return render_json_error I18n.t('avatar.missing') - end + return render_json_error I18n.t("avatar.missing") if upload.nil? # old safeguard user.create_user_avatar unless user.user_avatar guardian.ensure_can_pick_avatar!(user.user_avatar, upload) - if type == 'gravatar' + if type == "gravatar" user.user_avatar.gravatar_upload_id = upload_id else user.user_avatar.custom_upload_id = upload_id end end - if user.is_system_user? - SiteSetting.use_site_small_logo_as_system_avatar = false - end + SiteSetting.use_site_small_logo_as_system_avatar = false if user.is_system_user? user.uploaded_avatar_id = upload_id user.save! @@ -1209,17 +1296,13 @@ class UsersController < ApplicationController url = params[:url] - if url.blank? - return render json: failed_json, status: 422 - end + return render json: failed_json, status: 422 if url.blank? if SiteSetting.selectable_avatars_mode == "disabled" return render json: failed_json, status: 422 end - if SiteSetting.selectable_avatars.blank? - return render json: failed_json, status: 422 - end + return render json: failed_json, status: 422 if SiteSetting.selectable_avatars.blank? unless upload = Upload.get_from_url(url) return render json: failed_json, status: 422 @@ -1231,9 +1314,7 @@ class UsersController < ApplicationController user.uploaded_avatar_id = upload.id - if user.is_system_user? - SiteSetting.use_site_small_logo_as_system_avatar = false - end + SiteSetting.use_site_small_logo_as_system_avatar = false if user.is_system_user? user.save! @@ -1242,10 +1323,10 @@ class UsersController < ApplicationController avatar.save! render json: { - avatar_template: user.avatar_template, - custom_avatar_template: user.avatar_template, - uploaded_avatar_id: upload.id, - } + avatar_template: user.avatar_template, + custom_avatar_template: user.avatar_template, + uploaded_avatar_id: upload.id, + } end def destroy_user_image @@ -1280,8 +1361,7 @@ class UsersController < ApplicationController # the admin should be able to change notification levels # on behalf of other users, so we cannot rely on current_user # for this case - if params[:acting_user_id].present? && - params[:acting_user_id].to_i != current_user.id + if params[:acting_user_id].present? && params[:acting_user_id].to_i != current_user.id if current_user.staff? acting_user = User.find(params[:acting_user_id]) else @@ -1298,20 +1378,26 @@ class UsersController < ApplicationController if ignored_user.present? ignored_user.update(expiring_at: DateTime.parse(params[:expiring_at])) else - IgnoredUser.create!(user: acting_user, ignored_user: target_user, expiring_at: Time.parse(params[:expiring_at])) + IgnoredUser.create!( + user: acting_user, + ignored_user: target_user, + expiring_at: Time.parse(params[:expiring_at]), + ) end - elsif params[:notification_level] == "mute" @error_message = "mute_error" guardian.ensure_can_mute_user!(target_user) IgnoredUser.where(user: acting_user, ignored_user: target_user).delete_all MutedUser.find_or_create_by!(user: acting_user, muted_user: target_user) - elsif params[:notification_level] == "normal" MutedUser.where(user: acting_user, muted_user: target_user).delete_all IgnoredUser.where(user: acting_user, ignored_user: target_user).delete_all else - return render_json_error(I18n.t("notification_level.invalid_value", value: params[:notification_level])) + return( + render_json_error( + I18n.t("notification_level.invalid_value", value: params[:notification_level]), + ) + ) end render json: success_json @@ -1330,22 +1416,20 @@ class UsersController < ApplicationController def recent_searches if !SiteSetting.log_search_queries - return render json: failed_json.merge( - error: I18n.t("user_activity.no_log_search_queries") - ), status: 403 + return( + render json: failed_json.merge(error: I18n.t("user_activity.no_log_search_queries")), + status: 403 + ) end query = SearchLog.where(user_id: current_user.id) if current_user.user_option.oldest_search_log_date - query = query - .where("created_at > ?", current_user.user_option.oldest_search_log_date) + query = query.where("created_at > ?", current_user.user_option.oldest_search_log_date) end - results = query.group(:term) - .order("max(created_at) DESC") - .limit(MAX_RECENT_SEARCHES) - .pluck(:term) + results = + query.group(:term).order("max(created_at) DESC").limit(MAX_RECENT_SEARCHES).pluck(:term) render json: success_json.merge(recent_searches: results) end @@ -1361,12 +1445,14 @@ class UsersController < ApplicationController result = {} - %W{ - number_of_deleted_posts number_of_flagged_posts number_of_flags_given - number_of_suspensions warnings_received_count number_of_rejected_posts - }.each do |info| - result[info] = @user.public_send(info) - end + %W[ + number_of_deleted_posts + number_of_flagged_posts + number_of_flags_given + number_of_suspensions + warnings_received_count + number_of_rejected_posts + ].each { |info| result[info] = @user.public_send(info) } render json: result end @@ -1375,8 +1461,9 @@ class UsersController < ApplicationController @confirmation = AdminConfirmation.find_by_code(params[:token]) raise Discourse::NotFound unless @confirmation - raise Discourse::InvalidAccess.new unless - @confirmation.performed_by.id == (current_user&.id || @confirmation.performed_by.id) + unless @confirmation.performed_by.id == (current_user&.id || @confirmation.performed_by.id) + raise Discourse::InvalidAccess.new + end if request.post? @confirmation.email_confirmed! @@ -1385,75 +1472,86 @@ class UsersController < ApplicationController respond_to do |format| format.json { render json: success_json } - format.html { render layout: 'no_ember' } + format.html { render layout: "no_ember" } end end def list_second_factors - raise Discourse::NotFound if SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins + if SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins + raise Discourse::NotFound + end unless params[:password].empty? - RateLimiter.new(nil, "login-hr-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_hour, 1.hour).performed! - RateLimiter.new(nil, "login-min-#{request.remote_ip}", SiteSetting.max_logins_per_ip_per_minute, 1.minute).performed! + RateLimiter.new( + nil, + "login-hr-#{request.remote_ip}", + SiteSetting.max_logins_per_ip_per_hour, + 1.hour, + ).performed! + RateLimiter.new( + nil, + "login-min-#{request.remote_ip}", + SiteSetting.max_logins_per_ip_per_minute, + 1.minute, + ).performed! unless current_user.confirm_password?(params[:password]) - return render json: failed_json.merge( - error: I18n.t("login.incorrect_password") - ) + return render json: failed_json.merge(error: I18n.t("login.incorrect_password")) end confirm_secure_session end if secure_session_confirmed? - totp_second_factors = current_user.totps - .select(:id, :name, :last_used, :created_at, :method) - .where(enabled: true).order(:created_at) + totp_second_factors = + current_user + .totps + .select(:id, :name, :last_used, :created_at, :method) + .where(enabled: true) + .order(:created_at) - security_keys = current_user.security_keys.where(factor_type: UserSecurityKey.factor_types[:second_factor]).order(:created_at) + security_keys = + current_user + .security_keys + .where(factor_type: UserSecurityKey.factor_types[:second_factor]) + .order(:created_at) - render json: success_json.merge( - totps: totp_second_factors, - security_keys: security_keys - ) + render json: success_json.merge(totps: totp_second_factors, security_keys: security_keys) else - render json: success_json.merge( - password_required: true - ) + render json: success_json.merge(password_required: true) end end def create_second_factor_backup backup_codes = current_user.generate_backup_codes - render json: success_json.merge( - backup_codes: backup_codes - ) + render json: success_json.merge(backup_codes: backup_codes) end def create_second_factor_totp - require 'rotp' if !defined? ROTP + require "rotp" if !defined?(ROTP) totp_data = ROTP::Base32.random secure_session["staged-totp-#{current_user.id}"] = totp_data - qrcode_png = RQRCode::QRCode.new(current_user.totp_provisioning_uri(totp_data)).as_png( - border_modules: 1, - size: 240 - ) + qrcode_png = + RQRCode::QRCode.new(current_user.totp_provisioning_uri(totp_data)).as_png( + border_modules: 1, + size: 240, + ) - render json: success_json.merge( - key: totp_data.scan(/.{4}/).join(" "), - qr: qrcode_png.to_data_url - ) + render json: + success_json.merge(key: totp_data.scan(/.{4}/).join(" "), qr: qrcode_png.to_data_url) end def create_second_factor_security_key challenge_session = Webauthn.stage_challenge(current_user, secure_session) - render json: success_json.merge( - challenge: challenge_session.challenge, - rp_id: challenge_session.rp_id, - rp_name: challenge_session.rp_name, - supported_algorithms: ::Webauthn::SUPPORTED_ALGORITHMS, - user_secure_id: current_user.create_or_fetch_secure_identifier, - existing_active_credential_ids: current_user.second_factor_security_key_credential_ids - ) + render json: + success_json.merge( + challenge: challenge_session.challenge, + rp_id: challenge_session.rp_id, + rp_name: challenge_session.rp_name, + supported_algorithms: ::Webauthn::SUPPORTED_ALGORITHMS, + user_secure_id: current_user.create_or_fetch_secure_identifier, + existing_active_credential_ids: + current_user.second_factor_security_key_credential_ids, + ) end def register_second_factor_security_key @@ -1466,7 +1564,7 @@ class UsersController < ApplicationController params, challenge: Webauthn.challenge(current_user, secure_session), rp_id: Webauthn.rp_id(current_user, secure_session), - origin: Discourse.base_url + origin: Discourse.base_url, ).register_second_factor_security_key render json: success_json rescue ::Webauthn::SecurityKeyError => err @@ -1477,12 +1575,8 @@ class UsersController < ApplicationController user_security_key = current_user.security_keys.find_by(id: params[:id].to_i) raise Discourse::InvalidParameters unless user_security_key - if params[:name] && !params[:name].blank? - user_security_key.update!(name: params[:name]) - end - if params[:disable] == "true" - user_security_key.update!(enabled: false) - end + user_security_key.update!(name: params[:name]) if params[:name] && !params[:name].blank? + user_security_key.update!(enabled: false) if params[:disable] == "true" render json: success_json end @@ -1501,15 +1595,15 @@ class UsersController < ApplicationController rate_limit_second_factor!(current_user) - authenticated = !auth_token.blank? && totp_object.verify( - auth_token, - drift_ahead: SecondFactorManager::TOTP_ALLOWED_DRIFT_SECONDS, - drift_behind: SecondFactorManager::TOTP_ALLOWED_DRIFT_SECONDS - ) + authenticated = + !auth_token.blank? && + totp_object.verify( + auth_token, + drift_ahead: SecondFactorManager::TOTP_ALLOWED_DRIFT_SECONDS, + drift_behind: SecondFactorManager::TOTP_ALLOWED_DRIFT_SECONDS, + ) unless authenticated - return render json: failed_json.merge( - error: I18n.t("login.invalid_second_factor_code") - ) + return render json: failed_json.merge(error: I18n.t("login.invalid_second_factor_code")) end current_user.create_totp(data: totp_data, name: params[:name], enabled: true) render json: success_json @@ -1523,7 +1617,7 @@ class UsersController < ApplicationController Jobs.enqueue( :critical_user_email, type: "account_second_factor_disabled", - user_id: current_user.id + user_id: current_user.id, ) render json: success_json @@ -1543,24 +1637,26 @@ class UsersController < ApplicationController raise Discourse::InvalidParameters unless user_second_factor - if params[:name] && !params[:name].blank? - user_second_factor.update!(name: params[:name]) - end + user_second_factor.update!(name: params[:name]) if params[:name] && !params[:name].blank? if params[:disable] == "true" # Disabling backup codes deletes *all* backup codes if update_second_factor_method == UserSecondFactor.methods[:backup_codes] - current_user.user_second_factors.where(method: UserSecondFactor.methods[:backup_codes]).destroy_all + current_user + .user_second_factors + .where(method: UserSecondFactor.methods[:backup_codes]) + .destroy_all else user_second_factor.update!(enabled: false) end - end render json: success_json end def second_factor_check_confirmed_password - raise Discourse::NotFound if SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins + if SiteSetting.enable_discourse_connect || !SiteSetting.enable_local_logins + raise Discourse::NotFound + end raise Discourse::InvalidAccess.new unless current_user && secure_session_confirmed? end @@ -1585,9 +1681,9 @@ class UsersController < ApplicationController render json: success_json else render json: { - success: false, - message: I18n.t("associated_accounts.revoke_failed", provider_name: provider_name) - } + success: false, + message: I18n.t("associated_accounts.revoke_failed", provider_name: provider_name), + } end end end @@ -1599,7 +1695,9 @@ class UsersController < ApplicationController if params[:token_id] token = UserAuthToken.find_by(id: params[:token_id], user_id: user.id) # The user should not be able to revoke the auth token of current session. - raise Discourse::InvalidParameters.new(:token_id) if !token || guardian.auth_token == token.auth_token + if !token || guardian.auth_token == token.auth_token + raise Discourse::InvalidParameters.new(:token_id) + end UserAuthToken.where(id: params[:token_id], user_id: user.id).each(&:destroy!) MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] @@ -1615,7 +1713,12 @@ class UsersController < ApplicationController topic = Topic.find(params[:topic_id].to_i) if !guardian.can_feature_topic?(user, topic) - return render_json_error(I18n.t('activerecord.errors.models.user_profile.attributes.featured_topic_id.invalid'), 403) + return( + render_json_error( + I18n.t("activerecord.errors.models.user_profile.attributes.featured_topic_id.invalid"), + 403, + ) + ) end user.user_profile.update(featured_topic_id: topic.id) @@ -1640,24 +1743,27 @@ class UsersController < ApplicationController bookmark_list.load if bookmark_list.bookmarks.empty? - render json: { - bookmarks: [] - } + render json: { bookmarks: [] } else page = params[:page].to_i + 1 - bookmark_list.more_bookmarks_url = "#{Discourse.base_path}/u/#{params[:username]}/bookmarks.json?page=#{page}" + bookmark_list.more_bookmarks_url = + "#{Discourse.base_path}/u/#{params[:username]}/bookmarks.json?page=#{page}" render_serialized(bookmark_list, UserBookmarkListSerializer) end end format.ics do - @bookmark_reminders = Bookmark.with_reminders - .where(user_id: user.id) - .order(:reminder_at) - .map do |bookmark| - bookmark.registered_bookmarkable.serializer.new( - bookmark, scope: user_guardian, root: false - ) - end + @bookmark_reminders = + Bookmark + .with_reminders + .where(user_id: user.id) + .order(:reminder_at) + .map do |bookmark| + bookmark.registered_bookmarkable.serializer.new( + bookmark, + scope: user_guardian, + root: false, + ) + end end end end @@ -1668,22 +1774,24 @@ class UsersController < ApplicationController raise Discourse::InvalidAccess.new("username doesn't match current_user's username") end - reminder_notifications = Notification - .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) - .unread - .where(notification_type: Notification.types[:bookmark_reminder]) + reminder_notifications = + Notification + .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) + .unread + .where(notification_type: Notification.types[:bookmark_reminder]) if reminder_notifications.size < USER_MENU_LIST_LIMIT - exclude_bookmark_ids = reminder_notifications - .filter_map { |notification| notification.data_hash[:bookmark_id] } + exclude_bookmark_ids = + reminder_notifications.filter_map { |notification| notification.data_hash[:bookmark_id] } - bookmark_list = UserBookmarkList.new( - user: current_user, - guardian: guardian, - params: { - per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size - } - ) + bookmark_list = + UserBookmarkList.new( + user: current_user, + guardian: guardian, + params: { + per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size, + }, + ) bookmark_list.load do |query| if exclude_bookmark_ids.present? query.where("bookmarks.id NOT IN (?)", exclude_bookmark_ids) @@ -1692,27 +1800,26 @@ class UsersController < ApplicationController end if reminder_notifications.present? - serialized_notifications = ActiveModel::ArraySerializer.new( - reminder_notifications, - each_serializer: NotificationSerializer, - scope: guardian - ) + serialized_notifications = + ActiveModel::ArraySerializer.new( + reminder_notifications, + each_serializer: NotificationSerializer, + scope: guardian, + ) end if bookmark_list bookmark_list.bookmark_serializer_opts = { link_to_first_unread_post: true } - serialized_bookmarks = serialize_data( - bookmark_list, - UserBookmarkListSerializer, - scope: guardian, - root: false - )[:bookmarks] + serialized_bookmarks = + serialize_data(bookmark_list, UserBookmarkListSerializer, scope: guardian, root: false)[ + :bookmarks + ] end render json: { - notifications: serialized_notifications || [], - bookmarks: serialized_bookmarks || [] - } + notifications: serialized_notifications || [], + bookmarks: serialized_bookmarks || [], + } end def user_menu_messages @@ -1720,68 +1827,71 @@ class UsersController < ApplicationController raise Discourse::InvalidAccess.new("username doesn't match current_user's username") end - if !current_user.staff? && !current_user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) + if !current_user.staff? && + !current_user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) raise Discourse::InvalidAccess.new("personal messages are disabled.") end - unread_notifications = Notification - .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) - .unread - .where(notification_type: [Notification.types[:private_message], Notification.types[:group_message_summary]]) - .to_a + unread_notifications = + Notification + .for_user_menu(current_user.id, limit: USER_MENU_LIST_LIMIT) + .unread + .where( + notification_type: [ + Notification.types[:private_message], + Notification.types[:group_message_summary], + ], + ) + .to_a if unread_notifications.size < USER_MENU_LIST_LIMIT exclude_topic_ids = unread_notifications.filter_map(&:topic_id).uniq limit = USER_MENU_LIST_LIMIT - unread_notifications.size - messages_list = TopicQuery.new( - current_user, - per_page: limit - ).list_private_messages_direct_and_groups(current_user) do |query| - if exclude_topic_ids.present? - query.where("topics.id NOT IN (?)", exclude_topic_ids) - else - query - end - end - read_notifications = Notification - .for_user_menu(current_user.id, limit: limit) - .where( - read: true, - notification_type: Notification.types[:group_message_summary], - ) - .to_a + messages_list = + TopicQuery + .new(current_user, per_page: limit) + .list_private_messages_direct_and_groups(current_user) do |query| + if exclude_topic_ids.present? + query.where("topics.id NOT IN (?)", exclude_topic_ids) + else + query + end + end + read_notifications = + Notification + .for_user_menu(current_user.id, limit: limit) + .where(read: true, notification_type: Notification.types[:group_message_summary]) + .to_a end if unread_notifications.present? - serialized_unread_notifications = ActiveModel::ArraySerializer.new( - unread_notifications, - each_serializer: NotificationSerializer, - scope: guardian - ) + serialized_unread_notifications = + ActiveModel::ArraySerializer.new( + unread_notifications, + each_serializer: NotificationSerializer, + scope: guardian, + ) end if messages_list - serialized_messages = serialize_data( - messages_list, - TopicListSerializer, - scope: guardian, - root: false - )[:topics] + serialized_messages = + serialize_data(messages_list, TopicListSerializer, scope: guardian, root: false)[:topics] end if read_notifications.present? - serialized_read_notifications = ActiveModel::ArraySerializer.new( - read_notifications, - each_serializer: NotificationSerializer, - scope: guardian - ) + serialized_read_notifications = + ActiveModel::ArraySerializer.new( + read_notifications, + each_serializer: NotificationSerializer, + scope: guardian, + ) end render json: { - unread_notifications: serialized_unread_notifications || [], - read_notifications: serialized_read_notifications || [], - topics: serialized_messages || [] - } + unread_notifications: serialized_unread_notifications || [], + read_notifications: serialized_read_notifications || [], + topics: serialized_messages || [], + } end private @@ -1803,11 +1913,12 @@ class UsersController < ApplicationController end def password_reset_find_user(token, committing_change:) - @user = if committing_change - EmailToken.confirm(token, scope: EmailToken.scopes[:password_reset]) - else - EmailToken.confirmable(token, scope: EmailToken.scopes[:password_reset])&.user - end + @user = + if committing_change + EmailToken.confirm(token, scope: EmailToken.scopes[:password_reset]) + else + EmailToken.confirmable(token, scope: EmailToken.scopes[:password_reset])&.user + end if @user secure_session["password-#{token}"] = @user.id @@ -1816,16 +1927,16 @@ class UsersController < ApplicationController @user = User.find(user_id) if user_id > 0 end - @error = I18n.t('password_reset.no_token', base_url: Discourse.base_url) if !@user + @error = I18n.t("password_reset.no_token", base_url: Discourse.base_url) if !@user end def respond_to_suspicious_request if suspicious?(params) render json: { - success: true, - active: false, - message: I18n.t("login.activate_email", email: params[:email]) - } + success: true, + active: false, + message: I18n.t("login.activate_email", email: params[:email]), + } end end @@ -1837,30 +1948,30 @@ class UsersController < ApplicationController def honeypot_or_challenge_fails?(params) return false if is_api? params[:password_confirmation] != honeypot_value || - params[:challenge] != challenge_value.try(:reverse) + params[:challenge] != challenge_value.try(:reverse) end def user_params - permitted = [ - :name, - :email, - :password, - :username, - :title, - :date_of_birth, - :muted_usernames, - :allowed_pm_usernames, - :theme_ids, - :locale, - :bio_raw, - :location, - :website, - :dismissed_banner_key, - :profile_background_upload_url, - :card_background_upload_url, - :primary_group_id, - :flair_group_id, - :featured_topic_id, + permitted = %i[ + name + email + password + username + title + date_of_birth + muted_usernames + allowed_pm_usernames + theme_ids + locale + bio_raw + location + website + dismissed_banner_key + profile_background_upload_url + card_background_upload_url + primary_group_id + flair_group_id + featured_topic_id ] editable_custom_fields = User.editable_user_custom_fields(by_staff: current_user.try(:staff?)) @@ -1888,21 +1999,17 @@ class UsersController < ApplicationController if SiteSetting.enable_user_status permitted << :status - permitted << { status: [:emoji, :description, :ends_at] } + permitted << { status: %i[emoji description ends_at] } end - result = params - .permit(permitted, theme_ids: [], seen_popups: []) - .reverse_merge( + result = + params.permit(permitted, theme_ids: [], seen_popups: []).reverse_merge( ip_address: request.remote_ip, - registration_ip_address: request.remote_ip + registration_ip_address: request.remote_ip, ) - if !UsernameCheckerService.is_developer?(result['email']) && - is_api? && - current_user.present? && - current_user.admin? - + if !UsernameCheckerService.is_developer?(result["email"]) && is_api? && current_user.present? && + current_user.admin? result.merge!(params.permit(:active, :staged, :approved)) end @@ -1923,7 +2030,7 @@ class UsersController < ApplicationController ip = request.remote_ip user_id = (current_user.id if current_user) - Scheduler::Defer.later 'Track profile view visit' do + Scheduler::Defer.later "Track profile view visit" do UserProfileView.add(user_profile_id, ip, user_id) end end @@ -1956,17 +2063,12 @@ class UsersController < ApplicationController end def render_invite_error(message) - render json: { - invites: [], - can_see_invite_details: false, - error: message - } + render json: { invites: [], can_see_invite_details: false, error: message } end def serialize_found_users(users) - each_serializer = SiteSetting.enable_user_status? ? - FoundUserWithStatusSerializer : - FoundUserSerializer + each_serializer = + SiteSetting.enable_user_status? ? FoundUserWithStatusSerializer : FoundUserSerializer { users: ActiveModel::ArraySerializer.new(users, each_serializer: each_serializer).as_json } end diff --git a/app/controllers/users_email_controller.rb b/app/controllers/users_email_controller.rb index f3f30a47dd..98ac6458ed 100644 --- a/app/controllers/users_email_controller.rb +++ b/app/controllers/users_email_controller.rb @@ -1,35 +1,31 @@ # frozen_string_literal: true class UsersEmailController < ApplicationController + requires_login only: %i[index update] - requires_login only: [:index, :update] + skip_before_action :check_xhr, + only: %i[ + confirm_old_email + show_confirm_old_email + confirm_new_email + show_confirm_new_email + ] - skip_before_action :check_xhr, only: [ - :confirm_old_email, - :show_confirm_old_email, - :confirm_new_email, - :show_confirm_new_email - ] + skip_before_action :redirect_to_login_if_required, + only: %i[ + confirm_old_email + show_confirm_old_email + confirm_new_email + show_confirm_new_email + ] - skip_before_action :redirect_to_login_if_required, only: [ - :confirm_old_email, - :show_confirm_old_email, - :confirm_new_email, - :show_confirm_new_email - ] - - before_action :require_login, only: [ - :confirm_old_email, - :show_confirm_old_email - ] + before_action :require_login, only: %i[confirm_old_email show_confirm_old_email] def index end def create - if !SiteSetting.enable_secondary_emails - return render json: failed_json, status: 410 - end + return render json: failed_json, status: 410 if !SiteSetting.enable_secondary_emails params.require(:email) user = fetch_user_from_params @@ -40,9 +36,7 @@ class UsersEmailController < ApplicationController updater = EmailUpdater.new(guardian: guardian, user: user) updater.change_to(params[:email], add: true) - if updater.errors.present? - return render_json_error(updater.errors.full_messages) - end + return render_json_error(updater.errors.full_messages) if updater.errors.present? render body: nil rescue RateLimiter::LimitExceeded @@ -59,9 +53,7 @@ class UsersEmailController < ApplicationController updater = EmailUpdater.new(guardian: guardian, user: user) updater.change_to(params[:email]) - if updater.errors.present? - return render_json_error(updater.errors.full_messages) - end + return render_json_error(updater.errors.full_messages) if updater.errors.present? render body: nil rescue RateLimiter::LimitExceeded @@ -119,9 +111,7 @@ class UsersEmailController < ApplicationController def show_confirm_new_email load_change_request(:new) - if params[:done].to_s == "true" - @done = true - end + @done = true if params[:done].to_s == "true" if @change_request&.change_state != EmailChangeRequest.states[:authorizing_new] @error = I18n.t("change_email.already_done") @@ -135,21 +125,20 @@ class UsersEmailController < ApplicationController if params[:show_backup].to_s == "true" && @backup_codes_enabled @show_backup_codes = true else - if @user.totp_enabled? - @show_second_factor = true - end + @show_second_factor = true if @user.totp_enabled? if @user.security_keys_enabled? Webauthn.stage_challenge(@user, secure_session) @show_security_key = params[:show_totp].to_s == "true" ? false : true @security_key_challenge = Webauthn.challenge(@user, secure_session) - @security_key_allowed_credential_ids = Webauthn.allowed_credentials(@user, secure_session)[:allowed_credential_ids] + @security_key_allowed_credential_ids = + Webauthn.allowed_credentials(@user, secure_session)[:allowed_credential_ids] end end @to_email = @change_request.new_email end - render layout: 'no_ember' + render layout: "no_ember" end def confirm_old_email @@ -183,16 +172,14 @@ class UsersEmailController < ApplicationController @error = I18n.t("change_email.already_done") end - if params[:done].to_s == "true" - @almost_done = true - end + @almost_done = true if params[:done].to_s == "true" if !@error @from_email = @user.email @to_email = @change_request.new_email end - render layout: 'no_ember' + render layout: "no_ember" end private @@ -204,27 +191,24 @@ class UsersEmailController < ApplicationController if token if type == :old - @change_request = token.user&.email_change_requests.where(old_email_token_id: token.id).first + @change_request = + token.user&.email_change_requests.where(old_email_token_id: token.id).first elsif type == :new - @change_request = token.user&.email_change_requests.where(new_email_token_id: token.id).first + @change_request = + token.user&.email_change_requests.where(new_email_token_id: token.id).first end end @user = token&.user - if (!@user || !@change_request) - @error = I18n.t("change_email.already_done") - end + @error = I18n.t("change_email.already_done") if (!@user || !@change_request) if current_user && current_user.id != @user&.id - @error = I18n.t 'change_email.wrong_account_error' + @error = I18n.t "change_email.wrong_account_error" end end def require_login - if !current_user - redirect_to_login - end + redirect_to_login if !current_user end - end diff --git a/app/controllers/webhooks_controller.rb b/app/controllers/webhooks_controller.rb index 74024b3152..b84d59c269 100644 --- a/app/controllers/webhooks_controller.rb +++ b/app/controllers/webhooks_controller.rb @@ -95,8 +95,8 @@ class WebhooksController < ActionController::Base message_event = event.dig("msys", "message_event") next unless message_event - message_id = message_event.dig("rcpt_meta", "message_id") - to_address = message_event["rcpt_to"] + message_id = message_event.dig("rcpt_meta", "message_id") + to_address = message_event["rcpt_to"] bounce_class = message_event["bounce_class"] next unless bounce_class @@ -116,7 +116,7 @@ class WebhooksController < ActionController::Base end def aws - raw = request.raw_post + raw = request.raw_post json = JSON.parse(raw) case json["Type"] @@ -152,11 +152,14 @@ class WebhooksController < ActionController::Base return false if (Time.at(timestamp.to_i) - Time.now).abs > 12.hours.to_i # check the signature - signature == OpenSSL::HMAC.hexdigest("SHA256", SiteSetting.mailgun_api_key, "#{timestamp}#{token}") + signature == + OpenSSL::HMAC.hexdigest("SHA256", SiteSetting.mailgun_api_key, "#{timestamp}#{token}") end def handle_mailgun_legacy(params) - return mailgun_failure unless valid_mailgun_signature?(params["token"], params["timestamp"], params["signature"]) + unless valid_mailgun_signature?(params["token"], params["timestamp"], params["signature"]) + return mailgun_failure + end event = params["event"] message_id = Email::MessageIdService.message_id_clean(params["Message-Id"]) @@ -177,7 +180,13 @@ class WebhooksController < ActionController::Base def handle_mailgun_new(params) signature = params["signature"] - return mailgun_failure unless valid_mailgun_signature?(signature["token"], signature["timestamp"], signature["signature"]) + unless valid_mailgun_signature?( + signature["token"], + signature["timestamp"], + signature["signature"], + ) + return mailgun_failure + end data = params["event-data"] error_code = params.dig("delivery-status", "code") @@ -207,5 +216,4 @@ class WebhooksController < ActionController::Base Email::Receiver.update_bounce_score(email_log.user.email, bounce_score) end - end diff --git a/app/controllers/wizard_controller.rb b/app/controllers/wizard_controller.rb index e7581dcca5..cdce04c158 100644 --- a/app/controllers/wizard_controller.rb +++ b/app/controllers/wizard_controller.rb @@ -13,9 +13,7 @@ class WizardController < ApplicationController render_serialized(wizard, WizardSerializer) end - format.html do - render body: nil - end + format.html { render body: nil } end end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 06e8be2145..2520cd69b5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,7 +1,7 @@ # coding: utf-8 # frozen_string_literal: true -require 'current_user' -require 'canonical_url' +require "current_user" +require "canonical_url" module ApplicationHelper include CurrentUser @@ -14,7 +14,6 @@ module ApplicationHelper end def discourse_config_environment(testing: false) - # TODO: Can this come from Ember CLI somehow? config = { modulePrefix: "discourse", @@ -23,24 +22,27 @@ module ApplicationHelper locationType: "history", historySupportMiddleware: false, EmberENV: { - FEATURES: {}, - EXTEND_PROTOTYPES: { "Date": false }, + FEATURES: { + }, + EXTEND_PROTOTYPES: { + Date: false, + }, _APPLICATION_TEMPLATE_WRAPPER: false, _DEFAULT_ASYNC_OBSERVERS: true, - _JQUERY_INTEGRATION: true + _JQUERY_INTEGRATION: true, }, APP: { name: "discourse", version: "#{Discourse::VERSION::STRING} #{Discourse.git_version}", - exportApplicationGlobal: true - } + exportApplicationGlobal: true, + }, } if testing config[:environment] = "test" config[:locationType] = "none" config[:APP][:autoboot] = false - config[:APP][:rootElement] = '#ember-testing' + config[:APP][:rootElement] = "#ember-testing" end config.to_json @@ -48,15 +50,9 @@ module ApplicationHelper def google_universal_analytics_json(ua_domain_name = nil) result = {} - if ua_domain_name - result[:cookieDomain] = ua_domain_name.gsub(/^http(s)?:\/\//, '') - end - if current_user.present? - result[:userId] = current_user.id - end - if SiteSetting.ga_universal_auto_link_domains.present? - result[:allowLinker] = true - end + result[:cookieDomain] = ua_domain_name.gsub(%r{^http(s)?://}, "") if ua_domain_name + result[:userId] = current_user.id if current_user.present? + result[:allowLinker] = true if SiteSetting.ga_universal_auto_link_domains.present? result.to_json end @@ -73,7 +69,7 @@ module ApplicationHelper end def shared_session_key - if SiteSetting.long_polling_base_url != '/' && current_user + if SiteSetting.long_polling_base_url != "/" && current_user sk = "shared_session_key" return request.env[sk] if request.env[sk] @@ -95,10 +91,15 @@ module ApplicationHelper path = ActionController::Base.helpers.asset_path("#{script}.js") if GlobalSetting.use_s3? && GlobalSetting.s3_cdn_url - resolved_s3_asset_cdn_url = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url + resolved_s3_asset_cdn_url = + GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url if GlobalSetting.cdn_url folder = ActionController::Base.config.relative_url_root || "/" - path = path.gsub(File.join(GlobalSetting.cdn_url, folder, "/"), File.join(resolved_s3_asset_cdn_url, "/")) + path = + path.gsub( + File.join(GlobalSetting.cdn_url, folder, "/"), + File.join(resolved_s3_asset_cdn_url, "/"), + ) else # we must remove the subfolder path here, assets are uploaded to s3 # without it getting involved @@ -121,8 +122,8 @@ module ApplicationHelper path = path.gsub(/\.([^.]+)$/, '.gz.\1') end end - - elsif GlobalSetting.cdn_url&.start_with?("https") && is_brotli_req? && Rails.env != "development" + elsif GlobalSetting.cdn_url&.start_with?("https") && is_brotli_req? && + Rails.env != "development" path = path.gsub("#{GlobalSetting.cdn_url}/assets/", "#{GlobalSetting.cdn_url}/brotli_asset/") end @@ -136,14 +137,17 @@ module ApplicationHelper scripts.push(*chunks) end - scripts.map do |name| - path = script_asset_path(name) - preload_script_url(path) - end.join("\n").html_safe + scripts + .map do |name| + path = script_asset_path(name) + preload_script_url(path) + end + .join("\n") + .html_safe end def preload_script_url(url) - add_resource_preload_list(url, 'script') + add_resource_preload_list(url, "script") if GlobalSetting.preload_link_header <<~HTML.html_safe @@ -157,50 +161,47 @@ module ApplicationHelper end def add_resource_preload_list(resource_url, type) - @links_to_preload << %Q(<#{resource_url}>; rel="preload"; as="#{type}") if !@links_to_preload.nil? + if !@links_to_preload.nil? + @links_to_preload << %Q(<#{resource_url}>; rel="preload"; as="#{type}") + end end def discourse_csrf_tags # anon can not have a CSRF token cause these are all pages # that may be cached, causing a mismatch between session CSRF # and CSRF on page and horrible impossible to debug login issues - if current_user - csrf_meta_tags - end + csrf_meta_tags if current_user end def html_classes list = [] - list << (mobile_view? ? 'mobile-view' : 'desktop-view') - list << (mobile_device? ? 'mobile-device' : 'not-mobile-device') - list << 'ios-device' if ios_device? - list << 'rtl' if rtl? + list << (mobile_view? ? "mobile-view" : "desktop-view") + list << (mobile_device? ? "mobile-device" : "not-mobile-device") + list << "ios-device" if ios_device? + list << "rtl" if rtl? list << text_size_class - list << 'anon' unless current_user - list.join(' ') + list << "anon" unless current_user + list.join(" ") end def body_classes result = ApplicationHelper.extra_body_classes.to_a - if @category && @category.url.present? - result << "category-#{@category.slug_path.join('-')}" - end + result << "category-#{@category.slug_path.join("-")}" if @category && @category.url.present? - if current_user.present? && - current_user.primary_group_id && - primary_group_name = Group.where(id: current_user.primary_group_id).pluck_first(:name) + if current_user.present? && current_user.primary_group_id && + primary_group_name = Group.where(id: current_user.primary_group_id).pluck_first(:name) result << "primary-group-#{primary_group_name.downcase}" end - result.join(' ') + result.join(" ") end def text_size_class requested_cookie_size, cookie_seq = cookies[:text_size]&.split("|") server_seq = current_user&.user_option&.text_size_seq if cookie_seq && server_seq && cookie_seq.to_i >= server_seq && - UserOption.text_sizes.keys.include?(requested_cookie_size&.to_sym) + UserOption.text_sizes.keys.include?(requested_cookie_size&.to_sym) cookie_size = requested_cookie_size end @@ -211,11 +212,11 @@ module ApplicationHelper def escape_unicode(javascript) if javascript javascript = javascript.scrub - javascript.gsub!(/\342\200\250/u, '
') - javascript.gsub!(/(<\/)/u, '\u003C/') + javascript.gsub!(/\342\200\250/u, "
") + javascript.gsub!(%r{( 0 && opts[:like_count] && opts[:like_count] > 0 - result << tag(:meta, name: 'twitter:label1', value: I18n.t("reading_time")) - result << tag(:meta, name: 'twitter:data1', value: "#{opts[:read_time]} mins 🕑") - result << tag(:meta, name: 'twitter:label2', value: I18n.t("likes")) - result << tag(:meta, name: 'twitter:data2', value: "#{opts[:like_count]} ❤") + result << tag(:meta, name: "twitter:label1", value: I18n.t("reading_time")) + result << tag(:meta, name: "twitter:data1", value: "#{opts[:read_time]} mins 🕑") + result << tag(:meta, name: "twitter:label2", value: I18n.t("likes")) + result << tag(:meta, name: "twitter:data2", value: "#{opts[:like_count]} ❤") end if opts[:published_time] - result << tag(:meta, property: 'article:published_time', content: opts[:published_time]) + result << tag(:meta, property: "article:published_time", content: opts[:published_time]) end - if opts[:ignore_canonical] - result << tag(:meta, property: 'og:ignore_canonical', content: true) - end + result << tag(:meta, property: "og:ignore_canonical", content: true) if opts[:ignore_canonical] result.join("\n") end private def generate_twitter_card_metadata(result, opts) - img_url = opts[:twitter_summary_large_image].present? ? \ - opts[:twitter_summary_large_image] : - opts[:image] + img_url = + ( + if opts[:twitter_summary_large_image].present? + opts[:twitter_summary_large_image] + else + opts[:image] + end + ) # Twitter does not allow SVGs, see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup if img_url.ends_with?(".svg") @@ -339,29 +344,29 @@ module ApplicationHelper end if opts[:twitter_summary_large_image].present? && img_url.present? - result << tag(:meta, name: 'twitter:card', content: "summary_large_image") + result << tag(:meta, name: "twitter:card", content: "summary_large_image") result << tag(:meta, name: "twitter:image", content: img_url) elsif opts[:image].present? && img_url.present? - result << tag(:meta, name: 'twitter:card', content: "summary") + result << tag(:meta, name: "twitter:card", content: "summary") result << tag(:meta, name: "twitter:image", content: img_url) else - result << tag(:meta, name: 'twitter:card', content: "summary") + result << tag(:meta, name: "twitter:card", content: "summary") end end def render_sitelinks_search_tag - if current_page?('/') || current_page?(Discourse.base_path) + if current_page?("/") || current_page?(Discourse.base_path) json = { - '@context' => 'http://schema.org', - '@type' => 'WebSite', - url: Discourse.base_url, - potentialAction: { - '@type' => 'SearchAction', - target: "#{Discourse.base_url}/search?q={search_term_string}", - 'query-input' => 'required name=search_term_string', - } + "@context" => "http://schema.org", + "@type" => "WebSite", + :url => Discourse.base_url, + :potentialAction => { + "@type" => "SearchAction", + :target => "#{Discourse.base_url}/search?q={search_term_string}", + "query-input" => "required name=search_term_string", + }, } - content_tag(:script, MultiJson.dump(json).html_safe, type: 'application/ld+json') + content_tag(:script, MultiJson.dump(json).html_safe, type: "application/ld+json") end end @@ -370,33 +375,35 @@ module ApplicationHelper end def application_logo_url - @application_logo_url ||= begin - if mobile_view? - if dark_color_scheme? && SiteSetting.site_mobile_logo_dark_url.present? - SiteSetting.site_mobile_logo_dark_url - elsif SiteSetting.site_mobile_logo_url.present? - SiteSetting.site_mobile_logo_url - end - else - if dark_color_scheme? && SiteSetting.site_logo_dark_url.present? - SiteSetting.site_logo_dark_url + @application_logo_url ||= + begin + if mobile_view? + if dark_color_scheme? && SiteSetting.site_mobile_logo_dark_url.present? + SiteSetting.site_mobile_logo_dark_url + elsif SiteSetting.site_mobile_logo_url.present? + SiteSetting.site_mobile_logo_url + end else - SiteSetting.site_logo_url + if dark_color_scheme? && SiteSetting.site_logo_dark_url.present? + SiteSetting.site_logo_dark_url + else + SiteSetting.site_logo_url + end end end - end end def application_logo_dark_url - @application_logo_dark_url ||= begin - if dark_scheme_id != -1 - if mobile_view? && SiteSetting.site_mobile_logo_dark_url != application_logo_url - SiteSetting.site_mobile_logo_dark_url - elsif !mobile_view? && SiteSetting.site_logo_dark_url != application_logo_url - SiteSetting.site_logo_dark_url + @application_logo_dark_url ||= + begin + if dark_scheme_id != -1 + if mobile_view? && SiteSetting.site_mobile_logo_dark_url != application_logo_url + SiteSetting.site_mobile_logo_dark_url + elsif !mobile_view? && SiteSetting.site_logo_dark_url != application_logo_url + SiteSetting.site_logo_dark_url + end end end - end end def login_path @@ -437,8 +444,11 @@ module ApplicationHelper def ios_app_argument # argument only makes sense for DiscourseHub app - SiteSetting.ios_app_id == "1173672076" ? - ", app-argument=discourse://new?siteUrl=#{Discourse.base_url}" : "" + if SiteSetting.ios_app_id == "1173672076" + ", app-argument=discourse://new?siteUrl=#{Discourse.base_url}" + else + "" + end end def include_splash_screen? @@ -496,9 +506,9 @@ module ApplicationHelper uri = UrlHelper.encode_and_parse(link) uri = URI.parse("http://#{uri}") if uri.scheme.nil? host = uri.host.downcase - host.start_with?('www.') ? host[4..-1] : host - rescue - '' + host.start_with?("www.") ? host[4..-1] : host + rescue StandardError + "" end end @@ -529,7 +539,8 @@ module ApplicationHelper end def dark_scheme_id - cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id || SiteSetting.default_dark_mode_color_scheme_id + cookies[:dark_scheme_id] || current_user&.user_option&.dark_scheme_id || + SiteSetting.default_dark_mode_color_scheme_id end def current_homepage @@ -556,7 +567,7 @@ module ApplicationHelper theme_id, mobile_view? ? :mobile : :desktop, name, - skip_transformation: request.env[:skip_theme_ids_transformation].present? + skip_transformation: request.env[:skip_theme_ids_transformation].present?, ) end @@ -565,7 +576,7 @@ module ApplicationHelper theme_id, :translations, I18n.locale, - skip_transformation: request.env[:skip_theme_ids_transformation].present? + skip_transformation: request.env[:skip_theme_ids_transformation].present?, ) end @@ -574,42 +585,41 @@ module ApplicationHelper theme_id, :extra_js, nil, - skip_transformation: request.env[:skip_theme_ids_transformation].present? + skip_transformation: request.env[:skip_theme_ids_transformation].present?, ) end def discourse_stylesheet_preload_tag(name, opts = {}) manager = if opts.key?(:theme_id) - Stylesheet::Manager.new( - theme_id: customization_disabled? ? nil : opts[:theme_id] - ) + Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id]) else stylesheet_manager end - manager.stylesheet_preload_tag(name, 'all') + manager.stylesheet_preload_tag(name, "all") end def discourse_stylesheet_link_tag(name, opts = {}) manager = if opts.key?(:theme_id) - Stylesheet::Manager.new( - theme_id: customization_disabled? ? nil : opts[:theme_id] - ) + Stylesheet::Manager.new(theme_id: customization_disabled? ? nil : opts[:theme_id]) else stylesheet_manager end - manager.stylesheet_link_tag(name, 'all', self.method(:add_resource_preload_list)) + manager.stylesheet_link_tag(name, "all", self.method(:add_resource_preload_list)) end def discourse_preload_color_scheme_stylesheets result = +"" - result << stylesheet_manager.color_scheme_stylesheet_preload_tag(scheme_id, 'all') + result << stylesheet_manager.color_scheme_stylesheet_preload_tag(scheme_id, "all") if dark_scheme_id != -1 - result << stylesheet_manager.color_scheme_stylesheet_preload_tag(dark_scheme_id, '(prefers-color-scheme: dark)') + result << stylesheet_manager.color_scheme_stylesheet_preload_tag( + dark_scheme_id, + "(prefers-color-scheme: dark)", + ) end result.html_safe @@ -617,10 +627,18 @@ module ApplicationHelper def discourse_color_scheme_stylesheets result = +"" - result << stylesheet_manager.color_scheme_stylesheet_link_tag(scheme_id, 'all', self.method(:add_resource_preload_list)) + result << stylesheet_manager.color_scheme_stylesheet_link_tag( + scheme_id, + "all", + self.method(:add_resource_preload_list), + ) if dark_scheme_id != -1 - result << stylesheet_manager.color_scheme_stylesheet_link_tag(dark_scheme_id, '(prefers-color-scheme: dark)', self.method(:add_resource_preload_list)) + result << stylesheet_manager.color_scheme_stylesheet_link_tag( + dark_scheme_id, + "(prefers-color-scheme: dark)", + self.method(:add_resource_preload_list), + ) end result.html_safe @@ -630,12 +648,12 @@ module ApplicationHelper result = +"" if dark_scheme_id != -1 result << <<~HTML - - + + HTML else result << <<~HTML - + HTML end result.html_safe @@ -647,7 +665,7 @@ module ApplicationHelper end def preloaded_json - return '{}' if @preloaded.blank? + return "{}" if @preloaded.blank? @preloaded.transform_values { |value| escape_unicode(value) }.to_json end @@ -658,8 +676,8 @@ module ApplicationHelper base_uri: Discourse.base_path, environment: Rails.env, letter_avatar_version: LetterAvatar.version, - markdown_it_url: script_asset_path('markdown-it-bundle'), - service_worker_url: 'service-worker.js', + markdown_it_url: script_asset_path("markdown-it-bundle"), + service_worker_url: "service-worker.js", default_locale: SiteSetting.default_locale, asset_version: Discourse.assets_digest, disable_custom_css: loading_admin?, @@ -668,16 +686,14 @@ module ApplicationHelper enable_js_error_reporting: GlobalSetting.enable_js_error_reporting, color_scheme_is_dark: dark_color_scheme?, user_color_scheme_id: scheme_id, - user_dark_scheme_id: dark_scheme_id + user_dark_scheme_id: dark_scheme_id, } if Rails.env.development? setup_data[:svg_icon_list] = SvgSprite.all_icons(theme_id) - if ENV['DEBUG_PRELOADED_APP_DATA'] - setup_data[:debug_preloaded_app_data] = true - end - setup_data[:mb_last_file_change_id] = MessageBus.last_id('/file-change') + setup_data[:debug_preloaded_app_data] = true if ENV["DEBUG_PRELOADED_APP_DATA"] + setup_data[:mb_last_file_change_id] = MessageBus.last_id("/file-change") end if guardian.can_enable_safe_mode? && params["safe_mode"] @@ -694,10 +710,10 @@ module ApplicationHelper def get_absolute_image_url(link) absolute_url = link - if link.start_with?('//') + if link.start_with?("//") uri = URI(Discourse.base_url) absolute_url = "#{uri.scheme}:#{link}" - elsif link.start_with?('/uploads/', '/images/', '/user_avatar/') + elsif link.start_with?("/uploads/", "/images/", "/user_avatar/") absolute_url = "#{Discourse.base_url}#{link}" elsif GlobalSetting.relative_url_root && link.start_with?(GlobalSetting.relative_url_root) absolute_url = "#{Discourse.base_url_no_prefix}#{link}" @@ -713,9 +729,8 @@ module ApplicationHelper end def can_sign_up? - SiteSetting.allow_new_registrations && - !SiteSetting.invite_only && - !SiteSetting.enable_discourse_connect + SiteSetting.allow_new_registrations && !SiteSetting.invite_only && + !SiteSetting.enable_discourse_connect end def rss_creator(user) @@ -725,12 +740,11 @@ module ApplicationHelper def authentication_data return @authentication_data if defined?(@authentication_data) - @authentication_data = begin - value = cookies[:authentication_data] - if value - cookies.delete(:authentication_data, path: Discourse.base_path("/")) + @authentication_data = + begin + value = cookies[:authentication_data] + cookies.delete(:authentication_data, path: Discourse.base_path("/")) if value + current_user ? nil : value end - current_user ? nil : value - end end end diff --git a/app/helpers/email_helper.rb b/app/helpers/email_helper.rb index ba14776aff..64cb028c53 100644 --- a/app/helpers/email_helper.rb +++ b/app/helpers/email_helper.rb @@ -1,13 +1,15 @@ # frozen_string_literal: true -require 'erb' +require "erb" module EmailHelper - def mailing_list_topic(topic, post_count) render( partial: partial_for("mailing_list_post"), - locals: { topic: topic, post_count: post_count } + locals: { + topic: topic, + post_count: post_count, + }, ) end @@ -26,11 +28,13 @@ module EmailHelper end def email_html_template - EmailStyle.new.html - .sub('%{email_content}') { capture { yield } } - .gsub('%{html_lang}', html_lang) - .gsub('%{dark_mode_meta_tags}', dark_mode_meta_tags) - .gsub('%{dark_mode_styles}', dark_mode_styles) + EmailStyle + .new + .html + .sub("%{email_content}") { capture { yield } } + .gsub("%{html_lang}", html_lang) + .gsub("%{dark_mode_meta_tags}", dark_mode_meta_tags) + .gsub("%{dark_mode_styles}", dark_mode_styles) .html_safe end @@ -118,5 +122,4 @@ module EmailHelper def partial_for(name) SiteSetting.private_email? ? "email/secure_#{name}" : "email/#{name}" end - end diff --git a/app/helpers/embed_helper.rb b/app/helpers/embed_helper.rb index 42e728ddba..5e133167fb 100644 --- a/app/helpers/embed_helper.rb +++ b/app/helpers/embed_helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module EmbedHelper - def embed_post_date(dt) current = Time.now diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb index 9a1ed6b563..ea2c1da006 100644 --- a/app/helpers/posts_helper.rb +++ b/app/helpers/posts_helper.rb @@ -22,9 +22,7 @@ module PostsHelper url = Discourse.redis.get(key) # break cache if either slug or topic_id changes - if url && !url.start_with?(post.topic.url) - url = nil - end + url = nil if url && !url.start_with?(post.topic.url) if !url url = post.canonical_url diff --git a/app/helpers/qunit_helper.rb b/app/helpers/qunit_helper.rb index 3ca1b419bf..2813ed1e9d 100644 --- a/app/helpers/qunit_helper.rb +++ b/app/helpers/qunit_helper.rb @@ -6,10 +6,11 @@ module QunitHelper return "" if theme.blank? _, digest = theme.baked_js_tests_with_digest - src = "#{GlobalSetting.cdn_url}" \ - "#{Discourse.base_path}" \ - "/theme-javascripts/tests/#{theme.id}-#{digest}.js" \ - "?__ws=#{Discourse.current_hostname}" + src = + "#{GlobalSetting.cdn_url}" \ + "#{Discourse.base_path}" \ + "/theme-javascripts/tests/#{theme.id}-#{digest}.js" \ + "?__ws=#{Discourse.current_hostname}" "".html_safe end end diff --git a/app/helpers/splash_screen_helper.rb b/app/helpers/splash_screen_helper.rb index 7dfc419456..af2ec3824d 100644 --- a/app/helpers/splash_screen_helper.rb +++ b/app/helpers/splash_screen_helper.rb @@ -18,7 +18,10 @@ module SplashScreenHelper private def self.load_js - File.read("#{Rails.root}/app/assets/javascripts/discourse/dist/assets/splash-screen.js").sub("//# sourceMappingURL=splash-screen.map\n", "") + File.read("#{Rails.root}/app/assets/javascripts/discourse/dist/assets/splash-screen.js").sub( + "//# sourceMappingURL=splash-screen.map\n", + "", + ) rescue Errno::ENOENT Rails.logger.error("Unable to load splash screen JS") if Rails.env.production? "console.log('Unable to load splash screen JS')" diff --git a/app/helpers/topic_post_bookmarkable_helper.rb b/app/helpers/topic_post_bookmarkable_helper.rb index 43f5699b87..27bd6935ae 100644 --- a/app/helpers/topic_post_bookmarkable_helper.rb +++ b/app/helpers/topic_post_bookmarkable_helper.rb @@ -9,7 +9,7 @@ module TopicPostBookmarkableHelper TopicUser.change( user.id, topic.id, - bookmarked: Bookmark.for_user_in_topic(user.id, topic).exists? + bookmarked: Bookmark.for_user_in_topic(user.id, topic).exists?, ) end end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 0020423621..bd595814ae 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -20,5 +20,4 @@ module TopicsHelper Plugin::Filter.apply(:topic_categories_breadcrumb, topic, breadcrumb) end - end diff --git a/app/helpers/user_notifications_helper.rb b/app/helpers/user_notifications_helper.rb index 6d46583532..e92dbeccd9 100644 --- a/app/helpers/user_notifications_helper.rb +++ b/app/helpers/user_notifications_helper.rb @@ -6,9 +6,7 @@ module UserNotificationsHelper def indent(text, by = 2) spacer = " " * by result = +"" - text.each_line do |line| - result << spacer << line - end + text.each_line { |line| result << spacer << line } result end @@ -32,24 +30,28 @@ module UserNotificationsHelper end def first_paragraphs_from(html) - doc = Nokogiri::HTML5(html) + doc = Nokogiri.HTML5(html) result = +"" length = 0 - doc.css('body > p, aside.onebox, body > ul, body > blockquote').each do |node| - if node.text.present? - result << node.to_s - length += node.inner_text.length - return result if length >= SiteSetting.digest_min_excerpt_length + doc + .css("body > p, aside.onebox, body > ul, body > blockquote") + .each do |node| + if node.text.present? + result << node.to_s + length += node.inner_text.length + return result if length >= SiteSetting.digest_min_excerpt_length + end end - end return result unless result.blank? # If there is no first paragraph with text, return the first paragraph with # something else (an image) or div (a onebox). - doc.css('body > p:not(:empty), body > div:not(:empty), body > p > div.lightbox-wrapper img').first + doc.css( + "body > p:not(:empty), body > div:not(:empty), body > p > div.lightbox-wrapper img", + ).first end def email_excerpt(html_arg, post = nil) @@ -58,7 +60,7 @@ module UserNotificationsHelper end def normalize_name(name) - name.downcase.gsub(/[\s_-]/, '') + name.downcase.gsub(/[\s_-]/, "") end def show_username_on_post(post) @@ -70,9 +72,7 @@ module UserNotificationsHelper end def show_name_on_post(post) - SiteSetting.enable_names? && - SiteSetting.display_name_on_posts? && - post.user.name.present? && + SiteSetting.enable_names? && SiteSetting.display_name_on_posts? && post.user.name.present? && normalize_name(post.user.name) != normalize_name(post.user.username) end @@ -94,7 +94,7 @@ module UserNotificationsHelper end def show_image_with_url(url) - !(url.nil? || url.downcase.end_with?('svg')) + !(url.nil? || url.downcase.end_with?("svg")) end def email_image_url(basename) @@ -106,5 +106,4 @@ module UserNotificationsHelper rescue URI::Error href end - end diff --git a/app/jobs/base.rb b/app/jobs/base.rb index d38e57fcd2..0f671c95a5 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - def self.queued Sidekiq::Stats.new.enqueued end @@ -24,7 +23,7 @@ module Jobs def self.last_job_performed_at Sidekiq.redis do |r| - int = r.get('last_job_perform_at') + int = r.get("last_job_perform_at") int ? Time.at(int.to_i) : nil end end @@ -82,9 +81,9 @@ module Jobs if exception.present? @data["exception"] = exception # Exception - if job fails a json encoded exception - @data["status"] = 'failed' + @data["status"] = "failed" else - @data["status"] = 'success' # Status - fail, success, pending + @data["status"] = "success" # Status - fail, success, pending end write_to_log @@ -92,22 +91,27 @@ module Jobs end def self.raw_log(message) - @@logger ||= begin - f = File.open "#{Rails.root}/log/sidekiq.log", "a" - f.sync = true - Logger.new f - end + @@logger ||= + begin + f = File.open "#{Rails.root}/log/sidekiq.log", "a" + f.sync = true + Logger.new f + end @@log_queue ||= Queue.new if !defined?(@@log_thread) || !@@log_thread.alive? - @@log_thread = Thread.new do - loop do - @@logger << @@log_queue.pop - rescue Exception => e - Discourse.warn_exception(e, message: "Exception encountered while logging Sidekiq job") + @@log_thread = + Thread.new do + loop do + @@logger << @@log_queue.pop + rescue Exception => e + Discourse.warn_exception( + e, + message: "Exception encountered while logging Sidekiq job", + ) + end end - end end @@log_queue.push(message) @@ -136,18 +140,22 @@ module Jobs interval = ENV["DISCOURSE_LOG_SIDEKIQ_INTERVAL"] return if !interval interval = interval.to_i - @@interval_thread ||= Thread.new do - begin - loop do - sleep interval - mutex.synchronize do - @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + @@interval_thread ||= + Thread.new do + begin + loop do + sleep interval + mutex.synchronize do + @@active_jobs.each { |j| j.write_to_log if j.current_duration > interval } + end end + rescue Exception => e + Discourse.warn_exception( + e, + message: "Sidekiq interval logging thread terminated unexpectedly", + ) end - rescue Exception => e - Discourse.warn_exception(e, message: "Sidekiq interval logging thread terminated unexpectedly") end - end end end @@ -189,27 +197,29 @@ module Jobs @db_duration || 0 end + def perform_immediately(*args) + opts = args.extract_options!.with_indifferent_access + + if opts.has_key?(:current_site_id) && + opts[:current_site_id] != RailsMultisite::ConnectionManagement.current_db + raise ArgumentError.new( + "You can't connect to another database when executing a job synchronously.", + ) + else + begin + retval = execute(opts) + rescue => exc + Discourse.handle_job_exception(exc, error_context(opts)) + end + + retval + end + end + def perform(*args) opts = args.extract_options!.with_indifferent_access - if ::Jobs.run_later? - Sidekiq.redis do |r| - r.set('last_job_perform_at', Time.now.to_i) - end - end - - if opts.delete(:sync_exec) - if opts.has_key?(:current_site_id) && opts[:current_site_id] != RailsMultisite::ConnectionManagement.current_db - raise ArgumentError.new("You can't connect to another database when executing a job synchronously.") - else - begin - retval = execute(opts) - rescue => exc - Discourse.handle_job_exception(exc, error_context(opts)) - end - return retval - end - end + Sidekiq.redis { |r| r.set("last_job_perform_at", Time.now.to_i) } if ::Jobs.run_later? dbs = if opts[:current_site_id] @@ -224,9 +234,11 @@ module Jobs exception = {} RailsMultisite::ConnectionManagement.with_connection(db) do - job_instrumenter = JobInstrumenter.new(job_class: self.class, opts: opts, db: db, jid: jid) + job_instrumenter = + JobInstrumenter.new(job_class: self.class, opts: opts, db: db, jid: jid) begin - I18n.locale = SiteSetting.default_locale || SiteSettings::DefaultsProvider::DEFAULT_LOCALE + I18n.locale = + SiteSetting.default_locale || SiteSettings::DefaultsProvider::DEFAULT_LOCALE I18n.ensure_all_loaded! begin logster_env = {} @@ -256,7 +268,10 @@ module Jobs if exceptions.length > 0 exceptions.each do |exception_hash| - Discourse.handle_job_exception(exception_hash[:ex], error_context(opts, exception_hash[:code], exception_hash[:other])) + Discourse.handle_job_exception( + exception_hash[:ex], + error_context(opts, exception_hash[:code], exception_hash[:other]), + ) end raise HandledExceptionWrapper.new(exceptions[0][:ex]) end @@ -265,7 +280,6 @@ module Jobs ensure ActiveRecord::Base.connection_handler.clear_active_connections! end - end class HandledExceptionWrapper < StandardError @@ -280,9 +294,7 @@ module Jobs extend MiniScheduler::Schedule def perform(*args) - if (::Jobs::Heartbeat === self) || !Discourse.readonly_mode? - super - end + super if (::Jobs::Heartbeat === self) || !Discourse.readonly_mode? end end @@ -307,40 +319,26 @@ module Jobs # Simulate the args being dumped/parsed through JSON parsed_opts = JSON.parse(JSON.dump(opts)) - if opts != parsed_opts - Discourse.deprecate(<<~TEXT.squish, since: "2.9", drop_from: "3.0") + Discourse.deprecate(<<~TEXT.squish, since: "2.9", drop_from: "3.0") if opts != parsed_opts #{klass.name} was enqueued with argument values which do not cleanly serialize to/from JSON. This means that the job will be run with slightly different values than the ones supplied to `enqueue`. Argument values should be strings, booleans, numbers, or nil (or arrays/hashes of those value types). TEXT - end opts = parsed_opts if ::Jobs.run_later? - hash = { - 'class' => klass, - 'args' => [opts] - } + hash = { "class" => klass, "args" => [opts] } if delay - if delay.to_f > 0 - hash['at'] = Time.now.to_f + delay.to_f - end + hash["at"] = Time.now.to_f + delay.to_f if delay.to_f > 0 end - if queue - hash['queue'] = queue - end + hash["queue"] = queue if queue DB.after_commit { klass.client_push(hash) } else - # Otherwise execute the job right away - opts["sync_exec"] = true - if Rails.env == "development" - Scheduler::Defer.later("job") do - klass.new.perform(opts) - end + Scheduler::Defer.later("job") { klass.new.perform(opts) } else # Run the job synchronously # But never run a job inside another job @@ -360,7 +358,7 @@ module Jobs begin until queue.empty? queued_klass, queued_opts = queue.pop(true) - queued_klass.new.perform(queued_opts) + queued_klass.new.perform_immediately(queued_opts) end ensure Thread.current[:discourse_nested_job_queue] = nil @@ -368,7 +366,6 @@ module Jobs end end end - end def self.enqueue_in(secs, job_name, opts = {}) diff --git a/app/jobs/concerns/skippable.rb b/app/jobs/concerns/skippable.rb index 30d176e231..dfc6adf223 100644 --- a/app/jobs/concerns/skippable.rb +++ b/app/jobs/concerns/skippable.rb @@ -3,24 +3,22 @@ module Skippable extend ActiveSupport::Concern - def create_skipped_email_log(email_type:, - to_address:, - user_id:, - post_id:, - reason_type:) - + def create_skipped_email_log(email_type:, to_address:, user_id:, post_id:, reason_type:) attributes = { email_type: email_type, to_address: to_address, user_id: user_id, post_id: post_id, - reason_type: reason_type + reason_type: reason_type, } if reason_type == SkippedEmailLog.reason_types[:exceeded_emails_limit] - exists = SkippedEmailLog.exists?({ - created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day) - }.merge!(attributes.except(:post_id))) + exists = + SkippedEmailLog.exists?( + { created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day) }.merge!( + attributes.except(:post_id), + ), + ) return if exists end diff --git a/app/jobs/onceoff/clean_up_post_timings.rb b/app/jobs/onceoff/clean_up_post_timings.rb index 1399ddaa80..0e4088518c 100644 --- a/app/jobs/onceoff/clean_up_post_timings.rb +++ b/app/jobs/onceoff/clean_up_post_timings.rb @@ -2,7 +2,6 @@ module Jobs class CleanUpPostTimings < ::Jobs::Onceoff - # Remove post timings that are remnants of previous post moves # or other shenanigans and don't reference a valid user or post anymore. def execute_onceoff(args) diff --git a/app/jobs/onceoff/clean_up_sidekiq_statistic.rb b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb index 7b51cde58b..74147a0091 100644 --- a/app/jobs/onceoff/clean_up_sidekiq_statistic.rb +++ b/app/jobs/onceoff/clean_up_sidekiq_statistic.rb @@ -3,7 +3,7 @@ module Jobs class CleanUpSidekiqStatistic < ::Jobs::Onceoff def execute_onceoff(args) - Discourse.redis.without_namespace.del('sidekiq:sidekiq:statistic') + Discourse.redis.without_namespace.del("sidekiq:sidekiq:statistic") end end end diff --git a/app/jobs/onceoff/clean_up_user_export_topics.rb b/app/jobs/onceoff/clean_up_user_export_topics.rb index 8e5be5bf27..e6d62c87c5 100644 --- a/app/jobs/onceoff/clean_up_user_export_topics.rb +++ b/app/jobs/onceoff/clean_up_user_export_topics.rb @@ -3,14 +3,18 @@ module Jobs class CleanUpUserExportTopics < ::Jobs::Onceoff def execute_onceoff(args) - translated_keys = I18n.available_locales.map do |l| - I18n.with_locale(:"#{l}") { I18n.t("system_messages.csv_export_succeeded.subject_template") } - end.uniq + translated_keys = + I18n + .available_locales + .map do |l| + I18n.with_locale(:"#{l}") do + I18n.t("system_messages.csv_export_succeeded.subject_template") + end + end + .uniq slugs = [] - translated_keys.each do |k| - slugs << "%-#{Slug.for(k.gsub('[%{export_title}]', ''))}" - end + translated_keys.each { |k| slugs << "%-#{Slug.for(k.gsub("[%{export_title}]", ""))}" } # "[%{export_title}] 資料匯出已完成" gets converted to "%-topic", do not match that slug. slugs = slugs.reject { |s| s == "%-topic" } diff --git a/app/jobs/onceoff/correct_missing_dualstack_urls.rb b/app/jobs/onceoff/correct_missing_dualstack_urls.rb index 6cdfa55e84..d96b8ac022 100644 --- a/app/jobs/onceoff/correct_missing_dualstack_urls.rb +++ b/app/jobs/onceoff/correct_missing_dualstack_urls.rb @@ -9,7 +9,7 @@ module Jobs return if !base_url.match?(/s3\.dualstack/) - old = base_url.sub('s3.dualstack.', 's3-') + old = base_url.sub("s3.dualstack.", "s3-") old_like = %"#{old}%" DB.exec(<<~SQL, from: old, to: base_url, old_like: old_like) diff --git a/app/jobs/onceoff/create_tags_search_index.rb b/app/jobs/onceoff/create_tags_search_index.rb index f761707426..a0bea42331 100644 --- a/app/jobs/onceoff/create_tags_search_index.rb +++ b/app/jobs/onceoff/create_tags_search_index.rb @@ -3,9 +3,9 @@ module Jobs class CreateTagsSearchIndex < ::Jobs::Onceoff def execute_onceoff(args) - DB.query('select id, name from tags').each do |t| - SearchIndexer.update_tags_index(t.id, t.name) - end + DB + .query("select id, name from tags") + .each { |t| SearchIndexer.update_tags_index(t.id, t.name) } end end end diff --git a/app/jobs/onceoff/fix_encoded_category_slugs.rb b/app/jobs/onceoff/fix_encoded_category_slugs.rb index 28947125d9..2af1b6f759 100644 --- a/app/jobs/onceoff/fix_encoded_category_slugs.rb +++ b/app/jobs/onceoff/fix_encoded_category_slugs.rb @@ -1,17 +1,18 @@ # frozen_string_literal: true module Jobs - class FixEncodedCategorySlugs < ::Jobs::Onceoff def execute_onceoff(args) - return unless SiteSetting.slug_generation_method == 'encoded' + return unless SiteSetting.slug_generation_method == "encoded" #Make custom categories slugs nil and let the app regenerate with proper encoded ones - Category.all.reject { |c| c.seeded? }.each do |c| - c.slug = nil - c.save! - end + Category + .all + .reject { |c| c.seeded? } + .each do |c| + c.slug = nil + c.save! + end end end - end diff --git a/app/jobs/onceoff/fix_encoded_topic_slugs.rb b/app/jobs/onceoff/fix_encoded_topic_slugs.rb index 4640493fb9..f7463a58d6 100644 --- a/app/jobs/onceoff/fix_encoded_topic_slugs.rb +++ b/app/jobs/onceoff/fix_encoded_topic_slugs.rb @@ -1,14 +1,12 @@ # frozen_string_literal: true module Jobs - class FixEncodedTopicSlugs < ::Jobs::Onceoff def execute_onceoff(args) - return unless SiteSetting.slug_generation_method == 'encoded' + return unless SiteSetting.slug_generation_method == "encoded" #Make all slugs nil and let the app regenerate with proper encoded ones Topic.update_all(slug: nil) end end - end diff --git a/app/jobs/onceoff/fix_featured_link_for_topics.rb b/app/jobs/onceoff/fix_featured_link_for_topics.rb index 337d7d2efa..0b4f67ab9f 100644 --- a/app/jobs/onceoff/fix_featured_link_for_topics.rb +++ b/app/jobs/onceoff/fix_featured_link_for_topics.rb @@ -3,15 +3,17 @@ module Jobs class FixFeaturedLinkForTopics < ::Jobs::Onceoff def execute_onceoff(args) - Topic.where("featured_link IS NOT NULL").find_each do |topic| - featured_link = topic.featured_link + Topic + .where("featured_link IS NOT NULL") + .find_each do |topic| + featured_link = topic.featured_link - begin - URI.parse(featured_link) - rescue URI::Error - topic.update(featured_link: URI.extract(featured_link).first) + begin + URI.parse(featured_link) + rescue URI::Error + topic.update(featured_link: URI.extract(featured_link).first) + end end - end end end end diff --git a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb index 515b805871..2171c2d03f 100644 --- a/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb +++ b/app/jobs/onceoff/fix_invalid_gravatar_uploads.rb @@ -3,16 +3,23 @@ module Jobs class FixInvalidGravatarUploads < ::Jobs::Onceoff def execute_onceoff(args) - Upload.where(original_filename: "gravatar.png").find_each do |upload| - # note, this still feels pretty expensive for a once off - # we may need to re-evaluate this - extension = FastImage.type(Discourse.store.path_for(upload)) rescue nil - current_extension = upload.extension + Upload + .where(original_filename: "gravatar.png") + .find_each do |upload| + # note, this still feels pretty expensive for a once off + # we may need to re-evaluate this + extension = + begin + FastImage.type(Discourse.store.path_for(upload)) + rescue StandardError + nil + end + current_extension = upload.extension - if extension.to_s.downcase != current_extension.to_s.downcase - upload&.user&.user_avatar&.update_columns(last_gravatar_download_attempt: nil) + if extension.to_s.downcase != current_extension.to_s.downcase + upload&.user&.user_avatar&.update_columns(last_gravatar_download_attempt: nil) + end end - end end end end diff --git a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb index 5fb9ff218e..72f171d194 100644 --- a/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb +++ b/app/jobs/onceoff/fix_primary_emails_for_staged_users.rb @@ -7,22 +7,22 @@ module Jobs acting_user = Discourse.system_user destroyer = UserDestroyer.new(acting_user) - users.group("email_tokens.email") + users + .group("email_tokens.email") .having("COUNT(email_tokens.email) > 1") .count .each_key do |email| + query = users.where("email_tokens.email = ?", email).order(id: :asc) - query = users.where("email_tokens.email = ?", email).order(id: :asc) + original_user = query.first - original_user = query.first - - query.offset(1).each do |user| - user.posts.each do |post| - post.set_owner(original_user, acting_user) - end - destroyer.destroy(user, context: I18n.t("user.destroy_reasons.fixed_primary_email")) + query + .offset(1) + .each do |user| + user.posts.each { |post| post.set_owner(original_user, acting_user) } + destroyer.destroy(user, context: I18n.t("user.destroy_reasons.fixed_primary_email")) + end end - end DB.exec <<~SQL INSERT INTO user_emails ( diff --git a/app/jobs/onceoff/fix_retro_anniversary.rb b/app/jobs/onceoff/fix_retro_anniversary.rb index a6ab1c566f..92eaeb8011 100644 --- a/app/jobs/onceoff/fix_retro_anniversary.rb +++ b/app/jobs/onceoff/fix_retro_anniversary.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class FixRetroAnniversary < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges @@ -16,19 +15,19 @@ module Jobs users.each do |u| first = u.first_granted_at - badges = UserBadge.where( - "badge_id = ? AND user_id = ? AND granted_at > ?", - Badge::Anniversary, - u.user_id, - first - ).order('granted_at') + badges = + UserBadge.where( + "badge_id = ? AND user_id = ? AND granted_at > ?", + Badge::Anniversary, + u.user_id, + first, + ).order("granted_at") badges.each_with_index do |b, idx| award_date = (first + (idx + 1).years) UserBadge.where(id: b.id).update_all(["granted_at = ?", award_date]) end end - end end end diff --git a/app/jobs/onceoff/fix_s3_etags.rb b/app/jobs/onceoff/fix_s3_etags.rb index 226b09e937..156b3b2ebb 100644 --- a/app/jobs/onceoff/fix_s3_etags.rb +++ b/app/jobs/onceoff/fix_s3_etags.rb @@ -2,10 +2,10 @@ module Jobs class FixS3Etags < ::Jobs::Onceoff - def execute_onceoff(args) [Upload, OptimizedImage].each do |model| - sql = "UPDATE #{model.table_name} SET etag = REGEXP_REPLACE(etag, '\"', '', 'g') WHERE etag LIKE '\"%\"'" + sql = + "UPDATE #{model.table_name} SET etag = REGEXP_REPLACE(etag, '\"', '', 'g') WHERE etag LIKE '\"%\"'" DB.exec(sql) end end diff --git a/app/jobs/onceoff/grant_emoji.rb b/app/jobs/onceoff/grant_emoji.rb index 5abdb34d5d..40efada65c 100644 --- a/app/jobs/onceoff/grant_emoji.rb +++ b/app/jobs/onceoff/grant_emoji.rb @@ -1,25 +1,25 @@ # frozen_string_literal: true module Jobs - class GrantEmoji < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges to_award = {} - Post.secured(Guardian.new) + Post + .secured(Guardian.new) .select(:id, :created_at, :cooked, :user_id) .visible .public_posts .where("cooked LIKE '%emoji%'") .find_in_batches do |group| - group.each do |p| - doc = Nokogiri::HTML5::fragment(p.cooked) - if (doc.css("img.emoji") - doc.css(".quote img")).size > 0 - to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } + group.each do |p| + doc = Nokogiri::HTML5.fragment(p.cooked) + if (doc.css("img.emoji") - doc.css(".quote img")).size > 0 + to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } + end end end - end to_award.each do |user_id, opts| user = User.where(id: user_id).first @@ -30,7 +30,5 @@ module Jobs def badge Badge.find(Badge::FirstEmoji) end - end - end diff --git a/app/jobs/onceoff/grant_first_reply_by_email.rb b/app/jobs/onceoff/grant_first_reply_by_email.rb index c0287205d7..0f8129077c 100644 --- a/app/jobs/onceoff/grant_first_reply_by_email.rb +++ b/app/jobs/onceoff/grant_first_reply_by_email.rb @@ -1,24 +1,21 @@ # frozen_string_literal: true module Jobs - class GrantFirstReplyByEmail < ::Jobs::Onceoff - def execute_onceoff(args) return unless SiteSetting.enable_badges to_award = {} - Post.select(:id, :created_at, :user_id) + Post + .select(:id, :created_at, :user_id) .secured(Guardian.new) .visible .public_posts .where(via_email: true) .where("post_number > 1") .find_in_batches do |group| - group.each do |p| - to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } + group.each { |p| to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } } end - end to_award.each do |user_id, opts| user = User.where(id: user_id).first @@ -29,7 +26,5 @@ module Jobs def badge Badge.find(Badge::FirstReplyByEmail) end - end - end diff --git a/app/jobs/onceoff/grant_onebox.rb b/app/jobs/onceoff/grant_onebox.rb index 66d2cf2670..5eb6ec0538 100644 --- a/app/jobs/onceoff/grant_onebox.rb +++ b/app/jobs/onceoff/grant_onebox.rb @@ -1,35 +1,34 @@ # frozen_string_literal: true module Jobs - class GrantOnebox < ::Jobs::Onceoff - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute_onceoff(args) return unless SiteSetting.enable_badges to_award = {} - Post.secured(Guardian.new) + Post + .secured(Guardian.new) .select(:id, :created_at, :raw, :user_id) .visible .public_posts .where("raw LIKE '%http%'") .find_in_batches do |group| - group.each do |p| - begin - # Note we can't use `p.cooked` here because oneboxes have been cooked out - cooked = PrettyText.cook(p.raw) - doc = Nokogiri::HTML5::fragment(cooked) - if doc.search('a.onebox').size > 0 - to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } + group.each do |p| + begin + # Note we can't use `p.cooked` here because oneboxes have been cooked out + cooked = PrettyText.cook(p.raw) + doc = Nokogiri::HTML5.fragment(cooked) + if doc.search("a.onebox").size > 0 + to_award[p.user_id] ||= { post_id: p.id, created_at: p.created_at } + end + rescue StandardError + nil # if there is a problem cooking we don't care end - rescue - nil # if there is a problem cooking we don't care end end - end - to_award.each do |user_id, opts| user = User.where(id: user_id).first BadgeGranter.grant(badge, user, opts) if user @@ -39,7 +38,5 @@ module Jobs def badge Badge.find(Badge::FirstOnebox) end - end - end diff --git a/app/jobs/onceoff/migrate_badge_image_to_uploads.rb b/app/jobs/onceoff/migrate_badge_image_to_uploads.rb index 5585a508c2..b45595498f 100644 --- a/app/jobs/onceoff/migrate_badge_image_to_uploads.rb +++ b/app/jobs/onceoff/migrate_badge_image_to_uploads.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'uri' +require "uri" module Jobs class MigrateBadgeImageToUploads < ::Jobs::Onceoff @@ -14,72 +14,79 @@ module Jobs SQL return unless column_exists - Badge.where.not(image: nil).select(:id, :image_upload_id, :image).each do |badge| - if badge.image_upload.present? - DB.exec("UPDATE badges SET image = NULL WHERE id = ?", badge.id) - next - end - - image_url = badge[:image] - next if image_url.blank? || image_url !~ URI.regexp - - count = 0 - file = nil - sleep_interval = 5 - - loop do - url = UrlHelper.absolute_without_cdn(image_url) - - begin - file = FileHelper.download( - url, - max_file_size: [ - SiteSetting.max_image_size_kb.kilobytes, - 20.megabytes - ].max, - tmp_file_name: 'tmp_badge_image_upload', - skip_rate_limit: true, - follow_redirect: true - ) - rescue OpenURI::HTTPError, - OpenSSL::SSL::SSLError, - Net::OpenTimeout, - Net::ReadTimeout, - Errno::ECONNREFUSED, - EOFError, - SocketError, - Discourse::InvalidParameters => e - - logger.error( - "Error encountered when trying to download from URL '#{image_url}' " + - "for badge '#{badge[:id]}'.\n#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}" - ) + Badge + .where.not(image: nil) + .select(:id, :image_upload_id, :image) + .each do |badge| + if badge.image_upload.present? + DB.exec("UPDATE badges SET image = NULL WHERE id = ?", badge.id) + next end - count += 1 - break if file + image_url = badge[:image] + next if image_url.blank? || image_url !~ URI.regexp - logger.warn( - "Failed to download image from #{url} for badge '#{badge[:id]}'. Retrying (#{count}/3)..." - ) - break if count >= 3 - sleep(count * sleep_interval) + count = 0 + file = nil + sleep_interval = 5 + + loop do + url = UrlHelper.absolute_without_cdn(image_url) + + begin + file = + FileHelper.download( + url, + max_file_size: [SiteSetting.max_image_size_kb.kilobytes, 20.megabytes].max, + tmp_file_name: "tmp_badge_image_upload", + skip_rate_limit: true, + follow_redirect: true, + ) + rescue OpenURI::HTTPError, + OpenSSL::SSL::SSLError, + Net::OpenTimeout, + Net::ReadTimeout, + Errno::ECONNREFUSED, + EOFError, + SocketError, + Discourse::InvalidParameters => e + logger.error( + "Error encountered when trying to download from URL '#{image_url}' " + + "for badge '#{badge[:id]}'.\n#{e.class}: #{e.message}\n#{e.backtrace.join("\n")}", + ) + end + + count += 1 + break if file + + logger.warn( + "Failed to download image from #{url} for badge '#{badge[:id]}'. Retrying (#{count}/3)...", + ) + break if count >= 3 + sleep(count * sleep_interval) + end + + next if file.blank? + + upload = + UploadCreator.new( + file, + "image_for_badge_#{badge[:id]}", + origin: UrlHelper.absolute(image_url), + ).create_for(Discourse.system_user.id) + + if upload.errors.count > 0 || upload&.id.blank? + logger.error( + "Failed to create an upload for the image of badge '#{badge[:id]}'. Error: #{upload.errors.full_messages}", + ) + else + DB.exec( + "UPDATE badges SET image = NULL, image_upload_id = ? WHERE id = ?", + upload.id, + badge[:id], + ) + end end - - next if file.blank? - - upload = UploadCreator.new( - file, - "image_for_badge_#{badge[:id]}", - origin: UrlHelper.absolute(image_url) - ).create_for(Discourse.system_user.id) - - if upload.errors.count > 0 || upload&.id.blank? - logger.error("Failed to create an upload for the image of badge '#{badge[:id]}'. Error: #{upload.errors.full_messages}") - else - DB.exec("UPDATE badges SET image = NULL, image_upload_id = ? WHERE id = ?", upload.id, badge[:id]) - end - end end private diff --git a/app/jobs/onceoff/migrate_censored_words.rb b/app/jobs/onceoff/migrate_censored_words.rb index 5de876107b..605321d94c 100644 --- a/app/jobs/onceoff/migrate_censored_words.rb +++ b/app/jobs/onceoff/migrate_censored_words.rb @@ -5,9 +5,10 @@ module Jobs def execute_onceoff(args) row = DB.query_single("SELECT value FROM site_settings WHERE name = 'censored_words'") if row.count > 0 - row.first.split('|').each do |word| - WatchedWord.create(word: word, action: WatchedWord.actions[:censor]) - end + row + .first + .split("|") + .each { |word| WatchedWord.create(word: word, action: WatchedWord.actions[:censor]) } end end end diff --git a/app/jobs/onceoff/migrate_custom_emojis.rb b/app/jobs/onceoff/migrate_custom_emojis.rb index f359a23c99..62fdc6da45 100644 --- a/app/jobs/onceoff/migrate_custom_emojis.rb +++ b/app/jobs/onceoff/migrate_custom_emojis.rb @@ -9,11 +9,10 @@ module Jobs name = File.basename(path, File.extname(path)) File.open(path) do |file| - upload = UploadCreator.new( - file, - File.basename(path), - type: 'custom_emoji' - ).create_for(Discourse.system_user.id) + upload = + UploadCreator.new(file, File.basename(path), type: "custom_emoji").create_for( + Discourse.system_user.id, + ) if upload.persisted? custom_emoji = CustomEmoji.new(name: name, upload: upload) @@ -22,16 +21,16 @@ module Jobs warn("Failed to create custom emoji '#{name}': #{custom_emoji.errors.full_messages}") end else - warn("Failed to create upload for '#{name}' custom emoji: #{upload.errors.full_messages}") + warn( + "Failed to create upload for '#{name}' custom emoji: #{upload.errors.full_messages}", + ) end end end Emoji.clear_cache - Post.where("cooked LIKE ?", "%#{Emoji.base_url}%").find_each do |post| - post.rebake! - end + Post.where("cooked LIKE ?", "%#{Emoji.base_url}%").find_each { |post| post.rebake! } end def warn(message) diff --git a/app/jobs/onceoff/migrate_featured_links.rb b/app/jobs/onceoff/migrate_featured_links.rb index 45f4c11def..9139854fcc 100644 --- a/app/jobs/onceoff/migrate_featured_links.rb +++ b/app/jobs/onceoff/migrate_featured_links.rb @@ -1,30 +1,35 @@ # frozen_string_literal: true module Jobs - class MigrateFeaturedLinks < ::Jobs::Onceoff - def execute_onceoff(args) - TopicCustomField.where(name: "featured_link").find_each do |tcf| - if tcf.value.present? - Topic.where(id: tcf.topic_id).update_all(featured_link: tcf.value) + TopicCustomField + .where(name: "featured_link") + .find_each do |tcf| + Topic.where(id: tcf.topic_id).update_all(featured_link: tcf.value) if tcf.value.present? end - end # Plugin behaviour: only categories explicitly allowed to have featured links can have them. # All others implicitly DO NOT allow them. # If no categories were explicitly allowed to have them, then all implicitly DID allow them. - allowed = CategoryCustomField.where(name: "topic_featured_link_allowed").where(value: "true").pluck(:category_id) + allowed = + CategoryCustomField + .where(name: "topic_featured_link_allowed") + .where(value: "true") + .pluck(:category_id) if !allowed.empty? # all others are not allowed Category.where.not(id: allowed).update_all(topic_featured_link_allowed: false) else - not_allowed = CategoryCustomField.where(name: "topic_featured_link_allowed").where.not(value: "true").pluck(:category_id) + not_allowed = + CategoryCustomField + .where(name: "topic_featured_link_allowed") + .where.not(value: "true") + .pluck(:category_id) Category.where(id: not_allowed).update_all(topic_featured_link_allowed: false) end end end - end diff --git a/app/jobs/onceoff/migrate_tagging_plugin.rb b/app/jobs/onceoff/migrate_tagging_plugin.rb index d92d5692f4..8420b7543f 100644 --- a/app/jobs/onceoff/migrate_tagging_plugin.rb +++ b/app/jobs/onceoff/migrate_tagging_plugin.rb @@ -1,18 +1,25 @@ # frozen_string_literal: true module Jobs - class MigrateTaggingPlugin < ::Jobs::Onceoff - def execute_onceoff(args) - all_tags = TopicCustomField.where(name: "tags").select('DISTINCT value').all.map(&:value) - tag_id_lookup = Tag.create(all_tags.map { |tag_name| { name: tag_name } }).inject({}) { |h, v| h[v.name] = v.id; h } + all_tags = TopicCustomField.where(name: "tags").select("DISTINCT value").all.map(&:value) + tag_id_lookup = + Tag + .create(all_tags.map { |tag_name| { name: tag_name } }) + .inject({}) do |h, v| + h[v.name] = v.id + h + end - TopicCustomField.where(name: "tags").find_each do |tcf| - TopicTag.create(topic_id: tcf.topic_id, tag_id: tag_id_lookup[tcf.value] || Tag.find_by_name(tcf.value).try(:id)) - end + TopicCustomField + .where(name: "tags") + .find_each do |tcf| + TopicTag.create( + topic_id: tcf.topic_id, + tag_id: tag_id_lookup[tcf.value] || Tag.find_by_name(tcf.value).try(:id), + ) + end end - end - end diff --git a/app/jobs/onceoff/migrate_upload_extensions.rb b/app/jobs/onceoff/migrate_upload_extensions.rb index 846944e285..fd6eb72a43 100644 --- a/app/jobs/onceoff/migrate_upload_extensions.rb +++ b/app/jobs/onceoff/migrate_upload_extensions.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class MigrateUploadExtensions < ::Jobs::Onceoff def execute_onceoff(args) Upload.find_each do |upload| diff --git a/app/jobs/onceoff/onceoff.rb b/app/jobs/onceoff/onceoff.rb index d10b1f41a6..df9c94e54d 100644 --- a/app/jobs/onceoff/onceoff.rb +++ b/app/jobs/onceoff/onceoff.rb @@ -4,7 +4,7 @@ class Jobs::Onceoff < ::Jobs::Base sidekiq_options retry: false def self.name_for(klass) - klass.name.sub(/^Jobs\:\:/, '') + klass.name.sub(/^Jobs\:\:/, "") end def running_key_name @@ -26,18 +26,17 @@ class Jobs::Onceoff < ::Jobs::Base Discourse.redis.del(running_key_name) if has_lock end end - end def self.enqueue_all previously_ran = OnceoffLog.pluck(:job_name).uniq - ObjectSpace.each_object(Class).select { |klass| klass < self }.each do |klass| - job_name = name_for(klass) - unless previously_ran.include?(job_name) - Jobs.enqueue(job_name.underscore.to_sym) + ObjectSpace + .each_object(Class) + .select { |klass| klass < self } + .each do |klass| + job_name = name_for(klass) + Jobs.enqueue(job_name.underscore.to_sym) unless previously_ran.include?(job_name) end - end end - end diff --git a/app/jobs/onceoff/post_uploads_recovery.rb b/app/jobs/onceoff/post_uploads_recovery.rb index 5f942209eb..75d0b42c7b 100644 --- a/app/jobs/onceoff/post_uploads_recovery.rb +++ b/app/jobs/onceoff/post_uploads_recovery.rb @@ -6,17 +6,11 @@ module Jobs MAX_PERIOD = 120 def execute_onceoff(args) - UploadRecovery.new.recover(Post.where( - "baked_at >= ?", - grace_period.days.ago - )) + UploadRecovery.new.recover(Post.where("baked_at >= ?", grace_period.days.ago)) end def grace_period - SiteSetting.purge_deleted_uploads_grace_period_days.clamp( - MIN_PERIOD, - MAX_PERIOD - ) + SiteSetting.purge_deleted_uploads_grace_period_days.clamp(MIN_PERIOD, MAX_PERIOD) end end end diff --git a/app/jobs/onceoff/retro_grant_anniversary.rb b/app/jobs/onceoff/retro_grant_anniversary.rb index b0ca206bff..cba25214b3 100644 --- a/app/jobs/onceoff/retro_grant_anniversary.rb +++ b/app/jobs/onceoff/retro_grant_anniversary.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true module Jobs - class RetroGrantAnniversary < ::Jobs::Onceoff def execute_onceoff(args) return unless SiteSetting.enable_badges # Fill in the years of anniversary badges we missed - (2..3).each do |year| - ::Jobs::GrantAnniversaryBadges.new.execute(start_date: year.years.ago) - end + (2..3).each { |year| ::Jobs::GrantAnniversaryBadges.new.execute(start_date: year.years.ago) } end end - end diff --git a/app/jobs/regular/admin_confirmation_email.rb b/app/jobs/regular/admin_confirmation_email.rb index 48746de408..89ab3a4f2f 100644 --- a/app/jobs/regular/admin_confirmation_email.rb +++ b/app/jobs/regular/admin_confirmation_email.rb @@ -2,7 +2,7 @@ module Jobs class AdminConfirmationEmail < ::Jobs::Base - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" def execute(args) to_address = args[:to_address] @@ -18,6 +18,5 @@ module Jobs message = AdminConfirmationMailer.send_email(to_address, target_email, target_username, token) Email::Sender.new(message, :admin_confirmation_message).send end - end end diff --git a/app/jobs/regular/anonymize_user.rb b/app/jobs/regular/anonymize_user.rb index 7343163e49..7ae14c1173 100644 --- a/app/jobs/regular/anonymize_user.rb +++ b/app/jobs/regular/anonymize_user.rb @@ -2,8 +2,7 @@ module Jobs class AnonymizeUser < ::Jobs::Base - - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) @user_id = args[:user_id] @@ -22,7 +21,8 @@ module Jobs EmailLog.where(user_id: @user_id).delete_all IncomingEmail.where("user_id = ? OR from_address = ?", @user_id, @prev_email).delete_all - Post.with_deleted + Post + .with_deleted .where(user_id: @user_id) .where.not(raw_email: nil) .update_all(raw_email: nil) @@ -30,17 +30,17 @@ module Jobs anonymize_user_fields end - def ip_where(column = 'user_id') + def ip_where(column = "user_id") ["#{column} = :user_id AND ip_address IS NOT NULL", user_id: @user_id] end def anonymize_ips(new_ip) - IncomingLink.where(ip_where('current_user_id')).update_all(ip_address: new_ip) + IncomingLink.where(ip_where("current_user_id")).update_all(ip_address: new_ip) ScreenedEmail.where(email: @prev_email).update_all(ip_address: new_ip) SearchLog.where(ip_where).update_all(ip_address: new_ip) TopicLinkClick.where(ip_where).update_all(ip_address: new_ip) TopicViewItem.where(ip_where).update_all(ip_address: new_ip) - UserHistory.where(ip_where('acting_user_id')).update_all(ip_address: new_ip) + UserHistory.where(ip_where("acting_user_id")).update_all(ip_address: new_ip) UserProfileView.where(ip_where).update_all(ip_address: new_ip) # UserHistory for delete_user logs the user's IP. Note this is quite ugly but we don't diff --git a/app/jobs/regular/automatic_group_membership.rb b/app/jobs/regular/automatic_group_membership.rb index 6831078842..d2d043a39b 100644 --- a/app/jobs/regular/automatic_group_membership.rb +++ b/app/jobs/regular/automatic_group_membership.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class AutomaticGroupMembership < ::Jobs::Base - def execute(args) group_id = args[:group_id] raise Discourse::InvalidParameters.new(:group_id) if group_id.blank? @@ -14,13 +12,13 @@ module Jobs domains = group.automatic_membership_email_domains return if domains.blank? - Group.automatic_membership_users(domains).find_each do |user| - next unless user.email_confirmed? - group.add(user, automatic: true) - GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user) - end + Group + .automatic_membership_users(domains) + .find_each do |user| + next unless user.email_confirmed? + group.add(user, automatic: true) + GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(user) + end end - end - end diff --git a/app/jobs/regular/backfill_sidebar_site_settings.rb b/app/jobs/regular/backfill_sidebar_site_settings.rb index 8e07427e83..9313a9afca 100644 --- a/app/jobs/regular/backfill_sidebar_site_settings.rb +++ b/app/jobs/regular/backfill_sidebar_site_settings.rb @@ -2,8 +2,10 @@ class Jobs::BackfillSidebarSiteSettings < Jobs::Base def execute(args) - SidebarSiteSettingsBackfiller - .new(args[:setting_name], previous_value: args[:previous_value], new_value: args[:new_value]) - .backfill! + SidebarSiteSettingsBackfiller.new( + args[:setting_name], + previous_value: args[:previous_value], + new_value: args[:new_value], + ).backfill! end end diff --git a/app/jobs/regular/backup_chunks_merger.rb b/app/jobs/regular/backup_chunks_merger.rb index aa359711ac..4f6bb3e2b6 100644 --- a/app/jobs/regular/backup_chunks_merger.rb +++ b/app/jobs/regular/backup_chunks_merger.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true module Jobs - class BackupChunksMerger < ::Jobs::Base - sidekiq_options queue: 'critical', retry: false + sidekiq_options queue: "critical", retry: false def execute(args) - filename = args[:filename] + filename = args[:filename] identifier = args[:identifier] - chunks = args[:chunks].to_i + chunks = args[:chunks].to_i - raise Discourse::InvalidParameters.new(:filename) if filename.blank? + raise Discourse::InvalidParameters.new(:filename) if filename.blank? raise Discourse::InvalidParameters.new(:identifier) if identifier.blank? - raise Discourse::InvalidParameters.new(:chunks) if chunks <= 0 + raise Discourse::InvalidParameters.new(:chunks) if chunks <= 0 backup_path = "#{BackupRestore::LocalBackupStore.base_directory}/#{filename}" tmp_backup_path = "#{backup_path}.tmp" # path to tmp directory - tmp_directory = File.dirname(BackupRestore::LocalBackupStore.chunk_path(identifier, filename, 0)) + tmp_directory = + File.dirname(BackupRestore::LocalBackupStore.chunk_path(identifier, filename, 0)) # merge all chunks HandleChunkUpload.merge_chunks( @@ -26,15 +26,14 @@ module Jobs tmp_upload_path: tmp_backup_path, identifier: identifier, filename: filename, - tmp_directory: tmp_directory + tmp_directory: tmp_directory, ) # push an updated list to the clients store = BackupRestore::BackupStore.create - data = ActiveModel::ArraySerializer.new(store.files, each_serializer: BackupFileSerializer).as_json + data = + ActiveModel::ArraySerializer.new(store.files, each_serializer: BackupFileSerializer).as_json MessageBus.publish("/admin/backups", data, group_ids: [Group::AUTO_GROUPS[:staff]]) end - end - end diff --git a/app/jobs/regular/bulk_grant_trust_level.rb b/app/jobs/regular/bulk_grant_trust_level.rb index f61b747d20..5a5ccecf7e 100644 --- a/app/jobs/regular/bulk_grant_trust_level.rb +++ b/app/jobs/regular/bulk_grant_trust_level.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class BulkGrantTrustLevel < ::Jobs::Base - def execute(args) trust_level = args[:trust_level] user_ids = args[:user_ids] @@ -11,9 +9,7 @@ module Jobs raise Discourse::InvalidParameters.new(:trust_level) if trust_level.blank? raise Discourse::InvalidParameters.new(:user_ids) if user_ids.blank? - User.where(id: user_ids).find_each do |user| - TrustLevelGranter.grant(trust_level, user) - end + User.where(id: user_ids).find_each { |user| TrustLevelGranter.grant(trust_level, user) } end end end diff --git a/app/jobs/regular/bulk_invite.rb b/app/jobs/regular/bulk_invite.rb index ed83eb381d..173a27d4fd 100644 --- a/app/jobs/regular/bulk_invite.rb +++ b/app/jobs/regular/bulk_invite.rb @@ -7,13 +7,13 @@ module Jobs def initialize super - @logs = [] - @sent = 0 - @skipped = 0 - @warnings = 0 - @failed = 0 - @groups = {} - @user_fields = {} + @logs = [] + @sent = 0 + @skipped = 0 + @warnings = 0 + @failed = 0 + @groups = {} + @user_fields = {} @valid_groups = {} end @@ -64,7 +64,7 @@ module Jobs groups = [] if group_names - group_names = group_names.split(';') + group_names = group_names.split(";") group_names.each do |group_name| group = fetch_group(group_name) @@ -101,7 +101,10 @@ module Jobs user_fields = {} fields.each do |key, value| - @user_fields[key] ||= UserField.includes(:user_field_options).where('name ILIKE ?', key).first || :nil + @user_fields[key] ||= UserField + .includes(:user_field_options) + .where("name ILIKE ?", key) + .first || :nil if @user_fields[key] == :nil save_log "Invalid User Field '#{key}'" @warnings += 1 @@ -110,7 +113,8 @@ module Jobs # Automatically correct user field value if @user_fields[key].field_type == "dropdown" - value = @user_fields[key].user_field_options.find { |ufo| ufo.value.casecmp?(value) }&.value + value = + @user_fields[key].user_field_options.find { |ufo| ufo.value.casecmp?(value) }&.value end user_fields[@user_fields[key].id] = value @@ -133,17 +137,13 @@ module Jobs groups.each do |group| group.add(user) - GroupActionLogger - .new(@current_user, group) - .log_add_user_to_group(user) + GroupActionLogger.new(@current_user, group).log_add_user_to_group(user) end end end if user_fields.present? - user_fields.each do |user_field, value| - user.set_user_field(user_field, value) - end + user_fields.each { |user_field, value| user.set_user_field(user_field, value) } user.save_custom_fields end @@ -156,26 +156,19 @@ module Jobs else if user_fields.present? || locale.present? user = User.where(staged: true).find_by_email(email) - user ||= User.new(username: UserNameSuggester.suggest(email), email: email, staged: true) + user ||= + User.new(username: UserNameSuggester.suggest(email), email: email, staged: true) if user_fields.present? - user_fields.each do |user_field, value| - user.set_user_field(user_field, value) - end + user_fields.each { |user_field, value| user.set_user_field(user_field, value) } end - if locale.present? - user.locale = locale - end + user.locale = locale if locale.present? user.save! end - invite_opts = { - email: email, - topic: topic, - group_ids: groups.map(&:id), - } + invite_opts = { email: email, topic: topic, group_ids: groups.map(&:id) } if @invites.length > Invite::BULK_INVITE_EMAIL_LIMIT invite_opts[:emailed_status] = Invite.emailed_status_types[:bulk_pending] @@ -203,7 +196,7 @@ module Jobs sent: @sent, skipped: @skipped, warnings: @warnings, - logs: @logs.join("\n") + logs: @logs.join("\n"), ) else SystemMessage.create_from_system_user( @@ -213,7 +206,7 @@ module Jobs skipped: @skipped, warnings: @warnings, failed: @failed, - logs: @logs.join("\n") + logs: @logs.join("\n"), ) end end @@ -224,7 +217,7 @@ module Jobs group = @groups[group_name] unless group - group = Group.find_by('lower(name) = ?', group_name) + group = Group.find_by("lower(name) = ?", group_name) @groups[group_name] = group end diff --git a/app/jobs/regular/bulk_user_title_update.rb b/app/jobs/regular/bulk_user_title_update.rb index 6dae5219cc..f755f620a7 100644 --- a/app/jobs/regular/bulk_user_title_update.rb +++ b/app/jobs/regular/bulk_user_title_update.rb @@ -2,14 +2,19 @@ module Jobs class BulkUserTitleUpdate < ::Jobs::Base - UPDATE_ACTION = 'update' - RESET_ACTION = 'reset' + UPDATE_ACTION = "update" + RESET_ACTION = "reset" def execute(args) new_title = args[:new_title] granted_badge_id = args[:granted_badge_id] action = args[:action] - badge = Badge.find(granted_badge_id) rescue nil + badge = + begin + Badge.find(granted_badge_id) + rescue StandardError + nil + end return unless badge # Deleted badge protection @@ -20,6 +25,5 @@ module Jobs badge.reset_user_titles! end end - end end diff --git a/app/jobs/regular/close_topic.rb b/app/jobs/regular/close_topic.rb index 3385d63c34..c0b9035d03 100644 --- a/app/jobs/regular/close_topic.rb +++ b/app/jobs/regular/close_topic.rb @@ -23,7 +23,7 @@ module Jobs end # this handles deleting the topic timer as well, see TopicStatusUpdater - topic.update_status('autoclosed', true, user, { silent: silent }) + topic.update_status("autoclosed", true, user, { silent: silent }) MessageBus.publish("/topic/#{topic.id}", reload_topic: true) end diff --git a/app/jobs/regular/confirm_sns_subscription.rb b/app/jobs/regular/confirm_sns_subscription.rb index f68588f89c..bd1f0dcebf 100644 --- a/app/jobs/regular/confirm_sns_subscription.rb +++ b/app/jobs/regular/confirm_sns_subscription.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class ConfirmSnsSubscription < ::Jobs::Base sidekiq_options retry: false @@ -13,15 +12,14 @@ module Jobs require "aws-sdk-sns" return unless Aws::SNS::MessageVerifier.new.authentic?(raw) - uri = begin - URI.parse(subscribe_url) - rescue URI::Error - return - end + uri = + begin + URI.parse(subscribe_url) + rescue URI::Error + return + end Net::HTTP.get(uri) end - end - end diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index da7659ec53..40cf4f8647 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -require 'open-uri' -require 'nokogiri' -require 'excon' +require "open-uri" +require "nokogiri" +require "excon" module Jobs class CrawlTopicLink < ::Jobs::Base - - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) raise Discourse::InvalidParameters.new(:topic_link_id) unless args[:topic_link_id].present? @@ -16,10 +15,13 @@ module Jobs return if topic_link.blank? # Look for a topic embed for the URL. If it exists, use its title and don't crawl - topic_embed = TopicEmbed.where(embed_url: topic_link.url).includes(:topic).references(:topic).first + topic_embed = + TopicEmbed.where(embed_url: topic_link.url).includes(:topic).references(:topic).first # topic could be deleted, so skip if topic_embed && topic_embed.topic - TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', topic_embed.topic.title[0..255]]) + TopicLink.where(id: topic_link.id).update_all( + ["title = ?, crawled_at = CURRENT_TIMESTAMP", topic_embed.topic.title[0..255]], + ) return end @@ -31,22 +33,33 @@ module Jobs if FileHelper.is_supported_image?(topic_link.url) uri = URI(topic_link.url) filename = File.basename(uri.path) - crawled = (TopicLink.where(id: topic_link.id).update_all(["title = ?, crawled_at = CURRENT_TIMESTAMP", filename]) == 1) + crawled = + ( + TopicLink.where(id: topic_link.id).update_all( + ["title = ?, crawled_at = CURRENT_TIMESTAMP", filename], + ) == 1 + ) end unless crawled # Fetch the beginning of the document to find the title title = RetrieveTitle.crawl(topic_link.url) if title.present? - crawled = (TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', title[0..254]]) == 1) + crawled = + ( + TopicLink.where(id: topic_link.id).update_all( + ["title = ?, crawled_at = CURRENT_TIMESTAMP", title[0..254]], + ) == 1 + ) end end rescue Exception # If there was a connection error, do nothing ensure - TopicLink.where(id: topic_link.id).update_all('crawled_at = CURRENT_TIMESTAMP') if !crawled && topic_link.present? + if !crawled && topic_link.present? + TopicLink.where(id: topic_link.id).update_all("crawled_at = CURRENT_TIMESTAMP") + end end end - end end diff --git a/app/jobs/regular/create_avatar_thumbnails.rb b/app/jobs/regular/create_avatar_thumbnails.rb index 51988a4764..e4191e9f62 100644 --- a/app/jobs/regular/create_avatar_thumbnails.rb +++ b/app/jobs/regular/create_avatar_thumbnails.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module Jobs - class CreateAvatarThumbnails < ::Jobs::Base - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) return if Rails.env.test? @@ -13,11 +12,7 @@ module Jobs return unless upload = Upload.find_by(id: upload_id) - Discourse.avatar_sizes.each do |size| - OptimizedImage.create_for(upload, size, size) - end + Discourse.avatar_sizes.each { |size| OptimizedImage.create_for(upload, size, size) } end - end - end diff --git a/app/jobs/regular/create_backup.rb b/app/jobs/regular/create_backup.rb index dcd3a466f5..14022a7dc0 100644 --- a/app/jobs/regular/create_backup.rb +++ b/app/jobs/regular/create_backup.rb @@ -7,7 +7,12 @@ module Jobs sidekiq_options retry: false def execute(args) - BackupRestore.backup!(Discourse.system_user.id, publish_to_message_bus: false, with_uploads: SiteSetting.backup_with_uploads, fork: false) + BackupRestore.backup!( + Discourse.system_user.id, + publish_to_message_bus: false, + with_uploads: SiteSetting.backup_with_uploads, + fork: false, + ) end end end diff --git a/app/jobs/regular/create_linked_topic.rb b/app/jobs/regular/create_linked_topic.rb index e862b82ab4..f5edca6efc 100644 --- a/app/jobs/regular/create_linked_topic.rb +++ b/app/jobs/regular/create_linked_topic.rb @@ -2,7 +2,6 @@ module Jobs class CreateLinkedTopic < ::Jobs::Base - def execute(args) reference_post = Post.find_by(id: args[:post_id]) return unless reference_post.present? @@ -16,53 +15,83 @@ module Jobs ActiveRecord::Base.transaction do linked_topic_record = parent_topic.linked_topic if linked_topic_record.present? - raw_title = parent_title.delete_suffix(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: "", count: linked_topic_record.sequence)) + raw_title = + parent_title.delete_suffix( + I18n.t( + "create_linked_topic.topic_title_with_sequence", + topic_title: "", + count: linked_topic_record.sequence, + ), + ) original_topic_id = linked_topic_record.original_topic_id sequence = linked_topic_record.sequence + 1 else raw_title = parent_title # update parent topic title to append title_suffix_locale - parent_title = I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: parent_title, count: 1) + parent_title = + I18n.t( + "create_linked_topic.topic_title_with_sequence", + topic_title: parent_title, + count: 1, + ) parent_topic.title = parent_title parent_topic.save! # create linked topic record original_topic_id = parent_topic_id - LinkedTopic.create!(topic_id: parent_topic_id, original_topic_id: original_topic_id, sequence: 1) + LinkedTopic.create!( + topic_id: parent_topic_id, + original_topic_id: original_topic_id, + sequence: 1, + ) sequence = 2 end # fetch previous topic titles previous_topics = "" linked_topic_ids = LinkedTopic.where(original_topic_id: original_topic_id).pluck(:topic_id) - Topic.where(id: linked_topic_ids).order(:id).each do |topic| - previous_topics += "- #{topic.url}\n" - end + Topic + .where(id: linked_topic_ids) + .order(:id) + .each { |topic| previous_topics += "- #{topic.url}\n" } # create new topic - new_topic_title = I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: sequence) - new_topic_raw = I18n.t('create_linked_topic.post_raw', parent_url: reference_post.full_url, previous_topics: previous_topics) + new_topic_title = + I18n.t( + "create_linked_topic.topic_title_with_sequence", + topic_title: raw_title, + count: sequence, + ) + new_topic_raw = + I18n.t( + "create_linked_topic.post_raw", + parent_url: reference_post.full_url, + previous_topics: previous_topics, + ) system_user = Discourse.system_user - @post_creator = PostCreator.new( - system_user, - title: new_topic_title, - raw: new_topic_raw, - category: parent_category_id, - skip_validations: true, - skip_jobs: true) + @post_creator = + PostCreator.new( + system_user, + title: new_topic_title, + raw: new_topic_raw, + category: parent_category_id, + skip_validations: true, + skip_jobs: true, + ) new_post = @post_creator.create new_topic = new_post.topic new_topic_id = new_topic.id # create linked_topic record - LinkedTopic.create!(topic_id: new_topic_id, original_topic_id: original_topic_id, sequence: sequence) + LinkedTopic.create!( + topic_id: new_topic_id, + original_topic_id: original_topic_id, + sequence: sequence, + ) # copy over topic tracking state from old topic - params = { - old_topic_id: parent_topic_id, - new_topic_id: new_topic_id - } + params = { old_topic_id: parent_topic_id, new_topic_id: new_topic_id } DB.exec(<<~SQL, params) INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id) @@ -78,9 +107,15 @@ module Jobs SQL # update small action post on old topic to add new topic link - small_action_post = Post.where(topic_id: parent_topic_id, post_type: Post.types[:small_action], action_code: "closed.enabled").last + small_action_post = + Post.where( + topic_id: parent_topic_id, + post_type: Post.types[:small_action], + action_code: "closed.enabled", + ).last if small_action_post.present? - small_action_post.raw = "#{small_action_post.raw} #{I18n.t('create_linked_topic.small_action_post_raw', new_title: "[#{new_topic_title}](#{new_topic.url})")}" + small_action_post.raw = + "#{small_action_post.raw} #{I18n.t("create_linked_topic.small_action_post_raw", new_title: "[#{new_topic_title}](#{new_topic.url})")}" small_action_post.save! end end diff --git a/app/jobs/regular/create_user_reviewable.rb b/app/jobs/regular/create_user_reviewable.rb index b3c20267ed..5d4a69fbde 100644 --- a/app/jobs/regular/create_user_reviewable.rb +++ b/app/jobs/regular/create_user_reviewable.rb @@ -15,24 +15,25 @@ class Jobs::CreateUserReviewable < ::Jobs::Base if user = User.find_by(id: args[:user_id]) return if user.approved? - @reviewable = ReviewableUser.needs_review!( - target: user, - created_by: Discourse.system_user, - reviewable_by_moderator: true, - payload: { - username: user.username, - name: user.name, - email: user.email, - website: user.user_profile&.website - } - ) + @reviewable = + ReviewableUser.needs_review!( + target: user, + created_by: Discourse.system_user, + reviewable_by_moderator: true, + payload: { + username: user.username, + name: user.name, + email: user.email, + website: user.user_profile&.website, + }, + ) if @reviewable.created_new @reviewable.add_score( Discourse.system_user, ReviewableScore.types[:needs_approval], reason: reason, - force_review: true + force_review: true, ) end end diff --git a/app/jobs/regular/critical_user_email.rb b/app/jobs/regular/critical_user_email.rb index 47d2ef14e9..6911ff174a 100644 --- a/app/jobs/regular/critical_user_email.rb +++ b/app/jobs/regular/critical_user_email.rb @@ -2,8 +2,7 @@ module Jobs class CriticalUserEmail < UserEmail - - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" def quit_email_early? false diff --git a/app/jobs/regular/delete_inaccessible_notifications.rb b/app/jobs/regular/delete_inaccessible_notifications.rb index e33bfd6fb1..e0f0e55d4d 100644 --- a/app/jobs/regular/delete_inaccessible_notifications.rb +++ b/app/jobs/regular/delete_inaccessible_notifications.rb @@ -5,13 +5,13 @@ module Jobs def execute(args) raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].blank? - Notification.where(topic_id: args[:topic_id]).find_each do |notification| - next unless notification.user && notification.topic + Notification + .where(topic_id: args[:topic_id]) + .find_each do |notification| + next unless notification.user && notification.topic - if !Guardian.new(notification.user).can_see?(notification.topic) - notification.destroy + notification.destroy if !Guardian.new(notification.user).can_see?(notification.topic) end - end end end end diff --git a/app/jobs/regular/delete_replies.rb b/app/jobs/regular/delete_replies.rb index 7208f4e561..d235c39bcf 100644 --- a/app/jobs/regular/delete_replies.rb +++ b/app/jobs/regular/delete_replies.rb @@ -9,15 +9,25 @@ module Jobs end replies = topic.posts.where("posts.post_number > 1") - replies = replies.where("like_count < ?", SiteSetting.skip_auto_delete_reply_likes) if SiteSetting.skip_auto_delete_reply_likes > 0 + replies = + replies.where( + "like_count < ?", + SiteSetting.skip_auto_delete_reply_likes, + ) if SiteSetting.skip_auto_delete_reply_likes > 0 - replies.where('posts.created_at < ?', topic_timer.duration_minutes.minutes.ago).each do |post| - PostDestroyer.new(topic_timer.user, post, context: I18n.t("topic_statuses.auto_deleted_by_timer")).destroy - end + replies + .where("posts.created_at < ?", topic_timer.duration_minutes.minutes.ago) + .each do |post| + PostDestroyer.new( + topic_timer.user, + post, + context: I18n.t("topic_statuses.auto_deleted_by_timer"), + ).destroy + end - topic_timer.execute_at = (replies.minimum(:created_at) || Time.zone.now) + topic_timer.duration_minutes.minutes + topic_timer.execute_at = + (replies.minimum(:created_at) || Time.zone.now) + topic_timer.duration_minutes.minutes topic_timer.save end - end end diff --git a/app/jobs/regular/delete_topic.rb b/app/jobs/regular/delete_topic.rb index af109d8b10..d3a35d5d98 100644 --- a/app/jobs/regular/delete_topic.rb +++ b/app/jobs/regular/delete_topic.rb @@ -7,7 +7,9 @@ module Jobs first_post = topic.ordered_posts.first PostDestroyer.new( - topic_timer.user, first_post, context: I18n.t("topic_statuses.auto_deleted_by_timer") + topic_timer.user, + first_post, + context: I18n.t("topic_statuses.auto_deleted_by_timer"), ).destroy topic_timer.trash!(Discourse.system_user) diff --git a/app/jobs/regular/download_avatar_from_url.rb b/app/jobs/regular/download_avatar_from_url.rb index e1864c80b3..06ef524a25 100644 --- a/app/jobs/regular/download_avatar_from_url.rb +++ b/app/jobs/regular/download_avatar_from_url.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class DownloadAvatarFromUrl < ::Jobs::Base sidekiq_options retry: false @@ -15,16 +14,10 @@ module Jobs return unless user = User.find_by(id: user_id) begin - UserAvatar.import_url_for_user( - url, - user, - override_gravatar: args[:override_gravatar] - ) + UserAvatar.import_url_for_user(url, user, override_gravatar: args[:override_gravatar]) rescue Discourse::InvalidParameters => e - raise e unless e.message == 'url' + raise e unless e.message == "url" end end - end - end diff --git a/app/jobs/regular/download_backup_email.rb b/app/jobs/regular/download_backup_email.rb index 44e13495f2..000d0591f2 100644 --- a/app/jobs/regular/download_backup_email.rb +++ b/app/jobs/regular/download_backup_email.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Jobs - class DownloadBackupEmail < ::Jobs::Base - - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" def execute(args) user_id = args[:user_id] @@ -20,7 +18,5 @@ module Jobs message = DownloadBackupMailer.send_email(user.email, backup_file_path.to_s) Email::Sender.new(message, :download_backup_message).send end - end - end diff --git a/app/jobs/regular/download_profile_background_from_url.rb b/app/jobs/regular/download_profile_background_from_url.rb index 5d39c3d8c8..bf8e15ff03 100644 --- a/app/jobs/regular/download_profile_background_from_url.rb +++ b/app/jobs/regular/download_profile_background_from_url.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class DownloadProfileBackgroundFromUrl < ::Jobs::Base sidekiq_options retry: false @@ -15,16 +14,10 @@ module Jobs return unless user = User.find_by(id: user_id) begin - UserProfile.import_url_for_user( - url, - user, - is_card_background: args[:is_card_background], - ) + UserProfile.import_url_for_user(url, user, is_card_background: args[:is_card_background]) rescue Discourse::InvalidParameters => e - raise e unless e.message == 'url' + raise e unless e.message == "url" end end - end - end diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 806765309b..ab648e6572 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'excon' +require "excon" module Jobs class EmitWebHookEvent < ::Jobs::Base - sidekiq_options queue: 'low' + sidekiq_options queue: "low" - PING_EVENT = 'ping' + PING_EVENT = "ping" MAX_RETRY_COUNT = 4 RETRY_BACKOFF = 5 @@ -63,9 +63,10 @@ module Jobs if @retry_count >= MAX_RETRY_COUNT @web_hook.update!(active: false) - StaffActionLogger - .new(Discourse.system_user) - .log_web_hook_deactivate(@web_hook, web_hook_response.status) + StaffActionLogger.new(Discourse.system_user).log_web_hook_deactivate( + @web_hook, + web_hook_response.status, + ) end else retry_web_hook @@ -83,10 +84,11 @@ module Jobs end def publish_webhook_event(web_hook_event) - MessageBus.publish("/web_hook_events/#{@web_hook.id}", { - web_hook_event_id: web_hook_event.id, - event_type: @arguments[:event_type] - }, group_ids: [Group::AUTO_GROUPS[:staff]]) + MessageBus.publish( + "/web_hook_events/#{@web_hook.id}", + { web_hook_event_id: web_hook_event.id, event_type: @arguments[:event_type] }, + group_ids: [Group::AUTO_GROUPS[:staff]], + ) end def ping_event?(event_type) @@ -98,45 +100,50 @@ module Jobs end def group_webhook_invalid? - @web_hook.group_ids.present? && (@arguments[:group_ids].blank? || - (@web_hook.group_ids & @arguments[:group_ids]).blank?) + @web_hook.group_ids.present? && + (@arguments[:group_ids].blank? || (@web_hook.group_ids & @arguments[:group_ids]).blank?) end def category_webhook_invalid? - @web_hook.category_ids.present? && (!@arguments[:category_id].present? || - !@web_hook.category_ids.include?(@arguments[:category_id])) + @web_hook.category_ids.present? && + ( + !@arguments[:category_id].present? || + !@web_hook.category_ids.include?(@arguments[:category_id]) + ) end def tag_webhook_invalid? - @web_hook.tag_ids.present? && (@arguments[:tag_ids].blank? || - (@web_hook.tag_ids & @arguments[:tag_ids]).blank?) + @web_hook.tag_ids.present? && + (@arguments[:tag_ids].blank? || (@web_hook.tag_ids & @arguments[:tag_ids]).blank?) end def build_webhook_headers(uri, web_hook_body, web_hook_event) content_type = case @web_hook.content_type - when WebHook.content_types['application/x-www-form-urlencoded'] - 'application/x-www-form-urlencoded' + when WebHook.content_types["application/x-www-form-urlencoded"] + "application/x-www-form-urlencoded" else - 'application/json' + "application/json" end headers = { - 'Accept' => '*/*', - 'Connection' => 'close', - 'Content-Length' => web_hook_body.bytesize.to_s, - 'Content-Type' => content_type, - 'Host' => uri.host, - 'User-Agent' => "Discourse/#{Discourse::VERSION::STRING}", - 'X-Discourse-Instance' => Discourse.base_url, - 'X-Discourse-Event-Id' => web_hook_event.id.to_s, - 'X-Discourse-Event-Type' => @arguments[:event_type] + "Accept" => "*/*", + "Connection" => "close", + "Content-Length" => web_hook_body.bytesize.to_s, + "Content-Type" => content_type, + "Host" => uri.host, + "User-Agent" => "Discourse/#{Discourse::VERSION::STRING}", + "X-Discourse-Instance" => Discourse.base_url, + "X-Discourse-Event-Id" => web_hook_event.id.to_s, + "X-Discourse-Event-Type" => @arguments[:event_type], } - headers['X-Discourse-Event'] = @arguments[:event_name] if @arguments[:event_name].present? + headers["X-Discourse-Event"] = @arguments[:event_name] if @arguments[:event_name].present? if @web_hook.secret.present? - headers['X-Discourse-Event-Signature'] = "sha256=#{OpenSSL::HMAC.hexdigest("sha256", @web_hook.secret, web_hook_body)}" + headers[ + "X-Discourse-Event-Signature" + ] = "sha256=#{OpenSSL::HMAC.hexdigest("sha256", @web_hook.secret, web_hook_body)}" end headers @@ -146,7 +153,7 @@ module Jobs body = {} if ping_event?(@arguments[:event_type]) - body['ping'] = "OK" + body["ping"] = "OK" else body[@arguments[:event_type]] = JSON.parse(@arguments[:payload]) end @@ -158,6 +165,5 @@ module Jobs def create_webhook_event(web_hook_body) WebHookEvent.create!(web_hook: @web_hook, payload: web_hook_body) end - end end diff --git a/app/jobs/regular/enable_bootstrap_mode.rb b/app/jobs/regular/enable_bootstrap_mode.rb index bf804e24db..8b7d456f4c 100644 --- a/app/jobs/regular/enable_bootstrap_mode.rb +++ b/app/jobs/regular/enable_bootstrap_mode.rb @@ -2,7 +2,7 @@ module Jobs class EnableBootstrapMode < ::Jobs::Base - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" def execute(args) raise Discourse::InvalidParameters.new(:user_id) unless args[:user_id].present? @@ -13,14 +13,14 @@ module Jobs # let's enable bootstrap mode settings if SiteSetting.default_trust_level == TrustLevel[0] - SiteSetting.set_and_log('default_trust_level', TrustLevel[1]) + SiteSetting.set_and_log("default_trust_level", TrustLevel[1]) end - if SiteSetting.default_email_digest_frequency == 10080 - SiteSetting.set_and_log('default_email_digest_frequency', 1440) + if SiteSetting.default_email_digest_frequency == 10_080 + SiteSetting.set_and_log("default_email_digest_frequency", 1440) end - SiteSetting.set_and_log('bootstrap_mode_enabled', true) + SiteSetting.set_and_log("bootstrap_mode_enabled", true) end end end diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index 44305b4472..b725ab5dd2 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'csv' +require "csv" module Jobs - class ExportCsvFile < ::Jobs::Base sidekiq_options retry: false @@ -11,17 +10,53 @@ module Jobs attr_accessor :current_user attr_accessor :entity - HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( - user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced_till', 'active', 'admin', 'moderator', 'ip_address', 'staged', 'secondary_emails'], - user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'], - user_profile: ['location', 'website', 'views'], - user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'], - staff_action: ['staff_user', 'action', 'subject', 'created_at', 'details', 'context'], - screened_email: ['email', 'action', 'match_count', 'last_match_at', 'created_at', 'ip_address'], - screened_ip: ['ip_address', 'action', 'match_count', 'last_match_at', 'created_at'], - screened_url: ['domain', 'action', 'match_count', 'last_match_at', 'created_at'], - report: ['date', 'value'] - ) + HEADER_ATTRS_FOR ||= + HashWithIndifferentAccess.new( + user_list: %w[ + id + name + username + email + title + created_at + last_seen_at + last_posted_at + last_emailed_at + trust_level + approved + suspended_at + suspended_till + silenced_till + active + admin + moderator + ip_address + staged + secondary_emails + ], + user_stats: %w[ + topics_entered + posts_read_count + time_read + topic_count + post_count + likes_given + likes_received + ], + user_profile: %w[location website views], + user_sso: %w[ + external_id + external_email + external_username + external_name + external_avatar_url + ], + staff_action: %w[staff_user action subject created_at details context], + screened_email: %w[email action match_count last_match_at created_at ip_address], + screened_ip: %w[ip_address action match_count last_match_at created_at], + screened_url: %w[domain action match_count last_match_at created_at], + report: %w[date value], + ) def execute(args) @entity = args[:entity] @@ -35,19 +70,19 @@ module Jobs raise Discourse::InvalidParameters.new(:entity) unless respond_to?(entity[:method]) @timestamp ||= Time.now.strftime("%y%m%d-%H%M%S") - entity[:filename] = - if entity[:name] == "report" && @extra[:name].present? - "#{@extra[:name].dasherize}-#{@timestamp}" - else - "#{entity[:name].dasherize}-#{@timestamp}" - end + entity[:filename] = if entity[:name] == "report" && @extra[:name].present? + "#{@extra[:name].dasherize}-#{@timestamp}" + else + "#{entity[:name].dasherize}-#{@timestamp}" + end end - export_title = if @entity == "report" && @extra[:name].present? - I18n.t("reports.#{@extra[:name]}.title") - else - @entity.gsub('_', ' ').titleize - end + export_title = + if @entity == "report" && @extra[:name].present? + I18n.t("reports.#{@extra[:name]}.title") + else + @entity.gsub("_", " ").titleize + end filename = entities[0][:filename] # use first entity as a name for this export user_export = UserExport.create(file_name: filename, user_id: @current_user.id) @@ -77,12 +112,13 @@ module Jobs if File.exist?(zip_filename) File.open(zip_filename) do |file| - upload = UploadCreator.new( - file, - File.basename(zip_filename), - type: 'csv_export', - for_export: 'true' - ).create_for(@current_user.id) + upload = + UploadCreator.new( + file, + File.basename(zip_filename), + type: "csv_export", + for_export: "true", + ).create_for(@current_user.id) if upload.persisted? user_export.update_columns(upload_id: upload.id) @@ -99,7 +135,7 @@ module Jobs if user_export.present? && post.present? topic = post.topic user_export.update_columns(topic_id: topic.id) - topic.update_status('closed', true, Discourse.system_user) + topic.update_status("closed", true, Discourse.system_user) end end @@ -109,66 +145,67 @@ module Jobs user_field_ids = UserField.pluck(:id) condition = {} - if @extra && @extra[:trust_level] && trust_level = TrustLevel.levels[@extra[:trust_level].to_sym] + if @extra && @extra[:trust_level] && + trust_level = TrustLevel.levels[@extra[:trust_level].to_sym] condition = { trust_level: trust_level } end - includes = [:user_profile, :user_stat, :groups, :user_emails] - if SiteSetting.enable_discourse_connect - includes << [:single_sign_on_record] - end + includes = %i[user_profile user_stat groups user_emails] + includes << [:single_sign_on_record] if SiteSetting.enable_discourse_connect - User.where(condition).includes(*includes).find_each do |user| - user_info_array = get_base_user_array(user) - if SiteSetting.enable_discourse_connect - user_info_array = add_single_sign_on(user, user_info_array) + User + .where(condition) + .includes(*includes) + .find_each do |user| + user_info_array = get_base_user_array(user) + if SiteSetting.enable_discourse_connect + user_info_array = add_single_sign_on(user, user_info_array) + end + user_info_array = add_custom_fields(user, user_info_array, user_field_ids) + user_info_array = add_group_names(user, user_info_array) + yield user_info_array end - user_info_array = add_custom_fields(user, user_info_array, user_field_ids) - user_info_array = add_group_names(user, user_info_array) - yield user_info_array - end - end def staff_action_export return enum_for(:staff_action_export) unless block_given? - staff_action_data = if @current_user.admin? - UserHistory.only_staff_actions.order('id DESC') - else - UserHistory.where(admin_only: false).only_staff_actions.order('id DESC') - end + staff_action_data = + if @current_user.admin? + UserHistory.only_staff_actions.order("id DESC") + else + UserHistory.where(admin_only: false).only_staff_actions.order("id DESC") + end - staff_action_data.each do |staff_action| - yield get_staff_action_fields(staff_action) - end + staff_action_data.each { |staff_action| yield get_staff_action_fields(staff_action) } end def screened_email_export return enum_for(:screened_email_export) unless block_given? - ScreenedEmail.order('last_match_at DESC').each do |screened_email| - yield get_screened_email_fields(screened_email) - end + ScreenedEmail + .order("last_match_at DESC") + .each { |screened_email| yield get_screened_email_fields(screened_email) } end def screened_ip_export return enum_for(:screened_ip_export) unless block_given? - ScreenedIpAddress.order('id DESC').each do |screened_ip| - yield get_screened_ip_fields(screened_ip) - end + ScreenedIpAddress + .order("id DESC") + .each { |screened_ip| yield get_screened_ip_fields(screened_ip) } end def screened_url_export return enum_for(:screened_url_export) unless block_given? - ScreenedUrl.select("domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at") + ScreenedUrl + .select( + "domain, sum(match_count) as match_count, max(last_match_at) as last_match_at, min(created_at) as created_at", + ) .group(:domain) - .order('last_match_at DESC') - .each do |screened_url| - yield get_screened_url_fields(screened_url) - end + .order("last_match_at DESC") + .each { |screened_url| yield get_screened_url_fields(screened_url) } end def report_export @@ -176,16 +213,26 @@ module Jobs # If dates are invalid consider then `nil` if @extra[:start_date].is_a?(String) - @extra[:start_date] = @extra[:start_date].to_date.beginning_of_day rescue nil + @extra[:start_date] = begin + @extra[:start_date].to_date.beginning_of_day + rescue StandardError + nil + end end if @extra[:end_date].is_a?(String) - @extra[:end_date] = @extra[:end_date].to_date.end_of_day rescue nil + @extra[:end_date] = begin + @extra[:end_date].to_date.end_of_day + rescue StandardError + nil + end end @extra[:filters] = {} @extra[:filters][:category] = @extra[:category].to_i if @extra[:category].present? @extra[:filters][:group] = @extra[:group].to_i if @extra[:group].present? - @extra[:filters][:include_subcategories] = !!ActiveRecord::Type::Boolean.new.cast(@extra[:include_subcategories]) if @extra[:include_subcategories].present? + @extra[:filters][:include_subcategories] = !!ActiveRecord::Type::Boolean.new.cast( + @extra[:include_subcategories], + ) if @extra[:include_subcategories].present? report = Report.find(@extra[:name], @extra) @@ -227,9 +274,11 @@ module Jobs end def get_header(entity) - if entity == 'user_list' - header_array = HEADER_ATTRS_FOR['user_list'] + HEADER_ATTRS_FOR['user_stats'] + HEADER_ATTRS_FOR['user_profile'] - header_array.concat(HEADER_ATTRS_FOR['user_sso']) if SiteSetting.enable_discourse_connect + if entity == "user_list" + header_array = + HEADER_ATTRS_FOR["user_list"] + HEADER_ATTRS_FOR["user_stats"] + + HEADER_ATTRS_FOR["user_profile"] + header_array.concat(HEADER_ATTRS_FOR["user_sso"]) if SiteSetting.enable_discourse_connect user_custom_fields = UserField.all if user_custom_fields.present? user_custom_fields.each do |custom_field| @@ -299,7 +348,13 @@ module Jobs def add_single_sign_on(user, user_info_array) if user.single_sign_on_record - user_info_array.push(user.single_sign_on_record.external_id, user.single_sign_on_record.external_email, user.single_sign_on_record.external_username, escape_comma(user.single_sign_on_record.external_name), user.single_sign_on_record.external_avatar_url) + user_info_array.push( + user.single_sign_on_record.external_id, + user.single_sign_on_record.external_email, + user.single_sign_on_record.external_username, + escape_comma(user.single_sign_on_record.external_name), + user.single_sign_on_record.external_avatar_url, + ) else user_info_array.push(nil, nil, nil, nil, nil) end @@ -308,9 +363,7 @@ module Jobs def add_custom_fields(user, user_info_array, user_field_ids) if user_field_ids.present? - user.user_fields.each do |custom_field| - user_info_array << escape_comma(custom_field[1]) - end + user.user_fields.each { |custom_field| user_info_array << escape_comma(custom_field[1]) } end user_info_array end @@ -328,21 +381,25 @@ module Jobs def get_staff_action_fields(staff_action) staff_action_array = [] - HEADER_ATTRS_FOR['staff_action'].each do |attr| + HEADER_ATTRS_FOR["staff_action"].each do |attr| data = - if attr == 'action' + if attr == "action" UserHistory.actions.key(staff_action.attributes[attr]).to_s - elsif attr == 'staff_user' - user = User.find_by(id: staff_action.attributes['acting_user_id']) + elsif attr == "staff_user" + user = User.find_by(id: staff_action.attributes["acting_user_id"]) user.username if !user.nil? - elsif attr == 'subject' - user = User.find_by(id: staff_action.attributes['target_user_id']) - user.nil? ? staff_action.attributes[attr] : "#{user.username} #{staff_action.attributes[attr]}" + elsif attr == "subject" + user = User.find_by(id: staff_action.attributes["target_user_id"]) + if user.nil? + staff_action.attributes[attr] + else + "#{user.username} #{staff_action.attributes[attr]}" + end else staff_action.attributes[attr] end - staff_action_array.push(data) + staff_action_array.push(data) end staff_action_array end @@ -350,10 +407,10 @@ module Jobs def get_screened_email_fields(screened_email) screened_email_array = [] - HEADER_ATTRS_FOR['screened_email'].each do |attr| + HEADER_ATTRS_FOR["screened_email"].each do |attr| data = - if attr == 'action' - ScreenedEmail.actions.key(screened_email.attributes['action_type']).to_s + if attr == "action" + ScreenedEmail.actions.key(screened_email.attributes["action_type"]).to_s else screened_email.attributes[attr] end @@ -367,10 +424,10 @@ module Jobs def get_screened_ip_fields(screened_ip) screened_ip_array = [] - HEADER_ATTRS_FOR['screened_ip'].each do |attr| + HEADER_ATTRS_FOR["screened_ip"].each do |attr| data = - if attr == 'action' - ScreenedIpAddress.actions.key(screened_ip.attributes['action_type']).to_s + if attr == "action" + ScreenedIpAddress.actions.key(screened_ip.attributes["action_type"]).to_s else screened_ip.attributes[attr] end @@ -384,10 +441,10 @@ module Jobs def get_screened_url_fields(screened_url) screened_url_array = [] - HEADER_ATTRS_FOR['screened_url'].each do |attr| + HEADER_ATTRS_FOR["screened_url"].each do |attr| data = - if attr == 'action' - action = ScreenedUrl.actions.key(screened_url.attributes['action_type']).to_s + if attr == "action" + action = ScreenedUrl.actions.key(screened_url.attributes["action_type"]).to_s action = "do nothing" if action.blank? else screened_url.attributes[attr] @@ -403,16 +460,17 @@ module Jobs post = nil if @current_user - post = if upload - SystemMessage.create_from_system_user( - @current_user, - :csv_export_succeeded, - download_link: UploadMarkdown.new(upload).attachment_markdown, - export_title: export_title - ) - else - SystemMessage.create_from_system_user(@current_user, :csv_export_failed) - end + post = + if upload + SystemMessage.create_from_system_user( + @current_user, + :csv_export_succeeded, + download_link: UploadMarkdown.new(upload).attachment_markdown, + export_title: export_title, + ) + else + SystemMessage.create_from_system_user(@current_user, :csv_export_failed) + end end post diff --git a/app/jobs/regular/export_user_archive.rb b/app/jobs/regular/export_user_archive.rb index 657cc4aafe..59c60740b3 100644 --- a/app/jobs/regular/export_user_archive.rb +++ b/app/jobs/regular/export_user_archive.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'csv' +require "csv" module Jobs class ExportUserArchive < ::Jobs::Base @@ -10,7 +10,7 @@ module Jobs # note: contents provided entirely by user attr_accessor :extra - COMPONENTS ||= %w( + COMPONENTS ||= %w[ user_archive preferences auth_tokens @@ -23,22 +23,98 @@ module Jobs post_actions queued_posts visits - ) + ] - HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( - user_archive: ['topic_title', 'categories', 'is_pm', 'post_raw', 'post_cooked', 'like_count', 'reply_count', 'url', 'created_at'], - user_archive_profile: ['location', 'website', 'bio', 'views'], - auth_tokens: ['id', 'auth_token_hash', 'prev_auth_token_hash', 'auth_token_seen', 'client_ip', 'user_agent', 'seen_at', 'rotated_at', 'created_at', 'updated_at'], - auth_token_logs: ['id', 'action', 'user_auth_token_id', 'client_ip', 'auth_token_hash', 'created_at', 'path', 'user_agent'], - badges: ['badge_id', 'badge_name', 'granted_at', 'post_id', 'seq', 'granted_manually', 'notification_id', 'featured_rank'], - bookmarks: ['bookmarkable_id', 'bookmarkable_type', 'link', 'name', 'created_at', 'updated_at', 'reminder_at', 'reminder_last_sent_at', 'reminder_set_at', 'auto_delete_preference'], - category_preferences: ['category_id', 'category_names', 'notification_level', 'dismiss_new_timestamp'], - flags: ['id', 'post_id', 'flag_type', 'created_at', 'updated_at', 'deleted_at', 'deleted_by', 'related_post_id', 'targets_topic', 'was_take_action'], - likes: ['id', 'post_id', 'topic_id', 'post_number', 'created_at', 'updated_at', 'deleted_at', 'deleted_by'], - post_actions: ['id', 'post_id', 'post_action_type', 'created_at', 'updated_at', 'deleted_at', 'deleted_by', 'related_post_id'], - queued_posts: ['id', 'verdict', 'category_id', 'topic_id', 'post_raw', 'other_json'], - visits: ['visited_at', 'posts_read', 'mobile', 'time_read'], - ) + HEADER_ATTRS_FOR ||= + HashWithIndifferentAccess.new( + user_archive: %w[ + topic_title + categories + is_pm + post_raw + post_cooked + like_count + reply_count + url + created_at + ], + user_archive_profile: %w[location website bio views], + auth_tokens: %w[ + id + auth_token_hash + prev_auth_token_hash + auth_token_seen + client_ip + user_agent + seen_at + rotated_at + created_at + updated_at + ], + auth_token_logs: %w[ + id + action + user_auth_token_id + client_ip + auth_token_hash + created_at + path + user_agent + ], + badges: %w[ + badge_id + badge_name + granted_at + post_id + seq + granted_manually + notification_id + featured_rank + ], + bookmarks: %w[ + bookmarkable_id + bookmarkable_type + link + name + created_at + updated_at + reminder_at + reminder_last_sent_at + reminder_set_at + auto_delete_preference + ], + category_preferences: %w[ + category_id + category_names + notification_level + dismiss_new_timestamp + ], + flags: %w[ + id + post_id + flag_type + created_at + updated_at + deleted_at + deleted_by + related_post_id + targets_topic + was_take_action + ], + likes: %w[id post_id topic_id post_number created_at updated_at deleted_at deleted_by], + post_actions: %w[ + id + post_id + post_action_type + created_at + updated_at + deleted_at + deleted_by + related_post_id + ], + queued_posts: %w[id verdict category_id topic_id post_raw other_json], + visits: %w[visited_at posts_read mobile time_read], + ) def execute(args) @current_user = User.find_by(id: args[:user_id]) @@ -52,18 +128,14 @@ module Jobs h = { name: name, method: :"#{export_method}" } h[:filetype] = :csv filetype_method = :"#{name}_filetype" - if respond_to? filetype_method - h[:filetype] = public_send(filetype_method) - end + h[:filetype] = public_send(filetype_method) if respond_to? filetype_method condition_method = :"include_#{name}?" - if respond_to? condition_method - h[:skip] = !public_send(condition_method) - end + h[:skip] = !public_send(condition_method) if respond_to? condition_method h[:filename] = name components.push(h) end - export_title = 'user_archive'.titleize + export_title = "user_archive".titleize filename = "user_archive-#{@current_user.username}-#{@timestamp}" user_export = UserExport.create(file_name: filename, user_id: @current_user.id) @@ -89,7 +161,7 @@ module Jobs file.write MultiJson.dump(public_send(component[:method]), indent: 4) end else - raise 'unknown export filetype' + raise "unknown export filetype" end end @@ -103,17 +175,20 @@ module Jobs if File.exist?(zip_filename) File.open(zip_filename) do |file| - upload = UploadCreator.new( - file, - File.basename(zip_filename), - type: 'csv_export', - for_export: 'true' - ).create_for(@current_user.id) + upload = + UploadCreator.new( + file, + File.basename(zip_filename), + type: "csv_export", + for_export: "true", + ).create_for(@current_user.id) if upload.persisted? user_export.update_columns(upload_id: upload.id) else - Rails.logger.warn("Failed to upload the file #{zip_filename}: #{upload.errors.full_messages}") + Rails.logger.warn( + "Failed to upload the file #{zip_filename}: #{upload.errors.full_messages}", + ) end end @@ -125,21 +200,20 @@ module Jobs if user_export.present? && post.present? topic = post.topic user_export.update_columns(topic_id: topic.id) - topic.update_status('closed', true, Discourse.system_user) + topic.update_status("closed", true, Discourse.system_user) end end def user_archive_export return enum_for(:user_archive_export) unless block_given? - Post.includes(topic: :category) + Post + .includes(topic: :category) .where(user_id: @current_user.id) .select(:topic_id, :post_number, :raw, :cooked, :like_count, :reply_count, :created_at) .order(:created_at) .with_deleted - .each do |user_archive| - yield get_user_archive_fields(user_archive) - end + .each { |user_archive| yield get_user_archive_fields(user_archive) } end def user_archive_profile_export @@ -148,9 +222,7 @@ module Jobs UserProfile .where(user_id: @current_user.id) .select(:location, :website, :bio_raw, :views) - .each do |user_profile| - yield get_user_archive_profile_fields(user_profile) - end + .each { |user_profile| yield get_user_archive_profile_fields(user_profile) } end def preferences_export @@ -167,19 +239,21 @@ module Jobs UserAuthToken .where(user_id: @current_user.id) .each do |token| - yield [ - token.id, - token.auth_token.to_s[0..4] + "...", # hashed and truncated - token.prev_auth_token[0..4] + "...", - token.auth_token_seen, - token.client_ip, - token.user_agent, - token.seen_at, - token.rotated_at, - token.created_at, - token.updated_at, - ] - end + yield( + [ + token.id, + token.auth_token.to_s[0..4] + "...", # hashed and truncated + token.prev_auth_token[0..4] + "...", + token.auth_token_seen, + token.client_ip, + token.user_agent, + token.seen_at, + token.rotated_at, + token.created_at, + token.updated_at, + ] + ) + end end def include_auth_token_logs? @@ -193,17 +267,19 @@ module Jobs UserAuthTokenLog .where(user_id: @current_user.id) .each do |log| - yield [ - log.id, - log.action, - log.user_auth_token_id, - log.client_ip, - log.auth_token.to_s[0..4] + "...", # hashed and truncated - log.created_at, - log.path, - log.user_agent, - ] - end + yield( + [ + log.id, + log.action, + log.user_auth_token_id, + log.client_ip, + log.auth_token.to_s[0..4] + "...", # hashed and truncated + log.created_at, + log.path, + log.user_agent, + ] + ) + end end def badges_export @@ -212,49 +288,65 @@ module Jobs UserBadge .where(user_id: @current_user.id) .joins(:badge) - .select(:badge_id, :granted_at, :post_id, :seq, :granted_by_id, :notification_id, :featured_rank) + .select( + :badge_id, + :granted_at, + :post_id, + :seq, + :granted_by_id, + :notification_id, + :featured_rank, + ) .order(:granted_at) .each do |ub| - yield [ - ub.badge_id, - ub.badge.display_name, - ub.granted_at, - ub.post_id, - ub.seq, - # Hide the admin's identity, simply indicate human or system - User.human_user_id?(ub.granted_by_id), - ub.notification_id, - ub.featured_rank, - ] - end + yield( + [ + ub.badge_id, + ub.badge.display_name, + ub.granted_at, + ub.post_id, + ub.seq, + # Hide the admin's identity, simply indicate human or system + User.human_user_id?(ub.granted_by_id), + ub.notification_id, + ub.featured_rank, + ] + ) + end end def bookmarks_export return enum_for(:bookmarks_export) unless block_given? - @current_user.bookmarks.where.not(bookmarkable_type: nil).order(:id).each do |bookmark| - link = '' - if guardian.can_see_bookmarkable?(bookmark) - if bookmark.bookmarkable.respond_to?(:full_url) - link = bookmark.bookmarkable.full_url - else - link = bookmark.bookmarkable.url + @current_user + .bookmarks + .where.not(bookmarkable_type: nil) + .order(:id) + .each do |bookmark| + link = "" + if guardian.can_see_bookmarkable?(bookmark) + if bookmark.bookmarkable.respond_to?(:full_url) + link = bookmark.bookmarkable.full_url + else + link = bookmark.bookmarkable.url + end end - end - yield [ - bookmark.bookmarkable_id, - bookmark.bookmarkable_type, - link, - bookmark.name, - bookmark.created_at, - bookmark.updated_at, - bookmark.reminder_at, - bookmark.reminder_last_sent_at, - bookmark.reminder_set_at, - Bookmark.auto_delete_preferences[bookmark.auto_delete_preference], - ] - end + yield( + [ + bookmark.bookmarkable_id, + bookmark.bookmarkable_type, + link, + bookmark.name, + bookmark.created_at, + bookmark.updated_at, + bookmark.reminder_at, + bookmark.reminder_last_sent_at, + bookmark.reminder_set_at, + Bookmark.auto_delete_preferences[bookmark.auto_delete_preference], + ] + ) + end end def category_preferences_export @@ -265,12 +357,14 @@ module Jobs .includes(:category) .merge(Category.secured(guardian)) .each do |cu| - yield [ - cu.category_id, - piped_category_name(cu.category_id, cu.category), - NotificationLevels.all[cu.notification_level], - cu.last_seen_at - ] + yield( + [ + cu.category_id, + piped_category_name(cu.category_id, cu.category), + NotificationLevels.all[cu.notification_level], + cu.last_seen_at, + ] + ) end end @@ -283,20 +377,22 @@ module Jobs .where(post_action_type_id: PostActionType.flag_types.values) .order(:created_at) .each do |pa| - yield [ - pa.id, - pa.post_id, - PostActionType.flag_types[pa.post_action_type_id], - pa.created_at, - pa.updated_at, - pa.deleted_at, - self_or_other(pa.deleted_by_id), - pa.related_post_id, - pa.targets_topic, - # renamed to 'was_take_action' to avoid possibility of thinking this is a synonym of agreed_at - pa.staff_took_action, - ] - end + yield( + [ + pa.id, + pa.post_id, + PostActionType.flag_types[pa.post_action_type_id], + pa.created_at, + pa.updated_at, + pa.deleted_at, + self_or_other(pa.deleted_by_id), + pa.related_post_id, + pa.targets_topic, + # renamed to 'was_take_action' to avoid possibility of thinking this is a synonym of agreed_at + pa.staff_took_action, + ] + ) + end end def likes_export @@ -307,25 +403,29 @@ module Jobs .where(post_action_type_id: PostActionType.types[:like]) .order(:created_at) .each do |pa| - post = Post.with_deleted.find_by(id: pa.post_id) - yield [ - pa.id, - pa.post_id, - post&.topic_id, - post&.post_number, - pa.created_at, - pa.updated_at, - pa.deleted_at, - self_or_other(pa.deleted_by_id), - ] - end + post = Post.with_deleted.find_by(id: pa.post_id) + yield( + [ + pa.id, + pa.post_id, + post&.topic_id, + post&.post_number, + pa.created_at, + pa.updated_at, + pa.deleted_at, + self_or_other(pa.deleted_by_id), + ] + ) + end end def include_post_actions? # Most forums should not have post_action records other than flags and likes, but they are possible in historical oddities. PostAction .where(user_id: @current_user.id) - .where.not(post_action_type_id: PostActionType.flag_types.values + [PostActionType.types[:like]]) + .where.not( + post_action_type_id: PostActionType.flag_types.values + [PostActionType.types[:like]], + ) .exists? end @@ -334,20 +434,24 @@ module Jobs PostAction .with_deleted .where(user_id: @current_user.id) - .where.not(post_action_type_id: PostActionType.flag_types.values + [PostActionType.types[:like]]) + .where.not( + post_action_type_id: PostActionType.flag_types.values + [PostActionType.types[:like]], + ) .order(:created_at) .each do |pa| - yield [ - pa.id, - pa.post_id, - PostActionType.types[pa.post_action_type] || pa.post_action_type, - pa.created_at, - pa.updated_at, - pa.deleted_at, - self_or_other(pa.deleted_by_id), - pa.related_post_id, - ] - end + yield( + [ + pa.id, + pa.post_id, + PostActionType.types[pa.post_action_type] || pa.post_action_type, + pa.created_at, + pa.updated_at, + pa.deleted_at, + self_or_other(pa.deleted_by_id), + pa.related_post_id, + ] + ) + end end def queued_posts_export @@ -358,16 +462,17 @@ module Jobs .where(created_by: @current_user.id) .order(:created_at) .each do |rev| - - yield [ - rev.id, - rev.status, - rev.category_id, - rev.topic_id, - rev.payload['raw'], - MultiJson.dump(rev.payload.slice(*queued_posts_payload_permitted_keys)), - ] - end + yield( + [ + rev.id, + rev.status, + rev.category_id, + rev.topic_id, + rev.payload["raw"], + MultiJson.dump(rev.payload.slice(*queued_posts_payload_permitted_keys)), + ] + ) + end end def visits_export @@ -376,20 +481,15 @@ module Jobs UserVisit .where(user_id: @current_user.id) .order(visited_at: :asc) - .each do |uv| - yield [ - uv.visited_at, - uv.posts_read, - uv.mobile, - uv.time_read, - ] - end + .each { |uv| yield [uv.visited_at, uv.posts_read, uv.mobile, uv.time_read] } end def get_header(entity) - if entity == 'user_list' - header_array = HEADER_ATTRS_FOR['user_list'] + HEADER_ATTRS_FOR['user_stats'] + HEADER_ATTRS_FOR['user_profile'] - header_array.concat(HEADER_ATTRS_FOR['user_sso']) if SiteSetting.enable_discourse_connect + if entity == "user_list" + header_array = + HEADER_ATTRS_FOR["user_list"] + HEADER_ATTRS_FOR["user_stats"] + + HEADER_ATTRS_FOR["user_profile"] + header_array.concat(HEADER_ATTRS_FOR["user_sso"]) if SiteSetting.enable_discourse_connect user_custom_fields = UserField.all if user_custom_fields.present? user_custom_fields.each do |custom_field| @@ -424,9 +524,9 @@ module Jobs if user_id.nil? nil elsif user_id == @current_user.id - 'self' + "self" else - 'other' + "other" end end @@ -434,27 +534,37 @@ module Jobs user_archive_array = [] topic_data = user_archive.topic user_archive = user_archive.as_json - topic_data = Topic.with_deleted.includes(:category).find_by(id: user_archive['topic_id']) if topic_data.nil? + topic_data = + Topic + .with_deleted + .includes(:category) + .find_by(id: user_archive["topic_id"]) if topic_data.nil? return user_archive_array if topic_data.nil? categories = piped_category_name(topic_data.category_id, topic_data.category) - is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no") - url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}" + is_pm = + ( + if topic_data.archetype == "private_message" + I18n.t("csv_export.boolean_yes") + else + I18n.t("csv_export.boolean_no") + end + ) + url = + "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive["post_number"]}" topic_hash = { - "post_raw" => user_archive['raw'], + "post_raw" => user_archive["raw"], "post_cooked" => user_archive["cooked"], "topic_title" => topic_data.title, "categories" => categories, "is_pm" => is_pm, - "url" => url + "url" => url, } user_archive.merge!(topic_hash) - HEADER_ATTRS_FOR['user_archive'].each do |attr| - user_archive_array.push(user_archive[attr]) - end + HEADER_ATTRS_FOR["user_archive"].each { |attr| user_archive_array.push(user_archive[attr]) } user_archive_array end @@ -462,15 +572,15 @@ module Jobs def get_user_archive_profile_fields(user_profile) user_archive_profile = [] - HEADER_ATTRS_FOR['user_archive_profile'].each do |attr| + HEADER_ATTRS_FOR["user_archive_profile"].each do |attr| data = - if attr == 'bio' - user_profile.attributes['bio_raw'] + if attr == "bio" + user_profile.attributes["bio_raw"] else user_profile.attributes[attr] end - user_archive_profile.push(data) + user_archive_profile.push(data) end user_archive_profile @@ -483,30 +593,24 @@ module Jobs # where type = 'ReviewableQueuedPost' and (payload->'old_queued_post_id') IS NULL # # except raw, created_topic_id, created_post_id - %w{ - composer_open_duration_msecs - is_poll - reply_to_post_number - tags - title - typing_duration_msecs - } + %w[composer_open_duration_msecs is_poll reply_to_post_number tags title typing_duration_msecs] end def notify_user(upload, export_title) post = nil if @current_user - post = if upload.persisted? - SystemMessage.create_from_system_user( - @current_user, - :csv_export_succeeded, - download_link: UploadMarkdown.new(upload).attachment_markdown, - export_title: export_title - ) - else - SystemMessage.create_from_system_user(@current_user, :csv_export_failed) - end + post = + if upload.persisted? + SystemMessage.create_from_system_user( + @current_user, + :csv_export_succeeded, + download_link: UploadMarkdown.new(upload).attachment_markdown, + export_title: export_title, + ) + else + SystemMessage.create_from_system_user(@current_user, :csv_export_failed) + end end post diff --git a/app/jobs/regular/feature_topic_users.rb b/app/jobs/regular/feature_topic_users.rb index 84210a6baf..83266c725b 100644 --- a/app/jobs/regular/feature_topic_users.rb +++ b/app/jobs/regular/feature_topic_users.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class FeatureTopicUsers < ::Jobs::Base - def execute(args) topic_id = args[:topic_id] raise Discourse::InvalidParameters.new(:topic_id) unless topic_id.present? @@ -17,7 +15,5 @@ module Jobs topic.feature_topic_users(args) end - end - end diff --git a/app/jobs/regular/generate_topic_thumbnails.rb b/app/jobs/regular/generate_topic_thumbnails.rb index e670e0fe41..2ad13a2d71 100644 --- a/app/jobs/regular/generate_topic_thumbnails.rb +++ b/app/jobs/regular/generate_topic_thumbnails.rb @@ -2,7 +2,7 @@ module Jobs class GenerateTopicThumbnails < ::Jobs::Base - sidekiq_options queue: 'ultra_low' + sidekiq_options queue: "ultra_low" def execute(args) topic_id = args[:topic_id] @@ -13,6 +13,5 @@ module Jobs topic = Topic.find_by(id: topic_id) topic&.generate_thumbnails!(extra_sizes: extra_sizes) end - end end diff --git a/app/jobs/regular/group_pm_alert.rb b/app/jobs/regular/group_pm_alert.rb index 530560e16d..3f562801a9 100644 --- a/app/jobs/regular/group_pm_alert.rb +++ b/app/jobs/regular/group_pm_alert.rb @@ -12,12 +12,10 @@ module Jobs alerter = PostAlerter.new - group.users.where( - "group_users.notification_level = :level", - level: NotificationLevels.all[:tracking] - ).find_each do |u| - alerter.notify_group_summary(u, topic) - end + group + .users + .where("group_users.notification_level = :level", level: NotificationLevels.all[:tracking]) + .find_each { |u| alerter.notify_group_summary(u, topic) } notification_data = { notification_type: Notification.types[:invited_to_private_message], @@ -26,17 +24,18 @@ module Jobs data: { topic_title: topic.title, display_username: user.username, - group_id: group.id - }.to_json + group_id: group.id, + }.to_json, } - group.users.where( - "group_users.notification_level in (:levels) AND user_id != :id", - levels: [NotificationLevels.all[:watching], NotificationLevels.all[:watching_first_post]], - id: user.id - ).find_each do |u| - u.notifications.create!(notification_data) - end + group + .users + .where( + "group_users.notification_level in (:levels) AND user_id != :id", + levels: [NotificationLevels.all[:watching], NotificationLevels.all[:watching_first_post]], + id: user.id, + ) + .find_each { |u| u.notifications.create!(notification_data) } end end end diff --git a/app/jobs/regular/group_pm_update_summary.rb b/app/jobs/regular/group_pm_update_summary.rb index cb6c5a61f1..74f151c77c 100644 --- a/app/jobs/regular/group_pm_update_summary.rb +++ b/app/jobs/regular/group_pm_update_summary.rb @@ -10,13 +10,12 @@ module Jobs alerter = PostAlerter.new - group.users.where( - "group_users.notification_level = :level", - level: NotificationLevels.all[:tracking] - ).find_each do |u| - alerter.notify_group_summary(u, topic, acting_user_id: args[:acting_user_id]) - end - + group + .users + .where("group_users.notification_level = :level", level: NotificationLevels.all[:tracking]) + .find_each do |u| + alerter.notify_group_summary(u, topic, acting_user_id: args[:acting_user_id]) + end end end end diff --git a/app/jobs/regular/group_smtp_email.rb b/app/jobs/regular/group_smtp_email.rb index b0fc4a890e..6970c673ff 100644 --- a/app/jobs/regular/group_smtp_email.rb +++ b/app/jobs/regular/group_smtp_email.rb @@ -4,7 +4,7 @@ module Jobs class GroupSmtpEmail < ::Jobs::Base include Skippable - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" sidekiq_retry_in do |count, exception| # retry in an hour when SMTP server is busy @@ -25,9 +25,7 @@ module Jobs recipient_user = User.find_by_email(email, primary: true) post = Post.find_by(id: args[:post_id]) - if post.blank? - return skip(email, nil, recipient_user, :group_smtp_post_deleted) - end + return skip(email, nil, recipient_user, :group_smtp_post_deleted) if post.blank? group = Group.find_by(id: args[:group_id]) return if group.blank? @@ -40,9 +38,8 @@ module Jobs return skip(email, post, recipient_user, :group_smtp_topic_deleted) end - cc_addresses = args[:cc_emails].filter do |address| - EmailAddressValidator.valid_value?(address) - end + cc_addresses = + args[:cc_emails].filter { |address| EmailAddressValidator.valid_value?(address) } # Mask the email addresses of non-staged users so # they are not revealed unnecessarily when we are sending @@ -63,22 +60,29 @@ module Jobs # for example in cases where we are creating a new topic to reply to another # group PM and we need to send the participants the group OP email. if post.is_first_post? && group.imap_enabled - ImapSyncLog.warn("Aborting SMTP email for post #{post.id} in topic #{post.topic_id} to #{email}, the post is the OP and should not send an email.", group) + ImapSyncLog.warn( + "Aborting SMTP email for post #{post.id} in topic #{post.topic_id} to #{email}, the post is the OP and should not send an email.", + group, + ) return end - ImapSyncLog.debug("Sending SMTP email for post #{post.id} in topic #{post.topic_id} to #{email}.", group) + ImapSyncLog.debug( + "Sending SMTP email for post #{post.id} in topic #{post.topic_id} to #{email}.", + group, + ) # The EmailLog record created by the sender will have the raw email # stored, the group smtp ID, and any cc addresses recorded for later # cross referencing. - message = GroupSmtpMailer.send_mail( - group, - email, - post, - cc_addresses: cc_addresses, - bcc_addresses: bcc_addresses - ) + message = + GroupSmtpMailer.send_mail( + group, + email, + post, + cc_addresses: cc_addresses, + bcc_addresses: bcc_addresses, + ) Email::Sender.new(message, :group_smtp, recipient_user).send # Create an incoming email record to avoid importing again from IMAP @@ -95,12 +99,12 @@ module Jobs to_addresses: message.to, cc_addresses: message.cc, from_address: message.from, - created_via: IncomingEmail.created_via_types[:group_smtp] + created_via: IncomingEmail.created_via_types[:group_smtp], ) end def quit_email_early? - SiteSetting.disable_emails == 'yes' || !SiteSetting.enable_smtp + SiteSetting.disable_emails == "yes" || !SiteSetting.enable_smtp end def skip(email, post, recipient_user, reason) @@ -109,7 +113,7 @@ module Jobs to_address: email, user_id: recipient_user&.id, post_id: post&.id, - reason_type: SkippedEmailLog.reason_types[reason] + reason_type: SkippedEmailLog.reason_types[reason], ) end end diff --git a/app/jobs/regular/invite_email.rb b/app/jobs/regular/invite_email.rb index 5d0b0e68df..ad33a9ee5c 100644 --- a/app/jobs/regular/invite_email.rb +++ b/app/jobs/regular/invite_email.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module Jobs - # Asynchronously send an email class InviteEmail < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:invite_id) unless args[:invite_id].present? diff --git a/app/jobs/regular/invite_password_instructions_email.rb b/app/jobs/regular/invite_password_instructions_email.rb index 14ccec97c4..8989772ba8 100644 --- a/app/jobs/regular/invite_password_instructions_email.rb +++ b/app/jobs/regular/invite_password_instructions_email.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true module Jobs - # Asynchronously send an email class InvitePasswordInstructionsEmail < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:username) unless args[:username].present? user = User.find_by_username_or_email(args[:username]) message = InviteMailer.send_password_instructions(user) Email::Sender.new(message, :invite_password_instructions).send end - end - end diff --git a/app/jobs/regular/make_embedded_topic_visible.rb b/app/jobs/regular/make_embedded_topic_visible.rb index c3c4121b5f..0d8e90a618 100644 --- a/app/jobs/regular/make_embedded_topic_visible.rb +++ b/app/jobs/regular/make_embedded_topic_visible.rb @@ -2,14 +2,12 @@ module Jobs class MakeEmbeddedTopicVisible < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:topic_id) if args[:topic_id].blank? if topic = Topic.find_by(id: args[:topic_id]) - topic.update_status('visible', true, topic.user) + topic.update_status("visible", true, topic.user) end end - end end diff --git a/app/jobs/regular/merge_user.rb b/app/jobs/regular/merge_user.rb index e759b08d18..abfb3d10ab 100644 --- a/app/jobs/regular/merge_user.rb +++ b/app/jobs/regular/merge_user.rb @@ -2,7 +2,6 @@ module Jobs class MergeUser < ::Jobs::Base - def execute(args) target_user_id = args[:target_user_id] current_user_id = args[:current_user_id] @@ -15,9 +14,16 @@ module Jobs if user = UserMerger.new(user, target_user, current_user).merge! user_json = AdminDetailedUserSerializer.new(user, serializer_opts).as_json - ::MessageBus.publish '/merge_user', { success: 'OK' }.merge(merged: true, user: user_json), user_ids: [current_user.id] + ::MessageBus.publish "/merge_user", + { success: "OK" }.merge(merged: true, user: user_json), + user_ids: [current_user.id] else - ::MessageBus.publish '/merge_user', { failed: 'FAILED' }.merge(user: AdminDetailedUserSerializer.new(@user, serializer_opts).as_json), user_ids: [current_user.id] + ::MessageBus.publish "/merge_user", + { failed: "FAILED" }.merge( + user: + AdminDetailedUserSerializer.new(@user, serializer_opts).as_json, + ), + user_ids: [current_user.id] end end end diff --git a/app/jobs/regular/notify_category_change.rb b/app/jobs/regular/notify_category_change.rb index fbad7582e4..f1f6c020f0 100644 --- a/app/jobs/regular/notify_category_change.rb +++ b/app/jobs/regular/notify_category_change.rb @@ -7,7 +7,11 @@ module Jobs if post&.topic&.visible? post_alerter = PostAlerter.new - post_alerter.notify_post_users(post, User.where(id: args[:notified_user_ids]), include_tag_watchers: false) + post_alerter.notify_post_users( + post, + User.where(id: args[:notified_user_ids]), + include_tag_watchers: false, + ) post_alerter.notify_first_post_watchers(post, post_alerter.category_watchers(post.topic)) end end diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 03a431ab3f..467af7cff7 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true module Jobs - class NotifyMailingListSubscribers < ::Jobs::Base include Skippable RETRY_TIMES = [5.minute, 15.minute, 30.minute, 45.minute, 90.minute, 180.minute, 300.minute] - sidekiq_options queue: 'low' + sidekiq_options queue: "low" sidekiq_options retry: RETRY_TIMES.size @@ -28,72 +27,97 @@ module Jobs post_id = args[:post_id] post = post_id ? Post.with_deleted.find_by(id: post_id) : nil - return if !post || post.trashed? || post.user_deleted? || - !post.topic || post.raw.blank? || post.topic.private_message? + if !post || post.trashed? || post.user_deleted? || !post.topic || post.raw.blank? || + post.topic.private_message? + return + end users = - User.activated.not_silenced.not_suspended.real - .joins(:user_option) - .where('user_options.mailing_list_mode AND user_options.mailing_list_mode_frequency > 0') - .where('NOT EXISTS ( + User + .activated + .not_silenced + .not_suspended + .real + .joins(:user_option) + .where("user_options.mailing_list_mode AND user_options.mailing_list_mode_frequency > 0") + .where( + "NOT EXISTS ( SELECT 1 FROM muted_users mu WHERE mu.muted_user_id = ? AND mu.user_id = users.id - )', post.user_id) - .where('NOT EXISTS ( + )", + post.user_id, + ) + .where( + "NOT EXISTS ( SELECT 1 FROM ignored_users iu WHERE iu.ignored_user_id = ? AND iu.user_id = users.id - )', post.user_id) - .where('NOT EXISTS ( + )", + post.user_id, + ) + .where( + "NOT EXISTS ( SELECT 1 FROM topic_users tu WHERE tu.topic_id = ? AND tu.user_id = users.id AND tu.notification_level = ? - )', post.topic_id, TopicUser.notification_levels[:muted]) - .where('NOT EXISTS ( + )", + post.topic_id, + TopicUser.notification_levels[:muted], + ) + .where( + "NOT EXISTS ( SELECT 1 FROM category_users cu WHERE cu.category_id = ? AND cu.user_id = users.id AND cu.notification_level = ? - )', post.topic.category_id, CategoryUser.notification_levels[:muted]) + )", + post.topic.category_id, + CategoryUser.notification_levels[:muted], + ) if SiteSetting.tagging_enabled? - users = users.where('NOT EXISTS ( + users = + users.where( + "NOT EXISTS ( SELECT 1 FROM tag_users tu WHERE tu.tag_id in (:tag_ids) AND tu.user_id = users.id AND tu.notification_level = :muted - )', tag_ids: post.topic.tag_ids, muted: TagUser.notification_levels[:muted]) + )", + tag_ids: post.topic.tag_ids, + muted: TagUser.notification_levels[:muted], + ) end - if SiteSetting.must_approve_users - users = users.where(approved: true) - end + users = users.where(approved: true) if SiteSetting.must_approve_users - if SiteSetting.mute_all_categories_by_default - users = users.watching_topic(post.topic) - end + users = users.watching_topic(post.topic) if SiteSetting.mute_all_categories_by_default DiscourseEvent.trigger(:notify_mailing_list_subscribers, users, post) users.find_each do |user| if Guardian.new(user).can_see?(post) if EmailLog.reached_max_emails?(user) - skip(user.email, user.id, post.id, - SkippedEmailLog.reason_types[:exceeded_emails_limit] - ) + skip(user.email, user.id, post.id, SkippedEmailLog.reason_types[:exceeded_emails_limit]) next end if user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold - skip(user.email, user.id, post.id, - SkippedEmailLog.reason_types[:exceeded_bounces_limit] + skip( + user.email, + user.id, + post.id, + SkippedEmailLog.reason_types[:exceeded_bounces_limit], ) next end if (user.id == post.user_id) && (user.user_option.mailing_list_mode_frequency == 2) - skip(user.email, user.id, post.id, - SkippedEmailLog.reason_types[:mailing_list_no_echo_mode] + skip( + user.email, + user.id, + post.id, + SkippedEmailLog.reason_types[:mailing_list_no_echo_mode], ) next @@ -106,20 +130,27 @@ module Jobs end end rescue => e - Discourse.handle_job_exception(e, error_context(args, "Sending post to mailing list subscribers", user_id: user.id, user_email: user.email)) + Discourse.handle_job_exception( + e, + error_context( + args, + "Sending post to mailing list subscribers", + user_id: user.id, + user_email: user.email, + ), + ) end end end - end def skip(to_address, user_id, post_id, reason_type) create_skipped_email_log( - email_type: 'mailing_list', + email_type: "mailing_list", to_address: to_address, user_id: user_id, post_id: post_id, - reason_type: reason_type + reason_type: reason_type, ) end end diff --git a/app/jobs/regular/notify_moved_posts.rb b/app/jobs/regular/notify_moved_posts.rb index f02feec7a6..d97b52f216 100644 --- a/app/jobs/regular/notify_moved_posts.rb +++ b/app/jobs/regular/notify_moved_posts.rb @@ -1,33 +1,33 @@ # frozen_string_literal: true module Jobs - class NotifyMovedPosts < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:post_ids) if args[:post_ids].blank? raise Discourse::InvalidParameters.new(:moved_by_id) if args[:moved_by_id].blank? # Make sure we don't notify the same user twice (in case multiple posts were moved at once.) users_notified = Set.new - posts = Post.where(id: args[:post_ids]).where('user_id <> ?', args[:moved_by_id]).includes(:user, :topic) + posts = + Post + .where(id: args[:post_ids]) + .where("user_id <> ?", args[:moved_by_id]) + .includes(:user, :topic) if posts.present? moved_by = User.find_by(id: args[:moved_by_id]) posts.each do |p| unless users_notified.include?(p.user_id) - p.user.notifications.create(notification_type: Notification.types[:moved_post], - topic_id: p.topic_id, - post_number: p.post_number, - data: { topic_title: p.topic.title, - display_username: moved_by.username }.to_json) + p.user.notifications.create( + notification_type: Notification.types[:moved_post], + topic_id: p.topic_id, + post_number: p.post_number, + data: { topic_title: p.topic.title, display_username: moved_by.username }.to_json, + ) users_notified << p.user_id end end end - end - end - end diff --git a/app/jobs/regular/notify_post_revision.rb b/app/jobs/regular/notify_post_revision.rb index f4b0262e06..f37111bc25 100644 --- a/app/jobs/regular/notify_post_revision.rb +++ b/app/jobs/regular/notify_post_revision.rb @@ -9,18 +9,20 @@ module Jobs return if post_revision.nil? ActiveRecord::Base.transaction do - User.where(id: args[:user_ids]).find_each do |user| - next if post_revision.hidden && !user.staff? + User + .where(id: args[:user_ids]) + .find_each do |user| + next if post_revision.hidden && !user.staff? - PostActionNotifier.alerter.create_notification( - user, - Notification.types[:edited], - post_revision.post, - display_username: post_revision.user.username, - acting_user_id: post_revision&.user_id, - revision_number: post_revision.number - ) - end + PostActionNotifier.alerter.create_notification( + user, + Notification.types[:edited], + post_revision.post, + display_username: post_revision.user.username, + acting_user_id: post_revision&.user_id, + revision_number: post_revision.number, + ) + end end end end diff --git a/app/jobs/regular/notify_reviewable.rb b/app/jobs/regular/notify_reviewable.rb index 5b71ac557e..a59adc4ac1 100644 --- a/app/jobs/regular/notify_reviewable.rb +++ b/app/jobs/regular/notify_reviewable.rb @@ -11,16 +11,15 @@ class Jobs::NotifyReviewable < ::Jobs::Base all_updates = Hash.new { |h, k| h[k] = {} } if args[:updated_reviewable_ids].present? - Reviewable.where(id: args[:updated_reviewable_ids]).each do |r| - payload = { - last_performing_username: args[:performing_username], - status: r.status - } + Reviewable + .where(id: args[:updated_reviewable_ids]) + .each do |r| + payload = { last_performing_username: args[:performing_username], status: r.status } - all_updates[:admins][r.id] = payload - all_updates[:moderators][r.id] = payload if r.reviewable_by_moderator? - all_updates[r.reviewable_by_group_id][r.id] = payload if r.reviewable_by_group_id - end + all_updates[:admins][r.id] = payload + all_updates[:moderators][r.id] = payload if r.reviewable_by_moderator? + all_updates[r.reviewable_by_group_id][r.id] = payload if r.reviewable_by_group_id + end end counts = Hash.new(0) @@ -38,10 +37,7 @@ class Jobs::NotifyReviewable < ::Jobs::Base updates: all_updates[:admins], ) else - notify_users( - User.real.admins, - all_updates[:admins] - ) + notify_users(User.real.admins, all_updates[:admins]) end if reviewable.reviewable_by_moderator? @@ -54,7 +50,7 @@ class Jobs::NotifyReviewable < ::Jobs::Base else notify_users( User.real.moderators.where("id NOT IN (?)", @contacted), - all_updates[:moderators] + all_updates[:moderators], ) end end diff --git a/app/jobs/regular/notify_tag_change.rb b/app/jobs/regular/notify_tag_change.rb index 0fcbbc621e..0038dba193 100644 --- a/app/jobs/regular/notify_tag_change.rb +++ b/app/jobs/regular/notify_tag_change.rb @@ -9,10 +9,12 @@ module Jobs if post&.topic&.visible? post_alerter = PostAlerter.new - post_alerter.notify_post_users(post, User.where(id: args[:notified_user_ids]), + post_alerter.notify_post_users( + post, + User.where(id: args[:notified_user_ids]), group_ids: all_tags_in_hidden_groups?(args) ? tag_group_ids(args) : nil, include_topic_watchers: !post.topic.private_message?, - include_category_watchers: false + include_category_watchers: false, ) post_alerter.notify_first_post_watchers(post, post_alerter.tag_watchers(post.topic)) end @@ -32,7 +34,10 @@ module Jobs end def tag_group_ids(args) - Tag.where(name: args[:diff_tags]).joins(tag_groups: :tag_group_permissions).pluck("tag_group_permissions.group_id") + Tag + .where(name: args[:diff_tags]) + .joins(tag_groups: :tag_group_permissions) + .pluck("tag_group_permissions.group_id") end end end diff --git a/app/jobs/regular/open_topic.rb b/app/jobs/regular/open_topic.rb index f1e9fe0d2f..8726d2c09f 100644 --- a/app/jobs/regular/open_topic.rb +++ b/app/jobs/regular/open_topic.rb @@ -21,13 +21,12 @@ module Jobs topic.set_or_create_timer( TopicTimer.types[:open], SiteSetting.num_hours_to_close_topic, - by_user: Discourse.system_user + by_user: Discourse.system_user, ) else - # autoclosed, false is just another way of saying open. # this handles deleting the topic timer as well, see TopicStatusUpdater - topic.update_status('autoclosed', false, user) + topic.update_status("autoclosed", false, user) end topic.inherit_auto_close_from_category(timer_type: :close) diff --git a/app/jobs/regular/post_alert.rb b/app/jobs/regular/post_alert.rb index dc6b8b1440..b3aa1c1f00 100644 --- a/app/jobs/regular/post_alert.rb +++ b/app/jobs/regular/post_alert.rb @@ -2,7 +2,6 @@ module Jobs class PostAlert < ::Jobs::Base - def execute(args) post = Post.find_by(id: args[:post_id]) if post&.topic && post.raw.present? @@ -11,6 +10,5 @@ module Jobs PostAlerter.new(opts).after_save_post(post, new_record) end end - end end diff --git a/app/jobs/regular/post_update_topic_tracking_state.rb b/app/jobs/regular/post_update_topic_tracking_state.rb index c3d4610f80..da3a11268d 100644 --- a/app/jobs/regular/post_update_topic_tracking_state.rb +++ b/app/jobs/regular/post_update_topic_tracking_state.rb @@ -2,7 +2,6 @@ module Jobs class PostUpdateTopicTrackingState < ::Jobs::Base - def execute(args) post = Post.find_by(id: args[:post_id]) return if !post&.topic @@ -10,15 +9,9 @@ module Jobs topic = post.topic if topic.private_message? - if post.post_number > 1 - PrivateMessageTopicTrackingState.publish_unread(post) - end + PrivateMessageTopicTrackingState.publish_unread(post) if post.post_number > 1 - TopicGroup.new_message_update( - topic.last_poster, - topic.id, - post.post_number - ) + TopicGroup.new_message_update(topic.last_poster, topic.id, post.post_number) else TopicTrackingState.publish_unmuted(post.topic) if post.post_number > 1 @@ -28,6 +21,5 @@ module Jobs TopicTrackingState.publish_latest(post.topic, post.whisper?) end end - end end diff --git a/app/jobs/regular/process_bulk_invite_emails.rb b/app/jobs/regular/process_bulk_invite_emails.rb index f8b8a13846..26152b8a87 100644 --- a/app/jobs/regular/process_bulk_invite_emails.rb +++ b/app/jobs/regular/process_bulk_invite_emails.rb @@ -1,17 +1,19 @@ # frozen_string_literal: true module Jobs - class ProcessBulkInviteEmails < ::Jobs::Base - def execute(args) - pending_invite_ids = Invite.where(emailed_status: Invite.emailed_status_types[:bulk_pending]).limit(Invite::BULK_INVITE_EMAIL_LIMIT).pluck(:id) + pending_invite_ids = + Invite + .where(emailed_status: Invite.emailed_status_types[:bulk_pending]) + .limit(Invite::BULK_INVITE_EMAIL_LIMIT) + .pluck(:id) if pending_invite_ids.length > 0 - Invite.where(id: pending_invite_ids).update_all(emailed_status: Invite.emailed_status_types[:sending]) - pending_invite_ids.each do |invite_id| - ::Jobs.enqueue(:invite_email, invite_id: invite_id) - end + Invite.where(id: pending_invite_ids).update_all( + emailed_status: Invite.emailed_status_types[:sending], + ) + pending_invite_ids.each { |invite_id| ::Jobs.enqueue(:invite_email, invite_id: invite_id) } ::Jobs.enqueue_in(1.minute, :process_bulk_invite_emails) end end diff --git a/app/jobs/regular/process_email.rb b/app/jobs/regular/process_email.rb index 2ac10eddd1..0bad7eade2 100644 --- a/app/jobs/regular/process_email.rb +++ b/app/jobs/regular/process_email.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class ProcessEmail < ::Jobs::Base sidekiq_options retry: 3 @@ -9,14 +8,14 @@ module Jobs Email::Processor.process!( args[:mail], retry_on_rate_limit: args[:retry_on_rate_limit] || false, - source: args[:source]&.to_sym + source: args[:source]&.to_sym, ) end sidekiq_retries_exhausted do |msg| - Rails.logger.warn("Incoming email could not be processed after 3 retries.\n\n#{msg["args"][:mail]}") + Rails.logger.warn( + "Incoming email could not be processed after 3 retries.\n\n#{msg["args"][:mail]}", + ) end - end - end diff --git a/app/jobs/regular/process_post.rb b/app/jobs/regular/process_post.rb index 60f444fe98..976d2f75bb 100644 --- a/app/jobs/regular/process_post.rb +++ b/app/jobs/regular/process_post.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require 'image_sizer' +require "image_sizer" module Jobs - class ProcessPost < ::Jobs::Base - def execute(args) DistributedMutex.synchronize("process_post_#{args[:post_id]}", validity: 10.minutes) do post = Post.find_by(id: args[:post_id]) @@ -19,7 +17,11 @@ module Jobs cooking_options = args[:cooking_options] || {} cooking_options[:topic_id] = post.topic_id recooked = post.cook(post.raw, cooking_options.symbolize_keys) - post.update_columns(cooked: recooked, baked_at: Time.zone.now, baked_version: Post::BAKED_VERSION) + post.update_columns( + cooked: recooked, + baked_at: Time.zone.now, + baked_version: Post::BAKED_VERSION, + ) end cp = CookedPostProcessor.new(post, args) @@ -31,7 +33,9 @@ module Jobs if cooked != (recooked || orig_cooked) if orig_cooked.present? && cooked.blank? # TODO stop/restart the worker if needed, let's gather a few here first - Rails.logger.warn("Cooked post processor in FATAL state, bypassing. You need to urgently restart sidekiq\norig: #{orig_cooked}\nrecooked: #{recooked}\ncooked: #{cooked}\npost id: #{post.id}") + Rails.logger.warn( + "Cooked post processor in FATAL state, bypassing. You need to urgently restart sidekiq\norig: #{orig_cooked}\nrecooked: #{recooked}\ncooked: #{cooked}\npost id: #{post.id}", + ) else post.update_column(:cooked, cp.html) post.topic.update_excerpt(post.excerpt_for_topic) if post.is_first_post? @@ -50,7 +54,7 @@ module Jobs Discourse.system_user, post, :inappropriate, - reason: :watched_word + reason: :watched_word, ) end end @@ -68,5 +72,4 @@ module Jobs Jobs.enqueue(:pull_hotlinked_images, post_id: post.id) end end - end diff --git a/app/jobs/regular/process_sns_notification.rb b/app/jobs/regular/process_sns_notification.rb index 67e9b0e327..09e965a6ea 100644 --- a/app/jobs/regular/process_sns_notification.rb +++ b/app/jobs/regular/process_sns_notification.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class ProcessSnsNotification < ::Jobs::Base sidekiq_options retry: false @@ -10,11 +9,12 @@ module Jobs return unless json = args[:json].presence return unless message = json["Message"].presence - message = begin - JSON.parse(message) - rescue JSON::ParserError - nil - end + message = + begin + JSON.parse(message) + rescue JSON::ParserError + nil + end return unless message && message["notificationType"] == "Bounce" return unless message_id = message.dig("mail", "messageId").presence @@ -23,21 +23,29 @@ module Jobs require "aws-sdk-sns" return unless Aws::SNS::MessageVerifier.new.authentic?(raw) - message.dig("bounce", "bouncedRecipients").each do |r| - if email_log = EmailLog.order("created_at DESC").where(to_address: r["emailAddress"]).first - email_log.update_columns(bounced: true, bounce_error_code: r["status"]) + message + .dig("bounce", "bouncedRecipients") + .each do |r| + if email_log = + EmailLog.order("created_at DESC").where(to_address: r["emailAddress"]).first + email_log.update_columns(bounced: true, bounce_error_code: r["status"]) - if email_log.user&.email.present? - if email_log.user.user_stat.bounce_score.to_s.start_with?("4.") || bounce_type == "Transient" - Email::Receiver.update_bounce_score(email_log.user.email, SiteSetting.soft_bounce_score) - else - Email::Receiver.update_bounce_score(email_log.user.email, SiteSetting.hard_bounce_score) + if email_log.user&.email.present? + if email_log.user.user_stat.bounce_score.to_s.start_with?("4.") || + bounce_type == "Transient" + Email::Receiver.update_bounce_score( + email_log.user.email, + SiteSetting.soft_bounce_score, + ) + else + Email::Receiver.update_bounce_score( + email_log.user.email, + SiteSetting.hard_bounce_score, + ) + end end end end - end end - end - end diff --git a/app/jobs/regular/publish_group_membership_updates.rb b/app/jobs/regular/publish_group_membership_updates.rb index 6b93e87a0b..5a5727717b 100644 --- a/app/jobs/regular/publish_group_membership_updates.rb +++ b/app/jobs/regular/publish_group_membership_updates.rb @@ -11,13 +11,16 @@ module Jobs added_members = args[:type] == Group::AUTO_GROUPS_ADD - User.human_users.where(id: args[:user_ids]).each do |user| - if added_members - group.trigger_user_added_event(user, group.automatic?) - else - group.trigger_user_removed_event(user) + User + .human_users + .where(id: args[:user_ids]) + .each do |user| + if added_members + group.trigger_user_added_event(user, group.automatic?) + else + group.trigger_user_removed_event(user) + end end - end end end end diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 7c1accdb89..43a64f519e 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true module Jobs - class PullHotlinkedImages < ::Jobs::Base - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def initialize @max_size = SiteSetting.max_image_size_kb.kilobytes @@ -23,8 +22,12 @@ module Jobs changed_hotlink_records = false extract_images_from(post.cooked).each do |node| - download_src = original_src = node['src'] || node[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || node['href'] - download_src = "#{SiteSetting.force_https ? "https" : "http"}:#{original_src}" if original_src.start_with?("//") + download_src = + original_src = node["src"] || node[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || node["href"] + download_src = + "#{SiteSetting.force_https ? "https" : "http"}:#{original_src}" if original_src.start_with?( + "//", + ) normalized_src = normalize_src(download_src) next if !should_download_image?(download_src, post) @@ -32,10 +35,8 @@ module Jobs hotlink_record = hotlinked_map[normalized_src] if hotlink_record.nil? - hotlinked_map[normalized_src] = hotlink_record = PostHotlinkedMedia.new( - post: post, - url: normalized_src - ) + hotlinked_map[normalized_src] = hotlink_record = + PostHotlinkedMedia.new(post: post, url: normalized_src) begin hotlink_record.upload = attempt_download(download_src, post.user_id) hotlink_record.status = :downloaded @@ -54,13 +55,17 @@ module Jobs end rescue => e raise e if Rails.env.test? - log(:error, "Failed to pull hotlinked image (#{download_src}) post: #{@post_id}\n" + e.message + "\n" + e.backtrace.join("\n")) + log( + :error, + "Failed to pull hotlinked image (#{download_src}) post: #{@post_id}\n" + e.message + + "\n" + e.backtrace.join("\n"), + ) end if changed_hotlink_records post.trigger_post_process( bypass_bump: true, - skip_pull_hotlinked_images: true # Avoid an infinite loop of job scheduling + skip_pull_hotlinked_images: true, # Avoid an infinite loop of job scheduling ) end @@ -81,14 +86,15 @@ module Jobs Rails.logger.warn("Verbose Upload Logging: Downloading hotlinked image from #{src}") end - downloaded = FileHelper.download( - src, - max_file_size: @max_size, - retain_on_max_file_size_exceeded: true, - tmp_file_name: "discourse-hotlinked", - follow_redirect: true, - read_timeout: 15 - ) + downloaded = + FileHelper.download( + src, + max_file_size: @max_size, + retain_on_max_file_size_exceeded: true, + tmp_file_name: "discourse-hotlinked", + follow_redirect: true, + read_timeout: 15, + ) rescue => e if SiteSetting.verbose_upload_logging Rails.logger.warn("Verbose Upload Logging: Error '#{e.message}' while downloading #{src}") @@ -103,9 +109,12 @@ module Jobs downloaded end - class ImageTooLargeError < StandardError; end - class ImageBrokenError < StandardError; end - class UploadCreateError < StandardError; end + class ImageTooLargeError < StandardError + end + class ImageBrokenError < StandardError + end + class UploadCreateError < StandardError + end def attempt_download(src, user_id) # secure-uploads endpoint prevents anonymous downloads, so we @@ -123,31 +132,32 @@ module Jobs if upload.persisted? upload else - log(:info, "Failed to persist downloaded hotlinked image for post: #{@post_id}: #{src} - #{upload.errors.full_messages.join("\n")}") + log( + :info, + "Failed to persist downloaded hotlinked image for post: #{@post_id}: #{src} - #{upload.errors.full_messages.join("\n")}", + ) raise UploadCreateError end end def extract_images_from(html) - doc = Nokogiri::HTML5::fragment(html) + doc = Nokogiri::HTML5.fragment(html) doc.css("img[src], [#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}], a.lightbox[href]") - - doc.css("img.avatar") - - doc.css(".lightbox img[src]") + doc.css("img.avatar") - doc.css(".lightbox img[src]") end def should_download_image?(src, post = nil) # make sure we actually have a url return false unless src.present? - local_bases = [ - Discourse.base_url, - Discourse.asset_host, - SiteSetting.external_emoji_url.presence - ].compact.map { |s| normalize_src(s) } + local_bases = + [Discourse.base_url, Discourse.asset_host, SiteSetting.external_emoji_url.presence].compact + .map { |s| normalize_src(s) } - if Discourse.store.has_been_uploaded?(src) || normalize_src(src).start_with?(*local_bases) || src =~ /\A\/[^\/]/i - return false if !(src =~ /\/uploads\// || Upload.secure_uploads_url?(src)) + if Discourse.store.has_been_uploaded?(src) || normalize_src(src).start_with?(*local_bases) || + src =~ %r{\A/[^/]}i + return false if !(src =~ %r{/uploads/} || Upload.secure_uploads_url?(src)) # Someone could hotlink a file from a different site on the same CDN, # so check whether we have it in this database @@ -182,7 +192,7 @@ module Jobs def log(log_level, message) Rails.logger.public_send( log_level, - "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end @@ -202,19 +212,26 @@ module Jobs # log the site setting change reason = I18n.t("disable_remote_images_download_reason") staff_action_logger = StaffActionLogger.new(Discourse.system_user) - staff_action_logger.log_site_setting_change("download_remote_images_to_local", true, false, details: reason) + staff_action_logger.log_site_setting_change( + "download_remote_images_to_local", + true, + false, + details: reason, + ) # also send a private message to the site contact user notify_about_low_disk_space notify_about_low_disk_space end def notify_about_low_disk_space - SystemMessage.create_from_system_user(Discourse.site_contact_user, :download_remote_images_disabled) + SystemMessage.create_from_system_user( + Discourse.site_contact_user, + :download_remote_images_disabled, + ) end def available_disk_space 100 - DiskSpace.percent_free("#{Rails.root}/public/uploads") end end - end diff --git a/app/jobs/regular/pull_user_profile_hotlinked_images.rb b/app/jobs/regular/pull_user_profile_hotlinked_images.rb index 22c7b21e9d..89d0f91086 100644 --- a/app/jobs/regular/pull_user_profile_hotlinked_images.rb +++ b/app/jobs/regular/pull_user_profile_hotlinked_images.rb @@ -14,14 +14,20 @@ module Jobs downloaded_images = {} extract_images_from(user_profile.bio_cooked).each do |node| - download_src = original_src = node['src'] || node['href'] - download_src = "#{SiteSetting.force_https ? "https" : "http"}:#{original_src}" if original_src.start_with?("//") + download_src = original_src = node["src"] || node["href"] + download_src = + "#{SiteSetting.force_https ? "https" : "http"}:#{original_src}" if original_src.start_with?( + "//", + ) normalized_src = normalize_src(download_src) next if !should_download_image?(download_src) begin - already_attempted_download = downloaded_images.include?(normalized_src) || large_image_urls.include?(normalized_src) || broken_image_urls.include?(normalized_src) + already_attempted_download = + downloaded_images.include?(normalized_src) || + large_image_urls.include?(normalized_src) || + broken_image_urls.include?(normalized_src) if !already_attempted_download downloaded_images[normalized_src] = attempt_download(download_src, @user_id) end @@ -32,13 +38,18 @@ module Jobs end rescue => e raise e if Rails.env.test? - log(:error, "Failed to pull hotlinked image (#{download_src}) user: #{@user_id}\n" + e.message + "\n" + e.backtrace.join("\n")) + log( + :error, + "Failed to pull hotlinked image (#{download_src}) user: #{@user_id}\n" + e.message + + "\n" + e.backtrace.join("\n"), + ) end - user_profile.bio_raw = InlineUploads.replace_hotlinked_image_urls(raw: user_profile.bio_raw) do |match_src| - normalized_match_src = PostHotlinkedMedia.normalize_src(match_src) - downloaded_images[normalized_match_src] - end + user_profile.bio_raw = + InlineUploads.replace_hotlinked_image_urls(raw: user_profile.bio_raw) do |match_src| + normalized_match_src = PostHotlinkedMedia.normalize_src(match_src) + downloaded_images[normalized_match_src] + end user_profile.skip_pull_hotlinked_image = true user_profile.save! diff --git a/app/jobs/regular/push_notification.rb b/app/jobs/regular/push_notification.rb index 16026323e7..1459bebc2b 100644 --- a/app/jobs/regular/push_notification.rb +++ b/app/jobs/regular/push_notification.rb @@ -4,7 +4,9 @@ module Jobs class PushNotification < ::Jobs::Base def execute(args) notification = args["payload"] - notification["url"] = UrlHelper.absolute_without_cdn(Discourse.base_path + notification["post_url"]) + notification["url"] = UrlHelper.absolute_without_cdn( + Discourse.base_path + notification["post_url"], + ) notification.delete("post_url") payload = { @@ -15,24 +17,30 @@ module Jobs } clients = args["clients"] - clients.group_by { |r| r[1] }.each do |push_url, group| - notifications = group.map do |client_id, _| - notification.merge(client_id: client_id) + clients + .group_by { |r| r[1] } + .each do |push_url, group| + notifications = group.map { |client_id, _| notification.merge(client_id: client_id) } + + next unless push_url.present? + + result = + Excon.post( + push_url, + body: payload.merge(notifications: notifications).to_json, + headers: { + "Content-Type" => "application/json", + "Accept" => "application/json", + }, + ) + + if result.status != 200 + # we failed to push a notification ... log it + Rails.logger.warn( + "Failed to push a notification to #{push_url} Status: #{result.status}: #{result.status_line}", + ) + end end - - next unless push_url.present? - - result = Excon.post(push_url, - body: payload.merge(notifications: notifications).to_json, - headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' } - ) - - if result.status != 200 - # we failed to push a notification ... log it - Rails.logger.warn("Failed to push a notification to #{push_url} Status: #{result.status}: #{result.status_line}") - end - end - end end end diff --git a/app/jobs/regular/refresh_users_reviewable_counts.rb b/app/jobs/regular/refresh_users_reviewable_counts.rb index b16f72ef64..10fa49467f 100644 --- a/app/jobs/regular/refresh_users_reviewable_counts.rb +++ b/app/jobs/regular/refresh_users_reviewable_counts.rb @@ -4,8 +4,8 @@ class Jobs::RefreshUsersReviewableCounts < ::Jobs::Base def execute(args) group_ids = args[:group_ids] return if group_ids.blank? - User.where( - id: GroupUser.where(group_id: group_ids).distinct.pluck(:user_id) - ).each(&:publish_reviewable_counts) + User.where(id: GroupUser.where(group_id: group_ids).distinct.pluck(:user_id)).each( + &:publish_reviewable_counts + ) end end diff --git a/app/jobs/regular/remove_banner.rb b/app/jobs/regular/remove_banner.rb index 880c8696c9..a8c1405a68 100644 --- a/app/jobs/regular/remove_banner.rb +++ b/app/jobs/regular/remove_banner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class RemoveBanner < ::Jobs::Base - def execute(args) topic_id = args[:topic_id] @@ -12,7 +10,5 @@ module Jobs topic = Topic.find_by(id: topic_id) topic.remove_banner!(Discourse.system_user) if topic.present? end - end - end diff --git a/app/jobs/regular/retrieve_topic.rb b/app/jobs/regular/retrieve_topic.rb index ac3f0aacad..5cdcd3496b 100644 --- a/app/jobs/regular/retrieve_topic.rb +++ b/app/jobs/regular/retrieve_topic.rb @@ -1,20 +1,18 @@ # frozen_string_literal: true module Jobs - # Asynchronously retrieve a topic from an embedded site class RetrieveTopic < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:embed_url) unless args[:embed_url].present? user = nil - if args[:user_id] - user = User.find_by(id: args[:user_id]) - end - TopicRetriever.new(args[:embed_url], author_username: args[:author_username], no_throttle: user.try(:staff?)).retrieve + user = User.find_by(id: args[:user_id]) if args[:user_id] + TopicRetriever.new( + args[:embed_url], + author_username: args[:author_username], + no_throttle: user.try(:staff?), + ).retrieve end - end - end diff --git a/app/jobs/regular/run_heartbeat.rb b/app/jobs/regular/run_heartbeat.rb index 4a0691171a..549c57ac96 100644 --- a/app/jobs/regular/run_heartbeat.rb +++ b/app/jobs/regular/run_heartbeat.rb @@ -2,11 +2,10 @@ module Jobs class RunHeartbeat < ::Jobs::Base - - sidekiq_options queue: 'critical' + sidekiq_options queue: "critical" def self.heartbeat_key - 'heartbeat_last_run' + "heartbeat_last_run" end def execute(args) diff --git a/app/jobs/regular/send_push_notification.rb b/app/jobs/regular/send_push_notification.rb index 057e32dafb..d73013fb8b 100644 --- a/app/jobs/regular/send_push_notification.rb +++ b/app/jobs/regular/send_push_notification.rb @@ -4,7 +4,9 @@ module Jobs class SendPushNotification < ::Jobs::Base def execute(args) user = User.find_by(id: args[:user_id]) - return if !user || user.seen_since?(SiteSetting.push_notification_time_window_mins.minutes.ago) + if !user || user.seen_since?(SiteSetting.push_notification_time_window_mins.minutes.ago) + return + end PushNotificationPusher.push(user, args[:payload]) end diff --git a/app/jobs/regular/send_system_message.rb b/app/jobs/regular/send_system_message.rb index 23ebe656b2..2ee6559dca 100644 --- a/app/jobs/regular/send_system_message.rb +++ b/app/jobs/regular/send_system_message.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require 'image_sizer' +require "image_sizer" module Jobs - class SendSystemMessage < ::Jobs::Base - def execute(args) raise Discourse::InvalidParameters.new(:user_id) unless args[:user_id].present? raise Discourse::InvalidParameters.new(:message_type) unless args[:message_type].present? @@ -16,7 +14,5 @@ module Jobs system_message = SystemMessage.new(user) system_message.create(args[:message_type], args[:message_options]&.symbolize_keys || {}) end - end - end diff --git a/app/jobs/regular/suspicious_login.rb b/app/jobs/regular/suspicious_login.rb index a45b820de0..0301a8cc3e 100644 --- a/app/jobs/regular/suspicious_login.rb +++ b/app/jobs/regular/suspicious_login.rb @@ -1,25 +1,24 @@ # frozen_string_literal: true module Jobs - class SuspiciousLogin < ::Jobs::Base - def execute(args) if UserAuthToken.is_suspicious(args[:user_id], args[:client_ip]) + UserAuthToken.log( + action: "suspicious", + user_id: args[:user_id], + user_agent: args[:user_agent], + client_ip: args[:client_ip], + ) - UserAuthToken.log(action: 'suspicious', - user_id: args[:user_id], - user_agent: args[:user_agent], - client_ip: args[:client_ip]) - - ::Jobs.enqueue(:critical_user_email, - type: "suspicious_login", - user_id: args[:user_id], - client_ip: args[:client_ip], - user_agent: args[:user_agent]) + ::Jobs.enqueue( + :critical_user_email, + type: "suspicious_login", + user_id: args[:user_id], + client_ip: args[:client_ip], + user_agent: args[:user_agent], + ) end end - end - end diff --git a/app/jobs/regular/sync_acls_for_uploads.rb b/app/jobs/regular/sync_acls_for_uploads.rb index 237795cb5b..435c9dbc5a 100644 --- a/app/jobs/regular/sync_acls_for_uploads.rb +++ b/app/jobs/regular/sync_acls_for_uploads.rb @@ -14,26 +14,32 @@ module Jobs # Note...these log messages are set to warn to ensure this is working # as intended in initial production trials, this will be set to debug # after an acl_stale column is added to uploads. - time = Benchmark.measure do - Rails.logger.warn("Syncing ACL for upload ids: #{args[:upload_ids].join(", ")}") - Upload.includes(:optimized_images).where(id: args[:upload_ids]).find_in_batches do |uploads| - uploads.each do |upload| - begin - Discourse.store.update_upload_ACL(upload, optimized_images_preloaded: true) - rescue => err - Discourse.warn_exception( - err, - message: "Failed to update upload ACL", - env: { - upload_id: upload.id, - filename: upload.original_filename - } - ) + time = + Benchmark.measure do + Rails.logger.warn("Syncing ACL for upload ids: #{args[:upload_ids].join(", ")}") + Upload + .includes(:optimized_images) + .where(id: args[:upload_ids]) + .find_in_batches do |uploads| + uploads.each do |upload| + begin + Discourse.store.update_upload_ACL(upload, optimized_images_preloaded: true) + rescue => err + Discourse.warn_exception( + err, + message: "Failed to update upload ACL", + env: { + upload_id: upload.id, + filename: upload.original_filename, + }, + ) + end + end end - end + Rails.logger.warn( + "Completed syncing ACL for upload ids in #{time.to_s}. IDs: #{args[:upload_ids].join(", ")}", + ) end - Rails.logger.warn("Completed syncing ACL for upload ids in #{time.to_s}. IDs: #{args[:upload_ids].join(", ")}") - end end end end diff --git a/app/jobs/regular/toggle_topic_closed.rb b/app/jobs/regular/toggle_topic_closed.rb index 55f7bce88d..a02831ca61 100644 --- a/app/jobs/regular/toggle_topic_closed.rb +++ b/app/jobs/regular/toggle_topic_closed.rb @@ -12,9 +12,7 @@ module Jobs state = !!args[:state] timer_type = args[:silent] ? :silent_close : :close - if topic_timer.blank? || topic_timer.execute_at > Time.zone.now - return - end + return if topic_timer.blank? || topic_timer.execute_at > Time.zone.now if (topic = topic_timer.topic).blank? || topic.closed == state topic_timer.destroy! @@ -28,10 +26,10 @@ module Jobs topic.set_or_create_timer( TopicTimer.types[:open], SiteSetting.num_hours_to_close_topic, - by_user: Discourse.system_user + by_user: Discourse.system_user, ) else - topic.update_status('autoclosed', state, user, { silent: args[:silent] }) + topic.update_status("autoclosed", state, user, { silent: args[:silent] }) end topic.inherit_auto_close_from_category(timer_type: timer_type) if state == false diff --git a/app/jobs/regular/topic_action_converter.rb b/app/jobs/regular/topic_action_converter.rb index 370e8cbb02..1d64ff378d 100644 --- a/app/jobs/regular/topic_action_converter.rb +++ b/app/jobs/regular/topic_action_converter.rb @@ -1,20 +1,29 @@ # frozen_string_literal: true class Jobs::TopicActionConverter < ::Jobs::Base - # Re-creating all the user actions could be very slow, so let's do it in a job # to avoid a N+1 query on a front facing operation. def execute(args) topic = Topic.find_by(id: args[:topic_id]) return if topic.blank? - UserAction.where( - target_topic_id: topic.id, - action_type: [UserAction::GOT_PRIVATE_MESSAGE, UserAction::NEW_PRIVATE_MESSAGE]).find_each do |ua| - UserAction.remove_action!(ua.attributes.symbolize_keys.slice(:action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id)) + UserAction + .where( + target_topic_id: topic.id, + action_type: [UserAction::GOT_PRIVATE_MESSAGE, UserAction::NEW_PRIVATE_MESSAGE], + ) + .find_each do |ua| + UserAction.remove_action!( + ua.attributes.symbolize_keys.slice( + :action_type, + :user_id, + :acting_user_id, + :target_topic_id, + :target_post_id, + ), + ) end topic.posts.find_each { |post| UserActionManager.post_created(post) } UserActionManager.topic_created(topic) end - end diff --git a/app/jobs/regular/truncate_user_flag_stats.rb b/app/jobs/regular/truncate_user_flag_stats.rb index d75f7bb5aa..95a00ede7d 100644 --- a/app/jobs/regular/truncate_user_flag_stats.rb +++ b/app/jobs/regular/truncate_user_flag_stats.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Jobs::TruncateUserFlagStats < ::Jobs::Base - def self.truncate_to 100 end @@ -17,8 +16,11 @@ class Jobs::TruncateUserFlagStats < ::Jobs::Base total = user_stat.flags_agreed + user_stat.flags_disagreed + user_stat.flags_ignored next if total < self.class.truncate_to - params = ReviewableScore.statuses.slice(:agreed, :disagreed, :ignored). - merge(user_id: u, truncate_to: self.class.truncate_to) + params = + ReviewableScore + .statuses + .slice(:agreed, :disagreed, :ignored) + .merge(user_id: u, truncate_to: self.class.truncate_to) result = DB.query(<<~SQL, params) SELECT SUM(CASE WHEN x.status = :agreed THEN 1 ELSE 0 END) AS agreed, @@ -44,7 +46,5 @@ class Jobs::TruncateUserFlagStats < ::Jobs::Base flags_ignored: result[0].ignored || 0, ) end - end - end diff --git a/app/jobs/regular/unpin_topic.rb b/app/jobs/regular/unpin_topic.rb index 9d4f6b93ee..8e804b1a94 100644 --- a/app/jobs/regular/unpin_topic.rb +++ b/app/jobs/regular/unpin_topic.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class UnpinTopic < ::Jobs::Base - def execute(args) topic_id = args[:topic_id] @@ -12,7 +10,5 @@ module Jobs topic = Topic.find_by(id: topic_id) topic.update_pinned(false) if topic.present? end - end - end diff --git a/app/jobs/regular/update_gravatar.rb b/app/jobs/regular/update_gravatar.rb index de954b41ee..6133a92c0e 100644 --- a/app/jobs/regular/update_gravatar.rb +++ b/app/jobs/regular/update_gravatar.rb @@ -2,8 +2,7 @@ module Jobs class UpdateGravatar < ::Jobs::Base - - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) user = User.find_by(id: args[:user_id]) @@ -17,5 +16,4 @@ module Jobs end end end - end diff --git a/app/jobs/regular/update_group_mentions.rb b/app/jobs/regular/update_group_mentions.rb index 4df6b9145f..50147a3c0c 100644 --- a/app/jobs/regular/update_group_mentions.rb +++ b/app/jobs/regular/update_group_mentions.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class UpdateGroupMentions < ::Jobs::Base - def execute(args) group = Group.find_by(id: args[:group_id]) return unless group diff --git a/app/jobs/regular/update_hotlinked_raw.rb b/app/jobs/regular/update_hotlinked_raw.rb index 1fb425839b..e2e99cc7f8 100644 --- a/app/jobs/regular/update_hotlinked_raw.rb +++ b/app/jobs/regular/update_hotlinked_raw.rb @@ -2,7 +2,7 @@ module Jobs class UpdateHotlinkedRaw < ::Jobs::Base - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) @post_id = args[:post_id] @@ -15,10 +15,11 @@ module Jobs hotlinked_map = post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h - raw = InlineUploads.replace_hotlinked_image_urls(raw: post.raw) do |match_src| - normalized_match_src = PostHotlinkedMedia.normalize_src(match_src) - hotlinked_map[normalized_match_src]&.upload - end + raw = + InlineUploads.replace_hotlinked_image_urls(raw: post.raw) do |match_src| + normalized_match_src = PostHotlinkedMedia.normalize_src(match_src) + hotlinked_map[normalized_match_src]&.upload + end if post.raw != raw changes = { raw: raw, edit_reason: I18n.t("upload.edit_reason") } diff --git a/app/jobs/regular/update_post_uploads_secure_status.rb b/app/jobs/regular/update_post_uploads_secure_status.rb index b27b4c92b4..a7faa53c4a 100644 --- a/app/jobs/regular/update_post_uploads_secure_status.rb +++ b/app/jobs/regular/update_post_uploads_secure_status.rb @@ -6,9 +6,7 @@ module Jobs post = Post.find_by(id: args[:post_id]) return if post.blank? - post.uploads.each do |upload| - upload.update_secure_status(source: args[:source]) - end + post.uploads.each { |upload| upload.update_secure_status(source: args[:source]) } end end end diff --git a/app/jobs/regular/update_s3_inventory.rb b/app/jobs/regular/update_s3_inventory.rb index 46c34a59bf..699db49886 100644 --- a/app/jobs/regular/update_s3_inventory.rb +++ b/app/jobs/regular/update_s3_inventory.rb @@ -5,13 +5,13 @@ require "s3_inventory" module Jobs # if upload bucket changes or inventory bucket changes we want to update s3 bucket policy and inventory configuration class UpdateS3Inventory < ::Jobs::Base - def execute(args) - return unless SiteSetting.enable_s3_inventory? && - SiteSetting.Upload.enable_s3_uploads && - SiteSetting.s3_configure_inventory_policy + unless SiteSetting.enable_s3_inventory? && SiteSetting.Upload.enable_s3_uploads && + SiteSetting.s3_configure_inventory_policy + return + end - [:upload, :optimized].each do |type| + %i[upload optimized].each do |type| s3_inventory = S3Inventory.new(Discourse.store.s3_helper, type) s3_inventory.update_bucket_policy if type == :upload s3_inventory.update_bucket_inventory_configuration diff --git a/app/jobs/regular/update_top_redirection.rb b/app/jobs/regular/update_top_redirection.rb index 80ad6acefb..dae128799b 100644 --- a/app/jobs/regular/update_top_redirection.rb +++ b/app/jobs/regular/update_top_redirection.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Jobs - class UpdateTopRedirection < ::Jobs::Base - def execute(args) return if args[:user_id].blank? || args[:redirected_at].blank? @@ -13,5 +11,4 @@ module Jobs .update_all(last_redirected_to_top_at: args[:redirected_at]) end end - end diff --git a/app/jobs/regular/update_topic_upload_security.rb b/app/jobs/regular/update_topic_upload_security.rb index 2ac3deb2bd..dd2eceffe5 100644 --- a/app/jobs/regular/update_topic_upload_security.rb +++ b/app/jobs/regular/update_topic_upload_security.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true module Jobs - class UpdateTopicUploadSecurity < ::Jobs::Base - def execute(args) topic = Topic.find_by(id: args[:topic_id]) if topic.blank? - Rails.logger.info("Could not find topic #{args[:topic_id]} for topic upload security updater.") + Rails.logger.info( + "Could not find topic #{args[:topic_id]} for topic upload security updater.", + ) return end TopicUploadSecurityManager.new(topic).run diff --git a/app/jobs/regular/update_username.rb b/app/jobs/regular/update_username.rb index f9660c2358..3a0ed3194e 100644 --- a/app/jobs/regular/update_username.rb +++ b/app/jobs/regular/update_username.rb @@ -2,8 +2,7 @@ module Jobs class UpdateUsername < ::Jobs::Base - - sidekiq_options queue: 'low' + sidekiq_options queue: "low" def execute(args) @user_id = args[:user_id] @@ -14,7 +13,8 @@ module Jobs @new_username = args[:new_username].unicode_normalize @avatar_img = PrettyText.avatar_img(args[:avatar_template], "tiny") - @raw_mention_regex = / + @raw_mention_regex = + / (?: (? - (user.user_option&.digest_after_minutes || SiteSetting.default_email_digest_frequency.to_i).minutes.ago + if user.last_emailed_at && + user.last_emailed_at > + ( + user.user_option&.digest_after_minutes || + SiteSetting.default_email_digest_frequency.to_i + ).minutes.ago + return + end end - seen_recently = (user.last_seen_at.present? && user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago) + seen_recently = + ( + user.last_seen_at.present? && + user.last_seen_at > SiteSetting.email_time_window_mins.minutes.ago + ) if !args[:force_respect_seen_recently] && - (always_email_regular?(user, type) || always_email_private_message?(user, type) || user.staged) + ( + always_email_regular?(user, type) || always_email_private_message?(user, type) || + user.staged + ) seen_recently = false end email_args = {} if (post || notification || notification_type || args[:force_respect_seen_recently]) && - (seen_recently && !user.suspended?) - + (seen_recently && !user.suspended?) return skip_message(SkippedEmailLog.reason_types[:user_email_seen_recently]) end email_args[:post] = post if post if notification || notification_type - email_args[:notification_type] ||= notification_type || notification.try(:notification_type) - email_args[:notification_data_hash] ||= notification_data_hash || notification.try(:data_hash) + email_args[:notification_type] ||= notification_type || notification.try(:notification_type) + email_args[:notification_data_hash] ||= notification_data_hash || + notification.try(:data_hash) unless String === email_args[:notification_type] if Numeric === email_args[:notification_type] @@ -157,13 +158,12 @@ module Jobs email_args[:notification_type] = email_args[:notification_type].to_s end - if !SiteSetting.disable_mailing_list_mode && - user.user_option.mailing_list_mode? && - user.user_option.mailing_list_mode_frequency > 0 && # don't catch notifications for users on daily mailing list mode - (!post.try(:topic).try(:private_message?)) && - NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) + if !SiteSetting.disable_mailing_list_mode && user.user_option.mailing_list_mode? && + user.user_option.mailing_list_mode_frequency > 0 && # don't catch notifications for users on daily mailing list mode + (!post.try(:topic).try(:private_message?)) && + NOTIFICATIONS_SENT_BY_MAILING_LIST.include?(email_args[:notification_type]) # no need to log a reason when the mail was already sent via the mailing list job - return [nil, nil] + return nil, nil end unless always_email_regular?(user, type) || always_email_private_message?(user, type) @@ -177,7 +177,9 @@ module Jobs return skip_message(skip_reason_type) if skip_reason_type.present? # Make sure that mailer exists - raise Discourse::InvalidParameters.new("type=#{type}") unless UserNotifications.respond_to?(type) + unless UserNotifications.respond_to?(type) + raise Discourse::InvalidParameters.new("type=#{type}") + end if email_token.present? email_args[:email_token] = email_token @@ -185,13 +187,12 @@ module Jobs if type.to_s == "confirm_new_email" change_req = EmailChangeRequest.find_by_new_token(email_token) - if change_req - email_args[:requested_by_admin] = change_req.requested_by_admin? - end + email_args[:requested_by_admin] = change_req.requested_by_admin? if change_req end end - email_args[:new_email] = args[:new_email] || user.email if type.to_s == "notify_old_email" || type.to_s == "notify_old_email_add" + email_args[:new_email] = args[:new_email] || user.email if type.to_s == "notify_old_email" || + type.to_s == "notify_old_email_add" if args[:client_ip] && args[:user_agent] email_args[:client_ip] = args[:client_ip] @@ -202,7 +203,8 @@ module Jobs return skip_message(SkippedEmailLog.reason_types[:exceeded_emails_limit]) end - if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type.to_s) && user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold + if !EmailLog::CRITICAL_EMAIL_TYPES.include?(type.to_s) && + user.user_stat.bounce_score >= SiteSetting.bounce_score_threshold return skip_message(SkippedEmailLog.reason_types[:exceeded_bounces_limit]) end @@ -212,9 +214,10 @@ module Jobs email_args[:reject_reason] = args[:reject_reason] - message = EmailLog.unique_email_per_post(post, user) do - UserNotifications.public_send(type, user, email_args) - end + message = + EmailLog.unique_email_per_post(post, user) do + UserNotifications.public_send(type, user, email_args) + end # Update the to address if we have a custom one message.to = to_address if message && to_address.present? @@ -232,26 +235,24 @@ module Jobs def skip_email_for_post(post, user) return false unless post - if post.topic.blank? - return SkippedEmailLog.reason_types[:user_email_topic_nil] - end + return SkippedEmailLog.reason_types[:user_email_topic_nil] if post.topic.blank? - if post.user.blank? - return SkippedEmailLog.reason_types[:user_email_post_user_deleted] - end + return SkippedEmailLog.reason_types[:user_email_post_user_deleted] if post.user.blank? - if post.user_deleted? - return SkippedEmailLog.reason_types[:user_email_post_deleted] - end + return SkippedEmailLog.reason_types[:user_email_post_deleted] if post.user_deleted? if user.suspended? && (!post.user&.staff? || !post.user&.human?) return SkippedEmailLog.reason_types[:user_email_user_suspended] end - already_read = user.user_option.email_level != UserOption.email_level_types[:always] && PostTiming.exists?(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id) - if already_read - SkippedEmailLog.reason_types[:user_email_already_read] - end + already_read = + user.user_option.email_level != UserOption.email_level_types[:always] && + PostTiming.exists?( + topic_id: post.topic_id, + post_number: post.post_number, + user_id: user.id, + ) + SkippedEmailLog.reason_types[:user_email_already_read] if already_read end def skip(reason_type) @@ -260,17 +261,18 @@ module Jobs to_address: @skip_context[:to_address], user_id: @skip_context[:user_id], post_id: @skip_context[:post_id], - reason_type: reason_type + reason_type: reason_type, ) end def always_email_private_message?(user, type) - type.to_s == "user_private_message" && user.user_option.email_messages_level == UserOption.email_level_types[:always] + type.to_s == "user_private_message" && + user.user_option.email_messages_level == UserOption.email_level_types[:always] end def always_email_regular?(user, type) - type.to_s != "user_private_message" && user.user_option.email_level == UserOption.email_level_types[:always] + type.to_s != "user_private_message" && + user.user_option.email_level == UserOption.email_level_types[:always] end end - end diff --git a/app/jobs/scheduled/activation_reminder_emails.rb b/app/jobs/scheduled/activation_reminder_emails.rb index 5bfbf9e9f3..f3f4470ad9 100644 --- a/app/jobs/scheduled/activation_reminder_emails.rb +++ b/app/jobs/scheduled/activation_reminder_emails.rb @@ -5,22 +5,25 @@ module Jobs every 2.hours def execute(args) - User.joins("LEFT JOIN user_custom_fields ON users.id = user_id AND user_custom_fields.name = 'activation_reminder'") - .where(active: false, staged: false, user_custom_fields: { value: nil }) - .where('users.created_at BETWEEN ? AND ?', 3.days.ago, 2.days.ago) - .find_each do |user| - - user.custom_fields['activation_reminder'] = true - user.save_custom_fields - - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) - ::Jobs.enqueue( - :user_email, - type: "activation_reminder", - user_id: user.id, - email_token: email_token.token + User + .joins( + "LEFT JOIN user_custom_fields ON users.id = user_id AND user_custom_fields.name = 'activation_reminder'", ) - end + .where(active: false, staged: false, user_custom_fields: { value: nil }) + .where("users.created_at BETWEEN ? AND ?", 3.days.ago, 2.days.ago) + .find_each do |user| + user.custom_fields["activation_reminder"] = true + user.save_custom_fields + + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup]) + ::Jobs.enqueue( + :user_email, + type: "activation_reminder", + user_id: user.id, + email_token: email_token.token, + ) + end end end end diff --git a/app/jobs/scheduled/auto_expire_user_api_keys.rb b/app/jobs/scheduled/auto_expire_user_api_keys.rb index 4a4e041f7c..60bd24418d 100644 --- a/app/jobs/scheduled/auto_expire_user_api_keys.rb +++ b/app/jobs/scheduled/auto_expire_user_api_keys.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class AutoExpireUserApiKeys < ::Jobs::Scheduled every 1.day @@ -9,9 +8,10 @@ module Jobs if SiteSetting.expire_user_api_keys_days > 0 expire_user_api_keys_days = SiteSetting.expire_user_api_keys_days.days.ago - UserApiKey.where("last_used_at < ?", expire_user_api_keys_days).update_all(revoked_at: Time.zone.now) + UserApiKey.where("last_used_at < ?", expire_user_api_keys_days).update_all( + revoked_at: Time.zone.now, + ) end end end - end diff --git a/app/jobs/scheduled/auto_queue_handler.rb b/app/jobs/scheduled/auto_queue_handler.rb index 6bf75924c9..66150fb8c9 100644 --- a/app/jobs/scheduled/auto_queue_handler.rb +++ b/app/jobs/scheduled/auto_queue_handler.rb @@ -4,7 +4,6 @@ # queue for a long time. module Jobs class AutoQueueHandler < ::Jobs::Scheduled - every 1.day def execute(args) @@ -12,17 +11,16 @@ module Jobs Reviewable .pending - .where('created_at < ?', SiteSetting.auto_handle_queued_age.to_i.days.ago) + .where("created_at < ?", SiteSetting.auto_handle_queued_age.to_i.days.ago) .each do |reviewable| - - if reviewable.is_a?(ReviewableFlaggedPost) - reviewable.perform(Discourse.system_user, :ignore, expired: true) - elsif reviewable.is_a?(ReviewableQueuedPost) - reviewable.perform(Discourse.system_user, :reject_post) - elsif reviewable.is_a?(ReviewableUser) - reviewable.perform(Discourse.system_user, :delete_user) + if reviewable.is_a?(ReviewableFlaggedPost) + reviewable.perform(Discourse.system_user, :ignore, expired: true) + elsif reviewable.is_a?(ReviewableQueuedPost) + reviewable.perform(Discourse.system_user, :reject_post) + elsif reviewable.is_a?(ReviewableUser) + reviewable.perform(Discourse.system_user, :delete_user) + end end - end end end end diff --git a/app/jobs/scheduled/badge_grant.rb b/app/jobs/scheduled/badge_grant.rb index e9ad67a9b1..b9bc1ad4e8 100644 --- a/app/jobs/scheduled/badge_grant.rb +++ b/app/jobs/scheduled/badge_grant.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class BadgeGrant < ::Jobs::Scheduled def self.run self.new.execute(nil) @@ -17,7 +16,10 @@ module Jobs BadgeGranter.backfill(b) rescue => ex # TODO - expose errors in UI - Discourse.handle_job_exception(ex, error_context({}, code_desc: 'Exception granting badges', extra: { badge_id: b.id })) + Discourse.handle_job_exception( + ex, + error_context({}, code_desc: "Exception granting badges", extra: { badge_id: b.id }), + ) end end @@ -25,7 +27,5 @@ module Jobs UserBadge.ensure_consistency! # Badge granter sometimes uses raw SQL, so hooks do not run. Clean up data UserStat.update_distinct_badge_count end - end - end diff --git a/app/jobs/scheduled/bookmark_reminder_notifications.rb b/app/jobs/scheduled/bookmark_reminder_notifications.rb index 712a650fc0..5443b247da 100644 --- a/app/jobs/scheduled/bookmark_reminder_notifications.rb +++ b/app/jobs/scheduled/bookmark_reminder_notifications.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - # Runs periodically to send out bookmark reminders, capped at 300 at a time. # Any leftovers will be caught in the next run, because the reminder_at column # is set to NULL once a reminder has been sent. @@ -18,10 +17,10 @@ module Jobs end def execute(args = nil) - bookmarks = Bookmark.pending_reminders.includes(:user).order('reminder_at ASC') - bookmarks.limit(BookmarkReminderNotifications.max_reminder_notifications_per_run).each do |bookmark| - BookmarkReminderNotificationHandler.new(bookmark).send_notification - end + bookmarks = Bookmark.pending_reminders.includes(:user).order("reminder_at ASC") + bookmarks + .limit(BookmarkReminderNotifications.max_reminder_notifications_per_run) + .each { |bookmark| BookmarkReminderNotificationHandler.new(bookmark).send_notification } end end end diff --git a/app/jobs/scheduled/category_stats.rb b/app/jobs/scheduled/category_stats.rb index b1de99224e..81ff69031f 100644 --- a/app/jobs/scheduled/category_stats.rb +++ b/app/jobs/scheduled/category_stats.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module Jobs - class CategoryStats < ::Jobs::Scheduled every 24.hours def execute(args) Category.update_stats end - end - end diff --git a/app/jobs/scheduled/check_new_features.rb b/app/jobs/scheduled/check_new_features.rb index c3f98bf488..b69410f351 100644 --- a/app/jobs/scheduled/check_new_features.rb +++ b/app/jobs/scheduled/check_new_features.rb @@ -11,10 +11,7 @@ module Jobs if prev_most_recent admin_ids.each do |admin_id| if DiscourseUpdates.get_last_viewed_feature_date(admin_id).blank? - DiscourseUpdates.bump_last_viewed_feature_date( - admin_id, - prev_most_recent["created_at"] - ) + DiscourseUpdates.bump_last_viewed_feature_date(admin_id, prev_most_recent["created_at"]) end end end @@ -32,16 +29,15 @@ module Jobs most_recent_feature_date = Time.zone.parse(new_most_recent["created_at"]) admin_ids.each do |admin_id| admin_last_viewed_feature_date = DiscourseUpdates.get_last_viewed_feature_date(admin_id) - if admin_last_viewed_feature_date.blank? || admin_last_viewed_feature_date < most_recent_feature_date + if admin_last_viewed_feature_date.blank? || + admin_last_viewed_feature_date < most_recent_feature_date Notification.consolidate_or_create!( user_id: admin_id, notification_type: Notification.types[:new_features], - data: {} - ) - DiscourseUpdates.bump_last_viewed_feature_date( - admin_id, - new_most_recent["created_at"] + data: { + }, ) + DiscourseUpdates.bump_last_viewed_feature_date(admin_id, new_most_recent["created_at"]) end end end diff --git a/app/jobs/scheduled/check_out_of_date_themes.rb b/app/jobs/scheduled/check_out_of_date_themes.rb index 1003e25a90..11ec149e94 100644 --- a/app/jobs/scheduled/check_out_of_date_themes.rb +++ b/app/jobs/scheduled/check_out_of_date_themes.rb @@ -5,13 +5,16 @@ module Jobs every 1.day def execute(args) - target_themes = RemoteTheme - .joins("JOIN themes ON themes.remote_theme_id = remote_themes.id") - .where.not(remote_url: "") + target_themes = + RemoteTheme + .joins("JOIN themes ON themes.remote_theme_id = remote_themes.id") + .where.not(remote_url: "") target_themes.each do |remote| - remote.update_remote_version - remote.save! + Discourse.capture_exceptions(message: "Error updating theme #{remote.id}") do + remote.update_remote_version + remote.save! + end end end end diff --git a/app/jobs/scheduled/clean_dismissed_topic_users.rb b/app/jobs/scheduled/clean_dismissed_topic_users.rb index 72109ffbfc..7b21fb54ea 100644 --- a/app/jobs/scheduled/clean_dismissed_topic_users.rb +++ b/app/jobs/scheduled/clean_dismissed_topic_users.rb @@ -27,12 +27,15 @@ module Jobs END, users.created_at, :min_date) AND dtu1.id = dtu2.id SQL - sql = DB.sql_fragment(sql, - now: DateTime.now, - last_visit: User::NewTopicDuration::LAST_VISIT, - always: User::NewTopicDuration::ALWAYS, - default_duration: SiteSetting.default_other_new_topic_duration_minutes, - min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime) + sql = + DB.sql_fragment( + sql, + now: DateTime.now, + last_visit: User::NewTopicDuration::LAST_VISIT, + always: User::NewTopicDuration::ALWAYS, + default_duration: SiteSetting.default_other_new_topic_duration_minutes, + min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, + ) DB.exec(sql) end diff --git a/app/jobs/scheduled/clean_up_associated_accounts.rb b/app/jobs/scheduled/clean_up_associated_accounts.rb index ac2ba4802b..861b0bca1f 100644 --- a/app/jobs/scheduled/clean_up_associated_accounts.rb +++ b/app/jobs/scheduled/clean_up_associated_accounts.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module Jobs - class CleanUpAssociatedAccounts < ::Jobs::Scheduled every 1.day def execute(args) UserAssociatedAccount.cleanup! end - end - end diff --git a/app/jobs/scheduled/clean_up_crawler_stats.rb b/app/jobs/scheduled/clean_up_crawler_stats.rb index 09f704bd34..e333082b17 100644 --- a/app/jobs/scheduled/clean_up_crawler_stats.rb +++ b/app/jobs/scheduled/clean_up_crawler_stats.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true module Jobs - class CleanUpCrawlerStats < ::Jobs::Scheduled every 1.day def execute(args) - WebCrawlerRequest.where('date < ?', WebCrawlerRequest.max_record_age.ago).delete_all + WebCrawlerRequest.where("date < ?", WebCrawlerRequest.max_record_age.ago).delete_all # keep count of only the top user agents DB.exec <<~SQL @@ -24,5 +23,4 @@ module Jobs SQL end end - end diff --git a/app/jobs/scheduled/clean_up_email_change_requests.rb b/app/jobs/scheduled/clean_up_email_change_requests.rb index cd572b5e45..d2e1926a99 100644 --- a/app/jobs/scheduled/clean_up_email_change_requests.rb +++ b/app/jobs/scheduled/clean_up_email_change_requests.rb @@ -5,7 +5,7 @@ module Jobs every 1.day def execute(args) - EmailChangeRequest.where('updated_at < ?', 1.month.ago).delete_all + EmailChangeRequest.where("updated_at < ?", 1.month.ago).delete_all end end end diff --git a/app/jobs/scheduled/clean_up_email_logs.rb b/app/jobs/scheduled/clean_up_email_logs.rb index 9799764e71..9ddc1f0125 100644 --- a/app/jobs/scheduled/clean_up_email_logs.rb +++ b/app/jobs/scheduled/clean_up_email_logs.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class CleanUpEmailLogs < ::Jobs::Scheduled every 1.day @@ -13,7 +12,5 @@ module Jobs EmailLog.where("created_at < ?", threshold).delete_all SkippedEmailLog.where("created_at < ?", threshold).delete_all end - end - end diff --git a/app/jobs/scheduled/clean_up_email_tokens.rb b/app/jobs/scheduled/clean_up_email_tokens.rb index 0781115fd8..daff838eb8 100644 --- a/app/jobs/scheduled/clean_up_email_tokens.rb +++ b/app/jobs/scheduled/clean_up_email_tokens.rb @@ -5,10 +5,7 @@ module Jobs every 1.day def execute(args) - EmailToken - .where('NOT confirmed AND expired') - .where('created_at < ?', 1.month.ago) - .delete_all + EmailToken.where("NOT confirmed AND expired").where("created_at < ?", 1.month.ago).delete_all end end end diff --git a/app/jobs/scheduled/clean_up_inactive_users.rb b/app/jobs/scheduled/clean_up_inactive_users.rb index a54e1ee657..b52ae8d5e2 100644 --- a/app/jobs/scheduled/clean_up_inactive_users.rb +++ b/app/jobs/scheduled/clean_up_inactive_users.rb @@ -1,30 +1,32 @@ # frozen_string_literal: true module Jobs - class CleanUpInactiveUsers < ::Jobs::Scheduled every 1.day def execute(args) return if SiteSetting.clean_up_inactive_users_after_days <= 0 - User.joins("LEFT JOIN posts ON posts.user_id = users.id") - .where(last_posted_at: nil, trust_level: TrustLevel.levels[:newuser], admin: false, moderator: false) + User + .joins("LEFT JOIN posts ON posts.user_id = users.id") + .where( + last_posted_at: nil, + trust_level: TrustLevel.levels[:newuser], + admin: false, + moderator: false, + ) .where( "posts.user_id IS NULL AND users.last_seen_at < ?", - SiteSetting.clean_up_inactive_users_after_days.days.ago - ) + SiteSetting.clean_up_inactive_users_after_days.days.ago, + ) .limit(1000) - .pluck(:id).each_slice(50) do |slice| - destroy(slice) - end - + .pluck(:id) + .each_slice(50) { |slice| destroy(slice) } end private def destroy(ids) - destroyer = UserDestroyer.new(Discourse.system_user) User.transaction do @@ -32,11 +34,18 @@ module Jobs begin user = User.find_by(id: id) next unless user - destroyer.destroy(user, transaction: false, context: I18n.t("user.destroy_reasons.inactive_user")) + destroyer.destroy( + user, + transaction: false, + context: I18n.t("user.destroy_reasons.inactive_user"), + ) rescue => e - Discourse.handle_job_exception(e, - message: "Cleaning up inactive users", - extra: { user_id: id } + Discourse.handle_job_exception( + e, + message: "Cleaning up inactive users", + extra: { + user_id: id, + }, ) raise e end diff --git a/app/jobs/scheduled/clean_up_post_reply_keys.rb b/app/jobs/scheduled/clean_up_post_reply_keys.rb index 748da603f3..a3088101c8 100644 --- a/app/jobs/scheduled/clean_up_post_reply_keys.rb +++ b/app/jobs/scheduled/clean_up_post_reply_keys.rb @@ -9,7 +9,7 @@ module Jobs PostReplyKey.where( "created_at < ?", - SiteSetting.disallow_reply_by_email_after_days.days.ago + SiteSetting.disallow_reply_by_email_after_days.days.ago, ).delete_all end end diff --git a/app/jobs/scheduled/clean_up_unmatched_emails.rb b/app/jobs/scheduled/clean_up_unmatched_emails.rb index 58fb112e17..abcb85f172 100644 --- a/app/jobs/scheduled/clean_up_unmatched_emails.rb +++ b/app/jobs/scheduled/clean_up_unmatched_emails.rb @@ -1,18 +1,16 @@ # frozen_string_literal: true module Jobs - class CleanUpUnmatchedEmails < ::Jobs::Scheduled every 1.day def execute(args) last_match_threshold = SiteSetting.max_age_unmatched_emails.days.ago - ScreenedEmail.where(action_type: ScreenedEmail.actions[:block]) + ScreenedEmail + .where(action_type: ScreenedEmail.actions[:block]) .where("last_match_at < ?", last_match_threshold) .destroy_all end - end - end diff --git a/app/jobs/scheduled/clean_up_unmatched_ips.rb b/app/jobs/scheduled/clean_up_unmatched_ips.rb index fa16315f2d..fa1c097eef 100644 --- a/app/jobs/scheduled/clean_up_unmatched_ips.rb +++ b/app/jobs/scheduled/clean_up_unmatched_ips.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class CleanUpUnmatchedIPs < ::Jobs::Scheduled every 1.day @@ -12,11 +11,14 @@ module Jobs last_match_threshold = SiteSetting.max_age_unmatched_ips.days.ago # remove old unmatched IP addresses - ScreenedIpAddress.where(action_type: ScreenedIpAddress.actions[:block]) - .where("last_match_at < ? OR (last_match_at IS NULL AND created_at < ?)", last_match_threshold, last_match_threshold) + ScreenedIpAddress + .where(action_type: ScreenedIpAddress.actions[:block]) + .where( + "last_match_at < ? OR (last_match_at IS NULL AND created_at < ?)", + last_match_threshold, + last_match_threshold, + ) .destroy_all end - end - end diff --git a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb index ed8dfb5566..91d22aaf6b 100644 --- a/app/jobs/scheduled/clean_up_unsubscribe_keys.rb +++ b/app/jobs/scheduled/clean_up_unsubscribe_keys.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module Jobs - class CleanUpUnsubscribeKeys < ::Jobs::Scheduled every 1.day def execute(args) - UnsubscribeKey.where('created_at < ?', 2.months.ago).delete_all + UnsubscribeKey.where("created_at < ?", 2.months.ago).delete_all end - end - end diff --git a/app/jobs/scheduled/clean_up_unused_api_keys.rb b/app/jobs/scheduled/clean_up_unused_api_keys.rb index 4b9e756e63..81266c7050 100644 --- a/app/jobs/scheduled/clean_up_unused_api_keys.rb +++ b/app/jobs/scheduled/clean_up_unused_api_keys.rb @@ -1,14 +1,11 @@ # frozen_string_literal: true module Jobs - class CleanUpUnusedApiKeys < ::Jobs::Scheduled every 1.day def execute(args) ApiKey.revoke_unused_keys! end - end - end diff --git a/app/jobs/scheduled/clean_up_unused_staged_users.rb b/app/jobs/scheduled/clean_up_unused_staged_users.rb index be2cd17313..301de4efea 100644 --- a/app/jobs/scheduled/clean_up_unused_staged_users.rb +++ b/app/jobs/scheduled/clean_up_unused_staged_users.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class CleanUpUnusedStagedUsers < ::Jobs::Scheduled every 1.day @@ -11,23 +10,24 @@ module Jobs destroyer = UserDestroyer.new(Discourse.system_user) - User.joins("LEFT JOIN posts ON posts.user_id = users.id") + User + .joins("LEFT JOIN posts ON posts.user_id = users.id") .where("posts.user_id IS NULL") .where(staged: true, admin: false, moderator: false) .where("users.created_at < ?", clean_up_after_days.days.ago) .find_each do |user| - - begin - destroyer.destroy(user, context: I18n.t("user.destroy_reasons.unused_staged_user")) - rescue => e - Discourse.handle_job_exception(e, - message: "Cleaning up unused staged user", - extra: { user_id: user.id } - ) + begin + destroyer.destroy(user, context: I18n.t("user.destroy_reasons.unused_staged_user")) + rescue => e + Discourse.handle_job_exception( + e, + message: "Cleaning up unused staged user", + extra: { + user_id: user.id, + }, + ) + end end - end end - end - end diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index ecaa5d3876..4f8c89c11e 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -10,7 +10,9 @@ module Jobs # always remove invalid upload records Upload .by_users - .where("retain_hours IS NULL OR created_at < current_timestamp - interval '1 hour' * retain_hours") + .where( + "retain_hours IS NULL OR created_at < current_timestamp - interval '1 hour' * retain_hours", + ) .where("created_at < ?", grace_period.hour.ago) .where(url: "") .find_each(&:destroy!) @@ -21,19 +23,29 @@ module Jobs return if (Time.zone.now.to_i - c) < (grace_period / 2).hours end - base_url = Discourse.store.internal? ? Discourse.store.relative_base_url : Discourse.store.absolute_base_url + base_url = + ( + if Discourse.store.internal? + Discourse.store.relative_base_url + else + Discourse.store.absolute_base_url + end + ) s3_hostname = URI.parse(base_url).hostname s3_cdn_hostname = URI.parse(SiteSetting.Upload.s3_cdn_url || "").hostname result = Upload.by_users Upload.unused_callbacks&.each { |handler| result = handler.call(result) } - result = result - .where("uploads.retain_hours IS NULL OR uploads.created_at < current_timestamp - interval '1 hour' * uploads.retain_hours") - .where("uploads.created_at < ?", grace_period.hour.ago) - .where("uploads.access_control_post_id IS NULL") - .joins("LEFT JOIN upload_references ON upload_references.upload_id = uploads.id") - .where("upload_references.upload_id IS NULL") - .with_no_non_post_relations + result = + result + .where( + "uploads.retain_hours IS NULL OR uploads.created_at < current_timestamp - interval '1 hour' * uploads.retain_hours", + ) + .where("uploads.created_at < ?", grace_period.hour.ago) + .where("uploads.access_control_post_id IS NULL") + .joins("LEFT JOIN upload_references ON upload_references.upload_id = uploads.id") + .where("upload_references.upload_id IS NULL") + .with_no_non_post_relations result.find_each do |upload| next if Upload.in_use_callbacks&.any? { |callback| callback.call(upload) } @@ -41,9 +53,30 @@ module Jobs if upload.sha1.present? # TODO: Remove this check after UploadReferences records were created encoded_sha = Base62.encode(upload.sha1.hex) - next if ReviewableQueuedPost.pending.where("payload->>'raw' LIKE ? OR payload->>'raw' LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists? - next if Draft.where("data LIKE ? OR data LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists? - next if UserProfile.where("bio_raw LIKE ? OR bio_raw LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists? + if ReviewableQueuedPost + .pending + .where( + "payload->>'raw' LIKE ? OR payload->>'raw' LIKE ?", + "%#{upload.sha1}%", + "%#{encoded_sha}%", + ) + .exists? + next + end + if Draft.where( + "data LIKE ? OR data LIKE ?", + "%#{upload.sha1}%", + "%#{encoded_sha}%", + ).exists? + next + end + if UserProfile.where( + "bio_raw LIKE ? OR bio_raw LIKE ?", + "%#{upload.sha1}%", + "%#{encoded_sha}%", + ).exists? + next + end upload.destroy else @@ -74,6 +107,5 @@ module Jobs def last_cleanup_key "LAST_UPLOAD_CLEANUP" end - end end diff --git a/app/jobs/scheduled/create_missing_avatars.rb b/app/jobs/scheduled/create_missing_avatars.rb index 0847b84f2e..2043bafe0b 100644 --- a/app/jobs/scheduled/create_missing_avatars.rb +++ b/app/jobs/scheduled/create_missing_avatars.rb @@ -6,14 +6,13 @@ module Jobs def execute(args) # backfill in batches of 5000 an hour - UserAvatar.includes(:user) + UserAvatar + .includes(:user) .joins(:user) .where(last_gravatar_download_attempt: nil) .order("users.last_posted_at DESC") .limit(5000) - .each do |u| - u.user.refresh_avatar - end + .each { |u| u.user.refresh_avatar } end end end diff --git a/app/jobs/scheduled/create_recent_post_search_indexes.rb b/app/jobs/scheduled/create_recent_post_search_indexes.rb index 4f0ab41cd9..95d181024a 100644 --- a/app/jobs/scheduled/create_recent_post_search_indexes.rb +++ b/app/jobs/scheduled/create_recent_post_search_indexes.rb @@ -4,7 +4,7 @@ module Jobs class CreateRecentPostSearchIndexes < ::Jobs::Scheduled every 1.day - REGULAR_POST_SEARCH_DATA_INDEX_NAME = 'idx_recent_regular_post_search_data' + REGULAR_POST_SEARCH_DATA_INDEX_NAME = "idx_recent_regular_post_search_data" def execute(_) create_recent_regular_post_search_index @@ -13,7 +13,11 @@ module Jobs private def create_recent_regular_post_search_index - if !PostSearchData.where(private_message: false).offset(SiteSetting.search_enable_recent_regular_posts_offset_size - 1).limit(1).exists? + if !PostSearchData + .where(private_message: false) + .offset(SiteSetting.search_enable_recent_regular_posts_offset_size - 1) + .limit(1) + .exists? return end @@ -24,22 +28,22 @@ module Jobs SQL DB.exec(<<~SQL, post_id: SiteSetting.search_recent_regular_posts_offset_post_id) - CREATE INDEX #{Rails.env.test? ? '' : 'CONCURRENTLY'} temp_idx_recent_regular_post_search_data + CREATE INDEX #{Rails.env.test? ? "" : "CONCURRENTLY"} temp_idx_recent_regular_post_search_data ON post_search_data USING GIN(search_data) WHERE NOT private_message AND post_id >= :post_id SQL DB.exec(<<~SQL) - #{Rails.env.test? ? '' : "BEGIN;"} + #{Rails.env.test? ? "" : "BEGIN;"} DROP INDEX IF EXISTS #{REGULAR_POST_SEARCH_DATA_INDEX_NAME}; ALTER INDEX temp_idx_recent_regular_post_search_data RENAME TO #{REGULAR_POST_SEARCH_DATA_INDEX_NAME}; - #{Rails.env.test? ? '' : "COMMIT;"} + #{Rails.env.test? ? "" : "COMMIT;"} SQL end def regular_offset_post_id PostSearchData - .order('post_id DESC') + .order("post_id DESC") .where(private_message: false) .offset(SiteSetting.search_recent_posts_size - 1) .limit(1) diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index 2c1bd74fa3..a81d0b5d7b 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -8,7 +8,8 @@ module Jobs if persistent_problems? # If there have been problems reported on the dashboard for a while, # send a message to admins no more often than once per week. - group_message = GroupMessage.new(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) + group_message = + GroupMessage.new(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) Topic.transaction do group_message.delete_previous! group_message.create diff --git a/app/jobs/scheduled/destroy_old_hidden_posts.rb b/app/jobs/scheduled/destroy_old_hidden_posts.rb index fa282fcbdb..fc2ed50494 100644 --- a/app/jobs/scheduled/destroy_old_hidden_posts.rb +++ b/app/jobs/scheduled/destroy_old_hidden_posts.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class DestroyOldHiddenPosts < ::Jobs::Scheduled every 1.day @@ -9,7 +8,5 @@ module Jobs return unless SiteSetting.delete_old_hidden_posts PostDestroyer.destroy_old_hidden_posts end - end - end diff --git a/app/jobs/scheduled/disable_bootstrap_mode.rb b/app/jobs/scheduled/disable_bootstrap_mode.rb index 61650b3094..e9d1db4288 100644 --- a/app/jobs/scheduled/disable_bootstrap_mode.rb +++ b/app/jobs/scheduled/disable_bootstrap_mode.rb @@ -8,16 +8,17 @@ module Jobs return unless SiteSetting.bootstrap_mode_enabled total_users = User.human_users.count - if SiteSetting.bootstrap_mode_min_users == 0 || total_users > SiteSetting.bootstrap_mode_min_users + if SiteSetting.bootstrap_mode_min_users == 0 || + total_users > SiteSetting.bootstrap_mode_min_users if SiteSetting.default_trust_level == TrustLevel[1] - SiteSetting.set_and_log('default_trust_level', TrustLevel[0]) + SiteSetting.set_and_log("default_trust_level", TrustLevel[0]) end if SiteSetting.default_email_digest_frequency == 1440 - SiteSetting.set_and_log('default_email_digest_frequency', 10080) + SiteSetting.set_and_log("default_email_digest_frequency", 10_080) end - SiteSetting.set_and_log('bootstrap_mode_enabled', false) + SiteSetting.set_and_log("bootstrap_mode_enabled", false) end end end diff --git a/app/jobs/scheduled/enqueue_digest_emails.rb b/app/jobs/scheduled/enqueue_digest_emails.rb index 7d9ad9fa16..f700c31025 100644 --- a/app/jobs/scheduled/enqueue_digest_emails.rb +++ b/app/jobs/scheduled/enqueue_digest_emails.rb @@ -1,35 +1,45 @@ # frozen_string_literal: true module Jobs - class EnqueueDigestEmails < ::Jobs::Scheduled every 30.minutes def execute(args) - return if SiteSetting.disable_digest_emails? || SiteSetting.private_email? || SiteSetting.disable_emails == 'yes' + if SiteSetting.disable_digest_emails? || SiteSetting.private_email? || + SiteSetting.disable_emails == "yes" + return + end users = target_user_ids - users.each do |user_id| - ::Jobs.enqueue(:user_email, type: "digest", user_id: user_id) - end + users.each { |user_id| ::Jobs.enqueue(:user_email, type: "digest", user_id: user_id) } end def target_user_ids # Users who want to receive digest email within their chosen digest email frequency - query = User - .real - .activated - .not_suspended - .where(staged: false) - .joins(:user_option, :user_stat, :user_emails) - .where("user_options.email_digests") - .where("user_stats.bounce_score < ?", SiteSetting.bounce_score_threshold) - .where("user_emails.primary") - .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") - .where("COALESCE(user_stats.digest_attempted_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") - .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)") - .where("COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * ?)", SiteSetting.suppress_digest_email_after_days) - .order("user_stats.digest_attempted_at ASC NULLS FIRST") + query = + User + .real + .activated + .not_suspended + .where(staged: false) + .joins(:user_option, :user_stat, :user_emails) + .where("user_options.email_digests") + .where("user_stats.bounce_score < ?", SiteSetting.bounce_score_threshold) + .where("user_emails.primary") + .where( + "COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)", + ) + .where( + "COALESCE(user_stats.digest_attempted_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)", + ) + .where( + "COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 MINUTE'::INTERVAL * user_options.digest_after_minutes)", + ) + .where( + "COALESCE(last_seen_at, '2010-01-01') >= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * ?)", + SiteSetting.suppress_digest_email_after_days, + ) + .order("user_stats.digest_attempted_at ASC NULLS FIRST") # If the site requires approval, make sure the user is approved query = query.where("approved OR moderator OR admin") if SiteSetting.must_approve_users? @@ -38,7 +48,5 @@ module Jobs query.pluck(:id) end - end - end diff --git a/app/jobs/scheduled/enqueue_onceoffs.rb b/app/jobs/scheduled/enqueue_onceoffs.rb index 29692c6311..e0b3a4cedf 100644 --- a/app/jobs/scheduled/enqueue_onceoffs.rb +++ b/app/jobs/scheduled/enqueue_onceoffs.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class EnqueueOnceoffs < ::Jobs::Scheduled every 10.minutes @@ -9,5 +8,4 @@ module Jobs ::Jobs::Onceoff.enqueue_all end end - end diff --git a/app/jobs/scheduled/enqueue_suspect_users.rb b/app/jobs/scheduled/enqueue_suspect_users.rb index d6f568335c..765728e3f0 100644 --- a/app/jobs/scheduled/enqueue_suspect_users.rb +++ b/app/jobs/scheduled/enqueue_suspect_users.rb @@ -8,51 +8,56 @@ module Jobs return unless SiteSetting.approve_suspect_users return if SiteSetting.must_approve_users - users = User - .distinct - .activated - .human_users - .where(approved: false) - .joins(:user_profile, :user_stat) - .where("users.created_at <= ? AND users.created_at >= ?", 1.day.ago, 6.months.ago) - .where("LENGTH(COALESCE(user_profiles.bio_raw, user_profiles.website, '')) > 0") - .where("user_stats.posts_read_count <= 1 OR user_stats.topics_entered <= 1 OR user_stats.time_read < ?", 1.minute.to_i) - .joins("LEFT OUTER JOIN reviewables r ON r.target_id = users.id AND r.target_type = 'User'") - .where('r.id IS NULL') - .joins( - <<~SQL + users = + User + .distinct + .activated + .human_users + .where(approved: false) + .joins(:user_profile, :user_stat) + .where("users.created_at <= ? AND users.created_at >= ?", 1.day.ago, 6.months.ago) + .where("LENGTH(COALESCE(user_profiles.bio_raw, user_profiles.website, '')) > 0") + .where( + "user_stats.posts_read_count <= 1 OR user_stats.topics_entered <= 1 OR user_stats.time_read < ?", + 1.minute.to_i, + ) + .joins( + "LEFT OUTER JOIN reviewables r ON r.target_id = users.id AND r.target_type = 'User'", + ) + .where("r.id IS NULL") + .joins(<<~SQL) LEFT OUTER JOIN ( SELECT user_id FROM user_custom_fields WHERE user_custom_fields.name = 'import_id' ) AS ucf ON ucf.user_id = users.id SQL - ) - .where('ucf.user_id IS NULL') - .limit(10) + .where("ucf.user_id IS NULL") + .limit(10) users.each do |user| user_profile = user.user_profile - reviewable = ReviewableUser.needs_review!( - target: user, - created_by: Discourse.system_user, - reviewable_by_moderator: true, - payload: { - username: user.username, - name: user.name, - email: user.email, - bio: user_profile.bio_raw, - website: user_profile.website, - } - ) + reviewable = + ReviewableUser.needs_review!( + target: user, + created_by: Discourse.system_user, + reviewable_by_moderator: true, + payload: { + username: user.username, + name: user.name, + email: user.email, + bio: user_profile.bio_raw, + website: user_profile.website, + }, + ) if reviewable.created_new reviewable.add_score( Discourse.system_user, ReviewableScore.types[:needs_approval], reason: :suspect_user, - force_review: true + force_review: true, ) end end diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 6ecb928d07..dd38439845 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -23,7 +23,7 @@ module Jobs User, UserAvatar, Category, - TopicThumbnail + TopicThumbnail, ].each do |klass| klass.ensure_consistency! measure(klass) @@ -46,9 +46,7 @@ module Jobs def format_measure result = +"EnsureDbConsistency Times\n" - result << @measure_times.map do |name, duration| - " #{name}: #{duration}" - end.join("\n") + result << @measure_times.map { |name, duration| " #{name}: #{duration}" }.join("\n") result end @@ -59,11 +57,8 @@ module Jobs def measure(step = nil) @measure_now = Process.clock_gettime(Process::CLOCK_MONOTONIC) - if @measure_start - @measure_times << [step, @measure_now - @measure_start] - end + @measure_times << [step, @measure_now - @measure_start] if @measure_start @measure_start = @measure_now end - end end diff --git a/app/jobs/scheduled/ensure_s3_uploads_existence.rb b/app/jobs/scheduled/ensure_s3_uploads_existence.rb index 8a8e90643b..cf71f4253a 100644 --- a/app/jobs/scheduled/ensure_s3_uploads_existence.rb +++ b/app/jobs/scheduled/ensure_s3_uploads_existence.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class EnsureS3UploadsExistence < ::Jobs::Scheduled every 1.day @@ -11,7 +10,10 @@ module Jobs super ensure if @db_inventories - @db_inventories.values.each { |f| f.close; f.unlink } + @db_inventories.values.each do |f| + f.close + f.unlink + end end end @@ -27,18 +29,20 @@ module Jobs def execute(args) return if !executable? - require 's3_inventory' + require "s3_inventory" if !@db_inventories && Rails.configuration.multisite && GlobalSetting.use_s3? prepare_for_all_sites end - if @db_inventories && preloaded_inventory_file = @db_inventories[RailsMultisite::ConnectionManagement.current_db] + if @db_inventories && + preloaded_inventory_file = + @db_inventories[RailsMultisite::ConnectionManagement.current_db] S3Inventory.new( s3_helper, :upload, preloaded_inventory_file: preloaded_inventory_file, - preloaded_inventory_date: @inventory_date + preloaded_inventory_date: @inventory_date, ).backfill_etags_and_list_missing else S3Inventory.new(s3_helper, :upload).backfill_etags_and_list_missing diff --git a/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb b/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb index d176217d55..471490be0f 100644 --- a/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb +++ b/app/jobs/scheduled/fix_user_usernames_and_groups_names_clash.rb @@ -5,27 +5,24 @@ module Jobs every 1.week def execute(args) - User.joins("LEFT JOIN groups ON lower(groups.name) = users.username_lower") + User + .joins("LEFT JOIN groups ON lower(groups.name) = users.username_lower") .where("groups.id IS NOT NULL") .find_each do |user| + suffix = 1 + old_username = user.username - suffix = 1 - old_username = user.username + loop do + user.username = "#{old_username}#{suffix}" + suffix += 1 + break if user.valid? + end - loop do - user.username = "#{old_username}#{suffix}" - suffix += 1 - break if user.valid? + new_username = user.username + user.username = old_username + + UsernameChanger.new(user, new_username).change(asynchronous: false) end - - new_username = user.username - user.username = old_username - - UsernameChanger.new( - user, - new_username - ).change(asynchronous: false) - end end end end diff --git a/app/jobs/scheduled/grant_anniversary_badges.rb b/app/jobs/scheduled/grant_anniversary_badges.rb index a5246c3243..c244a2e92e 100644 --- a/app/jobs/scheduled/grant_anniversary_badges.rb +++ b/app/jobs/scheduled/grant_anniversary_badges.rb @@ -35,10 +35,9 @@ module Jobs HAVING COUNT(p.id) > 0 AND COUNT(ub.id) = 0 SQL - User.where(id: user_ids).find_each do |user| - BadgeGranter.grant(badge, user, created_at: end_date) - end + User + .where(id: user_ids) + .find_each { |user| BadgeGranter.grant(badge, user, created_at: end_date) } end - end end diff --git a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb index 3359b93417..ce169794bb 100644 --- a/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb +++ b/app/jobs/scheduled/grant_new_user_of_the_month_badges.rb @@ -13,9 +13,14 @@ module Jobs previous_month_beginning = 1.month.ago.beginning_of_month previous_month_end = 1.month.ago.end_of_month - return if UserBadge.where("badge_id = ? AND granted_at BETWEEN ? AND ?", - badge.id, previous_month_beginning, Time.zone.now - ).exists? + if UserBadge.where( + "badge_id = ? AND granted_at BETWEEN ? AND ?", + badge.id, + previous_month_beginning, + Time.zone.now, + ).exists? + return + end scores(previous_month_beginning, previous_month_end).each do |user_id, score| # Don't bother awarding to users who haven't received any likes @@ -24,9 +29,10 @@ module Jobs if user.badges.where(id: Badge::NewUserOfTheMonth).blank? BadgeGranter.grant(badge, user, created_at: previous_month_end) - SystemMessage.new(user).create('new_user_of_the_month', + SystemMessage.new(user).create( + "new_user_of_the_month", month_year: I18n.l(previous_month_beginning, format: :no_day), - url: "#{Discourse.base_url}/badges" + url: "#{Discourse.base_url}/badges", ) end end @@ -65,7 +71,7 @@ module Jobs LEFT OUTER JOIN topics AS t ON t.id = p.topic_id WHERE u.active AND u.id > 0 - AND u.id NOT IN (#{current_owners.join(',')}) + AND u.id NOT IN (#{current_owners.join(",")}) AND NOT u.staged AND NOT u.admin AND NOT u.moderator @@ -87,10 +93,9 @@ module Jobs *DB.query_single( sql, min_user_created_at: min_user_created_at, - max_user_created_at: max_user_created_at + max_user_created_at: max_user_created_at, ) ] end - end end diff --git a/app/jobs/scheduled/heartbeat.rb b/app/jobs/scheduled/heartbeat.rb index 16ab8b77bb..04b807ea8e 100644 --- a/app/jobs/scheduled/heartbeat.rb +++ b/app/jobs/scheduled/heartbeat.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - # used to ensure at least 1 sidekiq is running correctly class Heartbeat < ::Jobs::Scheduled every 3.minute diff --git a/app/jobs/scheduled/ignored_users_summary.rb b/app/jobs/scheduled/ignored_users_summary.rb index 59483c2b52..962176a69d 100644 --- a/app/jobs/scheduled/ignored_users_summary.rb +++ b/app/jobs/scheduled/ignored_users_summary.rb @@ -24,7 +24,11 @@ module Jobs private def notify_user(user) - params = SystemMessage.new(user).defaults.merge(ignores_threshold: SiteSetting.ignored_users_count_message_threshold) + params = + SystemMessage + .new(user) + .defaults + .merge(ignores_threshold: SiteSetting.ignored_users_count_message_threshold) title = I18n.t("system_messages.ignored_users_summary.subject_template") raw = I18n.t("system_messages.ignored_users_summary.text_body_template", params) @@ -35,7 +39,8 @@ module Jobs subtype: TopicSubtype.system_message, title: title, raw: raw, - skip_validations: true) + skip_validations: true, + ) IgnoredUser.where(ignored_user_id: user.id).update_all(summarized_at: Time.zone.now) end end diff --git a/app/jobs/scheduled/invalidate_inactive_admins.rb b/app/jobs/scheduled/invalidate_inactive_admins.rb index bc3e2500a1..3e78dae5a3 100644 --- a/app/jobs/scheduled/invalidate_inactive_admins.rb +++ b/app/jobs/scheduled/invalidate_inactive_admins.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class InvalidateInactiveAdmins < ::Jobs::Scheduled every 1.day @@ -10,29 +9,38 @@ module Jobs timestamp = SiteSetting.invalidate_inactive_admin_email_after_days.days.ago - User.human_users + User + .human_users .where(admin: true) .where(active: true) - .where('last_seen_at < ?', timestamp) - .where("NOT EXISTS ( SELECT 1 from api_keys WHERE api_keys.user_id = users.id AND COALESCE(last_used_at, updated_at) > ? )", timestamp) - .where("NOT EXISTS ( SELECT 1 from posts WHERE posts.user_id = users.id AND created_at > ?)", timestamp) + .where("last_seen_at < ?", timestamp) + .where( + "NOT EXISTS ( SELECT 1 from api_keys WHERE api_keys.user_id = users.id AND COALESCE(last_used_at, updated_at) > ? )", + timestamp, + ) + .where( + "NOT EXISTS ( SELECT 1 from posts WHERE posts.user_id = users.id AND created_at > ?)", + timestamp, + ) .each do |user| + User.transaction do + user.deactivate(Discourse.system_user) + user.email_tokens.update_all(confirmed: false, expired: true) - User.transaction do - user.deactivate(Discourse.system_user) - user.email_tokens.update_all(confirmed: false, expired: true) + reason = + I18n.t( + "user.deactivated_by_inactivity", + count: SiteSetting.invalidate_inactive_admin_email_after_days, + ) + StaffActionLogger.new(Discourse.system_user).log_user_deactivate(user, reason) - reason = I18n.t("user.deactivated_by_inactivity", count: SiteSetting.invalidate_inactive_admin_email_after_days) - StaffActionLogger.new(Discourse.system_user).log_user_deactivate(user, reason) - - Discourse.authenticators.each do |authenticator| - if authenticator.can_revoke? && authenticator.description_for_user(user).present? - authenticator.revoke(user) + Discourse.authenticators.each do |authenticator| + if authenticator.can_revoke? && authenticator.description_for_user(user).present? + authenticator.revoke(user) + end end end end - end end end - end diff --git a/app/jobs/scheduled/migrate_upload_scheme.rb b/app/jobs/scheduled/migrate_upload_scheme.rb index 601ad3afd8..bccae4d7b8 100644 --- a/app/jobs/scheduled/migrate_upload_scheme.rb +++ b/app/jobs/scheduled/migrate_upload_scheme.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class MigrateUploadScheme < ::Jobs::Scheduled every 10.minutes sidekiq_options retry: false @@ -10,51 +9,44 @@ module Jobs return unless SiteSetting.migrate_to_new_scheme # clean up failed uploads - Upload.where("created_at < ?", 1.hour.ago) + Upload + .where("created_at < ?", 1.hour.ago) .where("LENGTH(COALESCE(url, '')) = 0") - .find_each do |upload| - - upload.destroy! - end + .find_each { |upload| upload.destroy! } # migrate uploads to new scheme problems = Upload.migrate_to_new_scheme(limit: 50) problems.each do |hash| upload_id = hash[:upload].id - Discourse.handle_job_exception(hash[:ex], error_context(args, "Migrating upload id #{upload_id}", upload_id: upload_id)) + Discourse.handle_job_exception( + hash[:ex], + error_context(args, "Migrating upload id #{upload_id}", upload_id: upload_id), + ) end # clean up failed optimized images OptimizedImage .where("LENGTH(COALESCE(url, '')) = 0") - .find_each do |optimized_image| - - optimized_image.destroy! - end + .find_each { |optimized_image| optimized_image.destroy! } # Clean up orphan optimized images OptimizedImage .joins("LEFT JOIN uploads ON optimized_images.upload_id = uploads.id") .where("uploads.id IS NULL") - .find_each do |optimized_image| - - optimized_image.destroy! - end + .find_each { |optimized_image| optimized_image.destroy! } # Clean up optimized images that needs to be regenerated - OptimizedImage.joins(:upload) + OptimizedImage + .joins(:upload) .where("optimized_images.url NOT LIKE '%/optimized/_X/%'") .where("uploads.url LIKE '%/original/_X/%'") .limit(50) .find_each do |optimized_image| - - upload = optimized_image.upload - optimized_image.destroy! - upload.rebake_posts_on_old_scheme - end + upload = optimized_image.upload + optimized_image.destroy! + upload.rebake_posts_on_old_scheme + end end - end - end diff --git a/app/jobs/scheduled/old_keys_reminder.rb b/app/jobs/scheduled/old_keys_reminder.rb index de824fa5d0..cec2108b54 100644 --- a/app/jobs/scheduled/old_keys_reminder.rb +++ b/app/jobs/scheduled/old_keys_reminder.rb @@ -17,26 +17,34 @@ module Jobs raw: body, archetype: Archetype.private_message, target_usernames: admins.map(&:username), - validate: false + validate: false, ) end private def old_site_settings_keys - @old_site_settings_keys ||= SiteSetting.secret_settings.each_with_object([]) do |secret_name, old_keys| - site_setting = SiteSetting.find_by(name: secret_name) - next if site_setting&.value.blank? - next if site_setting.updated_at + OLD_CREDENTIALS_PERIOD > Time.zone.now - old_keys << site_setting - end.sort_by { |key| key.updated_at } + @old_site_settings_keys ||= + SiteSetting + .secret_settings + .each_with_object([]) do |secret_name, old_keys| + site_setting = SiteSetting.find_by(name: secret_name) + next if site_setting&.value.blank? + next if site_setting.updated_at + OLD_CREDENTIALS_PERIOD > Time.zone.now + old_keys << site_setting + end + .sort_by { |key| key.updated_at } end def old_api_keys - @old_api_keys ||= ApiKey.all.order(created_at: :asc).each_with_object([]) do |api_key, old_keys| - next if api_key.created_at + OLD_CREDENTIALS_PERIOD > Time.zone.now - old_keys << api_key - end + @old_api_keys ||= + ApiKey + .all + .order(created_at: :asc) + .each_with_object([]) do |api_key, old_keys| + next if api_key.created_at + OLD_CREDENTIALS_PERIOD > Time.zone.now + old_keys << api_key + end end def admins @@ -45,20 +53,24 @@ module Jobs def message_exists? message = Topic.private_messages.with_deleted.find_by(title: title) - message && message.created_at + SiteSetting.send_old_credential_reminder_days.to_i.days > Time.zone.now + message && + message.created_at + SiteSetting.send_old_credential_reminder_days.to_i.days > Time.zone.now end def title - I18n.t('old_keys_reminder.title') + I18n.t("old_keys_reminder.title") end def body - I18n.t('old_keys_reminder.body', keys: keys_list) + I18n.t("old_keys_reminder.body", keys: keys_list) end def keys_list - messages = old_site_settings_keys.map { |key| "#{key.name} - #{key.updated_at.to_date.to_fs(:db)}" } - old_api_keys.each_with_object(messages) { |key, array| array << "#{[key.description, key.user&.username, key.created_at.to_date.to_fs(:db)].compact.join(" - ")}" } + messages = + old_site_settings_keys.map { |key| "#{key.name} - #{key.updated_at.to_date.to_fs(:db)}" } + old_api_keys.each_with_object(messages) do |key, array| + array << "#{[key.description, key.user&.username, key.created_at.to_date.to_fs(:db)].compact.join(" - ")}" + end messages.join("\n") end end diff --git a/app/jobs/scheduled/pending_queued_posts_reminder.rb b/app/jobs/scheduled/pending_queued_posts_reminder.rb index 22b84fc701..78f5220adb 100644 --- a/app/jobs/scheduled/pending_queued_posts_reminder.rb +++ b/app/jobs/scheduled/pending_queued_posts_reminder.rb @@ -2,7 +2,6 @@ module Jobs class PendingQueuedPostsReminder < ::Jobs::Scheduled - every 15.minutes def execute(args) @@ -16,8 +15,16 @@ module Jobs target_group_names: Group[:moderators].name, archetype: Archetype.private_message, subtype: TopicSubtype.system_message, - title: I18n.t('system_messages.queued_posts_reminder.subject_template', count: queued_post_ids.size), - raw: I18n.t('system_messages.queued_posts_reminder.text_body_template', base_url: Discourse.base_url) + title: + I18n.t( + "system_messages.queued_posts_reminder.subject_template", + count: queued_post_ids.size, + ), + raw: + I18n.t( + "system_messages.queued_posts_reminder.text_body_template", + base_url: Discourse.base_url, + ), ) self.last_notified_id = queued_post_ids.max @@ -27,9 +34,10 @@ module Jobs end def should_notify_ids - ReviewableQueuedPost.pending.where( - 'created_at < ?', SiteSetting.notify_about_queued_posts_after.to_f.hours.ago - ).pluck(:id) + ReviewableQueuedPost + .pending + .where("created_at < ?", SiteSetting.notify_about_queued_posts_after.to_f.hours.ago) + .pluck(:id) end def last_notified_id diff --git a/app/jobs/scheduled/pending_reviewables_reminder.rb b/app/jobs/scheduled/pending_reviewables_reminder.rb index 1a94e70b83..deb784934e 100644 --- a/app/jobs/scheduled/pending_reviewables_reminder.rb +++ b/app/jobs/scheduled/pending_reviewables_reminder.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class PendingReviewablesReminder < ::Jobs::Scheduled every 15.minutes @@ -11,25 +10,30 @@ module Jobs @sent_reminder = false if SiteSetting.notify_about_flags_after > 0 - reviewable_ids = Reviewable - .pending - .default_visible - .where('latest_score < ?', SiteSetting.notify_about_flags_after.to_f.hours.ago) - .order('id DESC') - .pluck(:id) + reviewable_ids = + Reviewable + .pending + .default_visible + .where("latest_score < ?", SiteSetting.notify_about_flags_after.to_f.hours.ago) + .order("id DESC") + .pluck(:id) if reviewable_ids.size > 0 && self.class.last_notified_id < reviewable_ids[0] usernames = active_moderator_usernames - mentions = usernames.size > 0 ? "@#{usernames.join(', @')} " : "" + mentions = usernames.size > 0 ? "@#{usernames.join(", @")} " : "" - message = GroupMessage.new( - Group[:moderators].name, - 'reviewables_reminder', - { - limit_once_per: false, - message_params: { mentions: mentions, count: SiteSetting.notify_about_flags_after } - } - ) + message = + GroupMessage.new( + Group[:moderators].name, + "reviewables_reminder", + { + limit_once_per: false, + message_params: { + mentions: mentions, + count: SiteSetting.notify_about_flags_after, + }, + }, + ) Topic.transaction do message.delete_previous!(match_raw: false) @@ -58,13 +62,7 @@ module Jobs end def active_moderator_usernames - User.where(moderator: true) - .human_users - .order('last_seen_at DESC') - .limit(3) - .pluck(:username) + User.where(moderator: true).human_users.order("last_seen_at DESC").limit(3).pluck(:username) end - end - end diff --git a/app/jobs/scheduled/pending_users_reminder.rb b/app/jobs/scheduled/pending_users_reminder.rb index 0d8a2d393f..0ed3fbdc57 100644 --- a/app/jobs/scheduled/pending_users_reminder.rb +++ b/app/jobs/scheduled/pending_users_reminder.rb @@ -1,15 +1,18 @@ # frozen_string_literal: true module Jobs - class PendingUsersReminder < ::Jobs::Scheduled every 5.minutes def execute(args) if SiteSetting.must_approve_users && SiteSetting.pending_users_reminder_delay_minutes >= 0 - query = AdminUserIndexQuery.new(query: 'pending', stats: false).find_users_query # default order is: users.created_at DESC + query = AdminUserIndexQuery.new(query: "pending", stats: false).find_users_query # default order is: users.created_at DESC if SiteSetting.pending_users_reminder_delay_minutes > 0 - query = query.where('users.created_at < ?', SiteSetting.pending_users_reminder_delay_minutes.minutes.ago) + query = + query.where( + "users.created_at < ?", + SiteSetting.pending_users_reminder_delay_minutes.minutes.ago, + ) end newest_username = query.limit(1).select(:username).first&.username @@ -19,17 +22,24 @@ module Jobs count = query.count if count > 0 - target_usernames = Group[:moderators].users.map do |user| - next if user.bot? + target_usernames = + Group[:moderators] + .users + .map do |user| + next if user.bot? - unseen_count = user.notifications.joins(:topic) - .where("notifications.id > ?", user.seen_notification_id) - .where("notifications.read = false") - .where("topics.subtype = ?", TopicSubtype.pending_users_reminder) - .count + unseen_count = + user + .notifications + .joins(:topic) + .where("notifications.id > ?", user.seen_notification_id) + .where("notifications.read = false") + .where("topics.subtype = ?", TopicSubtype.pending_users_reminder) + .count - unseen_count == 0 ? user.username : nil - end.compact + unseen_count == 0 ? user.username : nil + end + .compact unless target_usernames.empty? PostCreator.create( @@ -37,8 +47,14 @@ module Jobs target_usernames: target_usernames, archetype: Archetype.private_message, subtype: TopicSubtype.pending_users_reminder, - title: I18n.t("system_messages.pending_users_reminder.subject_template", count: count), - raw: I18n.t("system_messages.pending_users_reminder.text_body_template", count: count, base_url: Discourse.base_url) + title: + I18n.t("system_messages.pending_users_reminder.subject_template", count: count), + raw: + I18n.t( + "system_messages.pending_users_reminder.text_body_template", + count: count, + base_url: Discourse.base_url, + ), ) self.previous_newest_username = newest_username @@ -60,7 +76,5 @@ module Jobs def previous_newest_username_cache_key "pending-users-reminder:newest-username" end - end - end diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index 5c5da8bac6..f74adc180c 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - # This job will run on a regular basis to update statistics and denormalized data. # If it does not run, the site will not function properly. class PeriodicalUpdates < ::Jobs::Scheduled @@ -25,11 +24,15 @@ module Jobs ScoreCalculator.new.calculate(args) # Forces rebake of old posts where needed, as long as no system avatars need updating - if !SiteSetting.automatically_download_gravatars || !UserAvatar.where("last_gravatar_download_attempt IS NULL").limit(1).first + if !SiteSetting.automatically_download_gravatars || + !UserAvatar.where("last_gravatar_download_attempt IS NULL").limit(1).first problems = Post.rebake_old(SiteSetting.rebake_old_posts_count, priority: :ultra_low) problems.each do |hash| post_id = hash[:post].id - Discourse.handle_job_exception(hash[:ex], error_context(args, "Rebaking post id #{post_id}", post_id: post_id)) + Discourse.handle_job_exception( + hash[:ex], + error_context(args, "Rebaking post id #{post_id}", post_id: post_id), + ) end end @@ -37,20 +40,19 @@ module Jobs problems = UserProfile.rebake_old(250) problems.each do |hash| user_id = hash[:profile].user_id - Discourse.handle_job_exception(hash[:ex], error_context(args, "Rebaking user id #{user_id}", user_id: user_id)) + Discourse.handle_job_exception( + hash[:ex], + error_context(args, "Rebaking user id #{user_id}", user_id: user_id), + ) end offset = (SiteSetting.max_new_topics).to_i - last_new_topic = Topic.order('created_at desc').offset(offset).select(:created_at).first - if last_new_topic - SiteSetting.min_new_topics_time = last_new_topic.created_at.to_i - end + last_new_topic = Topic.order("created_at desc").offset(offset).select(:created_at).first + SiteSetting.min_new_topics_time = last_new_topic.created_at.to_i if last_new_topic Category.auto_bump_topic! nil end - end - end diff --git a/app/jobs/scheduled/poll_mailbox.rb b/app/jobs/scheduled/poll_mailbox.rb index 19e5a09d65..f4535bc077 100644 --- a/app/jobs/scheduled/poll_mailbox.rb +++ b/app/jobs/scheduled/poll_mailbox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'net/pop' +require "net/pop" module Jobs class PollMailbox < ::Jobs::Scheduled @@ -43,26 +43,37 @@ module Jobs process_popmail(mail_string) p.delete if SiteSetting.pop3_polling_delete_from_server? rescue => e - Discourse.handle_job_exception(e, error_context(@args, "Failed to process incoming email.")) + Discourse.handle_job_exception( + e, + error_context(@args, "Failed to process incoming email."), + ) end end rescue Net::OpenTimeout => e count = Discourse.redis.incr(POLL_MAILBOX_TIMEOUT_ERROR_KEY).to_i - Discourse.redis.expire( - POLL_MAILBOX_TIMEOUT_ERROR_KEY, - SiteSetting.pop3_polling_period_mins.minutes * 3 - ) if count == 1 + if count == 1 + Discourse.redis.expire( + POLL_MAILBOX_TIMEOUT_ERROR_KEY, + SiteSetting.pop3_polling_period_mins.minutes * 3, + ) + end if count > 3 Discourse.redis.del(POLL_MAILBOX_TIMEOUT_ERROR_KEY) mark_as_errored! - add_admin_dashboard_problem_message('dashboard.poll_pop3_timeout') - Discourse.handle_job_exception(e, error_context(@args, "Connecting to '#{SiteSetting.pop3_polling_host}' for polling emails.")) + add_admin_dashboard_problem_message("dashboard.poll_pop3_timeout") + Discourse.handle_job_exception( + e, + error_context( + @args, + "Connecting to '#{SiteSetting.pop3_polling_host}' for polling emails.", + ), + ) end rescue Net::POPAuthenticationError => e mark_as_errored! - add_admin_dashboard_problem_message('dashboard.poll_pop3_auth_error') + add_admin_dashboard_problem_message("dashboard.poll_pop3_auth_error") Discourse.handle_job_exception(e, error_context(@args, "Signing in to poll incoming emails.")) end @@ -75,7 +86,7 @@ module Jobs def mail_too_old?(mail_string) mail = Mail.new(mail_string) - date_header = mail.header['Date'] + date_header = mail.header["Date"] return false if date_header.blank? date = Time.parse(date_header.to_s) @@ -90,9 +101,8 @@ module Jobs def add_admin_dashboard_problem_message(i18n_key) AdminDashboardData.add_problem_message( i18n_key, - SiteSetting.pop3_polling_period_mins.minutes + 5.minutes + SiteSetting.pop3_polling_period_mins.minutes + 5.minutes, ) end - end end diff --git a/app/jobs/scheduled/process_user_notification_schedules.rb b/app/jobs/scheduled/process_user_notification_schedules.rb index 1e793e01f0..1f8bbe4f83 100644 --- a/app/jobs/scheduled/process_user_notification_schedules.rb +++ b/app/jobs/scheduled/process_user_notification_schedules.rb @@ -5,13 +5,19 @@ module Jobs every 1.day def execute(args) - UserNotificationSchedule.enabled.includes(:user).each do |schedule| - begin - schedule.create_do_not_disturb_timings - rescue => e - Discourse.warn_exception(e, message: "Failed to process user_notification_schedule with ID #{schedule.id}") + UserNotificationSchedule + .enabled + .includes(:user) + .each do |schedule| + begin + schedule.create_do_not_disturb_timings + rescue => e + Discourse.warn_exception( + e, + message: "Failed to process user_notification_schedule with ID #{schedule.id}", + ) + end end - end end end end diff --git a/app/jobs/scheduled/purge_deleted_uploads.rb b/app/jobs/scheduled/purge_deleted_uploads.rb index 73c3de5054..ca5376e04f 100644 --- a/app/jobs/scheduled/purge_deleted_uploads.rb +++ b/app/jobs/scheduled/purge_deleted_uploads.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class PurgeDeletedUploads < ::Jobs::Scheduled every 1.day @@ -9,7 +8,5 @@ module Jobs grace_period = SiteSetting.purge_deleted_uploads_grace_period_days Discourse.store.purge_tombstone(grace_period) end - end - end diff --git a/app/jobs/scheduled/reindex_search.rb b/app/jobs/scheduled/reindex_search.rb index 26215cdc7a..db54093bad 100644 --- a/app/jobs/scheduled/reindex_search.rb +++ b/app/jobs/scheduled/reindex_search.rb @@ -113,7 +113,11 @@ module Jobs def load_problem_category_ids(limit) Category .joins("LEFT JOIN category_search_data ON category_id = categories.id") - .where("category_search_data.locale IS NULL OR category_search_data.locale != ? OR category_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::CATEGORY_INDEX_VERSION) + .where( + "category_search_data.locale IS NULL OR category_search_data.locale != ? OR category_search_data.version != ?", + SiteSetting.default_locale, + SearchIndexer::CATEGORY_INDEX_VERSION, + ) .order("categories.id ASC") .limit(limit) .pluck(:id) @@ -122,7 +126,11 @@ module Jobs def load_problem_tag_ids(limit) Tag .joins("LEFT JOIN tag_search_data ON tag_id = tags.id") - .where("tag_search_data.locale IS NULL OR tag_search_data.locale != ? OR tag_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TAG_INDEX_VERSION) + .where( + "tag_search_data.locale IS NULL OR tag_search_data.locale != ? OR tag_search_data.version != ?", + SiteSetting.default_locale, + SearchIndexer::TAG_INDEX_VERSION, + ) .order("tags.id ASC") .limit(limit) .pluck(:id) @@ -131,7 +139,11 @@ module Jobs def load_problem_topic_ids(limit) Topic .joins("LEFT JOIN topic_search_data ON topic_id = topics.id") - .where("topic_search_data.locale IS NULL OR topic_search_data.locale != ? OR topic_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::TOPIC_INDEX_VERSION) + .where( + "topic_search_data.locale IS NULL OR topic_search_data.locale != ? OR topic_search_data.version != ?", + SiteSetting.default_locale, + SearchIndexer::TOPIC_INDEX_VERSION, + ) .order("topics.id DESC") .limit(limit) .pluck(:id) @@ -143,7 +155,11 @@ module Jobs .joins("LEFT JOIN post_search_data ON post_id = posts.id") .where("posts.raw != ''") .where("topics.deleted_at IS NULL") - .where("post_search_data.locale IS NULL OR post_search_data.locale != ? OR post_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::POST_INDEX_VERSION) + .where( + "post_search_data.locale IS NULL OR post_search_data.locale != ? OR post_search_data.version != ?", + SiteSetting.default_locale, + SearchIndexer::POST_INDEX_VERSION, + ) .order("posts.id DESC") .limit(limit) .pluck(:id) @@ -152,11 +168,14 @@ module Jobs def load_problem_user_ids(limit) User .joins("LEFT JOIN user_search_data ON user_id = users.id") - .where("user_search_data.locale IS NULL OR user_search_data.locale != ? OR user_search_data.version != ?", SiteSetting.default_locale, SearchIndexer::USER_INDEX_VERSION) + .where( + "user_search_data.locale IS NULL OR user_search_data.locale != ? OR user_search_data.version != ?", + SiteSetting.default_locale, + SearchIndexer::USER_INDEX_VERSION, + ) .order("users.id ASC") .limit(limit) .pluck(:id) end - end end diff --git a/app/jobs/scheduled/reviewable_priorities.rb b/app/jobs/scheduled/reviewable_priorities.rb index b9c5e4c8b5..55c54ba78c 100644 --- a/app/jobs/scheduled/reviewable_priorities.rb +++ b/app/jobs/scheduled/reviewable_priorities.rb @@ -18,7 +18,9 @@ class Jobs::ReviewablePriorities < ::Jobs::Scheduled reviewable_count = Reviewable.approved.where("score > ?", min_priority_threshold).count return if reviewable_count < self.class.min_reviewables - res = DB.query_single(<<~SQL, target_count: self.class.target_count, min_priority: min_priority_threshold) + res = + DB.query_single( + <<~SQL, SELECT COALESCE(PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY score), 0.0) AS medium, COALESCE(PERCENTILE_DISC(0.85) WITHIN GROUP (ORDER BY score), 0.0) AS high FROM ( @@ -30,15 +32,14 @@ class Jobs::ReviewablePriorities < ::Jobs::Scheduled HAVING COUNT(*) >= :target_count ) AS x SQL + target_count: self.class.target_count, + min_priority: min_priority_threshold, + ) return unless res && res.size == 2 medium, high = res - Reviewable.set_priorities( - low: min_priority_threshold, - medium: medium, - high: high - ) + Reviewable.set_priorities(low: min_priority_threshold, medium: medium, high: high) end end diff --git a/app/jobs/scheduled/schedule_backup.rb b/app/jobs/scheduled/schedule_backup.rb index 703a4515c4..a4e0bcb893 100644 --- a/app/jobs/scheduled/schedule_backup.rb +++ b/app/jobs/scheduled/schedule_backup.rb @@ -30,7 +30,7 @@ module Jobs Discourse.system_user, :backup_failed, target_group_names: Group[:admins].name, - logs: "#{ex}\n" + ex.backtrace.join("\n") + logs: "#{ex}\n" + ex.backtrace.join("\n"), ) end end diff --git a/app/jobs/scheduled/tl3_promotions.rb b/app/jobs/scheduled/tl3_promotions.rb index af1d6350ff..9de8f711d4 100644 --- a/app/jobs/scheduled/tl3_promotions.rb +++ b/app/jobs/scheduled/tl3_promotions.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - class Tl3Promotions < ::Jobs::Scheduled daily at: 4.hours @@ -9,40 +8,40 @@ module Jobs if SiteSetting.default_trust_level < 3 # Demotions demoted_user_ids = [] - User.real - .joins("LEFT JOIN (SELECT gu.user_id, MAX(g.grant_trust_level) AS group_granted_trust_level FROM groups g, group_users gu WHERE g.id = gu.group_id GROUP BY gu.user_id) tl ON users.id = tl.user_id") - .where( - trust_level: TrustLevel[3], - manual_locked_trust_level: nil + User + .real + .joins( + "LEFT JOIN (SELECT gu.user_id, MAX(g.grant_trust_level) AS group_granted_trust_level FROM groups g, group_users gu WHERE g.id = gu.group_id GROUP BY gu.user_id) tl ON users.id = tl.user_id", + ) + .where(trust_level: TrustLevel[3], manual_locked_trust_level: nil) + .where( + "group_granted_trust_level IS NULL OR group_granted_trust_level < ?", + TrustLevel[3], ) - .where("group_granted_trust_level IS NULL OR group_granted_trust_level < ?", TrustLevel[3]) .find_each do |u| - # Don't demote too soon after being promoted - next if u.on_tl3_grace_period? + # Don't demote too soon after being promoted + next if u.on_tl3_grace_period? - if Promotion.tl3_lost?(u) - demoted_user_ids << u.id - Promotion.new(u).change_trust_level!(TrustLevel[2]) + if Promotion.tl3_lost?(u) + demoted_user_ids << u.id + Promotion.new(u).change_trust_level!(TrustLevel[2]) + end end - end end # Promotions - User.real.not_suspended.where( - trust_level: TrustLevel[2], - manual_locked_trust_level: nil - ).where.not(id: demoted_user_ids) + User + .real + .not_suspended + .where(trust_level: TrustLevel[2], manual_locked_trust_level: nil) + .where.not(id: demoted_user_ids) .joins(:user_stat) .where("user_stats.days_visited >= ?", SiteSetting.tl3_requires_days_visited) .where("user_stats.topics_entered >= ?", SiteSetting.tl3_requires_topics_viewed_all_time) .where("user_stats.posts_read_count >= ?", SiteSetting.tl3_requires_posts_read_all_time) .where("user_stats.likes_given >= ?", SiteSetting.tl3_requires_likes_given) .where("user_stats.likes_received >= ?", SiteSetting.tl3_requires_likes_received) - .find_each do |u| - Promotion.new(u).review_tl2 - end - + .find_each { |u| Promotion.new(u).review_tl2 } end end - end diff --git a/app/jobs/scheduled/topic_timer_enqueuer.rb b/app/jobs/scheduled/topic_timer_enqueuer.rb index 67204c4200..13322184e6 100644 --- a/app/jobs/scheduled/topic_timer_enqueuer.rb +++ b/app/jobs/scheduled/topic_timer_enqueuer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - # Runs periodically to look through topic timers that are ready to execute, # and enqueues their related jobs. # @@ -13,13 +12,15 @@ module Jobs def execute(_args = nil) TopicTimer.pending_timers.find_each do |timer| - # the typed job may not enqueue if it has already # been scheduled with enqueue_at begin timer.enqueue_typed_job rescue => err - Discourse.warn_exception(err, message: "Error when attempting to enqueue topic timer job for timer #{timer.id}") + Discourse.warn_exception( + err, + message: "Error when attempting to enqueue topic timer job for timer #{timer.id}", + ) end end end diff --git a/app/jobs/scheduled/unsilence_users.rb b/app/jobs/scheduled/unsilence_users.rb index f6f5f5e0ff..dbf0a90795 100644 --- a/app/jobs/scheduled/unsilence_users.rb +++ b/app/jobs/scheduled/unsilence_users.rb @@ -5,9 +5,9 @@ module Jobs every 15.minutes def execute(args) - User.where("silenced_till IS NOT NULL AND silenced_till < now()").find_each do |user| - UserSilencer.unsilence(user, Discourse.system_user) - end + User + .where("silenced_till IS NOT NULL AND silenced_till < now()") + .find_each { |user| UserSilencer.unsilence(user, Discourse.system_user) } end end end diff --git a/app/jobs/scheduled/update_animated_uploads.rb b/app/jobs/scheduled/update_animated_uploads.rb index 500e21d7b1..51ea6204a4 100644 --- a/app/jobs/scheduled/update_animated_uploads.rb +++ b/app/jobs/scheduled/update_animated_uploads.rb @@ -12,11 +12,11 @@ module Jobs .where(animated: nil) .limit(MAX_PROCESSED_GIF_IMAGES) .each do |upload| - uri = Discourse.store.path_for(upload) || upload.url - upload.animated = FastImage.animated?(uri) - upload.save(validate: false) - upload.optimized_images.destroy_all if upload.animated - end + uri = Discourse.store.path_for(upload) || upload.url + upload.animated = FastImage.animated?(uri) + upload.save(validate: false) + upload.optimized_images.destroy_all if upload.animated + end nil end diff --git a/app/jobs/scheduled/version_check.rb b/app/jobs/scheduled/version_check.rb index 37eebf01f9..6df0e103f8 100644 --- a/app/jobs/scheduled/version_check.rb +++ b/app/jobs/scheduled/version_check.rb @@ -5,25 +5,23 @@ module Jobs every 1.day def execute(args) - if SiteSetting.version_checks? && (DiscourseUpdates.updated_at.nil? || DiscourseUpdates.updated_at < (1.minute.ago)) + if SiteSetting.version_checks? && + (DiscourseUpdates.updated_at.nil? || DiscourseUpdates.updated_at < (1.minute.ago)) begin prev_missing_versions_count = DiscourseUpdates.missing_versions_count || 0 json = DiscourseHub.discourse_version_check DiscourseUpdates.last_installed_version = Discourse::VERSION::STRING - DiscourseUpdates.latest_version = json['latestVersion'] - DiscourseUpdates.critical_updates_available = json['criticalUpdates'] - DiscourseUpdates.missing_versions_count = json['missingVersionsCount'] + DiscourseUpdates.latest_version = json["latestVersion"] + DiscourseUpdates.critical_updates_available = json["criticalUpdates"] + DiscourseUpdates.missing_versions_count = json["missingVersionsCount"] DiscourseUpdates.updated_at = Time.zone.now - DiscourseUpdates.missing_versions = json['versions'] - - if SiteSetting.new_version_emails && - json['missingVersionsCount'] > (0) && - prev_missing_versions_count < (json['missingVersionsCount'].to_i) + DiscourseUpdates.missing_versions = json["versions"] + if SiteSetting.new_version_emails && json["missingVersionsCount"] > (0) && + prev_missing_versions_count < (json["missingVersionsCount"].to_i) message = VersionMailer.send_notice Email::Sender.new(message, :new_version).send - end rescue => e raise e unless Rails.env.development? # Fail version check silently in development mode @@ -31,6 +29,5 @@ module Jobs end true end - end end diff --git a/app/jobs/scheduled/weekly.rb b/app/jobs/scheduled/weekly.rb index a020fa11f1..ae023bdc34 100644 --- a/app/jobs/scheduled/weekly.rb +++ b/app/jobs/scheduled/weekly.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Jobs - # This job will run on a regular basis to update statistics and denormalized data. # If it does not run, the site will not function properly. class Weekly < ::Jobs::Scheduled diff --git a/app/mailers/admin_confirmation_mailer.rb b/app/mailers/admin_confirmation_mailer.rb index 9b6ffa2409..2cc20258db 100644 --- a/app/mailers/admin_confirmation_mailer.rb +++ b/app/mailers/admin_confirmation_mailer.rb @@ -6,10 +6,10 @@ class AdminConfirmationMailer < ActionMailer::Base def send_email(to_address, target_email, target_username, token) build_email( to_address, - template: 'admin_confirmation_mailer', + template: "admin_confirmation_mailer", target_email: target_email, target_username: target_username, - admin_confirm_url: confirm_admin_url(token: token, host: Discourse.base_url) + admin_confirm_url: confirm_admin_url(token: token, host: Discourse.base_url), ) end end diff --git a/app/mailers/download_backup_mailer.rb b/app/mailers/download_backup_mailer.rb index 62d5e58368..be0b26ffed 100644 --- a/app/mailers/download_backup_mailer.rb +++ b/app/mailers/download_backup_mailer.rb @@ -4,6 +4,6 @@ class DownloadBackupMailer < ActionMailer::Base include Email::BuildEmailHelper def send_email(to_address, backup_file_path) - build_email(to_address, template: 'download_backup_mailer', backup_file_path: backup_file_path) + build_email(to_address, template: "download_backup_mailer", backup_file_path: backup_file_path) end end diff --git a/app/mailers/group_smtp_mailer.rb b/app/mailers/group_smtp_mailer.rb index c394439026..5d0fe11f7a 100644 --- a/app/mailers/group_smtp_mailer.rb +++ b/app/mailers/group_smtp_mailer.rb @@ -4,7 +4,7 @@ class GroupSmtpMailer < ActionMailer::Base include Email::BuildEmailHelper def send_mail(from_group, to_address, post, cc_addresses: nil, bcc_addresses: nil) - raise 'SMTP is disabled' if !SiteSetting.enable_smtp + raise "SMTP is disabled" if !SiteSetting.enable_smtp op_incoming_email = post.topic.first_post.incoming_email recipient_user = User.find_by_email(to_address, primary: true) @@ -17,7 +17,7 @@ class GroupSmtpMailer < ActionMailer::Base password: from_group.email_password, authentication: GlobalSetting.smtp_authentication, enable_starttls_auto: from_group.smtp_ssl, - return_response: true + return_response: true, } group_name = from_group.name_full_preferred @@ -37,17 +37,17 @@ class GroupSmtpMailer < ActionMailer::Base private_reply: post.topic.private_message?, participants: UserNotifications.participants(post, recipient_user, reveal_staged_email: true), include_respond_instructions: true, - template: 'user_notifications.user_posted_pm', + template: "user_notifications.user_posted_pm", use_topic_title_subject: true, topic_title: op_incoming_email&.subject || post.topic.title, add_re_to_subject: true, locale: SiteSetting.default_locale, delivery_method_options: delivery_options, from: from_group.smtp_from_address, - from_alias: I18n.t('email_from_without_site', group_name: group_name), + from_alias: I18n.t("email_from_without_site", group_name: group_name), html_override: html_override(post), cc: cc_addresses, - bcc: bcc_addresses + bcc: bcc_addresses, ) end @@ -55,7 +55,7 @@ class GroupSmtpMailer < ActionMailer::Base def html_override(post) UserNotificationRenderer.render( - template: 'email/notification', + template: "email/notification", format: :html, locals: { context_posts: nil, @@ -63,9 +63,9 @@ class GroupSmtpMailer < ActionMailer::Base post: post, in_reply_to_post: nil, classes: Rtl.new(nil).css_class, - first_footer_classes: '', - reply_above_line: true - } + first_footer_classes: "", + reply_above_line: true, + }, ) end end diff --git a/app/mailers/invite_mailer.rb b/app/mailers/invite_mailer.rb index 6f7fee6346..f11883761a 100644 --- a/app/mailers/invite_mailer.rb +++ b/app/mailers/invite_mailer.rb @@ -3,7 +3,7 @@ class InviteMailer < ActionMailer::Base include Email::BuildEmailHelper - layout 'email_template' + layout "email_template" def send_invite(invite, invite_to_topic: false) # Find the first topic they were invited to @@ -15,16 +15,20 @@ class InviteMailer < ActionMailer::Base inviter_name = "#{invite.invited_by.name} (#{invite.invited_by.username})" end - sanitized_message = invite.custom_message.present? ? - ActionView::Base.full_sanitizer.sanitize(invite.custom_message.gsub(/\n+/, " ").strip) : nil + sanitized_message = + ( + if invite.custom_message.present? + ActionView::Base.full_sanitizer.sanitize(invite.custom_message.gsub(/\n+/, " ").strip) + else + nil + end + ) # If they were invited to a topic if invite_to_topic && first_topic.present? # get topic excerpt topic_excerpt = "" - if first_topic.excerpt - topic_excerpt = first_topic.excerpt.tr("\n", " ") - end + topic_excerpt = first_topic.excerpt.tr("\n", " ") if first_topic.excerpt topic_title = first_topic.try(:title) if SiteSetting.private_email? @@ -32,35 +36,41 @@ class InviteMailer < ActionMailer::Base topic_excerpt = "" end - build_email(invite.email, - template: sanitized_message ? 'custom_invite_mailer' : 'invite_mailer', - inviter_name: inviter_name, - site_domain_name: Discourse.current_hostname, - invite_link: invite.link(with_email_token: true), - topic_title: topic_title, - topic_excerpt: topic_excerpt, - site_description: SiteSetting.site_description, - site_title: SiteSetting.title, - user_custom_message: sanitized_message) + build_email( + invite.email, + template: sanitized_message ? "custom_invite_mailer" : "invite_mailer", + inviter_name: inviter_name, + site_domain_name: Discourse.current_hostname, + invite_link: invite.link(with_email_token: true), + topic_title: topic_title, + topic_excerpt: topic_excerpt, + site_description: SiteSetting.site_description, + site_title: SiteSetting.title, + user_custom_message: sanitized_message, + ) else - build_email(invite.email, - template: sanitized_message ? 'custom_invite_forum_mailer' : 'invite_forum_mailer', - inviter_name: inviter_name, - site_domain_name: Discourse.current_hostname, - invite_link: invite.link(with_email_token: true), - site_description: SiteSetting.site_description, - site_title: SiteSetting.title, - user_custom_message: sanitized_message) + build_email( + invite.email, + template: sanitized_message ? "custom_invite_forum_mailer" : "invite_forum_mailer", + inviter_name: inviter_name, + site_domain_name: Discourse.current_hostname, + invite_link: invite.link(with_email_token: true), + site_description: SiteSetting.site_description, + site_title: SiteSetting.title, + user_custom_message: sanitized_message, + ) end end def send_password_instructions(user) if user.present? - email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) - build_email(user.email, - template: 'invite_password_instructions', - email_token: email_token.token) + email_token = + user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:password_reset]) + build_email( + user.email, + template: "invite_password_instructions", + email_token: email_token.token, + ) end end - end diff --git a/app/mailers/rejection_mailer.rb b/app/mailers/rejection_mailer.rb index ecbf56fbf4..0178a97b2b 100644 --- a/app/mailers/rejection_mailer.rb +++ b/app/mailers/rejection_mailer.rb @@ -1,27 +1,29 @@ # frozen_string_literal: true -require 'email/message_builder' +require "email/message_builder" class RejectionMailer < ActionMailer::Base include Email::BuildEmailHelper - DISALLOWED_TEMPLATE_ARGS = [:to, - :from, - :base_url, - :user_preferences_url, - :include_respond_instructions, - :html_override, - :add_unsubscribe_link, - :respond_instructions, - :style, - :body, - :post_id, - :topic_id, - :subject, - :template, - :allow_reply_by_email, - :private_reply, - :from_alias] + DISALLOWED_TEMPLATE_ARGS = %i[ + to + from + base_url + user_preferences_url + include_respond_instructions + html_override + add_unsubscribe_link + respond_instructions + style + body + post_id + topic_id + subject + template + allow_reply_by_email + private_reply + from_alias + ] # Send an email rejection message. # @@ -32,10 +34,9 @@ class RejectionMailer < ActionMailer::Base # BuildEmailHelper. You can see the list in DISALLOWED_TEMPLATE_ARGS. def send_rejection(template, message_from, template_args) if template_args.keys.any? { |k| DISALLOWED_TEMPLATE_ARGS.include? k } - raise ArgumentError.new('Reserved key in template arguments') + raise ArgumentError.new("Reserved key in template arguments") end build_email(message_from, template_args.merge(template: "system_messages.#{template}")) end - end diff --git a/app/mailers/subscription_mailer.rb b/app/mailers/subscription_mailer.rb index c2c40752c8..57d4489ac5 100644 --- a/app/mailers/subscription_mailer.rb +++ b/app/mailers/subscription_mailer.rb @@ -9,6 +9,7 @@ class SubscriptionMailer < ActionMailer::Base template: "unsubscribe_mailer", site_title: SiteSetting.title, site_domain_name: Discourse.current_hostname, - confirm_unsubscribe_link: email_unsubscribe_url(unsubscribe_key, host: Discourse.base_url) + confirm_unsubscribe_link: + email_unsubscribe_url(unsubscribe_key, host: Discourse.base_url) end end diff --git a/app/mailers/test_mailer.rb b/app/mailers/test_mailer.rb index 4abd75263a..4c561e648a 100644 --- a/app/mailers/test_mailer.rb +++ b/app/mailers/test_mailer.rb @@ -4,6 +4,6 @@ class TestMailer < ActionMailer::Base include Email::BuildEmailHelper def send_test(to_address, opts = {}) - build_email(to_address, template: 'test_mailer', **opts) + build_email(to_address, template: "test_mailer", **opts) end end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 78bdbd722b..d558085827 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -4,37 +4,38 @@ class UserNotifications < ActionMailer::Base include UserNotificationsHelper include ApplicationHelper helper :application, :email - default charset: 'UTF-8' - layout 'email_template' + default charset: "UTF-8" + layout "email_template" include Email::BuildEmailHelper def signup(user, opts = {}) - build_user_email_token_by_template( - "user_notifications.signup", - user, - opts[:email_token] - ) + build_user_email_token_by_template("user_notifications.signup", user, opts[:email_token]) end def activation_reminder(user, opts = {}) build_user_email_token_by_template( "user_notifications.activation_reminder", user, - opts[:email_token] + opts[:email_token], ) end def signup_after_approval(user, opts = {}) locale = user_locale(user) - tips = I18n.t('system_messages.usage_tips.text_body_template', - base_url: Discourse.base_url, - locale: locale) + tips = + I18n.t( + "system_messages.usage_tips.text_body_template", + base_url: Discourse.base_url, + locale: locale, + ) - build_email(user.email, - template: 'user_notifications.signup_after_approval', - locale: locale, - new_user_tips: tips) + build_email( + user.email, + template: "user_notifications.signup_after_approval", + locale: locale, + new_user_tips: tips, + ) end def post_approved(user, opts = {}) @@ -43,20 +44,23 @@ class UserNotifications < ActionMailer::Base return if post_url.nil? locale = user_locale(user) - build_email(user.email, - template: 'user_notifications.post_approved', + build_email( + user.email, + template: "user_notifications.post_approved", locale: locale, base_url: Discourse.base_url, - post_url: post_url + post_url: post_url, ) end def signup_after_reject(user, opts = {}) locale = user_locale(user) - build_email(user.email, - template: 'user_notifications.signup_after_reject', - locale: locale, - reject_reason: opts[:reject_reason]) + build_email( + user.email, + template: "user_notifications.signup_after_reject", + locale: locale, + reject_reason: opts[:reject_reason], + ) end def suspicious_login(user, opts = {}) @@ -71,32 +75,36 @@ class UserNotifications < ActionMailer::Base template: "user_notifications.suspicious_login", locale: user_locale(user), client_ip: opts[:client_ip], - location: location.present? ? location : I18n.t('staff_action_logs.unknown'), + location: location.present? ? location : I18n.t("staff_action_logs.unknown"), browser: I18n.t("user_auth_tokens.browser.#{browser}"), device: I18n.t("user_auth_tokens.device.#{device}"), - os: I18n.t("user_auth_tokens.os.#{os}") + os: I18n.t("user_auth_tokens.os.#{os}"), ) end def notify_old_email(user, opts = {}) - build_email(user.email, - template: "user_notifications.notify_old_email", - locale: user_locale(user), - new_email: opts[:new_email]) + build_email( + user.email, + template: "user_notifications.notify_old_email", + locale: user_locale(user), + new_email: opts[:new_email], + ) end def notify_old_email_add(user, opts = {}) - build_email(user.email, - template: "user_notifications.notify_old_email_add", - locale: user_locale(user), - new_email: opts[:new_email]) + build_email( + user.email, + template: "user_notifications.notify_old_email_add", + locale: user_locale(user), + new_email: opts[:new_email], + ) end def confirm_old_email(user, opts = {}) build_user_email_token_by_template( "user_notifications.confirm_old_email", user, - opts[:email_token] + opts[:email_token], ) end @@ -104,15 +112,21 @@ class UserNotifications < ActionMailer::Base build_user_email_token_by_template( "user_notifications.confirm_old_email_add", user, - opts[:email_token] + opts[:email_token], ) end def confirm_new_email(user, opts = {}) build_user_email_token_by_template( - opts[:requested_by_admin] ? "user_notifications.confirm_new_email_via_admin" : "user_notifications.confirm_new_email", + ( + if opts[:requested_by_admin] + "user_notifications.confirm_new_email_via_admin" + else + "user_notifications.confirm_new_email" + end + ), user, - opts[:email_token] + opts[:email_token], ) end @@ -120,31 +134,23 @@ class UserNotifications < ActionMailer::Base build_user_email_token_by_template( user.has_password? ? "user_notifications.forgot_password" : "user_notifications.set_password", user, - opts[:email_token] + opts[:email_token], ) end def email_login(user, opts = {}) - build_user_email_token_by_template( - "user_notifications.email_login", - user, - opts[:email_token] - ) + build_user_email_token_by_template("user_notifications.email_login", user, opts[:email_token]) end def admin_login(user, opts = {}) - build_user_email_token_by_template( - "user_notifications.admin_login", - user, - opts[:email_token] - ) + build_user_email_token_by_template("user_notifications.admin_login", user, opts[:email_token]) end def account_created(user, opts = {}) build_user_email_token_by_template( "user_notifications.account_created", user, - opts[:email_token] + opts[:email_token], ) end @@ -158,7 +164,7 @@ class UserNotifications < ActionMailer::Base user.email, template: "user_notifications.account_silenced_forever", locale: user_locale(user), - reason: user_history.details + reason: user_history.details, ) else silenced_till = user.silenced_till.in_time_zone(user.user_option.timezone) @@ -167,7 +173,7 @@ class UserNotifications < ActionMailer::Base template: "user_notifications.account_silenced", locale: user_locale(user), reason: user_history.details, - silenced_till: I18n.l(silenced_till, format: :long) + silenced_till: I18n.l(silenced_till, format: :long), ) end end @@ -182,7 +188,7 @@ class UserNotifications < ActionMailer::Base user.email, template: "user_notifications.account_suspended_forever", locale: user_locale(user), - reason: user_history.details + reason: user_history.details, ) else suspended_till = user.suspended_till.in_time_zone(user.user_option.timezone) @@ -191,7 +197,7 @@ class UserNotifications < ActionMailer::Base template: "user_notifications.account_suspended", locale: user_locale(user), reason: user_history.details, - suspended_till: I18n.l(suspended_till, format: :long) + suspended_till: I18n.l(suspended_till, format: :long), ) end end @@ -199,18 +205,18 @@ class UserNotifications < ActionMailer::Base def account_exists(user, opts = {}) build_email( user.email, - template: 'user_notifications.account_exists', + template: "user_notifications.account_exists", locale: user_locale(user), - email: user.email + email: user.email, ) end def account_second_factor_disabled(user, opts = {}) build_email( user.email, - template: 'user_notifications.account_second_factor_disabled', + template: "user_notifications.account_second_factor_disabled", locale: user_locale(user), - email: user.email + email: user.email, ) end @@ -221,34 +227,59 @@ class UserNotifications < ActionMailer::Base min_date = opts[:since] || user.last_emailed_at || user.last_seen_at || 1.month.ago # Fetch some topics and posts to show - digest_opts = { limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, top_order: true } + digest_opts = { + limit: SiteSetting.digest_topics + SiteSetting.digest_other_topics, + top_order: true, + } topics_for_digest = Topic.for_digest(user, min_date, digest_opts).to_a if topics_for_digest.empty? && !user.user_option.try(:include_tl0_in_digests) # Find some topics from new users that are at least 24 hours old - topics_for_digest = Topic.for_digest(user, min_date, digest_opts.merge(include_tl0: true)).where('topics.created_at < ?', 24.hours.ago).to_a + topics_for_digest = + Topic + .for_digest(user, min_date, digest_opts.merge(include_tl0: true)) + .where("topics.created_at < ?", 24.hours.ago) + .to_a end @popular_topics = topics_for_digest[0, SiteSetting.digest_topics] if @popular_topics.present? - @other_new_for_you = topics_for_digest.size > SiteSetting.digest_topics ? topics_for_digest[SiteSetting.digest_topics..-1] : [] + @other_new_for_you = + ( + if topics_for_digest.size > SiteSetting.digest_topics + topics_for_digest[SiteSetting.digest_topics..-1] + else + [] + end + ) - @popular_posts = if SiteSetting.digest_posts > 0 - Post.order("posts.score DESC") - .for_mailing_list(user, min_date) - .where('posts.post_type = ?', Post.types[:regular]) - .where('posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false') - .where("posts.post_number > ? AND posts.score > ?", 1, ScoreCalculator.default_score_weights[:like_score] * 5.0) - .where('posts.created_at < ?', (SiteSetting.editing_grace_period || 0).seconds.ago) - .limit(SiteSetting.digest_posts) - else - [] - end + @popular_posts = + if SiteSetting.digest_posts > 0 + Post + .order("posts.score DESC") + .for_mailing_list(user, min_date) + .where("posts.post_type = ?", Post.types[:regular]) + .where( + "posts.deleted_at IS NULL AND posts.hidden = false AND posts.user_deleted = false", + ) + .where( + "posts.post_number > ? AND posts.score > ?", + 1, + ScoreCalculator.default_score_weights[:like_score] * 5.0, + ) + .where("posts.created_at < ?", (SiteSetting.editing_grace_period || 0).seconds.ago) + .limit(SiteSetting.digest_posts) + else + [] + end @excerpts = {} @popular_topics.map do |t| - @excerpts[t.first_post.id] = email_excerpt(t.first_post.cooked, t.first_post) if t.first_post.present? + @excerpts[t.first_post.id] = email_excerpt( + t.first_post.cooked, + t.first_post, + ) if t.first_post.present? end # Try to find 3 interesting stats for the top of the digest @@ -258,33 +289,60 @@ class UserNotifications < ActionMailer::Base # We used topics from new users instead, so count should match new_topics_count = topics_for_digest.size end - @counts = [{ label_key: 'user_notifications.digest.new_topics', - value: new_topics_count, - href: "#{Discourse.base_url}/new" }] + @counts = [ + { + label_key: "user_notifications.digest.new_topics", + value: new_topics_count, + href: "#{Discourse.base_url}/new", + }, + ] # totalling unread notifications (which are low-priority only) and unread # PMs and bookmark reminder notifications, so the total is both unread low # and high priority PMs value = user.unread_notifications + user.unread_high_priority_notifications - @counts << { label_key: 'user_notifications.digest.unread_notifications', value: value, href: "#{Discourse.base_url}/my/notifications" } if value > 0 + if value > 0 + @counts << { + label_key: "user_notifications.digest.unread_notifications", + value: value, + href: "#{Discourse.base_url}/my/notifications", + } + end if @counts.size < 3 value = user.unread_notifications_of_type(Notification.types[:liked]) - @counts << { label_key: 'user_notifications.digest.liked_received', value: value, href: "#{Discourse.base_url}/my/notifications" } if value > 0 + if value > 0 + @counts << { + label_key: "user_notifications.digest.liked_received", + value: value, + href: "#{Discourse.base_url}/my/notifications", + } + end end if @counts.size < 3 && user.user_option.digest_after_minutes.to_i >= 1440 value = summary_new_users_count(min_date) - @counts << { label_key: 'user_notifications.digest.new_users', value: value, href: "#{Discourse.base_url}/about" } if value > 0 + if value > 0 + @counts << { + label_key: "user_notifications.digest.new_users", + value: value, + href: "#{Discourse.base_url}/about", + } + end end @last_seen_at = short_date(user.last_seen_at || user.created_at) - @preheader_text = I18n.t('user_notifications.digest.preheader', last_seen_at: @last_seen_at) + @preheader_text = I18n.t("user_notifications.digest.preheader", last_seen_at: @last_seen_at) opts = { - from_alias: I18n.t('user_notifications.digest.from', site_name: Email.site_title), - subject: I18n.t('user_notifications.digest.subject_template', email_prefix: @email_prefix, date: short_date(Time.now)), + from_alias: I18n.t("user_notifications.digest.from", site_name: Email.site_title), + subject: + I18n.t( + "user_notifications.digest.subject_template", + email_prefix: @email_prefix, + date: short_date(Time.now), + ), add_unsubscribe_link: true, unsubscribe_url: "#{Discourse.base_url}/email/unsubscribe/#{@unsubscribe_key}", } @@ -351,7 +409,7 @@ class UserNotifications < ActionMailer::Base opts[:show_group_in_subject] = true if SiteSetting.group_in_subject # We use the 'user_posted' event when you are emailed a post in a PM. - opts[:notification_type] = 'posted' + opts[:notification_type] = "posted" notification_email(user, opts) end @@ -400,29 +458,33 @@ class UserNotifications < ActionMailer::Base def email_post_markdown(post, add_posted_by = false) result = +"#{post.raw}\n\n" if add_posted_by - result << "#{I18n.t('user_notifications.posted_by', username: post.username, post_date: post.created_at.strftime("%m/%d/%Y"))}\n\n" + result << "#{I18n.t("user_notifications.posted_by", username: post.username, post_date: post.created_at.strftime("%m/%d/%Y"))}\n\n" end result end def self.get_context_posts(post, topic_user, user) if (user.user_option.email_previous_replies == UserOption.previous_replies_type[:never]) || - SiteSetting.private_email? + SiteSetting.private_email? return [] end allowed_post_types = [Post.types[:regular]] allowed_post_types << Post.types[:whisper] if topic_user.try(:user).try(:staff?) - context_posts = Post.where(topic_id: post.topic_id) - .where("post_number < ?", post.post_number) - .where(user_deleted: false) - .where(hidden: false) - .where(post_type: allowed_post_types) - .order('created_at desc') - .limit(SiteSetting.email_posts_context) + context_posts = + Post + .where(topic_id: post.topic_id) + .where("post_number < ?", post.post_number) + .where(user_deleted: false) + .where(hidden: false) + .where(post_type: allowed_post_types) + .order("created_at desc") + .limit(SiteSetting.email_posts_context) - if topic_user && topic_user.last_emailed_post_number && user.user_option.email_previous_replies == UserOption.previous_replies_type[:unless_emailed] + if topic_user && topic_user.last_emailed_post_number && + user.user_option.email_previous_replies == + UserOption.previous_replies_type[:unless_emailed] context_posts = context_posts.where("post_number > ?", topic_user.last_emailed_post_number) end @@ -435,9 +497,7 @@ class UserNotifications < ActionMailer::Base post = opts[:post] unless String === notification_type - if Numeric === notification_type - notification_type = Notification.types[notification_type] - end + notification_type = Notification.types[notification_type] if Numeric === notification_type notification_type = notification_type.to_s end @@ -449,13 +509,16 @@ class UserNotifications < ActionMailer::Base end allow_reply_by_email = opts[:allow_reply_by_email] unless user.suspended? - original_username = notification_data[:original_username] || notification_data[:display_username] + original_username = + notification_data[:original_username] || notification_data[:display_username] if user.staged && post - original_subject = IncomingEmail.joins(:post) - .where("posts.topic_id = ? AND posts.post_number = 1", post.topic_id) - .pluck(:subject) - .first + original_subject = + IncomingEmail + .joins(:post) + .where("posts.topic_id = ? AND posts.post_number = 1", post.topic_id) + .pluck(:subject) + .first end if original_subject @@ -472,7 +535,7 @@ class UserNotifications < ActionMailer::Base title: topic_title, post: post, username: original_username, - from_alias: I18n.t('email_from', user_name: user_name, site_name: Email.site_title), + from_alias: I18n.t("email_from", user_name: user_name, site_name: Email.site_title), allow_reply_by_email: allow_reply_by_email, use_site_subject: opts[:use_site_subject], add_re_to_subject: opts[:add_re_to_subject], @@ -482,7 +545,7 @@ class UserNotifications < ActionMailer::Base notification_type: notification_type, use_invite_template: opts[:use_invite_template], use_topic_title_subject: use_topic_title_subject, - user: user + user: user, } if group_id = notification_data[:group_id] @@ -525,7 +588,8 @@ class UserNotifications < ActionMailer::Base # subcategory case if !category.parent_category_id.nil? - show_category_in_subject = "#{Category.where(id: category.parent_category_id).pluck_first(:name)}/#{show_category_in_subject}" + show_category_in_subject = + "#{Category.where(id: category.parent_category_id).pluck_first(:name)}/#{show_category_in_subject}" end else show_category_in_subject = nil @@ -538,7 +602,7 @@ class UserNotifications < ActionMailer::Base .visible_tags(Guardian.new(user)) .joins(:topic_tags) .where("topic_tags.topic_id = ?", post.topic_id) - .limit(3) + .limit(SiteSetting.max_tags_per_topic) .pluck(:name) show_tags_in_subject = tags.any? ? tags.join(" ") : nil @@ -555,7 +619,7 @@ class UserNotifications < ActionMailer::Base "[#{group.name}] " end else - I18n.t('subject_pm') + I18n.t("subject_pm") end participants = self.class.participants(post, user) @@ -573,27 +637,25 @@ class UserNotifications < ActionMailer::Base context_posts = context_posts.to_a if context_posts.present? - context << +"-- \n*#{I18n.t('user_notifications.previous_discussion')}*\n" - context_posts.each do |cp| - context << email_post_markdown(cp, true) - end + context << +"-- \n*#{I18n.t("user_notifications.previous_discussion")}*\n" + context_posts.each { |cp| context << email_post_markdown(cp, true) } end - translation_override_exists = TranslationOverride.where( - locale: SiteSetting.default_locale, - translation_key: "#{template}.text_body_template" - ).exists? + translation_override_exists = + TranslationOverride.where( + locale: SiteSetting.default_locale, + translation_key: "#{template}.text_body_template", + ).exists? if opts[:use_invite_template] invite_template = +"user_notifications.invited" invite_template << "_group" if group_name - invite_template << - if post.topic.private_message? - "_to_private_message_body" - else - "_to_topic_body" - end + invite_template << if post.topic.private_message? + "_to_private_message_body" + else + "_to_topic_body" + end topic_excerpt = post.excerpt.tr("\n", " ") if post.is_first_post? && post.excerpt topic_url = post.topic&.url @@ -603,28 +665,38 @@ class UserNotifications < ActionMailer::Base topic_url = "" end - message = I18n.t(invite_template, - username: username, - group_name: group_name, - topic_title: gsub_emoji_to_unicode(title), - topic_excerpt: topic_excerpt, - site_title: SiteSetting.title, - site_description: SiteSetting.site_description, - topic_url: topic_url - ) + message = + I18n.t( + invite_template, + username: username, + group_name: group_name, + topic_title: gsub_emoji_to_unicode(title), + topic_excerpt: topic_excerpt, + site_title: SiteSetting.title, + site_description: SiteSetting.site_description, + topic_url: topic_url, + ) html = PrettyText.cook(message, sanitize: false).html_safe else reached_limit = SiteSetting.max_emails_per_day_per_user > 0 - reached_limit &&= (EmailLog.where(user_id: user.id) - .where('created_at > ?', 1.day.ago) - .count) >= (SiteSetting.max_emails_per_day_per_user - 1) + reached_limit &&= + (EmailLog.where(user_id: user.id).where("created_at > ?", 1.day.ago).count) >= + (SiteSetting.max_emails_per_day_per_user - 1) in_reply_to_post = post.reply_to_post if user.user_option.email_in_reply_to if SiteSetting.private_email? - message = I18n.t('system_messages.contents_hidden') + message = I18n.t("system_messages.contents_hidden") else - message = email_post_markdown(post) + (reached_limit ? "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" : "") + message = + email_post_markdown(post) + + ( + if reached_limit + "\n\n#{I18n.t "user_notifications.reached_limit", count: SiteSetting.max_emails_per_day_per_user}" + else + "" + end + ) end first_footer_classes = "highlight" @@ -633,18 +705,20 @@ class UserNotifications < ActionMailer::Base end unless translation_override_exists - html = UserNotificationRenderer.render( - template: 'email/notification', - format: :html, - locals: { context_posts: context_posts, - reached_limit: reached_limit, - post: post, - in_reply_to_post: in_reply_to_post, - classes: Rtl.new(user).css_class, - first_footer_classes: first_footer_classes, - reply_above_line: false - } - ) + html = + UserNotificationRenderer.render( + template: "email/notification", + format: :html, + locals: { + context_posts: context_posts, + reached_limit: reached_limit, + post: post, + in_reply_to_post: in_reply_to_post, + classes: Rtl.new(user).css_class, + first_footer_classes: first_footer_classes, + reply_above_line: false, + }, + ) end end @@ -676,12 +750,10 @@ class UserNotifications < ActionMailer::Base site_description: SiteSetting.site_description, site_title: SiteSetting.title, site_title_url_encoded: UrlHelper.encode_component(SiteSetting.title), - locale: locale + locale: locale, } - unless translation_override_exists - email_opts[:html_override] = html - end + email_opts[:html_override] = html unless translation_override_exists # If we have a display name, change the from address email_opts[:from_alias] = from_alias if from_alias.present? @@ -701,20 +773,26 @@ class UserNotifications < ActionMailer::Base break if list.size >= SiteSetting.max_participant_names end - recent_posts_query = post.topic.posts - .select("user_id, MAX(post_number) AS post_number") - .where(post_type: Post.types[:regular], post_number: ..post.post_number) - .where.not(user_id: recipient_user.id) - .group(:user_id) - .order("post_number DESC") - .limit(SiteSetting.max_participant_names) - .to_sql + recent_posts_query = + post + .topic + .posts + .select("user_id, MAX(post_number) AS post_number") + .where(post_type: Post.types[:regular], post_number: ..post.post_number) + .where.not(user_id: recipient_user.id) + .group(:user_id) + .order("post_number DESC") + .limit(SiteSetting.max_participant_names) + .to_sql - allowed_users = post.topic.allowed_users - .joins("LEFT JOIN (#{recent_posts_query}) pu ON topic_allowed_users.user_id = pu.user_id") - .order("post_number DESC NULLS LAST", :id) - .where.not(id: recipient_user.id) - .human_users + allowed_users = + post + .topic + .allowed_users + .joins("LEFT JOIN (#{recent_posts_query}) pu ON topic_allowed_users.user_id = pu.user_id") + .order("post_number DESC NULLS LAST", :id) + .where.not(id: recipient_user.id) + .human_users allowed_users.each do |u| break if list.size >= SiteSetting.max_participant_names @@ -730,7 +808,11 @@ class UserNotifications < ActionMailer::Base others_count = allowed_groups.size + allowed_users.size - list.size if others_count > 0 - I18n.t("user_notifications.more_pm_participants", participants: participants, count: others_count) + I18n.t( + "user_notifications.more_pm_participants", + participants: participants, + count: others_count, + ) else participants end @@ -739,23 +821,18 @@ class UserNotifications < ActionMailer::Base private def build_user_email_token_by_template(template, user, email_token) - build_email( - user.email, - template: template, - locale: user_locale(user), - email_token: email_token - ) + build_email(user.email, template: template, locale: user_locale(user), email_token: email_token) end def build_summary_for(user) - @site_name = SiteSetting.email_prefix.presence || SiteSetting.title # used by I18n - @user = user - @date = short_date(Time.now) - @base_url = Discourse.base_url - @email_prefix = SiteSetting.email_prefix.presence || SiteSetting.title - @header_color = ColorScheme.hex_for_name('header_primary') - @header_bgcolor = ColorScheme.hex_for_name('header_background') - @anchor_color = ColorScheme.hex_for_name('tertiary') + @site_name = SiteSetting.email_prefix.presence || SiteSetting.title # used by I18n + @user = user + @date = short_date(Time.now) + @base_url = Discourse.base_url + @email_prefix = SiteSetting.email_prefix.presence || SiteSetting.title + @header_color = ColorScheme.hex_for_name("header_primary") + @header_bgcolor = ColorScheme.hex_for_name("header_background") + @anchor_color = ColorScheme.hex_for_name("tertiary") @markdown_linker = MarkdownLinker.new(@base_url) @disable_email_custom_styles = !SiteSetting.apply_custom_styles_to_digest end @@ -765,12 +842,19 @@ class UserNotifications < ActionMailer::Base end def summary_new_users_count(min_date) - min_date_str = min_date.is_a?(String) ? min_date : min_date.strftime('%Y-%m-%d') + min_date_str = min_date.is_a?(String) ? min_date : min_date.strftime("%Y-%m-%d") key = self.class.summary_new_users_count_key(min_date_str) - ((count = Discourse.redis.get(key)) && count.to_i) || begin - count = User.real.where(active: true, staged: false).not_suspended.where("created_at > ?", min_date_str).count - Discourse.redis.setex(key, 1.day, count) - count - end + ((count = Discourse.redis.get(key)) && count.to_i) || + begin + count = + User + .real + .where(active: true, staged: false) + .not_suspended + .where("created_at > ?", min_date_str) + .count + Discourse.redis.setex(key, 1.day, count) + count + end end end diff --git a/app/mailers/version_mailer.rb b/app/mailers/version_mailer.rb index 0b9445f452..c38cd29a90 100644 --- a/app/mailers/version_mailer.rb +++ b/app/mailers/version_mailer.rb @@ -6,17 +6,21 @@ class VersionMailer < ActionMailer::Base def send_notice if SiteSetting.contact_email.present? missing_versions = DiscourseUpdates.missing_versions - if missing_versions.present? && missing_versions.first['notes'].present? - build_email(SiteSetting.contact_email, - template: 'new_version_mailer_with_notes', - notes: missing_versions.first['notes'], - new_version: DiscourseUpdates.latest_version, - installed_version: Discourse::VERSION::STRING) + if missing_versions.present? && missing_versions.first["notes"].present? + build_email( + SiteSetting.contact_email, + template: "new_version_mailer_with_notes", + notes: missing_versions.first["notes"], + new_version: DiscourseUpdates.latest_version, + installed_version: Discourse::VERSION::STRING, + ) else - build_email(SiteSetting.contact_email, - template: 'new_version_mailer', - new_version: DiscourseUpdates.latest_version, - installed_version: Discourse::VERSION::STRING) + build_email( + SiteSetting.contact_email, + template: "new_version_mailer", + new_version: DiscourseUpdates.latest_version, + installed_version: Discourse::VERSION::STRING, + ) end end end diff --git a/app/models/about.rb b/app/models/about.rb index 44220472cc..1901fccc66 100644 --- a/app/models/about.rb +++ b/app/models/about.rb @@ -32,11 +32,10 @@ class About include ActiveModel::Serialization include StatsCacheable - attr_accessor :moderators, - :admins + attr_accessor :moderators, :admins def self.stats_cache_key - 'about-stats' + "about-stats" end def self.fetch_stats @@ -68,38 +67,37 @@ class About end def moderators - @moderators ||= User.where(moderator: true, admin: false) - .human_users - .order("last_seen_at DESC") + @moderators ||= User.where(moderator: true, admin: false).human_users.order("last_seen_at DESC") end def admins - @admins ||= User.where(admin: true) - .human_users - .order("last_seen_at DESC") + @admins ||= User.where(admin: true).human_users.order("last_seen_at DESC") end def stats @stats ||= { topic_count: Topic.listable_topics.count, - topics_last_day: Topic.listable_topics.where('created_at > ?', 1.days.ago).count, - topics_7_days: Topic.listable_topics.where('created_at > ?', 7.days.ago).count, - topics_30_days: Topic.listable_topics.where('created_at > ?', 30.days.ago).count, + topics_last_day: Topic.listable_topics.where("created_at > ?", 1.days.ago).count, + topics_7_days: Topic.listable_topics.where("created_at > ?", 7.days.ago).count, + topics_30_days: Topic.listable_topics.where("created_at > ?", 30.days.ago).count, post_count: Post.count, - posts_last_day: Post.where('created_at > ?', 1.days.ago).count, - posts_7_days: Post.where('created_at > ?', 7.days.ago).count, - posts_30_days: Post.where('created_at > ?', 30.days.ago).count, + posts_last_day: Post.where("created_at > ?", 1.days.ago).count, + posts_7_days: Post.where("created_at > ?", 7.days.ago).count, + posts_30_days: Post.where("created_at > ?", 30.days.ago).count, user_count: User.real.count, - users_last_day: User.real.where('created_at > ?', 1.days.ago).count, - users_7_days: User.real.where('created_at > ?', 7.days.ago).count, - users_30_days: User.real.where('created_at > ?', 30.days.ago).count, - active_users_last_day: User.where('last_seen_at > ?', 1.days.ago).count, - active_users_7_days: User.where('last_seen_at > ?', 7.days.ago).count, - active_users_30_days: User.where('last_seen_at > ?', 30.days.ago).count, + users_last_day: User.real.where("created_at > ?", 1.days.ago).count, + users_7_days: User.real.where("created_at > ?", 7.days.ago).count, + users_30_days: User.real.where("created_at > ?", 30.days.ago).count, + active_users_last_day: User.where("last_seen_at > ?", 1.days.ago).count, + active_users_7_days: User.where("last_seen_at > ?", 7.days.ago).count, + active_users_30_days: User.where("last_seen_at > ?", 30.days.ago).count, like_count: UserAction.where(action_type: UserAction::LIKE).count, - likes_last_day: UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 1.days.ago).count, - likes_7_days: UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 7.days.ago).count, - likes_30_days: UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 30.days.ago).count + likes_last_day: + UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 1.days.ago).count, + likes_7_days: + UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 7.days.ago).count, + likes_30_days: + UserAction.where(action_type: UserAction::LIKE).where("created_at > ?", 30.days.ago).count, }.merge(plugin_stats) end @@ -109,17 +107,21 @@ class About begin stats = stat_group.call rescue StandardError => err - Discourse.warn_exception(err, message: "Unexpected error when collecting #{plugin_stat_group_name} About stats.") + Discourse.warn_exception( + err, + message: "Unexpected error when collecting #{plugin_stat_group_name} About stats.", + ) next end - if !stats.key?(:last_day) || !stats.key?("7_days") || !stats.key?("30_days") || !stats.key?(:count) - Rails.logger.warn("Plugin stat group #{plugin_stat_group_name} for About stats does not have all required keys, skipping.") + if !stats.key?(:last_day) || !stats.key?("7_days") || !stats.key?("30_days") || + !stats.key?(:count) + Rails.logger.warn( + "Plugin stat group #{plugin_stat_group_name} for About stats does not have all required keys, skipping.", + ) else final_plugin_stats.merge!( - stats.transform_keys do |key| - "#{plugin_stat_group_name}_#{key}".to_sym - end + stats.transform_keys { |key| "#{plugin_stat_group_name}_#{key}".to_sym }, ) end end @@ -151,9 +153,7 @@ class About mods = User.where(id: results.map(&:user_ids).flatten.uniq).index_by(&:id) - results.map do |row| - CategoryMods.new(row.category_id, mods.values_at(*row.user_ids)) - end + results.map { |row| CategoryMods.new(row.category_id, mods.values_at(*row.user_ids)) } end def category_mods_limit diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index ae6dfa2535..8c3245a803 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -3,13 +3,10 @@ class AdminDashboardData include StatsCacheable - cattr_reader :problem_syms, - :problem_blocks, - :problem_messages, - :problem_scheduled_check_blocks + cattr_reader :problem_syms, :problem_blocks, :problem_messages, :problem_scheduled_check_blocks class Problem - VALID_PRIORITIES = ["low", "high"].freeze + VALID_PRIORITIES = %w[low high].freeze attr_reader :message, :priority, :identifier @@ -114,11 +111,12 @@ class AdminDashboardData found_problems_json = Discourse.redis.get(SCHEDULED_PROBLEM_STORAGE_KEY) return [] if found_problems_json.blank? begin - JSON.parse(found_problems_json).map do |problem| - Problem.from_h(problem) - end + JSON.parse(found_problems_json).map { |problem| Problem.from_h(problem) } rescue JSON::ParserError => err - Discourse.warn_exception(err, message: "Error parsing found problem JSON in admin dashboard: #{found_problems_json}") + Discourse.warn_exception( + err, + message: "Error parsing found problem JSON in admin dashboard: #{found_problems_json}", + ) [] end end @@ -127,16 +125,21 @@ class AdminDashboardData add_scheduled_problem_check(:group_smtp_credentials) do problems = GroupEmailCredentialsCheck.run problems.map do |p| - problem_message = I18n.t( - "dashboard.group_email_credentials_warning", - { - base_path: Discourse.base_path, - group_name: p[:group_name], - group_full_name: p[:group_full_name], - error: p[:message] - } + problem_message = + I18n.t( + "dashboard.group_email_credentials_warning", + { + base_path: Discourse.base_path, + group_name: p[:group_name], + group_full_name: p[:group_full_name], + error: p[:message], + }, + ) + Problem.new( + problem_message, + priority: "high", + identifier: "group_#{p[:group_id]}_email_credentials", ) - Problem.new(problem_message, priority: "high", identifier: "group_#{p[:group_id]}_email_credentials") end end end @@ -149,7 +152,10 @@ class AdminDashboardData begin problems = instance_exec(&blk) rescue StandardError => err - Discourse.warn_exception(err, message: "A scheduled admin dashboard problem check (#{check_identifier}) errored.") + Discourse.warn_exception( + err, + message: "A scheduled admin dashboard problem check (#{check_identifier}) errored.", + ) # we don't want to hold up other checks because this one errored next end @@ -177,26 +183,33 @@ class AdminDashboardData @@problem_blocks = [] @@problem_scheduled_check_blocks = {} - @@problem_messages = [ - 'dashboard.bad_favicon_url', - 'dashboard.poll_pop3_timeout', - 'dashboard.poll_pop3_auth_error', + @@problem_messages = %w[ + dashboard.bad_favicon_url + dashboard.poll_pop3_timeout + dashboard.poll_pop3_auth_error ] - add_problem_check :rails_env_check, :host_names_check, :force_https_check, - :ram_check, :google_oauth2_config_check, - :facebook_config_check, :twitter_config_check, - :github_config_check, :s3_config_check, :s3_cdn_check, - :image_magick_check, :failing_emails_check, + add_problem_check :rails_env_check, + :host_names_check, + :force_https_check, + :ram_check, + :google_oauth2_config_check, + :facebook_config_check, + :twitter_config_check, + :github_config_check, + :s3_config_check, + :s3_cdn_check, + :image_magick_check, + :failing_emails_check, :subfolder_ends_in_slash_check, :email_polling_errored_recently, - :out_of_date_themes, :unreachable_themes, :watched_words_check + :out_of_date_themes, + :unreachable_themes, + :watched_words_check register_default_scheduled_problem_checks - add_problem_check do - sidekiq_check || queue_size_check - end + add_problem_check { sidekiq_check || queue_size_check } end reset_problem_checks @@ -213,7 +226,7 @@ class AdminDashboardData end def self.problems_started_key - 'dash-problems-started-at' + "dash-problems-started-at" end def self.set_problems_started @@ -235,7 +248,11 @@ class AdminDashboardData end def self.problem_message_check(i18n_key) - Discourse.redis.get(problem_message_key(i18n_key)) ? I18n.t(i18n_key, base_path: Discourse.base_path) : nil + if Discourse.redis.get(problem_message_key(i18n_key)) + I18n.t(i18n_key, base_path: Discourse.base_path) + else + nil + end end ## @@ -264,97 +281,128 @@ class AdminDashboardData end def host_names_check - I18n.t("dashboard.host_names_warning") if ['localhost', 'production.localhost'].include?(Discourse.current_hostname) + if %w[localhost production.localhost].include?(Discourse.current_hostname) + I18n.t("dashboard.host_names_warning") + end end def sidekiq_check last_job_performed_at = Jobs.last_job_performed_at - I18n.t('dashboard.sidekiq_warning') if Jobs.queued > 0 && (last_job_performed_at.nil? || last_job_performed_at < 2.minutes.ago) + if Jobs.queued > 0 && (last_job_performed_at.nil? || last_job_performed_at < 2.minutes.ago) + I18n.t("dashboard.sidekiq_warning") + end end def queue_size_check queue_size = Jobs.queued - I18n.t('dashboard.queue_size_warning', queue_size: queue_size) unless queue_size < 100_000 + I18n.t("dashboard.queue_size_warning", queue_size: queue_size) unless queue_size < 100_000 end def ram_check - I18n.t('dashboard.memory_warning') if MemInfo.new.mem_total && MemInfo.new.mem_total < 950_000 + I18n.t("dashboard.memory_warning") if MemInfo.new.mem_total && MemInfo.new.mem_total < 950_000 end def google_oauth2_config_check - if SiteSetting.enable_google_oauth2_logins && (SiteSetting.google_oauth2_client_id.blank? || SiteSetting.google_oauth2_client_secret.blank?) - I18n.t('dashboard.google_oauth2_config_warning', base_path: Discourse.base_path) + if SiteSetting.enable_google_oauth2_logins && + ( + SiteSetting.google_oauth2_client_id.blank? || + SiteSetting.google_oauth2_client_secret.blank? + ) + I18n.t("dashboard.google_oauth2_config_warning", base_path: Discourse.base_path) end end def facebook_config_check - if SiteSetting.enable_facebook_logins && (SiteSetting.facebook_app_id.blank? || SiteSetting.facebook_app_secret.blank?) - I18n.t('dashboard.facebook_config_warning', base_path: Discourse.base_path) + if SiteSetting.enable_facebook_logins && + (SiteSetting.facebook_app_id.blank? || SiteSetting.facebook_app_secret.blank?) + I18n.t("dashboard.facebook_config_warning", base_path: Discourse.base_path) end end def twitter_config_check - if SiteSetting.enable_twitter_logins && (SiteSetting.twitter_consumer_key.blank? || SiteSetting.twitter_consumer_secret.blank?) - I18n.t('dashboard.twitter_config_warning', base_path: Discourse.base_path) + if SiteSetting.enable_twitter_logins && + (SiteSetting.twitter_consumer_key.blank? || SiteSetting.twitter_consumer_secret.blank?) + I18n.t("dashboard.twitter_config_warning", base_path: Discourse.base_path) end end def github_config_check - if SiteSetting.enable_github_logins && (SiteSetting.github_client_id.blank? || SiteSetting.github_client_secret.blank?) - I18n.t('dashboard.github_config_warning', base_path: Discourse.base_path) + if SiteSetting.enable_github_logins && + (SiteSetting.github_client_id.blank? || SiteSetting.github_client_secret.blank?) + I18n.t("dashboard.github_config_warning", base_path: Discourse.base_path) end end def s3_config_check # if set via global setting it is validated during the `use_s3?` call if !GlobalSetting.use_s3? - bad_keys = (SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?) && !SiteSetting.s3_use_iam_profile + bad_keys = + (SiteSetting.s3_access_key_id.blank? || SiteSetting.s3_secret_access_key.blank?) && + !SiteSetting.s3_use_iam_profile if SiteSetting.enable_s3_uploads && (bad_keys || SiteSetting.s3_upload_bucket.blank?) - return I18n.t('dashboard.s3_config_warning', base_path: Discourse.base_path) + return I18n.t("dashboard.s3_config_warning", base_path: Discourse.base_path) end - if SiteSetting.backup_location == BackupLocationSiteSetting::S3 && (bad_keys || SiteSetting.s3_backup_bucket.blank?) - return I18n.t('dashboard.s3_backup_config_warning', base_path: Discourse.base_path) + if SiteSetting.backup_location == BackupLocationSiteSetting::S3 && + (bad_keys || SiteSetting.s3_backup_bucket.blank?) + return I18n.t("dashboard.s3_backup_config_warning", base_path: Discourse.base_path) end end nil end def s3_cdn_check - if (GlobalSetting.use_s3? || SiteSetting.enable_s3_uploads) && SiteSetting.Upload.s3_cdn_url.blank? - I18n.t('dashboard.s3_cdn_warning') + if (GlobalSetting.use_s3? || SiteSetting.enable_s3_uploads) && + SiteSetting.Upload.s3_cdn_url.blank? + I18n.t("dashboard.s3_cdn_warning") end end def image_magick_check - I18n.t('dashboard.image_magick_warning') if SiteSetting.create_thumbnails && !system("command -v convert >/dev/null;") + if SiteSetting.create_thumbnails && !system("command -v convert >/dev/null;") + I18n.t("dashboard.image_magick_warning") + end end def failing_emails_check num_failed_jobs = Jobs.num_email_retry_jobs - I18n.t('dashboard.failing_emails_warning', num_failed_jobs: num_failed_jobs, base_path: Discourse.base_path) if num_failed_jobs > 0 + if num_failed_jobs > 0 + I18n.t( + "dashboard.failing_emails_warning", + num_failed_jobs: num_failed_jobs, + base_path: Discourse.base_path, + ) + end end def subfolder_ends_in_slash_check - I18n.t('dashboard.subfolder_ends_in_slash') if Discourse.base_path =~ /\/$/ + I18n.t("dashboard.subfolder_ends_in_slash") if Discourse.base_path =~ %r{/$} end def email_polling_errored_recently errors = Jobs::PollMailbox.errors_in_past_24_hours - I18n.t('dashboard.email_polling_errored_recently', count: errors, base_path: Discourse.base_path) if errors > 0 + if errors > 0 + I18n.t( + "dashboard.email_polling_errored_recently", + count: errors, + base_path: Discourse.base_path, + ) + end end def missing_mailgun_api_key return unless SiteSetting.reply_by_email_enabled - return unless ActionMailer::Base.smtp_settings[:address]['smtp.mailgun.org'] + return unless ActionMailer::Base.smtp_settings[:address]["smtp.mailgun.org"] return unless SiteSetting.mailgun_api_key.blank? - I18n.t('dashboard.missing_mailgun_api_key') + I18n.t("dashboard.missing_mailgun_api_key") end def force_https_check return unless @opts[:check_force_https] - I18n.t('dashboard.force_https_warning', base_path: Discourse.base_path) unless SiteSetting.force_https + unless SiteSetting.force_https + I18n.t("dashboard.force_https_warning", base_path: Discourse.base_path) + end end def watched_words_check @@ -363,7 +411,11 @@ class AdminDashboardData WordWatcher.word_matcher_regexp_list(action, raise_errors: true) rescue RegexpError => e translated_action = I18n.t("admin_js.admin.watched_words.actions.#{action}") - I18n.t('dashboard.watched_word_regexp_error', base_path: Discourse.base_path, action: translated_action) + I18n.t( + "dashboard.watched_word_regexp_error", + base_path: Discourse.base_path, + action: translated_action, + ) end end nil @@ -373,22 +425,25 @@ class AdminDashboardData old_themes = RemoteTheme.out_of_date_themes return unless old_themes.present? - themes_html_format(old_themes, 'dashboard.out_of_date_themes') + themes_html_format(old_themes, "dashboard.out_of_date_themes") end def unreachable_themes themes = RemoteTheme.unreachable_themes return unless themes.present? - themes_html_format(themes, 'dashboard.unreachable_themes') + themes_html_format(themes, "dashboard.unreachable_themes") end private def themes_html_format(themes, i18n_key) - html = themes.map do |name, id| - "
  • #{CGI.escapeHTML(name)}
  • " - end.join("\n") + html = + themes + .map do |name, id| + "
  • #{CGI.escapeHTML(name)}
  • " + end + .join("\n") "#{I18n.t(i18n_key)}" end diff --git a/app/models/admin_dashboard_general_data.rb b/app/models/admin_dashboard_general_data.rb index 06962ac290..eff6304c54 100644 --- a/app/models/admin_dashboard_general_data.rb +++ b/app/models/admin_dashboard_general_data.rb @@ -2,11 +2,13 @@ class AdminDashboardGeneralData < AdminDashboardData def get_json - days_since_update = Discourse.last_commit_date ? ((DateTime.now - Discourse.last_commit_date) / 1.day).to_i : nil + days_since_update = + Discourse.last_commit_date ? ((DateTime.now - Discourse.last_commit_date) / 1.day).to_i : nil { updated_at: Time.zone.now.as_json, discourse_updated_at: Discourse.last_commit_date, - release_notes_link: "https://meta.discourse.org/c/announcements/67?tags=release-notes&before=#{days_since_update}" + release_notes_link: + "https://meta.discourse.org/c/announcements/67?tags=release-notes&before=#{days_since_update}", } end diff --git a/app/models/admin_dashboard_index_data.rb b/app/models/admin_dashboard_index_data.rb index 92da9f0087..b8f92d2dae 100644 --- a/app/models/admin_dashboard_index_data.rb +++ b/app/models/admin_dashboard_index_data.rb @@ -2,9 +2,7 @@ class AdminDashboardIndexData < AdminDashboardData def get_json - { - updated_at: Time.zone.now.as_json - } + { updated_at: Time.zone.now.as_json } end def self.stats_cache_key diff --git a/app/models/anonymous_user.rb b/app/models/anonymous_user.rb index 5714762508..b6b42e2eee 100644 --- a/app/models/anonymous_user.rb +++ b/app/models/anonymous_user.rb @@ -2,7 +2,7 @@ class AnonymousUser < ActiveRecord::Base belongs_to :user - belongs_to :master_user, class_name: 'User' + belongs_to :master_user, class_name: "User" end # == Schema Information diff --git a/app/models/api_key.rb b/app/models/api_key.rb index 1849f2cd39..dc66d2fb40 100644 --- a/app/models/api_key.rb +++ b/app/models/api_key.rb @@ -1,19 +1,21 @@ # frozen_string_literal: true class ApiKey < ActiveRecord::Base - class KeyAccessError < StandardError; end + class KeyAccessError < StandardError + end has_many :api_key_scopes belongs_to :user - belongs_to :created_by, class_name: 'User' + belongs_to :created_by, class_name: "User" scope :active, -> { where("revoked_at IS NULL") } scope :revoked, -> { where("revoked_at IS NOT NULL") } - scope :with_key, ->(key) { - hashed = self.hash_key(key) - where(key_hash: hashed) - } + scope :with_key, + ->(key) { + hashed = self.hash_key(key) + where(key_hash: hashed) + } after_initialize :generate_key @@ -26,7 +28,9 @@ class ApiKey < ActiveRecord::Base end def key - raise KeyAccessError.new "API key is only accessible immediately after creation" unless key_available? + unless key_available? + raise KeyAccessError.new "API key is only accessible immediately after creation" + end @key end @@ -40,10 +44,12 @@ class ApiKey < ActiveRecord::Base def self.revoke_unused_keys! return if SiteSetting.revoke_api_keys_days == 0 # Never expire keys - to_revoke = active.where("GREATEST(last_used_at, created_at, updated_at, :epoch) < :threshold", - epoch: last_used_epoch, - threshold: SiteSetting.revoke_api_keys_days.days.ago - ) + to_revoke = + active.where( + "GREATEST(last_used_at, created_at, updated_at, :epoch) < :threshold", + epoch: last_used_epoch, + threshold: SiteSetting.revoke_api_keys_days.days.ago, + ) to_revoke.find_each do |api_key| ApiKey.transaction do @@ -53,7 +59,12 @@ class ApiKey < ActiveRecord::Base api_key, UserHistory.actions[:api_key_update], changes: api_key.saved_changes, - context: I18n.t("staff_action_logs.api_key.automatic_revoked", count: SiteSetting.revoke_api_keys_days)) + context: + I18n.t( + "staff_action_logs.api_key.automatic_revoked", + count: SiteSetting.revoke_api_keys_days, + ), + ) end end end @@ -63,7 +74,9 @@ class ApiKey < ActiveRecord::Base end def request_allowed?(env) - return false if allowed_ips.present? && allowed_ips.none? { |ip| ip.include?(Rack::Request.new(env).ip) } + if allowed_ips.present? && allowed_ips.none? { |ip| ip.include?(Rack::Request.new(env).ip) } + return false + end return true if RouteMatcher.new(methods: :get, actions: "session#scopes").match?(env: env) api_key_scopes.blank? || api_key_scopes.any? { |s| s.permits?(env) } diff --git a/app/models/api_key_scope.rb b/app/models/api_key_scope.rb index 18db82addd..c0d072d0bc 100644 --- a/app/models/api_key_scope.rb +++ b/app/models/api_key_scope.rb @@ -18,26 +18,48 @@ class ApiKeyScope < ActiveRecord::Base mappings = { global: { - read: { methods: %i[get] } + read: { + methods: %i[get], + }, }, topics: { - write: { actions: %w[posts#create], params: %i[topic_id] }, - update: { actions: %w[topics#update], params: %i[topic_id] }, + write: { + actions: %w[posts#create], + params: %i[topic_id], + }, + update: { + actions: %w[topics#update], + params: %i[topic_id], + }, read: { actions: %w[topics#show topics#feed topics#posts], - params: %i[topic_id], aliases: { topic_id: :id } + params: %i[topic_id], + aliases: { + topic_id: :id, + }, }, read_lists: { - actions: list_actions, params: %i[category_id], - aliases: { category_id: :category_slug_path_with_id } - } + actions: list_actions, + params: %i[category_id], + aliases: { + category_id: :category_slug_path_with_id, + }, + }, }, posts: { - edit: { actions: %w[posts#update], params: %i[id] } + edit: { + actions: %w[posts#update], + params: %i[id], + }, }, categories: { - list: { actions: %w[categories#index] }, - show: { actions: %w[categories#show], params: %i[id] } + list: { + actions: %w[categories#index], + }, + show: { + actions: %w[categories#show], + params: %i[id], + }, }, uploads: { create: { @@ -49,42 +71,95 @@ class ApiKeyScope < ActiveRecord::Base uploads#batch_presign_multipart_parts uploads#abort_multipart uploads#complete_multipart - ] - } + ], + }, }, users: { - bookmarks: { actions: %w[users#bookmarks], params: %i[username] }, - sync_sso: { actions: %w[admin/users#sync_sso], params: %i[sso sig] }, - show: { actions: %w[users#show], params: %i[username external_id external_provider] }, - check_emails: { actions: %w[users#check_emails], params: %i[username] }, - update: { actions: %w[users#update], params: %i[username] }, - log_out: { actions: %w[admin/users#log_out] }, - anonymize: { actions: %w[admin/users#anonymize] }, - delete: { actions: %w[admin/users#destroy] }, - list: { actions: %w[admin/users#index] }, + bookmarks: { + actions: %w[users#bookmarks], + params: %i[username], + }, + sync_sso: { + actions: %w[admin/users#sync_sso], + params: %i[sso sig], + }, + show: { + actions: %w[users#show], + params: %i[username external_id external_provider], + }, + check_emails: { + actions: %w[users#check_emails], + params: %i[username], + }, + update: { + actions: %w[users#update], + params: %i[username], + }, + log_out: { + actions: %w[admin/users#log_out], + }, + anonymize: { + actions: %w[admin/users#anonymize], + }, + delete: { + actions: %w[admin/users#destroy], + }, + list: { + actions: %w[admin/users#index], + }, }, user_status: { - read: { actions: %w[user_status#get] }, - update: { actions: %w[user_status#set user_status#clear] }, + read: { + actions: %w[user_status#get], + }, + update: { + actions: %w[user_status#set user_status#clear], + }, }, email: { - receive_emails: { actions: %w[admin/email#handle_mail admin/email#smtp_should_reject] } + receive_emails: { + actions: %w[admin/email#handle_mail admin/email#smtp_should_reject], + }, }, badges: { - create: { actions: %w[admin/badges#create] }, - show: { actions: %w[badges#show] }, - update: { actions: %w[admin/badges#update] }, - delete: { actions: %w[admin/badges#destroy] }, - list_user_badges: { actions: %w[user_badges#username], params: %i[username] }, - assign_badge_to_user: { actions: %w[user_badges#create], params: %i[username] }, - revoke_badge_from_user: { actions: %w[user_badges#destroy] }, + create: { + actions: %w[admin/badges#create], + }, + show: { + actions: %w[badges#show], + }, + update: { + actions: %w[admin/badges#update], + }, + delete: { + actions: %w[admin/badges#destroy], + }, + list_user_badges: { + actions: %w[user_badges#username], + params: %i[username], + }, + assign_badge_to_user: { + actions: %w[user_badges#create], + params: %i[username], + }, + revoke_badge_from_user: { + actions: %w[user_badges#destroy], + }, }, wordpress: { - publishing: { actions: %w[site#site posts#create topics#update topics#status topics#show] }, - commenting: { actions: %w[topics#wordpress] }, - discourse_connect: { actions: %w[admin/users#sync_sso admin/users#log_out admin/users#index users#show] }, - utilities: { actions: %w[users#create groups#index] } - } + publishing: { + actions: %w[site#site posts#create topics#update topics#status topics#show], + }, + commenting: { + actions: %w[topics#wordpress], + }, + discourse_connect: { + actions: %w[admin/users#sync_sso admin/users#log_out admin/users#index users#show], + }, + utilities: { + actions: %w[users#create groups#index], + }, + }, } parse_resources!(mappings) @@ -106,7 +181,10 @@ class ApiKeyScope < ActiveRecord::Base def parse_resources!(mappings) mappings.each_value do |resource_actions| resource_actions.each_value do |action_data| - action_data[:urls] = find_urls(actions: action_data[:actions], methods: action_data[:methods]) + action_data[:urls] = find_urls( + actions: action_data[:actions], + methods: action_data[:methods], + ) end end end @@ -128,12 +206,12 @@ class ApiKeyScope < ActiveRecord::Base set.routes.each do |route| defaults = route.defaults action = "#{defaults[:controller].to_s}##{defaults[:action]}" - path = route.path.spec.to_s.gsub(/\(\.:format\)/, '') - api_supported_path = ( - path.end_with?('.rss') || - !route.path.requirements[:format] || - route.path.requirements[:format].match?('json') - ) + path = route.path.spec.to_s.gsub(/\(\.:format\)/, "") + api_supported_path = + ( + path.end_with?(".rss") || !route.path.requirements[:format] || + route.path.requirements[:format].match?("json") + ) excluded_paths = %w[/new-topic /new-message /exception] if actions.include?(action) && api_supported_path && !excluded_paths.include?(path) @@ -143,18 +221,16 @@ class ApiKeyScope < ActiveRecord::Base end end - if methods.present? - methods.each do |method| - urls << "* (#{method})" - end - end + methods.each { |method| urls << "* (#{method})" } if methods.present? urls.to_a end end def permits?(env) - RouteMatcher.new(**mapping.except(:urls), allowed_param_values: allowed_parameters).match?(env: env) + RouteMatcher.new(**mapping.except(:urls), allowed_param_values: allowed_parameters).match?( + env: env, + ) end private diff --git a/app/models/application_request.rb b/app/models/application_request.rb index 81125566e4..d422a08c96 100644 --- a/app/models/application_request.rb +++ b/app/models/application_request.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true class ApplicationRequest < ActiveRecord::Base - - enum req_type: %i(http_total - http_2xx - http_background - http_3xx - http_4xx - http_5xx - page_view_crawler - page_view_logged_in - page_view_anon - page_view_logged_in_mobile - page_view_anon_mobile - api - user_api) + enum req_type: %i[ + http_total + http_2xx + http_background + http_3xx + http_4xx + http_5xx + page_view_crawler + page_view_logged_in + page_view_anon + page_view_logged_in_mobile + page_view_anon_mobile + api + user_api + ] include CachedCounting @@ -36,14 +37,12 @@ class ApplicationRequest < ActiveRecord::Base end def self.req_id(date, req_type, retries = 0) - req_type_id = req_types[req_type] # a poor man's upsert id = where(date: date, req_type: req_type_id).pluck_first(:id) id ||= create!(date: date, req_type: req_type_id, count: 0).id - - rescue # primary key violation + rescue StandardError # primary key violation if retries == 0 req_id(date, req_type, 1) else @@ -56,9 +55,9 @@ class ApplicationRequest < ActiveRecord::Base self.req_types.each do |key, i| query = self.where(req_type: i) - s["#{key}_total"] = query.sum(:count) + s["#{key}_total"] = query.sum(:count) s["#{key}_30_days"] = query.where("date > ?", 30.days.ago).sum(:count) - s["#{key}_7_days"] = query.where("date > ?", 7.days.ago).sum(:count) + s["#{key}_7_days"] = query.where("date > ?", 7.days.ago).sum(:count) end s diff --git a/app/models/associated_group.rb b/app/models/associated_group.rb index 9d6e9365dc..3c8c635094 100644 --- a/app/models/associated_group.rb +++ b/app/models/associated_group.rb @@ -14,9 +14,11 @@ class AssociatedGroup < ActiveRecord::Base end def self.cleanup! - AssociatedGroup.left_joins(:group_associated_groups, :user_associated_groups) + AssociatedGroup + .left_joins(:group_associated_groups, :user_associated_groups) .where("group_associated_groups.id IS NULL AND user_associated_groups.id IS NULL") - .where("last_used < ?", 1.week.ago).delete_all + .where("last_used < ?", 1.week.ago) + .delete_all end end diff --git a/app/models/auto_track_duration_site_setting.rb b/app/models/auto_track_duration_site_setting.rb index 9764d0d525..0aad12e6e4 100644 --- a/app/models/auto_track_duration_site_setting.rb +++ b/app/models/auto_track_duration_site_setting.rb @@ -1,28 +1,25 @@ # frozen_string_literal: true class AutoTrackDurationSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.auto_track_options.never', value: -1 }, - { name: 'user.auto_track_options.immediately', value: 0 }, - { name: 'user.auto_track_options.after_30_seconds', value: 1000 * 30 }, - { name: 'user.auto_track_options.after_1_minute', value: 1000 * 60 }, - { name: 'user.auto_track_options.after_2_minutes', value: 1000 * 60 * 2 }, - { name: 'user.auto_track_options.after_3_minutes', value: 1000 * 60 * 3 }, - { name: 'user.auto_track_options.after_4_minutes', value: 1000 * 60 * 4 }, - { name: 'user.auto_track_options.after_5_minutes', value: 1000 * 60 * 5 }, - { name: 'user.auto_track_options.after_10_minutes', value: 1000 * 60 * 10 }, + { name: "user.auto_track_options.never", value: -1 }, + { name: "user.auto_track_options.immediately", value: 0 }, + { name: "user.auto_track_options.after_30_seconds", value: 1000 * 30 }, + { name: "user.auto_track_options.after_1_minute", value: 1000 * 60 }, + { name: "user.auto_track_options.after_2_minutes", value: 1000 * 60 * 2 }, + { name: "user.auto_track_options.after_3_minutes", value: 1000 * 60 * 3 }, + { name: "user.auto_track_options.after_4_minutes", value: 1000 * 60 * 4 }, + { name: "user.auto_track_options.after_5_minutes", value: 1000 * 60 * 5 }, + { name: "user.auto_track_options.after_10_minutes", value: 1000 * 60 * 10 }, ] end def self.translate_names? true end - end diff --git a/app/models/backup_file.rb b/app/models/backup_file.rb index a8a09731a1..4637769104 100644 --- a/app/models/backup_file.rb +++ b/app/models/backup_file.rb @@ -3,10 +3,7 @@ class BackupFile include ActiveModel::SerializerSupport - attr_reader :filename, - :size, - :last_modified, - :source + attr_reader :filename, :size, :last_modified, :source def initialize(filename:, size:, last_modified:, source: nil) @filename = filename diff --git a/app/models/backup_location_site_setting.rb b/app/models/backup_location_site_setting.rb index 4236ca1de7..1bdbd2403d 100644 --- a/app/models/backup_location_site_setting.rb +++ b/app/models/backup_location_site_setting.rb @@ -11,7 +11,7 @@ class BackupLocationSiteSetting < EnumSiteSetting def self.values @values ||= [ { name: "admin.backups.location.local", value: LOCAL }, - { name: "admin.backups.location.s3", value: S3 } + { name: "admin.backups.location.s3", value: S3 }, ] end diff --git a/app/models/badge.rb b/app/models/badge.rb index 2a84344cad..d8b1ce794d 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -2,7 +2,7 @@ class Badge < ActiveRecord::Base # TODO: Drop in July 2021 - self.ignored_columns = %w{image} + self.ignored_columns = %w[image] include GlobalPath include HasSanitizableFields @@ -76,10 +76,15 @@ class Badge < ActiveRecord::Base attr_accessor :has_badge def self.trigger_hash - @trigger_hash ||= Badge::Trigger.constants.map do |k| - name = k.to_s.underscore - [name, Badge::Trigger.const_get(k)] unless name =~ /deprecated/ - end.compact.to_h + @trigger_hash ||= + Badge::Trigger + .constants + .map do |k| + name = k.to_s.underscore + [name, Badge::Trigger.const_get(k)] unless name =~ /deprecated/ + end + .compact + .to_h end module Trigger @@ -105,7 +110,7 @@ class Badge < ActiveRecord::Base belongs_to :badge_type belongs_to :badge_grouping - belongs_to :image_upload, class_name: 'Upload' + belongs_to :image_upload, class_name: "Upload" has_many :user_badges, dependent: :destroy has_many :upload_references, as: :target, dependent: :destroy @@ -134,11 +139,7 @@ class Badge < ActiveRecord::Base # fields that can not be edited on system badges def self.protected_system_fields - [ - :name, :badge_type_id, :multiple_grant, - :target_posts, :show_posts, :query, - :trigger, :auto_revoke, :listable - ] + %i[name badge_type_id multiple_grant target_posts show_posts query trigger auto_revoke listable] end def self.trust_level_badge_ids @@ -152,7 +153,7 @@ class Badge < ActiveRecord::Base GreatPost => 50, NiceTopic => 10, GoodTopic => 25, - GreatTopic => 50 + GreatTopic => 50, } end @@ -219,7 +220,7 @@ class Badge < ActiveRecord::Base end def self.i18n_name(name) - name.downcase.tr(' ', '_') + name.downcase.tr(" ", "_") end def self.display_name(name) @@ -231,8 +232,8 @@ class Badge < ActiveRecord::Base end def self.find_system_badge_id_from_translation_key(translation_key) - return unless translation_key.starts_with?('badges.') - badge_name_klass = translation_key.split('.').second.camelize + return unless translation_key.starts_with?("badges.") + badge_name_klass = translation_key.split(".").second.camelize Badge.const_defined?(badge_name_klass) ? "Badge::#{badge_name_klass}".constantize : nil end @@ -283,7 +284,12 @@ class Badge < ActiveRecord::Base def long_description key = "badges.#{i18n_name}.long_description" - I18n.t(key, default: self[:long_description] || '', base_uri: Discourse.base_path, max_likes_per_day: SiteSetting.max_likes_per_day) + I18n.t( + key, + default: self[:long_description] || "", + base_uri: Discourse.base_path, + max_likes_per_day: SiteSetting.max_likes_per_day, + ) end def long_description=(val) @@ -293,7 +299,12 @@ class Badge < ActiveRecord::Base def description key = "badges.#{i18n_name}.description" - I18n.t(key, default: self[:description] || '', base_uri: Discourse.base_path, max_likes_per_day: SiteSetting.max_likes_per_day) + I18n.t( + key, + default: self[:description] || "", + base_uri: Discourse.base_path, + max_likes_per_day: SiteSetting.max_likes_per_day, + ) end def description=(val) @@ -302,7 +313,7 @@ class Badge < ActiveRecord::Base end def slug - Slug.for(self.display_name, '-') + Slug.for(self.display_name, "-") end def manually_grantable? @@ -314,9 +325,7 @@ class Badge < ActiveRecord::Base end def image_url - if image_upload_id.present? - upload_cdn_path(image_upload.url) - end + upload_cdn_path(image_upload.url) if image_upload_id.present? end def for_beginners? @@ -330,9 +339,7 @@ class Badge < ActiveRecord::Base end def sanitize_description - if description_changed? - self.description = sanitize_field(self.description) - end + self.description = sanitize_field(self.description) if description_changed? end end diff --git a/app/models/badge_grouping.rb b/app/models/badge_grouping.rb index b95c78f1c4..c0afcbdbb1 100644 --- a/app/models/badge_grouping.rb +++ b/app/models/badge_grouping.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class BadgeGrouping < ActiveRecord::Base - GettingStarted = 1 Community = 2 Posting = 3 diff --git a/app/models/base_font_setting.rb b/app/models/base_font_setting.rb index 36c0321876..d4e3971f0d 100644 --- a/app/models/base_font_setting.rb +++ b/app/models/base_font_setting.rb @@ -8,8 +8,6 @@ class BaseFontSetting < EnumSiteSetting end def self.values - @values ||= DiscourseFonts.fonts.map do |font| - { name: font[:name], value: font[:key] } - end + @values ||= DiscourseFonts.fonts.map { |font| { name: font[:name], value: font[:key] } } end end diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index c6947cf078..3e1a6dab38 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -3,7 +3,7 @@ class Bookmark < ActiveRecord::Base self.ignored_columns = [ "post_id", # TODO (martin) (2022-08-01) remove - "for_topic" # TODO (martin) (2022-08-01) remove + "for_topic", # TODO (martin) (2022-08-01) remove ] cattr_accessor :registered_bookmarkables @@ -42,26 +42,24 @@ class Bookmark < ActiveRecord::Base belongs_to :bookmarkable, polymorphic: true def self.auto_delete_preferences - @auto_delete_preferences ||= Enum.new( - never: 0, - when_reminder_sent: 1, - on_owner_reply: 2, - clear_reminder: 3, - ) + @auto_delete_preferences ||= + Enum.new(never: 0, when_reminder_sent: 1, on_owner_reply: 2, clear_reminder: 3) end def self.select_type(bookmarks_relation, type) bookmarks_relation.select { |bm| bm.bookmarkable_type == type } end - validate :polymorphic_columns_present, on: [:create, :update] - validate :valid_bookmarkable_type, on: [:create, :update] + validate :polymorphic_columns_present, on: %i[create update] + validate :valid_bookmarkable_type, on: %i[create update] validate :unique_per_bookmarkable, - on: [:create, :update], - if: Proc.new { |b| - b.will_save_change_to_bookmarkable_id? || b.will_save_change_to_bookmarkable_type? || b.will_save_change_to_user_id? - } + on: %i[create update], + if: + Proc.new { |b| + b.will_save_change_to_bookmarkable_id? || b.will_save_change_to_bookmarkable_type? || + b.will_save_change_to_user_id? + } validate :ensure_sane_reminder_at_time, if: :will_save_change_to_reminder_at? validate :bookmark_limit_not_reached @@ -78,7 +76,13 @@ class Bookmark < ActiveRecord::Base end def unique_per_bookmarkable - return if !Bookmark.exists?(user_id: user_id, bookmarkable_id: bookmarkable_id, bookmarkable_type: bookmarkable_type) + if !Bookmark.exists?( + user_id: user_id, + bookmarkable_id: bookmarkable_id, + bookmarkable_type: bookmarkable_type, + ) + return + end self.errors.add(:base, I18n.t("bookmarks.errors.already_bookmarked", type: bookmarkable_type)) end @@ -102,15 +106,18 @@ class Bookmark < ActiveRecord::Base I18n.t( "bookmarks.errors.too_many", user_bookmarks_url: "#{Discourse.base_url}/my/activity/bookmarks", - limit: SiteSetting.max_bookmarks_per_user - ) + limit: SiteSetting.max_bookmarks_per_user, + ), ) end def valid_bookmarkable_type return if Bookmark.valid_bookmarkable_types.include?(self.bookmarkable_type) - self.errors.add(:base, I18n.t("bookmarks.errors.invalid_bookmarkable", type: self.bookmarkable_type)) + self.errors.add( + :base, + I18n.t("bookmarks.errors.invalid_bookmarkable", type: self.bookmarkable_type), + ) end def auto_delete_when_reminder_sent? @@ -126,54 +133,57 @@ class Bookmark < ActiveRecord::Base end def clear_reminder! - update!( - reminder_last_sent_at: Time.zone.now, - reminder_set_at: nil, - ) + update!(reminder_last_sent_at: Time.zone.now, reminder_set_at: nil) end - scope :with_reminders, -> do - where("reminder_at IS NOT NULL") - end + scope :with_reminders, -> { where("reminder_at IS NOT NULL") } - scope :pending_reminders, ->(before_time = Time.now.utc) do - with_reminders.where("reminder_at <= ?", before_time).where(reminder_last_sent_at: nil) - end + scope :pending_reminders, + ->(before_time = Time.now.utc) { + with_reminders.where("reminder_at <= ?", before_time).where(reminder_last_sent_at: nil) + } - scope :pending_reminders_for_user, ->(user) do - pending_reminders.where(user: user) - end + scope :pending_reminders_for_user, ->(user) { pending_reminders.where(user: user) } - scope :for_user_in_topic, ->(user_id, topic_id) { - joins("LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'") - .joins("LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR - (topics.id = posts.topic_id)") - .where( - "bookmarks.user_id = :user_id AND (topics.id = :topic_id OR posts.topic_id = :topic_id) + scope :for_user_in_topic, + ->(user_id, topic_id) { + joins( + "LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", + ).joins( + "LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR + (topics.id = posts.topic_id)", + ).where( + "bookmarks.user_id = :user_id AND (topics.id = :topic_id OR posts.topic_id = :topic_id) AND posts.deleted_at IS NULL AND topics.deleted_at IS NULL", - user_id: user_id, topic_id: topic_id - ) - } + user_id: user_id, + topic_id: topic_id, + ) + } def self.count_per_day(opts = nil) opts ||= {} - result = where('bookmarks.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago) + result = + where( + "bookmarks.created_at >= ?", + opts[:start_date] || (opts[:since_days_ago] || 30).days.ago, + ) - if opts[:end_date] - result = result.where('bookmarks.created_at <= ?', opts[:end_date]) - end + result = result.where("bookmarks.created_at <= ?", opts[:end_date]) if opts[:end_date] if opts[:category_id] - result = result - .joins("LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'") - .joins("LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR (topics.id = posts.topic_id)") - .where("topics.deleted_at IS NULL AND posts.deleted_at IS NULL") - .merge(Topic.in_category_and_subcategories(opts[:category_id])) + result = + result + .joins( + "LEFT JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", + ) + .joins( + "LEFT JOIN topics ON (topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic') OR (topics.id = posts.topic_id)", + ) + .where("topics.deleted_at IS NULL AND posts.deleted_at IS NULL") + .merge(Topic.in_category_and_subcategories(opts[:category_id])) end - result.group('date(bookmarks.created_at)') - .order('date(bookmarks.created_at)') - .count + result.group("date(bookmarks.created_at)").order("date(bookmarks.created_at)").count end ## diff --git a/app/models/category.rb b/app/models/category.rb index c68ad7f1c9..fc721f0940 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class Category < ActiveRecord::Base - RESERVED_SLUGS = [ - 'none' - ] + RESERVED_SLUGS = ["none"] self.ignored_columns = [ :suppress_from_latest, # TODO(2020-11-18): remove @@ -18,9 +16,9 @@ class Category < ActiveRecord::Base include AnonCacheInvalidator include HasDestroyedWebHook - REQUIRE_TOPIC_APPROVAL = 'require_topic_approval' - REQUIRE_REPLY_APPROVAL = 'require_reply_approval' - NUM_AUTO_BUMP_DAILY = 'num_auto_bump_daily' + REQUIRE_TOPIC_APPROVAL = "require_topic_approval" + REQUIRE_REPLY_APPROVAL = "require_reply_approval" + NUM_AUTO_BUMP_DAILY = "num_auto_bump_daily" register_custom_field_type(REQUIRE_TOPIC_APPROVAL, :boolean) register_custom_field_type(REQUIRE_REPLY_APPROVAL, :boolean) @@ -28,9 +26,9 @@ class Category < ActiveRecord::Base belongs_to :topic belongs_to :topic_only_relative_url, - -> { select "id, title, slug" }, - class_name: "Topic", - foreign_key: "topic_id" + -> { select "id, title, slug" }, + class_name: "Topic", + foreign_key: "topic_id" belongs_to :user belongs_to :latest_post, class_name: "Post" @@ -52,10 +50,20 @@ class Category < ActiveRecord::Base validates :user_id, presence: true - validates :name, if: Proc.new { |c| c.new_record? || c.will_save_change_to_name? || c.will_save_change_to_parent_category_id? }, - presence: true, - uniqueness: { scope: :parent_category_id, case_sensitive: false }, - length: { in: 1..50 } + validates :name, + if: + Proc.new { |c| + c.new_record? || c.will_save_change_to_name? || + c.will_save_change_to_parent_category_id? + }, + presence: true, + uniqueness: { + scope: :parent_category_id, + case_sensitive: false, + }, + length: { + in: 1..50, + } validates :num_featured_topics, numericality: { only_integer: true, greater_than: 0 } validates :search_priority, inclusion: { in: Searchable::PRIORITIES.values } @@ -65,7 +73,12 @@ class Category < ActiveRecord::Base validate :ensure_slug validate :permissions_compatibility_validator - validates :auto_close_hours, numericality: { greater_than: 0, less_than_or_equal_to: 87600 }, allow_nil: true + validates :auto_close_hours, + numericality: { + greater_than: 0, + less_than_or_equal_to: 87_600, + }, + allow_nil: true validates :slug, exclusion: { in: RESERVED_SLUGS } after_create :create_category_definition @@ -84,7 +97,8 @@ class Category < ActiveRecord::Base after_save :update_reviewables after_save do - if saved_change_to_uploaded_logo_id? || saved_change_to_uploaded_logo_dark_id? || saved_change_to_uploaded_background_id? + if saved_change_to_uploaded_logo_id? || saved_change_to_uploaded_logo_dark_id? || + saved_change_to_uploaded_background_id? upload_ids = [self.uploaded_logo_id, self.uploaded_logo_dark_id, self.uploaded_background_id] UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) end @@ -106,8 +120,8 @@ class Category < ActiveRecord::Base after_save_commit :index_search - belongs_to :parent_category, class_name: 'Category' - has_many :subcategories, class_name: 'Category', foreign_key: 'parent_category_id' + belongs_to :parent_category, class_name: "Category" + has_many :subcategories, class_name: "Category", foreign_key: "parent_category_id" has_many :category_tags, dependent: :destroy has_many :tags, through: :category_tags @@ -117,65 +131,74 @@ class Category < ActiveRecord::Base has_many :category_required_tag_groups, -> { order(order: :asc) }, dependent: :destroy has_many :sidebar_section_links, as: :linkable, dependent: :delete_all - belongs_to :reviewable_by_group, class_name: 'Group' + belongs_to :reviewable_by_group, class_name: "Group" - scope :latest, -> { order('topic_count DESC') } + scope :latest, -> { order("topic_count DESC") } - scope :secured, -> (guardian = nil) { - ids = guardian.secure_category_ids if guardian + scope :secured, + ->(guardian = nil) { + ids = guardian.secure_category_ids if guardian - if ids.present? - where("NOT categories.read_restricted OR categories.id IN (:cats)", cats: ids).references(:categories) - else - where("NOT categories.read_restricted").references(:categories) - end - } + if ids.present? + where( + "NOT categories.read_restricted OR categories.id IN (:cats)", + cats: ids, + ).references(:categories) + else + where("NOT categories.read_restricted").references(:categories) + end + } TOPIC_CREATION_PERMISSIONS ||= [:full] - POST_CREATION_PERMISSIONS ||= [:create_post, :full] + POST_CREATION_PERMISSIONS ||= %i[create_post full] - scope :topic_create_allowed, -> (guardian) do + scope :topic_create_allowed, + ->(guardian) { + scoped = scoped_to_permissions(guardian, TOPIC_CREATION_PERMISSIONS) - scoped = scoped_to_permissions(guardian, TOPIC_CREATION_PERMISSIONS) + if !SiteSetting.allow_uncategorized_topics && !guardian.is_staff? + scoped = scoped.where.not(id: SiteSetting.uncategorized_category_id) + end - if !SiteSetting.allow_uncategorized_topics && !guardian.is_staff? - scoped = scoped.where.not(id: SiteSetting.uncategorized_category_id) - end + scoped + } - scoped - end + scope :post_create_allowed, + ->(guardian) { scoped_to_permissions(guardian, POST_CREATION_PERMISSIONS) } - scope :post_create_allowed, -> (guardian) { scoped_to_permissions(guardian, POST_CREATION_PERMISSIONS) } - - delegate :post_template, to: 'self.class' + delegate :post_template, to: "self.class" # permission is just used by serialization # we may consider wrapping this in another spot - attr_accessor :displayable_topics, :permission, :subcategory_ids, :subcategory_list, :notification_level, :has_children + attr_accessor :displayable_topics, + :permission, + :subcategory_ids, + :subcategory_list, + :notification_level, + :has_children # Allows us to skip creating the category definition topic in tests. attr_accessor :skip_category_definition - @topic_id_cache = DistributedCache.new('category_topic_ids') + @topic_id_cache = DistributedCache.new("category_topic_ids") def self.topic_ids - @topic_id_cache['ids'] || reset_topic_ids_cache + @topic_id_cache["ids"] || reset_topic_ids_cache end def self.reset_topic_ids_cache - @topic_id_cache['ids'] = Set.new(Category.pluck(:topic_id).compact) + @topic_id_cache["ids"] = Set.new(Category.pluck(:topic_id).compact) end def reset_topic_ids_cache Category.reset_topic_ids_cache end - @@subcategory_ids = DistributedCache.new('subcategory_ids') + @@subcategory_ids = DistributedCache.new("subcategory_ids") def self.subcategory_ids(category_id) - @@subcategory_ids[category_id] ||= - begin - sql = <<~SQL + @@subcategory_ids[category_id] ||= begin + sql = <<~SQL WITH RECURSIVE subcategories AS ( SELECT :category_id id, 1 depth UNION @@ -186,12 +209,12 @@ class Category < ActiveRecord::Base ) SELECT id FROM subcategories SQL - DB.query_single( - sql, - category_id: category_id, - max_category_nesting: SiteSetting.max_category_nesting - ) - end + DB.query_single( + sql, + category_id: category_id, + max_category_nesting: SiteSetting.max_category_nesting, + ) + end end def self.clear_subcategory_ids @@ -217,7 +240,8 @@ class Category < ActiveRecord::Base end else permissions = permission_types.map { |p| CategoryGroup.permission_types[p] } - where("(:staged AND LENGTH(COALESCE(email_in, '')) > 0 AND email_in_allow_strangers) + where( + "(:staged AND LENGTH(COALESCE(email_in, '')) > 0 AND email_in_allow_strangers) OR categories.id NOT IN (SELECT category_id FROM category_groups) OR categories.id IN ( SELECT category_id @@ -228,16 +252,21 @@ class Category < ActiveRecord::Base staged: guardian.is_staged?, permissions: permissions, user_id: guardian.user.id, - everyone: Group::AUTO_GROUPS[:everyone]) + everyone: Group::AUTO_GROUPS[:everyone], + ) end end def self.update_stats - topics_with_post_count = Topic - .select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count") - .where("topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)") - .group("topics.category_id") - .visible.to_sql + topics_with_post_count = + Topic + .select("topics.category_id, COUNT(*) topic_count, SUM(topics.posts_count) post_count") + .where( + "topics.id NOT IN (select cc.topic_id from categories cc WHERE topic_id IS NOT NULL)", + ) + .group("topics.category_id") + .visible + .to_sql DB.exec <<~SQL UPDATE categories c @@ -265,29 +294,31 @@ class Category < ActiveRecord::Base Category.all.each do |c| topics = c.topics.visible - topics = topics.where(['topics.id <> ?', c.topic_id]) if c.topic_id - c.topics_year = topics.created_since(1.year.ago).count + topics = topics.where(["topics.id <> ?", c.topic_id]) if c.topic_id + c.topics_year = topics.created_since(1.year.ago).count c.topics_month = topics.created_since(1.month.ago).count - c.topics_week = topics.created_since(1.week.ago).count - c.topics_day = topics.created_since(1.day.ago).count + c.topics_week = topics.created_since(1.week.ago).count + c.topics_day = topics.created_since(1.day.ago).count posts = c.visible_posts - c.posts_year = posts.created_since(1.year.ago).count + c.posts_year = posts.created_since(1.year.ago).count c.posts_month = posts.created_since(1.month.ago).count - c.posts_week = posts.created_since(1.week.ago).count - c.posts_day = posts.created_since(1.day.ago).count + c.posts_week = posts.created_since(1.week.ago).count + c.posts_day = posts.created_since(1.day.ago).count c.save if c.changed? end end def visible_posts - query = Post.joins(:topic) - .where(['topics.category_id = ?', self.id]) - .where('topics.visible = true') - .where('posts.deleted_at IS NULL') - .where('posts.user_deleted = false') - self.topic_id ? query.where(['topics.id <> ?', self.topic_id]) : query + query = + Post + .joins(:topic) + .where(["topics.category_id = ?", self.id]) + .where("topics.visible = true") + .where("posts.deleted_at IS NULL") + .where("posts.user_deleted = false") + self.topic_id ? query.where(["topics.id <> ?", self.topic_id]) : query end # Internal: Generate the text of post prompting to enter category description. @@ -299,7 +330,13 @@ class Category < ActiveRecord::Base return if skip_category_definition Topic.transaction do - t = Topic.new(title: I18n.t("category.topic_prefix", category: name), user: user, pinned_at: Time.now, category_id: id) + t = + Topic.new( + title: I18n.t("category.topic_prefix", category: name), + user: user, + pinned_at: Time.now, + category_id: id, + ) t.skip_callbacks = true t.ignore_category_auto_close = true t.delete_topic_timer(TopicTimer.types[:close]) @@ -318,9 +355,7 @@ class Category < ActiveRecord::Base end def clear_related_site_settings - if self.id == SiteSetting.general_category_id - SiteSetting.general_category_id = -1 - end + SiteSetting.general_category_id = -1 if self.id == SiteSetting.general_category_id end def topic_url @@ -345,9 +380,7 @@ class Category < ActiveRecord::Base return nil unless self.description @@cache_excerpt ||= LruRedux::ThreadSafeCache.new(1000) - @@cache_excerpt.getset(self.description) do - PrettyText.excerpt(description, 300) - end + @@cache_excerpt.getset(self.description) { PrettyText.excerpt(description, 300) } end def access_category_via_group @@ -370,20 +403,20 @@ class Category < ActiveRecord::Base if slug.present? # if we don't unescape it first we strip the % from the encoded version - slug = SiteSetting.slug_generation_method == 'encoded' ? CGI.unescape(self.slug) : self.slug - self.slug = Slug.for(slug, '', method: :encoded) + slug = SiteSetting.slug_generation_method == "encoded" ? CGI.unescape(self.slug) : self.slug + self.slug = Slug.for(slug, "", method: :encoded) if self.slug.blank? errors.add(:slug, :invalid) - elsif SiteSetting.slug_generation_method == 'ascii' && !CGI.unescape(self.slug).ascii_only? + elsif SiteSetting.slug_generation_method == "ascii" && !CGI.unescape(self.slug).ascii_only? errors.add(:slug, I18n.t("category.errors.slug_contains_non_ascii_chars")) elsif duplicate_slug? errors.add(:slug, I18n.t("category.errors.is_already_in_use")) end else # auto slug - self.slug = Slug.for(name, '') - self.slug = '' if duplicate_slug? + self.slug = Slug.for(name, "") + self.slug = "" if duplicate_slug? end # only allow to use category itself id. @@ -403,30 +436,27 @@ class Category < ActiveRecord::Base if group_ids.present? MessageBus.publish( - '/categories', + "/categories", { categories: ActiveModel::ArraySerializer.new([self]).as_json }, - group_ids: group_ids + group_ids: group_ids, ) end else MessageBus.publish( - '/categories', - { categories: ActiveModel::ArraySerializer.new([self]).as_json } + "/categories", + { categories: ActiveModel::ArraySerializer.new([self]).as_json }, ) end end def remove_site_settings SiteSetting.all_settings.each do |s| - if s[:type] == 'category' && s[:value].to_i == self.id - SiteSetting.set(s[:setting], '') - end + SiteSetting.set(s[:setting], "") if s[:type] == "category" && s[:value].to_i == self.id end - end def publish_category_deletion - MessageBus.publish('/categories', deleted_categories: [self.id]) + MessageBus.publish("/categories", deleted_categories: [self.id]) end # This is used in a validation so has to produce accurate results before the @@ -492,7 +522,9 @@ class Category < ActiveRecord::Base errors.add(:base, I18n.t("category.errors.self_parent")) if parent_category_id == id total_depth = height_of_ancestors + 1 + depth_of_descendants - errors.add(:base, I18n.t("category.errors.depth")) if total_depth > SiteSetting.max_category_nesting + if total_depth > SiteSetting.max_category_nesting + errors.add(:base, I18n.t("category.errors.depth")) + end end end @@ -500,9 +532,7 @@ class Category < ActiveRecord::Base # this line bothers me, destroying in AR can not seem to be queued, thinking of extending it category_groups.destroy_all unless new_record? ids = Group.where(name: names.split(",")).pluck(:id) - ids.each do |id| - category_groups.build(group_id: id) - end + ids.each { |id| category_groups.build(group_id: id) } end # will reset permission on a topic to a particular @@ -527,11 +557,13 @@ class Category < ActiveRecord::Base def permissions_params hash = {} - category_groups.includes(:group).each do |category_group| - if category_group.group.present? - hash[category_group.group_name] = category_group.permission_type + category_groups + .includes(:group) + .each do |category_group| + if category_group.group.present? + hash[category_group.group_name] = category_group.permission_type + end end - end hash end @@ -551,17 +583,16 @@ class Category < ActiveRecord::Base everyone = Group::AUTO_GROUPS[:everyone] full = CategoryGroup.permission_types[:full] - mapped = permissions.map do |group, permission| - group_id = Group.group_id_from_param(group) - permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) + mapped = + permissions.map do |group, permission| + group_id = Group.group_id_from_param(group) + permission = CategoryGroup.permission_types[permission] unless permission.is_a?(Integer) - [group_id, permission] - end + [group_id, permission] + end mapped.each do |group, permission| - if group == everyone && permission == full - return [false, []] - end + return false, [] if group == everyone && permission == full read_restricted = false if group == everyone end @@ -587,7 +618,7 @@ class Category < ActiveRecord::Base def auto_bump_limiter return nil if num_auto_bump_daily.to_i == 0 - RateLimiter.new(nil, "auto_bump_limit_#{self.id}", 1, 86400 / num_auto_bump_daily.to_i) + RateLimiter.new(nil, "auto_bump_limit_#{self.id}", 1, 86_400 / num_auto_bump_daily.to_i) end def clear_auto_bump_cache! @@ -597,10 +628,11 @@ class Category < ActiveRecord::Base def self.auto_bump_topic! bumped = false - auto_bumps = CategoryCustomField - .where(name: Category::NUM_AUTO_BUMP_DAILY) - .where('NULLIF(value, \'\')::int > 0') - .pluck(:category_id) + auto_bumps = + CategoryCustomField + .where(name: Category::NUM_AUTO_BUMP_DAILY) + .where('NULLIF(value, \'\')::int > 0') + .pluck(:category_id) if (auto_bumps.length > 0) auto_bumps.shuffle.each do |category_id| @@ -625,23 +657,20 @@ class Category < ActiveRecord::Base relation = Topic - if filters.length > 0 - filters.each do |filter| - relation = filter.call(relation) - end - end + filters.each { |filter| relation = filter.call(relation) } if filters.length > 0 - topic = relation - .visible - .listable_topics - .exclude_scheduled_bump_topics - .where(category_id: self.id) - .where('id <> ?', self.topic_id) - .where('bumped_at < ?', 1.day.ago) - .where('pinned_at IS NULL AND NOT closed AND NOT archived') - .order('bumped_at ASC') - .limit(1) - .first + topic = + relation + .visible + .listable_topics + .exclude_scheduled_bump_topics + .where(category_id: self.id) + .where("id <> ?", self.topic_id) + .where("bumped_at < ?", 1.day.ago) + .where("pinned_at IS NULL AND NOT closed AND NOT archived") + .order("bumped_at ASC") + .limit(1) + .first if topic topic.add_small_action(Discourse.system_user, "autobumped", nil, bump: true) @@ -650,7 +679,6 @@ class Category < ActiveRecord::Base else false end - end def allowed_tags=(tag_names_arg) @@ -662,13 +690,20 @@ class Category < ActiveRecord::Base end def required_tag_groups=(required_groups) - map = Array(required_groups).map.with_index { |rg, i| [rg["name"], { min_count: rg["min_count"].to_i, order: i }] }.to_h + map = + Array(required_groups) + .map + .with_index { |rg, i| [rg["name"], { min_count: rg["min_count"].to_i, order: i }] } + .to_h tag_groups = TagGroup.where(name: map.keys) - self.category_required_tag_groups = tag_groups.map do |tag_group| - attrs = map[tag_group.name] - CategoryRequiredTagGroup.new(tag_group: tag_group, **attrs) - end.sort_by(&:order) + self.category_required_tag_groups = + tag_groups + .map do |tag_group| + attrs = map[tag_group.name] + CategoryRequiredTagGroup.new(tag_group: tag_group, **attrs) + end + .sort_by(&:order) end def downcase_email @@ -677,17 +712,32 @@ class Category < ActiveRecord::Base def email_in_validator return if self.email_in.blank? - email_in.split("|").each do |email| - - escaped = Rack::Utils.escape_html(email) - if !Email.is_valid?(email) - self.errors.add(:base, I18n.t('category.errors.invalid_email_in', email: escaped)) - elsif group = Group.find_by_email(email) - self.errors.add(:base, I18n.t('category.errors.email_already_used_in_group', email: escaped, group_name: Rack::Utils.escape_html(group.name))) - elsif category = Category.where.not(id: self.id).find_by_email(email) - self.errors.add(:base, I18n.t('category.errors.email_already_used_in_category', email: escaped, category_name: Rack::Utils.escape_html(category.name))) + email_in + .split("|") + .each do |email| + escaped = Rack::Utils.escape_html(email) + if !Email.is_valid?(email) + self.errors.add(:base, I18n.t("category.errors.invalid_email_in", email: escaped)) + elsif group = Group.find_by_email(email) + self.errors.add( + :base, + I18n.t( + "category.errors.email_already_used_in_group", + email: escaped, + group_name: Rack::Utils.escape_html(group.name), + ), + ) + elsif category = Category.where.not(id: self.id).find_by_email(email) + self.errors.add( + :base, + I18n.t( + "category.errors.email_already_used_in_category", + email: escaped, + category_name: Rack::Utils.escape_html(category.name), + ), + ) + end end - end end def downcase_name @@ -699,42 +749,45 @@ class Category < ActiveRecord::Base end def secure_group_ids - if self.read_restricted? - groups.pluck("groups.id") - end + groups.pluck("groups.id") if self.read_restricted? end def update_latest - latest_post_id = Post - .order("posts.created_at desc") - .where("NOT hidden") - .joins("join topics on topics.id = topic_id") - .where("topics.category_id = :id", id: self.id) - .limit(1) - .pluck("posts.id") - .first + latest_post_id = + Post + .order("posts.created_at desc") + .where("NOT hidden") + .joins("join topics on topics.id = topic_id") + .where("topics.category_id = :id", id: self.id) + .limit(1) + .pluck("posts.id") + .first - latest_topic_id = Topic - .order("topics.created_at desc") - .where("visible") - .where("topics.category_id = :id", id: self.id) - .limit(1) - .pluck("topics.id") - .first + latest_topic_id = + Topic + .order("topics.created_at desc") + .where("visible") + .where("topics.category_id = :id", id: self.id) + .limit(1) + .pluck("topics.id") + .first self.update(latest_topic_id: latest_topic_id, latest_post_id: latest_post_id) end def self.query_parent_category(parent_slug) - encoded_parent_slug = CGI.escape(parent_slug) if SiteSetting.slug_generation_method == 'encoded' - self.where(slug: (encoded_parent_slug || parent_slug), parent_category_id: nil).pluck_first(:id) || - self.where(id: parent_slug.to_i).pluck_first(:id) + encoded_parent_slug = CGI.escape(parent_slug) if SiteSetting.slug_generation_method == "encoded" + self.where(slug: (encoded_parent_slug || parent_slug), parent_category_id: nil).pluck_first( + :id, + ) || self.where(id: parent_slug.to_i).pluck_first(:id) end def self.query_category(slug_or_id, parent_category_id) - encoded_slug_or_id = CGI.escape(slug_or_id) if SiteSetting.slug_generation_method == 'encoded' - self.where(slug: (encoded_slug_or_id || slug_or_id), parent_category_id: parent_category_id).first || - self.where(id: slug_or_id.to_i, parent_category_id: parent_category_id).first + encoded_slug_or_id = CGI.escape(slug_or_id) if SiteSetting.slug_generation_method == "encoded" + self.where( + slug: (encoded_slug_or_id || slug_or_id), + parent_category_id: parent_category_id, + ).first || self.where(id: slug_or_id.to_i, parent_category_id: parent_category_id).first end def self.find_by_email(email) @@ -772,12 +825,16 @@ class Category < ActiveRecord::Base def url @@url_cache.defer_get_set(self.id) do - "#{Discourse.base_path}/c/#{slug_path.join('/')}/#{self.id}" + "#{Discourse.base_path}/c/#{slug_path.join("/")}/#{self.id}" end end def url_with_id - Discourse.deprecate("Category#url_with_id is deprecated. Use `Category#url` instead.", output_in_test: true, drop_from: '2.9.0') + Discourse.deprecate( + "Category#url_with_id is deprecated. Use `Category#url` instead.", + output_in_test: true, + drop_from: "2.9.0", + ) url end @@ -795,7 +852,7 @@ class Category < ActiveRecord::Base old_slug = saved_changes.transform_values(&:first)["slug"] url = +"#{Discourse.base_path}/c" - url << "/#{parent_category.slug_path.join('/')}" if parent_category_id + url << "/#{parent_category.slug_path.join("/")}" if parent_category_id url << "/#{old_slug}/#{id}" url = Permalink.normalize_url(url) @@ -807,7 +864,7 @@ class Category < ActiveRecord::Base end def delete_category_permalink - permalink = Permalink.find_by_url("c/#{slug_path.join('/')}") + permalink = Permalink.find_by_url("c/#{slug_path.join("/")}") permalink.destroy if permalink end @@ -816,7 +873,8 @@ class Category < ActiveRecord::Base end def index_search - Jobs.enqueue(:index_category_for_search, + Jobs.enqueue( + :index_category_for_search, category_id: self.id, force: saved_change_to_attribute?(:name), ) @@ -832,9 +890,7 @@ class Category < ActiveRecord::Base return nil if slug_path.empty? return nil if slug_path.size > SiteSetting.max_category_nesting - slug_path.map! do |slug| - CGI.escape(slug.downcase) - end + slug_path.map! { |slug| CGI.escape(slug.downcase) } query = slug_path.inject(nil) do |parent_id, slug| @@ -871,11 +927,7 @@ class Category < ActiveRecord::Base subcategory_list_style.end_with?("with_featured_topics") end - %i{ - category_created - category_updated - category_destroyed - }.each do |event| + %i[category_created category_updated category_destroyed].each do |event| define_method("trigger_#{event}_event") do DiscourseEvent.trigger(event, self) true @@ -888,10 +940,17 @@ class Category < ActiveRecord::Base return if parent_category.category_groups.empty? parent_permissions = parent_category.category_groups.pluck(:group_id, :permission_type) - child_permissions = @permissions.empty? ? [[Group[:everyone].id, CategoryGroup.permission_types[:full]]] : @permissions + child_permissions = + ( + if @permissions.empty? + [[Group[:everyone].id, CategoryGroup.permission_types[:full]]] + else + @permissions + end + ) check_permissions_compatibility(parent_permissions, child_permissions) - # when saving parent category + # when saving parent category elsif @permissions && subcategories.present? return if @permissions.empty? @@ -903,7 +962,6 @@ class Category < ActiveRecord::Base end def self.ensure_consistency! - sql = <<~SQL SELECT t.id FROM topics t JOIN categories c ON c.topic_id = t.id @@ -911,9 +969,7 @@ class Category < ActiveRecord::Base WHERE p.id IS NULL SQL - DB.query_single(sql).each do |id| - Topic.with_deleted.find_by(id: id).destroy! - end + DB.query_single(sql).each { |id| Topic.with_deleted.find_by(id: id).destroy! } sql = <<~SQL UPDATE categories c @@ -928,12 +984,10 @@ class Category < ActiveRecord::Base DB.exec(sql) Category - .joins('LEFT JOIN topics ON categories.topic_id = topics.id AND topics.deleted_at IS NULL') - .where('categories.id <> ?', SiteSetting.uncategorized_category_id) + .joins("LEFT JOIN topics ON categories.topic_id = topics.id AND topics.deleted_at IS NULL") + .where("categories.id <> ?", SiteSetting.uncategorized_category_id) .where(topics: { id: nil }) - .find_each do |category| - category.create_category_definition - end + .find_each { |category| category.create_category_definition } end def slug_path @@ -947,16 +1001,20 @@ class Category < ActiveRecord::Base end def cannot_delete_reason - return I18n.t('category.cannot_delete.uncategorized') if self.uncategorized? - return I18n.t('category.cannot_delete.has_subcategories') if self.has_children? + return I18n.t("category.cannot_delete.uncategorized") if self.uncategorized? + return I18n.t("category.cannot_delete.has_subcategories") if self.has_children? if self.topic_count != 0 - oldest_topic = self.topics.where.not(id: self.topic_id).order('created_at ASC').limit(1).first + oldest_topic = self.topics.where.not(id: self.topic_id).order("created_at ASC").limit(1).first if oldest_topic - I18n.t('category.cannot_delete.topic_exists', count: self.topic_count, topic_link: "#{CGI.escapeHTML(oldest_topic.title)}") + I18n.t( + "category.cannot_delete.topic_exists", + count: self.topic_count, + topic_link: "#{CGI.escapeHTML(oldest_topic.title)}", + ) else # This is a weird case, probably indicating a bug. - I18n.t('category.cannot_delete.topic_exists_no_oldest', count: self.topic_count) + I18n.t("category.cannot_delete.topic_exists_no_oldest", count: self.topic_count) end end end @@ -989,8 +1047,7 @@ class Category < ActiveRecord::Base everyone = Group[:everyone].id full = CategoryGroup.permission_types[:full] - result = - DB.query(<<-SQL, id: id, everyone: everyone, full: full) + result = DB.query(<<-SQL, id: id, everyone: everyone, full: full) SELECT category_groups.group_id, category_groups.permission_type FROM categories, category_groups WHERE categories.parent_category_id = :id diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb index 0e4a53ef55..1602dc7ad0 100644 --- a/app/models/category_featured_topic.rb +++ b/app/models/category_featured_topic.rb @@ -4,38 +4,40 @@ class CategoryFeaturedTopic < ActiveRecord::Base belongs_to :category belongs_to :topic - NEXT_CATEGORY_ID_KEY = 'category-featured-topic:next-category-id' + NEXT_CATEGORY_ID_KEY = "category-featured-topic:next-category-id" DEFAULT_BATCH_SIZE = 100 # Populates the category featured topics. def self.feature_topics(batched: false, batch_size: nil) current = {} - CategoryFeaturedTopic.select(:topic_id, :category_id).order(:rank).each do |f| - (current[f.category_id] ||= []) << f.topic_id - end + CategoryFeaturedTopic + .select(:topic_id, :category_id) + .order(:rank) + .each { |f| (current[f.category_id] ||= []) << f.topic_id } batch_size ||= DEFAULT_BATCH_SIZE next_category_id = batched ? Discourse.redis.get(NEXT_CATEGORY_ID_KEY).to_i : 0 - categories = Category.select(:id, :topic_id, :num_featured_topics) - .where('id >= ?', next_category_id) - .order('id ASC') - .limit(batch_size) - .to_a + categories = + Category + .select(:id, :topic_id, :num_featured_topics) + .where("id >= ?", next_category_id) + .order("id ASC") + .limit(batch_size) + .to_a if batched if categories.length == batch_size - next_id = Category.where('id > ?', categories.last.id).order('id asc').limit(1).pluck(:id)[0] + next_id = + Category.where("id > ?", categories.last.id).order("id asc").limit(1).pluck(:id)[0] next_id ? Discourse.redis.setex(NEXT_CATEGORY_ID_KEY, 1.day, next_id) : clear_batch! else clear_batch! end end - categories.each do |c| - CategoryFeaturedTopic.feature_topics_for(c, current[c.id] || []) - end + categories.each { |c| CategoryFeaturedTopic.feature_topics_for(c, current[c.id] || []) } end def self.clear_batch! @@ -49,7 +51,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base per_page: c.num_featured_topics, except_topic_ids: [c.topic_id], visible: true, - no_definitions: true + no_definitions: true, } # It may seem a bit odd that we are running 2 queries here, when admin diff --git a/app/models/category_group.rb b/app/models/category_group.rb index a4f5b51ef9..5f0b34cbdc 100644 --- a/app/models/category_group.rb +++ b/app/models/category_group.rb @@ -9,7 +9,6 @@ class CategoryGroup < ActiveRecord::Base def self.permission_types @permission_types ||= Enum.new(full: 1, create_post: 2, readonly: 3) end - end # == Schema Information diff --git a/app/models/category_list.rb b/app/models/category_list.rb index 21ebb279c8..38a55042a7 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -6,8 +6,7 @@ class CategoryList cattr_accessor :preloaded_topic_custom_fields self.preloaded_topic_custom_fields = Set.new - attr_accessor :categories, - :uncategorized + attr_accessor :categories, :uncategorized def initialize(guardian = nil, options = {}) @guardian = guardian || Guardian.new @@ -28,13 +27,9 @@ class CategoryList displayable_topics.compact! if displayable_topics.present? - Topic.preload_custom_fields( - displayable_topics, - preloaded_topic_custom_fields - ) + Topic.preload_custom_fields(displayable_topics, preloaded_topic_custom_fields) end end - end def preload_key @@ -46,11 +41,12 @@ class CategoryList categories.order(:position, :id) else allowed_category_ids = categories.pluck(:id) << nil # `nil` is necessary to include categories without any associated topics - categories.left_outer_joins(:featured_topics) + categories + .left_outer_joins(:featured_topics) .where(topics: { category_id: allowed_category_ids }) - .group('categories.id') + .group("categories.id") .order("max(topics.bumped_at) DESC NULLS LAST") - .order('categories.id ASC') + .order("categories.id ASC") end end @@ -60,22 +56,27 @@ class CategoryList @topics_by_id = {} @topics_by_category_id = {} - category_featured_topics = CategoryFeaturedTopic.select([:category_id, :topic_id]).order(:rank) + category_featured_topics = CategoryFeaturedTopic.select(%i[category_id topic_id]).order(:rank) - @all_topics = Topic - .where(id: category_featured_topics.map(&:topic_id)) - .includes( + @all_topics = + Topic.where(id: category_featured_topics.map(&:topic_id)).includes( :shared_draft, :category, - { topic_thumbnails: [:optimized_image, :upload] } + { topic_thumbnails: %i[optimized_image upload] }, ) - @all_topics = @all_topics.joins(:tags).where(tags: { name: @options[:tag] }) if @options[:tag].present? + @all_topics = @all_topics.joins(:tags).where(tags: { name: @options[:tag] }) if @options[ + :tag + ].present? if @guardian.authenticated? - @all_topics = @all_topics - .joins("LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{@guardian.user.id.to_i}") - .where('COALESCE(tu.notification_level,1) > :muted', muted: TopicUser.notification_levels[:muted]) + @all_topics = + @all_topics.joins( + "LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{@guardian.user.id.to_i}", + ).where( + "COALESCE(tu.notification_level,1) > :muted", + muted: TopicUser.notification_levels[:muted], + ) end @all_topics = TopicQuery.remove_muted_tags(@all_topics, @guardian.user).includes(:last_poster) @@ -94,7 +95,8 @@ class CategoryList def dismissed_topic?(topic) if @guardian.current_user - @dismissed_topic_users_lookup ||= DismissedTopicUser.lookup_for(@guardian.current_user, @all_topics) + @dismissed_topic_users_lookup ||= + DismissedTopicUser.lookup_for(@guardian.current_user, @all_topics) @dismissed_topic_users_lookup.include?(topic.id) else false @@ -102,15 +104,20 @@ class CategoryList end def find_categories - @categories = Category.includes( - :uploaded_background, - :uploaded_logo, - :uploaded_logo_dark, - :topic_only_relative_url, - subcategories: [:topic_only_relative_url] - ).secured(@guardian) + @categories = + Category.includes( + :uploaded_background, + :uploaded_logo, + :uploaded_logo_dark, + :topic_only_relative_url, + subcategories: [:topic_only_relative_url], + ).secured(@guardian) - @categories = @categories.where("categories.parent_category_id = ?", @options[:parent_category_id].to_i) if @options[:parent_category_id].present? + @categories = + @categories.where( + "categories.parent_category_id = ?", + @options[:parent_category_id].to_i, + ) if @options[:parent_category_id].present? @categories = self.class.order_categories(@categories) @@ -138,9 +145,7 @@ class CategoryList end @categories.each do |c| c.subcategory_ids = subcategory_ids[c.id] || [] - if include_subcategories - c.subcategory_list = subcategory_list[c.id] || [] - end + c.subcategory_list = subcategory_list[c.id] || [] if include_subcategories end @categories.delete_if { |c| to_delete.include?(c) } end @@ -149,7 +154,9 @@ class CategoryList categories_with_descendants.each do |category| category.notification_level = notification_levels[category.id] || default_notification_level - category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id) + category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?( + category.id, + ) category.has_children = category.subcategories.present? end @@ -193,9 +200,7 @@ class CategoryList c.displayable_topics.each do |t| unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data) end - unless unpinned.empty? - c.displayable_topics = (c.displayable_topics - unpinned) + unpinned - end + c.displayable_topics = (c.displayable_topics - unpinned) + unpinned unless unpinned.empty? end end end @@ -217,9 +222,7 @@ class CategoryList return @categories_with_children if @categories_with_children && (categories == @categories) return nil if categories.nil? - result = categories.flat_map do |c| - [c, *categories_with_descendants(c.subcategory_list)] - end + result = categories.flat_map { |c| [c, *categories_with_descendants(c.subcategory_list)] } @categories_with_children = result if categories == @categories diff --git a/app/models/category_page_style.rb b/app/models/category_page_style.rb index cf6381d58e..fe8c5d4197 100644 --- a/app/models/category_page_style.rb +++ b/app/models/category_page_style.rb @@ -3,26 +3,39 @@ require "enum_site_setting" class CategoryPageStyle < EnumSiteSetting - def self.valid_value?(val) values.any? { |v| v[:value].to_s == val.to_s } end def self.values @values ||= [ - { name: 'category_page_style.categories_only', value: 'categories_only' }, - { name: 'category_page_style.categories_with_featured_topics', value: 'categories_with_featured_topics' }, - { name: 'category_page_style.categories_and_latest_topics_created_date', value: 'categories_and_latest_topics_created_date' }, - { name: 'category_page_style.categories_and_latest_topics', value: 'categories_and_latest_topics' }, - { name: 'category_page_style.categories_and_top_topics', value: 'categories_and_top_topics' }, - { name: 'category_page_style.categories_boxes', value: 'categories_boxes' }, - { name: 'category_page_style.categories_boxes_with_topics', value: 'categories_boxes_with_topics' }, - { name: 'category_page_style.subcategories_with_featured_topics', value: 'subcategories_with_featured_topics' }, + { name: "category_page_style.categories_only", value: "categories_only" }, + { + name: "category_page_style.categories_with_featured_topics", + value: "categories_with_featured_topics", + }, + { + name: "category_page_style.categories_and_latest_topics_created_date", + value: "categories_and_latest_topics_created_date", + }, + { + name: "category_page_style.categories_and_latest_topics", + value: "categories_and_latest_topics", + }, + { name: "category_page_style.categories_and_top_topics", value: "categories_and_top_topics" }, + { name: "category_page_style.categories_boxes", value: "categories_boxes" }, + { + name: "category_page_style.categories_boxes_with_topics", + value: "categories_boxes_with_topics", + }, + { + name: "category_page_style.subcategories_with_featured_topics", + value: "subcategories_with_featured_topics", + }, ] end def self.translate_names? true end - end diff --git a/app/models/category_required_tag_group.rb b/app/models/category_required_tag_group.rb index 50d9ef6ab6..695b838f81 100644 --- a/app/models/category_required_tag_group.rb +++ b/app/models/category_required_tag_group.rb @@ -6,9 +6,7 @@ class CategoryRequiredTagGroup < ActiveRecord::Base validates :min_count, numericality: { only_integer: true, greater_than: 0 } - after_commit do - Site.clear_cache - end + after_commit { Site.clear_cache } end # == Schema Information diff --git a/app/models/category_tag.rb b/app/models/category_tag.rb index e9cba7c189..dad98b922b 100644 --- a/app/models/category_tag.rb +++ b/app/models/category_tag.rb @@ -4,9 +4,7 @@ class CategoryTag < ActiveRecord::Base belongs_to :category belongs_to :tag - after_commit do - Site.clear_cache - end + after_commit { Site.clear_cache } end # == Schema Information diff --git a/app/models/category_tag_group.rb b/app/models/category_tag_group.rb index ea27bc50c1..6122a00acf 100644 --- a/app/models/category_tag_group.rb +++ b/app/models/category_tag_group.rb @@ -4,9 +4,7 @@ class CategoryTagGroup < ActiveRecord::Base belongs_to :category belongs_to :tag_group - after_commit do - Site.clear_cache - end + after_commit { Site.clear_cache } end # == Schema Information diff --git a/app/models/category_tag_stat.rb b/app/models/category_tag_stat.rb index 5f2b1410a1..478aa069cc 100644 --- a/app/models/category_tag_stat.rb +++ b/app/models/category_tag_stat.rb @@ -6,9 +6,10 @@ class CategoryTagStat < ActiveRecord::Base def self.topic_moved(topic, from_category_id, to_category_id) if from_category_id - self.where(tag_id: topic.tags.map(&:id), category_id: from_category_id) - .where('topic_count > 0') - .update_all('topic_count = topic_count - 1') + self + .where(tag_id: topic.tags.map(&:id), category_id: from_category_id) + .where("topic_count > 0") + .update_all("topic_count = topic_count - 1") end if to_category_id diff --git a/app/models/category_user.rb b/app/models/category_user.rb index b07ac73e71..4623567656 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -23,27 +23,24 @@ class CategoryUser < ActiveRecord::Base changed = false # Update pre-existing category users - if category_ids.present? && CategoryUser - .where(user_id: user.id, category_id: category_ids) - .where.not(notification_level: level_num) - .update_all(notification_level: level_num) > 0 - + if category_ids.present? && + CategoryUser + .where(user_id: user.id, category_id: category_ids) + .where.not(notification_level: level_num) + .update_all(notification_level: level_num) > 0 changed = true end # Remove extraneous category users - if CategoryUser.where(user_id: user.id, notification_level: level_num) - .where.not(category_id: category_ids) - .delete_all > 0 - + if CategoryUser + .where(user_id: user.id, notification_level: level_num) + .where.not(category_id: category_ids) + .delete_all > 0 changed = true end if category_ids.present? - params = { - user_id: user.id, - level_num: level_num, - } + params = { user_id: user.id, level_num: level_num } sql = <<~SQL INSERT INTO category_users (user_id, category_id, notification_level) @@ -55,11 +52,8 @@ class CategoryUser < ActiveRecord::Base # into the query, plus it is a bit of a micro optimisation category_ids.each do |category_id| params[:category_id] = category_id - if DB.exec(sql, params) > 0 - changed = true - end + changed = true if DB.exec(sql, params) > 0 end - end if changed @@ -91,7 +85,6 @@ class CategoryUser < ActiveRecord::Base end def self.auto_track(opts = {}) - builder = DB.build <<~SQL UPDATE topic_users tu SET notification_level = :tracking, @@ -100,11 +93,13 @@ class CategoryUser < ActiveRecord::Base /*where*/ SQL - builder.where("tu.topic_id = t.id AND + builder.where( + "tu.topic_id = t.id AND cu.category_id = t.category_id AND cu.user_id = tu.user_id AND cu.notification_level = :tracking AND - tu.notification_level = :regular") + tu.notification_level = :regular", + ) if category_id = opts[:category_id] builder.where("t.category_id = :category_id", category_id: category_id) @@ -121,12 +116,11 @@ class CategoryUser < ActiveRecord::Base builder.exec( tracking: notification_levels[:tracking], regular: notification_levels[:regular], - auto_track_category: TopicUser.notification_reasons[:auto_track_category] + auto_track_category: TopicUser.notification_reasons[:auto_track_category], ) end def self.auto_watch(opts = {}) - builder = DB.build <<~SQL UPDATE topic_users tu SET notification_level = @@ -181,9 +175,8 @@ class CategoryUser < ActiveRecord::Base watching: notification_levels[:watching], tracking: notification_levels[:tracking], regular: notification_levels[:regular], - auto_watch_category: TopicUser.notification_reasons[:auto_watch_category] + auto_watch_category: TopicUser.notification_reasons[:auto_watch_category], ) - end def self.ensure_consistency! @@ -198,25 +191,30 @@ class CategoryUser < ActiveRecord::Base end def self.default_notification_level - SiteSetting.mute_all_categories_by_default ? notification_levels[:muted] : notification_levels[:regular] + if SiteSetting.mute_all_categories_by_default + notification_levels[:muted] + else + notification_levels[:regular] + end end def self.notification_levels_for(user) # Anonymous users have all default categories set to regular tracking, # except for default muted categories which stay muted. if user.blank? - notification_levels = [ - SiteSetting.default_categories_watching.split("|"), - SiteSetting.default_categories_tracking.split("|"), - SiteSetting.default_categories_watching_first_post.split("|"), - SiteSetting.default_categories_normal.split("|") - ].flatten.map do |id| - [id.to_i, self.notification_levels[:regular]] - end + notification_levels = + [ + SiteSetting.default_categories_watching.split("|"), + SiteSetting.default_categories_tracking.split("|"), + SiteSetting.default_categories_watching_first_post.split("|"), + SiteSetting.default_categories_normal.split("|"), + ].flatten.map { |id| [id.to_i, self.notification_levels[:regular]] } - notification_levels += SiteSetting.default_categories_muted.split("|").map do |id| - [id.to_i, self.notification_levels[:muted]] - end + notification_levels += + SiteSetting + .default_categories_muted + .split("|") + .map { |id| [id.to_i, self.notification_levels[:muted]] } else notification_levels = CategoryUser.where(user: user).pluck(:category_id, :notification_level) end @@ -238,22 +236,32 @@ class CategoryUser < ActiveRecord::Base def self.muted_category_ids_query(user, include_direct: false) query = Category query = query.where.not(parent_category_id: nil) if !include_direct - query = query - .joins("LEFT JOIN categories categories2 ON categories2.id = categories.parent_category_id") - .joins("LEFT JOIN category_users ON category_users.category_id = categories.id AND category_users.user_id = #{user.id}") - .joins("LEFT JOIN category_users category_users2 ON category_users2.category_id = categories2.id AND category_users2.user_id = #{user.id}") + query = + query + .joins("LEFT JOIN categories categories2 ON categories2.id = categories.parent_category_id") + .joins( + "LEFT JOIN category_users ON category_users.category_id = categories.id AND category_users.user_id = #{user.id}", + ) + .joins( + "LEFT JOIN category_users category_users2 ON category_users2.category_id = categories2.id AND category_users2.user_id = #{user.id}", + ) - direct_category_muted_sql = "COALESCE(category_users.notification_level, #{CategoryUser.default_notification_level}) = #{CategoryUser.notification_levels[:muted]}" + direct_category_muted_sql = + "COALESCE(category_users.notification_level, #{CategoryUser.default_notification_level}) = #{CategoryUser.notification_levels[:muted]}" parent_category_muted_sql = "(category_users.id IS NULL AND COALESCE(category_users2.notification_level, #{CategoryUser.default_notification_level}) = #{notification_levels[:muted]})" conditions = [parent_category_muted_sql] conditions.push(direct_category_muted_sql) if include_direct if SiteSetting.max_category_nesting === 3 - query = query - .joins("LEFT JOIN categories categories3 ON categories3.id = categories2.parent_category_id") - .joins("LEFT JOIN category_users category_users3 ON category_users3.category_id = categories3.id AND category_users3.user_id = #{user.id}") - grandparent_category_muted_sql = "(category_users.id IS NULL AND category_users2.id IS NULL AND COALESCE(category_users3.notification_level, #{CategoryUser.default_notification_level}) = #{notification_levels[:muted]})" + query = + query.joins( + "LEFT JOIN categories categories3 ON categories3.id = categories2.parent_category_id", + ).joins( + "LEFT JOIN category_users category_users3 ON category_users3.category_id = categories3.id AND category_users3.user_id = #{user.id}", + ) + grandparent_category_muted_sql = + "(category_users.id IS NULL AND category_users2.id IS NULL AND COALESCE(category_users3.notification_level, #{CategoryUser.default_notification_level}) = #{notification_levels[:muted]})" conditions.push(grandparent_category_muted_sql) end diff --git a/app/models/child_theme.rb b/app/models/child_theme.rb index 7ce4d0dc78..2f29e6c7b7 100644 --- a/app/models/child_theme.rb +++ b/app/models/child_theme.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class ChildTheme < ActiveRecord::Base - belongs_to :parent_theme, class_name: 'Theme' - belongs_to :child_theme, class_name: 'Theme' + belongs_to :parent_theme, class_name: "Theme" + belongs_to :child_theme, class_name: "Theme" validate :child_validations @@ -11,7 +11,8 @@ class ChildTheme < ActiveRecord::Base def child_validations if Theme.where( "(component IS true AND id = :parent) OR (component IS false AND id = :child)", - parent: parent_theme_id, child: child_theme_id + parent: parent_theme_id, + child: child_theme_id, ).exists? errors.add(:base, I18n.t("themes.errors.no_multilevels_components")) end diff --git a/app/models/color_scheme.rb b/app/models/color_scheme.rb index bdbef6df9e..99a1ee75ff 100644 --- a/app/models/color_scheme.rb +++ b/app/models/color_scheme.rb @@ -1,173 +1,172 @@ # frozen_string_literal: true class ColorScheme < ActiveRecord::Base - # rubocop:disable Layout/HashAlignment CUSTOM_SCHEMES = { - 'Dark': { - "primary" => 'dddddd', - "secondary" => '222222', - "tertiary" => '0f82af', - "quaternary" => 'c14924', - "header_background" => '111111', - "header_primary" => 'dddddd', - "highlight" => 'a87137', - "danger" => 'e45735', - "success" => '1ca551', - "love" => 'fa6c8d' + Dark: { + "primary" => "dddddd", + "secondary" => "222222", + "tertiary" => "0f82af", + "quaternary" => "c14924", + "header_background" => "111111", + "header_primary" => "dddddd", + "highlight" => "a87137", + "danger" => "e45735", + "success" => "1ca551", + "love" => "fa6c8d", }, # By @itsbhanusharma - 'Neutral': { - "primary" => '000000', - "secondary" => 'ffffff', - "tertiary" => '51839b', - "quaternary" => 'b85e48', - "header_background" => '333333', - "header_primary" => 'f3f3f3', - "highlight" => 'ecec70', - "danger" => 'b85e48', - "success" => '518751', - "love" => 'fa6c8d' + Neutral: { + "primary" => "000000", + "secondary" => "ffffff", + "tertiary" => "51839b", + "quaternary" => "b85e48", + "header_background" => "333333", + "header_primary" => "f3f3f3", + "highlight" => "ecec70", + "danger" => "b85e48", + "success" => "518751", + "love" => "fa6c8d", }, # By @Flower_Child - 'Grey Amber': { - "primary" => 'd9d9d9', - "secondary" => '3d4147', - "tertiary" => 'fdd459', - "quaternary" => 'fdd459', - "header_background" => '36393e', - "header_primary" => 'd9d9d9', - "highlight" => 'fdd459', - "danger" => 'e45735', - "success" => 'fdd459', - "love" => 'fdd459' + "Grey Amber": { + "primary" => "d9d9d9", + "secondary" => "3d4147", + "tertiary" => "fdd459", + "quaternary" => "fdd459", + "header_background" => "36393e", + "header_primary" => "d9d9d9", + "highlight" => "fdd459", + "danger" => "e45735", + "success" => "fdd459", + "love" => "fdd459", }, # By @rafafotes - 'Shades of Blue': { - "primary" => '203243', - "secondary" => 'eef4f7', - "tertiary" => '416376', - "quaternary" => '5e99b9', - "header_background" => '86bddb', - "header_primary" => '203243', - "highlight" => '86bddb', - "danger" => 'bf3c3c', - "success" => '70db82', - "love" => 'fc94cb' + "Shades of Blue": { + "primary" => "203243", + "secondary" => "eef4f7", + "tertiary" => "416376", + "quaternary" => "5e99b9", + "header_background" => "86bddb", + "header_primary" => "203243", + "highlight" => "86bddb", + "danger" => "bf3c3c", + "success" => "70db82", + "love" => "fc94cb", }, # By @mikechristopher - 'Latte': { - "primary" => 'f2e5d7', - "secondary" => '262322', - "tertiary" => 'f7f2ed', - "quaternary" => 'd7c9aa', - "header_background" => 'd7c9aa', - "header_primary" => '262322', - "highlight" => 'd7c9aa', - "danger" => 'db9584', - "success" => '78be78', - "love" => '8f6201' + Latte: { + "primary" => "f2e5d7", + "secondary" => "262322", + "tertiary" => "f7f2ed", + "quaternary" => "d7c9aa", + "header_background" => "d7c9aa", + "header_primary" => "262322", + "highlight" => "d7c9aa", + "danger" => "db9584", + "success" => "78be78", + "love" => "8f6201", }, # By @Flower_Child - 'Summer': { - "primary" => '874342', - "secondary" => 'fffff4', - "tertiary" => 'fe9896', - "quaternary" => 'fcc9d0', - "header_background" => '96ccbf', - "header_primary" => 'fff1e7', - "highlight" => 'f3c07f', - "danger" => 'cfebdc', - "success" => 'fcb4b5', - "love" => 'f3c07f' + Summer: { + "primary" => "874342", + "secondary" => "fffff4", + "tertiary" => "fe9896", + "quaternary" => "fcc9d0", + "header_background" => "96ccbf", + "header_primary" => "fff1e7", + "highlight" => "f3c07f", + "danger" => "cfebdc", + "success" => "fcb4b5", + "love" => "f3c07f", }, # By @Flower_Child - 'Dark Rose': { - "primary" => 'ca9cb2', - "secondary" => '3a2a37', - "tertiary" => 'fdd459', - "quaternary" => '7e566a', - "header_background" => 'a97189', - "header_primary" => 'd9b2bb', - "highlight" => '6c3e63', - "danger" => '6c3e63', - "success" => 'd9b2bb', - "love" => 'd9b2bb' + "Dark Rose": { + "primary" => "ca9cb2", + "secondary" => "3a2a37", + "tertiary" => "fdd459", + "quaternary" => "7e566a", + "header_background" => "a97189", + "header_primary" => "d9b2bb", + "highlight" => "6c3e63", + "danger" => "6c3e63", + "success" => "d9b2bb", + "love" => "d9b2bb", }, - "WCAG": { - "primary" => '000000', - "primary-medium" => '696969', - "primary-low-mid" => '909090', - "secondary" => 'ffffff', - "tertiary" => '3369FF', - "quaternary" => '3369FF', - "header_background" => 'ffffff', - "header_primary" => '000000', - "highlight" => '3369FF', - "highlight-high" => '0036E6', - "highlight-medium" => 'e0e9ff', - "highlight-low" => 'e0e9ff', - "danger" => 'BB1122', - "success" => '3d854d', - "love" => '9D256B' + WCAG: { + "primary" => "000000", + "primary-medium" => "696969", + "primary-low-mid" => "909090", + "secondary" => "ffffff", + "tertiary" => "3369FF", + "quaternary" => "3369FF", + "header_background" => "ffffff", + "header_primary" => "000000", + "highlight" => "3369FF", + "highlight-high" => "0036E6", + "highlight-medium" => "e0e9ff", + "highlight-low" => "e0e9ff", + "danger" => "BB1122", + "success" => "3d854d", + "love" => "9D256B", }, "WCAG Dark": { - "primary" => 'ffffff', - "primary-medium" => '999999', - "primary-low-mid" => '888888', - "secondary" => '0c0c0c', - "tertiary" => '759AFF', - "quaternary" => '759AFF', - "header_background" => '000000', - "header_primary" => 'ffffff', - "highlight" => '3369FF', - "danger" => 'BB1122', - "success" => '3d854d', - "love" => '9D256B' + "primary" => "ffffff", + "primary-medium" => "999999", + "primary-low-mid" => "888888", + "secondary" => "0c0c0c", + "tertiary" => "759AFF", + "quaternary" => "759AFF", + "header_background" => "000000", + "header_primary" => "ffffff", + "highlight" => "3369FF", + "danger" => "BB1122", + "success" => "3d854d", + "love" => "9D256B", }, # By @zenorocha - "Dracula": { + Dracula: { "primary_very_low" => "373A47", "primary_low" => "414350", "primary_low_mid" => "8C8D94", "primary_medium" => "A3A4AA", "primary_high" => "CCCCCF", - "primary" => 'f2f2f2', - "primary-50" => '3F414E', - "primary-100" => '535460', - "primary-200" => '666972', - "primary-300" => '7A7C84', - "primary-400" => '8D8F96', - "primary-500" => 'A2A3A9', - "primary-600" => 'B6B7BC', - "primary-700" => 'C7C7C7', - "primary-800" => 'DEDFE0', - "primary-900" => 'F5F5F5', + "primary" => "f2f2f2", + "primary-50" => "3F414E", + "primary-100" => "535460", + "primary-200" => "666972", + "primary-300" => "7A7C84", + "primary-400" => "8D8F96", + "primary-500" => "A2A3A9", + "primary-600" => "B6B7BC", + "primary-700" => "C7C7C7", + "primary-800" => "DEDFE0", + "primary-900" => "F5F5F5", "secondary_low" => "CCCCCF", "secondary_medium" => "91939A", "secondary_high" => "6A6C76", "secondary_very_high" => "3D404C", - "secondary" => '2d303e', + "secondary" => "2d303e", "tertiary_low" => "4A4463", "tertiary_medium" => "6E5D92", - "tertiary" => 'bd93f9', + "tertiary" => "bd93f9", "tertiary_high" => "9275C1", "quaternary_low" => "6AA8BA", - "quaternary" => '8be9fd', - "header_background" => '373A47', - "header_primary" => 'f2f2f2', + "quaternary" => "8be9fd", + "header_background" => "373A47", + "header_primary" => "f2f2f2", "highlight_low" => "686D55", "highlight_medium" => "52592B", - "highlight" => '52592B', + "highlight" => "52592B", "highlight_high" => "C0C879", "danger_low" => "957279", - "danger" => 'ff5555', + "danger" => "ff5555", "success_low" => "386D50", "success_medium" => "44B366", - "success" => '50fa7b', + "success" => "50fa7b", "love_low" => "6C4667", - "love" => 'ff79c6' + "love" => "ff79c6", }, # By @altercation "Solarized Light": { @@ -176,40 +175,40 @@ class ColorScheme < ActiveRecord::Base "primary_low_mid" => "A4AFA5", "primary_medium" => "7E918C", "primary_high" => "4C6869", - "primary" => '002B36', - "primary-50" => 'F0EBDA', - "primary-100" => 'DAD8CA', - "primary-200" => 'B2B9B3', - "primary-300" => '839496', - "primary-400" => '76898C', - "primary-500" => '697F83', - "primary-600" => '627A7E', - "primary-700" => '556F74', - "primary-800" => '415F66', - "primary-900" => '21454E', + "primary" => "002B36", + "primary-50" => "F0EBDA", + "primary-100" => "DAD8CA", + "primary-200" => "B2B9B3", + "primary-300" => "839496", + "primary-400" => "76898C", + "primary-500" => "697F83", + "primary-600" => "627A7E", + "primary-700" => "556F74", + "primary-800" => "415F66", + "primary-900" => "21454E", "secondary_low" => "325458", "secondary_medium" => "6C8280", "secondary_high" => "97A59D", "secondary_very_high" => "E8E6D3", - "secondary" => 'FCF6E1', + "secondary" => "FCF6E1", "tertiary_low" => "D6E6DE", "tertiary_medium" => "7EBFD7", - "tertiary" => '0088cc', + "tertiary" => "0088cc", "tertiary_high" => "329ED0", - "quaternary" => 'e45735', - "header_background" => 'FCF6E1', - "header_primary" => '002B36', + "quaternary" => "e45735", + "header_background" => "FCF6E1", + "header_primary" => "002B36", "highlight_low" => "FDF9AD", "highlight_medium" => "E3D0A3", - "highlight" => 'F2F481', + "highlight" => "F2F481", "highlight_high" => "BCAA7F", "danger_low" => "F8D9C2", - "danger" => 'e45735', + "danger" => "e45735", "success_low" => "CFE5B9", "success_medium" => "4CB544", - "success" => '009900', + "success" => "009900", "love_low" => "FCDDD2", - "love" => 'fa6c8d' + "love" => "fa6c8d", }, # By @altercation "Solarized Dark": { @@ -218,65 +217,59 @@ class ColorScheme < ActiveRecord::Base "primary_low_mid" => "798C88", "primary_medium" => "97A59D", "primary_high" => "B5BDB1", - "primary" => 'FCF6E1', - "primary-50" => '21454E', - "primary-100" => '415F66', - "primary-200" => '556F74', - "primary-300" => '627A7E', - "primary-400" => '697F83', - "primary-500" => '76898C', - "primary-600" => '839496', - "primary-700" => 'B2B9B3', - "primary-800" => 'DAD8CA', - "primary-900" => 'F0EBDA', + "primary" => "FCF6E1", + "primary-50" => "21454E", + "primary-100" => "415F66", + "primary-200" => "556F74", + "primary-300" => "627A7E", + "primary-400" => "697F83", + "primary-500" => "76898C", + "primary-600" => "839496", + "primary-700" => "B2B9B3", + "primary-800" => "DAD8CA", + "primary-900" => "F0EBDA", "secondary_low" => "B5BDB1", "secondary_medium" => "81938D", "secondary_high" => "4E6A6B", "secondary_very_high" => "143B44", - "secondary" => '002B36', + "secondary" => "002B36", "tertiary_low" => "003E54", "tertiary_medium" => "00557A", - "tertiary" => '0088cc', + "tertiary" => "0088cc", "tertiary_high" => "006C9F", "quaternary_low" => "944835", - "quaternary" => 'e45735', - "header_background" => '002B36', - "header_primary" => 'FCF6E1', + "quaternary" => "e45735", + "header_background" => "002B36", + "header_primary" => "FCF6E1", "highlight_low" => "4D6B3D", "highlight_medium" => "464C33", - "highlight" => 'F2F481', + "highlight" => "F2F481", "highlight_high" => "BFCA47", "danger_low" => "443836", "danger_medium" => "944835", - "danger" => 'e45735', + "danger" => "e45735", "success_low" => "004C26", "success_medium" => "007313", - "success" => '009900', + "success" => "009900", "love_low" => "4B3F50", - "love" => 'fa6c8d', - } + "love" => "fa6c8d", + }, } # rubocop:enable Layout/HashAlignment - LIGHT_THEME_ID = 'Light' + LIGHT_THEME_ID = "Light" def self.base_color_scheme_colors base_with_hash = [] - base_colors.each do |name, color| - base_with_hash << { name: name, hex: "#{color}" } - end + base_colors.each { |name, color| base_with_hash << { name: name, hex: "#{color}" } } - list = [ - { id: LIGHT_THEME_ID, colors: base_with_hash } - ] + list = [{ id: LIGHT_THEME_ID, colors: base_with_hash }] CUSTOM_SCHEMES.each do |k, v| colors = [] - v.each do |name, color| - colors << { name: name, hex: "#{color}" } - end + v.each { |name, color| colors << { name: name, hex: "#{color}" } } list.push(id: k.to_s, colors: colors) end @@ -290,7 +283,7 @@ class ColorScheme < ActiveRecord::Base attr_accessor :is_base attr_accessor :skip_publish - has_many :color_scheme_colors, -> { order('id ASC') }, dependent: :destroy + has_many :color_scheme_colors, -> { order("id ASC") }, dependent: :destroy alias_method :colors, :color_scheme_colors @@ -303,7 +296,8 @@ class ColorScheme < ActiveRecord::Base validates_associated :color_scheme_colors BASE_COLORS_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/colors.scss" - COLOR_TRANSFORMATION_FILE = "#{Rails.root}/app/assets/stylesheets/common/foundation/color_transformations.scss" + COLOR_TRANSFORMATION_FILE = + "#{Rails.root}/app/assets/stylesheets/common/foundation/color_transformations.scss" @mutex = Mutex.new @@ -312,10 +306,12 @@ class ColorScheme < ActiveRecord::Base @mutex.synchronize do return @base_colors if @base_colors base_colors = {} - File.readlines(BASE_COLORS_FILE).each do |line| - matches = /\$([\w]+):\s*#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[;]|\s)/.match(line.strip) - base_colors[matches[1]] = matches[2] if matches - end + File + .readlines(BASE_COLORS_FILE) + .each do |line| + matches = /\$([\w]+):\s*#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?:[;]|\s)/.match(line.strip) + base_colors[matches[1]] = matches[2] if matches + end @base_colors = base_colors end @base_colors @@ -326,10 +322,12 @@ class ColorScheme < ActiveRecord::Base @mutex.synchronize do return @transformation_variables if @transformation_variables transformation_variables = [] - File.readlines(COLOR_TRANSFORMATION_FILE).each do |line| - matches = /\$([\w\-_]+):.*/.match(line.strip) - transformation_variables.append(matches[1]) if matches - end + File + .readlines(COLOR_TRANSFORMATION_FILE) + .each do |line| + matches = /\$([\w\-_]+):.*/.match(line.strip) + transformation_variables.append(matches[1]) if matches + end @transformation_variables = transformation_variables end @transformation_variables @@ -337,7 +335,11 @@ class ColorScheme < ActiveRecord::Base def self.base_color_schemes base_color_scheme_colors.map do |hash| - scheme = new(name: I18n.t("color_schemes.#{hash[:id].downcase.gsub(' ', '_')}"), base_scheme_id: hash[:id]) + scheme = + new( + name: I18n.t("color_schemes.#{hash[:id].downcase.gsub(" ", "_")}"), + base_scheme_id: hash[:id], + ) scheme.colors = hash[:colors].map { |k| { name: k[:name], hex: k[:hex] } } scheme.is_base = true scheme @@ -346,7 +348,7 @@ class ColorScheme < ActiveRecord::Base def self.base return @base_color_scheme if @base_color_scheme - @base_color_scheme = new(name: I18n.t('color_schemes.base_theme_name')) + @base_color_scheme = new(name: I18n.t("color_schemes.base_theme_name")) @base_color_scheme.colors = base_colors.map { |name, hex| { name: name, hex: hex } } @base_color_scheme.is_base = true @base_color_scheme @@ -363,9 +365,10 @@ class ColorScheme < ActiveRecord::Base new_color_scheme.base_scheme_id = params[:base_scheme_id] new_color_scheme.user_selectable = true - colors = CUSTOM_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex| - { name: name, hex: hex } - end if params[:base_scheme_id] + colors = + CUSTOM_SCHEMES[params[:base_scheme_id].to_sym]&.map do |name, hex| + { name: name, hex: hex } + end if params[:base_scheme_id] colors ||= base.colors_hashes # Override base values @@ -394,13 +397,17 @@ class ColorScheme < ActiveRecord::Base def colors=(arr) @colors_by_name = nil - arr.each do |c| - self.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex]) - end + arr.each { |c| self.color_scheme_colors << ColorSchemeColor.new(name: c[:name], hex: c[:hex]) } end def colors_by_name - @colors_by_name ||= self.colors.inject({}) { |sum, c| sum[c.name] = c; sum; } + @colors_by_name ||= + self + .colors + .inject({}) do |sum, c| + sum[c.name] = c + sum + end end def clear_colors_cache @@ -408,16 +415,12 @@ class ColorScheme < ActiveRecord::Base end def colors_hashes - color_scheme_colors.map do |c| - { name: c.name, hex: c.hex } - end + color_scheme_colors.map { |c| { name: c.name, hex: c.hex } } end def base_colors colors = nil - if base_scheme_id && base_scheme_id != "Light" - colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] - end + colors = CUSTOM_SCHEMES[base_scheme_id.to_sym] if base_scheme_id && base_scheme_id != "Light" colors || ColorScheme.base_colors end @@ -425,21 +428,15 @@ class ColorScheme < ActiveRecord::Base resolved = ColorScheme.base_colors.dup if base_scheme_id && base_scheme_id != "Light" if scheme = CUSTOM_SCHEMES[base_scheme_id.to_sym] - scheme.each do |name, value| - resolved[name] = value - end + scheme.each { |name, value| resolved[name] = value } end end - colors.each do |c| - resolved[c.name] = c.hex - end + colors.each { |c| resolved[c.name] = c.hex } resolved end def publish_discourse_stylesheet - if self.id - self.class.publish_discourse_stylesheets!(self.id) - end + self.class.publish_discourse_stylesheets!(self.id) if self.id end def self.publish_discourse_stylesheets!(id = nil) @@ -458,7 +455,7 @@ class ColorScheme < ActiveRecord::Base theme_ids, with_scheme: true, clear_manager_cache: false, - all_themes: true + all_themes: true, ) end end @@ -469,9 +466,7 @@ class ColorScheme < ActiveRecord::Base end def bump_version - if self.id - self.version += 1 - end + self.version += 1 if self.id end def is_dark? @@ -484,7 +479,7 @@ class ColorScheme < ActiveRecord::Base end def is_wcag? - base_scheme_id&.start_with?('WCAG') + base_scheme_id&.start_with?("WCAG") end # Equivalent to dc-color-brightness() in variables.scss diff --git a/app/models/color_scheme_setting.rb b/app/models/color_scheme_setting.rb index 4003348b0e..8dc5ce5cc6 100644 --- a/app/models/color_scheme_setting.rb +++ b/app/models/color_scheme_setting.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true class ColorSchemeSetting < EnumSiteSetting - def self.valid_value?(val) val == -1 || ColorScheme.find_by_id(val) end def self.values values = [{ name: I18n.t("site_settings.dark_mode_none"), value: -1 }] - ColorScheme.all.map do |c| - values << { name: c.name, value: c.id } - end + ColorScheme.all.map { |c| values << { name: c.name, value: c.id } } values end - end diff --git a/app/models/concerns/anon_cache_invalidator.rb b/app/models/concerns/anon_cache_invalidator.rb index f87cada6c0..cb468212fa 100644 --- a/app/models/concerns/anon_cache_invalidator.rb +++ b/app/models/concerns/anon_cache_invalidator.rb @@ -4,12 +4,8 @@ module AnonCacheInvalidator extend ActiveSupport::Concern included do - after_destroy do - Site.clear_anon_cache! - end + after_destroy { Site.clear_anon_cache! } - after_save do - Site.clear_anon_cache! - end + after_save { Site.clear_anon_cache! } end end diff --git a/app/models/concerns/cached_counting.rb b/app/models/concerns/cached_counting.rb index f8083cc078..7598c7be22 100644 --- a/app/models/concerns/cached_counting.rb +++ b/app/models/concerns/cached_counting.rb @@ -53,9 +53,7 @@ module CachedCounting @last_ensure_thread = now - if !@thread&.alive? - @thread = nil - end + @thread = nil if !@thread&.alive? @thread ||= Thread.new { thread_loop } end end @@ -74,26 +72,20 @@ module CachedCounting end iterations += 1 end - rescue => ex if Redis::CommandError === ex && ex.message =~ /READONLY/ # do not warn for Redis readonly mode elsif PG::ReadOnlySqlTransaction === ex # do not warn for PG readonly mode else - Discourse.warn_exception( - ex, - message: 'Unexpected error while processing cached counts' - ) + Discourse.warn_exception(ex, message: "Unexpected error while processing cached counts") end end def self.flush @flush = true @thread.wakeup - while @flush - sleep 0.001 - end + sleep 0.001 while @flush end COUNTER_REDIS_HASH = "CounterCacheHash" @@ -122,25 +114,23 @@ module CachedCounting redis = Discourse.redis.without_namespace DistributedMutex.synchronize("flush_counters_to_db", redis: redis, validity: 5.minutes) do if allowed_to_flush_to_db? - redis.hkeys(COUNTER_REDIS_HASH).each do |key| + redis + .hkeys(COUNTER_REDIS_HASH) + .each do |key| + val = LUA_HGET_DEL.eval(redis, [COUNTER_REDIS_HASH, key]).to_i - val = LUA_HGET_DEL.eval( - redis, - [COUNTER_REDIS_HASH, key] - ).to_i + # unlikely (protected by mutex), but protect just in case + # could be a race condition in test + if val > 0 + klass_name, db, date, local_key = key.split(",", 4) + date = Date.strptime(date, "%Y%m%d") + klass = Module.const_get(klass_name) - # unlikely (protected by mutex), but protect just in case - # could be a race condition in test - if val > 0 - klass_name, db, date, local_key = key.split(",", 4) - date = Date.strptime(date, "%Y%m%d") - klass = Module.const_get(klass_name) - - RailsMultisite::ConnectionManagement.with_connection(db) do - klass.write_cache!(local_key, val, date) + RailsMultisite::ConnectionManagement.with_connection(db) do + klass.write_cache!(local_key, val, date) + end end end - end end end end @@ -154,7 +144,12 @@ module CachedCounting end def self.allowed_to_flush_to_db? - Discourse.redis.without_namespace.set(DB_COOLDOWN_KEY, "1", ex: DB_FLUSH_COOLDOWN_SECONDS, nx: true) + Discourse.redis.without_namespace.set( + DB_COOLDOWN_KEY, + "1", + ex: DB_FLUSH_COOLDOWN_SECONDS, + nx: true, + ) end def self.queue(key, klass) @@ -176,6 +171,5 @@ module CachedCounting def write_cache!(key, count, date) raise NotImplementedError end - end end diff --git a/app/models/concerns/category_hashtag.rb b/app/models/concerns/category_hashtag.rb index db28efe789..52d4d01b5b 100644 --- a/app/models/concerns/category_hashtag.rb +++ b/app/models/concerns/category_hashtag.rb @@ -55,9 +55,7 @@ module CategoryHashtag end end else - categories.find do |cat| - cat.slug.downcase == parent_slug && cat.top_level? - end + categories.find { |cat| cat.slug.downcase == parent_slug && cat.top_level? } end end .compact diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 07ebc880ce..2b2f64f08a 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -4,7 +4,6 @@ module HasCustomFields extend ActiveSupport::Concern module Helpers - def self.append_field(target, key, value, types) if target.has_key?(key) target[key] = [target[key]] if !target[key].is_a? Array @@ -14,18 +13,14 @@ module HasCustomFields end end - CUSTOM_FIELD_TRUE ||= ['1', 't', 'true', 'T', 'True', 'TRUE'].freeze + CUSTOM_FIELD_TRUE ||= %w[1 t true T True TRUE].freeze def self.get_custom_field_type(types, key) return unless types - sorted_types = types.keys.select { |k| k.end_with?("*") } - .sort_by(&:length) - .reverse + sorted_types = types.keys.select { |k| k.end_with?("*") }.sort_by(&:length).reverse - sorted_types.each do |t| - return types[t] if key =~ /^#{t}/i - end + sorted_types.each { |t| return types[t] if key =~ /^#{t}/i } types[key] end @@ -42,9 +37,12 @@ module HasCustomFields result = case type - when :boolean then !!CUSTOM_FIELD_TRUE.include?(value) - when :integer then value.to_i - when :json then parse_json_value(value, key) + when :boolean + !!CUSTOM_FIELD_TRUE.include?(value) + when :integer + value.to_i + when :json + parse_json_value(value, key) else value end @@ -55,7 +53,9 @@ module HasCustomFields def self.parse_json_value(value, key) ::JSON.parse(value) rescue JSON::ParserError - Rails.logger.warn("Value '#{value}' for custom field '#{key}' is not json, it is being ignored.") + Rails.logger.warn( + "Value '#{value}' for custom field '#{key}' is not json, it is being ignored.", + ) {} end end @@ -80,11 +80,13 @@ module HasCustomFields return result if allowed_fields.blank? - klass.where(foreign_key => ids, :name => allowed_fields) - .pluck(foreign_key, :name, :value).each do |cf| - result[cf[0]] ||= {} - append_custom_field(result[cf[0]], cf[1], cf[2]) - end + klass + .where(foreign_key => ids, :name => allowed_fields) + .pluck(foreign_key, :name, :value) + .each do |cf| + result[cf[0]] ||= {} + append_custom_field(result[cf[0]], cf[1], cf[2]) + end result end @@ -108,9 +110,7 @@ module HasCustomFields map = {} empty = {} - fields.each do |field| - empty[field] = nil - end + fields.each { |field| empty[field] = nil } objects.each do |obj| map[obj.id] = obj @@ -119,20 +119,18 @@ module HasCustomFields fk = (name.underscore << "_id") - "#{name}CustomField".constantize + "#{name}CustomField" + .constantize .where("#{fk} in (?)", map.keys) .where("name in (?)", fields) - .pluck(fk, :name, :value).each do |id, name, value| + .pluck(fk, :name, :value) + .each do |id, name, value| + preloaded = map[id].preloaded_custom_fields - preloaded = map[id].preloaded_custom_fields - - if preloaded[name].nil? - preloaded.delete(name) - end + preloaded.delete(name) if preloaded[name].nil? HasCustomFields::Helpers.append_field(preloaded, name, value, @custom_field_types) - end - + end end end end @@ -160,7 +158,8 @@ module HasCustomFields @custom_fields_orig = nil end - class NotPreloadedError < StandardError; end + class NotPreloadedError < StandardError + end class PreloadedProxy def initialize(preloaded, klass_with_custom_fields) @preloaded = preloaded @@ -172,7 +171,8 @@ module HasCustomFields @preloaded[key] else # for now you can not mix preload an non preload, it better just to fail - raise NotPreloadedError, "Attempted to access the non preloaded custom field '#{key}' on the '#{@klass_with_custom_fields}' class. This is disallowed to prevent N+1 queries." + raise NotPreloadedError, + "Attempted to access the non preloaded custom field '#{key}' on the '#{@klass_with_custom_fields}' class. This is disallowed to prevent N+1 queries." end end end @@ -210,9 +210,7 @@ module HasCustomFields def upsert_custom_fields(fields) fields.each do |k, v| row_count = _custom_fields.where(name: k).update_all(value: v) - if row_count == 0 - _custom_fields.create!(name: k, value: v) - end + _custom_fields.create!(name: k, value: v) if row_count == 0 custom_fields[k.to_s] = v # We normalize custom_fields as strings end @@ -281,8 +279,8 @@ module HasCustomFields # update the same custom field we should catch the error and perform an update instead. def create_singular(name, value, field_type = nil) write_value = value.is_a?(Hash) || field_type == :json ? value.to_json : value - write_value = 't' if write_value.is_a?(TrueClass) - write_value = 'f' if write_value.is_a?(FalseClass) + write_value = "t" if write_value.is_a?(TrueClass) + write_value = "f" if write_value.is_a?(FalseClass) row_count = DB.exec(<<~SQL, name: name, value: write_value, id: id, now: Time.zone.now) INSERT INTO #{_custom_fields.table_name} (#{custom_fields_fk}, name, value, created_at, updated_at) VALUES (:id, :name, :value, :now, :now) @@ -291,15 +289,15 @@ module HasCustomFields _custom_fields.where(name: name).update_all(value: write_value) if row_count == 0 end -protected + protected def refresh_custom_fields_from_db target = HashWithIndifferentAccess.new - _custom_fields.order('id asc').pluck(:name, :value).each do |key, value| - self.class.append_custom_field(target, key, value) - end + _custom_fields + .order("id asc") + .pluck(:name, :value) + .each { |key, value| self.class.append_custom_field(target, key, value) } @custom_fields_orig = target @custom_fields = @custom_fields_orig.deep_dup end - end diff --git a/app/models/concerns/has_destroyed_web_hook.rb b/app/models/concerns/has_destroyed_web_hook.rb index 0b05f437a1..85ce31ae6f 100644 --- a/app/models/concerns/has_destroyed_web_hook.rb +++ b/app/models/concerns/has_destroyed_web_hook.rb @@ -3,9 +3,7 @@ module HasDestroyedWebHook extend ActiveSupport::Concern - included do - around_destroy :enqueue_destroyed_web_hook - end + included { around_destroy :enqueue_destroyed_web_hook } def enqueue_destroyed_web_hook type = self.class.name.underscore.to_sym @@ -13,10 +11,7 @@ module HasDestroyedWebHook if WebHook.active_web_hooks(type).exists? payload = WebHook.generate_payload(type, self) yield - WebHook.enqueue_hooks(type, "#{type}_destroyed".to_sym, - id: id, - payload: payload - ) + WebHook.enqueue_hooks(type, "#{type}_destroyed".to_sym, id: id, payload: payload) else yield end diff --git a/app/models/concerns/has_sanitizable_fields.rb b/app/models/concerns/has_sanitizable_fields.rb index b0db07de00..5897dddd0d 100644 --- a/app/models/concerns/has_sanitizable_fields.rb +++ b/app/models/concerns/has_sanitizable_fields.rb @@ -6,7 +6,7 @@ module HasSanitizableFields def sanitize_field(field, additional_attributes: []) if field sanitizer = Rails::Html::SafeListSanitizer.new - allowed_attributes = Rails::Html::SafeListSanitizer.allowed_attributes + allowed_attributes = Rails::Html::SafeListSanitizer.allowed_attributes.dup if additional_attributes.present? allowed_attributes = allowed_attributes.merge(additional_attributes) @@ -15,7 +15,7 @@ module HasSanitizableFields field = CGI.unescape_html(sanitizer.sanitize(field, attributes: allowed_attributes)) # Just replace the characters that our translations use for interpolation. # Calling CGI.unescape removes characters like '+', which will corrupt the original value. - field = field.gsub('%7B', '{').gsub('%7D', '}') + field = field.gsub("%7B", "{").gsub("%7D", "}") end field diff --git a/app/models/concerns/has_search_data.rb b/app/models/concerns/has_search_data.rb index e997083019..1ff9a16358 100644 --- a/app/models/concerns/has_search_data.rb +++ b/app/models/concerns/has_search_data.rb @@ -4,7 +4,7 @@ module HasSearchData extend ActiveSupport::Concern included do - _associated_record_name = self.name.sub('SearchData', '').underscore + _associated_record_name = self.name.sub("SearchData", "").underscore self.primary_key = "#{_associated_record_name}_id" belongs_to _associated_record_name.to_sym validates_presence_of :search_data diff --git a/app/models/concerns/has_url.rb b/app/models/concerns/has_url.rb index dc6c58cee1..d11aa69321 100644 --- a/app/models/concerns/has_url.rb +++ b/app/models/concerns/has_url.rb @@ -21,10 +21,11 @@ module HasUrl def get_from_url(url) return if url.blank? - uri = begin - URI(UrlHelper.unencode(url)) - rescue URI::Error - end + uri = + begin + URI(UrlHelper.unencode(url)) + rescue URI::Error + end return if uri&.path.blank? data = extract_url(uri.path) @@ -52,10 +53,11 @@ module HasUrl upload_urls.each do |url| next if url.blank? - uri = begin - URI(UrlHelper.unencode(url)) - rescue URI::Error - end + uri = + begin + URI(UrlHelper.unencode(url)) + rescue URI::Error + end next if uri&.path.blank? urls << uri.path diff --git a/app/models/concerns/positionable.rb b/app/models/concerns/positionable.rb index d353e30637..6ee93cdf4c 100644 --- a/app/models/concerns/positionable.rb +++ b/app/models/concerns/positionable.rb @@ -3,14 +3,9 @@ module Positionable extend ActiveSupport::Concern - included do - before_save do - self.position ||= self.class.count - end - end + included { before_save { self.position ||= self.class.count } } def move_to(position_arg) - position = [[position_arg, 0].max, self.class.count - 1].min if self.position.nil? || position > (self.position) @@ -18,13 +13,15 @@ module Positionable UPDATE #{self.class.table_name} SET position = position - 1 WHERE position > :current_position and position <= :new_position", - current_position: self.position, new_position: position + current_position: self.position, + new_position: position elsif position < self.position DB.exec " UPDATE #{self.class.table_name} SET position = position + 1 WHERE position >= :new_position and position < :current_position", - current_position: self.position, new_position: position + current_position: self.position, + new_position: position else # Not moving to a new position return @@ -33,6 +30,8 @@ module Positionable DB.exec " UPDATE #{self.class.table_name} SET position = :position - WHERE id = :id", id: id, position: position + WHERE id = :id", + id: id, + position: position end end diff --git a/app/models/concerns/reports/bookmarks.rb b/app/models/concerns/reports/bookmarks.rb index c520855b1d..3dac869021 100644 --- a/app/models/concerns/reports/bookmarks.rb +++ b/app/models/concerns/reports/bookmarks.rb @@ -5,20 +5,20 @@ module Reports::Bookmarks class_methods do def report_bookmarks(report) - report.icon = 'bookmark' + report.icon = "bookmark" category_filter = report.filters.dig(:category) - report.add_filter('category', default: category_filter) + report.add_filter("category", default: category_filter) report.data = [] - Bookmark.count_per_day( - category_id: category_filter, - start_date: report.start_date, - end_date: report.end_date - ).each do |date, count| - report.data << { x: date, y: count } - end - add_counts report, Bookmark, 'bookmarks.created_at' + Bookmark + .count_per_day( + category_id: category_filter, + start_date: report.start_date, + end_date: report.end_date, + ) + .each { |date, count| report.data << { x: date, y: count } } + add_counts report, Bookmark, "bookmarks.created_at" end end end diff --git a/app/models/concerns/reports/consolidated_api_requests.rb b/app/models/concerns/reports/consolidated_api_requests.rb index ab25b1f73f..60184f92dd 100644 --- a/app/models/concerns/reports/consolidated_api_requests.rb +++ b/app/models/concerns/reports/consolidated_api_requests.rb @@ -5,29 +5,28 @@ module Reports::ConsolidatedApiRequests class_methods do def report_consolidated_api_requests(report) - filters = %w[ - api - user_api - ] + filters = %w[api user_api] report.modes = [:stacked_chart] - tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' - danger = ColorScheme.hex_for_name('danger') || 'e45735' + tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc" + danger = ColorScheme.hex_for_name("danger") || "e45735" - requests = filters.map do |filter| - color = filter == "api" ? report.rgba_color(tertiary) : report.rgba_color(danger) + requests = + filters.map do |filter| + color = filter == "api" ? report.rgba_color(tertiary) : report.rgba_color(danger) - { - req: filter, - label: I18n.t("reports.consolidated_api_requests.xaxis.#{filter}"), - color: color, - data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) - } - end + { + req: filter, + label: I18n.t("reports.consolidated_api_requests.xaxis.#{filter}"), + color: color, + data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]), + } + end requests.each do |request| - request[:data] = request[:data].where('date >= ? AND date <= ?', report.start_date, report.end_date) + request[:data] = request[:data] + .where("date >= ? AND date <= ?", report.start_date, report.end_date) .order(date: :asc) .group(:date) .sum(:count) diff --git a/app/models/concerns/reports/consolidated_page_views.rb b/app/models/concerns/reports/consolidated_page_views.rb index 35757d7fbe..41854f5668 100644 --- a/app/models/concerns/reports/consolidated_page_views.rb +++ b/app/models/concerns/reports/consolidated_page_views.rb @@ -5,38 +5,32 @@ module Reports::ConsolidatedPageViews class_methods do def report_consolidated_page_views(report) - filters = %w[ - page_view_logged_in - page_view_anon - page_view_crawler - ] + filters = %w[page_view_logged_in page_view_anon page_view_crawler] report.modes = [:stacked_chart] - tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' - danger = ColorScheme.hex_for_name('danger') || 'e45735' + tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc" + danger = ColorScheme.hex_for_name("danger") || "e45735" - requests = filters.map do |filter| - color = report.rgba_color(tertiary) + requests = + filters.map do |filter| + color = report.rgba_color(tertiary) - if filter == "page_view_anon" - color = report.lighten_color(tertiary, 0.25) + color = report.lighten_color(tertiary, 0.25) if filter == "page_view_anon" + + color = report.rgba_color(danger, 0.75) if filter == "page_view_crawler" + + { + req: filter, + label: I18n.t("reports.consolidated_page_views.xaxis.#{filter}"), + color: color, + data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]), + } end - if filter == "page_view_crawler" - color = report.rgba_color(danger, 0.75) - end - - { - req: filter, - label: I18n.t("reports.consolidated_page_views.xaxis.#{filter}"), - color: color, - data: ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) - } - end - requests.each do |request| - request[:data] = request[:data].where('date >= ? AND date <= ?', report.start_date, report.end_date) + request[:data] = request[:data] + .where("date >= ? AND date <= ?", report.start_date, report.end_date) .order(date: :asc) .group(:date) .sum(:count) diff --git a/app/models/concerns/reports/daily_engaged_users.rb b/app/models/concerns/reports/daily_engaged_users.rb index 9e4dfdbd59..f2be595c9a 100644 --- a/app/models/concerns/reports/daily_engaged_users.rb +++ b/app/models/concerns/reports/daily_engaged_users.rb @@ -12,27 +12,23 @@ module Reports::DailyEngagedUsers data = UserAction.count_daily_engaged_users(report.start_date, report.end_date) if report.facets.include?(:prev30Days) - prev30DaysData = UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date) + prev30DaysData = + UserAction.count_daily_engaged_users(report.start_date - 30.days, report.start_date) report.prev30Days = prev30DaysData.sum { |k, v| v } end - if report.facets.include?(:total) - report.total = UserAction.count_daily_engaged_users - end + report.total = UserAction.count_daily_engaged_users if report.facets.include?(:total) if report.facets.include?(:prev_period) - prev_data = UserAction.count_daily_engaged_users(report.prev_start_date, report.prev_end_date) + prev_data = + UserAction.count_daily_engaged_users(report.prev_start_date, report.prev_end_date) prev = prev_data.sum { |k, v| v } - if prev > 0 - prev = prev / ((report.end_date - report.start_date) / 1.day) - end + prev = prev / ((report.end_date - report.start_date) / 1.day) if prev > 0 report.prev_period = prev end - data.each do |key, value| - report.data << { x: key, y: value } - end + data.each { |key, value| report.data << { x: key, y: value } } end end end diff --git a/app/models/concerns/reports/dau_by_mau.rb b/app/models/concerns/reports/dau_by_mau.rb index 69956c8e57..4d97531fed 100644 --- a/app/models/concerns/reports/dau_by_mau.rb +++ b/app/models/concerns/reports/dau_by_mau.rb @@ -6,16 +6,8 @@ module Reports::DauByMau class_methods do def report_dau_by_mau(report) report.labels = [ - { - type: :date, - property: :x, - title: I18n.t("reports.default.labels.day") - }, - { - type: :percent, - property: :y, - title: I18n.t("reports.default.labels.percent") - }, + { type: :date, property: :x, title: I18n.t("reports.default.labels.day") }, + { type: :percent, property: :y, title: I18n.t("reports.default.labels.percent") }, ] report.average = true @@ -25,21 +17,23 @@ module Reports::DauByMau report.data = [] - compute_dau_by_mau = Proc.new { |data_point| - if data_point["mau"] == 0 - 0 - else - ((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil(2) + compute_dau_by_mau = + Proc.new do |data_point| + if data_point["mau"] == 0 + 0 + else + ((data_point["dau"].to_f / data_point["mau"].to_f) * 100).ceil(2) + end end - } - dau_avg = Proc.new { |start_date, end_date| - data_points = UserVisit.count_by_active_users(start_date, end_date) - if !data_points.empty? - sum = data_points.sum { |data_point| compute_dau_by_mau.call(data_point) } - (sum.to_f / data_points.count.to_f).ceil(2) + dau_avg = + Proc.new do |start_date, end_date| + data_points = UserVisit.count_by_active_users(start_date, end_date) + if !data_points.empty? + sum = data_points.sum { |data_point| compute_dau_by_mau.call(data_point) } + (sum.to_f / data_points.count.to_f).ceil(2) + end end - } data_points.each do |data_point| report.data << { x: data_point["date"], y: compute_dau_by_mau.call(data_point) } diff --git a/app/models/concerns/reports/flags.rb b/app/models/concerns/reports/flags.rb index 8c8cd8424a..99c73085fa 100644 --- a/app/models/concerns/reports/flags.rb +++ b/app/models/concerns/reports/flags.rb @@ -7,7 +7,7 @@ module Reports::Flags def report_flags(report) category_id, include_subcategories = report.add_category_filter - report.icon = 'flag' + report.icon = "flag" report.higher_is_better = false basic_report_about( @@ -17,20 +17,21 @@ module Reports::Flags report.start_date, report.end_date, category_id, - include_subcategories + include_subcategories, ) countable = ReviewableFlaggedPost.scores_with_topics if category_id if include_subcategories - countable = countable.where('topics.category_id IN (?)', Category.subcategory_ids(category_id)) + countable = + countable.where("topics.category_id IN (?)", Category.subcategory_ids(category_id)) else - countable = countable.where('topics.category_id = ?', category_id) + countable = countable.where("topics.category_id = ?", category_id) end end - add_counts report, countable, 'reviewable_scores.created_at' + add_counts report, countable, "reviewable_scores.created_at" end end end diff --git a/app/models/concerns/reports/flags_status.rb b/app/models/concerns/reports/flags_status.rb index 1349f13b0b..572185ebc5 100644 --- a/app/models/concerns/reports/flags_status.rb +++ b/app/models/concerns/reports/flags_status.rb @@ -13,42 +13,42 @@ module Reports::FlagsStatus properties: { topic_id: :topic_id, number: :post_number, - truncated_raw: :post_type + truncated_raw: :post_type, }, - title: I18n.t("reports.flags_status.labels.flag") + title: I18n.t("reports.flags_status.labels.flag"), }, { type: :user, properties: { username: :staff_username, id: :staff_id, - avatar: :staff_avatar_template + avatar: :staff_avatar_template, }, - title: I18n.t("reports.flags_status.labels.assigned") + title: I18n.t("reports.flags_status.labels.assigned"), }, { type: :user, properties: { username: :poster_username, id: :poster_id, - avatar: :poster_avatar_template + avatar: :poster_avatar_template, }, - title: I18n.t("reports.flags_status.labels.poster") + title: I18n.t("reports.flags_status.labels.poster"), }, { type: :user, properties: { username: :flagger_username, id: :flagger_id, - avatar: :flagger_avatar_template - }, - title: I18n.t("reports.flags_status.labels.flagger") + avatar: :flagger_avatar_template, + }, + title: I18n.t("reports.flags_status.labels.flagger"), }, { type: :seconds, property: :response_time, - title: I18n.t("reports.flags_status.labels.time_to_resolution") - } + title: I18n.t("reports.flags_status.labels.time_to_resolution"), + }, ] report.data = [] @@ -70,7 +70,7 @@ module Reports::FlagsStatus user_id, COALESCE(disagreed_at, agreed_at, deferred_at) AS responded_at FROM post_actions - WHERE post_action_type_id IN (#{flag_types.values.join(',')}) + WHERE post_action_type_id IN (#{flag_types.values.join(",")}) AND created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' ORDER BY created_at DESC @@ -136,43 +136,54 @@ module Reports::FlagsStatus ON pd.id = pa.id SQL - DB.query(sql).each do |row| - data = {} + DB + .query(sql) + .each do |row| + data = {} - data[:post_type] = flag_types.key(row.post_action_type_id).to_s - data[:post_number] = row.post_number - data[:topic_id] = row.topic_id + data[:post_type] = flag_types.key(row.post_action_type_id).to_s + data[:post_number] = row.post_number + data[:topic_id] = row.topic_id - if row.staff_id - data[:staff_username] = row.staff_username - data[:staff_id] = row.staff_id - data[:staff_avatar_template] = User.avatar_template(row.staff_username, row.staff_avatar_id) + if row.staff_id + data[:staff_username] = row.staff_username + data[:staff_id] = row.staff_id + data[:staff_avatar_template] = User.avatar_template( + row.staff_username, + row.staff_avatar_id, + ) + end + + if row.poster_id + data[:poster_username] = row.poster_username + data[:poster_id] = row.poster_id + data[:poster_avatar_template] = User.avatar_template( + row.poster_username, + row.poster_avatar_id, + ) + end + + if row.flagger_id + data[:flagger_id] = row.flagger_id + data[:flagger_username] = row.flagger_username + data[:flagger_avatar_template] = User.avatar_template( + row.flagger_username, + row.flagger_avatar_id, + ) + end + + if row.agreed_by_id + data[:resolution] = I18n.t("reports.flags_status.values.agreed") + elsif row.disagreed_by_id + data[:resolution] = I18n.t("reports.flags_status.values.disagreed") + elsif row.deferred_by_id + data[:resolution] = I18n.t("reports.flags_status.values.deferred") + else + data[:resolution] = I18n.t("reports.flags_status.values.no_action") + end + data[:response_time] = row.responded_at ? row.responded_at - row.created_at : nil + report.data << data end - - if row.poster_id - data[:poster_username] = row.poster_username - data[:poster_id] = row.poster_id - data[:poster_avatar_template] = User.avatar_template(row.poster_username, row.poster_avatar_id) - end - - if row.flagger_id - data[:flagger_id] = row.flagger_id - data[:flagger_username] = row.flagger_username - data[:flagger_avatar_template] = User.avatar_template(row.flagger_username, row.flagger_avatar_id) - end - - if row.agreed_by_id - data[:resolution] = I18n.t("reports.flags_status.values.agreed") - elsif row.disagreed_by_id - data[:resolution] = I18n.t("reports.flags_status.values.disagreed") - elsif row.deferred_by_id - data[:resolution] = I18n.t("reports.flags_status.values.deferred") - else - data[:resolution] = I18n.t("reports.flags_status.values.no_action") - end - data[:response_time] = row.responded_at ? row.responded_at - row.created_at : nil - report.data << data - end end end end diff --git a/app/models/concerns/reports/likes.rb b/app/models/concerns/reports/likes.rb index 0815a9e79c..9f5f3375e4 100644 --- a/app/models/concerns/reports/likes.rb +++ b/app/models/concerns/reports/likes.rb @@ -5,7 +5,7 @@ module Reports::Likes class_methods do def report_likes(report) - report.icon = 'heart' + report.icon = "heart" post_action_report report, PostActionType.types[:like] end diff --git a/app/models/concerns/reports/mobile_visits.rb b/app/models/concerns/reports/mobile_visits.rb index a5e331891a..652b12ee46 100644 --- a/app/models/concerns/reports/mobile_visits.rb +++ b/app/models/concerns/reports/mobile_visits.rb @@ -7,7 +7,15 @@ module Reports::MobileVisits def report_mobile_visits(report) basic_report_about report, UserVisit, :mobile_by_day, report.start_date, report.end_date report.total = UserVisit.where(mobile: true).count - report.prev30Days = UserVisit.where(mobile: true).where("visited_at >= ? and visited_at < ?", report.start_date - 30.days, report.start_date).count + report.prev30Days = + UserVisit + .where(mobile: true) + .where( + "visited_at >= ? and visited_at < ?", + report.start_date - 30.days, + report.start_date, + ) + .count end end end diff --git a/app/models/concerns/reports/moderator_warning_private_messages.rb b/app/models/concerns/reports/moderator_warning_private_messages.rb index ba99559428..16047034d7 100644 --- a/app/models/concerns/reports/moderator_warning_private_messages.rb +++ b/app/models/concerns/reports/moderator_warning_private_messages.rb @@ -5,7 +5,7 @@ module Reports::ModeratorWarningPrivateMessages class_methods do def report_moderator_warning_private_messages(report) - report.icon = 'envelope' + report.icon = "envelope" private_messages_report report, TopicSubtype.moderator_warning end end diff --git a/app/models/concerns/reports/moderators_activity.rb b/app/models/concerns/reports/moderators_activity.rb index 135e4f0946..b66bf0df74 100644 --- a/app/models/concerns/reports/moderators_activity.rb +++ b/app/models/concerns/reports/moderators_activity.rb @@ -18,33 +18,33 @@ module Reports::ModeratorsActivity { property: :flag_count, type: :number, - title: I18n.t("reports.moderators_activity.labels.flag_count") + title: I18n.t("reports.moderators_activity.labels.flag_count"), }, { type: :seconds, property: :time_read, - title: I18n.t("reports.moderators_activity.labels.time_read") + title: I18n.t("reports.moderators_activity.labels.time_read"), }, { property: :topic_count, type: :number, - title: I18n.t("reports.moderators_activity.labels.topic_count") + title: I18n.t("reports.moderators_activity.labels.topic_count"), }, { property: :pm_count, type: :number, - title: I18n.t("reports.moderators_activity.labels.pm_count") + title: I18n.t("reports.moderators_activity.labels.pm_count"), }, { property: :post_count, type: :number, - title: I18n.t("reports.moderators_activity.labels.post_count") + title: I18n.t("reports.moderators_activity.labels.post_count"), }, { property: :revision_count, type: :number, - title: I18n.t("reports.moderators_activity.labels.revision_count") - } + title: I18n.t("reports.moderators_activity.labels.revision_count"), + }, ] report.modes = [:table] @@ -75,7 +75,7 @@ module Reports::ModeratorsActivity SELECT agreed_by_id, disagreed_by_id FROM post_actions - WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(',')}) + WHERE post_action_type_id IN (#{PostActionType.flag_types_without_custom.values.join(",")}) AND created_at >= '#{report.start_date}' AND created_at <= '#{report.end_date}' ), @@ -173,19 +173,21 @@ module Reports::ModeratorsActivity ORDER BY m.username SQL - DB.query(query).each do |row| - mod = {} - mod[:username] = row.username - mod[:user_id] = row.user_id - mod[:user_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - mod[:time_read] = row.time_read - mod[:flag_count] = row.flag_count - mod[:revision_count] = row.revision_count - mod[:topic_count] = row.topic_count - mod[:post_count] = row.post_count - mod[:pm_count] = row.pm_count - report.data << mod - end + DB + .query(query) + .each do |row| + mod = {} + mod[:username] = row.username + mod[:user_id] = row.user_id + mod[:user_avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + mod[:time_read] = row.time_read + mod[:flag_count] = row.flag_count + mod[:revision_count] = row.revision_count + mod[:topic_count] = row.topic_count + mod[:post_count] = row.post_count + mod[:pm_count] = row.pm_count + report.data << mod + end end end end diff --git a/app/models/concerns/reports/new_contributors.rb b/app/models/concerns/reports/new_contributors.rb index c8a8afe209..89b2fdda17 100644 --- a/app/models/concerns/reports/new_contributors.rb +++ b/app/models/concerns/reports/new_contributors.rb @@ -10,23 +10,21 @@ module Reports::NewContributors data = User.real.count_by_first_post(report.start_date, report.end_date) if report.facets.include?(:prev30Days) - prev30DaysData = User.real.count_by_first_post(report.start_date - 30.days, report.start_date) + prev30DaysData = + User.real.count_by_first_post(report.start_date - 30.days, report.start_date) report.prev30Days = prev30DaysData.sum { |k, v| v } end - if report.facets.include?(:total) - report.total = User.real.count_by_first_post - end + report.total = User.real.count_by_first_post if report.facets.include?(:total) if report.facets.include?(:prev_period) - prev_period_data = User.real.count_by_first_post(report.prev_start_date, report.prev_end_date) + prev_period_data = + User.real.count_by_first_post(report.prev_start_date, report.prev_end_date) report.prev_period = prev_period_data.sum { |k, v| v } # report.prev_data = prev_period_data.map { |k, v| { x: k, y: v } } end - data.each do |key, value| - report.data << { x: key, y: value } - end + data.each { |key, value| report.data << { x: key, y: value } } end end end diff --git a/app/models/concerns/reports/notify_moderators_private_messages.rb b/app/models/concerns/reports/notify_moderators_private_messages.rb index 77c2df12d5..e7dcfed2e2 100644 --- a/app/models/concerns/reports/notify_moderators_private_messages.rb +++ b/app/models/concerns/reports/notify_moderators_private_messages.rb @@ -5,7 +5,7 @@ module Reports::NotifyModeratorsPrivateMessages class_methods do def report_notify_moderators_private_messages(report) - report.icon = 'envelope' + report.icon = "envelope" private_messages_report report, TopicSubtype.notify_moderators end end diff --git a/app/models/concerns/reports/notify_user_private_messages.rb b/app/models/concerns/reports/notify_user_private_messages.rb index aac3dca4cc..655fa812af 100644 --- a/app/models/concerns/reports/notify_user_private_messages.rb +++ b/app/models/concerns/reports/notify_user_private_messages.rb @@ -5,7 +5,7 @@ module Reports::NotifyUserPrivateMessages class_methods do def report_notify_user_private_messages(report) - report.icon = 'envelope' + report.icon = "envelope" private_messages_report report, TopicSubtype.notify_user end end diff --git a/app/models/concerns/reports/post_edits.rb b/app/models/concerns/reports/post_edits.rb index 554fea563a..af97ddc19a 100644 --- a/app/models/concerns/reports/post_edits.rb +++ b/app/models/concerns/reports/post_edits.rb @@ -6,7 +6,7 @@ module Reports::PostEdits class_methods do def report_post_edits(report) category_id, include_subcategories = report.add_category_filter - editor_username = report.filters['editor'] + editor_username = report.filters["editor"] report.modes = [:table] @@ -14,16 +14,16 @@ module Reports::PostEdits { type: :date, property: :created_at, - title: I18n.t("reports.post_edits.labels.edited_at") + title: I18n.t("reports.post_edits.labels.edited_at"), }, { type: :post, properties: { topic_id: :topic_id, number: :post_number, - truncated_raw: :post_raw + truncated_raw: :post_raw, }, - title: I18n.t("reports.post_edits.labels.post") + title: I18n.t("reports.post_edits.labels.post"), }, { type: :user, @@ -32,7 +32,7 @@ module Reports::PostEdits id: :editor_id, avatar: :editor_avatar_template, }, - title: I18n.t("reports.post_edits.labels.editor") + title: I18n.t("reports.post_edits.labels.editor"), }, { type: :user, @@ -41,12 +41,12 @@ module Reports::PostEdits id: :author_id, avatar: :author_avatar_template, }, - title: I18n.t("reports.post_edits.labels.author") + title: I18n.t("reports.post_edits.labels.author"), }, { type: :text, property: :edit_reason, - title: I18n.t("reports.post_edits.labels.edit_reason") + title: I18n.t("reports.post_edits.labels.edit_reason"), }, ] @@ -105,10 +105,16 @@ module Reports::PostEdits revision = {} revision[:editor_id] = r.editor_id revision[:editor_username] = r.editor_username - revision[:editor_avatar_template] = User.avatar_template(r.editor_username, r.editor_avatar_id) + revision[:editor_avatar_template] = User.avatar_template( + r.editor_username, + r.editor_avatar_id, + ) revision[:author_id] = r.author_id revision[:author_username] = r.author_username - revision[:author_avatar_template] = User.avatar_template(r.author_username, r.author_avatar_id) + revision[:author_avatar_template] = User.avatar_template( + r.author_username, + r.author_avatar_id, + ) revision[:edit_reason] = r.revision_version == r.post_version ? r.edit_reason : nil revision[:created_at] = r.created_at revision[:post_raw] = r.post_raw diff --git a/app/models/concerns/reports/posts.rb b/app/models/concerns/reports/posts.rb index 5a692d8f5a..b2ac519909 100644 --- a/app/models/concerns/reports/posts.rb +++ b/app/models/concerns/reports/posts.rb @@ -5,22 +5,32 @@ module Reports::Posts class_methods do def report_posts(report) - report.modes = [:table, :chart] + report.modes = %i[table chart] category_id, include_subcategories = report.add_category_filter - basic_report_about report, Post, :public_posts_count_per_day, report.start_date, report.end_date, category_id, include_subcategories + basic_report_about report, + Post, + :public_posts_count_per_day, + report.start_date, + report.end_date, + category_id, + include_subcategories countable = Post.public_posts.where(post_type: Post.types[:regular]) if category_id if include_subcategories - countable = countable.joins(:topic).where('topics.category_id IN (?)', Category.subcategory_ids(category_id)) + countable = + countable.joins(:topic).where( + "topics.category_id IN (?)", + Category.subcategory_ids(category_id), + ) else - countable = countable.joins(:topic).where('topics.category_id = ?', category_id) + countable = countable.joins(:topic).where("topics.category_id = ?", category_id) end end - add_counts report, countable, 'posts.created_at' + add_counts report, countable, "posts.created_at" end end end diff --git a/app/models/concerns/reports/profile_views.rb b/app/models/concerns/reports/profile_views.rb index 4f630a92c8..0fccea8396 100644 --- a/app/models/concerns/reports/profile_views.rb +++ b/app/models/concerns/reports/profile_views.rb @@ -6,14 +6,24 @@ module Reports::ProfileViews class_methods do def report_profile_views(report) group_filter = report.filters.dig(:group) - report.add_filter('group', type: 'group', default: group_filter) + report.add_filter("group", type: "group", default: group_filter) start_date = report.start_date end_date = report.end_date - basic_report_about report, UserProfileView, :profile_views_by_day, start_date, end_date, group_filter + basic_report_about report, + UserProfileView, + :profile_views_by_day, + start_date, + end_date, + group_filter report.total = UserProfile.sum(:views) - report.prev30Days = UserProfileView.where('viewed_at >= ? AND viewed_at < ?', start_date - 30.days, start_date + 1).count + report.prev30Days = + UserProfileView.where( + "viewed_at >= ? AND viewed_at < ?", + start_date - 30.days, + start_date + 1, + ).count end end end diff --git a/app/models/concerns/reports/signups.rb b/app/models/concerns/reports/signups.rb index 380fb0c9bc..200d229dfe 100644 --- a/app/models/concerns/reports/signups.rb +++ b/app/models/concerns/reports/signups.rb @@ -5,14 +5,19 @@ module Reports::Signups class_methods do def report_signups(report) - report.icon = 'user-plus' + report.icon = "user-plus" group_filter = report.filters.dig(:group) - report.add_filter('group', type: 'group', default: group_filter) + report.add_filter("group", type: "group", default: group_filter) if group_filter - basic_report_about report, User.real, :count_by_signup_date, report.start_date, report.end_date, group_filter - add_counts report, User.real, 'users.created_at' + basic_report_about report, + User.real, + :count_by_signup_date, + report.start_date, + report.end_date, + group_filter + add_counts report, User.real, "users.created_at" else report_about report, User.real, :count_by_signup_date end diff --git a/app/models/concerns/reports/staff_logins.rb b/app/models/concerns/reports/staff_logins.rb index 1636150baa..d65c2e2df0 100644 --- a/app/models/concerns/reports/staff_logins.rb +++ b/app/models/concerns/reports/staff_logins.rb @@ -17,17 +17,14 @@ module Reports::StaffLogins id: :user_id, avatar: :avatar_template, }, - title: I18n.t("reports.staff_logins.labels.user") - }, - { - property: :location, - title: I18n.t("reports.staff_logins.labels.location") + title: I18n.t("reports.staff_logins.labels.user"), }, + { property: :location, title: I18n.t("reports.staff_logins.labels.location") }, { property: :created_at, type: :precise_date, - title: I18n.t("reports.staff_logins.labels.login_at") - } + title: I18n.t("reports.staff_logins.labels.login_at"), + }, ] sql = <<~SQL @@ -40,7 +37,7 @@ module Reports::StaffLogins FROM ( SELECT DISTINCT ON (t.client_ip, t.user_id) t.client_ip, t.user_id, t.created_at FROM user_auth_token_logs t - WHERE t.user_id IN (#{User.admins.pluck(:id).join(',')}) + WHERE t.user_id IN (#{User.admins.pluck(:id).join(",")}) AND t.created_at >= :start_date AND t.created_at <= :end_date ORDER BY t.client_ip, t.user_id, t.created_at DESC @@ -50,16 +47,18 @@ module Reports::StaffLogins ORDER BY created_at DESC SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - data = {} - data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - data[:user_id] = row.user_id - data[:username] = row.username - data[:location] = DiscourseIpInfo.get(row.client_ip)[:location] - data[:created_at] = row.created_at + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + data = {} + data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + data[:user_id] = row.user_id + data[:username] = row.username + data[:location] = DiscourseIpInfo.get(row.client_ip)[:location] + data[:created_at] = row.created_at - report.data << data - end + report.data << data + end end end end diff --git a/app/models/concerns/reports/storage_stats.rb b/app/models/concerns/reports/storage_stats.rb index c95c537135..1efb79057f 100644 --- a/app/models/concerns/reports/storage_stats.rb +++ b/app/models/concerns/reports/storage_stats.rb @@ -5,18 +5,19 @@ module Reports::StorageStats class_methods do def report_storage_stats(report) - backup_stats = begin - BackupRestore::BackupStore.create.stats - rescue BackupRestore::BackupStore::StorageError - nil - end + backup_stats = + begin + BackupRestore::BackupStore.create.stats + rescue BackupRestore::BackupStore::StorageError + nil + end report.data = { backups: backup_stats, uploads: { used_bytes: DiskSpace.uploads_used_bytes, - free_bytes: DiskSpace.uploads_free_bytes - } + free_bytes: DiskSpace.uploads_free_bytes, + }, } end end diff --git a/app/models/concerns/reports/suspicious_logins.rb b/app/models/concerns/reports/suspicious_logins.rb index 47a726e558..cd07d427d1 100644 --- a/app/models/concerns/reports/suspicious_logins.rb +++ b/app/models/concerns/reports/suspicious_logins.rb @@ -15,32 +15,17 @@ module Reports::SuspiciousLogins id: :user_id, avatar: :avatar_template, }, - title: I18n.t("reports.suspicious_logins.labels.user") - }, - { - property: :client_ip, - title: I18n.t("reports.suspicious_logins.labels.client_ip") - }, - { - property: :location, - title: I18n.t("reports.suspicious_logins.labels.location") - }, - { - property: :browser, - title: I18n.t("reports.suspicious_logins.labels.browser") - }, - { - property: :device, - title: I18n.t("reports.suspicious_logins.labels.device") - }, - { - property: :os, - title: I18n.t("reports.suspicious_logins.labels.os") + title: I18n.t("reports.suspicious_logins.labels.user"), }, + { property: :client_ip, title: I18n.t("reports.suspicious_logins.labels.client_ip") }, + { property: :location, title: I18n.t("reports.suspicious_logins.labels.location") }, + { property: :browser, title: I18n.t("reports.suspicious_logins.labels.browser") }, + { property: :device, title: I18n.t("reports.suspicious_logins.labels.device") }, + { property: :os, title: I18n.t("reports.suspicious_logins.labels.os") }, { type: :date, property: :login_time, - title: I18n.t("reports.suspicious_logins.labels.login_time") + title: I18n.t("reports.suspicious_logins.labels.login_time"), }, ] @@ -56,26 +41,28 @@ module Reports::SuspiciousLogins ORDER BY t.created_at DESC SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - data = {} + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + data = {} - ipinfo = DiscourseIpInfo.get(row.client_ip) - browser = BrowserDetection.browser(row.user_agent) - device = BrowserDetection.device(row.user_agent) - os = BrowserDetection.os(row.user_agent) + ipinfo = DiscourseIpInfo.get(row.client_ip) + browser = BrowserDetection.browser(row.user_agent) + device = BrowserDetection.device(row.user_agent) + os = BrowserDetection.os(row.user_agent) - data[:username] = row.username - data[:user_id] = row.user_id - data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) - data[:client_ip] = row.client_ip.to_s - data[:location] = ipinfo[:location] - data[:browser] = I18n.t("user_auth_tokens.browser.#{browser}") - data[:device] = I18n.t("user_auth_tokens.device.#{device}") - data[:os] = I18n.t("user_auth_tokens.os.#{os}") - data[:login_time] = row.login_time + data[:username] = row.username + data[:user_id] = row.user_id + data[:avatar_template] = User.avatar_template(row.username, row.uploaded_avatar_id) + data[:client_ip] = row.client_ip.to_s + data[:location] = ipinfo[:location] + data[:browser] = I18n.t("user_auth_tokens.browser.#{browser}") + data[:device] = I18n.t("user_auth_tokens.device.#{device}") + data[:os] = I18n.t("user_auth_tokens.os.#{os}") + data[:login_time] = row.login_time - report.data << data - end + report.data << data + end end end end diff --git a/app/models/concerns/reports/system_private_messages.rb b/app/models/concerns/reports/system_private_messages.rb index 0c04625078..8fd142b0e7 100644 --- a/app/models/concerns/reports/system_private_messages.rb +++ b/app/models/concerns/reports/system_private_messages.rb @@ -5,7 +5,7 @@ module Reports::SystemPrivateMessages class_methods do def report_system_private_messages(report) - report.icon = 'envelope' + report.icon = "envelope" private_messages_report report, TopicSubtype.system_message end end diff --git a/app/models/concerns/reports/time_to_first_response.rb b/app/models/concerns/reports/time_to_first_response.rb index 6217591ab5..bd710b11eb 100644 --- a/app/models/concerns/reports/time_to_first_response.rb +++ b/app/models/concerns/reports/time_to_first_response.rb @@ -8,16 +8,27 @@ module Reports::TimeToFirstResponse category_filter = report.filters.dig(:category) category_id, include_subcategories = report.add_category_filter - report.icon = 'reply' + report.icon = "reply" report.higher_is_better = false report.data = [] report.average = true - Topic.time_to_first_response_per_day(report.start_date, report.end_date, category_id: category_id, include_subcategories: include_subcategories).each do |r| - report.data << { x: r['date'], y: r['hours'].to_f.round(2) } - end + Topic + .time_to_first_response_per_day( + report.start_date, + report.end_date, + category_id: category_id, + include_subcategories: include_subcategories, + ) + .each { |r| report.data << { x: r["date"], y: r["hours"].to_f.round(2) } } - report.prev30Days = Topic.time_to_first_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: category_id, include_subcategories: include_subcategories) + report.prev30Days = + Topic.time_to_first_response_total( + start_date: report.start_date - 30.days, + end_date: report.start_date, + category_id: category_id, + include_subcategories: include_subcategories, + ) end end end diff --git a/app/models/concerns/reports/top_ignored_users.rb b/app/models/concerns/reports/top_ignored_users.rb index 3b02f08195..3403de992e 100644 --- a/app/models/concerns/reports/top_ignored_users.rb +++ b/app/models/concerns/reports/top_ignored_users.rb @@ -15,22 +15,18 @@ module Reports::TopIgnoredUsers username: :ignored_username, avatar: :ignored_user_avatar_template, }, - title: I18n.t("reports.top_ignored_users.labels.ignored_user") + title: I18n.t("reports.top_ignored_users.labels.ignored_user"), }, { type: :number, - properties: [ - :ignores_count, - ], - title: I18n.t("reports.top_ignored_users.labels.ignores_count") + properties: [:ignores_count], + title: I18n.t("reports.top_ignored_users.labels.ignores_count"), }, { type: :number, - properties: [ - :mutes_count, - ], - title: I18n.t("reports.top_ignored_users.labels.mutes_count") - } + properties: [:mutes_count], + title: I18n.t("reports.top_ignored_users.labels.mutes_count"), + }, ] report.data = [] @@ -69,15 +65,18 @@ module Reports::TopIgnoredUsers ORDER BY total DESC SQL - DB.query(sql, limit: report.limit || 250).each do |row| - report.data << { - ignored_user_id: row.user_id, - ignored_username: row.username, - ignored_user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), - ignores_count: row.ignores_count, - mutes_count: row.mutes_count, - } - end + DB + .query(sql, limit: report.limit || 250) + .each do |row| + report.data << { + ignored_user_id: row.user_id, + ignored_username: row.username, + ignored_user_avatar_template: + User.avatar_template(row.username, row.uploaded_avatar_id), + ignores_count: row.ignores_count, + mutes_count: row.mutes_count, + } + end end end end diff --git a/app/models/concerns/reports/top_referred_topics.rb b/app/models/concerns/reports/top_referred_topics.rb index 348b6537c1..c8cacb7929 100644 --- a/app/models/concerns/reports/top_referred_topics.rb +++ b/app/models/concerns/reports/top_referred_topics.rb @@ -14,15 +14,15 @@ module Reports::TopReferredTopics type: :topic, properties: { title: :topic_title, - id: :topic_id + id: :topic_id, }, - title: I18n.t('reports.top_referred_topics.labels.topic') + title: I18n.t("reports.top_referred_topics.labels.topic"), }, { property: :num_clicks, type: :number, - title: I18n.t('reports.top_referred_topics.labels.num_clicks') - } + title: I18n.t("reports.top_referred_topics.labels.num_clicks"), + }, ] options = { @@ -30,7 +30,7 @@ module Reports::TopReferredTopics start_date: report.start_date, limit: report.limit || 8, category_id: category_id, - include_subcategories: include_subcategories + include_subcategories: include_subcategories, } result = nil result = IncomingLinksReport.find(:top_referred_topics, options) diff --git a/app/models/concerns/reports/top_referrers.rb b/app/models/concerns/reports/top_referrers.rb index 3e8483ef53..3454f17632 100644 --- a/app/models/concerns/reports/top_referrers.rb +++ b/app/models/concerns/reports/top_referrers.rb @@ -15,24 +15,24 @@ module Reports::TopReferrers id: :user_id, avatar: :user_avatar_template, }, - title: I18n.t("reports.top_referrers.labels.user") + title: I18n.t("reports.top_referrers.labels.user"), }, { property: :num_clicks, type: :number, - title: I18n.t("reports.top_referrers.labels.num_clicks") + title: I18n.t("reports.top_referrers.labels.num_clicks"), }, { property: :num_topics, type: :number, - title: I18n.t("reports.top_referrers.labels.num_topics") - } + title: I18n.t("reports.top_referrers.labels.num_topics"), + }, ] options = { end_date: report.end_date, start_date: report.start_date, - limit: report.limit || 8 + limit: report.limit || 8, } result = IncomingLinksReport.find(:top_referrers, options) diff --git a/app/models/concerns/reports/top_traffic_sources.rb b/app/models/concerns/reports/top_traffic_sources.rb index b6602bd1d2..a1b313f01f 100644 --- a/app/models/concerns/reports/top_traffic_sources.rb +++ b/app/models/concerns/reports/top_traffic_sources.rb @@ -10,20 +10,17 @@ module Reports::TopTrafficSources report.modes = [:table] report.labels = [ - { - property: :domain, - title: I18n.t('reports.top_traffic_sources.labels.domain') - }, + { property: :domain, title: I18n.t("reports.top_traffic_sources.labels.domain") }, { property: :num_clicks, type: :number, - title: I18n.t('reports.top_traffic_sources.labels.num_clicks') + title: I18n.t("reports.top_traffic_sources.labels.num_clicks"), }, { property: :num_topics, type: :number, - title: I18n.t('reports.top_traffic_sources.labels.num_topics') - } + title: I18n.t("reports.top_traffic_sources.labels.num_topics"), + }, ] options = { @@ -31,7 +28,7 @@ module Reports::TopTrafficSources start_date: report.start_date, limit: report.limit || 8, category_id: category_id, - include_subcategories: include_subcategories + include_subcategories: include_subcategories, } result = IncomingLinksReport.find(:top_traffic_sources, options) diff --git a/app/models/concerns/reports/top_uploads.rb b/app/models/concerns/reports/top_uploads.rb index a3a53ce969..d5e2955b7d 100644 --- a/app/models/concerns/reports/top_uploads.rb +++ b/app/models/concerns/reports/top_uploads.rb @@ -8,20 +8,18 @@ module Reports::TopUploads report.modes = [:table] extension_filter = report.filters.dig(:file_extension) - report.add_filter('file_extension', - type: 'list', - default: extension_filter || 'any', - choices: (SiteSetting.authorized_extensions.split('|') + Array(extension_filter)).uniq + report.add_filter( + "file_extension", + type: "list", + default: extension_filter || "any", + choices: (SiteSetting.authorized_extensions.split("|") + Array(extension_filter)).uniq, ) report.labels = [ { type: :link, - properties: [ - :file_url, - :file_name, - ], - title: I18n.t("reports.top_uploads.labels.filename") + properties: %i[file_url file_name], + title: I18n.t("reports.top_uploads.labels.filename"), }, { type: :user, @@ -30,18 +28,14 @@ module Reports::TopUploads id: :author_id, avatar: :author_avatar_template, }, - title: I18n.t("reports.top_uploads.labels.author") + title: I18n.t("reports.top_uploads.labels.author"), }, { type: :text, property: :extension, - title: I18n.t("reports.top_uploads.labels.extension") - }, - { - type: :bytes, - property: :filesize, - title: I18n.t("reports.top_uploads.labels.filesize") + title: I18n.t("reports.top_uploads.labels.extension"), }, + { type: :bytes, property: :filesize, title: I18n.t("reports.top_uploads.labels.filesize") }, ] report.data = [] @@ -64,12 +58,15 @@ module Reports::TopUploads SQL builder = DB.build(sql) - builder.where("up.id > :seeded_id_threshold", seeded_id_threshold: Upload::SEEDED_ID_THRESHOLD) + builder.where( + "up.id > :seeded_id_threshold", + seeded_id_threshold: Upload::SEEDED_ID_THRESHOLD, + ) builder.where("up.created_at >= :start_date", start_date: report.start_date) builder.where("up.created_at < :end_date", end_date: report.end_date) if extension_filter - builder.where("up.extension = :extension", extension: extension_filter.sub(/^\./, '')) + builder.where("up.extension = :extension", extension: extension_filter.sub(/^\./, "")) end builder.query.each do |row| diff --git a/app/models/concerns/reports/top_users_by_likes_received.rb b/app/models/concerns/reports/top_users_by_likes_received.rb index 2f1b7c1132..2861cbefc5 100644 --- a/app/models/concerns/reports/top_users_by_likes_received.rb +++ b/app/models/concerns/reports/top_users_by_likes_received.rb @@ -5,7 +5,7 @@ module Reports::TopUsersByLikesReceived class_methods do def report_top_users_by_likes_received(report) - report.icon = 'heart' + report.icon = "heart" report.data = [] report.modes = [:table] @@ -20,12 +20,12 @@ module Reports::TopUsersByLikesReceived username: :username, avatar: :user_avatar_template, }, - title: I18n.t("reports.top_users_by_likes_received.labels.user") + title: I18n.t("reports.top_users_by_likes_received.labels.user"), }, { type: :number, property: :qtt_like, - title: I18n.t("reports.top_users_by_likes_received.labels.qtt_like") + title: I18n.t("reports.top_users_by_likes_received.labels.qtt_like"), }, ] @@ -44,15 +44,16 @@ module Reports::TopUsersByLikesReceived LIMIT 10 SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - report.data << { - user_id: row.user_id, - username: row.username, - user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), - qtt_like: row.qtt_like, - } - end - + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + qtt_like: row.qtt_like, + } + end end end end diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb index 0bf6a7348c..a6a42c7218 100644 --- a/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb +++ b/app/models/concerns/reports/top_users_by_likes_received_from_a_variety_of_people.rb @@ -5,7 +5,7 @@ module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople class_methods do def report_top_users_by_likes_received_from_a_variety_of_people(report) - report.icon = 'heart' + report.icon = "heart" report.data = [] report.modes = [:table] @@ -20,12 +20,13 @@ module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople username: :username, avatar: :user_avatar_template, }, - title: I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.user") + title: I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.user"), }, { type: :number, property: :qtt_like, - title: I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.qtt_like") + title: + I18n.t("reports.top_users_by_likes_received_from_a_variety_of_people.labels.qtt_like"), }, ] @@ -46,15 +47,16 @@ module Reports::TopUsersByLikesReceivedFromAVarietyOfPeople LIMIT 10 SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - report.data << { - user_id: row.user_id, - username: row.username, - user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), - qtt_like: row.qtt_like, - } - end - + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + qtt_like: row.qtt_like, + } + end end end end diff --git a/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb index d0b41d57d6..0daad06783 100644 --- a/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb +++ b/app/models/concerns/reports/top_users_by_likes_received_from_inferior_trust_level.rb @@ -5,7 +5,7 @@ module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel class_methods do def report_top_users_by_likes_received_from_inferior_trust_level(report) - report.icon = 'heart' + report.icon = "heart" report.data = [] report.modes = [:table] @@ -20,17 +20,22 @@ module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel username: :username, avatar: :user_avatar_template, }, - title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.user") + title: + I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.user"), }, { type: :number, property: :trust_level, - title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.trust_level") + title: + I18n.t( + "reports.top_users_by_likes_received_from_inferior_trust_level.labels.trust_level", + ), }, { type: :number, property: :qtt_like, - title: I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.qtt_like") + title: + I18n.t("reports.top_users_by_likes_received_from_inferior_trust_level.labels.qtt_like"), }, ] @@ -56,16 +61,17 @@ module Reports::TopUsersByLikesReceivedFromInferiorTrustLevel WHERE rank <= 10 SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - report.data << { - user_id: row.user_id, - username: row.username, - user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), - trust_level: row.trust_level, - qtt_like: row.qtt_like, - } - end - + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + report.data << { + user_id: row.user_id, + username: row.username, + user_avatar_template: User.avatar_template(row.username, row.uploaded_avatar_id), + trust_level: row.trust_level, + qtt_like: row.qtt_like, + } + end end end end diff --git a/app/models/concerns/reports/topics.rb b/app/models/concerns/reports/topics.rb index 19c527b314..19412a6a4d 100644 --- a/app/models/concerns/reports/topics.rb +++ b/app/models/concerns/reports/topics.rb @@ -7,12 +7,21 @@ module Reports::Topics def report_topics(report) category_id, include_subcategories = report.add_category_filter - basic_report_about report, Topic, :listable_count_per_day, report.start_date, report.end_date, category_id, include_subcategories + basic_report_about report, + Topic, + :listable_count_per_day, + report.start_date, + report.end_date, + category_id, + include_subcategories countable = Topic.listable_topics - countable = countable.where(category_id: include_subcategories ? Category.subcategory_ids(category_id) : category_id) if category_id + countable = + countable.where( + category_id: include_subcategories ? Category.subcategory_ids(category_id) : category_id, + ) if category_id - add_counts report, countable, 'topics.created_at' + add_counts report, countable, "topics.created_at" end end end diff --git a/app/models/concerns/reports/topics_with_no_response.rb b/app/models/concerns/reports/topics_with_no_response.rb index e09c3f08c5..4cd4b762a2 100644 --- a/app/models/concerns/reports/topics_with_no_response.rb +++ b/app/models/concerns/reports/topics_with_no_response.rb @@ -8,13 +8,28 @@ module Reports::TopicsWithNoResponse category_id, include_subcategories = report.add_category_filter report.data = [] - Topic.with_no_response_per_day(report.start_date, report.end_date, category_id, include_subcategories).each do |r| - report.data << { x: r['date'], y: r['count'].to_i } - end + Topic + .with_no_response_per_day( + report.start_date, + report.end_date, + category_id, + include_subcategories, + ) + .each { |r| report.data << { x: r["date"], y: r["count"].to_i } } - report.total = Topic.with_no_response_total(category_id: category_id, include_subcategories: include_subcategories) + report.total = + Topic.with_no_response_total( + category_id: category_id, + include_subcategories: include_subcategories, + ) - report.prev30Days = Topic.with_no_response_total(start_date: report.start_date - 30.days, end_date: report.start_date, category_id: category_id, include_subcategories: include_subcategories) + report.prev30Days = + Topic.with_no_response_total( + start_date: report.start_date - 30.days, + end_date: report.start_date, + category_id: category_id, + include_subcategories: include_subcategories, + ) end end end diff --git a/app/models/concerns/reports/trending_search.rb b/app/models/concerns/reports/trending_search.rb index b30e522744..8d4f5ff38a 100644 --- a/app/models/concerns/reports/trending_search.rb +++ b/app/models/concerns/reports/trending_search.rb @@ -6,38 +6,28 @@ module Reports::TrendingSearch class_methods do def report_trending_search(report) report.labels = [ - { - property: :term, - type: :text, - title: I18n.t("reports.trending_search.labels.term") - }, + { property: :term, type: :text, title: I18n.t("reports.trending_search.labels.term") }, { property: :searches, type: :number, - title: I18n.t("reports.trending_search.labels.searches") + title: I18n.t("reports.trending_search.labels.searches"), }, { type: :percent, property: :ctr, - title: I18n.t("reports.trending_search.labels.click_through") - } + title: I18n.t("reports.trending_search.labels.click_through"), + }, ] report.data = [] report.modes = [:table] - trends = SearchLog.trending_from(report.start_date, - end_date: report.end_date, - limit: report.limit - ) + trends = + SearchLog.trending_from(report.start_date, end_date: report.end_date, limit: report.limit) trends.each do |trend| - report.data << { - term: trend.term, - searches: trend.searches, - ctr: trend.ctr - } + report.data << { term: trend.term, searches: trend.searches, ctr: trend.ctr } end end end diff --git a/app/models/concerns/reports/trust_level_growth.rb b/app/models/concerns/reports/trust_level_growth.rb index 0071b579ff..d1f82f0689 100644 --- a/app/models/concerns/reports/trust_level_growth.rb +++ b/app/models/concerns/reports/trust_level_growth.rb @@ -7,12 +7,7 @@ module Reports::TrustLevelGrowth def report_trust_level_growth(report) report.modes = [:stacked_chart] - filters = %w[ - tl1_reached - tl2_reached - tl3_reached - tl4_reached - ] + filters = %w[tl1_reached tl2_reached tl3_reached tl4_reached] sql = <<~SQL SELECT @@ -42,41 +37,36 @@ module Reports::TrustLevelGrowth ORDER BY date(created_at) SQL - data = Hash[ filters.collect { |x| [x, []] } ] + data = Hash[filters.collect { |x| [x, []] }] builder = DB.build(sql) builder.query.each do |row| filters.each do |filter| data[filter] << { x: row.date.strftime("%Y-%m-%d"), - y: row.instance_variable_get("@#{filter}") + y: row.instance_variable_get("@#{filter}"), } end end - tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' - quaternary = ColorScheme.hex_for_name('quaternary') || 'e45735' + tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc" + quaternary = ColorScheme.hex_for_name("quaternary") || "e45735" - requests = filters.map do |filter| - color = report.rgba_color(quaternary) + requests = + filters.map do |filter| + color = report.rgba_color(quaternary) - if filter == "tl1_reached" - color = report.lighten_color(tertiary, 0.25) - end - if filter == "tl2_reached" - color = report.rgba_color(tertiary) - end - if filter == "tl3_reached" - color = report.lighten_color(quaternary, 0.25) - end + color = report.lighten_color(tertiary, 0.25) if filter == "tl1_reached" + color = report.rgba_color(tertiary) if filter == "tl2_reached" + color = report.lighten_color(quaternary, 0.25) if filter == "tl3_reached" - { - req: filter, - label: I18n.t("reports.trust_level_growth.xaxis.#{filter}"), - color: color, - data: data[filter] - } - end + { + req: filter, + label: I18n.t("reports.trust_level_growth.xaxis.#{filter}"), + color: color, + data: data[filter], + } + end report.data = requests end diff --git a/app/models/concerns/reports/user_flagging_ratio.rb b/app/models/concerns/reports/user_flagging_ratio.rb index 96c2439106..0147792a8d 100644 --- a/app/models/concerns/reports/user_flagging_ratio.rb +++ b/app/models/concerns/reports/user_flagging_ratio.rb @@ -19,27 +19,27 @@ module Reports::UserFlaggingRatio id: :user_id, avatar: :avatar_template, }, - title: I18n.t("reports.user_flagging_ratio.labels.user") + title: I18n.t("reports.user_flagging_ratio.labels.user"), }, { type: :number, property: :disagreed_flags, - title: I18n.t("reports.user_flagging_ratio.labels.disagreed_flags") + title: I18n.t("reports.user_flagging_ratio.labels.disagreed_flags"), }, { type: :number, property: :agreed_flags, - title: I18n.t("reports.user_flagging_ratio.labels.agreed_flags") + title: I18n.t("reports.user_flagging_ratio.labels.agreed_flags"), }, { type: :number, property: :ignored_flags, - title: I18n.t("reports.user_flagging_ratio.labels.ignored_flags") + title: I18n.t("reports.user_flagging_ratio.labels.ignored_flags"), }, { type: :number, property: :score, - title: I18n.t("reports.user_flagging_ratio.labels.score") + title: I18n.t("reports.user_flagging_ratio.labels.score"), }, ] @@ -74,18 +74,20 @@ module Reports::UserFlaggingRatio LIMIT 100 SQL - DB.query(sql, start_date: report.start_date, end_date: report.end_date).each do |row| - flagger = {} - flagger[:user_id] = row.id - flagger[:username] = row.username - flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) - flagger[:disagreed_flags] = row.disagreed_flags - flagger[:ignored_flags] = row.ignored_flags - flagger[:agreed_flags] = row.agreed_flags - flagger[:score] = row.score + DB + .query(sql, start_date: report.start_date, end_date: report.end_date) + .each do |row| + flagger = {} + flagger[:user_id] = row.id + flagger[:username] = row.username + flagger[:avatar_template] = User.avatar_template(row.username, row.avatar_id) + flagger[:disagreed_flags] = row.disagreed_flags + flagger[:ignored_flags] = row.ignored_flags + flagger[:agreed_flags] = row.agreed_flags + flagger[:score] = row.score - report.data << flagger - end + report.data << flagger + end end end end diff --git a/app/models/concerns/reports/user_to_user_private_messages.rb b/app/models/concerns/reports/user_to_user_private_messages.rb index 7a996400bc..5d38191545 100644 --- a/app/models/concerns/reports/user_to_user_private_messages.rb +++ b/app/models/concerns/reports/user_to_user_private_messages.rb @@ -5,7 +5,7 @@ module Reports::UserToUserPrivateMessages class_methods do def report_user_to_user_private_messages(report) - report.icon = 'envelope' + report.icon = "envelope" private_messages_report report, TopicSubtype.user_to_user end end diff --git a/app/models/concerns/reports/user_to_user_private_messages_with_replies.rb b/app/models/concerns/reports/user_to_user_private_messages_with_replies.rb index 42dfc590e4..76c78e6be7 100644 --- a/app/models/concerns/reports/user_to_user_private_messages_with_replies.rb +++ b/app/models/concerns/reports/user_to_user_private_messages_with_replies.rb @@ -5,12 +5,17 @@ module Reports::UserToUserPrivateMessagesWithReplies class_methods do def report_user_to_user_private_messages_with_replies(report) - report.icon = 'envelope' + report.icon = "envelope" topic_subtype = TopicSubtype.user_to_user - subject = Post.where('posts.user_id > 0') - basic_report_about report, subject, :private_messages_count_per_day, report.start_date, report.end_date, topic_subtype - subject = Post.private_posts.where('posts.user_id > 0').with_topic_subtype(topic_subtype) - add_counts report, subject, 'posts.created_at' + subject = Post.where("posts.user_id > 0") + basic_report_about report, + subject, + :private_messages_count_per_day, + report.start_date, + report.end_date, + topic_subtype + subject = Post.private_posts.where("posts.user_id > 0").with_topic_subtype(topic_subtype) + add_counts report, subject, "posts.created_at" end end end diff --git a/app/models/concerns/reports/users_by_trust_level.rb b/app/models/concerns/reports/users_by_trust_level.rb index ac2d1b94f1..f0226a90c8 100644 --- a/app/models/concerns/reports/users_by_trust_level.rb +++ b/app/models/concerns/reports/users_by_trust_level.rb @@ -12,22 +12,20 @@ module Reports::UsersByTrustLevel report.dates_filtering = false report.labels = [ - { - property: :key, - title: I18n.t("reports.users_by_trust_level.labels.level") - }, - { - property: :y, - type: :number, - title: I18n.t("reports.default.labels.count") - } + { property: :key, title: I18n.t("reports.users_by_trust_level.labels.level") }, + { property: :y, type: :number, title: I18n.t("reports.default.labels.count") }, ] - User.real.group('trust_level').count.sort.each do |level, count| - key = TrustLevel.levels.key(level.to_i) - url = Proc.new { |k| "/admin/users/list/#{k}" } - report.data << { url: url.call(key), key: key, x: level.to_i, y: count } - end + User + .real + .group("trust_level") + .count + .sort + .each do |level, count| + key = TrustLevel.levels.key(level.to_i) + url = Proc.new { |k| "/admin/users/list/#{k}" } + report.data << { url: url.call(key), key: key, x: level.to_i, y: count } + end end end end diff --git a/app/models/concerns/reports/users_by_type.rb b/app/models/concerns/reports/users_by_type.rb index 33952f3dbb..5fd407dd3f 100644 --- a/app/models/concerns/reports/users_by_type.rb +++ b/app/models/concerns/reports/users_by_type.rb @@ -12,31 +12,56 @@ module Reports::UsersByType report.dates_filtering = false report.labels = [ - { - property: :x, - title: I18n.t("reports.users_by_type.labels.type") - }, - { - property: :y, - type: :number, - title: I18n.t("reports.default.labels.count") - } + { property: :x, title: I18n.t("reports.users_by_type.labels.type") }, + { property: :y, type: :number, title: I18n.t("reports.default.labels.count") }, ] label = Proc.new { |x| I18n.t("reports.users_by_type.xaxis_labels.#{x}") } url = Proc.new { |key| "/admin/users/list/#{key}" } admins = User.real.admins.count - report.data << { url: url.call("admins"), icon: "shield-alt", key: "admins", x: label.call("admin"), y: admins } if admins > 0 + if admins > 0 + report.data << { + url: url.call("admins"), + icon: "shield-alt", + key: "admins", + x: label.call("admin"), + y: admins, + } + end moderators = User.real.moderators.count - report.data << { url: url.call("moderators"), icon: "shield-alt", key: "moderators", x: label.call("moderator"), y: moderators } if moderators > 0 + if moderators > 0 + report.data << { + url: url.call("moderators"), + icon: "shield-alt", + key: "moderators", + x: label.call("moderator"), + y: moderators, + } + end suspended = User.real.suspended.count - report.data << { url: url.call("suspended"), icon: "ban", key: "suspended", x: label.call("suspended"), y: suspended } if suspended > 0 + if suspended > 0 + report.data << { + url: url.call("suspended"), + icon: "ban", + key: "suspended", + x: label.call("suspended"), + y: suspended, + } + end silenced = User.real.silenced.count - report.data << { url: url.call("silenced"), icon: "ban", key: "silenced", x: label.call("silenced"), y: silenced } if silenced > 0 + if silenced > 0 + report.data << { + url: url.call("silenced"), + icon: "ban", + key: "silenced", + x: label.call("silenced"), + y: silenced, + } + end end end end diff --git a/app/models/concerns/reports/visits.rb b/app/models/concerns/reports/visits.rb index 676cf77202..7878d0a3b7 100644 --- a/app/models/concerns/reports/visits.rb +++ b/app/models/concerns/reports/visits.rb @@ -6,14 +6,24 @@ module Reports::Visits class_methods do def report_visits(report) group_filter = report.filters.dig(:group) - report.add_filter('group', type: 'group', default: group_filter) + report.add_filter("group", type: "group", default: group_filter) - report.icon = 'user' + report.icon = "user" - basic_report_about report, UserVisit, :by_day, report.start_date, report.end_date, group_filter - add_counts report, UserVisit, 'visited_at' + basic_report_about report, + UserVisit, + :by_day, + report.start_date, + report.end_date, + group_filter + add_counts report, UserVisit, "visited_at" - report.prev30Days = UserVisit.where('visited_at >= ? and visited_at < ?', report.start_date - 30.days, report.start_date).count + report.prev30Days = + UserVisit.where( + "visited_at >= ? and visited_at < ?", + report.start_date - 30.days, + report.start_date, + ).count end end end diff --git a/app/models/concerns/reports/web_crawlers.rb b/app/models/concerns/reports/web_crawlers.rb index 6db61d11a9..cdb731eeab 100644 --- a/app/models/concerns/reports/web_crawlers.rb +++ b/app/models/concerns/reports/web_crawlers.rb @@ -9,22 +9,25 @@ module Reports::WebCrawlers { type: :string, property: :user_agent, - title: I18n.t('reports.web_crawlers.labels.user_agent') + title: I18n.t("reports.web_crawlers.labels.user_agent"), }, { property: :count, type: :number, - title: I18n.t('reports.web_crawlers.labels.page_views') - } + title: I18n.t("reports.web_crawlers.labels.page_views"), + }, ] report.modes = [:table] - report.data = WebCrawlerRequest.where('date >= ? and date <= ?', report.start_date, report.end_date) - .limit(200) - .order('sum_count DESC') - .group(:user_agent).sum(:count) - .map { |ua, count| { user_agent: ua, count: count } } + report.data = + WebCrawlerRequest + .where("date >= ? and date <= ?", report.start_date, report.end_date) + .limit(200) + .order("sum_count DESC") + .group(:user_agent) + .sum(:count) + .map { |ua, count| { user_agent: ua, count: count } } end end end diff --git a/app/models/concerns/roleable.rb b/app/models/concerns/roleable.rb index 0400db789d..ebfe7ac662 100644 --- a/app/models/concerns/roleable.rb +++ b/app/models/concerns/roleable.rb @@ -19,37 +19,38 @@ module Roleable end def whisperer? - @whisperer ||= begin - whispers_allowed_group_ids = SiteSetting.whispers_allowed_group_ids - return false if whispers_allowed_group_ids.blank? - return true if admin - return true if whispers_allowed_group_ids.include?(primary_group_id) - group_users&.exists?(group_id: whispers_allowed_group_ids) - end + @whisperer ||= + begin + whispers_allowed_group_ids = SiteSetting.whispers_allowed_group_ids + return false if whispers_allowed_group_ids.blank? + return true if admin + return true if whispers_allowed_group_ids.include?(primary_group_id) + group_users&.exists?(group_id: whispers_allowed_group_ids) + end end def grant_moderation! return if moderator - set_permission('moderator', true) + set_permission("moderator", true) auto_approve_user enqueue_staff_welcome_message(:moderator) set_default_notification_levels(:moderators) end def revoke_moderation! - set_permission('moderator', false) + set_permission("moderator", false) end def grant_admin! return if admin - set_permission('admin', true) + set_permission("admin", true) auto_approve_user enqueue_staff_welcome_message(:admin) set_default_notification_levels(:admins) end def revoke_admin! - set_permission('admin', false) + set_permission("admin", false) end def save_and_refresh_staff_groups! diff --git a/app/models/concerns/searchable.rb b/app/models/concerns/searchable.rb index ed01ecff8e..84ba85843b 100644 --- a/app/models/concerns/searchable.rb +++ b/app/models/concerns/searchable.rb @@ -3,16 +3,7 @@ module Searchable extend ActiveSupport::Concern - PRIORITIES = Enum.new( - ignore: 1, - very_low: 2, - low: 3, - normal: 0, - high: 4, - very_high: 5 - ) + PRIORITIES = Enum.new(ignore: 1, very_low: 2, low: 3, normal: 0, high: 4, very_high: 5) - included do - has_one "#{self.name.underscore}_search_data".to_sym, dependent: :destroy - end + included { has_one "#{self.name.underscore}_search_data".to_sym, dependent: :destroy } end diff --git a/app/models/concerns/second_factor_manager.rb b/app/models/concerns/second_factor_manager.rb index e21957e75f..dc74d541e0 100644 --- a/app/models/concerns/second_factor_manager.rb +++ b/app/models/concerns/second_factor_manager.rb @@ -5,24 +5,27 @@ module SecondFactorManager extend ActiveSupport::Concern - SecondFactorAuthenticationResult = Struct.new( - :ok, - :error, - :reason, - :backup_enabled, - :security_key_enabled, - :totp_enabled, - :multiple_second_factor_methods, - :used_2fa_method, - ) + SecondFactorAuthenticationResult = + Struct.new( + :ok, + :error, + :reason, + :backup_enabled, + :security_key_enabled, + :totp_enabled, + :multiple_second_factor_methods, + :used_2fa_method, + ) def create_totp(opts = {}) require_rotp - UserSecondFactor.create!({ - user_id: self.id, - method: UserSecondFactor.methods[:totp], - data: ROTP::Base32.random - }.merge(opts)) + UserSecondFactor.create!( + { + user_id: self.id, + method: UserSecondFactor.methods[:totp], + data: ROTP::Base32.random, + }.merge(opts), + ) end def get_totp_object(data) @@ -38,19 +41,18 @@ module SecondFactorManager totps = self&.user_second_factors.totps authenticated = false totps.each do |totp| - last_used = 0 - if totp.last_used - last_used = totp.last_used.to_i - end + last_used = totp.last_used.to_i if totp.last_used - authenticated = !token.blank? && totp.totp_object.verify( - token, - drift_ahead: TOTP_ALLOWED_DRIFT_SECONDS, - drift_behind: TOTP_ALLOWED_DRIFT_SECONDS, - after: last_used - ) + authenticated = + !token.blank? && + totp.totp_object.verify( + token, + drift_ahead: TOTP_ALLOWED_DRIFT_SECONDS, + drift_behind: TOTP_ALLOWED_DRIFT_SECONDS, + after: last_used, + ) if authenticated totp.update!(last_used: DateTime.now) @@ -61,21 +63,21 @@ module SecondFactorManager end def totp_enabled? - !SiteSetting.enable_discourse_connect && - SiteSetting.enable_local_logins && + !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins && self&.user_second_factors.totps.exists? end def backup_codes_enabled? - !SiteSetting.enable_discourse_connect && - SiteSetting.enable_local_logins && + !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins && self&.user_second_factors.backup_codes.exists? end def security_keys_enabled? - !SiteSetting.enable_discourse_connect && - SiteSetting.enable_local_logins && - self&.security_keys.where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true).exists? + !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins && + self + &.security_keys + .where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) + .exists? end def has_any_second_factor_methods_enabled? @@ -166,35 +168,35 @@ module SecondFactorManager security_key_credential, challenge: Webauthn.challenge(self, secure_session), rp_id: Webauthn.rp_id(self, secure_session), - origin: Discourse.base_url + origin: Discourse.base_url, ).authenticate_security_key end def invalid_totp_or_backup_code_result invalid_second_factor_authentication_result( I18n.t("login.invalid_second_factor_code"), - "invalid_second_factor" + "invalid_second_factor", ) end def invalid_security_key_result(error_message = nil) invalid_second_factor_authentication_result( error_message || I18n.t("login.invalid_security_key"), - "invalid_security_key" + "invalid_security_key", ) end def invalid_second_factor_method_result invalid_second_factor_authentication_result( I18n.t("login.invalid_second_factor_method"), - "invalid_second_factor_method" + "invalid_second_factor_method", ) end def not_enabled_second_factor_method_result invalid_second_factor_authentication_result( I18n.t("login.not_enabled_second_factor_method"), - "not_enabled_second_factor_method" + "not_enabled_second_factor_method", ) end @@ -206,22 +208,19 @@ module SecondFactorManager backup_codes_enabled?, security_keys_enabled?, totp_enabled?, - has_multiple_second_factor_methods? + has_multiple_second_factor_methods?, ) end def generate_backup_codes codes = [] - 10.times do - codes << SecureRandom.hex(16) - end + 10.times { codes << SecureRandom.hex(16) } - codes_json = codes.map do |code| - salt = SecureRandom.hex(16) - { salt: salt, - code_hash: hash_backup_code(code, salt) - } - end + codes_json = + codes.map do |code| + salt = SecureRandom.hex(16) + { salt: salt, code_hash: hash_backup_code(code, salt) } + end if self.user_second_factors.backup_codes.empty? create_backup_codes(codes_json) @@ -239,7 +238,7 @@ module SecondFactorManager user_id: self.id, data: code.to_json, enabled: true, - method: UserSecondFactor.methods[:backup_codes] + method: UserSecondFactor.methods[:backup_codes], ) end end @@ -264,10 +263,15 @@ module SecondFactorManager end def hash_backup_code(code, salt) - Pbkdf2.hash_password(code, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm) + Pbkdf2.hash_password( + code, + salt, + Rails.configuration.pbkdf2_iterations, + Rails.configuration.pbkdf2_algorithm, + ) end def require_rotp - require 'rotp' if !defined? ROTP + require "rotp" if !defined?(ROTP) end end diff --git a/app/models/concerns/stats_cacheable.rb b/app/models/concerns/stats_cacheable.rb index 31d4908097..027c02f83f 100644 --- a/app/models/concerns/stats_cacheable.rb +++ b/app/models/concerns/stats_cacheable.rb @@ -5,11 +5,11 @@ module StatsCacheable module ClassMethods def stats_cache_key - raise 'Stats cache key has not been set.' + raise "Stats cache key has not been set." end def fetch_stats - raise 'Not implemented.' + raise "Not implemented." end # Could be configurable, multisite need to support it. diff --git a/app/models/concerns/topic_tracking_state_publishable.rb b/app/models/concerns/topic_tracking_state_publishable.rb index 77dd75a841..079bda6dbe 100644 --- a/app/models/concerns/topic_tracking_state_publishable.rb +++ b/app/models/concerns/topic_tracking_state_publishable.rb @@ -4,17 +4,19 @@ module TopicTrackingStatePublishable extend ActiveSupport::Concern class_methods do - def publish_read_message(message_type:, - channel_name:, - topic_id:, - user:, - last_read_post_number:, - notification_level: nil) - - highest_post_number = DB.query_single( - "SELECT #{user.whisperer? ? "highest_staff_post_number" : "highest_post_number"} FROM topics WHERE id = ?", - topic_id - ).first + def publish_read_message( + message_type:, + channel_name:, + topic_id:, + user:, + last_read_post_number:, + notification_level: nil + ) + highest_post_number = + DB.query_single( + "SELECT #{user.whisperer? ? "highest_staff_post_number" : "highest_post_number"} FROM topics WHERE id = ?", + topic_id, + ).first message = { message_type: message_type, @@ -22,8 +24,8 @@ module TopicTrackingStatePublishable payload: { last_read_post_number: last_read_post_number, notification_level: notification_level, - highest_post_number: highest_post_number - } + highest_post_number: highest_post_number, + }, }.as_json MessageBus.publish(channel_name, message, user_ids: [user.id]) diff --git a/app/models/concerns/trashable.rb b/app/models/concerns/trashable.rb index 2a7e51fa81..cdd52c0842 100644 --- a/app/models/concerns/trashable.rb +++ b/app/models/concerns/trashable.rb @@ -7,7 +7,7 @@ module Trashable default_scope { where(deleted_at: nil) } scope :with_deleted, -> { unscope(where: :deleted_at) } - belongs_to :deleted_by, class_name: 'User' + belongs_to :deleted_by, class_name: "User" end def trashed? @@ -33,5 +33,4 @@ module Trashable def trash_update(deleted_at, deleted_by_id) self.update_columns(deleted_at: deleted_at, deleted_by_id: deleted_by_id) end - end diff --git a/app/models/developer.rb b/app/models/developer.rb index a789dd6dd6..ee25a849c9 100644 --- a/app/models/developer.rb +++ b/app/models/developer.rb @@ -6,7 +6,7 @@ class Developer < ActiveRecord::Base after_save :rebuild_cache after_destroy :rebuild_cache - @id_cache = DistributedCache.new('developer_ids') + @id_cache = DistributedCache.new("developer_ids") def self.user_ids @id_cache["ids"] || rebuild_cache diff --git a/app/models/digest_email_site_setting.rb b/app/models/digest_email_site_setting.rb index 23cc4e28ec..08055b5309 100644 --- a/app/models/digest_email_site_setting.rb +++ b/app/models/digest_email_site_setting.rb @@ -1,26 +1,23 @@ # frozen_string_literal: true class DigestEmailSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'never', value: 0 }, - { name: 'every_30_minutes', value: 30 }, - { name: 'every_hour', value: 60 }, - { name: 'daily', value: 1440 }, - { name: 'weekly', value: 10080 }, - { name: 'every_month', value: 43200 }, - { name: 'every_six_months', value: 259200 } + { name: "never", value: 0 }, + { name: "every_30_minutes", value: 30 }, + { name: "every_hour", value: 60 }, + { name: "daily", value: 1440 }, + { name: "weekly", value: 10_080 }, + { name: "every_month", value: 43_200 }, + { name: "every_six_months", value: 259_200 }, ] end def self.translate_names? true end - end diff --git a/app/models/directory_column.rb b/app/models/directory_column.rb index 3a290432c2..73a0762a1e 100644 --- a/app/models/directory_column.rb +++ b/app/models/directory_column.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DirectoryColumn < ActiveRecord::Base - # TODO(2021-06-18): Remove automatic column self.ignored_columns = ["automatic"] self.inheritance_column = nil @@ -9,17 +8,23 @@ class DirectoryColumn < ActiveRecord::Base enum type: { automatic: 0, user_field: 1, plugin: 2 }, _scopes: false def self.automatic_column_names - @automatic_column_names ||= [:likes_received, - :likes_given, - :topics_entered, - :topic_count, - :post_count, - :posts_read, - :days_visited] + @automatic_column_names ||= %i[ + likes_received + likes_given + topics_entered + topic_count + post_count + posts_read + days_visited + ] end def self.active_column_names - DirectoryColumn.where(type: [:automatic, :plugin]).where(enabled: true).pluck(:name).map(&:to_sym) + DirectoryColumn + .where(type: %i[automatic plugin]) + .where(enabled: true) + .pluck(:name) + .map(&:to_sym) end @@plugin_directory_columns = [] @@ -35,14 +40,15 @@ class DirectoryColumn < ActiveRecord::Base end def self.find_or_create_plugin_directory_column(attrs) - directory_column = find_or_create_by( - name: attrs[:column_name], - icon: attrs[:icon], - type: DirectoryColumn.types[:plugin] - ) do |column| - column.position = DirectoryColumn.maximum("position") + 1 - column.enabled = false - end + directory_column = + find_or_create_by( + name: attrs[:column_name], + icon: attrs[:icon], + type: DirectoryColumn.types[:plugin], + ) do |column| + column.position = DirectoryColumn.maximum("position") + 1 + column.enabled = false + end unless @@plugin_directory_columns.include?(directory_column.name) @@plugin_directory_columns << directory_column.name diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 817697a434..0dce208086 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -7,12 +7,7 @@ class DirectoryItem < ActiveRecord::Base @@plugin_queries = [] def self.period_types - @types ||= Enum.new(all: 1, - yearly: 2, - monthly: 3, - weekly: 4, - daily: 5, - quarterly: 6) + @types ||= Enum.new(all: 1, yearly: 2, monthly: 3, weekly: 4, daily: 5, quarterly: 6) end def self.refresh! @@ -43,36 +38,49 @@ class DirectoryItem < ActiveRecord::Base since = case period_type - when :daily then 1.day.ago - when :weekly then 1.week.ago - when :monthly then 1.month.ago - when :quarterly then 3.months.ago - when :yearly then 1.year.ago - else 1000.years.ago + when :daily + 1.day.ago + when :weekly + 1.week.ago + when :monthly + 1.month.ago + when :quarterly + 3.months.ago + when :yearly + 1.year.ago + else + 1000.years.ago end ActiveRecord::Base.transaction do # Delete records that belonged to users who have been deleted - DB.exec("DELETE FROM directory_items + DB.exec( + "DELETE FROM directory_items USING directory_items di LEFT JOIN users u ON (u.id = user_id AND u.active AND u.silenced_till IS NULL AND u.id > 0) WHERE di.id = directory_items.id AND u.id IS NULL AND - di.period_type = :period_type", period_type: period_types[period_type]) + di.period_type = :period_type", + period_type: period_types[period_type], + ) # Create new records for users who don't have one yet - column_names = DirectoryColumn.automatic_column_names + DirectoryColumn.plugin_directory_columns - DB.exec("INSERT INTO directory_items(period_type, user_id, #{column_names.map(&:to_s).join(", ")}) + column_names = + DirectoryColumn.automatic_column_names + DirectoryColumn.plugin_directory_columns + DB.exec( + "INSERT INTO directory_items(period_type, user_id, #{column_names.map(&:to_s).join(", ")}) SELECT :period_type, u.id, - #{Array.new(column_names.count) { |_| 0 }.join(", ") } + #{Array.new(column_names.count) { |_| 0 }.join(", ")} FROM users u LEFT JOIN directory_items di ON di.user_id = u.id AND di.period_type = :period_type WHERE di.id IS NULL AND u.id > 0 AND u.silenced_till IS NULL AND u.active - #{SiteSetting.must_approve_users ? 'AND u.approved' : ''} - ", period_type: period_types[period_type]) + #{SiteSetting.must_approve_users ? "AND u.approved" : ""} + ", + period_type: period_types[period_type], + ) # Calculate new values and update records # @@ -88,10 +96,11 @@ class DirectoryItem < ActiveRecord::Base was_liked_type: UserAction::WAS_LIKED, new_topic_type: UserAction::NEW_TOPIC, reply_type: UserAction::REPLY, - regular_post_type: Post.types[:regular] + regular_post_type: Post.types[:regular], } - DB.exec("WITH x AS (SELECT + DB.exec( + "WITH x AS (SELECT u.id user_id, SUM(CASE WHEN p.id IS NOT NULL AND t.id IS NOT NULL AND ua.action_type = :was_liked_type THEN 1 ELSE 0 END) likes_received, SUM(CASE WHEN p.id IS NOT NULL AND t.id IS NOT NULL AND ua.action_type = :like_type THEN 1 ELSE 0 END) likes_given, @@ -131,15 +140,12 @@ class DirectoryItem < ActiveRecord::Base di.post_count <> x.post_count ) ", - query_args - ) + query_args, + ) - @@plugin_queries.each do |plugin_query| - DB.exec(plugin_query, query_args) - end + @@plugin_queries.each { |plugin_query| DB.exec(plugin_query, query_args) } - if period_type == :all - DB.exec <<~SQL + DB.exec <<~SQL if period_type == :all UPDATE user_stats s SET likes_given = d.likes_given, likes_received = d.likes_received, @@ -155,7 +161,6 @@ class DirectoryItem < ActiveRecord::Base s.post_count <> d.post_count ) SQL - end end end end diff --git a/app/models/discourse_connect.rb b/app/models/discourse_connect.rb index ede77372f8..850784173f 100644 --- a/app/models/discourse_connect.rb +++ b/app/models/discourse_connect.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true class DiscourseConnect < DiscourseConnectBase - - class BlankExternalId < StandardError; end - class BannedExternalId < StandardError; end + class BlankExternalId < StandardError + end + class BannedExternalId < StandardError + end def self.sso_url SiteSetting.discourse_connect_url @@ -34,7 +35,11 @@ class DiscourseConnect < DiscourseConnectBase if SiteSetting.discourse_connect_csrf_protection @secure_session.set(nonce_key, return_path, expires: DiscourseConnectBase.nonce_expiry_time) else - Discourse.cache.write(nonce_key, return_path, expires_in: DiscourseConnectBase.nonce_expiry_time) + Discourse.cache.write( + nonce_key, + return_path, + expires_in: DiscourseConnectBase.nonce_expiry_time, + ) end end end @@ -73,7 +78,11 @@ class DiscourseConnect < DiscourseConnectBase Discourse.cache.delete nonce_key end - Discourse.cache.write(used_nonce_key, return_path, expires_in: DiscourseConnectBase.used_nonce_expiry_time) + Discourse.cache.write( + used_nonce_key, + return_path, + expires_in: DiscourseConnectBase.used_nonce_expiry_time, + ) end end @@ -85,20 +94,15 @@ class DiscourseConnect < DiscourseConnectBase "USED_SSO_NONCE_#{nonce}" end - BANNED_EXTERNAL_IDS = %w{none nil blank null} + BANNED_EXTERNAL_IDS = %w[none nil blank null] def lookup_or_create_user(ip_address = nil) - # we don't want to ban 0 from being an external id external_id = self.external_id.to_s - if external_id.blank? - raise BlankExternalId - end + raise BlankExternalId if external_id.blank? - if BANNED_EXTERNAL_IDS.include?(external_id.downcase) - raise BannedExternalId, external_id - end + raise BannedExternalId, external_id if BANNED_EXTERNAL_IDS.include?(external_id.downcase) # we protect here to ensure there is no situation where the same external id # concurrently attempts to create or update sso records @@ -130,13 +134,11 @@ class DiscourseConnect < DiscourseConnectBase if sso_record && (user = sso_record.user) && !user.active && !require_activation user.active = true user.save! - user.enqueue_welcome_message('welcome_user') unless suppress_welcome_message + user.enqueue_welcome_message("welcome_user") unless suppress_welcome_message user.set_automatic_groups end - custom_fields.each do |k, v| - user.custom_fields[k] = v - end + custom_fields.each { |k, v| user.custom_fields[k] = v } user.ip_address = ip_address @@ -149,9 +151,7 @@ class DiscourseConnect < DiscourseConnectBase user.user_avatar.save! if user.user_avatar user.save! - if @email_changed && user.active - user.set_automatic_groups - end + user.set_automatic_groups if @email_changed && user.active # The user might require approval user.create_reviewable @@ -177,9 +177,7 @@ class DiscourseConnect < DiscourseConnectBase sso_record.save! - if sso_record.user - apply_group_rules(sso_record.user) - end + apply_group_rules(sso_record.user) if sso_record.user sso_record && sso_record.user end @@ -188,7 +186,7 @@ class DiscourseConnect < DiscourseConnectBase names = (groups || "").split(",").map(&:downcase) current_groups = user.groups.where(automatic: false) - desired_groups = Group.where('LOWER(NAME) in (?) AND NOT automatic', names) + desired_groups = Group.where("LOWER(NAME) in (?) AND NOT automatic", names) to_be_added = desired_groups if current_groups.present? @@ -218,7 +216,7 @@ class DiscourseConnect < DiscourseConnectBase if add_groups split = add_groups.split(",").map(&:downcase) if split.length > 0 - to_be_added = Group.where('LOWER(name) in (?) AND NOT automatic', split) + to_be_added = Group.where("LOWER(name) in (?) AND NOT automatic", split) if already_member = GroupUser.where(user_id: user.id).pluck(:group_id).presence to_be_added = to_be_added.where("id NOT IN (?)", already_member) end @@ -229,10 +227,11 @@ class DiscourseConnect < DiscourseConnectBase if remove_groups split = remove_groups.split(",").map(&:downcase) if split.length > 0 - to_be_removed = Group - .joins(:group_users) - .where(automatic: false, group_users: { user_id: user.id }) - .where("LOWER(name) IN (?)", split) + to_be_removed = + Group + .joins(:group_users) + .where(automatic: false, group_users: { user_id: user.id }) + .where("LOWER(name) IN (?)", split) end end @@ -255,7 +254,7 @@ class DiscourseConnect < DiscourseConnectBase primary_email: UserEmail.new(email: email, primary: true), name: resolve_name, username: resolve_username, - ip_address: ip_address + ip_address: ip_address, } if SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale) @@ -271,7 +270,9 @@ class DiscourseConnect < DiscourseConnectBase user.save! if SiteSetting.verbose_discourse_connect_logging - Rails.logger.warn("Verbose SSO log: New User (user_id: #{user.id}) Params: #{user_params} User Params: #{user.attributes} User Errors: #{user.errors.full_messages} Email: #{user.primary_email.attributes} Email Error: #{user.primary_email.errors.full_messages}") + Rails.logger.warn( + "Verbose SSO log: New User (user_id: #{user.id}) Params: #{user_params} User Params: #{user.attributes} User Errors: #{user.errors.full_messages} Email: #{user.primary_email.attributes} Email Error: #{user.primary_email.errors.full_messages}", + ) end end @@ -281,26 +282,29 @@ class DiscourseConnect < DiscourseConnectBase sso_record.external_id = external_id else if avatar_url.present? - Jobs.enqueue(:download_avatar_from_url, + Jobs.enqueue( + :download_avatar_from_url, url: avatar_url, user_id: user.id, - override_gravatar: SiteSetting.discourse_connect_overrides_avatar + override_gravatar: SiteSetting.discourse_connect_overrides_avatar, ) end if profile_background_url.present? - Jobs.enqueue(:download_profile_background_from_url, + Jobs.enqueue( + :download_profile_background_from_url, url: profile_background_url, user_id: user.id, - is_card_background: false + is_card_background: false, ) end if card_background_url.present? - Jobs.enqueue(:download_profile_background_from_url, + Jobs.enqueue( + :download_profile_background_from_url, url: card_background_url, user_id: user.id, - is_card_background: true + is_card_background: true, ) end @@ -312,7 +316,7 @@ class DiscourseConnect < DiscourseConnectBase external_name: name, external_avatar_url: avatar_url, external_profile_background_url: profile_background_url, - external_card_background_url: card_background_url + external_card_background_url: card_background_url, ) end end @@ -338,44 +342,58 @@ class DiscourseConnect < DiscourseConnectBase user.name = name || User.suggest_name(username.blank? ? email : username) end - if locale_force_update && SiteSetting.allow_user_locale && locale && LocaleSiteSetting.valid_value?(locale) + if locale_force_update && SiteSetting.allow_user_locale && locale && + LocaleSiteSetting.valid_value?(locale) user.locale = locale end avatar_missing = user.uploaded_avatar_id.nil? || !Upload.exists?(user.uploaded_avatar_id) - if (avatar_missing || avatar_force_update || SiteSetting.discourse_connect_overrides_avatar) && avatar_url.present? + if (avatar_missing || avatar_force_update || SiteSetting.discourse_connect_overrides_avatar) && + avatar_url.present? avatar_changed = sso_record.external_avatar_url != avatar_url if avatar_force_update || avatar_changed || avatar_missing - Jobs.enqueue(:download_avatar_from_url, url: avatar_url, user_id: user.id, override_gravatar: SiteSetting.discourse_connect_overrides_avatar) + Jobs.enqueue( + :download_avatar_from_url, + url: avatar_url, + user_id: user.id, + override_gravatar: SiteSetting.discourse_connect_overrides_avatar, + ) end end if profile_background_url.present? - profile_background_missing = user.user_profile.profile_background_upload.blank? || Upload.get_from_url(user.user_profile.profile_background_upload.url).blank? + profile_background_missing = + user.user_profile.profile_background_upload.blank? || + Upload.get_from_url(user.user_profile.profile_background_upload.url).blank? if profile_background_missing || SiteSetting.discourse_connect_overrides_profile_background - profile_background_changed = sso_record.external_profile_background_url != profile_background_url + profile_background_changed = + sso_record.external_profile_background_url != profile_background_url if profile_background_changed || profile_background_missing - Jobs.enqueue(:download_profile_background_from_url, - url: profile_background_url, - user_id: user.id, - is_card_background: false + Jobs.enqueue( + :download_profile_background_from_url, + url: profile_background_url, + user_id: user.id, + is_card_background: false, ) end end end if card_background_url.present? - card_background_missing = user.user_profile.card_background_upload.blank? || Upload.get_from_url(user.user_profile.card_background_upload.url).blank? + card_background_missing = + user.user_profile.card_background_upload.blank? || + Upload.get_from_url(user.user_profile.card_background_upload.url).blank? if card_background_missing || SiteSetting.discourse_connect_overrides_card_background card_background_changed = sso_record.external_card_background_url != card_background_url if card_background_changed || card_background_missing - Jobs.enqueue(:download_profile_background_from_url, - url: card_background_url, - user_id: user.id, - is_card_background: true + Jobs.enqueue( + :download_profile_background_from_url, + url: card_background_url, + user_id: user.id, + is_card_background: true, ) end end diff --git a/app/models/do_not_disturb_timing.rb b/app/models/do_not_disturb_timing.rb index 62d7d65cbd..56bbb6524e 100644 --- a/app/models/do_not_disturb_timing.rb +++ b/app/models/do_not_disturb_timing.rb @@ -6,9 +6,7 @@ class DoNotDisturbTiming < ActiveRecord::Base validate :ends_at_greater_than_starts_at def ends_at_greater_than_starts_at - if starts_at > ends_at - errors.add(:ends_at, :invalid) - end + errors.add(:ends_at, :invalid) if starts_at > ends_at end end diff --git a/app/models/draft.rb b/app/models/draft.rb index ced0d4df5b..a9dff7f277 100644 --- a/app/models/draft.rb +++ b/app/models/draft.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true class Draft < ActiveRecord::Base - NEW_TOPIC ||= 'new_topic' - NEW_PRIVATE_MESSAGE ||= 'new_private_message' - EXISTING_TOPIC ||= 'topic_' + NEW_TOPIC ||= "new_topic" + NEW_PRIVATE_MESSAGE ||= "new_private_message" + EXISTING_TOPIC ||= "topic_" belongs_to :user - after_commit :update_draft_count, on: [:create, :destroy] + after_commit :update_draft_count, on: %i[create destroy] - class OutOfSequence < StandardError; end + class OutOfSequence < StandardError + end def self.set(user, key, sequence, data, owner = nil, force_save: false) return 0 if !User.human_user_id?(user.id) force_save = force_save.to_s == "true" - if SiteSetting.backup_drafts_to_pm_length > 0 && SiteSetting.backup_drafts_to_pm_length < data.length + if SiteSetting.backup_drafts_to_pm_length > 0 && + SiteSetting.backup_drafts_to_pm_length < data.length backup_draft(user, key, sequence, data) end @@ -44,21 +46,16 @@ class Draft < ActiveRecord::Base current_sequence ||= 0 if draft_id - if !force_save && (current_sequence != sequence) - raise Draft::OutOfSequence - end + raise Draft::OutOfSequence if !force_save && (current_sequence != sequence) sequence = current_sequence if force_save sequence += 1 # we need to keep upping our sequence on every save # if we do not do that there are bad race conditions - DraftSequence.upsert({ - sequence: sequence, - draft_key: key, - user_id: user.id, - }, - unique_by: [:user_id, :draft_key] + DraftSequence.upsert( + { sequence: sequence, draft_key: key, user_id: user.id }, + unique_by: %i[user_id draft_key], ) DB.exec(<<~SQL, id: draft_id, sequence: sequence, data: data, owner: owner || current_owner) @@ -70,17 +67,10 @@ class Draft < ActiveRecord::Base , updated_at = CURRENT_TIMESTAMP WHERE id = :id SQL - elsif sequence != current_sequence raise Draft::OutOfSequence else - opts = { - user_id: user.id, - draft_key: key, - data: data, - sequence: sequence, - owner: owner - } + opts = { user_id: user.id, draft_key: key, data: data, sequence: sequence, owner: owner } draft_id = DB.query_single(<<~SQL, opts).first INSERT INTO drafts (user_id, draft_key, data, sequence, owner, created_at, updated_at) @@ -101,8 +91,8 @@ class Draft < ActiveRecord::Base UploadReference.ensure_exist!( upload_ids: Upload.extract_upload_ids(data), - target_type: 'Draft', - target_id: draft_id + target_type: "Draft", + target_id: draft_id, ) sequence @@ -111,11 +101,7 @@ class Draft < ActiveRecord::Base def self.get(user, key, sequence) return if !user || !user.id || !User.human_user_id?(user.id) - opts = { - user_id: user.id, - draft_key: key, - sequence: sequence - } + opts = { user_id: user.id, draft_key: key, sequence: sequence } current_sequence, data, draft_sequence = DB.query_single(<<~SQL, opts) WITH draft AS ( @@ -136,9 +122,7 @@ class Draft < ActiveRecord::Base current_sequence ||= 0 - if sequence != current_sequence - raise Draft::OutOfSequence - end + raise Draft::OutOfSequence if sequence != current_sequence data if current_sequence == draft_sequence end @@ -155,9 +139,7 @@ class Draft < ActiveRecord::Base current_sequence = DraftSequence.current(user, key) # bad caller is a reason to complain - if sequence != current_sequence - raise Draft::OutOfSequence - end + raise Draft::OutOfSequence if sequence != current_sequence # corrupt data is not a reason not to leave data Draft.where(user_id: user.id, draft_key: key).destroy_all @@ -176,9 +158,7 @@ class Draft < ActiveRecord::Base end def topic_id - if draft_key.starts_with?(EXISTING_TOPIC) - draft_key.gsub(EXISTING_TOPIC, "").to_i - end + draft_key.gsub(EXISTING_TOPIC, "").to_i if draft_key.starts_with?(EXISTING_TOPIC) end def topic_preloaded? @@ -186,7 +166,9 @@ class Draft < ActiveRecord::Base end def topic - topic_preloaded? ? @topic : @topic = Draft.allowed_draft_topics_for_user(user).find_by(id: topic_id) + topic_preloaded? ? + @topic : + @topic = Draft.allowed_draft_topics_for_user(user).find_by(id: topic_id) end def preload_topic(topic) @@ -240,10 +222,7 @@ class Draft < ActiveRecord::Base offset = (opts[:offset] || 0).to_i limit = (opts[:limit] || 30).to_i - stream = Draft.where(user_id: user_id) - .order(updated_at: :desc) - .offset(offset) - .limit(limit) + stream = Draft.where(user_id: user_id).order(updated_at: :desc).offset(offset).limit(limit) # Preload posts and topics to avoid N+1 queries Draft.preload_data(stream, opts[:user]) @@ -276,13 +255,9 @@ class Draft < ActiveRecord::Base post_id = BackupDraftPost.where(user_id: user.id, key: key).pluck_first(:post_id) post = Post.where(id: post_id).first if post_id - if post_id && !post - BackupDraftPost.where(user_id: user.id, key: key).delete_all - end + BackupDraftPost.where(user_id: user.id, key: key).delete_all if post_id && !post - indented_reply = reply.split("\n").map! do |l| - " #{l}" - end + indented_reply = reply.split("\n").map! { |l| " #{l}" } draft_body = <<~MD #{indented_reply.join("\n")} @@ -297,13 +272,14 @@ class Draft < ActiveRecord::Base if !post topic = ensure_draft_topic!(user) Post.transaction do - post = PostCreator.new( - user, - raw: draft_body, - skip_jobs: true, - skip_validations: true, - topic_id: topic.id, - ).create + post = + PostCreator.new( + user, + raw: draft_body, + skip_jobs: true, + skip_validations: true, + topic_id: topic.id, + ).create BackupDraftPost.create!(user_id: user.id, key: key, post_id: post.id) end elsif post.last_version_at > 5.minutes.ago @@ -311,18 +287,19 @@ class Draft < ActiveRecord::Base post.update_columns( raw: draft_body, cooked: PrettyText.cook(draft_body), - updated_at: Time.zone.now + updated_at: Time.zone.now, ) else revisor = PostRevisor.new(post, post.topic) - revisor.revise!(user, { raw: draft_body }, + revisor.revise!( + user, + { raw: draft_body }, bypass_bump: true, skip_validations: true, skip_staff_log: true, - bypass_rate_limiter: true + bypass_rate_limiter: true, ) end - rescue => e Discourse.warn_exception(e, message: "Failed to backup draft") end @@ -331,28 +308,26 @@ class Draft < ActiveRecord::Base topic_id = BackupDraftTopic.where(user_id: user.id).pluck_first(:topic_id) topic = Topic.find_by(id: topic_id) if topic_id - if topic_id && !topic - BackupDraftTopic.where(user_id: user.id).delete_all - end + BackupDraftTopic.where(user_id: user.id).delete_all if topic_id && !topic if !topic Topic.transaction do - creator = PostCreator.new( - user, - title: I18n.t("draft_backup.pm_title"), - archetype: Archetype.private_message, - raw: I18n.t("draft_backup.pm_body"), - skip_jobs: true, - skip_validations: true, - target_usernames: user.username - ) + creator = + PostCreator.new( + user, + title: I18n.t("draft_backup.pm_title"), + archetype: Archetype.private_message, + raw: I18n.t("draft_backup.pm_body"), + skip_jobs: true, + skip_validations: true, + target_usernames: user.username, + ) topic = creator.create.topic BackupDraftTopic.create!(topic_id: topic.id, user_id: user.id) end end topic - end def update_draft_count diff --git a/app/models/draft_sequence.rb b/app/models/draft_sequence.rb index cfe1a27bef..3570df5aa3 100644 --- a/app/models/draft_sequence.rb +++ b/app/models/draft_sequence.rb @@ -9,8 +9,7 @@ class DraftSequence < ActiveRecord::Base return 0 if !User.human_user_id?(user_id) - sequence = - DB.query_single(<<~SQL, user_id: user_id, draft_key: key).first + sequence = DB.query_single(<<~SQL, user_id: user_id, draft_key: key).first INSERT INTO draft_sequences (user_id, draft_key, sequence) VALUES (:user_id, :draft_key, 1) ON CONFLICT (user_id, draft_key) DO @@ -21,7 +20,12 @@ class DraftSequence < ActiveRecord::Base RETURNING sequence SQL - DB.exec("DELETE FROM drafts WHERE user_id = :user_id AND draft_key = :draft_key AND sequence < :sequence", draft_key: key, user_id: user_id, sequence: sequence) + DB.exec( + "DELETE FROM drafts WHERE user_id = :user_id AND draft_key = :draft_key AND sequence < :sequence", + draft_key: key, + user_id: user_id, + sequence: sequence, + ) UserStat.update_draft_count(user_id) @@ -37,7 +41,12 @@ class DraftSequence < ActiveRecord::Base return 0 if !User.human_user_id?(user_id) # perf critical path - r, _ = DB.query_single('select sequence from draft_sequences where user_id = ? and draft_key = ?', user_id, key) + r, _ = + DB.query_single( + "select sequence from draft_sequences where user_id = ? and draft_key = ?", + user_id, + key, + ) r.to_i end end diff --git a/app/models/email_change_request.rb b/app/models/email_change_request.rb index 44ac085c2d..e0e175e033 100644 --- a/app/models/email_change_request.rb +++ b/app/models/email_change_request.rb @@ -2,8 +2,8 @@ class EmailChangeRequest < ActiveRecord::Base belongs_to :user - belongs_to :old_email_token, class_name: 'EmailToken', dependent: :destroy - belongs_to :new_email_token, class_name: 'EmailToken', dependent: :destroy + belongs_to :old_email_token, class_name: "EmailToken", dependent: :destroy + belongs_to :new_email_token, class_name: "EmailToken", dependent: :destroy belongs_to :requested_by, class_name: "User", foreign_key: :requested_by_user_id validates :new_email, presence: true, format: { with: EmailAddressValidator.email_regex } @@ -14,7 +14,9 @@ class EmailChangeRequest < ActiveRecord::Base def self.find_by_new_token(token) EmailChangeRequest - .joins("INNER JOIN email_tokens ON email_tokens.id = email_change_requests.new_email_token_id") + .joins( + "INNER JOIN email_tokens ON email_tokens.id = email_change_requests.new_email_token_id", + ) .where("email_tokens.token_hash = ?", EmailToken.hash_token(token)) .last end diff --git a/app/models/email_level_site_setting.rb b/app/models/email_level_site_setting.rb index 9f805dd253..9d5131e50c 100644 --- a/app/models/email_level_site_setting.rb +++ b/app/models/email_level_site_setting.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true class EmailLevelSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.email_level.always', value: 0 }, - { name: 'user.email_level.only_when_away', value: 1 }, - { name: 'user.email_level.never', value: 2 }, + { name: "user.email_level.always", value: 0 }, + { name: "user.email_level.only_when_away", value: 1 }, + { name: "user.email_level.never", value: 2 }, ] end def self.translate_names? true end - end diff --git a/app/models/email_log.rb b/app/models/email_log.rb index b5ab6d6402..c3dc39e036 100644 --- a/app/models/email_log.rb +++ b/app/models/email_log.rb @@ -1,32 +1,32 @@ # frozen_string_literal: true class EmailLog < ActiveRecord::Base - CRITICAL_EMAIL_TYPES ||= Set.new %w{ - account_created - admin_login - confirm_new_email - confirm_old_email - confirm_old_email_add - forgot_password - notify_old_email - notify_old_email_add - signup - signup_after_approval - } + CRITICAL_EMAIL_TYPES ||= + Set.new %w[ + account_created + admin_login + confirm_new_email + confirm_old_email + confirm_old_email_add + forgot_password + notify_old_email + notify_old_email_add + signup + signup_after_approval + ] # cf. https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml SMTP_ERROR_CODE_REGEXP = Regexp.new(/\d\.\d\.\d+|\d{3}/).freeze belongs_to :user belongs_to :post - belongs_to :smtp_group, class_name: 'Group' + belongs_to :smtp_group, class_name: "Group" validates :email_type, :to_address, presence: true scope :bounced, -> { where(bounced: true) } - scope :addressed_to_user, ->(user) do - where(<<~SQL, user_id: user.id) + scope :addressed_to_user, ->(user) { where(<<~SQL, user_id: user.id) } EXISTS( SELECT 1 FROM user_emails @@ -35,7 +35,6 @@ class EmailLog < ActiveRecord::Base email_logs.cc_addresses ILIKE '%' || user_emails.email || '%') ) SQL - end before_save do if self.bounce_error_code.present? @@ -66,11 +65,11 @@ class EmailLog < ActiveRecord::Base end def self.reached_max_emails?(user, email_type = nil) - return false if SiteSetting.max_emails_per_day_per_user == 0 || CRITICAL_EMAIL_TYPES.include?(email_type) + if SiteSetting.max_emails_per_day_per_user == 0 || CRITICAL_EMAIL_TYPES.include?(email_type) + return false + end - count = where('created_at > ?', 1.day.ago) - .where(user_id: user.id) - .count + count = where("created_at > ?", 1.day.ago).where(user_id: user.id).count count >= SiteSetting.max_emails_per_day_per_user end @@ -87,15 +86,11 @@ class EmailLog < ActiveRecord::Base end def self.last_sent_email_address - self.where(email_type: "signup") - .order(created_at: :desc) - .limit(1) - .pluck(:to_address) - .first + self.where(email_type: "signup").order(created_at: :desc).limit(1).pluck(:to_address).first end def bounce_key - super&.delete('-') + super&.delete("-") end def cc_users diff --git a/app/models/email_style.rb b/app/models/email_style.rb index 9aeb483f01..88b4b7692a 100644 --- a/app/models/email_style.rb +++ b/app/models/email_style.rb @@ -6,7 +6,7 @@ class EmailStyle attr_accessor :html, :css, :default_html, :default_css def id - 'email-style' + "email-style" end def html @@ -30,12 +30,11 @@ class EmailStyle end def self.default_template - @_default_template ||= File.read( - File.join(Rails.root, 'app', 'views', 'email', 'default_template.html') - ) + @_default_template ||= + File.read(File.join(Rails.root, "app", "views", "email", "default_template.html")) end def self.default_css - '' + "" end end diff --git a/app/models/email_token.rb b/app/models/email_token.rb index 8cd7dbdd03..7a04e7c229 100644 --- a/app/models/email_token.rb +++ b/app/models/email_token.rb @@ -1,14 +1,21 @@ # frozen_string_literal: true class EmailToken < ActiveRecord::Base - class TokenAccessError < StandardError; end + class TokenAccessError < StandardError + end belongs_to :user validates :user_id, :email, :token_hash, presence: true scope :unconfirmed, -> { where(confirmed: false) } - scope :active, -> { where(expired: false).where('created_at >= ?', SiteSetting.email_token_valid_hours.hours.ago) } + scope :active, + -> { + where(expired: false).where( + "created_at >= ?", + SiteSetting.email_token_valid_hours.hours.ago, + ) + } after_initialize do if self.token_hash.blank? @@ -25,9 +32,7 @@ class EmailToken < ActiveRecord::Base .update_all(expired: true) end - before_validation do - self.email = self.email.downcase if self.email - end + before_validation { self.email = self.email.downcase if self.email } before_save do if self.scope.blank? @@ -36,15 +41,10 @@ class EmailToken < ActiveRecord::Base end # TODO(2022-01-01): Remove - self.ignored_columns = %w{token} + self.ignored_columns = %w[token] def self.scopes - @scopes ||= Enum.new( - signup: 1, - password_reset: 2, - email_login: 3, - email_update: 4, - ) + @scopes ||= Enum.new(signup: 1, password_reset: 2, email_login: 3, email_update: 4) end def token @@ -64,7 +64,7 @@ class EmailToken < ActiveRecord::Base user.send_welcome_message = !user.active? user.email = email_token.email user.active = true - user.custom_fields.delete('activation_reminder') + user.custom_fields.delete("activation_reminder") user.save! user.create_reviewable if !skip_reviewable user.set_automatic_groups @@ -80,9 +80,7 @@ class EmailToken < ActiveRecord::Base def self.confirmable(token, scope: nil) return nil if token.blank? - relation = unconfirmed.active - .includes(:user) - .where(token_hash: hash_token(token)) + relation = unconfirmed.active.includes(:user).where(token_hash: hash_token(token)) # TODO(2022-01-01): All email tokens should have scopes by now if !scope @@ -98,7 +96,7 @@ class EmailToken < ActiveRecord::Base type: "signup", user_id: email_token.user_id, email_token: email_token.token, - to_address: to_address + to_address: to_address, ) end diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb index f638ff235a..b9c63aff5b 100644 --- a/app/models/embeddable_host.rb +++ b/app/models/embeddable_host.rb @@ -6,8 +6,8 @@ class EmbeddableHost < ActiveRecord::Base after_destroy :reset_embedding_settings before_validation do - self.host.sub!(/^https?:\/\//, '') - self.host.sub!(/\/.*$/, '') + self.host.sub!(%r{^https?://}, "") + self.host.sub!(%r{/.*$}, "") end # TODO(2021-07-23): Remove @@ -15,10 +15,11 @@ class EmbeddableHost < ActiveRecord::Base def self.record_for_url(uri) if uri.is_a?(String) - uri = begin - URI(UrlHelper.normalized_encode(uri)) - rescue URI::Error, Addressable::URI::InvalidURIError - end + uri = + begin + URI(UrlHelper.normalized_encode(uri)) + rescue URI::Error, Addressable::URI::InvalidURIError + end end return false unless uri.present? @@ -26,9 +27,7 @@ class EmbeddableHost < ActiveRecord::Base host = uri.host return false unless host.present? - if uri.port.present? && uri.port != 80 && uri.port != 443 - host << ":#{uri.port}" - end + host << ":#{uri.port}" if uri.port.present? && uri.port != 80 && uri.port != 443 path = uri.path path << "?" << uri.query if uri.query.present? @@ -49,10 +48,11 @@ class EmbeddableHost < ActiveRecord::Base # Work around IFRAME reload on WebKit where the referer will be set to the Forum URL return true if url&.starts_with?(Discourse.base_url) && EmbeddableHost.exists? - uri = begin - URI(UrlHelper.normalized_encode(url)) - rescue URI::Error - end + uri = + begin + URI(UrlHelper.normalized_encode(url)) + rescue URI::Error + end uri.present? && record_for_url(uri).present? end @@ -67,9 +67,9 @@ class EmbeddableHost < ActiveRecord::Base def host_must_be_valid if host !~ /\A[a-z0-9]+([\-\.]+{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i && - host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ && - host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i - errors.add(:host, I18n.t('errors.messages.invalid')) + host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ && + host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i + errors.add(:host, I18n.t("errors.messages.invalid")) end end end diff --git a/app/models/embedding.rb b/app/models/embedding.rb index 909ba1c272..cf6585130e 100644 --- a/app/models/embedding.rb +++ b/app/models/embedding.rb @@ -1,19 +1,21 @@ # frozen_string_literal: true -require 'has_errors' +require "has_errors" class Embedding < OpenStruct include HasErrors def self.settings - %i(embed_by_username - embed_post_limit - embed_title_scrubber - embed_truncate - embed_unlisted - allowed_embed_selectors - blocked_embed_selectors - allowed_embed_classnames) + %i[ + embed_by_username + embed_post_limit + embed_title_scrubber + embed_truncate + embed_unlisted + allowed_embed_selectors + blocked_embed_selectors + allowed_embed_classnames + ] end def base_url @@ -21,9 +23,7 @@ class Embedding < OpenStruct end def save - Embedding.settings.each do |s| - SiteSetting.set(s, public_send(s)) - end + Embedding.settings.each { |s| SiteSetting.set(s, public_send(s)) } true rescue Discourse::InvalidParameters => p errors.add :base, p.to_s @@ -35,7 +35,7 @@ class Embedding < OpenStruct end def self.find - embedding_args = { id: 'default' } + embedding_args = { id: "default" } Embedding.settings.each { |s| embedding_args[s] = SiteSetting.get(s) } Embedding.new(embedding_args) end diff --git a/app/models/emoji.rb b/app/models/emoji.rb index 750be81283..94b85eb154 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -4,7 +4,7 @@ class Emoji # update this to clear the cache EMOJI_VERSION = "12" - FITZPATRICK_SCALE ||= [ "1f3fb", "1f3fc", "1f3fd", "1f3fe", "1f3ff" ] + FITZPATRICK_SCALE ||= %w[1f3fb 1f3fc 1f3fd 1f3fe 1f3ff] DEFAULT_GROUP ||= "default" @@ -29,11 +29,11 @@ class Emoji end def self.aliases - db['aliases'] + db["aliases"] end def self.search_aliases - db['searchAliases'] + db["searchAliases"] end def self.translations @@ -45,11 +45,11 @@ class Emoji end def self.tonable_emojis - db['tonableEmojis'] + db["tonableEmojis"] end def self.custom?(name) - name = name.delete_prefix(':').delete_suffix(':') + name = name.delete_prefix(":").delete_suffix(":") Emoji.custom.detect { |e| e.name == name }.present? end @@ -58,29 +58,28 @@ class Emoji end def self.[](name) - name = name.delete_prefix(':').delete_suffix(':') + name = name.delete_prefix(":").delete_suffix(":") is_toned = name.match?(/.+:t[1-6]/) normalized_name = name.gsub(/(.+):t[1-6]/, '\1') found_emoji = nil [[global_emoji_cache, :standard], [site_emoji_cache, :custom]].each do |cache, list_key| - cache_postfix, found_emoji = cache.defer_get_set(normalized_name) do - emoji = Emoji.public_send(list_key).detect do |e| - e.name == normalized_name && - (!is_toned || (is_toned && e.tonable)) + cache_postfix, found_emoji = + cache.defer_get_set(normalized_name) do + emoji = + Emoji + .public_send(list_key) + .detect { |e| e.name == normalized_name && (!is_toned || (is_toned && e.tonable)) } + [self.cache_postfix, emoji] end - [self.cache_postfix, emoji] - end if found_emoji && (cache_postfix != self.cache_postfix) cache.delete(normalized_name) redo end - if found_emoji - break - end + break if found_emoji end found_emoji @@ -89,7 +88,7 @@ class Emoji def self.create_from_db_item(emoji) name = emoji["name"] return unless group = groups[name] - filename = emoji['filename'] || name + filename = emoji["filename"] || name Emoji.new.tap do |e| e.name = name @@ -101,7 +100,7 @@ class Emoji end def self.url_for(name) - name = name.delete_prefix(':').delete_suffix(':').gsub(/(.+):t([1-6])/, '\1/\2') + name = name.delete_prefix(":").delete_suffix(":").gsub(/(.+):t([1-6])/, '\1/\2') if SiteSetting.external_emoji_url.blank? "#{Discourse.base_path}/images/emoji/#{SiteSetting.emoji_set}/#{name}.png?v=#{EMOJI_VERSION}" else @@ -118,7 +117,7 @@ class Emoji end def self.clear_cache - %w{custom standard translations all}.each do |key| + %w[custom standard translations all].each do |key| Discourse.cache.delete(cache_key("#{key}_emojis")) end global_emoji_cache.clear @@ -130,17 +129,16 @@ class Emoji end def self.groups - @groups ||= begin - groups = {} + @groups ||= + begin + groups = {} - File.open(groups_file, "r:UTF-8") { |f| JSON.parse(f.read) }.each do |group| - group["icons"].each do |icon| - groups[icon["name"]] = group["name"] - end + File + .open(groups_file, "r:UTF-8") { |f| JSON.parse(f.read) } + .each { |group| group["icons"].each { |icon| groups[icon["name"]] = group["name"] } } + + groups end - - groups - end end def self.db_file @@ -152,27 +150,30 @@ class Emoji end def self.load_standard - db['emojis'].map { |e| Emoji.create_from_db_item(e) }.compact + db["emojis"].map { |e| Emoji.create_from_db_item(e) }.compact end def self.load_custom result = [] if !GlobalSetting.skip_db? - CustomEmoji.includes(:upload).order(:name).each do |emoji| - result << Emoji.new.tap do |e| - e.name = emoji.name - e.url = emoji.upload&.url - e.group = emoji.group || DEFAULT_GROUP + CustomEmoji + .includes(:upload) + .order(:name) + .each do |emoji| + result << Emoji.new.tap do |e| + e.name = emoji.name + e.url = emoji.upload&.url + e.group = emoji.group || DEFAULT_GROUP + end end - end end Plugin::CustomEmoji.emojis.each do |group, emojis| emojis.each do |name, url| result << Emoji.new.tap do |e| e.name = name - url = (Discourse.base_path + url) if url[/^\/[^\/]/] + url = (Discourse.base_path + url) if url[%r{^/[^/]}] e.url = url e.group = group || DEFAULT_GROUP end @@ -196,47 +197,45 @@ class Emoji end def self.replacement_code(code) - code - .split('-') - .map!(&:hex) - .pack("U*") + code.split("-").map!(&:hex).pack("U*") end def self.unicode_replacements - @unicode_replacements ||= begin - replacements = {} - is_tonable_emojis = Emoji.tonable_emojis - fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) } + @unicode_replacements ||= + begin + replacements = {} + is_tonable_emojis = Emoji.tonable_emojis + fitzpatrick_scales = FITZPATRICK_SCALE.map { |scale| scale.to_i(16) } - db['emojis'].each do |e| - name = e['name'] + db["emojis"].each do |e| + name = e["name"] - # special cased as we prefer to keep these as symbols - next if name == 'registered' - next if name == 'copyright' - next if name == 'tm' - next if name == 'left_right_arrow' + # special cased as we prefer to keep these as symbols + next if name == "registered" + next if name == "copyright" + next if name == "tm" + next if name == "left_right_arrow" - code = replacement_code(e['code']) - next unless code + code = replacement_code(e["code"]) + next unless code - replacements[code] = name - if is_tonable_emojis.include?(name) - fitzpatrick_scales.each_with_index do |scale, index| - toned_code = code.codepoints.insert(1, scale).pack("U*") - replacements[toned_code] = "#{name}:t#{index + 2}" + replacements[code] = name + if is_tonable_emojis.include?(name) + fitzpatrick_scales.each_with_index do |scale, index| + toned_code = code.codepoints.insert(1, scale).pack("U*") + replacements[toned_code] = "#{name}:t#{index + 2}" + end end end + + replacements["\u{2639}"] = "frowning" + replacements["\u{263B}"] = "slight_smile" + replacements["\u{2661}"] = "heart" + replacements["\u{2665}"] = "heart" + replacements["\u{263A}"] = "relaxed" + + replacements end - - replacements["\u{2639}"] = 'frowning' - replacements["\u{263B}"] = 'slight_smile' - replacements["\u{2661}"] = 'heart' - replacements["\u{2665}"] = 'heart' - replacements["\u{263A}"] = 'relaxed' - - replacements - end end def self.unicode_unescape(string) @@ -244,40 +243,37 @@ class Emoji end def self.gsub_emoji_to_unicode(str) - if str - str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } - end + str.gsub(/:([\w\-+]*(?::t\d)?):/) { |name| Emoji.lookup_unicode($1) || name } if str end def self.lookup_unicode(name) - @reverse_map ||= begin - map = {} - is_tonable_emojis = Emoji.tonable_emojis + @reverse_map ||= + begin + map = {} + is_tonable_emojis = Emoji.tonable_emojis - db['emojis'].each do |e| - next if e['name'] == 'tm' + db["emojis"].each do |e| + next if e["name"] == "tm" - code = replacement_code(e['code']) - next unless code + code = replacement_code(e["code"]) + next unless code - map[e['name']] = code - if is_tonable_emojis.include?(e['name']) - FITZPATRICK_SCALE.each_with_index do |scale, index| - toned_code = (code.codepoints.insert(1, scale.to_i(16))).pack("U*") - map["#{e['name']}:t#{index + 2}"] = toned_code + map[e["name"]] = code + if is_tonable_emojis.include?(e["name"]) + FITZPATRICK_SCALE.each_with_index do |scale, index| + toned_code = (code.codepoints.insert(1, scale.to_i(16))).pack("U*") + map["#{e["name"]}:t#{index + 2}"] = toned_code + end end end - end - Emoji.aliases.each do |key, alias_names| - next unless alias_code = map[key] - alias_names.each do |alias_name| - map[alias_name] = alias_code + Emoji.aliases.each do |key, alias_names| + next unless alias_code = map[key] + alias_names.each { |alias_name| map[alias_name] = alias_code } end - end - map - end + map + end @reverse_map[name] end @@ -288,17 +284,18 @@ class Emoji def self.codes_to_img(str) return if str.blank? - str = str.gsub(/:([\w\-+]*(?::t\d)?):/) do |name| - code = $1 + str = + str.gsub(/:([\w\-+]*(?::t\d)?):/) do |name| + code = $1 - if code && Emoji.custom?(code) - emoji = Emoji[code] - "\"#{code}\"" - elsif code && Emoji.exists?(code) - "\"#{code}\"" - else - name + if code && Emoji.custom?(code) + emoji = Emoji[code] + "\"#{code}\"" + elsif code && Emoji.exists?(code) + "\"#{code}\"" + else + name + end end - end end end diff --git a/app/models/emoji_set_site_setting.rb b/app/models/emoji_set_site_setting.rb index 0fe4772390..172b249dee 100644 --- a/app/models/emoji_set_site_setting.rb +++ b/app/models/emoji_set_site_setting.rb @@ -1,26 +1,24 @@ # frozen_string_literal: true -require 'enum_site_setting' +require "enum_site_setting" class EmojiSetSiteSetting < EnumSiteSetting - def self.valid_value?(val) values.any? { |v| v[:value] == val.to_s } end def self.values @values ||= [ - { name: 'emoji_set.apple_international', value: 'apple' }, - { name: 'emoji_set.google', value: 'google' }, - { name: 'emoji_set.twitter', value: 'twitter' }, - { name: 'emoji_set.win10', value: 'win10' }, - { name: 'emoji_set.google_classic', value: 'google_classic' }, - { name: 'emoji_set.facebook_messenger', value: 'facebook_messenger' }, + { name: "emoji_set.apple_international", value: "apple" }, + { name: "emoji_set.google", value: "google" }, + { name: "emoji_set.twitter", value: "twitter" }, + { name: "emoji_set.win10", value: "win10" }, + { name: "emoji_set.google_classic", value: "google_classic" }, + { name: "emoji_set.facebook_messenger", value: "facebook_messenger" }, ] end def self.translate_names? true end - end diff --git a/app/models/external_upload_stub.rb b/app/models/external_upload_stub.rb index 00fe247942..99ed046b97 100644 --- a/app/models/external_upload_stub.rb +++ b/app/models/external_upload_stub.rb @@ -7,27 +7,32 @@ class ExternalUploadStub < ActiveRecord::Base UPLOADED_EXPIRY_HOURS = 24 FAILED_EXPIRY_HOURS = 48 - belongs_to :created_by, class_name: 'User' + belongs_to :created_by, class_name: "User" - validates :filesize, numericality: { - allow_nil: false, only_integer: true, greater_than_or_equal_to: 1 - } + validates :filesize, + numericality: { + allow_nil: false, + only_integer: true, + greater_than_or_equal_to: 1, + } - scope :expired_created, -> { - where( - "status = ? AND created_at <= ?", - ExternalUploadStub.statuses[:created], - CREATED_EXPIRY_HOURS.hours.ago - ) - } + scope :expired_created, + -> { + where( + "status = ? AND created_at <= ?", + ExternalUploadStub.statuses[:created], + CREATED_EXPIRY_HOURS.hours.ago, + ) + } - scope :expired_uploaded, -> { - where( - "status = ? AND created_at <= ?", - ExternalUploadStub.statuses[:uploaded], - UPLOADED_EXPIRY_HOURS.hours.ago - ) - } + scope :expired_uploaded, + -> { + where( + "status = ? AND created_at <= ?", + ExternalUploadStub.statuses[:uploaded], + UPLOADED_EXPIRY_HOURS.hours.ago, + ) + } before_create do self.unique_identifier = SecureRandom.uuid @@ -35,11 +40,7 @@ class ExternalUploadStub < ActiveRecord::Base end def self.statuses - @statuses ||= Enum.new( - created: 1, - uploaded: 2, - failed: 3, - ) + @statuses ||= Enum.new(created: 1, uploaded: 2, failed: 3) end # TODO (martin): Lifecycle rule would be best to clean stuff up in the external diff --git a/app/models/given_daily_like.rb b/app/models/given_daily_like.rb index e5483ce360..5b2096d867 100644 --- a/app/models/given_daily_like.rb +++ b/app/models/given_daily_like.rb @@ -12,14 +12,15 @@ class GivenDailyLike < ActiveRecord::Base given_date = Date.today # upsert would be nice here - rows = find_for(user_id, given_date).update_all('likes_given = likes_given + 1') + rows = find_for(user_id, given_date).update_all("likes_given = likes_given + 1") if rows == 0 create(user_id: user_id, given_date: given_date, likes_given: 1) else - find_for(user_id, given_date) - .where('limit_reached = false AND likes_given >= :limit', limit: SiteSetting.max_likes_per_day) - .update_all(limit_reached: true) + find_for(user_id, given_date).where( + "limit_reached = false AND likes_given >= :limit", + limit: SiteSetting.max_likes_per_day, + ).update_all(limit_reached: true) end end @@ -28,10 +29,11 @@ class GivenDailyLike < ActiveRecord::Base given_date = Date.today - find_for(user_id, given_date).update_all('likes_given = likes_given - 1') - find_for(user_id, given_date) - .where('limit_reached = true AND likes_given < :limit', limit: SiteSetting.max_likes_per_day) - .update_all(limit_reached: false) + find_for(user_id, given_date).update_all("likes_given = likes_given - 1") + find_for(user_id, given_date).where( + "limit_reached = true AND likes_given < :limit", + limit: SiteSetting.max_likes_per_day, + ).update_all(limit_reached: false) end end diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index ff1ac7a506..efd83403a0 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -1,17 +1,14 @@ # frozen_string_literal: true class GlobalSetting - def self.register(key, default) - define_singleton_method(key) do - provider.lookup(key, default) - end + define_singleton_method(key) { provider.lookup(key, default) } end VALID_SECRET_KEY ||= /^[0-9a-f]{128}$/ # this is named SECRET_TOKEN as opposed to SECRET_KEY_BASE # for legacy reasons - REDIS_SECRET_KEY ||= 'SECRET_TOKEN' + REDIS_SECRET_KEY ||= "SECRET_TOKEN" REDIS_VALIDATE_SECONDS ||= 30 @@ -23,58 +20,59 @@ class GlobalSetting # - generate a token on the fly if needed and cache in redis # - enforce rules about token format falling back to redis if needed def self.safe_secret_key_base - - if @safe_secret_key_base && @token_in_redis && (@token_last_validated + REDIS_VALIDATE_SECONDS) < Time.now + if @safe_secret_key_base && @token_in_redis && + (@token_last_validated + REDIS_VALIDATE_SECONDS) < Time.now @token_last_validated = Time.now token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY) - if token.nil? - Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, @safe_secret_key_base) - end + Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, @safe_secret_key_base) if token.nil? end - @safe_secret_key_base ||= begin - token = secret_key_base - if token.blank? || token !~ VALID_SECRET_KEY + @safe_secret_key_base ||= + begin + token = secret_key_base + if token.blank? || token !~ VALID_SECRET_KEY + @token_in_redis = true + @token_last_validated = Time.now - @token_in_redis = true - @token_last_validated = Time.now - - token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY) - unless token && token =~ VALID_SECRET_KEY - token = SecureRandom.hex(64) - Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, token) + token = Discourse.redis.without_namespace.get(REDIS_SECRET_KEY) + unless token && token =~ VALID_SECRET_KEY + token = SecureRandom.hex(64) + Discourse.redis.without_namespace.set(REDIS_SECRET_KEY, token) + end end + if !secret_key_base.blank? && token != secret_key_base + STDERR.puts "WARNING: DISCOURSE_SECRET_KEY_BASE is invalid, it was re-generated" + end + token end - if !secret_key_base.blank? && token != secret_key_base - STDERR.puts "WARNING: DISCOURSE_SECRET_KEY_BASE is invalid, it was re-generated" - end - token - end rescue Redis::CommandError => e @safe_secret_key_base = SecureRandom.hex(64) if e.message =~ /READONLY/ end def self.load_defaults - default_provider = FileProvider.from(File.expand_path('../../../config/discourse_defaults.conf', __FILE__)) - default_provider.keys.concat(@provider.keys).uniq.each do |key| - default = default_provider.lookup(key, nil) + default_provider = + FileProvider.from(File.expand_path("../../../config/discourse_defaults.conf", __FILE__)) + default_provider + .keys + .concat(@provider.keys) + .uniq + .each do |key| + default = default_provider.lookup(key, nil) - instance_variable_set("@#{key}_cache", nil) + instance_variable_set("@#{key}_cache", nil) - define_singleton_method(key) do - val = instance_variable_get("@#{key}_cache") - unless val.nil? - val == :missing ? nil : val - else - val = provider.lookup(key, default) - if val.nil? - val = :missing + define_singleton_method(key) do + val = instance_variable_get("@#{key}_cache") + unless val.nil? + val == :missing ? nil : val + else + val = provider.lookup(key, default) + val = :missing if val.nil? + instance_variable_set("@#{key}_cache", val) + val == :missing ? nil : val end - instance_variable_set("@#{key}_cache", val) - val == :missing ? nil : val end end - end end def self.skip_db=(v) @@ -94,13 +92,17 @@ class GlobalSetting end def self.use_s3? - (@use_s3 ||= - begin - s3_bucket && - s3_region && ( - s3_use_iam_profile || (s3_access_key_id && s3_secret_access_key) - ) ? :true : :false - end) == :true + ( + @use_s3 ||= + begin + if s3_bucket && s3_region && + (s3_use_iam_profile || (s3_access_key_id && s3_secret_access_key)) + :true + else + :false + end + end + ) == :true end def self.s3_bucket_name @@ -122,7 +124,7 @@ class GlobalSetting def self.database_config hash = { "adapter" => "postgresql" } - %w{ + %w[ pool connect_timeout timeout @@ -135,13 +137,13 @@ class GlobalSetting password replica_host replica_port - }.each do |s| + ].each do |s| if val = self.public_send("db_#{s}") hash[s] = val end end - hostnames = [ hostname ] + hostnames = [hostname] hostnames << backup_hostname if backup_hostname.present? hostnames << URI.parse(cdn_url).host if cdn_url.present? @@ -154,11 +156,11 @@ class GlobalSetting hash["reaping_frequency"] = connection_reaper_interval if connection_reaper_interval.present? hash["advisory_locks"] = !!self.db_advisory_locks - db_variables = provider.keys.filter { |k| k.to_s.starts_with? 'db_variables_' } + db_variables = provider.keys.filter { |k| k.to_s.starts_with? "db_variables_" } if db_variables.length > 0 hash["variables"] = {} db_variables.each do |k| - hash["variables"][k.slice(('db_variables_'.length)..)] = self.public_send(k) + hash["variables"][k.slice(("db_variables_".length)..)] = self.public_send(k) end end @@ -183,12 +185,16 @@ class GlobalSetting def self.get_message_bus_redis_replica_host return message_bus_redis_replica_host if message_bus_redis_replica_host.present? - message_bus_redis_slave_host if respond_to?(:message_bus_redis_slave_host) && message_bus_redis_slave_host.present? + if respond_to?(:message_bus_redis_slave_host) && message_bus_redis_slave_host.present? + message_bus_redis_slave_host + end end def self.get_message_bus_redis_replica_port return message_bus_redis_replica_port if message_bus_redis_replica_port.present? - message_bus_redis_slave_port if respond_to?(:message_bus_redis_slave_port) && message_bus_redis_slave_port.present? + if respond_to?(:message_bus_redis_slave_port) && message_bus_redis_slave_port.present? + message_bus_redis_slave_port + end end def self.redis_config @@ -239,11 +245,7 @@ class GlobalSetting end def self.add_default(name, default) - unless self.respond_to? name - define_singleton_method(name) do - default - end - end + define_singleton_method(name) { default } unless self.respond_to? name end class BaseProvider @@ -259,7 +261,7 @@ class GlobalSetting current else default.present? ? default : nil - end + end, ) end end @@ -267,9 +269,7 @@ class GlobalSetting class FileProvider < BaseProvider attr_reader :data def self.from(file) - if File.exist?(file) - parse(file) - end + parse(file) if File.exist?(file) end def initialize(file) @@ -278,11 +278,15 @@ class GlobalSetting end def read - ERB.new(File.read(@file)).result().split("\n").each do |line| - if line =~ /^\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ - @data[$1.strip.to_sym] = ($4 || $3 || $2).strip + ERB + .new(File.read(@file)) + .result() + .split("\n") + .each do |line| + if line =~ /^\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ + @data[$1.strip.to_sym] = ($4 || $3 || $2).strip + end end - end end def lookup(key, default) @@ -306,7 +310,7 @@ class GlobalSetting class EnvProvider < BaseProvider def lookup(key, default) var = ENV["DISCOURSE_" + key.to_s.upcase] - resolve(var , var.nil? ? default : nil) + resolve(var, var.nil? ? default : nil) end def keys @@ -316,7 +320,6 @@ class GlobalSetting class BlankProvider < BaseProvider def lookup(key, default) - if key == :redis_port return ENV["DISCOURSE_REDIS_PORT"] if ENV["DISCOURSE_REDIS_PORT"] end @@ -337,8 +340,8 @@ class GlobalSetting @provider = BlankProvider.new else @provider = - FileProvider.from(File.expand_path('../../../config/discourse.conf', __FILE__)) || - EnvProvider.new + FileProvider.from(File.expand_path("../../../config/discourse.conf", __FILE__)) || + EnvProvider.new end end diff --git a/app/models/group.rb b/app/models/group.rb index b4dbd85eab..c31436815a 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true -require 'net/imap' +require "net/imap" class Group < ActiveRecord::Base # TODO(2021-05-26): remove - self.ignored_columns = %w{ - flair_url - } + self.ignored_columns = %w[flair_url] include HasCustomFields include AnonCacheInvalidator @@ -28,17 +26,20 @@ class Group < ActiveRecord::Base has_many :users, through: :group_users has_many :requesters, through: :group_requests, source: :user has_many :group_histories, dependent: :destroy - has_many :category_reviews, class_name: 'Category', foreign_key: :reviewable_by_group_id, dependent: :nullify + has_many :category_reviews, + class_name: "Category", + foreign_key: :reviewable_by_group_id, + dependent: :nullify has_many :reviewables, foreign_key: :reviewable_by_group_id, dependent: :nullify has_many :group_category_notification_defaults, dependent: :destroy has_many :group_tag_notification_defaults, dependent: :destroy has_many :associated_groups, through: :group_associated_groups, dependent: :destroy - belongs_to :flair_upload, class_name: 'Upload' + belongs_to :flair_upload, class_name: "Upload" has_many :upload_references, as: :target, dependent: :destroy - belongs_to :smtp_updated_by, class_name: 'User' - belongs_to :imap_updated_by, class_name: 'User' + belongs_to :smtp_updated_by, class_name: "User" + belongs_to :imap_updated_by, class_name: "User" has_and_belongs_to_many :web_hooks @@ -50,7 +51,7 @@ class Group < ActiveRecord::Base after_save :update_title after_save :enqueue_update_mentions_job, - if: Proc.new { |g| g.name_before_last_save && g.saved_change_to_name? } + if: Proc.new { |g| g.name_before_last_save && g.saved_change_to_name? } after_save do if saved_change_to_flair_upload_id? @@ -61,11 +62,11 @@ class Group < ActiveRecord::Base after_save :expire_cache after_destroy :expire_cache - after_commit :automatic_group_membership, on: [:create, :update] + after_commit :automatic_group_membership, on: %i[create update] after_commit :trigger_group_created_event, on: :create after_commit :trigger_group_updated_event, on: :update after_commit :trigger_group_destroyed_event, on: :destroy - after_commit :set_default_notifications, on: [:create, :update] + after_commit :set_default_notifications, on: %i[create update] def expire_cache ApplicationSerializer.expire_cache_fragment!("group_names") @@ -97,31 +98,31 @@ class Group < ActiveRecord::Base trust_level_1: 11, trust_level_2: 12, trust_level_3: 13, - trust_level_4: 14 + trust_level_4: 14, } AUTO_GROUP_IDS = Hash[*AUTO_GROUPS.to_a.flatten.reverse] - STAFF_GROUPS = [:admins, :moderators, :staff] + STAFF_GROUPS = %i[admins moderators staff] AUTO_GROUPS_ADD = "add" AUTO_GROUPS_REMOVE = "remove" - IMAP_SETTING_ATTRIBUTES = [ - "imap_server", - "imap_port", - "imap_ssl", - "imap_mailbox_name", - "email_username", - "email_password" + IMAP_SETTING_ATTRIBUTES = %w[ + imap_server + imap_port + imap_ssl + imap_mailbox_name + email_username + email_password ] - SMTP_SETTING_ATTRIBUTES = [ - "imap_server", - "imap_port", - "imap_ssl", - "email_username", - "email_password", - "email_from_alias" + SMTP_SETTING_ATTRIBUTES = %w[ + imap_server + imap_port + imap_ssl + email_username + email_password + email_from_alias ] ALIAS_LEVELS = { @@ -130,45 +131,38 @@ class Group < ActiveRecord::Base mods_and_admins: 2, members_mods_and_admins: 3, owners_mods_and_admins: 4, - everyone: 99 + everyone: 99, } VALID_DOMAIN_REGEX = /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i def self.visibility_levels - @visibility_levels = Enum.new( - public: 0, - logged_on_users: 1, - members: 2, - staff: 3, - owners: 4 - ) + @visibility_levels = Enum.new(public: 0, logged_on_users: 1, members: 2, staff: 3, owners: 4) end validates :mentionable_level, inclusion: { in: ALIAS_LEVELS.values } validates :messageable_level, inclusion: { in: ALIAS_LEVELS.values } - scope :with_imap_configured, -> { where(imap_enabled: true).where.not(imap_mailbox_name: '') } + scope :with_imap_configured, -> { where(imap_enabled: true).where.not(imap_mailbox_name: "") } scope :with_smtp_configured, -> { where(smtp_enabled: true) } - scope :visible_groups, Proc.new { |user, order, opts| - groups = self - groups = groups.order(order) if order - groups = groups.order("groups.name ASC") unless order&.include?("name") + scope :visible_groups, + Proc.new { |user, order, opts| + groups = self + groups = groups.order(order) if order + groups = groups.order("groups.name ASC") unless order&.include?("name") - if !opts || !opts[:include_everyone] - groups = groups.where("groups.id > 0") - end + groups = groups.where("groups.id > 0") if !opts || !opts[:include_everyone] - if !user&.admin - is_staff = !!user&.staff? + if !user&.admin + is_staff = !!user&.staff? - if user.blank? - sql = "groups.visibility_level = :public" - elsif is_staff - sql = "groups.visibility_level IN (:public, :logged_on_users, :members, :staff)" - else - sql = <<~SQL + if user.blank? + sql = "groups.visibility_level = :public" + elsif is_staff + sql = "groups.visibility_level IN (:public, :logged_on_users, :members, :staff)" + else + sql = <<~SQL groups.id IN ( SELECT id FROM groups @@ -189,31 +183,31 @@ class Group < ActiveRecord::Base WHERE g.visibility_level IN (:staff, :owners) ) SQL - end + end - params = Group.visibility_levels.to_h.merge(user_id: user&.id, is_staff: is_staff) - groups = groups.where(sql, params) - end + params = Group.visibility_levels.to_h.merge(user_id: user&.id, is_staff: is_staff) + groups = groups.where(sql, params) + end - groups - } + groups + } - scope :members_visible_groups, Proc.new { |user, order, opts| - groups = self.order(order || "name ASC") + scope :members_visible_groups, + Proc.new { |user, order, opts| + groups = self.order(order || "name ASC") - if !opts || !opts[:include_everyone] - groups = groups.where("groups.id > 0") - end + groups = groups.where("groups.id > 0") if !opts || !opts[:include_everyone] - if !user&.admin - is_staff = !!user&.staff? + if !user&.admin + is_staff = !!user&.staff? - if user.blank? - sql = "groups.members_visibility_level = :public" - elsif is_staff - sql = "groups.members_visibility_level IN (:public, :logged_on_users, :members, :staff)" - else - sql = <<~SQL + if user.blank? + sql = "groups.members_visibility_level = :public" + elsif is_staff + sql = + "groups.members_visibility_level IN (:public, :logged_on_users, :members, :staff)" + else + sql = <<~SQL groups.id IN ( SELECT id FROM groups @@ -234,32 +228,39 @@ class Group < ActiveRecord::Base WHERE g.members_visibility_level IN (:staff, :owners) ) SQL - end + end - params = Group.visibility_levels.to_h.merge(user_id: user&.id, is_staff: is_staff) - groups = groups.where(sql, params) - end + params = Group.visibility_levels.to_h.merge(user_id: user&.id, is_staff: is_staff) + groups = groups.where(sql, params) + end - groups - } + groups + } - scope :mentionable, lambda { |user, include_public: true| - where(self.mentionable_sql_clause(include_public: include_public), - levels: alias_levels(user), - user_id: user&.id - ) - } + scope :mentionable, + lambda { |user, include_public: true| + where( + self.mentionable_sql_clause(include_public: include_public), + levels: alias_levels(user), + user_id: user&.id, + ) + } - scope :messageable, lambda { |user| - where("messageable_level in (:levels) OR + scope :messageable, + lambda { |user| + where( + "messageable_level in (:levels) OR ( messageable_level = #{ALIAS_LEVELS[:members_mods_and_admins]} AND id in ( SELECT group_id FROM group_users WHERE user_id = :user_id) ) OR ( messageable_level = #{ALIAS_LEVELS[:owners_mods_and_admins]} AND id in ( SELECT group_id FROM group_users WHERE user_id = :user_id AND owner IS TRUE) - )", levels: alias_levels(user), user_id: user && user.id) - } + )", + levels: alias_levels(user), + user_id: user && user.id, + ) + } def self.mentionable_sql_clause(include_public: true) clause = +<<~SQL @@ -275,9 +276,7 @@ class Group < ActiveRecord::Base ) SQL - if include_public - clause << "OR visibility_level = #{Group.visibility_levels[:public]}" - end + clause << "OR visibility_level = #{Group.visibility_levels[:public]}" if include_public clause end @@ -330,8 +329,18 @@ class Group < ActiveRecord::Base self.smtp_updated_by_id = user.id end - self.smtp_enabled = [self.smtp_port, self.smtp_server, self.email_password, self.email_username].all?(&:present?) - self.imap_enabled = [self.imap_port, self.imap_server, self.email_password, self.email_username].all?(&:present?) + self.smtp_enabled = [ + self.smtp_port, + self.smtp_server, + self.email_password, + self.email_username, + ].all?(&:present?) + self.imap_enabled = [ + self.imap_port, + self.imap_server, + self.email_password, + self.email_username, + ].all?(&:present?) self.save end @@ -339,71 +348,96 @@ class Group < ActiveRecord::Base def incoming_email_validator return if self.automatic || self.incoming_email.blank? - incoming_email.split("|").each do |email| - escaped = Rack::Utils.escape_html(email) - if !Email.is_valid?(email) - self.errors.add(:base, I18n.t('groups.errors.invalid_incoming_email', email: escaped)) - elsif group = Group.where.not(id: self.id).find_by_email(email) - self.errors.add(:base, I18n.t('groups.errors.email_already_used_in_group', email: escaped, group_name: Rack::Utils.escape_html(group.name))) - elsif category = Category.find_by_email(email) - self.errors.add(:base, I18n.t('groups.errors.email_already_used_in_category', email: escaped, category_name: Rack::Utils.escape_html(category.name))) + incoming_email + .split("|") + .each do |email| + escaped = Rack::Utils.escape_html(email) + if !Email.is_valid?(email) + self.errors.add(:base, I18n.t("groups.errors.invalid_incoming_email", email: escaped)) + elsif group = Group.where.not(id: self.id).find_by_email(email) + self.errors.add( + :base, + I18n.t( + "groups.errors.email_already_used_in_group", + email: escaped, + group_name: Rack::Utils.escape_html(group.name), + ), + ) + elsif category = Category.find_by_email(email) + self.errors.add( + :base, + I18n.t( + "groups.errors.email_already_used_in_category", + email: escaped, + category_name: Rack::Utils.escape_html(category.name), + ), + ) + end end - end end def posts_for(guardian, opts = nil) opts ||= {} - result = Post.joins(:topic, user: :groups, topic: :category) - .preload(:topic, user: :groups, topic: :category) - .references(:posts, :topics, :category) - .where(groups: { id: id }) - .where('topics.archetype <> ?', Archetype.private_message) - .where('topics.visible') - .where(post_type: [Post.types[:regular], Post.types[:moderator_action]]) + result = + Post + .joins(:topic, user: :groups, topic: :category) + .preload(:topic, user: :groups, topic: :category) + .references(:posts, :topics, :category) + .where(groups: { id: id }) + .where("topics.archetype <> ?", Archetype.private_message) + .where("topics.visible") + .where(post_type: [Post.types[:regular], Post.types[:moderator_action]]) if opts[:category_id].present? - result = result.where('topics.category_id = ?', opts[:category_id].to_i) + result = result.where("topics.category_id = ?", opts[:category_id].to_i) end result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] - result.order('posts.created_at desc') + result = result.where("posts.id < ?", opts[:before_post_id].to_i) if opts[:before_post_id] + result.order("posts.created_at desc") end def messages_for(guardian, opts = nil) opts ||= {} - result = Post.includes(:user, :topic, topic: :category) - .references(:posts, :topics, :category) - .where('topics.archetype = ?', Archetype.private_message) - .where(post_type: Post.types[:regular]) - .where('topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = ?)', self.id) + result = + Post + .includes(:user, :topic, topic: :category) + .references(:posts, :topics, :category) + .where("topics.archetype = ?", Archetype.private_message) + .where(post_type: Post.types[:regular]) + .where( + "topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = ?)", + self.id, + ) if opts[:category_id].present? - result = result.where('topics.category_id = ?', opts[:category_id].to_i) + result = result.where("topics.category_id = ?", opts[:category_id].to_i) end result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] - result.order('posts.created_at desc') + result = result.where("posts.id < ?", opts[:before_post_id].to_i) if opts[:before_post_id] + result.order("posts.created_at desc") end def mentioned_posts_for(guardian, opts = nil) opts ||= {} - result = Post.joins(:group_mentions) - .includes(:user, :topic, topic: :category) - .references(:posts, :topics, :category) - .where('topics.archetype <> ?', Archetype.private_message) - .where(post_type: Post.types[:regular]) - .where('group_mentions.group_id = ?', self.id) + result = + Post + .joins(:group_mentions) + .includes(:user, :topic, topic: :category) + .references(:posts, :topics, :category) + .where("topics.archetype <> ?", Archetype.private_message) + .where(post_type: Post.types[:regular]) + .where("group_mentions.group_id = ?", self.id) if opts[:category_id].present? - result = result.where('topics.category_id = ?', opts[:category_id].to_i) + result = result.where("topics.category_id = ?", opts[:category_id].to_i) end result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] - result.order('posts.created_at desc') + result = result.where("posts.id < ?", opts[:before_post_id].to_i) if opts[:before_post_id] + result.order("posts.created_at desc") end def self.trust_group_ids @@ -411,22 +445,29 @@ class Group < ActiveRecord::Base end def set_message_default_notification_levels!(topic, ignore_existing: false) - group_users.pluck(:user_id, :notification_level).each do |user_id, notification_level| - next if user_id == -1 - next if user_id == topic.user_id - next if ignore_existing && TopicUser.where(user_id: user_id, topic_id: topic.id).exists? + group_users + .pluck(:user_id, :notification_level) + .each do |user_id, notification_level| + next if user_id == -1 + next if user_id == topic.user_id + next if ignore_existing && TopicUser.where(user_id: user_id, topic_id: topic.id).exists? - action = - case notification_level - when TopicUser.notification_levels[:tracking] then "track!" - when TopicUser.notification_levels[:regular] then "regular!" - when TopicUser.notification_levels[:muted] then "mute!" - when TopicUser.notification_levels[:watching] then "watch!" - else "track!" - end + action = + case notification_level + when TopicUser.notification_levels[:tracking] + "track!" + when TopicUser.notification_levels[:regular] + "regular!" + when TopicUser.notification_levels[:muted] + "mute!" + when TopicUser.notification_levels[:watching] + "watch!" + else + "track!" + end - topic.notifier.public_send(action, user_id) - end + topic.notifier.public_send(action, user_id) + end end def self.set_category_and_tag_default_notification_levels!(user, group_name) @@ -455,9 +496,7 @@ class Group < ActiveRecord::Base localized_name = I18n.t("groups.default_names.#{name}", locale: SiteSetting.default_locale) validator = UsernameValidator.new(localized_name) - if validator.valid_format? && !User.username_exists?(localized_name) - group.name = localized_name - end + group.name = localized_name if validator.valid_format? && !User.username_exists?(localized_name) # the everyone group is special, it can include non-users so there is no # way to have the membership in a table @@ -470,7 +509,9 @@ class Group < ActiveRecord::Base group.update!(messageable_level: ALIAS_LEVELS[:everyone]) end - group.update!(visibility_level: Group.visibility_levels[:logged_on_users]) if group.visibility_level == Group.visibility_levels[:public] + if group.visibility_level == Group.visibility_levels[:public] + group.update!(visibility_level: Group.visibility_levels[:logged_on_users]) + end # Remove people from groups they don't belong in. remove_subquery = @@ -496,7 +537,9 @@ class Group < ActiveRecord::Base if removed_user_ids.present? Jobs.enqueue( :publish_group_membership_updates, - user_ids: removed_user_ids, group_id: group.id, type: AUTO_GROUPS_REMOVE + user_ids: removed_user_ids, + group_id: group.id, + type: AUTO_GROUPS_REMOVE, ) end @@ -529,7 +572,9 @@ class Group < ActiveRecord::Base if added_user_ids.present? Jobs.enqueue( :publish_group_membership_updates, - user_ids: added_user_ids, group_id: group.id, type: AUTO_GROUPS_ADD + user_ids: added_user_ids, + group_id: group.id, + type: AUTO_GROUPS_ADD, ) end @@ -554,7 +599,7 @@ class Group < ActiveRecord::Base end def self.reset_groups_user_count!(only_group_ids: []) - where_sql = '' + where_sql = "" if only_group_ids.present? where_sql = "WHERE group_id IN (#{only_group_ids.map(&:to_i).join(",")})" @@ -596,9 +641,7 @@ class Group < ActiveRecord::Base end def self.ensure_automatic_groups! - AUTO_GROUPS.each_key do |name| - refresh_automatic_group!(name) unless lookup_group(name) - end + AUTO_GROUPS.each_key { |name| refresh_automatic_group!(name) unless lookup_group(name) } end def self.[](name) @@ -608,18 +651,18 @@ class Group < ActiveRecord::Base def self.search_groups(name, groups: nil, custom_scope: {}, sort: :none) groups ||= Group - relation = groups.where( - "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" - ) + relation = + groups.where("name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%") if sort == :auto prefix = "#{name.gsub("_", "\\_")}%" - relation = relation.reorder( - DB.sql_fragment( - "CASE WHEN name ILIKE :like OR full_name ILIKE :like THEN 0 ELSE 1 END ASC, name ASC", - like: prefix + relation = + relation.reorder( + DB.sql_fragment( + "CASE WHEN name ILIKE :like OR full_name ILIKE :like THEN 0 ELSE 1 END ASC, name ASC", + like: prefix, + ), ) - ) end relation @@ -652,9 +695,7 @@ class Group < ActiveRecord::Base end def self.desired_trust_level_groups(trust_level) - trust_group_ids.keep_if do |id| - id == AUTO_GROUPS[:trust_level_0] || (trust_level + 10) >= id - end + trust_group_ids.keep_if { |id| id == AUTO_GROUPS[:trust_level_0] || (trust_level + 10) >= id } end def self.user_trust_level_change!(user_id, trust_level) @@ -696,21 +737,21 @@ class Group < ActiveRecord::Base additions = expected - current deletions = current - expected - map = Hash[*User.where(username: additions + deletions) - .select('id,username') - .map { |u| [u.username, u.id] }.flatten] + map = + Hash[ + *User + .where(username: additions + deletions) + .select("id,username") + .map { |u| [u.username, u.id] } + .flatten + ] deletions = Set.new(deletions.map { |d| map[d] }) @deletions = [] - group_users.each do |gu| - @deletions << gu if deletions.include?(gu.user_id) - end - - additions.each do |a| - group_users.build(user_id: map[a]) - end + group_users.each { |gu| @deletions << gu if deletions.include?(gu.user_id) } + additions.each { |a| group_users.build(user_id: map[a]) } end def usernames @@ -728,14 +769,16 @@ class Group < ActiveRecord::Base Notification.create!( notification_type: Notification.types[:membership_request_accepted], user_id: user.id, - data: { group_id: id, group_name: name }.to_json + data: { group_id: id, group_name: name }.to_json, ) end if self.categories.count < PUBLISH_CATEGORIES_LIMIT - MessageBus.publish('/categories', { - categories: ActiveModel::ArraySerializer.new(self.categories).as_json - }, user_ids: [user.id]) + MessageBus.publish( + "/categories", + { categories: ActiveModel::ArraySerializer.new(self.categories).as_json }, + user_ids: [user.id], + ) else Discourse.request_refresh!(user_ids: [user.id]) end @@ -750,13 +793,18 @@ class Group < ActiveRecord::Base return false if group_user.blank? has_webhooks = WebHook.active_web_hooks(:group_user) - payload = WebHook.generate_payload(:group_user, group_user, WebHookGroupUserSerializer) if has_webhooks + payload = + WebHook.generate_payload(:group_user, group_user, WebHookGroupUserSerializer) if has_webhooks group_user.destroy trigger_user_removed_event(user) - WebHook.enqueue_hooks(:group_user, :user_removed_from_group, - id: group_user.id, - payload: payload - ) if has_webhooks + if has_webhooks + WebHook.enqueue_hooks( + :group_user, + :user_removed_from_group, + id: group_user.id, + payload: payload, + ) + end true end @@ -781,7 +829,7 @@ class Group < ActiveRecord::Base "email_username = :email OR string_to_array(incoming_email, '|') @> ARRAY[:email] OR email_from_alias = :email", - email: Email.downcase(email) + email: Email.downcase(email), ).first end @@ -810,17 +858,11 @@ class Group < ActiveRecord::Base user_attributes = {} - if self.primary_group? - user_attributes[:primary_group_id] = self.id - end + user_attributes[:primary_group_id] = self.id if self.primary_group? - if self.title.present? - user_attributes[:title] = self.title - end + user_attributes[:title] = self.title if self.title.present? - if user_attributes.present? - User.where(id: user_ids).update_all(user_attributes) - end + User.where(id: user_ids).update_all(user_attributes) if user_attributes.present? # update group user count DB.exec <<~SQL @@ -834,10 +876,7 @@ class Group < ActiveRecord::Base end if self.grant_trust_level.present? - Jobs.enqueue(:bulk_grant_trust_level, - user_ids: user_ids, - trust_level: self.grant_trust_level - ) + Jobs.enqueue(:bulk_grant_trust_level, user_ids: user_ids, trust_level: self.grant_trust_level) end self @@ -862,20 +901,17 @@ class Group < ActiveRecord::Base end def self.member_of(groups, user) - groups - .joins("LEFT JOIN group_users gu ON gu.group_id = groups.id ") - .where("gu.user_id = ?", user.id) + groups.joins("LEFT JOIN group_users gu ON gu.group_id = groups.id ").where( + "gu.user_id = ?", + user.id, + ) end def self.owner_of(groups, user) self.member_of(groups, user).where("gu.owner") end - %i{ - group_created - group_updated - group_destroyed - }.each do |event| + %i[group_created group_updated group_destroyed].each do |event| define_method("trigger_#{event}_event") do DiscourseEvent.trigger(event, self) true @@ -894,7 +930,7 @@ class Group < ActiveRecord::Base nil end - [:muted, :regular, :tracking, :watching, :watching_first_post].each do |level| + %i[muted regular tracking watching watching_first_post].each do |level| define_method("#{level}_category_ids=") do |category_ids| @category_notifications ||= {} @category_notifications[level] = category_ids @@ -923,26 +959,28 @@ class Group < ActiveRecord::Base def imap_mailboxes return [] if !self.imap_enabled || !SiteSetting.enable_imap - Discourse.cache.fetch("group_imap_mailboxes_#{self.id}", expires_in: 30.minutes) do - Rails.logger.info("[IMAP] Refreshing mailboxes list for group #{self.name}") - mailboxes = [] + Discourse + .cache + .fetch("group_imap_mailboxes_#{self.id}", expires_in: 30.minutes) do + Rails.logger.info("[IMAP] Refreshing mailboxes list for group #{self.name}") + mailboxes = [] - begin - imap_provider = Imap::Providers::Detector.init_with_detected_provider( - self.imap_config - ) - imap_provider.connect! - mailboxes = imap_provider.filter_mailboxes(imap_provider.list_mailboxes_with_attributes) - imap_provider.disconnect! + begin + imap_provider = Imap::Providers::Detector.init_with_detected_provider(self.imap_config) + imap_provider.connect! + mailboxes = imap_provider.filter_mailboxes(imap_provider.list_mailboxes_with_attributes) + imap_provider.disconnect! - update_columns(imap_last_error: nil) - rescue => ex - Rails.logger.warn("[IMAP] Mailbox refresh failed for group #{self.name} with error: #{ex}") - update_columns(imap_last_error: ex.message) + update_columns(imap_last_error: nil) + rescue => ex + Rails.logger.warn( + "[IMAP] Mailbox refresh failed for group #{self.name} with error: #{ex}", + ) + update_columns(imap_last_error: ex.message) + end + + mailboxes end - - mailboxes - end end def imap_config @@ -951,16 +989,16 @@ class Group < ActiveRecord::Base port: self.imap_port, ssl: self.imap_ssl, username: self.email_username, - password: self.email_password + password: self.email_password, } end def email_username_domain - email_username.split('@').last + email_username.split("@").last end def email_username_user - email_username.split('@').first + email_username.split("@").first end def email_username_regex @@ -976,7 +1014,7 @@ class Group < ActiveRecord::Base user, owner ? :user_added_to_group_as_owner : :user_added_to_group_as_member, group_name: name_full_preferred, - group_path: "/g/#{self.name}" + group_path: "/g/#{self.name}", ) end @@ -1005,21 +1043,25 @@ class Group < ActiveRecord::Base self.name = stripped end - UsernameValidator.perform_validation(self, 'name') || begin - normalized_name = User.normalize_username(self.name) + UsernameValidator.perform_validation(self, "name") || + begin + normalized_name = User.normalize_username(self.name) - if self.will_save_change_to_name? && User.normalize_username(self.name_was) != normalized_name && User.username_exists?(self.name) - errors.add(:name, I18n.t("activerecord.errors.messages.taken")) + if self.will_save_change_to_name? && + User.normalize_username(self.name_was) != normalized_name && + User.username_exists?(self.name) + errors.add(:name, I18n.t("activerecord.errors.messages.taken")) + end end - end end def automatic_membership_email_domains_format_validator return if self.automatic_membership_email_domains.blank? - domains = Group.get_valid_email_domains(self.automatic_membership_email_domains) do |domain| - self.errors.add :base, (I18n.t('groups.errors.invalid_domain', domain: domain)) - end + domains = + Group.get_valid_email_domains(self.automatic_membership_email_domains) do |domain| + self.errors.add :base, (I18n.t("groups.errors.invalid_domain", domain: domain)) + end self.automatic_membership_email_domains = domains.join("|") end @@ -1029,7 +1071,11 @@ class Group < ActiveRecord::Base if @deletions @deletions.each do |gu| gu.destroy - User.where('id = ? AND primary_group_id = ?', gu.user_id, gu.group_id).update_all 'primary_group_id = NULL' + User.where( + "id = ? AND primary_group_id = ?", + gu.user_id, + gu.group_id, + ).update_all "primary_group_id = NULL" end end @deletions = nil @@ -1067,7 +1113,7 @@ class Group < ActiveRecord::Base /*where*/ SQL - [:primary_group_id, :flair_group_id].each do |column| + %i[primary_group_id flair_group_id].each do |column| builder = DB.build(sql) builder.where(<<~SQL, id: id) id IN ( @@ -1091,10 +1137,19 @@ class Group < ActiveRecord::Base end def self.automatic_membership_users(domains, group_id = nil) - pattern = "@(#{domains.gsub('.', '\.')})$" + pattern = "@(#{domains.gsub(".", '\.')})$" - users = User.joins(:user_emails).where("user_emails.email ~* ?", pattern).activated.where(staged: false) - users = users.where("users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", group_id) if group_id.present? + users = + User + .joins(:user_emails) + .where("user_emails.email ~* ?", pattern) + .activated + .where(staged: false) + users = + users.where( + "users.id NOT IN (SELECT user_id FROM group_users WHERE group_users.group_id = ?)", + group_id, + ) if group_id.present? users end @@ -1102,16 +1157,18 @@ class Group < ActiveRecord::Base def self.get_valid_email_domains(value) valid_domains = [] - value.split("|").each do |domain| - domain.sub!(/^https?:\/\//, '') - domain.sub!(/\/.*$/, '') + value + .split("|") + .each do |domain| + domain.sub!(%r{^https?://}, "") + domain.sub!(%r{/.*$}, "") - if domain =~ Group::VALID_DOMAIN_REGEX - valid_domains << domain - else - yield domain if block_given? + if domain =~ Group::VALID_DOMAIN_REGEX + valid_domains << domain + else + yield domain if block_given? + end end - end valid_domains end @@ -1120,10 +1177,10 @@ class Group < ActiveRecord::Base def validate_grant_trust_level unless TrustLevel.valid?(self.grant_trust_level) - self.errors.add(:base, I18n.t( - 'groups.errors.grant_trust_level_not_valid', - trust_level: self.grant_trust_level - )) + self.errors.add( + :base, + I18n.t("groups.errors.grant_trust_level_not_valid", trust_level: self.grant_trust_level), + ) end end @@ -1137,15 +1194,14 @@ class Group < ActiveRecord::Base self.group_users.any?(&:owner) end - if !valid - self.errors.add(:base, I18n.t('groups.errors.cant_allow_membership_requests')) - end + self.errors.add(:base, I18n.t("groups.errors.cant_allow_membership_requests")) if !valid end def enqueue_update_mentions_job - Jobs.enqueue(:update_group_mentions, + Jobs.enqueue( + :update_group_mentions, previous_name: self.name_before_last_save, - group_id: self.id + group_id: self.id, ) end end diff --git a/app/models/group_archived_message.rb b/app/models/group_archived_message.rb index 72d71af025..30d9feb5ce 100644 --- a/app/models/group_archived_message.rb +++ b/app/models/group_archived_message.rb @@ -15,7 +15,7 @@ class GroupArchivedMessage < ActiveRecord::Base :group_pm_update_summary, group_id: group_id, topic_id: topic_id, - acting_user_id: opts[:acting_user_id] + acting_user_id: opts[:acting_user_id], ) end @@ -31,20 +31,19 @@ class GroupArchivedMessage < ActiveRecord::Base :group_pm_update_summary, group_id: group_id, topic_id: topic_id, - acting_user_id: opts[:acting_user_id] + acting_user_id: opts[:acting_user_id], ) end def self.trigger(event, group_id, topic_id) group = Group.find_by(id: group_id) topic = Topic.find_by(id: topic_id) - if group && topic - DiscourseEvent.trigger(event, group: group, topic: topic) - end + DiscourseEvent.trigger(event, group: group, topic: topic) if group && topic end def self.set_imap_sync(topic_id) - IncomingEmail.joins(:post) + IncomingEmail + .joins(:post) .where.not(imap_uid: nil) .where(topic_id: topic_id, posts: { post_number: 1 }) .update_all(imap_sync: true) @@ -55,7 +54,7 @@ class GroupArchivedMessage < ActiveRecord::Base PrivateMessageTopicTrackingState.publish_group_archived( topic: topic, group_id: group_id, - acting_user_id: acting_user_id + acting_user_id: acting_user_id, ) end private_class_method :publish_topic_tracking_state diff --git a/app/models/group_associated_group.rb b/app/models/group_associated_group.rb index be71f09eb0..4e9f963fbd 100644 --- a/app/models/group_associated_group.rb +++ b/app/models/group_associated_group.rb @@ -3,22 +3,22 @@ class GroupAssociatedGroup < ActiveRecord::Base belongs_to :group belongs_to :associated_group - after_commit :add_associated_users, on: [:create, :update] + after_commit :add_associated_users, on: %i[create update] before_destroy :remove_associated_users def add_associated_users with_mutex do associated_group.users.in_batches do |users| - users.each do |user| - group.add_automatically(user, subject: associated_group.label) - end + users.each { |user| group.add_automatically(user, subject: associated_group.label) } end end end def remove_associated_users with_mutex do - User.where("NOT EXISTS( + User + .where( + "NOT EXISTS( SELECT 1 FROM user_associated_groups uag JOIN group_associated_groups gag @@ -26,11 +26,13 @@ class GroupAssociatedGroup < ActiveRecord::Base WHERE uag.user_id = users.id AND gag.id != :gag_id AND gag.group_id = :group_id - )", gag_id: id, group_id: group_id).in_batches do |users| - users.each do |user| - group.remove_automatically(user, subject: associated_group.label) + )", + gag_id: id, + group_id: group_id, + ) + .in_batches do |users| + users.each { |user| group.remove_automatically(user, subject: associated_group.label) } end - end end end diff --git a/app/models/group_category_notification_default.rb b/app/models/group_category_notification_default.rb index 858ef422b5..b75183ef1f 100644 --- a/app/models/group_category_notification_default.rb +++ b/app/models/group_category_notification_default.rb @@ -19,28 +19,24 @@ class GroupCategoryNotificationDefault < ActiveRecord::Base changed = false # Update pre-existing - if category_ids.present? && GroupCategoryNotificationDefault - .where(group_id: group.id, category_id: category_ids) - .where.not(notification_level: level_num) - .update_all(notification_level: level_num) > 0 - + if category_ids.present? && + GroupCategoryNotificationDefault + .where(group_id: group.id, category_id: category_ids) + .where.not(notification_level: level_num) + .update_all(notification_level: level_num) > 0 changed = true end # Remove extraneous category users if GroupCategoryNotificationDefault - .where(group_id: group.id, notification_level: level_num) - .where.not(category_id: category_ids) - .delete_all > 0 - + .where(group_id: group.id, notification_level: level_num) + .where.not(category_id: category_ids) + .delete_all > 0 changed = true end if category_ids.present? - params = { - group_id: group.id, - level_num: level_num, - } + params = { group_id: group.id, level_num: level_num } sql = <<~SQL INSERT INTO group_category_notification_defaults (group_id, category_id, notification_level) @@ -52,9 +48,7 @@ class GroupCategoryNotificationDefault < ActiveRecord::Base # into the query, plus it is a bit of a micro optimisation category_ids.each do |category_id| params[:category_id] = category_id - if DB.exec(sql, params) > 0 - changed = true - end + changed = true if DB.exec(sql, params) > 0 end end diff --git a/app/models/group_history.rb b/app/models/group_history.rb index 83583824a6..9f58072e58 100644 --- a/app/models/group_history.rb +++ b/app/models/group_history.rb @@ -2,43 +2,43 @@ class GroupHistory < ActiveRecord::Base belongs_to :group - belongs_to :acting_user, class_name: 'User' - belongs_to :target_user, class_name: 'User' + belongs_to :acting_user, class_name: "User" + belongs_to :target_user, class_name: "User" validates :acting_user_id, presence: true validates :group_id, presence: true validates :action, presence: true def self.actions - @actions ||= Enum.new( - change_group_setting: 1, - add_user_to_group: 2, - remove_user_from_group: 3, - make_user_group_owner: 4, - remove_user_as_group_owner: 5 - ) + @actions ||= + Enum.new( + change_group_setting: 1, + add_user_to_group: 2, + remove_user_from_group: 3, + make_user_group_owner: 4, + remove_user_as_group_owner: 5, + ) end def self.filters - [ - :acting_user, - :target_user, - :action, - :subject - ] + %i[acting_user target_user action subject] end def self.with_filters(group, params = {}) - records = self.includes(:acting_user, :target_user) - .where(group_id: group.id) - .order('group_histories.created_at DESC') + records = + self + .includes(:acting_user, :target_user) + .where(group_id: group.id) + .order("group_histories.created_at DESC") if !params.blank? params = params.slice(*filters) - records = records.where(action: self.actions[params[:action].to_sym]) unless params[:action].blank? + records = records.where(action: self.actions[params[:action].to_sym]) unless params[ + :action + ].blank? records = records.where(subject: params[:subject]) unless params[:subject].blank? - [:acting_user, :target_user].each do |filter| + %i[acting_user target_user].each do |filter| unless params[filter].blank? id = User.where(username_lower: params[filter]).pluck(:id) records = records.where("#{filter}_id" => id) diff --git a/app/models/group_tag_notification_default.rb b/app/models/group_tag_notification_default.rb index a72d554b5b..312dd2bd68 100644 --- a/app/models/group_tag_notification_default.rb +++ b/app/models/group_tag_notification_default.rb @@ -21,15 +21,16 @@ class GroupTagNotificationDefault < ActiveRecord::Base tag_ids = tag_names.empty? ? [] : Tag.where_name(tag_names).pluck(:id) - Tag.where_name(tag_names).joins(:target_tag).each do |tag| - tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id - end + Tag + .where_name(tag_names) + .joins(:target_tag) + .each { |tag| tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id } tag_ids.uniq! remove = (old_ids - tag_ids) if remove.present? - records.where('tag_id in (?)', remove).destroy_all + records.where("tag_id in (?)", remove).destroy_all changed = true end diff --git a/app/models/group_user.rb b/app/models/group_user.rb index 1c5df59922..cfd27f01f3 100644 --- a/app/models/group_user.rb +++ b/app/models/group_user.rb @@ -29,7 +29,8 @@ class GroupUser < ActiveRecord::Base def self.update_first_unread_pm(last_seen, limit: 10_000) whisperers_group_ids = SiteSetting.whispers_allowed_group_ids - DB.exec(<<~SQL, archetype: Archetype.private_message, last_seen: last_seen, limit: limit, now: 10.minutes.ago, whisperers_group_ids: whisperers_group_ids) + DB.exec( + <<~SQL, UPDATE group_users gu SET first_unread_pm_at = Y.min_date FROM ( @@ -56,7 +57,7 @@ class GroupUser < ActiveRecord::Base WHERE t.deleted_at IS NULL AND t.archetype = :archetype AND tu.last_read_post_number < CASE - WHEN u.admin OR u.moderator #{whisperers_group_ids.present? ? 'OR gu2.group_id IN (:whisperers_group_ids)' : ''} + WHEN u.admin OR u.moderator #{whisperers_group_ids.present? ? "OR gu2.group_id IN (:whisperers_group_ids)" : ""} THEN t.highest_staff_post_number ELSE t.highest_post_number END @@ -75,6 +76,12 @@ class GroupUser < ActiveRecord::Base ) Y WHERE gu.user_id = Y.user_id AND gu.group_id = Y.group_id SQL + archetype: Archetype.private_message, + last_seen: last_seen, + limit: limit, + now: 10.minutes.ago, + whisperers_group_ids: whisperers_group_ids, + ) end protected @@ -105,10 +112,12 @@ class GroupUser < ActiveRecord::Base def update_title if group.title.present? - DB.exec(" + DB.exec( + " UPDATE users SET title = :title WHERE (title IS NULL OR title = '') AND id = :id", - id: user_id, title: group.title + id: user_id, + title: group.title, ) end end @@ -131,28 +140,34 @@ class GroupUser < ActiveRecord::Base end def self.set_category_notifications(group, user) - group_levels = group.group_category_notification_defaults.each_with_object({}) do |r, h| - h[r.notification_level] ||= [] - h[r.notification_level] << r.category_id - end + group_levels = + group + .group_category_notification_defaults + .each_with_object({}) do |r, h| + h[r.notification_level] ||= [] + h[r.notification_level] << r.category_id + end return if group_levels.empty? - user_levels = CategoryUser.where(user_id: user.id).each_with_object({}) do |r, h| - h[r.notification_level] ||= [] - h[r.notification_level] << r.category_id - end + user_levels = + CategoryUser + .where(user_id: user.id) + .each_with_object({}) do |r, h| + h[r.notification_level] ||= [] + h[r.notification_level] << r.category_id + end higher_level_category_ids = user_levels.values.flatten - [:muted, :regular, :tracking, :watching_first_post, :watching].each do |level| + %i[muted regular tracking watching_first_post watching].each do |level| level_num = NotificationLevels.all[level] higher_level_category_ids -= (user_levels[level_num] || []) if group_category_ids = group_levels[level_num] CategoryUser.batch_set( user, level, - group_category_ids + (user_levels[level_num] || []) - higher_level_category_ids + group_category_ids + (user_levels[level_num] || []) - higher_level_category_ids, ) end end @@ -163,28 +178,34 @@ class GroupUser < ActiveRecord::Base end def self.set_tag_notifications(group, user) - group_levels = group.group_tag_notification_defaults.each_with_object({}) do |r, h| - h[r.notification_level] ||= [] - h[r.notification_level] << r.tag_id - end + group_levels = + group + .group_tag_notification_defaults + .each_with_object({}) do |r, h| + h[r.notification_level] ||= [] + h[r.notification_level] << r.tag_id + end return if group_levels.empty? - user_levels = TagUser.where(user_id: user.id).each_with_object({}) do |r, h| - h[r.notification_level] ||= [] - h[r.notification_level] << r.tag_id - end + user_levels = + TagUser + .where(user_id: user.id) + .each_with_object({}) do |r, h| + h[r.notification_level] ||= [] + h[r.notification_level] << r.tag_id + end higher_level_tag_ids = user_levels.values.flatten - [:muted, :regular, :tracking, :watching_first_post, :watching].each do |level| + %i[muted regular tracking watching_first_post watching].each do |level| level_num = NotificationLevels.all[level] higher_level_tag_ids -= (user_levels[level_num] || []) if group_tag_ids = group_levels[level_num] TagUser.batch_set( user, level, - group_tag_ids + (user_levels[level_num] || []) - higher_level_tag_ids + group_tag_ids + (user_levels[level_num] || []) - higher_level_tag_ids, ) end end diff --git a/app/models/imap_sync_log.rb b/app/models/imap_sync_log.rb index e1073dcd16..28f216c4cb 100644 --- a/app/models/imap_sync_log.rb +++ b/app/models/imap_sync_log.rb @@ -12,14 +12,18 @@ class ImapSyncLog < ActiveRecord::Base def self.log(message, level, group_id = nil, db = true) now = Time.now.strftime("%Y-%m-%d %H:%M:%S.%L") - new_log = if db - create(message: message, level: ImapSyncLog.levels[level], group_id: group_id) - end + new_log = (create(message: message, level: ImapSyncLog.levels[level], group_id: group_id) if db) if ENV["DEBUG_IMAP"] - Rails.logger.send(:warn, "#{level[0].upcase}, [#{now}] [IMAP] (group_id #{group_id}) #{message}") + Rails.logger.send( + :warn, + "#{level[0].upcase}, [#{now}] [IMAP] (group_id #{group_id}) #{message}", + ) else - Rails.logger.send(level, "#{level[0].upcase}, [#{now}] [IMAP] (group_id #{group_id}) #{message}") + Rails.logger.send( + level, + "#{level[0].upcase}, [#{now}] [IMAP] (group_id #{group_id}) #{message}", + ) end new_log diff --git a/app/models/incoming_domain.rb b/app/models/incoming_domain.rb index e9ef69ffaa..94d56cecd9 100644 --- a/app/models/incoming_domain.rb +++ b/app/models/incoming_domain.rb @@ -25,9 +25,7 @@ class IncomingDomain < ActiveRecord::Base def to_url url = +"http#{https ? "s" : ""}://#{name}" - if https && port != 443 || !https && port != 80 - url << ":#{port}" - end + url << ":#{port}" if https && port != 443 || !https && port != 80 url end diff --git a/app/models/incoming_email.rb b/app/models/incoming_email.rb index b33ae191a8..f565bbce67 100644 --- a/app/models/incoming_email.rb +++ b/app/models/incoming_email.rb @@ -4,22 +4,19 @@ class IncomingEmail < ActiveRecord::Base belongs_to :user belongs_to :topic belongs_to :post - belongs_to :group, foreign_key: :imap_group_id, class_name: 'Group' + belongs_to :group, foreign_key: :imap_group_id, class_name: "Group" validates :created_via, presence: true - scope :errored, -> { where("NOT is_bounce AND error IS NOT NULL") } + scope :errored, -> { where("NOT is_bounce AND error IS NOT NULL") } - scope :addressed_to, -> (email) do - where(<<~SQL, email: "%#{email}%") + scope :addressed_to, ->(email) { where(<<~SQL, email: "%#{email}%") } incoming_emails.from_address = :email OR incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email SQL - end - scope :addressed_to_user, ->(user) do - where(<<~SQL, user_id: user.id) + scope :addressed_to_user, ->(user) { where(<<~SQL, user_id: user.id) } EXISTS( SELECT 1 FROM user_emails @@ -29,18 +26,11 @@ class IncomingEmail < ActiveRecord::Base incoming_emails.cc_addresses ILIKE '%' || user_emails.email || '%') ) SQL - end scope :without_raw, -> { select(self.column_names - ["raw"]) } def self.created_via_types - @types ||= Enum.new( - unknown: 0, - handle_mail: 1, - pop3_poll: 2, - imap: 3, - group_smtp: 4 - ) + @types ||= Enum.new(unknown: 0, handle_mail: 1, pop3_poll: 2, imap: 3, group_smtp: 4) end def as_mail_message @@ -64,23 +54,17 @@ class IncomingEmail < ActiveRecord::Base end def to_addresses=(to) - if to&.is_a?(Array) - to = to.map(&:downcase).join(";") - end + to = to.map(&:downcase).join(";") if to&.is_a?(Array) super(to) end def cc_addresses=(cc) - if cc&.is_a?(Array) - cc = cc.map(&:downcase).join(";") - end + cc = cc.map(&:downcase).join(";") if cc&.is_a?(Array) super(cc) end def from_address=(from) - if from&.is_a?(Array) - from = from.first - end + from = from.first if from&.is_a?(Array) super(from) end end diff --git a/app/models/incoming_link.rb b/app/models/incoming_link.rb index 4e51a66823..acb7d0b395 100644 --- a/app/models/incoming_link.rb +++ b/app/models/incoming_link.rb @@ -35,35 +35,32 @@ class IncomingLink < ActiveRecord::Base end if host != opts[:host] && (user_id || referer) - post_id = opts[:post_id] - post_id ||= Post.where(topic_id: opts[:topic_id], - post_number: opts[:post_number] || 1) - .pluck_first(:id) + post_id ||= + Post.where(topic_id: opts[:topic_id], post_number: opts[:post_number] || 1).pluck_first(:id) cid = current_user ? (current_user.id) : (nil) ip_address = nil if cid unless cid && cid == user_id - - create(referer: referer, - user_id: user_id, - post_id: post_id, - current_user_id: cid, - ip_address: ip_address) if post_id - + if post_id + create( + referer: referer, + user_id: user_id, + post_id: post_id, + current_user_id: cid, + ip_address: ip_address, + ) + end end end - end def referer=(referer) self.incoming_referer_id = nil # will set incoming_referer_id - unless referer.present? - return - end + return unless referer.present? parsed = URI.parse(referer) @@ -73,7 +70,6 @@ class IncomingLink < ActiveRecord::Base referer_record = IncomingReferer.add!(path: parsed.path, incoming_domain: domain) if domain self.incoming_referer_id = referer_record.id if referer_record end - rescue URI::Error # ignore end @@ -85,19 +81,23 @@ class IncomingLink < ActiveRecord::Base end def domain - if incoming_referer - incoming_referer.incoming_domain.name - end + incoming_referer.incoming_domain.name if incoming_referer end # Internal: Update appropriate link counts. def update_link_counts - DB.exec("UPDATE topics + DB.exec( + "UPDATE topics SET incoming_link_count = incoming_link_count + 1 - WHERE id = (SELECT topic_id FROM posts where id = ?)", post_id) - DB.exec("UPDATE posts + WHERE id = (SELECT topic_id FROM posts where id = ?)", + post_id, + ) + DB.exec( + "UPDATE posts SET incoming_link_count = incoming_link_count + 1 - WHERE id = ?", post_id) + WHERE id = ?", + post_id, + ) end protected @@ -106,7 +106,7 @@ class IncomingLink < ActiveRecord::Base return true unless referer if (referer.length < 3 || referer.length > 100) || (domain.length < 1 || domain.length > 100) # internal, no need to localize - errors.add(:referer, 'referer is invalid') + errors.add(:referer, "referer is invalid") false else true diff --git a/app/models/incoming_links_report.rb b/app/models/incoming_links_report.rb index e17f893a93..3c17ff3810 100644 --- a/app/models/incoming_links_report.rb +++ b/app/models/incoming_links_report.rb @@ -1,8 +1,14 @@ # frozen_string_literal: true class IncomingLinksReport - - attr_accessor :type, :data, :y_titles, :start_date, :end_date, :limit, :category_id, :include_subcategories + attr_accessor :type, + :data, + :y_titles, + :start_date, + :end_date, + :limit, + :category_id, + :include_subcategories def initialize(type) @type = type @@ -20,7 +26,7 @@ class IncomingLinksReport ytitles: self.y_titles, data: self.data, start_date: start_date, - end_date: end_date + end_date: end_date, } end @@ -46,18 +52,31 @@ class IncomingLinksReport report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.num_clicks") report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") - num_clicks = link_count_per_user(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id, include_subcategories: report.include_subcategories) - num_topics = topic_count_per_user(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id, include_subcategories: report.include_subcategories) - user_id_lookup = User - .where(username: num_clicks.keys) - .select(:id, :username, :uploaded_avatar_id) - .inject({}) { |sum, v| - sum[v.username] = { - id: v.id, - user_avatar_template: User.avatar_template(v.username, v.uploaded_avatar_id) - } - sum - } + num_clicks = + link_count_per_user( + start_date: report.start_date, + end_date: report.end_date, + category_id: report.category_id, + include_subcategories: report.include_subcategories, + ) + num_topics = + topic_count_per_user( + start_date: report.start_date, + end_date: report.end_date, + category_id: report.category_id, + include_subcategories: report.include_subcategories, + ) + user_id_lookup = + User + .where(username: num_clicks.keys) + .select(:id, :username, :uploaded_avatar_id) + .inject({}) do |sum, v| + sum[v.username] = { + id: v.id, + user_avatar_template: User.avatar_template(v.username, v.uploaded_avatar_id), + } + sum + end report.data = [] num_clicks.each_key do |username| @@ -66,7 +85,7 @@ class IncomingLinksReport user_id: user_id_lookup[username][:id], user_avatar_template: user_id_lookup[username][:user_avatar_template], num_clicks: num_clicks[username], - num_topics: num_topics[username] + num_topics: num_topics[username], } end report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] @@ -74,17 +93,31 @@ class IncomingLinksReport def self.per_user(start_date:, end_date:, category_id:, include_subcategories:) public_incoming_links(category_id: category_id, include_subcategories: include_subcategories) - .where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND incoming_links.user_id IS NOT NULL', start_date, end_date) + .where( + "incoming_links.created_at > ? AND incoming_links.created_at < ? AND incoming_links.user_id IS NOT NULL", + start_date, + end_date, + ) .joins(:user) - .group('users.username') + .group("users.username") end def self.link_count_per_user(start_date:, end_date:, category_id:, include_subcategories:) - per_user(start_date: start_date, end_date: end_date, category_id: category_id, include_subcategories: include_subcategories).count + per_user( + start_date: start_date, + end_date: end_date, + category_id: category_id, + include_subcategories: include_subcategories, + ).count end def self.topic_count_per_user(start_date:, end_date:, category_id:, include_subcategories:) - per_user(start_date: start_date, end_date: end_date, category_id: category_id, include_subcategories: include_subcategories).joins(:post).count("DISTINCT posts.topic_id") + per_user( + start_date: start_date, + end_date: end_date, + category_id: category_id, + include_subcategories: include_subcategories, + ).joins(:post).count("DISTINCT posts.topic_id") end # Return top 10 domains that brought traffic to the site within the last 30 days @@ -93,30 +126,58 @@ class IncomingLinksReport report.y_titles[:num_topics] = I18n.t("reports.#{report.type}.num_topics") report.y_titles[:num_users] = I18n.t("reports.#{report.type}.num_users") - num_clicks = link_count_per_domain(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id, include_subcategories: report.include_subcategories) - num_topics = topic_count_per_domain(num_clicks.keys, category_id: report.category_id, include_subcategories: report.include_subcategories) + num_clicks = + link_count_per_domain( + start_date: report.start_date, + end_date: report.end_date, + category_id: report.category_id, + include_subcategories: report.include_subcategories, + ) + num_topics = + topic_count_per_domain( + num_clicks.keys, + category_id: report.category_id, + include_subcategories: report.include_subcategories, + ) report.data = [] num_clicks.each_key do |domain| - report.data << { domain: domain, num_clicks: num_clicks[domain], num_topics: num_topics[domain] } + report.data << { + domain: domain, + num_clicks: num_clicks[domain], + num_topics: num_topics[domain], + } end report.data = report.data.sort_by { |x| x[:num_clicks] }.reverse[0, 10] end - def self.link_count_per_domain(limit: 10, start_date:, end_date:, category_id:, include_subcategories:) + def self.link_count_per_domain( + limit: 10, + start_date:, + end_date:, + category_id:, + include_subcategories: + ) public_incoming_links(category_id: category_id, include_subcategories: include_subcategories) - .where('incoming_links.created_at > ? AND incoming_links.created_at < ?', start_date, end_date) + .where( + "incoming_links.created_at > ? AND incoming_links.created_at < ?", + start_date, + end_date, + ) .joins(incoming_referer: :incoming_domain) - .group('incoming_domains.name') - .order('count_all DESC') + .group("incoming_domains.name") + .order("count_all DESC") .limit(limit) .count end def self.per_domain(domains, options = {}) - public_incoming_links(category_id: options[:category_id], include_subcategories: options[:include_subcategories]) + public_incoming_links( + category_id: options[:category_id], + include_subcategories: options[:include_subcategories], + ) .joins(incoming_referer: :incoming_domain) - .where('incoming_links.created_at > ? AND incoming_domains.name IN (?)', 30.days.ago, domains) - .group('incoming_domains.name') + .where("incoming_links.created_at > ? AND incoming_domains.name IN (?)", 30.days.ago, domains) + .group("incoming_domains.name") end def self.topic_count_per_domain(domains, options = {}) @@ -126,17 +187,38 @@ class IncomingLinksReport def self.report_top_referred_topics(report) report.y_titles[:num_clicks] = I18n.t("reports.#{report.type}.labels.num_clicks") - num_clicks = link_count_per_topic(start_date: report.start_date, end_date: report.end_date, category_id: report.category_id, include_subcategories: report.include_subcategories) + num_clicks = + link_count_per_topic( + start_date: report.start_date, + end_date: report.end_date, + category_id: report.category_id, + include_subcategories: report.include_subcategories, + ) num_clicks = num_clicks.to_a.sort_by { |x| x[1] }.last(report.limit || 10).reverse report.data = [] - topics = Topic.select('id, slug, title').where('id in (?)', num_clicks.map { |z| z[0] }) + topics = Topic.select("id, slug, title").where("id in (?)", num_clicks.map { |z| z[0] }) if report.category_id - topics = topics.where(category_id: report.include_subcategories ? Category.subcategory_ids(report.category_id) : report.category_id) + topics = + topics.where( + category_id: + ( + if report.include_subcategories + Category.subcategory_ids(report.category_id) + else + report.category_id + end + ), + ) end num_clicks.each do |topic_id, num_clicks_element| topic = topics.find { |t| t.id == topic_id } if topic - report.data << { topic_id: topic_id, topic_title: topic.title, topic_url: topic.relative_url, num_clicks: num_clicks_element } + report.data << { + topic_id: topic_id, + topic_title: topic.title, + topic_url: topic.relative_url, + num_clicks: num_clicks_element, + } end end report.data @@ -144,15 +226,17 @@ class IncomingLinksReport def self.link_count_per_topic(start_date:, end_date:, category_id:, include_subcategories:) public_incoming_links(category_id: category_id, include_subcategories: include_subcategories) - .where('incoming_links.created_at > ? AND incoming_links.created_at < ? AND topic_id IS NOT NULL', start_date, end_date) - .group('topic_id') + .where( + "incoming_links.created_at > ? AND incoming_links.created_at < ? AND topic_id IS NOT NULL", + start_date, + end_date, + ) + .group("topic_id") .count end def self.public_incoming_links(category_id: nil, include_subcategories: nil) - links = IncomingLink - .joins(post: :topic) - .where("topics.archetype = ?", Archetype.default) + links = IncomingLink.joins(post: :topic).where("topics.archetype = ?", Archetype.default) if category_id if include_subcategories diff --git a/app/models/invite.rb b/app/models/invite.rb index 2712760ab1..dae62e3231 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -1,25 +1,26 @@ # frozen_string_literal: true class Invite < ActiveRecord::Base - class UserExists < StandardError; end - class RedemptionFailed < StandardError; end - class ValidationFailed < StandardError; end + class UserExists < StandardError + end + class RedemptionFailed < StandardError + end + class ValidationFailed < StandardError + end include RateLimiter::OnCreateRecord include Trashable # TODO(2021-05-22): remove - self.ignored_columns = %w{ - user_id - redeemed_at - } + self.ignored_columns = %w[user_id redeemed_at] BULK_INVITE_EMAIL_LIMIT = 200 - DOMAIN_REGEX = /\A(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])\z/ + DOMAIN_REGEX = + /\A(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])\z/ rate_limit :limit_invites_per_day - belongs_to :invited_by, class_name: 'User' + belongs_to :invited_by, class_name: "User" has_many :invited_users has_many :users, through: :invited_users @@ -42,19 +43,16 @@ class Invite < ActiveRecord::Base end before_save do - if will_save_change_to_email? - self.email_token = email.present? ? SecureRandom.hex : nil - end + self.email_token = email.present? ? SecureRandom.hex : nil if will_save_change_to_email? end - before_validation do - self.email = Email.downcase(email) unless email.nil? - end + before_validation { self.email = Email.downcase(email) unless email.nil? } attribute :email_already_exists def self.emailed_status_types - @emailed_status_types ||= Enum.new(not_required: 0, pending: 1, bulk_pending: 2, sending: 3, sent: 4) + @emailed_status_types ||= + Enum.new(not_required: 0, pending: 1, bulk_pending: 2, sending: 3, sent: 4) end def user_doesnt_already_exist @@ -69,9 +67,7 @@ class Invite < ActiveRecord::Base end def email_xor_domain - if email.present? && domain.present? - errors.add(:base, I18n.t('invite.email_xor_domain')) - end + errors.add(:base, I18n.t("invite.email_xor_domain")) if email.present? && domain.present? end # Even if a domain is specified on the invite, it still counts as @@ -107,7 +103,7 @@ class Invite < ActiveRecord::Base end def domain_matches?(email) - _, domain = email.split('@') + _, domain = email.split("@") self.domain == domain end @@ -124,8 +120,11 @@ class Invite < ActiveRecord::Base end def link(with_email_token: false) - with_email_token ? "#{Discourse.base_url}/invites/#{invite_key}?t=#{email_token}" - : "#{Discourse.base_url}/invites/#{invite_key}" + if with_email_token + "#{Discourse.base_url}/invites/#{invite_key}?t=#{email_token}" + else + "#{Discourse.base_url}/invites/#{invite_key}" + end end def link_valid? @@ -140,11 +139,12 @@ class Invite < ActiveRecord::Base raise UserExists.new(new.user_exists_error_msg(email)) if find_user_by_email(email) if email.present? - invite = Invite - .with_deleted - .where(email: email, invited_by_id: invited_by.id) - .order('created_at DESC') - .first + invite = + Invite + .with_deleted + .where(email: email, invited_by_id: invited_by.id) + .order("created_at DESC") + .first if invite && (invite.expired? || invite.deleted_at) invite.destroy @@ -154,25 +154,27 @@ class Invite < ActiveRecord::Base RateLimiter.new(invited_by, "reinvites-per-day-#{email_digest}", 3, 1.day.to_i).performed! end - emailed_status = if opts[:skip_email] || invite&.emailed_status == emailed_status_types[:not_required] - emailed_status_types[:not_required] - elsif opts[:emailed_status].present? - opts[:emailed_status] - elsif email.present? - emailed_status_types[:pending] - else - emailed_status_types[:not_required] - end + emailed_status = + if opts[:skip_email] || invite&.emailed_status == emailed_status_types[:not_required] + emailed_status_types[:not_required] + elsif opts[:emailed_status].present? + opts[:emailed_status] + elsif email.present? + emailed_status_types[:pending] + else + emailed_status_types[:not_required] + end if invite invite.update_columns( created_at: Time.zone.now, updated_at: Time.zone.now, expires_at: opts[:expires_at] || SiteSetting.invite_expiry_days.days.from_now, - emailed_status: emailed_status + emailed_status: emailed_status, ) else - create_args = opts.slice(:email, :domain, :moderator, :custom_message, :max_redemptions_allowed) + create_args = + opts.slice(:email, :domain, :moderator, :custom_message, :max_redemptions_allowed) create_args[:invited_by] = invited_by create_args[:email] = email create_args[:emailed_status] = emailed_status @@ -182,15 +184,11 @@ class Invite < ActiveRecord::Base end topic_id = opts[:topic]&.id || opts[:topic_id] - if topic_id.present? - invite.topic_invites.find_or_create_by!(topic_id: topic_id) - end + invite.topic_invites.find_or_create_by!(topic_id: topic_id) if topic_id.present? group_ids = opts[:group_ids] if group_ids.present? - group_ids.each do |group_id| - invite.invited_groups.find_or_create_by!(group_id: group_id) - end + group_ids.each { |group_id| invite.invited_groups.find_or_create_by!(group_id: group_id) } end if emailed_status == emailed_status_types[:pending] @@ -224,7 +222,7 @@ class Invite < ActiveRecord::Base ip_address: ip_address, session: session, email_token: email_token, - redeeming_user: redeeming_user + redeeming_user: redeeming_user, ).redeem end @@ -241,35 +239,37 @@ class Invite < ActiveRecord::Base end def self.pending(inviter) - Invite.distinct + Invite + .distinct .joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id") .joins("LEFT JOIN users ON invited_users.user_id = users.id") .where(invited_by_id: inviter.id) - .where('redemption_count < max_redemptions_allowed') - .where('expires_at > ?', Time.zone.now) - .order('invites.updated_at DESC') + .where("redemption_count < max_redemptions_allowed") + .where("expires_at > ?", Time.zone.now) + .order("invites.updated_at DESC") end def self.expired(inviter) - Invite.distinct + Invite + .distinct .joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id") .joins("LEFT JOIN users ON invited_users.user_id = users.id") .where(invited_by_id: inviter.id) - .where('redemption_count < max_redemptions_allowed') - .where('expires_at < ?', Time.zone.now) - .order('invites.expires_at ASC') + .where("redemption_count < max_redemptions_allowed") + .where("expires_at < ?", Time.zone.now) + .order("invites.expires_at ASC") end def self.redeemed_users(inviter) InvitedUser .joins("LEFT JOIN invites ON invites.id = invited_users.invite_id") .includes(user: :user_stat) - .where('invited_users.user_id IS NOT NULL') - .where('invites.invited_by_id = ?', inviter.id) - .order('invited_users.redeemed_at DESC') - .references('invite') - .references('user') - .references('user_stat') + .where("invited_users.user_id IS NOT NULL") + .where("invites.invited_by_id = ?", inviter.id) + .order("invited_users.redeemed_at DESC") + .references("invite") + .references("user") + .references("user_stat") end def self.invalidate_for_email(email) @@ -280,7 +280,11 @@ class Invite < ActiveRecord::Base end def resend_invite - self.update_columns(updated_at: Time.zone.now, invalidated_at: nil, expires_at: SiteSetting.invite_expiry_days.days.from_now) + self.update_columns( + updated_at: Time.zone.now, + invalidated_at: nil, + expires_at: SiteSetting.invite_expiry_days.days.from_now, + ) Jobs.enqueue(:invite_email, invite_id: self.id) end @@ -289,27 +293,48 @@ class Invite < ActiveRecord::Base end def self.base_directory - File.join(Rails.root, "public", "uploads", "csv", RailsMultisite::ConnectionManagement.current_db) + File.join( + Rails.root, + "public", + "uploads", + "csv", + RailsMultisite::ConnectionManagement.current_db, + ) end def ensure_max_redemptions_allowed if self.max_redemptions_allowed.nil? self.max_redemptions_allowed = 1 else - limit = invited_by&.staff? ? SiteSetting.invite_link_max_redemptions_limit - : SiteSetting.invite_link_max_redemptions_limit_users + limit = + ( + if invited_by&.staff? + SiteSetting.invite_link_max_redemptions_limit + else + SiteSetting.invite_link_max_redemptions_limit_users + end + ) if self.email.present? && self.max_redemptions_allowed != 1 errors.add(:max_redemptions_allowed, I18n.t("invite.max_redemptions_allowed_one")) elsif !self.max_redemptions_allowed.between?(1, limit) - errors.add(:max_redemptions_allowed, I18n.t("invite_link.max_redemptions_limit", max_limit: limit)) + errors.add( + :max_redemptions_allowed, + I18n.t("invite_link.max_redemptions_limit", max_limit: limit), + ) end end end def valid_redemption_count if self.redemption_count > self.max_redemptions_allowed - errors.add(:redemption_count, I18n.t("invite.redemption_count_less_than_max", max_redemptions_allowed: self.max_redemptions_allowed)) + errors.add( + :redemption_count, + I18n.t( + "invite.redemption_count_less_than_max", + max_redemptions_allowed: self.max_redemptions_allowed, + ), + ) end end @@ -319,7 +344,7 @@ class Invite < ActiveRecord::Base self.domain.downcase! if self.domain !~ Invite::DOMAIN_REGEX - self.errors.add(:base, I18n.t('invite.domain_not_allowed')) + self.errors.add(:base, I18n.t("invite.domain_not_allowed")) end end diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index 6a7ae4b22a..3fa9ac7313 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -15,15 +15,15 @@ # (email IS NULL) on the Invite model. class InviteRedeemer attr_reader :invite, - :email, - :username, - :name, - :password, - :user_custom_fields, - :ip_address, - :session, - :email_token, - :redeeming_user + :email, + :username, + :name, + :password, + :user_custom_fields, + :ip_address, + :session, + :email_token, + :redeeming_user def initialize( invite:, @@ -35,7 +35,8 @@ class InviteRedeemer ip_address: nil, session: nil, email_token: nil, - redeeming_user: nil) + redeeming_user: nil + ) @invite = invite @username = username @name = name @@ -81,8 +82,19 @@ class InviteRedeemer # This will _never_ be called if there is a redeeming_user being passed # in to InviteRedeemer -- see invited_user below. - def self.create_user_from_invite(email:, invite:, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil, email_token: nil) - if username && UsernameValidator.new(username).valid_format? && User.username_available?(username, email) + def self.create_user_from_invite( + email:, + invite:, + username: nil, + name: nil, + password: nil, + user_custom_fields: nil, + ip_address: nil, + session: nil, + email_token: nil + ) + if username && UsernameValidator.new(username).valid_format? && + User.username_available?(username, email) available_username = username else available_username = UserNameSuggester.suggest(email) @@ -99,12 +111,11 @@ class InviteRedeemer active: false, trust_level: SiteSetting.default_invitee_trust_level, ip_address: ip_address, - registration_ip_address: ip_address + registration_ip_address: ip_address, } if (!SiteSetting.must_approve_users && SiteSetting.invite_only) || - (SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email)) - + (SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email)) ReviewableUser.set_approved_fields!(user, Discourse.system_user) end @@ -115,7 +126,9 @@ class InviteRedeemer user_fields.each do |f| field_val = field_params[f.id.to_s] - fields["#{User::USER_FIELD_PREFIX}#{f.id}"] = field_val[0...UserField.max_length] unless field_val.blank? + fields["#{User::USER_FIELD_PREFIX}#{f.id}"] = field_val[ + 0...UserField.max_length + ] unless field_val.blank? end user.custom_fields = fields end @@ -143,9 +156,7 @@ class InviteRedeemer authenticator.finish if invite.emailed_status != Invite.emailed_status_types[:not_required] && - email == invite.email && - invite.email_token.present? && - email_token == invite.email_token + email == invite.email && invite.email_token.present? && email_token == invite.email_token user.activate end @@ -159,9 +170,7 @@ class InviteRedeemer return false if email.blank? # Invite scoped to email has already been redeemed by anyone. - if invite.is_email_invite? && InvitedUser.exists?(invite_id: invite.id) - return false - end + return false if invite.is_email_invite? && InvitedUser.exists?(invite_id: invite.id) # The email will be present for either an invite link (where the user provides # us the email manually) or for an invite scoped to an email, where we @@ -171,17 +180,18 @@ class InviteRedeemer email_to_check = redeeming_user&.email || email if invite.email.present? && !invite.email_matches?(email_to_check) - raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.not_matching_email')) + raise ActiveRecord::RecordNotSaved.new(I18n.t("invite.not_matching_email")) end if invite.domain.present? && !invite.domain_matches?(email_to_check) - raise ActiveRecord::RecordNotSaved.new(I18n.t('invite.domain_not_allowed')) + raise ActiveRecord::RecordNotSaved.new(I18n.t("invite.domain_not_allowed")) end # Anon user is trying to redeem an invitation, if an existing user already # redeemed it then we cannot redeem now. redeeming_user ||= User.where(admin: false, staged: false).find_by_email(email) - if redeeming_user.present? && InvitedUser.exists?(user_id: redeeming_user.id, invite_id: invite.id) + if redeeming_user.present? && + InvitedUser.exists?(user_id: redeeming_user.id, invite_id: invite.id) return false end @@ -205,17 +215,18 @@ class InviteRedeemer # If there was no logged in user then we must attempt to create # one based on the provided params. - invited_user ||= InviteRedeemer.create_user_from_invite( - email: email, - invite: invite, - username: username, - name: name, - password: password, - user_custom_fields: user_custom_fields, - ip_address: ip_address, - session: session, - email_token: email_token - ) + invited_user ||= + InviteRedeemer.create_user_from_invite( + email: email, + invite: invite, + username: username, + name: name, + password: password, + user_custom_fields: user_custom_fields, + ip_address: ip_address, + session: session, + email_token: email_token, + ) invited_user.send_welcome_message = false @invited_user = invited_user @invited_user @@ -243,11 +254,13 @@ class InviteRedeemer # Should not happen because of ensure_email_is_present!, but better to cover bases. return if email.blank? - topic_ids = TopicInvite.joins(:invite) - .joins(:topic) - .where("topics.archetype = ?", Archetype::private_message) - .where("invites.email = ?", email) - .pluck(:topic_id) + topic_ids = + TopicInvite + .joins(:invite) + .joins(:topic) + .where("topics.archetype = ?", Archetype.private_message) + .where("invites.email = ?", email) + .pluck(:topic_id) topic_ids.each do |id| if !TopicAllowedUser.exists?(user_id: invited_user.id, topic_id: id) TopicAllowedUser.create!(user_id: invited_user.id, topic_id: id) @@ -277,7 +290,7 @@ class InviteRedeemer return if invite.invited_by.blank? invite.invited_by.notifications.create!( notification_type: Notification.types[:invitee_accepted], - data: { display_username: invited_user.username }.to_json + data: { display_username: invited_user.username }.to_json, ) end @@ -286,10 +299,10 @@ class InviteRedeemer return if email.blank? Invite - .where('invites.max_redemptions_allowed = 1') + .where("invites.max_redemptions_allowed = 1") .joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id") - .where('invited_users.user_id IS NULL') - .where('invites.email = ? AND invites.id != ?', email, invite.id) + .where("invited_users.user_id IS NULL") + .where("invites.email = ? AND invites.id != ?", email, invite.id) .delete_all end end diff --git a/app/models/like_notification_frequency_site_setting.rb b/app/models/like_notification_frequency_site_setting.rb index 19d7043627..5d8622e7b5 100644 --- a/app/models/like_notification_frequency_site_setting.rb +++ b/app/models/like_notification_frequency_site_setting.rb @@ -1,23 +1,20 @@ # frozen_string_literal: true class LikeNotificationFrequencySiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.like_notification_frequency.always', value: 0 }, - { name: 'user.like_notification_frequency.first_time_and_daily', value: 1 }, - { name: 'user.like_notification_frequency.first_time', value: 2 }, - { name: 'user.like_notification_frequency.never', value: 3 }, + { name: "user.like_notification_frequency.always", value: 0 }, + { name: "user.like_notification_frequency.first_time_and_daily", value: 1 }, + { name: "user.like_notification_frequency.first_time", value: 2 }, + { name: "user.like_notification_frequency.never", value: 3 }, ] end def self.translate_names? true end - end diff --git a/app/models/locale_site_setting.rb b/app/models/locale_site_setting.rb index 6843187164..8d48568d67 100644 --- a/app/models/locale_site_setting.rb +++ b/app/models/locale_site_setting.rb @@ -1,19 +1,16 @@ # frozen_string_literal: true class LocaleSiteSetting < EnumSiteSetting - def self.valid_value?(val) supported_locales.include?(val) end def self.values - @values ||= supported_locales.map do |locale| - lang = language_names[locale] || language_names[locale.split("_")[0]] - { - name: lang ? lang['nativeName'] : locale, - value: locale - } - end + @values ||= + supported_locales.map do |locale| + lang = language_names[locale] || language_names[locale.split("_")[0]] + { name: lang ? lang["nativeName"] : locale, value: locale } + end end @lock = Mutex.new @@ -22,42 +19,41 @@ class LocaleSiteSetting < EnumSiteSetting return @language_names if @language_names @lock.synchronize do - @language_names ||= begin - names = YAML.safe_load(File.read(File.join(Rails.root, 'config', 'locales', 'names.yml'))) + @language_names ||= + begin + names = YAML.safe_load(File.read(File.join(Rails.root, "config", "locales", "names.yml"))) - DiscoursePluginRegistry.locales.each do |locale, options| - if !names.key?(locale) && options[:name] && options[:nativeName] - names[locale] = { "name" => options[:name], "nativeName" => options[:nativeName] } + DiscoursePluginRegistry.locales.each do |locale, options| + if !names.key?(locale) && options[:name] && options[:nativeName] + names[locale] = { "name" => options[:name], "nativeName" => options[:nativeName] } + end end - end - names - end + names + end end end def self.supported_locales @lock.synchronize do - @supported_locales ||= begin - locales = Dir.glob( - File.join(Rails.root, 'config', 'locales', 'client.*.yml') - ).map { |x| x.split('.')[-2] } + @supported_locales ||= + begin + locales = + Dir + .glob(File.join(Rails.root, "config", "locales", "client.*.yml")) + .map { |x| x.split(".")[-2] } - locales += DiscoursePluginRegistry.locales.keys - locales.uniq.sort - end + locales += DiscoursePluginRegistry.locales.keys + locales.uniq.sort + end end end def self.reset! - @lock.synchronize do - @values = @language_names = @supported_locales = nil - end + @lock.synchronize { @values = @language_names = @supported_locales = nil } end - FALLBACKS ||= { - en_GB: :en - } + FALLBACKS ||= { en_GB: :en } def self.fallback_locale(locale) fallback_locale = FALLBACKS[locale.to_sym] diff --git a/app/models/mailing_list_mode_site_setting.rb b/app/models/mailing_list_mode_site_setting.rb index 2192a6fbf7..859b780dd3 100644 --- a/app/models/mailing_list_mode_site_setting.rb +++ b/app/models/mailing_list_mode_site_setting.rb @@ -2,14 +2,13 @@ class MailingListModeSiteSetting < EnumSiteSetting def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.mailing_list_mode.individual', value: 1 }, - { name: 'user.mailing_list_mode.individual_no_echo', value: 2 } + { name: "user.mailing_list_mode.individual", value: 1 }, + { name: "user.mailing_list_mode.individual_no_echo", value: 2 }, ] end diff --git a/app/models/muted_user.rb b/app/models/muted_user.rb index 4be90200cd..dd0a544c63 100644 --- a/app/models/muted_user.rb +++ b/app/models/muted_user.rb @@ -2,7 +2,7 @@ class MutedUser < ActiveRecord::Base belongs_to :user - belongs_to :muted_user, class_name: 'User' + belongs_to :muted_user, class_name: "User" end # == Schema Information diff --git a/app/models/navigation_menu_site_setting.rb b/app/models/navigation_menu_site_setting.rb index d71cac2fdd..bc7b38540d 100644 --- a/app/models/navigation_menu_site_setting.rb +++ b/app/models/navigation_menu_site_setting.rb @@ -13,7 +13,7 @@ class NavigationMenuSiteSetting < EnumSiteSetting @values ||= [ { name: "admin.navigation_menu.sidebar", value: SIDEBAR }, { name: "admin.navigation_menu.header_dropdown", value: HEADER_DROPDOWN }, - { name: "admin.navigation_menu.legacy", value: LEGACY } + { name: "admin.navigation_menu.legacy", value: LEGACY }, ] end diff --git a/app/models/new_topic_duration_site_setting.rb b/app/models/new_topic_duration_site_setting.rb index 3ed58210ae..e1f5b1fdae 100644 --- a/app/models/new_topic_duration_site_setting.rb +++ b/app/models/new_topic_duration_site_setting.rb @@ -1,25 +1,22 @@ # frozen_string_literal: true class NewTopicDurationSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.new_topic_duration.not_viewed', value: -1 }, - { name: 'user.new_topic_duration.after_1_day', value: 60 * 24 }, - { name: 'user.new_topic_duration.after_2_days', value: 60 * 24 * 2 }, - { name: 'user.new_topic_duration.after_1_week', value: 60 * 24 * 7 }, - { name: 'user.new_topic_duration.after_2_weeks', value: 60 * 24 * 7 * 2 }, - { name: 'user.new_topic_duration.last_here', value: -2 }, + { name: "user.new_topic_duration.not_viewed", value: -1 }, + { name: "user.new_topic_duration.after_1_day", value: 60 * 24 }, + { name: "user.new_topic_duration.after_2_days", value: 60 * 24 * 2 }, + { name: "user.new_topic_duration.after_1_week", value: 60 * 24 * 7 }, + { name: "user.new_topic_duration.after_2_weeks", value: 60 * 24 * 7 * 2 }, + { name: "user.new_topic_duration.last_here", value: -2 }, ] end def self.translate_names? true end - end diff --git a/app/models/notification.rb b/app/models/notification.rb index c4843a70e6..f8aeab2b73 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -12,48 +12,58 @@ class Notification < ActiveRecord::Base validates_presence_of :notification_type scope :unread, lambda { where(read: false) } - scope :recent, lambda { |n = nil| n ||= 10; order('notifications.created_at desc').limit(n) } - scope :visible , lambda { joins('LEFT JOIN topics ON notifications.topic_id = topics.id') - .where('topics.id IS NULL OR topics.deleted_at IS NULL') } - scope :unread_type, ->(user, type, limit = 30) do - unread_types(user, [type], limit) - end - scope :unread_types, ->(user, types, limit = 30) do - where(user_id: user.id, read: false, notification_type: types) - .visible - .includes(:topic) - .limit(limit) - end - scope :prioritized, ->(deprioritized_types = []) do - scope = order("notifications.high_priority AND NOT notifications.read DESC") - if deprioritized_types.present? - scope = scope.order(DB.sql_fragment("NOT notifications.read AND notifications.notification_type NOT IN (?) DESC", deprioritized_types)) - else - scope = scope.order("NOT notifications.read DESC") - end - scope.order("notifications.created_at DESC") - end - scope :for_user_menu, ->(user_id, limit: 30) do - where(user_id: user_id) - .visible - .prioritized - .includes(:topic) - .limit(limit) - end + scope :recent, + lambda { |n = nil| + n ||= 10 + order("notifications.created_at desc").limit(n) + } + scope :visible, + lambda { + joins("LEFT JOIN topics ON notifications.topic_id = topics.id").where( + "topics.id IS NULL OR topics.deleted_at IS NULL", + ) + } + scope :unread_type, ->(user, type, limit = 30) { unread_types(user, [type], limit) } + scope :unread_types, + ->(user, types, limit = 30) { + where(user_id: user.id, read: false, notification_type: types) + .visible + .includes(:topic) + .limit(limit) + } + scope :prioritized, + ->(deprioritized_types = []) { + scope = order("notifications.high_priority AND NOT notifications.read DESC") + if deprioritized_types.present? + scope = + scope.order( + DB.sql_fragment( + "NOT notifications.read AND notifications.notification_type NOT IN (?) DESC", + deprioritized_types, + ), + ) + else + scope = scope.order("NOT notifications.read DESC") + end + scope.order("notifications.created_at DESC") + } + scope :for_user_menu, + ->(user_id, limit: 30) { + where(user_id: user_id).visible.prioritized.includes(:topic).limit(limit) + } attr_accessor :skip_send_email - after_commit :refresh_notification_count, on: [:create, :update, :destroy] + after_commit :refresh_notification_count, on: %i[create update destroy] after_commit :send_email, on: :create - after_commit(on: :create) do - DiscourseEvent.trigger(:notification_created, self) - end + after_commit(on: :create) { DiscourseEvent.trigger(:notification_created, self) } before_create do # if we have manually set the notification to high_priority on create then # make sure that is respected - self.high_priority = self.high_priority || Notification.high_priority_types.include?(self.notification_type) + self.high_priority = + self.high_priority || Notification.high_priority_types.include?(self.notification_type) end def self.consolidate_or_create!(notification_params) @@ -103,54 +113,53 @@ class Notification < ActiveRecord::Base end def self.types - @types ||= Enum.new(mentioned: 1, - replied: 2, - quoted: 3, - edited: 4, - liked: 5, - private_message: 6, - invited_to_private_message: 7, - invitee_accepted: 8, - posted: 9, - moved_post: 10, - linked: 11, - granted_badge: 12, - invited_to_topic: 13, - custom: 14, - group_mentioned: 15, - group_message_summary: 16, - watching_first_post: 17, - topic_reminder: 18, - liked_consolidated: 19, - post_approved: 20, - code_review_commit_approved: 21, - membership_request_accepted: 22, - membership_request_consolidated: 23, - bookmark_reminder: 24, - reaction: 25, - votes_released: 26, - event_reminder: 27, - event_invitation: 28, - chat_mention: 29, - chat_message: 30, - chat_invitation: 31, - chat_group_mention: 32, # March 2022 - This is obsolete, as all chat_mentions use `chat_mention` type - chat_quoted: 33, - assigned: 34, - question_answer_user_commented: 35, # Used by https://github.com/discourse/discourse-question-answer - watching_category_or_tag: 36, - new_features: 37, - following: 800, # Used by https://github.com/discourse/discourse-follow - following_created_topic: 801, # Used by https://github.com/discourse/discourse-follow - following_replied: 802, # Used by https://github.com/discourse/discourse-follow - ) + @types ||= + Enum.new( + mentioned: 1, + replied: 2, + quoted: 3, + edited: 4, + liked: 5, + private_message: 6, + invited_to_private_message: 7, + invitee_accepted: 8, + posted: 9, + moved_post: 10, + linked: 11, + granted_badge: 12, + invited_to_topic: 13, + custom: 14, + group_mentioned: 15, + group_message_summary: 16, + watching_first_post: 17, + topic_reminder: 18, + liked_consolidated: 19, + post_approved: 20, + code_review_commit_approved: 21, + membership_request_accepted: 22, + membership_request_consolidated: 23, + bookmark_reminder: 24, + reaction: 25, + votes_released: 26, + event_reminder: 27, + event_invitation: 28, + chat_mention: 29, + chat_message: 30, + chat_invitation: 31, + chat_group_mention: 32, # March 2022 - This is obsolete, as all chat_mentions use `chat_mention` type + chat_quoted: 33, + assigned: 34, + question_answer_user_commented: 35, # Used by https://github.com/discourse/discourse-question-answer + watching_category_or_tag: 36, + new_features: 37, + following: 800, # Used by https://github.com/discourse/discourse-follow + following_created_topic: 801, # Used by https://github.com/discourse/discourse-follow + following_replied: 802, # Used by https://github.com/discourse/discourse-follow + ) end def self.high_priority_types - @high_priority_types ||= [ - types[:private_message], - types[:bookmark_reminder] - ] + @high_priority_types ||= [types[:private_message], types[:bookmark_reminder]] end def self.normal_priority_types @@ -158,24 +167,16 @@ class Notification < ActiveRecord::Base end def self.mark_posts_read(user, topic_id, post_numbers) - Notification - .where( - user_id: user.id, - topic_id: topic_id, - post_number: post_numbers, - read: false - ) - .update_all(read: true) + Notification.where( + user_id: user.id, + topic_id: topic_id, + post_number: post_numbers, + read: false, + ).update_all(read: true) end def self.read(user, notification_ids) - Notification - .where( - id: notification_ids, - user_id: user.id, - read: false - ) - .update_all(read: true) + Notification.where(id: notification_ids, user_id: user.id, read: false).update_all(read: true) end def self.read_types(user, types = nil) @@ -185,15 +186,19 @@ class Notification < ActiveRecord::Base end def self.interesting_after(min_date) - result = where("created_at > ?", min_date) - .includes(:topic) - .visible - .unread - .limit(20) - .order("CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1 + result = + where("created_at > ?", min_date) + .includes(:topic) + .visible + .unread + .limit(20) + .order( + "CASE WHEN notification_type = #{Notification.types[:replied]} THEN 1 WHEN notification_type = #{Notification.types[:mentioned]} THEN 2 ELSE 3 - END, created_at DESC").to_a + END, created_at DESC", + ) + .to_a # Remove any duplicates by type and topic if result.present? @@ -222,14 +227,15 @@ class Notification < ActiveRecord::Base # Be wary of calling this frequently. O(n) JSON parsing can suck. def data_hash - @data_hash ||= begin - return {} if data.blank? + @data_hash ||= + begin + return {} if data.blank? - parsed = JSON.parse(data) - return {} if parsed.blank? + parsed = JSON.parse(data) + return {} if parsed.blank? - parsed.with_indifferent_access - end + parsed.with_indifferent_access + end end def url @@ -245,26 +251,27 @@ class Notification < ActiveRecord::Base [ Notification.types[:liked], Notification.types[:liked_consolidated], - Notification.types[:reaction] + Notification.types[:reaction], ] end def self.prioritized_list(user, count: 30, types: []) return [] if !user&.user_option - notifications = user.notifications - .includes(:topic) - .visible - .prioritized(types.present? ? [] : like_types) - .limit(count) + notifications = + user + .notifications + .includes(:topic) + .visible + .prioritized(types.present? ? [] : like_types) + .limit(count) if types.present? notifications = notifications.where(notification_type: types) - elsif user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never] + elsif user.user_option.like_notification_frequency == + UserOption.like_notification_frequency_type[:never] like_types.each do |notification_type| - notifications = notifications.where( - 'notification_type <> ?', notification_type - ) + notifications = notifications.where("notification_type <> ?", notification_type) end end notifications.to_a @@ -275,20 +282,16 @@ class Notification < ActiveRecord::Base return unless user && user.user_option count ||= 10 - notifications = user.notifications - .visible - .recent(count) - .includes(:topic) + notifications = user.notifications.visible.recent(count).includes(:topic) notifications = notifications.where(notification_type: types) if types.present? - if user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never] + if user.user_option.like_notification_frequency == + UserOption.like_notification_frequency_type[:never] [ Notification.types[:liked], - Notification.types[:liked_consolidated] + Notification.types[:liked_consolidated], ].each do |notification_type| - notifications = notifications.where( - 'notification_type <> ?', notification_type - ) + notifications = notifications.where("notification_type <> ?", notification_type) end end @@ -313,27 +316,30 @@ class Notification < ActiveRecord::Base ids = builder.query_single if ids.length > 0 - notifications += user - .notifications - .order('notifications.created_at DESC') - .where(id: ids) - .joins(:topic) - .limit(count) + notifications += + user + .notifications + .order("notifications.created_at DESC") + .where(id: ids) + .joins(:topic) + .limit(count) end - notifications.uniq(&:id).sort do |x, y| - if x.unread_high_priority? && !y.unread_high_priority? - -1 - elsif y.unread_high_priority? && !x.unread_high_priority? - 1 - else - y.created_at <=> x.created_at + notifications + .uniq(&:id) + .sort do |x, y| + if x.unread_high_priority? && !y.unread_high_priority? + -1 + elsif y.unread_high_priority? && !x.unread_high_priority? + 1 + else + y.created_at <=> x.created_at + end end - end.take(count) + .take(count) else [] end - end def unread_high_priority? @@ -347,19 +353,18 @@ class Notification < ActiveRecord::Base protected def refresh_notification_count - if user_id - User.find_by(id: user_id)&.publish_notifications_state - end + User.find_by(id: user_id)&.publish_notifications_state if user_id end def send_email return if skip_send_email - user.do_not_disturb? ? - ShelvedNotification.create(notification_id: self.id) : + if user.do_not_disturb? + ShelvedNotification.create(notification_id: self.id) + else NotificationEmailer.process_notification(self) + end end - end # == Schema Information diff --git a/app/models/notification_level_when_replying_site_setting.rb b/app/models/notification_level_when_replying_site_setting.rb index c71927815f..e12b97b635 100644 --- a/app/models/notification_level_when_replying_site_setting.rb +++ b/app/models/notification_level_when_replying_site_setting.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true class NotificationLevelWhenReplyingSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.notification_levels @@ -13,14 +11,13 @@ class NotificationLevelWhenReplyingSiteSetting < EnumSiteSetting def self.values @values ||= [ - { name: 'topic.notifications.watching.title', value: notification_levels[:watching] }, - { name: 'topic.notifications.tracking.title', value: notification_levels[:tracking] }, - { name: 'topic.notifications.regular.title', value: notification_levels[:regular] } + { name: "topic.notifications.watching.title", value: notification_levels[:watching] }, + { name: "topic.notifications.tracking.title", value: notification_levels[:tracking] }, + { name: "topic.notifications.regular.title", value: notification_levels[:regular] }, ] end def self.translate_names? true end - end diff --git a/app/models/oauth2_user_info.rb b/app/models/oauth2_user_info.rb index bf758ee911..34a9e62f56 100644 --- a/app/models/oauth2_user_info.rb +++ b/app/models/oauth2_user_info.rb @@ -4,7 +4,11 @@ class Oauth2UserInfo < ActiveRecord::Base belongs_to :user before_save do - Discourse.deprecate("Oauth2UserInfo is deprecated. Use `ManagedAuthenticator` and `UserAssociatedAccount` instead. For more information, see https://meta.discourse.org/t/106695", drop_from: '2.9.0', output_in_test: true) + Discourse.deprecate( + "Oauth2UserInfo is deprecated. Use `ManagedAuthenticator` and `UserAssociatedAccount` instead. For more information, see https://meta.discourse.org/t/106695", + drop_from: "2.9.0", + output_in_test: true, + ) end end diff --git a/app/models/optimized_image.rb b/app/models/optimized_image.rb index 901e762328..dbfa6d10c6 100644 --- a/app/models/optimized_image.rb +++ b/app/models/optimized_image.rb @@ -6,7 +6,7 @@ class OptimizedImage < ActiveRecord::Base # BUMP UP if optimized image algorithm changes VERSION = 2 - URL_REGEX ||= /(\/optimized\/\dX[\/\.\w]*\/([a-zA-Z0-9]+)[\.\w]*)/ + URL_REGEX ||= %r{(/optimized/\dX[/\.\w]*/([a-zA-Z0-9]+)[\.\w]*)} def self.lock(upload_id, width, height) @hostname ||= Discourse.os_hostname @@ -15,14 +15,10 @@ class OptimizedImage < ActiveRecord::Base # # we can not afford this blocking in Sidekiq cause it can lead to starvation if Sidekiq.server? - DistributedMutex.synchronize("optimized_image_#{upload_id}_#{width}_#{height}") do - yield - end + DistributedMutex.synchronize("optimized_image_#{upload_id}_#{width}_#{height}") { yield } else DistributedMutex.synchronize("optimized_image_host_#{@hostname}") do - DistributedMutex.synchronize("optimized_image_#{upload_id}_#{width}_#{height}") do - yield - end + DistributedMutex.synchronize("optimized_image_#{upload_id}_#{width}_#{height}") { yield } end end end @@ -32,9 +28,7 @@ class OptimizedImage < ActiveRecord::Base return if upload.try(:sha1).blank? # no extension so try to guess it - if (!upload.extension) - upload.fix_image_extension - end + upload.fix_image_extension if (!upload.extension) if !upload.extension.match?(IM_DECODERS) && upload.extension != "svg" if !opts[:raise_on_error] @@ -61,7 +55,12 @@ class OptimizedImage < ActiveRecord::Base if original_path.blank? # download is protected with a DistributedMutex - external_copy = Discourse.store.download(upload) rescue nil + external_copy = + begin + Discourse.store.download(upload) + rescue StandardError + nil + end original_path = external_copy.try(:path) end @@ -78,14 +77,13 @@ class OptimizedImage < ActiveRecord::Base # create a temp file with the same extension as the original extension = ".#{opts[:format] || upload.extension}" - if extension.length == 1 - return nil - end + return nil if extension.length == 1 temp_file = Tempfile.new(["discourse-thumbnail", extension]) temp_path = temp_file.path - target_quality = upload.target_image_quality(original_path, SiteSetting.image_preview_jpg_quality) + target_quality = + upload.target_image_quality(original_path, SiteSetting.image_preview_jpg_quality) opts = opts.merge(quality: target_quality) if target_quality if upload.extension == "svg" @@ -98,25 +96,29 @@ class OptimizedImage < ActiveRecord::Base end if resized - thumbnail = OptimizedImage.create!( - upload_id: upload.id, - sha1: Upload.generate_digest(temp_path), - extension: extension, - width: width, - height: height, - url: "", - filesize: File.size(temp_path), - version: VERSION - ) + thumbnail = + OptimizedImage.create!( + upload_id: upload.id, + sha1: Upload.generate_digest(temp_path), + extension: extension, + width: width, + height: height, + url: "", + filesize: File.size(temp_path), + version: VERSION, + ) # store the optimized image and update its url File.open(temp_path) do |file| - url = Discourse.store.store_optimized_image(file, thumbnail, nil, secure: upload.secure?) + url = + Discourse.store.store_optimized_image(file, thumbnail, nil, secure: upload.secure?) if url.present? thumbnail.url = url thumbnail.save else - Rails.logger.error("Failed to store optimized image of size #{width}x#{height} from url: #{upload.url}\nTemp image path: #{temp_path}") + Rails.logger.error( + "Failed to store optimized image of size #{width}x#{height} from url: #{upload.url}\nTemp image path: #{temp_path}", + ) end end end @@ -126,9 +128,7 @@ class OptimizedImage < ActiveRecord::Base end # make sure we remove the cached copy from external stores - if Discourse.store.external? - external_copy&.close - end + external_copy&.close if Discourse.store.external? thumbnail end @@ -142,7 +142,7 @@ class OptimizedImage < ActiveRecord::Base end def local? - !(url =~ /^(https?:)?\/\//) + !(url =~ %r{^(https?:)?//}) end def calculate_filesize @@ -162,9 +162,7 @@ class OptimizedImage < ActiveRecord::Base size = calculate_filesize write_attribute(:filesize, size) - if !new_record? - update_columns(filesize: size) - end + update_columns(filesize: size) if !new_record? size end end @@ -173,14 +171,12 @@ class OptimizedImage < ActiveRecord::Base # this matches instructions which call #to_s path = path.to_s return false if path != File.expand_path(path) - return false if path !~ /\A[\w\-\.\/]+\z/m + return false if path !~ %r{\A[\w\-\./]+\z}m true end def self.ensure_safe_paths!(*paths) - paths.each do |path| - raise Discourse::InvalidAccess unless safe_path?(path) - end + paths.each { |path| raise Discourse::InvalidAccess unless safe_path?(path) } end IM_DECODERS ||= /\A(jpe?g|png|ico|gif|webp)\z/i @@ -215,29 +211,35 @@ class OptimizedImage < ActiveRecord::Base from = prepend_decoder!(from, to, opts) to = prepend_decoder!(to, to, opts) - instructions = ['convert', "#{from}[0]"] + instructions = ["convert", "#{from}[0]"] - if opts[:colors] - instructions << "-colors" << opts[:colors].to_s - end + instructions << "-colors" << opts[:colors].to_s if opts[:colors] - if opts[:quality] - instructions << "-quality" << opts[:quality].to_s - end + instructions << "-quality" << opts[:quality].to_s if opts[:quality] # NOTE: ORDER is important! - instructions.concat(%W{ - -auto-orient - -gravity center - -background transparent - -#{thumbnail_or_resize} #{dimensions}^ - -extent #{dimensions} - -interpolate catrom - -unsharp 2x0.5+0.7+0 - -interlace none - -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} - #{to} - }) + instructions.concat( + %W[ + -auto-orient + -gravity + center + -background + transparent + -#{thumbnail_or_resize} + #{dimensions}^ + -extent + #{dimensions} + -interpolate + catrom + -unsharp + 2x0.5+0.7+0 + -interlace + none + -profile + #{File.join(Rails.root, "vendor", "data", "RT_sRGB.icm")} + #{to} + ], + ) end def self.crop_instructions(from, to, dimensions, opts = {}) @@ -250,18 +252,23 @@ class OptimizedImage < ActiveRecord::Base convert #{from}[0] -auto-orient - -gravity north - -background transparent - -#{thumbnail_or_resize} #{dimensions}^ - -crop #{dimensions}+0+0 - -unsharp 2x0.5+0.7+0 - -interlace none - -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} + -gravity + north + -background + transparent + -#{thumbnail_or_resize} + #{dimensions}^ + -crop + #{dimensions}+0+0 + -unsharp + 2x0.5+0.7+0 + -interlace + none + -profile + #{File.join(Rails.root, "vendor", "data", "RT_sRGB.icm")} } - if opts[:quality] - instructions << "-quality" << opts[:quality].to_s - end + instructions << "-quality" << opts[:quality].to_s if opts[:quality] instructions << to end @@ -276,11 +283,16 @@ class OptimizedImage < ActiveRecord::Base convert #{from}[0] -auto-orient - -gravity center - -background transparent - -interlace none - -resize #{dimensions} - -profile #{File.join(Rails.root, 'vendor', 'data', 'RT_sRGB.icm')} + -gravity + center + -background + transparent + -interlace + none + -resize + #{dimensions} + -profile + #{File.join(Rails.root, "vendor", "data", "RT_sRGB.icm")} #{to} } end @@ -308,7 +320,13 @@ class OptimizedImage < ActiveRecord::Base MAX_CONVERT_SECONDS = 20 def self.convert_with(instructions, to, opts = {}) - Discourse::Utils.execute_command("nice", "-n", "10", *instructions, timeout: MAX_CONVERT_SECONDS) + Discourse::Utils.execute_command( + "nice", + "-n", + "10", + *instructions, + timeout: MAX_CONVERT_SECONDS, + ) allow_pngquant = to.downcase.ends_with?(".png") && File.size(to) < MAX_PNGQUANT_SIZE FileHelper.optimize_image!(to, allow_pngquant: allow_pngquant) diff --git a/app/models/permalink.rb b/app/models/permalink.rb index 11ea908d9f..028313fcae 100644 --- a/app/models/permalink.rb +++ b/app/models/permalink.rb @@ -15,15 +15,11 @@ class Permalink < ActiveRecord::Base def initialize(source) @source = source - if source.present? - @rules = source.split("|").map do |rule| - parse_rule(rule) - end.compact - end + @rules = source.split("|").map { |rule| parse_rule(rule) }.compact if source.present? end def parse_rule(rule) - return unless rule =~ /\/.*\// + return unless rule =~ %r{/.*/} escaping = false regex = +"" @@ -41,32 +37,27 @@ class Permalink < ActiveRecord::Base end end - if regex.length > 1 - [Regexp.new(regex[1..-1]), sub[1..-1] || ""] - end - + [Regexp.new(regex[1..-1]), sub[1..-1] || ""] if regex.length > 1 end def normalize(url) return url unless @rules - @rules.each do |(regex, sub)| - url = url.sub(regex, sub) - end + @rules.each { |(regex, sub)| url = url.sub(regex, sub) } url end - end def self.normalize_url(url) if url url = url.strip - url = url[1..-1] if url[0, 1] == '/' + url = url[1..-1] if url[0, 1] == "/" end normalizations = SiteSetting.permalink_normalizations - @normalizer = Normalizer.new(normalizations) unless @normalizer && @normalizer.source == normalizations + @normalizer = Normalizer.new(normalizations) unless @normalizer && + @normalizer.source == normalizations @normalizer.normalize(url) end @@ -88,11 +79,10 @@ class Permalink < ActiveRecord::Base end def self.filter_by(url = nil) - permalinks = Permalink - .includes(:topic, :post, :category, :tag) - .order('permalinks.created_at desc') + permalinks = + Permalink.includes(:topic, :post, :category, :tag).order("permalinks.created_at desc") - permalinks.where!('url ILIKE :url OR external_url ILIKE :url', url: "%#{url}%") if url.present? + permalinks.where!("url ILIKE :url OR external_url ILIKE :url", url: "%#{url}%") if url.present? permalinks.limit!(100) permalinks.to_a end diff --git a/app/models/plugin_store.rb b/app/models/plugin_store.rb index ed8dd6dc00..15504cc857 100644 --- a/app/models/plugin_store.rb +++ b/app/models/plugin_store.rb @@ -31,7 +31,7 @@ class PluginStore end def self.get_all(plugin_name, keys) - rows = PluginStoreRow.where('plugin_name = ? AND key IN (?)', plugin_name, keys).to_a + rows = PluginStoreRow.where("plugin_name = ? AND key IN (?)", plugin_name, keys).to_a Hash[rows.map { |row| [row.key, cast_value(row.type_name, row.value)] }] end @@ -72,10 +72,14 @@ class PluginStore def self.cast_value(type, value) case type - when "Integer", "Fixnum" then value.to_i - when "TrueClass", "FalseClass" then value == "true" - when "JSON" then map_json(::JSON.parse(value)) - else value + when "Integer", "Fixnum" + value.to_i + when "TrueClass", "FalseClass" + value == "true" + when "JSON" + map_json(::JSON.parse(value)) + else + value end end end diff --git a/app/models/post.rb b/app/models/post.rb index 7d1f551414..7aa9155264 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'archetype' -require 'digest/sha1' +require "archetype" +require "digest/sha1" class Post < ActiveRecord::Base include RateLimiter::OnCreateRecord @@ -12,7 +12,7 @@ class Post < ActiveRecord::Base self.ignored_columns = [ "avg_time", # TODO(2021-01-04): remove - "image_url" # TODO(2021-06-01): remove + "image_url", # TODO(2021-06-01): remove ] cattr_accessor :plugin_permitted_create_params, :plugin_permitted_update_params @@ -54,7 +54,7 @@ class Post < ActiveRecord::Base has_many :post_details has_many :post_revisions - has_many :revisions, -> { order(:number) }, foreign_key: :post_id, class_name: 'PostRevision' + has_many :revisions, -> { order(:number) }, foreign_key: :post_id, class_name: "PostRevision" has_many :user_actions, foreign_key: :target_post_id @@ -67,11 +67,17 @@ class Post < ActiveRecord::Base after_commit :index_search # We can pass several creating options to a post via attributes - attr_accessor :image_sizes, :quoted_post_numbers, :no_bump, :invalidate_oneboxes, :cooking_options, :skip_unique_check, :skip_validation + attr_accessor :image_sizes, + :quoted_post_numbers, + :no_bump, + :invalidate_oneboxes, + :cooking_options, + :skip_unique_check, + :skip_validation - MISSING_UPLOADS ||= "missing uploads" + MISSING_UPLOADS ||= "missing uploads" MISSING_UPLOADS_IGNORED ||= "missing uploads ignored" - NOTICE ||= "notice" + NOTICE ||= "notice" SHORT_POST_CHARS ||= 1200 @@ -80,46 +86,56 @@ class Post < ActiveRecord::Base register_custom_field_type(NOTICE, :json) - scope :private_posts_for_user, ->(user) do - where( - "topics.id IN (#{Topic::PRIVATE_MESSAGES_SQL_USER}) + scope :private_posts_for_user, + ->(user) { + where( + "topics.id IN (#{Topic::PRIVATE_MESSAGES_SQL_USER}) OR topics.id IN (#{Topic::PRIVATE_MESSAGES_SQL_GROUP})", - user_id: user.id - ) - end + user_id: user.id, + ) + } - scope :by_newest, -> { order('created_at DESC, id DESC') } - scope :by_post_number, -> { order('post_number ASC') } + scope :by_newest, -> { order("created_at DESC, id DESC") } + scope :by_post_number, -> { order("post_number ASC") } scope :with_user, -> { includes(:user) } - scope :created_since, -> (time_ago) { where('posts.created_at > ?', time_ago) } - scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) } - scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) } - scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) } - scope :visible, -> { joins(:topic).where('topics.visible = true').where(hidden: false) } - scope :secured, -> (guardian) { where('posts.post_type IN (?)', Topic.visible_post_types(guardian&.user)) } + scope :created_since, ->(time_ago) { where("posts.created_at > ?", time_ago) } + scope :public_posts, + -> { joins(:topic).where("topics.archetype <> ?", Archetype.private_message) } + scope :private_posts, + -> { joins(:topic).where("topics.archetype = ?", Archetype.private_message) } + scope :with_topic_subtype, ->(subtype) { joins(:topic).where("topics.subtype = ?", subtype) } + scope :visible, -> { joins(:topic).where("topics.visible = true").where(hidden: false) } + scope :secured, + ->(guardian) { where("posts.post_type IN (?)", Topic.visible_post_types(guardian&.user)) } - scope :for_mailing_list, ->(user, since) { - q = created_since(since) - .joins("INNER JOIN (#{Topic.for_digest(user, Time.at(0)).select(:id).to_sql}) AS digest_topics ON digest_topics.id = posts.topic_id") # we want all topics with new content, regardless when they were created - .order('posts.created_at ASC') + scope :for_mailing_list, + ->(user, since) { + q = + created_since(since).joins( + "INNER JOIN (#{Topic.for_digest(user, Time.at(0)).select(:id).to_sql}) AS digest_topics ON digest_topics.id = posts.topic_id", + ) # we want all topics with new content, regardless when they were created + .order("posts.created_at ASC") - q = q.where.not(post_type: Post.types[:whisper]) unless user.staff? - q - } + q = q.where.not(post_type: Post.types[:whisper]) unless user.staff? + q + } - scope :raw_match, -> (pattern, type = 'string') { - type = type&.downcase + scope :raw_match, + ->(pattern, type = "string") { + type = type&.downcase - case type - when 'string' - where('raw ILIKE ?', "%#{pattern}%") - when 'regex' - where('raw ~* ?', "(?n)#{pattern}") - end - } + case type + when "string" + where("raw ILIKE ?", "%#{pattern}%") + when "regex" + where("raw ~* ?", "(?n)#{pattern}") + end + } - scope :have_uploads, -> { - where(" + scope :have_uploads, + -> { + where( + " ( posts.cooked LIKE '% 1 - RateLimiter.new(user, "first-day-replies-per-day", SiteSetting.max_replies_in_first_day, 1.day.to_i) + RateLimiter.new( + user, + "first-day-replies-per-day", + SiteSetting.max_replies_in_first_day, + 1.day.to_i, + ) end end @@ -207,7 +225,7 @@ class Post < ActiveRecord::Base user_id: user_id, last_editor_id: last_editor_id, type: type, - version: version + version: version, }.merge(opts) publish_message!("/topic/#{topic_id}", message) @@ -220,14 +238,10 @@ class Post < ActiveRecord::Base if Topic.visible_post_types.include?(post_type) opts.merge!(topic.secure_audience_publish_messages) else - opts[:user_ids] = User.human_users - .where("admin OR moderator OR id = ?", user_id) - .pluck(:id) + opts[:user_ids] = User.human_users.where("admin OR moderator OR id = ?", user_id).pluck(:id) end - if opts[:user_ids] != [] && opts[:group_ids] != [] - MessageBus.publish(channel, message, opts) - end + MessageBus.publish(channel, message, opts) if opts[:user_ids] != [] && opts[:group_ids] != [] end def trash!(trashed_by = nil) @@ -241,9 +255,7 @@ class Post < ActiveRecord::Base recover_public_post_actions TopicLink.extract_from(self) QuotedPost.extract_from(self) - if topic && topic.category_id && topic.category - topic.category.update_latest - end + topic.category.update_latest if topic && topic.category_id && topic.category end # The key we use in redis to ensure unique posts @@ -268,7 +280,7 @@ class Post < ActiveRecord::Base end def self.allowed_image_classes - @allowed_image_classes ||= ['avatar', 'favicon', 'thumbnail', 'emoji', 'ytp-thumbnail-image'] + @allowed_image_classes ||= %w[avatar favicon thumbnail emoji ytp-thumbnail-image] end def post_analyzer @@ -276,17 +288,15 @@ class Post < ActiveRecord::Base @post_analyzers[raw_hash] ||= PostAnalyzer.new(raw, topic_id) end - %w{raw_mentions + %w[ + raw_mentions linked_hosts embedded_media_count attachment_count link_count raw_links - has_oneboxes?}.each do |attr| - define_method(attr) do - post_analyzer.public_send(attr) - end - end + has_oneboxes? + ].each { |attr| define_method(attr) { post_analyzer.public_send(attr) } } def add_nofollow? return false if user&.staff? @@ -318,11 +328,16 @@ class Post < ActiveRecord::Base each_upload_url do |url| uri = URI.parse(url) if FileHelper.is_supported_media?(File.basename(uri.path)) - raw = raw.sub( - url, Rails.application.routes.url_for( - controller: "uploads", action: "show_secure", path: uri.path[1..-1], host: Discourse.current_hostname + raw = + raw.sub( + url, + Rails.application.routes.url_for( + controller: "uploads", + action: "show_secure", + path: uri.path[1..-1], + host: Discourse.current_hostname, + ), ) - ) end end end @@ -358,43 +373,46 @@ class Post < ActiveRecord::Base end def allowed_spam_hosts - hosts = SiteSetting - .allowed_spam_host_domains - .split('|') - .map { |h| h.strip } - .reject { |h| !h.include?('.') } + hosts = + SiteSetting + .allowed_spam_host_domains + .split("|") + .map { |h| h.strip } + .reject { |h| !h.include?(".") } hosts << GlobalSetting.hostname hosts << RailsMultisite::ConnectionManagement.current_hostname - end def total_hosts_usage hosts = linked_hosts.clone allowlisted = allowed_spam_hosts - hosts.reject! do |h| - allowlisted.any? do |w| - h.end_with?(w) - end - end + hosts.reject! { |h| allowlisted.any? { |w| h.end_with?(w) } } return hosts if hosts.length == 0 - TopicLink.where(domain: hosts.keys, user_id: acting_user.id) + TopicLink + .where(domain: hosts.keys, user_id: acting_user.id) .group(:domain, :post_id) .count .each_key do |tuple| - domain = tuple[0] - hosts[domain] = (hosts[domain] || 0) + 1 - end + domain = tuple[0] + hosts[domain] = (hosts[domain] || 0) + 1 + end hosts end # Prevent new users from posting the same hosts too many times. def has_host_spam? - return false if acting_user.present? && (acting_user.staged? || acting_user.mature_staged? || acting_user.has_trust_level?(TrustLevel[1])) + if acting_user.present? && + ( + acting_user.staged? || acting_user.mature_staged? || + acting_user.has_trust_level?(TrustLevel[1]) + ) + return false + end return false if topic&.private_message? total_hosts_usage.values.any? { |count| count >= SiteSetting.newuser_spam_host_threshold } @@ -409,15 +427,15 @@ class Post < ActiveRecord::Base end def self.reverse_order - order('sort_order desc, post_number desc') + order("sort_order desc, post_number desc") end def self.summary(topic_id) topic_id = topic_id.to_i # percent rank has tons of ties - where(topic_id: topic_id) - .where([ + where(topic_id: topic_id).where( + [ "id = ANY( ( SELECT posts.id @@ -435,8 +453,9 @@ class Post < ActiveRecord::Base ) )", SiteSetting.summary_percent_filter.to_f / 100.0, - SiteSetting.summary_max_results - ]) + SiteSetting.summary_max_results, + ], + ) end def delete_post_notices @@ -445,7 +464,8 @@ class Post < ActiveRecord::Base end def recover_public_post_actions - PostAction.publics + PostAction + .publics .with_deleted .where(post_id: self.id, id: self.custom_fields["deleted_public_actions"]) .find_each do |post_action| @@ -463,10 +483,10 @@ class Post < ActiveRecord::Base # We only filter quotes when there is exactly 1 return cooked unless (quote_count == 1) - parent_raw = parent_post.raw.sub(/\[quote.+\/quote\]/m, '') + parent_raw = parent_post.raw.sub(%r{\[quote.+/quote\]}m, "") if raw[parent_raw] || (parent_raw.size < SHORT_POST_CHARS) - return cooked.sub(/\/m, '') + return cooked.sub(%r{\}m, "") end cooked @@ -478,12 +498,22 @@ class Post < ActiveRecord::Base def reply_to_post return if reply_to_post_number.blank? - @reply_to_post ||= Post.find_by("topic_id = :topic_id AND post_number = :post_number", topic_id: topic_id, post_number: reply_to_post_number) + @reply_to_post ||= + Post.find_by( + "topic_id = :topic_id AND post_number = :post_number", + topic_id: topic_id, + post_number: reply_to_post_number, + ) end def reply_notification_target return if reply_to_post_number.blank? - Post.find_by("topic_id = :topic_id AND post_number = :post_number AND user_id <> :user_id", topic_id: topic_id, post_number: reply_to_post_number, user_id: user_id).try(:user) + Post.find_by( + "topic_id = :topic_id AND post_number = :post_number AND user_id <> :user_id", + topic_id: topic_id, + post_number: reply_to_post_number, + user_id: user_id, + ).try(:user) end def self.excerpt(cooked, maxlength = nil, options = {}) @@ -497,13 +527,17 @@ class Post < ActiveRecord::Base end def excerpt_for_topic - Post.excerpt(cooked, SiteSetting.topic_excerpt_maxlength, strip_links: true, strip_images: true, post: self) + Post.excerpt( + cooked, + SiteSetting.topic_excerpt_maxlength, + strip_links: true, + strip_images: true, + post: self, + ) end def is_first_post? - post_number.blank? ? - topic.try(:highest_post_number) == 0 : - post_number == 1 + post_number.blank? ? topic.try(:highest_post_number) == 0 : post_number == 1 end def is_category_description? @@ -515,7 +549,10 @@ class Post < ActiveRecord::Base end def is_flagged? - post_actions.where(post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil).count != 0 + post_actions.where( + post_action_type_id: PostActionType.flag_types_without_custom.values, + deleted_at: nil, + ).count != 0 end def reviewable_flag @@ -524,16 +561,21 @@ class Post < ActiveRecord::Base def with_secure_uploads? return false if !SiteSetting.secure_uploads? - SiteSetting.login_required? || \ + SiteSetting.login_required? || (topic.present? && (topic.private_message? || topic.category&.read_restricted)) end def hide!(post_action_type_id, reason = nil, custom_message: nil) return if hidden? - reason ||= hidden_at ? - Post.hidden_reasons[:flag_threshold_reached_again] : - Post.hidden_reasons[:flag_threshold_reached] + reason ||= + ( + if hidden_at + Post.hidden_reasons[:flag_threshold_reached_again] + else + Post.hidden_reasons[:flag_threshold_reached] + end + ) hiding_again = hidden_at.present? @@ -547,7 +589,7 @@ class Post < ActiveRecord::Base Topic.where( "id = :topic_id AND NOT EXISTS(SELECT 1 FROM POSTS WHERE topic_id = :topic_id AND NOT hidden)", - topic_id: topic_id + topic_id: topic_id, ).update_all(visible: false) UserStatCountUpdater.decrement!(self) @@ -558,24 +600,23 @@ class Post < ActiveRecord::Base options = { url: url, edit_delay: SiteSetting.cooldown_minutes_after_hiding_posts, - flag_reason: I18n.t( - "flag_reasons.#{PostActionType.types[post_action_type_id]}", - locale: SiteSetting.default_locale, - base_path: Discourse.base_path - ) + flag_reason: + I18n.t( + "flag_reasons.#{PostActionType.types[post_action_type_id]}", + locale: SiteSetting.default_locale, + base_path: Discourse.base_path, + ), } message = custom_message - if message.nil? - message = hiding_again ? :post_hidden_again : :post_hidden - end + message = hiding_again ? :post_hidden_again : :post_hidden if message.nil? Jobs.enqueue_in( 5.seconds, :send_system_message, user_id: user.id, message_type: message.to_s, - message_options: options + message_options: options, ) end end @@ -610,9 +651,7 @@ class Post < ActiveRecord::Base page = "" - if topic_view.page > 1 - page = "?page=#{topic_view.page}" - end + page = "?page=#{topic_view.page}" if topic_view.page > 1 "#{topic.url}#{page}#post_#{post_number}" end @@ -636,11 +675,11 @@ class Post < ActiveRecord::Base ids = post_ids.map { |u| u } if ids.length > 0 urls = {} - Topic.joins(:posts).where('posts.id' => ids). - select(['posts.id as post_id', 'post_number', 'topics.slug', 'topics.title', 'topics.id']). - each do |t| - urls[t.post_id.to_i] = url(t.slug, t.id, t.post_number) - end + Topic + .joins(:posts) + .where("posts.id" => ids) + .select(["posts.id as post_id", "post_number", "topics.slug", "topics.title", "topics.id"]) + .each { |t| urls[t.post_id.to_i] = url(t.slug, t.id, t.post_number) } urls else {} @@ -652,47 +691,50 @@ class Post < ActiveRecord::Base end def self.rebake_old(limit, priority: :normal, rate_limiter: true) - - limiter = RateLimiter.new( - nil, - "global_periodical_rebake_limit", - GlobalSetting.max_old_rebakes_per_15_minutes, - 900, - global: true - ) + limiter = + RateLimiter.new( + nil, + "global_periodical_rebake_limit", + GlobalSetting.max_old_rebakes_per_15_minutes, + 900, + global: true, + ) problems = [] - Post.where('baked_version IS NULL OR baked_version < ?', BAKED_VERSION) - .order('id desc') - .limit(limit).pluck(:id).each do |id| - begin - - break if !limiter.can_perform? - - post = Post.find(id) - post.rebake!(priority: priority) - + Post + .where("baked_version IS NULL OR baked_version < ?", BAKED_VERSION) + .order("id desc") + .limit(limit) + .pluck(:id) + .each do |id| begin - limiter.performed! if rate_limiter - rescue RateLimiter::LimitExceeded - break + break if !limiter.can_perform? + + post = Post.find(id) + post.rebake!(priority: priority) + + begin + limiter.performed! if rate_limiter + rescue RateLimiter::LimitExceeded + break + end + rescue => e + problems << { post: post, ex: e } + + attempts = post.custom_fields["rebake_attempts"].to_i + + if attempts > 3 + post.update_columns(baked_version: BAKED_VERSION) + Discourse.warn_exception( + e, + message: "Can not rebake post# #{post.id} after 3 attempts, giving up", + ) + else + post.custom_fields["rebake_attempts"] = attempts + 1 + post.save_custom_fields + end end - - rescue => e - problems << { post: post, ex: e } - - attempts = post.custom_fields["rebake_attempts"].to_i - - if attempts > 3 - post.update_columns(baked_version: BAKED_VERSION) - Discourse.warn_exception(e, message: "Can not rebake post# #{post.id} after 3 attempts, giving up") - else - post.custom_fields["rebake_attempts"] = attempts + 1 - post.save_custom_fields - end - end - end problems end @@ -700,15 +742,9 @@ class Post < ActiveRecord::Base new_cooked = cook(raw, topic_id: topic_id, invalidate_oneboxes: invalidate_oneboxes) old_cooked = cooked - update_columns( - cooked: new_cooked, - baked_at: Time.zone.now, - baked_version: BAKED_VERSION - ) + update_columns(cooked: new_cooked, baked_at: Time.zone.now, baked_version: BAKED_VERSION) - if is_first_post? - topic&.update_excerpt(excerpt_for_topic) - end + topic&.update_excerpt(excerpt_for_topic) if is_first_post? if invalidate_broken_images post_hotlinked_media.download_failed.destroy_all @@ -730,31 +766,29 @@ class Post < ActiveRecord::Base def set_owner(new_user, actor, skip_revision = false) return if user_id == new_user.id - edit_reason = I18n.t('change_owner.post_revision_text', locale: SiteSetting.default_locale) + edit_reason = I18n.t("change_owner.post_revision_text", locale: SiteSetting.default_locale) revise( actor, { raw: self.raw, user_id: new_user.id, edit_reason: edit_reason }, - bypass_bump: true, skip_revision: skip_revision, skip_validations: true + bypass_bump: true, + skip_revision: skip_revision, + skip_validations: true, ) - if post_number == topic.highest_post_number - topic.update_columns(last_post_user_id: new_user.id) - end + topic.update_columns(last_post_user_id: new_user.id) if post_number == topic.highest_post_number end - before_create do - PostCreator.before_create_tasks(self) - end + before_create { PostCreator.before_create_tasks(self) } def self.estimate_posts_per_day val = Discourse.redis.get("estimated_posts_per_day") return val.to_i if val - posts_per_day = Topic.listable_topics.secured.joins(:posts).merge(Post.created_since(30.days.ago)).count / 30 + posts_per_day = + Topic.listable_topics.secured.joins(:posts).merge(Post.created_since(30.days.ago)).count / 30 Discourse.redis.setex("estimated_posts_per_day", 1.day.to_i, posts_per_day.to_s) posts_per_day - end before_save do @@ -778,13 +812,15 @@ class Post < ActiveRecord::Base temp_collector = [] # Create relationships for the quotes - raw.scan(/\[quote=\"([^"]+)"\]/).each do |quote| - args = parse_quote_into_arguments(quote) - # If the topic attribute is present, ensure it's the same topic - if !(args[:topic].present? && topic_id != args[:topic]) && args[:post] != post_number - temp_collector << args[:post] + raw + .scan(/\[quote=\"([^"]+)"\]/) + .each do |quote| + args = parse_quote_into_arguments(quote) + # If the topic attribute is present, ensure it's the same topic + if !(args[:topic].present? && topic_id != args[:topic]) && args[:post] != post_number + temp_collector << args[:post] + end end - end temp_collector.uniq! self.quoted_post_numbers = temp_collector @@ -803,7 +839,12 @@ class Post < ActiveRecord::Base end # Enqueue post processing for this post - def trigger_post_process(bypass_bump: false, priority: :normal, new_post: false, skip_pull_hotlinked_images: false) + def trigger_post_process( + bypass_bump: false, + priority: :normal, + new_post: false, + skip_pull_hotlinked_images: false + ) args = { bypass_bump: bypass_bump, cooking_options: self.cooking_options, @@ -820,30 +861,36 @@ class Post < ActiveRecord::Base DiscourseEvent.trigger(:after_trigger_post_process, self) end - def self.public_posts_count_per_day(start_date, end_date, category_id = nil, include_subcategories = false) - result = public_posts - .where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date) - .where(post_type: Post.types[:regular]) + def self.public_posts_count_per_day( + start_date, + end_date, + category_id = nil, + include_subcategories = false + ) + result = + public_posts.where( + "posts.created_at >= ? AND posts.created_at <= ?", + start_date, + end_date, + ).where(post_type: Post.types[:regular]) if category_id if include_subcategories - result = result.where('topics.category_id IN (?)', Category.subcategory_ids(category_id)) + result = result.where("topics.category_id IN (?)", Category.subcategory_ids(category_id)) else - result = result.where('topics.category_id = ?', category_id) + result = result.where("topics.category_id = ?", category_id) end end - result - .group('date(posts.created_at)') - .order('date(posts.created_at)') - .count + result.group("date(posts.created_at)").order("date(posts.created_at)").count end def self.private_messages_count_per_day(start_date, end_date, topic_subtype) - private_posts.with_topic_subtype(topic_subtype) - .where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date) - .group('date(posts.created_at)') - .order('date(posts.created_at)') + private_posts + .with_topic_subtype(topic_subtype) + .where("posts.created_at >= ? AND posts.created_at <= ?", start_date, end_date) + .group("date(posts.created_at)") + .order("date(posts.created_at)") .count end @@ -962,28 +1009,26 @@ class Post < ActiveRecord::Base upload_ids << upload.id if upload.present? end - upload_references = upload_ids.map do |upload_id| - { - target_id: self.id, - target_type: self.class.name, - upload_id: upload_id, - created_at: Time.zone.now, - updated_at: Time.zone.now - } - end + upload_references = + upload_ids.map do |upload_id| + { + target_id: self.id, + target_type: self.class.name, + upload_id: upload_id, + created_at: Time.zone.now, + updated_at: Time.zone.now, + } + end UploadReference.transaction do UploadReference.where(target: self).delete_all UploadReference.insert_all(upload_references) if upload_references.size > 0 if SiteSetting.secure_uploads? - Upload.where( - id: upload_ids, access_control_post_id: nil - ).where( - 'id NOT IN (SELECT upload_id FROM custom_emojis)' - ).update_all( - access_control_post_id: self.id - ) + Upload + .where(id: upload_ids, access_control_post_id: nil) + .where("id NOT IN (SELECT upload_id FROM custom_emojis)") + .update_all(access_control_post_id: self.id) end end end @@ -997,25 +1042,30 @@ class Post < ActiveRecord::Base def each_upload_url(fragments: nil, include_local_upload: true) current_db = RailsMultisite::ConnectionManagement.current_db upload_patterns = [ - /\/uploads\/#{current_db}\//, - /\/original\//, - /\/optimized\//, - /\/uploads\/short-url\/[a-zA-Z0-9]+(\.[a-z0-9]+)?/ + %r{/uploads/#{current_db}/}, + %r{/original/}, + %r{/optimized/}, + %r{/uploads/short-url/[a-zA-Z0-9]+(\.[a-z0-9]+)?}, ] - fragments ||= Nokogiri::HTML5::fragment(self.cooked) + fragments ||= Nokogiri::HTML5.fragment(self.cooked) selectors = fragments.css("a/@href", "img/@src", "source/@src", "track/@src", "video/@poster") - links = selectors.map do |media| - src = media.value - next if src.blank? + links = + selectors + .map do |media| + src = media.value + next if src.blank? - if src.end_with?("/images/transparent.png") && (parent = media.parent)["data-orig-src"].present? - parent["data-orig-src"] - else - src - end - end.compact.uniq + if src.end_with?("/images/transparent.png") && + (parent = media.parent)["data-orig-src"].present? + parent["data-orig-src"] + else + src + end + end + .compact + .uniq links.each do |src| src = src.split("?")[0] @@ -1034,13 +1084,19 @@ class Post < ActiveRecord::Base next if Rails.configuration.multisite && src.exclude?(current_db) src = "#{SiteSetting.force_https ? "https" : "http"}:#{src}" if src.start_with?("//") - next unless Discourse.store.has_been_uploaded?(src) || Upload.secure_uploads_url?(src) || (include_local_upload && src =~ /\A\/[^\/]/i) - - path = begin - URI(UrlHelper.unencode(GlobalSetting.cdn_url ? src.sub(GlobalSetting.cdn_url, "") : src))&.path - rescue URI::Error + unless Discourse.store.has_been_uploaded?(src) || Upload.secure_uploads_url?(src) || + (include_local_upload && src =~ %r{\A/[^/]}i) + next end + path = + begin + URI( + UrlHelper.unencode(GlobalSetting.cdn_url ? src.sub(GlobalSetting.cdn_url, "") : src), + )&.path + rescue URI::Error + end + next if path.blank? sha1 = @@ -1061,20 +1117,24 @@ class Post < ActiveRecord::Base DistributedMutex.synchronize("find_missing_uploads", validity: 30.minutes) do PostCustomField.where(name: Post::MISSING_UPLOADS).delete_all - query = Post - .have_uploads - .joins(:topic) - .joins("LEFT JOIN post_custom_fields ON posts.id = post_custom_fields.post_id AND post_custom_fields.name = '#{Post::MISSING_UPLOADS_IGNORED}'") - .where("post_custom_fields.id IS NULL") - .select(:id, :cooked) + query = + Post + .have_uploads + .joins(:topic) + .joins( + "LEFT JOIN post_custom_fields ON posts.id = post_custom_fields.post_id AND post_custom_fields.name = '#{Post::MISSING_UPLOADS_IGNORED}'", + ) + .where("post_custom_fields.id IS NULL") + .select(:id, :cooked) query.find_in_batches do |posts| ids = posts.pluck(:id) - sha1s = Upload - .joins(:upload_references) - .where(upload_references: { target_type: "Post" }) - .where("upload_references.target_id BETWEEN ? AND ?", ids.min, ids.max) - .pluck(:sha1) + sha1s = + Upload + .joins(:upload_references) + .where(upload_references: { target_type: "Post" }) + .where("upload_references.target_id BETWEEN ? AND ?", ids.min, ids.max) + .pluck(:sha1) posts.each do |post| post.each_upload_url do |src, path, sha1| @@ -1099,14 +1159,19 @@ class Post < ActiveRecord::Base end end - missing_post_uploads = missing_post_uploads.reject do |post_id, uploads| - if uploads.present? - PostCustomField.create!(post_id: post_id, name: Post::MISSING_UPLOADS, value: uploads.to_json) - count += uploads.count - end + missing_post_uploads = + missing_post_uploads.reject do |post_id, uploads| + if uploads.present? + PostCustomField.create!( + post_id: post_id, + name: Post::MISSING_UPLOADS, + value: uploads.to_json, + ) + count += uploads.count + end - uploads.empty? - end + uploads.empty? + end end { uploads: missing_uploads, post_uploads: missing_post_uploads, count: count } @@ -1123,8 +1188,11 @@ class Post < ActiveRecord::Base def cannot_permanently_delete_reason(user) if self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago - time_left = RateLimiter.time_left(Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i) - I18n.t('post.cannot_permanently_delete.wait_or_different_admin', time_left: time_left) + time_left = + RateLimiter.time_left( + Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i, + ) + I18n.t("post.cannot_permanently_delete.wait_or_different_admin", time_left: time_left) end end @@ -1137,9 +1205,7 @@ class Post < ActiveRecord::Base def parse_quote_into_arguments(quote) return {} unless quote.present? args = HashWithIndifferentAccess.new - quote.first.scan(/([a-z]+)\:(\d+)/).each do |arg| - args[arg[0]] = arg[1].to_i - end + quote.first.scan(/([a-z]+)\:(\d+)/).each { |arg| args[arg[0]] = arg[1].to_i } args end @@ -1154,7 +1220,7 @@ class Post < ActiveRecord::Base post_reply = post.post_replies.new(reply_post_id: id) if post_reply.save if Topic.visible_post_types.include?(self.post_type) - Post.where(id: post.id).update_all ['reply_count = reply_count + 1'] + Post.where(id: post.id).update_all ["reply_count = reply_count + 1"] end end end diff --git a/app/models/post_action.rb b/app/models/post_action.rb index e2fa99e45f..a02c8908cf 100644 --- a/app/models/post_action.rb +++ b/app/models/post_action.rb @@ -7,8 +7,8 @@ class PostAction < ActiveRecord::Base belongs_to :post belongs_to :user belongs_to :post_action_type - belongs_to :related_post, class_name: 'Post' - belongs_to :target_user, class_name: 'User' + belongs_to :related_post, class_name: "Post" + belongs_to :target_user, class_name: "User" rate_limit :post_action_rate_limiter @@ -55,9 +55,9 @@ class PostAction < ActiveRecord::Base ORDER BY p.topic_id, p.post_number SQL - builder.query(user_id: user.id, post_action_type_id: post_action_type_id, topic_ids: topic_ids).each do |row| - (map[row.topic_id] ||= []) << row.post_number - end + builder + .query(user_id: user.id, post_action_type_id: post_action_type_id, topic_ids: topic_ids) + .each { |row| (map[row.topic_id] ||= []) << row.post_number } map end @@ -65,18 +65,24 @@ class PostAction < ActiveRecord::Base def self.count_per_day_for_type(post_action_type, opts = nil) opts ||= {} result = unscoped.where(post_action_type_id: post_action_type) - result = result.where('post_actions.created_at >= ?', opts[:start_date] || (opts[:since_days_ago] || 30).days.ago) - result = result.where('post_actions.created_at <= ?', opts[:end_date]) if opts[:end_date] + result = + result.where( + "post_actions.created_at >= ?", + opts[:start_date] || (opts[:since_days_ago] || 30).days.ago, + ) + result = result.where("post_actions.created_at <= ?", opts[:end_date]) if opts[:end_date] if opts[:category_id] if opts[:include_subcategories] - result = result.joins(post: :topic).where('topics.category_id IN (?)', Category.subcategory_ids(opts[:category_id])) + result = + result.joins(post: :topic).where( + "topics.category_id IN (?)", + Category.subcategory_ids(opts[:category_id]), + ) else - result = result.joins(post: :topic).where('topics.category_id = ?', opts[:category_id]) + result = result.joins(post: :topic).where("topics.category_id = ?", opts[:category_id]) end end - result.group('date(post_actions.created_at)') - .order('date(post_actions.created_at)') - .count + result.group("date(post_actions.created_at)").order("date(post_actions.created_at)").count end def add_moderator_post_if_needed(moderator, disposition, delete_post = false) @@ -95,7 +101,13 @@ class PostAction < ActiveRecord::Base end def staff_already_replied?(topic) - topic.posts.where("user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)", regular_post_type: Post.types[:regular]).exists? + topic + .posts + .where( + "user_id IN (SELECT id FROM users WHERE moderator OR admin) OR (post_type != :regular_post_type)", + regular_post_type: Post.types[:regular], + ) + .exists? end def self.limit_action!(user, post, post_action_type_id) @@ -106,21 +118,17 @@ class PostAction < ActiveRecord::Base Discourse.deprecate( "PostAction.act is deprecated. Use `PostActionCreator` instead.", output_in_test: true, - drop_from: '2.9.0', + drop_from: "2.9.0", ) - result = PostActionCreator.new( - created_by, - post, - post_action_type_id, - message: opts[:message] - ).perform + result = + PostActionCreator.new(created_by, post, post_action_type_id, message: opts[:message]).perform result.success? ? result.post_action : nil end def self.copy(original_post, target_post) - cols_to_copy = (column_names - %w{id post_id}).join(', ') + cols_to_copy = (column_names - %w[id post_id]).join(", ") DB.exec <<~SQL INSERT INTO post_actions(post_id, #{cols_to_copy}) @@ -136,7 +144,7 @@ class PostAction < ActiveRecord::Base Discourse.deprecate( "PostAction.remove_act is deprecated. Use `PostActionDestroyer` instead.", output_in_test: true, - drop_from: '2.9.0', + drop_from: "2.9.0", ) PostActionDestroyer.new(user, post, post_action_type_id).perform @@ -160,7 +168,7 @@ class PostAction < ActiveRecord::Base def is_private_message? post_action_type_id == PostActionType.types[:notify_user] || - post_action_type_id == PostActionType.types[:notify_moderators] + post_action_type_id == PostActionType.types[:notify_moderators] end # A custom rate limiter for this model @@ -169,12 +177,13 @@ class PostAction < ActiveRecord::Base return @rate_limiter if @rate_limiter.present? - %w(like flag).each do |type| + %w[like flag].each do |type| if public_send("is_#{type}?") limit = SiteSetting.get("max_#{type}s_per_day") if (is_flag? || is_like?) && user && user.trust_level >= 2 - multiplier = SiteSetting.get("tl#{user.trust_level}_additional_#{type}s_per_day_multiplier").to_f + multiplier = + SiteSetting.get("tl#{user.trust_level}_additional_#{type}s_per_day_multiplier").to_f multiplier = 1.0 if multiplier < 1.0 limit = (limit * multiplier).to_i @@ -189,13 +198,15 @@ class PostAction < ActiveRecord::Base def ensure_unique_actions post_action_type_ids = is_flag? ? PostActionType.notify_flag_types.values : post_action_type_id - acted = PostAction.where(user_id: user_id) - .where(post_id: post_id) - .where(post_action_type_id: post_action_type_ids) - .where(deleted_at: nil) - .where(disagreed_at: nil) - .where(targets_topic: targets_topic) - .exists? + acted = + PostAction + .where(user_id: user_id) + .where(post_id: post_id) + .where(post_action_type_id: post_action_type_ids) + .where(deleted_at: nil) + .where(disagreed_at: nil) + .where(targets_topic: targets_topic) + .exists? errors.add(:post_action_type_id) if acted end @@ -207,18 +218,24 @@ class PostAction < ActiveRecord::Base def update_counters # Update denormalized counts column = "#{post_action_type_key}_count" - count = PostAction.where(post_id: post_id) - .where(post_action_type_id: post_action_type_id) - .count + count = PostAction.where(post_id: post_id).where(post_action_type_id: post_action_type_id).count # We probably want to refactor this method to something cleaner. case post_action_type_key when :like # 'like_score' is weighted higher for staff accounts - score = PostAction.joins(:user) - .where(post_id: post_id) - .sum("CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END") - Post.where(id: post_id).update_all ["like_count = :count, like_score = :score", count: count, score: score] + score = + PostAction + .joins(:user) + .where(post_id: post_id) + .sum( + "CASE WHEN users.moderator OR users.admin THEN #{SiteSetting.staff_like_weight} ELSE 1 END", + ) + Post.where(id: post_id).update_all [ + "like_count = :count, like_score = :score", + count: count, + score: score, + ] else if ActiveRecord::Base.connection.column_exists?(:posts, column) Post.where(id: post_id).update_all ["#{column} = ?", count] @@ -229,15 +246,14 @@ class PostAction < ActiveRecord::Base # topic_user if post_action_type_key == :like - TopicUser.update_post_action_cache(user_id: user_id, - topic_id: topic_id, - post_action_type: post_action_type_key) - end - - if column == "like_count" - Topic.find_by(id: topic_id)&.update_action_counts + TopicUser.update_post_action_cache( + user_id: user_id, + topic_id: topic_id, + post_action_type: post_action_type_key, + ) end + Topic.find_by(id: topic_id)&.update_action_counts if column == "like_count" end end diff --git a/app/models/post_action_type.rb b/app/models/post_action_type.rb index a209a79b6b..db785c307f 100644 --- a/app/models/post_action_type.rb +++ b/app/models/post_action_type.rb @@ -24,16 +24,14 @@ class PostActionType < ActiveRecord::Base end def ordered - order('position asc') + order("position asc") end def types unless @types # NOTE: Previously bookmark was type 1 but that has been superseded # by the separate Bookmark model and functionality - @types = Enum.new( - like: 2 - ) + @types = Enum.new(like: 2) @types.merge!(flag_settings.flag_types) end @@ -85,39 +83,22 @@ class PostActionType < ActiveRecord::Base def initialize_flag_settings @flag_settings = FlagSettings.new - @flag_settings.add( - 3, - :off_topic, - notify_type: true, - auto_action_type: true, - ) + @flag_settings.add(3, :off_topic, notify_type: true, auto_action_type: true) @flag_settings.add( 4, :inappropriate, topic_type: true, notify_type: true, auto_action_type: true, - ) - @flag_settings.add( - 8, - :spam, - topic_type: true, - notify_type: true, - auto_action_type: true, - ) - @flag_settings.add( - 6, - :notify_user, - topic_type: false, - notify_type: false, - custom_type: true ) + @flag_settings.add(8, :spam, topic_type: true, notify_type: true, auto_action_type: true) + @flag_settings.add(6, :notify_user, topic_type: false, notify_type: false, custom_type: true) @flag_settings.add( 7, :notify_moderators, topic_type: true, notify_type: true, - custom_type: true + custom_type: true, ) end end diff --git a/app/models/post_analyzer.rb b/app/models/post_analyzer.rb index bbd5bdd7f3..2d2622c207 100644 --- a/app/models/post_analyzer.rb +++ b/app/models/post_analyzer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class PostAnalyzer - def initialize(raw, topic_id) @raw = raw @topic_id = topic_id @@ -32,19 +31,20 @@ class PostAnalyzer end limit = SiteSetting.max_oneboxes_per_post - result = Oneboxer.apply(cooked, extra_paths: ".inline-onebox-loading") do |url, element| - if opts[:invalidate_oneboxes] - Oneboxer.invalidate(url) - InlineOneboxer.invalidate(url) + result = + Oneboxer.apply(cooked, extra_paths: ".inline-onebox-loading") do |url, element| + if opts[:invalidate_oneboxes] + Oneboxer.invalidate(url) + InlineOneboxer.invalidate(url) + end + next if element["class"] != Oneboxer::ONEBOX_CSS_CLASS + next if limit <= 0 + limit -= 1 + @onebox_urls << url + onebox = Oneboxer.cached_onebox(url) + @found_oneboxes = true if onebox.present? + onebox end - next if element["class"] != Oneboxer::ONEBOX_CSS_CLASS - next if limit <= 0 - limit -= 1 - @onebox_urls << url - onebox = Oneboxer.cached_onebox(url) - @found_oneboxes = true if onebox.present? - onebox - end if result.changed? PrettyText.sanitize_hotlinked_media(result.doc) @@ -59,19 +59,26 @@ class PostAnalyzer return 0 unless @raw.present? # TODO - do we need to look for tags other than img, video and audio? - cooked_stripped.css("img", "video", "audio").reject do |t| - if dom_class = t["class"] - (Post.allowed_image_classes & dom_class.split).count > 0 + cooked_stripped + .css("img", "video", "audio") + .reject do |t| + if dom_class = t["class"] + (Post.allowed_image_classes & dom_class.split).count > 0 + end end - end.count + .count end # How many attachments are present in the post def attachment_count return 0 unless @raw.present? - attachments = cooked_stripped.css("a.attachment[href^=\"#{Discourse.store.absolute_base_url}\"]") - attachments += cooked_stripped.css("a.attachment[href^=\"#{Discourse.store.relative_base_url}\"]") if Discourse.store.internal? + attachments = + cooked_stripped.css("a.attachment[href^=\"#{Discourse.store.absolute_base_url}\"]") + attachments += + cooked_stripped.css( + "a.attachment[href^=\"#{Discourse.store.relative_base_url}\"]", + ) if Discourse.store.internal? attachments.count end @@ -116,15 +123,17 @@ class PostAnalyzer return @raw_links if @raw_links.present? @raw_links = [] - cooked_stripped.css("a").each do |l| - # Don't include @mentions in the link count - next if link_is_a_mention?(l) - # Don't include heading anchor in the link count - next if link_is_an_anchor?(l) - # Don't include hashtags in the link count - next if link_is_a_hashtag?(l) - @raw_links << l['href'].to_s - end + cooked_stripped + .css("a") + .each do |l| + # Don't include @mentions in the link count + next if link_is_a_mention?(l) + # Don't include heading anchor in the link count + next if link_is_an_anchor?(l) + # Don't include hashtags in the link count + next if link_is_a_hashtag?(l) + @raw_links << l["href"].to_s + end @raw_links end @@ -135,27 +144,37 @@ class PostAnalyzer end def cooked_stripped - @cooked_stripped ||= begin - doc = Nokogiri::HTML5.fragment(cook(@raw, topic_id: @topic_id)) - doc.css("pre .mention, aside.quote > .title, aside.quote .mention, aside.quote .mention-group, .onebox, .elided").remove - doc - end + @cooked_stripped ||= + begin + doc = Nokogiri::HTML5.fragment(cook(@raw, topic_id: @topic_id)) + doc.css( + "pre .mention, aside.quote > .title, aside.quote .mention, aside.quote .mention-group, .onebox, .elided", + ).remove + doc + end end private def link_is_a_mention?(l) - href = l['href'].to_s - l['class'].to_s['mention'] && (href.start_with?("#{Discourse.base_path}/u/") || href.start_with?("#{Discourse.base_path}/users/")) + href = l["href"].to_s + l["class"].to_s["mention"] && + ( + href.start_with?("#{Discourse.base_path}/u/") || + href.start_with?("#{Discourse.base_path}/users/") + ) end def link_is_an_anchor?(l) - l['class'].to_s['anchor'] && l['href'].to_s.start_with?('#') + l["class"].to_s["anchor"] && l["href"].to_s.start_with?("#") end def link_is_a_hashtag?(l) - href = l['href'].to_s - l['class'].to_s['hashtag'] && (href.start_with?("#{Discourse.base_path}/c/") || href.start_with?("#{Discourse.base_path}/tag/")) + href = l["href"].to_s + l["class"].to_s["hashtag"] && + ( + href.start_with?("#{Discourse.base_path}/c/") || + href.start_with?("#{Discourse.base_path}/tag/") + ) end - end diff --git a/app/models/post_detail.rb b/app/models/post_detail.rb index 56fbed2ac5..c28d501d8e 100644 --- a/app/models/post_detail.rb +++ b/app/models/post_detail.rb @@ -3,7 +3,7 @@ class PostDetail < ActiveRecord::Base belongs_to :post - validates_presence_of :key, :value + validates_presence_of :key, :value validates_uniqueness_of :key, scope: :post_id end diff --git a/app/models/post_hotlinked_media.rb b/app/models/post_hotlinked_media.rb index fc8e5814df..de7d6aca7a 100644 --- a/app/models/post_hotlinked_media.rb +++ b/app/models/post_hotlinked_media.rb @@ -4,11 +4,11 @@ class PostHotlinkedMedia < ActiveRecord::Base belongs_to :post belongs_to :upload enum status: { - downloaded: "downloaded", - too_large: "too_large", - download_failed: "download_failed", - upload_create_failed: "upload_create_failed" - } + downloaded: "downloaded", + too_large: "too_large", + download_failed: "download_failed", + upload_create_failed: "upload_create_failed", + } def self.normalize_src(src) uri = Addressable::URI.heuristic_parse(src) diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index f8e77c7cd6..ac6e90471a 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -19,13 +19,11 @@ class PostMover topic = Topic.find_by_id(id) if topic.archetype != @original_topic.archetype && - [@original_topic.archetype, topic.archetype].include?(Archetype.private_message) + [@original_topic.archetype, topic.archetype].include?(Archetype.private_message) raise Discourse::InvalidParameters end - Topic.transaction do - move_posts_to topic - end + Topic.transaction { move_posts_to topic } add_allowed_users(participants) if participants.present? && @move_to_pm enqueue_jobs(topic) topic @@ -38,20 +36,22 @@ class PostMover raise Discourse::InvalidParameters unless post archetype = @move_to_pm ? Archetype.private_message : Archetype.default - topic = Topic.transaction do - new_topic = Topic.create!( - user: post.user, - title: title, - category_id: category_id, - created_at: post.created_at, - archetype: archetype - ) - DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags) - move_posts_to new_topic - watch_new_topic - update_topic_excerpt new_topic - new_topic - end + topic = + Topic.transaction do + new_topic = + Topic.create!( + user: post.user, + title: title, + category_id: category_id, + created_at: post.created_at, + archetype: archetype, + ) + DiscourseTagging.tag_topic_by_names(new_topic, Guardian.new(user), tags) + move_posts_to new_topic + watch_new_topic + update_topic_excerpt new_topic + new_topic + end enqueue_jobs(topic) topic end @@ -73,9 +73,15 @@ class PostMover # we should only exclude whispers with action_code: 'split_topic' # because we use such whispers as a small-action posts when moving posts to the secret message # (in this case we don't want everyone to see that posts were moved, that's why we use whispers) - original_topic_posts_count = @original_topic.posts - .where("post_type = ? or (post_type = ? and action_code != 'split_topic')", Post.types[:regular], Post.types[:whisper]) - .count + original_topic_posts_count = + @original_topic + .posts + .where( + "post_type = ? or (post_type = ? and action_code != 'split_topic')", + Post.types[:regular], + Post.types[:whisper], + ) + .count moving_all_posts = original_topic_posts_count == posts.length create_temp_table @@ -87,9 +93,7 @@ class PostMover update_upload_security_status update_bookmarks - if moving_all_posts - close_topic_and_schedule_deletion - end + close_topic_and_schedule_deletion if moving_all_posts destination_topic.reload destination_topic @@ -152,19 +156,20 @@ class PostMover end def create_first_post(post) - @post_creator = PostCreator.new( - post.user, - raw: post.raw, - topic_id: destination_topic.id, - acting_user: user, - cook_method: post.cook_method, - via_email: post.via_email, - raw_email: post.raw_email, - skip_validations: true, - created_at: post.created_at, - guardian: Guardian.new(user), - skip_jobs: true - ) + @post_creator = + PostCreator.new( + post.user, + raw: post.raw, + topic_id: destination_topic.id, + acting_user: user, + cook_method: post.cook_method, + via_email: post.via_email, + raw_email: post.raw_email, + skip_validations: true, + created_at: post.created_at, + guardian: Guardian.new(user), + skip_jobs: true, + ) new_post = @post_creator.create! move_email_logs(post, new_post) @@ -192,12 +197,10 @@ class PostMover reply_to_post_number: @move_map[post.reply_to_post_number], topic_id: destination_topic.id, sort_order: @move_map[post.post_number], - baked_version: nil + baked_version: nil, } - unless @move_map[post.reply_to_post_number] - update[:reply_to_user_id] = nil - end + update[:reply_to_user_id] = nil unless @move_map[post.reply_to_post_number] post.attributes = update post.save(validate: false) @@ -217,7 +220,7 @@ class PostMover old_post_number: post.post_number, new_topic_id: destination_topic.id, new_post_number: @move_map[post.post_number], - new_topic_title: destination_topic.title + new_topic_title: destination_topic.title, } end @@ -241,9 +244,7 @@ class PostMover end def move_email_logs(old_post, new_post) - EmailLog - .where(post_id: old_post.id) - .update_all(post_id: new_post.id) + EmailLog.where(post_id: old_post.id).update_all(post_id: new_post.id) end def move_notifications @@ -349,7 +350,7 @@ class PostMover old_topic_id: original_topic.id, new_topic_id: destination_topic.id, old_highest_post_number: destination_topic.highest_post_number, - old_highest_staff_post_number: destination_topic.highest_staff_post_number + old_highest_staff_post_number: destination_topic.highest_staff_post_number, } DB.exec(<<~SQL, params) @@ -423,7 +424,10 @@ class PostMover def update_statistics destination_topic.update_statistics original_topic.update_statistics - TopicUser.update_post_action_cache(topic_id: [original_topic.id, destination_topic.id], post_id: @post_ids) + TopicUser.update_post_action_cache( + topic_id: [original_topic.id, destination_topic.id], + post_id: @post_ids, + ) end def update_user_actions @@ -434,35 +438,42 @@ class PostMover move_type_str = PostMover.move_types[@move_type].to_s move_type_str.sub!("topic", "message") if @move_to_pm - message = I18n.with_locale(SiteSetting.default_locale) do - I18n.t( - "move_posts.#{move_type_str}_moderator_post", - count: posts.length, - topic_link: posts.first.is_first_post? ? - "[#{destination_topic.title}](#{destination_topic.relative_url})" : - "[#{destination_topic.title}](#{posts.first.url})" - ) - end + message = + I18n.with_locale(SiteSetting.default_locale) do + I18n.t( + "move_posts.#{move_type_str}_moderator_post", + count: posts.length, + topic_link: + ( + if posts.first.is_first_post? + "[#{destination_topic.title}](#{destination_topic.relative_url})" + else + "[#{destination_topic.title}](#{posts.first.url})" + end + ), + ) + end post_type = @move_to_pm ? Post.types[:whisper] : Post.types[:small_action] original_topic.add_moderator_post( - user, message, + user, + message, post_type: post_type, action_code: "split_topic", - post_number: @first_post_number_moved + post_number: @first_post_number_moved, ) end def posts - @posts ||= begin - Post.where(topic: @original_topic, id: post_ids) - .where.not(post_type: Post.types[:small_action]) - .where.not(raw: '') - .order(:created_at).tap do |posts| - - raise Discourse::InvalidParameters.new(:post_ids) if posts.empty? + @posts ||= + begin + Post + .where(topic: @original_topic, id: post_ids) + .where.not(post_type: Post.types[:small_action]) + .where.not(raw: "") + .order(:created_at) + .tap { |posts| raise Discourse::InvalidParameters.new(:post_ids) if posts.empty? } end - end end def update_last_post_stats @@ -478,9 +489,7 @@ class PostMover end def update_upload_security_status - DB.after_commit do - Jobs.enqueue(:update_topic_upload_security, topic_id: @destination_topic.id) - end + DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: @destination_topic.id) } end def update_bookmarks @@ -493,9 +502,18 @@ class PostMover def watch_new_topic if @destination_topic.archetype == Archetype.private_message if @original_topic.archetype == Archetype.private_message - notification_levels = TopicUser.where(topic_id: @original_topic.id, user_id: posts.pluck(:user_id)).pluck(:user_id, :notification_level).to_h + notification_levels = + TopicUser + .where(topic_id: @original_topic.id, user_id: posts.pluck(:user_id)) + .pluck(:user_id, :notification_level) + .to_h else - notification_levels = posts.pluck(:user_id).uniq.map { |user_id| [user_id, TopicUser.notification_levels[:watching]] }.to_h + notification_levels = + posts + .pluck(:user_id) + .uniq + .map { |user_id| [user_id, TopicUser.notification_levels[:watching]] } + .to_h end else notification_levels = [[@destination_topic.user_id, TopicUser.notification_levels[:watching]]] @@ -506,7 +524,10 @@ class PostMover user_id, @destination_topic.id, notification_level: notification_level, - notifications_reason_id: TopicUser.notification_reasons[destination_topic.user_id == user_id ? :created_topic : :created_post] + notifications_reason_id: + TopicUser.notification_reasons[ + destination_topic.user_id == user_id ? :created_topic : :created_post + ], ) end end @@ -514,37 +535,34 @@ class PostMover def add_allowed_users(usernames) return unless usernames.present? - names = usernames.split(',').flatten - User.where(username: names).find_each do |user| - destination_topic.topic_allowed_users.build(user_id: user.id) unless destination_topic.topic_allowed_users.where(user_id: user.id).exists? - end + names = usernames.split(",").flatten + User + .where(username: names) + .find_each do |user| + unless destination_topic.topic_allowed_users.where(user_id: user.id).exists? + destination_topic.topic_allowed_users.build(user_id: user.id) + end + end destination_topic.save! end def enqueue_jobs(topic) @post_creator.enqueue_jobs if @post_creator - Jobs.enqueue( - :notify_moved_posts, - post_ids: post_ids, - moved_by_id: user.id - ) + Jobs.enqueue(:notify_moved_posts, post_ids: post_ids, moved_by_id: user.id) - Jobs.enqueue( - :delete_inaccessible_notifications, - topic_id: topic.id - ) + Jobs.enqueue(:delete_inaccessible_notifications, topic_id: topic.id) end def close_topic_and_schedule_deletion - @original_topic.update_status('closed', true, @user) + @original_topic.update_status("closed", true, @user) days_to_deleting = SiteSetting.delete_merged_stub_topics_after_days if days_to_deleting > 0 @original_topic.set_or_create_timer( TopicTimer.types[:delete], days_to_deleting * 24, - by_user: @user + by_user: @user, ) end end diff --git a/app/models/post_reply.rb b/app/models/post_reply.rb index 1cedb847a9..d2cce1de36 100644 --- a/app/models/post_reply.rb +++ b/app/models/post_reply.rb @@ -2,7 +2,7 @@ class PostReply < ActiveRecord::Base belongs_to :post - belongs_to :reply, foreign_key: :reply_post_id, class_name: 'Post' + belongs_to :reply, foreign_key: :reply_post_id, class_name: "Post" validates_uniqueness_of :reply_post_id, scope: :post_id validate :ensure_same_topic @@ -11,10 +11,7 @@ class PostReply < ActiveRecord::Base def ensure_same_topic if post.topic_id != reply.topic_id - self.errors.add( - :base, - I18n.t("activerecord.errors.models.post_reply.base.different_topic") - ) + self.errors.add(:base, I18n.t("activerecord.errors.models.post_reply.base.different_topic")) end end end diff --git a/app/models/post_reply_key.rb b/app/models/post_reply_key.rb index 83686df18b..1084996ec8 100644 --- a/app/models/post_reply_key.rb +++ b/app/models/post_reply_key.rb @@ -11,7 +11,7 @@ class PostReplyKey < ActiveRecord::Base validates :reply_key, presence: true def reply_key - super&.delete('-') + super&.delete("-") end def self.generate_reply_key diff --git a/app/models/post_revision.rb b/app/models/post_revision.rb index 9d9d48c319..9155485238 100644 --- a/app/models/post_revision.rb +++ b/app/models/post_revision.rb @@ -39,7 +39,6 @@ class PostRevision < ActiveRecord::Base def create_notification PostActionNotifier.after_create_post_revision(self) end - end # == Schema Information diff --git a/app/models/post_timing.rb b/app/models/post_timing.rb index 9efb3e59f2..1a3863672f 100644 --- a/app/models/post_timing.rb +++ b/app/models/post_timing.rb @@ -10,7 +10,8 @@ class PostTiming < ActiveRecord::Base def self.pretend_read(topic_id, actual_read_post_number, pretend_read_post_number) # This is done in SQL cause the logic is quite tricky and we want to do this in one db hit # - DB.exec("INSERT INTO post_timings(topic_id, user_id, post_number, msecs) + DB.exec( + "INSERT INTO post_timings(topic_id, user_id, post_number, msecs) SELECT :topic_id, user_id, :pretend_read_post_number, 1 FROM post_timings pt WHERE topic_id = :topic_id AND @@ -22,27 +23,32 @@ class PostTiming < ActiveRecord::Base pt1.user_id = pt.user_id ) ", - pretend_read_post_number: pretend_read_post_number, - topic_id: topic_id, - actual_read_post_number: actual_read_post_number - ) + pretend_read_post_number: pretend_read_post_number, + topic_id: topic_id, + actual_read_post_number: actual_read_post_number, + ) TopicUser.ensure_consistency!(topic_id) end def self.record_new_timing(args) - row_count = DB.exec("INSERT INTO post_timings (topic_id, user_id, post_number, msecs) + row_count = + DB.exec( + "INSERT INTO post_timings (topic_id, user_id, post_number, msecs) SELECT :topic_id, :user_id, :post_number, :msecs ON CONFLICT DO NOTHING", - args) + args, + ) # concurrency is hard, we are not running serialized so this can possibly # still happen, if it happens we just don't care, its an invalid record anyway return if row_count == 0 - Post.where(['topic_id = :topic_id and post_number = :post_number', args]).update_all 'reads = reads + 1' + Post.where( + ["topic_id = :topic_id and post_number = :post_number", args], + ).update_all "reads = reads + 1" return if Topic.exists?(id: args[:topic_id], archetype: Archetype.private_message) - UserStat.where(user_id: args[:user_id]).update_all 'posts_read_count = posts_read_count + 1' + UserStat.where(user_id: args[:user_id]).update_all "posts_read_count = posts_read_count + 1" end # Increases a timer if a row exists, otherwise create it @@ -65,13 +71,16 @@ class PostTiming < ActiveRecord::Base last_read = post_number - 1 PostTiming.transaction do - PostTiming.where("topic_id = ? AND user_id = ? AND post_number > ?", topic.id, user.id, last_read).delete_all - if last_read < 1 - last_read = nil - end + PostTiming.where( + "topic_id = ? AND user_id = ? AND post_number > ?", + topic.id, + user.id, + last_read, + ).delete_all + last_read = nil if last_read < 1 TopicUser.where(user_id: user.id, topic_id: topic.id).update_all( - last_read_post_number: last_read + last_read_post_number: last_read, ) topic.posts.find_by(post_number: post_number).decrement!(:reads) @@ -86,28 +95,23 @@ class PostTiming < ActiveRecord::Base def self.destroy_for(user_id, topic_ids) PostTiming.transaction do - PostTiming - .where('user_id = ? and topic_id in (?)', user_id, topic_ids) - .delete_all + PostTiming.where("user_id = ? and topic_id in (?)", user_id, topic_ids).delete_all - TopicUser - .where('user_id = ? and topic_id in (?)', user_id, topic_ids) - .delete_all + TopicUser.where("user_id = ? and topic_id in (?)", user_id, topic_ids).delete_all - Post.where(topic_id: topic_ids).update_all('reads = reads - 1') + Post.where(topic_id: topic_ids).update_all("reads = reads - 1") date = Topic.listable_topics.where(id: topic_ids).minimum(:updated_at) - if date - set_minimum_first_unread!(user_id: user_id, date: date) - end + set_minimum_first_unread!(user_id: user_id, date: date) if date end end def self.set_minimum_first_unread_pm!(topic:, user_id:, date:) if topic.topic_allowed_users.exists?(user_id: user_id) - UserStat.where("first_unread_pm_at > ? AND user_id = ?", date, user_id) - .update_all(first_unread_pm_at: date) + UserStat.where("first_unread_pm_at > ? AND user_id = ?", date, user_id).update_all( + first_unread_pm_at: date, + ) else DB.exec(<<~SQL, date: date, user_id: user_id, topic_id: topic.id) UPDATE group_users gu @@ -155,12 +159,10 @@ class PostTiming < ActiveRecord::Base end timings.each_with_index do |(post_number, time), index| - join_table << "SELECT #{topic_id.to_i} topic_id, #{post_number.to_i} post_number, #{current_user.id.to_i} user_id, #{time.to_i} msecs, #{index} idx" - highest_seen = post_number.to_i > highest_seen ? - post_number.to_i : highest_seen + highest_seen = post_number.to_i > highest_seen ? post_number.to_i : highest_seen end if join_table.length > 0 @@ -188,10 +190,12 @@ SQL timings.each_with_index do |(post_number, time), index| unless existing.include?(index) - PostTiming.record_new_timing(topic_id: topic_id, - post_number: post_number, - user_id: current_user.id, - msecs: time) + PostTiming.record_new_timing( + topic_id: topic_id, + post_number: post_number, + user_id: current_user.id, + msecs: time, + ) end end end @@ -203,7 +207,14 @@ SQL topic_time = max_time_per_post if topic_time > max_time_per_post - TopicUser.update_last_read(current_user, topic_id, highest_seen, new_posts_read, topic_time, opts) + TopicUser.update_last_read( + current_user, + topic_id, + highest_seen, + new_posts_read, + topic_time, + opts, + ) TopicGroup.update_last_read(current_user, topic_id, highest_seen) if total_changed > 0 diff --git a/app/models/previous_replies_site_setting.rb b/app/models/previous_replies_site_setting.rb index 59b7c2d246..ff1ed64b9a 100644 --- a/app/models/previous_replies_site_setting.rb +++ b/app/models/previous_replies_site_setting.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true class PreviousRepliesSiteSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - values.any? { |v| v[:value] == val.to_i } + val.to_i.to_s == val.to_s && values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - { name: 'user.email_previous_replies.always', value: 0 }, - { name: 'user.email_previous_replies.unless_emailed', value: 1 }, - { name: 'user.email_previous_replies.never', value: 2 }, + { name: "user.email_previous_replies.always", value: 0 }, + { name: "user.email_previous_replies.unless_emailed", value: 1 }, + { name: "user.email_previous_replies.never", value: 2 }, ] end def self.translate_names? true end - end diff --git a/app/models/private_message_topic_tracking_state.rb b/app/models/private_message_topic_tracking_state.rb index cd03054c6f..48db452375 100644 --- a/app/models/private_message_topic_tracking_state.rb +++ b/app/models/private_message_topic_tracking_state.rb @@ -30,8 +30,8 @@ class PrivateMessageTopicTrackingState sql + "\n\n LIMIT :max_topics", { max_topics: TopicTrackingState::MAX_TOPICS, - min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime - } + min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, + }, ) end @@ -41,9 +41,7 @@ class PrivateMessageTopicTrackingState sql << report_raw_sql(user, skip_new: true) end - def self.report_raw_sql(user, skip_unread: false, - skip_new: false) - + def self.report_raw_sql(user, skip_unread: false, skip_new: false) unread = if skip_unread "1=0" @@ -101,9 +99,7 @@ class PrivateMessageTopicTrackingState topic = post.topic return unless topic.private_message? - scope = TopicUser - .tracking(post.topic_id) - .includes(user: [:user_stat, :user_option]) + scope = TopicUser.tracking(post.topic_id).includes(user: %i[user_stat user_option]) allowed_group_ids = topic.allowed_groups.pluck(:id) @@ -115,9 +111,11 @@ class PrivateMessageTopicTrackingState end if group_ids.present? - scope = scope - .joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id") - .where("gu.group_id IN (?)", group_ids) + scope = + scope.joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id").where( + "gu.group_id IN (?)", + group_ids, + ) end # Note: At some point we may want to make the same peformance optimisation @@ -127,31 +125,27 @@ class PrivateMessageTopicTrackingState # # cf. f6c852bf8e7f4dea519425ba87a114f22f52a8f4 scope - .select([:user_id, :last_read_post_number, :notification_level]) + .select(%i[user_id last_read_post_number notification_level]) .each do |tu| + if tu.last_read_post_number.nil? && + topic.created_at < tu.user.user_option.treat_as_new_topic_start_date + next + end - if tu.last_read_post_number.nil? && - topic.created_at < tu.user.user_option.treat_as_new_topic_start_date - - next - end - - message = { - topic_id: post.topic_id, - message_type: UNREAD_MESSAGE_TYPE, - payload: { - last_read_post_number: tu.last_read_post_number, - highest_post_number: post.post_number, - notification_level: tu.notification_level, - group_ids: allowed_group_ids, - created_by_user_id: post.user_id + message = { + topic_id: post.topic_id, + message_type: UNREAD_MESSAGE_TYPE, + payload: { + last_read_post_number: tu.last_read_post_number, + highest_post_number: post.post_number, + notification_level: tu.notification_level, + group_ids: allowed_group_ids, + created_by_user_id: post.user_id, + }, } - } - MessageBus.publish(self.user_channel(tu.user_id), message.as_json, - user_ids: [tu.user_id] - ) - end + MessageBus.publish(self.user_channel(tu.user_id), message.as_json, user_ids: [tu.user_id]) + end end def self.publish_new(topic) @@ -165,16 +159,22 @@ class PrivateMessageTopicTrackingState highest_post_number: 1, group_ids: topic.allowed_groups.pluck(:id), created_by_user_id: topic.user_id, - } + }, }.as_json - topic.allowed_users.pluck(:id).each do |user_id| - MessageBus.publish(self.user_channel(user_id), message, user_ids: [user_id]) - end + topic + .allowed_users + .pluck(:id) + .each do |user_id| + MessageBus.publish(self.user_channel(user_id), message, user_ids: [user_id]) + end - topic.allowed_groups.pluck(:id).each do |group_id| - MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id]) - end + topic + .allowed_groups + .pluck(:id) + .each do |group_id| + MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id]) + end end def self.publish_group_archived(topic:, group_id:, acting_user_id: nil) @@ -185,15 +185,11 @@ class PrivateMessageTopicTrackingState topic_id: topic.id, payload: { group_ids: [group_id], - acting_user_id: acting_user_id - } + acting_user_id: acting_user_id, + }, }.as_json - MessageBus.publish( - self.group_channel(group_id), - message, - group_ids: [group_id] - ) + MessageBus.publish(self.group_channel(group_id), message, group_ids: [group_id]) end def self.publish_read(topic_id, last_read_post_number, user, notification_level = nil) @@ -203,7 +199,7 @@ class PrivateMessageTopicTrackingState topic_id: topic_id, user: user, last_read_post_number: last_read_post_number, - notification_level: notification_level + notification_level: notification_level, ) end diff --git a/app/models/published_page.rb b/app/models/published_page.rb index 8345ae05bb..937ba59310 100644 --- a/app/models/published_page.rb +++ b/app/models/published_page.rb @@ -10,7 +10,7 @@ class PublishedPage < ActiveRecord::Base def slug_format if slug !~ /^[a-zA-Z\-\_0-9]+$/ errors.add(:slug, I18n.t("publish_page.slug_errors.invalid")) - elsif ["check-slug", "by-topic"].include?(slug) + elsif %w[check-slug by-topic].include?(slug) errors.add(:slug, I18n.t("publish_page.slug_errors.unavailable")) end end @@ -26,16 +26,17 @@ class PublishedPage < ActiveRecord::Base def self.publish!(publisher, topic, slug, options = {}) pp = nil - results = transaction do - pp = find_or_initialize_by(topic: topic) - pp.slug = slug.strip - pp.public = options[:public] || false + results = + transaction do + pp = find_or_initialize_by(topic: topic) + pp.slug = slug.strip + pp.public = options[:public] || false - if pp.save - StaffActionLogger.new(publisher).log_published_page(topic.id, slug) - [true, pp] + if pp.save + StaffActionLogger.new(publisher).log_published_page(topic.id, slug) + [true, pp] + end end - end results || [false, pp] end diff --git a/app/models/quoted_post.rb b/app/models/quoted_post.rb index 9a6a96e9eb..54d68c9a92 100644 --- a/app/models/quoted_post.rb +++ b/app/models/quoted_post.rb @@ -2,36 +2,36 @@ class QuotedPost < ActiveRecord::Base belongs_to :post - belongs_to :quoted_post, class_name: 'Post' + belongs_to :quoted_post, class_name: "Post" # NOTE we already have a path that does this for topic links, # however topic links exclude quotes and links within a topic # we are double parsing this fragment, this may be worth optimising later def self.extract_from(post) - doc = Nokogiri::HTML5.fragment(post.cooked) uniq = {} - doc.css("aside.quote[data-topic]").each do |a| - topic_id = a['data-topic'].to_i - post_number = a['data-post'].to_i + doc + .css("aside.quote[data-topic]") + .each do |a| + topic_id = a["data-topic"].to_i + post_number = a["data-post"].to_i - next if topic_id == 0 || post_number == 0 - next if uniq[[topic_id, post_number]] - next if post.topic_id == topic_id && post.post_number == post_number + next if topic_id == 0 || post_number == 0 + next if uniq[[topic_id, post_number]] + next if post.topic_id == topic_id && post.post_number == post_number - uniq[[topic_id, post_number]] = true - end + uniq[[topic_id, post_number]] = true + end if uniq.length == 0 DB.exec("DELETE FROM quoted_posts WHERE post_id = :post_id", post_id: post.id) else - args = { post_id: post.id, topic_ids: uniq.keys.map(&:first), - post_numbers: uniq.keys.map(&:second) + post_numbers: uniq.keys.map(&:second), } DB.exec(<<~SQL, args) @@ -67,14 +67,14 @@ class QuotedPost < ActiveRecord::Base reply_quoted = false if post.reply_to_post_number - reply_post_id = Post.where(topic_id: post.topic_id, post_number: post.reply_to_post_number).pluck_first(:id) - reply_quoted = reply_post_id.present? && QuotedPost.where(post_id: post.id, quoted_post_id: reply_post_id).count > 0 - end - - if reply_quoted != post.reply_quoted - post.update_columns(reply_quoted: reply_quoted) + reply_post_id = + Post.where(topic_id: post.topic_id, post_number: post.reply_to_post_number).pluck_first(:id) + reply_quoted = + reply_post_id.present? && + QuotedPost.where(post_id: post.id, quoted_post_id: reply_post_id).count > 0 end + post.update_columns(reply_quoted: reply_quoted) if reply_quoted != post.reply_quoted end end diff --git a/app/models/remote_theme.rb b/app/models/remote_theme.rb index 2c7f4e1ed1..baed645654 100644 --- a/app/models/remote_theme.rb +++ b/app/models/remote_theme.rb @@ -1,28 +1,35 @@ # frozen_string_literal: true class RemoteTheme < ActiveRecord::Base - METADATA_PROPERTIES = %i{ - license_url - about_url - authors - theme_version - minimum_discourse_version - maximum_discourse_version - } + METADATA_PROPERTIES = %i[ + license_url + about_url + authors + theme_version + minimum_discourse_version + maximum_discourse_version + ] - class ImportError < StandardError; end + class ImportError < StandardError + end - ALLOWED_FIELDS = %w{scss embedded_scss head_tag header after_header body_tag footer} + ALLOWED_FIELDS = %w[scss embedded_scss head_tag header after_header body_tag footer] - GITHUB_REGEXP = /^https?:\/\/github\.com\// - GITHUB_SSH_REGEXP = /^ssh:\/\/git@github\.com:/ + GITHUB_REGEXP = %r{^https?://github\.com/} + GITHUB_SSH_REGEXP = %r{^ssh://git@github\.com:} has_one :theme, autosave: false - scope :joined_remotes, -> { - joins("JOIN themes ON themes.remote_theme_id = remote_themes.id").where.not(remote_url: "") - } + scope :joined_remotes, + -> { + joins("JOIN themes ON themes.remote_theme_id = remote_themes.id").where.not( + remote_url: "", + ) + } - validates_format_of :minimum_discourse_version, :maximum_discourse_version, with: Discourse::VERSION_REGEXP, allow_nil: true + validates_format_of :minimum_discourse_version, + :maximum_discourse_version, + with: Discourse::VERSION_REGEXP, + allow_nil: true def self.extract_theme_info(importer) json = JSON.parse(importer["about.json"]) @@ -32,7 +39,14 @@ class RemoteTheme < ActiveRecord::Base raise ImportError.new I18n.t("themes.import_error.about_json") end - def self.update_zipped_theme(filename, original_filename, match_theme: false, user: Discourse.system_user, theme_id: nil, update_components: nil) + def self.update_zipped_theme( + filename, + original_filename, + match_theme: false, + user: Discourse.system_user, + theme_id: nil, + update_components: nil + ) importer = ThemeStore::ZipImporter.new(filename, original_filename) importer.import! @@ -60,10 +74,18 @@ class RemoteTheme < ActiveRecord::Base child_components = child_components.map { |url| ThemeStore::GitImporter.new(url.strip).url } if update_components == "sync" - ChildTheme.joins(child_theme: :remote_theme).where("remote_themes.remote_url NOT IN (?)", child_components).delete_all + ChildTheme + .joins(child_theme: :remote_theme) + .where("remote_themes.remote_url NOT IN (?)", child_components) + .delete_all end - child_components -= theme.child_themes.joins(:remote_theme).where("remote_themes.remote_url IN (?)", child_components).pluck("remote_themes.remote_url") + child_components -= + theme + .child_themes + .joins(:remote_theme) + .where("remote_themes.remote_url IN (?)", child_components) + .pluck("remote_themes.remote_url") theme.child_components = child_components theme.update_child_components end @@ -106,7 +128,9 @@ class RemoteTheme < ActiveRecord::Base end def self.out_of_date_themes - self.joined_remotes.where("commits_behind > 0 OR remote_version <> local_version") + self + .joined_remotes + .where("commits_behind > 0 OR remote_version <> local_version") .where(themes: { enabled: true }) .pluck("themes.name", "themes.id") end @@ -164,13 +188,28 @@ class RemoteTheme < ActiveRecord::Base if path = importer.real_path(relative_path) new_path = "#{File.dirname(path)}/#{SecureRandom.hex}#{File.extname(path)}" File.rename(path, new_path) # OptimizedImage has strict file name restrictions, so rename temporarily - upload = UploadCreator.new(File.open(new_path), File.basename(relative_path), for_theme: true).create_for(theme.user_id) + upload = + UploadCreator.new( + File.open(new_path), + File.basename(relative_path), + for_theme: true, + ).create_for(theme.user_id) if !upload.errors.empty? - raise ImportError, I18n.t("themes.import_error.upload", name: name, errors: upload.errors.full_messages.join(",")) + raise ImportError, + I18n.t( + "themes.import_error.upload", + name: name, + errors: upload.errors.full_messages.join(","), + ) end - updated_fields << theme.set_field(target: :common, name: name, type: :theme_upload_var, upload_id: upload.id) + updated_fields << theme.set_field( + target: :common, + name: name, + type: :theme_upload_var, + upload_id: upload.id, + ) end end @@ -185,14 +224,25 @@ class RemoteTheme < ActiveRecord::Base self.public_send(:"#{property}=", theme_info[property.to_s]) end if !self.valid? - raise ImportError, I18n.t("themes.import_error.about_json_values", errors: self.errors.full_messages.join(",")) + raise ImportError, + I18n.t( + "themes.import_error.about_json_values", + errors: self.errors.full_messages.join(","), + ) end ThemeModifierSet.modifiers.keys.each do |modifier_name| - theme.theme_modifier_set.public_send(:"#{modifier_name}=", theme_info.dig("modifiers", modifier_name.to_s)) + theme.theme_modifier_set.public_send( + :"#{modifier_name}=", + theme_info.dig("modifiers", modifier_name.to_s), + ) end if !theme.theme_modifier_set.valid? - raise ImportError, I18n.t("themes.import_error.modifier_values", errors: theme.theme_modifier_set.errors.full_messages.join(",")) + raise ImportError, + I18n.t( + "themes.import_error.modifier_values", + errors: theme.theme_modifier_set.errors.full_messages.join(","), + ) end importer.all_files.each do |filename| @@ -230,9 +280,7 @@ class RemoteTheme < ActiveRecord::Base return unless hex override = hex.downcase - if override !~ /\A[0-9a-f]{6}\z/ - override = nil - end + override = nil if override !~ /\A[0-9a-f]{6}\z/ override end @@ -247,8 +295,9 @@ class RemoteTheme < ActiveRecord::Base # Update main colors ColorScheme.base.colors_hashes.each do |color| override = normalize_override(colors[color[:name]]) - color_scheme_color = scheme.color_scheme_colors.to_a.find { |c| c.name == color[:name] } || - scheme.color_scheme_colors.build(name: color[:name]) + color_scheme_color = + scheme.color_scheme_colors.to_a.find { |c| c.name == color[:name] } || + scheme.color_scheme_colors.build(name: color[:name]) color_scheme_color.hex = override || color[:hex] theme.notify_color_change(color_scheme_color) if color_scheme_color.hex_changed? end @@ -275,9 +324,7 @@ class RemoteTheme < ActiveRecord::Base # we may have stuff pointed at the incorrect scheme? end - if theme.new_record? - theme.color_scheme = ordered_schemes.first - end + theme.color_scheme = ordered_schemes.first if theme.new_record? end def github_diff_link diff --git a/app/models/remove_muted_tags_from_latest_site_setting.rb b/app/models/remove_muted_tags_from_latest_site_setting.rb index 9ca328e328..fb07d94b7a 100644 --- a/app/models/remove_muted_tags_from_latest_site_setting.rb +++ b/app/models/remove_muted_tags_from_latest_site_setting.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RemoveMutedTagsFromLatestSiteSetting < EnumSiteSetting - ALWAYS ||= "always" ONLY_MUTED ||= "only_muted" NEVER ||= "never" @@ -14,7 +13,7 @@ class RemoveMutedTagsFromLatestSiteSetting < EnumSiteSetting @values ||= [ { name: "admin.tags.remove_muted_tags_from_latest.always", value: ALWAYS }, { name: "admin.tags.remove_muted_tags_from_latest.only_muted", value: ONLY_MUTED }, - { name: "admin.tags.remove_muted_tags_from_latest.never", value: NEVER } + { name: "admin.tags.remove_muted_tags_from_latest.never", value: NEVER }, ] end diff --git a/app/models/report.rb b/app/models/report.rb index fa893edfcb..cd9b7623ac 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -5,7 +5,16 @@ class Report # and you want to ensure cache is reset SCHEMA_VERSION = 4 - FILTERS = [:name, :start_date, :end_date, :category, :group, :trust_level, :file_extension, :include_subcategories] + FILTERS = %i[ + name + start_date + end_date + category + group + trust_level + file_extension + include_subcategories + ] include Reports::PostEdits include Reports::TopTrafficSources @@ -51,11 +60,30 @@ class Report include Reports::TopUsersByLikesReceivedFromInferiorTrustLevel include Reports::TopUsersByLikesReceivedFromAVarietyOfPeople - attr_accessor :type, :data, :total, :prev30Days, :start_date, - :end_date, :labels, :prev_period, :facets, :limit, :average, - :percent, :higher_is_better, :icon, :modes, :prev_data, - :prev_start_date, :prev_end_date, :dates_filtering, :error, - :primary_color, :secondary_color, :filters, :available_filters + attr_accessor :type, + :data, + :total, + :prev30Days, + :start_date, + :end_date, + :labels, + :prev_period, + :facets, + :limit, + :average, + :percent, + :higher_is_better, + :icon, + :modes, + :prev_data, + :prev_start_date, + :prev_end_date, + :dates_filtering, + :error, + :primary_color, + :secondary_color, + :filters, + :available_filters def self.default_days 30 @@ -63,16 +91,8 @@ class Report def self.default_labels [ - { - type: :date, - property: :x, - title: I18n.t("reports.default.labels.day") - }, - { - type: :number, - property: :y, - title: I18n.t("reports.default.labels.count") - }, + { type: :date, property: :x, title: I18n.t("reports.default.labels.day") }, + { type: :number, property: :y, title: I18n.t("reports.default.labels.count") }, ] end @@ -84,13 +104,13 @@ class Report @average = false @percent = false @higher_is_better = true - @modes = [:table, :chart] + @modes = %i[table chart] @prev_data = nil @dates_filtering = true @available_filters = {} @filters = {} - tertiary = ColorScheme.hex_for_name('tertiary') || '0088cc' + tertiary = ColorScheme.hex_for_name("tertiary") || "0088cc" @primary_color = rgba_color(tertiary) @secondary_color = rgba_color(tertiary, 0.1) end @@ -105,13 +125,16 @@ class Report report.limit, report.filters.blank? ? nil : MultiJson.dump(report.filters), SCHEMA_VERSION, - ].compact.map(&:to_s).join(':') + ].compact.map(&:to_s).join(":") end def add_filter(name, options = {}) if options[:type].blank? options[:type] = name - Discourse.deprecate("#{name} filter should define a `:type` option. Temporarily setting type to #{name}.", drop_from: '2.9.0') + Discourse.deprecate( + "#{name} filter should define a `:type` option. Temporarily setting type to #{name}.", + drop_from: "2.9.0", + ) end available_filters[name] = options @@ -123,12 +146,12 @@ class Report def add_category_filter category_id = filters[:category].to_i if filters[:category].present? - add_filter('category', type: 'category', default: category_id) + add_filter("category", type: "category", default: category_id) return if category_id.blank? include_subcategories = filters[:include_subcategories] include_subcategories = !!ActiveRecord::Type::Boolean.new.cast(include_subcategories) - add_filter('include_subcategories', type: 'bool', default: include_subcategories) + add_filter("include_subcategories", type: "bool", default: include_subcategories) [category_id, include_subcategories] end @@ -136,12 +159,10 @@ class Report def self.clear_cache(type = nil) pattern = type ? "reports:#{type}:*" : "reports:*" - Discourse.cache.keys(pattern).each do |key| - Discourse.cache.redis.del(key) - end + Discourse.cache.keys(pattern).each { |key| Discourse.cache.redis.del(key) } end - def self.wrap_slow_query(timeout = 20000) + def self.wrap_slow_query(timeout = 20_000) ActiveRecord::Base.connection.transaction do # Allows only read only transactions DB.exec "SET TRANSACTION READ ONLY" @@ -195,8 +216,12 @@ class Report json[:prev30Days] = self.prev30Days if self.prev30Days json[:limit] = self.limit if self.limit - if type == 'page_view_crawler_reqs' - json[:related_report] = Report.find('web_crawlers', start_date: start_date, end_date: end_date)&.as_json + if type == "page_view_crawler_reqs" + json[:related_report] = Report.find( + "web_crawlers", + start_date: start_date, + end_date: end_date, + )&.as_json end end end @@ -212,7 +237,7 @@ class Report report = Report.new(type) report.start_date = opts[:start_date] if opts[:start_date] report.end_date = opts[:end_date] if opts[:end_date] - report.facets = opts[:facets] || [:total, :prev30Days] + report.facets = opts[:facets] || %i[total prev30Days] report.limit = opts[:limit] if opts[:limit] report.average = opts[:average] if opts[:average] report.percent = opts[:percent] if opts[:percent] @@ -268,7 +293,9 @@ class Report # given reports can be added by plugins we don’t want dashboard failures # on report computation, however we do want to log which report is provoking # an error - Rails.logger.error("Error while computing report `#{report.type}`: #{e.message}\n#{e.backtrace.join("\n")}") + Rails.logger.error( + "Error while computing report `#{report.type}`: #{e.message}\n#{e.backtrace.join("\n")}", + ) end report @@ -277,32 +304,35 @@ class Report def self.req_report(report, filter = nil) data = if filter == :page_view_total - ApplicationRequest.where(req_type: [ - ApplicationRequest.req_types.reject { |k, v| k =~ /mobile/ }.map { |k, v| v if k =~ /page_view/ }.compact - ].flatten) + ApplicationRequest.where( + req_type: [ + ApplicationRequest + .req_types + .reject { |k, v| k =~ /mobile/ } + .map { |k, v| v if k =~ /page_view/ } + .compact, + ].flatten, + ) else ApplicationRequest.where(req_type: ApplicationRequest.req_types[filter]) end - if filter == :page_view_total - report.icon = 'file' - end + report.icon = "file" if filter == :page_view_total report.data = [] - data.where('date >= ? AND date <= ?', report.start_date, report.end_date) + data + .where("date >= ? AND date <= ?", report.start_date, report.end_date) .order(date: :asc) .group(:date) .sum(:count) - .each do |date, count| - report.data << { x: date, y: count } - end + .each { |date, count| report.data << { x: date, y: count } } report.total = data.sum(:count) - report.prev30Days = data.where( - 'date >= ? AND date < ?', - (report.start_date - 31.days), report.start_date - ).sum(:count) + report.prev30Days = + data.where("date >= ? AND date < ?", (report.start_date - 31.days), report.start_date).sum( + :count, + ) end def self.report_about(report, subject_class, report_method = :count_per_day) @@ -313,9 +343,9 @@ class Report def self.basic_report_about(report, subject_class, report_method, *args) report.data = [] - subject_class.public_send(report_method, *args).each do |date, count| - report.data << { x: date, y: count } - end + subject_class + .public_send(report_method, *args) + .each { |date, count| report.data << { x: date, y: count } } end def self.add_prev_data(report, subject_class, report_method, *args) @@ -325,25 +355,27 @@ class Report end end - def self.add_counts(report, subject_class, query_column = 'created_at') + def self.add_counts(report, subject_class, query_column = "created_at") if report.facets.include?(:prev_period) - prev_data = subject_class - .where("#{query_column} >= ? and #{query_column} < ?", + prev_data = + subject_class.where( + "#{query_column} >= ? and #{query_column} < ?", report.prev_start_date, - report.prev_end_date) + report.prev_end_date, + ) report.prev_period = prev_data.count end - if report.facets.include?(:total) - report.total = subject_class.count - end + report.total = subject_class.count if report.facets.include?(:total) if report.facets.include?(:prev30Days) - report.prev30Days = subject_class - .where("#{query_column} >= ? and #{query_column} < ?", + report.prev30Days = + subject_class.where( + "#{query_column} >= ? and #{query_column} < ?", report.start_date - 30.days, - report.start_date).count + report.start_date, + ).count end end @@ -351,28 +383,43 @@ class Report category_id, include_subcategories = report.add_category_filter report.data = [] - PostAction.count_per_day_for_type(post_action_type, category_id: category_id, include_subcategories: include_subcategories, start_date: report.start_date, end_date: report.end_date).each do |date, count| - report.data << { x: date, y: count } - end + PostAction + .count_per_day_for_type( + post_action_type, + category_id: category_id, + include_subcategories: include_subcategories, + start_date: report.start_date, + end_date: report.end_date, + ) + .each { |date, count| report.data << { x: date, y: count } } countable = PostAction.unscoped.where(post_action_type_id: post_action_type) if category_id if include_subcategories - countable = countable.joins(post: :topic).where('topics.category_id IN (?)', Category.subcategory_ids(category_id)) + countable = + countable.joins(post: :topic).where( + "topics.category_id IN (?)", + Category.subcategory_ids(category_id), + ) else - countable = countable.joins(post: :topic).where('topics.category_id = ?', category_id) + countable = countable.joins(post: :topic).where("topics.category_id = ?", category_id) end end - add_counts report, countable, 'post_actions.created_at' + add_counts report, countable, "post_actions.created_at" end def self.private_messages_report(report, topic_subtype) - report.icon = 'envelope' - subject = Topic.where('topics.user_id > 0') - basic_report_about report, subject, :private_message_topics_count_per_day, report.start_date, report.end_date, topic_subtype - subject = Topic.private_messages.where('topics.user_id > 0').with_subtype(topic_subtype) - add_counts report, subject, 'topics.created_at' + report.icon = "envelope" + subject = Topic.where("topics.user_id > 0") + basic_report_about report, + subject, + :private_message_topics_count_per_day, + report.start_date, + report.end_date, + topic_subtype + subject = Topic.private_messages.where("topics.user_id > 0").with_subtype(topic_subtype) + add_counts report, subject, "topics.created_at" end def lighten_color(hex, amount) @@ -386,31 +433,27 @@ class Report def rgba_color(hex, opacity = 1) rgbs = hex_to_rgbs(adjust_hex(hex)) - "rgba(#{rgbs.join(',')},#{opacity})" + "rgba(#{rgbs.join(",")},#{opacity})" end private def adjust_hex(hex) - hex = hex.gsub('#', '') + hex = hex.gsub("#", "") if hex.size == 3 chars = hex.scan(/\w/) hex = chars.zip(chars).flatten.join end - if hex.size < 3 - hex = hex.ljust(6, hex.last) - end + hex = hex.ljust(6, hex.last) if hex.size < 3 hex end def hex_to_rgbs(hex_color) - hex_color = hex_color.gsub('#', '') + hex_color = hex_color.gsub("#", "") rgbs = hex_color.scan(/../) - rgbs - .map! { |color| color.hex } - .map! { |rgb| rgb.to_i } + rgbs.map! { |color| color.hex }.map! { |rgb| rgb.to_i } end end diff --git a/app/models/reviewable.rb b/app/models/reviewable.rb index 1d010d0cf6..8f389cb670 100644 --- a/app/models/reviewable.rb +++ b/app/models/reviewable.rb @@ -4,10 +4,11 @@ class Reviewable < ActiveRecord::Base TYPE_TO_BASIC_SERIALIZER = { ReviewableFlaggedPost: BasicReviewableFlaggedPostSerializer, ReviewableQueuedPost: BasicReviewableQueuedPostSerializer, - ReviewableUser: BasicReviewableUserSerializer + ReviewableUser: BasicReviewableUserSerializer, } - class UpdateConflict < StandardError; end + class UpdateConflict < StandardError + end class InvalidAction < StandardError def initialize(action_id, klass) @@ -20,9 +21,9 @@ class Reviewable < ActiveRecord::Base attr_accessor :created_new validates_presence_of :type, :status, :created_by_id belongs_to :target, polymorphic: true - belongs_to :created_by, class_name: 'User' - belongs_to :target_created_by, class_name: 'User' - belongs_to :reviewable_by_group, class_name: 'Group' + belongs_to :created_by, class_name: "User" + belongs_to :target_created_by, class_name: "User" + belongs_to :reviewable_by_group, class_name: "Group" # Optional, for filtering belongs_to :topic @@ -31,34 +32,15 @@ class Reviewable < ActiveRecord::Base has_many :reviewable_histories, dependent: :destroy has_many :reviewable_scores, -> { order(created_at: :desc) }, dependent: :destroy - enum :status, { - pending: 0, - approved: 1, - rejected: 2, - ignored: 3, - deleted: 4 - } - enum :priority, { - low: 0, - medium: 5, - high: 10 - }, scopes: false, suffix: true - enum :sensitivity, { - disabled: 0, - low: 9, - medium: 6, - high: 3 - }, scopes: false, suffix: true + enum :status, { pending: 0, approved: 1, rejected: 2, ignored: 3, deleted: 4 } + enum :priority, { low: 0, medium: 5, high: 10 }, scopes: false, suffix: true + enum :sensitivity, { disabled: 0, low: 9, medium: 6, high: 3 }, scopes: false, suffix: true - after_create do - log_history(:created, created_by) - end + after_create { log_history(:created, created_by) } - after_commit(on: :create) do - DiscourseEvent.trigger(:reviewable_created, self) - end + after_commit(on: :create) { DiscourseEvent.trigger(:reviewable_created, self) } - after_commit(on: [:create, :update]) do + after_commit(on: %i[create update]) do Jobs.enqueue(:notify_reviewable, reviewable_id: self.id) if pending? end @@ -119,14 +101,15 @@ class Reviewable < ActiveRecord::Base reviewable_by_moderator: false, potential_spam: true ) - reviewable = new( - target: target, - topic: topic, - created_by: created_by, - reviewable_by_moderator: reviewable_by_moderator, - payload: payload, - potential_spam: potential_spam - ) + reviewable = + new( + target: target, + topic: topic, + created_by: created_by, + reviewable_by_moderator: reviewable_by_moderator, + payload: payload, + potential_spam: potential_spam, + ) reviewable.created_new! if target.blank? || !Reviewable.where(target: target, type: reviewable.type).exists? @@ -147,7 +130,7 @@ class Reviewable < ActiveRecord::Base status: statuses[:pending], id: target.id, type: target.class.name, - potential_spam: potential_spam == true ? true : nil + potential_spam: potential_spam == true ? true : nil, } row = DB.query_single(<<~SQL, update_args) @@ -187,22 +170,22 @@ class Reviewable < ActiveRecord::Base meta_topic_id: nil, force_review: false ) - type_bonus = PostActionType.where(id: reviewable_score_type).pluck(:score_bonus)[0] || 0 take_action_bonus = take_action ? 5.0 : 0.0 user_accuracy_bonus = ReviewableScore.user_accuracy_bonus(user) sub_total = ReviewableScore.calculate_score(user, type_bonus, take_action_bonus) - rs = reviewable_scores.new( - user: user, - status: :pending, - reviewable_score_type: reviewable_score_type, - score: sub_total, - user_accuracy_bonus: user_accuracy_bonus, - meta_topic_id: meta_topic_id, - take_action_bonus: take_action_bonus, - created_at: created_at || Time.zone.now - ) + rs = + reviewable_scores.new( + user: user, + status: :pending, + reviewable_score_type: reviewable_score_type, + score: sub_total, + user_accuracy_bonus: user_accuracy_bonus, + meta_topic_id: meta_topic_id, + take_action_bonus: take_action_bonus, + created_at: created_at || Time.zone.now, + ) rs.reason = reason.to_s if reason rs.save! @@ -217,7 +200,7 @@ class Reviewable < ActiveRecord::Base def self.set_priorities(values) values.each do |k, v| id = priorities[k] - PluginStore.set('reviewables', "priority_#{id}", v) unless id.nil? + PluginStore.set("reviewables", "priority_#{id}", v) unless id.nil? end end @@ -225,10 +208,8 @@ class Reviewable < ActiveRecord::Base return Float::MAX if sensitivity == 0 ratio = sensitivity / sensitivities[:low].to_f - high = ( - PluginStore.get('reviewables', "priority_#{priorities[:high]}") || - typical_sensitivity - ).to_f + high = + (PluginStore.get("reviewables", "priority_#{priorities[:high]}") || typical_sensitivity).to_f # We want this to be hard to reach ((high.to_f * ratio) * scale).truncate(2) @@ -257,7 +238,7 @@ class Reviewable < ActiveRecord::Base priority ||= SiteSetting.reviewable_default_visibility id = priorities[priority] return 0.0 if id.nil? - PluginStore.get('reviewables', "priority_#{id}").to_f + PluginStore.get("reviewables", "priority_#{id}").to_f end def history @@ -269,14 +250,15 @@ class Reviewable < ActiveRecord::Base reviewable_history_type: reviewable_history_type, status: status, created_by: performed_by, - edited: edited + edited: edited, ) end def apply_review_group - return unless SiteSetting.enable_category_group_moderation? && - category.present? && - category.reviewable_by_group_id + unless SiteSetting.enable_category_group_moderation? && category.present? && + category.reviewable_by_group_id + return + end self.reviewable_by_group_id = category.reviewable_by_group_id end @@ -284,16 +266,14 @@ class Reviewable < ActiveRecord::Base def actions_for(guardian, args = nil) args ||= {} - Actions.new(self, guardian).tap do |actions| - build_actions(actions, guardian, args) - end + Actions.new(self, guardian).tap { |actions| build_actions(actions, guardian, args) } end def editable_for(guardian, args = nil) args ||= {} - EditableFields.new(self, guardian, args).tap do |fields| - build_editable_fields(fields, guardian, args) - end + EditableFields + .new(self, guardian, args) + .tap { |fields| build_editable_fields(fields, guardian, args) } end # subclasses must implement "build_actions" to list the actions they're capable of @@ -316,7 +296,7 @@ class Reviewable < ActiveRecord::Base Reviewable.transaction do increment_version!(version) changes_json = changes.as_json - changes_json.delete('version') + changes_json.delete("version") result = save log_history(:edited, performed_by, edited: changes_json) if result @@ -331,7 +311,7 @@ class Reviewable < ActiveRecord::Base args ||= {} # Support this action or any aliases aliases = self.class.action_aliases - valid = [ action_id, aliases.to_a.select { |k, v| v == action_id }.map(&:first) ].flatten + valid = [action_id, aliases.to_a.select { |k, v| v == action_id }.map(&:first)].flatten # Ensure the user has access to the action actions = actions_for(Guardian.new(performed_by), args) @@ -353,16 +333,14 @@ class Reviewable < ActiveRecord::Base recalculate_score if result.recalculate_score end - if result && result.after_commit - result.after_commit.call - end + result.after_commit.call if result && result.after_commit if update_count || result.remove_reviewable_ids.present? Jobs.enqueue( :notify_reviewable, reviewable_id: self.id, performing_username: performed_by.username, - updated_reviewable_ids: result.remove_reviewable_ids + updated_reviewable_ids: result.remove_reviewable_ids, ) end @@ -380,7 +358,7 @@ class Reviewable < ActiveRecord::Base reviewable_scores.pending.update_all( status: score_status, reviewed_by_id: performed_by.id, - reviewed_at: Time.zone.now + reviewed_at: Time.zone.now, ) end @@ -391,40 +369,45 @@ class Reviewable < ActiveRecord::Base Discourse.deprecate( "Reviewable#post_options is deprecated. Please use #payload instead.", output_in_test: true, - drop_from: '2.9.0', + drop_from: "2.9.0", ) end def self.bulk_perform_targets(performed_by, action, type, target_ids, args = nil) args ||= {} - viewable_by(performed_by).where(type: type, target_id: target_ids).each do |r| - r.perform(performed_by, action, args) - end + viewable_by(performed_by) + .where(type: type, target_id: target_ids) + .each { |r| r.perform(performed_by, action, args) } end def self.viewable_by(user, order: nil, preload: true) return none unless user.present? - result = self.order(order || 'reviewables.score desc, reviewables.created_at desc') + result = self.order(order || "reviewables.score desc, reviewables.created_at desc") if preload - result = result.includes( - { created_by: :user_stat }, - :topic, - :target, - :target_created_by, - :reviewable_histories - ).includes(reviewable_scores: { user: :user_stat, meta_topic: :posts }) + result = + result.includes( + { created_by: :user_stat }, + :topic, + :target, + :target_created_by, + :reviewable_histories, + ).includes(reviewable_scores: { user: :user_stat, meta_topic: :posts }) end return result if user.admin? - group_ids = SiteSetting.enable_category_group_moderation? ? user.group_users.pluck(:group_id) : [] + group_ids = + SiteSetting.enable_category_group_moderation? ? user.group_users.pluck(:group_id) : [] result.where( - '(reviewables.reviewable_by_moderator AND :staff) OR (reviewables.reviewable_by_group_id IN (:group_ids))', + "(reviewables.reviewable_by_moderator AND :staff) OR (reviewables.reviewable_by_group_id IN (:group_ids))", staff: user.staff?, - group_ids: group_ids - ).where("reviewables.category_id IS NULL OR reviewables.category_id IN (?)", Guardian.new(user).allowed_category_ids) + group_ids: group_ids, + ).where( + "reviewables.category_id IS NULL OR reviewables.category_id IN (?)", + Guardian.new(user).allowed_category_ids, + ) end def self.pending_count(user) @@ -454,16 +437,17 @@ class Reviewable < ActiveRecord::Base preload: true, include_claimed_by_others: true ) - order = case sort_order - when 'score_asc' - 'reviewables.score ASC, reviewables.created_at DESC' - when 'created_at' - 'reviewables.created_at DESC, reviewables.score DESC' - when 'created_at_asc' - 'reviewables.created_at ASC, reviewables.score DESC' - else - 'reviewables.score DESC, reviewables.created_at DESC' - end + order = + case sort_order + when "score_asc" + "reviewables.score ASC, reviewables.created_at DESC" + when "created_at" + "reviewables.created_at DESC, reviewables.score DESC" + when "created_at_asc" + "reviewables.created_at ASC, reviewables.score DESC" + else + "reviewables.score DESC, reviewables.created_at DESC" + end if username.present? user_id = User.find_by_username(username)&.id @@ -475,9 +459,9 @@ class Reviewable < ActiveRecord::Base result = by_status(result, status) result = result.where(id: ids) if ids - result = result.where('reviewables.type = ?', type) if type - result = result.where('reviewables.category_id = ?', category_id) if category_id - result = result.where('reviewables.topic_id = ?', topic_id) if topic_id + result = result.where("reviewables.type = ?", type) if type + result = result.where("reviewables.category_id = ?", category_id) if category_id + result = result.where("reviewables.topic_id = ?", topic_id) if topic_id result = result.where("reviewables.created_at >= ?", from_date) if from_date result = result.where("reviewables.created_at <= ?", to_date) if to_date @@ -485,7 +469,7 @@ class Reviewable < ActiveRecord::Base reviewed_by_id = User.find_by_username(reviewed_by)&.id return none if reviewed_by_id.nil? - result = result.joins(<<~SQL + result = result.joins(<<~SQL) INNER JOIN( SELECT reviewable_id FROM reviewable_histories @@ -493,7 +477,6 @@ class Reviewable < ActiveRecord::Base status <> #{statuses[:pending]} AND created_by_id = #{reviewed_by_id} ) AS rh ON rh.reviewable_id = reviewables.id SQL - ) end min_score = min_score_for_priority(priority) @@ -505,28 +488,31 @@ class Reviewable < ActiveRecord::Base end if !custom_filters.empty? - result = custom_filters.reduce(result) do |memo, filter| - key = filter.first - filter_query = filter.last + result = + custom_filters.reduce(result) do |memo, filter| + key = filter.first + filter_query = filter.last - next(memo) unless additional_filters[key] - filter_query.call(result, additional_filters[key]) - end + next(memo) unless additional_filters[key] + filter_query.call(result, additional_filters[key]) + end end # If a reviewable doesn't have a target, allow us to filter on who created that reviewable. if user_id - result = result.where( - "(reviewables.target_created_by_id IS NULL AND reviewables.created_by_id = :user_id) + result = + result.where( + "(reviewables.target_created_by_id IS NULL AND reviewables.created_by_id = :user_id) OR (reviewables.target_created_by_id = :user_id)", - user_id: user_id - ) + user_id: user_id, + ) end if !include_claimed_by_others - result = result - .joins("LEFT JOIN reviewable_claimed_topics rct ON reviewables.topic_id = rct.topic_id") - .where("rct.user_id IS NULL OR rct.user_id = ?", user.id) + result = + result.joins( + "LEFT JOIN reviewable_claimed_topics rct ON reviewables.topic_id = rct.topic_id", + ).where("rct.user_id IS NULL OR rct.user_id = ?", user.id) end result = result.limit(limit) if limit result = result.offset(offset) if offset @@ -536,10 +522,7 @@ class Reviewable < ActiveRecord::Base def self.unseen_list_for(user, preload: true, limit: nil) results = list_for(user, preload: preload, limit: limit, include_claimed_by_others: false) if user.last_seen_reviewable_id - results = results.where( - "reviewables.id > ?", - user.last_seen_reviewable_id - ) + results = results.where("reviewables.id > ?", user.last_seen_reviewable_id) end results end @@ -584,7 +567,8 @@ class Reviewable < ActiveRecord::Base end def self.count_by_date(start_date, end_date, category_id = nil, include_subcategories = false) - query = scores_with_topics.where('reviewable_scores.created_at BETWEEN ? AND ?', start_date, end_date) + query = + scores_with_topics.where("reviewable_scores.created_at BETWEEN ? AND ?", start_date, end_date) if category_id if include_subcategories @@ -596,7 +580,7 @@ class Reviewable < ActiveRecord::Base query .group("date(reviewable_scores.created_at)") - .order('date(reviewable_scores.created_at)') + .order("date(reviewable_scores.created_at)") .count end @@ -634,12 +618,13 @@ class Reviewable < ActiveRecord::Base RETURNING score SQL - result = DB.query( - sql, - id: self.id, - pending: ReviewableScore.statuses[:pending], - agreed: ReviewableScore.statuses[:agreed] - ) + result = + DB.query( + sql, + id: self.id, + pending: ReviewableScore.statuses[:pending], + agreed: ReviewableScore.statuses[:agreed], + ) # Update topic score sql = <<~SQL @@ -657,7 +642,7 @@ class Reviewable < ActiveRecord::Base sql, topic_id: topic_id, pending: self.class.statuses[:pending], - approved: self.class.statuses[:approved] + approved: self.class.statuses[:approved], ) self.score = result[0].score @@ -668,44 +653,47 @@ class Reviewable < ActiveRecord::Base end def delete_user_actions(actions, require_reject_reason: false) - reject = actions.add_bundle( - 'reject_user', - icon: 'user-times', - label: 'reviewables.actions.reject_user.title' - ) + reject = + actions.add_bundle( + "reject_user", + icon: "user-times", + label: "reviewables.actions.reject_user.title", + ) actions.add(:delete_user, bundle: reject) do |a| - a.icon = 'user-times' + a.icon = "user-times" a.label = "reviewables.actions.reject_user.delete.title" a.require_reject_reason = require_reject_reason a.description = "reviewables.actions.reject_user.delete.description" end actions.add(:delete_user_block, bundle: reject) do |a| - a.icon = 'ban' + a.icon = "ban" a.label = "reviewables.actions.reject_user.block.title" a.require_reject_reason = require_reject_reason a.description = "reviewables.actions.reject_user.block.description" end end -protected + protected def increment_version!(version = nil) version_result = nil if version - version_result = DB.query_single( - "UPDATE reviewables SET version = version + 1 WHERE id = :id AND version = :version RETURNING version", - version: version, - id: self.id - ) + version_result = + DB.query_single( + "UPDATE reviewables SET version = version + 1 WHERE id = :id AND version = :version RETURNING version", + version: version, + id: self.id, + ) else # We didn't supply a version to update safely, so just increase it - version_result = DB.query_single( - "UPDATE reviewables SET version = version + 1 WHERE id = :id RETURNING version", - id: self.id - ) + version_result = + DB.query_single( + "UPDATE reviewables SET version = version + 1 WHERE id = :id RETURNING version", + id: self.id, + ) end if version_result && version_result[0] @@ -725,10 +713,10 @@ protected end end -private + private def update_flag_stats(status:, user_ids:) - return unless [:agreed, :disagreed, :ignored].include?(status) + return unless %i[agreed disagreed ignored].include?(status) # Don't count self-flags user_ids -= [post&.user_id] @@ -741,7 +729,8 @@ private RETURNING user_id, flags_agreed + flags_disagreed + flags_ignored AS total SQL - user_ids = result.select { |r| r.total > Jobs::TruncateUserFlagStats.truncate_to }.map(&:user_id) + user_ids = + result.select { |r| r.total > Jobs::TruncateUserFlagStats.truncate_to }.map(&:user_id) return if user_ids.blank? Jobs.enqueue(:truncate_user_flag_stats, user_ids: user_ids) diff --git a/app/models/reviewable_claimed_topic.rb b/app/models/reviewable_claimed_topic.rb index dc54c4abf1..a4b81864c7 100644 --- a/app/models/reviewable_claimed_topic.rb +++ b/app/models/reviewable_claimed_topic.rb @@ -7,10 +7,11 @@ class ReviewableClaimedTopic < ActiveRecord::Base def self.claimed_hash(topic_ids) result = {} - if SiteSetting.reviewable_claiming != 'disabled' - ReviewableClaimedTopic.where(topic_id: topic_ids).includes(:user).each do |rct| - result[rct.topic_id] = rct.user - end + if SiteSetting.reviewable_claiming != "disabled" + ReviewableClaimedTopic + .where(topic_id: topic_ids) + .includes(:user) + .each { |rct| result[rct.topic_id] = rct.user } end result end diff --git a/app/models/reviewable_flagged_post.rb b/app/models/reviewable_flagged_post.rb index 44011bf6fb..8015cd6e12 100644 --- a/app/models/reviewable_flagged_post.rb +++ b/app/models/reviewable_flagged_post.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true class ReviewableFlaggedPost < Reviewable - scope :pending_and_default_visible, -> { - pending.default_visible - } + scope :pending_and_default_visible, -> { pending.default_visible } # Penalties are handled by the modal after the action is performed def self.action_aliases - { agree_and_keep_hidden: :agree_and_keep, + { + agree_and_keep_hidden: :agree_and_keep, agree_and_silence: :agree_and_keep, agree_and_suspend: :agree_and_keep, - disagree_and_restore: :disagree } + disagree_and_restore: :disagree, + } end def self.counts_for(posts) @@ -43,68 +43,84 @@ class ReviewableFlaggedPost < Reviewable return unless pending? return if post.blank? - agree = actions.add_bundle("#{id}-agree", icon: 'thumbs-up', label: 'reviewables.actions.agree.title') + agree = + actions.add_bundle("#{id}-agree", icon: "thumbs-up", label: "reviewables.actions.agree.title") if !post.user_deleted? && !post.hidden? - build_action(actions, :agree_and_hide, icon: 'far-eye-slash', bundle: agree) + build_action(actions, :agree_and_hide, icon: "far-eye-slash", bundle: agree) end if post.hidden? - build_action(actions, :agree_and_keep_hidden, icon: 'thumbs-up', bundle: agree) + build_action(actions, :agree_and_keep_hidden, icon: "thumbs-up", bundle: agree) else - build_action(actions, :agree_and_keep, icon: 'thumbs-up', bundle: agree) + build_action(actions, :agree_and_keep, icon: "thumbs-up", bundle: agree) end if guardian.can_suspend?(target_created_by) - build_action(actions, :agree_and_suspend, icon: 'ban', bundle: agree, client_action: 'suspend') - build_action(actions, :agree_and_silence, icon: 'microphone-slash', bundle: agree, client_action: 'silence') + build_action( + actions, + :agree_and_suspend, + icon: "ban", + bundle: agree, + client_action: "suspend", + ) + build_action( + actions, + :agree_and_silence, + icon: "microphone-slash", + bundle: agree, + client_action: "silence", + ) end - if post.user_deleted? - build_action(actions, :agree_and_restore, icon: 'far-eye', bundle: agree) - end + build_action(actions, :agree_and_restore, icon: "far-eye", bundle: agree) if post.user_deleted? if post.hidden? - build_action(actions, :disagree_and_restore, icon: 'thumbs-down') + build_action(actions, :disagree_and_restore, icon: "thumbs-down") else - build_action(actions, :disagree, icon: 'thumbs-down') + build_action(actions, :disagree, icon: "thumbs-down") end - build_action(actions, :ignore, icon: 'external-link-alt') + build_action(actions, :ignore, icon: "external-link-alt") - if potential_spam? && guardian.can_delete_user?(target_created_by) - delete_user_actions(actions) - end + delete_user_actions(actions) if potential_spam? && guardian.can_delete_user?(target_created_by) if guardian.can_delete_post_or_topic?(post) - delete = actions.add_bundle("#{id}-delete", icon: "far-trash-alt", label: "reviewables.actions.delete.title") - build_action(actions, :delete_and_ignore, icon: 'external-link-alt', bundle: delete) + delete = + actions.add_bundle( + "#{id}-delete", + icon: "far-trash-alt", + label: "reviewables.actions.delete.title", + ) + build_action(actions, :delete_and_ignore, icon: "external-link-alt", bundle: delete) if post.reply_count > 0 build_action( actions, :delete_and_ignore_replies, - icon: 'external-link-alt', + icon: "external-link-alt", confirm: true, - bundle: delete + bundle: delete, ) end - build_action(actions, :delete_and_agree, icon: 'thumbs-up', bundle: delete) + build_action(actions, :delete_and_agree, icon: "thumbs-up", bundle: delete) if post.reply_count > 0 build_action( actions, :delete_and_agree_replies, - icon: 'external-link-alt', + icon: "external-link-alt", bundle: delete, - confirm: true + confirm: true, ) end end end def perform_ignore(performed_by, args) - actions = PostAction.active - .where(post_id: target_id) - .where(post_action_type_id: PostActionType.notify_flag_type_ids) + actions = + PostAction + .active + .where(post_id: target_id) + .where(post_action_type_id: PostActionType.notify_flag_type_ids) actions.each do |action| action.deferred_at = Time.zone.now @@ -142,9 +158,7 @@ class ReviewableFlaggedPost < Reviewable def perform_delete_user_block(performed_by, args) delete_options = delete_opts - if Rails.env.production? - delete_options.merge!(block_email: true, block_ip: true) - end + delete_options.merge!(block_email: true, block_ip: true) if Rails.env.production? UserDestroyer.new(performed_by).destroy(post.user, delete_options) @@ -152,15 +166,11 @@ class ReviewableFlaggedPost < Reviewable end def perform_agree_and_hide(performed_by, args) - agree(performed_by, args) do |pa| - post.hide!(pa.post_action_type_id) - end + agree(performed_by, args) { |pa| post.hide!(pa.post_action_type_id) } end def perform_agree_and_restore(performed_by, args) - agree(performed_by, args) do - PostDestroyer.new(performed_by, post).recover - end + agree(performed_by, args) { PostDestroyer.new(performed_by, post).recover } end def perform_disagree(performed_by, args) @@ -172,7 +182,8 @@ class ReviewableFlaggedPost < Reviewable PostActionType.notify_flag_type_ids end - actions = PostAction.active.where(post_id: target_id).where(post_action_type_id: action_type_ids) + actions = + PostAction.active.where(post_id: target_id).where(post_action_type_id: action_type_ids) actions.each do |action| action.disagreed_at = Time.zone.now @@ -234,12 +245,14 @@ class ReviewableFlaggedPost < Reviewable result end -protected + protected def agree(performed_by, args) - actions = PostAction.active - .where(post_id: target_id) - .where(post_action_type_id: PostActionType.notify_flag_types.values) + actions = + PostAction + .active + .where(post_id: target_id) + .where(post_action_type_id: PostActionType.notify_flag_types.values) trigger_spam = false actions.each do |action| @@ -270,7 +283,15 @@ protected end end - def build_action(actions, id, icon:, button_class: nil, bundle: nil, client_action: nil, confirm: false) + def build_action( + actions, + id, + icon:, + button_class: nil, + bundle: nil, + client_action: nil, + confirm: false + ) actions.add(id, bundle: bundle) do |action| prefix = "reviewables.actions.#{id}" action.icon = icon @@ -284,15 +305,14 @@ protected def unassign_topic(performed_by, post) topic = post.topic - return unless topic && performed_by && SiteSetting.reviewable_claiming != 'disabled' + return unless topic && performed_by && SiteSetting.reviewable_claiming != "disabled" ReviewableClaimedTopic.where(topic_id: topic.id).delete_all - topic.reviewables.find_each do |reviewable| - reviewable.log_history(:unclaimed, performed_by) - end + topic.reviewables.find_each { |reviewable| reviewable.log_history(:unclaimed, performed_by) } user_ids = User.staff.pluck(:id) - if SiteSetting.enable_category_group_moderation? && group_id = topic.category&.reviewable_by_group_id.presence + if SiteSetting.enable_category_group_moderation? && + group_id = topic.category&.reviewable_by_group_id.presence user_ids.concat(GroupUser.where(group_id: group_id).pluck(:user_id)) user_ids.uniq! end @@ -302,7 +322,7 @@ protected MessageBus.publish("/reviewable_claimed", data, user_ids: user_ids) end -private + private def delete_opts { @@ -310,7 +330,7 @@ private prepare_for_destroy: true, block_urls: true, delete_as_spammer: true, - context: "review" + context: "review", } end @@ -327,8 +347,8 @@ private message_type: "flags_disagreed", message_options: { flagged_post_raw_content: post.raw, - url: post.url - } + url: post.url, + }, ) end end diff --git a/app/models/reviewable_history.rb b/app/models/reviewable_history.rb index c01d41a935..b5df88cb42 100644 --- a/app/models/reviewable_history.rb +++ b/app/models/reviewable_history.rb @@ -2,24 +2,12 @@ class ReviewableHistory < ActiveRecord::Base belongs_to :reviewable - belongs_to :created_by, class_name: 'User' + belongs_to :created_by, class_name: "User" - enum status: { - pending: 0, - approved: 1, - rejected: 2, - ignored: 3, - deleted: 4 - } + enum status: { pending: 0, approved: 1, rejected: 2, ignored: 3, deleted: 4 } alias_attribute :type, :reviewable_history_type - enum type: { - created: 0, - transitioned: 1, - edited: 2, - claimed: 3, - unclaimed: 4 - } + enum type: { created: 0, transitioned: 1, edited: 2, claimed: 3, unclaimed: 4 } end # == Schema Information diff --git a/app/models/reviewable_post.rb b/app/models/reviewable_post.rb index 983a96c054..f9fb8afb37 100644 --- a/app/models/reviewable_post.rb +++ b/app/models/reviewable_post.rb @@ -9,7 +9,10 @@ class ReviewablePost < Reviewable return unless SiteSetting.review_every_post return if post.post_type != Post.types[:regular] || post.topic.private_message? return if Reviewable.pending.where(target: post).exists? - return if created_or_edited_by.bot? || created_or_edited_by.staff? || created_or_edited_by.has_trust_level?(TrustLevel[4]) + if created_or_edited_by.bot? || created_or_edited_by.staff? || + created_or_edited_by.has_trust_level?(TrustLevel[4]) + return + end system_user = Discourse.system_user needs_review!( @@ -17,13 +20,9 @@ class ReviewablePost < Reviewable topic: post.topic, created_by: system_user, reviewable_by_moderator: true, - potential_spam: false + potential_spam: false, ).tap do |reviewable| - reviewable.add_score( - system_user, - ReviewableScore.types[:needs_approval], - force_review: true - ) + reviewable.add_score(system_user, ReviewableScore.types[:needs_approval], force_review: true) end end @@ -31,26 +30,41 @@ class ReviewablePost < Reviewable return unless pending? if post.trashed? && guardian.can_recover_post?(post) - build_action(actions, :approve_and_restore, icon: 'check') + build_action(actions, :approve_and_restore, icon: "check") elsif post.hidden? - build_action(actions, :approve_and_unhide, icon: 'check') + build_action(actions, :approve_and_unhide, icon: "check") else - build_action(actions, :approve, icon: 'check') + build_action(actions, :approve, icon: "check") end - reject = actions.add_bundle( - "#{id}-reject", icon: 'times', label: 'reviewables.actions.reject.bundle_title' - ) + reject = + actions.add_bundle( + "#{id}-reject", + icon: "times", + label: "reviewables.actions.reject.bundle_title", + ) if post.trashed? - build_action(actions, :reject_and_keep_deleted, icon: 'trash-alt', bundle: reject) + build_action(actions, :reject_and_keep_deleted, icon: "trash-alt", bundle: reject) elsif guardian.can_delete_post_or_topic?(post) - build_action(actions, :reject_and_delete, icon: 'trash-alt', bundle: reject) + build_action(actions, :reject_and_delete, icon: "trash-alt", bundle: reject) end if guardian.can_suspend?(target_created_by) - build_action(actions, :reject_and_suspend, icon: 'ban', bundle: reject, client_action: 'suspend') - build_action(actions, :reject_and_silence, icon: 'microphone-slash', bundle: reject, client_action: 'silence') + build_action( + actions, + :reject_and_suspend, + icon: "ban", + bundle: reject, + client_action: "suspend", + ) + build_action( + actions, + :reject_and_silence, + icon: "microphone-slash", + bundle: reject, + client_action: "silence", + ) end end @@ -90,7 +104,15 @@ class ReviewablePost < Reviewable @post ||= (target || Post.with_deleted.find_by(id: target_id)) end - def build_action(actions, id, icon:, button_class: nil, bundle: nil, client_action: nil, confirm: false) + def build_action( + actions, + id, + icon:, + button_class: nil, + bundle: nil, + client_action: nil, + confirm: false + ) actions.add(id, bundle: bundle) do |action| prefix = "reviewables.actions.#{id}" action.icon = icon @@ -103,7 +125,7 @@ class ReviewablePost < Reviewable end def successful_transition(to_state, recalculate_score: true) - create_result(:success, to_state) do |result| + create_result(:success, to_state) do |result| result.recalculate_score = recalculate_score result.update_flag_stats = { status: to_state, user_ids: [created_by_id] } end diff --git a/app/models/reviewable_priority_setting.rb b/app/models/reviewable_priority_setting.rb index 42afd49775..04547837a4 100644 --- a/app/models/reviewable_priority_setting.rb +++ b/app/models/reviewable_priority_setting.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ReviewablePrioritySetting < EnumSiteSetting - def self.valid_value?(val) values.any? { |v| v[:value].to_s == val.to_s } end @@ -15,5 +14,4 @@ class ReviewablePrioritySetting < EnumSiteSetting def self.translate_names? false end - end diff --git a/app/models/reviewable_queued_post.rb b/app/models/reviewable_queued_post.rb index 50a453c734..ecf6297cdc 100644 --- a/app/models/reviewable_queued_post.rb +++ b/app/models/reviewable_queued_post.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class ReviewableQueuedPost < Reviewable - after_create do # Backwards compatibility, new code should listen for `reviewable_created` DiscourseEvent.trigger(:queued_post_created, self) end after_save do - if saved_change_to_payload? && self.status == Reviewable.statuses[:pending] && self.payload&.[]('raw').present? - upload_ids = Upload.extract_upload_ids(self.payload['raw']) + if saved_change_to_payload? && self.status == Reviewable.statuses[:pending] && + self.payload&.[]("raw").present? + upload_ids = Upload.extract_upload_ids(self.payload["raw"]) UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) end end @@ -17,18 +17,16 @@ class ReviewableQueuedPost < Reviewable after_commit :compute_user_stats, only: %i[create update] def build_actions(actions, guardian, args) - unless approved? - if topic&.closed? actions.add(:approve_post_closed) do |a| - a.icon = 'check' + a.icon = "check" a.label = "reviewables.actions.approve_post.title" a.confirm_message = "reviewables.actions.approve_post.confirm_closed" end else actions.add(:approve_post) do |a| - a.icon = 'check' + a.icon = "check" a.label = "reviewables.actions.approve_post.title" end end @@ -36,32 +34,28 @@ class ReviewableQueuedPost < Reviewable unless rejected? actions.add(:reject_post) do |a| - a.icon = 'times' + a.icon = "times" a.label = "reviewables.actions.reject_post.title" end end - if pending? && guardian.can_delete_user?(created_by) - delete_user_actions(actions) - end + delete_user_actions(actions) if pending? && guardian.can_delete_user?(created_by) actions.add(:delete) if guardian.can_delete?(self) end def build_editable_fields(fields, guardian, args) - # We can edit category / title if it's a new topic if topic_id.blank? - # Only staff can edit category for now, since in theory a category group reviewer could # post in a category they don't have access to. - fields.add('category_id', :category) if guardian.is_staff? + fields.add("category_id", :category) if guardian.is_staff? - fields.add('payload.title', :text) - fields.add('payload.tags', :tags) + fields.add("payload.title", :text) + fields.add("payload.tags", :tags) end - fields.add('payload.raw', :editor) + fields.add("payload.raw", :editor) end def create_options @@ -74,13 +68,16 @@ class ReviewableQueuedPost < Reviewable def perform_approve_post(performed_by, args) created_post = nil + opts = + create_options.merge( + skip_validations: true, + skip_jobs: true, + skip_events: true, + skip_guardian: true, + ) + opts.merge!(guardian: Guardian.new(performed_by)) if performed_by.staff? - creator = PostCreator.new(created_by, create_options.merge( - skip_validations: true, - skip_jobs: true, - skip_events: true, - skip_guardian: true - )) + creator = PostCreator.new(created_by, opts) created_post = creator.create unless created_post && creator.errors.blank? @@ -88,9 +85,7 @@ class ReviewableQueuedPost < Reviewable end self.target = created_post - if topic_id.nil? - self.topic_id = created_post.topic_id - end + self.topic_id = created_post.topic_id if topic_id.nil? save UserSilencer.unsilence(created_by, performed_by) if created_by.silenced? @@ -105,17 +100,17 @@ class ReviewableQueuedPost < Reviewable user_id: created_by.id, data: { post_url: created_post.url }.to_json, topic_id: created_post.topic_id, - post_number: created_post.post_number + post_number: created_post.post_number, ) create_result(:success, :approved) do |result| result.created_post = created_post # Do sidekiq work outside of the transaction - result.after_commit = -> { + result.after_commit = -> do creator.enqueue_jobs creator.trigger_after_events - } + end end end @@ -143,9 +138,7 @@ class ReviewableQueuedPost < Reviewable def perform_delete_user_block(performed_by, args) delete_options = delete_opts - if Rails.env.production? - delete_options.merge!(block_email: true, block_ip: true) - end + delete_options.merge!(block_email: true, block_ip: true) if Rails.env.production? delete_user(performed_by, delete_options) end @@ -160,10 +153,10 @@ class ReviewableQueuedPost < Reviewable def delete_opts { - context: I18n.t('reviewables.actions.delete_user.reason'), + context: I18n.t("reviewables.actions.delete_user.reason"), delete_posts: true, block_urls: true, - delete_as_spammer: true + delete_as_spammer: true, } end @@ -173,8 +166,7 @@ class ReviewableQueuedPost < Reviewable end def status_changed_from_or_to_pending? - saved_change_to_id?(from: nil) && pending? || - saved_change_to_status?(from: "pending") + saved_change_to_id?(from: nil) && pending? || saved_change_to_status?(from: "pending") end end diff --git a/app/models/reviewable_score.rb b/app/models/reviewable_score.rb index 2d650b4131..774a538533 100644 --- a/app/models/reviewable_score.rb +++ b/app/models/reviewable_score.rb @@ -3,22 +3,15 @@ class ReviewableScore < ActiveRecord::Base belongs_to :reviewable belongs_to :user - belongs_to :reviewed_by, class_name: 'User' - belongs_to :meta_topic, class_name: 'Topic' + belongs_to :reviewed_by, class_name: "User" + belongs_to :meta_topic, class_name: "Topic" - enum status: { - pending: 0, - agreed: 1, - disagreed: 2, - ignored: 3 - } + enum status: { pending: 0, agreed: 1, disagreed: 2, ignored: 3 } # To keep things simple the types correspond to `PostActionType` for backwards # compatibility, but we can add extra reasons for scores. def self.types - @types ||= PostActionType.flag_types.merge( - needs_approval: 9 - ) + @types ||= PostActionType.flag_types.merge(needs_approval: 9) end # When extending post action flags, we need to call this method in order to @@ -31,17 +24,11 @@ class ReviewableScore < ActiveRecord::Base def self.add_new_types(type_names) next_id = types.values.max + 1 - type_names.each_with_index do |name, idx| - @types[name] = next_id + idx - end + type_names.each_with_index { |name, idx| @types[name] = next_id + idx } end def self.score_transitions - { - approved: statuses[:agreed], - rejected: statuses[:disagreed], - ignored: statuses[:ignored] - } + { approved: statuses[:agreed], rejected: statuses[:disagreed], ignored: statuses[:ignored] } end def score_type @@ -88,22 +75,21 @@ class ReviewableScore < ActiveRecord::Base bottom = positive_accuracy ? accuracy_axis : 0.0 top = positive_accuracy ? 1.0 : accuracy_axis - absolute_distance = positive_accuracy ? - percent_correct - bottom : - top - percent_correct + absolute_distance = positive_accuracy ? percent_correct - bottom : top - percent_correct axis_distance_multiplier = 1.0 / (top - bottom) positivity_multiplier = positive_accuracy ? 1.0 : -1.0 - (absolute_distance * axis_distance_multiplier * positivity_multiplier * (Math.log(total, 4) * 5.0)) - .round(2) + ( + absolute_distance * axis_distance_multiplier * positivity_multiplier * + (Math.log(total, 4) * 5.0) + ).round(2) end def reviewable_conversation return if meta_topic.blank? Reviewable::Conversation.new(meta_topic) end - end # == Schema Information diff --git a/app/models/reviewable_sensitivity_setting.rb b/app/models/reviewable_sensitivity_setting.rb index c6fa405c3d..5eb60bed4e 100644 --- a/app/models/reviewable_sensitivity_setting.rb +++ b/app/models/reviewable_sensitivity_setting.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ReviewableSensitivitySetting < EnumSiteSetting - def self.valid_value?(val) values.any? { |v| v[:value].to_s == val.to_s } end @@ -15,5 +14,4 @@ class ReviewableSensitivitySetting < EnumSiteSetting def self.translate_names? false end - end diff --git a/app/models/reviewable_user.rb b/app/models/reviewable_user.rb index c885c8718c..d7d2d8da09 100644 --- a/app/models/reviewable_user.rb +++ b/app/models/reviewable_user.rb @@ -1,12 +1,8 @@ # frozen_string_literal: true class ReviewableUser < Reviewable - def self.create_for(user) - create( - created_by_id: Discourse.system_user.id, - target: user - ) + create(created_by_id: Discourse.system_user.id, target: user) end def build_actions(actions, guardian, args) @@ -14,7 +10,7 @@ class ReviewableUser < Reviewable if guardian.can_approve?(target) actions.add(:approve_user) do |a| - a.icon = 'user-plus' + a.icon = "user-plus" a.label = "reviewables.actions.approve_user.title" end end @@ -29,11 +25,7 @@ class ReviewableUser < Reviewable DiscourseEvent.trigger(:user_approved, target) if args[:send_email] != false && SiteSetting.must_approve_users? - Jobs.enqueue( - :critical_user_email, - type: "signup_after_approval", - user_id: target.id - ) + Jobs.enqueue(:critical_user_email, type: "signup_after_approval", user_id: target.id) end StaffActionLogger.new(performed_by).log_user_approve(target) @@ -52,11 +44,9 @@ class ReviewableUser < Reviewable if args[:send_email] && SiteSetting.must_approve_users? # Execute job instead of enqueue because user has to exists to send email - Jobs::CriticalUserEmail.new.execute({ - type: :signup_after_reject, - user_id: target.id, - reject_reason: self.reject_reason - }) + Jobs::CriticalUserEmail.new.execute( + { type: :signup_after_reject, user_id: target.id, reject_reason: self.reject_reason }, + ) end delete_args = {} @@ -95,7 +85,7 @@ class ReviewableUser < Reviewable end def is_a_suspect_user? - reviewable_scores.any? { |rs| rs.reason == 'suspect_user' } + reviewable_scores.any? { |rs| rs.reason == "suspect_user" } end end diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index bf7d36740e..e7b0c2756c 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class S3RegionSiteSetting < EnumSiteSetting - def self.valid_value?(val) valid_values.include? val end @@ -11,29 +10,29 @@ class S3RegionSiteSetting < EnumSiteSetting end def self.valid_values - [ - 'ap-northeast-1', - 'ap-northeast-2', - 'ap-east-1', - 'ap-south-1', - 'ap-southeast-1', - 'ap-southeast-2', - 'ca-central-1', - 'cn-north-1', - 'cn-northwest-1', - 'eu-central-1', - 'eu-north-1', - 'eu-south-1', - 'eu-west-1', - 'eu-west-2', - 'eu-west-3', - 'sa-east-1', - 'us-east-1', - 'us-east-2', - 'us-gov-east-1', - 'us-gov-west-1', - 'us-west-1', - 'us-west-2', + %w[ + ap-northeast-1 + ap-northeast-2 + ap-east-1 + ap-south-1 + ap-southeast-1 + ap-southeast-2 + ca-central-1 + cn-north-1 + cn-northwest-1 + eu-central-1 + eu-north-1 + eu-south-1 + eu-west-1 + eu-west-2 + eu-west-3 + sa-east-1 + us-east-1 + us-east-2 + us-gov-east-1 + us-gov-west-1 + us-west-1 + us-west-2 ] end diff --git a/app/models/screened_email.rb b/app/models/screened_email.rb index 3f8a604d9b..d86fa0a0c5 100644 --- a/app/models/screened_email.rb +++ b/app/models/screened_email.rb @@ -5,7 +5,6 @@ # (or some other form) matches a ScreenedEmail record, an action can be # performed based on the action_type. class ScreenedEmail < ActiveRecord::Base - include ScreeningModel default_action :block @@ -19,11 +18,9 @@ class ScreenedEmail < ActiveRecord::Base end def self.canonical(email) - name, domain = email.split('@', 2) - name = name.gsub(/\+.*/, '') - if ['gmail.com', 'googlemail.com'].include?(domain.downcase) - name = name.gsub('.', '') - end + name, domain = email.split("@", 2) + name = name.gsub(/\+.*/, "") + name = name.gsub(".", "") if %w[gmail.com googlemail.com].include?(domain.downcase) "#{name}@#{domain}".downcase end @@ -33,18 +30,21 @@ class ScreenedEmail < ActiveRecord::Base end def self.should_block?(email) - email = canonical(email) screened_emails = ScreenedEmail.order(created_at: :desc).limit(100) distances = {} - screened_emails.each { |se| distances[se.email] = levenshtein(se.email.downcase, email.downcase) } + screened_emails.each do |se| + distances[se.email] = levenshtein(se.email.downcase, email.downcase) + end max_distance = SiteSetting.levenshtein_distance_spammer_emails - screened_email = screened_emails.select { |se| distances[se.email] <= max_distance } - .sort { |se| distances[se.email] } - .first + screened_email = + screened_emails + .select { |se| distances[se.email] <= max_distance } + .sort { |se| distances[se.email] } + .first screened_email.record_match! if screened_email @@ -53,26 +53,19 @@ class ScreenedEmail < ActiveRecord::Base def self.levenshtein(first, second) matrix = [(0..first.length).to_a] - (1..second.length).each do |j| - matrix << [j] + [0] * (first.length) - end + (1..second.length).each { |j| matrix << [j] + [0] * (first.length) } (1..second.length).each do |i| (1..first.length).each do |j| if first[j - 1] == second[i - 1] matrix[i][j] = matrix[i - 1][j - 1] else - matrix[i][j] = [ - matrix[i - 1][j], - matrix[i][j - 1], - matrix[i - 1][j - 1], - ].min + 1 + matrix[i][j] = [matrix[i - 1][j], matrix[i][j - 1], matrix[i - 1][j - 1]].min + 1 end end end matrix.last.last end - end # == Schema Information diff --git a/app/models/screened_ip_address.rb b/app/models/screened_ip_address.rb index 13bf4a9092..ed6cc3d800 100644 --- a/app/models/screened_ip_address.rb +++ b/app/models/screened_ip_address.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'screening_model' +require "screening_model" # A ScreenedIpAddress record represents an IP address or subnet that is being watched, # and possibly blocked from creating accounts. class ScreenedIpAddress < ActiveRecord::Base - include ScreeningModel default_action :block @@ -25,7 +24,8 @@ class ScreenedIpAddress < ActiveRecord::Base ] def self.watch(ip_address, opts = {}) - match_for_ip_address(ip_address) || create(opts.slice(:action_type).merge(ip_address: ip_address)) + match_for_ip_address(ip_address) || + create(opts.slice(:action_type).merge(ip_address: ip_address)) end def check_for_match @@ -60,8 +60,8 @@ class ScreenedIpAddress < ActiveRecord::Base write_attribute(:ip_address, v) - # this gets even messier, Ruby 1.9.2 raised a different exception to Ruby 2.0.0 - # handle both exceptions + # this gets even messier, Ruby 1.9.2 raised a different exception to Ruby 2.0.0 + # handle both exceptions rescue ArgumentError, IPAddr::InvalidAddressError self.errors.add(:ip_address, :invalid) end @@ -79,7 +79,7 @@ class ScreenedIpAddress < ActiveRecord::Base # http://www.postgresql.org/docs/9.1/static/datatype-net-types.html # http://www.postgresql.org/docs/9.1/static/functions-net.html ip_address = IPAddr === ip_address ? ip_address.to_cidr_s : ip_address.to_s - order('masklen(ip_address) DESC').find_by("? <<= ip_address", ip_address) + order("masklen(ip_address) DESC").find_by("? <<= ip_address", ip_address) end def self.should_block?(ip_address) @@ -134,31 +134,33 @@ class ScreenedIpAddress < ActiveRecord::Base def self.roll_up(current_user = Discourse.system_user) ROLLED_UP_BLOCKS.each do |family, from_masklen, to_masklen| - ScreenedIpAddress.subnets(family, from_masklen, to_masklen).map do |subnet| - next if ScreenedIpAddress.where("? <<= ip_address", subnet).exists? + ScreenedIpAddress + .subnets(family, from_masklen, to_masklen) + .map do |subnet| + next if ScreenedIpAddress.where("? <<= ip_address", subnet).exists? - old_ips = ScreenedIpAddress - .where(action_type: ScreenedIpAddress.actions[:block]) - .where("ip_address << ?", subnet) - .where("family(ip_address) = ?", family) - .where("masklen(ip_address) IN (?)", from_masklen) + old_ips = + ScreenedIpAddress + .where(action_type: ScreenedIpAddress.actions[:block]) + .where("ip_address << ?", subnet) + .where("family(ip_address) = ?", family) + .where("masklen(ip_address) IN (?)", from_masklen) - sum_match_count, max_last_match_at, min_created_at = - old_ips.pluck_first('SUM(match_count), MAX(last_match_at), MIN(created_at)') + sum_match_count, max_last_match_at, min_created_at = + old_ips.pluck_first("SUM(match_count), MAX(last_match_at), MIN(created_at)") - ScreenedIpAddress.create!( - ip_address: subnet, - match_count: sum_match_count, - last_match_at: max_last_match_at, - created_at: min_created_at, - ) + ScreenedIpAddress.create!( + ip_address: subnet, + match_count: sum_match_count, + last_match_at: max_last_match_at, + created_at: min_created_at, + ) - StaffActionLogger.new(current_user).log_roll_up(subnet, old_ips.map(&:ip_address)) - old_ips.delete_all - end + StaffActionLogger.new(current_user).log_roll_up(subnet, old_ips.map(&:ip_address)) + old_ips.delete_all + end end end - end # == Schema Information diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb index 0140dd2b47..0e827cf522 100644 --- a/app/models/screened_url.rb +++ b/app/models/screened_url.rb @@ -6,7 +6,6 @@ # For now, nothing is done. We're just collecting the data and will decide # what to do with it later. class ScreenedUrl < ActiveRecord::Base - include ScreeningModel default_action :do_nothing @@ -18,7 +17,7 @@ class ScreenedUrl < ActiveRecord::Base def normalize self.url = ScreenedUrl.normalize_url(self.url) if self.url - self.domain = self.domain.downcase.sub(/^www\./, '') if self.domain + self.domain = self.domain.downcase.sub(/^www\./, "") if self.domain end def self.watch(url, domain, opts = {}) @@ -30,9 +29,9 @@ class ScreenedUrl < ActiveRecord::Base end def self.normalize_url(url) - normalized = url.gsub(/http(s?):\/\//i, '') - normalized.gsub!(/(\/)+$/, '') # trim trailing slashes - normalized.gsub!(/^([^\/]+)(?:\/)?/) { |m| m.downcase } # downcase the domain part of the url + normalized = url.gsub(%r{http(s?)://}i, "") + normalized.gsub!(%r{(/)+$}, "") # trim trailing slashes + normalized.gsub!(%r{^([^/]+)(?:/)?}) { |m| m.downcase } # downcase the domain part of the url normalized end end diff --git a/app/models/search_log.rb b/app/models/search_log.rb index ecaa2db6e7..8d4e1d7e7e 100644 --- a/app/models/search_log.rb +++ b/app/models/search_log.rb @@ -14,19 +14,11 @@ class SearchLog < ActiveRecord::Base end def self.search_types - @search_types ||= Enum.new( - header: 1, - full_page: 2 - ) + @search_types ||= Enum.new(header: 1, full_page: 2) end def self.search_result_types - @search_result_types ||= Enum.new( - topic: 1, - user: 2, - category: 3, - tag: 4 - ) + @search_result_types ||= Enum.new(topic: 1, user: 2, category: 3, tag: 4) end def self.redis_key(ip_address:, user_id: nil) @@ -39,13 +31,10 @@ class SearchLog < ActiveRecord::Base # for testing def self.clear_debounce_cache! - Discourse.redis.keys("__SEARCH__LOG_*").each do |k| - Discourse.redis.del(k) - end + Discourse.redis.keys("__SEARCH__LOG_*").each { |k| Discourse.redis.del(k) } end def self.log(term:, search_type:, ip_address:, user_id: nil) - return [:error] if term.blank? search_type = search_types[search_type] @@ -60,22 +49,15 @@ class SearchLog < ActiveRecord::Base id, old_term = existing.split(",", 2) if term.start_with?(old_term) - where(id: id.to_i).update_all( - created_at: Time.zone.now, - term: term - ) + where(id: id.to_i).update_all(created_at: Time.zone.now, term: term) result = [:updated, id.to_i] end end if !result - log = self.create!( - term: term, - search_type: search_type, - ip_address: ip_address, - user_id: user_id - ) + log = + self.create!(term: term, search_type: search_type, ip_address: ip_address, user_id: user_id) result = [:created, log.id] end @@ -88,21 +70,21 @@ class SearchLog < ActiveRecord::Base def self.term_details(term, period = :weekly, search_type = :all) details = [] - result = SearchLog.select("COUNT(*) AS count, created_at::date AS date") - .where( - 'lower(term) = ? AND created_at > ?', - term.downcase, start_of(period) + result = + SearchLog.select("COUNT(*) AS count, created_at::date AS date").where( + "lower(term) = ? AND created_at > ?", + term.downcase, + start_of(period), ) - result = result.where('search_type = ?', search_types[search_type]) if search_type == :header || search_type == :full_page - result = result.where('search_result_id IS NOT NULL') if search_type == :click_through_only + result = result.where("search_type = ?", search_types[search_type]) if search_type == :header || + search_type == :full_page + result = result.where("search_result_id IS NOT NULL") if search_type == :click_through_only result .order("date") .group("date") - .each do |record| - details << { x: Date.parse(record['date'].to_s), y: record['count'] } - end + .each { |record| details << { x: Date.parse(record["date"].to_s), y: record["count"] } } { type: "search_log_term", @@ -110,7 +92,7 @@ class SearchLog < ActiveRecord::Base start_date: start_of(period), end_date: Time.zone.now, data: details, - period: period.to_s + period: period.to_s, } end @@ -132,39 +114,40 @@ class SearchLog < ActiveRecord::Base END) AS click_through SQL - result = SearchLog.select(select_sql) - .where('created_at > ?', start_date) + result = SearchLog.select(select_sql).where("created_at > ?", start_date) - if end_date - result = result.where('created_at < ?', end_date) - end + result = result.where("created_at < ?", end_date) if end_date - unless search_type == :all - result = result.where('search_type = ?', search_types[search_type]) - end + result = result.where("search_type = ?", search_types[search_type]) unless search_type == :all - result.group('lower(term)') - .order('searches DESC, click_through DESC, term ASC') - .limit(limit) + result.group("lower(term)").order("searches DESC, click_through DESC, term ASC").limit(limit) end def self.clean_up - search_id = SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id) - if search_id.present? - SearchLog.where('id < ?', search_id[0]).delete_all - end - SearchLog.where('created_at < TIMESTAMP ?', SiteSetting.search_query_log_max_retention_days.days.ago).delete_all + search_id = + SearchLog.order(:id).offset(SiteSetting.search_query_log_max_size).limit(1).pluck(:id) + SearchLog.where("id < ?", search_id[0]).delete_all if search_id.present? + SearchLog.where( + "created_at < TIMESTAMP ?", + SiteSetting.search_query_log_max_retention_days.days.ago, + ).delete_all end def self.start_of(period) period = case period - when :yearly then 1.year.ago - when :monthly then 1.month.ago - when :quarterly then 3.months.ago - when :weekly then 1.week.ago - when :daily then Time.zone.now - else 1000.years.ago + when :yearly + 1.year.ago + when :monthly + 1.month.ago + when :quarterly + 3.months.ago + when :weekly + 1.week.ago + when :daily + Time.zone.now + else + 1000.years.ago end period&.to_date diff --git a/app/models/sidebar_section_link.rb b/app/models/sidebar_section_link.rb index 0b6f7bfebe..b6b23a7357 100644 --- a/app/models/sidebar_section_link.rb +++ b/app/models/sidebar_section_link.rb @@ -4,16 +4,20 @@ class SidebarSectionLink < ActiveRecord::Base belongs_to :user belongs_to :linkable, polymorphic: true - validates :user_id, presence: true, uniqueness: { scope: [:linkable_type, :linkable_id] } + validates :user_id, presence: true, uniqueness: { scope: %i[linkable_type linkable_id] } validates :linkable_id, presence: true validates :linkable_type, presence: true validate :ensure_supported_linkable_type, if: :will_save_change_to_linkable_type? - SUPPORTED_LINKABLE_TYPES = %w{Category Tag} + SUPPORTED_LINKABLE_TYPES = %w[Category Tag] private def ensure_supported_linkable_type - if (!SUPPORTED_LINKABLE_TYPES.include?(self.linkable_type)) || (self.linkable_type == 'Tag' && !SiteSetting.tagging_enabled) - self.errors.add(:linkable_type, I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid")) + if (!SUPPORTED_LINKABLE_TYPES.include?(self.linkable_type)) || + (self.linkable_type == "Tag" && !SiteSetting.tagging_enabled) + self.errors.add( + :linkable_type, + I18n.t("activerecord.errors.models.sidebar_section_link.attributes.linkable_type.invalid"), + ) end end end diff --git a/app/models/site.rb b/app/models/site.rb index 2899a932cb..5d3b4565bc 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -67,84 +67,90 @@ class Site # # Do note that any new association added to the eager loading needs a # corresponding ActiveRecord callback to clear the categories cache. - Discourse.cache.fetch(categories_cache_key, expires_in: 30.minutes) do - categories = Category - .includes(:uploaded_logo, :uploaded_logo_dark, :uploaded_background, :tags, :tag_groups, category_required_tag_groups: :tag_group) - .joins('LEFT JOIN topics t on t.id = categories.topic_id') - .select('categories.*, t.slug topic_slug') - .order(:position) - .to_a + Discourse + .cache + .fetch(categories_cache_key, expires_in: 30.minutes) do + categories = + Category + .includes( + :uploaded_logo, + :uploaded_logo_dark, + :uploaded_background, + :tags, + :tag_groups, + category_required_tag_groups: :tag_group, + ) + .joins("LEFT JOIN topics t on t.id = categories.topic_id") + .select("categories.*, t.slug topic_slug") + .order(:position) + .to_a - if preloaded_category_custom_fields.present? - Category.preload_custom_fields( + if preloaded_category_custom_fields.present? + Category.preload_custom_fields(categories, preloaded_category_custom_fields) + end + + ActiveModel::ArraySerializer.new( categories, - preloaded_category_custom_fields - ) + each_serializer: SiteCategorySerializer, + ).as_json end - - ActiveModel::ArraySerializer.new( - categories, - each_serializer: SiteCategorySerializer - ).as_json - end end def categories - @categories ||= begin - categories = [] + @categories ||= + begin + categories = [] - self.class.all_categories_cache.each do |category| - if @guardian.can_see_serialized_category?(category_id: category[:id], read_restricted: category[:read_restricted]) - categories << category + self.class.all_categories_cache.each do |category| + if @guardian.can_see_serialized_category?( + category_id: category[:id], + read_restricted: category[:read_restricted], + ) + categories << category + end end - end - with_children = Set.new - categories.each do |c| - if c[:parent_category_id] - with_children << c[:parent_category_id] + with_children = Set.new + categories.each { |c| with_children << c[:parent_category_id] if c[:parent_category_id] } + + allowed_topic_create = nil + unless @guardian.is_admin? + allowed_topic_create_ids = + @guardian.anonymous? ? [] : Category.topic_create_allowed(@guardian).pluck(:id) + allowed_topic_create = Set.new(allowed_topic_create_ids) end + + by_id = {} + + notification_levels = CategoryUser.notification_levels_for(@guardian.user) + default_notification_level = CategoryUser.default_notification_level + + categories.each do |category| + category[:notification_level] = notification_levels[category[:id]] || + default_notification_level + category[:permission] = CategoryGroup.permission_types[ + :full + ] if allowed_topic_create&.include?(category[:id]) || @guardian.is_admin? + category[:has_children] = with_children.include?(category[:id]) + + category[:can_edit] = @guardian.can_edit_serialized_category?( + category_id: category[:id], + read_restricted: category[:read_restricted], + ) + + by_id[category[:id]] = category + end + + categories.reject! { |c| c[:parent_category_id] && !by_id[c[:parent_category_id]] } + + self.class.categories_callbacks.each { |callback| callback.call(categories, @guardian) } + + categories end - - allowed_topic_create = nil - unless @guardian.is_admin? - allowed_topic_create_ids = - @guardian.anonymous? ? [] : Category.topic_create_allowed(@guardian).pluck(:id) - allowed_topic_create = Set.new(allowed_topic_create_ids) - end - - by_id = {} - - notification_levels = CategoryUser.notification_levels_for(@guardian.user) - default_notification_level = CategoryUser.default_notification_level - - categories.each do |category| - category[:notification_level] = notification_levels[category[:id]] || default_notification_level - category[:permission] = CategoryGroup.permission_types[:full] if allowed_topic_create&.include?(category[:id]) || @guardian.is_admin? - category[:has_children] = with_children.include?(category[:id]) - - category[:can_edit] = @guardian.can_edit_serialized_category?( - category_id: category[:id], - read_restricted: category[:read_restricted] - ) - - by_id[category[:id]] = category - end - - categories.reject! { |c| c[:parent_category_id] && !by_id[c[:parent_category_id]] } - - self.class.categories_callbacks.each do |callback| - callback.call(categories, @guardian) - end - - categories - end end def groups - Group - .visible_groups(@guardian.user, "name ASC", include_everyone: true) - .includes(:flair_upload) + Group.visible_groups(@guardian.user, "name ASC", include_everyone: true).includes(:flair_upload) end def archetypes @@ -157,24 +163,31 @@ class Site def self.json_for(guardian) if guardian.anonymous? && SiteSetting.login_required - return { - periods: TopTopic.periods.map(&:to_s), - filters: Discourse.filters.map(&:to_s), - user_fields: UserField.includes(:user_field_options).order(:position).all.map do |userfield| - UserFieldSerializer.new(userfield, root: false, scope: guardian) - end, - auth_providers: Discourse.enabled_auth_providers.map do |provider| - AuthProviderSerializer.new(provider, root: false, scope: guardian) - end - }.to_json + return( + { + periods: TopTopic.periods.map(&:to_s), + filters: Discourse.filters.map(&:to_s), + user_fields: + UserField + .includes(:user_field_options) + .order(:position) + .all + .map { |userfield| UserFieldSerializer.new(userfield, root: false, scope: guardian) }, + auth_providers: + Discourse.enabled_auth_providers.map do |provider| + AuthProviderSerializer.new(provider, root: false, scope: guardian) + end, + }.to_json + ) end seq = nil if guardian.anonymous? - seq = MessageBus.last_id('/site_json') + seq = MessageBus.last_id("/site_json") - cached_json, cached_seq, cached_version = Discourse.redis.mget('site_json', 'site_json_seq', 'site_json_version') + cached_json, cached_seq, cached_version = + Discourse.redis.mget("site_json", "site_json_seq", "site_json_version") if cached_json && seq == cached_seq.to_i && Discourse.git_version == cached_version return cached_json @@ -186,21 +199,21 @@ class Site if guardian.anonymous? Discourse.redis.multi do |transaction| - transaction.setex 'site_json', 1800, json - transaction.set 'site_json_seq', seq - transaction.set 'site_json_version', Discourse.git_version + transaction.setex "site_json", 1800, json + transaction.set "site_json_seq", seq + transaction.set "site_json_version", Discourse.git_version end end json end - SITE_JSON_CHANNEL = '/site_json' + SITE_JSON_CHANNEL = "/site_json" def self.clear_anon_cache! # publishing forces the sequence up # the cache is validated based on the sequence - MessageBus.publish(SITE_JSON_CHANNEL, '') + MessageBus.publish(SITE_JSON_CHANNEL, "") end def self.welcome_topic_banner_cache_key(user_id) @@ -208,12 +221,14 @@ class Site end def self.welcome_topic_exists_and_is_not_edited? - Post.joins(:topic) + Post + .joins(:topic) .where( "topics.id = :topic_id AND topics.deleted_at IS NULL AND posts.post_number = 1 AND posts.version = 1 AND posts.created_at > :created_at", topic_id: SiteSetting.welcome_topic_id, - created_at: 1.month.ago - ).exists? + created_at: 1.month.ago, + ) + .exists? end def self.show_welcome_topic_banner?(guardian) @@ -223,11 +238,12 @@ class Site show_welcome_topic_banner = Discourse.cache.read(welcome_topic_banner_cache_key(user_id)) return show_welcome_topic_banner unless show_welcome_topic_banner.nil? - show_welcome_topic_banner = if (user_id == User.first_login_admin_id) - welcome_topic_exists_and_is_not_edited? - else - false - end + show_welcome_topic_banner = + if (user_id == User.first_login_admin_id) + welcome_topic_exists_and_is_not_edited? + else + false + end Discourse.cache.write(welcome_topic_banner_cache_key(user_id), show_welcome_topic_banner) show_welcome_topic_banner diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index dfd2ae8bf3..01af33da64 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -14,19 +14,21 @@ class SiteSetting < ActiveRecord::Base if self.data_type == SiteSettings::TypeSupervisor.types[:upload] UploadReference.ensure_exist!(upload_ids: [self.value], target: self) elsif self.data_type == SiteSettings::TypeSupervisor.types[:uploaded_image_list] - upload_ids = self.value.split('|').compact.uniq + upload_ids = self.value.split("|").compact.uniq UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) end end end def self.load_settings(file, plugin: nil) - SiteSettings::YamlLoader.new(file).load do |category, name, default, opts| - setting(name, default, opts.merge(category: category, plugin: plugin)) - end + SiteSettings::YamlLoader + .new(file) + .load do |category, name, default, opts| + setting(name, default, opts.merge(category: category, plugin: plugin)) + end end - load_settings(File.join(Rails.root, 'config', 'site_settings.yml')) + load_settings(File.join(Rails.root, "config", "site_settings.yml")) if GlobalSetting.load_plugins? Dir[File.join(Rails.root, "plugins", "*", "config", "settings.yml")].each do |file| @@ -62,7 +64,7 @@ class SiteSetting < ActiveRecord::Base end def self.top_menu_items - top_menu.split('|').map { |menu_item| TopMenuItem.new(menu_item) } + top_menu.split("|").map { |menu_item| TopMenuItem.new(menu_item) } end def self.homepage @@ -74,7 +76,8 @@ class SiteSetting < ActiveRecord::Base end def self.anonymous_homepage - top_menu_items.map { |item| item.name } + top_menu_items + .map { |item| item.name } .select { |item| anonymous_menu_items.include?(item) } .first end @@ -98,7 +101,10 @@ class SiteSetting < ActiveRecord::Base end def self.queue_jobs=(val) - Discourse.deprecate("queue_jobs is deprecated. Please use Jobs.run_immediately! instead", drop_from: '2.9.0') + Discourse.deprecate( + "queue_jobs is deprecated. Please use Jobs.run_immediately! instead", + drop_from: "2.9.0", + ) val ? Jobs.run_later! : Jobs.run_immediately! end @@ -159,12 +165,19 @@ class SiteSetting < ActiveRecord::Base def self.s3_base_url path = self.s3_upload_bucket.split("/", 2)[1] - "#{self.absolute_base_url}#{path ? '/' + path : ''}" + "#{self.absolute_base_url}#{path ? "/" + path : ""}" end def self.absolute_base_url - url_basename = SiteSetting.s3_endpoint.split('/')[-1] - bucket = SiteSetting.enable_s3_uploads ? Discourse.store.s3_bucket_name : GlobalSetting.s3_bucket_name + url_basename = SiteSetting.s3_endpoint.split("/")[-1] + bucket = + ( + if SiteSetting.enable_s3_uploads + Discourse.store.s3_bucket_name + else + GlobalSetting.s3_bucket_name + end + ) # cf. http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region if SiteSetting.s3_endpoint.blank? || SiteSetting.s3_endpoint.end_with?("amazonaws.com") @@ -196,7 +209,7 @@ class SiteSetting < ActiveRecord::Base end client_settings << :require_invite_code - %i{ + %i[ site_logo_url site_logo_small_url site_mobile_logo_url @@ -204,9 +217,9 @@ class SiteSetting < ActiveRecord::Base site_logo_dark_url site_logo_small_dark_url site_mobile_logo_dark_url - }.each { |client_setting| client_settings << client_setting } + ].each { |client_setting| client_settings << client_setting } - %i{ + %i[ logo logo_small digest_logo @@ -221,14 +234,14 @@ class SiteSetting < ActiveRecord::Base twitter_summary_large_image opengraph_image push_notifications_icon - }.each do |setting_name| + ].each do |setting_name| define_singleton_method("site_#{setting_name}_url") do if SiteIconManager.respond_to?("#{setting_name}_url") return SiteIconManager.public_send("#{setting_name}_url") end upload = self.public_send(setting_name) - upload ? full_cdn_url(upload.url) : '' + upload ? full_cdn_url(upload.url) : "" end end @@ -242,34 +255,42 @@ class SiteSetting < ActiveRecord::Base end ALLOWLIST_DEPRECATED_SITE_SETTINGS = { - 'email_domains_blacklist': 'blocked_email_domains', - 'email_domains_whitelist': 'allowed_email_domains', - 'unicode_username_character_whitelist': 'allowed_unicode_username_characters', - 'user_website_domains_whitelist': 'allowed_user_website_domains', - 'whitelisted_link_domains': 'allowed_link_domains', - 'embed_whitelist_selector': 'allowed_embed_selectors', - 'auto_generated_whitelist': 'auto_generated_allowlist', - 'attachment_content_type_blacklist': 'blocked_attachment_content_types', - 'attachment_filename_blacklist': 'blocked_attachment_filenames', - 'use_admin_ip_whitelist': 'use_admin_ip_allowlist', - 'blacklist_ip_blocks': 'blocked_ip_blocks', - 'whitelist_internal_hosts': 'allowed_internal_hosts', - 'whitelisted_crawler_user_agents': 'allowed_crawler_user_agents', - 'blacklisted_crawler_user_agents': 'blocked_crawler_user_agents', - 'onebox_domains_blacklist': 'blocked_onebox_domains', - 'inline_onebox_domains_whitelist': 'allowed_inline_onebox_domains', - 'white_listed_spam_host_domains': 'allowed_spam_host_domains', - 'embed_blacklist_selector': 'blocked_embed_selectors', - 'embed_classname_whitelist': 'allowed_embed_classnames', + email_domains_blacklist: "blocked_email_domains", + email_domains_whitelist: "allowed_email_domains", + unicode_username_character_whitelist: "allowed_unicode_username_characters", + user_website_domains_whitelist: "allowed_user_website_domains", + whitelisted_link_domains: "allowed_link_domains", + embed_whitelist_selector: "allowed_embed_selectors", + auto_generated_whitelist: "auto_generated_allowlist", + attachment_content_type_blacklist: "blocked_attachment_content_types", + attachment_filename_blacklist: "blocked_attachment_filenames", + use_admin_ip_whitelist: "use_admin_ip_allowlist", + blacklist_ip_blocks: "blocked_ip_blocks", + whitelist_internal_hosts: "allowed_internal_hosts", + whitelisted_crawler_user_agents: "allowed_crawler_user_agents", + blacklisted_crawler_user_agents: "blocked_crawler_user_agents", + onebox_domains_blacklist: "blocked_onebox_domains", + inline_onebox_domains_whitelist: "allowed_inline_onebox_domains", + white_listed_spam_host_domains: "allowed_spam_host_domains", + embed_blacklist_selector: "blocked_embed_selectors", + embed_classname_whitelist: "allowed_embed_classnames", } ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |old_method, new_method| self.define_singleton_method(old_method) do - Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6", raise_error: true) + Discourse.deprecate( + "#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", + drop_from: "2.6", + raise_error: true, + ) send(new_method) end self.define_singleton_method("#{old_method}=") do |args| - Discourse.deprecate("#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", drop_from: "2.6", raise_error: true) + Discourse.deprecate( + "#{old_method.to_s} is deprecated, use the #{new_method.to_s}.", + drop_from: "2.6", + raise_error: true, + ) send("#{new_method}=", args) end end diff --git a/app/models/sitemap.rb b/app/models/sitemap.rb index a6ead4692e..a0ca9a6df2 100644 --- a/app/models/sitemap.rb +++ b/app/models/sitemap.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Sitemap < ActiveRecord::Base - RECENT_SITEMAP_NAME = 'recent' - NEWS_SITEMAP_NAME = 'news' + RECENT_SITEMAP_NAME = "recent" + NEWS_SITEMAP_NAME = "news" class << self def regenerate_sitemaps @@ -26,10 +26,7 @@ class Sitemap < ActiveRecord::Base def touch(name) find_or_initialize_by(name: name).tap do |sitemap| - sitemap.update!( - last_posted_at: sitemap.last_posted_topic || 3.days.ago, - enabled: true - ) + sitemap.update!(last_posted_at: sitemap.last_posted_topic || 3.days.ago, enabled: true) end end end @@ -55,15 +52,13 @@ class Sitemap < ActiveRecord::Base private def sitemap_topics - indexable_topics = Topic - .where(visible: true) - .joins(:category) - .where(categories: { read_restricted: false }) + indexable_topics = + Topic.where(visible: true).joins(:category).where(categories: { read_restricted: false }) if name == RECENT_SITEMAP_NAME - indexable_topics.where('bumped_at > ?', 3.days.ago).order(bumped_at: :desc) + indexable_topics.where("bumped_at > ?", 3.days.ago).order(bumped_at: :desc) elsif name == NEWS_SITEMAP_NAME - indexable_topics.where('bumped_at > ?', 72.hours.ago).order(bumped_at: :desc) + indexable_topics.where("bumped_at > ?", 72.hours.ago).order(bumped_at: :desc) else offset = (name.to_i - 1) * max_page_size diff --git a/app/models/skipped_email_log.rb b/app/models/skipped_email_log.rb index 2af1b2dd12..eb28a0749f 100644 --- a/app/models/skipped_email_log.rb +++ b/app/models/skipped_email_log.rb @@ -14,37 +14,38 @@ class SkippedEmailLog < ActiveRecord::Base validate :ensure_valid_reason_type def self.reason_types - @types ||= Enum.new( - custom: 1, - exceeded_emails_limit: 2, - exceeded_bounces_limit: 3, - mailing_list_no_echo_mode: 4, - user_email_no_user: 5, - user_email_post_not_found: 6, - user_email_anonymous_user: 7, - user_email_user_suspended_not_pm: 8, - user_email_seen_recently: 9, - user_email_notification_already_read: 10, - user_email_topic_nil: 11, - user_email_post_user_deleted: 12, - user_email_post_deleted: 13, - user_email_user_suspended: 14, - user_email_already_read: 15, - sender_message_blank: 16, - sender_message_to_blank: 17, - sender_text_part_body_blank: 18, - sender_body_blank: 19, - sender_post_deleted: 20, - sender_message_to_invalid: 21, - user_email_access_denied: 22, - sender_topic_deleted: 23, - user_email_no_email: 24, - group_smtp_post_deleted: 25, - group_smtp_topic_deleted: 26, - group_smtp_disabled_for_group: 27, - # you need to add the reason in server.en.yml below the "skipped_email_log" key - # when you add a new enum value - ) + @types ||= + Enum.new( + custom: 1, + exceeded_emails_limit: 2, + exceeded_bounces_limit: 3, + mailing_list_no_echo_mode: 4, + user_email_no_user: 5, + user_email_post_not_found: 6, + user_email_anonymous_user: 7, + user_email_user_suspended_not_pm: 8, + user_email_seen_recently: 9, + user_email_notification_already_read: 10, + user_email_topic_nil: 11, + user_email_post_user_deleted: 12, + user_email_post_deleted: 13, + user_email_user_suspended: 14, + user_email_already_read: 15, + sender_message_blank: 16, + sender_message_to_blank: 17, + sender_text_part_body_blank: 18, + sender_body_blank: 19, + sender_post_deleted: 20, + sender_message_to_invalid: 21, + user_email_access_denied: 22, + sender_topic_deleted: 23, + user_email_no_email: 24, + group_smtp_post_deleted: 25, + group_smtp_topic_deleted: 26, + group_smtp_disabled_for_group: 27, + # you need to add the reason in server.en.yml below the "skipped_email_log" key + # when you add a new enum value + ) end def reason @@ -56,7 +57,7 @@ class SkippedEmailLog < ActiveRecord::Base I18n.t( "skipped_email_log.#{SkippedEmailLog.reason_types[type]}", user_id: self.user_id, - post_id: self.post_id + post_id: self.post_id, ) end end @@ -68,9 +69,7 @@ class SkippedEmailLog < ActiveRecord::Base end def ensure_valid_reason_type - unless self.class.reason_types[self.reason_type] - self.errors.add(:reason_type, :invalid) - end + self.errors.add(:reason_type, :invalid) unless self.class.reason_types[self.reason_type] end end diff --git a/app/models/slug_setting.rb b/app/models/slug_setting.rb index 65460f8390..bd8912996d 100644 --- a/app/models/slug_setting.rb +++ b/app/models/slug_setting.rb @@ -1,17 +1,13 @@ # frozen_string_literal: true class SlugSetting < EnumSiteSetting - - VALUES = %w(ascii encoded none) + VALUES = %w[ascii encoded none] def self.valid_value?(val) VALUES.include?(val) end def self.values - VALUES.map do |l| - { name: l, value: l } - end + VALUES.map { |l| { name: l, value: l } } end - end diff --git a/app/models/stylesheet_cache.rb b/app/models/stylesheet_cache.rb index 819f9ab366..df2c493e15 100644 --- a/app/models/stylesheet_cache.rb +++ b/app/models/stylesheet_cache.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class StylesheetCache < ActiveRecord::Base - self.table_name = 'stylesheet_cache' + self.table_name = "stylesheet_cache" MAX_TO_KEEP = 50 CLEANUP_AFTER_DAYS = 150 @@ -12,21 +12,14 @@ class StylesheetCache < ActiveRecord::Base return false if where(target: target, digest: digest).exists? - if Rails.env.development? - ActiveRecord::Base.logger = nil - end + ActiveRecord::Base.logger = nil if Rails.env.development? success = create(target: target, digest: digest, content: content, source_map: source_map) count = StylesheetCache.count if count > max_to_keep - - remove_lower = StylesheetCache - .where(target: target) - .limit(max_to_keep) - .order('id desc') - .pluck(:id) - .last + remove_lower = + StylesheetCache.where(target: target).limit(max_to_keep).order("id desc").pluck(:id).last DB.exec(<<~SQL, id: remove_lower, target: target) DELETE FROM stylesheet_cache @@ -38,15 +31,12 @@ class StylesheetCache < ActiveRecord::Base rescue ActiveRecord::RecordNotUnique, ActiveRecord::ReadOnlyError false ensure - if Rails.env.development? && old_logger - ActiveRecord::Base.logger = old_logger - end + ActiveRecord::Base.logger = old_logger if Rails.env.development? && old_logger end def self.clean_up - StylesheetCache.where('created_at < ?', CLEANUP_AFTER_DAYS.days.ago).delete_all + StylesheetCache.where("created_at < ?", CLEANUP_AFTER_DAYS.days.ago).delete_all end - end # == Schema Information diff --git a/app/models/tag.rb b/app/models/tag.rb index 6311dd3dc2..64b932d02e 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -5,29 +5,30 @@ class Tag < ActiveRecord::Base include HasDestroyedWebHook RESERVED_TAGS = [ - 'none', - 'constructor' # prevents issues with javascript's constructor of objects + "none", + "constructor", # prevents issues with javascript's constructor of objects ] - validates :name, - presence: true, - uniqueness: { case_sensitive: false } + validates :name, presence: true, uniqueness: { case_sensitive: false } - validate :target_tag_validator, if: Proc.new { |t| t.new_record? || t.will_save_change_to_target_tag_id? } + validate :target_tag_validator, + if: Proc.new { |t| t.new_record? || t.will_save_change_to_target_tag_id? } validate :name_validator validates :description, length: { maximum: 280 } - scope :where_name, ->(name) do - name = Array(name).map(&:downcase) - where("lower(tags.name) IN (?)", name) - end + scope :where_name, + ->(name) { + name = Array(name).map(&:downcase) + where("lower(tags.name) IN (?)", name) + } # tags that have never been used and don't belong to a tag group - scope :unused, -> do - where(topic_count: 0, pm_topic_count: 0) - .joins("LEFT JOIN tag_group_memberships tgm ON tags.id = tgm.tag_id") - .where("tgm.tag_id IS NULL") - end + scope :unused, + -> { + where(topic_count: 0, pm_topic_count: 0).joins( + "LEFT JOIN tag_group_memberships tgm ON tags.id = tgm.tag_id", + ).where("tgm.tag_id IS NULL") + } scope :base_tags, -> { where(target_tag_id: nil) } @@ -93,7 +94,7 @@ class Tag < ActiveRecord::Base end def self.find_by_name(name) - self.find_by('lower(name) = ?', name.downcase) + self.find_by("lower(name) = ?", name.downcase) end def self.top_tags(limit_arg: nil, category: nil, guardian: nil) @@ -102,19 +103,24 @@ class Tag < ActiveRecord::Base limit = limit_arg || (SiteSetting.max_tags_in_filter_list + 1) scope_category_ids = (guardian || Guardian.new).allowed_category_ids - if category - scope_category_ids &= ([category.id] + category.subcategories.pluck(:id)) - end + scope_category_ids &= ([category.id] + category.subcategories.pluck(:id)) if category return [] if scope_category_ids.empty? - filter_sql = guardian&.is_staff? ? '' : " AND tags.id IN (#{DiscourseTagging.visible_tags(guardian).select(:id).to_sql})" + filter_sql = + ( + if guardian&.is_staff? + "" + else + " AND tags.id IN (#{DiscourseTagging.visible_tags(guardian).select(:id).to_sql})" + end + ) tag_names_with_counts = DB.query <<~SQL SELECT tags.name as tag_name, SUM(stats.topic_count) AS sum_topic_count FROM category_tag_stats stats JOIN tags ON stats.tag_id = tags.id AND stats.topic_count > 0 - WHERE stats.category_id in (#{scope_category_ids.join(',')}) + WHERE stats.category_id in (#{scope_category_ids.join(",")}) #{filter_sql} GROUP BY tags.name ORDER BY sum_topic_count DESC, tag_name ASC @@ -181,16 +187,16 @@ class Tag < ActiveRecord::Base def update_synonym_associations if target_tag_id && saved_change_to_target_tag_id? - target_tag.tag_groups.each { |tag_group| tag_group.tags << self unless tag_group.tags.include?(self) } - target_tag.categories.each { |category| category.tags << self unless category.tags.include?(self) } + target_tag.tag_groups.each do |tag_group| + tag_group.tags << self unless tag_group.tags.include?(self) + end + target_tag.categories.each do |category| + category.tags << self unless category.tags.include?(self) + end end end - %i{ - tag_created - tag_updated - tag_destroyed - }.each do |event| + %i[tag_created tag_updated tag_destroyed].each do |event| define_method("trigger_#{event}_event") do DiscourseEvent.trigger(event, self) true @@ -200,9 +206,7 @@ class Tag < ActiveRecord::Base private def name_validator - if name.present? && RESERVED_TAGS.include?(self.name.strip.downcase) - errors.add(:name, :invalid) - end + errors.add(:name, :invalid) if name.present? && RESERVED_TAGS.include?(self.name.strip.downcase) end end diff --git a/app/models/tag_group.rb b/app/models/tag_group.rb index c80c65f95c..54ee33bc1c 100644 --- a/app/models/tag_group.rb +++ b/app/models/tag_group.rb @@ -10,7 +10,7 @@ class TagGroup < ActiveRecord::Base has_many :categories, through: :category_tag_groups has_many :tag_group_permissions, dependent: :destroy - belongs_to :parent_tag, class_name: 'Tag' + belongs_to :parent_tag, class_name: "Tag" before_create :init_permissions before_save :apply_permissions @@ -28,7 +28,11 @@ class TagGroup < ActiveRecord::Base if tag_names_arg.empty? self.parent_tag = nil else - if tag_name = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user)).first + if tag_name = + DiscourseTagging.tags_for_saving( + tag_names_arg, + Guardian.new(Discourse.system_user), + ).first self.parent_tag = Tag.find_by_name(tag_name) || Tag.create(name: tag_name) end end @@ -40,11 +44,7 @@ class TagGroup < ActiveRecord::Base # TODO: long term we can cache this if TONs of tag groups exist def self.find_id_by_slug(slug) - self.pluck(:id, :name).each do |id, name| - if Slug.for(name) == slug - return id - end - end + self.pluck(:id, :name).each { |id, name| return id if Slug.for(name) == slug } nil end @@ -60,7 +60,7 @@ class TagGroup < ActiveRecord::Base unless tag_group_permissions.present? || @permissions tag_group_permissions.build( group_id: Group::AUTO_GROUPS[:everyone], - permission_type: TagGroupPermission.permission_types[:full] + permission_type: TagGroupPermission.permission_types[:full], ) end end @@ -98,7 +98,11 @@ class TagGroup < ActiveRecord::Base AND id IN (SELECT tag_group_id FROM tag_group_permissions WHERE group_id IN (?)) SQL - TagGroup.where(filter_sql, guardian.allowed_category_ids, DiscourseTagging.permitted_group_ids(guardian)) + TagGroup.where( + filter_sql, + guardian.allowed_category_ids, + DiscourseTagging.permitted_group_ids(guardian), + ) end end end diff --git a/app/models/tag_user.rb b/app/models/tag_user.rb index d9437ec9f4..21a01e5c92 100644 --- a/app/models/tag_user.rb +++ b/app/models/tag_user.rb @@ -4,20 +4,27 @@ class TagUser < ActiveRecord::Base belongs_to :tag belongs_to :user - scope :notification_level_visible, -> (notification_levels = TagUser.notification_levels.values) { - select("tag_users.*") - .distinct - .joins("LEFT OUTER JOIN tag_group_memberships ON tag_users.tag_id = tag_group_memberships.tag_id") - .joins("LEFT OUTER JOIN tag_group_permissions ON tag_group_memberships.tag_group_id = tag_group_permissions.tag_group_id") - .joins("LEFT OUTER JOIN group_users on group_users.user_id = tag_users.user_id") - .where("(tag_group_permissions.group_id IS NULL + scope :notification_level_visible, + ->(notification_levels = TagUser.notification_levels.values) { + select("tag_users.*") + .distinct + .joins( + "LEFT OUTER JOIN tag_group_memberships ON tag_users.tag_id = tag_group_memberships.tag_id", + ) + .joins( + "LEFT OUTER JOIN tag_group_permissions ON tag_group_memberships.tag_group_id = tag_group_permissions.tag_group_id", + ) + .joins("LEFT OUTER JOIN group_users on group_users.user_id = tag_users.user_id") + .where( + "(tag_group_permissions.group_id IS NULL OR tag_group_permissions.group_id IN (:everyone_group_id, group_users.group_id) OR group_users.group_id = :staff_group_id) AND tag_users.notification_level IN (:notification_levels)", - staff_group_id: Group::AUTO_GROUPS[:staff], - everyone_group_id: Group::AUTO_GROUPS[:everyone], - notification_levels: notification_levels) - } + staff_group_id: Group::AUTO_GROUPS[:staff], + everyone_group_id: Group::AUTO_GROUPS[:everyone], + notification_levels: notification_levels, + ) + } def self.notification_levels NotificationLevels.all @@ -34,46 +41,48 @@ class TagUser < ActiveRecord::Base records = TagUser.where(user: user, notification_level: notification_levels[level]) old_ids = records.pluck(:tag_id) - tag_ids = if tags.empty? - [] - elsif tags.first&.is_a?(String) - Tag.where_name(tags).pluck(:id) - else - tags - end + tag_ids = + if tags.empty? + [] + elsif tags.first&.is_a?(String) + Tag.where_name(tags).pluck(:id) + else + tags + end - Tag.where(id: tag_ids).joins(:target_tag).each do |tag| - tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id - end + Tag + .where(id: tag_ids) + .joins(:target_tag) + .each { |tag| tag_ids[tag_ids.index(tag.id)] = tag.target_tag_id } tag_ids.uniq! if tag_ids.present? && - TagUser.where(user_id: user.id, tag_id: tag_ids) - .where - .not(notification_level: notification_levels[level]) - .update_all(notification_level: notification_levels[level]) > 0 - + TagUser + .where(user_id: user.id, tag_id: tag_ids) + .where.not(notification_level: notification_levels[level]) + .update_all(notification_level: notification_levels[level]) > 0 changed = true end remove = (old_ids - tag_ids) if remove.present? - records.where('tag_id in (?)', remove).destroy_all + records.where("tag_id in (?)", remove).destroy_all changed = true end now = Time.zone.now - new_records_attrs = (tag_ids - old_ids).map do |tag_id| - { - user_id: user.id, - tag_id: tag_id, - notification_level: notification_levels[level], - created_at: now, - updated_at: now - } - end + new_records_attrs = + (tag_ids - old_ids).map do |tag_id| + { + user_id: user.id, + tag_id: tag_id, + notification_level: notification_levels[level], + created_at: now, + updated_at: now, + } + end unless new_records_attrs.empty? result = TagUser.insert_all(new_records_attrs) @@ -96,9 +105,7 @@ class TagUser < ActiveRecord::Base tag = Tag.find_by_id(tag_id) end - if tag.synonym? - tag_id = tag.target_tag_id - end + tag_id = tag.target_tag_id if tag.synonym? user_id = user_id.id if user_id.is_a?(::User) @@ -168,11 +175,12 @@ class TagUser < ActiveRecord::Base builder.where("tu.user_id = :user_id", user_id: user_id) end - builder.exec(watching: notification_levels[:watching], - tracking: notification_levels[:tracking], - regular: notification_levels[:regular], - auto_watch_tag: TopicUser.notification_reasons[:auto_watch_tag]) - + builder.exec( + watching: notification_levels[:watching], + tracking: notification_levels[:tracking], + regular: notification_levels[:regular], + auto_watch_tag: TopicUser.notification_reasons[:auto_watch_tag], + ) end def self.auto_track(opts) @@ -202,36 +210,40 @@ class TagUser < ActiveRecord::Base builder.where("tu.user_id = :user_id", user_id: user_id) end - builder.exec(tracking: notification_levels[:tracking], - regular: notification_levels[:regular], - auto_track_tag: TopicUser.notification_reasons[:auto_track_tag]) + builder.exec( + tracking: notification_levels[:tracking], + regular: notification_levels[:regular], + auto_track_tag: TopicUser.notification_reasons[:auto_track_tag], + ) end def self.notification_levels_for(user) # Anonymous users have all default tags set to regular tracking, # except for default muted tags which stay muted. if user.blank? - notification_levels = [ - SiteSetting.default_tags_watching_first_post.split("|"), - SiteSetting.default_tags_watching.split("|"), - SiteSetting.default_tags_tracking.split("|") - ].flatten.map do |name| - [name, self.notification_levels[:regular]] - end + notification_levels = + [ + SiteSetting.default_tags_watching_first_post.split("|"), + SiteSetting.default_tags_watching.split("|"), + SiteSetting.default_tags_tracking.split("|"), + ].flatten.map { |name| [name, self.notification_levels[:regular]] } - notification_levels += SiteSetting.default_tags_muted.split("|").map do |name| - [name, self.notification_levels[:muted]] - end + notification_levels += + SiteSetting + .default_tags_muted + .split("|") + .map { |name| [name, self.notification_levels[:muted]] } else - notification_levels = TagUser - .notification_level_visible - .where(user: user) - .joins(:tag).pluck("tags.name", :notification_level) + notification_levels = + TagUser + .notification_level_visible + .where(user: user) + .joins(:tag) + .pluck("tags.name", :notification_level) end Hash[*notification_levels.flatten] end - end # == Schema Information diff --git a/app/models/theme.rb b/app/models/theme.rb index e14fa527ed..23701642e5 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'csv' -require 'json_schemer' +require "csv" +require "json_schemer" class Theme < ActiveRecord::Base include GlobalPath @@ -17,44 +17,67 @@ class Theme < ActiveRecord::Base has_many :theme_fields, dependent: :destroy has_many :theme_settings, dependent: :destroy has_many :theme_translation_overrides, dependent: :destroy - has_many :child_theme_relation, class_name: 'ChildTheme', foreign_key: 'parent_theme_id', dependent: :destroy - has_many :parent_theme_relation, class_name: 'ChildTheme', foreign_key: 'child_theme_id', dependent: :destroy + has_many :child_theme_relation, + class_name: "ChildTheme", + foreign_key: "parent_theme_id", + dependent: :destroy + has_many :parent_theme_relation, + class_name: "ChildTheme", + foreign_key: "child_theme_id", + dependent: :destroy has_many :child_themes, -> { order(:name) }, through: :child_theme_relation, source: :child_theme - has_many :parent_themes, -> { order(:name) }, through: :parent_theme_relation, source: :parent_theme + has_many :parent_themes, + -> { order(:name) }, + through: :parent_theme_relation, + source: :parent_theme has_many :color_schemes belongs_to :remote_theme, dependent: :destroy has_one :theme_modifier_set, dependent: :destroy - has_one :settings_field, -> { where(target_id: Theme.targets[:settings], name: "yaml") }, class_name: 'ThemeField' + has_one :settings_field, + -> { where(target_id: Theme.targets[:settings], name: "yaml") }, + class_name: "ThemeField" has_one :javascript_cache, dependent: :destroy - has_many :locale_fields, -> { filter_locale_fields(I18n.fallbacks[I18n.locale]) }, class_name: 'ThemeField' - has_many :upload_fields, -> { where(type_id: ThemeField.types[:theme_upload_var]).preload(:upload) }, class_name: 'ThemeField' - has_many :extra_scss_fields, -> { where(target_id: Theme.targets[:extra_scss]) }, class_name: 'ThemeField' - has_many :yaml_theme_fields, -> { where("name = 'yaml' AND type_id = ?", ThemeField.types[:yaml]) }, class_name: 'ThemeField' - has_many :var_theme_fields, -> { where("type_id IN (?)", ThemeField.theme_var_type_ids) }, class_name: 'ThemeField' - has_many :builder_theme_fields, -> { where("name IN (?)", ThemeField.scss_fields) }, class_name: 'ThemeField' + has_many :locale_fields, + -> { filter_locale_fields(I18n.fallbacks[I18n.locale]) }, + class_name: "ThemeField" + has_many :upload_fields, + -> { where(type_id: ThemeField.types[:theme_upload_var]).preload(:upload) }, + class_name: "ThemeField" + has_many :extra_scss_fields, + -> { where(target_id: Theme.targets[:extra_scss]) }, + class_name: "ThemeField" + has_many :yaml_theme_fields, + -> { where("name = 'yaml' AND type_id = ?", ThemeField.types[:yaml]) }, + class_name: "ThemeField" + has_many :var_theme_fields, + -> { where("type_id IN (?)", ThemeField.theme_var_type_ids) }, + class_name: "ThemeField" + has_many :builder_theme_fields, + -> { where("name IN (?)", ThemeField.scss_fields) }, + class_name: "ThemeField" validate :component_validations after_create :update_child_components - scope :user_selectable, ->() { - where('user_selectable OR id = ?', SiteSetting.default_theme_id) - } + scope :user_selectable, -> { where("user_selectable OR id = ?", SiteSetting.default_theme_id) } - scope :include_relations, -> { - includes(:child_themes, - :parent_themes, - :remote_theme, - :theme_settings, - :settings_field, - :locale_fields, - :user, - :color_scheme, - :theme_translation_overrides, - theme_fields: :upload - ) - } + scope :include_relations, + -> { + includes( + :child_themes, + :parent_themes, + :remote_theme, + :theme_settings, + :settings_field, + :locale_fields, + :user, + :color_scheme, + :theme_translation_overrides, + theme_fields: :upload, + ) + } def notify_color_change(color, scheme: nil) scheme ||= color.color_scheme @@ -78,11 +101,11 @@ class Theme < ActiveRecord::Base theme_modifier_set.save! - if saved_change_to_name? - theme_fields.select(&:basic_html_field?).each(&:invalidate_baked!) - end + theme_fields.select(&:basic_html_field?).each(&:invalidate_baked!) if saved_change_to_name? - Theme.expire_site_cache! if saved_change_to_color_scheme_id? || saved_change_to_user_selectable? || saved_change_to_name? + if saved_change_to_color_scheme_id? || saved_change_to_user_selectable? || saved_change_to_name? + Theme.expire_site_cache! + end notify_with_scheme = saved_change_to_color_scheme_id? reload @@ -115,11 +138,12 @@ class Theme < ActiveRecord::Base end def update_javascript_cache! - all_extra_js = theme_fields - .where(target_id: Theme.targets[:extra_js]) - .order(:name, :id) - .pluck(:name, :value) - .to_h + all_extra_js = + theme_fields + .where(target_id: Theme.targets[:extra_js]) + .order(:name, :id) + .pluck(:name, :value) + .to_h if all_extra_js.present? js_compiler = ThemeJavascriptCompiler.new(id, name) @@ -135,9 +159,7 @@ class Theme < ActiveRecord::Base after_destroy do remove_from_cache! - if SiteSetting.default_theme_id == self.id - Theme.clear_default! - end + Theme.clear_default! if SiteSetting.default_theme_id == self.id if self.id ColorScheme @@ -145,9 +167,7 @@ class Theme < ActiveRecord::Base .where("id NOT IN (SELECT color_scheme_id FROM themes where color_scheme_id IS NOT NULL)") .destroy_all - ColorScheme - .where(theme_id: self.id) - .update_all(theme_id: nil) + ColorScheme.where(theme_id: self.id).update_all(theme_id: nil) end Theme.expire_site_cache! @@ -162,7 +182,7 @@ class Theme < ActiveRecord::Base GlobalSetting.s3_cdn_url, GlobalSetting.s3_endpoint, GlobalSetting.s3_bucket, - Discourse.current_hostname + Discourse.current_hostname, ] Digest::SHA1.hexdigest(dependencies.join) end @@ -199,10 +219,7 @@ class Theme < ActiveRecord::Base get_set_cache "allowed_remote_theme_ids" do urls = GlobalSetting.allowed_theme_repos.split(",").map(&:strip) - Theme - .joins(:remote_theme) - .where('remote_themes.remote_url in (?)', urls) - .pluck(:id) + Theme.joins(:remote_theme).where("remote_themes.remote_url in (?)", urls).pluck(:id) end end @@ -239,10 +256,12 @@ class Theme < ActiveRecord::Base [id] end - disabled_ids = Theme.where(id: all_ids) - .includes(:remote_theme) - .select { |t| !t.supported? || !t.enabled? } - .map(&:id) + disabled_ids = + Theme + .where(id: all_ids) + .includes(:remote_theme) + .select { |t| !t.supported? || !t.enabled? } + .map(&:id) all_ids - disabled_ids end @@ -250,9 +269,7 @@ class Theme < ActiveRecord::Base def set_default! if component - raise Discourse::InvalidParameters.new( - I18n.t("themes.errors.component_no_default") - ) + raise Discourse::InvalidParameters.new(I18n.t("themes.errors.component_no_default")) end SiteSetting.default_theme_id = id Theme.expire_site_cache! @@ -335,22 +352,35 @@ class Theme < ActiveRecord::Base end def self.clear_cache! - DB.after_commit do - @cache.clear - end + DB.after_commit { @cache.clear } end def self.targets - @targets ||= Enum.new(common: 0, desktop: 1, mobile: 2, settings: 3, translations: 4, extra_scss: 5, extra_js: 6, tests_js: 7) + @targets ||= + Enum.new( + common: 0, + desktop: 1, + mobile: 2, + settings: 3, + translations: 4, + extra_scss: 5, + extra_js: 6, + tests_js: 7, + ) end def self.lookup_target(target_id) self.targets.invert[target_id] end - def self.notify_theme_change(theme_ids, with_scheme: false, clear_manager_cache: true, all_themes: false) + def self.notify_theme_change( + theme_ids, + with_scheme: false, + clear_manager_cache: true, + all_themes: false + ) Stylesheet::Manager.clear_theme_cache! - targets = [:mobile_theme, :desktop_theme] + targets = %i[mobile_theme desktop_theme] if with_scheme targets.prepend(:desktop, :mobile, :admin) @@ -364,7 +394,7 @@ class Theme < ActiveRecord::Base message = refresh_message_for_targets(targets, theme_ids).flatten end - MessageBus.publish('/file-change', message) + MessageBus.publish("/file-change", message) end def notify_theme_change(with_scheme: false) @@ -386,20 +416,22 @@ class Theme < ActiveRecord::Base def self.resolve_baked_field(theme_ids, target, name) if target == :extra_js - require_rebake = ThemeField.where(theme_id: theme_ids, target_id: Theme.targets[:extra_js]). - where("compiler_version <> ?", Theme.compiler_version) + require_rebake = + ThemeField.where(theme_id: theme_ids, target_id: Theme.targets[:extra_js]).where( + "compiler_version <> ?", + Theme.compiler_version, + ) require_rebake.each { |tf| tf.ensure_baked! } - require_rebake.map(&:theme_id).uniq.each do |theme_id| - Theme.find(theme_id).update_javascript_cache! - end + require_rebake + .map(&:theme_id) + .uniq + .each { |theme_id| Theme.find(theme_id).update_javascript_cache! } caches = JavascriptCache.where(theme_id: theme_ids) caches = caches.sort_by { |cache| theme_ids.index(cache.theme_id) } - return caches.map do |c| - <<~HTML.html_safe + return caches.map { |c| <<~HTML.html_safe }.join("\n") HTML - end.join("\n") end list_baked_fields(theme_ids, target, name).map { |f| f.value_baked || f.value }.join("\n") end @@ -413,8 +445,10 @@ class Theme < ActiveRecord::Base else target = :mobile if target == :mobile_theme target = :desktop if target == :desktop_theme - fields = ThemeField.find_by_theme_ids(theme_ids) - .where(target_id: [Theme.targets[target], Theme.targets[:common]]) + fields = + ThemeField.find_by_theme_ids(theme_ids).where( + target_id: [Theme.targets[target], Theme.targets[:common]], + ) fields = fields.where(name: name.to_s) unless name.nil? fields = fields.order(:target_id) end @@ -455,12 +489,14 @@ class Theme < ActiveRecord::Base target_id = Theme.targets[target.to_sym] raise "Unknown target #{target} passed to set field" unless target_id - type_id ||= type ? ThemeField.types[type.to_sym] : ThemeField.guess_type(name: name, target: target) + type_id ||= + type ? ThemeField.types[type.to_sym] : ThemeField.guess_type(name: name, target: target) raise "Unknown type #{type} passed to set field" unless type_id value ||= "" - field = theme_fields.find { |f| f.name == name && f.target_id == target_id && f.type_id == type_id } + field = + theme_fields.find { |f| f.name == name && f.target_id == target_id && f.type_id == type_id } if field if value.blank? && !upload_id theme_fields.delete field.destroy @@ -473,16 +509,25 @@ class Theme < ActiveRecord::Base end field else - theme_fields.build(target_id: target_id, value: value, name: name, type_id: type_id, upload_id: upload_id) if value.present? || upload_id.present? + if value.present? || upload_id.present? + theme_fields.build( + target_id: target_id, + value: value, + name: name, + type_id: type_id, + upload_id: upload_id, + ) + end end end def add_relative_theme!(kind, theme) - new_relation = if kind == :child - child_theme_relation.new(child_theme_id: theme.id) - else - parent_theme_relation.new(parent_theme_id: theme.id) - end + new_relation = + if kind == :child + child_theme_relation.new(child_theme_id: theme.id) + else + parent_theme_relation.new(parent_theme_id: theme.id) + end if new_relation.save child_themes.reload parent_themes.reload @@ -500,13 +545,20 @@ class Theme < ActiveRecord::Base def translations(internal: false) fallbacks = I18n.fallbacks[I18n.locale] begin - data = locale_fields.first&.translation_data(with_overrides: false, internal: internal, fallback_fields: locale_fields) + data = + locale_fields.first&.translation_data( + with_overrides: false, + internal: internal, + fallback_fields: locale_fields, + ) return {} if data.nil? best_translations = {} - fallbacks.reverse.each do |locale| - best_translations.deep_merge! data[locale] if data[locale] - end - ThemeTranslationManager.list_from_hash(theme: self, hash: best_translations, locale: I18n.locale) + fallbacks.reverse.each { |locale| best_translations.deep_merge! data[locale] if data[locale] } + ThemeTranslationManager.list_from_hash( + theme: self, + hash: best_translations, + locale: I18n.locale, + ) rescue ThemeTranslationParser::InvalidYaml {} end @@ -517,9 +569,11 @@ class Theme < ActiveRecord::Base return [] unless field && field.error.nil? settings = [] - ThemeSettingsParser.new(field).load do |name, default, type, opts| - settings << ThemeSettingsManager.create(name, default, type, self, opts) - end + ThemeSettingsParser + .new(field) + .load do |name, default, type, opts| + settings << ThemeSettingsManager.create(name, default, type, self, opts) + end settings end @@ -532,15 +586,13 @@ class Theme < ActiveRecord::Base def cached_default_settings Theme.get_set_cache "default_settings_for_theme_#{self.id}" do settings_hash = {} - self.settings.each do |setting| - settings_hash[setting.name] = setting.default - end + self.settings.each { |setting| settings_hash[setting.name] = setting.default } theme_uploads = build_theme_uploads_hash - settings_hash['theme_uploads'] = theme_uploads if theme_uploads.present? + settings_hash["theme_uploads"] = theme_uploads if theme_uploads.present? theme_uploads_local = build_local_theme_uploads_hash - settings_hash['theme_uploads_local'] = theme_uploads_local if theme_uploads_local.present? + settings_hash["theme_uploads_local"] = theme_uploads_local if theme_uploads_local.present? settings_hash end @@ -548,15 +600,13 @@ class Theme < ActiveRecord::Base def build_settings_hash hash = {} - self.settings.each do |setting| - hash[setting.name] = setting.value - end + self.settings.each { |setting| hash[setting.name] = setting.value } theme_uploads = build_theme_uploads_hash - hash['theme_uploads'] = theme_uploads if theme_uploads.present? + hash["theme_uploads"] = theme_uploads if theme_uploads.present? theme_uploads_local = build_local_theme_uploads_hash - hash['theme_uploads_local'] = theme_uploads_local if theme_uploads_local.present? + hash["theme_uploads_local"] = theme_uploads_local if theme_uploads_local.present? hash end @@ -564,9 +614,7 @@ class Theme < ActiveRecord::Base def build_theme_uploads_hash hash = {} upload_fields.each do |field| - if field.upload&.url - hash[field.name] = Discourse.store.cdn_url(field.upload.url) - end + hash[field.name] = Discourse.store.cdn_url(field.upload.url) if field.upload&.url end hash end @@ -574,9 +622,7 @@ class Theme < ActiveRecord::Base def build_local_theme_uploads_hash hash = {} upload_fields.each do |field| - if field.javascript_cache - hash[field.name] = field.javascript_cache.local_url - end + hash[field.name] = field.javascript_cache.local_url if field.javascript_cache end hash end @@ -587,9 +633,7 @@ class Theme < ActiveRecord::Base target_setting.value = new_value - if target_setting.requests_refresh? - self.theme_setting_requests_refresh = true - end + self.theme_setting_requests_refresh = true if target_setting.requests_refresh? end def update_translation(translation_key, new_value) @@ -603,9 +647,7 @@ class Theme < ActiveRecord::Base theme_translation_overrides.each do |override| cursor = hash path = [override.locale] + override.translation_key.split(".") - path[0..-2].each do |key| - cursor = (cursor[key] ||= {}) - end + path[0..-2].each { |key| cursor = (cursor[key] ||= {}) } cursor[path[-1]] = override.value end hash @@ -622,9 +664,9 @@ class Theme < ActiveRecord::Base end meta[:assets] = {}.tap do |hash| - theme_fields.where(type_id: ThemeField.types[:theme_upload_var]).each do |field| - hash[field.name] = field.file_path - end + theme_fields + .where(type_id: ThemeField.types[:theme_upload_var]) + .each { |field| hash[field.name] = field.file_path } end meta[:color_schemes] = {}.tap do |hash| @@ -632,7 +674,9 @@ class Theme < ActiveRecord::Base # The selected color scheme may not belong to the theme, so include it anyway schemes = [self.color_scheme] + schemes if self.color_scheme schemes.uniq.each do |scheme| - hash[scheme.name] = {}.tap { |colors| scheme.colors.each { |color| colors[color.name] = color.hex } } + hash[scheme.name] = {}.tap do |colors| + scheme.colors.each { |color| colors[color.name] = color.hex } + end end end @@ -643,8 +687,9 @@ class Theme < ActiveRecord::Base end end - meta[:learn_more] = "https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966" - + meta[ + :learn_more + ] = "https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966" end end @@ -659,9 +704,9 @@ class Theme < ActiveRecord::Base def with_scss_load_paths return yield([]) if self.extra_scss_fields.empty? - ThemeStore::ZipExporter.new(self).with_export_dir(extra_scss_only: true) do |dir| - yield ["#{dir}/stylesheets"] - end + ThemeStore::ZipExporter + .new(self) + .with_export_dir(extra_scss_only: true) { |dir| yield ["#{dir}/stylesheets"] } end def scss_variables @@ -696,12 +741,15 @@ class Theme < ActiveRecord::Base setting_row = ThemeSetting.where(theme_id: self.id, name: setting.name.to_s).first if setting_row && setting_row.data_type != setting.type - if (setting_row.data_type == ThemeSetting.types[:list] && - setting.type == ThemeSetting.types[:string] && - setting.json_schema.present?) + if ( + setting_row.data_type == ThemeSetting.types[:list] && + setting.type == ThemeSetting.types[:string] && setting.json_schema.present? + ) convert_list_to_json_schema(setting_row, setting) else - Rails.logger.warn("Theme setting type has changed but cannot be converted. \n\n #{setting.inspect}") + Rails.logger.warn( + "Theme setting type has changed but cannot be converted. \n\n #{setting.inspect}", + ) end end end @@ -713,10 +761,10 @@ class Theme < ActiveRecord::Base keys = schema["items"]["properties"].keys return if !keys - current_values = CSV.parse(setting_row.value, **{ col_sep: '|' }).flatten + current_values = CSV.parse(setting_row.value, **{ col_sep: "|" }).flatten new_values = [] current_values.each do |item| - parts = CSV.parse(item, **{ col_sep: ',' }).flatten + parts = CSV.parse(item, **{ col_sep: "," }).flatten props = parts.map.with_index { |p, idx| [keys[idx], p] }.to_h new_values << props end @@ -730,13 +778,14 @@ class Theme < ActiveRecord::Base end def baked_js_tests_with_digest - tests_tree = theme_fields - .where(target_id: Theme.targets[:tests_js]) - .order(name: :asc) - .pluck(:name, :value) - .to_h + tests_tree = + theme_fields + .where(target_id: Theme.targets[:tests_js]) + .order(name: :asc) + .pluck(:name, :value) + .to_h - return [nil, nil] if tests_tree.blank? + return nil, nil if tests_tree.blank? compiler = ThemeJavascriptCompiler.new(id, name) compiler.append_tree(tests_tree, for_tests: true) @@ -748,7 +797,8 @@ class Theme < ActiveRecord::Base content = compiler.content if compiler.source_map - content += "\n//# sourceMappingURL=data:application/json;base64,#{Base64.strict_encode64(compiler.source_map)}\n" + content += + "\n//# sourceMappingURL=data:application/json;base64,#{Base64.strict_encode64(compiler.source_map)}\n" end [content, Digest::SHA1.hexdigest(content)] @@ -765,7 +815,11 @@ class Theme < ActiveRecord::Base def find_disable_action_log if component? && !enabled? - @disable_log ||= UserHistory.where(context: id.to_s, action: UserHistory.actions[:disable_theme_component]).order("created_at DESC").first + @disable_log ||= + UserHistory + .where(context: id.to_s, action: UserHistory.actions[:disable_theme_component]) + .order("created_at DESC") + .first end end end diff --git a/app/models/theme_field.rb b/app/models/theme_field.rb index 4c45b08b60..780789c42b 100644 --- a/app/models/theme_field.rb +++ b/app/models/theme_field.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ThemeField < ActiveRecord::Base - belongs_to :upload has_one :javascript_cache, dependent: :destroy has_one :upload_reference, as: :target, dependent: :destroy @@ -12,45 +11,50 @@ class ThemeField < ActiveRecord::Base end end - scope :find_by_theme_ids, ->(theme_ids) { - return none unless theme_ids.present? + scope :find_by_theme_ids, + ->(theme_ids) { + return none unless theme_ids.present? - where(theme_id: theme_ids) - .joins( - "JOIN ( + where(theme_id: theme_ids).joins( + "JOIN ( SELECT #{theme_ids.map.with_index { |id, idx| "#{id.to_i} AS theme_id, #{idx} AS theme_sort_column" }.join(" UNION ALL SELECT ")} - ) as X ON X.theme_id = theme_fields.theme_id") - .order("theme_sort_column") - } + ) as X ON X.theme_id = theme_fields.theme_id", + ).order("theme_sort_column") + } - scope :filter_locale_fields, ->(locale_codes) { - return none unless locale_codes.present? + scope :filter_locale_fields, + ->(locale_codes) { + return none unless locale_codes.present? - where(target_id: Theme.targets[:translations], name: locale_codes) - .joins(DB.sql_fragment( - "JOIN ( + where(target_id: Theme.targets[:translations], name: locale_codes).joins( + DB.sql_fragment( + "JOIN ( SELECT * FROM (VALUES #{locale_codes.map { "(?)" }.join(",")}) as Y (locale_code, locale_sort_column) ) as Y ON Y.locale_code = theme_fields.name", - *locale_codes.map.with_index { |code, index| [code, index] } - )) - .order("Y.locale_sort_column") - } + *locale_codes.map.with_index { |code, index| [code, index] }, + ), + ).order("Y.locale_sort_column") + } - scope :find_first_locale_fields, ->(theme_ids, locale_codes) { - find_by_theme_ids(theme_ids) - .filter_locale_fields(locale_codes) - .reorder("X.theme_sort_column", "Y.locale_sort_column") - .select("DISTINCT ON (X.theme_sort_column) *") - } + scope :find_first_locale_fields, + ->(theme_ids, locale_codes) { + find_by_theme_ids(theme_ids) + .filter_locale_fields(locale_codes) + .reorder("X.theme_sort_column", "Y.locale_sort_column") + .select("DISTINCT ON (X.theme_sort_column) *") + } def self.types - @types ||= Enum.new(html: 0, - scss: 1, - theme_upload_var: 2, - theme_color_var: 3, # No longer used - theme_var: 4, # No longer used - yaml: 5, - js: 6) + @types ||= + Enum.new( + html: 0, + scss: 1, + theme_upload_var: 2, + theme_color_var: 3, # No longer used + theme_var: 4, # No longer used + yaml: 5, + js: 6, + ) end def self.theme_var_type_ids @@ -68,8 +72,11 @@ class ThemeField < ActiveRecord::Base end end - validates :name, format: { with: /\A[a-z_][a-z0-9_-]*\z/i }, - if: Proc.new { |field| ThemeField.theme_var_type_ids.include?(field.type_id) } + validates :name, + format: { + with: /\A[a-z_][a-z0-9_-]*\z/i, + }, + if: Proc.new { |field| ThemeField.theme_var_type_ids.include?(field.type_id) } belongs_to :theme @@ -83,36 +90,38 @@ class ThemeField < ActiveRecord::Base doc = Nokogiri::HTML5.fragment(html) - doc.css('script[type="text/x-handlebars"]').each do |node| - name = node["name"] || node["data-template-name"] || "broken" - is_raw = name =~ /\.(raw|hbr)$/ - hbs_template = node.inner_html + doc + .css('script[type="text/x-handlebars"]') + .each do |node| + name = node["name"] || node["data-template-name"] || "broken" + is_raw = name =~ /\.(raw|hbr)$/ + hbs_template = node.inner_html - begin - if is_raw - js_compiler.append_raw_template(name, hbs_template) - else - js_compiler.append_ember_template("discourse/templates/#{name}", hbs_template) + begin + if is_raw + js_compiler.append_raw_template(name, hbs_template) + else + js_compiler.append_ember_template("discourse/templates/#{name}", hbs_template) + end + rescue ThemeJavascriptCompiler::CompileError => ex + js_compiler.append_js_error("discourse/templates/#{name}", ex.message) + errors << ex.message end - rescue ThemeJavascriptCompiler::CompileError => ex - js_compiler.append_js_error("discourse/templates/#{name}", ex.message) - errors << ex.message + + node.remove end - node.remove - end + doc + .css('script[type="text/discourse-plugin"]') + .each_with_index do |node, index| + version = node["version"] + next if version.blank? - doc.css('script[type="text/discourse-plugin"]').each_with_index do |node, index| - version = node['version'] - next if version.blank? - - initializer_name = "theme-field" + - "-#{self.id}" + - "-#{Theme.targets[self.target_id]}" + - "-#{ThemeField.types[self.type_id]}" + - "-script-#{index + 1}" - begin - js = <<~JS + initializer_name = + "theme-field" + "-#{self.id}" + "-#{Theme.targets[self.target_id]}" + + "-#{ThemeField.types[self.type_id]}" + "-script-#{index + 1}" + begin + js = <<~JS import { withPluginApi } from "discourse/lib/plugin-api"; export default { @@ -127,43 +136,60 @@ class ThemeField < ActiveRecord::Base }; JS - js_compiler.append_module(js, "discourse/initializers/#{initializer_name}", include_variables: true) - rescue ThemeJavascriptCompiler::CompileError => ex - js_compiler.append_js_error("discourse/initializers/#{initializer_name}", ex.message) - errors << ex.message + js_compiler.append_module( + js, + "discourse/initializers/#{initializer_name}", + include_variables: true, + ) + rescue ThemeJavascriptCompiler::CompileError => ex + js_compiler.append_js_error("discourse/initializers/#{initializer_name}", ex.message) + errors << ex.message + end + + node.remove end - node.remove - end - - doc.css('script').each_with_index do |node, index| - next unless inline_javascript?(node) - js_compiler.append_raw_script("_html/#{Theme.targets[self.target_id]}/#{name}_#{index + 1}.js", node.inner_html) - node.remove - end + doc + .css("script") + .each_with_index do |node, index| + next unless inline_javascript?(node) + js_compiler.append_raw_script( + "_html/#{Theme.targets[self.target_id]}/#{name}_#{index + 1}.js", + node.inner_html, + ) + node.remove + end settings_hash = theme.build_settings_hash - js_compiler.prepend_settings(settings_hash) if js_compiler.has_content? && settings_hash.present? + if js_compiler.has_content? && settings_hash.present? + js_compiler.prepend_settings(settings_hash) + end javascript_cache.content = js_compiler.content javascript_cache.source_map = js_compiler.source_map javascript_cache.save! - if javascript_cache.content.present? - doc.add_child( - <<~HTML.html_safe + doc.add_child(<<~HTML.html_safe) if javascript_cache.content.present? HTML - ) - end [doc.to_s, errors&.join("\n")] end def validate_svg_sprite_xml - upload = Upload.find(self.upload_id) rescue nil + upload = + begin + Upload.find(self.upload_id) + rescue StandardError + nil + end if Discourse.store.external? - external_copy = Discourse.store.download(upload) rescue nil + external_copy = + begin + Discourse.store.download(upload) + rescue StandardError + nil + end path = external_copy.try(:path) else path = Discourse.store.path_for(upload) @@ -173,9 +199,7 @@ class ThemeField < ActiveRecord::Base begin content = File.read(path) - Nokogiri::XML(content) do |config| - config.options = Nokogiri::XML::ParseOptions::NOBLANKS - end + Nokogiri.XML(content) { |config| config.options = Nokogiri::XML::ParseOptions::NOBLANKS } rescue => e error = "Error with #{self.name}: #{e.inspect}" end @@ -190,16 +214,17 @@ class ThemeField < ActiveRecord::Base def translation_data(with_overrides: true, internal: false, fallback_fields: nil) fallback_fields ||= theme.theme_fields.filter_locale_fields(I18n.fallbacks[name]) - fallback_data = fallback_fields.each_with_index.map do |field, index| - begin - field.raw_translation_data(internal: internal) - rescue ThemeTranslationParser::InvalidYaml - # If this is the locale with the error, raise it. - # If not, let the other theme_field raise the error when it processes itself - raise if field.id == id - {} + fallback_data = + fallback_fields.each_with_index.map do |field, index| + begin + field.raw_translation_data(internal: internal) + rescue ThemeTranslationParser::InvalidYaml + # If this is the locale with the error, raise it. + # If not, let the other theme_field raise the error when it processes itself + raise if field.id == id + {} + end end - end # TODO: Deduplicate the fallback data in the same way as JSLocaleHelper#load_translations_merged # this would reduce the size of the payload, without affecting functionality @@ -239,7 +264,11 @@ class ThemeField < ActiveRecord::Base }; JS - js_compiler.append_module(js, "discourse/pre-initializers/theme-#{theme_id}-translations", include_variables: false) + js_compiler.append_module( + js, + "discourse/pre-initializers/theme-#{theme_id}-translations", + include_variables: false, + ) rescue ThemeTranslationParser::InvalidYaml => e errors << e.message end @@ -248,13 +277,10 @@ class ThemeField < ActiveRecord::Base javascript_cache.source_map = js_compiler.source_map javascript_cache.save! doc = "" - if javascript_cache.content.present? - doc = - <<~HTML.html_safe + doc = <<~HTML.html_safe if javascript_cache.content.present? HTML - end [doc, errors&.join("\n")] end @@ -263,32 +289,32 @@ class ThemeField < ActiveRecord::Base errors = [] begin - ThemeSettingsParser.new(self).load do |name, default, type, opts| - setting = ThemeSetting.new(name: name, data_type: type, theme: theme) - translation_key = "themes.settings_errors" + ThemeSettingsParser + .new(self) + .load do |name, default, type, opts| + setting = ThemeSetting.new(name: name, data_type: type, theme: theme) + translation_key = "themes.settings_errors" - if setting.invalid? - setting.errors.details.each_pair do |attribute, _errors| - _errors.each do |hash| - errors << I18n.t("#{translation_key}.#{attribute}_#{hash[:error]}", name: name) + if setting.invalid? + setting.errors.details.each_pair do |attribute, _errors| + _errors.each do |hash| + errors << I18n.t("#{translation_key}.#{attribute}_#{hash[:error]}", name: name) + end end end - end - if default.nil? - errors << I18n.t("#{translation_key}.default_value_missing", name: name) - end + errors << I18n.t("#{translation_key}.default_value_missing", name: name) if default.nil? - if (min = opts[:min]) && (max = opts[:max]) - unless ThemeSetting.value_in_range?(default, (min..max), type) - errors << I18n.t("#{translation_key}.default_out_range", name: name) + if (min = opts[:min]) && (max = opts[:max]) + unless ThemeSetting.value_in_range?(default, (min..max), type) + errors << I18n.t("#{translation_key}.default_out_range", name: name) + end + end + + unless ThemeSetting.acceptable_value_for_type?(default, type) + errors << I18n.t("#{translation_key}.default_not_match_type", name: name) end end - - unless ThemeSetting.acceptable_value_for_type?(default, type) - errors << I18n.t("#{translation_key}.default_not_match_type", name: name) - end - end rescue ThemeSettingsParser::InvalidYaml => e errors << e.message end @@ -311,15 +337,15 @@ class ThemeField < ActiveRecord::Base end def self.html_fields - @html_fields ||= %w(body_tag head_tag header footer after_header) + @html_fields ||= %w[body_tag head_tag header footer after_header] end def self.scss_fields - @scss_fields ||= %w(scss embedded_scss color_definitions) + @scss_fields ||= %w[scss embedded_scss color_definitions] end def self.basic_targets - @basic_targets ||= %w(common desktop mobile) + @basic_targets ||= %w[common desktop mobile] end def basic_html_field? @@ -353,7 +379,8 @@ class ThemeField < ActiveRecord::Base end def svg_sprite_field? - ThemeField.theme_var_type_ids.include?(self.type_id) && self.name == SvgSprite.theme_sprite_variable_name + ThemeField.theme_var_type_ids.include?(self.type_id) && + self.name == SvgSprite.theme_sprite_variable_name end def ensure_baked! @@ -361,7 +388,8 @@ class ThemeField < ActiveRecord::Base return unless needs_baking if basic_html_field? || translation_field? - self.value_baked, self.error = translation_field? ? process_translation : process_html(self.value) + self.value_baked, self.error = + translation_field? ? process_translation : process_html(self.value) self.error = nil unless self.error.present? self.compiler_version = Theme.compiler_version DB.after_commit { CSP::Extension.clear_theme_extensions_cache! } @@ -385,13 +413,13 @@ class ThemeField < ActiveRecord::Base self.compiler_version = Theme.compiler_version end - if self.will_save_change_to_value_baked? || - self.will_save_change_to_compiler_version? || - self.will_save_change_to_error? - - self.update_columns(value_baked: value_baked, - compiler_version: compiler_version, - error: error) + if self.will_save_change_to_value_baked? || self.will_save_change_to_compiler_version? || + self.will_save_change_to_error? + self.update_columns( + value_baked: value_baked, + compiler_version: compiler_version, + error: error, + ) end end @@ -399,23 +427,25 @@ class ThemeField < ActiveRecord::Base prepended_scss ||= Stylesheet::Importer.new({}).prepended_scss self.theme.with_scss_load_paths do |load_paths| - Stylesheet::Compiler.compile("#{prepended_scss} #{self.theme.scss_variables.to_s} #{self.value}", + Stylesheet::Compiler.compile( + "#{prepended_scss} #{self.theme.scss_variables.to_s} #{self.value}", "#{Theme.targets[self.target_id]}.scss", theme: self.theme, - load_paths: load_paths + load_paths: load_paths, ) end end def compiled_css(prepended_scss) - css, _source_map = begin - compile_scss(prepended_scss) - rescue SassC::SyntaxError => e - # We don't want to raise a blocking error here - # admin theme editor or discourse_theme CLI will show it nonetheless - Rails.logger.error "SCSS compilation error: #{e.message}" - ["", nil] - end + css, _source_map = + begin + compile_scss(prepended_scss) + rescue SassC::SyntaxError => e + # We don't want to raise a blocking error here + # admin theme editor or discourse_theme CLI will show it nonetheless + Rails.logger.error "SCSS compilation error: #{e.message}" + ["", nil] + end css end @@ -450,7 +480,7 @@ class ThemeField < ActiveRecord::Base end class ThemeFileMatcher - OPTIONS = %i{name type target} + OPTIONS = %i[name type target] # regex: used to match file names to fields (import). # can contain named capture groups for name/type/target # canonical: a lambda which converts name/type/target @@ -480,55 +510,100 @@ class ThemeField < ActiveRecord::Base end def filename_from_opts(opts) - is_match = OPTIONS.all? do |option| - plural = :"#{option}s" - next true if @allowed_values[plural] == nil # Allows any value - next true if @allowed_values[plural].include?(opts[option]) # Value is allowed - end + is_match = + OPTIONS.all? do |option| + plural = :"#{option}s" + next true if @allowed_values[plural] == nil # Allows any value + next true if @allowed_values[plural].include?(opts[option]) # Value is allowed + end is_match ? @canonical.call(opts) : nil end end FILE_MATCHERS = [ - ThemeFileMatcher.new(regex: /^(?(?:mobile|desktop|common))\/(?(?:head_tag|header|after_header|body_tag|footer))\.html$/, - targets: [:mobile, :desktop, :common], names: ["head_tag", "header", "after_header", "body_tag", "footer"], types: :html, - canonical: -> (h) { "#{h[:target]}/#{h[:name]}.html" }), - ThemeFileMatcher.new(regex: /^(?(?:mobile|desktop|common))\/(?:\k)\.scss$/, - targets: [:mobile, :desktop, :common], names: "scss", types: :scss, - canonical: -> (h) { "#{h[:target]}/#{h[:target]}.scss" }), - ThemeFileMatcher.new(regex: /^common\/embedded\.scss$/, - targets: :common, names: "embedded_scss", types: :scss, - canonical: -> (h) { "common/embedded.scss" }), - ThemeFileMatcher.new(regex: /^common\/color_definitions\.scss$/, - targets: :common, names: "color_definitions", types: :scss, - canonical: -> (h) { "common/color_definitions.scss" }), - ThemeFileMatcher.new(regex: /^(?:scss|stylesheets)\/(?.+)\.scss$/, - targets: :extra_scss, names: nil, types: :scss, - canonical: -> (h) { "stylesheets/#{h[:name]}.scss" }), - ThemeFileMatcher.new(regex: /^javascripts\/(?.+)$/, - targets: :extra_js, names: nil, types: :js, - canonical: -> (h) { "javascripts/#{h[:name]}" }), - ThemeFileMatcher.new(regex: /^test\/(?.+)$/, - targets: :tests_js, names: nil, types: :js, - canonical: -> (h) { "test/#{h[:name]}" }), - ThemeFileMatcher.new(regex: /^settings\.ya?ml$/, - names: "yaml", types: :yaml, targets: :settings, - canonical: -> (h) { "settings.yml" }), - ThemeFileMatcher.new(regex: /^locales\/(?(?:#{I18n.available_locales.join("|")}))\.yml$/, - names: I18n.available_locales.map(&:to_s), types: :yaml, targets: :translations, - canonical: -> (h) { "locales/#{h[:name]}.yml" }), - ThemeFileMatcher.new(regex: /(?!)/, # Never match uploads by filename, they must be named in about.json - names: nil, types: :theme_upload_var, targets: :common, - canonical: -> (h) { "assets/#{h[:name]}#{File.extname(h[:filename])}" }), + ThemeFileMatcher.new( + regex: + %r{^(?(?:mobile|desktop|common))/(?(?:head_tag|header|after_header|body_tag|footer))\.html$}, + targets: %i[mobile desktop common], + names: %w[head_tag header after_header body_tag footer], + types: :html, + canonical: ->(h) { "#{h[:target]}/#{h[:name]}.html" }, + ), + ThemeFileMatcher.new( + regex: %r{^(?(?:mobile|desktop|common))/(?:\k)\.scss$}, + targets: %i[mobile desktop common], + names: "scss", + types: :scss, + canonical: ->(h) { "#{h[:target]}/#{h[:target]}.scss" }, + ), + ThemeFileMatcher.new( + regex: %r{^common/embedded\.scss$}, + targets: :common, + names: "embedded_scss", + types: :scss, + canonical: ->(h) { "common/embedded.scss" }, + ), + ThemeFileMatcher.new( + regex: %r{^common/color_definitions\.scss$}, + targets: :common, + names: "color_definitions", + types: :scss, + canonical: ->(h) { "common/color_definitions.scss" }, + ), + ThemeFileMatcher.new( + regex: %r{^(?:scss|stylesheets)/(?.+)\.scss$}, + targets: :extra_scss, + names: nil, + types: :scss, + canonical: ->(h) { "stylesheets/#{h[:name]}.scss" }, + ), + ThemeFileMatcher.new( + regex: %r{^javascripts/(?.+)$}, + targets: :extra_js, + names: nil, + types: :js, + canonical: ->(h) { "javascripts/#{h[:name]}" }, + ), + ThemeFileMatcher.new( + regex: %r{^test/(?.+)$}, + targets: :tests_js, + names: nil, + types: :js, + canonical: ->(h) { "test/#{h[:name]}" }, + ), + ThemeFileMatcher.new( + regex: /^settings\.ya?ml$/, + names: "yaml", + types: :yaml, + targets: :settings, + canonical: ->(h) { "settings.yml" }, + ), + ThemeFileMatcher.new( + regex: %r{^locales/(?(?:#{I18n.available_locales.join("|")}))\.yml$}, + names: I18n.available_locales.map(&:to_s), + types: :yaml, + targets: :translations, + canonical: ->(h) { "locales/#{h[:name]}.yml" }, + ), + ThemeFileMatcher.new( + regex: /(?!)/, # Never match uploads by filename, they must be named in about.json + names: nil, + types: :theme_upload_var, + targets: :common, + canonical: ->(h) { "assets/#{h[:name]}#{File.extname(h[:filename])}" }, + ), ] # For now just work for standard fields def file_path FILE_MATCHERS.each do |matcher| - if filename = matcher.filename_from_opts(target: target_name.to_sym, - name: name, - type: ThemeField.types[type_id], - filename: upload&.original_filename) + if filename = + matcher.filename_from_opts( + target: target_name.to_sym, + name: name, + type: ThemeField.types[type_id], + filename: upload&.original_filename, + ) return filename end end @@ -546,11 +621,19 @@ class ThemeField < ActiveRecord::Base def dependent_fields if extra_scss_field? - return theme.theme_fields.where(target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] }, - name: ThemeField.scss_fields) + return( + theme.theme_fields.where( + target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] }, + name: ThemeField.scss_fields, + ) + ) elsif settings_field? - return theme.theme_fields.where(target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] }, - name: ThemeField.scss_fields + ThemeField.html_fields) + return( + theme.theme_fields.where( + target_id: ThemeField.basic_targets.map { |t| Theme.targets[t.to_sym] }, + name: ThemeField.scss_fields + ThemeField.html_fields, + ) + ) end ThemeField.none end @@ -561,7 +644,8 @@ class ThemeField < ActiveRecord::Base end before_save do - if (will_save_change_to_value? || will_save_change_to_upload_id?) && !will_save_change_to_value_baked? + if (will_save_change_to_value? || will_save_change_to_upload_id?) && + !will_save_change_to_value_baked? self.value_baked = nil end if upload && upload.extension == "js" @@ -572,29 +656,19 @@ class ThemeField < ActiveRecord::Base end end - after_save do - dependent_fields.each(&:invalidate_baked!) - end + after_save { dependent_fields.each(&:invalidate_baked!) } - after_destroy do - if svg_sprite_field? - DB.after_commit { SvgSprite.expire_cache } - end - end + after_destroy { DB.after_commit { SvgSprite.expire_cache } if svg_sprite_field? } private - JAVASCRIPT_TYPES = %w( - text/javascript - application/javascript - application/ecmascript - ) + JAVASCRIPT_TYPES = %w[text/javascript application/javascript application/ecmascript] def inline_javascript?(node) - if node['src'].present? + if node["src"].present? false - elsif node['type'].present? - JAVASCRIPT_TYPES.include?(node['type'].downcase) + elsif node["type"].present? + JAVASCRIPT_TYPES.include?(node["type"].downcase) else true end diff --git a/app/models/theme_modifier_set.rb b/app/models/theme_modifier_set.rb index 781ea42f51..1d1c8e59f2 100644 --- a/app/models/theme_modifier_set.rb +++ b/app/models/theme_modifier_set.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class ThemeModifierSet < ActiveRecord::Base - class ThemeModifierSetError < StandardError; end + class ThemeModifierSetError < StandardError + end belongs_to :theme @@ -26,12 +27,8 @@ class ThemeModifierSet < ActiveRecord::Base end after_save do - if saved_change_to_svg_icons? - SvgSprite.expire_cache - end - if saved_change_to_csp_extensions? - CSP::Extension.clear_theme_extensions_cache! - end + SvgSprite.expire_cache if saved_change_to_svg_icons? + CSP::Extension.clear_theme_extensions_cache! if saved_change_to_csp_extensions? end # Given the ids of multiple active themes / theme components, this function @@ -39,7 +36,11 @@ class ThemeModifierSet < ActiveRecord::Base def self.resolve_modifier_for_themes(theme_ids, modifier_name) return nil if !(config = self.modifiers[modifier_name]) - all_values = self.where(theme_id: theme_ids).where.not(modifier_name => nil).map { |s| s.public_send(modifier_name) } + all_values = + self + .where(theme_id: theme_ids) + .where.not(modifier_name => nil) + .map { |s| s.public_send(modifier_name) } case config[:type] when :boolean all_values.any? @@ -55,17 +56,21 @@ class ThemeModifierSet < ActiveRecord::Base return if array.nil? - array.map do |dimension| - parts = dimension.split("x") - next if parts.length != 2 - [parts[0].to_i, parts[1].to_i] - end.filter(&:present?) + array + .map do |dimension| + parts = dimension.split("x") + next if parts.length != 2 + [parts[0].to_i, parts[1].to_i] + end + .filter(&:present?) end def topic_thumbnail_sizes=(val) return write_attribute(:topic_thumbnail_sizes, val) if val.nil? return write_attribute(:topic_thumbnail_sizes, val) if !val.is_a?(Array) - return write_attribute(:topic_thumbnail_sizes, val) if !val.all? { |v| v.is_a?(Array) && v.length == 2 } + if !val.all? { |v| v.is_a?(Array) && v.length == 2 } + return write_attribute(:topic_thumbnail_sizes, val) + end super(val.map { |dim| "#{dim[0]}x#{dim[1]}" }) end @@ -77,7 +82,7 @@ class ThemeModifierSet < ActiveRecord::Base def self.load_modifiers hash = {} columns_hash.each do |column_name, info| - next if ["id", "theme_id"].include?(column_name) + next if %w[id theme_id].include?(column_name) type = nil if info.type == :string && info.array? @@ -85,7 +90,9 @@ class ThemeModifierSet < ActiveRecord::Base elsif info.type == :boolean && !info.array? type = :boolean else - raise ThemeModifierSetError "Invalid theme modifier column type" if ![:boolean, :string].include?(info.type) + if !%i[boolean string].include?(info.type) + raise ThemeModifierSetError "Invalid theme modifier column type" + end end hash[column_name.to_sym] = { type: type } diff --git a/app/models/top_menu_item.rb b/app/models/top_menu_item.rb index fad4c9b1ad..8334f73104 100644 --- a/app/models/top_menu_item.rb +++ b/app/models/top_menu_item.rb @@ -26,7 +26,7 @@ # item.specific_category # => "hardware" class TopMenuItem def initialize(value) - parts = value.split(',') + parts = value.split(",") @name = parts[0] @filter = initialize_filter(parts[1]) end @@ -38,18 +38,18 @@ class TopMenuItem end def has_specific_category? - name.split('/')[0] == 'category' + name.split("/")[0] == "category" end def specific_category - name.split('/')[1] + name.split("/")[1] end private def initialize_filter(value) if value - if value.start_with?('-') + if value.start_with?("-") value[1..-1] # all but the leading - else Rails.logger.warn "WARNING: found top_menu_item with invalid filter, ignoring '#{value}'..." diff --git a/app/models/top_topic.rb b/app/models/top_topic.rb index 3f09fda10e..975adc8ec7 100644 --- a/app/models/top_topic.rb +++ b/app/models/top_topic.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TopTopic < ActiveRecord::Base - belongs_to :topic # The top topics we want to refresh often @@ -16,13 +15,9 @@ class TopTopic < ActiveRecord::Base # We don't have to refresh these as often def self.refresh_older! - older_periods = periods - [:daily, :all] + older_periods = periods - %i[daily all] - transaction do - older_periods.each do |period| - update_counts_and_compute_scores_for(period) - end - end + transaction { older_periods.each { |period| update_counts_and_compute_scores_for(period) } } compute_top_score_for(:all) end @@ -33,16 +28,11 @@ class TopTopic < ActiveRecord::Base end def self.periods - @@periods ||= [:all, :yearly, :quarterly, :monthly, :weekly, :daily].freeze + @@periods ||= %i[all yearly quarterly monthly weekly daily].freeze end def self.sorted_periods - ascending_periods ||= Enum.new(daily: 1, - weekly: 2, - monthly: 3, - quarterly: 4, - yearly: 5, - all: 6) + ascending_periods ||= Enum.new(daily: 1, weekly: 2, monthly: 3, quarterly: 4, yearly: 5, all: 6) end def self.score_column_for_period(period) @@ -52,25 +42,26 @@ class TopTopic < ActiveRecord::Base def self.validate_period(period) if period.blank? || !periods.include?(period.to_sym) - raise Discourse::InvalidParameters.new("Invalid period. Valid periods are #{periods.join(", ")}") + raise Discourse::InvalidParameters.new( + "Invalid period. Valid periods are #{periods.join(", ")}", + ) end end private def self.sort_orders - @@sort_orders ||= [:posts, :views, :likes, :op_likes].freeze + @@sort_orders ||= %i[posts views likes op_likes].freeze end def self.update_counts_and_compute_scores_for(period) - sort_orders.each do |sort| - TopTopic.public_send("update_#{sort}_count_for", period) - end + sort_orders.each { |sort| TopTopic.public_send("update_#{sort}_count_for", period) } compute_top_score_for(period) end def self.remove_invisible_topics - DB.exec("WITH category_definition_topic_ids AS ( + DB.exec( + "WITH category_definition_topic_ids AS ( SELECT COALESCE(topic_id, 0) AS id FROM categories ), invisible_topic_ids AS ( SELECT id @@ -83,11 +74,13 @@ class TopTopic < ActiveRecord::Base ) DELETE FROM top_topics WHERE topic_id IN (SELECT id FROM invisible_topic_ids)", - private_message: Archetype::private_message) + private_message: Archetype.private_message, + ) end def self.add_new_visible_topics - DB.exec("WITH category_definition_topic_ids AS ( + DB.exec( + "WITH category_definition_topic_ids AS ( SELECT COALESCE(topic_id, 0) AS id FROM categories ), visible_topics AS ( SELECT t.id @@ -102,11 +95,13 @@ class TopTopic < ActiveRecord::Base ) INSERT INTO top_topics (topic_id) SELECT id FROM visible_topics", - private_message: Archetype::private_message) + private_message: Archetype.private_message, + ) end def self.update_posts_count_for(period) - sql = "SELECT topic_id, GREATEST(COUNT(*), 1) AS count + sql = + "SELECT topic_id, GREATEST(COUNT(*), 1) AS count FROM posts WHERE created_at >= :from AND deleted_at IS NULL @@ -119,7 +114,8 @@ class TopTopic < ActiveRecord::Base end def self.update_views_count_for(period) - sql = "SELECT topic_id, COUNT(*) AS count + sql = + "SELECT topic_id, COUNT(*) AS count FROM topic_views WHERE viewed_at >= :from GROUP BY topic_id" @@ -128,7 +124,8 @@ class TopTopic < ActiveRecord::Base end def self.update_likes_count_for(period) - sql = "SELECT topic_id, SUM(like_count) AS count + sql = + "SELECT topic_id, SUM(like_count) AS count FROM posts WHERE created_at >= :from AND deleted_at IS NULL @@ -140,7 +137,8 @@ class TopTopic < ActiveRecord::Base end def self.update_op_likes_count_for(period) - sql = "SELECT topic_id, like_count AS count + sql = + "SELECT topic_id, like_count AS count FROM posts WHERE created_at >= :from AND post_number = 1 @@ -152,18 +150,19 @@ class TopTopic < ActiveRecord::Base end def self.compute_top_score_for(period) - log_views_multiplier = SiteSetting.top_topics_formula_log_views_multiplier.to_f log_views_multiplier = 2 if log_views_multiplier == 0 first_post_likes_multiplier = SiteSetting.top_topics_formula_first_post_likes_multiplier.to_f first_post_likes_multiplier = 0.5 if first_post_likes_multiplier == 0 - least_likes_per_post_multiplier = SiteSetting.top_topics_formula_least_likes_per_post_multiplier.to_f + least_likes_per_post_multiplier = + SiteSetting.top_topics_formula_least_likes_per_post_multiplier.to_f least_likes_per_post_multiplier = 3 if least_likes_per_post_multiplier == 0 if period == :all - top_topics = "( + top_topics = + "( SELECT t.like_count all_likes_count, t.id topic_id, t.posts_count all_posts_count, @@ -213,22 +212,29 @@ class TopTopic < ActiveRecord::Base def self.start_of(period) case period - when :yearly then 1.year.ago - when :monthly then 1.month.ago - when :quarterly then 3.months.ago - when :weekly then 1.week.ago - when :daily then 1.day.ago + when :yearly + 1.year.ago + when :monthly + 1.month.ago + when :quarterly + 3.months.ago + when :weekly + 1.week.ago + when :daily + 1.day.ago end end def self.update_top_topics(period, sort, inner_join) - DB.exec("UPDATE top_topics + DB.exec( + "UPDATE top_topics SET #{period}_#{sort}_count = c.count FROM top_topics tt INNER JOIN (#{inner_join}) c ON tt.topic_id = c.topic_id WHERE tt.topic_id = top_topics.topic_id AND tt.#{period}_#{sort}_count <> c.count", - from: start_of(period)) + from: start_of(period), + ) end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 8c407873e1..b15bbc519f 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class Topic < ActiveRecord::Base - class UserExists < StandardError; end - class NotAllowed < StandardError; end + class UserExists < StandardError + end + class NotAllowed < StandardError + end include RateLimiter::OnCreateRecord include HasCustomFields include Trashable @@ -14,7 +16,7 @@ class Topic < ActiveRecord::Base self.ignored_columns = [ "avg_time", # TODO(2021-01-04): remove - "image_url" # TODO(2021-06-01): remove + "image_url", # TODO(2021-06-01): remove ] def_delegator :featured_users, :user_ids, :featured_user_ids @@ -37,7 +39,7 @@ class Topic < ActiveRecord::Base end def self.thumbnail_sizes - [ self.share_thumbnail_size ] + DiscoursePluginRegistry.topic_thumbnail_sizes + [self.share_thumbnail_size] + DiscoursePluginRegistry.topic_thumbnail_sizes end def thumbnail_job_redis_key(sizes) @@ -49,7 +51,9 @@ class Topic < ActiveRecord::Base return nil unless original.read_attribute(:width) && original.read_attribute(:height) thumbnail_sizes = Topic.thumbnail_sizes + extra_sizes - topic_thumbnails.filter { |record| thumbnail_sizes.include?([record.max_width, record.max_height]) } + topic_thumbnails.filter do |record| + thumbnail_sizes.include?([record.max_width, record.max_height]) + end end def thumbnail_info(enqueue_if_missing: false, extra_sizes: []) @@ -59,12 +63,12 @@ class Topic < ActiveRecord::Base infos = [] infos << { # Always add original - max_width: nil, - max_height: nil, - width: original.width, - height: original.height, - url: original.url - } + max_width: nil, + max_height: nil, + width: original.width, + height: original.height, + url: original.url, + } records = filtered_topic_thumbnails(extra_sizes: extra_sizes) @@ -76,15 +80,14 @@ class Topic < ActiveRecord::Base max_height: record.max_height, width: record.optimized_image&.width, height: record.optimized_image&.height, - url: record.optimized_image&.url + url: record.optimized_image&.url, } end thumbnail_sizes = Topic.thumbnail_sizes + extra_sizes - if SiteSetting.create_thumbnails && - enqueue_if_missing && - records.length < thumbnail_sizes.length && - Discourse.redis.set(thumbnail_job_redis_key(extra_sizes), 1, nx: true, ex: 1.minute) + if SiteSetting.create_thumbnails && enqueue_if_missing && + records.length < thumbnail_sizes.length && + Discourse.redis.set(thumbnail_job_redis_key(extra_sizes), 1, nx: true, ex: 1.minute) Jobs.enqueue(:generate_topic_thumbnails, { topic_id: id, extra_sizes: extra_sizes }) end @@ -106,19 +109,17 @@ class Topic < ActiveRecord::Base end def image_url(enqueue_if_missing: false) - thumbnail = topic_thumbnails.detect do |record| - record.max_width == Topic.share_thumbnail_size[0] && - record.max_height == Topic.share_thumbnail_size[1] - end + thumbnail = + topic_thumbnails.detect do |record| + record.max_width == Topic.share_thumbnail_size[0] && + record.max_height == Topic.share_thumbnail_size[1] + end - if thumbnail.nil? && - image_upload && - SiteSetting.create_thumbnails && - image_upload.filesize < SiteSetting.max_image_size_kb.kilobytes && - image_upload.read_attribute(:width) && - image_upload.read_attribute(:height) && - enqueue_if_missing && - Discourse.redis.set(thumbnail_job_redis_key([]), 1, nx: true, ex: 1.minute) + if thumbnail.nil? && image_upload && SiteSetting.create_thumbnails && + image_upload.filesize < SiteSetting.max_image_size_kb.kilobytes && + image_upload.read_attribute(:width) && image_upload.read_attribute(:height) && + enqueue_if_missing && + Discourse.redis.set(thumbnail_job_redis_key([]), 1, nx: true, ex: 1.minute) Jobs.enqueue(:generate_topic_thumbnails, { topic_id: id }) end @@ -169,34 +170,42 @@ class Topic < ActiveRecord::Base rate_limit :limit_topics_per_day rate_limit :limit_private_messages_per_day - validates :title, if: Proc.new { |t| t.new_record? || t.title_changed? }, - presence: true, - topic_title_length: true, - censored_words: true, - watched_words: true, - quality_title: { unless: :private_message? }, - max_emojis: true, - unique_among: { unless: Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, - message: :has_already_been_used, - allow_blank: true, - case_sensitive: false, - collection: Proc.new { |t| - SiteSetting.allow_duplicate_topic_titles_category? ? - Topic.listable_topics.where("category_id = ?", t.category_id) : - Topic.listable_topics - } - } + validates :title, + if: Proc.new { |t| t.new_record? || t.title_changed? }, + presence: true, + topic_title_length: true, + censored_words: true, + watched_words: true, + quality_title: { + unless: :private_message?, + }, + max_emojis: true, + unique_among: { + unless: + Proc.new { |t| (SiteSetting.allow_duplicate_topic_titles? || t.private_message?) }, + message: :has_already_been_used, + allow_blank: true, + case_sensitive: false, + collection: + Proc.new { |t| + if SiteSetting.allow_duplicate_topic_titles_category? + Topic.listable_topics.where("category_id = ?", t.category_id) + else + Topic.listable_topics + end + }, + } validates :category_id, presence: true, exclusion: { - in: Proc.new { [SiteSetting.uncategorized_category_id] } + in: Proc.new { [SiteSetting.uncategorized_category_id] }, }, - if: Proc.new { |t| - (t.new_record? || t.category_id_changed?) && - !SiteSetting.allow_uncategorized_topics && - (t.archetype.nil? || t.regular?) - } + if: + Proc.new { |t| + (t.new_record? || t.category_id_changed?) && + !SiteSetting.allow_uncategorized_topics && (t.archetype.nil? || t.regular?) + } validates :featured_link, allow_nil: true, url: true validate if: :featured_link do @@ -205,10 +214,22 @@ class Topic < ActiveRecord::Base end end - validates :external_id, allow_nil: true, uniqueness: { case_sensitive: false }, length: { maximum: EXTERNAL_ID_MAX_LENGTH }, format: { with: /\A[\w-]+\z/ } + validates :external_id, + allow_nil: true, + uniqueness: { + case_sensitive: false, + }, + length: { + maximum: EXTERNAL_ID_MAX_LENGTH, + }, + format: { + with: /\A[\w-]+\z/, + } before_validation do - self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[:title].empty? + self.title = TextCleaner.clean_title(TextSentinel.title_sentinel(title).text) if errors[ + :title + ].empty? self.featured_link = self.featured_link.strip.presence if self.featured_link end @@ -241,11 +262,11 @@ class Topic < ActiveRecord::Base has_one :published_page belongs_to :user - belongs_to :last_poster, class_name: 'User', foreign_key: :last_post_user_id - belongs_to :featured_user1, class_name: 'User', foreign_key: :featured_user1_id - belongs_to :featured_user2, class_name: 'User', foreign_key: :featured_user2_id - belongs_to :featured_user3, class_name: 'User', foreign_key: :featured_user3_id - belongs_to :featured_user4, class_name: 'User', foreign_key: :featured_user4_id + belongs_to :last_poster, class_name: "User", foreign_key: :last_post_user_id + belongs_to :featured_user1, class_name: "User", foreign_key: :featured_user1_id + belongs_to :featured_user2, class_name: "User", foreign_key: :featured_user2_id + belongs_to :featured_user3, class_name: "User", foreign_key: :featured_user3_id + belongs_to :featured_user4, class_name: "User", foreign_key: :featured_user4_id has_many :topic_users has_many :dismissed_topic_users @@ -257,12 +278,12 @@ class Topic < ActiveRecord::Base has_many :user_profiles has_one :user_warning - has_one :first_post, -> { where post_number: 1 }, class_name: 'Post' + has_one :first_post, -> { where post_number: 1 }, class_name: "Post" has_one :topic_search_data has_one :topic_embed, dependent: :destroy has_one :linked_topic, dependent: :destroy - belongs_to :image_upload, class_name: 'Upload' + belongs_to :image_upload, class_name: "Upload" has_many :topic_thumbnails, through: :image_upload # When we want to temporarily attach some data to a forum topic (usually before serialization) @@ -270,7 +291,7 @@ class Topic < ActiveRecord::Base attr_accessor :category_user_data attr_accessor :dismissed - attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code + attr_accessor :posters # TODO: can replace with posters_summary once we remove old list code attr_accessor :participants attr_accessor :topic_list attr_accessor :meta_data @@ -278,7 +299,7 @@ class Topic < ActiveRecord::Base attr_accessor :import_mode # set to true to optimize creation and save for imports # The regular order - scope :topic_list_order, -> { order('topics.bumped_at desc') } + scope :topic_list_order, -> { order("topics.bumped_at desc") } # Return private message topics scope :private_messages, -> { where(archetype: Archetype.private_message) } @@ -295,50 +316,57 @@ class Topic < ActiveRecord::Base JOIN group_users gu ON gu.user_id = :user_id AND gu.group_id = tg.group_id SQL - scope :private_messages_for_user, ->(user) do - private_messages.where( - "topics.id IN (#{PRIVATE_MESSAGES_SQL_USER}) + scope :private_messages_for_user, + ->(user) { + private_messages.where( + "topics.id IN (#{PRIVATE_MESSAGES_SQL_USER}) OR topics.id IN (#{PRIVATE_MESSAGES_SQL_GROUP})", - user_id: user.id - ) - end + user_id: user.id, + ) + } - scope :listable_topics, -> { where('topics.archetype <> ?', Archetype.private_message) } + scope :listable_topics, -> { where("topics.archetype <> ?", Archetype.private_message) } - scope :by_newest, -> { order('topics.created_at desc, topics.id desc') } + scope :by_newest, -> { order("topics.created_at desc, topics.id desc") } scope :visible, -> { where(visible: true) } - scope :created_since, lambda { |time_ago| where('topics.created_at > ?', time_ago) } + scope :created_since, lambda { |time_ago| where("topics.created_at > ?", time_ago) } scope :exclude_scheduled_bump_topics, -> { where.not(id: TopicTimer.scheduled_bump_topics) } - scope :secured, lambda { |guardian = nil| - ids = guardian.secure_category_ids if guardian + scope :secured, + lambda { |guardian = nil| + ids = guardian.secure_category_ids if guardian - # Query conditions - condition = if ids.present? - ["NOT read_restricted OR id IN (:cats)", cats: ids] - else - ["NOT read_restricted"] - end + # Query conditions + condition = + if ids.present? + ["NOT read_restricted OR id IN (:cats)", cats: ids] + else + ["NOT read_restricted"] + end - where("topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{condition[0]})", condition[1]) - } + where( + "topics.category_id IS NULL OR topics.category_id IN (SELECT id FROM categories WHERE #{condition[0]})", + condition[1], + ) + } - scope :in_category_and_subcategories, lambda { |category_id| - where("topics.category_id IN (?)", Category.subcategory_ids(category_id.to_i)) if category_id - } + scope :in_category_and_subcategories, + lambda { |category_id| + if category_id + where("topics.category_id IN (?)", Category.subcategory_ids(category_id.to_i)) + end + } - scope :with_subtype, ->(subtype) { where('topics.subtype = ?', subtype) } + scope :with_subtype, ->(subtype) { where("topics.subtype = ?", subtype) } attr_accessor :ignore_category_auto_close attr_accessor :skip_callbacks attr_accessor :advance_draft - before_create do - initialize_default_values - end + before_create { initialize_default_values } after_create do unless skip_callbacks @@ -348,13 +376,9 @@ class Topic < ActiveRecord::Base end before_save do - unless skip_callbacks - ensure_topic_has_a_category - end + ensure_topic_has_a_category unless skip_callbacks - if title_changed? - write_attribute(:fancy_title, Topic.fancy_title(title)) - end + write_attribute(:fancy_title, Topic.fancy_title(title)) if title_changed? if category_id_changed? || new_record? inherit_auto_close_from_category @@ -369,7 +393,8 @@ class Topic < ActiveRecord::Base ApplicationController.banner_json_cache.clear end - if tags_changed || saved_change_to_attribute?(:category_id) || saved_change_to_attribute?(:title) + if tags_changed || saved_change_to_attribute?(:category_id) || + saved_change_to_attribute?(:title) SearchIndexer.queue_post_reindex(self.id) if tags_changed @@ -418,11 +443,11 @@ class Topic < ActiveRecord::Base end def self.top_viewed(max = 10) - Topic.listable_topics.visible.secured.order('views desc').limit(max) + Topic.listable_topics.visible.secured.order("views desc").limit(max) end def self.recent(max = 10) - Topic.listable_topics.visible.secured.order('created_at desc').limit(max) + Topic.listable_topics.visible.secured.order("created_at desc").limit(max) end def self.count_exceeds_minimum? @@ -430,7 +455,11 @@ class Topic < ActiveRecord::Base end def best_post - posts.where(post_type: Post.types[:regular], user_deleted: false).order('score desc nulls last').limit(1).first + posts + .where(post_type: Post.types[:regular], user_deleted: false) + .order("score desc nulls last") + .limit(1) + .first end def self.has_flag_scope @@ -447,8 +476,11 @@ class Topic < ActiveRecord::Base # all users (in groups or directly targeted) that are going to get the pm def all_allowed_users - moderators_sql = " UNION #{User.moderators.to_sql}" if private_message? && (has_flags? || is_official_warning?) - User.from("(#{allowed_users.to_sql} UNION #{allowed_group_users.to_sql}#{moderators_sql}) as users") + moderators_sql = " UNION #{User.moderators.to_sql}" if private_message? && + (has_flags? || is_official_warning?) + User.from( + "(#{allowed_users.to_sql} UNION #{allowed_group_users.to_sql}#{moderators_sql}) as users", + ) end # Additional rate limits on topics: per day and private messages per day @@ -482,7 +514,11 @@ class Topic < ActiveRecord::Base if !new_record? && !Discourse.readonly_mode? # make sure data is set in table, this also allows us to change algorithm # by simply nulling this column - DB.exec("UPDATE topics SET fancy_title = :fancy_title where id = :id", id: self.id, fancy_title: fancy_title) + DB.exec( + "UPDATE topics SET fancy_title = :fancy_title where id = :id", + id: self.id, + fancy_title: fancy_title, + ) end end @@ -494,25 +530,34 @@ class Topic < ActiveRecord::Base opts = opts || {} period = ListController.best_period_for(since) - topics = Topic - .visible - .secured(Guardian.new(user)) - .joins("LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{user.id.to_i}") - .joins("LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id.to_i}") - .joins("LEFT OUTER JOIN users ON users.id = topics.user_id") - .where(closed: false, archived: false) - .where("COALESCE(topic_users.notification_level, 1) <> ?", TopicUser.notification_levels[:muted]) - .created_since(since) - .where('topics.created_at < ?', (SiteSetting.editing_grace_period || 0).seconds.ago) - .listable_topics - .includes(:category) + topics = + Topic + .visible + .secured(Guardian.new(user)) + .joins( + "LEFT OUTER JOIN topic_users ON topic_users.topic_id = topics.id AND topic_users.user_id = #{user.id.to_i}", + ) + .joins( + "LEFT OUTER JOIN category_users ON category_users.category_id = topics.category_id AND category_users.user_id = #{user.id.to_i}", + ) + .joins("LEFT OUTER JOIN users ON users.id = topics.user_id") + .where(closed: false, archived: false) + .where( + "COALESCE(topic_users.notification_level, 1) <> ?", + TopicUser.notification_levels[:muted], + ) + .created_since(since) + .where("topics.created_at < ?", (SiteSetting.editing_grace_period || 0).seconds.ago) + .listable_topics + .includes(:category) unless opts[:include_tl0] || user.user_option.try(:include_tl0_in_digests) topics = topics.where("COALESCE(users.trust_level, 0) > 0") end if !!opts[:top_order] - topics = topics.joins("LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id").order(<<~SQL) + topics = + topics.joins("LEFT OUTER JOIN top_topics ON top_topics.topic_id = topics.id").order(<<~SQL) COALESCE(topic_users.notification_level, 1) DESC, COALESCE(category_users.notification_level, 1) DESC, COALESCE(top_topics.#{TopTopic.score_column_for_period(period)}, 0) DESC, @@ -520,27 +565,34 @@ class Topic < ActiveRecord::Base SQL end - if opts[:limit] - topics = topics.limit(opts[:limit]) - end + topics = topics.limit(opts[:limit]) if opts[:limit] # Remove category topics category_topic_ids = Category.pluck(:topic_id).compact! - if category_topic_ids.present? - topics = topics.where("topics.id NOT IN (?)", category_topic_ids) - end + topics = topics.where("topics.id NOT IN (?)", category_topic_ids) if category_topic_ids.present? # Remove muted and shared draft categories - remove_category_ids = CategoryUser.where(user_id: user.id, notification_level: CategoryUser.notification_levels[:muted]).pluck(:category_id) + remove_category_ids = + CategoryUser.where( + user_id: user.id, + notification_level: CategoryUser.notification_levels[:muted], + ).pluck(:category_id) if SiteSetting.digest_suppress_categories.present? - topics = topics.where("topics.category_id NOT IN (?)", SiteSetting.digest_suppress_categories.split("|").map(&:to_i)) - end - if SiteSetting.shared_drafts_enabled? - remove_category_ids << SiteSetting.shared_drafts_category + topics = + topics.where( + "topics.category_id NOT IN (?)", + SiteSetting.digest_suppress_categories.split("|").map(&:to_i), + ) end + remove_category_ids << SiteSetting.shared_drafts_category if SiteSetting.shared_drafts_enabled? if remove_category_ids.present? remove_category_ids.uniq! - topics = topics.where("topic_users.notification_level != ? OR topics.category_id NOT IN (?)", TopicUser.notification_levels[:muted], remove_category_ids) + topics = + topics.where( + "topic_users.notification_level != ? OR topics.category_id NOT IN (?)", + TopicUser.notification_levels[:muted], + remove_category_ids, + ) end # Remove muted tags @@ -548,9 +600,12 @@ class Topic < ActiveRecord::Base unless muted_tag_ids.empty? # If multiple tags per topic, include topics with tags that aren't muted, # and don't forget untagged topics. - topics = topics.where( - "EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id AND tag_id NOT IN (?) ) - OR NOT EXISTS (SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id)", muted_tag_ids) + topics = + topics.where( + "EXISTS ( SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id AND tag_id NOT IN (?) ) + OR NOT EXISTS (SELECT 1 FROM topic_tags WHERE topic_tags.topic_id = topics.id)", + muted_tag_ids, + ) end topics @@ -585,10 +640,23 @@ class Topic < ActiveRecord::Base ((Time.zone.now - created_at) / 1.minute).round end - def self.listable_count_per_day(start_date, end_date, category_id = nil, include_subcategories = false) - result = listable_topics.where("topics.created_at >= ? AND topics.created_at <= ?", start_date, end_date) - result = result.group('date(topics.created_at)').order('date(topics.created_at)') - result = result.where(category_id: include_subcategories ? Category.subcategory_ids(category_id) : category_id) if category_id + def self.listable_count_per_day( + start_date, + end_date, + category_id = nil, + include_subcategories = false + ) + result = + listable_topics.where( + "topics.created_at >= ? AND topics.created_at <= ?", + start_date, + end_date, + ) + result = result.group("date(topics.created_at)").order("date(topics.created_at)") + result = + result.where( + category_id: include_subcategories ? Category.subcategory_ids(category_id) : category_id, + ) if category_id result.count end @@ -613,20 +681,16 @@ class Topic < ActiveRecord::Base return [] if search_data.blank? - tsquery = Search.set_tsquery_weight_filter(search_data, 'A') + tsquery = Search.set_tsquery_weight_filter(search_data, "A") if raw.present? - cooked = SearchIndexer::HtmlScrubber.scrub( - PrettyText.cook(raw[0...MAX_SIMILAR_BODY_LENGTH].strip) - ) + cooked = + SearchIndexer::HtmlScrubber.scrub(PrettyText.cook(raw[0...MAX_SIMILAR_BODY_LENGTH].strip)) prepared_data = cooked.present? && Search.prepare_data(cooked) if prepared_data.present? - raw_tsquery = Search.set_tsquery_weight_filter( - prepared_data, - 'B' - ) + raw_tsquery = Search.set_tsquery_weight_filter(prepared_data, "B") tsquery = "#{tsquery} & #{raw_tsquery}" end @@ -636,46 +700,62 @@ class Topic < ActiveRecord::Base guardian = Guardian.new(user) - excluded_category_ids_sql = Category.secured(guardian).where(search_priority: Searchable::PRIORITIES[:ignore]).select(:id).to_sql + excluded_category_ids_sql = + Category + .secured(guardian) + .where(search_priority: Searchable::PRIORITIES[:ignore]) + .select(:id) + .to_sql - if user - excluded_category_ids_sql = <<~SQL + excluded_category_ids_sql = <<~SQL if user #{excluded_category_ids_sql} UNION #{CategoryUser.muted_category_ids_query(user, include_direct: true).select("categories.id").to_sql} SQL - end - candidates = Topic - .visible - .listable_topics - .secured(guardian) - .joins("JOIN topic_search_data s ON topics.id = s.topic_id") - .joins("LEFT JOIN categories c ON topics.id = c.topic_id") - .where("search_data @@ #{tsquery}") - .where("c.topic_id IS NULL") - .where("topics.category_id NOT IN (#{excluded_category_ids_sql})") - .order("ts_rank(search_data, #{tsquery}) DESC") - .limit(SiteSetting.max_similar_results * 3) + candidates = + Topic + .visible + .listable_topics + .secured(guardian) + .joins("JOIN topic_search_data s ON topics.id = s.topic_id") + .joins("LEFT JOIN categories c ON topics.id = c.topic_id") + .where("search_data @@ #{tsquery}") + .where("c.topic_id IS NULL") + .where("topics.category_id NOT IN (#{excluded_category_ids_sql})") + .order("ts_rank(search_data, #{tsquery}) DESC") + .limit(SiteSetting.max_similar_results * 3) candidate_ids = candidates.pluck(:id) return [] if candidate_ids.blank? - similars = Topic - .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") - .where("topics.id IN (?)", candidate_ids) - .order("similarity DESC") - .limit(SiteSetting.max_similar_results) + similars = + Topic + .joins("JOIN posts AS p ON p.topic_id = topics.id AND p.post_number = 1") + .where("topics.id IN (?)", candidate_ids) + .order("similarity DESC") + .limit(SiteSetting.max_similar_results) if raw.present? - similars - .select(DB.sql_fragment("topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb", title: title, raw: raw)) - .where("similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2", title: title, raw: raw) + similars.select( + DB.sql_fragment( + "topics.*, similarity(topics.title, :title) + similarity(p.raw, :raw) AS similarity, p.cooked AS blurb", + title: title, + raw: raw, + ), + ).where( + "similarity(topics.title, :title) + similarity(p.raw, :raw) > 0.2", + title: title, + raw: raw, + ) else - similars - .select(DB.sql_fragment("topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb", title: title)) - .where("similarity(topics.title, :title) > 0.2", title: title) + similars.select( + DB.sql_fragment( + "topics.*, similarity(topics.title, :title) AS similarity, p.cooked AS blurb", + title: title, + ), + ).where("similarity(topics.title, :title) > 0.2", title: title) end end @@ -683,30 +763,34 @@ class Topic < ActiveRecord::Base TopicStatusUpdater.new(self, user).update!(status, enabled, opts) DiscourseEvent.trigger(:topic_status_updated, self, status, enabled) - if status == 'closed' + if status == "closed" StaffActionLogger.new(user).log_topic_closed(self, closed: enabled) - elsif status == 'archived' + elsif status == "archived" StaffActionLogger.new(user).log_topic_archived(self, archived: enabled) end if enabled && private_message? && status.to_s["closed"] group_ids = user.groups.pluck(:id) if group_ids.present? - allowed_group_ids = self.allowed_groups - .where('topic_allowed_groups.group_id IN (?)', group_ids).pluck(:id) - allowed_group_ids.each do |id| - GroupArchivedMessage.archive!(id, self) - end + allowed_group_ids = + self.allowed_groups.where("topic_allowed_groups.group_id IN (?)", group_ids).pluck(:id) + allowed_group_ids.each { |id| GroupArchivedMessage.archive!(id, self) } end end end # Atomically creates the next post number def self.next_post_number(topic_id, opts = {}) - highest = DB.query_single("SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", topic_id).first.to_i + highest = + DB + .query_single( + "SELECT coalesce(max(post_number),0) AS max FROM posts WHERE topic_id = ?", + topic_id, + ) + .first + .to_i if opts[:whisper] - result = DB.query_single(<<~SQL, highest, topic_id) UPDATE topics SET highest_staff_post_number = ? + 1 @@ -715,11 +799,9 @@ class Topic < ActiveRecord::Base SQL result.first.to_i - else - reply_sql = opts[:reply] ? ", reply_count = reply_count + 1" : "" - posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : "" + posts_sql = opts[:post] ? ", posts_count = posts_count + 1" : "" result = DB.query_single(<<~SQL, highest: highest, topic_id: topic_id) UPDATE topics @@ -814,7 +896,8 @@ class Topic < ActiveRecord::Base archetype = Topic.where(id: topic_id).pluck_first(:archetype) # ignore small_action replies for private messages - post_type = archetype == Archetype.private_message ? " AND post_type <> #{Post.types[:small_action]}" : '' + post_type = + archetype == Archetype.private_message ? " AND post_type <> #{Post.types[:small_action]}" : "" result = DB.query_single(<<~SQL, topic_id: topic_id) UPDATE topics @@ -875,7 +958,10 @@ class Topic < ActiveRecord::Base def changed_to_category(new_category) return true if new_category.blank? || Category.exists?(topic_id: id) - return false if new_category.id == SiteSetting.uncategorized_category_id && !SiteSetting.allow_uncategorized_topics + if new_category.id == SiteSetting.uncategorized_category_id && + !SiteSetting.allow_uncategorized_topics + return false + end Topic.transaction do old_category = category @@ -884,9 +970,7 @@ class Topic < ActiveRecord::Base self.update_attribute(:category_id, new_category.id) if old_category - Category - .where(id: old_category.id) - .update_all("topic_count = topic_count - 1") + Category.where(id: old_category.id).update_all("topic_count = topic_count - 1") end # when a topic changes category we may have to start watching it @@ -897,7 +981,11 @@ class Topic < ActiveRecord::Base if !SiteSetting.disable_category_edit_notifications && (post = self.ordered_posts.first) notified_user_ids = [post.user_id, post.last_editor_id].uniq DB.after_commit do - Jobs.enqueue(:notify_category_change, post_id: post.id, notified_user_ids: notified_user_ids) + Jobs.enqueue( + :notify_category_change, + post_id: post.id, + notified_user_ids: notified_user_ids, + ) end end @@ -905,16 +993,16 @@ class Topic < ActiveRecord::Base # linked to posts secure/not secure depending on whether the # category is private. this is only done if the category # has actually changed to avoid noise. - DB.after_commit do - Jobs.enqueue(:update_topic_upload_security, topic_id: self.id) - end + DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: self.id) } end Category.where(id: new_category.id).update_all("topic_count = topic_count + 1") if Topic.update_featured_topics != false CategoryFeaturedTopic.feature_topics_for(old_category) unless @import_mode - CategoryFeaturedTopic.feature_topics_for(new_category) unless @import_mode || old_category.try(:id) == new_category.id + unless @import_mode || old_category.try(:id) == new_category.id + CategoryFeaturedTopic.feature_topics_for(new_category) + end end end @@ -924,11 +1012,12 @@ class Topic < ActiveRecord::Base def add_small_action(user, action_code, who = nil, opts = {}) custom_fields = {} custom_fields["action_code_who"] = who if who.present? - opts = opts.merge( - post_type: Post.types[:small_action], - action_code: action_code, - custom_fields: custom_fields - ) + opts = + opts.merge( + post_type: Post.types[:small_action], + action_code: action_code, + custom_fields: custom_fields, + ) add_moderator_post(user, nil, opts) end @@ -936,22 +1025,27 @@ class Topic < ActiveRecord::Base def add_moderator_post(user, text, opts = nil) opts ||= {} new_post = nil - creator = PostCreator.new(user, - raw: text, - post_type: opts[:post_type] || Post.types[:moderator_action], - action_code: opts[:action_code], - no_bump: opts[:bump].blank?, - topic_id: self.id, - silent: opts[:silent], - skip_validations: true, - custom_fields: opts[:custom_fields], - import_mode: opts[:import_mode]) + creator = + PostCreator.new( + user, + raw: text, + post_type: opts[:post_type] || Post.types[:moderator_action], + action_code: opts[:action_code], + no_bump: opts[:bump].blank?, + topic_id: self.id, + silent: opts[:silent], + skip_validations: true, + custom_fields: opts[:custom_fields], + import_mode: opts[:import_mode], + ) if (new_post = creator.create) && new_post.present? increment!(:moderator_posts_count) if new_post.persisted? # If we are moving posts, we want to insert the moderator post where the previous posts were # in the stream, not at the end. - new_post.update!(post_number: opts[:post_number], sort_order: opts[:post_number]) if opts[:post_number].present? + if opts[:post_number].present? + new_post.update!(post_number: opts[:post_number], sort_order: opts[:post_number]) + end # Grab any links that are present TopicLink.extract_from(new_post) @@ -1016,14 +1110,16 @@ class Topic < ActiveRecord::Base def reached_recipients_limit? return false unless private_message? - topic_allowed_users.count + topic_allowed_groups.count >= SiteSetting.max_allowed_message_recipients + topic_allowed_users.count + topic_allowed_groups.count >= + SiteSetting.max_allowed_message_recipients end def invite_group(user, group) TopicAllowedGroup.create!(topic_id: self.id, group_id: group.id) self.allowed_groups.reload - last_post = self.posts.order('post_number desc').where('not hidden AND posts.deleted_at IS NULL').first + last_post = + self.posts.order("post_number desc").where("not hidden AND posts.deleted_at IS NULL").first if last_post Jobs.enqueue(:post_alert, post_id: last_post.id) add_small_action(user, "invited_group", group.name) @@ -1045,12 +1141,14 @@ class Topic < ActiveRecord::Base topic_allowed_users.user_id != :op_user_id ) SQL - User.where([ - allowed_user_where_clause, - { group_id: group.id, topic_id: self.id, op_user_id: self.user_id } - ]).find_each do |allowed_user| - remove_allowed_user(Discourse.system_user, allowed_user) - end + User + .where( + [ + allowed_user_where_clause, + { group_id: group.id, topic_id: self.id, op_user_id: self.user_id }, + ], + ) + .find_each { |allowed_user| remove_allowed_user(Discourse.system_user, allowed_user) } true end @@ -1068,11 +1166,11 @@ class Topic < ActiveRecord::Base raise NotAllowed.new(I18n.t("not_accepting_pms", username: target_user.username)) end - if TopicUser - .where(topic: self, - user: target_user, - notification_level: TopicUser.notification_levels[:muted]) - .exists? + if TopicUser.where( + topic: self, + user: target_user, + notification_level: TopicUser.notification_levels[:muted], + ).exists? raise NotAllowed.new(I18n.t("topic_invite.muted_topic")) end @@ -1080,7 +1178,10 @@ class Topic < ActiveRecord::Base raise NotAllowed.new(I18n.t("topic_invite.receiver_does_not_allow_pm")) end - if UserCommScreener.new(acting_user: target_user, target_user_ids: invited_by.id).disallowing_pms_from_actor?(invited_by.id) + if UserCommScreener.new( + acting_user: target_user, + target_user_ids: invited_by.id, + ).disallowing_pms_from_actor?(invited_by.id) raise NotAllowed.new(I18n.t("topic_invite.sender_does_not_allow_pm")) end @@ -1090,12 +1191,13 @@ class Topic < ActiveRecord::Base !!invite_to_topic(invited_by, target_user, group_ids, guardian) end elsif username_or_email =~ /^.+@.+$/ && guardian.can_invite_via_email?(self) - !!Invite.generate(invited_by, + !!Invite.generate( + invited_by, email: username_or_email, topic: self, group_ids: group_ids, custom_message: custom_message, - invite_to_topic: true + invite_to_topic: true, ) end end @@ -1106,7 +1208,9 @@ class Topic < ActiveRecord::Base def grant_permission_to_user(lower_email) user = User.find_by_email(lower_email) - topic_allowed_users.create!(user_id: user.id) unless topic_allowed_users.exists?(user_id: user.id) + unless topic_allowed_users.exists?(user_id: user.id) + topic_allowed_users.create!(user_id: user.id) + end end def max_post_number @@ -1114,15 +1218,18 @@ class Topic < ActiveRecord::Base end def move_posts(moved_by, post_ids, opts) - post_mover = PostMover.new(self, moved_by, post_ids, move_to_pm: opts[:archetype].present? && opts[:archetype] == "private_message") + post_mover = + PostMover.new( + self, + moved_by, + post_ids, + move_to_pm: opts[:archetype].present? && opts[:archetype] == "private_message", + ) if opts[:destination_topic_id] topic = post_mover.to_topic(opts[:destination_topic_id], participants: opts[:participants]) - DiscourseEvent.trigger(:topic_merged, - post_mover.original_topic, - post_mover.destination_topic - ) + DiscourseEvent.trigger(:topic_merged, post_mover.original_topic, post_mover.destination_topic) topic elsif opts[:title] @@ -1142,10 +1249,7 @@ class Topic < ActiveRecord::Base def update_action_counts update_column( :like_count, - Post - .where.not(post_type: Post.types[:whisper]) - .where(topic_id: id) - .sum(:like_count) + Post.where.not(post_type: Post.types[:whisper]).where(topic_id: id).sum(:like_count), ) end @@ -1159,26 +1263,26 @@ class Topic < ActiveRecord::Base def make_banner!(user, bannered_until = nil) if bannered_until - bannered_until = begin - Time.parse(bannered_until) - rescue ArgumentError - raise Discourse::InvalidParameters.new(:bannered_until) - end + bannered_until = + begin + Time.parse(bannered_until) + rescue ArgumentError + raise Discourse::InvalidParameters.new(:bannered_until) + end end # only one banner at the same time previous_banner = Topic.where(archetype: Archetype.banner).first previous_banner.remove_banner!(user) if previous_banner.present? - UserProfile.where("dismissed_banner_key IS NOT NULL") - .update_all(dismissed_banner_key: nil) + UserProfile.where("dismissed_banner_key IS NOT NULL").update_all(dismissed_banner_key: nil) self.archetype = Archetype.banner self.bannered_until = bannered_until self.add_small_action(user, "banner.enabled") self.save - MessageBus.publish('/site/banner', banner) + MessageBus.publish("/site/banner", banner) Jobs.cancel_scheduled_job(:remove_banner, topic_id: self.id) Jobs.enqueue_at(bannered_until, :remove_banner, topic_id: self.id) if bannered_until @@ -1190,7 +1294,7 @@ class Topic < ActiveRecord::Base self.add_small_action(user, "banner.disabled") self.save - MessageBus.publish('/site/banner', nil) + MessageBus.publish("/site/banner", nil) Jobs.cancel_scheduled_job(:remove_banner, topic_id: self.id) end @@ -1198,24 +1302,18 @@ class Topic < ActiveRecord::Base def banner post = self.ordered_posts.first - { - html: post.cooked, - key: self.id, - url: self.url - } + { html: post.cooked, key: self.id, url: self.url } end cattr_accessor :slug_computed_callbacks self.slug_computed_callbacks = [] def slug_for_topic(title) - return '' unless title.present? + return "" unless title.present? slug = Slug.for(title) # this is a hook for plugins that need to modify the generated slug - self.class.slug_computed_callbacks.each do |callback| - slug = callback.call(self, slug, title) - end + self.class.slug_computed_callbacks.each { |callback| slug = callback.call(self, slug, title) } slug end @@ -1223,7 +1321,7 @@ class Topic < ActiveRecord::Base # Even if the slug column in the database is null, topic.slug will return something: def slug unless slug = read_attribute(:slug) - return '' unless title.present? + return "" unless title.present? slug = slug_for_topic(title) if new_record? write_attribute(:slug, slug) @@ -1295,17 +1393,18 @@ class Topic < ActiveRecord::Base def update_pinned(status, global = false, pinned_until = nil) if pinned_until - pinned_until = begin - Time.parse(pinned_until) - rescue ArgumentError - raise Discourse::InvalidParameters.new(:pinned_until) - end + pinned_until = + begin + Time.parse(pinned_until) + rescue ArgumentError + raise Discourse::InvalidParameters.new(:pinned_until) + end end update_columns( pinned_at: status ? Time.zone.now : nil, pinned_globally: global, - pinned_until: pinned_until + pinned_until: pinned_until, ) Jobs.cancel_scheduled_job(:unpin_topic, topic_id: self.id) @@ -1321,17 +1420,19 @@ class Topic < ActiveRecord::Base end def muted?(user) - if user && user.id - notifier.muted?(user.id) - end + notifier.muted?(user.id) if user && user.id end def self.ensure_consistency! # unpin topics that might have been missed - Topic.where('pinned_until < ?', Time.now).update_all(pinned_at: nil, pinned_globally: false, pinned_until: nil) - Topic.where('bannered_until < ?', Time.now).find_each do |topic| - topic.remove_banner!(Discourse.system_user) - end + Topic.where("pinned_until < ?", Time.now).update_all( + pinned_at: nil, + pinned_globally: false, + pinned_until: nil, + ) + Topic + .where("bannered_until < ?", Time.now) + .find_each { |topic| topic.remove_banner!(Discourse.system_user) } end def inherit_slow_mode_from_category @@ -1343,11 +1444,8 @@ class Topic < ActiveRecord::Base def inherit_auto_close_from_category(timer_type: :close) auto_close_hours = self.category&.auto_close_hours - if self.open? && - !@ignore_category_auto_close && - auto_close_hours.present? && - public_topic_timer&.execute_at.blank? - + if self.open? && !@ignore_category_auto_close && auto_close_hours.present? && + public_topic_timer&.execute_at.blank? based_on_last_post = self.category.auto_close_based_on_last_post duration_minutes = based_on_last_post ? auto_close_hours * 60 : nil @@ -1374,7 +1472,7 @@ class Topic < ActiveRecord::Base auto_close_time, by_user: Discourse.system_user, based_on_last_post: based_on_last_post, - duration_minutes: duration_minutes + duration_minutes: duration_minutes, ) end end @@ -1407,8 +1505,18 @@ class Topic < ActiveRecord::Base # * duration_minutes: The duration of the timer in minutes, which is used if the timer is based # on the last post or if the timer type is delete_replies. # * silent: Affects whether the close topic timer status change will be silent or not. - def set_or_create_timer(status_type, time, by_user: nil, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id, duration_minutes: nil, silent: nil) - return delete_topic_timer(status_type, by_user: by_user) if time.blank? && duration_minutes.blank? + def set_or_create_timer( + status_type, + time, + by_user: nil, + based_on_last_post: false, + category_id: SiteSetting.uncategorized_category_id, + duration_minutes: nil, + silent: nil + ) + if time.blank? && duration_minutes.blank? + return delete_topic_timer(status_type, by_user: by_user) + end duration_minutes = duration_minutes ? duration_minutes.to_i : 0 public_topic_timer = !!TopicTimer.public_types[status_type] @@ -1427,21 +1535,30 @@ class Topic < ActiveRecord::Base if topic_timer.based_on_last_post if duration_minutes > 0 - last_post_created_at = self.ordered_posts.last.present? ? self.ordered_posts.last.created_at : time_now + last_post_created_at = + self.ordered_posts.last.present? ? self.ordered_posts.last.created_at : time_now topic_timer.duration_minutes = duration_minutes topic_timer.execute_at = last_post_created_at + duration_minutes.minutes topic_timer.created_at = last_post_created_at end elsif topic_timer.status_type == TopicTimer.types[:delete_replies] if duration_minutes > 0 - first_reply_created_at = (self.ordered_posts.where("post_number > 1").minimum(:created_at) || time_now) + first_reply_created_at = + (self.ordered_posts.where("post_number > 1").minimum(:created_at) || time_now) topic_timer.duration_minutes = duration_minutes topic_timer.execute_at = first_reply_created_at + duration_minutes.minutes topic_timer.created_at = first_reply_created_at end else utc = Time.find_zone("UTC") - is_float = (Float(time) rescue nil) + is_float = + ( + begin + Float(time) + rescue StandardError + nil + end + ) if is_float num_hours = time.to_f @@ -1458,7 +1575,14 @@ class Topic < ActiveRecord::Base if by_user&.staff? || by_user&.trust_level == TrustLevel[4] topic_timer.user = by_user else - topic_timer.user ||= (self.user.staff? || self.user.trust_level == TrustLevel[4] ? self.user : Discourse.system_user) + topic_timer.user ||= + ( + if self.user.staff? || self.user.trust_level == TrustLevel[4] + self.user + else + Discourse.system_user + end + ) end if self.persisted? @@ -1490,9 +1614,8 @@ class Topic < ActiveRecord::Base end def secure_group_ids - @secure_group_ids ||= if self.category && self.category.read_restricted? - self.category.secure_group_ids - end + @secure_group_ids ||= + (self.category.secure_group_ids if self.category && self.category.read_restricted?) end def has_topic_embed? @@ -1584,7 +1707,10 @@ class Topic < ActiveRecord::Base end def self.time_to_first_response_per_day(start_date, end_date, opts = {}) - time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, opts.merge(start_date: start_date, end_date: end_date)) + time_to_first_response( + TIME_TO_FIRST_RESPONSE_SQL, + opts.merge(start_date: start_date, end_date: end_date), + ) end def self.time_to_first_response_total(opts = nil) @@ -1606,7 +1732,12 @@ class Topic < ActiveRecord::Base ORDER BY tt.created_at SQL - def self.with_no_response_per_day(start_date, end_date, category_id = nil, include_subcategories = nil) + def self.with_no_response_per_day( + start_date, + end_date, + category_id = nil, + include_subcategories = nil + ) builder = DB.build(WITH_NO_RESPONSE_SQL) builder.where("t.created_at >= :start_date", start_date: start_date) if start_date builder.where("t.created_at < :end_date", end_date: end_date) if end_date @@ -1662,9 +1793,7 @@ class Topic < ActiveRecord::Base def update_excerpt(excerpt) update_column(:excerpt, excerpt) - if archetype == "banner" - ApplicationController.banner_json_cache.clear - end + ApplicationController.banner_json_cache.clear if archetype == "banner" end def pm_with_non_human_user? @@ -1692,9 +1821,9 @@ class Topic < ActiveRecord::Base def self.private_message_topics_count_per_day(start_date, end_date, topic_subtype) private_messages .with_subtype(topic_subtype) - .where('topics.created_at >= ? AND topics.created_at <= ?', start_date, end_date) - .group('date(topics.created_at)') - .order('date(topics.created_at)') + .where("topics.created_at >= ? AND topics.created_at <= ?", start_date, end_date) + .group("date(topics.created_at)") + .order("date(topics.created_at)") .count end @@ -1703,11 +1832,12 @@ class Topic < ActiveRecord::Base end def reset_bumped_at - post = ordered_posts.where( - user_deleted: false, - hidden: false, - post_type: Post.types[:regular] - ).last || first_post + post = + ordered_posts.where( + user_deleted: false, + hidden: false, + post_type: Post.types[:regular], + ).last || first_post self.bumped_at = post.created_at self.save(validate: false) @@ -1716,21 +1846,26 @@ class Topic < ActiveRecord::Base def auto_close_threshold_reached? return if user&.staff? - scores = ReviewableScore.pending - .joins(:reviewable) - .where('reviewable_scores.score >= ?', Reviewable.min_score_for_priority) - .where('reviewables.topic_id = ?', self.id) - .pluck('COUNT(DISTINCT reviewable_scores.user_id), COALESCE(SUM(reviewable_scores.score), 0.0)') - .first + scores = + ReviewableScore + .pending + .joins(:reviewable) + .where("reviewable_scores.score >= ?", Reviewable.min_score_for_priority) + .where("reviewables.topic_id = ?", self.id) + .pluck( + "COUNT(DISTINCT reviewable_scores.user_id), COALESCE(SUM(reviewable_scores.score), 0.0)", + ) + .first - scores[0] >= SiteSetting.num_flaggers_to_close_topic && scores[1] >= Reviewable.score_to_auto_close_topic + scores[0] >= SiteSetting.num_flaggers_to_close_topic && + scores[1] >= Reviewable.score_to_auto_close_topic end def update_category_topic_count_by(num) if category_id.present? Category - .where('id = ?', category_id) - .where('topic_id != ? OR topic_id IS NULL', self.id) + .where("id = ?", category_id) + .where("topic_id != ? OR topic_id IS NULL", self.id) .update_all("topic_count = topic_count + #{num.to_i}") end end @@ -1749,40 +1884,46 @@ class Topic < ActiveRecord::Base # TODO(martin) Look at improving this N1, it will just get slower the # more replies/incoming emails there are for the topic. - self.incoming_email.where("created_at <= ?", received_before).each do |incoming_email| - to_addresses = incoming_email.to_addresses_split - cc_addresses = incoming_email.cc_addresses_split - combined_addresses = [to_addresses, cc_addresses].flatten + self + .incoming_email + .where("created_at <= ?", received_before) + .each do |incoming_email| + to_addresses = incoming_email.to_addresses_split + cc_addresses = incoming_email.cc_addresses_split + combined_addresses = [to_addresses, cc_addresses].flatten - # We only care about the emails addressed to the group or CC'd to the - # group if the group is present. If combined addresses is empty we do - # not need to do this check, and instead can proceed on to adding the - # from address. - # - # Will not include test1@gmail.com if the only IncomingEmail - # is: - # - # from: test1@gmail.com - # to: test+support@discoursemail.com - # - # Because we don't care about the from addresses and also the to address - # is not the email_username, which will be something like test1@gmail.com. - if group.present? && combined_addresses.any? - next if combined_addresses.none? { |address| address =~ group.email_username_regex } + # We only care about the emails addressed to the group or CC'd to the + # group if the group is present. If combined addresses is empty we do + # not need to do this check, and instead can proceed on to adding the + # from address. + # + # Will not include test1@gmail.com if the only IncomingEmail + # is: + # + # from: test1@gmail.com + # to: test+support@discoursemail.com + # + # Because we don't care about the from addresses and also the to address + # is not the email_username, which will be something like test1@gmail.com. + if group.present? && combined_addresses.any? + next if combined_addresses.none? { |address| address =~ group.email_username_regex } + end + + email_addresses.add(incoming_email.from_address) + email_addresses.merge(combined_addresses) end - email_addresses.add(incoming_email.from_address) - email_addresses.merge(combined_addresses) - end - - email_addresses.subtract([nil, '']) + email_addresses.subtract([nil, ""]) email_addresses.delete(group.email_username) if group.present? email_addresses.to_a end def create_invite_notification!(target_user, notification_type, invited_by, post_number: 1) - if UserCommScreener.new(acting_user: invited_by, target_user_ids: target_user.id).ignoring_or_muting_actor?(target_user.id) + if UserCommScreener.new( + acting_user: invited_by, + target_user_ids: target_user.id, + ).ignoring_or_muting_actor?(target_user.id) raise NotAllowed.new(I18n.t("not_accepting_pms", username: target_user.username)) end @@ -1794,8 +1935,8 @@ class Topic < ActiveRecord::Base topic_title: self.title, display_username: invited_by.username, original_user_id: user.id, - original_username: user.username - }.to_json + original_username: user.username, + }.to_json, ) end @@ -1804,28 +1945,35 @@ class Topic < ActiveRecord::Base invited_by, "topic-invitations-per-day", SiteSetting.max_topic_invitations_per_day, - 1.day.to_i + 1.day.to_i, ).performed! RateLimiter.new( invited_by, "topic-invitations-per-minute", SiteSetting.max_topic_invitations_per_minute, - 1.day.to_i + 1.day.to_i, ).performed! end def cannot_permanently_delete_reason(user) - all_posts_count = Post.with_deleted - .where(topic_id: self.id) - .where(post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]]) - .count + all_posts_count = + Post + .with_deleted + .where(topic_id: self.id) + .where( + post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]], + ) + .count if posts_count > 0 || all_posts_count > 1 - I18n.t('post.cannot_permanently_delete.many_posts') + I18n.t("post.cannot_permanently_delete.many_posts") elsif self.deleted_by_id == user&.id && self.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago - time_left = RateLimiter.time_left(Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i) - I18n.t('post.cannot_permanently_delete.wait_or_different_admin', time_left: time_left) + time_left = + RateLimiter.time_left( + Post::PERMANENT_DELETE_TIMER.to_i - Time.zone.now.to_i + self.deleted_at.to_i, + ) + I18n.t("post.cannot_permanently_delete.wait_or_different_admin", time_left: time_left) end end @@ -1855,9 +2003,11 @@ class Topic < ActiveRecord::Base when :liked, :unliked stats = { like_count: topic.like_count } when :created, :destroyed, :deleted, :recovered - stats = { posts_count: topic.posts_count, - last_posted_at: topic.last_posted_at.as_json, - last_poster: BasicUserSerializer.new(topic.last_poster, root: false).as_json } + stats = { + posts_count: topic.posts_count, + last_posted_at: topic.last_posted_at.as_json, + last_poster: BasicUserSerializer.new(topic.last_poster, root: false).as_json, + } else stats = nil end @@ -1866,11 +2016,7 @@ class Topic < ActiveRecord::Base secure_audience = topic.secure_audience_publish_messages if secure_audience[:user_ids] != [] && secure_audience[:group_ids] != [] - message = stats.merge({ - id: topic_id, - updated_at: Time.now, - type: :stats, - }) + message = stats.merge({ id: topic_id, updated_at: Time.now, type: :stats }) MessageBus.publish("/topic/#{topic_id}", message, opts.merge(secure_audience)) end end @@ -1880,15 +2026,15 @@ class Topic < ActiveRecord::Base def invite_to_private_message(invited_by, target_user, guardian) if !guardian.can_send_private_message?(target_user) - raise UserExists.new(I18n.t( - "activerecord.errors.models.topic.attributes.base.cant_send_pm" - )) + raise UserExists.new(I18n.t("activerecord.errors.models.topic.attributes.base.cant_send_pm")) end rate_limit_topic_invitation(invited_by) Topic.transaction do - topic_allowed_users.create!(user_id: target_user.id) unless topic_allowed_users.exists?(user_id: target_user.id) + unless topic_allowed_users.exists?(user_id: target_user.id) + topic_allowed_users.create!(user_id: target_user.id) + end user_in_allowed_group = (user.group_ids & topic_allowed_groups.map(&:group_id)).present? add_small_action(invited_by, "invited_user", target_user.username) if !user_in_allowed_group @@ -1896,7 +2042,7 @@ class Topic < ActiveRecord::Base create_invite_notification!( target_user, Notification.types[:invited_to_private_message], - invited_by + invited_by, ) end end @@ -1908,24 +2054,18 @@ class Topic < ActiveRecord::Base if group_ids.present? ( self.category.groups.where(id: group_ids).where(automatic: false) - - target_user.groups.where(automatic: false) + target_user.groups.where(automatic: false) ).each do |group| if guardian.can_edit_group?(group) group.add(target_user) - GroupActionLogger - .new(invited_by, group) - .log_add_user_to_group(target_user) + GroupActionLogger.new(invited_by, group).log_add_user_to_group(target_user) end end end if Guardian.new(target_user).can_see_topic?(self) - create_invite_notification!( - target_user, - Notification.types[:invited_to_topic], - invited_by - ) + create_invite_notification!(target_user, Notification.types[:invited_to_topic], invited_by) end end end diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index 47c8b4a3c7..8eab3a31d0 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TopicConverter - attr_reader :topic def initialize(topic, user) @@ -17,16 +16,17 @@ class TopicConverter elsif SiteSetting.allow_uncategorized_topics SiteSetting.uncategorized_category_id else - Category.where(read_restricted: false) + Category + .where(read_restricted: false) .where.not(id: SiteSetting.uncategorized_category_id) - .order('id asc') + .order("id asc") .pluck_first(:id) end PostRevisor.new(@topic.first_post, @topic).revise!( @user, category_id: category_id, - archetype: Archetype.default + archetype: Archetype.default, ) update_user_stats @@ -45,7 +45,7 @@ class TopicConverter PostRevisor.new(@topic.first_post, @topic).revise!( @user, category_id: nil, - archetype: Archetype.private_message + archetype: Archetype.private_message, ) add_allowed_users @@ -63,9 +63,12 @@ class TopicConverter private def posters - @posters ||= @topic.posts - .where.not(post_type: [Post.types[:small_action], Post.types[:whisper]]) - .distinct.pluck(:user_id) + @posters ||= + @topic + .posts + .where.not(post_type: [Post.types[:small_action], Post.types[:whisper]]) + .distinct + .pluck(:user_id) end def increment_users_post_count @@ -134,8 +137,6 @@ class TopicConverter end def update_post_uploads_secure_status - DB.after_commit do - Jobs.enqueue(:update_topic_upload_security, topic_id: @topic.id) - end + DB.after_commit { Jobs.enqueue(:update_topic_upload_security, topic_id: @topic.id) } end end diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 6b0e3662f8..3b9545bf81 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -9,7 +9,13 @@ class TopicEmbed < ActiveRecord::Base validates_uniqueness_of :embed_url before_validation(on: :create) do - unless (topic_embed = TopicEmbed.with_deleted.where('deleted_at IS NOT NULL AND embed_url = ?', embed_url).first).nil? + unless ( + topic_embed = + TopicEmbed + .with_deleted + .where("deleted_at IS NOT NULL AND embed_url = ?", embed_url) + .first + ).nil? topic_embed.destroy! end end @@ -19,23 +25,21 @@ class TopicEmbed < ActiveRecord::Base end def self.normalize_url(url) - url.downcase.sub(/\/$/, '').sub(/\-+/, '-').strip + url.downcase.sub(%r{/$}, "").sub(/\-+/, "-").strip end def self.imported_from_html(url) I18n.with_locale(SiteSetting.default_locale) do - "\n
    \n#{I18n.t('embed.imported_from', link: "
    #{url}")}\n" + "\n
    \n#{I18n.t("embed.imported_from", link: "#{url}")}\n" end end # Import an article from a source (RSS/Atom/Other) def self.import(user, url, title, contents, category_id: nil, cook_method: nil, tags: nil) - return unless url =~ /^https?\:\/\// + return unless url =~ %r{^https?\://} - if SiteSetting.embed_truncate && cook_method.nil? - contents = first_paragraph_from(contents) - end - contents ||= '' + contents = first_paragraph_from(contents) if SiteSetting.embed_truncate && cook_method.nil? + contents ||= "" contents = contents.dup << imported_from_html(url) url = normalize_url(url) @@ -49,11 +53,12 @@ class TopicEmbed < ActiveRecord::Base Topic.transaction do eh = EmbeddableHost.record_for_url(url) - cook_method ||= if SiteSetting.embed_support_markdown - Post.cook_methods[:regular] - else - Post.cook_methods[:raw_html] - end + cook_method ||= + if SiteSetting.embed_support_markdown + Post.cook_methods[:regular] + else + Post.cook_methods[:raw_html] + end create_args = { title: title, @@ -63,17 +68,17 @@ class TopicEmbed < ActiveRecord::Base category: category_id || eh.try(:category_id), tags: SiteSetting.tagging_enabled ? tags : nil, } - if SiteSetting.embed_unlisted? - create_args[:visible] = false - end + create_args[:visible] = false if SiteSetting.embed_unlisted? creator = PostCreator.new(user, create_args) post = creator.create if post.present? - TopicEmbed.create!(topic_id: post.topic_id, - embed_url: url, - content_sha1: content_sha1, - post_id: post.id) + TopicEmbed.create!( + topic_id: post.topic_id, + embed_url: url, + content_sha1: content_sha1, + post_id: post.id, + ) end end else @@ -87,7 +92,7 @@ class TopicEmbed < ActiveRecord::Base post_ids: [post.id], topic_id: post.topic_id, new_owner: user, - acting_user: Discourse.system_user + acting_user: Discourse.system_user, ).change_owner! # make sure the post returned has the right author @@ -108,16 +113,11 @@ class TopicEmbed < ActiveRecord::Base end def self.find_remote(url) - require 'ruby-readability' + require "ruby-readability" url = UrlHelper.normalized_encode(url) original_uri = URI.parse(url) - fd = FinalDestination.new( - url, - validate_uri: true, - max_redirects: 5, - follow_canonical: true, - ) + fd = FinalDestination.new(url, validate_uri: true, max_redirects: 5, follow_canonical: true) uri = fd.resolve return if uri.blank? @@ -125,12 +125,17 @@ class TopicEmbed < ActiveRecord::Base opts = { tags: %w[div p code pre h1 h2 h3 b em i strong a img ul li ol blockquote], attributes: %w[href src class], - remove_empty_nodes: false + remove_empty_nodes: false, } - opts[:whitelist] = SiteSetting.allowed_embed_selectors if SiteSetting.allowed_embed_selectors.present? - opts[:blacklist] = SiteSetting.blocked_embed_selectors if SiteSetting.blocked_embed_selectors.present? - allowed_embed_classnames = SiteSetting.allowed_embed_classnames if SiteSetting.allowed_embed_classnames.present? + opts[ + :whitelist + ] = SiteSetting.allowed_embed_selectors if SiteSetting.allowed_embed_selectors.present? + opts[ + :blacklist + ] = SiteSetting.blocked_embed_selectors if SiteSetting.blocked_embed_selectors.present? + allowed_embed_classnames = + SiteSetting.allowed_embed_classnames if SiteSetting.allowed_embed_classnames.present? response = FetchResponse.new begin @@ -139,7 +144,7 @@ class TopicEmbed < ActiveRecord::Base return end - raw_doc = Nokogiri::HTML5(html) + raw_doc = Nokogiri.HTML5(html) auth_element = raw_doc.at('meta[@name="author"]') if auth_element.present? response.author = User.where(username_lower: auth_element[:content].strip).first @@ -147,39 +152,51 @@ class TopicEmbed < ActiveRecord::Base read_doc = Readability::Document.new(html, opts) - title = +(raw_doc.title || '') + title = +(raw_doc.title || "") title.strip! if SiteSetting.embed_title_scrubber.present? - title.sub!(Regexp.new(SiteSetting.embed_title_scrubber), '') + title.sub!(Regexp.new(SiteSetting.embed_title_scrubber), "") title.strip! end response.title = title - doc = Nokogiri::HTML5(read_doc.content) + doc = Nokogiri.HTML5(read_doc.content) - tags = { 'img' => 'src', 'script' => 'src', 'a' => 'href' } - doc.search(tags.keys.join(',')).each do |node| - url_param = tags[node.name] - src = node[url_param] - unless (src.nil? || src.empty?) - begin - # convert URL to absolute form - node[url_param] = URI.join(url, UrlHelper.normalized_encode(src)).to_s - rescue URI::Error, Addressable::URI::InvalidURIError - # If there is a mistyped URL, just do nothing + tags = { "img" => "src", "script" => "src", "a" => "href" } + doc + .search(tags.keys.join(",")) + .each do |node| + url_param = tags[node.name] + src = node[url_param] + unless (src.nil? || src.empty?) + begin + # convert URL to absolute form + node[url_param] = URI.join(url, UrlHelper.normalized_encode(src)).to_s + rescue URI::Error, Addressable::URI::InvalidURIError + # If there is a mistyped URL, just do nothing + end end + # only allow classes in the allowlist + allowed_classes = + if allowed_embed_classnames.blank? + [] + else + allowed_embed_classnames.split(/[ ,]+/i) + end + doc + .search('[class]:not([class=""])') + .each do |classnode| + classes = + classnode[:class] + .split(" ") + .select { |classname| allowed_classes.include?(classname) } + if classes.length === 0 + classnode.delete("class") + else + classnode[:class] = classes.join(" ") + end + end end - # only allow classes in the allowlist - allowed_classes = if allowed_embed_classnames.blank? then [] else allowed_embed_classnames.split(/[ ,]+/i) end - doc.search('[class]:not([class=""])').each do |classnode| - classes = classnode[:class].split(' ').select { |classname| allowed_classes.include?(classname) } - if classes.length === 0 - classnode.delete('class') - else - classnode[:class] = classes.join(' ') - end - end - end response.body = doc.to_html response @@ -208,59 +225,69 @@ class TopicEmbed < ActiveRecord::Base prefix += ":#{uri.port}" if uri.port != 80 && uri.port != 443 fragment = Nokogiri::HTML5.fragment("
    #{contents}
    ") - fragment.css('a').each do |a| - if a['href'].present? - begin - a['href'] = URI.join(prefix, a['href']).to_s - rescue URI::InvalidURIError - # NOOP, URL is malformed + fragment + .css("a") + .each do |a| + if a["href"].present? + begin + a["href"] = URI.join(prefix, a["href"]).to_s + rescue URI::InvalidURIError + # NOOP, URL is malformed + end end end - end - fragment.css('img').each do |a| - if a['src'].present? - begin - a['src'] = URI.join(prefix, a['src']).to_s - rescue URI::InvalidURIError - # NOOP, URL is malformed + fragment + .css("img") + .each do |a| + if a["src"].present? + begin + a["src"] = URI.join(prefix, a["src"]).to_s + rescue URI::InvalidURIError + # NOOP, URL is malformed + end end end - end - fragment.at('div').inner_html + fragment.at("div").inner_html end def self.topic_id_for_embed(embed_url) - embed_url = normalize_url(embed_url).sub(/^https?\:\/\//, '') - TopicEmbed.where("embed_url ~* ?", "^https?://#{Regexp.escape(embed_url)}$").pluck_first(:topic_id) + embed_url = normalize_url(embed_url).sub(%r{^https?\://}, "") + TopicEmbed.where("embed_url ~* ?", "^https?://#{Regexp.escape(embed_url)}$").pluck_first( + :topic_id, + ) end def self.first_paragraph_from(html) - doc = Nokogiri::HTML5(html) + doc = Nokogiri.HTML5(html) result = +"" - doc.css('p').each do |p| - if p.text.present? - result << p.to_s - return result if result.size >= 100 + doc + .css("p") + .each do |p| + if p.text.present? + result << p.to_s + return result if result.size >= 100 + end end - end return result unless result.blank? # If there is no first paragraph, return the first div (onebox) - doc.css('div').first.to_s + doc.css("div").first.to_s end def self.expanded_for(post) - Discourse.cache.fetch("embed-topic:#{post.topic_id}", expires_in: 10.minutes) do - url = TopicEmbed.where(topic_id: post.topic_id).pluck_first(:embed_url) - response = TopicEmbed.find_remote(url) + Discourse + .cache + .fetch("embed-topic:#{post.topic_id}", expires_in: 10.minutes) do + url = TopicEmbed.where(topic_id: post.topic_id).pluck_first(:embed_url) + response = TopicEmbed.find_remote(url) - body = response.body - body << TopicEmbed.imported_from_html(url) - body - end + body = response.body + body << TopicEmbed.imported_from_html(url) + body + end end end diff --git a/app/models/topic_featured_users.rb b/app/models/topic_featured_users.rb index 3cd3c7fda3..5383ae6655 100644 --- a/app/models/topic_featured_users.rb +++ b/app/models/topic_featured_users.rb @@ -18,14 +18,15 @@ class TopicFeaturedUsers end def user_ids - [topic.featured_user1_id, - topic.featured_user2_id, - topic.featured_user3_id, - topic.featured_user4_id].uniq.compact + [ + topic.featured_user1_id, + topic.featured_user2_id, + topic.featured_user3_id, + topic.featured_user4_id, + ].uniq.compact end def self.ensure_consistency!(topic_id = nil) - filter = "#{"AND t.id = #{topic_id.to_i}" if topic_id}" filter2 = "#{"AND tt.id = #{topic_id.to_i}" if topic_id}" @@ -87,7 +88,11 @@ SQL private def update_participant_count - count = topic.posts.where('NOT hidden AND post_type in (?)', Topic.visible_post_types).count('distinct user_id') + count = + topic + .posts + .where("NOT hidden AND post_type in (?)", Topic.visible_post_types) + .count("distinct user_id") topic.update_columns(participant_count: count) end end diff --git a/app/models/topic_group.rb b/app/models/topic_group.rb index 7f3c4060f3..f9e0f65fb0 100644 --- a/app/models/topic_group.rb +++ b/app/models/topic_group.rb @@ -31,10 +31,14 @@ class TopicGroup < ActiveRecord::Base tg.group_id SQL - updated_groups = DB.query( - update_query, - user_id: user.id, topic_id: topic_id, post_number: post_number, now: DateTime.now - ) + updated_groups = + DB.query( + update_query, + user_id: user.id, + topic_id: topic_id, + post_number: post_number, + now: DateTime.now, + ) end def self.create_topic_group(user, topic_id, post_number, updated_group_ids) @@ -47,7 +51,8 @@ class TopicGroup < ActiveRecord::Base AND tag.topic_id = :topic_id SQL - query += 'AND NOT(tag.group_id IN (:already_updated_groups))' unless updated_group_ids.length.zero? + query += + "AND NOT(tag.group_id IN (:already_updated_groups))" unless updated_group_ids.length.zero? query += <<~SQL ON CONFLICT(topic_id, group_id) @@ -56,7 +61,11 @@ class TopicGroup < ActiveRecord::Base DB.exec( query, - user_id: user.id, topic_id: topic_id, post_number: post_number, now: DateTime.now, already_updated_groups: updated_group_ids + user_id: user.id, + topic_id: topic_id, + post_number: post_number, + now: DateTime.now, + already_updated_groups: updated_group_ids, ) end end diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb index 690779cb06..03bc3587b4 100644 --- a/app/models/topic_link.rb +++ b/app/models/topic_link.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'uri' +require "uri" class TopicLink < ActiveRecord::Base - def self.max_domain_length 100 end @@ -15,14 +14,14 @@ class TopicLink < ActiveRecord::Base belongs_to :topic belongs_to :user belongs_to :post - belongs_to :link_topic, class_name: 'Topic' - belongs_to :link_post, class_name: 'Post' + belongs_to :link_topic, class_name: "Topic" + belongs_to :link_post, class_name: "Post" validates_presence_of :url validates_length_of :url, maximum: 500 - validates_uniqueness_of :url, scope: [:topic_id, :post_id] + validates_uniqueness_of :url, scope: %i[topic_id post_id] has_many :topic_link_clicks, dependent: :destroy @@ -36,7 +35,6 @@ class TopicLink < ActiveRecord::Base end def self.topic_map(guardian, topic_id) - # Sam: complicated reports are really hard in AR builder = DB.build(<<~SQL) SELECT ftl.url, @@ -56,16 +54,18 @@ class TopicLink < ActiveRecord::Base LIMIT 50 SQL - builder.where('ftl.topic_id = :topic_id', topic_id: topic_id) - builder.where('ft.deleted_at IS NULL') + builder.where("ftl.topic_id = :topic_id", topic_id: topic_id) + builder.where("ft.deleted_at IS NULL") builder.where("ftl.extension IS NULL OR ftl.extension NOT IN ('png','jpg','gif')") - builder.where("COALESCE(ft.archetype, 'regular') <> :archetype", archetype: Archetype.private_message) + builder.where( + "COALESCE(ft.archetype, 'regular') <> :archetype", + archetype: Archetype.private_message, + ) builder.where("clicks > 0") builder.secure_category(guardian.secure_category_ids) builder.query - end def self.counts_for(guardian, topic, posts) @@ -73,7 +73,9 @@ class TopicLink < ActiveRecord::Base # Sam: this is not tidy in AR and also happens to be a critical path # for topic view - builder = DB.build("SELECT + builder = + DB.build( + "SELECT l.post_id, l.url, l.clicks, @@ -86,28 +88,39 @@ class TopicLink < ActiveRecord::Base LEFT JOIN categories AS c ON c.id = t.category_id /*left_join*/ /*where*/ - ORDER BY reflection ASC, clicks DESC") + ORDER BY reflection ASC, clicks DESC", + ) - builder.where('t.deleted_at IS NULL') - builder.where("COALESCE(t.archetype, 'regular') <> :archetype", archetype: Archetype.private_message) + builder.where("t.deleted_at IS NULL") + builder.where( + "COALESCE(t.archetype, 'regular') <> :archetype", + archetype: Archetype.private_message, + ) if guardian.authenticated? - builder.left_join("topic_users AS tu ON (t.id = tu.topic_id AND tu.user_id = #{guardian.user.id.to_i})") - builder.where('COALESCE(tu.notification_level,1) > :muted', muted: TopicUser.notification_levels[:muted]) + builder.left_join( + "topic_users AS tu ON (t.id = tu.topic_id AND tu.user_id = #{guardian.user.id.to_i})", + ) + builder.where( + "COALESCE(tu.notification_level,1) > :muted", + muted: TopicUser.notification_levels[:muted], + ) end # not certain if pluck is right, cause it may interfere with caching - builder.where('l.post_id in (:post_ids)', post_ids: posts.map(&:id)) + builder.where("l.post_id in (:post_ids)", post_ids: posts.map(&:id)) builder.secure_category(guardian.secure_category_ids) result = {} builder.query.each do |l| result[l.post_id] ||= [] - result[l.post_id] << { url: l.url, - clicks: l.clicks, - title: l.title, - internal: l.internal, - reflection: l.reflection } + result[l.post_id] << { + url: l.url, + clicks: l.clicks, + title: l.title, + internal: l.internal, + reflection: l.reflection, + } end result end @@ -127,22 +140,20 @@ class TopicLink < ActiveRecord::Base .reject { |_, p| p.nil? || "mailto" == p.scheme } .uniq { |_, p| p } .each do |link, parsed| - - TopicLink.transaction do - begin - url, reflected_id = self.ensure_entry_for(post, link, parsed) - current_urls << url unless url.nil? - reflected_ids << reflected_id unless reflected_id.nil? - rescue URI::Error - # if the URI is invalid, don't store it. - rescue ActionController::RoutingError - # If we can't find the route, no big deal + TopicLink.transaction do + begin + url, reflected_id = self.ensure_entry_for(post, link, parsed) + current_urls << url unless url.nil? + reflected_ids << reflected_id unless reflected_id.nil? + rescue URI::Error + # if the URI is invalid, don't store it. + rescue ActionController::RoutingError + # If we can't find the route, no big deal + end end end - end self.cleanup_entries(post, current_urls, reflected_ids) - end def self.crawl_link_title(topic_link_id) @@ -154,20 +165,23 @@ class TopicLink < ActiveRecord::Base end def self.duplicate_lookup(topic) - results = TopicLink - .includes(:post, :user) - .joins(:post, :user) - .where("posts.id IS NOT NULL AND users.id IS NOT NULL") - .where(topic_id: topic.id, reflection: false) - .last(200) + results = + TopicLink + .includes(:post, :user) + .joins(:post, :user) + .where("posts.id IS NOT NULL AND users.id IS NOT NULL") + .where(topic_id: topic.id, reflection: false) + .last(200) lookup = {} results.each do |tl| - normalized = tl.url.downcase.sub(/^https?:\/\//, '').sub(/\/$/, '') - lookup[normalized] = { domain: tl.domain, - username: tl.user.username_lower, - posted_at: tl.post.created_at, - post_number: tl.post.post_number } + normalized = tl.url.downcase.sub(%r{^https?://}, "").sub(%r{/$}, "") + lookup[normalized] = { + domain: tl.domain, + username: tl.user.username_lower, + posted_at: tl.post.created_at, + post_number: tl.post.post_number, + } end lookup @@ -199,7 +213,6 @@ class TopicLink < ActiveRecord::Base extension: nil, reflection: false ) - domain ||= Discourse.current_hostname sql = <<~SQL @@ -242,26 +255,24 @@ class TopicLink < ActiveRecord::Base ), (SELECT id FROM new_row) IS NOT NULL SQL - topic_link_id, new_record = DB.query_single(sql, - post_id: post_id, - user_id: user_id, - topic_id: topic_id, - url: url, - domain: domain, - internal: internal, - link_topic_id: link_topic_id, - link_post_id: link_post_id, - quote: quote, - extension: extension, - reflection: reflection, - now: Time.now - ) + topic_link_id, new_record = + DB.query_single( + sql, + post_id: post_id, + user_id: user_id, + topic_id: topic_id, + url: url, + domain: domain, + internal: internal, + link_topic_id: link_topic_id, + link_post_id: link_post_id, + quote: quote, + extension: extension, + reflection: reflection, + now: Time.now, + ) - if new_record - DB.after_commit do - crawl_link_title(topic_link_id) - end - end + DB.after_commit { crawl_link_title(topic_link_id) } if new_record topic_link_id end @@ -314,7 +325,8 @@ class TopicLink < ActiveRecord::Base url = url[0...TopicLink.max_url_length] return nil if parsed && parsed.host && parsed.host.length > TopicLink.max_domain_length - file_extension = File.extname(parsed.path)[1..10].downcase unless parsed.path.nil? || File.extname(parsed.path).empty? + file_extension = File.extname(parsed.path)[1..10].downcase unless parsed.path.nil? || + File.extname(parsed.path).empty? safe_create_topic_link( post_id: post.id, @@ -332,22 +344,23 @@ class TopicLink < ActiveRecord::Base reflected_id = nil # Create the reflection if we can - if topic && post.topic && topic.archetype != 'private_message' && post.topic.archetype != 'private_message' && post.topic.visible? + if topic && post.topic && topic.archetype != "private_message" && + post.topic.archetype != "private_message" && post.topic.visible? prefix = Discourse.base_url_no_prefix reflected_url = "#{prefix}#{post.topic.relative_url(post.post_number)}" - reflected_id = safe_create_topic_link( - user_id: post.user_id, - topic_id: topic&.id, - post_id: reflected_post&.id, - url: reflected_url, - domain: Discourse.current_hostname, - reflection: true, - internal: true, - link_topic_id: post.topic_id, - link_post_id: post.id - ) - + reflected_id = + safe_create_topic_link( + user_id: post.user_id, + topic_id: topic&.id, + post_id: reflected_post&.id, + url: reflected_url, + domain: Discourse.current_hostname, + reflection: true, + internal: true, + link_topic_id: post.topic_id, + link_post_id: post.id, + ) end [url, reflected_id] @@ -358,27 +371,25 @@ class TopicLink < ActiveRecord::Base if current_urls.present? TopicLink.where( "(url not in (:urls)) AND (post_id = :post_id AND NOT reflection)", - urls: current_urls, post_id: post.id + urls: current_urls, + post_id: post.id, ).delete_all current_reflected_ids.compact! if current_reflected_ids.present? TopicLink.where( "(id not in (:reflected_ids)) AND (link_post_id = :post_id AND reflection)", - reflected_ids: current_reflected_ids, post_id: post.id + reflected_ids: current_reflected_ids, + post_id: post.id, ).delete_all else - TopicLink - .where("link_post_id = :post_id AND reflection", post_id: post.id) - .delete_all + TopicLink.where("link_post_id = :post_id AND reflection", post_id: post.id).delete_all end else - TopicLink - .where( - "(post_id = :post_id AND NOT reflection) OR (link_post_id = :post_id AND reflection)", - post_id: post.id - ) - .delete_all + TopicLink.where( + "(post_id = :post_id AND NOT reflection) OR (link_post_id = :post_id AND reflection)", + post_id: post.id, + ).delete_all end end end diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb index 8770567997..a1f121ee84 100644 --- a/app/models/topic_link_click.rb +++ b/app/models/topic_link_click.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'ipaddr' -require 'url_helper' +require "ipaddr" +require "url_helper" class TopicLinkClick < ActiveRecord::Base belongs_to :topic_link, counter_cache: :clicks @@ -9,9 +9,9 @@ class TopicLinkClick < ActiveRecord::Base validates_presence_of :topic_link_id - ALLOWED_REDIRECT_HOSTNAMES = Set.new(%W{www.youtube.com youtu.be}) + ALLOWED_REDIRECT_HOSTNAMES = Set.new(%W[www.youtube.com youtu.be]) include ActiveSupport::Deprecation::DeprecatedConstantAccessor - deprecate_constant 'WHITELISTED_REDIRECT_HOSTNAMES', 'TopicLinkClick::ALLOWED_REDIRECT_HOSTNAMES' + deprecate_constant "WHITELISTED_REDIRECT_HOSTNAMES", "TopicLinkClick::ALLOWED_REDIRECT_HOSTNAMES" # Create a click from a URL and post_id def self.create_from(args = {}) @@ -22,32 +22,33 @@ class TopicLinkClick < ActiveRecord::Base urls = Set.new urls << url if url =~ /^http/ - urls << url.sub(/^https/, 'http') - urls << url.sub(/^http:/, 'https:') + urls << url.sub(/^https/, "http") + urls << url.sub(/^http:/, "https:") urls << UrlHelper.schemaless(url) end urls << UrlHelper.absolute_without_cdn(url) urls << uri.path if uri.try(:host) == Discourse.current_hostname - query = url.index('?') + query = url.index("?") unless query.nil? - endpos = url.index('#') || url.size + endpos = url.index("#") || url.size urls << url[0..query - 1] + url[endpos..-1] end # link can have query params, and analytics can add more to the end: i = url.length - while i = url.rindex('&', i - 1) + while i = url.rindex("&", i - 1) urls << url[0...i] end # add a cdn link if uri if Discourse.asset_host.present? - cdn_uri = begin - URI.parse(Discourse.asset_host) - rescue URI::Error - end + cdn_uri = + begin + URI.parse(Discourse.asset_host) + rescue URI::Error + end if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) is_cdn_link = true @@ -56,10 +57,11 @@ class TopicLinkClick < ActiveRecord::Base end if SiteSetting.Upload.s3_cdn_url.present? - cdn_uri = begin - URI.parse(SiteSetting.Upload.s3_cdn_url) - rescue URI::Error - end + cdn_uri = + begin + URI.parse(SiteSetting.Upload.s3_cdn_url) + rescue URI::Error + end if cdn_uri && cdn_uri.hostname == uri.hostname && uri.path.starts_with?(cdn_uri.path) is_cdn_link = true @@ -80,12 +82,15 @@ class TopicLinkClick < ActiveRecord::Base link = link.where(topic_id: args[:topic_id]) if args[:topic_id].present? # select the TopicLink associated to first url - link = link.order("array_position(ARRAY[#{urls.map { |s| "#{ActiveRecord::Base.connection.quote(s)}" }.join(',')}], url::text)").first + link = + link.order( + "array_position(ARRAY[#{urls.map { |s| "#{ActiveRecord::Base.connection.quote(s)}" }.join(",")}], url::text)", + ).first # If no link is found... unless link.present? # ... return the url for relative links or when using the same host - return url if url =~ /^\/[^\/]/ || uri.try(:host) == Discourse.current_hostname + return url if url =~ %r{^/[^/]} || uri.try(:host) == Discourse.current_hostname # If we have it somewhere else on the site, just allow the redirect. # This is likely due to a onebox of another topic. @@ -112,7 +117,6 @@ class TopicLinkClick < ActiveRecord::Base url end - end # == Schema Information diff --git a/app/models/topic_list.rb b/app/models/topic_list.rb index 815e9c2308..b4be8c367f 100644 --- a/app/models/topic_list.rb +++ b/app/models/topic_list.rb @@ -13,16 +13,12 @@ class TopicList def self.cancel_preload(&blk) if @preload @preload.delete blk - if @preload.length == 0 - @preload = nil - end + @preload = nil if @preload.length == 0 end end def self.preload(topics, object) - if @preload - @preload.each { |preload| preload.call(topics, object) } - end + @preload.each { |preload| preload.call(topics, object) } if @preload end def self.on_preload_user_ids(&blk) @@ -49,7 +45,7 @@ class TopicList :tags, :shared_drafts, :category, - :publish_read_state + :publish_read_state, ) def initialize(filter, current_user, topics, opts = nil) @@ -58,13 +54,9 @@ class TopicList @topics_input = topics @opts = opts || {} - if @opts[:category] - @category = Category.find_by(id: @opts[:category_id]) - end + @category = Category.find_by(id: @opts[:category_id]) if @opts[:category] - if @opts[:tags] - @tags = Tag.where(id: @opts[:tags]).all - end + @tags = Tag.where(id: @opts[:tags]).all if @opts[:tags] @publish_read_state = !!@opts[:publish_read_state] end @@ -89,19 +81,19 @@ class TopicList # Attach some data for serialization to each topic @topic_lookup = TopicUser.lookup_for(@current_user, @topics) if @current_user - @dismissed_topic_users_lookup = DismissedTopicUser.lookup_for(@current_user, @topics) if @current_user + @dismissed_topic_users_lookup = + DismissedTopicUser.lookup_for(@current_user, @topics) if @current_user post_action_type = if @current_user if @opts[:filter].present? - if @opts[:filter] == "liked" - PostActionType.types[:like] - end + PostActionType.types[:like] if @opts[:filter] == "liked" end end # Data for bookmarks or likes - post_action_lookup = PostAction.lookup_for(@current_user, @topics, post_action_type) if post_action_type + post_action_lookup = + PostAction.lookup_for(@current_user, @topics, post_action_type) if post_action_type # Create a lookup for all the user ids we need user_ids = [] @@ -125,23 +117,19 @@ class TopicList ft.user_data.post_action_data = { post_action_type => actions } end - ft.posters = ft.posters_summary( - user_lookup: user_lookup - ) + ft.posters = ft.posters_summary(user_lookup: user_lookup) - ft.participants = ft.participants_summary( - user_lookup: user_lookup, - user: @current_user - ) + ft.participants = ft.participants_summary(user_lookup: user_lookup, user: @current_user) ft.topic_list = self end topic_preloader_associations = [:image_upload, { topic_thumbnails: :optimized_image }] topic_preloader_associations.concat(DiscoursePluginRegistry.topic_preloader_associations.to_a) - ActiveRecord::Associations::Preloader - .new(records: @topics, associations: topic_preloader_associations) - .call + ActiveRecord::Associations::Preloader.new( + records: @topics, + associations: topic_preloader_associations, + ).call if preloaded_custom_fields.present? Topic.preload_custom_fields(@topics, preloaded_custom_fields) @@ -153,18 +141,19 @@ class TopicList end def attributes - { 'more_topics_url' => page } + { "more_topics_url" => page } end private def category_user_lookup - @category_user_lookup ||= begin - if @current_user - CategoryUser.lookup_for(@current_user, @topics.map(&:category_id).uniq) - else - [] + @category_user_lookup ||= + begin + if @current_user + CategoryUser.lookup_for(@current_user, @topics.map(&:category_id).uniq) + else + [] + end end - end end end diff --git a/app/models/topic_notifier.rb b/app/models/topic_notifier.rb index 2bcc6beb1c..d733698d3d 100644 --- a/app/models/topic_notifier.rb +++ b/app/models/topic_notifier.rb @@ -5,15 +5,15 @@ class TopicNotifier @topic = topic end - { watch!: :watching, + { + watch!: :watching, track!: :tracking, regular!: :regular, - mute!: :muted }.each_pair do |method_name, level| - + mute!: :muted, + }.each_pair do |method_name, level| define_method method_name do |user_id| change_level user_id, level end - end def watch_topic!(user_id, reason = :created_topic) diff --git a/app/models/topic_participants_summary.rb b/app/models/topic_participants_summary.rb index ba36507522..8f098bd84c 100644 --- a/app/models/topic_participants_summary.rb +++ b/app/models/topic_participants_summary.rb @@ -18,7 +18,7 @@ class TopicParticipantsSummary def new_topic_poster_for(user) TopicPoster.new.tap do |topic_poster| topic_poster.user = user - topic_poster.extras = 'latest' if is_latest_poster?(user) + topic_poster.extras = "latest" if is_latest_poster?(user) end end diff --git a/app/models/topic_poster.rb b/app/models/topic_poster.rb index 762734799c..99d8e3741d 100644 --- a/app/models/topic_poster.rb +++ b/app/models/topic_poster.rb @@ -7,11 +7,11 @@ class TopicPoster < OpenStruct def attributes { - 'user' => user, - 'description' => description, - 'extras' => extras, - 'id' => id, - 'primary_group' => primary_group + "user" => user, + "description" => description, + "extras" => extras, + "id" => id, + "primary_group" => primary_group, } end diff --git a/app/models/topic_posters_summary.rb b/app/models/topic_posters_summary.rb index cded3ebbce..abf47658ef 100644 --- a/app/models/topic_posters_summary.rb +++ b/app/models/topic_posters_summary.rb @@ -2,7 +2,6 @@ # This is used in topic lists class TopicPostersSummary - # localization is fast, but this allows us to avoid # calling it in a loop which adds up def self.translations @@ -10,7 +9,7 @@ class TopicPostersSummary original_poster: I18n.t(:original_poster), most_recent_poster: I18n.t(:most_recent_poster), frequent_poster: I18n.t(:frequent_poster), - joiner: I18n.t(:poster_description_joiner) + joiner: I18n.t(:poster_description_joiner), } end @@ -35,34 +34,35 @@ class TopicPostersSummary topic_poster.primary_group = user_lookup.primary_groups[user.id] topic_poster.flair_group = user_lookup.flair_groups[user.id] if topic.last_post_user_id == user.id - topic_poster.extras = +'latest' - topic_poster.extras << ' single' if user_ids.uniq.size == 1 + topic_poster.extras = +"latest" + topic_poster.extras << " single" if user_ids.uniq.size == 1 end topic_poster end def descriptions_by_id(ids: nil) - @descriptions_by_id ||= begin - result = {} - ids = ids || user_ids + @descriptions_by_id ||= + begin + result = {} + ids = ids || user_ids - if id = ids.shift - result[id] ||= [] - result[id] << @translations[:original_poster] + if id = ids.shift + result[id] ||= [] + result[id] << @translations[:original_poster] + end + + if id = ids.shift + result[id] ||= [] + result[id] << @translations[:most_recent_poster] + end + + while id = ids.shift + result[id] ||= [] + result[id] << @translations[:frequent_poster] + end + + result end - - if id = ids.shift - result[id] ||= [] - result[id] << @translations[:most_recent_poster] - end - - while id = ids.shift - result[id] ||= [] - result[id] << @translations[:frequent_poster] - end - - result - end end def descriptions_for(user) @@ -90,7 +90,7 @@ class TopicPostersSummary end def user_ids - [ topic.user_id, topic.last_post_user_id, *topic.featured_user_ids ] + [topic.user_id, topic.last_post_user_id, *topic.featured_user_ids] end def user_lookup diff --git a/app/models/topic_tag.rb b/app/models/topic_tag.rb index 60943b393c..67a8218d94 100644 --- a/app/models/topic_tag.rb +++ b/app/models/topic_tag.rb @@ -27,7 +27,8 @@ class TopicTag < ActiveRecord::Base if topic.archetype == Archetype.private_message tag.decrement!(:pm_topic_count) else - if topic.category_id && stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id) + if topic.category_id && + stat = CategoryTagStat.find_by(tag_id: tag_id, category: topic.category_id) stat.topic_count == 1 ? stat.destroy : stat.decrement!(:topic_count) end diff --git a/app/models/topic_thumbnail.rb b/app/models/topic_thumbnail.rb index f12161510f..7800e4ed0b 100644 --- a/app/models/topic_thumbnail.rb +++ b/app/models/topic_thumbnail.rb @@ -10,30 +10,34 @@ class TopicThumbnail < ActiveRecord::Base belongs_to :upload belongs_to :optimized_image - def self.find_or_create_for!(original, max_width: , max_height:) - existing = TopicThumbnail.find_by(upload: original, max_width: max_width, max_height: max_height) + def self.find_or_create_for!(original, max_width:, max_height:) + existing = + TopicThumbnail.find_by(upload: original, max_width: max_width, max_height: max_height) return existing if existing return nil if !SiteSetting.create_thumbnails? - target_width, target_height = ImageSizer.resize(original.width, original.height, { max_width: max_width, max_height: max_height }) + target_width, target_height = + ImageSizer.resize( + original.width, + original.height, + { max_width: max_width, max_height: max_height }, + ) if target_width < original.width && target_height < original.height optimized = OptimizedImage.create_for(original, target_width, target_height) end # may have been associated already, bulk insert will skip dupes - TopicThumbnail.insert_all([ - upload_id: original.id, - max_width: max_width, - max_height: max_height, - optimized_image_id: optimized&.id - ]) - - TopicThumbnail.find_by( - upload: original, - max_width: max_width, - max_height: max_height + TopicThumbnail.insert_all( + [ + upload_id: original.id, + max_width: max_width, + max_height: max_height, + optimized_image_id: optimized&.id, + ], ) + + TopicThumbnail.find_by(upload: original, max_width: max_width, max_height: max_height) end def self.ensure_consistency! @@ -48,8 +52,11 @@ class TopicThumbnail < ActiveRecord::Base .delete_all # Delete records for sizes which are no longer needed - sizes = Topic.thumbnail_sizes + ThemeModifierHelper.new(theme_ids: Theme.pluck(:id)).topic_thumbnail_sizes - sizes_sql = sizes.map { |s| "(max_width = #{s[0].to_i} AND max_height = #{s[1].to_i})" }.join(" OR ") + sizes = + Topic.thumbnail_sizes + + ThemeModifierHelper.new(theme_ids: Theme.pluck(:id)).topic_thumbnail_sizes + sizes_sql = + sizes.map { |s| "(max_width = #{s[0].to_i} AND max_height = #{s[1].to_i})" }.join(" OR ") TopicThumbnail.where.not(sizes_sql).delete_all end end diff --git a/app/models/topic_timer.rb b/app/models/topic_timer.rb index 82a00e8dc3..22abd8b3c9 100644 --- a/app/models/topic_timer.rb +++ b/app/models/topic_timer.rb @@ -13,25 +13,26 @@ class TopicTimer < ActiveRecord::Base validates :topic_id, presence: true validates :execute_at, presence: true validates :status_type, presence: true - validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }, if: :public_type? - validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at, :user_id] }, if: :private_type? + validates :status_type, uniqueness: { scope: %i[topic_id deleted_at] }, if: :public_type? + validates :status_type, uniqueness: { scope: %i[topic_id deleted_at user_id] }, if: :private_type? validates :category_id, presence: true, if: :publishing_to_category? validate :executed_at_in_future? validate :duration_in_range? - scope :scheduled_bump_topics, -> { where(status_type: TopicTimer.types[:bump], deleted_at: nil).pluck(:topic_id) } - scope :pending_timers, ->(before_time = Time.now.utc) do - where("execute_at <= :before_time AND deleted_at IS NULL", before_time: before_time) - end + scope :scheduled_bump_topics, + -> { where(status_type: TopicTimer.types[:bump], deleted_at: nil).pluck(:topic_id) } + scope :pending_timers, + ->(before_time = Time.now.utc) { + where("execute_at <= :before_time AND deleted_at IS NULL", before_time: before_time) + } before_save do self.created_at ||= Time.zone.now if execute_at self.public_type = self.public_type? - if (will_save_change_to_execute_at? && - !attribute_in_database(:execute_at).nil?) || - will_save_change_to_user_id? + if (will_save_change_to_execute_at? && !attribute_in_database(:execute_at).nil?) || + will_save_change_to_user_id? end end @@ -46,10 +47,10 @@ class TopicTimer < ActiveRecord::Base after_save do if (saved_change_to_execute_at? || saved_change_to_user_id?) if status_type == TopicTimer.types[:silent_close] || status_type == TopicTimer.types[:close] - topic.update_status('closed', false, user) if topic.closed? + topic.update_status("closed", false, user) if topic.closed? end if status_type == TopicTimer.types[:open] - topic.update_status('closed', true, user) if topic.open? + topic.update_status("closed", true, user) if topic.open? end end end @@ -72,22 +73,23 @@ class TopicTimer < ActiveRecord::Base bump: :bump_topic, delete_replies: :delete_replies, silent_close: :close_topic, - clear_slow_mode: :clear_slow_mode + clear_slow_mode: :clear_slow_mode, } end def self.types - @types ||= Enum.new( - close: 1, - open: 2, - publish_to_category: 3, - delete: 4, - reminder: 5, - bump: 6, - delete_replies: 7, - silent_close: 8, - clear_slow_mode: 9 - ) + @types ||= + Enum.new( + close: 1, + open: 2, + publish_to_category: 3, + delete: 4, + reminder: 5, + bump: 6, + delete_replies: 7, + silent_close: 8, + clear_slow_mode: 9, + ) end def self.public_types @@ -126,24 +128,29 @@ class TopicTimer < ActiveRecord::Base return if duration_minutes.blank? if duration_minutes.to_i <= 0 - errors.add(:duration_minutes, I18n.t( - 'activerecord.errors.models.topic_timer.attributes.duration_minutes.cannot_be_zero' - )) + errors.add( + :duration_minutes, + I18n.t("activerecord.errors.models.topic_timer.attributes.duration_minutes.cannot_be_zero"), + ) end if duration_minutes.to_i > MAX_DURATION_MINUTES - errors.add(:duration_minutes, I18n.t( - 'activerecord.errors.models.topic_timer.attributes.duration_minutes.exceeds_maximum' - )) + errors.add( + :duration_minutes, + I18n.t( + "activerecord.errors.models.topic_timer.attributes.duration_minutes.exceeds_maximum", + ), + ) end end def executed_at_in_future? return if created_at.blank? || (execute_at > created_at) - errors.add(:execute_at, I18n.t( - 'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past' - )) + errors.add( + :execute_at, + I18n.t("activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past"), + ) end def schedule_auto_delete_replies_job diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 65bf2fa59c..10fa727450 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -35,9 +35,7 @@ class TopicTrackingState return unless topic.regular? tag_ids, tags = nil - if SiteSetting.tagging_enabled - tag_ids, tags = topic.tags.pluck(:id, :name).transpose - end + tag_ids, tags = topic.tags.pluck(:id, :name).transpose if SiteSetting.tagging_enabled payload = { last_read_post_number: nil, @@ -45,7 +43,7 @@ class TopicTrackingState created_at: topic.created_at, category_id: topic.category_id, archetype: topic.archetype, - created_in_new_period: true + created_in_new_period: true, } if tags @@ -53,13 +51,9 @@ class TopicTrackingState payload[:topic_tag_ids] = tag_ids end - message = { - topic_id: topic.id, - message_type: NEW_TOPIC_MESSAGE_TYPE, - payload: payload - } + message = { topic_id: topic.id, message_type: NEW_TOPIC_MESSAGE_TYPE, payload: payload } - group_ids = topic.category && topic.category.secure_group_ids + group_ids = secure_category_group_ids(topic) MessageBus.publish("/new", message.as_json, group_ids: group_ids) publish_read(topic.id, 1, topic.user) @@ -69,9 +63,7 @@ class TopicTrackingState return unless topic.regular? tag_ids, tags = nil - if SiteSetting.tagging_enabled - tag_ids, tags = topic.tags.pluck(:id, :name).transpose - end + tag_ids, tags = topic.tags.pluck(:id, :name).transpose if SiteSetting.tagging_enabled message = { topic_id: topic.id, @@ -79,8 +71,8 @@ class TopicTrackingState payload: { bumped_at: topic.bumped_at, category_id: topic.category_id, - archetype: topic.archetype - } + archetype: topic.archetype, + }, } if tags @@ -92,8 +84,9 @@ class TopicTrackingState if whisper [Group::AUTO_GROUPS[:staff], *SiteSetting.whispers_allowed_group_ids] else - topic.category && topic.category.secure_group_ids + secure_category_group_ids(topic) end + MessageBus.publish("/latest", message.as_json, group_ids: group_ids) end @@ -102,32 +95,30 @@ class TopicTrackingState end def self.publish_muted(topic) - user_ids = topic.topic_users - .where(notification_level: NotificationLevels.all[:muted]) - .joins(:user) - .where("users.last_seen_at > ?", 7.days.ago) - .order("users.last_seen_at DESC") - .limit(100) - .pluck(:user_id) + user_ids = + topic + .topic_users + .where(notification_level: NotificationLevels.all[:muted]) + .joins(:user) + .where("users.last_seen_at > ?", 7.days.ago) + .order("users.last_seen_at DESC") + .limit(100) + .pluck(:user_id) return if user_ids.blank? - message = { - topic_id: topic.id, - message_type: MUTED_MESSAGE_TYPE, - } + message = { topic_id: topic.id, message_type: MUTED_MESSAGE_TYPE } MessageBus.publish("/latest", message.as_json, user_ids: user_ids) end def self.publish_unmuted(topic) - user_ids = User.watching_topic(topic) - .where("users.last_seen_at > ?", 7.days.ago) - .order("users.last_seen_at DESC") - .limit(100) - .pluck(:id) + user_ids = + User + .watching_topic(topic) + .where("users.last_seen_at > ?", 7.days.ago) + .order("users.last_seen_at DESC") + .limit(100) + .pluck(:id) return if user_ids.blank? - message = { - topic_id: topic.id, - message_type: UNMUTED_MESSAGE_TYPE, - } + message = { topic_id: topic.id, message_type: UNMUTED_MESSAGE_TYPE } MessageBus.publish("/latest", message.as_json, user_ids: user_ids) end @@ -137,16 +128,12 @@ class TopicTrackingState # perhaps cut down to users that are around in the last 7 days as well tags = nil tag_ids = nil - if include_tags_in_report? - tag_ids, tags = post.topic.tags.pluck(:id, :name).transpose - end + tag_ids, tags = post.topic.tags.pluck(:id, :name).transpose if include_tags_in_report? # We don't need to publish unread to the person who just made the post, # this is why they are excluded from the initial scope. - scope = TopicUser - .tracking(post.topic_id) - .includes(user: :user_stat) - .where.not(user_id: post.user_id) + scope = + TopicUser.tracking(post.topic_id).includes(user: :user_stat).where.not(user_id: post.user_id) group_ids = if post.post_type == Post.types[:whisper] @@ -156,9 +143,11 @@ class TopicTrackingState end if group_ids.present? - scope = scope - .joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id") - .where("gu.group_id IN (?)", group_ids) + scope = + scope.joins("INNER JOIN group_users gu ON gu.user_id = topic_users.user_id").where( + "gu.group_id IN (?)", + group_ids, + ) end user_ids = scope.pluck(:user_id) @@ -177,47 +166,31 @@ class TopicTrackingState payload[:topic_tag_ids] = tag_ids end - message = { - topic_id: post.topic_id, - message_type: UNREAD_MESSAGE_TYPE, - payload: payload - } + message = { topic_id: post.topic_id, message_type: UNREAD_MESSAGE_TYPE, payload: payload } - MessageBus.publish("/unread", message.as_json, - user_ids: user_ids - ) + MessageBus.publish("/unread", message.as_json, user_ids: user_ids) end def self.publish_recover(topic) - group_ids = topic.category && topic.category.secure_group_ids + group_ids = secure_category_group_ids(topic) - message = { - topic_id: topic.id, - message_type: RECOVER_MESSAGE_TYPE - } + message = { topic_id: topic.id, message_type: RECOVER_MESSAGE_TYPE } MessageBus.publish("/recover", message.as_json, group_ids: group_ids) - end def self.publish_delete(topic) - group_ids = topic.category && topic.category.secure_group_ids + group_ids = secure_category_group_ids(topic) - message = { - topic_id: topic.id, - message_type: DELETE_MESSAGE_TYPE - } + message = { topic_id: topic.id, message_type: DELETE_MESSAGE_TYPE } MessageBus.publish("/delete", message.as_json, group_ids: group_ids) end def self.publish_destroy(topic) - group_ids = topic.category && topic.category.secure_group_ids + group_ids = secure_category_group_ids(topic) - message = { - topic_id: topic.id, - message_type: DESTROY_MESSAGE_TYPE - } + message = { topic_id: topic.id, message_type: DESTROY_MESSAGE_TYPE } MessageBus.publish("/destroy", message.as_json, group_ids: group_ids) end @@ -229,25 +202,21 @@ class TopicTrackingState topic_id: topic_id, user: user, last_read_post_number: last_read_post_number, - notification_level: notification_level + notification_level: notification_level, ) end def self.publish_dismiss_new(user_id, topic_ids: []) - message = { - message_type: DISMISS_NEW_MESSAGE_TYPE, - payload: { - topic_ids: topic_ids - } - } + message = { message_type: DISMISS_NEW_MESSAGE_TYPE, payload: { topic_ids: topic_ids } } MessageBus.publish(self.unread_channel_key(user_id), message.as_json, user_ids: [user_id]) end def self.new_filter_sql - TopicQuery.new_filter( - Topic, treat_as_new_topic_clause_sql: treat_as_new_topic_clause - ).where_clause.ast.to_sql + - " AND topics.created_at > :min_new_topic_date" + + TopicQuery + .new_filter(Topic, treat_as_new_topic_clause_sql: treat_as_new_topic_clause) + .where_clause + .ast + .to_sql + " AND topics.created_at > :min_new_topic_date" + " AND dismissed_topic_users.id IS NULL" end @@ -256,13 +225,18 @@ class TopicTrackingState end def self.treat_as_new_topic_clause - User.where("GREATEST(CASE + User + .where( + "GREATEST(CASE WHEN COALESCE(uo.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at WHEN COALESCE(uo.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at) ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(uo.new_topic_duration_minutes, :default_duration)) END, u.created_at, :min_date)", - treat_as_new_topic_params - ).where_clause.ast.to_sql + treat_as_new_topic_params, + ) + .where_clause + .ast + .to_sql end def self.treat_as_new_topic_params @@ -271,7 +245,7 @@ class TopicTrackingState last_visit: User::NewTopicDuration::LAST_VISIT, always: User::NewTopicDuration::ALWAYS, default_duration: SiteSetting.default_other_new_topic_duration_minutes, - min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime + min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, } end @@ -296,31 +270,32 @@ class TopicTrackingState sql = new_and_unread_sql(topic_id, user, tag_ids) sql = tags_included_wrapped_sql(sql) - report = DB.query( - sql + "\n\n LIMIT :max_topics", - { - user_id: user.id, - topic_id: topic_id, - min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, - max_topics: TopicTrackingState::MAX_TOPICS, - } - .merge(treat_as_new_topic_params) - ) + report = + DB.query( + sql + "\n\n LIMIT :max_topics", + { + user_id: user.id, + topic_id: topic_id, + min_new_topic_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, + max_topics: TopicTrackingState::MAX_TOPICS, + }.merge(treat_as_new_topic_params), + ) report end def self.new_and_unread_sql(topic_id, user, tag_ids) - sql = report_raw_sql( - topic_id: topic_id, - skip_unread: true, - skip_order: true, - staff: user.staff?, - admin: user.admin?, - whisperer: user.whisperer?, - user: user, - muted_tag_ids: tag_ids - ) + sql = + report_raw_sql( + topic_id: topic_id, + skip_unread: true, + skip_order: true, + staff: user.staff?, + admin: user.admin?, + whisperer: user.whisperer?, + user: user, + muted_tag_ids: tag_ids, + ) sql << "\nUNION ALL\n\n" @@ -333,13 +308,12 @@ class TopicTrackingState admin: user.admin?, whisperer: user.whisperer?, user: user, - muted_tag_ids: tag_ids + muted_tag_ids: tag_ids, ) end def self.tags_included_wrapped_sql(sql) - if SiteSetting.tagging_enabled && TopicTrackingState.include_tags_in_report? - return <<~SQL + return <<~SQL if SiteSetting.tagging_enabled && TopicTrackingState.include_tags_in_report? WITH tags_included_cte AS ( #{sql} ) @@ -350,7 +324,6 @@ class TopicTrackingState ) tags FROM tags_included_cte SQL - end sql end @@ -395,7 +368,9 @@ class TopicTrackingState new_filter_sql end - select_sql = select || " + select_sql = + select || + " DISTINCT topics.id as topic_id, u.id as user_id, topics.created_at, @@ -441,11 +416,13 @@ class TopicTrackingState tags_filter = "" - if muted_tag_ids.present? && ['always', 'only_muted'].include?(SiteSetting.remove_muted_tags_from_latest) - existing_tags_sql = "(select array_agg(tag_id) from topic_tags where topic_tags.topic_id = topics.id)" - muted_tags_array_sql = "ARRAY[#{muted_tag_ids.join(',')}]" + if muted_tag_ids.present? && + %w[always only_muted].include?(SiteSetting.remove_muted_tags_from_latest) + existing_tags_sql = + "(select array_agg(tag_id) from topic_tags where topic_tags.topic_id = topics.id)" + muted_tags_array_sql = "ARRAY[#{muted_tag_ids.join(",")}]" - if SiteSetting.remove_muted_tags_from_latest == 'always' + if SiteSetting.remove_muted_tags_from_latest == "always" tags_filter = <<~SQL NOT ( COALESCE(#{existing_tags_sql}, ARRAY[]::int[]) && #{muted_tags_array_sql} @@ -487,13 +464,9 @@ class TopicTrackingState ) SQL - if topic_id - sql << " AND topics.id = :topic_id" - end + sql << " AND topics.id = :topic_id" if topic_id - unless skip_order - sql << " ORDER BY topics.bumped_at DESC" - end + sql << " ORDER BY topics.bumped_at DESC" unless skip_order sql end @@ -503,7 +476,11 @@ class TopicTrackingState end def self.publish_read_indicator_on_write(topic_id, last_read_post_number, user_id) - topic = Topic.includes(:allowed_groups).select(:highest_post_number, :archetype, :id).find_by(id: topic_id) + topic = + Topic + .includes(:allowed_groups) + .select(:highest_post_number, :archetype, :id) + .find_by(id: topic_id) if topic&.private_message? groups = read_allowed_groups_of(topic) @@ -512,7 +489,11 @@ class TopicTrackingState end def self.publish_read_indicator_on_read(topic_id, last_read_post_number, user_id) - topic = Topic.includes(:allowed_groups).select(:highest_post_number, :archetype, :id).find_by(id: topic_id) + topic = + Topic + .includes(:allowed_groups) + .select(:highest_post_number, :archetype, :id) + .find_by(id: topic_id) if topic&.private_message? groups = read_allowed_groups_of(topic) @@ -523,14 +504,21 @@ class TopicTrackingState end def self.read_allowed_groups_of(topic) - topic.allowed_groups + topic + .allowed_groups .joins(:group_users) .where(publish_read_state: true) - .select('ARRAY_AGG(group_users.user_id) AS members', :name, :id) - .group('groups.id') + .select("ARRAY_AGG(group_users.user_id) AS members", :name, :id) + .group("groups.id") end - def self.update_topic_list_read_indicator(topic, groups, last_read_post_number, user_id, write_event) + def self.update_topic_list_read_indicator( + topic, + groups, + last_read_post_number, + user_id, + write_event + ) return unless last_read_post_number == topic.highest_post_number message = { topic_id: topic.id, show_indicator: write_event }.as_json groups_to_update = [] @@ -546,7 +534,11 @@ class TopicTrackingState end return if groups_to_update.empty? - MessageBus.publish("/private-messages/unread-indicator/#{topic.id}", message, user_ids: groups_to_update.flat_map(&:members)) + MessageBus.publish( + "/private-messages/unread-indicator/#{topic.id}", + message, + user_ids: groups_to_update.flat_map(&:members), + ) end def self.trigger_post_read_count_update(post, groups, last_read_post_number, user_id) @@ -555,4 +547,15 @@ class TopicTrackingState opts = { readers_count: post.readers_count, reader_id: user_id } post.publish_change_to_clients!(:read, opts) end + + def self.secure_category_group_ids(topic) + ids = topic&.category&.secure_group_ids + + if ids.blank? + [Group::AUTO_GROUPS[:admin]] + else + ids + end + end + private_class_method :secure_category_group_ids end diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 638fa4f891..0e00f68f2d 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -2,7 +2,7 @@ class TopicUser < ActiveRecord::Base self.ignored_columns = [ - :highest_seen_post_number # Remove after 01 Jan 2022 + :highest_seen_post_number, # Remove after 01 Jan 2022 ] belongs_to :user @@ -11,20 +11,18 @@ class TopicUser < ActiveRecord::Base # used for serialization attr_accessor :post_action_data - scope :level, lambda { |topic_id, level| - where(topic_id: topic_id) - .where("COALESCE(topic_users.notification_level, :regular) >= :level", - regular: TopicUser.notification_levels[:regular], - level: TopicUser.notification_levels[level]) - } + scope :level, + lambda { |topic_id, level| + where(topic_id: topic_id).where( + "COALESCE(topic_users.notification_level, :regular) >= :level", + regular: TopicUser.notification_levels[:regular], + level: TopicUser.notification_levels[level], + ) + } - scope :tracking, lambda { |topic_id| - level(topic_id, :tracking) - } + scope :tracking, lambda { |topic_id| level(topic_id, :tracking) } - scope :watching, lambda { |topic_id| - level(topic_id, :watching) - } + scope :watching, lambda { |topic_id| level(topic_id, :watching) } def topic_bookmarks Bookmark.where(topic: topic, user: user) @@ -32,38 +30,62 @@ class TopicUser < ActiveRecord::Base # Class methods class << self - # Enums def notification_levels NotificationLevels.topic_levels end def notification_reasons - @notification_reasons ||= Enum.new(created_topic: 1, - user_changed: 2, - user_interacted: 3, - created_post: 4, - auto_watch: 5, - auto_watch_category: 6, - auto_mute_category: 7, - auto_track_category: 8, - plugin_changed: 9, - auto_watch_tag: 10, - auto_mute_tag: 11, - auto_track_tag: 12) + @notification_reasons ||= + Enum.new( + created_topic: 1, + user_changed: 2, + user_interacted: 3, + created_post: 4, + auto_watch: 5, + auto_watch_category: 6, + auto_mute_category: 7, + auto_track_category: 8, + plugin_changed: 9, + auto_watch_tag: 10, + auto_mute_tag: 11, + auto_track_tag: 12, + ) end def auto_notification(user_id, topic_id, reason, notification_level) - should_change = TopicUser - .where(user_id: user_id, topic_id: topic_id) - .where("notifications_reason_id IS NULL OR (notification_level < :max AND notification_level > :min)", max: notification_level, min: notification_levels[:regular]) - .exists? + should_change = + TopicUser + .where(user_id: user_id, topic_id: topic_id) + .where( + "notifications_reason_id IS NULL OR (notification_level < :max AND notification_level > :min)", + max: notification_level, + min: notification_levels[:regular], + ) + .exists? - change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) if should_change + if should_change + change( + user_id, + topic_id, + notification_level: notification_level, + notifications_reason_id: reason, + ) + end end - def auto_notification_for_staging(user_id, topic_id, reason, notification_level = notification_levels[:watching]) - change(user_id, topic_id, notification_level: notification_level, notifications_reason_id: reason) + def auto_notification_for_staging( + user_id, + topic_id, + reason, + notification_level = notification_levels[:watching] + ) + change( + user_id, + topic_id, + notification_level: notification_level, + notifications_reason_id: reason, + ) end def unwatch_categories!(user, category_ids) @@ -80,14 +102,15 @@ class TopicUser < ActiveRecord::Base WHERE t.id = tu.topic_id AND tu.notification_level <> :muted AND category_id IN (:category_ids) AND tu.user_id = :user_id SQL - DB.exec(sql, + DB.exec( + sql, watching: notification_levels[:watching], tracking: notification_levels[:tracking], regular: notification_levels[:regular], muted: notification_levels[:muted], category_ids: category_ids, user_id: user.id, - track_threshold: track_threshold + track_threshold: track_threshold, ) end @@ -119,9 +142,7 @@ class TopicUser < ActiveRecord::Base # it then creates the row instead. def change(user_id, topic_id, attrs) # For plugin compatibility, remove after 01 Jan 2022 - if attrs[:highest_seen_post_number] - attrs.delete(:highest_seen_post_number) - end + attrs.delete(:highest_seen_post_number) if attrs[:highest_seen_post_number] # Sometimes people pass objs instead of the ids. We can handle that. topic_id = topic_id.id if topic_id.is_a?(::Topic) @@ -143,15 +164,17 @@ class TopicUser < ActiveRecord::Base rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all([attrs_sql, *vals]) - if rows == 0 - create_missing_record(user_id, topic_id, attrs) - end + create_missing_record(user_id, topic_id, attrs) if rows == 0 end if attrs[:notification_level] - notification_level_change(user_id, topic_id, attrs[:notification_level], attrs[:notifications_reason_id]) + notification_level_change( + user_id, + topic_id, + attrs[:notification_level], + attrs[:notifications_reason_id], + ) end - rescue ActiveRecord::RecordNotUnique # In case of a race condition to insert, do nothing end @@ -161,67 +184,90 @@ class TopicUser < ActiveRecord::Base message[:notifications_reason_id] = reason_id if reason_id MessageBus.publish("/topic/#{topic_id}", message, user_ids: [user_id]) - DiscourseEvent.trigger(:topic_notification_level_changed, + DiscourseEvent.trigger( + :topic_notification_level_changed, notification_level, user_id, - topic_id + topic_id, ) - end def create_missing_record(user_id, topic_id, attrs) now = DateTime.now unless attrs[:notification_level] - category_notification_level = CategoryUser.where(user_id: user_id) - .where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id) - .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], - CategoryUser.notification_levels[:tracking]]) - .order("notification_level DESC") - .limit(1) - .pluck(:notification_level) - .first + category_notification_level = + CategoryUser + .where(user_id: user_id) + .where("category_id IN (SELECT category_id FROM topics WHERE id = :id)", id: topic_id) + .where( + "notification_level IN (:levels)", + levels: [ + CategoryUser.notification_levels[:watching], + CategoryUser.notification_levels[:tracking], + ], + ) + .order("notification_level DESC") + .limit(1) + .pluck(:notification_level) + .first - tag_notification_level = TagUser.where(user_id: user_id) - .where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id) - .where("notification_level IN (:levels)", levels: [CategoryUser.notification_levels[:watching], - CategoryUser.notification_levels[:tracking]]) - .order("notification_level DESC") - .limit(1) - .pluck(:notification_level) - .first + tag_notification_level = + TagUser + .where(user_id: user_id) + .where("tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :id)", id: topic_id) + .where( + "notification_level IN (:levels)", + levels: [ + CategoryUser.notification_levels[:watching], + CategoryUser.notification_levels[:tracking], + ], + ) + .order("notification_level DESC") + .limit(1) + .pluck(:notification_level) + .first - if category_notification_level && !(tag_notification_level && (tag_notification_level > category_notification_level)) + if category_notification_level && + !(tag_notification_level && (tag_notification_level > category_notification_level)) attrs[:notification_level] = category_notification_level attrs[:notifications_changed_at] = DateTime.now - attrs[:notifications_reason_id] = category_notification_level == CategoryUser.notification_levels[:watching] ? - TopicUser.notification_reasons[:auto_watch_category] : + attrs[:notifications_reason_id] = ( + if category_notification_level == CategoryUser.notification_levels[:watching] + TopicUser.notification_reasons[:auto_watch_category] + else TopicUser.notification_reasons[:auto_track_category] - + end + ) elsif tag_notification_level attrs[:notification_level] = tag_notification_level attrs[:notifications_changed_at] = DateTime.now - attrs[:notifications_reason_id] = tag_notification_level == TagUser.notification_levels[:watching] ? - TopicUser.notification_reasons[:auto_watch_tag] : + attrs[:notifications_reason_id] = ( + if tag_notification_level == TagUser.notification_levels[:watching] + TopicUser.notification_reasons[:auto_watch_tag] + else TopicUser.notification_reasons[:auto_track_tag] + end + ) end - end unless attrs[:notification_level] if Topic.private_messages.where(id: topic_id).exists? && - Notification.where( - user_id: user_id, - topic_id: topic_id, - notification_type: Notification.types[:invited_to_private_message] - ).exists? - - group_notification_level = Group - .joins("LEFT OUTER JOIN group_users gu ON gu.group_id = groups.id AND gu.user_id = #{user_id}") - .joins("LEFT OUTER JOIN topic_allowed_groups tag ON tag.topic_id = #{topic_id}") - .where("gu.id IS NOT NULL AND tag.id IS NOT NULL") - .pluck(:default_notification_level) - .first + Notification.where( + user_id: user_id, + topic_id: topic_id, + notification_type: Notification.types[:invited_to_private_message], + ).exists? + group_notification_level = + Group + .joins( + "LEFT OUTER JOIN group_users gu ON gu.group_id = groups.id AND gu.user_id = #{user_id}", + ) + .joins("LEFT OUTER JOIN topic_allowed_groups tag ON tag.topic_id = #{topic_id}") + .where("gu.id IS NOT NULL AND tag.id IS NOT NULL") + .pluck(:default_notification_level) + .first if group_notification_level.present? attrs[:notification_level] = group_notification_level @@ -229,7 +275,8 @@ class TopicUser < ActiveRecord::Base attrs[:notification_level] = notification_levels[:watching] end else - auto_track_after = UserOption.where(user_id: user_id).pluck_first(:auto_track_topics_after_msecs) + auto_track_after = + UserOption.where(user_id: user_id).pluck_first(:auto_track_topics_after_msecs) auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed].to_i || 0) @@ -238,12 +285,14 @@ class TopicUser < ActiveRecord::Base end end - TopicUser.create!(attrs.merge!( - user_id: user_id, - topic_id: topic_id, - first_visited_at: now , - last_visited_at: now - )) + TopicUser.create!( + attrs.merge!( + user_id: user_id, + topic_id: topic_id, + first_visited_at: now, + last_visited_at: now, + ), + ) DiscourseEvent.trigger(:topic_first_visited_by_user, topic_id, user_id) end @@ -252,9 +301,7 @@ class TopicUser < ActiveRecord::Base now = DateTime.now rows = TopicUser.where(topic_id: topic_id, user_id: user_id).update_all(last_visited_at: now) - if rows == 0 - change(user_id, topic_id, last_visited_at: now, first_visited_at: now) - end + change(user_id, topic_id, last_visited_at: now, first_visited_at: now) if rows == 0 end # Update the last read and the last seen post count, but only if it doesn't exist. @@ -289,7 +336,8 @@ class TopicUser < ActiveRecord::Base t.archetype SQL - INSERT_TOPIC_USER_SQL = "INSERT INTO topic_users (user_id, topic_id, last_read_post_number, last_visited_at, first_visited_at, notification_level) + INSERT_TOPIC_USER_SQL = + "INSERT INTO topic_users (user_id, topic_id, last_read_post_number, last_visited_at, first_visited_at, notification_level) SELECT :user_id, :topic_id, :post_number, :now, :now, :new_status FROM topics AS ft JOIN users u on u.id = :user_id @@ -309,7 +357,7 @@ class TopicUser < ActiveRecord::Base now: DateTime.now, msecs: msecs, tracking: notification_levels[:tracking], - threshold: SiteSetting.default_other_auto_track_topics_after_msecs + threshold: SiteSetting.default_other_auto_track_topics_after_msecs, } rows = DB.query(UPDATE_TOPIC_USER_SQL, args) @@ -327,23 +375,22 @@ class TopicUser < ActiveRecord::Base post_number: post_number, user: user, notification_level: after, - private_message: archetype == Archetype.private_message + private_message: archetype == Archetype.private_message, ) end - if new_posts_read > 0 - user.update_posts_read!(new_posts_read, mobile: opts[:mobile]) - end + user.update_posts_read!(new_posts_read, mobile: opts[:mobile]) if new_posts_read > 0 - if before != after - notification_level_change(user.id, topic_id, after, nil) - end + notification_level_change(user.id, topic_id, after, nil) if before != after end if rows.length == 0 # The user read at least one post in a topic that they haven't viewed before. args[:new_status] = notification_levels[:regular] - if (user.user_option.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs) == 0 + if ( + user.user_option.auto_track_topics_after_msecs || + SiteSetting.default_other_auto_track_topics_after_msecs + ) == 0 args[:new_status] = notification_levels[:tracking] end @@ -352,10 +399,7 @@ class TopicUser < ActiveRecord::Base post_number: post_number, user: user, notification_level: args[:new_status], - private_message: Topic.exists?( - archetype: Archetype.private_message, - id: topic_id - ) + private_message: Topic.exists?(archetype: Archetype.private_message, id: topic_id), ) user.update_posts_read!(new_posts_read, mobile: opts[:mobile]) @@ -387,14 +431,8 @@ class TopicUser < ActiveRecord::Base TopicTrackingState end - klass.publish_read( - topic_id, - post_number, - user, - notification_level - ) + klass.publish_read(topic_id, post_number, user, notification_level) end - end # Update the cached topic_user.liked column based on data @@ -441,16 +479,17 @@ class TopicUser < ActiveRecord::Base WHERE x.topic_id = tu.topic_id AND x.user_id = tu.user_id AND x.state != tu.#{action_type_name} SQL - if user_id - builder.where("tu2.user_id IN (:user_id)", user_id: user_id) - end + builder.where("tu2.user_id IN (:user_id)", user_id: user_id) if user_id - if topic_id - builder.where("tu2.topic_id IN (:topic_id)", topic_id: topic_id) - end + builder.where("tu2.topic_id IN (:topic_id)", topic_id: topic_id) if topic_id if post_id - builder.where("tu2.topic_id IN (SELECT topic_id FROM posts WHERE id IN (:post_id))", post_id: post_id) if !topic_id + if !topic_id + builder.where( + "tu2.topic_id IN (SELECT topic_id FROM posts WHERE id IN (:post_id))", + post_id: post_id, + ) + end builder.where(<<~SQL, post_id: post_id) tu2.user_id IN ( SELECT user_id FROM post_actions @@ -515,13 +554,10 @@ SQL ) SQL - if topic_id - builder.where("t.topic_id = :topic_id", topic_id: topic_id) - end + builder.where("t.topic_id = :topic_id", topic_id: topic_id) if topic_id builder.exec end - end # == Schema Information diff --git a/app/models/topic_view_item.rb b/app/models/topic_view_item.rb index d37e8ec7a6..4472ed4b1d 100644 --- a/app/models/topic_view_item.rb +++ b/app/models/topic_view_item.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'ipaddr' +require "ipaddr" # awkward TopicView is taken class TopicViewItem < ActiveRecord::Base - self.table_name = 'topic_views' + self.table_name = "topic_views" belongs_to :user belongs_to :topic validates_presence_of :topic_id, :ip_address, :viewed_at @@ -24,7 +24,8 @@ class TopicViewItem < ActiveRecord::Base TopicViewItem.transaction do # this is called real frequently, working hard to avoid exceptions - sql = "INSERT INTO topic_views (topic_id, ip_address, viewed_at, user_id) + sql = + "INSERT INTO topic_views (topic_id, ip_address, viewed_at, user_id) SELECT :topic_id, :ip_address, :viewed_at, :user_id WHERE NOT EXISTS ( SELECT 1 FROM topic_views @@ -42,17 +43,18 @@ class TopicViewItem < ActiveRecord::Base result = builder.exec(topic_id: topic_id, ip_address: ip, viewed_at: at, user_id: user_id) - Topic.where(id: topic_id).update_all 'views = views + 1' + Topic.where(id: topic_id).update_all "views = views + 1" if result > 0 - UserStat.where(user_id: user_id).update_all 'topics_entered = topics_entered + 1' if user_id + if user_id + UserStat.where(user_id: user_id).update_all "topics_entered = topics_entered + 1" + end end # Update the views count in the parent, if it exists. end end end - end # == Schema Information diff --git a/app/models/translation_override.rb b/app/models/translation_override.rb index 932ac08fa6..73137243ed 100644 --- a/app/models/translation_override.rb +++ b/app/models/translation_override.rb @@ -3,17 +3,17 @@ class TranslationOverride < ActiveRecord::Base # Allowlist i18n interpolation keys that can be included when customizing translations ALLOWED_CUSTOM_INTERPOLATION_KEYS = { - [ - "user_notifications.user_", - "user_notifications.only_reply_by_email", - "user_notifications.reply_by_email", - "user_notifications.visit_link_to_respond", - "user_notifications.header_instructions", - "user_notifications.pm_participants", - "unsubscribe_mailing_list", - "unsubscribe_link_and_mail", - "unsubscribe_link", - ] => %w{ + %w[ + user_notifications.user_ + user_notifications.only_reply_by_email + user_notifications.reply_by_email + user_notifications.visit_link_to_respond + user_notifications.header_instructions + user_notifications.pm_participants + unsubscribe_mailing_list + unsubscribe_link_and_mail + unsubscribe_link + ] => %w[ topic_title topic_title_url_encoded message @@ -34,12 +34,13 @@ class TranslationOverride < ActiveRecord::Base optional_pm optional_cat optional_tags - } + ], } include HasSanitizableFields include ActiveSupport::Deprecation::DeprecatedConstantAccessor - deprecate_constant 'CUSTOM_INTERPOLATION_KEYS_WHITELIST', 'TranslationOverride::ALLOWED_CUSTOM_INTERPOLATION_KEYS' + deprecate_constant "CUSTOM_INTERPOLATION_KEYS_WHITELIST", + "TranslationOverride::ALLOWED_CUSTOM_INTERPOLATION_KEYS" validates_uniqueness_of :translation_key, scope: :locale validates_presence_of :locale, :translation_key, :value @@ -50,10 +51,11 @@ class TranslationOverride < ActiveRecord::Base params = { locale: locale, translation_key: key } translation_override = find_or_initialize_by(params) - sanitized_value = translation_override.sanitize_field(value, additional_attributes: ['data-auto-route']) + sanitized_value = + translation_override.sanitize_field(value, additional_attributes: ["data-auto-route"]) data = { value: sanitized_value } - if key.end_with?('_MF') + if key.end_with?("_MF") _, filename = JsLocaleHelper.find_message_format_locale([locale], fallback_to_english: false) data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, sanitized_value) end @@ -74,22 +76,18 @@ class TranslationOverride < ActiveRecord::Base overrides = TranslationOverride.pluck(:locale, :translation_key) overrides = overrides.group_by(&:first).map { |k, a| [k, a.map(&:last)] } - overrides.each do |locale, keys| - clear_cached_keys!(locale, keys) - end + overrides.each { |locale, keys| clear_cached_keys!(locale, keys) } end def self.reload_locale! I18n.reload! ExtraLocalesController.clear_cache! - MessageBus.publish('/i18n-flush', refresh: true) + MessageBus.publish("/i18n-flush", refresh: true) end def self.clear_cached_keys!(locale, keys) should_clear_anon_cache = false - keys.each do |key| - should_clear_anon_cache |= expire_cache(locale, key) - end + keys.each { |key| should_clear_anon_cache |= expire_cache(locale, key) } Site.clear_anon_cache! if should_clear_anon_cache end @@ -99,9 +97,9 @@ class TranslationOverride < ActiveRecord::Base end def self.expire_cache(locale, key) - if key.starts_with?('post_action_types.') + if key.starts_with?("post_action_types.") ApplicationSerializer.expire_cache_fragment!("post_action_types_#{locale}") - elsif key.starts_with?('topic_flag_types.') + elsif key.starts_with?("topic_flag_types.") ApplicationSerializer.expire_cache_fragment!("post_action_flag_types_#{locale}") else return false @@ -119,9 +117,7 @@ class TranslationOverride < ActiveRecord::Base def check_interpolation_keys transformed_key = transform_pluralized_key(translation_key) - original_text = I18n.overrides_disabled do - I18n.t(transformed_key, locale: :en) - end + original_text = I18n.overrides_disabled { I18n.t(transformed_key, locale: :en) } if original_text original_interpolation_keys = I18nInterpolationKeysFinder.find(original_text) @@ -129,20 +125,21 @@ class TranslationOverride < ActiveRecord::Base custom_interpolation_keys = [] ALLOWED_CUSTOM_INTERPOLATION_KEYS.select do |keys, value| - if keys.any? { |key| transformed_key.start_with?(key) } - custom_interpolation_keys = value - end + custom_interpolation_keys = value if keys.any? { |key| transformed_key.start_with?(key) } end - invalid_keys = (original_interpolation_keys | new_interpolation_keys) - - original_interpolation_keys - - custom_interpolation_keys + invalid_keys = + (original_interpolation_keys | new_interpolation_keys) - original_interpolation_keys - + custom_interpolation_keys if invalid_keys.present? - self.errors.add(:base, I18n.t( - 'activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys', - keys: invalid_keys.join(', ') - )) + self.errors.add( + :base, + I18n.t( + "activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys", + keys: invalid_keys.join(", "), + ), + ) false end @@ -151,7 +148,7 @@ class TranslationOverride < ActiveRecord::Base def transform_pluralized_key(key) match = key.match(/(.*)\.(zero|two|few|many)$/) - match ? match.to_a.second + '.other' : key + match ? match.to_a.second + ".other" : key end end diff --git a/app/models/trust_level3_requirements.rb b/app/models/trust_level3_requirements.rb index 61970b2b9e..8c2889d6fa 100644 --- a/app/models/trust_level3_requirements.rb +++ b/app/models/trust_level3_requirements.rb @@ -3,13 +3,12 @@ # This class performs calculations to determine if a user qualifies for # the Leader (3) trust level. class TrustLevel3Requirements - class PenaltyCounts attr_reader :silenced, :suspended def initialize(user, row) - @silenced = row['silence_count'] || 0 - @suspended = row['suspend_count'] || 0 + @silenced = row["silence_count"] || 0 + @suspended = row["suspend_count"] || 0 # If penalty started more than 6 months ago and still continues, it will # not be selected by the query from 'penalty_counts'. @@ -27,19 +26,32 @@ class TrustLevel3Requirements LOW_WATER_MARK = 0.9 FORGIVENESS_PERIOD = 6.months - attr_accessor :days_visited, :min_days_visited, - :num_topics_replied_to, :min_topics_replied_to, - :topics_viewed, :min_topics_viewed, - :posts_read, :min_posts_read, - :topics_viewed_all_time, :min_topics_viewed_all_time, - :posts_read_all_time, :min_posts_read_all_time, - :num_flagged_posts, :max_flagged_posts, - :num_likes_given, :min_likes_given, - :num_likes_received, :min_likes_received, - :num_likes_received, :min_likes_received, - :num_likes_received_days, :min_likes_received_days, - :num_likes_received_users, :min_likes_received_users, - :trust_level_locked, :on_grace_period + attr_accessor :days_visited, + :min_days_visited, + :num_topics_replied_to, + :min_topics_replied_to, + :topics_viewed, + :min_topics_viewed, + :posts_read, + :min_posts_read, + :topics_viewed_all_time, + :min_topics_viewed_all_time, + :posts_read_all_time, + :min_posts_read_all_time, + :num_flagged_posts, + :max_flagged_posts, + :num_likes_given, + :min_likes_given, + :num_likes_received, + :min_likes_received, + :num_likes_received, + :min_likes_received, + :num_likes_received_days, + :min_likes_received_days, + :num_likes_received_users, + :min_likes_received_users, + :trust_level_locked, + :on_grace_period def initialize(user) @user = user @@ -48,18 +60,12 @@ class TrustLevel3Requirements def requirements_met? return false if trust_level_locked - (!@user.suspended?) && - (!@user.silenced?) && - penalty_counts.total == 0 && - days_visited >= min_days_visited && - num_topics_replied_to >= min_topics_replied_to && - topics_viewed >= min_topics_viewed && - posts_read >= min_posts_read && - num_flagged_posts <= max_flagged_posts && - num_flagged_by_users <= max_flagged_by_users && + (!@user.suspended?) && (!@user.silenced?) && penalty_counts.total == 0 && + days_visited >= min_days_visited && num_topics_replied_to >= min_topics_replied_to && + topics_viewed >= min_topics_viewed && posts_read >= min_posts_read && + num_flagged_posts <= max_flagged_posts && num_flagged_by_users <= max_flagged_by_users && topics_viewed_all_time >= min_topics_viewed_all_time && - posts_read_all_time >= min_posts_read_all_time && - num_likes_given >= min_likes_given && + posts_read_all_time >= min_posts_read_all_time && num_likes_given >= min_likes_given && num_likes_received >= min_likes_received && num_likes_received_users >= min_likes_received_users && num_likes_received_days >= min_likes_received_days @@ -69,14 +75,11 @@ class TrustLevel3Requirements return false if trust_level_locked return false if SiteSetting.default_trust_level > 2 - @user.suspended? || - @user.silenced? || - penalty_counts.total > 0 || + @user.suspended? || @user.silenced? || penalty_counts.total > 0 || days_visited < min_days_visited * LOW_WATER_MARK || num_topics_replied_to < min_topics_replied_to * LOW_WATER_MARK || topics_viewed < min_topics_viewed * LOW_WATER_MARK || - posts_read < min_posts_read * LOW_WATER_MARK || - num_flagged_posts > max_flagged_posts || + posts_read < min_posts_read * LOW_WATER_MARK || num_flagged_posts > max_flagged_posts || num_flagged_by_users > max_flagged_by_users || topics_viewed_all_time < min_topics_viewed_all_time || posts_read_all_time < min_posts_read_all_time || @@ -110,7 +113,7 @@ class TrustLevel3Requirements unsilence_user: UserHistory.actions[:unsilence_user], suspend_user: UserHistory.actions[:suspend_user], unsuspend_user: UserHistory.actions[:unsuspend_user], - since: FORGIVENESS_PERIOD.ago + since: FORGIVENESS_PERIOD.ago, } sql = <<~SQL @@ -151,31 +154,38 @@ class TrustLevel3Requirements end def topics_viewed_query - TopicViewItem.where(user_id: @user.id) + TopicViewItem + .where(user_id: @user.id) .joins(:topic) .where("topics.archetype <> ?", Archetype.private_message) .select("topic_id") end def topics_viewed - topics_viewed_query.where('viewed_at > ?', time_period.days.ago).count + topics_viewed_query.where("viewed_at > ?", time_period.days.ago).count end def min_topics_viewed [ - (TrustLevel3Requirements.num_topics_in_time_period.to_i * (SiteSetting.tl3_requires_topics_viewed.to_f / 100.0)).round, - SiteSetting.tl3_requires_topics_viewed_cap + ( + TrustLevel3Requirements.num_topics_in_time_period.to_i * + (SiteSetting.tl3_requires_topics_viewed.to_f / 100.0) + ).round, + SiteSetting.tl3_requires_topics_viewed_cap, ].min end def posts_read - @user.user_visits.where('visited_at > ?', time_period.days.ago).pluck(:posts_read).sum + @user.user_visits.where("visited_at > ?", time_period.days.ago).pluck(:posts_read).sum end def min_posts_read [ - (TrustLevel3Requirements.num_posts_in_time_period.to_i * (SiteSetting.tl3_requires_posts_read.to_f / 100.0)).round, - SiteSetting.tl3_requires_posts_read_cap + ( + TrustLevel3Requirements.num_posts_in_time_period.to_i * + (SiteSetting.tl3_requires_posts_read.to_f / 100.0) + ).round, + SiteSetting.tl3_requires_posts_read_cap, ].min end @@ -196,12 +206,14 @@ class TrustLevel3Requirements end def num_flagged_posts - PostAction.with_deleted + PostAction + .with_deleted .where(post_id: flagged_post_ids) .where.not(user_id: @user.id) .where.not(agreed_at: nil) .pluck(:post_id) - .uniq.count + .uniq + .count end def max_flagged_posts @@ -209,12 +221,15 @@ class TrustLevel3Requirements end def num_flagged_by_users - @_num_flagged_by_users ||= PostAction.with_deleted - .where(post_id: flagged_post_ids) - .where.not(user_id: @user.id) - .where.not(agreed_at: nil) - .pluck(:user_id) - .uniq.count + @_num_flagged_by_users ||= + PostAction + .with_deleted + .where(post_id: flagged_post_ids) + .where.not(user_id: @user.id) + .where.not(agreed_at: nil) + .pluck(:user_id) + .uniq + .count end def max_flagged_by_users @@ -222,7 +237,8 @@ class TrustLevel3Requirements end def num_likes_given - UserAction.where(user_id: @user.id, action_type: UserAction::LIKE) + UserAction + .where(user_id: @user.id, action_type: UserAction::LIKE) .where("user_actions.created_at > ?", time_period.days.ago) .joins(:target_topic) .where("topics.archetype <> ?", Archetype.private_message) @@ -234,7 +250,8 @@ class TrustLevel3Requirements end def num_likes_received_query - UserAction.where(user_id: @user.id, action_type: UserAction::WAS_LIKED) + UserAction + .where(user_id: @user.id, action_type: UserAction::WAS_LIKED) .where("user_actions.created_at > ?", time_period.days.ago) .joins(:target_topic) .where("topics.archetype <> ?", Archetype.private_message) @@ -250,7 +267,7 @@ class TrustLevel3Requirements def num_likes_received_days # don't do a COUNT(DISTINCT date(created_at)) here! - num_likes_received_query.pluck('date(user_actions.created_at)').uniq.size + num_likes_received_query.pluck("date(user_actions.created_at)").uniq.size end def min_likes_received_days @@ -275,28 +292,36 @@ class TrustLevel3Requirements CACHE_DURATION = 1.day.seconds - 60 NUM_TOPICS_KEY = "tl3_num_topics" - NUM_POSTS_KEY = "tl3_num_posts" + NUM_POSTS_KEY = "tl3_num_posts" def self.num_topics_in_time_period - Discourse.redis.get(NUM_TOPICS_KEY) || begin - count = Topic.listable_topics.visible.created_since(SiteSetting.tl3_time_period.days.ago).count - Discourse.redis.setex NUM_TOPICS_KEY, CACHE_DURATION, count - count - end + Discourse.redis.get(NUM_TOPICS_KEY) || + begin + count = + Topic.listable_topics.visible.created_since(SiteSetting.tl3_time_period.days.ago).count + Discourse.redis.setex NUM_TOPICS_KEY, CACHE_DURATION, count + count + end end def self.num_posts_in_time_period - Discourse.redis.get(NUM_POSTS_KEY) || begin - count = Post.public_posts.visible.created_since(SiteSetting.tl3_time_period.days.ago).count - Discourse.redis.setex NUM_POSTS_KEY, CACHE_DURATION, count - count - end + Discourse.redis.get(NUM_POSTS_KEY) || + begin + count = Post.public_posts.visible.created_since(SiteSetting.tl3_time_period.days.ago).count + Discourse.redis.setex NUM_POSTS_KEY, CACHE_DURATION, count + count + end end def flagged_post_ids - @_flagged_post_ids ||= @user.posts - .with_deleted - .where('created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)', time_period.days.ago) - .pluck(:id) + @_flagged_post_ids ||= + @user + .posts + .with_deleted + .where( + "created_at > ? AND (spam_count > 0 OR inappropriate_count > 0)", + time_period.days.ago, + ) + .pluck(:id) end end diff --git a/app/models/trust_level_and_staff_and_disabled_setting.rb b/app/models/trust_level_and_staff_and_disabled_setting.rb index 3b1a4336c2..4edc2c9519 100644 --- a/app/models/trust_level_and_staff_and_disabled_setting.rb +++ b/app/models/trust_level_and_staff_and_disabled_setting.rb @@ -6,12 +6,12 @@ class TrustLevelAndStaffAndDisabledSetting < TrustLevelAndStaffSetting end def self.valid_values - ['disabled'] + TrustLevel.valid_range.to_a + special_groups + ["disabled"] + TrustLevel.valid_range.to_a + special_groups end def self.translation(value) - if value == 'disabled' - I18n.t('site_settings.disabled') + if value == "disabled" + I18n.t("site_settings.disabled") else super end @@ -19,11 +19,11 @@ class TrustLevelAndStaffAndDisabledSetting < TrustLevelAndStaffSetting def self.matches?(value, user) case value - when 'disabled' + when "disabled" false - when 'staff' + when "staff" user.staff? - when 'admin' + when "admin" user.admin? else user.has_trust_level?(value.to_i) || user.staff? diff --git a/app/models/trust_level_and_staff_setting.rb b/app/models/trust_level_and_staff_setting.rb index a72f634667..17cc8bead7 100644 --- a/app/models/trust_level_and_staff_setting.rb +++ b/app/models/trust_level_and_staff_setting.rb @@ -2,9 +2,7 @@ class TrustLevelAndStaffSetting < TrustLevelSetting def self.valid_value?(val) - special_group?(val) || - (val.to_i.to_s == val.to_s && - valid_values.any? { |v| v == val.to_i }) + special_group?(val) || (val.to_i.to_s == val.to_s && valid_values.any? { |v| v == val.to_i }) end def self.valid_values @@ -16,7 +14,7 @@ class TrustLevelAndStaffSetting < TrustLevelSetting end def self.special_groups - ['staff', 'admin'] + %w[staff admin] end def self.translation(value) diff --git a/app/models/trust_level_setting.rb b/app/models/trust_level_setting.rb index fc48bf8a10..e1d4f2c0ce 100644 --- a/app/models/trust_level_setting.rb +++ b/app/models/trust_level_setting.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true class TrustLevelSetting < EnumSiteSetting - def self.valid_value?(val) - val.to_i.to_s == val.to_s && - valid_values.any? { |v| v == val.to_i } + val.to_i.to_s == val.to_s && valid_values.any? { |v| v == val.to_i } end def self.values - valid_values.map do |value| - { name: translation(value), value: value } - end + valid_values.map { |value| { name: translation(value), value: value } } end def self.valid_values @@ -18,11 +14,7 @@ class TrustLevelSetting < EnumSiteSetting end def self.translation(value) - I18n.t( - "js.trust_levels.detailed_name", - level: value, - name: TrustLevel.name(value) - ) + I18n.t("js.trust_levels.detailed_name", level: value, name: TrustLevel.name(value)) end private_class_method :valid_values diff --git a/app/models/unsubscribe_key.rb b/app/models/unsubscribe_key.rb index da81fdfb33..fbc199e169 100644 --- a/app/models/unsubscribe_key.rb +++ b/app/models/unsubscribe_key.rb @@ -7,9 +7,9 @@ class UnsubscribeKey < ActiveRecord::Base before_create :generate_random_key - ALL_TYPE = 'all' - DIGEST_TYPE = 'digest' - TOPIC_TYPE = 'topic' + ALL_TYPE = "all" + DIGEST_TYPE = "digest" + TOPIC_TYPE = "topic" class << self def create_key_for(user, type, post: nil) @@ -32,7 +32,7 @@ class UnsubscribeKey < ActiveRecord::Base strategies = { DIGEST_TYPE => EmailControllerHelper::DigestEmailUnsubscriber, TOPIC_TYPE => EmailControllerHelper::TopicEmailUnsubscriber, - ALL_TYPE => EmailControllerHelper::BaseEmailUnsubscriber + ALL_TYPE => EmailControllerHelper::BaseEmailUnsubscriber, } DiscoursePluginRegistry.email_unsubscribers.each do |unsubcriber| diff --git a/app/models/upload.rb b/app/models/upload.rb index ad004f6e66..964ef52d1d 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -8,12 +8,12 @@ class Upload < ActiveRecord::Base SHA1_LENGTH = 40 SEEDED_ID_THRESHOLD = 0 - URL_REGEX ||= /(\/original\/\dX[\/\.\w]*\/(\h+)[\.\w]*)/ + URL_REGEX ||= %r{(/original/\dX[/\.\w]*/(\h+)[\.\w]*)} MAX_IDENTIFY_SECONDS = 5 DOMINANT_COLOR_COMMAND_TIMEOUT_SECONDS = 5 belongs_to :user - belongs_to :access_control_post, class_name: 'Post' + belongs_to :access_control_post, class_name: "Post" # when we access this post we don't care if the post # is deleted @@ -25,7 +25,7 @@ class Upload < ActiveRecord::Base has_many :optimized_images, dependent: :destroy has_many :user_uploads, dependent: :destroy has_many :upload_references, dependent: :destroy - has_many :posts, through: :upload_references, source: :target, source_type: 'Post' + has_many :posts, through: :upload_references, source: :target, source_type: "Post" has_many :topic_thumbnails attr_accessor :for_group_message @@ -44,7 +44,9 @@ class Upload < ActiveRecord::Base before_destroy do UserProfile.where(card_background_upload_id: self.id).update_all(card_background_upload_id: nil) - UserProfile.where(profile_background_upload_id: self.id).update_all(profile_background_upload_id: nil) + UserProfile.where(profile_background_upload_id: self.id).update_all( + profile_background_upload_id: nil, + ) end after_destroy do @@ -56,11 +58,7 @@ class Upload < ActiveRecord::Base scope :by_users, -> { where("uploads.id > ?", SEEDED_ID_THRESHOLD) } def self.verification_statuses - @verification_statuses ||= Enum.new( - unchecked: 1, - verified: 2, - invalid_etag: 3 - ) + @verification_statuses ||= Enum.new(unchecked: 1, verified: 2, invalid_etag: 3) end def self.add_unused_callback(&block) @@ -88,9 +86,9 @@ class Upload < ActiveRecord::Base end def self.with_no_non_post_relations - self - .joins("LEFT JOIN upload_references ur ON ur.upload_id = uploads.id AND ur.target_type != 'Post'") - .where("ur.upload_id IS NULL") + self.joins( + "LEFT JOIN upload_references ur ON ur.upload_id = uploads.id AND ur.target_type != 'Post'", + ).where("ur.upload_id IS NULL") end def initialize(*args) @@ -114,18 +112,14 @@ class Upload < ActiveRecord::Base return unless SiteSetting.create_thumbnails? opts ||= {} - if get_optimized_image(width, height, opts) - save(validate: false) - end + save(validate: false) if get_optimized_image(width, height, opts) end # this method attempts to correct old incorrect extensions def get_optimized_image(width, height, opts = nil) opts ||= {} - if (!extension || extension.length == 0) - fix_image_extension - end + fix_image_extension if (!extension || extension.length == 0) opts = opts.merge(raise_on_error: true) begin @@ -152,9 +146,7 @@ class Upload < ActiveRecord::Base File.read(original_path) ensure - if external_copy - File.unlink(external_copy.path) - end + File.unlink(external_copy.path) if external_copy end def fix_image_extension @@ -164,18 +156,28 @@ class Upload < ActiveRecord::Base # this is relatively cheap once cached original_path = Discourse.store.path_for(self) if original_path.blank? - external_copy = Discourse.store.download(self) rescue nil + external_copy = + begin + Discourse.store.download(self) + rescue StandardError + nil + end original_path = external_copy.try(:path) end - image_info = FastImage.new(original_path) rescue nil + image_info = + begin + FastImage.new(original_path) + rescue StandardError + nil + end new_extension = image_info&.type&.to_s || "unknown" if new_extension != self.extension self.update_columns(extension: new_extension) true end - rescue + rescue StandardError self.update_columns(extension: "unknown") true end @@ -211,7 +213,9 @@ class Upload < ActiveRecord::Base def self.consider_for_reuse(upload, post) return upload if !SiteSetting.secure_uploads? || upload.blank? || post.blank? - return nil if !upload.matching_access_control_post?(post) || upload.uploaded_before_secure_uploads_enabled? + if !upload.matching_access_control_post?(post) || upload.uploaded_before_secure_uploads_enabled? + return nil + end upload end @@ -220,7 +224,8 @@ class Upload < ActiveRecord::Base # have secure-uploads in the URL e.g. /t/secure-uploads-are-cool/223452 route = UrlHelper.rails_route_from_url(url) return false if route.blank? - route[:action] == "show_secure" && route[:controller] == "uploads" && FileHelper.is_supported_media?(url) + route[:action] == "show_secure" && route[:controller] == "uploads" && + FileHelper.is_supported_media?(url) rescue ActionController::RoutingError false end @@ -239,17 +244,14 @@ class Upload < ActiveRecord::Base controller: "uploads", action: "show_secure", path: uri.path[1..-1], - only_path: true + only_path: true, ) end def self.short_path(sha1:, extension:) @url_helpers ||= Rails.application.routes.url_helpers - @url_helpers.upload_short_path( - base62: self.base62_sha1(sha1), - extension: extension - ) + @url_helpers.upload_short_path(base62: self.base62_sha1(sha1), extension: extension) end def self.base62_sha1(sha1) @@ -261,7 +263,7 @@ class Upload < ActiveRecord::Base end def local? - !(url =~ /^(https?:)?\/\//) + !(url =~ %r{^(https?:)?//}) end def fix_dimensions! @@ -275,8 +277,19 @@ class Upload < ActiveRecord::Base end begin - if extension == 'svg' - w, h = Discourse::Utils.execute_command("identify", "-format", "%w %h", path, timeout: MAX_IDENTIFY_SECONDS).split(' ') rescue [0, 0] + if extension == "svg" + w, h = + begin + Discourse::Utils.execute_command( + "identify", + "-format", + "%w %h", + path, + timeout: MAX_IDENTIFY_SECONDS, + ).split(" ") + rescue StandardError + [0, 0] + end else w, h = FastImage.new(path, raise_on_failure: true).size end @@ -290,7 +303,7 @@ class Upload < ActiveRecord::Base width: width, height: height, thumbnail_width: thumbnail_width, - thumbnail_height: thumbnail_height + thumbnail_height: thumbnail_height, ) rescue => e Discourse.warn_exception(e, message: "Error getting image dimensions") @@ -337,9 +350,7 @@ class Upload < ActiveRecord::Base def calculate_dominant_color!(local_path = nil) color = nil - if !FileHelper.is_supported_image?("image.#{extension}") || extension == "svg" - color = "" - end + color = "" if !FileHelper.is_supported_image?("image.#{extension}") || extension == "svg" if color.nil? local_path ||= @@ -360,37 +371,39 @@ class Upload < ActiveRecord::Base color = "" end - color ||= begin - data = Discourse::Utils.execute_command( - "nice", - "-n", - "10", - "convert", - local_path, - "-resize", - "1x1", - "-define", - "histogram:unique-colors=true", - "-format", - "%c", - "histogram:info:", - timeout: DOMINANT_COLOR_COMMAND_TIMEOUT_SECONDS - ) + color ||= + begin + data = + Discourse::Utils.execute_command( + "nice", + "-n", + "10", + "convert", + local_path, + "-resize", + "1x1", + "-define", + "histogram:unique-colors=true", + "-format", + "%c", + "histogram:info:", + timeout: DOMINANT_COLOR_COMMAND_TIMEOUT_SECONDS, + ) - # Output format: - # 1: (110.873,116.226,93.8821) #6F745E srgb(43.4798%,45.5789%,36.8165%) + # Output format: + # 1: (110.873,116.226,93.8821) #6F745E srgb(43.4798%,45.5789%,36.8165%) - color = data[/#([0-9A-F]{6})/, 1] + color = data[/#([0-9A-F]{6})/, 1] - raise "Calculated dominant color but unable to parse output:\n#{data}" if color.nil? + raise "Calculated dominant color but unable to parse output:\n#{data}" if color.nil? - color - rescue Discourse::Utils::CommandError => e - # Timeout or unable to parse image - # This can happen due to bad user input - ignore and save - # an empty string to prevent re-evaluation - "" - end + color + rescue Discourse::Utils::CommandError => e + # Timeout or unable to parse image + # This can happen due to bad user input - ignore and save + # an empty string to prevent re-evaluation + "" + end end if persisted? @@ -401,23 +414,28 @@ class Upload < ActiveRecord::Base end def target_image_quality(local_path, test_quality) - @file_quality ||= Discourse::Utils.execute_command("identify", "-format", "%Q", local_path, timeout: MAX_IDENTIFY_SECONDS).to_i rescue 0 + @file_quality ||= + begin + Discourse::Utils.execute_command( + "identify", + "-format", + "%Q", + local_path, + timeout: MAX_IDENTIFY_SECONDS, + ).to_i + rescue StandardError + 0 + end - if @file_quality == 0 || @file_quality > test_quality - test_quality - end + test_quality if @file_quality == 0 || @file_quality > test_quality end def self.sha1_from_short_path(path) - if path =~ /(\/uploads\/short-url\/)([a-zA-Z0-9]+)(\..*)?/ - self.sha1_from_base62_encoded($2) - end + self.sha1_from_base62_encoded($2) if path =~ %r{(/uploads/short-url/)([a-zA-Z0-9]+)(\..*)?} end def self.sha1_from_short_url(url) - if url =~ /(upload:\/\/)?([a-zA-Z0-9]+)(\..*)?/ - self.sha1_from_base62_encoded($2) - end + self.sha1_from_base62_encoded($2) if url =~ %r{(upload://)?([a-zA-Z0-9]+)(\..*)?} end def self.sha1_from_base62_encoded(encoded_sha1) @@ -426,7 +444,7 @@ class Upload < ActiveRecord::Base if sha1.length > SHA1_LENGTH nil else - sha1.rjust(SHA1_LENGTH, '0') + sha1.rjust(SHA1_LENGTH, "0") end end @@ -457,7 +475,10 @@ class Upload < ActiveRecord::Base begin Discourse.store.update_upload_ACL(self) rescue Aws::S3::Errors::NotImplemented => err - Discourse.warn_exception(err, message: "The file store object storage provider does not support setting ACLs") + Discourse.warn_exception( + err, + message: "The file store object storage provider does not support setting ACLs", + ) end end @@ -468,7 +489,7 @@ class Upload < ActiveRecord::Base { secure: secure, security_last_changed_reason: reason + " | source: #{source}", - security_last_changed_at: Time.zone.now + security_last_changed_at: Time.zone.now, } end @@ -479,15 +500,17 @@ class Upload < ActiveRecord::Base if SiteSetting.migrate_to_new_scheme max_file_size_kb = [ SiteSetting.max_image_size_kb, - SiteSetting.max_attachment_size_kb + SiteSetting.max_attachment_size_kb, ].max.kilobytes local_store = FileStore::LocalStore.new db = RailsMultisite::ConnectionManagement.current_db - scope = Upload.by_users - .where("url NOT LIKE '%/original/_X/%' AND url LIKE ?", "%/uploads/#{db}%") - .order(id: :desc) + scope = + Upload + .by_users + .where("url NOT LIKE '%/original/_X/%' AND url LIKE ?", "%/uploads/#{db}%") + .order(id: :desc) scope = scope.limit(limit) if limit @@ -503,7 +526,7 @@ class Upload < ActiveRecord::Base # keep track of the url previous_url = upload.url.dup # where is the file currently stored? - external = previous_url =~ /^\/\// + external = previous_url =~ %r{^//} # download if external if external url = SiteSetting.scheme + ":" + previous_url @@ -511,12 +534,13 @@ class Upload < ActiveRecord::Base begin retries ||= 0 - file = FileHelper.download( - url, - max_file_size: max_file_size_kb, - tmp_file_name: "discourse", - follow_redirect: true - ) + file = + FileHelper.download( + url, + max_file_size: max_file_size_kb, + tmp_file_name: "discourse", + follow_redirect: true, + ) rescue OpenURI::HTTPError retry if (retries += 1) < 1 next @@ -527,9 +551,7 @@ class Upload < ActiveRecord::Base path = local_store.path_for(upload) end # compute SHA if missing - if upload.sha1.blank? - upload.sha1 = Upload.generate_digest(path) - end + upload.sha1 = Upload.generate_digest(path) if upload.sha1.blank? # store to new location & update the filesize File.open(path) do |f| @@ -543,7 +565,7 @@ class Upload < ActiveRecord::Base DbHelper.remap( previous_url, upload.url, - excluded_tables: %w{ + excluded_tables: %w[ posts post_search_data incoming_emails @@ -555,28 +577,32 @@ class Upload < ActiveRecord::Base user_emails draft_sequences optimized_images - } + ], ) - remap_scope ||= begin - Post.with_deleted - .where("raw ~ '/uploads/#{db}/\\d+/' OR raw ~ '/uploads/#{db}/original/(\\d|[a-z])/'") - .select(:id, :raw, :cooked) - .all - end + remap_scope ||= + begin + Post + .with_deleted + .where( + "raw ~ '/uploads/#{db}/\\d+/' OR raw ~ '/uploads/#{db}/original/(\\d|[a-z])/'", + ) + .select(:id, :raw, :cooked) + .all + end remap_scope.each do |post| post.raw.gsub!(previous_url, upload.url) post.cooked.gsub!(previous_url, upload.url) - Post.with_deleted.where(id: post.id).update_all(raw: post.raw, cooked: post.cooked) if post.changed? + if post.changed? + Post.with_deleted.where(id: post.id).update_all(raw: post.raw, cooked: post.cooked) + end end upload.optimized_images.find_each(&:destroy!) upload.rebake_posts_on_old_scheme # remove the old file (when local) - unless external - FileUtils.rm(path, force: true) - end + FileUtils.rm(path, force: true) unless external rescue => e problems << { upload: upload, ex: e } ensure @@ -595,21 +621,21 @@ class Upload < ActiveRecord::Base sha1s = [] - raw.scan(/\/(\h{40})/).each do |match| - sha1s << match[0] - end + raw.scan(/\/(\h{40})/).each { |match| sha1s << match[0] } - raw.scan(/\/([a-zA-Z0-9]+)/).each do |match| - sha1s << Upload.sha1_from_base62_encoded(match[0]) - end + raw + .scan(%r{/([a-zA-Z0-9]+)}) + .each { |match| sha1s << Upload.sha1_from_base62_encoded(match[0]) } Upload.where(sha1: sha1s.uniq).pluck(:id) end def self.backfill_dominant_colors!(count) - Upload.where(dominant_color: nil).order("id desc").first(count).each do |upload| - upload.calculate_dominant_color! - end + Upload + .where(dominant_color: nil) + .order("id desc") + .first(count) + .each { |upload| upload.calculate_dominant_color! } end private @@ -617,7 +643,6 @@ class Upload < ActiveRecord::Base def short_url_basename "#{Upload.base62_sha1(sha1)}#{extension.present? ? ".#{extension}" : ""}" end - end # == Schema Information diff --git a/app/models/upload_reference.rb b/app/models/upload_reference.rb index 58cf8f86d1..73de830f54 100644 --- a/app/models/upload_reference.rb +++ b/app/models/upload_reference.rb @@ -5,7 +5,9 @@ class UploadReference < ActiveRecord::Base belongs_to :target, polymorphic: true def self.ensure_exist!(upload_ids: [], target: nil, target_type: nil, target_id: nil) - raise "target OR target_type and target_id are required" if !target && !(target_type && target_id) + if !target && !(target_type && target_id) + raise "target OR target_type and target_id are required" + end if target.present? target_type = target.class @@ -16,22 +18,21 @@ class UploadReference < ActiveRecord::Base target_type = target_type.to_s if upload_ids.empty? - UploadReference - .where(target_type: target_type, target_id: target_id) - .delete_all + UploadReference.where(target_type: target_type, target_id: target_id).delete_all return end - rows = upload_ids.map do |upload_id| - { - upload_id: upload_id, - target_type: target_type, - target_id: target_id, - created_at: Time.zone.now, - updated_at: Time.zone.now, - } - end + rows = + upload_ids.map do |upload_id| + { + upload_id: upload_id, + target_type: target_type, + target_id: target_id, + created_at: Time.zone.now, + updated_at: Time.zone.now, + } + end UploadReference.transaction do |transaction| UploadReference diff --git a/app/models/user.rb b/app/models/user.rb index 54d358ec73..d0d9846a43 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -34,22 +34,40 @@ class User < ActiveRecord::Base has_many :user_warnings, dependent: :destroy has_many :api_keys, dependent: :destroy has_many :push_subscriptions, dependent: :destroy - has_many :acting_group_histories, dependent: :destroy, foreign_key: :acting_user_id, class_name: 'GroupHistory' - has_many :targeted_group_histories, dependent: :destroy, foreign_key: :target_user_id, class_name: 'GroupHistory' + has_many :acting_group_histories, + dependent: :destroy, + foreign_key: :acting_user_id, + class_name: "GroupHistory" + has_many :targeted_group_histories, + dependent: :destroy, + foreign_key: :target_user_id, + class_name: "GroupHistory" has_many :reviewable_scores, dependent: :destroy has_many :invites, foreign_key: :invited_by_id, dependent: :destroy has_many :user_custom_fields, dependent: :destroy has_many :user_associated_groups, dependent: :destroy - has_many :pending_posts, -> { merge(Reviewable.pending) }, class_name: 'ReviewableQueuedPost', foreign_key: :created_by_id + has_many :pending_posts, + -> { merge(Reviewable.pending) }, + class_name: "ReviewableQueuedPost", + foreign_key: :created_by_id has_one :user_option, dependent: :destroy has_one :user_avatar, dependent: :destroy - has_one :primary_email, -> { where(primary: true) }, class_name: 'UserEmail', dependent: :destroy, autosave: true, validate: false + has_one :primary_email, + -> { where(primary: true) }, + class_name: "UserEmail", + dependent: :destroy, + autosave: true, + validate: false has_one :user_stat, dependent: :destroy has_one :user_profile, dependent: :destroy, inverse_of: :user has_one :single_sign_on_record, dependent: :destroy - has_one :anonymous_user_master, class_name: 'AnonymousUser', dependent: :destroy - has_one :anonymous_user_shadow, ->(record) { where(active: true) }, foreign_key: :master_user_id, class_name: 'AnonymousUser', dependent: :destroy + has_one :anonymous_user_master, class_name: "AnonymousUser", dependent: :destroy + has_one :anonymous_user_shadow, + ->(record) { where(active: true) }, + foreign_key: :master_user_id, + class_name: "AnonymousUser", + dependent: :destroy has_one :invited_user, dependent: :destroy has_one :user_notification_schedule, dependent: :destroy @@ -61,8 +79,8 @@ class User < ActiveRecord::Base has_many :user_visits, dependent: :delete_all has_many :user_auth_token_logs, dependent: :delete_all has_many :group_requests, dependent: :delete_all - has_many :muted_user_records, class_name: 'MutedUser', dependent: :delete_all - has_many :ignored_user_records, class_name: 'IgnoredUser', dependent: :delete_all + has_many :muted_user_records, class_name: "MutedUser", dependent: :delete_all + has_many :ignored_user_records, class_name: "IgnoredUser", dependent: :delete_all has_many :do_not_disturb_timings, dependent: :delete_all has_one :user_status, dependent: :destroy @@ -72,16 +90,22 @@ class User < ActiveRecord::Base has_many :post_timings has_many :directory_items has_many :email_logs - has_many :security_keys, -> { - where(enabled: true) - }, class_name: "UserSecurityKey" + has_many :security_keys, -> { where(enabled: true) }, class_name: "UserSecurityKey" has_many :badges, through: :user_badges - has_many :default_featured_user_badges, -> { - max_featured_rank = SiteSetting.max_favorite_badges > 0 ? SiteSetting.max_favorite_badges + 1 - : DEFAULT_FEATURED_BADGE_COUNT - for_enabled_badges.grouped_with_count.where("featured_rank <= ?", max_featured_rank) - }, class_name: "UserBadge" + has_many :default_featured_user_badges, + -> { + max_featured_rank = + ( + if SiteSetting.max_favorite_badges > 0 + SiteSetting.max_favorite_badges + 1 + else + DEFAULT_FEATURED_BADGE_COUNT + end + ) + for_enabled_badges.grouped_with_count.where("featured_rank <= ?", max_featured_rank) + }, + class_name: "UserBadge" has_many :topics_allowed, through: :topic_allowed_users, source: :topic has_many :groups, through: :group_users @@ -89,27 +113,32 @@ class User < ActiveRecord::Base has_many :associated_groups, through: :user_associated_groups, dependent: :destroy # deleted in user_second_factors relationship - has_many :totps, -> { - where(method: UserSecondFactor.methods[:totp], enabled: true) - }, class_name: "UserSecondFactor" + has_many :totps, + -> { where(method: UserSecondFactor.methods[:totp], enabled: true) }, + class_name: "UserSecondFactor" has_one :master_user, through: :anonymous_user_master has_one :shadow_user, through: :anonymous_user_shadow, source: :user has_one :profile_background_upload, through: :user_profile has_one :card_background_upload, through: :user_profile - belongs_to :approved_by, class_name: 'User' - belongs_to :primary_group, class_name: 'Group' - belongs_to :flair_group, class_name: 'Group' + belongs_to :approved_by, class_name: "User" + belongs_to :primary_group, class_name: "Group" + belongs_to :flair_group, class_name: "Group" has_many :muted_users, through: :muted_user_records has_many :ignored_users, through: :ignored_user_records - belongs_to :uploaded_avatar, class_name: 'Upload' + belongs_to :uploaded_avatar, class_name: "Upload" has_many :sidebar_section_links, dependent: :delete_all - has_many :category_sidebar_section_links, -> { where(linkable_type: "Category") }, class_name: 'SidebarSectionLink' - has_many :custom_sidebar_tags, through: :sidebar_section_links, source: :linkable, source_type: "Tag" + has_many :category_sidebar_section_links, + -> { where(linkable_type: "Category") }, + class_name: "SidebarSectionLink" + has_many :custom_sidebar_tags, + through: :sidebar_section_links, + source: :linkable, + source_type: "Tag" delegate :last_sent_email_address, to: :email_logs @@ -121,7 +150,8 @@ class User < ActiveRecord::Base validates :ip_address, allowed_ip_address: { on: :create, message: :signup_not_allowed } validates :primary_email, presence: true validates :validatable_user_fields_values, watched_words: true, unless: :custom_fields_clean? - validates_associated :primary_email, message: -> (_, user_email) { user_email[:value]&.errors[:email]&.first } + validates_associated :primary_email, + message: ->(_, user_email) { user_email[:value]&.errors[:email]&.first } after_initialize :add_trust_level @@ -137,17 +167,12 @@ class User < ActiveRecord::Base after_create :set_default_tags_preferences after_create :add_default_sidebar_section_links - after_update :update_default_sidebar_section_links, if: Proc.new { - self.saved_change_to_admin? - } + after_update :update_default_sidebar_section_links, if: Proc.new { self.saved_change_to_admin? } - after_update :add_default_sidebar_section_links, if: Proc.new { - self.saved_change_to_staged? - } + after_update :add_default_sidebar_section_links, if: Proc.new { self.saved_change_to_staged? } - after_update :trigger_user_updated_event, if: Proc.new { - self.human? && self.saved_change_to_uploaded_avatar_id? - } + after_update :trigger_user_updated_event, + if: Proc.new { self.human? && self.saved_change_to_uploaded_avatar_id? } after_update :trigger_user_automatic_group_refresh, if: :saved_change_to_staged? @@ -178,7 +203,10 @@ class User < ActiveRecord::Base # These tables don't have primary keys, so destroying them with activerecord is tricky: PostTiming.where(user_id: self.id).delete_all TopicViewItem.where(user_id: self.id).delete_all - UserAction.where('user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id', user_id: self.id).delete_all + UserAction.where( + "user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id", + user_id: self.id, + ).delete_all # we need to bypass the default scope here, which appears not bypassed for :delete_all # however :destroy it is bypassed @@ -186,8 +214,9 @@ class User < ActiveRecord::Base # This is a perf optimisation to ensure we hit the index # without this we need to scan a much larger number of rows - DirectoryItem.where(user_id: self.id) - .where('period_type in (?)', DirectoryItem.period_types.values) + DirectoryItem + .where(user_id: self.id) + .where("period_type in (?)", DirectoryItem.period_types.values) .delete_all # our relationship filters on enabled, this makes sure everything is deleted @@ -216,70 +245,95 @@ class User < ActiveRecord::Base # Cache for user custom fields. Currently it is used to display quick search results attr_accessor :custom_data - scope :with_email, ->(email) do - joins(:user_emails).where("lower(user_emails.email) IN (?)", email) - end + scope :with_email, + ->(email) { joins(:user_emails).where("lower(user_emails.email) IN (?)", email) } - scope :with_primary_email, ->(email) do - joins(:user_emails).where("lower(user_emails.email) IN (?) AND user_emails.primary", email) - end + scope :with_primary_email, + ->(email) { + joins(:user_emails).where( + "lower(user_emails.email) IN (?) AND user_emails.primary", + email, + ) + } - scope :human_users, -> { where('users.id > 0') } + scope :human_users, -> { where("users.id > 0") } # excluding fake users like the system user or anonymous users - scope :real, -> { human_users.where('NOT EXISTS( + scope :real, + -> { + human_users.where( + "NOT EXISTS( SELECT 1 FROM anonymous_users a WHERE a.user_id = users.id - )') } + )", + ) + } # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop # may want to create an index on (active, silence, suspended_till)? scope :silenced, -> { where("silenced_till IS NOT NULL AND silenced_till > ?", Time.zone.now) } scope :not_silenced, -> { where("silenced_till IS NULL OR silenced_till <= ?", Time.zone.now) } - scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) } - scope :not_suspended, -> { where('suspended_till IS NULL OR suspended_till <= ?', Time.zone.now) } + scope :suspended, -> { where("suspended_till IS NOT NULL AND suspended_till > ?", Time.zone.now) } + scope :not_suspended, -> { where("suspended_till IS NULL OR suspended_till <= ?", Time.zone.now) } scope :activated, -> { where(active: true) } scope :not_staged, -> { where(staged: false) } - scope :filter_by_username, ->(filter) do - if filter.is_a?(Array) - where('username_lower ~* ?', "(#{filter.join('|')})") - else - where('username_lower ILIKE ?', "%#{filter}%") - end - end + scope :filter_by_username, + ->(filter) { + if filter.is_a?(Array) + where("username_lower ~* ?", "(#{filter.join("|")})") + else + where("username_lower ILIKE ?", "%#{filter}%") + end + } - scope :filter_by_username_or_email, ->(filter) do - if filter.is_a?(String) && filter =~ /.+@.+/ - # probably an email so try the bypass - if user_id = UserEmail.where("lower(email) = ?", filter.downcase).pluck_first(:user_id) - return where('users.id = ?', user_id) - end - end + scope :filter_by_username_or_email, + ->(filter) { + if filter.is_a?(String) && filter =~ /.+@.+/ + # probably an email so try the bypass + if user_id = UserEmail.where("lower(email) = ?", filter.downcase).pluck_first(:user_id) + return where("users.id = ?", user_id) + end + end - users = joins(:primary_email) + users = joins(:primary_email) - if filter.is_a?(Array) - users.where( - 'username_lower ~* :filter OR lower(user_emails.email) SIMILAR TO :filter', - filter: "(#{filter.join('|')})" - ) - else - users.where( - 'username_lower ILIKE :filter OR lower(user_emails.email) ILIKE :filter', - filter: "%#{filter}%" - ) - end - end + if filter.is_a?(Array) + users.where( + "username_lower ~* :filter OR lower(user_emails.email) SIMILAR TO :filter", + filter: "(#{filter.join("|")})", + ) + else + users.where( + "username_lower ILIKE :filter OR lower(user_emails.email) ILIKE :filter", + filter: "%#{filter}%", + ) + end + } - scope :watching_topic, ->(topic) do - joins(DB.sql_fragment("LEFT JOIN category_users ON category_users.user_id = users.id AND category_users.category_id = :category_id", category_id: topic.category_id)) - .joins(DB.sql_fragment("LEFT JOIN topic_users ON topic_users.user_id = users.id AND topic_users.topic_id = :topic_id", topic_id: topic.id)) - .joins("LEFT JOIN tag_users ON tag_users.user_id = users.id AND tag_users.tag_id IN (#{topic.tag_ids.join(",").presence || 'NULL'})") - .where("category_users.notification_level > 0 OR topic_users.notification_level > 0 OR tag_users.notification_level > 0") - end + scope :watching_topic, + ->(topic) { + joins( + DB.sql_fragment( + "LEFT JOIN category_users ON category_users.user_id = users.id AND category_users.category_id = :category_id", + category_id: topic.category_id, + ), + ) + .joins( + DB.sql_fragment( + "LEFT JOIN topic_users ON topic_users.user_id = users.id AND topic_users.topic_id = :topic_id", + topic_id: topic.id, + ), + ) + .joins( + "LEFT JOIN tag_users ON tag_users.user_id = users.id AND tag_users.tag_id IN (#{topic.tag_ids.join(",").presence || "NULL"})", + ) + .where( + "category_users.notification_level > 0 OR topic_users.notification_level > 0 OR tag_users.notification_level > 0", + ) + } module NewTopicDuration ALWAYS = -1 @@ -289,13 +343,14 @@ class User < ActiveRecord::Base MAX_STAFF_DELETE_POST_COUNT ||= 5 def self.user_tips - @user_tips ||= Enum.new( - first_notification: 1, - topic_timeline: 2, - post_menu: 3, - topic_notification_levels: 4, - suggested_topics: 5, - ) + @user_tips ||= + Enum.new( + first_notification: 1, + topic_timeline: 2, + post_menu: 3, + topic_notification_levels: 4, + suggested_topics: 5, + ) end def visible_sidebar_tags(user_guardian = nil) @@ -318,10 +373,18 @@ class User < ActiveRecord::Base def self.username_available?(username, email = nil, allow_reserved_username: false) lower = normalize_username(username) return false if !allow_reserved_username && reserved_username?(lower) - return true if !username_exists?(lower) + return true if !username_exists?(lower) # staged users can use the same username since they will take over the account - email.present? && User.joins(:user_emails).exists?(staged: true, username_lower: lower, user_emails: { primary: true, email: email }) + email.present? && + User.joins(:user_emails).exists?( + staged: true, + username_lower: lower, + user_emails: { + primary: true, + email: email, + }, + ) end def self.reserved_username?(username) @@ -329,9 +392,11 @@ class User < ActiveRecord::Base return true if SiteSetting.here_mention == username - SiteSetting.reserved_usernames.unicode_normalize.split("|").any? do |reserved| - username.match?(/^#{Regexp.escape(reserved).gsub('\*', '.*')}$/) - end + SiteSetting + .reserved_usernames + .unicode_normalize + .split("|") + .any? { |reserved| username.match?(/^#{Regexp.escape(reserved).gsub('\*', ".*")}$/) } end def self.editable_user_custom_fields(by_staff: false) @@ -348,12 +413,12 @@ class User < ActiveRecord::Base fields.push(*DiscoursePluginRegistry.public_user_custom_fields) if SiteSetting.public_user_custom_fields.present? - fields.push(*SiteSetting.public_user_custom_fields.split('|')) + fields.push(*SiteSetting.public_user_custom_fields.split("|")) end if guardian.is_staff? if SiteSetting.staff_user_custom_fields.present? - fields.push(*SiteSetting.staff_user_custom_fields.split('|')) + fields.push(*SiteSetting.staff_user_custom_fields.split("|")) end fields.push(*DiscoursePluginRegistry.staff_user_custom_fields) @@ -386,7 +451,7 @@ class User < ActiveRecord::Base bookmarks.where(bookmarkable_type: type) end - EMAIL = %r{([^@]+)@([^\.]+)} + EMAIL = /([^@]+)@([^\.]+)/ FROM_STAGED = "from_staged" def self.new_from_params(params) @@ -417,7 +482,7 @@ class User < ActiveRecord::Base end def self.find_by_username_or_email(username_or_email) - if username_or_email.include?('@') + if username_or_email.include?("@") find_by_email(username_or_email) else find_by_username(username_or_email) @@ -445,10 +510,7 @@ class User < ActiveRecord::Base end def group_granted_trust_level - GroupUser - .where(user_id: id) - .includes(:group) - .maximum("groups.grant_trust_level") + GroupUser.where(user_id: id).includes(:group).maximum("groups.grant_trust_level") end def visible_groups @@ -477,10 +539,10 @@ class User < ActiveRecord::Base Jobs.enqueue( :send_system_message, user_id: id, - message_type: 'welcome_staff', + message_type: "welcome_staff", message_options: { - role: role.to_s - } + role: role.to_s, + }, ) end @@ -501,7 +563,8 @@ class User < ActiveRecord::Base end def invited_by - used_invite = Invite.with_deleted.joins(:invited_users).where("invited_users.user_id = ?", self.id).first + used_invite = + Invite.with_deleted.joins(:invited_users).where("invited_users.user_id = ?", self.id).first used_invite.try(:invited_by) end @@ -605,7 +668,7 @@ class User < ActiveRecord::Base args = { user_id: self.id, seen_notification_id: self.seen_notification_id, - private_message: Notification.types[:private_message] + private_message: Notification.types[:private_message], } DB.query_single(<<~SQL, args).first @@ -632,9 +695,10 @@ class User < ActiveRecord::Base end def unread_notifications - @unread_notifications ||= begin - # perf critical, much more efficient than AR - sql = <<~SQL + @unread_notifications ||= + begin + # perf critical, much more efficient than AR + sql = <<~SQL SELECT COUNT(*) FROM ( SELECT 1 FROM notifications n @@ -648,17 +712,21 @@ class User < ActiveRecord::Base ) AS X SQL - DB.query_single(sql, - user_id: id, - seen_notification_id: seen_notification_id, - limit: User.max_unread_notifications - )[0].to_i - end + DB.query_single( + sql, + user_id: id, + seen_notification_id: seen_notification_id, + limit: User.max_unread_notifications, + )[ + 0 + ].to_i + end end def all_unread_notifications_count - @all_unread_notifications_count ||= begin - sql = <<~SQL + @all_unread_notifications_count ||= + begin + sql = <<~SQL SELECT COUNT(*) FROM ( SELECT 1 FROM notifications n @@ -671,12 +739,15 @@ class User < ActiveRecord::Base ) AS X SQL - DB.query_single(sql, - user_id: id, - seen_notification_id: seen_notification_id, - limit: User.max_unread_notifications - )[0].to_i - end + DB.query_single( + sql, + user_id: id, + seen_notification_id: seen_notification_id, + limit: User.max_unread_notifications, + )[ + 0 + ].to_i + end end def total_unread_notifications @@ -684,10 +755,7 @@ class User < ActiveRecord::Base end def reviewable_count - Reviewable.list_for( - self, - include_claimed_by_others: !redesigned_user_menu_enabled? - ).count + Reviewable.list_for(self, include_claimed_by_others: !redesigned_user_menu_enabled?).count end def saw_notification_id(notification_id) @@ -704,9 +772,7 @@ class User < ActiveRecord::Base def bump_last_seen_notification! query = self.notifications.visible - if seen_notification_id - query = query.where("notifications.id > ?", seen_notification_id) - end + query = query.where("notifications.id > ?", seen_notification_id) if seen_notification_id if max_notification_id = query.maximum(:id) update!(seen_notification_id: max_notification_id) true @@ -718,9 +784,7 @@ class User < ActiveRecord::Base def bump_last_seen_reviewable! query = Reviewable.unseen_list_for(self, preload: false) - if last_seen_reviewable_id - query = query.where("reviewables.id > ?", last_seen_reviewable_id) - end + query = query.where("reviewables.id > ?", last_seen_reviewable_id) if last_seen_reviewable_id max_reviewable_id = query.maximum(:id) if max_reviewable_id @@ -732,7 +796,7 @@ class User < ActiveRecord::Base def publish_reviewable_counts(extra_data = nil) data = { reviewable_count: self.reviewable_count, - unseen_reviewable_count: Reviewable.unseen_reviewable_count(self) + unseen_reviewable_count: Reviewable.unseen_reviewable_count(self), } data.merge!(extra_data) if extra_data.present? MessageBus.publish("/reviewable_counts/#{self.id}", data, user_ids: [self.id]) @@ -741,10 +805,13 @@ class User < ActiveRecord::Base TRACK_FIRST_NOTIFICATION_READ_DURATION = 1.week.to_i def read_first_notification? - if (trust_level > TrustLevel[1] || - (first_seen_at.present? && first_seen_at < TRACK_FIRST_NOTIFICATION_READ_DURATION.seconds.ago) || - user_option.skip_new_user_tips) - + if ( + trust_level > TrustLevel[1] || + ( + first_seen_at.present? && + first_seen_at < TRACK_FIRST_NOTIFICATION_READ_DURATION.seconds.ago + ) || user_option.skip_new_user_tips + ) return true end @@ -755,7 +822,7 @@ class User < ActiveRecord::Base return if !self.allow_live_notifications? # publish last notification json with the message so we can apply an update - notification = notifications.visible.order('notifications.created_at desc').first + notification = notifications.visible.order("notifications.created_at desc").first json = NotificationSerializer.new(notification).as_json if notification sql = (<<~SQL) @@ -783,9 +850,7 @@ class User < ActiveRecord::Base ) AS y SQL - recent = DB.query(sql, user_id: id).map! do |r| - [r.id, r.read] - end + recent = DB.query(sql, user_id: id).map! { |r| [r.id, r.read] } payload = { unread_notifications: unread_notifications, @@ -800,7 +865,9 @@ class User < ActiveRecord::Base if self.redesigned_user_menu_enabled? payload[:all_unread_notifications_count] = all_unread_notifications_count payload[:grouped_unread_notifications] = grouped_unread_notifications - payload[:new_personal_messages_notifications_count] = new_personal_messages_notifications_count + payload[ + :new_personal_messages_notifications_count + ] = new_personal_messages_notifications_count end MessageBus.publish("/notification/#{id}", payload, user_ids: [id]) @@ -815,24 +882,26 @@ class User < ActiveRecord::Base payload = { description: status.description, emoji: status.emoji, - ends_at: status.ends_at&.iso8601 + ends_at: status.ends_at&.iso8601, } else payload = nil end - MessageBus.publish("/user-status", { id => payload }, group_ids: [Group::AUTO_GROUPS[:trust_level_0]]) + MessageBus.publish( + "/user-status", + { id => payload }, + group_ids: [Group::AUTO_GROUPS[:trust_level_0]], + ) end def password=(password) # special case for passwordless accounts - unless password.blank? - @raw_password = password - end + @raw_password = password unless password.blank? end def password - '' # so that validator doesn't complain that a password attribute doesn't exist + "" # so that validator doesn't complain that a password attribute doesn't exist end # Indicate that this is NOT a passwordless account for the purposes of validation @@ -862,14 +931,15 @@ class User < ActiveRecord::Base end def new_user_posting_on_first_day? - !staff? && - trust_level < TrustLevel[2] && - (trust_level == TrustLevel[0] || self.first_post_created_at.nil? || self.first_post_created_at >= 24.hours.ago) + !staff? && trust_level < TrustLevel[2] && + ( + trust_level == TrustLevel[0] || self.first_post_created_at.nil? || + self.first_post_created_at >= 24.hours.ago + ) end def new_user? - (created_at >= 24.hours.ago || trust_level == TrustLevel[0]) && - trust_level < TrustLevel[2] && + (created_at >= 24.hours.ago || trust_level == TrustLevel[0]) && trust_level < TrustLevel[2] && !staff? end @@ -883,7 +953,11 @@ class User < ActiveRecord::Base def create_visit_record!(date, opts = {}) user_stat.update_column(:days_visited, user_stat.days_visited + 1) - user_visits.create!(visited_at: date, posts_read: opts[:posts_read] || 0, mobile: opts[:mobile] || false) + user_visits.create!( + visited_at: date, + posts_read: opts[:posts_read] || 0, + mobile: opts[:mobile] || false, + ) end def visit_record_for(date) @@ -898,9 +972,7 @@ class User < ActiveRecord::Base return if timezone.blank? || !TimezoneValidator.valid?(timezone) # we only want to update the user's timezone if they have not set it themselves - UserOption - .where(user_id: self.id, timezone: nil) - .update_all(timezone: timezone) + UserOption.where(user_id: self.id, timezone: nil).update_all(timezone: timezone) end def update_posts_read!(num_posts, opts = {}) @@ -927,7 +999,6 @@ class User < ActiveRecord::Base def self.update_ip_address!(user_id, new_ip:, old_ip:) unless old_ip == new_ip || new_ip.blank? - DB.exec(<<~SQL, user_id: user_id, ip_address: new_ip) UPDATE users SET ip_address = :ip_address @@ -979,9 +1050,10 @@ class User < ActiveRecord::Base return true if SiteSetting.active_user_rate_limit_secs <= 0 Discourse.redis.set( - last_seen_redis_key(user_id, now), "1", + last_seen_redis_key(user_id, now), + "1", nx: true, - ex: SiteSetting.active_user_rate_limit_secs + ex: SiteSetting.active_user_rate_limit_secs, ) end @@ -1015,9 +1087,12 @@ class User < ActiveRecord::Base end def self.username_hash(username) - username.each_char.reduce(0) do |result, char| - [((result << 5) - result) + char.ord].pack('L').unpack('l').first - end.abs + username + .each_char + .reduce(0) do |result, char| + [((result << 5) - result) + char.ord].pack("L").unpack("l").first + end + .abs end def self.default_template(username) @@ -1042,10 +1117,11 @@ class User < ActiveRecord::Base # TODO it may be worth caching this in a distributed cache, should be benched if SiteSetting.external_system_avatars_enabled url = SiteSetting.external_system_avatars_url.dup - url = +"#{Discourse.base_path}#{url}" unless url =~ /^https?:\/\// + url = +"#{Discourse.base_path}#{url}" unless url =~ %r{^https?://} url.gsub! "{color}", letter_avatar_color(normalized_username) url.gsub! "{username}", UrlHelper.encode_component(username) - url.gsub! "{first_letter}", UrlHelper.encode_component(normalized_username.grapheme_clusters.first) + url.gsub! "{first_letter}", + UrlHelper.encode_component(normalized_username.grapheme_clusters.first) url.gsub! "{hostname}", Discourse.current_hostname url else @@ -1064,7 +1140,7 @@ class User < ActiveRecord::Base colors[index, hex_length] else color = LetterAvatar::COLORS[color_index(username, LetterAvatar::COLORS.length)] - color.map { |c| c.to_s(16).rjust(2, '0') }.join + color.map { |c| c.to_s(16).rjust(2, "0") }.join end end @@ -1077,8 +1153,8 @@ class User < ActiveRecord::Base end def avatar_template - use_small_logo = is_system_user? && - SiteSetting.logo_small && SiteSetting.use_site_small_logo_as_system_avatar + use_small_logo = + is_system_user? && SiteSetting.logo_small && SiteSetting.use_site_small_logo_as_system_avatar if use_small_logo Discourse.store.cdn_url(SiteSetting.logo_small.url) @@ -1110,7 +1186,10 @@ class User < ActiveRecord::Base end def flags_given_count - PostAction.where(user_id: id, post_action_type_id: PostActionType.flag_types_without_custom.values).count + PostAction.where( + user_id: id, + post_action_type_id: PostActionType.flag_types_without_custom.values, + ).count end def warnings_received_count @@ -1118,7 +1197,10 @@ class User < ActiveRecord::Base end def flags_received_count - posts.includes(:post_actions).where('post_actions.post_action_type_id' => PostActionType.flag_types_without_custom.values).count + posts + .includes(:post_actions) + .where("post_actions.post_action_type_id" => PostActionType.flag_types_without_custom.values) + .count end def private_topics_count @@ -1134,7 +1216,7 @@ class User < ActiveRecord::Base last_action_in_topic = UserAction.last_action_in_topic(id, topic_id) since_reply = Post.where(user_id: id, topic_id: topic_id) - since_reply = since_reply.where('id > ?', last_action_in_topic) if last_action_in_topic + since_reply = since_reply.where("id > ?", last_action_in_topic) if last_action_in_topic (since_reply.count >= SiteSetting.newuser_max_replies_per_topic) end @@ -1144,9 +1226,10 @@ class User < ActiveRecord::Base Reviewable.where(created_by_id: id).delete_all - posts.order("post_number desc").limit(batch_size).each do |p| - PostDestroyer.new(guardian.user, p).destroy - end + posts + .order("post_number desc") + .limit(batch_size) + .each { |p| PostDestroyer.new(guardian.user, p).destroy } end def suspended? @@ -1158,7 +1241,7 @@ class User < ActiveRecord::Base end def silenced_record - UserHistory.for(self, :silence_user).order('id DESC').first + UserHistory.for(self, :silence_user).order("id DESC").first end def silence_reason @@ -1174,7 +1257,7 @@ class User < ActiveRecord::Base end def suspend_record - UserHistory.for(self, :suspend_user).order('id DESC').first + UserHistory.for(self, :suspend_user).order("id DESC").first end def full_suspend_reason @@ -1201,9 +1284,11 @@ class User < ActiveRecord::Base end end - I18n.t(message, - date: I18n.l(suspended_till, format: :date_only), - reason: Rack::Utils.escape_html(suspend_reason)) + I18n.t( + message, + date: I18n.l(suspended_till, format: :date_only), + reason: Rack::Utils.escape_html(suspend_reason), + ) end def suspended_forever? @@ -1213,16 +1298,14 @@ class User < ActiveRecord::Base # Use this helper to determine if the user has a particular trust level. # Takes into account admin, etc. def has_trust_level?(level) - unless TrustLevel.valid?(level) - raise InvalidTrustLevel.new("Invalid trust level #{level}") - end + raise InvalidTrustLevel.new("Invalid trust level #{level}") unless TrustLevel.valid?(level) admin? || moderator? || staged? || TrustLevel.compare(trust_level, level) end def has_trust_level_or_staff?(level) - return admin? if level.to_s == 'admin' - return staff? if level.to_s == 'staff' + return admin? if level.to_s == "admin" + return staff? if level.to_s == "staff" has_trust_level?(level.to_i) end @@ -1236,13 +1319,12 @@ class User < ActiveRecord::Base end def username_format_validator - UsernameValidator.perform_validation(self, 'username') + UsernameValidator.perform_validation(self, "username") end def email_confirmed? - email_tokens.where(email: email, confirmed: true).present? || - email_tokens.empty? || - single_sign_on_record&.external_email&.downcase == email + email_tokens.where(email: email, confirmed: true).present? || email_tokens.empty? || + single_sign_on_record&.external_email&.downcase == email end def activate @@ -1297,11 +1379,16 @@ class User < ActiveRecord::Base end def self.count_by_first_post(start_date = nil, end_date = nil) - result = joins('INNER JOIN user_stats AS us ON us.user_id = users.id') + result = joins("INNER JOIN user_stats AS us ON us.user_id = users.id") if start_date && end_date result = result.group("date(us.first_post_created_at)") - result = result.where("us.first_post_created_at > ? AND us.first_post_created_at < ?", start_date, end_date) + result = + result.where( + "us.first_post_created_at > ? AND us.first_post_created_at < ?", + start_date, + end_date, + ) result = result.order("date(us.first_post_created_at)") end @@ -1316,7 +1403,7 @@ class User < ActiveRecord::Base secure_categories.references(:categories) end - cats.pluck('categories.id').sort + cats.pluck("categories.id").sort end def topic_create_allowed_category_ids @@ -1327,21 +1414,24 @@ class User < ActiveRecord::Base def flag_linked_posts_as_spam results = [] - disagreed_flag_post_ids = PostAction.where(post_action_type_id: PostActionType.types[:spam]) - .where.not(disagreed_at: nil) - .pluck(:post_id) + disagreed_flag_post_ids = + PostAction + .where(post_action_type_id: PostActionType.types[:spam]) + .where.not(disagreed_at: nil) + .pluck(:post_id) - topic_links.includes(:post) + topic_links + .includes(:post) .where.not(post_id: disagreed_flag_post_ids) .each do |tl| - - message = I18n.t( - 'flag_reason.spam_hosts', - base_path: Discourse.base_path, - locale: SiteSetting.default_locale - ) - results << PostActionCreator.create(Discourse.system_user, tl.post, :spam, message: message) - end + message = + I18n.t( + "flag_reason.spam_hosts", + base_path: Discourse.base_path, + locale: SiteSetting.default_locale, + ) + results << PostActionCreator.create(Discourse.system_user, tl.post, :spam, message: message) + end results end @@ -1351,7 +1441,12 @@ class User < ActiveRecord::Base end def find_email - last_sent_email_address.present? && EmailAddressValidator.valid_value?(last_sent_email_address) ? last_sent_email_address : email + if last_sent_email_address.present? && + EmailAddressValidator.valid_value?(last_sent_email_address) + last_sent_email_address + else + email + end end def tl3_requirements @@ -1361,8 +1456,9 @@ class User < ActiveRecord::Base def on_tl3_grace_period? return true if SiteSetting.tl3_promotion_min_duration.to_i.days.ago.year < 2013 - UserHistory.for(self, :auto_trust_level_change) - .where('created_at >= ?', SiteSetting.tl3_promotion_min_duration.to_i.days.ago) + UserHistory + .for(self, :auto_trust_level_change) + .where("created_at >= ?", SiteSetting.tl3_promotion_min_duration.to_i.days.ago) .where(previous_value: TrustLevel[2].to_s) .where(new_value: TrustLevel[3].to_s) .exists? @@ -1373,10 +1469,8 @@ class User < ActiveRecord::Base avatar = user_avatar || create_user_avatar - if self.primary_email.present? && - SiteSetting.automatically_download_gravatars? && - !avatar.last_gravatar_download_attempt - + if self.primary_email.present? && SiteSetting.automatically_download_gravatars? && + !avatar.last_gravatar_download_attempt Jobs.cancel_scheduled_job(:update_gravatar, user_id: self.id, avatar_id: avatar.id) Jobs.enqueue_in(1.second, :update_gravatar, user_id: self.id, avatar_id: avatar.id) end @@ -1395,10 +1489,7 @@ class User < ActiveRecord::Base Discourse.authenticators.each do |authenticator| account_description = authenticator.description_for_user(self) unless account_description.empty? - result << { - name: authenticator.name, - description: account_description, - } + result << { name: authenticator.name, description: account_description } end end @@ -1408,14 +1499,12 @@ class User < ActiveRecord::Base USER_FIELD_PREFIX ||= "user_field_" def user_fields(field_ids = nil) - if field_ids.nil? - field_ids = (@all_user_field_ids ||= UserField.pluck(:id)) - end + field_ids = (@all_user_field_ids ||= UserField.pluck(:id)) if field_ids.nil? @user_fields_cache ||= {} # Memoize based on requested fields - @user_fields_cache[field_ids.join(':')] ||= {}.tap do |hash| + @user_fields_cache[field_ids.join(":")] ||= {}.tap do |hash| field_ids.each do |fid| # The hash keys are strings for backwards compatibility hash[fid.to_s] = custom_fields["#{USER_FIELD_PREFIX}#{fid}"] @@ -1439,16 +1528,14 @@ class User < ActiveRecord::Base def validatable_user_fields # ignore multiselect fields since they are admin-set and thus not user generated content - @public_user_field_ids ||= UserField.public_fields.where.not(field_type: 'multiselect').pluck(:id) + @public_user_field_ids ||= + UserField.public_fields.where.not(field_type: "multiselect").pluck(:id) user_fields(@public_user_field_ids) end def number_of_deleted_posts - Post.with_deleted - .where(user_id: self.id) - .where.not(deleted_at: nil) - .count + Post.with_deleted.where(user_id: self.id).where.not(deleted_at: nil).count end def number_of_flagged_posts @@ -1456,14 +1543,12 @@ class User < ActiveRecord::Base end def number_of_rejected_posts - ReviewableQueuedPost - .rejected - .where(created_by_id: self.id) - .count + ReviewableQueuedPost.rejected.where(created_by_id: self.id).count end def number_of_flags_given - PostAction.where(user_id: self.id) + PostAction + .where(user_id: self.id) .where(disagreed_at: nil) .where(post_action_type_id: PostActionType.notify_flag_type_ids) .count @@ -1487,9 +1572,7 @@ class User < ActiveRecord::Base end def anonymous? - SiteSetting.allow_anonymous_posting && - trust_level >= 1 && - !!anonymous_user_master + SiteSetting.allow_anonymous_posting && trust_level >= 1 && !!anonymous_user_master end def is_singular_admin? @@ -1504,25 +1587,23 @@ class User < ActiveRecord::Base def logged_in DiscourseEvent.trigger(:user_logged_in, self) - if !self.seen_before? - DiscourseEvent.trigger(:user_first_logged_in, self) - end + DiscourseEvent.trigger(:user_first_logged_in, self) if !self.seen_before? end def set_automatic_groups return if !active || staged || !email_confirmed? - Group.where(automatic: false) + Group + .where(automatic: false) .where("LENGTH(COALESCE(automatic_membership_email_domains, '')) > 0") .each do |group| + domains = group.automatic_membership_email_domains.gsub(".", '\.') - domains = group.automatic_membership_email_domains.gsub('.', '\.') - - if email =~ Regexp.new("@(#{domains})$", true) && !group.users.include?(self) - group.add(self) - GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(self) + if email =~ Regexp.new("@(#{domains})$", true) && !group.users.include?(self) + group.add(self) + GroupActionLogger.new(Discourse.system_user, group).log_add_user_to_group(self) + end end - end @belonging_to_group_ids = nil end @@ -1540,7 +1621,10 @@ class User < ActiveRecord::Base build_primary_email email: new_email, skip_validate_email: !should_validate_email_address? end - if secondary_match = user_emails.detect { |ue| !ue.primary && Email.downcase(ue.email) == Email.downcase(new_email) } + if secondary_match = + user_emails.detect { |ue| + !ue.primary && Email.downcase(ue.email) == Email.downcase(new_email) + } secondary_match.mark_for_destruction primary_email.skip_validate_unique_email = true end @@ -1557,16 +1641,21 @@ class User < ActiveRecord::Base end def unconfirmed_emails - self.email_change_requests.where.not(change_state: EmailChangeRequest.states[:complete]).pluck(:new_email) + self + .email_change_requests + .where.not(change_state: EmailChangeRequest.states[:complete]) + .pluck(:new_email) end RECENT_TIME_READ_THRESHOLD ||= 60.days def self.preload_recent_time_read(users) - times = UserVisit.where(user_id: users.map(&:id)) - .where('visited_at >= ?', RECENT_TIME_READ_THRESHOLD.ago) - .group(:user_id) - .sum(:time_read) + times = + UserVisit + .where(user_id: users.map(&:id)) + .where("visited_at >= ?", RECENT_TIME_READ_THRESHOLD.ago) + .group(:user_id) + .sum(:time_read) users.each { |u| u.preload_recent_time_read(times[u.id] || 0) } end @@ -1575,7 +1664,8 @@ class User < ActiveRecord::Base end def recent_time_read - @recent_time_read ||= self.user_visits.where('visited_at >= ?', RECENT_TIME_READ_THRESHOLD.ago).sum(:time_read) + @recent_time_read ||= + self.user_visits.where("visited_at >= ?", RECENT_TIME_READ_THRESHOLD.ago).sum(:time_read) end def from_staged? @@ -1588,7 +1678,8 @@ class User < ActiveRecord::Base def next_best_title group_titles_query = groups.where("groups.title <> ''") - group_titles_query = group_titles_query.order("groups.id = #{primary_group_id} DESC") if primary_group_id + group_titles_query = + group_titles_query.order("groups.id = #{primary_group_id} DESC") if primary_group_id group_titles_query = group_titles_query.order("groups.primary_group DESC").limit(1) if next_best_group_title = group_titles_query.pluck_first(:title) @@ -1661,7 +1752,7 @@ class User < ActiveRecord::Base def active_do_not_disturb_timings now = Time.zone.now - do_not_disturb_timings.where('starts_at <= ? AND ends_at > ?', now, now) + do_not_disturb_timings.where("starts_at <= ? AND ends_at > ?", now, now) end def do_not_disturb_until @@ -1698,12 +1789,7 @@ class User < ActiveRecord::Base end def set_status!(description, emoji, ends_at = nil) - status = { - description: description, - emoji: emoji, - set_at: Time.zone.now, - ends_at: ends_at - } + status = { description: description, emoji: emoji, set_at: Time.zone.now, ends_at: ends_at } if user_status user_status.update!(status) @@ -1730,7 +1816,7 @@ class User < ActiveRecord::Base def expire_old_email_tokens if saved_change_to_password_hash? && !saved_change_to_id? - email_tokens.where('not expired').update_all(expired: true) + email_tokens.where("not expired").update_all(expired: true) end end @@ -1785,7 +1871,12 @@ class User < ActiveRecord::Base def hash_password(password, salt) raise StandardError.new("password is too long") if password.size > User.max_password_length - Pbkdf2.hash_password(password, salt, Rails.configuration.pbkdf2_iterations, Rails.configuration.pbkdf2_algorithm) + Pbkdf2.hash_password( + password, + salt, + Rails.configuration.pbkdf2_iterations, + Rails.configuration.pbkdf2_algorithm, + ) end def add_trust_level @@ -1815,25 +1906,22 @@ class User < ActiveRecord::Base end def username_validator - username_format_validator || begin - if will_save_change_to_username? - existing = DB.query( - USERNAME_EXISTS_SQL, - username: self.class.normalize_username(username) - ) + username_format_validator || + begin + if will_save_change_to_username? + existing = + DB.query(USERNAME_EXISTS_SQL, username: self.class.normalize_username(username)) - user_id = existing.select { |u| u.is_user }.first&.id - same_user = user_id && user_id == self.id + user_id = existing.select { |u| u.is_user }.first&.id + same_user = user_id && user_id == self.id - if existing.present? && !same_user - errors.add(:username, I18n.t(:'user.username.unique')) - end + errors.add(:username, I18n.t(:"user.username.unique")) if existing.present? && !same_user - if confirm_password?(username) || confirm_password?(username.downcase) - errors.add(:username, :same_as_password) + if confirm_password?(username) || confirm_password?(username.downcase) + errors.add(:username, :same_as_password) + end end end - end end def name_validator @@ -1858,14 +1946,14 @@ class User < ActiveRecord::Base # * default_categories_watching_first_post # * default_categories_normal # * default_categories_muted - %w{watching watching_first_post tracking normal muted}.each do |setting| + %w[watching watching_first_post tracking normal muted].each do |setting| category_ids = SiteSetting.get("default_categories_#{setting}").split("|").map(&:to_i) category_ids.each do |category_id| next if category_id == 0 values << { user_id: self.id, category_id: category_id, - notification_level: CategoryUser.notification_levels[setting.to_sym] + notification_level: CategoryUser.notification_levels[setting.to_sym], } end end @@ -1885,19 +1973,22 @@ class User < ActiveRecord::Base # * default_tags_tracking # * default_tags_watching_first_post # * default_tags_muted - %w{watching watching_first_post tracking muted}.each do |setting| + %w[watching watching_first_post tracking muted].each do |setting| tag_names = SiteSetting.get("default_tags_#{setting}").split("|") now = Time.zone.now - Tag.where(name: tag_names).pluck(:id).each do |tag_id| - values << { - user_id: self.id, - tag_id: tag_id, - notification_level: TagUser.notification_levels[setting.to_sym], - created_at: now, - updated_at: now - } - end + Tag + .where(name: tag_names) + .pluck(:id) + .each do |tag_id| + values << { + user_id: self.id, + tag_id: tag_id, + notification_level: TagUser.notification_levels[setting.to_sym], + created_at: now, + updated_at: now, + } + end end TagUser.insert_all!(values) if values.present? @@ -1912,20 +2003,24 @@ class User < ActiveRecord::Base .where(active: false) .where("created_at < ?", SiteSetting.purge_unactivated_users_grace_period_days.days.ago) .where("NOT admin AND NOT moderator") - .where("NOT EXISTS + .where( + "NOT EXISTS (SELECT 1 FROM topic_allowed_users tu JOIN topics t ON t.id = tu.topic_id AND t.user_id > 0 WHERE tu.user_id = users.id LIMIT 1) - ") - .where("NOT EXISTS + ", + ) + .where( + "NOT EXISTS (SELECT 1 FROM posts p WHERE p.user_id = users.id LIMIT 1) - ") + ", + ) .limit(200) .find_each do |user| - begin - destroyer.destroy(user, context: I18n.t(:purge_reason)) - rescue Discourse::InvalidAccess - # keep going + begin + destroyer.destroy(user, context: I18n.t(:purge_reason)) + rescue Discourse::InvalidAccess + # keep going + end end - end end def match_primary_group_changes @@ -1935,16 +2030,15 @@ class User < ActiveRecord::Base self.title = primary_group&.title end - if flair_group_id == primary_group_id_was - self.flair_group_id = primary_group&.id - end + self.flair_group_id = primary_group&.id if flair_group_id == primary_group_id_was end def self.first_login_admin_id - User.where(admin: true) + User + .where(admin: true) .human_users .joins(:user_auth_tokens) - .order('user_auth_tokens.created_at') + .order("user_auth_tokens.created_at") .pluck_first(:id) end @@ -1958,15 +2052,18 @@ class User < ActiveRecord::Base categories_to_update = SiteSetting.default_sidebar_categories.split("|") if update - filtered_default_category_ids = Category.secured(self.guardian).where(id: categories_to_update).pluck(:id) - existing_category_ids = SidebarSectionLink.where(user: self, linkable_type: 'Category').pluck(:linkable_id) + filtered_default_category_ids = + Category.secured(self.guardian).where(id: categories_to_update).pluck(:id) + existing_category_ids = + SidebarSectionLink.where(user: self, linkable_type: "Category").pluck(:linkable_id) - categories_to_update = existing_category_ids + (filtered_default_category_ids & self.secure_category_ids) + categories_to_update = + existing_category_ids + (filtered_default_category_ids & self.secure_category_ids) end SidebarSectionLinksUpdater.update_category_section_links( self, - category_ids: categories_to_update + category_ids: categories_to_update, ) end @@ -1975,18 +2072,24 @@ class User < ActiveRecord::Base if update default_tag_ids = Tag.where(name: tags_to_update).pluck(:id) - filtered_default_tags = DiscourseTagging.filter_visible(Tag, self.guardian).where(id: default_tag_ids).pluck(:name) + filtered_default_tags = + DiscourseTagging + .filter_visible(Tag, self.guardian) + .where(id: default_tag_ids) + .pluck(:name) - existing_tag_ids = SidebarSectionLink.where(user: self, linkable_type: 'Tag').pluck(:linkable_id) - existing_tags = DiscourseTagging.filter_visible(Tag, self.guardian).where(id: existing_tag_ids).pluck(:name) + existing_tag_ids = + SidebarSectionLink.where(user: self, linkable_type: "Tag").pluck(:linkable_id) + existing_tags = + DiscourseTagging + .filter_visible(Tag, self.guardian) + .where(id: existing_tag_ids) + .pluck(:name) tags_to_update = existing_tags + (filtered_default_tags & DiscourseTagging.hidden_tag_names) end - SidebarSectionLinksUpdater.update_tag_section_links( - self, - tag_names: tags_to_update - ) + SidebarSectionLinksUpdater.update_tag_section_links(self, tag_names: tags_to_update) end end @@ -2003,9 +2106,7 @@ class User < ActiveRecord::Base end def trigger_user_automatic_group_refresh - if !staged - Group.user_trust_level_change!(id, trust_level) - end + Group.user_trust_level_change!(id, trust_level) if !staged true end @@ -2016,12 +2117,14 @@ class User < ActiveRecord::Base def check_if_title_is_badged_granted if title_changed? && !new_record? && user_profile - badge_matching_title = title && badges.find do |badge| - badge.allow_title? && (badge.display_name == title || badge.name == title) - end + badge_matching_title = + title && + badges.find do |badge| + badge.allow_title? && (badge.display_name == title || badge.name == title) + end user_profile.update( badge_granted_title: badge_matching_title.present?, - granted_title_badge_id: badge_matching_title&.id + granted_title_badge_id: badge_matching_title&.id, ) end end @@ -2032,9 +2135,7 @@ class User < ActiveRecord::Base def update_previous_visit(timestamp) update_visit_record!(timestamp.to_date) - if previous_visit_at_update_required?(timestamp) - update_column(:previous_visit_at, last_seen_at) - end + update_column(:previous_visit_at, last_seen_at) if previous_visit_at_update_required?(timestamp) end def trigger_user_created_event @@ -2048,16 +2149,14 @@ class User < ActiveRecord::Base end def set_skip_validate_email - if self.primary_email - self.primary_email.skip_validate_email = !should_validate_email_address? - end + self.primary_email.skip_validate_email = !should_validate_email_address? if self.primary_email true end def check_site_contact_username if (saved_change_to_admin? || saved_change_to_moderator?) && - self.username == SiteSetting.site_contact_username && !staff? + self.username == SiteSetting.site_contact_username && !staff? SiteSetting.set_and_log(:site_contact_username, SiteSetting.defaults[:site_contact_username]) end end diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 25eabac3ff..d605324de4 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -21,59 +21,58 @@ class UserAction < ActiveRecord::Base SOLVED = 15 ASSIGNED = 16 - ORDER = Hash[*[ - GOT_PRIVATE_MESSAGE, - NEW_PRIVATE_MESSAGE, - NEW_TOPIC, - REPLY, - RESPONSE, - LIKE, - WAS_LIKED, - MENTION, - QUOTE, - EDIT, - SOLVED, - ASSIGNED, - ].each_with_index.to_a.flatten] + ORDER = + Hash[ + *[ + GOT_PRIVATE_MESSAGE, + NEW_PRIVATE_MESSAGE, + NEW_TOPIC, + REPLY, + RESPONSE, + LIKE, + WAS_LIKED, + MENTION, + QUOTE, + EDIT, + SOLVED, + ASSIGNED, + ].each_with_index.to_a.flatten + ] USER_ACTED_TYPES = [LIKE, NEW_TOPIC, REPLY, NEW_PRIVATE_MESSAGE] def self.types - @types ||= Enum.new( - like: 1, - was_liked: 2, - # NOTE: Previously type 3 was bookmark but this was removed when we - # changed to using the Bookmark model. - new_topic: 4, - reply: 5, - response: 6, - mention: 7, - quote: 9, - edit: 11, - new_private_message: 12, - got_private_message: 13, - solved: 15, - assigned: 16) + @types ||= + Enum.new( + like: 1, + was_liked: 2, + # NOTE: Previously type 3 was bookmark but this was removed when we + # changed to using the Bookmark model. + new_topic: 4, + reply: 5, + response: 6, + mention: 7, + quote: 9, + edit: 11, + new_private_message: 12, + got_private_message: 13, + solved: 15, + assigned: 16, + ) end def self.private_types - @private_types ||= [ - WAS_LIKED, - RESPONSE, - MENTION, - QUOTE, - EDIT - ] + @private_types ||= [WAS_LIKED, RESPONSE, MENTION, QUOTE, EDIT] end def self.last_action_in_topic(user_id, topic_id) - UserAction.where(user_id: user_id, - target_topic_id: topic_id, - action_type: [RESPONSE, MENTION, QUOTE]).order('created_at DESC').pluck_first(:target_post_id) + UserAction + .where(user_id: user_id, target_topic_id: topic_id, action_type: [RESPONSE, MENTION, QUOTE]) + .order("created_at DESC") + .pluck_first(:target_post_id) end def self.stats(user_id, guardian) - # Sam: I tried this in AR and it got complex builder = DB.build <<~SQL @@ -87,7 +86,7 @@ class UserAction < ActiveRecord::Base GROUP BY action_type SQL - builder.where('a.user_id = :user_id', user_id: user_id) + builder.where("a.user_id = :user_id", user_id: user_id) apply_common_filters(builder, user_id, guardian) @@ -128,23 +127,20 @@ class UserAction < ActiveRecord::Base result = { all: all, mine: mine, unread: unread } - DB.query(sql, user_id: user_id).each do |row| - (result[:groups] ||= []) << { name: row.name, count: row.count.to_i } - end + DB + .query(sql, user_id: user_id) + .each { |row| (result[:groups] ||= []) << { name: row.name, count: row.count.to_i } } result - end def self.count_daily_engaged_users(start_date = nil, end_date = nil) - result = select(:user_id) - .distinct - .where(action_type: USER_ACTED_TYPES) + result = select(:user_id).distinct.where(action_type: USER_ACTED_TYPES) if start_date && end_date - result = result.group('date(created_at)') - result = result.where('created_at > ? AND created_at < ?', start_date, end_date) - result = result.order('date(created_at)') + result = result.group("date(created_at)") + result = result.where("created_at > ? AND created_at < ?", start_date, end_date) + result = result.order("date(created_at)") end result.count @@ -154,28 +150,29 @@ class UserAction < ActiveRecord::Base stream(action_id: action_id, guardian: guardian).first end - NULL_QUEUED_STREAM_COLS = %i{ - cooked - uploaded_avatar_id - acting_name - acting_username - acting_user_id - target_name - target_username - target_user_id - post_number - post_id - deleted - hidden - post_type - action_type - action_code - action_code_who - action_code_path - topic_closed - topic_id - topic_archived - }.map! { |s| "NULL as #{s}" }.join(", ") + NULL_QUEUED_STREAM_COLS = + %i[ + cooked + uploaded_avatar_id + acting_name + acting_username + acting_user_id + target_name + target_username + target_user_id + post_number + post_id + deleted + hidden + post_type + action_type + action_code + action_code_who + action_code_path + topic_closed + topic_id + topic_archived + ].map! { |s| "NULL as #{s}" }.join(", ") def self.stream(opts = nil) opts ||= {} @@ -191,13 +188,10 @@ class UserAction < ActiveRecord::Base # Acting user columns. Can be extended by plugins to include custom avatar # columns - acting_cols = [ - 'u.id AS acting_user_id', - 'u.name AS acting_name' - ] + acting_cols = ["u.id AS acting_user_id", "u.name AS acting_name"] UserLookup.lookup_columns.each do |c| - next if c == :id || c['.'] + next if c == :id || c["."] acting_cols << "u.#{c} AS acting_#{c}" end @@ -213,7 +207,7 @@ class UserAction < ActiveRecord::Base p.reply_to_post_number, pu.username, pu.name, pu.id user_id, pu.uploaded_avatar_id, - #{acting_cols.join(', ')}, + #{acting_cols.join(", ")}, coalesce(p.cooked, p2.cooked) cooked, CASE WHEN coalesce(p.deleted_at, p2.deleted_at, t.deleted_at) IS NULL THEN false ELSE true END deleted, p.hidden, @@ -245,11 +239,14 @@ class UserAction < ActiveRecord::Base builder.where("a.id = :id", id: action_id.to_i) else builder.where("a.user_id = :user_id", user_id: user_id.to_i) - builder.where("a.action_type in (:action_types)", action_types: action_types) if action_types && action_types.length > 0 + if action_types && action_types.length > 0 + builder.where("a.action_type in (:action_types)", action_types: action_types) + end if acting_username - builder.where("u.username_lower = :acting_username", - acting_username: acting_username.downcase + builder.where( + "u.username_lower = :acting_username", + acting_username: acting_username.downcase, ) end @@ -257,17 +254,14 @@ class UserAction < ActiveRecord::Base builder.where("a.action_type <> :mention_type", mention_type: UserAction::MENTION) end - builder - .order_by("a.created_at desc") - .offset(offset.to_i) - .limit(limit.to_i) + builder.order_by("a.created_at desc").offset(offset.to_i).limit(limit.to_i) end builder.query end def self.log_action!(hash) - required_parameters = [:action_type, :user_id, :acting_user_id] + required_parameters = %i[action_type user_id acting_user_id] required_parameters << :target_post_id required_parameters << :target_topic_id @@ -284,18 +278,14 @@ class UserAction < ActiveRecord::Base action = self.new(hash) - if hash[:created_at] - action.created_at = hash[:created_at] - end + action.created_at = hash[:created_at] if hash[:created_at] action.save! user_id = hash[:user_id] topic = Topic.includes(:category).find_by(id: hash[:target_topic_id]) - if topic && !topic.private_message? - update_like_count(user_id, hash[:action_type], 1) - end + update_like_count(user_id, hash[:action_type], 1) if topic && !topic.private_message? group_ids = nil if topic && topic.category && topic.category.read_restricted @@ -304,11 +294,15 @@ class UserAction < ActiveRecord::Base end if action.user - MessageBus.publish("/u/#{action.user.username_lower}", action.id, user_ids: [user_id], group_ids: group_ids) + MessageBus.publish( + "/u/#{action.user.username_lower}", + action.id, + user_ids: [user_id], + group_ids: group_ids, + ) end action - rescue ActiveRecord::RecordNotUnique # can happen, don't care already logged raise ActiveRecord::Rollback @@ -317,7 +311,14 @@ class UserAction < ActiveRecord::Base end def self.remove_action!(hash) - require_parameters(hash, :action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id) + require_parameters( + hash, + :action_type, + :user_id, + :acting_user_id, + :target_topic_id, + :target_post_id, + ) if action = UserAction.find_by(hash.except(:created_at)) action.destroy MessageBus.publish("/user/#{hash[:user_id]}", user_action_id: action.id, remove: true) @@ -329,7 +330,6 @@ class UserAction < ActiveRecord::Base end def self.synchronize_target_topic_ids(post_ids = nil, limit: nil) - # nuke all dupes, using magic builder = DB.build <<~SQL DELETE FROM user_actions USING user_actions ua2 @@ -345,19 +345,15 @@ class UserAction < ActiveRecord::Base user_actions.id > ua2.id SQL - if limit - builder.where(<<~SQL, limit: limit) + builder.where(<<~SQL, limit: limit) if limit user_actions.target_post_id IN ( SELECT target_post_id FROM user_actions WHERE created_at > :limit ) SQL - end - if post_ids - builder.where("user_actions.target_post_id in (:post_ids)", post_ids: post_ids) - end + builder.where("user_actions.target_post_id in (:post_ids)", post_ids: post_ids) if post_ids builder.exec @@ -368,19 +364,15 @@ class UserAction < ActiveRecord::Base SQL builder.where("target_topic_id <> (select topic_id from posts where posts.id = target_post_id)") - if post_ids - builder.where("target_post_id in (:post_ids)", post_ids: post_ids) - end + builder.where("target_post_id in (:post_ids)", post_ids: post_ids) if post_ids - if limit - builder.where(<<~SQL, limit: limit) + builder.where(<<~SQL, limit: limit) if limit target_post_id IN ( SELECT target_post_id FROM user_actions WHERE created_at > :limit ) SQL - end builder.exec end @@ -407,11 +399,17 @@ class UserAction < ActiveRecord::Base current_user_id = -2 current_user_id = guardian.user.id if guardian.user - builder.where("NOT COALESCE(p.hidden, false) OR p.user_id = :current_user_id", current_user_id: current_user_id) + builder.where( + "NOT COALESCE(p.hidden, false) OR p.user_id = :current_user_id", + current_user_id: current_user_id, + ) end visible_post_types = Topic.visible_post_types(guardian.user) - builder.where("COALESCE(p.post_type, p2.post_type) IN (:visible_post_types)", visible_post_types: visible_post_types) + builder.where( + "COALESCE(p.post_type, p2.post_type) IN (:visible_post_types)", + visible_post_types: visible_post_types, + ) unless (guardian.user && guardian.user.id == user_id) || guardian.is_staff? builder.where("t.visible") @@ -423,7 +421,7 @@ class UserAction < ActiveRecord::Base def self.filter_private_messages(builder, user_id, guardian, ignore_private_messages = false) if !guardian.can_see_private_messages?(user_id) || ignore_private_messages || !guardian.user - builder.where("t.archetype <> :private_message", private_message: Archetype::private_message) + builder.where("t.archetype <> :private_message", private_message: Archetype.private_message) else unless guardian.is_admin? sql = <<~SQL @@ -438,7 +436,11 @@ class UserAction < ActiveRecord::Base ) SQL - builder.where(sql, private_message: Archetype::private_message, current_user_id: guardian.user.id) + builder.where( + sql, + private_message: Archetype.private_message, + current_user_id: guardian.user.id, + ) end end builder @@ -448,9 +450,12 @@ class UserAction < ActiveRecord::Base unless guardian.is_admin? allowed = guardian.secure_category_ids if allowed.present? - builder.where("( c.read_restricted IS NULL OR + builder.where( + "( c.read_restricted IS NULL OR NOT c.read_restricted OR - (c.read_restricted and c.id in (:cats)) )", cats: guardian.secure_category_ids) + (c.read_restricted and c.id in (:cats)) )", + cats: guardian.secure_category_ids, + ) else builder.where("(c.read_restricted IS NULL OR NOT c.read_restricted)") end @@ -459,9 +464,7 @@ class UserAction < ActiveRecord::Base end def self.require_parameters(data, *params) - params.each do |p| - raise Discourse::InvalidParameters.new(p) if data[p].nil? - end + params.each { |p| raise Discourse::InvalidParameters.new(p) if data[p].nil? } end end diff --git a/app/models/user_api_key.rb b/app/models/user_api_key.rb index d0238781bc..59815a5c70 100644 --- a/app/models/user_api_key.rb +++ b/app/models/user_api_key.rb @@ -2,7 +2,7 @@ class UserApiKey < ActiveRecord::Base self.ignored_columns = [ - "scopes" # TODO(2020-12-18): remove + "scopes", # TODO(2020-12-18): remove ] REVOKE_MATCHER = RouteMatcher.new(actions: "user_api_keys#revoke", methods: :post, params: [:id]) @@ -23,7 +23,9 @@ class UserApiKey < ActiveRecord::Base end def key - raise ApiKey::KeyAccessError.new "API key is only accessible immediately after creation" unless key_available? + unless key_available? + raise ApiKey::KeyAccessError.new "API key is only accessible immediately after creation" + end @key end @@ -41,7 +43,7 @@ class UserApiKey < ActiveRecord::Base # invalidate old dupe api key for client if needed UserApiKey .where(client_id: client_id, user_id: self.user_id) - .where('id <> ?', self.id) + .where("id <> ?", self.id) .destroy_all update_args[:client_id] = client_id @@ -59,8 +61,7 @@ class UserApiKey < ActiveRecord::Base end def has_push? - scopes.any? { |s| s.name == "push" || s.name == "notifications" } && - push_url.present? && + scopes.any? { |s| s.name == "push" || s.name == "notifications" } && push_url.present? && SiteSetting.allowed_user_api_push_urls.include?(push_url) end @@ -69,8 +70,9 @@ class UserApiKey < ActiveRecord::Base end def self.invalid_auth_redirect?(auth_redirect) - SiteSetting.allowed_user_api_auth_redirects - .split('|') + SiteSetting + .allowed_user_api_auth_redirects + .split("|") .none? { |u| WildcardUrlChecker.check_url(u, auth_redirect) } end diff --git a/app/models/user_api_key_scope.rb b/app/models/user_api_key_scope.rb index d2f0c408a3..0ea9427964 100644 --- a/app/models/user_api_key_scope.rb +++ b/app/models/user_api_key_scope.rb @@ -2,26 +2,33 @@ class UserApiKeyScope < ActiveRecord::Base SCOPES = { - read: [ RouteMatcher.new(methods: :get) ], - write: [ RouteMatcher.new(methods: [:get, :post, :patch, :put, :delete]) ], - message_bus: [ RouteMatcher.new(methods: :post, actions: 'message_bus') ], + read: [RouteMatcher.new(methods: :get)], + write: [RouteMatcher.new(methods: %i[get post patch put delete])], + message_bus: [RouteMatcher.new(methods: :post, actions: "message_bus")], push: [], one_time_password: [], notifications: [ - RouteMatcher.new(methods: :post, actions: 'message_bus'), - RouteMatcher.new(methods: :get, actions: 'notifications#index'), - RouteMatcher.new(methods: :put, actions: 'notifications#mark_read') + RouteMatcher.new(methods: :post, actions: "message_bus"), + RouteMatcher.new(methods: :get, actions: "notifications#index"), + RouteMatcher.new(methods: :put, actions: "notifications#mark_read"), ], session_info: [ - RouteMatcher.new(methods: :get, actions: 'session#current'), - RouteMatcher.new(methods: :get, actions: 'users#topic_tracking_state') + RouteMatcher.new(methods: :get, actions: "session#current"), + RouteMatcher.new(methods: :get, actions: "users#topic_tracking_state"), + ], + bookmarks_calendar: [ + RouteMatcher.new( + methods: :get, + actions: "users#bookmarks", + formats: :ics, + params: %i[username], + ), ], - bookmarks_calendar: [ RouteMatcher.new(methods: :get, actions: 'users#bookmarks', formats: :ics, params: %i[username]) ], user_status: [ - RouteMatcher.new(methods: :get, actions: 'user_status#get'), - RouteMatcher.new(methods: :put, actions: 'user_status#set'), - RouteMatcher.new(methods: :delete, actions: 'user_status#clear') - ] + RouteMatcher.new(methods: :get, actions: "user_status#get"), + RouteMatcher.new(methods: :put, actions: "user_status#set"), + RouteMatcher.new(methods: :delete, actions: "user_status#clear"), + ], } def self.all_scopes diff --git a/app/models/user_archived_message.rb b/app/models/user_archived_message.rb index 2410f50e2e..787209c702 100644 --- a/app/models/user_archived_message.rb +++ b/app/models/user_archived_message.rb @@ -7,11 +7,15 @@ class UserArchivedMessage < ActiveRecord::Base def self.move_to_inbox!(user_id, topic) topic_id = topic.id - return if (TopicUser.where( - user_id: user_id, - topic_id: topic_id, - notification_level: TopicUser.notification_levels[:muted] - ).exists?) + if ( + TopicUser.where( + user_id: user_id, + topic_id: topic_id, + notification_level: TopicUser.notification_levels[:muted], + ).exists? + ) + return + end UserArchivedMessage.where(user_id: user_id, topic_id: topic_id).destroy_all trigger(:move_to_inbox, user_id, topic_id) @@ -29,9 +33,7 @@ class UserArchivedMessage < ActiveRecord::Base def self.trigger(event, user_id, topic_id) user = User.find_by(id: user_id) topic = Topic.find_by(id: topic_id) - if user && topic - DiscourseEvent.trigger(event, user: user, topic: topic) - end + DiscourseEvent.trigger(event, user: user, topic: topic) if user && topic end end diff --git a/app/models/user_associated_group.rb b/app/models/user_associated_group.rb index 1413efb7c6..815309d1aa 100644 --- a/app/models/user_associated_group.rb +++ b/app/models/user_associated_group.rb @@ -4,7 +4,7 @@ class UserAssociatedGroup < ActiveRecord::Base belongs_to :user belongs_to :associated_group - after_commit :add_to_associated_groups, on: [:create, :update] + after_commit :add_to_associated_groups, on: %i[create update] before_destroy :remove_from_associated_groups def add_to_associated_groups @@ -14,7 +14,9 @@ class UserAssociatedGroup < ActiveRecord::Base end def remove_from_associated_groups - Group.where("NOT EXISTS( + Group + .where( + "NOT EXISTS( SELECT 1 FROM user_associated_groups uag JOIN group_associated_groups gag @@ -22,9 +24,11 @@ class UserAssociatedGroup < ActiveRecord::Base WHERE uag.user_id = :user_id AND uag.id != :uag_id AND gag.group_id = groups.id - )", uag_id: id, user_id: user_id).each do |group| - group.remove_automatically(user, subject: associated_group.label) - end + )", + uag_id: id, + user_id: user_id, + ) + .each { |group| group.remove_automatically(user, subject: associated_group.label) } end end diff --git a/app/models/user_auth_token.rb b/app/models/user_auth_token.rb index 6566371b20..97eb0372ff 100644 --- a/app/models/user_auth_token.rb +++ b/app/models/user_auth_token.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'digest/sha1' +require "digest/sha1" class UserAuthToken < ActiveRecord::Base belongs_to :user @@ -11,13 +11,13 @@ class UserAuthToken < ActiveRecord::Base MAX_SESSION_COUNT = 60 - USER_ACTIONS = ['generate'] + USER_ACTIONS = ["generate"] attr_accessor :unhashed_auth_token before_destroy do UserAuthToken.log_verbose( - action: 'destroy', + action: "destroy", user_auth_token_id: self.id, user_id: self.user_id, user_agent: self.user_agent, @@ -31,9 +31,7 @@ class UserAuthToken < ActiveRecord::Base end def self.log_verbose(info) - if SiteSetting.verbose_auth_token_logging - log(info) - end + log(info) if SiteSetting.verbose_auth_token_logging end RAD_PER_DEG = Math::PI / 180 @@ -49,8 +47,10 @@ class UserAuthToken < ActiveRecord::Base lat1_rad, lon1_rad = loc1[0] * RAD_PER_DEG, loc1[1] * RAD_PER_DEG lat2_rad, lon2_rad = loc2[0] * RAD_PER_DEG, loc2[1] * RAD_PER_DEG - a = Math.sin((lat2_rad - lat1_rad) / 2)**2 + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin((lon2_rad - lon1_rad) / 2)**2 - c = 2 * Math::atan2(Math::sqrt(a), Math::sqrt(1 - a)) + a = + Math.sin((lat2_rad - lat1_rad) / 2)**2 + + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.sin((lon2_rad - lon1_rad) / 2)**2 + c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) c * EARTH_RADIUS_KM end @@ -72,21 +72,29 @@ class UserAuthToken < ActiveRecord::Base end end - def self.generate!(user_id: , user_agent: nil, client_ip: nil, path: nil, staff: nil, impersonate: false) + def self.generate!( + user_id:, + user_agent: nil, + client_ip: nil, + path: nil, + staff: nil, + impersonate: false + ) token = SecureRandom.hex(16) hashed_token = hash_token(token) - user_auth_token = UserAuthToken.create!( - user_id: user_id, - user_agent: user_agent, - client_ip: client_ip, - auth_token: hashed_token, - prev_auth_token: hashed_token, - rotated_at: Time.zone.now - ) + user_auth_token = + UserAuthToken.create!( + user_id: user_id, + user_agent: user_agent, + client_ip: client_ip, + auth_token: hashed_token, + prev_auth_token: hashed_token, + rotated_at: Time.zone.now, + ) user_auth_token.unhashed_auth_token = token log( - action: 'generate', + action: "generate", user_auth_token_id: user_auth_token.id, user_id: user_id, user_agent: user_agent, @@ -96,10 +104,12 @@ class UserAuthToken < ActiveRecord::Base ) if staff && !impersonate - Jobs.enqueue(:suspicious_login, + Jobs.enqueue( + :suspicious_login, user_id: user_id, client_ip: client_ip, - user_agent: user_agent) + user_agent: user_agent, + ) end user_auth_token @@ -111,12 +121,15 @@ class UserAuthToken < ActiveRecord::Base token = hash_token(unhashed_token) expire_before = SiteSetting.maximum_session_age.hours.ago - user_token = find_by("(auth_token = :token OR + user_token = + find_by( + "(auth_token = :token OR prev_auth_token = :token) AND rotated_at > :expire_before", - token: token, expire_before: expire_before) + token: token, + expire_before: expire_before, + ) if !user_token - log_verbose( action: "miss token", user_id: nil, @@ -129,11 +142,13 @@ class UserAuthToken < ActiveRecord::Base return nil end - if user_token.auth_token != token && user_token.prev_auth_token == token && user_token.auth_token_seen - changed_rows = UserAuthToken - .where("rotated_at < ?", 1.minute.ago) - .where(id: user_token.id, prev_auth_token: token) - .update_all(auth_token_seen: false) + if user_token.auth_token != token && user_token.prev_auth_token == token && + user_token.auth_token_seen + changed_rows = + UserAuthToken + .where("rotated_at < ?", 1.minute.ago) + .where(id: user_token.id, prev_auth_token: token) + .update_all(auth_token_seen: false) # not updating AR model cause we want to give it one more req # with wrong cookie @@ -144,15 +159,17 @@ class UserAuthToken < ActiveRecord::Base auth_token: user_token.auth_token, user_agent: opts && opts[:user_agent], path: opts && opts[:path], - client_ip: opts && opts[:client_ip] + client_ip: opts && opts[:client_ip], ) end if mark_seen && user_token && !user_token.auth_token_seen && user_token.auth_token == token # we must protect against concurrency issues here - changed_rows = UserAuthToken - .where(id: user_token.id, auth_token: token) - .update_all(auth_token_seen: true, seen_at: Time.zone.now) + changed_rows = + UserAuthToken.where(id: user_token.id, auth_token: token).update_all( + auth_token_seen: true, + seen_at: Time.zone.now, + ) if changed_rows == 1 # not doing a reload so we don't risk loading a rotated token @@ -179,15 +196,17 @@ class UserAuthToken < ActiveRecord::Base end def self.cleanup! - if SiteSetting.verbose_auth_token_logging - UserAuthTokenLog.where('created_at < :time', - time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME).delete_all + UserAuthTokenLog.where( + "created_at < :time", + time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME, + ).delete_all end - where('rotated_at < :time', - time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME).delete_all - + where( + "rotated_at < :time", + time: SiteSetting.maximum_session_age.hours.ago - ROTATE_TIME, + ).delete_all end def rotate!(info = nil) @@ -196,7 +215,9 @@ class UserAuthToken < ActiveRecord::Base token = SecureRandom.hex(16) - result = DB.exec(" + result = + DB.exec( + " UPDATE user_auth_tokens SET auth_token_seen = false, @@ -207,13 +228,14 @@ class UserAuthToken < ActiveRecord::Base auth_token = :new_token, rotated_at = :now WHERE id = :id AND (auth_token_seen or rotated_at < :safeguard_time) -", id: self.id, - user_agent: user_agent, - client_ip: client_ip&.to_s, - now: Time.zone.now, - new_token: UserAuthToken.hash_token(token), - safeguard_time: 30.seconds.ago - ) +", + id: self.id, + user_agent: user_agent, + client_ip: client_ip&.to_s, + now: Time.zone.now, + new_token: UserAuthToken.hash_token(token), + safeguard_time: 30.seconds.ago, + ) if result > 0 reload @@ -226,20 +248,21 @@ class UserAuthToken < ActiveRecord::Base auth_token: auth_token, user_agent: user_agent, client_ip: client_ip, - path: info && info[:path] + path: info && info[:path], ) true else false end - end def self.enforce_session_count_limit!(user_id) - tokens_to_destroy = where(user_id: user_id). - where('rotated_at > ?', SiteSetting.maximum_session_age.hours.ago). - order("rotated_at DESC").offset(MAX_SESSION_COUNT) + tokens_to_destroy = + where(user_id: user_id) + .where("rotated_at > ?", SiteSetting.maximum_session_age.hours.ago) + .order("rotated_at DESC") + .offset(MAX_SESSION_COUNT) tokens_to_destroy.delete_all # Returns the number of deleted rows end diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index 349509d014..20f0e1d21b 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -2,8 +2,8 @@ class UserAvatar < ActiveRecord::Base belongs_to :user - belongs_to :gravatar_upload, class_name: 'Upload' - belongs_to :custom_upload, class_name: 'Upload' + belongs_to :gravatar_upload, class_name: "Upload" + belongs_to :custom_upload, class_name: "Upload" has_many :upload_references, as: :target, dependent: :destroy after_save do @@ -14,7 +14,7 @@ class UserAvatar < ActiveRecord::Base end @@custom_user_gravatar_email_hash = { - Discourse::SYSTEM_USER_ID => User.email_hash("info@discourse.org") + Discourse::SYSTEM_USER_ID => User.email_hash("info@discourse.org"), } def self.register_custom_user_gravatar_email_hash(user_id, email) @@ -36,33 +36,36 @@ class UserAvatar < ActiveRecord::Base return if user.blank? || user.primary_email.blank? email_hash = @@custom_user_gravatar_email_hash[user_id] || user.email_hash - gravatar_url = "https://#{SiteSetting.gravatar_base_url}/avatar/#{email_hash}.png?s=#{max}&d=404&reset_cache=#{SecureRandom.urlsafe_base64(5)}" + gravatar_url = + "https://#{SiteSetting.gravatar_base_url}/avatar/#{email_hash}.png?s=#{max}&d=404&reset_cache=#{SecureRandom.urlsafe_base64(5)}" if SiteSetting.verbose_upload_logging Rails.logger.warn("Verbose Upload Logging: Downloading gravatar from #{gravatar_url}") end # follow redirects in case gravatar change rules on us - tempfile = FileHelper.download( - gravatar_url, - max_file_size: SiteSetting.max_image_size_kb.kilobytes, - tmp_file_name: "gravatar", - skip_rate_limit: true, - verbose: false, - follow_redirect: true - ) + tempfile = + FileHelper.download( + gravatar_url, + max_file_size: SiteSetting.max_image_size_kb.kilobytes, + tmp_file_name: "gravatar", + skip_rate_limit: true, + verbose: false, + follow_redirect: true, + ) if tempfile ext = File.extname(tempfile) - ext = '.png' if ext.blank? + ext = ".png" if ext.blank? - upload = UploadCreator.new( - tempfile, - "gravatar#{ext}", - origin: gravatar_url, - type: "avatar", - for_gravatar: true - ).create_for(user_id) + upload = + UploadCreator.new( + tempfile, + "gravatar#{ext}", + origin: gravatar_url, + type: "avatar", + for_gravatar: true, + ).create_for(user_id) if gravatar_upload_id != upload.id User.transaction do @@ -75,9 +78,7 @@ class UserAvatar < ActiveRecord::Base end end rescue OpenURI::HTTPError => e - if e.io&.status[0].to_i != 404 - raise e - end + raise e if e.io&.status[0].to_i != 404 ensure tempfile&.close! end @@ -111,19 +112,26 @@ class UserAvatar < ActiveRecord::Base Rails.logger.warn("Verbose Upload Logging: Downloading sso-avatar from #{avatar_url}") end - tempfile = FileHelper.download( - avatar_url, - max_file_size: SiteSetting.max_image_size_kb.kilobytes, - tmp_file_name: "sso-avatar", - follow_redirect: true - ) + tempfile = + FileHelper.download( + avatar_url, + max_file_size: SiteSetting.max_image_size_kb.kilobytes, + tmp_file_name: "sso-avatar", + follow_redirect: true, + ) return unless tempfile ext = FastImage.type(tempfile).to_s tempfile.rewind - upload = UploadCreator.new(tempfile, "external-avatar." + ext, origin: avatar_url, type: "avatar").create_for(user.id) + upload = + UploadCreator.new( + tempfile, + "external-avatar." + ext, + origin: avatar_url, + type: "avatar", + ).create_for(user.id) user.create_user_avatar! unless user.user_avatar @@ -132,13 +140,10 @@ class UserAvatar < ActiveRecord::Base override_gravatar = !options || options[:override_gravatar] if user.uploaded_avatar_id.nil? || - !user.user_avatar.contains_upload?(user.uploaded_avatar_id) || - override_gravatar - + !user.user_avatar.contains_upload?(user.uploaded_avatar_id) || override_gravatar user.update!(uploaded_avatar_id: upload.id) end end - rescue Net::ReadTimeout, OpenURI::HTTPError # skip saving, we are not connected to the net ensure diff --git a/app/models/user_badge.rb b/app/models/user_badge.rb index ec820a1808..dd91915631 100644 --- a/app/models/user_badge.rb +++ b/app/models/user_badge.rb @@ -3,49 +3,49 @@ class UserBadge < ActiveRecord::Base belongs_to :badge belongs_to :user - belongs_to :granted_by, class_name: 'User' + belongs_to :granted_by, class_name: "User" belongs_to :notification, dependent: :destroy belongs_to :post - BOOLEAN_ATTRIBUTES = %w(is_favorite) + BOOLEAN_ATTRIBUTES = %w[is_favorite] - scope :grouped_with_count, -> { - group(:badge_id, :user_id) - .select_for_grouping - .order('MAX(featured_rank) ASC') - .includes(:user, :granted_by, { badge: :badge_type }, post: :topic) - } + scope :grouped_with_count, + -> { + group(:badge_id, :user_id) + .select_for_grouping + .order("MAX(featured_rank) ASC") + .includes(:user, :granted_by, { badge: :badge_type }, post: :topic) + } - scope :select_for_grouping, -> { - select( - UserBadge.attribute_names.map do |name| - operation = BOOLEAN_ATTRIBUTES.include?(name) ? "BOOL_OR" : "MAX" - "#{operation}(user_badges.#{name}) AS #{name}" - end, - 'COUNT(*) AS "count"' - ) - } + scope :select_for_grouping, + -> { + select( + UserBadge.attribute_names.map do |name| + operation = BOOLEAN_ATTRIBUTES.include?(name) ? "BOOL_OR" : "MAX" + "#{operation}(user_badges.#{name}) AS #{name}" + end, + 'COUNT(*) AS "count"', + ) + } - scope :for_enabled_badges, -> { where('user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)') } + scope :for_enabled_badges, + -> { where("user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)") } - validates :badge_id, - presence: true, - uniqueness: { scope: :user_id }, - if: :single_grant_badge? + validates :badge_id, presence: true, uniqueness: { scope: :user_id }, if: :single_grant_badge? validates :user_id, presence: true validates :granted_at, presence: true validates :granted_by, presence: true after_create do - Badge.increment_counter 'grant_count', self.badge_id + Badge.increment_counter "grant_count", self.badge_id UserStat.update_distinct_badge_count self.user_id UserBadge.update_featured_ranks! self.user_id self.trigger_user_badge_granted_event end after_destroy do - Badge.decrement_counter 'grant_count', self.badge_id + Badge.decrement_counter "grant_count", self.badge_id UserStat.update_distinct_badge_count self.user_id UserBadge.update_featured_ranks! self.user_id DiscourseEvent.trigger(:user_badge_removed, self.badge_id, self.user_id) diff --git a/app/models/user_badges.rb b/app/models/user_badges.rb index 4b0f858830..65de5a6616 100644 --- a/app/models/user_badges.rb +++ b/app/models/user_badges.rb @@ -2,7 +2,7 @@ # view model for user badges class UserBadges - alias :read_attribute_for_serialization :send + alias read_attribute_for_serialization send attr_accessor :user_badges, :username, :grant_count diff --git a/app/models/user_custom_field.rb b/app/models/user_custom_field.rb index a4f757536e..92255cc564 100644 --- a/app/models/user_custom_field.rb +++ b/app/models/user_custom_field.rb @@ -3,7 +3,12 @@ class UserCustomField < ActiveRecord::Base belongs_to :user - scope :searchable, -> { joins("INNER JOIN user_fields ON user_fields.id = REPLACE(user_custom_fields.name, 'user_field_', '')::INTEGER AND user_fields.searchable IS TRUE AND user_custom_fields.name like 'user_field_%'") } + scope :searchable, + -> { + joins( + "INNER JOIN user_fields ON user_fields.id = REPLACE(user_custom_fields.name, 'user_field_', '')::INTEGER AND user_fields.searchable IS TRUE AND user_custom_fields.name like 'user_field_%'", + ) + } end # == Schema Information diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 50ee27318b..ff175a9bfd 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -12,22 +12,23 @@ class UserEmail < ActiveRecord::Base validates :email, presence: true validates :email, email: true, if: :validate_email? - validates :primary, uniqueness: { scope: [:user_id] }, if: [:user_id, :primary] + validates :primary, uniqueness: { scope: [:user_id] }, if: %i[user_id primary] validate :user_id_not_changed, if: :primary validate :unique_email, if: :validate_unique_email? scope :secondary, -> { where(primary: false) } - before_save ->() { destroy_email_tokens(self.email_was) }, if: :will_save_change_to_email? + before_save -> { destroy_email_tokens(self.email_was) }, if: :will_save_change_to_email? after_destroy { destroy_email_tokens(self.email) } def normalize_email - self.normalized_email = if self.email.present? - username, domain = self.email.split('@', 2) - username = username.gsub('.', '').gsub(/\+.*/, '') - "#{username}@#{domain}" - end + self.normalized_email = + if self.email.present? + username, domain = self.email.split("@", 2) + username = username.gsub(".", "").gsub(/\+.*/, "") + "#{username}@#{domain}" + end end private @@ -50,19 +51,26 @@ class UserEmail < ActiveRecord::Base end def unique_email - email_exists = if SiteSetting.normalize_emails? - self.class.where("lower(email) = ? OR lower(normalized_email) = ?", email, normalized_email).exists? - else - self.class.where("lower(email) = ?", email).exists? - end + email_exists = + if SiteSetting.normalize_emails? + self + .class + .where("lower(email) = ? OR lower(normalized_email) = ?", email, normalized_email) + .exists? + else + self.class.where("lower(email) = ?", email).exists? + end self.errors.add(:email, :taken) if email_exists end def user_id_not_changed if self.will_save_change_to_user_id? && self.persisted? - self.errors.add(:user_id, I18n.t( - 'active_record.errors.model.user_email.attributes.user_id.reassigning_primary_email') + self.errors.add( + :user_id, + I18n.t( + "active_record.errors.model.user_email.attributes.user_id.reassigning_primary_email", + ), ) end end diff --git a/app/models/user_export.rb b/app/models/user_export.rb index 3505914725..32fcca5f75 100644 --- a/app/models/user_export.rb +++ b/app/models/user_export.rb @@ -16,22 +16,31 @@ class UserExport < ActiveRecord::Base DESTROY_CREATED_BEFORE = 2.days.ago def self.remove_old_exports - UserExport.where('created_at < ?', DESTROY_CREATED_BEFORE).find_each do |user_export| - UserExport.transaction do - begin - Post.where(topic_id: user_export.topic_id).find_each { |p| p.destroy! } - user_export.destroy! - rescue => e - Rails.logger.warn("Failed to remove user_export record with id #{user_export.id}: #{e.message}\n#{e.backtrace.join("\n")}") + UserExport + .where("created_at < ?", DESTROY_CREATED_BEFORE) + .find_each do |user_export| + UserExport.transaction do + begin + Post.where(topic_id: user_export.topic_id).find_each { |p| p.destroy! } + user_export.destroy! + rescue => e + Rails.logger.warn( + "Failed to remove user_export record with id #{user_export.id}: #{e.message}\n#{e.backtrace.join("\n")}", + ) + end end end - end end def self.base_directory - File.join(Rails.root, "public", "uploads", "csv_exports", RailsMultisite::ConnectionManagement.current_db) + File.join( + Rails.root, + "public", + "uploads", + "csv_exports", + RailsMultisite::ConnectionManagement.current_db, + ) end - end # == Schema Information diff --git a/app/models/user_field.rb b/app/models/user_field.rb index ea8330cc69..3d492ab28e 100644 --- a/app/models/user_field.rb +++ b/app/models/user_field.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserField < ActiveRecord::Base - include AnonCacheInvalidator include HasSanitizableFields @@ -21,14 +20,16 @@ class UserField < ActiveRecord::Base end def queue_index_search - SearchIndexer.queue_users_reindex(UserCustomField.where(name: "user_field_#{self.id}").pluck(:user_id)) + SearchIndexer.queue_users_reindex( + UserCustomField.where(name: "user_field_#{self.id}").pluck(:user_id), + ) end private def sanitize_description if description_changed? - self.description = sanitize_field(self.description) + self.description = sanitize_field(self.description, additional_attributes: ["target"]) end end end diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 1ddde529e9..e3a98fe9b2 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -4,8 +4,8 @@ # like deleting users, changing site settings, dismissing notifications, etc. # Use other classes, like StaffActionLogger, to log records to this table. class UserHistory < ActiveRecord::Base - belongs_to :acting_user, class_name: 'User' - belongs_to :target_user, class_name: 'User' + belongs_to :acting_user, class_name: "User" + belongs_to :target_user, class_name: "User" belongs_to :post belongs_to :topic @@ -18,200 +18,201 @@ class UserHistory < ActiveRecord::Base before_save :set_admin_only def self.actions - @actions ||= Enum.new( - delete_user: 1, - change_trust_level: 2, - change_site_setting: 3, - change_theme: 4, - delete_theme: 5, - checked_for_custom_avatar: 6, # not used anymore - notified_about_avatar: 7, - notified_about_sequential_replies: 8, - notified_about_dominating_topic: 9, - suspend_user: 10, - unsuspend_user: 11, - facebook_no_email: 12, # not used anymore - grant_badge: 13, - revoke_badge: 14, - auto_trust_level_change: 15, - check_email: 16, - delete_post: 17, - delete_topic: 18, - impersonate: 19, - roll_up: 20, - change_username: 21, - custom: 22, - custom_staff: 23, - anonymize_user: 24, - reviewed_post: 25, - change_category_settings: 26, - delete_category: 27, - create_category: 28, - change_site_text: 29, - silence_user: 30, - unsilence_user: 31, - grant_admin: 32, - revoke_admin: 33, - grant_moderation: 34, - revoke_moderation: 35, - backup_create: 36, - rate_limited_like: 37, # not used anymore - revoke_email: 38, - deactivate_user: 39, - wizard_step: 40, - lock_trust_level: 41, - unlock_trust_level: 42, - activate_user: 43, - change_readonly_mode: 44, - backup_download: 45, - backup_destroy: 46, - notified_about_get_a_room: 47, - change_name: 48, - post_locked: 49, - post_unlocked: 50, - check_personal_message: 51, - disabled_second_factor: 52, - post_edit: 53, - topic_published: 54, - recover_topic: 55, - post_approved: 56, - create_badge: 57, - change_badge: 58, - delete_badge: 59, - removed_silence_user: 60, - removed_suspend_user: 61, - removed_unsilence_user: 62, - removed_unsuspend_user: 63, - post_rejected: 64, - merge_user: 65, - entity_export: 66, - change_password: 67, - topic_timestamps_changed: 68, - approve_user: 69, - web_hook_create: 70, - web_hook_update: 71, - web_hook_destroy: 72, - embeddable_host_create: 73, - embeddable_host_update: 74, - embeddable_host_destroy: 75, - web_hook_deactivate: 76, - change_theme_setting: 77, - disable_theme_component: 78, - enable_theme_component: 79, - api_key_create: 80, - api_key_update: 81, - api_key_destroy: 82, - revoke_title: 83, - change_title: 84, - override_upload_secure_status: 85, - page_published: 86, - page_unpublished: 87, - add_email: 88, - update_email: 89, - destroy_email: 90, - topic_closed: 91, - topic_opened: 92, - topic_archived: 93, - topic_unarchived: 94, - post_staff_note_create: 95, - post_staff_note_destroy: 96, - watched_word_create: 97, - watched_word_destroy: 98, - delete_group: 99 - ) + @actions ||= + Enum.new( + delete_user: 1, + change_trust_level: 2, + change_site_setting: 3, + change_theme: 4, + delete_theme: 5, + checked_for_custom_avatar: 6, # not used anymore + notified_about_avatar: 7, + notified_about_sequential_replies: 8, + notified_about_dominating_topic: 9, + suspend_user: 10, + unsuspend_user: 11, + facebook_no_email: 12, # not used anymore + grant_badge: 13, + revoke_badge: 14, + auto_trust_level_change: 15, + check_email: 16, + delete_post: 17, + delete_topic: 18, + impersonate: 19, + roll_up: 20, + change_username: 21, + custom: 22, + custom_staff: 23, + anonymize_user: 24, + reviewed_post: 25, + change_category_settings: 26, + delete_category: 27, + create_category: 28, + change_site_text: 29, + silence_user: 30, + unsilence_user: 31, + grant_admin: 32, + revoke_admin: 33, + grant_moderation: 34, + revoke_moderation: 35, + backup_create: 36, + rate_limited_like: 37, # not used anymore + revoke_email: 38, + deactivate_user: 39, + wizard_step: 40, + lock_trust_level: 41, + unlock_trust_level: 42, + activate_user: 43, + change_readonly_mode: 44, + backup_download: 45, + backup_destroy: 46, + notified_about_get_a_room: 47, + change_name: 48, + post_locked: 49, + post_unlocked: 50, + check_personal_message: 51, + disabled_second_factor: 52, + post_edit: 53, + topic_published: 54, + recover_topic: 55, + post_approved: 56, + create_badge: 57, + change_badge: 58, + delete_badge: 59, + removed_silence_user: 60, + removed_suspend_user: 61, + removed_unsilence_user: 62, + removed_unsuspend_user: 63, + post_rejected: 64, + merge_user: 65, + entity_export: 66, + change_password: 67, + topic_timestamps_changed: 68, + approve_user: 69, + web_hook_create: 70, + web_hook_update: 71, + web_hook_destroy: 72, + embeddable_host_create: 73, + embeddable_host_update: 74, + embeddable_host_destroy: 75, + web_hook_deactivate: 76, + change_theme_setting: 77, + disable_theme_component: 78, + enable_theme_component: 79, + api_key_create: 80, + api_key_update: 81, + api_key_destroy: 82, + revoke_title: 83, + change_title: 84, + override_upload_secure_status: 85, + page_published: 86, + page_unpublished: 87, + add_email: 88, + update_email: 89, + destroy_email: 90, + topic_closed: 91, + topic_opened: 92, + topic_archived: 93, + topic_unarchived: 94, + post_staff_note_create: 95, + post_staff_note_destroy: 96, + watched_word_create: 97, + watched_word_destroy: 98, + delete_group: 99, + ) end # Staff actions is a subset of all actions, used to audit actions taken by staff users. def self.staff_actions - @staff_actions ||= [ - :delete_user, - :change_trust_level, - :change_site_setting, - :change_theme, - :delete_theme, - :change_site_text, - :suspend_user, - :unsuspend_user, - :removed_suspend_user, - :removed_unsuspend_user, - :grant_badge, - :revoke_badge, - :check_email, - :delete_post, - :delete_topic, - :impersonate, - :roll_up, - :change_username, - :custom_staff, - :anonymize_user, - :reviewed_post, - :change_category_settings, - :delete_category, - :create_category, - :silence_user, - :unsilence_user, - :removed_silence_user, - :removed_unsilence_user, - :grant_admin, - :revoke_admin, - :grant_moderation, - :revoke_moderation, - :backup_create, - :revoke_email, - :deactivate_user, - :lock_trust_level, - :unlock_trust_level, - :activate_user, - :change_readonly_mode, - :backup_download, - :backup_destroy, - :post_locked, - :post_unlocked, - :check_personal_message, - :disabled_second_factor, - :post_edit, - :topic_published, - :recover_topic, - :post_approved, - :create_badge, - :change_badge, - :delete_badge, - :post_rejected, - :merge_user, - :entity_export, - :change_name, - :topic_timestamps_changed, - :approve_user, - :web_hook_create, - :web_hook_update, - :web_hook_destroy, - :web_hook_deactivate, - :embeddable_host_create, - :embeddable_host_update, - :embeddable_host_destroy, - :change_theme_setting, - :disable_theme_component, - :enable_theme_component, - :revoke_title, - :change_title, - :api_key_create, - :api_key_update, - :api_key_destroy, - :override_upload_secure_status, - :page_published, - :page_unpublished, - :add_email, - :update_email, - :destroy_email, - :topic_closed, - :topic_opened, - :topic_archived, - :topic_unarchived, - :post_staff_note_create, - :post_staff_note_destroy, - :watched_word_create, - :watched_word_destroy, - :delete_group + @staff_actions ||= %i[ + delete_user + change_trust_level + change_site_setting + change_theme + delete_theme + change_site_text + suspend_user + unsuspend_user + removed_suspend_user + removed_unsuspend_user + grant_badge + revoke_badge + check_email + delete_post + delete_topic + impersonate + roll_up + change_username + custom_staff + anonymize_user + reviewed_post + change_category_settings + delete_category + create_category + silence_user + unsilence_user + removed_silence_user + removed_unsilence_user + grant_admin + revoke_admin + grant_moderation + revoke_moderation + backup_create + revoke_email + deactivate_user + lock_trust_level + unlock_trust_level + activate_user + change_readonly_mode + backup_download + backup_destroy + post_locked + post_unlocked + check_personal_message + disabled_second_factor + post_edit + topic_published + recover_topic + post_approved + create_badge + change_badge + delete_badge + post_rejected + merge_user + entity_export + change_name + topic_timestamps_changed + approve_user + web_hook_create + web_hook_update + web_hook_destroy + web_hook_deactivate + embeddable_host_create + embeddable_host_update + embeddable_host_destroy + change_theme_setting + disable_theme_component + enable_theme_component + revoke_title + change_title + api_key_create + api_key_update + api_key_destroy + override_upload_secure_status + page_published + page_unpublished + add_email + update_email + destroy_email + topic_closed + topic_opened + topic_archived + topic_unarchived + post_staff_note_create + post_staff_note_destroy + watched_word_create + watched_word_destroy + delete_group ] end @@ -228,7 +229,7 @@ class UserHistory < ActiveRecord::Base query = query.where(action: filters[:action_id]) if filters[:action_id].present? query = query.where(custom_type: filters[:custom_type]) if filters[:custom_type].present? - [:acting_user, :target_user].each do |key| + %i[acting_user target_user].each do |key| if filters[key] && (obj_id = User.where(username_lower: filters[key].downcase).pluck(:id)) query = query.where("#{key}_id = ?", obj_id) end @@ -249,7 +250,7 @@ class UserHistory < ActiveRecord::Base end def self.staff_filters - [:action_id, :custom_type, :acting_user, :target_user, :subject, :action_name] + %i[action_id custom_type acting_user target_user subject action_name] end def self.staff_action_records(viewer, opts = nil) @@ -262,11 +263,12 @@ class UserHistory < ActiveRecord::Base opts[:action_id] = self.actions[opts[:action_name].to_sym] if opts[:action_name] end - query = self - .with_filters(opts.slice(*staff_filters)) - .only_staff_actions - .order('id DESC') - .includes(:acting_user, :target_user) + query = + self + .with_filters(opts.slice(*staff_filters)) + .only_staff_actions + .order("id DESC") + .includes(:acting_user, :target_user) query = query.where(admin_only: false) unless viewer && viewer.admin? query end diff --git a/app/models/user_notification_schedule.rb b/app/models/user_notification_schedule.rb index b80b4406a5..4a2bfac0fe 100644 --- a/app/models/user_notification_schedule.rb +++ b/app/models/user_notification_schedule.rb @@ -3,17 +3,17 @@ class UserNotificationSchedule < ActiveRecord::Base belongs_to :user - DEFAULT = -> { + DEFAULT = -> do attrs = { enabled: false } 7.times do |n| attrs["day_#{n}_start_time".to_sym] = 480 attrs["day_#{n}_end_time".to_sym] = 1020 end attrs - }.call + end.call validate :has_valid_times - validates :enabled, inclusion: { in: [ true, false ] } + validates :enabled, inclusion: { in: [true, false] } scope :enabled, -> { where(enabled: true) } @@ -37,9 +37,7 @@ class UserNotificationSchedule < ActiveRecord::Base errors.add(start_key, "is invalid") end - if self[end_key].nil? || self[end_key] > 1440 - errors.add(end_key, "is invalid") - end + errors.add(end_key, "is invalid") if self[end_key].nil? || self[end_key] > 1440 if self[start_key] && self[end_key] && self[start_key] > self[end_key] errors.add(start_key, "is after end time") diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 1bf0062d20..70d80d8052 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -11,10 +11,15 @@ class UserOption < ActiveRecord::Base after_save :update_tracked_topics - scope :human_users, -> { where('user_id > 0') } + scope :human_users, -> { where("user_id > 0") } enum default_calendar: { none_selected: 0, ics: 1, google: 2 }, _scopes: false - enum sidebar_list_destination: { none_selected: 0, default: 0, unread_new: 1 }, _prefix: "sidebar_list" + enum sidebar_list_destination: { + none_selected: 0, + default: 0, + unread_new: 1, + }, + _prefix: "sidebar_list" def self.ensure_consistency! sql = <<~SQL @@ -23,9 +28,7 @@ class UserOption < ActiveRecord::Base WHERE o.user_id IS NULL SQL - DB.query_single(sql).each do |id| - UserOption.create(user_id: id) - end + DB.query_single(sql).each { |id| UserOption.create(user_id: id) } end def self.previous_replies_type @@ -33,7 +36,8 @@ class UserOption < ActiveRecord::Base end def self.like_notification_frequency_type - @like_notification_frequency_type ||= Enum.new(always: 0, first_time_and_daily: 1, first_time: 2, never: 3) + @like_notification_frequency_type ||= + Enum.new(always: 0, first_time_and_daily: 1, first_time: 2, never: 3) end def self.text_sizes @@ -70,7 +74,8 @@ class UserOption < ActiveRecord::Base self.new_topic_duration_minutes = SiteSetting.default_other_new_topic_duration_minutes self.auto_track_topics_after_msecs = SiteSetting.default_other_auto_track_topics_after_msecs - self.notification_level_when_replying = SiteSetting.default_other_notification_level_when_replying + self.notification_level_when_replying = + SiteSetting.default_other_notification_level_when_replying self.like_notification_frequency = SiteSetting.default_other_like_notification_frequency @@ -111,7 +116,12 @@ class UserOption < ActiveRecord::Base Discourse.redis.expire(key, delay) # delay the update - Jobs.enqueue_in(delay / 2, :update_top_redirection, user_id: self.user_id, redirected_at: Time.zone.now.to_s) + Jobs.enqueue_in( + delay / 2, + :update_top_redirection, + user_id: self.user_id, + redirected_at: Time.zone.now.to_s, + ) end def should_be_redirected_to_top @@ -133,16 +143,10 @@ class UserOption < ActiveRecord::Base if !user.seen_before? || (user.trust_level == 0 && !redirected_to_top_yet?) update_last_redirected_to_top! - return { - reason: I18n.t('redirected_to_top_reasons.new_user'), - period: period - } + return { reason: I18n.t("redirected_to_top_reasons.new_user"), period: period } elsif user.last_seen_at < 1.month.ago update_last_redirected_to_top! - return { - reason: I18n.t('redirected_to_top_reasons.not_seen_in_a_month'), - period: period - } + return { reason: I18n.t("redirected_to_top_reasons.not_seen_in_a_month"), period: period } end # don't redirect to top @@ -150,7 +154,8 @@ class UserOption < ActiveRecord::Base end def treat_as_new_topic_start_date - duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i + duration = + new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i times = [ case duration when User::NewTopicDuration::ALWAYS @@ -161,7 +166,7 @@ class UserOption < ActiveRecord::Base duration.minutes.ago end, user.created_at, - Time.at(SiteSetting.min_new_topics_time).to_datetime + Time.at(SiteSetting.min_new_topics_time).to_datetime, ] times.max @@ -169,14 +174,22 @@ class UserOption < ActiveRecord::Base def homepage case homepage_id - when 1 then "latest" - when 2 then "categories" - when 3 then "unread" - when 4 then "new" - when 5 then "top" - when 6 then "bookmarks" - when 7 then "unseen" - else SiteSetting.homepage + when 1 + "latest" + when 2 + "categories" + when 3 + "unread" + when 4 + "new" + when 5 + "top" + when 6 + "bookmarks" + when 7 + "unseen" + else + SiteSetting.homepage end end @@ -197,9 +210,7 @@ class UserOption < ActiveRecord::Base end def unsubscribed_from_all? - !mailing_list_mode && - !email_digests && - email_level == UserOption.email_level_types[:never] && + !mailing_list_mode && !email_digests && email_level == UserOption.email_level_types[:never] && email_messages_level == UserOption.email_level_types[:never] end @@ -208,14 +219,16 @@ class UserOption < ActiveRecord::Base end def self.user_tzinfo(user_id) - timezone = UserOption.where(user_id: user_id).pluck(:timezone).first || 'UTC' + timezone = UserOption.where(user_id: user_id).pluck(:timezone).first || "UTC" tzinfo = nil begin tzinfo = ActiveSupport::TimeZone.find_tzinfo(timezone) rescue TZInfo::InvalidTimezoneIdentifier - Rails.logger.warn("#{User.find_by(id: user_id)&.username} has the timezone #{timezone} set, we do not know how to parse it in Rails, fallback to UTC") - tzinfo = ActiveSupport::TimeZone.find_tzinfo('UTC') + Rails.logger.warn( + "#{User.find_by(id: user_id)&.username} has the timezone #{timezone} set, we do not know how to parse it in Rails, fallback to UTC", + ) + tzinfo = ActiveSupport::TimeZone.find_tzinfo("UTC") end tzinfo @@ -227,7 +240,6 @@ class UserOption < ActiveRecord::Base return unless saved_change_to_auto_track_topics_after_msecs? TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call end - end # == Schema Information diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb index e3136c6526..2ed6d1b027 100644 --- a/app/models/user_profile.rb +++ b/app/models/user_profile.rb @@ -7,13 +7,19 @@ class UserProfile < ActiveRecord::Base belongs_to :card_background_upload, class_name: "Upload" belongs_to :profile_background_upload, class_name: "Upload" belongs_to :granted_title_badge, class_name: "Badge" - belongs_to :featured_topic, class_name: 'Topic' + belongs_to :featured_topic, class_name: "Topic" has_many :upload_references, as: :target, dependent: :destroy has_many :user_profile_views, dependent: :destroy validates :bio_raw, length: { maximum: 3000 }, watched_words: true - validates :website, url: true, length: { maximum: 3000 }, allow_blank: true, if: :validate_website? + validates :website, + url: true, + length: { + maximum: 3000, + }, + allow_blank: true, + if: :validate_website? validates :location, length: { maximum: 3000 }, watched_words: true validates :user, presence: true @@ -26,8 +32,11 @@ class UserProfile < ActiveRecord::Base after_save :pull_hotlinked_image after_save do - if saved_change_to_profile_background_upload_id? || saved_change_to_card_background_upload_id? || saved_change_to_bio_raw? - upload_ids = [self.profile_background_upload_id, self.card_background_upload_id] + Upload.extract_upload_ids(self.bio_raw) + if saved_change_to_profile_background_upload_id? || + saved_change_to_card_background_upload_id? || saved_change_to_bio_raw? + upload_ids = + [self.profile_background_upload_id, self.card_background_upload_id] + + Upload.extract_upload_ids(self.bio_raw) UploadReference.ensure_exist!(upload_ids: upload_ids, target: self) end end @@ -36,13 +45,15 @@ class UserProfile < ActiveRecord::Base def bio_excerpt(length = 350, opts = {}) return nil if bio_cooked.blank? - excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
    $/, '') + excerpt = PrettyText.excerpt(bio_cooked, length, opts).sub(/
    $/, "") return excerpt if excerpt.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) PrettyText.strip_links(excerpt) end def bio_processed - return bio_cooked if bio_cooked.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) + if bio_cooked.blank? || (user.has_trust_level?(TrustLevel[1]) && !user.suspended?) + return bio_cooked + end PrettyText.strip_links(bio_cooked) end @@ -73,14 +84,16 @@ class UserProfile < ActiveRecord::Base def self.rebake_old(limit) problems = [] - UserProfile.where('bio_cooked_version IS NULL OR bio_cooked_version < ?', BAKED_VERSION) - .limit(limit).each do |p| - begin - p.rebake! - rescue => e - problems << { profile: p, ex: e } + UserProfile + .where("bio_cooked_version IS NULL OR bio_cooked_version < ?", BAKED_VERSION) + .limit(limit) + .each do |p| + begin + p.rebake! + rescue => e + problems << { profile: p, ex: e } + end end - end problems end @@ -90,15 +103,18 @@ class UserProfile < ActiveRecord::Base def self.import_url_for_user(background_url, user, options = nil) if SiteSetting.verbose_upload_logging - Rails.logger.warn("Verbose Upload Logging: Downloading profile background from #{background_url}") + Rails.logger.warn( + "Verbose Upload Logging: Downloading profile background from #{background_url}", + ) end - tempfile = FileHelper.download( - background_url, - max_file_size: SiteSetting.max_image_size_kb.kilobytes, - tmp_file_name: "sso-profile-background", - follow_redirect: true - ) + tempfile = + FileHelper.download( + background_url, + max_file_size: SiteSetting.max_image_size_kb.kilobytes, + tmp_file_name: "sso-profile-background", + follow_redirect: true, + ) return unless tempfile @@ -108,14 +124,19 @@ class UserProfile < ActiveRecord::Base is_card_background = !options || options[:is_card_background] type = is_card_background ? "card_background" : "profile_background" - upload = UploadCreator.new(tempfile, "external-profile-background." + ext, origin: background_url, type: type).create_for(user.id) + upload = + UploadCreator.new( + tempfile, + "external-profile-background." + ext, + origin: background_url, + type: type, + ).create_for(user.id) if (is_card_background) user.user_profile.upload_card_background(upload) else user.user_profile.upload_profile_background(upload) end - rescue Net::ReadTimeout, OpenURI::HTTPError # skip saving, we are not connected to the net ensure @@ -133,7 +154,7 @@ class UserProfile < ActiveRecord::Base Jobs.enqueue_in( SiteSetting.editing_grace_period, :pull_user_profile_hotlinked_images, - user_id: self.user_id + user_id: self.user_id, ) end end @@ -146,7 +167,10 @@ class UserProfile < ActiveRecord::Base def cooked if self.bio_raw.present? - PrettyText.cook(self.bio_raw, omit_nofollow: user.has_trust_level?(TrustLevel[3]) && !SiteSetting.tl3_links_no_follow) + PrettyText.cook( + self.bio_raw, + omit_nofollow: user.has_trust_level?(TrustLevel[3]) && !SiteSetting.tl3_links_no_follow, + ) else nil end @@ -171,11 +195,20 @@ class UserProfile < ActiveRecord::Base allowed_domains = SiteSetting.allowed_user_website_domains return if (allowed_domains.blank? || self.website.blank?) - domain = begin - URI.parse(self.website).host - rescue URI::Error + domain = + begin + URI.parse(self.website).host + rescue URI::Error + end + unless allowed_domains.split("|").include?(domain) + self.errors.add :base, + ( + I18n.t( + "user.website.domain_not_allowed", + domains: allowed_domains.split("|").join(", "), + ) + ) end - self.errors.add :base, (I18n.t('user.website.domain_not_allowed', domains: allowed_domains.split('|').join(", "))) unless allowed_domains.split('|').include?(domain) end def validate_website? diff --git a/app/models/user_profile_view.rb b/app/models/user_profile_view.rb index 5bc6acbe14..0648b75eb0 100644 --- a/app/models/user_profile_view.rb +++ b/app/models/user_profile_view.rb @@ -16,11 +16,13 @@ class UserProfileView < ActiveRecord::Base redis_key << ":ip-#{ip}" end - if skip_redis || Discourse.redis.setnx(redis_key, '1') - skip_redis || Discourse.redis.expire(redis_key, SiteSetting.user_profile_view_duration_hours.hours) + if skip_redis || Discourse.redis.setnx(redis_key, "1") + skip_redis || + Discourse.redis.expire(redis_key, SiteSetting.user_profile_view_duration_hours.hours) self.transaction do - sql = "INSERT INTO user_profile_views (user_profile_id, ip_address, viewed_at, user_id) + sql = + "INSERT INTO user_profile_views (user_profile_id, ip_address, viewed_at, user_id) SELECT :user_profile_id, :ip_address, :viewed_at, :user_id WHERE NOT EXISTS ( SELECT 1 FROM user_profile_views @@ -30,16 +32,24 @@ class UserProfileView < ActiveRecord::Base builder = DB.build(sql) if !user_id - builder.where("viewed_at = :viewed_at AND ip_address = :ip_address AND user_profile_id = :user_profile_id AND user_id IS NULL") + builder.where( + "viewed_at = :viewed_at AND ip_address = :ip_address AND user_profile_id = :user_profile_id AND user_id IS NULL", + ) else - builder.where("viewed_at = :viewed_at AND user_id = :user_id AND user_profile_id = :user_profile_id") + builder.where( + "viewed_at = :viewed_at AND user_id = :user_id AND user_profile_id = :user_profile_id", + ) end - result = builder.exec(user_profile_id: user_profile_id, ip_address: ip, viewed_at: at, user_id: user_id) + result = + builder.exec( + user_profile_id: user_profile_id, + ip_address: ip, + viewed_at: at, + user_id: user_id, + ) - if result > 0 - UserProfile.find(user_profile_id).increment!(:views) - end + UserProfile.find(user_profile_id).increment!(:views) if result > 0 end end end @@ -47,8 +57,10 @@ class UserProfileView < ActiveRecord::Base def self.profile_views_by_day(start_date, end_date, group_id = nil) profile_views = self.where("viewed_at >= ? AND viewed_at < ?", start_date, end_date + 1.day) if group_id - profile_views = profile_views.joins("INNER JOIN users ON users.id = user_profile_views.user_id") - profile_views = profile_views.joins("INNER JOIN group_users ON group_users.user_id = users.id") + profile_views = + profile_views.joins("INNER JOIN users ON users.id = user_profile_views.user_id") + profile_views = + profile_views.joins("INNER JOIN group_users ON group_users.user_id = users.id") profile_views = profile_views.where("group_users.group_id = ?", group_id) end profile_views.group("date(viewed_at)").order("date(viewed_at)").count diff --git a/app/models/user_search.rb b/app/models/user_search.rb index baa25dfffa..7b21ff6b7f 100644 --- a/app/models/user_search.rb +++ b/app/models/user_search.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserSearch - MAX_SIZE_PRIORITY_MENTION ||= 500 def initialize(term, opts = {}) @@ -31,17 +30,16 @@ class UserSearch users = users.not_suspended unless @searching_user&.staff? if @groups - users = users - .joins(:group_users) - .where("group_users.group_id IN (?)", @groups.map(&:id)) + users = users.joins(:group_users).where("group_users.group_id IN (?)", @groups.map(&:id)) end # Only show users who have access to private topic if @topic_allowed_users == "true" && @topic&.category&.read_restricted - users = users - .references(:categories) - .includes(:secure_categories) - .where("users.admin OR categories.id = ?", @topic.category_id) + users = + users + .references(:categories) + .includes(:secure_categories) + .where("users.admin OR categories.id = ?", @topic.category_id) end users @@ -70,27 +68,27 @@ class UserSearch exact_matches = scoped_users.where(username_lower: @term) # don't pollute mentions with users who haven't shown up in over a year - exact_matches = exact_matches.where('last_seen_at > ?', 1.year.ago) if @topic_id || @category_id + exact_matches = exact_matches.where("last_seen_at > ?", 1.year.ago) if @topic_id || + @category_id - exact_matches - .limit(@limit) - .pluck(:id) - .each { |id| users << id } + exact_matches.limit(@limit).pluck(:id).each { |id| users << id } end return users.to_a if users.size >= @limit # 2. in topic if @topic_id - in_topic = filtered_by_term_users - .where('users.id IN (SELECT user_id FROM posts WHERE topic_id = ? AND post_type = ? AND deleted_at IS NULL)', @topic_id, Post.types[:regular]) + in_topic = + filtered_by_term_users.where( + "users.id IN (SELECT user_id FROM posts WHERE topic_id = ? AND post_type = ? AND deleted_at IS NULL)", + @topic_id, + Post.types[:regular], + ) - if @searching_user.present? - in_topic = in_topic.where('users.id <> ?', @searching_user.id) - end + in_topic = in_topic.where("users.id <> ?", @searching_user.id) if @searching_user.present? in_topic - .order('last_seen_at DESC NULLS LAST') + .order("last_seen_at DESC NULLS LAST") .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } @@ -131,8 +129,7 @@ class UserSearch category_groups = category_groups.members_visible_groups(@searching_user) end - in_category = filtered_by_term_users - .where(<<~SQL, category_groups.pluck(:id)) + in_category = filtered_by_term_users.where(<<~SQL, category_groups.pluck(:id)) users.id IN ( SELECT gu.user_id FROM group_users gu @@ -142,11 +139,11 @@ class UserSearch SQL if @searching_user.present? - in_category = in_category.where('users.id <> ?', @searching_user.id) + in_category = in_category.where("users.id <> ?", @searching_user.id) end in_category - .order('last_seen_at DESC NULLS LAST') + .order("last_seen_at DESC NULLS LAST") .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } @@ -157,7 +154,7 @@ class UserSearch # 4. global matches if @term.present? filtered_by_term_users - .order('last_seen_at DESC NULLS LAST') + .order("last_seen_at DESC NULLS LAST") .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } @@ -166,7 +163,7 @@ class UserSearch # 5. last seen users (for search auto-suggestions) if @last_seen_users scoped_users - .order('last_seen_at DESC NULLS LAST') + .order("last_seen_at DESC NULLS LAST") .limit(@limit - users.size) .pluck(:id) .each { |id| users << id } @@ -179,16 +176,15 @@ class UserSearch ids = search_ids return User.where("0=1") if ids.empty? - results = User.joins("JOIN (SELECT unnest uid, row_number() OVER () AS rn + results = + User.joins( + "JOIN (SELECT unnest uid, row_number() OVER () AS rn FROM unnest('{#{ids.join(",")}}'::int[]) - ) x on uid = users.id") - .order("rn") + ) x on uid = users.id", + ).order("rn") - if SiteSetting.enable_user_status - results = results.includes(:user_status) - end + results = results.includes(:user_status) if SiteSetting.enable_user_status results end - end diff --git a/app/models/user_second_factor.rb b/app/models/user_second_factor.rb index 7846f27ad8..3c71c6b65e 100644 --- a/app/models/user_second_factor.rb +++ b/app/models/user_second_factor.rb @@ -4,24 +4,14 @@ class UserSecondFactor < ActiveRecord::Base include SecondFactorManager belongs_to :user - scope :backup_codes, -> do - where(method: UserSecondFactor.methods[:backup_codes], enabled: true) - end + scope :backup_codes, -> { where(method: UserSecondFactor.methods[:backup_codes], enabled: true) } - scope :totps, -> do - where(method: UserSecondFactor.methods[:totp], enabled: true) - end + scope :totps, -> { where(method: UserSecondFactor.methods[:totp], enabled: true) } - scope :all_totps, -> do - where(method: UserSecondFactor.methods[:totp]) - end + scope :all_totps, -> { where(method: UserSecondFactor.methods[:totp]) } def self.methods - @methods ||= Enum.new( - totp: 1, - backup_codes: 2, - security_key: 3, - ) + @methods ||= Enum.new(totp: 1, backup_codes: 2, security_key: 3) end def totp_object @@ -31,7 +21,6 @@ class UserSecondFactor < ActiveRecord::Base def totp_provisioning_uri totp_object.provisioning_uri(user.email) end - end # == Schema Information diff --git a/app/models/user_security_key.rb b/app/models/user_security_key.rb index 229dbbdf96..5447ee23ad 100644 --- a/app/models/user_security_key.rb +++ b/app/models/user_security_key.rb @@ -3,16 +3,11 @@ class UserSecurityKey < ActiveRecord::Base belongs_to :user - scope :second_factors, -> do - where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) - end + scope :second_factors, + -> { where(factor_type: UserSecurityKey.factor_types[:second_factor], enabled: true) } def self.factor_types - @factor_types ||= Enum.new( - second_factor: 0, - first_factor: 1, - multi_factor: 2, - ) + @factor_types ||= Enum.new(second_factor: 0, first_factor: 1, multi_factor: 2) end end diff --git a/app/models/user_stat.rb b/app/models/user_stat.rb index e6a86c9115..894a888c1f 100644 --- a/app/models/user_stat.rb +++ b/app/models/user_stat.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true class UserStat < ActiveRecord::Base - belongs_to :user after_save :trigger_badges @@ -21,7 +20,8 @@ class UserStat < ActiveRecord::Base def self.update_first_unread_pm(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT) whisperers_group_ids = SiteSetting.whispers_allowed_group_ids - DB.exec(<<~SQL, archetype: Archetype.private_message, now: UPDATE_UNREAD_MINUTES_AGO.minutes.ago, last_seen: last_seen, limit: limit, whisperers_group_ids: whisperers_group_ids) + DB.exec( + <<~SQL, UPDATE user_stats us SET first_unread_pm_at = COALESCE(Z.min_date, :now) FROM ( @@ -37,11 +37,11 @@ class UserStat < ActiveRecord::Base INNER JOIN topics t ON t.id = tau.topic_id INNER JOIN users u ON u.id = tau.user_id LEFT JOIN topic_users tu ON t.id = tu.topic_id AND tu.user_id = tau.user_id - #{whisperers_group_ids.present? ? 'LEFT JOIN group_users gu ON gu.group_id IN (:whisperers_group_ids) AND gu.user_id = u.id' : ''} + #{whisperers_group_ids.present? ? "LEFT JOIN group_users gu ON gu.group_id IN (:whisperers_group_ids) AND gu.user_id = u.id" : ""} WHERE t.deleted_at IS NULL AND t.archetype = :archetype AND tu.last_read_post_number < CASE - WHEN u.admin OR u.moderator #{whisperers_group_ids.present? ? 'OR gu.id IS NOT NULL' : ''} + WHEN u.admin OR u.moderator #{whisperers_group_ids.present? ? "OR gu.id IS NOT NULL" : ""} THEN t.highest_staff_post_number ELSE t.highest_post_number END @@ -67,6 +67,12 @@ class UserStat < ActiveRecord::Base ) AS Z WHERE us.user_id = Z.user_id SQL + archetype: Archetype.private_message, + now: UPDATE_UNREAD_MINUTES_AGO.minutes.ago, + last_seen: last_seen, + limit: limit, + whisperers_group_ids: whisperers_group_ids, + ) end def self.update_first_unread(last_seen, limit: UPDATE_UNREAD_USERS_LIMIT) @@ -140,14 +146,14 @@ class UserStat < ActiveRecord::Base end def self.reset_bounce_scores - UserStat.where("reset_bounce_score_after < now()") + UserStat + .where("reset_bounce_score_after < now()") .where("bounce_score > 0") .update_all(bounce_score: 0) end # Updates the denormalized view counts for all users def self.update_view_counts(last_seen = 1.hour.ago) - # NOTE: we only update the counts for users we have seen in the last hour # this avoids a very expensive query that may run on the entire user base # we also ensure we only touch the table if data changes @@ -210,7 +216,8 @@ class UserStat < ActiveRecord::Base def self.update_draft_count(user_id = nil) if user_id.present? - draft_count, has_topic_draft = DB.query_single <<~SQL, user_id: user_id, new_topic: Draft::NEW_TOPIC + draft_count, has_topic_draft = + DB.query_single <<~SQL, user_id: user_id, new_topic: Draft::NEW_TOPIC UPDATE user_stats SET draft_count = (SELECT COUNT(*) FROM drafts WHERE user_id = :user_id) WHERE user_id = :user_id @@ -219,11 +226,8 @@ class UserStat < ActiveRecord::Base MessageBus.publish( "/user-drafts/#{user_id}", - { - draft_count: draft_count, - has_topic_draft: !!has_topic_draft - }, - user_ids: [user_id] + { draft_count: draft_count, has_topic_draft: !!has_topic_draft }, + user_ids: [user_id], ) else DB.exec <<~SQL @@ -249,7 +253,7 @@ class UserStat < ActiveRecord::Base AND topics.user_id <> posts.user_id AND posts.deleted_at IS NULL AND topics.deleted_at IS NULL AND topics.archetype <> 'private_message' - #{start_time.nil? ? '' : 'AND posts.created_at > ?'} + #{start_time.nil? ? "" : "AND posts.created_at > ?"} SQL if start_time.nil? DB.query_single(sql, self.user_id).first @@ -303,7 +307,7 @@ class UserStat < ActiveRecord::Base "/u/#{user.username_lower}/counters", { pending_posts_count: pending_posts_count }, user_ids: [user.id], - group_ids: [Group::AUTO_GROUPS[:staff]] + group_ids: [Group::AUTO_GROUPS[:staff]], ) end diff --git a/app/models/user_summary.rb b/app/models/user_summary.rb index f30507117d..c5e4c93fee 100644 --- a/app/models/user_summary.rb +++ b/app/models/user_summary.rb @@ -3,11 +3,10 @@ # ViewModel used on Summary tab on User page class UserSummary - MAX_SUMMARY_RESULTS = 6 MAX_BADGES = 6 - alias :read_attribute_for_serialization :send + alias read_attribute_for_serialization send def initialize(user, guardian) @user = user @@ -20,14 +19,14 @@ class UserSummary .listable_topics .visible .where(user: @user) - .order('like_count DESC, created_at DESC') + .order("like_count DESC, created_at DESC") .limit(MAX_SUMMARY_RESULTS) end def replies post_query - .where('post_number > 1') - .order('posts.like_count DESC, posts.created_at DESC') + .where("post_number > 1") + .order("posts.like_count DESC, posts.created_at DESC") .limit(MAX_SUMMARY_RESULTS) end @@ -36,11 +35,11 @@ class UserSummary .joins(:topic, :post) .where(posts: { user_id: @user.id }) .includes(:topic, :post) - .where('posts.post_type IN (?)', Topic.visible_post_types(@guardian && @guardian.user)) + .where("posts.post_type IN (?)", Topic.visible_post_types(@guardian && @guardian.user)) .merge(Topic.listable_topics.visible.secured(@guardian)) .where(user: @user) .where(internal: false, reflection: false, quote: false) - .order('clicks DESC, topic_links.created_at DESC') + .order("clicks DESC, topic_links.created_at DESC") .limit(MAX_SUMMARY_RESULTS) end @@ -50,14 +49,15 @@ class UserSummary def most_liked_by_users likers = {} - UserAction.joins(:target_topic, :target_post) + UserAction + .joins(:target_topic, :target_post) .merge(Topic.listable_topics.visible.secured(@guardian)) .where(user: @user) .where(action_type: UserAction::WAS_LIKED) .group(:acting_user_id) - .order('COUNT(*) DESC') + .order("COUNT(*) DESC") .limit(MAX_SUMMARY_RESULTS) - .pluck('acting_user_id, COUNT(*)') + .pluck("acting_user_id, COUNT(*)") .each { |l| likers[l[0]] = l[1] } user_counts(likers) @@ -65,14 +65,15 @@ class UserSummary def most_liked_users liked_users = {} - UserAction.joins(:target_topic, :target_post) + UserAction + .joins(:target_topic, :target_post) .merge(Topic.listable_topics.visible.secured(@guardian)) .where(action_type: UserAction::WAS_LIKED) .where(acting_user_id: @user.id) .group(:user_id) - .order('COUNT(*) DESC') + .order("COUNT(*) DESC") .limit(MAX_SUMMARY_RESULTS) - .pluck('user_actions.user_id, COUNT(*)') + .pluck("user_actions.user_id, COUNT(*)") .each { |l| liked_users[l[0]] = l[1] } user_counts(liked_users) @@ -84,12 +85,14 @@ class UserSummary replied_users = {} post_query - .joins('JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number') - .where('replies.user_id <> ?', @user.id) - .group('replies.user_id') - .order('COUNT(*) DESC') + .joins( + "JOIN posts replies ON posts.topic_id = replies.topic_id AND posts.reply_to_post_number = replies.post_number", + ) + .where("replies.user_id <> ?", @user.id) + .group("replies.user_id") + .order("COUNT(*) DESC") .limit(MAX_SUMMARY_RESULTS) - .pluck('replies.user_id, COUNT(*)') + .pluck("replies.user_id, COUNT(*)") .each { |r| replied_users[r[0]] = r[1] } user_counts(replied_users) @@ -121,44 +124,42 @@ class UserSummary class CategoryWithCounts < OpenStruct include ActiveModel::SerializerSupport - KEYS = [:id, :name, :color, :text_color, :slug, :read_restricted, :parent_category_id] + KEYS = %i[id name color text_color slug read_restricted parent_category_id] end def top_categories - post_count_query = post_query.group('topics.category_id') + post_count_query = post_query.group("topics.category_id") top_categories = {} - Category.where(id: post_count_query.order("count(*) DESC").limit(MAX_SUMMARY_RESULTS).pluck('category_id')) + Category + .where( + id: post_count_query.order("count(*) DESC").limit(MAX_SUMMARY_RESULTS).pluck("category_id"), + ) .pluck(:id, :name, :color, :text_color, :slug, :read_restricted, :parent_category_id) .each do |c| top_categories[c[0].to_i] = CategoryWithCounts.new( - Hash[CategoryWithCounts::KEYS.zip(c)].merge( - topic_count: 0, - post_count: 0 - ) + Hash[CategoryWithCounts::KEYS.zip(c)].merge(topic_count: 0, post_count: 0), ) end - post_count_query.where('post_number > 1') - .where('topics.category_id in (?)', top_categories.keys) - .pluck('category_id, COUNT(*)') - .each do |r| - top_categories[r[0].to_i].post_count = r[1] - end + post_count_query + .where("post_number > 1") + .where("topics.category_id in (?)", top_categories.keys) + .pluck("category_id, COUNT(*)") + .each { |r| top_categories[r[0].to_i].post_count = r[1] } - Topic.listable_topics.visible.secured(@guardian) - .where('topics.category_id in (?)', top_categories.keys) + Topic + .listable_topics + .visible + .secured(@guardian) + .where("topics.category_id in (?)", top_categories.keys) .where(user: @user) - .group('topics.category_id') - .pluck('category_id, COUNT(*)') - .each do |r| - top_categories[r[0].to_i].topic_count = r[1] - end + .group("topics.category_id") + .pluck("category_id, COUNT(*)") + .each { |r| top_categories[r[0].to_i].topic_count = r[1] } - top_categories.values.sort_by do |r| - -(r[:post_count] + r[:topic_count]) - end + top_categories.values.sort_by { |r| -(r[:post_count] + r[:topic_count]) } end delegate :likes_given, @@ -171,37 +172,42 @@ class UserSummary :time_read, to: :user_stat -protected + protected def user_counts(user_hash) user_ids = user_hash.keys lookup = UserLookup.new(user_ids) - user_ids.map do |user_id| - lookup_hash = lookup[user_id] + user_ids + .map do |user_id| + lookup_hash = lookup[user_id] - if lookup_hash.present? - primary_group = lookup.primary_groups[user_id] - flair_group = lookup.flair_groups[user_id] + if lookup_hash.present? + primary_group = lookup.primary_groups[user_id] + flair_group = lookup.flair_groups[user_id] - UserWithCount.new( - lookup_hash.attributes.merge( - count: user_hash[user_id], - primary_group: primary_group, - flair_group: flair_group + UserWithCount.new( + lookup_hash.attributes.merge( + count: user_hash[user_id], + primary_group: primary_group, + flair_group: flair_group, + ), ) - ) + end end - end.compact.sort_by { |u| -u[:count] } + .compact + .sort_by { |u| -u[:count] } end def post_query Post .joins(:topic) .includes(:topic) - .where('posts.post_type IN (?)', Topic.visible_post_types(@guardian&.user, include_moderator_actions: false)) + .where( + "posts.post_type IN (?)", + Topic.visible_post_types(@guardian&.user, include_moderator_actions: false), + ) .merge(Topic.listable_topics.visible.secured(@guardian)) .where(user: @user) end - end diff --git a/app/models/user_visit.rb b/app/models/user_visit.rb index ca08bc6747..57353a91a9 100644 --- a/app/models/user_visit.rb +++ b/app/models/user_visit.rb @@ -2,7 +2,7 @@ class UserVisit < ActiveRecord::Base def self.counts_by_day_query(start_date, end_date, group_id = nil) - result = where('visited_at >= ? and visited_at <= ?', start_date.to_date, end_date.to_date) + result = where("visited_at >= ? and visited_at <= ?", start_date.to_date, end_date.to_date) if group_id result = result.joins("INNER JOIN users ON users.id = user_visits.user_id") diff --git a/app/models/user_warning.rb b/app/models/user_warning.rb index b11bff2994..1160288041 100644 --- a/app/models/user_warning.rb +++ b/app/models/user_warning.rb @@ -3,7 +3,7 @@ class UserWarning < ActiveRecord::Base belongs_to :user belongs_to :topic - belongs_to :created_by, class_name: 'User' + belongs_to :created_by, class_name: "User" end # == Schema Information diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 747eb2f777..8c82653b20 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -54,16 +54,14 @@ class UsernameValidator def username_present? return unless errors.empty? - if username.blank? - self.errors << I18n.t(:'user.username.blank') - end + self.errors << I18n.t(:"user.username.blank") if username.blank? end def username_length_min? return unless errors.empty? if username_grapheme_clusters.size < User.username_length.begin - self.errors << I18n.t(:'user.username.short', min: User.username_length.begin) + self.errors << I18n.t(:"user.username.short", min: User.username_length.begin) end end @@ -71,9 +69,9 @@ class UsernameValidator return unless errors.empty? if username_grapheme_clusters.size > User.username_length.end - self.errors << I18n.t(:'user.username.long', max: User.username_length.end) + self.errors << I18n.t(:"user.username.long", max: User.username_length.end) elsif username.length > MAX_CHARS - self.errors << I18n.t(:'user.username.too_long') + self.errors << I18n.t(:"user.username.too_long") end end @@ -81,7 +79,7 @@ class UsernameValidator return unless errors.empty? if self.class.invalid_char_pattern.match?(username) - self.errors << I18n.t(:'user.username.characters') + self.errors << I18n.t(:"user.username.characters") end end @@ -89,7 +87,7 @@ class UsernameValidator return unless errors.empty? && self.class.char_allowlist_exists? if username.chars.any? { |c| !self.class.allowed_char?(c) } - self.errors << I18n.t(:'user.username.characters') + self.errors << I18n.t(:"user.username.characters") end end @@ -97,7 +95,7 @@ class UsernameValidator return unless errors.empty? if INVALID_LEADING_CHAR_PATTERN.match?(username_grapheme_clusters.first) - self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric_or_underscore') + self.errors << I18n.t(:"user.username.must_begin_with_alphanumeric_or_underscore") end end @@ -105,7 +103,7 @@ class UsernameValidator return unless errors.empty? if INVALID_TRAILING_CHAR_PATTERN.match?(username_grapheme_clusters.last) - self.errors << I18n.t(:'user.username.must_end_with_alphanumeric') + self.errors << I18n.t(:"user.username.must_end_with_alphanumeric") end end @@ -113,7 +111,7 @@ class UsernameValidator return unless errors.empty? if REPEATED_SPECIAL_CHAR_PATTERN.match?(username) - self.errors << I18n.t(:'user.username.must_not_contain_two_special_chars_in_seq') + self.errors << I18n.t(:"user.username.must_not_contain_two_special_chars_in_seq") end end @@ -121,7 +119,7 @@ class UsernameValidator return unless errors.empty? if CONFUSING_EXTENSIONS.match?(username) - self.errors << I18n.t(:'user.username.must_not_end_with_confusing_suffix') + self.errors << I18n.t(:"user.username.must_not_end_with_confusing_suffix") end end diff --git a/app/models/watched_word.rb b/app/models/watched_word.rb index e396544e87..e493f50e41 100644 --- a/app/models/watched_word.rb +++ b/app/models/watched_word.rb @@ -1,30 +1,31 @@ # frozen_string_literal: true class WatchedWord < ActiveRecord::Base - def self.actions - @actions ||= Enum.new( - block: 1, - censor: 2, - require_approval: 3, - flag: 4, - link: 8, - replace: 5, - tag: 6, - silence: 7, - ) + @actions ||= + Enum.new( + block: 1, + censor: 2, + require_approval: 3, + flag: 4, + link: 8, + replace: 5, + tag: 6, + silence: 7, + ) end MAX_WORDS_PER_ACTION = 2000 before_validation do self.word = self.class.normalize_word(self.word) - if self.action == WatchedWord.actions[:link] && !(self.replacement =~ /^https?:\/\//) - self.replacement = "#{Discourse.base_url}#{self.replacement&.starts_with?("/") ? "" : "/"}#{self.replacement}" + if self.action == WatchedWord.actions[:link] && !(self.replacement =~ %r{^https?://}) + self.replacement = + "#{Discourse.base_url}#{self.replacement&.starts_with?("/") ? "" : "/"}#{self.replacement}" end end - validates :word, presence: true, uniqueness: true, length: { maximum: 100 } + validates :word, presence: true, uniqueness: true, length: { maximum: 100 } validates :action, presence: true validate :replacement_is_url, if: -> { action == WatchedWord.actions[:link] } @@ -36,26 +37,28 @@ class WatchedWord < ActiveRecord::Base end end - after_save :clear_cache + after_save :clear_cache after_destroy :clear_cache scope :by_action, -> { order("action ASC, word ASC") } - scope :for, ->(word:) do - where("(word ILIKE :word AND case_sensitive = 'f') OR (word LIKE :word AND case_sensitive = 't')", word: word) - end + scope :for, + ->(word:) { + where( + "(word ILIKE :word AND case_sensitive = 'f') OR (word LIKE :word AND case_sensitive = 't')", + word: word, + ) + } def self.normalize_word(w) - w.strip.squeeze('*') + w.strip.squeeze("*") end def replacement_is_url - if !(replacement =~ URI::regexp) - errors.add(:base, :invalid_url) - end + errors.add(:base, :invalid_url) if !(replacement =~ URI.regexp) end def replacement_is_tag_list - tag_list = replacement&.split(',') + tag_list = replacement&.split(",") tags = Tag.where(name: tag_list) if (tag_list.blank? || tags.empty? || tag_list.size != tags.size) errors.add(:base, :invalid_tag_list) diff --git a/app/models/web_crawler_request.rb b/app/models/web_crawler_request.rb index 7f7ceff987..c1660f6555 100644 --- a/app/models/web_crawler_request.rb +++ b/app/models/web_crawler_request.rb @@ -16,8 +16,9 @@ class WebCrawlerRequest < ActiveRecord::Base end def self.write_cache!(user_agent, count, date) - where(id: request_id(date: date, user_agent: user_agent)) - .update_all(["count = count + ?", count]) + where(id: request_id(date: date, user_agent: user_agent)).update_all( + ["count = count + ?", count], + ) end protected @@ -25,14 +26,13 @@ class WebCrawlerRequest < ActiveRecord::Base def self.request_id(date:, user_agent:, retries: 0) id = where(date: date, user_agent: user_agent).pluck_first(:id) id ||= create!({ date: date, user_agent: user_agent }.merge(count: 0)).id - rescue # primary key violation + rescue StandardError # primary key violation if retries == 0 request_id(date: date, user_agent: user_agent, retries: 1) else raise end end - end # == Schema Information diff --git a/app/models/web_hook.rb b/app/models/web_hook.rb index d14a0dddad..7ebfd01de1 100644 --- a/app/models/web_hook.rb +++ b/app/models/web_hook.rb @@ -8,9 +8,9 @@ class WebHook < ActiveRecord::Base has_many :web_hook_events, dependent: :destroy - default_scope { order('id ASC') } + default_scope { order("id ASC") } - validates :payload_url, presence: true, format: URI::regexp(%w(http https)) + validates :payload_url, presence: true, format: URI.regexp(%w[http https]) validates :secret, length: { minimum: 12 }, allow_blank: true validates_presence_of :content_type validates_presence_of :last_delivery_status @@ -24,15 +24,11 @@ class WebHook < ActiveRecord::Base end def self.content_types - @content_types ||= Enum.new('application/json' => 1, - 'application/x-www-form-urlencoded' => 2) + @content_types ||= Enum.new("application/json" => 1, "application/x-www-form-urlencoded" => 2) end def self.last_delivery_statuses - @last_delivery_statuses ||= Enum.new(inactive: 1, - failed: 2, - successful: 3, - disabled: 4) + @last_delivery_statuses ||= Enum.new(inactive: 1, failed: 2, successful: 3, disabled: 4) end def self.default_event_types @@ -44,7 +40,8 @@ class WebHook < ActiveRecord::Base end def self.active_web_hooks(type) - WebHook.where(active: true) + WebHook + .where(active: true) .joins(:web_hook_event_types) .where("web_hooks.wildcard_web_hook = ? OR web_hook_event_types.name = ?", true, type.to_s) .distinct @@ -52,9 +49,10 @@ class WebHook < ActiveRecord::Base def self.enqueue_hooks(type, event, opts = {}) active_web_hooks(type).each do |web_hook| - Jobs.enqueue(:emit_web_hook_event, opts.merge( - web_hook_id: web_hook.id, event_name: event.to_s, event_type: type.to_s - )) + Jobs.enqueue( + :emit_web_hook_event, + opts.merge(web_hook_id: web_hook.id, event_name: event.to_s, event_type: type.to_s), + ) end end @@ -62,39 +60,40 @@ class WebHook < ActiveRecord::Base if active_web_hooks(type).exists? payload = WebHook.generate_payload(type, object, serializer) - WebHook.enqueue_hooks(type, event, opts.merge( - id: object.id, - payload: payload - ) - ) + WebHook.enqueue_hooks(type, event, opts.merge(id: object.id, payload: payload)) end end def self.enqueue_topic_hooks(event, topic, payload = nil) - if active_web_hooks('topic').exists? && topic.present? - payload ||= begin - topic_view = TopicView.new(topic.id, Discourse.system_user) - WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) - end + if active_web_hooks("topic").exists? && topic.present? + payload ||= + begin + topic_view = TopicView.new(topic.id, Discourse.system_user) + WebHook.generate_payload(:topic, topic_view, WebHookTopicViewSerializer) + end - WebHook.enqueue_hooks(:topic, event, + WebHook.enqueue_hooks( + :topic, + event, id: topic.id, category_id: topic.category_id, tag_ids: topic.tags.pluck(:id), - payload: payload + payload: payload, ) end end def self.enqueue_post_hooks(event, post, payload = nil) - if active_web_hooks('post').exists? && post.present? + if active_web_hooks("post").exists? && post.present? payload ||= WebHook.generate_payload(:post, post) - WebHook.enqueue_hooks(:post, event, + WebHook.enqueue_hooks( + :post, + event, id: post.id, category_id: post.topic&.category_id, tag_ids: post.topic&.tags&.pluck(:id), - payload: payload + payload: payload, ) end end @@ -103,10 +102,7 @@ class WebHook < ActiveRecord::Base serializer ||= TagSerializer if type == :tag serializer ||= "WebHook#{type.capitalize}Serializer".constantize - serializer.new(object, - scope: self.guardian, - root: false - ).to_json + serializer.new(object, scope: self.guardian, root: false).to_json end private @@ -121,15 +117,14 @@ class WebHook < ActiveRecord::Base return if payload_url.blank? uri = URI(payload_url.strip) - allowed = begin - FinalDestination::SSRFDetector.lookup_and_filter_ips(uri.hostname).present? - rescue FinalDestination::SSRFDetector::DisallowedIpError - false - end + allowed = + begin + FinalDestination::SSRFDetector.lookup_and_filter_ips(uri.hostname).present? + rescue FinalDestination::SSRFDetector::DisallowedIpError + false + end - if !allowed - self.errors.add(:base, I18n.t("webhooks.payload_url.blocked_or_internal")) - end + self.errors.add(:base, I18n.t("webhooks.payload_url.blocked_or_internal")) if !allowed end end diff --git a/app/models/web_hook_event.rb b/app/models/web_hook_event.rb index 9e7ac9cc60..d9a53041f2 100644 --- a/app/models/web_hook_event.rb +++ b/app/models/web_hook_event.rb @@ -5,12 +5,10 @@ class WebHookEvent < ActiveRecord::Base after_save :update_web_hook_delivery_status - default_scope { order('created_at DESC') } + default_scope { order("created_at DESC") } def self.purge_old - where( - 'created_at < ?', SiteSetting.retain_web_hook_events_period_days.days.ago - ).delete_all + where("created_at < ?", SiteSetting.retain_web_hook_events_period_days.days.ago).delete_all end def update_web_hook_delivery_status diff --git a/app/models/web_hook_event_type.rb b/app/models/web_hook_event_type.rb index a8852aeeb9..df3274802b 100644 --- a/app/models/web_hook_event_type.rb +++ b/app/models/web_hook_event_type.rb @@ -18,18 +18,21 @@ class WebHookEventType < ActiveRecord::Base has_and_belongs_to_many :web_hooks - default_scope { order('id ASC') } + default_scope { order("id ASC") } validates :name, presence: true, uniqueness: true def self.active ids_to_exclude = [] - ids_to_exclude << SOLVED unless defined?(SiteSetting.solved_enabled) && SiteSetting.solved_enabled - ids_to_exclude << ASSIGN unless defined?(SiteSetting.assign_enabled) && SiteSetting.assign_enabled + unless defined?(SiteSetting.solved_enabled) && SiteSetting.solved_enabled + ids_to_exclude << SOLVED + end + unless defined?(SiteSetting.assign_enabled) && SiteSetting.assign_enabled + ids_to_exclude << ASSIGN + end self.where.not(id: ids_to_exclude) end - end # == Schema Information diff --git a/app/serializers/about_serializer.rb b/app/serializers/about_serializer.rb index f55bdd3476..7a26822d29 100644 --- a/app/serializers/about_serializer.rb +++ b/app/serializers/about_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AboutSerializer < ApplicationSerializer - class UserAboutSerializer < BasicUserSerializer attributes :title, :last_seen_at end diff --git a/app/serializers/admin_detailed_user_serializer.rb b/app/serializers/admin_detailed_user_serializer.rb index 525036512b..e82bf8ac34 100644 --- a/app/serializers/admin_detailed_user_serializer.rb +++ b/app/serializers/admin_detailed_user_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminDetailedUserSerializer < AdminUserSerializer - attributes :moderator, :can_grant_admin, :can_revoke_admin, @@ -108,11 +107,11 @@ class AdminDetailedUserSerializer < AdminUserSerializer def next_penalty step_number = penalty_counts.total - steps = SiteSetting.penalty_step_hours.split('|') + steps = SiteSetting.penalty_step_hours.split("|") step_number = [step_number, steps.length].min penalty_hours = steps[step_number] Integer(penalty_hours, 10).hours.from_now - rescue + rescue StandardError nil end diff --git a/app/serializers/admin_email_template_serializer.rb b/app/serializers/admin_email_template_serializer.rb index 07bfa92991..d48256dcab 100644 --- a/app/serializers/admin_email_template_serializer.rb +++ b/app/serializers/admin_email_template_serializer.rb @@ -11,7 +11,7 @@ class AdminEmailTemplateSerializer < ApplicationSerializer if I18n.exists?("#{object}.title") I18n.t("#{object}.title") else - object.gsub(/.*\./, '').titleize + object.gsub(/.*\./, "").titleize end end diff --git a/app/serializers/admin_user_action_serializer.rb b/app/serializers/admin_user_action_serializer.rb index 1bff61203d..a07f99b594 100644 --- a/app/serializers/admin_user_action_serializer.rb +++ b/app/serializers/admin_user_action_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'post_item_excerpt' +require_relative "post_item_excerpt" class AdminUserActionSerializer < ApplicationSerializer include PostItemExcerpt @@ -24,7 +24,7 @@ class AdminUserActionSerializer < ApplicationSerializer :deleted_at, :deleted_by, :reply_to_post_number, - :action_type + :action_type, ) def post_id @@ -64,7 +64,8 @@ class AdminUserActionSerializer < ApplicationSerializer end def moderator_action - object.post_type == Post.types[:moderator_action] || object.post_type == Post.types[:small_action] + object.post_type == Post.types[:moderator_action] || + object.post_type == Post.types[:small_action] end def deleted_by @@ -76,9 +77,12 @@ class AdminUserActionSerializer < ApplicationSerializer end def action_type - object.user_actions.select { |ua| ua.user_id = object.user_id } + object + .user_actions + .select { |ua| ua.user_id = object.user_id } .select { |ua| [UserAction::REPLY, UserAction::RESPONSE].include? ua.action_type } - .first.try(:action_type) + .first + .try(:action_type) end # we need this to handle deleted topics which aren't loaded via diff --git a/app/serializers/admin_user_list_serializer.rb b/app/serializers/admin_user_list_serializer.rb index dd9ce3f6f0..9a6be3c71a 100644 --- a/app/serializers/admin_user_list_serializer.rb +++ b/app/serializers/admin_user_list_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminUserListSerializer < BasicUserSerializer - attributes :email, :secondary_emails, :active, @@ -28,7 +27,7 @@ class AdminUserListSerializer < BasicUserSerializer :staged, :second_factor_enabled - [:days_visited, :posts_read_count, :topics_entered, :post_count].each do |sym| + %i[days_visited posts_read_count topics_entered post_count].each do |sym| attributes sym define_method sym do object.user_stat.public_send(sym) @@ -106,13 +105,11 @@ class AdminUserListSerializer < BasicUserSerializer end def include_second_factor_enabled? - !SiteSetting.enable_discourse_connect && - SiteSetting.enable_local_logins && + !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins && object.has_any_second_factor_methods_enabled? end def second_factor_enabled true end - end diff --git a/app/serializers/admin_user_serializer.rb b/app/serializers/admin_user_serializer.rb index e864578f91..fc396fc4bd 100644 --- a/app/serializers/admin_user_serializer.rb +++ b/app/serializers/admin_user_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminUserSerializer < AdminUserListSerializer - attributes :name, :associated_accounts, :can_send_activation_email, @@ -40,5 +39,4 @@ class AdminUserSerializer < AdminUserListSerializer def registration_ip_address object.registration_ip_address.try(:to_s) end - end diff --git a/app/serializers/admin_web_hook_serializer.rb b/app/serializers/admin_web_hook_serializer.rb index cf3134188b..1fa79e384e 100644 --- a/app/serializers/admin_web_hook_serializer.rb +++ b/app/serializers/admin_web_hook_serializer.rb @@ -12,7 +12,12 @@ class AdminWebHookSerializer < ApplicationSerializer :web_hook_event_types has_many :categories, serializer: BasicCategorySerializer, embed: :ids, include: false - has_many :tags, key: :tag_names, serializer: TagSerializer, embed: :ids, embed_key: :name, include: false + has_many :tags, + key: :tag_names, + serializer: TagSerializer, + embed: :ids, + embed_key: :name, + include: false has_many :groups, serializer: BasicGroupSerializer, embed: :ids, include: false def web_hook_event_types diff --git a/app/serializers/api_key_scope_serializer.rb b/app/serializers/api_key_scope_serializer.rb index f874efcf20..3386ab387a 100644 --- a/app/serializers/api_key_scope_serializer.rb +++ b/app/serializers/api_key_scope_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class ApiKeyScopeSerializer < ApplicationSerializer - - attributes :resource, - :action, - :parameters, - :urls, - :allowed_parameters, - :key + attributes :resource, :action, :parameters, :urls, :allowed_parameters, :key def parameters ApiKeyScope.scope_mappings.dig(object.resource.to_sym, object.action.to_sym, :params).to_a @@ -18,7 +12,7 @@ class ApiKeyScopeSerializer < ApplicationSerializer end def action - object.action.to_s.gsub('_', ' ') + object.action.to_s.gsub("_", " ") end def key diff --git a/app/serializers/api_key_serializer.rb b/app/serializers/api_key_serializer.rb index ab2a7a2e4d..d1ccac58de 100644 --- a/app/serializers/api_key_serializer.rb +++ b/app/serializers/api_key_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ApiKeySerializer < ApplicationSerializer - attributes :id, :key, :truncated_key, diff --git a/app/serializers/application_serializer.rb b/app/serializers/application_serializer.rb index 40ee57b062..aeecb04591 100644 --- a/app/serializers/application_serializer.rb +++ b/app/serializers/application_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'distributed_cache' +require "distributed_cache" class ApplicationSerializer < ActiveModel::Serializer embed :ids, include: true @@ -19,7 +19,9 @@ class ApplicationSerializer < ActiveModel::Serializer when String fragment_cache.delete(name_or_regexp) when Regexp - fragment_cache.hash.keys + fragment_cache + .hash + .keys .select { |k| k =~ name_or_regexp } .each { |k| fragment_cache.delete(k) } end diff --git a/app/serializers/archetype_serializer.rb b/app/serializers/archetype_serializer.rb index 469b1195f3..8b74d5140a 100644 --- a/app/serializers/archetype_serializer.rb +++ b/app/serializers/archetype_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ArchetypeSerializer < ApplicationSerializer - attributes :id, :name, :options def options @@ -10,7 +9,7 @@ class ArchetypeSerializer < ApplicationSerializer key: k, title: I18n.t("archetypes.#{object.id}.options.#{k}.title"), description: I18n.t("archetypes.#{object.id}.options.#{k}.description"), - option_type: object.options[k] + option_type: object.options[k], } end end @@ -18,5 +17,4 @@ class ArchetypeSerializer < ApplicationSerializer def name I18n.t("archetypes.#{object.id}.title") end - end diff --git a/app/serializers/associated_group_serializer.rb b/app/serializers/associated_group_serializer.rb index db7dec8723..121afd7d0b 100644 --- a/app/serializers/associated_group_serializer.rb +++ b/app/serializers/associated_group_serializer.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true class AssociatedGroupSerializer < ApplicationSerializer - attributes :id, - :name, - :provider_name, - :label + attributes :id, :name, :provider_name, :label end diff --git a/app/serializers/auth_provider_serializer.rb b/app/serializers/auth_provider_serializer.rb index 78cfcffd0b..d7bc83afc2 100644 --- a/app/serializers/auth_provider_serializer.rb +++ b/app/serializers/auth_provider_serializer.rb @@ -1,9 +1,14 @@ # frozen_string_literal: true class AuthProviderSerializer < ApplicationSerializer - - attributes :name, :custom_url, :pretty_name_override, :title_override, - :frame_width, :frame_height, :can_connect, :can_revoke, + attributes :name, + :custom_url, + :pretty_name_override, + :title_override, + :frame_width, + :frame_height, + :can_connect, + :can_revoke, :icon def title_override @@ -15,5 +20,4 @@ class AuthProviderSerializer < ApplicationSerializer return SiteSetting.get(object.pretty_name_setting) if object.pretty_name_setting object.pretty_name end - end diff --git a/app/serializers/backup_file_serializer.rb b/app/serializers/backup_file_serializer.rb index 1ccab21a15..239c8952c9 100644 --- a/app/serializers/backup_file_serializer.rb +++ b/app/serializers/backup_file_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class BackupFileSerializer < ApplicationSerializer - attributes :filename, - :size, - :last_modified + attributes :filename, :size, :last_modified end diff --git a/app/serializers/badge_index_serializer.rb b/app/serializers/badge_index_serializer.rb index 81e58b3d9e..127bec384a 100644 --- a/app/serializers/badge_index_serializer.rb +++ b/app/serializers/badge_index_serializer.rb @@ -11,5 +11,4 @@ class BadgeIndexSerializer < BadgeSerializer def has_badge @options[:user_badges].include?(object.id) end - end diff --git a/app/serializers/badge_serializer.rb b/app/serializers/badge_serializer.rb index cd837ac1cd..004f019323 100644 --- a/app/serializers/badge_serializer.rb +++ b/app/serializers/badge_serializer.rb @@ -1,9 +1,22 @@ # frozen_string_literal: true class BadgeSerializer < ApplicationSerializer - attributes :id, :name, :description, :grant_count, :allow_title, - :multiple_grant, :icon, :image_url, :listable, :enabled, :badge_grouping_id, - :system, :long_description, :slug, :has_badge, :manually_grantable? + attributes :id, + :name, + :description, + :grant_count, + :allow_title, + :multiple_grant, + :icon, + :image_url, + :listable, + :enabled, + :badge_grouping_id, + :system, + :long_description, + :slug, + :has_badge, + :manually_grantable? has_one :badge_type diff --git a/app/serializers/basic_category_serializer.rb b/app/serializers/basic_category_serializer.rb index fe1b8cd2f7..06dfa9628e 100644 --- a/app/serializers/basic_category_serializer.rb +++ b/app/serializers/basic_category_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class BasicCategorySerializer < ApplicationSerializer - attributes :id, :name, :color, @@ -42,19 +41,35 @@ class BasicCategorySerializer < ApplicationSerializer end def name - object.uncategorized? ? I18n.t('uncategorized_category_name', locale: SiteSetting.default_locale) : object.name + if object.uncategorized? + I18n.t("uncategorized_category_name", locale: SiteSetting.default_locale) + else + object.name + end end def description_text - object.uncategorized? ? I18n.t('category.uncategorized_description', locale: SiteSetting.default_locale) : object.description_text + if object.uncategorized? + I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale) + else + object.description_text + end end def description - object.uncategorized? ? I18n.t('category.uncategorized_description', locale: SiteSetting.default_locale) : object.description + if object.uncategorized? + I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale) + else + object.description + end end def description_excerpt - object.uncategorized? ? I18n.t('category.uncategorized_description', locale: SiteSetting.default_locale) : object.description_excerpt + if object.uncategorized? + I18n.t("category.uncategorized_description", locale: SiteSetting.default_locale) + else + object.description_excerpt + end end def can_edit diff --git a/app/serializers/basic_group_history_serializer.rb b/app/serializers/basic_group_history_serializer.rb index afd1c60f70..40518bd8ec 100644 --- a/app/serializers/basic_group_history_serializer.rb +++ b/app/serializers/basic_group_history_serializer.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class BasicGroupHistorySerializer < ApplicationSerializer - attributes :action, - :subject, - :prev_value, - :new_value, - :created_at + attributes :action, :subject, :prev_value, :new_value, :created_at has_one :acting_user, embed: :objects, serializer: BasicUserSerializer has_one :target_user, embed: :objects, serializer: BasicUserSerializer diff --git a/app/serializers/basic_group_serializer.rb b/app/serializers/basic_group_serializer.rb index e51e600002..ab64df800a 100644 --- a/app/serializers/basic_group_serializer.rb +++ b/app/serializers/basic_group_serializer.rb @@ -44,7 +44,9 @@ class BasicGroupSerializer < ApplicationSerializer end def bio_excerpt - PrettyText.excerpt(object.bio_cooked, 110, keep_emoji_images: true) if object.bio_cooked.present? + if object.bio_cooked.present? + PrettyText.excerpt(object.bio_cooked, 110, keep_emoji_images: true) + end end def include_incoming_email? diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index 3486c47bc2..029b04bdbf 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -2,13 +2,7 @@ # The most basic attributes of a topic that we need to create a link for it. class BasicPostSerializer < ApplicationSerializer - attributes :id, - :name, - :username, - :avatar_template, - :created_at, - :cooked, - :cooked_hidden + attributes :id, :name, :username, :avatar_template, :created_at, :cooked, :cooked_hidden attr_accessor :topic_view @@ -35,9 +29,9 @@ class BasicPostSerializer < ApplicationSerializer def cooked if cooked_hidden if scope.current_user && object.user_id == scope.current_user.id - I18n.t('flagging.you_must_edit', path: "/my/messages") + I18n.t("flagging.you_must_edit", path: "/my/messages") else - I18n.t('flagging.user_must_edit') + I18n.t("flagging.user_must_edit") end else object.filter_quotes(@parent_post) @@ -49,11 +43,11 @@ class BasicPostSerializer < ApplicationSerializer end def post_custom_fields - @post_custom_fields ||= if @topic_view - (@topic_view.post_custom_fields || {})[object.id] || {} - else - object.custom_fields - end + @post_custom_fields ||= + if @topic_view + (@topic_view.post_custom_fields || {})[object.id] || {} + else + object.custom_fields + end end - end diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index fc580931f6..769002d0f0 100644 --- a/app/serializers/basic_user_serializer.rb +++ b/app/serializers/basic_user_serializer.rb @@ -28,9 +28,9 @@ class BasicUserSerializer < ApplicationSerializer end def categories_with_notification_level(lookup_level) - category_user_notification_levels.select do |id, level| - level == CategoryUser.notification_levels[lookup_level] - end.keys + category_user_notification_levels + .select { |id, level| level == CategoryUser.notification_levels[lookup_level] } + .keys end def category_user_notification_levels diff --git a/app/serializers/category_and_topic_lists_serializer.rb b/app/serializers/category_and_topic_lists_serializer.rb index 863164fb10..55983ebc4a 100644 --- a/app/serializers/category_and_topic_lists_serializer.rb +++ b/app/serializers/category_and_topic_lists_serializer.rb @@ -7,9 +7,7 @@ class CategoryAndTopicListsSerializer < ApplicationSerializer has_many :primary_groups, serializer: PrimaryGroupSerializer, embed: :objects def users - users = object.topic_list.topics.map do |t| - t.posters.map { |poster| poster.try(:user) } - end + users = object.topic_list.topics.map { |t| t.posters.map { |poster| poster.try(:user) } } users.flatten! users.compact! users.uniq!(&:id) @@ -17,13 +15,11 @@ class CategoryAndTopicListsSerializer < ApplicationSerializer end def primary_groups - groups = object.topic_list.topics.map do |t| - t.posters.map { |poster| poster.try(:primary_group) } - end + groups = + object.topic_list.topics.map { |t| t.posters.map { |poster| poster.try(:primary_group) } } groups.flatten! groups.compact! groups.uniq!(&:id) groups end - end diff --git a/app/serializers/category_detailed_serializer.rb b/app/serializers/category_detailed_serializer.rb index 1349cd44aa..39197008e2 100644 --- a/app/serializers/category_detailed_serializer.rb +++ b/app/serializers/category_detailed_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class CategoryDetailedSerializer < BasicCategorySerializer - attributes :topic_count, :post_count, :topics_day, @@ -14,7 +13,10 @@ class CategoryDetailedSerializer < BasicCategorySerializer has_many :displayable_topics, serializer: ListableTopicSerializer, embed: :objects, key: :topics - has_many :subcategory_list, serializer: CategoryDetailedSerializer, embed: :objects, key: :subcategory_list + has_many :subcategory_list, + serializer: CategoryDetailedSerializer, + embed: :objects, + key: :subcategory_list def include_displayable_topics? displayable_topics.present? @@ -55,11 +57,8 @@ class CategoryDetailedSerializer < BasicCategorySerializer def count_with_subcategories(method) count = object.public_send(method) || 0 - object.subcategories.each do |category| - count += (category.public_send(method) || 0) - end + object.subcategories.each { |category| count += (category.public_send(method) || 0) } count end - end diff --git a/app/serializers/category_list_serializer.rb b/app/serializers/category_list_serializer.rb index 1fe73e2229..9f159e6fe1 100644 --- a/app/serializers/category_list_serializer.rb +++ b/app/serializers/category_list_serializer.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class CategoryListSerializer < ApplicationSerializer - - attributes :can_create_category, - :can_create_topic + attributes :can_create_category, :can_create_topic has_many :categories, serializer: CategoryDetailedSerializer, embed: :objects @@ -14,5 +12,4 @@ class CategoryListSerializer < ApplicationSerializer def can_create_topic scope.can_create?(Topic) end - end diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index b7ab43b580..d2bfbb1612 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class CategorySerializer < SiteCategorySerializer - attributes :read_restricted, :available_groups, :auto_close_hours, @@ -32,25 +31,25 @@ class CategorySerializer < SiteCategorySerializer end def group_permissions - @group_permissions ||= begin - perms = object - .category_groups - .joins(:group) - .includes(:group) - .merge(Group.visible_groups(scope&.user, "groups.name ASC", include_everyone: true)) - .map do |cg| - { - permission_type: cg.permission_type, - group_name: cg.group.name + @group_permissions ||= + begin + perms = + object + .category_groups + .joins(:group) + .includes(:group) + .merge(Group.visible_groups(scope&.user, "groups.name ASC", include_everyone: true)) + .map { |cg| { permission_type: cg.permission_type, group_name: cg.group.name } } + + if perms.length == 0 && !object.read_restricted + perms << { + permission_type: CategoryGroup.permission_types[:full], + group_name: Group[:everyone]&.name.presence || :everyone, } end - if perms.length == 0 && !object.read_restricted - perms << { permission_type: CategoryGroup.permission_types[:full], group_name: Group[:everyone]&.name.presence || :everyone } + perms end - - perms - end end def include_group_permissions? @@ -70,8 +69,11 @@ class CategorySerializer < SiteCategorySerializer end def include_is_special? - [SiteSetting.meta_category_id, SiteSetting.staff_category_id, SiteSetting.uncategorized_category_id] - .include? object.id + [ + SiteSetting.meta_category_id, + SiteSetting.staff_category_id, + SiteSetting.uncategorized_category_id, + ].include? object.id end def is_special @@ -101,8 +103,8 @@ class CategorySerializer < SiteCategorySerializer def notification_level user = scope && scope.user object.notification_level || - (user && CategoryUser.where(user: user, category: object).first.try(:notification_level)) || - CategoryUser.default_notification_level + (user && CategoryUser.where(user: user, category: object).first.try(:notification_level)) || + CategoryUser.default_notification_level end def custom_fields diff --git a/app/serializers/concerns/email_logs_mixin.rb b/app/serializers/concerns/email_logs_mixin.rb index f0fe239309..4f76679f74 100644 --- a/app/serializers/concerns/email_logs_mixin.rb +++ b/app/serializers/concerns/email_logs_mixin.rb @@ -3,12 +3,12 @@ module EmailLogsMixin def self.included(klass) klass.attributes :id, - :to_address, - :email_type, - :user_id, - :created_at, - :post_url, - :post_description + :to_address, + :email_type, + :user_id, + :created_at, + :post_url, + :post_description klass.has_one :user, serializer: BasicUserSerializer, embed: :objects end diff --git a/app/serializers/concerns/topic_tags_mixin.rb b/app/serializers/concerns/topic_tags_mixin.rb index 5c39cd5a5d..2704ba3361 100644 --- a/app/serializers/concerns/topic_tags_mixin.rb +++ b/app/serializers/concerns/topic_tags_mixin.rb @@ -27,10 +27,15 @@ module TopicTagsMixin def all_tags return @tags if defined?(@tags) # Calling method `pluck` or `order` along with `includes` causing N+1 queries - tags = (SiteSetting.tags_sort_alphabetically ? topic.tags.sort_by(&:name) : topic.tags.sort_by(&:topic_count).reverse) - if !scope.is_staff? - tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) } - end + tags = + ( + if SiteSetting.tags_sort_alphabetically + topic.tags.sort_by(&:name) + else + topic.tags.sort_by(&:topic_count).reverse + end + ) + tags = tags.reject { |tag| scope.hidden_tag_names.include?(tag[:name]) } if !scope.is_staff? @tags = tags end end diff --git a/app/serializers/concerns/user_auth_tokens_mixin.rb b/app/serializers/concerns/user_auth_tokens_mixin.rb index 8e963f7fad..aaa0b752ea 100644 --- a/app/serializers/concerns/user_auth_tokens_mixin.rb +++ b/app/serializers/concerns/user_auth_tokens_mixin.rb @@ -3,16 +3,7 @@ module UserAuthTokensMixin extend ActiveSupport::Concern - included do - attributes :id, - :client_ip, - :location, - :browser, - :device, - :os, - :icon, - :created_at - end + included { attributes :id, :client_ip, :location, :browser, :device, :os, :icon, :created_at } def client_ip object.client_ip.to_s @@ -20,7 +11,7 @@ module UserAuthTokensMixin def location ipinfo = DiscourseIpInfo.get(client_ip, locale: I18n.locale) - ipinfo[:location].presence || I18n.t('staff_action_logs.unknown') + ipinfo[:location].presence || I18n.t("staff_action_logs.unknown") end def browser @@ -41,17 +32,17 @@ module UserAuthTokensMixin def icon case BrowserDetection.os(object.user_agent) when :android - 'fab-android' + "fab-android" when :chromeos - 'fab-chrome' + "fab-chrome" when :macos, :ios - 'fab-apple' + "fab-apple" when :linux - 'fab-linux' + "fab-linux" when :windows - 'fab-windows' + "fab-windows" else - 'question' + "question" end end end diff --git a/app/serializers/concerns/user_primary_group_mixin.rb b/app/serializers/concerns/user_primary_group_mixin.rb index 65441ebba4..0bd2538b1e 100644 --- a/app/serializers/concerns/user_primary_group_mixin.rb +++ b/app/serializers/concerns/user_primary_group_mixin.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module UserPrimaryGroupMixin - def self.included(klass) klass.attributes :primary_group_name, :flair_name, diff --git a/app/serializers/concerns/user_sidebar_mixin.rb b/app/serializers/concerns/user_sidebar_mixin.rb index 1149b63960..cd87219585 100644 --- a/app/serializers/concerns/user_sidebar_mixin.rb +++ b/app/serializers/concerns/user_sidebar_mixin.rb @@ -2,13 +2,11 @@ module UserSidebarMixin def sidebar_tags - object.visible_sidebar_tags(scope) + object + .visible_sidebar_tags(scope) .pluck(:name, :topic_count, :pm_topic_count) .reduce([]) do |tags, sidebar_tag| - tags.push( - name: sidebar_tag[0], - pm_only: sidebar_tag[1] == 0 && sidebar_tag[2] > 0 - ) + tags.push(name: sidebar_tag[0], pm_only: sidebar_tag[1] == 0 && sidebar_tag[2] > 0) end end @@ -33,7 +31,11 @@ module UserSidebarMixin end def sidebar_list_destination - object.user_option.sidebar_list_none_selected? ? SiteSetting.default_sidebar_list_destination : object.user_option.sidebar_list_destination + if object.user_option.sidebar_list_none_selected? + SiteSetting.default_sidebar_list_destination + else + object.user_option.sidebar_list_destination + end end def include_sidebar_list_destination? diff --git a/app/serializers/concerns/user_tag_notifications_mixin.rb b/app/serializers/concerns/user_tag_notifications_mixin.rb index dfa367d616..74c56ec1db 100644 --- a/app/serializers/concerns/user_tag_notifications_mixin.rb +++ b/app/serializers/concerns/user_tag_notifications_mixin.rb @@ -22,9 +22,9 @@ module UserTagNotificationsMixin end def tags_with_notification_level(lookup_level) - tag_user_notification_levels.select do |id, level| - level == TagUser.notification_levels[lookup_level] - end.keys + tag_user_notification_levels + .select { |id, level| level == TagUser.notification_levels[lookup_level] } + .keys end def tag_user_notification_levels diff --git a/app/serializers/current_user_option_serializer.rb b/app/serializers/current_user_option_serializer.rb index edfabcf3d6..93c57a4904 100644 --- a/app/serializers/current_user_option_serializer.rb +++ b/app/serializers/current_user_option_serializer.rb @@ -17,7 +17,7 @@ class CurrentUserOptionSerializer < ApplicationSerializer :seen_popups, :should_be_redirected_to_top, :redirected_to_top, - :treat_as_new_topic_start_date, + :treat_as_new_topic_start_date def likes_notifications_disabled object.likes_notifications_disabled? diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index cb4153edef..9a7cb1d9f1 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -79,11 +79,14 @@ class CurrentUserSerializer < BasicUserSerializer def groups owned_group_ids = GroupUser.where(user_id: id, owner: true).pluck(:group_id).to_set - object.visible_groups.pluck(:id, :name, :has_messages).map do |id, name, has_messages| - group = { id: id, name: name, has_messages: has_messages } - group[:owner] = true if owned_group_ids.include?(id) - group - end + object + .visible_groups + .pluck(:id, :name, :has_messages) + .map do |id, name, has_messages| + group = { id: id, name: name, has_messages: has_messages } + group[:owner] = true if owned_group_ids.include?(id) + group + end end def link_posting_access @@ -141,7 +144,7 @@ class CurrentUserSerializer < BasicUserSerializer def custom_fields fields = nil if SiteSetting.public_user_custom_fields.present? - fields = SiteSetting.public_user_custom_fields.split('|') + fields = SiteSetting.public_user_custom_fields.split("|") end DiscoursePluginRegistry.serialized_current_user_fields.each do |f| fields ||= [] @@ -184,15 +187,21 @@ class CurrentUserSerializer < BasicUserSerializer end def top_category_ids - omitted_notification_levels = [CategoryUser.notification_levels[:muted], CategoryUser.notification_levels[:regular]] - CategoryUser.where(user_id: object.id) + omitted_notification_levels = [ + CategoryUser.notification_levels[:muted], + CategoryUser.notification_levels[:regular], + ] + CategoryUser + .where(user_id: object.id) .where.not(notification_level: omitted_notification_levels) - .order(" + .order( + " CASE WHEN notification_level = 3 THEN 1 WHEN notification_level = 2 THEN 2 WHEN notification_level = 4 THEN 3 - END") + END", + ) .pluck(:category_id) .slice(0, SiteSetting.header_dropdown_category_count) end @@ -293,7 +302,9 @@ class CurrentUserSerializer < BasicUserSerializer def redesigned_topic_timeline_enabled if SiteSetting.enable_experimental_topic_timeline_groups.present? - object.in_any_groups?(SiteSetting.enable_experimental_topic_timeline_groups.split("|").map(&:to_i)) + object.in_any_groups?( + SiteSetting.enable_experimental_topic_timeline_groups.split("|").map(&:to_i), + ) else false end diff --git a/app/serializers/detailed_tag_serializer.rb b/app/serializers/detailed_tag_serializer.rb index 79354be61f..83570c3d5e 100644 --- a/app/serializers/detailed_tag_serializer.rb +++ b/app/serializers/detailed_tag_serializer.rb @@ -28,9 +28,8 @@ class DetailedTagSerializer < TagSerializer private def category_ids - @_category_ids ||= object.categories.pluck(:id) + - object.tag_groups.includes(:categories).map do |tg| - tg.categories.map(&:id) - end.flatten + @_category_ids ||= + object.categories.pluck(:id) + + object.tag_groups.includes(:categories).map { |tg| tg.categories.map(&:id) }.flatten end end diff --git a/app/serializers/detailed_user_badge_serializer.rb b/app/serializers/detailed_user_badge_serializer.rb index e31d8f8e8a..693ceffb0e 100644 --- a/app/serializers/detailed_user_badge_serializer.rb +++ b/app/serializers/detailed_user_badge_serializer.rb @@ -33,7 +33,7 @@ class DetailedUserBadgeSerializer < BasicUserBadgeSerializer def can_favorite SiteSetting.max_favorite_badges > 0 && - (scope.current_user.present? && object.user_id == scope.current_user.id) && - !(1..4).include?(object.badge_id) + (scope.current_user.present? && object.user_id == scope.current_user.id) && + !(1..4).include?(object.badge_id) end end diff --git a/app/serializers/directory_column_serializer.rb b/app/serializers/directory_column_serializer.rb index 4e172d3e9e..608ec4d2dd 100644 --- a/app/serializers/directory_column_serializer.rb +++ b/app/serializers/directory_column_serializer.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class DirectoryColumnSerializer < ApplicationSerializer - attributes :id, - :name, - :type, - :position, - :icon, - :user_field_id + attributes :id, :name, :type, :position, :icon, :user_field_id def name object.name || object.user_field.name diff --git a/app/serializers/directory_item_serializer.rb b/app/serializers/directory_item_serializer.rb index c4923cbbd1..bd218f982b 100644 --- a/app/serializers/directory_item_serializer.rb +++ b/app/serializers/directory_item_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DirectoryItemSerializer < ApplicationSerializer - class UserSerializer < UserNameSerializer include UserPrimaryGroupMixin @@ -12,9 +11,7 @@ class DirectoryItemSerializer < ApplicationSerializer object.user_custom_fields.each do |cuf| user_field_id = @options[:user_custom_field_map][cuf.name] - if user_field_id - fields[user_field_id] = cuf.value - end + fields[user_field_id] = cuf.value if user_field_id end fields @@ -38,9 +35,7 @@ class DirectoryItemSerializer < ApplicationSerializer def attributes hash = super - @options[:attributes].each do |attr| - hash.merge!("#{attr}": object[attr]) - end + @options[:attributes].each { |attr| hash.merge!("#{attr}": object[attr]) } if object.period_type == DirectoryItem.period_types[:all] hash.merge!(time_read: object.user_stat.time_read) diff --git a/app/serializers/draft_serializer.rb b/app/serializers/draft_serializer.rb index bcdfa257fc..8ca8111ccf 100644 --- a/app/serializers/draft_serializer.rb +++ b/app/serializers/draft_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'post_item_excerpt' +require_relative "post_item_excerpt" class DraftSerializer < ApplicationSerializer include PostItemExcerpt @@ -24,7 +24,7 @@ class DraftSerializer < ApplicationSerializer :archived def cooked - object.parsed_data['reply'] || "" + object.parsed_data["reply"] || "" end def draft_username @@ -86,5 +86,4 @@ class DraftSerializer < ApplicationSerializer def include_category_id? object.topic&.category_id&.present? end - end diff --git a/app/serializers/edit_directory_column_serializer.rb b/app/serializers/edit_directory_column_serializer.rb index 7c703d5965..ac9824c500 100644 --- a/app/serializers/edit_directory_column_serializer.rb +++ b/app/serializers/edit_directory_column_serializer.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class EditDirectoryColumnSerializer < DirectoryColumnSerializer - attributes :enabled, - :automatic_position + attributes :enabled, :automatic_position has_one :user_field, serializer: UserFieldSerializer, embed: :objects end diff --git a/app/serializers/email_log_serializer.rb b/app/serializers/email_log_serializer.rb index 803a55c2e5..0e8640bc16 100644 --- a/app/serializers/email_log_serializer.rb +++ b/app/serializers/email_log_serializer.rb @@ -3,10 +3,7 @@ class EmailLogSerializer < ApplicationSerializer include EmailLogsMixin - attributes :reply_key, - :bounced, - :has_bounce_key, - :smtp_transaction_response + attributes :reply_key, :bounced, :has_bounce_key, :smtp_transaction_response has_one :user, serializer: BasicUserSerializer, embed: :objects diff --git a/app/serializers/embeddable_host_serializer.rb b/app/serializers/embeddable_host_serializer.rb index 28bf3e905b..d6a36de5c2 100644 --- a/app/serializers/embeddable_host_serializer.rb +++ b/app/serializers/embeddable_host_serializer.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true class EmbeddableHostSerializer < ApplicationSerializer - - TO_SERIALIZE = [:id, :host, :allowed_paths, :class_name, :category_id] + TO_SERIALIZE = %i[id host allowed_paths class_name category_id] attributes *TO_SERIALIZE - TO_SERIALIZE.each do |attr| - define_method(attr) { object.public_send(attr) } - end - + TO_SERIALIZE.each { |attr| define_method(attr) { object.public_send(attr) } } end diff --git a/app/serializers/flagged_topic_summary_serializer.rb b/app/serializers/flagged_topic_summary_serializer.rb index 0679e1ac4b..251bcd4ca1 100644 --- a/app/serializers/flagged_topic_summary_serializer.rb +++ b/app/serializers/flagged_topic_summary_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class FlaggedTopicSummarySerializer < ActiveModel::Serializer - - attributes( - :id, - :flag_counts, - :user_ids, - :last_flag_at - ) + attributes(:id, :flag_counts, :user_ids, :last_flag_at) has_one :topic, serializer: FlaggedTopicSerializer diff --git a/app/serializers/flagged_user_serializer.rb b/app/serializers/flagged_user_serializer.rb index 92a383559a..ed98e77ac1 100644 --- a/app/serializers/flagged_user_serializer.rb +++ b/app/serializers/flagged_user_serializer.rb @@ -39,11 +39,8 @@ class FlaggedUserSerializer < BasicUserSerializer fields = User.allowed_user_custom_fields(scope) result = {} - fields.each do |k| - result[k] = object.custom_fields[k] if object.custom_fields[k].present? - end + fields.each { |k| result[k] = object.custom_fields[k] if object.custom_fields[k].present? } result end - end diff --git a/app/serializers/group_post_serializer.rb b/app/serializers/group_post_serializer.rb index 1176fc3609..0eed59b7a6 100644 --- a/app/serializers/group_post_serializer.rb +++ b/app/serializers/group_post_serializer.rb @@ -1,18 +1,11 @@ # frozen_string_literal: true -require_relative 'post_item_excerpt' +require_relative "post_item_excerpt" class GroupPostSerializer < ApplicationSerializer include PostItemExcerpt - attributes :id, - :created_at, - :title, - :url, - :category_id, - :post_number, - :topic_id, - :post_type + attributes :id, :created_at, :title, :url, :category_id, :post_number, :topic_id, :post_type has_one :user, serializer: GroupPostUserSerializer, embed: :object has_one :topic, serializer: BasicTopicSerializer, embed: :object diff --git a/app/serializers/group_show_serializer.rb b/app/serializers/group_show_serializer.rb index f3b520d19a..f70c379a4a 100644 --- a/app/serializers/group_show_serializer.rb +++ b/app/serializers/group_show_serializer.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true class GroupShowSerializer < BasicGroupSerializer - attributes :is_group_user, :is_group_owner, :is_group_owner_display, :mentionable, :messageable, :flair_icon, :flair_type + attributes :is_group_user, + :is_group_owner, + :is_group_owner_display, + :mentionable, + :messageable, + :flair_icon, + :flair_type def self.admin_attributes(*attrs) attributes(*attrs) @@ -108,19 +114,16 @@ class GroupShowSerializer < BasicGroupSerializer flair_type.present? && (is_group_owner || scope.is_admin?) end - [:watching, :regular, :tracking, :watching_first_post, :muted].each do |level| + %i[watching regular tracking watching_first_post muted].each do |level| define_method("#{level}_category_ids") do group_category_notifications[NotificationLevels.all[level]] || [] end define_method("include_#{level}_tags?") do - SiteSetting.tagging_enabled? && - scope.is_admin? || (include_is_group_owner? && is_group_owner) + SiteSetting.tagging_enabled? && scope.is_admin? || (include_is_group_owner? && is_group_owner) end - define_method("#{level}_tags") do - group_tag_notifications[NotificationLevels.all[level]] || [] - end + define_method("#{level}_tags") { group_tag_notifications[NotificationLevels.all[level]] || [] } end def associated_group_ids @@ -144,7 +147,8 @@ class GroupShowSerializer < BasicGroupSerializer def group_category_notifications @group_category_notification_defaults ||= - GroupCategoryNotificationDefault.where(group_id: object.id) + GroupCategoryNotificationDefault + .where(group_id: object.id) .pluck(:notification_level, :category_id) .inject({}) do |h, arr| h[arr[0]] ||= [] @@ -155,7 +159,8 @@ class GroupShowSerializer < BasicGroupSerializer def group_tag_notifications @group_tag_notification_defaults ||= - GroupTagNotificationDefault.where(group_id: object.id) + GroupTagNotificationDefault + .where(group_id: object.id) .joins(:tag) .pluck(:notification_level, :name) .inject({}) do |h, arr| diff --git a/app/serializers/group_user_serializer.rb b/app/serializers/group_user_serializer.rb index fa5bbda786..bc286d109b 100644 --- a/app/serializers/group_user_serializer.rb +++ b/app/serializers/group_user_serializer.rb @@ -3,13 +3,7 @@ class GroupUserSerializer < BasicUserSerializer include UserPrimaryGroupMixin - attributes :name, - :title, - :last_posted_at, - :last_seen_at, - :added_at, - :timezone, - :status + attributes :name, :title, :last_posted_at, :last_seen_at, :added_at, :timezone, :status def timezone user.user_option.timezone diff --git a/app/serializers/grouped_screened_url_serializer.rb b/app/serializers/grouped_screened_url_serializer.rb index 9e2a4a1ff7..5117420070 100644 --- a/app/serializers/grouped_screened_url_serializer.rb +++ b/app/serializers/grouped_screened_url_serializer.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true class GroupedScreenedUrlSerializer < ApplicationSerializer - attributes :domain, - :action, - :match_count, - :last_match_at, - :created_at + attributes :domain, :action, :match_count, :last_match_at, :created_at def action - 'do_nothing' + "do_nothing" end end diff --git a/app/serializers/grouped_search_result_serializer.rb b/app/serializers/grouped_search_result_serializer.rb index 653ac70ac8..66c4960269 100644 --- a/app/serializers/grouped_search_result_serializer.rb +++ b/app/serializers/grouped_search_result_serializer.rb @@ -6,7 +6,14 @@ class GroupedSearchResultSerializer < ApplicationSerializer has_many :categories, serializer: BasicCategorySerializer has_many :tags, serializer: TagSerializer has_many :groups, serializer: BasicGroupSerializer - attributes :more_posts, :more_users, :more_categories, :term, :search_log_id, :more_full_page_results, :can_create_topic, :error + attributes :more_posts, + :more_users, + :more_categories, + :term, + :search_log_id, + :more_full_page_results, + :can_create_topic, + :error def search_log_id object.search_log_id @@ -23,5 +30,4 @@ class GroupedSearchResultSerializer < ApplicationSerializer def can_create_topic scope.can_create?(Topic) end - end diff --git a/app/serializers/hidden_profile_serializer.rb b/app/serializers/hidden_profile_serializer.rb index f2a04db807..f0b8e00d3b 100644 --- a/app/serializers/hidden_profile_serializer.rb +++ b/app/serializers/hidden_profile_serializer.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class HiddenProfileSerializer < BasicUserSerializer - attributes( - :profile_hidden?, - :title, - :primary_group_name - ) + attributes(:profile_hidden?, :title, :primary_group_name) def profile_hidden? true diff --git a/app/serializers/incoming_email_details_serializer.rb b/app/serializers/incoming_email_details_serializer.rb index d248a3ac58..490b692e88 100644 --- a/app/serializers/incoming_email_details_serializer.rb +++ b/app/serializers/incoming_email_details_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class IncomingEmailDetailsSerializer < ApplicationSerializer - - attributes :error, - :error_description, - :rejection_message, - :headers, - :subject, - :body + attributes :error, :error_description, :rejection_message, :headers, :subject, :body def initialize(incoming_email, opts) super @@ -39,15 +33,30 @@ class IncomingEmailDetailsSerializer < ApplicationSerializer end def body - body = @mail.text_part.decoded rescue nil - body ||= @mail.html_part.decoded rescue nil - body ||= @mail.body.decoded rescue nil + body = + begin + @mail.text_part.decoded + rescue StandardError + nil + end + body ||= + begin + @mail.html_part.decoded + rescue StandardError + nil + end + body ||= + begin + @mail.body.decoded + rescue StandardError + nil + end return I18n.t("emails.incoming.no_body") if body.blank? - body.encode("utf-8", invalid: :replace, undef: :replace, replace: "") + body + .encode("utf-8", invalid: :replace, undef: :replace, replace: "") .strip .truncate_words(100, escape: false) end - end diff --git a/app/serializers/incoming_email_serializer.rb b/app/serializers/incoming_email_serializer.rb index 54830995a1..116c36a43a 100644 --- a/app/serializers/incoming_email_serializer.rb +++ b/app/serializers/incoming_email_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class IncomingEmailSerializer < ApplicationSerializer - attributes :id, :created_at, :from_address, @@ -34,5 +33,4 @@ class IncomingEmailSerializer < ApplicationSerializer def error @object.error.presence || I18n.t("emails.incoming.unrecognized_error") end - end diff --git a/app/serializers/invite_link_serializer.rb b/app/serializers/invite_link_serializer.rb index 21a6450918..8f454f8727 100644 --- a/app/serializers/invite_link_serializer.rb +++ b/app/serializers/invite_link_serializer.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true class InviteLinkSerializer < ApplicationSerializer - attributes :id, :invite_key, :created_at, :max_redemptions_allowed, :redemption_count, :expires_at, :group_names + attributes :id, + :invite_key, + :created_at, + :max_redemptions_allowed, + :redemption_count, + :expires_at, + :group_names def group_names object.groups.pluck(:name).join(", ") diff --git a/app/serializers/invited_serializer.rb b/app/serializers/invited_serializer.rb index a0f76017e4..c1acf056b2 100644 --- a/app/serializers/invited_serializer.rb +++ b/app/serializers/invited_serializer.rb @@ -6,10 +6,17 @@ class InvitedSerializer < ApplicationSerializer def invites ActiveModel::ArraySerializer.new( object.invite_list, - each_serializer: object.type == "pending" || object.type == "expired" ? InviteSerializer : InvitedUserSerializer, + each_serializer: + ( + if object.type == "pending" || object.type == "expired" + InviteSerializer + else + InvitedUserSerializer + end + ), scope: scope, root: false, - show_emails: object.show_emails + show_emails: object.show_emails, ).as_json end diff --git a/app/serializers/invited_user_record_serializer.rb b/app/serializers/invited_user_record_serializer.rb index 027e8f60da..66cd1e5d20 100644 --- a/app/serializers/invited_user_record_serializer.rb +++ b/app/serializers/invited_user_record_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class InvitedUserRecordSerializer < BasicUserSerializer - attributes :topics_entered, :posts_read_count, :last_seen_at, @@ -56,5 +55,4 @@ class InvitedUserRecordSerializer < BasicUserSerializer def can_see_invite_details? @can_see_invite_details ||= scope.can_see_invite_details?(invited_by) end - end diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index afcbb6fa6c..de117bc102 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ListableTopicSerializer < BasicTopicSerializer - attributes :reply_count, :highest_post_number, :image_url, @@ -114,29 +113,30 @@ class ListableTopicSerializer < BasicTopicSerializer object.excerpt end - alias :include_last_read_post_number? :has_user_data + alias include_last_read_post_number? has_user_data # TODO: For backwards compatibility with themes, # Remove once Discourse 2.8 is released def unread 0 end - alias :include_unread? :has_user_data + alias include_unread? has_user_data # TODO: For backwards compatibility with themes, # Remove once Discourse 2.8 is released def new_posts unread_helper.unread_posts end - alias :include_new_posts? :has_user_data + alias include_new_posts? has_user_data def unread_posts unread_helper.unread_posts end - alias :include_unread_posts? :has_user_data + alias include_unread_posts? has_user_data def include_excerpt? - pinned || SiteSetting.always_include_topic_excerpts || theme_modifier_helper.serialize_topic_excerpts + pinned || SiteSetting.always_include_topic_excerpts || + theme_modifier_helper.serialize_topic_excerpts end def pinned @@ -170,5 +170,4 @@ class ListableTopicSerializer < BasicTopicSerializer def theme_modifier_helper @theme_modifier_helper ||= ThemeModifierHelper.new(request: scope.request) end - end diff --git a/app/serializers/new_post_result_serializer.rb b/app/serializers/new_post_result_serializer.rb index e15af2fad1..f5ad629c80 100644 --- a/app/serializers/new_post_result_serializer.rb +++ b/app/serializers/new_post_result_serializer.rb @@ -1,14 +1,7 @@ # frozen_string_literal: true class NewPostResultSerializer < ApplicationSerializer - attributes :action, - :post, - :errors, - :success, - :pending_count, - :reason, - :message, - :route_to + attributes :action, :post, :errors, :success, :pending_count, :reason, :message, :route_to has_one :pending_post, serializer: TopicPendingPostSerializer, root: false, embed: :objects @@ -81,5 +74,4 @@ class NewPostResultSerializer < ApplicationSerializer def include_message? object.message.present? end - end diff --git a/app/serializers/notification_serializer.rb b/app/serializers/notification_serializer.rb index 40ff319b76..63092363f2 100644 --- a/app/serializers/notification_serializer.rb +++ b/app/serializers/notification_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class NotificationSerializer < ApplicationSerializer - attributes :id, :user_id, :external_id, @@ -47,5 +46,4 @@ class NotificationSerializer < ApplicationSerializer def include_external_id? SiteSetting.enable_discourse_connect end - end diff --git a/app/serializers/permalink_serializer.rb b/app/serializers/permalink_serializer.rb index 78b46fd9a7..7aa66affa2 100644 --- a/app/serializers/permalink_serializer.rb +++ b/app/serializers/permalink_serializer.rb @@ -1,10 +1,22 @@ # frozen_string_literal: true class PermalinkSerializer < ApplicationSerializer - attributes :id, :url, :topic_id, :topic_title, :topic_url, - :post_id, :post_url, :post_number, :post_topic_title, - :category_id, :category_name, :category_url, :external_url, - :tag_id, :tag_name, :tag_url + attributes :id, + :url, + :topic_id, + :topic_title, + :topic_url, + :post_id, + :post_url, + :post_number, + :post_topic_title, + :category_id, + :category_name, + :category_url, + :external_url, + :tag_id, + :tag_name, + :tag_url def topic_title object&.topic&.title diff --git a/app/serializers/post_action_type_serializer.rb b/app/serializers/post_action_type_serializer.rb index 915edb4da1..a829d91fd8 100644 --- a/app/serializers/post_action_type_serializer.rb +++ b/app/serializers/post_action_type_serializer.rb @@ -1,16 +1,7 @@ # frozen_string_literal: true class PostActionTypeSerializer < ApplicationSerializer - - attributes( - :id, - :name_key, - :name, - :description, - :short_description, - :is_flag, - :is_custom_flag - ) + attributes(:id, :name_key, :name, :description, :short_description, :is_flag, :is_custom_flag) include ConfigurableUrls @@ -23,15 +14,15 @@ class PostActionTypeSerializer < ApplicationSerializer end def name - i18n('title') + i18n("title") end def description - i18n('description', tos_url: tos_path, base_path: Discourse.base_path) + i18n("description", tos_url: tos_path, base_path: Discourse.base_path) end def short_description - i18n('short_description', tos_url: tos_path, base_path: Discourse.base_path) + i18n("short_description", tos_url: tos_path, base_path: Discourse.base_path) end def name_key diff --git a/app/serializers/post_action_user_serializer.rb b/app/serializers/post_action_user_serializer.rb index 40d81451ac..b58a5723e3 100644 --- a/app/serializers/post_action_user_serializer.rb +++ b/app/serializers/post_action_user_serializer.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class PostActionUserSerializer < BasicUserSerializer - attributes :post_url, - :username_lower, - :unknown + attributes :post_url, :username_lower, :unknown def id object.user.id @@ -32,5 +30,4 @@ class PostActionUserSerializer < BasicUserSerializer def include_unknown? (@options[:unknown_user_ids] || []).include?(object.user.id) end - end diff --git a/app/serializers/post_item_excerpt.rb b/app/serializers/post_item_excerpt.rb index e7f9f00415..164eb861de 100644 --- a/app/serializers/post_item_excerpt.rb +++ b/app/serializers/post_item_excerpt.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module PostItemExcerpt - def self.included(base) base.attributes(:excerpt, :truncated) end @@ -22,5 +21,4 @@ module PostItemExcerpt def include_truncated? cooked.length > 300 end - end diff --git a/app/serializers/post_revision_serializer.rb b/app/serializers/post_revision_serializer.rb index de4430fafc..46c2835ff0 100644 --- a/app/serializers/post_revision_serializer.rb +++ b/app/serializers/post_revision_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class PostRevisionSerializer < ApplicationSerializer - attributes :created_at, :post_id, # which revision is hidden @@ -35,13 +34,9 @@ class PostRevisionSerializer < ApplicationSerializer changes_name = "#{field}_changes".to_sym self.attributes changes_name - define_method(changes_name) do - { previous: previous[field], current: current[field] } - end + define_method(changes_name) { { previous: previous[field], current: current[field] } } - define_method("include_#{changes_name}?") do - previous[field] != current[field] - end + define_method("include_#{changes_name}?") { previous[field] != current[field] } end add_compared_field :wiki @@ -59,9 +54,12 @@ class PostRevisionSerializer < ApplicationSerializer end def previous_revision - @previous_revision ||= revisions.select { |r| r["revision"] >= first_revision } - .select { |r| r["revision"] < current_revision } - .last.try(:[], "revision") + @previous_revision ||= + revisions + .select { |r| r["revision"] >= first_revision } + .select { |r| r["revision"] < current_revision } + .last + .try(:[], "revision") end def current_revision @@ -69,9 +67,12 @@ class PostRevisionSerializer < ApplicationSerializer end def next_revision - @next_revision ||= revisions.select { |r| r["revision"] <= last_revision } - .select { |r| r["revision"] > current_revision } - .first.try(:[], "revision") + @next_revision ||= + revisions + .select { |r| r["revision"] <= last_revision } + .select { |r| r["revision"] > current_revision } + .first + .try(:[], "revision") end def last_revision @@ -108,8 +109,9 @@ class PostRevisionSerializer < ApplicationSerializer def edit_reason # only show 'edit_reason' when revisions are consecutive - current["edit_reason"] if scope.can_view_hidden_post_revisions? || - current["revision"] == previous["revision"] + 1 + if scope.can_view_hidden_post_revisions? || current["revision"] == previous["revision"] + 1 + current["edit_reason"] + end end def body_changes @@ -119,23 +121,20 @@ class PostRevisionSerializer < ApplicationSerializer { inline: cooked_diff.inline_html, side_by_side: cooked_diff.side_by_side_html, - side_by_side_markdown: raw_diff.side_by_side_markdown + side_by_side_markdown: raw_diff.side_by_side_markdown, } end def title_changes - prev = "
    #{previous["title"] && CGI::escapeHTML(previous["title"])}
    " - cur = "
    #{current["title"] && CGI::escapeHTML(current["title"])}
    " + prev = "
    #{previous["title"] && CGI.escapeHTML(previous["title"])}
    " + cur = "
    #{current["title"] && CGI.escapeHTML(current["title"])}
    " # always show the title for post_number == 1 return if object.post.post_number > 1 && prev == cur diff = DiscourseDiff.new(prev, cur) - { - inline: diff.inline_html, - side_by_side: diff.side_by_side_html - } + { inline: diff.inline_html, side_by_side: diff.side_by_side_html } end def user_changes @@ -148,23 +147,23 @@ class PostRevisionSerializer < ApplicationSerializer current = User.find_by(id: cur) || Discourse.system_user { - previous: { - username: previous.username_lower, - display_username: previous.username, - avatar_template: previous.avatar_template - }, - current: { - username: current.username_lower, - display_username: current.username, - avatar_template: current.avatar_template - } + previous: { + username: previous.username_lower, + display_username: previous.username, + avatar_template: previous.avatar_template, + }, + current: { + username: current.username_lower, + display_username: current.username, + avatar_template: current.avatar_template, + }, } end def tags_changes changes = { previous: filter_visible_tags(previous["tags"]), - current: filter_visible_tags(current["tags"]) + current: filter_visible_tags(current["tags"]), } changes[:previous] == changes[:current] ? nil : changes end @@ -184,18 +183,15 @@ class PostRevisionSerializer < ApplicationSerializer end def revisions - @revisions ||= all_revisions.select { |r| scope.can_view_hidden_post_revisions? || !r["hidden"] } + @revisions ||= + all_revisions.select { |r| scope.can_view_hidden_post_revisions? || !r["hidden"] } end def all_revisions return @all_revisions if @all_revisions - post_revisions = PostRevision - .where(post_id: object.post_id) - .order(number: :desc) - .limit(99) - .to_a - .reverse + post_revisions = + PostRevision.where(post_id: object.post_id).order(number: :desc).limit(99).to_a.reverse latest_modifications = { "raw" => [post.raw], @@ -203,24 +199,24 @@ class PostRevisionSerializer < ApplicationSerializer "edit_reason" => [post.edit_reason], "wiki" => [post.wiki], "post_type" => [post.post_type], - "user_id" => [post.user_id] + "user_id" => [post.user_id], } # Retrieve any `tracked_topic_fields` PostRevisor.tracked_topic_fields.each_key do |field| next if field == :tags # Special handling below - if topic.respond_to?(field) - latest_modifications[field.to_s] = [topic.public_send(field)] - end + latest_modifications[field.to_s] = [topic.public_send(field)] if topic.respond_to?(field) end - latest_modifications["featured_link"] = [post.topic.featured_link] if SiteSetting.topic_featured_link_enabled + latest_modifications["featured_link"] = [ + post.topic.featured_link, + ] if SiteSetting.topic_featured_link_enabled latest_modifications["tags"] = [topic.tags.pluck(:name)] if scope.can_see_tags?(topic) post_revisions << PostRevision.new( number: post_revisions.last.number + 1, hidden: post.hidden, - modifications: latest_modifications + modifications: latest_modifications, ) @all_revisions = [] @@ -231,22 +227,20 @@ class PostRevisionSerializer < ApplicationSerializer revision[:revision] = pr.number revision[:hidden] = pr.hidden - pr.modifications.each_key do |field| - revision[field] = pr.modifications[field][0] - end + pr.modifications.each_key { |field| revision[field] = pr.modifications[field][0] } @all_revisions << revision end # waterfall - (@all_revisions.count - 1).downto(1).each do |r| - cur = @all_revisions[r] - prev = @all_revisions[r - 1] + (@all_revisions.count - 1) + .downto(1) + .each do |r| + cur = @all_revisions[r] + prev = @all_revisions[r - 1] - cur.each_key do |field| - prev[field] = prev.has_key?(field) ? prev[field] : cur[field] + cur.each_key { |field| prev[field] = prev.has_key?(field) ? prev[field] : cur[field] } end - end @all_revisions end @@ -272,5 +266,4 @@ class PostRevisionSerializer < ApplicationSerializer tags end end - end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 426c31f220..78e559c031 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -1,22 +1,19 @@ # frozen_string_literal: true class PostSerializer < BasicPostSerializer - # To pass in additional information we might need - INSTANCE_VARS ||= [ - :parent_post, - :add_raw, - :add_title, - :single_post_link_counts, - :draft_sequence, - :post_actions, - :all_post_actions, - :add_excerpt + INSTANCE_VARS ||= %i[ + parent_post + add_raw + add_title + single_post_link_counts + draft_sequence + post_actions + all_post_actions + add_excerpt ] - INSTANCE_VARS.each do |v| - self.public_send(:attr_accessor, v) - end + INSTANCE_VARS.each { |v| self.public_send(:attr_accessor, v) } attributes :post_number, :post_type, @@ -95,9 +92,7 @@ class PostSerializer < BasicPostSerializer super(object, opts) PostSerializer::INSTANCE_VARS.each do |name| - if opts.include? name - self.public_send("#{name}=", opts[name]) - end + self.public_send("#{name}=", opts[name]) if opts.include? name end end @@ -150,13 +145,14 @@ class PostSerializer < BasicPostSerializer end def include_group_moderator? - @group_moderator ||= begin - if @topic_view - @topic_view.category_group_moderator_user_ids.include?(object.user_id) - else - object&.user&.guardian&.is_category_group_moderator?(object&.topic&.category) + @group_moderator ||= + begin + if @topic_view + @topic_view.category_group_moderator_user_ids.include?(object.user_id) + else + object&.user&.guardian&.is_category_group_moderator?(object&.topic&.category) + end end - end end def yours @@ -260,7 +256,7 @@ class PostSerializer < BasicPostSerializer { username: object.reply_to_user.username, name: object.reply_to_user.name, - avatar_template: object.reply_to_user.avatar_template + avatar_template: object.reply_to_user.avatar_template, } end @@ -290,16 +286,22 @@ class PostSerializer < BasicPostSerializer count = object.public_send(count_col) if object.respond_to?(count_col) summary = { id: id, count: count } - if scope.post_can_act?(object, sym, opts: { taken_actions: actions }, can_see_post: can_see_post) + if scope.post_can_act?( + object, + sym, + opts: { + taken_actions: actions, + }, + can_see_post: can_see_post, + ) summary[:can_act] = true end if sym == :notify_user && - ( - (scope.current_user.present? && scope.current_user == object.user) || - (object.user && object.user.bot?) - ) - + ( + (scope.current_user.present? && scope.current_user == object.user) || + (object.user && object.user.bot?) + ) summary.delete(:can_act) end @@ -316,9 +318,7 @@ class PostSerializer < BasicPostSerializer summary.delete(:count) if summary[:count] == 0 # Only include it if the user can do it or it has a count - if summary[:can_act] || summary[:count] - result << summary - end + result << summary if summary[:can_act] || summary[:count] end result @@ -339,7 +339,8 @@ class PostSerializer < BasicPostSerializer def include_link_counts? return true if @single_post_link_counts.present? - @topic_view.present? && @topic_view.link_counts.present? && @topic_view.link_counts[object.id].present? + @topic_view.present? && @topic_view.link_counts.present? && + @topic_view.link_counts[object.id].present? end def include_read? @@ -496,9 +497,7 @@ class PostSerializer < BasicPostSerializer end def include_last_wiki_edit? - object.wiki && - object.post_number == 1 && - object.revisions.size > 0 + object.wiki && object.post_number == 1 && object.revisions.size > 0 end def include_hidden_reason_id? @@ -563,9 +562,7 @@ class PostSerializer < BasicPostSerializer def mentioned_users if @topic_view && (mentions = @topic_view.mentions[object.id]) - users = mentions - .map { |username| @topic_view.mentioned_users[username] } - .compact + users = mentions.map { |username| @topic_view.mentioned_users[username] }.compact else users = User.where(username: object.mentions) end @@ -573,7 +570,7 @@ class PostSerializer < BasicPostSerializer users.map { |user| BasicUserWithStatusSerializer.new(user, root: false) } end -private + private def can_review_topic? return @can_review_topic unless @can_review_topic.nil? diff --git a/app/serializers/post_stream_serializer_mixin.rb b/app/serializers/post_stream_serializer_mixin.rb index 662e187b82..e3e8612aab 100644 --- a/app/serializers/post_stream_serializer_mixin.rb +++ b/app/serializers/post_stream_serializer_mixin.rb @@ -42,17 +42,17 @@ module PostStreamSerializerMixin end def posts - @posts ||= begin - (object.posts || []).map do |post| - post.topic = object.topic + @posts ||= + begin + (object.posts || []).map do |post| + post.topic = object.topic - serializer = PostSerializer.new(post, scope: scope, root: false) - serializer.add_raw = true if @options[:include_raw] - serializer.topic_view = object + serializer = PostSerializer.new(post, scope: scope, root: false) + serializer.add_raw = true if @options[:include_raw] + serializer.topic_view = object - serializer.as_json + serializer.as_json + end end - end end - end diff --git a/app/serializers/post_wordpress_serializer.rb b/app/serializers/post_wordpress_serializer.rb index a415d22583..d8839115d6 100644 --- a/app/serializers/post_wordpress_serializer.rb +++ b/app/serializers/post_wordpress_serializer.rb @@ -11,5 +11,4 @@ class PostWordpressSerializer < BasicPostSerializer nil end end - end diff --git a/app/serializers/queued_post_serializer.rb b/app/serializers/queued_post_serializer.rb index f9dc0dcdef..91d64f26bf 100644 --- a/app/serializers/queued_post_serializer.rb +++ b/app/serializers/queued_post_serializer.rb @@ -14,13 +14,13 @@ class QueuedPostSerializer < ApplicationSerializer :post_options, :created_at, :category_id, - :can_delete_user + :can_delete_user, ) has_one :created_by, serializer: AdminUserListSerializer, root: :users has_one :topic, serializer: BasicTopicSerializer def queue - 'default' + "default" end def user_id @@ -40,11 +40,11 @@ class QueuedPostSerializer < ApplicationSerializer end def raw - object.payload['raw'] + object.payload["raw"] end def post_options - object.payload.except('raw') + object.payload.except("raw") end def can_delete_user @@ -58,9 +58,6 @@ class QueuedPostSerializer < ApplicationSerializer private def post_history - object. - reviewable_histories. - transitioned. - order(:created_at) + object.reviewable_histories.transitioned.order(:created_at) end end diff --git a/app/serializers/reviewable_action_serializer.rb b/app/serializers/reviewable_action_serializer.rb index fbba925f27..469164bc26 100644 --- a/app/serializers/reviewable_action_serializer.rb +++ b/app/serializers/reviewable_action_serializer.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true class ReviewableActionSerializer < ApplicationSerializer - attributes :id, :icon, :button_class, :label, :confirm_message, :description, :client_action, :require_reject_reason, :custom_modal + attributes :id, + :icon, + :button_class, + :label, + :confirm_message, + :description, + :client_action, + :require_reject_reason, + :custom_modal def label I18n.t(object.label) diff --git a/app/serializers/reviewable_bundled_action_serializer.rb b/app/serializers/reviewable_bundled_action_serializer.rb index 45875ac2bd..e55241af05 100644 --- a/app/serializers/reviewable_bundled_action_serializer.rb +++ b/app/serializers/reviewable_bundled_action_serializer.rb @@ -2,7 +2,7 @@ class ReviewableBundledActionSerializer < ApplicationSerializer attributes :id, :icon, :label - has_many :actions, serializer: ReviewableActionSerializer, root: 'actions' + has_many :actions, serializer: ReviewableActionSerializer, root: "actions" def label I18n.t(object.label, default: nil) diff --git a/app/serializers/reviewable_conversation_post_serializer.rb b/app/serializers/reviewable_conversation_post_serializer.rb index cccd746b36..e6608db5f8 100644 --- a/app/serializers/reviewable_conversation_post_serializer.rb +++ b/app/serializers/reviewable_conversation_post_serializer.rb @@ -2,5 +2,5 @@ class ReviewableConversationPostSerializer < ApplicationSerializer attributes :id, :excerpt - has_one :user, serializer: BasicUserSerializer, root: 'users' + has_one :user, serializer: BasicUserSerializer, root: "users" end diff --git a/app/serializers/reviewable_explanation_serializer.rb b/app/serializers/reviewable_explanation_serializer.rb index f268d12794..d2cc51c606 100644 --- a/app/serializers/reviewable_explanation_serializer.rb +++ b/app/serializers/reviewable_explanation_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class ReviewableExplanationSerializer < ApplicationSerializer - attributes( - :id, - :total_score, - :scores, - :min_score_visibility, - :hide_post_score - ) + attributes(:id, :total_score, :scores, :min_score_visibility, :hide_post_score) has_many :scores, serializer: ReviewableScoreExplanationSerializer, embed: :objects diff --git a/app/serializers/reviewable_history_serializer.rb b/app/serializers/reviewable_history_serializer.rb index d4ae2e7838..30feff7730 100644 --- a/app/serializers/reviewable_history_serializer.rb +++ b/app/serializers/reviewable_history_serializer.rb @@ -6,5 +6,5 @@ class ReviewableHistorySerializer < ApplicationSerializer attribute :reviewable_history_type_for_database, key: :reviewable_history_type attribute :status_for_database, key: :status - has_one :created_by, serializer: BasicUserSerializer, root: 'users' + has_one :created_by, serializer: BasicUserSerializer, root: "users" end diff --git a/app/serializers/reviewable_perform_result_serializer.rb b/app/serializers/reviewable_perform_result_serializer.rb index b5ff078fe8..84b8974fa6 100644 --- a/app/serializers/reviewable_perform_result_serializer.rb +++ b/app/serializers/reviewable_perform_result_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ReviewablePerformResultSerializer < ApplicationSerializer - attributes( :success, :transition_to, @@ -11,7 +10,7 @@ class ReviewablePerformResultSerializer < ApplicationSerializer :remove_reviewable_ids, :version, :reviewable_count, - :unseen_reviewable_count + :unseen_reviewable_count, ) def success diff --git a/app/serializers/reviewable_queued_post_serializer.rb b/app/serializers/reviewable_queued_post_serializer.rb index c2ce6fecf2..94a7918a23 100644 --- a/app/serializers/reviewable_queued_post_serializer.rb +++ b/app/serializers/reviewable_queued_post_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ReviewableQueuedPostSerializer < ReviewableSerializer - attributes :reply_to_post_number payload_attributes( @@ -18,15 +17,14 @@ class ReviewableQueuedPostSerializer < ReviewableSerializer :composer_open_duration_msecs, :tags, :via_email, - :raw_email + :raw_email, ) def reply_to_post_number - object.payload['reply_to_post_number'].to_i + object.payload["reply_to_post_number"].to_i end def include_reply_to_post_number? - object.payload.present? && object.payload['reply_to_post_number'].present? + object.payload.present? && object.payload["reply_to_post_number"].present? end - end diff --git a/app/serializers/reviewable_score_explanation_serializer.rb b/app/serializers/reviewable_score_explanation_serializer.rb index f008202288..0c100c2392 100644 --- a/app/serializers/reviewable_score_explanation_serializer.rb +++ b/app/serializers/reviewable_score_explanation_serializer.rb @@ -10,6 +10,6 @@ class ReviewableScoreExplanationSerializer < ApplicationSerializer :flags_disagreed, :flags_ignored, :user_accuracy_bonus, - :score + :score, ) end diff --git a/app/serializers/reviewable_score_serializer.rb b/app/serializers/reviewable_score_serializer.rb index 6a0eac38d0..bbf26b62ce 100644 --- a/app/serializers/reviewable_score_serializer.rb +++ b/app/serializers/reviewable_score_serializer.rb @@ -2,33 +2,33 @@ class ReviewableScoreSerializer < ApplicationSerializer REASONS_AND_SETTINGS = { - post_count: 'approve_post_count', - trust_level: 'approve_unless_trust_level', - new_topics_unless_trust_level: 'approve_new_topics_unless_trust_level', - fast_typer: 'min_first_post_typing_time', - auto_silence_regex: 'auto_silence_first_post_regex', - staged: 'approve_unless_staged', - must_approve_users: 'must_approve_users', - invite_only: 'invite_only', - email_spam: 'email_in_spam_header', - suspect_user: 'approve_suspect_users', - contains_media: 'review_media_unless_trust_level', + post_count: "approve_post_count", + trust_level: "approve_unless_trust_level", + new_topics_unless_trust_level: "approve_new_topics_unless_trust_level", + fast_typer: "min_first_post_typing_time", + auto_silence_regex: "auto_silence_first_post_regex", + staged: "approve_unless_staged", + must_approve_users: "must_approve_users", + invite_only: "invite_only", + email_spam: "email_in_spam_header", + suspect_user: "approve_suspect_users", + contains_media: "review_media_unless_trust_level", } attributes :id, :score, :agree_stats, :reason, :created_at, :reviewed_at attribute :status_for_database, key: :status - has_one :user, serializer: BasicUserSerializer, root: 'users' + has_one :user, serializer: BasicUserSerializer, root: "users" has_one :score_type, serializer: ReviewableScoreTypeSerializer has_one :reviewable_conversation, serializer: ReviewableConversationSerializer - has_one :reviewed_by, serializer: BasicUserSerializer, root: 'users' + has_one :reviewed_by, serializer: BasicUserSerializer, root: "users" def agree_stats { agreed: user.user_stat.flags_agreed, disagreed: user.user_stat.flags_disagreed, - ignored: user.user_stat.flags_ignored + ignored: user.user_stat.flags_ignored, } end @@ -69,18 +69,18 @@ class ReviewableScoreSerializer < ApplicationSerializer def url_for(reason, text) case reason - when 'watched_word' + when "watched_word" "#{Discourse.base_url}/admin/customize/watched_words" - when 'category' - "#{Discourse.base_url}/c/#{object.reviewable.category&.name}/edit/settings" + when "category" + "#{Discourse.base_url}/c/#{object.reviewable.category&.slug}/edit/settings" else "#{Discourse.base_url}/admin/site_settings/category/all_results?filter=#{text}" end end def build_link_for(reason, text) - return text.gsub('_', ' ') unless scope.is_staff? + return text.gsub("_", " ") unless scope.is_staff? - "#{text.gsub('_', ' ')}" + "#{text.gsub("_", " ")}" end end diff --git a/app/serializers/reviewable_score_type_serializer.rb b/app/serializers/reviewable_score_type_serializer.rb index 60368ebcbc..e31e5fafd9 100644 --- a/app/serializers/reviewable_score_type_serializer.rb +++ b/app/serializers/reviewable_score_type_serializer.rb @@ -20,5 +20,4 @@ class ReviewableScoreTypeSerializer < ApplicationSerializer def icon "flag" end - end diff --git a/app/serializers/reviewable_serializer.rb b/app/serializers/reviewable_serializer.rb index 2260a4f9db..20c02e4b72 100644 --- a/app/serializers/reviewable_serializer.rb +++ b/app/serializers/reviewable_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ReviewableSerializer < ApplicationSerializer - class_attribute :_payload_for_serialization attributes( @@ -16,18 +15,18 @@ class ReviewableSerializer < ApplicationSerializer :can_edit, :score, :version, - :target_created_by_trust_level + :target_created_by_trust_level, ) attribute :status_for_database, key: :status - has_one :created_by, serializer: UserWithCustomFieldsSerializer, root: 'users' - has_one :target_created_by, serializer: UserWithCustomFieldsSerializer, root: 'users' + has_one :created_by, serializer: UserWithCustomFieldsSerializer, root: "users" + has_one :target_created_by, serializer: UserWithCustomFieldsSerializer, root: "users" has_one :topic, serializer: ListableTopicSerializer has_many :editable_fields, serializer: ReviewableEditableFieldSerializer, embed: :objects has_many :reviewable_scores, serializer: ReviewableScoreSerializer has_many :bundled_actions, serializer: ReviewableBundledActionSerializer - has_one :claimed_by, serializer: UserWithCustomFieldsSerializer, root: 'users' + has_one :claimed_by, serializer: UserWithCustomFieldsSerializer, root: "users" # Used to keep track of our payload attributes class_attribute :_payload_for_serialization @@ -73,9 +72,7 @@ class ReviewableSerializer < ApplicationSerializer # This is easier than creating an AMS method for each attribute def self.target_attributes(*attributes) - attributes.each do |a| - create_attribute(a, "object.target&.#{a}") - end + attributes.each { |a| create_attribute(a, "object.target&.#{a}") } end def self.payload_attributes(*attributes) @@ -108,7 +105,9 @@ class ReviewableSerializer < ApplicationSerializer end def target_url - return Discourse.base_url + object.target.url if object.target.is_a?(Post) && object.target.present? + if object.target.is_a?(Post) && object.target.present? + return Discourse.base_url + object.target.url + end topic_url end @@ -135,5 +134,4 @@ class ReviewableSerializer < ApplicationSerializer def target_created_by_trust_level object&.target_created_by&.trust_level end - end diff --git a/app/serializers/reviewable_settings_serializer.rb b/app/serializers/reviewable_settings_serializer.rb index f2a34e4e96..5b21101000 100644 --- a/app/serializers/reviewable_settings_serializer.rb +++ b/app/serializers/reviewable_settings_serializer.rb @@ -14,8 +14,6 @@ class ReviewableSettingsSerializer < ApplicationSerializer end def reviewable_priorities - Reviewable.priorities.map do |p| - { id: p[1], name: I18n.t("reviewables.priorities.#{p[0]}") } - end + Reviewable.priorities.map { |p| { id: p[1], name: I18n.t("reviewables.priorities.#{p[0]}") } } end end diff --git a/app/serializers/reviewable_topic_serializer.rb b/app/serializers/reviewable_topic_serializer.rb index 70e0d7fa03..59bf143f86 100644 --- a/app/serializers/reviewable_topic_serializer.rb +++ b/app/serializers/reviewable_topic_serializer.rb @@ -12,10 +12,10 @@ class ReviewableTopicSerializer < ApplicationSerializer :archetype, :relative_url, :stats, - :reviewable_score + :reviewable_score, ) - has_one :claimed_by, serializer: BasicUserSerializer, root: 'users' + has_one :claimed_by, serializer: BasicUserSerializer, root: "users" def stats @options[:stats][object.id] @@ -28,5 +28,4 @@ class ReviewableTopicSerializer < ApplicationSerializer def include_claimed_by? @options[:claimed_topics] end - end diff --git a/app/serializers/reviewable_user_serializer.rb b/app/serializers/reviewable_user_serializer.rb index 5841899cfd..c517338201 100644 --- a/app/serializers/reviewable_user_serializer.rb +++ b/app/serializers/reviewable_user_serializer.rb @@ -1,16 +1,9 @@ # frozen_string_literal: true class ReviewableUserSerializer < ReviewableSerializer - attributes :link_admin, :user_fields, :reject_reason - payload_attributes( - :username, - :email, - :name, - :bio, - :website - ) + payload_attributes(:username, :email, :name, :bio, :website) def link_admin scope.is_staff? && object.target.present? @@ -23,5 +16,4 @@ class ReviewableUserSerializer < ReviewableSerializer def include_user_fields? object.target.present? && object.target.user_fields.present? end - end diff --git a/app/serializers/screened_email_serializer.rb b/app/serializers/screened_email_serializer.rb index bb38bd9093..799d122a3f 100644 --- a/app/serializers/screened_email_serializer.rb +++ b/app/serializers/screened_email_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class ScreenedEmailSerializer < ApplicationSerializer - attributes :email, - :action, - :match_count, - :last_match_at, - :created_at, - :ip_address, - :id + attributes :email, :action, :match_count, :last_match_at, :created_at, :ip_address, :id def action ScreenedEmail.actions.key(object.action_type).to_s @@ -16,5 +10,4 @@ class ScreenedEmailSerializer < ApplicationSerializer def ip_address object.ip_address.try(:to_s) end - end diff --git a/app/serializers/screened_ip_address_serializer.rb b/app/serializers/screened_ip_address_serializer.rb index f833e0b5fd..75853a2aaa 100644 --- a/app/serializers/screened_ip_address_serializer.rb +++ b/app/serializers/screened_ip_address_serializer.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class ScreenedIpAddressSerializer < ApplicationSerializer - attributes :id, - :ip_address, - :action_name, - :match_count, - :last_match_at, - :created_at + attributes :id, :ip_address, :action_name, :match_count, :last_match_at, :created_at def action_name ScreenedIpAddress.actions.key(object.action_type).to_s @@ -15,5 +10,4 @@ class ScreenedIpAddressSerializer < ApplicationSerializer def ip_address object.ip_address_with_mask end - end diff --git a/app/serializers/screened_url_serializer.rb b/app/serializers/screened_url_serializer.rb index 7c071349fa..c4f561071e 100644 --- a/app/serializers/screened_url_serializer.rb +++ b/app/serializers/screened_url_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class ScreenedUrlSerializer < ApplicationSerializer - attributes :url, - :domain, - :action, - :match_count, - :last_match_at, - :created_at, - :ip_address + attributes :url, :domain, :action, :match_count, :last_match_at, :created_at, :ip_address def action ScreenedUrl.actions.key(object.action_type).to_s @@ -16,5 +10,4 @@ class ScreenedUrlSerializer < ApplicationSerializer def ip_address object.ip_address.try(:to_s) end - end diff --git a/app/serializers/search_logs_serializer.rb b/app/serializers/search_logs_serializer.rb index 81f70e0694..910aeb6606 100644 --- a/app/serializers/search_logs_serializer.rb +++ b/app/serializers/search_logs_serializer.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true class SearchLogsSerializer < ApplicationSerializer - attributes :term, - :searches, - :ctr + attributes :term, :searches, :ctr end diff --git a/app/serializers/similar_admin_user_serializer.rb b/app/serializers/similar_admin_user_serializer.rb index 3f343ed841..542d4885c2 100644 --- a/app/serializers/similar_admin_user_serializer.rb +++ b/app/serializers/similar_admin_user_serializer.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class SimilarAdminUserSerializer < AdminUserListSerializer - attributes :can_be_suspended, - :can_be_silenced + attributes :can_be_suspended, :can_be_silenced def can_be_suspended scope.can_suspend?(object) diff --git a/app/serializers/similar_topic_serializer.rb b/app/serializers/similar_topic_serializer.rb index 68d6ec7374..7c73730f2b 100644 --- a/app/serializers/similar_topic_serializer.rb +++ b/app/serializers/similar_topic_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SimilarTopicSerializer < ApplicationSerializer - has_one :topic, serializer: TopicListItemSerializer, embed: :ids attributes :id, :blurb, :created_at, :url diff --git a/app/serializers/single_sign_on_record_serializer.rb b/app/serializers/single_sign_on_record_serializer.rb index 55af0fb7bc..5bc8803715 100644 --- a/app/serializers/single_sign_on_record_serializer.rb +++ b/app/serializers/single_sign_on_record_serializer.rb @@ -1,9 +1,12 @@ # frozen_string_literal: true class SingleSignOnRecordSerializer < ApplicationSerializer - attributes :user_id, :external_id, - :created_at, :updated_at, - :external_username, :external_name, + attributes :user_id, + :external_id, + :created_at, + :updated_at, + :external_username, + :external_name, :external_avatar_url, :external_profile_background_url, :external_card_background_url diff --git a/app/serializers/site_category_serializer.rb b/app/serializers/site_category_serializer.rb index 1d29b7c7af..869e9b7868 100644 --- a/app/serializers/site_category_serializer.rb +++ b/app/serializers/site_category_serializer.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class SiteCategorySerializer < BasicCategorySerializer - - attributes :allowed_tags, - :allowed_tag_groups, - :allow_global_tags, - :read_only_banner + attributes :allowed_tags, :allowed_tag_groups, :allow_global_tags, :read_only_banner has_many :category_required_tag_groups, key: :required_tag_groups, embed: :objects diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 0ddc329fd8..4aa2186232 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SiteSerializer < ApplicationSerializer - attributes( :default_archetype, :notification_types, @@ -40,7 +39,7 @@ class SiteSerializer < ApplicationSerializer :displayed_about_plugin_stat_groups, :show_welcome_topic_banner, :anonymous_default_sidebar_tags, - :whispers_allowed_groups_names + :whispers_allowed_groups_names, ) has_many :archetypes, embed: :objects, serializer: ArchetypeSerializer @@ -49,19 +48,29 @@ class SiteSerializer < ApplicationSerializer def user_themes cache_fragment("user_themes") do - Theme.where('id = :default OR user_selectable', - default: SiteSetting.default_theme_id) + Theme + .where("id = :default OR user_selectable", default: SiteSetting.default_theme_id) .order("lower(name)") .pluck(:id, :name, :color_scheme_id) - .map { |id, n, cs| { theme_id: id, name: n, default: id == SiteSetting.default_theme_id, color_scheme_id: cs } } + .map do |id, n, cs| + { + theme_id: id, + name: n, + default: id == SiteSetting.default_theme_id, + color_scheme_id: cs, + } + end .as_json end end def user_color_schemes cache_fragment("user_color_schemes") do - schemes = ColorScheme.includes(:color_scheme_colors).where('user_selectable').order(:name) - ActiveModel::ArraySerializer.new(schemes, each_serializer: ColorSchemeSelectableSerializer).as_json + schemes = ColorScheme.includes(:color_scheme_colors).where("user_selectable").order(:name) + ActiveModel::ArraySerializer.new( + schemes, + each_serializer: ColorSchemeSelectableSerializer, + ).as_json end end @@ -71,7 +80,9 @@ class SiteSerializer < ApplicationSerializer def groups cache_anon_fragment("group_names") do - object.groups.order(:name) + object + .groups + .order(:name) .select(:id, :name, :flair_icon, :flair_upload_id, :flair_bg_color, :flair_color) .map do |g| { @@ -81,7 +92,8 @@ class SiteSerializer < ApplicationSerializer flair_bg_color: g.flair_bg_color, flair_color: g.flair_color, } - end.as_json + end + .as_json end end @@ -244,7 +256,8 @@ class SiteSerializer < ApplicationSerializer end def include_anonymous_default_sidebar_tags? - scope.anonymous? && !SiteSetting.legacy_navigation_menu? && SiteSetting.tagging_enabled && SiteSetting.default_sidebar_tags.present? + scope.anonymous? && !SiteSetting.legacy_navigation_menu? && SiteSetting.tagging_enabled && + SiteSetting.default_sidebar_tags.present? end def whispers_allowed_groups_names diff --git a/app/serializers/suggested_topic_serializer.rb b/app/serializers/suggested_topic_serializer.rb index f2ad535fbb..5796ede912 100644 --- a/app/serializers/suggested_topic_serializer.rb +++ b/app/serializers/suggested_topic_serializer.rb @@ -10,7 +10,12 @@ class SuggestedTopicSerializer < ListableTopicSerializer has_one :user, serializer: BasicUserSerializer, embed: :objects end - attributes :archetype, :like_count, :views, :category_id, :featured_link, :featured_link_root_domain + attributes :archetype, + :like_count, + :views, + :category_id, + :featured_link, + :featured_link_root_domain has_many :posters, serializer: SuggestedPosterSerializer, embed: :objects def posters diff --git a/app/serializers/suggested_topics_mixin.rb b/app/serializers/suggested_topics_mixin.rb index cf0de7b4a1..a899d73f7d 100644 --- a/app/serializers/suggested_topics_mixin.rb +++ b/app/serializers/suggested_topics_mixin.rb @@ -26,10 +26,12 @@ module SuggestedTopicsMixin return if object.topic.topic_allowed_users.exists?(user_id: scope.user.id) if object.topic_allowed_group_ids.present? - Group.joins(:group_users) + Group + .joins(:group_users) .where( "group_users.group_id IN (?) AND group_users.user_id = ?", - object.topic_allowed_group_ids, scope.user.id + object.topic_allowed_group_ids, + scope.user.id, ) .pluck_first(:name) end diff --git a/app/serializers/tag_group_serializer.rb b/app/serializers/tag_group_serializer.rb index db61936fce..6ccdd3e2d6 100644 --- a/app/serializers/tag_group_serializer.rb +++ b/app/serializers/tag_group_serializer.rb @@ -12,17 +12,22 @@ class TagGroupSerializer < ApplicationSerializer end def permissions - @permissions ||= begin - h = {} + @permissions ||= + begin + h = {} - object.tag_group_permissions.joins(:group).includes(:group).find_each do |tgp| - name = Group::AUTO_GROUP_IDS.fetch(tgp.group_id, tgp.group.name).to_s - h[name] = tgp.permission_type + object + .tag_group_permissions + .joins(:group) + .includes(:group) + .find_each do |tgp| + name = Group::AUTO_GROUP_IDS.fetch(tgp.group_id, tgp.group.name).to_s + h[name] = tgp.permission_type + end + + h["everyone"] = TagGroupPermission.permission_types[:full] if h.empty? + + h end - - h["everyone"] = TagGroupPermission.permission_types[:full] if h.empty? - - h - end end end diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb index b0edeaa7c4..d3307a6825 100644 --- a/app/serializers/theme_serializer.rb +++ b/app/serializers/theme_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'base64' +require "base64" class ThemeFieldSerializer < ApplicationSerializer attributes :name, :target, :value, :error, :type_id, :upload_id, :url, :filename @@ -47,9 +47,23 @@ class BasicThemeSerializer < ApplicationSerializer end class RemoteThemeSerializer < ApplicationSerializer - attributes :id, :remote_url, :remote_version, :local_version, :commits_behind, :branch, - :remote_updated_at, :updated_at, :github_diff_link, :last_error_text, :is_git?, - :license_url, :about_url, :authors, :theme_version, :minimum_discourse_version, :maximum_discourse_version + attributes :id, + :remote_url, + :remote_version, + :local_version, + :commits_behind, + :branch, + :remote_updated_at, + :updated_at, + :github_diff_link, + :last_error_text, + :is_git?, + :license_url, + :about_url, + :authors, + :theme_version, + :minimum_discourse_version, + :maximum_discourse_version # wow, AMS has some pretty nutty logic where it tries to find the path here # from action dispatch, tell it not to @@ -63,9 +77,17 @@ class RemoteThemeSerializer < ApplicationSerializer end class ThemeSerializer < BasicThemeSerializer - attributes :color_scheme, :color_scheme_id, :user_selectable, :auto_update, - :remote_theme_id, :settings, :errors, :supported?, :description, - :enabled?, :disabled_at + attributes :color_scheme, + :color_scheme_id, + :user_selectable, + :auto_update, + :remote_theme_id, + :settings, + :errors, + :supported?, + :description, + :enabled?, + :disabled_at has_one :user, serializer: UserNameSerializer, embed: :object has_one :disabled_by, serializer: UserNameSerializer, embed: :object @@ -80,9 +102,7 @@ class ThemeSerializer < BasicThemeSerializer super @errors = [] - object.theme_fields.each do |o| - @errors << o.error if o.error - end + object.theme_fields.each { |o| @errors << o.error if o.error } end def child_themes @@ -113,7 +133,7 @@ class ThemeSerializer < BasicThemeSerializer end def description - object.internal_translations.find { |t| t.key == "theme_metadata.description" } &.value + object.internal_translations.find { |t| t.key == "theme_metadata.description" }&.value end def include_disabled_at? diff --git a/app/serializers/theme_settings_serializer.rb b/app/serializers/theme_settings_serializer.rb index 987003d06a..cbd01da5a8 100644 --- a/app/serializers/theme_settings_serializer.rb +++ b/app/serializers/theme_settings_serializer.rb @@ -1,8 +1,15 @@ # frozen_string_literal: true class ThemeSettingsSerializer < ApplicationSerializer - attributes :setting, :type, :default, :value, :description, :valid_values, - :list_type, :textarea, :json_schema + attributes :setting, + :type, + :default, + :value, + :description, + :valid_values, + :list_type, + :textarea, + :json_schema def setting object.name @@ -21,7 +28,12 @@ class ThemeSettingsSerializer < ApplicationSerializer end def description - locale_file_description = object.theme.internal_translations.find { |t| t.key == "theme_metadata.settings.#{setting}" } &.value + locale_file_description = + object + .theme + .internal_translations + .find { |t| t.key == "theme_metadata.settings.#{setting}" } + &.value locale_file_description || object.description end diff --git a/app/serializers/topic_embed_serializer.rb b/app/serializers/topic_embed_serializer.rb index d31ab6e89a..928d3ec5d0 100644 --- a/app/serializers/topic_embed_serializer.rb +++ b/app/serializers/topic_embed_serializer.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class TopicEmbedSerializer < ApplicationSerializer - attributes \ - :topic_id, - :post_id, - :topic_slug, - :comment_count + attributes :topic_id, :post_id, :topic_slug, :comment_count def topic_slug object.topic.slug diff --git a/app/serializers/topic_flag_type_serializer.rb b/app/serializers/topic_flag_type_serializer.rb index 6532f4918d..ccee54a0aa 100644 --- a/app/serializers/topic_flag_type_serializer.rb +++ b/app/serializers/topic_flag_type_serializer.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true class TopicFlagTypeSerializer < PostActionTypeSerializer - protected def i18n(field, vars = nil) key = "topic_flag_types.#{name_key}.#{field}" vars ? I18n.t(key, vars) : I18n.t(key) end - end diff --git a/app/serializers/topic_link_serializer.rb b/app/serializers/topic_link_serializer.rb index d3d3fb686c..a31feaa2a8 100644 --- a/app/serializers/topic_link_serializer.rb +++ b/app/serializers/topic_link_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TopicLinkSerializer < ApplicationSerializer - attributes :url, :title, # :fancy_title, @@ -11,7 +10,7 @@ class TopicLinkSerializer < ApplicationSerializer :clicks, :user_id, :domain, - :root_domain, + :root_domain def attachment Discourse.store.has_been_uploaded?(object.url) @@ -24,5 +23,4 @@ class TopicLinkSerializer < ApplicationSerializer def root_domain MiniSuffix.domain(domain) end - end diff --git a/app/serializers/topic_list_item_serializer.rb b/app/serializers/topic_list_item_serializer.rb index 4a8f12b37c..3d43c0848a 100644 --- a/app/serializers/topic_list_item_serializer.rb +++ b/app/serializers/topic_list_item_serializer.rb @@ -32,7 +32,6 @@ class TopicListItemSerializer < ListableTopicSerializer end def category_id - # If it's a shared draft, show the destination topic instead if object.includes_destination_category && object.shared_draft return object.shared_draft.category_id @@ -50,8 +49,7 @@ class TopicListItemSerializer < ListableTopicSerializer end def include_post_action?(action) - object.user_data && - object.user_data.post_action_data && + object.user_data && object.user_data.post_action_data && object.user_data.post_action_data.key?(PostActionType.types[action]) end @@ -86,5 +84,4 @@ class TopicListItemSerializer < ListableTopicSerializer def include_allowed_user_count? object.private_message? end - end diff --git a/app/serializers/topic_list_serializer.rb b/app/serializers/topic_list_serializer.rb index a3ece4822f..5e78f344a3 100644 --- a/app/serializers/topic_list_serializer.rb +++ b/app/serializers/topic_list_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TopicListSerializer < ApplicationSerializer - attributes :can_create_topic, :more_topics_url, :for_period, diff --git a/app/serializers/topic_pending_post_serializer.rb b/app/serializers/topic_pending_post_serializer.rb index c07fc479e5..da02c07358 100644 --- a/app/serializers/topic_pending_post_serializer.rb +++ b/app/serializers/topic_pending_post_serializer.rb @@ -4,11 +4,10 @@ class TopicPendingPostSerializer < ApplicationSerializer attributes :id, :raw, :created_at def raw - object.payload['raw'] + object.payload["raw"] end def include_raw? - object.payload && object.payload['raw'].present? + object.payload && object.payload["raw"].present? end - end diff --git a/app/serializers/topic_post_count_serializer.rb b/app/serializers/topic_post_count_serializer.rb index 90f5a89fa4..5283910f16 100644 --- a/app/serializers/topic_post_count_serializer.rb +++ b/app/serializers/topic_post_count_serializer.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true class TopicPostCountSerializer < BasicUserSerializer - - attributes :post_count, :primary_group_name, - :flair_name, :flair_url, :flair_color, :flair_bg_color, - :admin, :moderator, :trust_level, + attributes :post_count, + :primary_group_name, + :flair_name, + :flair_url, + :flair_color, + :flair_bg_color, + :admin, + :moderator, + :trust_level def id object[:user].id @@ -58,5 +63,4 @@ class TopicPostCountSerializer < BasicUserSerializer def trust_level object[:user].trust_level end - end diff --git a/app/serializers/topic_timer_serializer.rb b/app/serializers/topic_timer_serializer.rb index b7f3b88788..9f9934f634 100644 --- a/app/serializers/topic_timer_serializer.rb +++ b/app/serializers/topic_timer_serializer.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true class TopicTimerSerializer < ApplicationSerializer - attributes :id, - :execute_at, - :duration_minutes, - :based_on_last_post, - :status_type, - :category_id + attributes :id, :execute_at, :duration_minutes, :based_on_last_post, :status_type, :category_id def status_type TopicTimer.types[object.status_type] diff --git a/app/serializers/topic_view_details_serializer.rb b/app/serializers/topic_view_details_serializer.rb index ffe51d4db3..22ae968e3f 100644 --- a/app/serializers/topic_view_details_serializer.rb +++ b/app/serializers/topic_view_details_serializer.rb @@ -1,29 +1,30 @@ # frozen_string_literal: true class TopicViewDetailsSerializer < ApplicationSerializer - def self.can_attributes - [:can_move_posts, - :can_delete, - :can_permanently_delete, - :can_recover, - :can_remove_allowed_users, - :can_invite_to, - :can_invite_via_email, - :can_create_post, - :can_reply_as_new_topic, - :can_flag_topic, - :can_convert_topic, - :can_review_topic, - :can_edit_tags, - :can_publish_page, - :can_close_topic, - :can_archive_topic, - :can_split_merge_topic, - :can_edit_staff_notes, - :can_toggle_topic_visibility, - :can_pin_unpin_topic, - :can_moderate_category] + %i[ + can_move_posts + can_delete + can_permanently_delete + can_recover + can_remove_allowed_users + can_invite_to + can_invite_via_email + can_create_post + can_reply_as_new_topic + can_flag_topic + can_convert_topic + can_review_topic + can_edit_tags + can_publish_page + can_close_topic + can_archive_topic + can_split_merge_topic + can_edit_staff_notes + can_toggle_topic_visibility + can_pin_unpin_topic + can_moderate_category + ] end # NOTE: `can_edit` is defined as an attribute because we explicitly want @@ -35,7 +36,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer *can_attributes, :can_remove_self_id, :participants, - :allowed_users + :allowed_users, ) has_one :created_by, serializer: BasicUserSerializer, embed: :objects @@ -46,9 +47,10 @@ class TopicViewDetailsSerializer < ApplicationSerializer has_many :allowed_groups, serializer: BasicGroupSerializer, embed: :objects def participants - object.post_counts_by_user.reject { |p| object.participants[p].blank? }.map do |pc| - { user: object.participants[pc[0]], post_count: pc[1] } - end + object + .post_counts_by_user + .reject { |p| object.participants[p].blank? } + .map { |pc| { user: object.participants[pc[0]], post_count: pc[1] } } end def include_participants? @@ -88,9 +90,7 @@ class TopicViewDetailsSerializer < ApplicationSerializer scope.can_remove_allowed_users?(object.topic, scope.user) end - can_attributes.each do |ca| - define_method(ca) { true } - end + can_attributes.each { |ca| define_method(ca) { true } } # NOTE: A Category Group Moderator moving a topic to a different category # may result in the 'can_edit?' result changing from `true` to `false`. @@ -160,13 +160,14 @@ class TopicViewDetailsSerializer < ApplicationSerializer end def can_perform_action_available_to_group_moderators? - @can_perform_action_available_to_group_moderators ||= scope.can_perform_action_available_to_group_moderators?(object.topic) + @can_perform_action_available_to_group_moderators ||= + scope.can_perform_action_available_to_group_moderators?(object.topic) end - alias :include_can_close_topic? :can_perform_action_available_to_group_moderators? - alias :include_can_archive_topic? :can_perform_action_available_to_group_moderators? - alias :include_can_split_merge_topic? :can_perform_action_available_to_group_moderators? - alias :include_can_edit_staff_notes? :can_perform_action_available_to_group_moderators? - alias :include_can_moderate_category? :can_perform_action_available_to_group_moderators? + alias include_can_close_topic? can_perform_action_available_to_group_moderators? + alias include_can_archive_topic? can_perform_action_available_to_group_moderators? + alias include_can_split_merge_topic? can_perform_action_available_to_group_moderators? + alias include_can_edit_staff_notes? can_perform_action_available_to_group_moderators? + alias include_can_moderate_category? can_perform_action_available_to_group_moderators? def include_can_publish_page? scope.can_publish_page?(object.topic) @@ -189,5 +190,4 @@ class TopicViewDetailsSerializer < ApplicationSerializer def include_allowed_groups? object.personal_message end - end diff --git a/app/serializers/topic_view_posts_serializer.rb b/app/serializers/topic_view_posts_serializer.rb index 8afcac3e9f..8dbb4d2b05 100644 --- a/app/serializers/topic_view_posts_serializer.rb +++ b/app/serializers/topic_view_posts_serializer.rb @@ -21,5 +21,4 @@ class TopicViewPostsSerializer < ApplicationSerializer def include_timeline_lookup? false end - end diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb index 57bd35aaf4..91eb2a6f2f 100644 --- a/app/serializers/topic_view_serializer.rb +++ b/app/serializers/topic_view_serializer.rb @@ -42,7 +42,7 @@ class TopicViewSerializer < ApplicationSerializer :pinned_until, :image_url, :slow_mode_seconds, - :external_id + :external_id, ) attributes( @@ -77,7 +77,7 @@ class TopicViewSerializer < ApplicationSerializer :thumbnails, :user_last_posted_at, :is_shared_draft, - :slow_mode_enabled_until + :slow_mode_enabled_until, ) has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects @@ -247,7 +247,8 @@ class TopicViewSerializer < ApplicationSerializer end def include_destination_category_id? - scope.can_see_shared_draft? && SiteSetting.shared_drafts_enabled? && object.topic.shared_draft.present? + scope.can_see_shared_draft? && SiteSetting.shared_drafts_enabled? && + object.topic.shared_draft.present? end def is_shared_draft @@ -276,20 +277,21 @@ class TopicViewSerializer < ApplicationSerializer Group .joins(:group_users) .where( - id: object.topic.custom_fields['requested_group_id'].to_i, - group_users: { user_id: scope.user.id, owner: true } + id: object.topic.custom_fields["requested_group_id"].to_i, + group_users: { + user_id: scope.user.id, + owner: true, + }, ) .pluck_first(:name) end def include_requested_group_name? - object.personal_message && object.topic.custom_fields['requested_group_id'] + object.personal_message && object.topic.custom_fields["requested_group_id"] end def include_published_page? - SiteSetting.enable_page_publishing? && - scope.is_staff? && - object.published_page.present? && + SiteSetting.enable_page_publishing? && scope.is_staff? && object.published_page.present? && !SiteSetting.secure_uploads end diff --git a/app/serializers/topic_view_wordpress_serializer.rb b/app/serializers/topic_view_wordpress_serializer.rb index b4e7610319..ce94b630b4 100644 --- a/app/serializers/topic_view_wordpress_serializer.rb +++ b/app/serializers/topic_view_wordpress_serializer.rb @@ -1,13 +1,8 @@ # frozen_string_literal: true class TopicViewWordpressSerializer < ApplicationSerializer - # These attributes will be delegated to the topic - attributes :id, - :category_id, - :posts_count, - :filtered_posts_count, - :posts + attributes :id, :category_id, :posts_count, :filtered_posts_count, :posts has_many :participants, serializer: UserWordpressSerializer, embed: :objects has_many :posts, serializer: PostWordpressSerializer, embed: :objects @@ -35,5 +30,4 @@ class TopicViewWordpressSerializer < ApplicationSerializer def posts object.posts end - end diff --git a/app/serializers/trust_level3_requirements_serializer.rb b/app/serializers/trust_level3_requirements_serializer.rb index 2cf8331759..690e968239 100644 --- a/app/serializers/trust_level3_requirements_serializer.rb +++ b/app/serializers/trust_level3_requirements_serializer.rb @@ -1,25 +1,37 @@ # frozen_string_literal: true class TrustLevel3RequirementsSerializer < ApplicationSerializer - has_one :penalty_counts, embed: :object, serializer: PenaltyCountsSerializer attributes :time_period, :requirements_met, :requirements_lost, - :trust_level_locked, :on_grace_period, - :days_visited, :min_days_visited, - :num_topics_replied_to, :min_topics_replied_to, - :topics_viewed, :min_topics_viewed, - :posts_read, :min_posts_read, - :topics_viewed_all_time, :min_topics_viewed_all_time, - :posts_read_all_time, :min_posts_read_all_time, - :num_flagged_posts, :max_flagged_posts, - :num_flagged_by_users, :max_flagged_by_users, - :num_likes_given, :min_likes_given, - :num_likes_received, :min_likes_received, - :num_likes_received_days, :min_likes_received_days, - :num_likes_received_users, :min_likes_received_users + :trust_level_locked, + :on_grace_period, + :days_visited, + :min_days_visited, + :num_topics_replied_to, + :min_topics_replied_to, + :topics_viewed, + :min_topics_viewed, + :posts_read, + :min_posts_read, + :topics_viewed_all_time, + :min_topics_viewed_all_time, + :posts_read_all_time, + :min_posts_read_all_time, + :num_flagged_posts, + :max_flagged_posts, + :num_flagged_by_users, + :max_flagged_by_users, + :num_likes_given, + :min_likes_given, + :num_likes_received, + :min_likes_received, + :num_likes_received_days, + :min_likes_received_days, + :num_likes_received_users, + :min_likes_received_users def requirements_met object.requirements_met? diff --git a/app/serializers/upload_serializer.rb b/app/serializers/upload_serializer.rb index 7626c0e1e2..eb32b01181 100644 --- a/app/serializers/upload_serializer.rb +++ b/app/serializers/upload_serializer.rb @@ -17,6 +17,10 @@ class UploadSerializer < ApplicationSerializer :dominant_color def url - object.for_site_setting ? object.url : UrlHelper.cook_url(object.url, secure: SiteSetting.secure_uploads? && object.secure) + if object.for_site_setting + object.url + else + UrlHelper.cook_url(object.url, secure: SiteSetting.secure_uploads? && object.secure) + end end end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 29ed526dc0..0958de3d9d 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'post_item_excerpt' +require_relative "post_item_excerpt" class UserActionSerializer < ApplicationSerializer include PostItemExcerpt @@ -98,5 +98,4 @@ class UserActionSerializer < ApplicationSerializer def action_code_path object.action_code_path end - end diff --git a/app/serializers/user_badge_serializer.rb b/app/serializers/user_badge_serializer.rb index 4b2b536ef5..752ae5c834 100644 --- a/app/serializers/user_badge_serializer.rb +++ b/app/serializers/user_badge_serializer.rb @@ -6,9 +6,7 @@ class UserBadgeSerializer < ApplicationSerializer class UserSerializer < BasicUserSerializer include UserPrimaryGroupMixin - attributes :name, - :moderator, - :admin + attributes :name, :moderator, :admin end attributes :id, :granted_at, :created_at, :count, :post_id, :post_number @@ -26,7 +24,7 @@ class UserBadgeSerializer < ApplicationSerializer include_post_attributes? end - alias :include_post_number? :include_post_id? + alias include_post_number? include_post_id? def post_number object.post && object.post.post_number diff --git a/app/serializers/user_bookmark_list_serializer.rb b/app/serializers/user_bookmark_list_serializer.rb index cefdd95114..b70349a1e8 100644 --- a/app/serializers/user_bookmark_list_serializer.rb +++ b/app/serializers/user_bookmark_list_serializer.rb @@ -9,7 +9,7 @@ class UserBookmarkListSerializer < ApplicationSerializer bm, **object.bookmark_serializer_opts, scope: scope, - root: false + root: false, ) end end diff --git a/app/serializers/user_card_serializer.rb b/app/serializers/user_card_serializer.rb index 5e208169c7..d2c32facdc 100644 --- a/app/serializers/user_card_serializer.rb +++ b/app/serializers/user_card_serializer.rb @@ -73,11 +73,7 @@ class UserCardSerializer < BasicUserSerializer :pending_posts_count, :status - untrusted_attributes :bio_excerpt, - :website, - :website_name, - :location, - :card_background_upload_url + untrusted_attributes :bio_excerpt, :website, :website_name, :location, :card_background_upload_url staff_attributes :staged @@ -91,8 +87,7 @@ class UserCardSerializer < BasicUserSerializer end def include_email? - (object.id && object.id == scope.user.try(:id)) || - (scope.is_staff? && object.staged?) + (object.id && object.id == scope.user.try(:id)) || (scope.is_staff? && object.staged?) end alias_method :include_secondary_emails?, :include_email? @@ -111,13 +106,14 @@ class UserCardSerializer < BasicUserSerializer end def website_name - uri = begin - URI(website.to_s) - rescue URI::Error - end + uri = + begin + URI(website.to_s) + rescue URI::Error + end return if uri.nil? || uri.host.nil? - uri.host.sub(/^www\./, '') + uri.path + uri.host.sub(/^www\./, "") + uri.path end def ignored diff --git a/app/serializers/user_history_serializer.rb b/app/serializers/user_history_serializer.rb index be3f26c97d..d188eccb99 100644 --- a/app/serializers/user_history_serializer.rb +++ b/app/serializers/user_history_serializer.rb @@ -22,7 +22,7 @@ class UserHistorySerializer < ApplicationSerializer def action_name key = UserHistory.actions.key(object.action) - [:custom, :custom_staff].include?(key) ? object.custom_type : key.to_s + %i[custom custom_staff].include?(key) ? object.custom_type : key.to_s end def new_value diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index 8cc65d1779..d78dad82eb 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -36,14 +36,15 @@ class UserOptionSerializer < ApplicationSerializer :skip_new_user_tips, :default_calendar, :oldest_search_log_date, - :seen_popups, + :seen_popups def auto_track_topics_after_msecs object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs end def notification_level_when_replying - object.notification_level_when_replying || SiteSetting.default_other_notification_level_when_replying + object.notification_level_when_replying || + SiteSetting.default_other_notification_level_when_replying end def new_topic_duration_minutes diff --git a/app/serializers/user_post_topic_bookmark_base_serializer.rb b/app/serializers/user_post_topic_bookmark_base_serializer.rb index ca5d50e684..02798d061a 100644 --- a/app/serializers/user_post_topic_bookmark_base_serializer.rb +++ b/app/serializers/user_post_topic_bookmark_base_serializer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'post_item_excerpt' +require_relative "post_item_excerpt" class UserPostTopicBookmarkBaseSerializer < UserBookmarkBaseSerializer include TopicTagsMixin diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 505d736454..91c267d9ad 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -31,9 +31,7 @@ class UserSerializer < UserCardSerializer can_edit end - staff_attributes :post_count, - :can_be_deleted, - :can_delete_all_posts + staff_attributes :post_count, :can_be_deleted, :can_delete_all_posts private_attributes :locale, :muted_category_ids, @@ -69,9 +67,7 @@ class UserSerializer < UserCardSerializer :sidebar_list_destination, :display_sidebar_tags - untrusted_attributes :bio_raw, - :bio_cooked, - :profile_background_upload_url, + untrusted_attributes :bio_raw, :bio_cooked, :profile_background_upload_url ### ### ATTRIBUTES @@ -87,8 +83,7 @@ class UserSerializer < UserCardSerializer end def groups - object.groups.order(:id) - .visible_groups(scope.user).members_visible_groups(scope.user) + object.groups.order(:id).visible_groups(scope.user).members_visible_groups(scope.user) end def group_users @@ -144,15 +139,19 @@ class UserSerializer < UserCardSerializer end def user_api_keys - keys = object.user_api_keys.where(revoked_at: nil).map do |k| - { - id: k.id, - application_name: k.application_name, - scopes: k.scopes.map { |s| I18n.t("user_api_key.scopes.#{s.name}") }, - created_at: k.created_at, - last_used_at: k.last_used_at, - } - end + keys = + object + .user_api_keys + .where(revoked_at: nil) + .map do |k| + { + id: k.id, + application_name: k.application_name, + scopes: k.scopes.map { |s| I18n.t("user_api_key.scopes.#{s.name}") }, + created_at: k.created_at, + last_used_at: k.last_used_at, + } + end keys.sort! { |a, b| a[:last_used_at].to_time <=> b[:last_used_at].to_time } keys.length > 0 ? keys : nil @@ -162,7 +161,7 @@ class UserSerializer < UserCardSerializer ActiveModel::ArraySerializer.new( object.user_auth_tokens, each_serializer: UserAuthTokenSerializer, - scope: scope + scope: scope, ) end @@ -306,7 +305,8 @@ class UserSerializer < UserCardSerializer end def use_logo_small_as_avatar - object.is_system_user? && SiteSetting.logo_small && SiteSetting.use_site_small_logo_as_system_avatar + object.is_system_user? && SiteSetting.logo_small && + SiteSetting.use_site_small_logo_as_system_avatar end private @@ -314,11 +314,8 @@ class UserSerializer < UserCardSerializer def custom_field_keys fields = super - if scope.can_edit?(object) - fields += DiscoursePluginRegistry.serialized_current_user_fields.to_a - end + fields += DiscoursePluginRegistry.serialized_current_user_fields.to_a if scope.can_edit?(object) fields end - end diff --git a/app/serializers/user_summary_serializer.rb b/app/serializers/user_summary_serializer.rb index f6e87bb055..03faf5b90f 100644 --- a/app/serializers/user_summary_serializer.rb +++ b/app/serializers/user_summary_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserSummarySerializer < ApplicationSerializer - class TopicSerializer < BasicTopicSerializer attributes :category_id, :like_count, :created_at end @@ -65,9 +64,15 @@ class UserSummarySerializer < ApplicationSerializer end class CategoryWithCountsSerializer < ApplicationSerializer - attributes :topic_count, :post_count, - :id, :name, :color, :text_color, :slug, - :read_restricted, :parent_category_id + attributes :topic_count, + :post_count, + :id, + :name, + :color, + :text_color, + :slug, + :read_restricted, + :parent_category_id end has_many :topics, serializer: TopicSerializer diff --git a/app/serializers/user_tag_notifications_serializer.rb b/app/serializers/user_tag_notifications_serializer.rb index d2d0766a3e..e3967cf4d4 100644 --- a/app/serializers/user_tag_notifications_serializer.rb +++ b/app/serializers/user_tag_notifications_serializer.rb @@ -3,11 +3,7 @@ class UserTagNotificationsSerializer < ApplicationSerializer include UserTagNotificationsMixin - attributes :watched_tags, - :watching_first_post_tags, - :tracked_tags, - :muted_tags, - :regular_tags + attributes :watched_tags, :watching_first_post_tags, :tracked_tags, :muted_tags, :regular_tags def user object diff --git a/app/serializers/user_wordpress_serializer.rb b/app/serializers/user_wordpress_serializer.rb index c43f91076f..6f8510b189 100644 --- a/app/serializers/user_wordpress_serializer.rb +++ b/app/serializers/user_wordpress_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserWordpressSerializer < BasicUserSerializer - def avatar_template if Hash === object UrlHelper.absolute User.avatar_template(user[:username], user[:uploaded_avatar_id]) @@ -9,5 +8,4 @@ class UserWordpressSerializer < BasicUserSerializer UrlHelper.absolute object.avatar_template end end - end diff --git a/app/serializers/watched_word_list_serializer.rb b/app/serializers/watched_word_list_serializer.rb index ec3cac72a9..4b47ce16f7 100644 --- a/app/serializers/watched_word_list_serializer.rb +++ b/app/serializers/watched_word_list_serializer.rb @@ -4,14 +4,15 @@ class WatchedWordListSerializer < ApplicationSerializer attributes :actions, :words, :compiled_regular_expressions def actions - SiteSetting.tagging_enabled ? WatchedWord.actions.keys - : WatchedWord.actions.keys.filter { |k| k != :tag } + if SiteSetting.tagging_enabled + WatchedWord.actions.keys + else + WatchedWord.actions.keys.filter { |k| k != :tag } + end end def words - object.map do |word| - WatchedWordSerializer.new(word, root: false) - end + object.map { |word| WatchedWordSerializer.new(word, root: false) } end def compiled_regular_expressions diff --git a/app/serializers/web_hook_category_serializer.rb b/app/serializers/web_hook_category_serializer.rb index b7fd7ba79c..dfbd8aa184 100644 --- a/app/serializers/web_hook_category_serializer.rb +++ b/app/serializers/web_hook_category_serializer.rb @@ -1,15 +1,7 @@ # frozen_string_literal: true class WebHookCategorySerializer < CategorySerializer - - %i{ - can_edit - notification_level - available_groups - }.each do |attr| - define_method("include_#{attr}?") do - false - end + %i[can_edit notification_level available_groups].each do |attr| + define_method("include_#{attr}?") { false } end - end diff --git a/app/serializers/web_hook_flag_serializer.rb b/app/serializers/web_hook_flag_serializer.rb index ae2bf85754..d37ca1d5a7 100644 --- a/app/serializers/web_hook_flag_serializer.rb +++ b/app/serializers/web_hook_flag_serializer.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true class WebHookFlagSerializer < ApplicationSerializer - attributes :id, - :post, - :flag_type, - :created_by, - :created_at, - :resolved_at, - :resolved_by + attributes :id, :post, :flag_type, :created_by, :created_at, :resolved_at, :resolved_by def post WebHookPostSerializer.new(object.post, scope: scope, root: false).as_json @@ -41,7 +35,7 @@ class WebHookFlagSerializer < ApplicationSerializer disposed_by_id.present? end -protected + protected def disposed_by_id object.disagreed_by_id || object.agreed_by_id || object.deferred_by_id diff --git a/app/serializers/web_hook_group_serializer.rb b/app/serializers/web_hook_group_serializer.rb index d1947c2d84..e613447505 100644 --- a/app/serializers/web_hook_group_serializer.rb +++ b/app/serializers/web_hook_group_serializer.rb @@ -1,14 +1,5 @@ # frozen_string_literal: true class WebHookGroupSerializer < BasicGroupSerializer - - %i{ - is_group_user - is_group_owner - }.each do |attr| - define_method("include_#{attr}?") do - false - end - end - + %i[is_group_user is_group_owner].each { |attr| define_method("include_#{attr}?") { false } } end diff --git a/app/serializers/web_hook_group_user_serializer.rb b/app/serializers/web_hook_group_user_serializer.rb index 342b5a5412..23a181a28e 100644 --- a/app/serializers/web_hook_group_user_serializer.rb +++ b/app/serializers/web_hook_group_user_serializer.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true class WebHookGroupUserSerializer < BasicGroupUserSerializer - attributes :id, - :created_at + attributes :id, :created_at end diff --git a/app/serializers/web_hook_post_serializer.rb b/app/serializers/web_hook_post_serializer.rb index beaa673831..f804e00833 100644 --- a/app/serializers/web_hook_post_serializer.rb +++ b/app/serializers/web_hook_post_serializer.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true class WebHookPostSerializer < PostSerializer - - attributes :topic_posts_count, - :topic_filtered_posts_count, - :topic_archetype, - :category_slug + attributes :topic_posts_count, :topic_filtered_posts_count, :topic_archetype, :category_slug def include_topic_title? true @@ -19,7 +15,7 @@ class WebHookPostSerializer < PostSerializer true end - %i{ + %i[ can_view can_edit can_delete @@ -33,11 +29,7 @@ class WebHookPostSerializer < PostSerializer flair_color notice mentioned_users - }.each do |attr| - define_method("include_#{attr}?") do - false - end - end + ].each { |attr| define_method("include_#{attr}?") { false } } def topic_posts_count object.topic ? object.topic.posts_count : 0 @@ -48,7 +40,7 @@ class WebHookPostSerializer < PostSerializer end def topic_archetype - object.topic ? object.topic.archetype : '' + object.topic ? object.topic.archetype : "" end def include_category_slug? @@ -56,7 +48,7 @@ class WebHookPostSerializer < PostSerializer end def category_slug - object.topic && object.topic.category ? object.topic.category.slug_for_url : '' + object.topic && object.topic.category ? object.topic.category.slug_for_url : "" end def include_readers_count? diff --git a/app/serializers/web_hook_topic_view_serializer.rb b/app/serializers/web_hook_topic_view_serializer.rb index 2098c7a67b..ff11424db1 100644 --- a/app/serializers/web_hook_topic_view_serializer.rb +++ b/app/serializers/web_hook_topic_view_serializer.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true class WebHookTopicViewSerializer < TopicViewSerializer - attributes :created_by, - :last_poster + attributes :created_by, :last_poster - %i{ + %i[ post_stream timeline_lookup pm_with_non_human_user @@ -23,11 +22,7 @@ class WebHookTopicViewSerializer < TopicViewSerializer slow_mode_seconds slow_mode_enabled_until bookmarks - }.each do |attr| - define_method("include_#{attr}?") do - false - end - end + ].each { |attr| define_method("include_#{attr}?") { false } } def include_show_read_indicator? false diff --git a/app/serializers/web_hook_user_serializer.rb b/app/serializers/web_hook_user_serializer.rb index e077b19100..5de175413e 100644 --- a/app/serializers/web_hook_user_serializer.rb +++ b/app/serializers/web_hook_user_serializer.rb @@ -7,7 +7,7 @@ class WebHookUserSerializer < UserSerializer def staff_attributes(*attrs) end - %i{ + %i[ unconfirmed_emails can_edit can_edit_username @@ -38,11 +38,7 @@ class WebHookUserSerializer < UserSerializer use_logo_small_as_avatar pending_posts_count status - }.each do |attr| - define_method("include_#{attr}?") do - false - end - end + ].each { |attr| define_method("include_#{attr}?") { false } } def include_email? scope.is_admin? @@ -57,5 +53,4 @@ class WebHookUserSerializer < UserSerializer def external_id object.single_sign_on_record.external_id end - end diff --git a/app/serializers/wizard_field_serializer.rb b/app/serializers/wizard_field_serializer.rb index 12c3db2060..b4cb8a3b56 100644 --- a/app/serializers/wizard_field_serializer.rb +++ b/app/serializers/wizard_field_serializer.rb @@ -1,7 +1,17 @@ # frozen_string_literal: true class WizardFieldSerializer < ApplicationSerializer - attributes :id, :type, :required, :value, :label, :placeholder, :description, :extra_description, :icon, :disabled, :show_in_sidebar + attributes :id, + :type, + :required, + :value, + :label, + :placeholder, + :description, + :extra_description, + :icon, + :disabled, + :show_in_sidebar has_many :choices, serializer: WizardFieldChoiceSerializer, embed: :objects def id diff --git a/app/serializers/wizard_step_serializer.rb b/app/serializers/wizard_step_serializer.rb index 802a1a4be9..76ec852124 100644 --- a/app/serializers/wizard_step_serializer.rb +++ b/app/serializers/wizard_step_serializer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class WizardStepSerializer < ApplicationSerializer - attributes :id, :next, :previous, :description, :title, :index, :banner, :emoji has_many :fields, serializer: WizardFieldSerializer, embed: :objects @@ -68,5 +67,4 @@ class WizardStepSerializer < ApplicationSerializer def emoji object.emoji end - end diff --git a/app/services/anonymous_shadow_creator.rb b/app/services/anonymous_shadow_creator.rb index 77d353b2f9..2a769c7a68 100644 --- a/app/services/anonymous_shadow_creator.rb +++ b/app/services/anonymous_shadow_creator.rb @@ -30,9 +30,8 @@ class AnonymousShadowCreator shadow = user.shadow_user - if shadow && (shadow.post_count + shadow.topic_count) > 0 && - shadow.last_posted_at && - shadow.last_posted_at < SiteSetting.anonymous_account_duration_minutes.minutes.ago + if shadow && (shadow.post_count + shadow.topic_count) > 0 && shadow.last_posted_at && + shadow.last_posted_at < SiteSetting.anonymous_account_duration_minutes.minutes.ago shadow = nil end @@ -45,23 +44,24 @@ class AnonymousShadowCreator username = resolve_username User.transaction do - shadow = User.create!( - password: SecureRandom.hex, - email: "#{SecureRandom.hex}@anon.#{Discourse.current_hostname}", - skip_email_validation: true, - name: username, # prevents error when names are required - username: username, - active: true, - trust_level: 1, - manual_locked_trust_level: 1, - approved: true, - approved_at: 1.day.ago, - created_at: 1.day.ago # bypass new user restrictions - ) + shadow = + User.create!( + password: SecureRandom.hex, + email: "#{SecureRandom.hex}@anon.#{Discourse.current_hostname}", + skip_email_validation: true, + name: username, # prevents error when names are required + username: username, + active: true, + trust_level: 1, + manual_locked_trust_level: 1, + approved: true, + approved_at: 1.day.ago, + created_at: 1.day.ago, # bypass new user restrictions + ) shadow.user_option.update_columns( email_messages_level: UserOption.email_level_types[:never], - email_digests: false + email_digests: false, ) shadow.email_tokens.update_all(confirmed: true) diff --git a/app/services/badge_granter.rb b/app/services/badge_granter.rb index da264036c6..f32d1321f3 100644 --- a/app/services/badge_granter.rb +++ b/app/services/badge_granter.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class BadgeGranter - class GrantError < StandardError; end + class GrantError < StandardError + end def self.disable_queue @queue_disabled = true @@ -21,7 +22,12 @@ class BadgeGranter BadgeGranter.new(badge, user, opts).grant end - def self.enqueue_mass_grant_for_users(badge, emails: [], usernames: [], ensure_users_have_badge_once: true) + def self.enqueue_mass_grant_for_users( + badge, + emails: [], + usernames: [], + ensure_users_have_badge_once: true + ) emails = emails.map(&:downcase) usernames = usernames.map(&:downcase) usernames_map_to_ids = {} @@ -30,7 +36,7 @@ class BadgeGranter usernames_map_to_ids = User.where(username_lower: usernames).pluck(:username_lower, :id).to_h end if emails.size > 0 - emails_map_to_ids = User.with_email(emails).pluck('LOWER(user_emails.email)', :id).to_h + emails_map_to_ids = User.with_email(emails).pluck("LOWER(user_emails.email)", :id).to_h end count_per_user = {} @@ -57,18 +63,13 @@ class BadgeGranter count_per_user.each do |user_id, count| next if ensure_users_have_badge_once && existing_owners_ids.include?(user_id) - Jobs.enqueue( - :mass_award_badge, - user: user_id, - badge: badge.id, - count: count - ) + Jobs.enqueue(:mass_award_badge, user: user_id, badge: badge.id, count: count) end { unmatched_entries: unmatched.to_a, matched_users_count: count_per_user.size, - unmatched_entries_count: unmatched.size + unmatched_entries_count: unmatched.size, } end @@ -78,7 +79,8 @@ class BadgeGranter raise ArgumentError.new("count can't be less than 1") if count < 1 UserBadge.transaction do - DB.exec(<<~SQL * count, now: Time.zone.now, system: Discourse.system_user.id, user_id: user.id, badge_id: badge.id) + DB.exec( + <<~SQL * count, INSERT INTO user_badges (granted_at, created_at, granted_by_id, user_id, badge_id, seq) VALUES @@ -95,6 +97,11 @@ class BadgeGranter ), 0) ); SQL + now: Time.zone.now, + system: Discourse.system_user.id, + user_id: user.id, + badge_id: badge.id, + ) notification = send_notification(user.id, user.username, user.locale, badge) DB.exec(<<~SQL, notification_id: notification.id, user_id: user.id, badge_id: badge.id) @@ -114,9 +121,7 @@ class BadgeGranter find_by = { badge_id: @badge.id, user_id: @user.id } - if @badge.multiple_grant? - find_by[:post_id] = @post_id - end + find_by[:post_id] = @post_id if @badge.multiple_grant? user_badge = UserBadge.find_by(find_by) @@ -128,12 +133,15 @@ class BadgeGranter seq = (seq || -1) + 1 end - user_badge = UserBadge.create!(badge: @badge, - user: @user, - granted_by: @granted_by, - granted_at: @opts[:created_at] || Time.now, - post_id: @post_id, - seq: seq) + user_badge = + UserBadge.create!( + badge: @badge, + user: @user, + granted_by: @granted_by, + granted_at: @opts[:created_at] || Time.now, + post_id: @post_id, + seq: seq, + ) return unless SiteSetting.enable_badges @@ -143,7 +151,8 @@ class BadgeGranter skip_new_user_tips = @user.user_option.skip_new_user_tips unless self.class.suppress_notification?(@badge, user_badge.granted_at, skip_new_user_tips) - notification = self.class.send_notification(@user.id, @user.username, @user.effective_locale, @badge) + notification = + self.class.send_notification(@user.id, @user.username, @user.effective_locale, @badge) user_badge.update!(notification_id: notification.id) end end @@ -160,16 +169,18 @@ class BadgeGranter end # If the user's title is the same as the badge name OR the custom badge name, remove their title. - custom_badge_name = TranslationOverride.find_by(translation_key: user_badge.badge.translation_key)&.value + custom_badge_name = + TranslationOverride.find_by(translation_key: user_badge.badge.translation_key)&.value user_title_is_badge_name = user_badge.user.title == user_badge.badge.name - user_title_is_custom_badge_name = custom_badge_name.present? && user_badge.user.title == custom_badge_name + user_title_is_custom_badge_name = + custom_badge_name.present? && user_badge.user.title == custom_badge_name if user_title_is_badge_name || user_title_is_custom_badge_name if options[:revoked_by] StaffActionLogger.new(options[:revoked_by]).log_title_revoke( user_badge.user, - revoke_reason: 'user title was same as revoked badge name or custom badge name', - previous_value: user_badge.user.title + revoke_reason: "user title was same as revoked badge name or custom badge name", + previous_value: user_badge.user.title, ) end user_badge.user.title = nil @@ -179,10 +190,15 @@ class BadgeGranter end def self.revoke_all(badge) - custom_badge_names = TranslationOverride.where(translation_key: badge.translation_key).pluck(:value) + custom_badge_names = + TranslationOverride.where(translation_key: badge.translation_key).pluck(:value) - users = User.joins(:user_badges).where(user_badges: { badge_id: badge.id }).where(title: badge.name) - users = users.or(User.joins(:user_badges).where(title: custom_badge_names)) unless custom_badge_names.empty? + users = + User.joins(:user_badges).where(user_badges: { badge_id: badge.id }).where(title: badge.name) + users = + users.or( + User.joins(:user_badges).where(title: custom_badge_names), + ) unless custom_badge_names.empty? users.update_all(title: nil) UserBadge.where(badge: badge).delete_all @@ -195,28 +211,16 @@ class BadgeGranter case type when Badge::Trigger::PostRevision post = opt[:post] - payload = { - type: "PostRevision", - post_ids: [post.id] - } + payload = { type: "PostRevision", post_ids: [post.id] } when Badge::Trigger::UserChange user = opt[:user] - payload = { - type: "UserChange", - user_ids: [user.id] - } + payload = { type: "UserChange", user_ids: [user.id] } when Badge::Trigger::TrustLevelChange user = opt[:user] - payload = { - type: "TrustLevelChange", - user_ids: [user.id] - } + payload = { type: "TrustLevelChange", user_ids: [user.id] } when Badge::Trigger::PostAction action = opt[:post_action] - payload = { - type: "PostAction", - post_ids: [action.post_id, action.related_post_id].compact! - } + payload = { type: "PostAction", post_ids: [action.post_id, action.related_post_id].compact! } end Discourse.redis.lpush queue_key, payload.to_json if payload @@ -242,9 +246,7 @@ class BadgeGranter next unless post_ids.present? || user_ids.present? - find_by_type(type).each do |badge| - backfill(badge, post_ids: post_ids, user_ids: user_ids) - end + find_by_type(type).each { |badge| backfill(badge, post_ids: post_ids, user_ids: user_ids) } end end @@ -263,27 +265,47 @@ class BadgeGranter return if sql.blank? if Badge::Trigger.uses_post_ids?(opts[:trigger]) - raise("Contract violation:\nQuery triggers on posts, but does not reference the ':post_ids' array") unless sql.match(/:post_ids/) - raise "Contract violation:\nQuery triggers on posts, but references the ':user_ids' array" if sql.match(/:user_ids/) + unless sql.match(/:post_ids/) + raise( + "Contract violation:\nQuery triggers on posts, but does not reference the ':post_ids' array", + ) + end + if sql.match(/:user_ids/) + raise "Contract violation:\nQuery triggers on posts, but references the ':user_ids' array" + end end if Badge::Trigger.uses_user_ids?(opts[:trigger]) - raise "Contract violation:\nQuery triggers on users, but does not reference the ':user_ids' array" unless sql.match(/:user_ids/) - raise "Contract violation:\nQuery triggers on users, but references the ':post_ids' array" if sql.match(/:post_ids/) + unless sql.match(/:user_ids/) + raise "Contract violation:\nQuery triggers on users, but does not reference the ':user_ids' array" + end + if sql.match(/:post_ids/) + raise "Contract violation:\nQuery triggers on users, but references the ':post_ids' array" + end end if opts[:trigger] && !Badge::Trigger.is_none?(opts[:trigger]) - raise "Contract violation:\nQuery is triggered, but does not reference the ':backfill' parameter.\n(Hint: if :backfill is TRUE, you should ignore the :post_ids/:user_ids)" unless sql.match(/:backfill/) + unless sql.match(/:backfill/) + raise "Contract violation:\nQuery is triggered, but does not reference the ':backfill' parameter.\n(Hint: if :backfill is TRUE, you should ignore the :post_ids/:user_ids)" + end end # TODO these three conditions have a lot of false negatives if opts[:target_posts] - raise "Contract violation:\nQuery targets posts, but does not return a 'post_id' column" unless sql.match(/post_id/) + unless sql.match(/post_id/) + raise "Contract violation:\nQuery targets posts, but does not return a 'post_id' column" + end end - raise "Contract violation:\nQuery does not return a 'user_id' column" unless sql.match(/user_id/) - raise "Contract violation:\nQuery does not return a 'granted_at' column" unless sql.match(/granted_at/) - raise "Contract violation:\nQuery ends with a semicolon. Remove the semicolon; your sql will be used in a subquery." if sql.match(/;\s*\z/) + unless sql.match(/user_id/) + raise "Contract violation:\nQuery does not return a 'user_id' column" + end + unless sql.match(/granted_at/) + raise "Contract violation:\nQuery does not return a 'granted_at' column" + end + if sql.match(/;\s*\z/) + raise "Contract violation:\nQuery ends with a semicolon. Remove the semicolon; your sql will be used in a subquery." + end end # Options: @@ -305,8 +327,9 @@ class BadgeGranter SQL grant_count = DB.query_single(count_sql, params).first.to_i - grants_sql = if opts[:target_posts] - <<~SQL + grants_sql = + if opts[:target_posts] + <<~SQL SELECT u.id, u.username, q.post_id, t.title, q.granted_at FROM ( #{sql} @@ -317,8 +340,8 @@ class BadgeGranter WHERE :backfill = :backfill LIMIT 10 SQL - else - <<~SQL + else + <<~SQL SELECT u.id, u.username, q.granted_at FROM ( #{sql} @@ -327,7 +350,7 @@ class BadgeGranter WHERE :backfill = :backfill LIMIT 10 SQL - end + end query_plan = nil # HACK: active record sanitization too flexible, force it to go down the sanitization path that cares not for % stuff @@ -337,11 +360,17 @@ class BadgeGranter sample = DB.query(grants_sql, params) sample.each do |result| - raise "Query returned a non-existent user ID:\n#{result.id}" unless User.exists?(id: result.id) - raise "Query did not return a badge grant time\n(Try using 'current_timestamp granted_at')" unless result.granted_at + unless User.exists?(id: result.id) + raise "Query returned a non-existent user ID:\n#{result.id}" + end + unless result.granted_at + raise "Query did not return a badge grant time\n(Try using 'current_timestamp granted_at')" + end if opts[:target_posts] raise "Query did not return a post ID" unless result.post_id - raise "Query returned a non-existent post ID:\n#{result.post_id}" unless Post.exists?(result.post_id).present? + unless Post.exists?(result.post_id).present? + raise "Query returned a non-existent post ID:\n#{result.post_id}" + end end end @@ -362,7 +391,7 @@ class BadgeGranter # safeguard fall back to full backfill if more than 200 if (post_ids && post_ids.size > MAX_ITEMS_FOR_DELTA) || - (user_ids && user_ids.size > MAX_ITEMS_FOR_DELTA) + (user_ids && user_ids.size > MAX_ITEMS_FOR_DELTA) post_ids = nil user_ids = nil end @@ -388,14 +417,16 @@ class BadgeGranter ) SQL - DB.exec( - sql, - id: badge.id, - post_ids: [-1], - user_ids: [-2], - backfill: true, - multiple_grant: true # cheat here, cause we only run on backfill and are deleting - ) if badge.auto_revoke && full_backfill + if badge.auto_revoke && full_backfill + DB.exec( + sql, + id: badge.id, + post_ids: [-1], + user_ids: [-2], + backfill: true, + multiple_grant: true, # cheat here, cause we only run on backfill and are deleting + ) + end sql = <<~SQL WITH w as ( @@ -434,25 +465,27 @@ class BadgeGranter return end - builder.query( - id: badge.id, - multiple_grant: badge.multiple_grant, - backfill: full_backfill, - post_ids: post_ids || [-2], - user_ids: user_ids || [-2]).each do |row| - - next if suppress_notification?(badge, row.granted_at, row.skip_new_user_tips) - next if row.staff && badge.awarded_for_trust_level? - - notification = send_notification(row.user_id, row.username, row.locale, badge) - UserBadge.trigger_user_badge_granted_event(badge.id, row.user_id) - - DB.exec( - "UPDATE user_badges SET notification_id = :notification_id WHERE id = :id", - notification_id: notification.id, - id: row.id + builder + .query( + id: badge.id, + multiple_grant: badge.multiple_grant, + backfill: full_backfill, + post_ids: post_ids || [-2], + user_ids: user_ids || [-2], ) - end + .each do |row| + next if suppress_notification?(badge, row.granted_at, row.skip_new_user_tips) + next if row.staff && badge.awarded_for_trust_level? + + notification = send_notification(row.user_id, row.username, row.locale, badge) + UserBadge.trigger_user_badge_granted_event(badge.id, row.user_id) + + DB.exec( + "UPDATE user_badges SET notification_id = :notification_id WHERE id = :id", + notification_id: notification.id, + id: row.id, + ) + end badge.reset_grant_count! rescue => e @@ -505,8 +538,8 @@ class BadgeGranter badge_name: badge.display_name, badge_slug: badge.slug, badge_title: badge.allow_title, - username: username - }.to_json + username: username, + }.to_json, ) end end diff --git a/app/services/base_bookmarkable.rb b/app/services/base_bookmarkable.rb index 6976eece62..77c19c7147 100644 --- a/app/services/base_bookmarkable.rb +++ b/app/services/base_bookmarkable.rb @@ -119,16 +119,17 @@ class BaseBookmarkable # created. # @return [void] def self.send_reminder_notification(bookmark, notification_data) - if notification_data[:data].blank? || - notification_data[:data][:bookmarkable_url].blank? || - notification_data[:data][:title].blank? - raise Discourse::InvalidParameters.new("A `data` key must be present with at least `bookmarkable_url` and `title` entries.") + if notification_data[:data].blank? || notification_data[:data][:bookmarkable_url].blank? || + notification_data[:data][:title].blank? + raise Discourse::InvalidParameters.new( + "A `data` key must be present with at least `bookmarkable_url` and `title` entries.", + ) end notification_data[:data] = notification_data[:data].merge( display_username: bookmark.user.username, bookmark_name: bookmark.name, - bookmark_id: bookmark.id + bookmark_id: bookmark.id, ).to_json notification_data[:notification_type] = Notification.types[:bookmark_reminder] bookmark.user.notifications.create!(notification_data) diff --git a/app/services/color_scheme_revisor.rb b/app/services/color_scheme_revisor.rb index 7515d513d3..937c6bf7d4 100644 --- a/app/services/color_scheme_revisor.rb +++ b/app/services/color_scheme_revisor.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ColorSchemeRevisor - def initialize(color_scheme, params = {}) @color_scheme = color_scheme @params = params @@ -14,7 +13,9 @@ class ColorSchemeRevisor def revise ColorScheme.transaction do @color_scheme.name = @params[:name] if @params.has_key?(:name) - @color_scheme.user_selectable = @params[:user_selectable] if @params.has_key?(:user_selectable) + @color_scheme.user_selectable = @params[:user_selectable] if @params.has_key?( + :user_selectable, + ) @color_scheme.base_scheme_id = @params[:base_scheme_id] if @params.has_key?(:base_scheme_id) has_colors = @params[:colors] @@ -29,15 +30,12 @@ class ColorSchemeRevisor @color_scheme.clear_colors_cache end - if has_colors || - @color_scheme.saved_change_to_name? || - @color_scheme.will_save_change_to_user_selectable? || - @color_scheme.saved_change_to_base_scheme_id? - + if has_colors || @color_scheme.saved_change_to_name? || + @color_scheme.will_save_change_to_user_selectable? || + @color_scheme.saved_change_to_base_scheme_id? @color_scheme.save end end @color_scheme end - end diff --git a/app/services/destroy_task.rb b/app/services/destroy_task.rb index d06a4ad755..fda78c9583 100644 --- a/app/services/destroy_task.rb +++ b/app/services/destroy_task.rb @@ -18,9 +18,7 @@ class DestroyTask topics.find_each do |topic| @io.puts "Deleting #{topic.slug}..." first_post = topic.ordered_posts.first - if first_post.nil? - return @io.puts "Topic.ordered_posts.first was nil" - end + return @io.puts "Topic.ordered_posts.first was nil" if first_post.nil? @io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy end end @@ -45,17 +43,17 @@ class DestroyTask def destroy_topics_all_categories categories = Category.all - categories.each do |c| - @io.puts destroy_topics(c.slug, c.parent_category&.slug) - end + categories.each { |c| @io.puts destroy_topics(c.slug, c.parent_category&.slug) } end def destroy_private_messages - Topic.where(archetype: "private_message").find_each do |pm| - @io.puts "Destroying #{pm.slug} pm" - first_post = pm.ordered_posts.first - @io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy - end + Topic + .where(archetype: "private_message") + .find_each do |pm| + @io.puts "Destroying #{pm.slug} pm" + first_post = pm.ordered_posts.first + @io.puts PostDestroyer.new(Discourse.system_user, first_post).destroy + end end def destroy_category(category_id, destroy_system_topics = false) @@ -63,9 +61,7 @@ class DestroyTask return @io.puts "A category with the id: #{category_id} could not be found" if c.nil? subcategories = Category.where(parent_category_id: c.id) @io.puts "There are #{subcategories.count} subcategories to delete" if subcategories - subcategories.each do |s| - category_topic_destroyer(s, destroy_system_topics) - end + subcategories.each { |s| category_topic_destroyer(s, destroy_system_topics) } category_topic_destroyer(c, destroy_system_topics) end @@ -78,21 +74,30 @@ class DestroyTask end def destroy_users - User.human_users.where(admin: false).find_each do |user| - begin - if UserDestroyer.new(Discourse.system_user).destroy(user, delete_posts: true, context: "destroy task") - @io.puts "#{user.username} deleted" - else - @io.puts "#{user.username} not deleted" + User + .human_users + .where(admin: false) + .find_each do |user| + begin + if UserDestroyer.new(Discourse.system_user).destroy( + user, + delete_posts: true, + context: "destroy task", + ) + @io.puts "#{user.username} deleted" + else + @io.puts "#{user.username} not deleted" + end + rescue UserDestroyer::PostsExistError + raise Discourse::InvalidAccess.new( + "User #{user.username} has #{user.post_count} posts, so can't be deleted.", + ) + rescue NoMethodError + @io.puts "#{user.username} could not be deleted" + rescue Discourse::InvalidAccess => e + @io.puts "#{user.username} #{e.message}" end - rescue UserDestroyer::PostsExistError - raise Discourse::InvalidAccess.new("User #{user.username} has #{user.post_count} posts, so can't be deleted.") - rescue NoMethodError - @io.puts "#{user.username} could not be deleted" - rescue Discourse::InvalidAccess => e - @io.puts "#{user.username} #{e.message}" end - end end def destroy_stats @@ -112,5 +117,4 @@ class DestroyTask @io.puts "Destroying #{category.slug} category" category.destroy end - end diff --git a/app/services/email_settings_exception_handler.rb b/app/services/email_settings_exception_handler.rb index ddb3fb908b..b85a9aaa95 100644 --- a/app/services/email_settings_exception_handler.rb +++ b/app/services/email_settings_exception_handler.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'net/imap' -require 'net/smtp' -require 'net/pop' +require "net/imap" +require "net/smtp" +require "net/pop" class EmailSettingsExceptionHandler EXPECTED_EXCEPTIONS = [ @@ -17,7 +17,7 @@ class EmailSettingsExceptionHandler Net::OpenTimeout, Net::ReadTimeout, SocketError, - Errno::ECONNREFUSED + Errno::ECONNREFUSED, ] class GenericProvider @@ -63,7 +63,10 @@ class EmailSettingsExceptionHandler if @exception.message.match(/Invalid credentials/) I18n.t("email_settings.imap_authentication_error") else - I18n.t("email_settings.imap_no_response_error", message: @exception.message.gsub(" (Failure)", "")) + I18n.t( + "email_settings.imap_no_response_error", + message: @exception.message.gsub(" (Failure)", ""), + ) end end diff --git a/app/services/email_settings_validator.rb b/app/services/email_settings_validator.rb index 238b586962..bd6dbccfe6 100644 --- a/app/services/email_settings_validator.rb +++ b/app/services/email_settings_validator.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'net/imap' -require 'net/smtp' -require 'net/pop' +require "net/imap" +require "net/smtp" +require "net/pop" # Usage: # @@ -84,15 +84,14 @@ class EmailSettingsValidator debug: Rails.env.development? ) begin - port, enable_tls, enable_starttls_auto = provider_specific_ssl_overrides( - host, port, enable_tls, enable_starttls_auto - ) + port, enable_tls, enable_starttls_auto = + provider_specific_ssl_overrides(host, port, enable_tls, enable_starttls_auto) if enable_tls && enable_starttls_auto raise ArgumentError, "TLS and STARTTLS are mutually exclusive" end - if ![:plain, :login, :cram_md5].include?(authentication.to_sym) + if !%i[plain login cram_md5].include?(authentication.to_sym) raise ArgumentError, "Invalid authentication method. Must be plain, login, or cram_md5." end @@ -100,7 +99,6 @@ class EmailSettingsValidator if Rails.env.development? domain = "localhost" else - # Because we are using the SMTP settings here to send emails, # the domain should just be the TLD of the host. domain = MiniSuffix.domain(host) @@ -154,7 +152,11 @@ class EmailSettingsValidator begin imap = Net::IMAP.new(host, port: port, ssl: ssl, open_timeout: open_timeout) imap.login(username, password) - imap.logout rescue nil + begin + imap.logout + rescue StandardError + nil + end imap.disconnect rescue => err log_and_raise(err, debug) @@ -163,7 +165,9 @@ class EmailSettingsValidator def self.log_and_raise(err, debug) if debug - Rails.logger.warn("[EmailSettingsValidator] Error encountered when validating email settings: #{err.message} #{err.backtrace.join("\n")}") + Rails.logger.warn( + "[EmailSettingsValidator] Error encountered when validating email settings: #{err.message} #{err.backtrace.join("\n")}", + ) end raise err end diff --git a/app/services/email_style_updater.rb b/app/services/email_style_updater.rb index d13e84b78d..42ebb0b704 100644 --- a/app/services/email_style_updater.rb +++ b/app/services/email_style_updater.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class EmailStyleUpdater - attr_reader :errors def initialize(user) @@ -10,11 +9,8 @@ class EmailStyleUpdater end def update(attrs) - if attrs.has_key?(:html) && !attrs[:html].include?('%{email_content}') - @errors << I18n.t( - 'email_style.html_missing_placeholder', - placeholder: '%{email_content}' - ) + if attrs.has_key?(:html) && !attrs[:html].include?("%{email_content}") + @errors << I18n.t("email_style.html_missing_placeholder", placeholder: "%{email_content}") end if attrs.has_key?(:css) diff --git a/app/services/external_upload_manager.rb b/app/services/external_upload_manager.rb index cfeca323c7..23523ab4bc 100644 --- a/app/services/external_upload_manager.rb +++ b/app/services/external_upload_manager.rb @@ -7,10 +7,14 @@ class ExternalUploadManager UPLOAD_TYPES_EXCLUDED_FROM_UPLOAD_PROMOTION = ["backup"].freeze - class ChecksumMismatchError < StandardError; end - class DownloadFailedError < StandardError; end - class CannotPromoteError < StandardError; end - class SizeMismatchError < StandardError; end + class ChecksumMismatchError < StandardError + end + class DownloadFailedError < StandardError + end + class CannotPromoteError < StandardError + end + class SizeMismatchError < StandardError + end attr_reader :external_upload_stub @@ -24,51 +28,54 @@ class ExternalUploadManager def self.create_direct_upload(current_user:, file_name:, file_size:, upload_type:, metadata: {}) store = store_for_upload_type(upload_type) - url = store.signed_url_for_temporary_upload( - file_name, metadata: metadata - ) + url = store.signed_url_for_temporary_upload(file_name, metadata: metadata) key = store.s3_helper.path_from_url(url) - upload_stub = ExternalUploadStub.create!( - key: key, - created_by: current_user, - original_filename: file_name, - upload_type: upload_type, - filesize: file_size - ) + upload_stub = + ExternalUploadStub.create!( + key: key, + created_by: current_user, + original_filename: file_name, + upload_type: upload_type, + filesize: file_size, + ) { url: url, key: key, unique_identifier: upload_stub.unique_identifier } end def self.create_direct_multipart_upload( - current_user:, file_name:, file_size:, upload_type:, metadata: {} + current_user:, + file_name:, + file_size:, + upload_type:, + metadata: {} ) content_type = MiniMime.lookup_by_filename(file_name)&.content_type store = store_for_upload_type(upload_type) - multipart_upload = store.create_multipart( - file_name, content_type, metadata: metadata - ) + multipart_upload = store.create_multipart(file_name, content_type, metadata: metadata) - upload_stub = ExternalUploadStub.create!( - key: multipart_upload[:key], - created_by: current_user, - original_filename: file_name, - upload_type: upload_type, - external_upload_identifier: multipart_upload[:upload_id], - multipart: true, - filesize: file_size - ) + upload_stub = + ExternalUploadStub.create!( + key: multipart_upload[:key], + created_by: current_user, + original_filename: file_name, + upload_type: upload_type, + external_upload_identifier: multipart_upload[:upload_id], + multipart: true, + filesize: file_size, + ) { external_upload_identifier: upload_stub.external_upload_identifier, key: upload_stub.key, - unique_identifier: upload_stub.unique_identifier + unique_identifier: upload_stub.unique_identifier, } end def self.store_for_upload_type(upload_type) if upload_type == "backup" - if !SiteSetting.enable_backups? || SiteSetting.backup_location != BackupLocationSiteSetting::S3 + if !SiteSetting.enable_backups? || + SiteSetting.backup_location != BackupLocationSiteSetting::S3 raise Discourse::InvalidAccess.new end BackupRestore::BackupStore.create @@ -98,9 +105,11 @@ class ExternalUploadManager if external_size != external_upload_stub.filesize ExternalUploadManager.ban_user_from_external_uploads!( user: external_upload_stub.created_by, - ban_minutes: SIZE_MISMATCH_BAN_MINUTES + ban_minutes: SIZE_MISMATCH_BAN_MINUTES, ) - raise SizeMismatchError.new("expected: #{external_upload_stub.filesize}, actual: #{external_size}") + raise SizeMismatchError.new( + "expected: #{external_upload_stub.filesize}, actual: #{external_size}", + ) end if UPLOAD_TYPES_EXCLUDED_FROM_UPLOAD_PROMOTION.include?(external_upload_stub.upload_type) @@ -108,7 +117,7 @@ class ExternalUploadManager else promote_to_upload end - rescue + rescue StandardError if !SiteSetting.enable_upload_debug_mode # We don't need to do anything special to abort multipart uploads here, # because at this point (calling promote_to_upload!), the multipart @@ -137,9 +146,7 @@ class ExternalUploadManager raise DownloadFailedError if tempfile.blank? actual_sha1 = Upload.generate_digest(tempfile) - if external_sha1 && external_sha1 != actual_sha1 - raise ChecksumMismatchError - end + raise ChecksumMismatchError if external_sha1 && external_sha1 != actual_sha1 end # TODO (martin): See if these additional opts will be needed @@ -148,11 +155,11 @@ class ExternalUploadManager type: external_upload_stub.upload_type, existing_external_upload_key: external_upload_stub.key, external_upload_too_big: external_size > DOWNLOAD_LIMIT, - filesize: external_size + filesize: external_size, }.merge(@upload_create_opts) UploadCreator.new(tempfile, external_upload_stub.original_filename, opts).create_for( - external_upload_stub.created_by_id + external_upload_stub.created_by_id, ) ensure tempfile&.close! @@ -163,7 +170,7 @@ class ExternalUploadManager @store.move_existing_stored_upload( existing_external_upload_key: external_upload_stub.key, original_filename: external_upload_stub.original_filename, - content_type: content_type + content_type: content_type, ) Struct.new(:errors).new([]) end @@ -190,7 +197,7 @@ class ExternalUploadManager url, max_file_size: DOWNLOAD_LIMIT, tmp_file_name: "discourse-upload-#{type}", - follow_redirect: true + follow_redirect: true, ) end end diff --git a/app/services/group_action_logger.rb b/app/services/group_action_logger.rb index d38d0e7c5a..02299afb48 100644 --- a/app/services/group_action_logger.rb +++ b/app/services/group_action_logger.rb @@ -1,64 +1,71 @@ # frozen_string_literal: true class GroupActionLogger - def initialize(acting_user, group) @acting_user = acting_user @group = group end def log_make_user_group_owner(target_user) - GroupHistory.create!(default_params.merge( - action: GroupHistory.actions[:make_user_group_owner], - target_user: target_user - )) + GroupHistory.create!( + default_params.merge( + action: GroupHistory.actions[:make_user_group_owner], + target_user: target_user, + ), + ) end def log_remove_user_as_group_owner(target_user) - GroupHistory.create!(default_params.merge( - action: GroupHistory.actions[:remove_user_as_group_owner], - target_user: target_user - )) + GroupHistory.create!( + default_params.merge( + action: GroupHistory.actions[:remove_user_as_group_owner], + target_user: target_user, + ), + ) end def log_add_user_to_group(target_user, subject = nil) - GroupHistory.create!(default_params.merge( - action: GroupHistory.actions[:add_user_to_group], - target_user: target_user, - subject: subject - )) + GroupHistory.create!( + default_params.merge( + action: GroupHistory.actions[:add_user_to_group], + target_user: target_user, + subject: subject, + ), + ) end def log_remove_user_from_group(target_user, subject = nil) - GroupHistory.create!(default_params.merge( - action: GroupHistory.actions[:remove_user_from_group], - target_user: target_user, - subject: subject - )) + GroupHistory.create!( + default_params.merge( + action: GroupHistory.actions[:remove_user_from_group], + target_user: target_user, + subject: subject, + ), + ) end def log_change_group_settings - @group.previous_changes.except(*excluded_attributes).each do |attribute_name, value| - next if value[0].blank? && value[1].blank? + @group + .previous_changes + .except(*excluded_attributes) + .each do |attribute_name, value| + next if value[0].blank? && value[1].blank? - GroupHistory.create!(default_params.merge( - action: GroupHistory.actions[:change_group_setting], - subject: attribute_name, - prev_value: value[0], - new_value: value[1] - )) - end + GroupHistory.create!( + default_params.merge( + action: GroupHistory.actions[:change_group_setting], + subject: attribute_name, + prev_value: value[0], + new_value: value[1], + ), + ) + end end private def excluded_attributes - [ - :bio_cooked, - :updated_at, - :created_at, - :user_count - ] + %i[bio_cooked updated_at created_at user_count] end def default_params diff --git a/app/services/group_mentions_updater.rb b/app/services/group_mentions_updater.rb index 5130baa43e..8099dcde2b 100644 --- a/app/services/group_mentions_updater.rb +++ b/app/services/group_mentions_updater.rb @@ -2,15 +2,16 @@ class GroupMentionsUpdater def self.update(current_name, previous_name) - Post.where( - "cooked LIKE '%class=\"mention-group%' AND raw LIKE :previous_name", - previous_name: "%@#{previous_name}%" - ).find_in_batches do |posts| - - posts.each do |post| - post.raw.gsub!(/(^|\s)(@#{previous_name})(\s|$)/, "\\1@#{current_name}\\3") - post.save!(validate: false) + Post + .where( + "cooked LIKE '%class=\"mention-group%' AND raw LIKE :previous_name", + previous_name: "%@#{previous_name}%", + ) + .find_in_batches do |posts| + posts.each do |post| + post.raw.gsub!(/(^|\s)(@#{previous_name})(\s|$)/, "\\1@#{current_name}\\3") + post.save!(validate: false) + end end - end end end diff --git a/app/services/group_message.rb b/app/services/group_message.rb index 1f24289434..47ee093a3f 100644 --- a/app/services/group_message.rb +++ b/app/services/group_message.rb @@ -11,7 +11,6 @@ # The default is 24 hours. Set to false to always send the message. class GroupMessage - include Rails.application.routes.url_helpers RECENT_MESSAGE_PERIOD = 3.months @@ -29,14 +28,15 @@ class GroupMessage def create return false if sent_recently? - post = PostCreator.create( - Discourse.system_user, - target_group_names: [@group_name], - archetype: Archetype.private_message, - subtype: TopicSubtype.system_message, - title: I18n.t("system_messages.#{@message_type}.subject_template", message_params), - raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params) - ) + post = + PostCreator.create( + Discourse.system_user, + target_group_names: [@group_name], + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + title: I18n.t("system_messages.#{@message_type}.subject_template", message_params), + raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params), + ) remember_message_sent post end @@ -44,38 +44,44 @@ class GroupMessage def delete_previous!(respect_sent_recently: true, match_raw: true) return false if respect_sent_recently && sent_recently? - posts = Post - .joins(topic: { topic_allowed_groups: :group }) - .where(topic: { - posts_count: 1, - user_id: Discourse.system_user, - archetype: Archetype.private_message, - subtype: TopicSubtype.system_message, - title: I18n.t("system_messages.#{@message_type}.subject_template", message_params), - topic_allowed_groups: { - groups: { name: @group_name } - } - }) - .where("posts.created_at > ?", RECENT_MESSAGE_PERIOD.ago) + posts = + Post + .joins(topic: { topic_allowed_groups: :group }) + .where( + topic: { + posts_count: 1, + user_id: Discourse.system_user, + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + title: I18n.t("system_messages.#{@message_type}.subject_template", message_params), + topic_allowed_groups: { + groups: { + name: @group_name, + }, + }, + }, + ) + .where("posts.created_at > ?", RECENT_MESSAGE_PERIOD.ago) if match_raw - posts = posts.where(raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params).rstrip) + posts = + posts.where( + raw: I18n.t("system_messages.#{@message_type}.text_body_template", message_params).rstrip, + ) end - posts.find_each do |post| - PostDestroyer.new(Discourse.system_user, post).destroy - end + posts.find_each { |post| PostDestroyer.new(Discourse.system_user, post).destroy } end def message_params - @message_params ||= begin - h = { base_url: Discourse.base_url }.merge(@opts[:message_params] || {}) - if @opts[:user] - h.merge!(username: @opts[:user].username, - user_url: user_path(@opts[:user].username)) + @message_params ||= + begin + h = { base_url: Discourse.base_url }.merge(@opts[:message_params] || {}) + if @opts[:user] + h.merge!(username: @opts[:user].username, user_url: user_path(@opts[:user].username)) + end + h end - h - end end def sent_recently? @@ -85,10 +91,12 @@ class GroupMessage # default is to send no more than once every 24 hours (24 * 60 * 60 = 86,400 seconds) def remember_message_sent - Discourse.redis.setex(sent_recently_key, @opts[:limit_once_per].try(:to_i) || 86_400, 1) unless @opts[:limit_once_per] == false + unless @opts[:limit_once_per] == false + Discourse.redis.setex(sent_recently_key, @opts[:limit_once_per].try(:to_i) || 86_400, 1) + end end def sent_recently_key - "grpmsg:#{@group_name}:#{@message_type}:#{@opts[:user] ? @opts[:user].username : ''}" + "grpmsg:#{@group_name}:#{@message_type}:#{@opts[:user] ? @opts[:user].username : ""}" end end diff --git a/app/services/handle_chunk_upload.rb b/app/services/handle_chunk_upload.rb index d966f659aa..cc69c1c5ba 100644 --- a/app/services/handle_chunk_upload.rb +++ b/app/services/handle_chunk_upload.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class HandleChunkUpload - def initialize(chunk, params = {}) @chunk = chunk @params = params @@ -21,7 +20,8 @@ class HandleChunkUpload def check_chunk # check whether the chunk has already been uploaded - has_chunk_been_uploaded = File.exist?(@chunk) && File.size(@chunk) == @params[:current_chunk_size] + has_chunk_been_uploaded = + File.exist?(@chunk) && File.size(@chunk) == @params[:current_chunk_size] # 200 = exists, 404 = not uploaded yet status = has_chunk_been_uploaded ? 200 : 404 end @@ -36,11 +36,11 @@ class HandleChunkUpload end def merge_chunks - upload_path = @params[:upload_path] + upload_path = @params[:upload_path] tmp_upload_path = @params[:tmp_upload_path] - identifier = @params[:identifier] - filename = @params[:filename] - tmp_directory = @params[:tmp_directory] + identifier = @params[:identifier] + filename = @params[:filename] + tmp_directory = @params[:tmp_directory] # delete destination files begin @@ -68,5 +68,4 @@ class HandleChunkUpload rescue Errno::ENOENT end end - end diff --git a/app/services/heat_settings_updater.rb b/app/services/heat_settings_updater.rb index 56666b71f4..325e87117c 100644 --- a/app/services/heat_settings_updater.rb +++ b/app/services/heat_settings_updater.rb @@ -70,23 +70,20 @@ class HeatSettingsUpdater if SiteSetting.get(name) != SiteSetting.defaults[name] SiteSetting.set_and_log(name, SiteSetting.defaults[name]) end - elsif SiteSetting.get(name) == 0 || - (new_value.to_f / SiteSetting.get(name) - 1.0).abs >= 0.05 - - rounded_new_value = if new_value.is_a?(Integer) - if new_value > 9 - digits = new_value.digits.reverse - (digits[0] * 10 + digits[1]) * 10.pow(digits[2..-1].size) + elsif SiteSetting.get(name) == 0 || (new_value.to_f / SiteSetting.get(name) - 1.0).abs >= 0.05 + rounded_new_value = + if new_value.is_a?(Integer) + if new_value > 9 + digits = new_value.digits.reverse + (digits[0] * 10 + digits[1]) * 10.pow(digits[2..-1].size) + else + new_value + end else - new_value + new_value.round(2) end - else - new_value.round(2) - end - if SiteSetting.get(name) != rounded_new_value - SiteSetting.set_and_log(name, rounded_new_value) - end + SiteSetting.set_and_log(name, rounded_new_value) if SiteSetting.get(name) != rounded_new_value end end end diff --git a/app/services/inline_uploads.rb b/app/services/inline_uploads.rb index 62d081caa3..8feb0efc93 100644 --- a/app/services/inline_uploads.rb +++ b/app/services/inline_uploads.rb @@ -16,14 +16,20 @@ class InlineUploads end end - cooked_fragment = Nokogiri::HTML5::fragment(PrettyText.cook(markdown, disable_emojis: true)) + cooked_fragment = Nokogiri::HTML5.fragment(PrettyText.cook(markdown, disable_emojis: true)) link_occurrences = [] cooked_fragment.traverse do |node| if node.name == "img" # Do nothing - elsif !(node.children.count == 1 && (node.children[0].name != "img" && node.children[0].children.blank?)) && - !(node.name == "a" && node.children.count > 1 && !node_children_names(node).include?("img")) + elsif !( + node.children.count == 1 && + (node.children[0].name != "img" && node.children[0].children.blank?) + ) && + !( + node.name == "a" && node.children.count > 1 && + !node_children_names(node).include?("img") + ) next end @@ -55,7 +61,7 @@ class InlineUploads end regexps = [ - /(https?:\/\/[a-zA-Z0-9\.\/-]+\/#{Discourse.store.upload_path}#{UPLOAD_REGEXP_PATTERN})/, + %r{(https?://[a-zA-Z0-9\./-]+/#{Discourse.store.upload_path}#{UPLOAD_REGEXP_PATTERN})}, ] if Discourse.store.external? @@ -103,41 +109,38 @@ class InlineUploads raw_matches .sort { |a, b| a[3] <=> b[3] } .each do |match, link, replace_with, _index| + node_info = link_occurrences.shift + next unless node_info&.dig(:is_valid) - node_info = link_occurrences.shift - next unless node_info&.dig(:is_valid) - - if link.include?(node_info[:link]) - begin - uri = URI(link) - rescue URI::Error - end - - if !Discourse.store.external? - host = uri&.host - - hosts = [Discourse.current_hostname] - - if cdn_url = GlobalSetting.cdn_url - hosts << URI(GlobalSetting.cdn_url).hostname + if link.include?(node_info[:link]) + begin + uri = URI(link) + rescue URI::Error end - if host && !hosts.include?(host) - next + if !Discourse.store.external? + host = uri&.host + + hosts = [Discourse.current_hostname] + + if cdn_url = GlobalSetting.cdn_url + hosts << URI(GlobalSetting.cdn_url).hostname + end + + next if host && !hosts.include?(host) end - end - upload = Upload.get_from_url(link) + upload = Upload.get_from_url(link) - if upload - replace_with.sub!(PLACEHOLDER, upload.short_url) - replace_with.sub!(PATH_PLACEHOLDER, upload.short_path) - markdown.sub!(match, replace_with) - else - on_missing.call(link) if on_missing + if upload + replace_with.sub!(PLACEHOLDER, upload.short_url) + replace_with.sub!(PATH_PLACEHOLDER, upload.short_path) + markdown.sub!(match, replace_with) + else + on_missing.call(link) if on_missing + end end end - end markdown.scan(/(__(\h{40})__)/) do |match| upload = Upload.find_by(sha1: match[1]) @@ -161,7 +164,7 @@ class InlineUploads end def self.match_bbcode_img(markdown, external_src: false) - markdown.scan(/(\[img\]\s*([^\[\]\s]+)\s*\[\/img\])/i) do |match| + markdown.scan(%r{(\[img\]\s*([^\[\]\s]+)\s*\[/img\])}i) do |match| if (external_src || (matched_uploads(match[1]).present?)) && block_given? yield(match[0], match[1], +"![](#{PLACEHOLDER})", $~.offset(0)[0]) end @@ -182,9 +185,9 @@ class InlineUploads end def self.match_anchor(markdown, external_href: false) - markdown.scan(/(()([^<\a>]*?)<\/a>)/i) do |match| - node = Nokogiri::HTML5::fragment(match[0]).children[0] - href = node.attributes["href"]&.value + markdown.scan(%r{(()([^<\a>]*?))}i) do |match| + node = Nokogiri::HTML5.fragment(match[0]).children[0] + href = node.attributes["href"]&.value if href && (external_href || matched_uploads(href).present?) has_attachment = node.attributes["class"]&.value @@ -198,8 +201,8 @@ class InlineUploads end def self.match_img(markdown, external_src: false, uploads: nil) - markdown.scan(/(<(?!img)[^<>]+\/?>)?(\s*)(\n]+>)/i) do |match| - node = Nokogiri::HTML5::fragment(match[2].strip).children[0] + markdown.scan(%r{(<(?!img)[^<>]+/?>)?(\s*)(\n]+>)}i) do |match| + node = Nokogiri::HTML5.fragment(match[2].strip).children[0] src = node&.attributes&.[]("src")&.value if src && (external_src || matched_uploads(src).present?) @@ -215,22 +218,20 @@ class InlineUploads end def self.replace_hotlinked_image_urls(raw:, &blk) - replace = Proc.new do |match, match_src, replacement, _index| - upload = blk.call(match_src) - next if !upload + replace = + Proc.new do |match, match_src, replacement, _index| + upload = blk.call(match_src) + next if !upload - replacement = - if replacement.include?(InlineUploads::PLACEHOLDER) - replacement.sub(InlineUploads::PLACEHOLDER, upload.short_url) - elsif replacement.include?(InlineUploads::PATH_PLACEHOLDER) - replacement.sub(InlineUploads::PATH_PLACEHOLDER, upload.short_path) - end + replacement = + if replacement.include?(InlineUploads::PLACEHOLDER) + replacement.sub(InlineUploads::PLACEHOLDER, upload.short_url) + elsif replacement.include?(InlineUploads::PATH_PLACEHOLDER) + replacement.sub(InlineUploads::PATH_PLACEHOLDER, upload.short_path) + end - raw = raw.gsub( - match, - replacement - ) - end + raw = raw.gsub(match, replacement) + end # there are 6 ways to insert an image in a post # HTML tag - @@ -245,40 +246,41 @@ class InlineUploads # Markdown inline - ![alt](http://... "image title") InlineUploads.match_md_inline_img(raw, external_src: true, &replace) - raw = raw.gsub(/^(https?:\/\/\S+)(\s?)$/) do |match| - if upload = blk.call(match) - "![](#{upload.short_url})" - else - match + raw = + raw.gsub(%r{^(https?://\S+)(\s?)$}) do |match| + if upload = blk.call(match) + "![](#{upload.short_url})" + else + match + end end - end raw end def self.matched_uploads(node) upload_path = Discourse.store.upload_path - base_url = Discourse.base_url.sub(/https?:\/\//, "(https?://)") + base_url = Discourse.base_url.sub(%r{https?://}, "(https?://)") regexps = [ - /(upload:\/\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/, - /(\/uploads\/short-url\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/, - /(#{base_url}\/uploads\/short-url\/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)/, - /(#{GlobalSetting.relative_url_root}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/, - /(#{base_url}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/, + %r{(upload://([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)}, + %r{(/uploads/short-url/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)}, + %r{(#{base_url}/uploads/short-url/([a-zA-Z0-9]+)[a-zA-Z0-9\.]*)}, + %r{(#{GlobalSetting.relative_url_root}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})}, + %r{(#{base_url}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})}, ] - if GlobalSetting.cdn_url && (cdn_url = GlobalSetting.cdn_url.sub(/https?:\/\//, "(https?://)")) - regexps << /(#{cdn_url}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/ + if GlobalSetting.cdn_url && (cdn_url = GlobalSetting.cdn_url.sub(%r{https?://}, "(https?://)")) + regexps << %r{(#{cdn_url}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})} if GlobalSetting.relative_url_root.present? - regexps << /(#{cdn_url}#{GlobalSetting.relative_url_root}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/ + regexps << %r{(#{cdn_url}#{GlobalSetting.relative_url_root}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})} end end if Discourse.store.external? if Rails.configuration.multisite - regexps << /((https?:)?#{SiteSetting.Upload.s3_base_url}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/ - regexps << /(#{SiteSetting.Upload.s3_cdn_url}\/#{upload_path}#{UPLOAD_REGEXP_PATTERN})/ + regexps << %r{((https?:)?#{SiteSetting.Upload.s3_base_url}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})} + regexps << %r{(#{SiteSetting.Upload.s3_cdn_url}/#{upload_path}#{UPLOAD_REGEXP_PATTERN})} else regexps << /((https?:)?#{SiteSetting.Upload.s3_base_url}#{UPLOAD_REGEXP_PATTERN})/ regexps << /(#{SiteSetting.Upload.s3_cdn_url}#{UPLOAD_REGEXP_PATTERN})/ @@ -289,9 +291,7 @@ class InlineUploads node = node.to_s regexps.each do |regexp| - node.scan(/(^|[\n\s"'\(>])#{regexp}($|[\n\s"'\)<])/) do |matched| - matches << matched[1] - end + node.scan(/(^|[\n\s"'\(>])#{regexp}($|[\n\s"'\)<])/) { |matched| matches << matched[1] } end matches @@ -304,9 +304,7 @@ class InlineUploads return names end - node.children.each do |child| - names = node_children_names(child, names) - end + node.children.each { |child| names = node_children_names(child, names) } names end diff --git a/app/services/notification_emailer.rb b/app/services/notification_emailer.rb index b768ae5b68..60d124dc96 100644 --- a/app/services/notification_emailer.rb +++ b/app/services/notification_emailer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class NotificationEmailer - class EmailUser attr_reader :notification, :no_delay @@ -84,7 +83,6 @@ class NotificationEmailer end def enqueue_private(type, delay = private_delay) - if notification.user.user_option.nil? # this can happen if we roll back user creation really early # or delete user @@ -92,7 +90,9 @@ class NotificationEmailer return end - return if notification.user.user_option.email_messages_level == UserOption.email_level_types[:never] + if notification.user.user_option.email_messages_level == UserOption.email_level_types[:never] + return + end perform_enqueue(type, delay) end @@ -116,13 +116,13 @@ class NotificationEmailer end def post_type - @post_type ||= begin - type = notification.data_hash["original_post_type"] if notification.data_hash - type ||= notification.post.try(:post_type) - type - end + @post_type ||= + begin + type = notification.data_hash["original_post_type"] if notification.data_hash + type ||= notification.post.try(:post_type) + type + end end - end def self.disable @@ -136,9 +136,8 @@ class NotificationEmailer def self.process_notification(notification, no_delay: false) return if @disabled - email_user = EmailUser.new(notification, no_delay: no_delay) + email_user = EmailUser.new(notification, no_delay: no_delay) email_method = Notification.types[notification.notification_type] email_user.public_send(email_method) if email_user.respond_to? email_method end - end diff --git a/app/services/notifications/consolidate_notifications.rb b/app/services/notifications/consolidate_notifications.rb index adfeaee10f..b50eb504e1 100644 --- a/app/services/notifications/consolidate_notifications.rb +++ b/app/services/notifications/consolidate_notifications.rb @@ -34,7 +34,14 @@ module Notifications class ConsolidateNotifications < ConsolidationPlan - def initialize(from:, to:, consolidation_window: nil, unconsolidated_query_blk: nil, consolidated_query_blk: nil, threshold:) + def initialize( + from:, + to:, + consolidation_window: nil, + unconsolidated_query_blk: nil, + consolidated_query_blk: nil, + threshold: + ) @from = from @to = to @threshold = threshold @@ -67,15 +74,21 @@ module Notifications return unless can_consolidate_data?(notification) update_consolidated_notification!(notification) || - create_consolidated_notification!(notification) || - notification.tap(&:save!) + create_consolidated_notification!(notification) || notification.tap(&:save!) end private attr_reader( - :notification, :from, :to, :data, :threshold, :consolidated_query_blk, - :unconsolidated_query_blk, :consolidation_window, :bump_notification + :notification, + :from, + :to, + :data, + :threshold, + :consolidated_query_blk, + :unconsolidated_query_blk, + :consolidation_window, + :bump_notification, ) def update_consolidated_notification!(notification) @@ -90,18 +103,12 @@ module Notifications data_hash = consolidated.data_hash.merge(data) data_hash[:count] += 1 if data_hash[:count].present? - if @before_update_blk - @before_update_blk.call(consolidated, data_hash, notification) - end + @before_update_blk.call(consolidated, data_hash, notification) if @before_update_blk # Hack: We don't want to cache the old data if we're about to update it. consolidated.instance_variable_set(:@data_hash, nil) - consolidated.update!( - data: data_hash.to_json, - read: false, - updated_at: timestamp, - ) + consolidated.update!(data: data_hash.to_json, read: false, updated_at: timestamp) consolidated end @@ -119,22 +126,21 @@ module Notifications timestamp = notifications.last.created_at data[:count] = count_after_saving_notification - if @before_consolidation_blk - @before_consolidation_blk.call(notifications, data) - end + @before_consolidation_blk.call(notifications, data) if @before_consolidation_blk consolidated = nil Notification.transaction do notifications.destroy_all - consolidated = Notification.create!( - notification_type: to, - user_id: notification.user_id, - data: data.to_json, - updated_at: timestamp, - created_at: timestamp - ) + consolidated = + Notification.create!( + notification_type: to, + user_id: notification.user_id, + data: data.to_json, + updated_at: timestamp, + created_at: timestamp, + ) end consolidated @@ -148,7 +154,7 @@ module Notifications notifications = super(notification, type) if consolidation_window.present? - notifications = notifications.where('created_at > ?', consolidation_window.ago) + notifications = notifications.where("created_at > ?", consolidation_window.ago) end notifications diff --git a/app/services/notifications/consolidation_planner.rb b/app/services/notifications/consolidation_planner.rb index 6d1f5cb44d..87704b010e 100644 --- a/app/services/notifications/consolidation_planner.rb +++ b/app/services/notifications/consolidation_planner.rb @@ -12,105 +12,125 @@ module Notifications private def plan_for(notification) - consolidation_plans = [liked_by_two_users, liked, group_message_summary, group_membership, new_features_notification] + consolidation_plans = [ + liked_by_two_users, + liked, + group_message_summary, + group_membership, + new_features_notification, + ] consolidation_plans.concat(DiscoursePluginRegistry.notification_consolidation_plans) consolidation_plans.detect { |plan| plan.can_consolidate_data?(notification) } end def liked - ConsolidateNotifications.new( - from: Notification.types[:liked], - to: Notification.types[:liked_consolidated], - threshold: -> { SiteSetting.notification_consolidation_threshold }, - consolidation_window: SiteSetting.likes_notification_consolidation_window_mins.minutes, - unconsolidated_query_blk: Proc.new do |notifications, data| - key = 'display_username' - value = data[key.to_sym] - filtered = notifications.where("data::json ->> 'username2' IS NULL") + ConsolidateNotifications + .new( + from: Notification.types[:liked], + to: Notification.types[:liked_consolidated], + threshold: -> { SiteSetting.notification_consolidation_threshold }, + consolidation_window: SiteSetting.likes_notification_consolidation_window_mins.minutes, + unconsolidated_query_blk: + Proc.new do |notifications, data| + key = "display_username" + value = data[key.to_sym] + filtered = notifications.where("data::json ->> 'username2' IS NULL") - filtered = filtered.where("data::json ->> '#{key}' = ?", value) if value + filtered = filtered.where("data::json ->> '#{key}' = ?", value) if value - filtered - end, - consolidated_query_blk: filtered_by_data_attribute('display_username') - ).set_mutations( - set_data_blk: Proc.new do |notification| - data = notification.data_hash - data.merge(username: data[:display_username]) - end - ).set_precondition(precondition_blk: Proc.new { |data| data[:username2].blank? }) + filtered + end, + consolidated_query_blk: filtered_by_data_attribute("display_username"), + ) + .set_mutations( + set_data_blk: + Proc.new do |notification| + data = notification.data_hash + data.merge(username: data[:display_username]) + end, + ) + .set_precondition(precondition_blk: Proc.new { |data| data[:username2].blank? }) end def liked_by_two_users - DeletePreviousNotifications.new( - type: Notification.types[:liked], - previous_query_blk: Proc.new do |notifications, data| - notifications.where(id: data[:previous_notification_id]) - end - ).set_mutations( - set_data_blk: Proc.new do |notification| - existing_notification_of_same_type = Notification - .where(user: notification.user) - .order("notifications.id DESC") - .where(topic_id: notification.topic_id, post_number: notification.post_number) - .where(notification_type: notification.notification_type) - .where('created_at > ?', 1.day.ago) - .first + DeletePreviousNotifications + .new( + type: Notification.types[:liked], + previous_query_blk: + Proc.new do |notifications, data| + notifications.where(id: data[:previous_notification_id]) + end, + ) + .set_mutations( + set_data_blk: + Proc.new do |notification| + existing_notification_of_same_type = + Notification + .where(user: notification.user) + .order("notifications.id DESC") + .where(topic_id: notification.topic_id, post_number: notification.post_number) + .where(notification_type: notification.notification_type) + .where("created_at > ?", 1.day.ago) + .first - data = notification.data_hash - if existing_notification_of_same_type - same_type_data = existing_notification_of_same_type.data_hash - data.merge( - previous_notification_id: existing_notification_of_same_type.id, - username2: same_type_data[:display_username], - count: (same_type_data[:count] || 1).to_i + 1 - ) - else - data - end - end - ).set_precondition( - precondition_blk: Proc.new do |data, notification| - always_freq = UserOption.like_notification_frequency_type[:always] + data = notification.data_hash + if existing_notification_of_same_type + same_type_data = existing_notification_of_same_type.data_hash + data.merge( + previous_notification_id: existing_notification_of_same_type.id, + username2: same_type_data[:display_username], + count: (same_type_data[:count] || 1).to_i + 1, + ) + else + data + end + end, + ) + .set_precondition( + precondition_blk: + Proc.new do |data, notification| + always_freq = UserOption.like_notification_frequency_type[:always] - notification.user&.user_option&.like_notification_frequency == always_freq && - data[:previous_notification_id].present? - end - ) + notification.user&.user_option&.like_notification_frequency == always_freq && + data[:previous_notification_id].present? + end, + ) end def group_membership - ConsolidateNotifications.new( - from: Notification.types[:private_message], - to: Notification.types[:membership_request_consolidated], - threshold: -> { SiteSetting.notification_consolidation_threshold }, - consolidation_window: Notification::MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS.hours, - unconsolidated_query_blk: filtered_by_data_attribute('topic_title'), - consolidated_query_blk: filtered_by_data_attribute('group_name') - ).set_precondition( - precondition_blk: Proc.new { |data| data[:group_name].present? } - ).set_mutations( - set_data_blk: Proc.new do |notification| - data = notification.data_hash - post_id = data[:original_post_id] - custom_field = PostCustomField.select(:value).find_by(post_id: post_id, name: "requested_group_id") - group_id = custom_field&.value - group_name = group_id.present? ? Group.select(:name).find_by(id: group_id.to_i)&.name : nil + ConsolidateNotifications + .new( + from: Notification.types[:private_message], + to: Notification.types[:membership_request_consolidated], + threshold: -> { SiteSetting.notification_consolidation_threshold }, + consolidation_window: Notification::MEMBERSHIP_REQUEST_CONSOLIDATION_WINDOW_HOURS.hours, + unconsolidated_query_blk: filtered_by_data_attribute("topic_title"), + consolidated_query_blk: filtered_by_data_attribute("group_name"), + ) + .set_precondition(precondition_blk: Proc.new { |data| data[:group_name].present? }) + .set_mutations( + set_data_blk: + Proc.new do |notification| + data = notification.data_hash + post_id = data[:original_post_id] + custom_field = + PostCustomField.select(:value).find_by(post_id: post_id, name: "requested_group_id") + group_id = custom_field&.value + group_name = + group_id.present? ? Group.select(:name).find_by(id: group_id.to_i)&.name : nil - data[:group_name] = group_name - data - end - ) + data[:group_name] = group_name + data + end, + ) end def group_message_summary DeletePreviousNotifications.new( type: Notification.types[:group_message_summary], - previous_query_blk: filtered_by_data_attribute('group_id') - ).set_precondition( - precondition_blk: Proc.new { |data| data[:group_id].present? } - ) + previous_query_blk: filtered_by_data_attribute("group_id"), + ).set_precondition(precondition_blk: Proc.new { |data| data[:group_id].present? }) end def filtered_by_data_attribute(attribute_name) diff --git a/app/services/notifications/delete_previous_notifications.rb b/app/services/notifications/delete_previous_notifications.rb index 955af73a01..d6f561c1b6 100644 --- a/app/services/notifications/delete_previous_notifications.rb +++ b/app/services/notifications/delete_previous_notifications.rb @@ -35,9 +35,7 @@ module Notifications return unless can_consolidate_data?(notification) notifications = user_notifications(notification, type) - if previous_query_blk.present? - notifications = previous_query_blk.call(notifications, data) - end + notifications = previous_query_blk.call(notifications, data) if previous_query_blk.present? notification.data = data.to_json diff --git a/app/services/post_action_notifier.rb b/app/services/post_action_notifier.rb index 71bf1e5d80..c90fa52034 100644 --- a/app/services/post_action_notifier.rb +++ b/app/services/post_action_notifier.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class PostActionNotifier - def self.disable @disabled = true end @@ -22,11 +21,14 @@ class PostActionNotifier def self.refresh_like_notification(post, read) return unless post && post.user_id && post.topic - usernames = post.post_actions.where(post_action_type_id: PostActionType.types[:like]) - .joins(:user) - .order('post_actions.created_at desc') - .where('post_actions.created_at > ?', 1.day.ago) - .pluck(:username) + usernames = + post + .post_actions + .where(post_action_type_id: PostActionType.types[:like]) + .joins(:user) + .order("post_actions.created_at desc") + .where("post_actions.created_at > ?", 1.day.ago) + .pluck(:username) if usernames.length > 0 data = { @@ -34,7 +36,7 @@ class PostActionNotifier username: usernames[0], display_username: usernames[0], username2: usernames[1], - count: usernames.length + count: usernames.length, } Notification.create( notification_type: Notification.types[:liked], @@ -42,7 +44,7 @@ class PostActionNotifier post_number: post.post_number, user_id: post.user_id, read: read, - data: data.to_json + data: data.to_json, ) end end @@ -54,18 +56,19 @@ class PostActionNotifier return if post_action.deleted_at.blank? if post_action.post_action_type_id == PostActionType.types[:like] && post_action.post - read = true - Notification.where( - topic_id: post_action.post.topic_id, - user_id: post_action.post.user_id, - post_number: post_action.post.post_number, - notification_type: Notification.types[:liked] - ).each do |notification| - read = false unless notification.read - notification.destroy - end + Notification + .where( + topic_id: post_action.post.topic_id, + user_id: post_action.post.user_id, + post_number: post_action.post.post_number, + notification_type: Notification.types[:liked], + ) + .each do |notification| + read = false unless notification.read + notification.destroy + end refresh_like_notification(post_action.post, read) else @@ -89,7 +92,7 @@ class PostActionNotifier post, display_username: post_action.user.username, post_action_id: post_action.id, - user_id: post_action.user_id + user_id: post_action.user_id, ) end @@ -106,19 +109,18 @@ class PostActionNotifier user_ids = [] - if post_revision.user_id != post.user_id - user_ids << post.user_id - end + user_ids << post.user_id if post_revision.user_id != post.user_id # Notify all users watching the topic when the OP of a wiki topic is edited # or if the topic category allows unlimited owner edits on the OP. if post.is_first_post? && - (post.wiki? || post.topic.category_allows_unlimited_owner_edits_on_first_post?) + (post.wiki? || post.topic.category_allows_unlimited_owner_edits_on_first_post?) user_ids.concat( - TopicUser.watching(post.topic_id) + TopicUser + .watching(post.topic_id) .where.not(user_id: post_revision.user_id) .where(topic: post.topic) - .pluck(:user_id) + .pluck(:user_id), ) end @@ -128,10 +130,7 @@ class PostActionNotifier if user_ids.present? DB.after_commit do - Jobs.enqueue(:notify_post_revision, - user_ids: user_ids, - post_revision_id: post_revision.id - ) + Jobs.enqueue(:notify_post_revision, user_ids: user_ids, post_revision_id: post_revision.id) end end end @@ -145,7 +144,7 @@ class PostActionNotifier Notification.types[:edited], post, display_username: post.last_editor.username, - acting_user_id: post.last_editor.id + acting_user_id: post.last_editor.id, ) end end @@ -162,8 +161,13 @@ class PostActionNotifier def self.notification_is_disabled?(post_revision) modifications = post_revision.modifications - (SiteSetting.disable_system_edit_notifications && post_revision.user_id == Discourse::SYSTEM_USER_ID) || - (SiteSetting.disable_category_edit_notifications && modifications&.dig("category_id").present?) || - (SiteSetting.disable_tags_edit_notifications && modifications&.dig("tags").present?) + ( + SiteSetting.disable_system_edit_notifications && + post_revision.user_id == Discourse::SYSTEM_USER_ID + ) || + ( + SiteSetting.disable_category_edit_notifications && + modifications&.dig("category_id").present? + ) || (SiteSetting.disable_tags_edit_notifications && modifications&.dig("tags").present?) end end diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index c20ed382f0..8b8de6aa82 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -18,13 +18,14 @@ class PostAlerter if post_url = post.url payload = { - notification_type: notification_type, - post_number: post.post_number, - topic_title: post.topic.title, - topic_id: post.topic.id, - excerpt: excerpt || post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true), - username: username || post.username, - post_url: post_url + notification_type: notification_type, + post_number: post.post_number, + topic_title: post.topic.title, + topic_id: post.topic.id, + excerpt: + excerpt || post.excerpt(400, text_entities: true, strip_links: true, remap_emoji: true), + username: username || post.username, + post_url: post_url, } DiscourseEvent.trigger(:pre_notification_alert, user, payload) @@ -51,22 +52,25 @@ class PostAlerter SiteSetting.push_notification_time_window_mins.minutes, :send_push_notification, user_id: user.id, - payload: payload + payload: payload, ) else Jobs.enqueue(:send_push_notification, user_id: user.id, payload: payload) end end - if SiteSetting.allow_user_api_key_scopes.split("|").include?("push") && SiteSetting.allowed_user_api_push_urls.present? - clients = user.user_api_keys - .joins(:scopes) - .where("user_api_key_scopes.name IN ('push', 'notifications')") - .where("push_url IS NOT NULL AND push_url <> ''") - .where("position(push_url IN ?) > 0", SiteSetting.allowed_user_api_push_urls) - .where("revoked_at IS NULL") - .order(client_id: :asc) - .pluck(:client_id, :push_url) + if SiteSetting.allow_user_api_key_scopes.split("|").include?("push") && + SiteSetting.allowed_user_api_push_urls.present? + clients = + user + .user_api_keys + .joins(:scopes) + .where("user_api_key_scopes.name IN ('push', 'notifications')") + .where("push_url IS NOT NULL AND push_url <> ''") + .where("position(push_url IN ?) > 0", SiteSetting.allowed_user_api_push_urls) + .where("revoked_at IS NULL") + .order(client_id: :asc) + .pluck(:client_id, :push_url) if clients.length > 0 Jobs.enqueue(:push_notification, clients: clients, payload: payload, user_id: user.id) @@ -79,9 +83,7 @@ class PostAlerter end def not_allowed?(user, post) - user.blank? || - user.bot? || - user.id == post.user_id + user.blank? || user.bot? || user.id == post.user_id end def all_allowed_users(post) @@ -130,7 +132,11 @@ class PostAlerter if post.last_editor_id != post.user_id # Mention comes from an edit by someone else, so notification should say who added the mention. - mentioned_opts = { user_id: editor.id, original_username: editor.username, display_username: editor.username } + mentioned_opts = { + user_id: editor.id, + original_username: editor.username, + display_username: editor.username, + } end if mentioned_users @@ -141,7 +147,8 @@ class PostAlerter expand_group_mentions(mentioned_groups, post) do |group, users| users = only_allowed_users(users, post) - notified += notify_users(users - notified, :group_mentioned, post, mentioned_opts.merge(group: group)) + notified += + notify_users(users - notified, :group_mentioned, post, mentioned_opts.merge(group: group)) end if mentioned_here @@ -162,7 +169,8 @@ class PostAlerter end topic_author = post.topic.user - if topic_author && !notified.include?(topic_author) && user_watching_topic?(topic_author, post.topic) + if topic_author && !notified.include?(topic_author) && + user_watching_topic?(topic_author, post.topic) notified += notify_non_pm_users(topic_author, :replied, post) end end @@ -187,8 +195,22 @@ class PostAlerter notified += notify_pm_users(post, reply_to_user, quoted_users, notified) elsif notify_about_reply?(post) # posts - notified += notify_post_users(post, notified, new_record: new_record, include_category_watchers: false, include_tag_watchers: false) - notified += notify_post_users(post, notified, new_record: new_record, include_topic_watchers: false, notification_type: :watching_category_or_tag) + notified += + notify_post_users( + post, + notified, + new_record: new_record, + include_category_watchers: false, + include_tag_watchers: false, + ) + notified += + notify_post_users( + post, + notified, + new_record: new_record, + include_topic_watchers: false, + notification_type: :watching_category_or_tag, + ) end end @@ -213,7 +235,7 @@ class PostAlerter def group_watchers(topic) GroupUser.where( group_id: topic.allowed_groups.pluck(:group_id), - notification_level: GroupUser.notification_levels[:watching_first_post] + notification_level: GroupUser.notification_levels[:watching_first_post], ).pluck(:user_id) end @@ -221,11 +243,13 @@ class PostAlerter topic .tag_users .notification_level_visible([TagUser.notification_levels[:watching_first_post]]) - .distinct(:user_id).pluck(:user_id) + .distinct(:user_id) + .pluck(:user_id) end def category_watchers(topic) - topic.category_users + topic + .category_users .where(notification_level: CategoryUser.notification_levels[:watching_first_post]) .pluck(:user_id) end @@ -259,23 +283,23 @@ class PostAlerter # running concurrently GroupMention.insert_all( mentioned_groups.map do |group| - { - post_id: post.id, - group_id: group.id, - created_at: now, - updated_at: now, - } - end + { post_id: post.id, group_id: group.id, created_at: now, updated_at: now } + end, ) end def unread_posts(user, topic) - Post.secured(Guardian.new(user)) - .where('post_number > COALESCE(( + Post + .secured(Guardian.new(user)) + .where( + "post_number > COALESCE(( SELECT last_read_post_number FROM topic_users tu - WHERE tu.user_id = ? AND tu.topic_id = ? ),0)', - user.id, topic.id) - .where('reply_to_user_id = :user_id + WHERE tu.user_id = ? AND tu.topic_id = ? ),0)", + user.id, + topic.id, + ) + .where( + "reply_to_user_id = :user_id OR exists(SELECT 1 from topic_users tu WHERE tu.user_id = :user_id AND tu.topic_id = :topic_id AND @@ -287,19 +311,19 @@ class PostAlerter OR exists(SELECT 1 from tag_users tu WHERE tu.user_id = :user_id AND tu.tag_id IN (SELECT tag_id FROM topic_tags WHERE topic_id = :topic_id) AND - notification_level = :tag_level)', + notification_level = :tag_level)", user_id: user.id, topic_id: topic.id, category_id: topic.category_id, topic_level: TopicUser.notification_levels[:watching], category_level: CategoryUser.notification_levels[:watching], - tag_level: TagUser.notification_levels[:watching] + tag_level: TagUser.notification_levels[:watching], ) .where(topic_id: topic.id) end def first_unread_post(user, topic) - unread_posts(user, topic).order('post_number').first + unread_posts(user, topic).order("post_number").first end def unread_count(user, topic) @@ -311,28 +335,26 @@ class PostAlerter return unless Guardian.new(user).can_see?(topic) User.transaction do - user.notifications.where( - notification_type: types, - topic_id: topic.id - ).destroy_all + user.notifications.where(notification_type: types, topic_id: topic.id).destroy_all # Reload so notification counts sync up correctly user.reload end end - NOTIFIABLE_TYPES = [ - :mentioned, - :replied, - :quoted, - :posted, - :linked, - :private_message, - :group_mentioned, - :watching_first_post, - :event_reminder, - :event_invitation - ].map { |t| Notification.types[t] } + NOTIFIABLE_TYPES = + %i[ + mentioned + replied + quoted + posted + linked + private_message + group_mentioned + watching_first_post + event_reminder + event_invitation + ].map { |t| Notification.types[t] } def group_stats(topic) sql = <<~SQL @@ -346,7 +368,7 @@ class PostAlerter { group_id: g.id, group_name: g.name, - inbox_count: DB.query_single(sql, group_id: g.id).first.to_i + inbox_count: DB.query_single(sql, group_id: g.id).first.to_i, } end end @@ -356,10 +378,8 @@ class PostAlerter stats = (@group_stats[topic.id] ||= group_stats(topic)) return unless stats - group_id = topic - .topic_allowed_groups - .where(group_id: user.groups.pluck(:id)) - .pluck_first(:group_id) + group_id = + topic.topic_allowed_groups.where(group_id: user.groups.pluck(:id)).pluck_first(:group_id) stat = stats.find { |s| s[:group_id] == group_id } return unless stat @@ -374,11 +394,12 @@ class PostAlerter group_id: stat[:group_id], group_name: stat[:group_name], inbox_count: stat[:inbox_count], - username: user.username_lower - }.to_json + username: user.username_lower, + }.to_json, ) else - Notification.where(user_id: user.id, notification_type: Notification.types[:group_message_summary]) + Notification + .where(user_id: user.id, notification_type: Notification.types[:group_message_summary]) .where("data::json ->> 'group_id' = ?", stat[:group_id].to_s) .delete_all end @@ -389,20 +410,31 @@ class PostAlerter def should_notify_edit?(notification, post, opts) notification.created_at < 1.day.ago || - notification.data_hash["display_username"] != (opts[:display_username].presence || post.user.username) + notification.data_hash["display_username"] != + (opts[:display_username].presence || post.user.username) end def should_notify_like?(user, notification) - return true if user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:always] - return true if user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:first_time_and_daily] && notification.created_at < 1.day.ago + if user.user_option.like_notification_frequency == + UserOption.like_notification_frequency_type[:always] + return true + end + if user.user_option.like_notification_frequency == + UserOption.like_notification_frequency_type[:first_time_and_daily] && + notification.created_at < 1.day.ago + return true + end false end def should_notify_previous?(user, post, notification, opts) case notification.notification_type - when Notification.types[:edited] then should_notify_edit?(notification, post, opts) - when Notification.types[:liked] then should_notify_like?(user, notification) - else false + when Notification.types[:edited] + should_notify_edit?(notification, post, opts) + when Notification.types[:liked] + should_notify_like?(user, notification) + else + false end end @@ -422,7 +454,11 @@ class PostAlerter return if (topic = post.topic).blank? is_liked = type == Notification.types[:liked] - return if is_liked && user.user_option.like_notification_frequency == UserOption.like_notification_frequency_type[:never] + if is_liked && + user.user_option.like_notification_frequency == + UserOption.like_notification_frequency_type[:never] + return + end # Make sure the user can see the post return unless Guardian.new(user).can_see?(post) @@ -430,48 +466,60 @@ class PostAlerter return if user.staged? && topic.category&.mailinglist_mirror? notifier_id = opts[:user_id] || post.user_id # xxxxx look at revision history - return if notifier_id && UserCommScreener.new( - acting_user_id: notifier_id, target_user_ids: user.id - ).ignoring_or_muting_actor?(user.id) + if notifier_id && + UserCommScreener.new( + acting_user_id: notifier_id, + target_user_ids: user.id, + ).ignoring_or_muting_actor?(user.id) + return + end # skip if muted on the topic - return if TopicUser.where( - topic: topic, - user: user, - notification_level: TopicUser.notification_levels[:muted] - ).exists? + if TopicUser.where( + topic: topic, + user: user, + notification_level: TopicUser.notification_levels[:muted], + ).exists? + return + end # skip if muted on the group if group = opts[:group] - return if GroupUser.where( - group_id: opts[:group_id], - user_id: user.id, - notification_level: TopicUser.notification_levels[:muted] - ).exists? + if GroupUser.where( + group_id: opts[:group_id], + user_id: user.id, + notification_level: TopicUser.notification_levels[:muted], + ).exists? + return + end end - existing_notifications = user.notifications - .order("notifications.id DESC") - .where( - topic_id: post.topic_id, - post_number: post.post_number - ).limit(10) + existing_notifications = + user + .notifications + .order("notifications.id DESC") + .where(topic_id: post.topic_id, post_number: post.post_number) + .limit(10) # Don't notify the same user about the same type of notification on the same post - existing_notification_of_same_type = existing_notifications.find { |n| n.notification_type == type } + existing_notification_of_same_type = + existing_notifications.find { |n| n.notification_type == type } - if existing_notification_of_same_type && !should_notify_previous?(user, post, existing_notification_of_same_type, opts) + if existing_notification_of_same_type && + !should_notify_previous?(user, post, existing_notification_of_same_type, opts) return end # linked, quoted, mentioned, chat_quoted may be suppressed if you already have a reply notification if [ - Notification.types[:quoted], - Notification.types[:linked], - Notification.types[:mentioned], - Notification.types[:chat_quoted] - ].include?(type) - return if existing_notifications.find { |n| n.notification_type == Notification.types[:replied] } + Notification.types[:quoted], + Notification.types[:linked], + Notification.types[:mentioned], + Notification.types[:chat_quoted], + ].include?(type) + if existing_notifications.find { |n| n.notification_type == Notification.types[:replied] } + return + end end collapsed = false @@ -489,7 +537,7 @@ class PostAlerter count = unread_count(user, topic) if count > 1 I18n.with_locale(user.effective_locale) do - opts[:display_username] = I18n.t('embed.replies', count: count) + opts[:display_username] = I18n.t("embed.replies", count: count) end end end @@ -513,9 +561,7 @@ class PostAlerter display_username: opts[:display_username] || post.user.username, } - if opts[:custom_data]&.is_a?(Hash) - opts[:custom_data].each { |k, v| notification_data[k] = v } - end + opts[:custom_data].each { |k, v| notification_data[k] = v } if opts[:custom_data]&.is_a?(Hash) if group = opts[:group] notification_data[:group_id] = group.id @@ -527,23 +573,29 @@ class PostAlerter elsif original_post.via_email && (incoming_email = original_post.incoming_email) skip_send_email = incoming_email.to_addresses_split.include?(user.email) || - incoming_email.cc_addresses_split.include?(user.email) + incoming_email.cc_addresses_split.include?(user.email) else skip_send_email = opts[:skip_send_email] end # Create the notification - created = user.notifications.consolidate_or_create!( - notification_type: type, - topic_id: post.topic_id, - post_number: post.post_number, - post_action_id: opts[:post_action_id], - data: notification_data.to_json, - skip_send_email: skip_send_email - ) + created = + user.notifications.consolidate_or_create!( + notification_type: type, + topic_id: post.topic_id, + post_number: post.post_number, + post_action_id: opts[:post_action_id], + data: notification_data.to_json, + skip_send_email: skip_send_email, + ) if created.id && existing_notifications.empty? && NOTIFIABLE_TYPES.include?(type) - create_notification_alert(user: user, post: original_post, notification_type: type, username: original_username) + create_notification_alert( + user: user, + post: original_post, + notification_type: type, + username: original_username, + ) end created.id ? created : nil @@ -555,7 +607,7 @@ class PostAlerter post: post, notification_type: notification_type, excerpt: excerpt, - username: username + username: username, ) end @@ -566,11 +618,13 @@ class PostAlerter def expand_group_mentions(groups, post) return unless post.user && groups - Group.mentionable(post.user, include_public: false).where(id: groups.map(&:id)).each do |group| - next if group.user_count >= SiteSetting.max_users_notified_per_group_mention - yield group, group.users - end - + Group + .mentionable(post.user, include_public: false) + .where(id: groups.map(&:id)) + .each do |group| + next if group.user_count >= SiteSetting.max_users_notified_per_group_mention + yield group, group.users + end end def expand_here_mention(post, exclude_ids: nil) @@ -583,9 +637,7 @@ class PostAlerter posts = posts.where(post_type: Post.types[:regular]) end - User.real - .where(id: posts.select(:user_id)) - .limit(SiteSetting.max_here_mentioned) + User.real.where(id: posts.select(:user_id)).limit(SiteSetting.max_here_mentioned) end # TODO: Move to post-analyzer? @@ -593,12 +645,16 @@ class PostAlerter mentions = post.raw_mentions return if mentions.blank? - groups = Group.where('LOWER(name) IN (?)', mentions) + groups = Group.where("LOWER(name) IN (?)", mentions) mentions -= groups.map(&:name).map(&:downcase) groups = nil if groups.empty? if mentions.present? - users = User.where(username_lower: mentions).includes(:do_not_disturb_timings).where.not(id: post.user_id) + users = + User + .where(username_lower: mentions) + .includes(:do_not_disturb_timings) + .where.not(id: post.user_id) users = nil if users.empty? end @@ -611,22 +667,28 @@ class PostAlerter # TODO: Move to post-analyzer? # Returns a list of users who were quoted in the post def extract_quoted_users(post) - usernames = if SiteSetting.display_name_on_posts && !SiteSetting.prioritize_username_in_ux - post.raw.scan(/username:([[:alnum:]]*)"(?=\])/) - else - post.raw.scan(/\[quote=\"([^,]+),.+\"\]/) - end.uniq.map { |q| q.first.strip.downcase } + usernames = + if SiteSetting.display_name_on_posts && !SiteSetting.prioritize_username_in_ux + post.raw.scan(/username:([[:alnum:]]*)"(?=\])/) + else + post.raw.scan(/\[quote=\"([^,]+),.+\"\]/) + end.uniq.map { |q| q.first.strip.downcase } User.where.not(id: post.user_id).where(username_lower: usernames) end def extract_linked_users(post) - users = post.topic_links.where(reflection: false).map do |link| - linked_post = link.link_post - if !linked_post && topic = link.link_topic - linked_post = topic.posts.find_by(post_number: 1) - end - (linked_post && post.user_id != linked_post.user_id && linked_post.user) || nil - end.compact + users = + post + .topic_links + .where(reflection: false) + .map do |link| + linked_post = link.link_post + if !linked_post && topic = link.link_topic + linked_post = topic.posts.find_by(post_number: 1) + end + (linked_post && post.user_id != linked_post.user_id && linked_post.user) || nil + end + .compact DiscourseEvent.trigger(:after_extract_linked_users, users, post) @@ -647,9 +709,7 @@ class PostAlerter warn_if_not_sidekiq DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) - users.each do |u| - create_notification(u, Notification.types[type], post, opts) - end + users.each { |u| create_notification(u, Notification.types[type], post, opts) } users end @@ -681,7 +741,12 @@ class PostAlerter DiscourseEvent.trigger(:before_create_notifications_for_users, users, post) users.each do |user| if reply_to_user == user || pm_watching_users(post).include?(user) || user.staged? - create_notification(user, Notification.types[:private_message], post, skip_send_email_to: emails_to_skip_send) + create_notification( + user, + Notification.types[:private_message], + post, + skip_send_email_to: emails_to_skip_send, + ) end end @@ -711,17 +776,13 @@ class PostAlerter return if !SiteSetting.enable_smtp || post.post_type != Post.types[:regular] return if post.topic.allowed_groups.none? - if post.topic.allowed_groups.count == 1 - return post.topic.first_smtp_enabled_group - end + return post.topic.first_smtp_enabled_group if post.topic.allowed_groups.count == 1 topic_incoming_email = post.topic.incoming_email.first return if topic_incoming_email.blank? group = Group.find_by_email(topic_incoming_email.to_addresses) - if !group&.smtp_enabled - return post.topic.first_smtp_enabled_group - end + return post.topic.first_smtp_enabled_group if !group&.smtp_enabled group end @@ -735,9 +796,13 @@ class PostAlerter # We need to use topic_allowed_users here instead of directly_targeted_users # because we want to make sure the to_address goes to the OP of the topic. - topic_allowed_users_by_age = post.topic.topic_allowed_users.includes(:user).order(:created_at).reject do |tau| - not_allowed?(tau.user, post) - end + topic_allowed_users_by_age = + post + .topic + .topic_allowed_users + .includes(:user) + .order(:created_at) + .reject { |tau| not_allowed?(tau.user, post) } return emails_to_skip_send if topic_allowed_users_by_age.empty? # This should usually be the OP of the topic, unless they are the one @@ -778,7 +843,7 @@ class PostAlerter group_id: group.id, post_id: post.id, email: to_address, - cc_emails: cc_addresses + cc_emails: cc_addresses, ) # Add the group's email_username into the array, because it is used for @@ -790,7 +855,16 @@ class PostAlerter emails_to_skip_send.uniq end - def notify_post_users(post, notified, group_ids: nil, include_topic_watchers: true, include_category_watchers: true, include_tag_watchers: true, new_record: false, notification_type: nil) + def notify_post_users( + post, + notified, + group_ids: nil, + include_topic_watchers: true, + include_category_watchers: true, + include_tag_watchers: true, + new_record: false, + notification_type: nil + ) return [] unless post.topic warn_if_not_sidekiq @@ -803,18 +877,15 @@ class PostAlerter /*tags*/ ) SQL - if include_topic_watchers - condition.sub! "/*topic*/", <<~SQL + condition.sub! "/*topic*/", <<~SQL if include_topic_watchers UNION SELECT user_id FROM topic_users WHERE notification_level = :watching AND topic_id = :topic_id SQL - end - if include_category_watchers - condition.sub! "/*category*/", <<~SQL + condition.sub! "/*category*/", <<~SQL if include_category_watchers UNION SELECT cu.user_id @@ -825,12 +896,10 @@ class PostAlerter AND cu.category_id = :category_id AND (tu.user_id IS NULL OR tu.notification_level = :watching) SQL - end - tag_ids = post.topic.topic_tags.pluck('topic_tags.tag_id') + tag_ids = post.topic.topic_tags.pluck("topic_tags.tag_id") - if include_tag_watchers && tag_ids.present? - condition.sub! "/*tags*/", <<~SQL + condition.sub! "/*tags*/", <<~SQL if include_tag_watchers && tag_ids.present? UNION SELECT tag_users.user_id @@ -850,36 +919,36 @@ class PostAlerter AND tag_users.tag_id IN (:tag_ids) AND (tu.user_id IS NULL OR tu.notification_level = :watching)) SQL - end - notify = User.where(condition, - watching: TopicUser.notification_levels[:watching], - topic_id: post.topic_id, - category_id: post.topic.category_id, - tag_ids: tag_ids, - staff_group_id: Group::AUTO_GROUPS[:staff], - everyone_group_id: Group::AUTO_GROUPS[:everyone] - ) + notify = + User.where( + condition, + watching: TopicUser.notification_levels[:watching], + topic_id: post.topic_id, + category_id: post.topic.category_id, + tag_ids: tag_ids, + staff_group_id: Group::AUTO_GROUPS[:staff], + everyone_group_id: Group::AUTO_GROUPS[:everyone], + ) if group_ids.present? notify = notify.joins(:group_users).where("group_users.group_id IN (?)", group_ids) end - if post.topic.private_message? - notify = notify.where(staged: false).staff - end + notify = notify.where(staged: false).staff if post.topic.private_message? exclude_user_ids = notified.map(&:id) notify = notify.where("users.id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present? DiscourseEvent.trigger(:before_create_notifications_for_users, notify, post) - already_seen_user_ids = Set.new( - TopicUser - .where(topic_id: post.topic.id) - .where("last_read_post_number >= ?", post.post_number) - .pluck(:user_id) - ) + already_seen_user_ids = + Set.new( + TopicUser + .where(topic_id: post.topic.id) + .where("last_read_post_number >= ?", post.post_number) + .pluck(:user_id), + ) each_user_in_batches(notify) do |user| calculated_type = @@ -891,7 +960,8 @@ class PostAlerter Notification.types[:posted] end opts = {} - opts[:display_username] = post.last_editor.username if calculated_type == Notification.types[:edited] + opts[:display_username] = post.last_editor.username if calculated_type == + Notification.types[:edited] create_notification(user, calculated_type, post, opts) end @@ -899,20 +969,31 @@ class PostAlerter end def warn_if_not_sidekiq - Rails.logger.warn("PostAlerter.#{caller_locations(1, 1)[0].label} was called outside of sidekiq") unless Sidekiq.server? + unless Sidekiq.server? + Rails.logger.warn( + "PostAlerter.#{caller_locations(1, 1)[0].label} was called outside of sidekiq", + ) + end end private def each_user_in_batches(users) # This is race-condition-safe, unlike #find_in_batches - users.pluck(:id).each_slice(USER_BATCH_SIZE) do |user_ids_batch| - User.where(id: user_ids_batch).includes(:do_not_disturb_timings).each { |user| yield(user) } - end + users + .pluck(:id) + .each_slice(USER_BATCH_SIZE) do |user_ids_batch| + User.where(id: user_ids_batch).includes(:do_not_disturb_timings).each { |user| yield(user) } + end end def create_pm_notification(user, post, emails_to_skip_send) - create_notification(user, Notification.types[:private_message], post, skip_send_email_to: emails_to_skip_send) + create_notification( + user, + Notification.types[:private_message], + post, + skip_send_email_to: emails_to_skip_send, + ) end def is_replying?(user, reply_to_user, quoted_users) @@ -923,7 +1004,7 @@ class PostAlerter TopicUser.exists?( user_id: user.id, topic_id: topic.id, - notification_level: TopicUser.notification_levels[:watching] + notification_level: TopicUser.notification_levels[:watching], ) end end diff --git a/app/services/post_bookmarkable.rb b/app/services/post_bookmarkable.rb index 09373f6e99..97086d9ba4 100644 --- a/app/services/post_bookmarkable.rb +++ b/app/services/post_bookmarkable.rb @@ -18,23 +18,26 @@ class PostBookmarkable < BaseBookmarkable def self.list_query(user, guardian) topics = Topic.listable_topics.secured(guardian) pms = Topic.private_messages_for_user(user) - post_bookmarks = user - .bookmarks_of_type("Post") - .joins("INNER JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'") - .joins("LEFT JOIN topics ON topics.id = posts.topic_id") - .joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id") - .where("topic_users.user_id = ?", user.id) + post_bookmarks = + user + .bookmarks_of_type("Post") + .joins( + "INNER JOIN posts ON posts.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", + ) + .joins("LEFT JOIN topics ON topics.id = posts.topic_id") + .joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id") + .where("topic_users.user_id = ?", user.id) guardian.filter_allowed_categories( - post_bookmarks.merge(topics.or(pms)).merge(Post.secured(guardian)) + post_bookmarks.merge(topics.or(pms)).merge(Post.secured(guardian)), ) end def self.search_query(bookmarks, query, ts_query, &bookmarkable_search) bookmarkable_search.call( bookmarks.joins( - "LEFT JOIN post_search_data ON post_search_data.post_id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'" + "LEFT JOIN post_search_data ON post_search_data.post_id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Post'", ), - "#{ts_query} @@ post_search_data.search_data" + "#{ts_query} @@ post_search_data.search_data", ) end @@ -45,8 +48,8 @@ class PostBookmarkable < BaseBookmarkable post_number: bookmark.bookmarkable.post_number, data: { title: bookmark.bookmarkable.topic.title, - bookmarkable_url: bookmark.bookmarkable.url - } + bookmarkable_url: bookmark.bookmarkable.url, + }, ) end @@ -59,14 +62,14 @@ class PostBookmarkable < BaseBookmarkable end def self.bookmark_metadata(bookmark, user) - { topic_bookmarked: Bookmark.for_user_in_topic(user.id, bookmark.bookmarkable.topic_id).exists? } + { + topic_bookmarked: Bookmark.for_user_in_topic(user.id, bookmark.bookmarkable.topic_id).exists?, + } end def self.validate_before_create(guardian, bookmarkable) - if bookmarkable.blank? || - bookmarkable.topic.blank? || - !guardian.can_see_topic?(bookmarkable.topic) || - !guardian.can_see_post?(bookmarkable) + if bookmarkable.blank? || bookmarkable.topic.blank? || + !guardian.can_see_topic?(bookmarkable.topic) || !guardian.can_see_post?(bookmarkable) raise Discourse::InvalidAccess end end diff --git a/app/services/post_owner_changer.rb b/app/services/post_owner_changer.rb index 702e6a8a17..7880aa95f2 100644 --- a/app/services/post_owner_changer.rb +++ b/app/services/post_owner_changer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class PostOwnerChanger - def initialize(params) @post_ids = params[:post_ids] @topic = Topic.with_deleted.find_by(id: params[:topic_id].to_i) @@ -9,7 +8,7 @@ class PostOwnerChanger @acting_user = params[:acting_user] @skip_revision = params[:skip_revision] || false - [:post_ids, :topic, :new_owner, :acting_user].each do |arg| + %i[post_ids topic new_owner acting_user].each do |arg| raise ArgumentError.new(arg) if self.instance_variable_get("@#{arg}").blank? end end @@ -28,20 +27,31 @@ class PostOwnerChanger PostActionDestroyer.destroy(@new_owner, post, :like, skip_delete_check: true) level = post.is_first_post? ? :watching : :tracking - TopicUser.change(@new_owner.id, @topic.id, notification_level: NotificationLevels.topic_levels[level], posted: true) + TopicUser.change( + @new_owner.id, + @topic.id, + notification_level: NotificationLevels.topic_levels[level], + posted: true, + ) - if post == @topic.posts.order("post_number DESC").where("NOT hidden AND posts.deleted_at IS NULL").first + if post == + @topic + .posts + .order("post_number DESC") + .where("NOT hidden AND posts.deleted_at IS NULL") + .first @topic.last_poster = @new_owner end @topic.update_statistics @new_owner.user_stat.update( - first_post_created_at: @new_owner.reload.posts.order('created_at ASC').first&.created_at + first_post_created_at: @new_owner.reload.posts.order("created_at ASC").first&.created_at, ) - Post.where(topic_id: @topic.id, reply_to_post_number: post.post_number) - .update_all(reply_to_user_id: @new_owner.id) + Post.where(topic_id: @topic.id, reply_to_post_number: post.post_number).update_all( + reply_to_user_id: @new_owner.id, + ) @topic.save!(validate: false) end diff --git a/app/services/push_notification_pusher.rb b/app/services/push_notification_pusher.rb index a0b7093544..736f3ea2bf 100644 --- a/app/services/push_notification_pusher.rb +++ b/app/services/push_notification_pusher.rb @@ -8,30 +8,36 @@ class PushNotificationPusher message = nil I18n.with_locale(user.effective_locale) do notification_icon_name = Notification.types[payload[:notification_type]] - if !File.exist?(File.expand_path("../../app/assets/images/push-notifications/#{notification_icon_name}.png", __dir__)) + if !File.exist?( + File.expand_path( + "../../app/assets/images/push-notifications/#{notification_icon_name}.png", + __dir__, + ), + ) notification_icon_name = "discourse" end - notification_icon = ActionController::Base.helpers.image_url("push-notifications/#{notification_icon_name}.png") + notification_icon = + ActionController::Base.helpers.image_url("push-notifications/#{notification_icon_name}.png") message = { - title: payload[:translated_title] || I18n.t( - "discourse_push_notifications.popup.#{Notification.types[payload[:notification_type]]}", - site_title: SiteSetting.title, - topic: payload[:topic_title], - username: payload[:username] - ), + title: + payload[:translated_title] || + I18n.t( + "discourse_push_notifications.popup.#{Notification.types[payload[:notification_type]]}", + site_title: SiteSetting.title, + topic: payload[:topic_title], + username: payload[:username], + ), body: payload[:excerpt], badge: get_badge, icon: notification_icon, tag: payload[:tag] || "#{Discourse.current_hostname}-#{payload[:topic_id]}", base_url: Discourse.base_url, url: payload[:post_url], - hide_when_active: true + hide_when_active: true, } - subscriptions(user).each do |subscription| - send_notification(user, subscription, message) - end + subscriptions(user).each { |subscription| send_notification(user, subscription, message) } end message @@ -50,21 +56,22 @@ class PushNotificationPusher subscriptions = PushSubscription.where(user: user, data: data) subscriptions_count = subscriptions.count - new_subscription = if subscriptions_count > 1 - subscriptions.destroy_all - PushSubscription.create!(user: user, data: data) - elsif subscriptions_count == 0 - PushSubscription.create!(user: user, data: data) - end + new_subscription = + if subscriptions_count > 1 + subscriptions.destroy_all + PushSubscription.create!(user: user, data: data) + elsif subscriptions_count == 0 + PushSubscription.create!(user: user, data: data) + end if send_confirmation == "true" message = { - title: I18n.t("discourse_push_notifications.popup.confirm_title", - site_title: SiteSetting.title), + title: + I18n.t("discourse_push_notifications.popup.confirm_title", site_title: SiteSetting.title), body: I18n.t("discourse_push_notifications.popup.confirm_body"), icon: ActionController::Base.helpers.image_url("push-notifications/check.png"), badge: get_badge, - tag: "#{Discourse.current_hostname}-subscription" + tag: "#{Discourse.current_hostname}-subscription", } send_notification(user, new_subscription, message) @@ -84,7 +91,7 @@ class PushNotificationPusher end MAX_ERRORS ||= 3 - MIN_ERROR_DURATION ||= 86400 # 1 day + MIN_ERROR_DURATION ||= 86_400 # 1 day def self.handle_generic_error(subscription, error, user, endpoint, message) subscription.error_count += 1 @@ -103,8 +110,8 @@ class PushNotificationPusher env: { user_id: user.id, endpoint: endpoint, - message: message.to_json - } + message: message.to_json, + }, ) end @@ -130,11 +137,11 @@ class PushNotificationPusher subject: Discourse.base_url, public_key: SiteSetting.vapid_public_key, private_key: SiteSetting.vapid_private_key, - expiration: TOKEN_VALID_FOR_SECONDS + expiration: TOKEN_VALID_FOR_SECONDS, }, open_timeout: CONNECTION_TIMEOUT_SECONDS, read_timeout: CONNECTION_TIMEOUT_SECONDS, - ssl_timeout: CONNECTION_TIMEOUT_SECONDS + ssl_timeout: CONNECTION_TIMEOUT_SECONDS, ) if subscription.first_error_at || subscription.error_count != 0 @@ -155,5 +162,4 @@ class PushNotificationPusher private_class_method :send_notification private_class_method :handle_generic_error - end diff --git a/app/services/random_topic_selector.rb b/app/services/random_topic_selector.rb index 8b8d121bef..85f3334864 100644 --- a/app/services/random_topic_selector.rb +++ b/app/services/random_topic_selector.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RandomTopicSelector - BACKFILL_SIZE = 3000 BACKFILL_LOW_WATER_MARK = 500 @@ -11,7 +10,7 @@ class RandomTopicSelector options = { per_page: category ? category.num_featured_topics : 3, visible: true, - no_definitions: true + no_definitions: true, } options[:except_topic_ids] = [category.topic_id] if exclude @@ -20,9 +19,7 @@ class RandomTopicSelector options[:category] = category.id # NOTE: at the moment this site setting scopes tightly to a category (excluding subcats) # this is done so we don't populate a junk cache - if SiteSetting.limit_suggested_to_category - options[:no_subcategories] = true - end + options[:no_subcategories] = true if SiteSetting.limit_suggested_to_category # don't leak private categories into the "everything" group options[:guardian] = Guardian.new(Discourse.system_user) @@ -30,12 +27,15 @@ class RandomTopicSelector query = TopicQuery.new(nil, options) - results = query.latest_results.order('RANDOM()') - .where(closed: false, archived: false) - .where("topics.created_at > ?", SiteSetting.suggested_topics_max_days_old.days.ago) - .limit(BACKFILL_SIZE) - .reorder('RANDOM()') - .pluck(:id) + results = + query + .latest_results + .order("RANDOM()") + .where(closed: false, archived: false) + .where("topics.created_at > ?", SiteSetting.suggested_topics_max_days_old.days.ago) + .limit(BACKFILL_SIZE) + .reorder("RANDOM()") + .pluck(:id) key = cache_key(category) @@ -56,10 +56,11 @@ class RandomTopicSelector return results if count < 1 - results = Discourse.redis.multi do |transaction| - transaction.lrange(key, 0, count - 1) - transaction.ltrim(key, count, -1) - end + results = + Discourse.redis.multi do |transaction| + transaction.lrange(key, 0, count - 1) + transaction.ltrim(key, count, -1) + end if !results.is_a?(Array) # Redis is in readonly mode results = Discourse.redis.lrange(key, 0, count - 1) @@ -81,9 +82,7 @@ class RandomTopicSelector end if !backfilled && Discourse.redis.llen(key) < BACKFILL_LOW_WATER_MARK - Scheduler::Defer.later("backfill") do - backfill(category) - end + Scheduler::Defer.later("backfill") { backfill(category) } end results @@ -96,5 +95,4 @@ class RandomTopicSelector def self.clear_cache! Discourse.redis.delete_prefixed(cache_key) end - end diff --git a/app/services/registered_bookmarkable.rb b/app/services/registered_bookmarkable.rb index fc40dee521..50cd45735a 100644 --- a/app/services/registered_bookmarkable.rb +++ b/app/services/registered_bookmarkable.rb @@ -65,12 +65,10 @@ class RegisteredBookmarkable return if bookmarks_of_type.empty? if bookmarkable_klass.has_preloads? - ActiveRecord::Associations::Preloader - .new( - records: bookmarks_of_type, - associations: [bookmarkable: bookmarkable_klass.preload_associations] - ) - .call + ActiveRecord::Associations::Preloader.new( + records: bookmarks_of_type, + associations: [bookmarkable: bookmarkable_klass.preload_associations], + ).call end bookmarkable_klass.perform_custom_preload!(bookmarks_of_type, guardian) diff --git a/app/services/search_indexer.rb b/app/services/search_indexer.rb index 33d18b41c9..a17ac7b34b 100644 --- a/app/services/search_indexer.rb +++ b/app/services/search_indexer.rb @@ -17,25 +17,17 @@ class SearchIndexer @disabled = false end - def self.update_index(table: , id: , a_weight: nil, b_weight: nil, c_weight: nil, d_weight: nil) - raw_data = { - a: a_weight, - b: b_weight, - c: c_weight, - d: d_weight, - } + def self.update_index(table:, id:, a_weight: nil, b_weight: nil, c_weight: nil, d_weight: nil) + raw_data = { a: a_weight, b: b_weight, c: c_weight, d: d_weight } # The version used in excerpts - search_data = raw_data.transform_values do |data| - Search.prepare_data(data || "", :index) - end + search_data = raw_data.transform_values { |data| Search.prepare_data(data || "", :index) } # The version used to build the index - indexed_data = search_data.transform_values do |data| - data.gsub(/\S+/) { |word| - word[0...SiteSetting.search_max_indexed_word_length] - } - end + indexed_data = + search_data.transform_values do |data| + data.gsub(/\S+/) { |word| word[0...SiteSetting.search_max_indexed_word_length] } + end table_name = "#{table}_search_data" foreign_key = "#{table}_id" @@ -53,30 +45,32 @@ class SearchIndexer tsvector = DB.query_single("SELECT #{ranked_index}", indexed_data)[0] additional_lexemes = [] - tsvector.scan(/'(([a-zA-Z0-9]+\.)+[a-zA-Z0-9]+)'\:([\w+,]+)/).reduce(additional_lexemes) do |array, (lexeme, _, positions)| - count = 0 + tsvector + .scan(/'(([a-zA-Z0-9]+\.)+[a-zA-Z0-9]+)'\:([\w+,]+)/) + .reduce(additional_lexemes) do |array, (lexeme, _, positions)| + count = 0 - if lexeme !~ /^(\d+\.)?(\d+\.)*(\*|\d+)$/ - loop do - count += 1 - break if count >= 10 # Safeguard here to prevent infinite loop when a term has many dots - term, _, remaining = lexeme.partition(".") - break if remaining.blank? - array << "'#{remaining}':#{positions}" - lexeme = remaining + if lexeme !~ /^(\d+\.)?(\d+\.)*(\*|\d+)$/ + loop do + count += 1 + break if count >= 10 # Safeguard here to prevent infinite loop when a term has many dots + term, _, remaining = lexeme.partition(".") + break if remaining.blank? + array << "'#{remaining}':#{positions}" + lexeme = remaining + end end + + array end - array - end - - tsvector = "#{tsvector} #{additional_lexemes.join(' ')}" + tsvector = "#{tsvector} #{additional_lexemes.join(" ")}" indexed_data = if table.to_s == "post" clean_post_raw_data!(search_data[:d]) else - search_data.values.select { |d| d.length > 0 }.join(' ') + search_data.values.select { |d| d.length > 0 }.join(" ") end params = { @@ -99,7 +93,9 @@ class SearchIndexer Discourse.warn_exception( e, message: "Unexpected error while indexing #{table} for search", - env: { id: id } + env: { + id: id, + }, ) end end @@ -108,16 +104,23 @@ class SearchIndexer # a bit inconsistent that we use title as A and body as B when in # the post index body is D update_index( - table: 'topic', + table: "topic", id: topic_id, a_weight: title, - b_weight: HtmlScrubber.scrub(cooked)[0...Topic::MAX_SIMILAR_BODY_LENGTH] + b_weight: HtmlScrubber.scrub(cooked)[0...Topic::MAX_SIMILAR_BODY_LENGTH], ) end - def self.update_posts_index(post_id:, topic_title:, category_name:, topic_tags:, cooked:, private_message:) + def self.update_posts_index( + post_id:, + topic_title:, + category_name:, + topic_tags:, + cooked:, + private_message: + ) update_index( - table: 'post', + table: "post", id: post_id, a_weight: topic_title, b_weight: category_name, @@ -126,36 +129,26 @@ class SearchIndexer # the original string. Since there is no way to estimate the length of # the expected tsvector, we limit the input to ~50% of the maximum # length of a tsvector (1_048_576 bytes). - d_weight: HtmlScrubber.scrub(cooked)[0..600_000] - ) do |params| - params["private_message"] = private_message - end + d_weight: HtmlScrubber.scrub(cooked)[0..600_000], + ) { |params| params["private_message"] = private_message } end def self.update_users_index(user_id, username, name, custom_fields) update_index( - table: 'user', + table: "user", id: user_id, a_weight: username, b_weight: name, - c_weight: custom_fields + c_weight: custom_fields, ) end def self.update_categories_index(category_id, name) - update_index( - table: 'category', - id: category_id, - a_weight: name - ) + update_index(table: "category", id: category_id, a_weight: name) end def self.update_tags_index(tag_id, name) - update_index( - table: 'tag', - id: tag_id, - a_weight: name.downcase - ) + update_index(table: "tag", id: tag_id, a_weight: name.downcase) end def self.queue_category_posts_reindex(category_id) @@ -213,17 +206,13 @@ class SearchIndexer tags = topic.tags.select(:id, :name).to_a if tags.present? - tag_names = (tags.map(&:name) + Tag.where(target_tag_id: tags.map(&:id)).pluck(:name)).join(' ') + tag_names = + (tags.map(&:name) + Tag.where(target_tag_id: tags.map(&:id)).pluck(:name)).join(" ") end end if Post === obj && obj.raw.present? && - ( - force || - obj.saved_change_to_cooked? || - obj.saved_change_to_topic_id? - ) - + (force || obj.saved_change_to_cooked? || obj.saved_change_to_topic_id?) if topic SearchIndexer.update_posts_index( post_id: obj.id, @@ -231,7 +220,7 @@ class SearchIndexer category_name: category_name, topic_tags: tag_names, cooked: obj.cooked, - private_message: topic.private_message? + private_message: topic.private_message?, ) SearchIndexer.update_topics_index(topic.id, topic.title, obj.cooked) if obj.is_first_post? @@ -239,10 +228,12 @@ class SearchIndexer end if User === obj && (obj.saved_change_to_username? || obj.saved_change_to_name? || force) - SearchIndexer.update_users_index(obj.id, - obj.username_lower || '', - obj.name ? obj.name.downcase : '', - obj.user_custom_fields.searchable.map(&:value).join(" ")) + SearchIndexer.update_users_index( + obj.id, + obj.username_lower || "", + obj.name ? obj.name.downcase : "", + obj.user_custom_fields.searchable.map(&:value).join(" "), + ) end if Topic === obj && (obj.saved_change_to_title? || force) @@ -254,7 +245,7 @@ class SearchIndexer category_name: category_name, topic_tags: tag_names, cooked: post.cooked, - private_message: obj.private_message? + private_message: obj.private_message?, ) SearchIndexer.update_topics_index(obj.id, obj.title, post.cooked) @@ -293,7 +284,6 @@ class SearchIndexer private_class_method :clean_post_raw_data! class HtmlScrubber < Nokogiri::XML::SAX::Document - attr_reader :scrubbed def initialize @@ -304,63 +294,55 @@ class SearchIndexer return +"" if html.blank? begin - document = Nokogiri::HTML5("
    #{html}
    ", nil, Encoding::UTF_8.to_s) + document = Nokogiri.HTML5("
    #{html}
    ", nil, Encoding::UTF_8.to_s) rescue ArgumentError return +"" end - nodes = document.css( - "div.#{CookedPostProcessor::LIGHTBOX_WRAPPER_CSS_CLASS}" - ) + nodes = document.css("div.#{CookedPostProcessor::LIGHTBOX_WRAPPER_CSS_CLASS}") if nodes.present? nodes.each do |node| node.traverse do |child_node| next if child_node == node - if %w{a img}.exclude?(child_node.name) + if %w[a img].exclude?(child_node.name) child_node.remove elsif child_node.name == "a" - ATTRIBUTES.each do |attribute| - child_node.remove_attribute(attribute) - end + ATTRIBUTES.each { |attribute| child_node.remove_attribute(attribute) } end end end end - document.css("img.emoji").each do |node| - node.remove_attribute("alt") - end + document.css("img.emoji").each { |node| node.remove_attribute("alt") } - document.css("a[href]").each do |node| - if node["href"] == node.text || MENTION_CLASSES.include?(node["class"]) - node.remove_attribute("href") - end + document + .css("a[href]") + .each do |node| + if node["href"] == node.text || MENTION_CLASSES.include?(node["class"]) + node.remove_attribute("href") + end - if node["class"] == "anchor" && node["href"].starts_with?("#") - node.remove_attribute("href") + if node["class"] == "anchor" && node["href"].starts_with?("#") + node.remove_attribute("href") + end end - end html_scrubber = new Nokogiri::HTML::SAX::Parser.new(html_scrubber).parse(document.to_html) html_scrubber.scrubbed.squish end - MENTION_CLASSES ||= %w{mention mention-group} - ATTRIBUTES ||= %w{alt title href data-youtube-title} + MENTION_CLASSES ||= %w[mention mention-group] + ATTRIBUTES ||= %w[alt title href data-youtube-title] def start_element(_name, attributes = []) attributes = Hash[*attributes.flatten] ATTRIBUTES.each do |attribute_name| if attributes[attribute_name].present? && - !( - attribute_name == "href" && - UrlHelper.is_local(attributes[attribute_name]) - ) - + !(attribute_name == "href" && UrlHelper.is_local(attributes[attribute_name])) characters(attributes[attribute_name]) end end diff --git a/app/services/sidebar_section_links_updater.rb b/app/services/sidebar_section_links_updater.rb index 98d0d76857..b3faacb4f0 100644 --- a/app/services/sidebar_section_links_updater.rb +++ b/app/services/sidebar_section_links_updater.rb @@ -3,23 +3,21 @@ class SidebarSectionLinksUpdater def self.update_category_section_links(user, category_ids:) if category_ids.blank? - delete_section_links(user: user, linkable_type: 'Category') + delete_section_links(user: user, linkable_type: "Category") else category_ids = Category.secured(Guardian.new(user)).where(id: category_ids).pluck(:id) - update_section_links(user: user, linkable_type: 'Category', new_linkable_ids: category_ids) + update_section_links(user: user, linkable_type: "Category", new_linkable_ids: category_ids) end end def self.update_tag_section_links(user, tag_names:) if tag_names.blank? - delete_section_links(user: user, linkable_type: 'Tag') + delete_section_links(user: user, linkable_type: "Tag") else - tag_ids = DiscourseTagging - .filter_visible(Tag, Guardian.new(user)) - .where(name: tag_names) - .pluck(:id) + tag_ids = + DiscourseTagging.filter_visible(Tag, Guardian.new(user)).where(name: tag_names).pluck(:id) - update_section_links(user: user, linkable_type: 'Tag', new_linkable_ids: tag_ids) + update_section_links(user: user, linkable_type: "Tag", new_linkable_ids: tag_ids) end end @@ -30,20 +28,24 @@ class SidebarSectionLinksUpdater def self.update_section_links(user:, linkable_type:, new_linkable_ids:) SidebarSectionLink.transaction do - existing_linkable_ids = SidebarSectionLink.where(user: user, linkable_type: linkable_type).pluck(:linkable_id) + existing_linkable_ids = + SidebarSectionLink.where(user: user, linkable_type: linkable_type).pluck(:linkable_id) to_delete = existing_linkable_ids - new_linkable_ids to_insert = new_linkable_ids - existing_linkable_ids - to_insert_attributes = to_insert.map do |linkable_id| - { - linkable_type: linkable_type, - linkable_id: linkable_id, - user_id: user.id - } - end + to_insert_attributes = + to_insert.map do |linkable_id| + { linkable_type: linkable_type, linkable_id: linkable_id, user_id: user.id } + end - SidebarSectionLink.where(user: user, linkable_type: linkable_type, linkable_id: to_delete).delete_all if to_delete.present? + if to_delete.present? + SidebarSectionLink.where( + user: user, + linkable_type: linkable_type, + linkable_id: to_delete, + ).delete_all + end SidebarSectionLink.insert_all(to_insert_attributes) if to_insert_attributes.present? end end diff --git a/app/services/sidebar_site_settings_backfiller.rb b/app/services/sidebar_site_settings_backfiller.rb index f24383d84b..7fe13f8162 100644 --- a/app/services/sidebar_site_settings_backfiller.rb +++ b/app/services/sidebar_site_settings_backfiller.rb @@ -14,21 +14,17 @@ class SidebarSiteSettingsBackfiller @linkable_klass, previous_ids, new_ids = case setting_name when "default_sidebar_categories" - [ - Category, - previous_value.split("|"), - new_value.split("|") - ] + [Category, previous_value.split("|"), new_value.split("|")] when "default_sidebar_tags" klass = Tag [ klass, klass.where(name: previous_value.split("|")).pluck(:id), - klass.where(name: new_value.split("|")).pluck(:id) + klass.where(name: new_value.split("|")).pluck(:id), ] else - raise 'Invalid setting_name' + raise "Invalid setting_name" end @added_ids = new_ids - previous_ids @@ -37,34 +33,43 @@ class SidebarSiteSettingsBackfiller def backfill! DistributedMutex.synchronize("backfill_sidebar_site_settings_#{@setting_name}") do - SidebarSectionLink.where(linkable_type: @linkable_klass.to_s, linkable_id: @removed_ids).delete_all + SidebarSectionLink.where( + linkable_type: @linkable_klass.to_s, + linkable_id: @removed_ids, + ).delete_all - User.real.where(staged: false).select(:id).find_in_batches do |users| - rows = [] + User + .real + .where(staged: false) + .select(:id) + .find_in_batches do |users| + rows = [] - users.each do |user| - @added_ids.each do |linkable_id| - rows << { user_id: user[:id], linkable_type: @linkable_klass.to_s, linkable_id: linkable_id } + users.each do |user| + @added_ids.each do |linkable_id| + rows << { + user_id: user[:id], + linkable_type: @linkable_klass.to_s, + linkable_id: linkable_id, + } + end end - end - SidebarSectionLink.insert_all(rows) if rows.present? - end + SidebarSectionLink.insert_all(rows) if rows.present? + end end end def number_of_users_to_backfill select_statements = [] - if @removed_ids.present? - select_statements.push(<<~SQL) + select_statements.push(<<~SQL) if @removed_ids.present? SELECT sidebar_section_links.user_id FROM sidebar_section_links WHERE sidebar_section_links.linkable_type = '#{@linkable_klass.to_s}' AND sidebar_section_links.linkable_id IN (#{@removed_ids.join(",")}) SQL - end if @added_ids.present? # Returns the ids of users that will receive the new additions by excluding the users that already have the additions @@ -84,6 +89,8 @@ class SidebarSiteSettingsBackfiller SQL end + return 0 if select_statements.blank? + DB.query_single(<<~SQL)[0] SELECT COUNT(*) diff --git a/app/services/site_settings_task.rb b/app/services/site_settings_task.rb index 356d8eccf1..35d94378fb 100644 --- a/app/services/site_settings_task.rb +++ b/app/services/site_settings_task.rb @@ -16,7 +16,7 @@ class SiteSettingsTask counts = { updated: 0, not_found: 0, errors: 0 } log = [] - site_settings = YAML::safe_load(yml) + site_settings = YAML.safe_load(yml) site_settings.each do |site_setting| key = site_setting[0] val = site_setting[1] diff --git a/app/services/spam_rule/auto_silence.rb b/app/services/spam_rule/auto_silence.rb index 81acb86cd2..76f481b963 100644 --- a/app/services/spam_rule/auto_silence.rb +++ b/app/services/spam_rule/auto_silence.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SpamRule::AutoSilence - attr_reader :group_message def initialize(user, post = nil) @@ -10,9 +9,7 @@ class SpamRule::AutoSilence end def perform - I18n.with_locale(SiteSetting.default_locale) do - silence_user if should_autosilence? - end + I18n.with_locale(SiteSetting.default_locale) { silence_user if should_autosilence? } end def self.prevent_posting?(user) @@ -36,7 +33,7 @@ class SpamRule::AutoSilence user_id: @user.id, spam_type: PostActionType.types[:spam], pending: ReviewableScore.statuses[:pending], - agreed: ReviewableScore.statuses[:agreed] + agreed: ReviewableScore.statuses[:agreed], } result = DB.query(<<~SQL, params) @@ -53,23 +50,30 @@ class SpamRule::AutoSilence end def flagged_post_ids - Post.where(user_id: @user.id) - .where('spam_count > 0 OR off_topic_count > 0 OR inappropriate_count > 0') + Post + .where(user_id: @user.id) + .where("spam_count > 0 OR off_topic_count > 0 OR inappropriate_count > 0") .pluck(:id) end def silence_user Post.transaction do - - silencer = UserSilencer.new( - @user, - Discourse.system_user, - message: :too_many_spam_flags, - post_id: @post&.id - ) + silencer = + UserSilencer.new( + @user, + Discourse.system_user, + message: :too_many_spam_flags, + post_id: @post&.id, + ) if silencer.silence && SiteSetting.notify_mods_when_user_silenced - @group_message = GroupMessage.create(Group[:moderators].name, :user_automatically_silenced, user: @user, limit_once_per: false) + @group_message = + GroupMessage.create( + Group[:moderators].name, + :user_automatically_silenced, + user: @user, + limit_once_per: false, + ) end end end diff --git a/app/services/spam_rule/flag_sockpuppets.rb b/app/services/spam_rule/flag_sockpuppets.rb index c51d67f402..2a8cd2772e 100644 --- a/app/services/spam_rule/flag_sockpuppets.rb +++ b/app/services/spam_rule/flag_sockpuppets.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class SpamRule::FlagSockpuppets - def initialize(post) @post = post end @@ -21,23 +20,20 @@ class SpamRule::FlagSockpuppets return false if @post.try(:post_number) == 1 return false if first_post.user.nil? - !first_post.user.staff? && - !@post.user.staff? && - !first_post.user.staged? && - !@post.user.staged? && - @post.user != first_post.user && - @post.user.ip_address == first_post.user.ip_address && - @post.user.new_user? && - !ScreenedIpAddress.is_allowed?(@post.user.ip_address) + !first_post.user.staff? && !@post.user.staff? && !first_post.user.staged? && + !@post.user.staged? && @post.user != first_post.user && + @post.user.ip_address == first_post.user.ip_address && @post.user.new_user? && + !ScreenedIpAddress.is_allowed?(@post.user.ip_address) end def flag_sockpuppet_users - message = I18n.t( - 'flag_reason.sockpuppet', - ip_address: @post.user.ip_address, - base_path: Discourse.base_path, - locale: SiteSetting.default_locale - ) + message = + I18n.t( + "flag_reason.sockpuppet", + ip_address: @post.user.ip_address, + base_path: Discourse.base_path, + locale: SiteSetting.default_locale, + ) flag_post(@post, message) @@ -55,5 +51,4 @@ class SpamRule::FlagSockpuppets def first_post @first_post ||= @post.topic.posts.by_post_number.first end - end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index a4203452bb..70c937e561 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -2,9 +2,8 @@ # Responsible for logging the actions of admins and moderators. class StaffActionLogger - def self.base_attrs - [:topic_id, :post_id, :context, :subject, :ip_address, :previous_value, :new_value] + %i[topic_id post_id context subject ip_address previous_value new_value] end def initialize(admin) @@ -12,20 +11,22 @@ class StaffActionLogger raise Discourse::InvalidParameters.new(:admin) unless @admin && @admin.is_a?(User) end - USER_FIELDS ||= %i{id username name created_at trust_level last_seen_at last_emailed_at} + USER_FIELDS ||= %i[id username name created_at trust_level last_seen_at last_emailed_at] def log_user_deletion(deleted_user, opts = {}) - raise Discourse::InvalidParameters.new(:deleted_user) unless deleted_user && deleted_user.is_a?(User) + unless deleted_user && deleted_user.is_a?(User) + raise Discourse::InvalidParameters.new(:deleted_user) + end - details = USER_FIELDS.map do |x| - "#{x}: #{deleted_user.public_send(x)}" - end.join("\n") + details = USER_FIELDS.map { |x| "#{x}: #{deleted_user.public_send(x)}" }.join("\n") - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:delete_user], - ip_address: deleted_user.ip_address.to_s, - details: details - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:delete_user], + ip_address: deleted_user.ip_address.to_s, + details: details, + ), + ) end def log_custom(custom_type, details = nil) @@ -46,13 +47,15 @@ class StaffActionLogger end def log_post_deletion(deleted_post, opts = {}) - raise Discourse::InvalidParameters.new(:deleted_post) unless deleted_post && deleted_post.is_a?(Post) + unless deleted_post && deleted_post.is_a?(Post) + raise Discourse::InvalidParameters.new(:deleted_post) + end topic = deleted_post.topic || Topic.with_deleted.find_by(id: deleted_post.topic_id) - username = deleted_post.user.try(:username) || I18n.t('staff_action_logs.unknown') - name = deleted_post.user.try(:name) || I18n.t('staff_action_logs.unknown') - topic_title = topic.try(:title) || I18n.t('staff_action_logs.not_found') + username = deleted_post.user.try(:username) || I18n.t("staff_action_logs.unknown") + name = deleted_post.user.try(:name) || I18n.t("staff_action_logs.unknown") + topic_title = topic.try(:title) || I18n.t("staff_action_logs.not_found") details = [ "id: #{deleted_post.id}", @@ -60,14 +63,16 @@ class StaffActionLogger "user: #{username} (#{name})", "topic: #{topic_title}", "post_number: #{deleted_post.post_number}", - "raw: #{deleted_post.raw}" + "raw: #{deleted_post.raw}", ] - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:delete_post], - post_id: deleted_post.id, - details: details.join("\n") - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:delete_post], + post_id: deleted_post.id, + details: details.join("\n"), + ), + ) end def log_topic_delete_recover(topic, action = "delete_topic", opts = {}) @@ -79,99 +84,120 @@ class StaffActionLogger "id: #{topic.id}", "created_at: #{topic.created_at}", "user: #{user}", - "title: #{topic.title}" + "title: #{topic.title}", ] if first_post = topic.ordered_posts.with_deleted.first details << "raw: #{first_post.raw}" end - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[action.to_sym], - topic_id: topic.id, - details: details.join("\n") - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[action.to_sym], + topic_id: topic.id, + details: details.join("\n"), + ), + ) end def log_trust_level_change(user, old_trust_level, new_trust_level, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User) - raise Discourse::InvalidParameters.new(:old_trust_level) unless TrustLevel.valid? old_trust_level - raise Discourse::InvalidParameters.new(:new_trust_level) unless TrustLevel.valid? new_trust_level - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_trust_level], - target_user_id: user.id, - previous_value: old_trust_level, - new_value: new_trust_level, - )) + unless TrustLevel.valid? old_trust_level + raise Discourse::InvalidParameters.new(:old_trust_level) + end + unless TrustLevel.valid? new_trust_level + raise Discourse::InvalidParameters.new(:new_trust_level) + end + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_trust_level], + target_user_id: user.id, + previous_value: old_trust_level, + new_value: new_trust_level, + ), + ) end def log_lock_trust_level(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user && user.is_a?(User) - action = UserHistory.actions[user.manual_locked_trust_level.nil? ? :unlock_trust_level : :lock_trust_level] - UserHistory.create!(params(opts).merge( - action: action, - target_user_id: user.id - )) + action = + UserHistory.actions[ + user.manual_locked_trust_level.nil? ? :unlock_trust_level : :lock_trust_level + ] + UserHistory.create!(params(opts).merge(action: action, target_user_id: user.id)) end def log_topic_published(topic, opts = {}) raise Discourse::InvalidParameters.new(:topic) unless topic && topic.is_a?(Topic) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:topic_published], - topic_id: topic.id) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:topic_published], topic_id: topic.id), ) end def log_topic_timestamps_changed(topic, new_timestamp, previous_timestamp, opts = {}) raise Discourse::InvalidParameters.new(:topic) unless topic && topic.is_a?(Topic) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:topic_timestamps_changed], - topic_id: topic.id, - new_value: new_timestamp, - previous_value: previous_timestamp) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:topic_timestamps_changed], + topic_id: topic.id, + new_value: new_timestamp, + previous_value: previous_timestamp, + ), ) end def log_post_lock(post, opts = {}) raise Discourse::InvalidParameters.new(:post) unless post && post.is_a?(Post) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[opts[:locked] ? :post_locked : :post_unlocked], - post_id: post.id) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[opts[:locked] ? :post_locked : :post_unlocked], + post_id: post.id, + ), ) end def log_post_edit(post, opts = {}) raise Discourse::InvalidParameters.new(:post) unless post && post.is_a?(Post) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:post_edit], - post_id: post.id, - details: "#{opts[:old_raw]}\n\n---\n\n#{post.raw}" - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:post_edit], + post_id: post.id, + details: "#{opts[:old_raw]}\n\n---\n\n#{post.raw}", + ), + ) end def log_topic_closed(topic, opts = {}) raise Discourse::InvalidParameters.new(:topic) unless topic && topic.is_a?(Topic) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[opts[:closed] ? :topic_closed : :topic_opened], - topic_id: topic.id - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[opts[:closed] ? :topic_closed : :topic_opened], + topic_id: topic.id, + ), + ) end def log_topic_archived(topic, opts = {}) raise Discourse::InvalidParameters.new(:topic) unless topic && topic.is_a?(Topic) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[opts[:archived] ? :topic_archived : :topic_unarchived], - topic_id: topic.id - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[opts[:archived] ? :topic_archived : :topic_unarchived], + topic_id: topic.id, + ), + ) end def log_post_staff_note(post, opts = {}) raise Discourse::InvalidParameters.new(:post) unless post && post.is_a?(Post) - args = params(opts).merge( - action: UserHistory.actions[opts[:new_value].present? ? :post_staff_note_create : :post_staff_note_destroy], - post_id: post.id - ) + args = + params(opts).merge( + action: + UserHistory.actions[ + opts[:new_value].present? ? :post_staff_note_create : :post_staff_note_destroy + ], + post_id: post.id, + ) args[:new_value] = opts[:new_value] if opts[:new_value].present? args[:previous_value] = opts[:old_value] if opts[:old_value].present? @@ -179,13 +205,17 @@ class StaffActionLogger end def log_site_setting_change(setting_name, previous_value, new_value, opts = {}) - raise Discourse::InvalidParameters.new(:setting_name) unless setting_name.present? && SiteSetting.respond_to?(setting_name) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_site_setting], - subject: setting_name, - previous_value: previous_value&.to_s, - new_value: new_value&.to_s - )) + unless setting_name.present? && SiteSetting.respond_to?(setting_name) + raise Discourse::InvalidParameters.new(:setting_name) + end + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_site_setting], + subject: setting_name, + previous_value: previous_value&.to_s, + new_value: new_value&.to_s, + ), + ) end def theme_json(theme) @@ -193,7 +223,7 @@ class StaffActionLogger end def strip_duplicates(old, cur) - return [old, cur] unless old && cur + return old, cur unless old && cur old = JSON.parse(old) cur = JSON.parse(cur) @@ -217,79 +247,97 @@ class StaffActionLogger old_json, new_json = strip_duplicates(old_json, new_json) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_theme], - subject: new_theme.name, - previous_value: old_json, - new_value: new_json - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_theme], + subject: new_theme.name, + previous_value: old_json, + new_value: new_json, + ), + ) end def log_theme_destroy(theme, opts = {}) raise Discourse::InvalidParameters.new(:theme) unless theme - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:delete_theme], - subject: theme.name, - previous_value: theme_json(theme) - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:delete_theme], + subject: theme.name, + previous_value: theme_json(theme), + ), + ) end def log_theme_component_disabled(component) - UserHistory.create!(params.merge( - action: UserHistory.actions[:disable_theme_component], - subject: component.name, - context: component.id - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:disable_theme_component], + subject: component.name, + context: component.id, + ), + ) end def log_theme_component_enabled(component) - UserHistory.create!(params.merge( - action: UserHistory.actions[:enable_theme_component], - subject: component.name, - context: component.id - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:enable_theme_component], + subject: component.name, + context: component.id, + ), + ) end def log_theme_setting_change(setting_name, previous_value, new_value, theme, opts = {}) raise Discourse::InvalidParameters.new(:theme) unless theme - raise Discourse::InvalidParameters.new(:setting_name) unless theme.cached_settings.has_key?(setting_name) + unless theme.cached_settings.has_key?(setting_name) + raise Discourse::InvalidParameters.new(:setting_name) + end - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_theme_setting], - subject: "#{theme.name}: #{setting_name.to_s}", - previous_value: previous_value, - new_value: new_value - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_theme_setting], + subject: "#{theme.name}: #{setting_name.to_s}", + previous_value: previous_value, + new_value: new_value, + ), + ) end def log_site_text_change(subject, new_text = nil, old_text = nil, opts = {}) raise Discourse::InvalidParameters.new(:subject) unless subject.present? - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_site_text], - subject: subject, - previous_value: old_text, - new_value: new_text - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_site_text], + subject: subject, + previous_value: old_text, + new_value: new_text, + ), + ) end def log_username_change(user, old_username, new_username, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_username], - target_user_id: user.id, - previous_value: old_username, - new_value: new_username - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_username], + target_user_id: user.id, + previous_value: old_username, + new_value: new_username, + ), + ) end def log_name_change(user_id, old_name, new_name, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user_id - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_name], - target_user_id: user_id, - previous_value: old_name, - new_value: new_name - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_name], + target_user_id: user_id, + previous_value: old_name, + new_value: new_name, + ), + ) end def log_user_suspend(user, reason, opts = {}) @@ -297,161 +345,201 @@ class StaffActionLogger details = StaffMessageFormat.new(:suspend, reason, opts[:message]).format - args = params(opts).merge( - action: UserHistory.actions[:suspend_user], - target_user_id: user.id, - details: details - ) + args = + params(opts).merge( + action: UserHistory.actions[:suspend_user], + target_user_id: user.id, + details: details, + ) args[:post_id] = opts[:post_id] if opts[:post_id] UserHistory.create!(args) end def log_user_unsuspend(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:unsuspend_user], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:unsuspend_user], target_user_id: user.id), + ) end def log_user_merge(user, source_username, source_email, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:merge_user], - target_user_id: user.id, - context: I18n.t("staff_action_logs.user_merged", username: source_username), - email: source_email - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:merge_user], + target_user_id: user.id, + context: I18n.t("staff_action_logs.user_merged", username: source_username), + email: source_email, + ), + ) end - BADGE_FIELDS ||= %i{id name description long_description icon image_upload_id badge_type_id - badge_grouping_id query allow_title multiple_grant listable target_posts - enabled auto_revoke show_posts system} + BADGE_FIELDS ||= %i[ + id + name + description + long_description + icon + image_upload_id + badge_type_id + badge_grouping_id + query + allow_title + multiple_grant + listable + target_posts + enabled + auto_revoke + show_posts + system + ] def log_badge_creation(badge) raise Discourse::InvalidParameters.new(:badge) unless badge - details = BADGE_FIELDS.map do |f| - [f, badge.public_send(f)] - end.select { |f, v| v.present? }.map { |f, v| "#{f}: #{v}" } + details = + BADGE_FIELDS + .map { |f| [f, badge.public_send(f)] } + .select { |f, v| v.present? } + .map { |f, v| "#{f}: #{v}" } - UserHistory.create!(params.merge( - action: UserHistory.actions[:create_badge], - details: details.join("\n") - )) + UserHistory.create!( + params.merge(action: UserHistory.actions[:create_badge], details: details.join("\n")), + ) end def log_badge_change(badge) raise Discourse::InvalidParameters.new(:badge) unless badge details = ["id: #{badge.id}"] - badge.previous_changes.each { |f, values| details << "#{f}: #{values[1]}" if BADGE_FIELDS.include?(f.to_sym) } - UserHistory.create!(params.merge( - action: UserHistory.actions[:change_badge], - details: details.join("\n") - )) + badge.previous_changes.each do |f, values| + details << "#{f}: #{values[1]}" if BADGE_FIELDS.include?(f.to_sym) + end + UserHistory.create!( + params.merge(action: UserHistory.actions[:change_badge], details: details.join("\n")), + ) end def log_badge_deletion(badge) raise Discourse::InvalidParameters.new(:badge) unless badge - details = BADGE_FIELDS.map do |f| - [f, badge.public_send(f)] - end.select { |f, v| v.present? }.map { |f, v| "#{f}: #{v}" } + details = + BADGE_FIELDS + .map { |f| [f, badge.public_send(f)] } + .select { |f, v| v.present? } + .map { |f, v| "#{f}: #{v}" } - UserHistory.create!(params.merge( - action: UserHistory.actions[:delete_badge], - details: details.join("\n") - )) + UserHistory.create!( + params.merge(action: UserHistory.actions[:delete_badge], details: details.join("\n")), + ) end def log_badge_grant(user_badge, opts = {}) raise Discourse::InvalidParameters.new(:user_badge) unless user_badge - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:grant_badge], - target_user_id: user_badge.user_id, - details: user_badge.badge.name - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:grant_badge], + target_user_id: user_badge.user_id, + details: user_badge.badge.name, + ), + ) end def log_badge_revoke(user_badge, opts = {}) raise Discourse::InvalidParameters.new(:user_badge) unless user_badge - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:revoke_badge], - target_user_id: user_badge.user_id, - details: user_badge.badge.name - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:revoke_badge], + target_user_id: user_badge.user_id, + details: user_badge.badge.name, + ), + ) end def log_title_revoke(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:revoke_title], - target_user_id: user.id, - details: opts[:revoke_reason], - previous_value: opts[:previous_value] - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:revoke_title], + target_user_id: user.id, + details: opts[:revoke_reason], + previous_value: opts[:previous_value], + ), + ) end def log_title_change(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:change_title], - target_user_id: user.id, - details: opts[:details], - new_value: opts[:new_value], - previous_value: opts[:previous_value] - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:change_title], + target_user_id: user.id, + details: opts[:details], + new_value: opts[:new_value], + previous_value: opts[:previous_value], + ), + ) end def log_change_upload_secure_status(opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:override_upload_secure_status], - details: [ - "upload_id: #{opts[:upload_id]}", - "reason: #{I18n.t("uploads.marked_insecure_from_theme_component_reason")}" - ].join("\n"), - new_value: opts[:new_value] - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:override_upload_secure_status], + details: [ + "upload_id: #{opts[:upload_id]}", + "reason: #{I18n.t("uploads.marked_insecure_from_theme_component_reason")}", + ].join("\n"), + new_value: opts[:new_value], + ), + ) end def log_check_email(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:check_email], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:check_email], target_user_id: user.id), + ) end def log_show_emails(users, opts = {}) return if users.blank? - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:check_email], - details: users.map { |u| "[#{u.id}] #{u.username}" }.join("\n") - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:check_email], + details: users.map { |u| "[#{u.id}] #{u.username}" }.join("\n"), + ), + ) end def log_impersonate(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:impersonate], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:impersonate], target_user_id: user.id), + ) end def log_roll_up(subnet, ips, opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:roll_up], - details: "#{subnet} from #{ips.join(", ")}" - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:roll_up], + details: "#{subnet} from #{ips.join(", ")}", + ), + ) end - def log_category_settings_change(category, category_params, old_permissions: nil, old_custom_fields: nil) + def log_category_settings_change( + category, + category_params, + old_permissions: nil, + old_custom_fields: nil + ) validate_category(category) changed_attributes = category.previous_changes.slice(*category_params.keys) if !old_permissions.empty? && (old_permissions != category_params[:permissions]) - changed_attributes.merge!(permissions: [old_permissions.to_json, category_params[:permissions].to_json]) + changed_attributes.merge!( + permissions: [old_permissions.to_json, category_params[:permissions].to_json], + ) end if old_custom_fields && category_params[:custom_fields] @@ -462,14 +550,16 @@ class StaffActionLogger end changed_attributes.each do |key, value| - UserHistory.create!(params.merge( - action: UserHistory.actions[:change_category_settings], - category_id: category.id, - context: category.url, - subject: key, - previous_value: value[0], - new_value: value[1] - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:change_category_settings], + category_id: category.id, + context: category.url, + subject: key, + previous_value: value[0], + new_value: value[1], + ), + ) end end @@ -479,45 +569,47 @@ class StaffActionLogger details = [ "created_at: #{category.created_at}", "name: #{category.name}", - "permissions: #{category.permissions_params}" + "permissions: #{category.permissions_params}", ] if parent_category = category.parent_category details << "parent_category: #{parent_category.name}" end - UserHistory.create!(params.merge( - action: UserHistory.actions[:delete_category], - category_id: category.id, - details: details.join("\n"), - context: category.url - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:delete_category], + category_id: category.id, + details: details.join("\n"), + context: category.url, + ), + ) end def log_category_creation(category) validate_category(category) - details = [ - "created_at: #{category.created_at}", - "name: #{category.name}" - ] + details = ["created_at: #{category.created_at}", "name: #{category.name}"] - UserHistory.create!(params.merge( - action: UserHistory.actions[:create_category], - details: details.join("\n"), - category_id: category.id, - context: category.url - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:create_category], + details: details.join("\n"), + category_id: category.id, + context: category.url, + ), + ) end def log_silence_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - create_args = params(opts).merge( - action: UserHistory.actions[:silence_user], - target_user_id: user.id, - details: opts[:details] - ) + create_args = + params(opts).merge( + action: UserHistory.actions[:silence_user], + target_user_id: user.id, + details: opts[:details], + ) create_args[:post_id] = opts[:post_id] if opts[:post_id] UserHistory.create!(create_args) @@ -525,224 +617,235 @@ class StaffActionLogger def log_unsilence_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:unsilence_user], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:unsilence_user], target_user_id: user.id), + ) end def log_disable_second_factor_auth(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:disabled_second_factor], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:disabled_second_factor], + target_user_id: user.id, + ), + ) end def log_grant_admin(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:grant_admin], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:grant_admin], target_user_id: user.id), + ) end def log_revoke_admin(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:revoke_admin], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:revoke_admin], target_user_id: user.id), + ) end def log_grant_moderation(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:grant_moderation], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:grant_moderation], target_user_id: user.id), + ) end def log_revoke_moderation(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:revoke_moderation], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:revoke_moderation], target_user_id: user.id), + ) end def log_backup_create(opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:backup_create], - ip_address: @admin.ip_address.to_s - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:backup_create], + ip_address: @admin.ip_address.to_s, + ), + ) end def log_entity_export(entity, opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:entity_export], - ip_address: @admin.ip_address.to_s, - subject: entity - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:entity_export], + ip_address: @admin.ip_address.to_s, + subject: entity, + ), + ) end def log_backup_download(backup, opts = {}) raise Discourse::InvalidParameters.new(:backup) unless backup - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:backup_download], - ip_address: @admin.ip_address.to_s, - details: backup.filename - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:backup_download], + ip_address: @admin.ip_address.to_s, + details: backup.filename, + ), + ) end def log_backup_destroy(backup, opts = {}) raise Discourse::InvalidParameters.new(:backup) unless backup - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:backup_destroy], - ip_address: @admin.ip_address.to_s, - details: backup.filename - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:backup_destroy], + ip_address: @admin.ip_address.to_s, + details: backup.filename, + ), + ) end def log_revoke_email(user, reason, opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:revoke_email], - target_user_id: user.id, - details: reason - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:revoke_email], + target_user_id: user.id, + details: reason, + ), + ) end def log_user_approve(user, opts = {}) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:approve_user], - target_user_id: user.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:approve_user], target_user_id: user.id), + ) end def log_user_deactivate(user, reason, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:deactivate_user], - target_user_id: user.id, - details: reason - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:deactivate_user], + target_user_id: user.id, + details: reason, + ), + ) end def log_user_activate(user, reason, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:activate_user], - target_user_id: user.id, - details: reason - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:activate_user], + target_user_id: user.id, + details: reason, + ), + ) end def log_wizard_step(step, opts = {}) raise Discourse::InvalidParameters.new(:step) unless step - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:wizard_step], - context: step.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:wizard_step], context: step.id), + ) end def log_change_readonly_mode(state) - UserHistory.create!(params.merge( - action: UserHistory.actions[:change_readonly_mode], - previous_value: !state, - new_value: state - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:change_readonly_mode], + previous_value: !state, + new_value: state, + ), + ) end def log_check_personal_message(topic, opts = {}) raise Discourse::InvalidParameters.new(:topic) unless topic && topic.is_a?(Topic) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:check_personal_message], - topic_id: topic.id, - context: topic.relative_url - )) + UserHistory.create!( + params(opts).merge( + action: UserHistory.actions[:check_personal_message], + topic_id: topic.id, + context: topic.relative_url, + ), + ) end def log_post_approved(post, opts = {}) raise Discourse::InvalidParameters.new(:post) unless post.is_a?(Post) - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:post_approved], - post_id: post.id - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:post_approved], post_id: post.id), + ) end def log_post_rejected(reviewable, rejected_at, opts = {}) raise Discourse::InvalidParameters.new(:rejected_post) unless reviewable.is_a?(Reviewable) topic = reviewable.topic || Topic.with_deleted.find_by(id: reviewable.topic_id) - topic_title = topic&.title || I18n.t('staff_action_logs.not_found') - username = reviewable.created_by&.username || I18n.t('staff_action_logs.unknown') - name = reviewable.created_by&.name || I18n.t('staff_action_logs.unknown') + topic_title = topic&.title || I18n.t("staff_action_logs.not_found") + username = reviewable.created_by&.username || I18n.t("staff_action_logs.unknown") + name = reviewable.created_by&.name || I18n.t("staff_action_logs.unknown") details = [ "created_at: #{reviewable.created_at}", "rejected_at: #{rejected_at}", "user: #{username} (#{name})", "topic: #{topic_title}", - "raw: #{reviewable.payload['raw']}", + "raw: #{reviewable.payload["raw"]}", ] - UserHistory.create!(params(opts).merge( - action: UserHistory.actions[:post_rejected], - details: details.join("\n") - )) + UserHistory.create!( + params(opts).merge(action: UserHistory.actions[:post_rejected], details: details.join("\n")), + ) end def log_web_hook(web_hook, action, opts = {}) - details = [ - "webhook_id: #{web_hook.id}", - "payload_url: #{web_hook.payload_url}" - ] + details = ["webhook_id: #{web_hook.id}", "payload_url: #{web_hook.payload_url}"] old_values, new_values = get_changes(opts[:changes]) - UserHistory.create!(params(opts).merge( - action: action, - context: details.join(", "), - previous_value: old_values&.join(", "), - new_value: new_values&.join(", ") - )) + UserHistory.create!( + params(opts).merge( + action: action, + context: details.join(", "), + previous_value: old_values&.join(", "), + new_value: new_values&.join(", "), + ), + ) end def log_web_hook_deactivate(web_hook, response_http_status, opts = {}) - context = [ - "webhook_id: #{web_hook.id}", - "webhook_response_status: #{response_http_status}" - ] + context = ["webhook_id: #{web_hook.id}", "webhook_response_status: #{response_http_status}"] - UserHistory.create!(params.merge( - action: UserHistory.actions[:web_hook_deactivate], - context: context, - details: I18n.t('staff_action_logs.webhook_deactivation_reason', status: response_http_status) - )) + UserHistory.create!( + params.merge( + action: UserHistory.actions[:web_hook_deactivate], + context: context, + details: + I18n.t("staff_action_logs.webhook_deactivation_reason", status: response_http_status), + ), + ) end def log_embeddable_host(embeddable_host, action, opts = {}) old_values, new_values = get_changes(opts[:changes]) - UserHistory.create!(params(opts).merge( - action: action, - context: "host: #{embeddable_host.host}", - previous_value: old_values&.join(", "), - new_value: new_values&.join(", ") - )) + UserHistory.create!( + params(opts).merge( + action: action, + context: "host: #{embeddable_host.host}", + previous_value: old_values&.join(", "), + new_value: new_values&.join(", "), + ), + ) end def log_api_key(api_key, action, opts = {}) opts[:changes]&.delete("key") # Do not log the full key - history_params = params(opts).merge( - action: action, - subject: api_key.truncated_key - ) + history_params = params(opts).merge(action: action, subject: api_key.truncated_key) if opts[:changes] old_values, new_values = get_changes(opts[:changes]) - history_params[:previous_value] = old_values&.join(", ") unless opts[:changes].keys.include?("id") + history_params[:previous_value] = old_values&.join(", ") unless opts[:changes].keys.include?( + "id", + ) history_params[:new_value] = new_values&.join(", ") end @@ -750,35 +853,39 @@ class StaffActionLogger end def log_api_key_revoke(api_key) - UserHistory.create!(params.merge( - subject: api_key.truncated_key, - action: UserHistory.actions[:api_key_update], - details: I18n.t("staff_action_logs.api_key.revoked") - )) + UserHistory.create!( + params.merge( + subject: api_key.truncated_key, + action: UserHistory.actions[:api_key_update], + details: I18n.t("staff_action_logs.api_key.revoked"), + ), + ) end def log_api_key_restore(api_key) - UserHistory.create!(params.merge( - subject: api_key.truncated_key, - action: UserHistory.actions[:api_key_update], - details: I18n.t("staff_action_logs.api_key.restored") - )) + UserHistory.create!( + params.merge( + subject: api_key.truncated_key, + action: UserHistory.actions[:api_key_update], + details: I18n.t("staff_action_logs.api_key.restored"), + ), + ) end def log_published_page(topic_id, slug) - UserHistory.create!(params.merge( - subject: slug, - topic_id: topic_id, - action: UserHistory.actions[:page_published] - )) + UserHistory.create!( + params.merge(subject: slug, topic_id: topic_id, action: UserHistory.actions[:page_published]), + ) end def log_unpublished_page(topic_id, slug) - UserHistory.create!(params.merge( - subject: slug, - topic_id: topic_id, - action: UserHistory.actions[:page_unpublished] - )) + UserHistory.create!( + params.merge( + subject: slug, + topic_id: topic_id, + action: UserHistory.actions[:page_unpublished], + ), + ) end def log_add_email(user) @@ -787,7 +894,7 @@ class StaffActionLogger UserHistory.create!( action: UserHistory.actions[:add_email], acting_user_id: @admin.id, - target_user_id: user.id + target_user_id: user.id, ) end @@ -797,7 +904,7 @@ class StaffActionLogger UserHistory.create!( action: UserHistory.actions[:update_email], acting_user_id: @admin.id, - target_user_id: user.id + target_user_id: user.id, ) end @@ -807,7 +914,7 @@ class StaffActionLogger UserHistory.create!( action: UserHistory.actions[:destroy_email], acting_user_id: @admin.id, - target_user_id: user.id + target_user_id: user.id, ) end @@ -818,7 +925,7 @@ class StaffActionLogger action: UserHistory.actions[:watched_word_create], acting_user_id: @admin.id, details: watched_word.action_log_details, - context: WatchedWord.actions[watched_word.action] + context: WatchedWord.actions[watched_word.action], ) end @@ -829,26 +936,21 @@ class StaffActionLogger action: UserHistory.actions[:watched_word_destroy], acting_user_id: @admin.id, details: watched_word.action_log_details, - context: WatchedWord.actions[watched_word.action] + context: WatchedWord.actions[watched_word.action], ) end def log_group_deletetion(group) raise Discourse::InvalidParameters.new(:group) if group.nil? - details = [ - "name: #{group.name}", - "id: #{group.id}" - ] + details = ["name: #{group.name}", "id: #{group.id}"] - if group.grant_trust_level - details << "grant_trust_level: #{group.grant_trust_level}" - end + details << "grant_trust_level: #{group.grant_trust_level}" if group.grant_trust_level UserHistory.create!( acting_user_id: @admin.id, action: UserHistory.actions[:delete_group], - details: details.join(', ') + details: details.join(", "), ) end diff --git a/app/services/themes_install_task.rb b/app/services/themes_install_task.rb index ac32739a1b..3eaa931944 100644 --- a/app/services/themes_install_task.rb +++ b/app/services/themes_install_task.rb @@ -48,20 +48,27 @@ class ThemesInstallTask end def repo_name - @url.gsub(Regexp.union('git@github.com:', 'https://github.com/', '.git'), '') + @url.gsub(Regexp.union("git@github.com:", "https://github.com/", ".git"), "") end def theme_exists? - @remote_theme = RemoteTheme - .where("remote_url like ?", "%#{repo_name}%") - .where(branch: @options.fetch(:branch, nil)) - .first + @remote_theme = + RemoteTheme + .where("remote_url like ?", "%#{repo_name}%") + .where(branch: @options.fetch(:branch, nil)) + .first @theme = @remote_theme&.theme @theme.present? end def install - @theme = RemoteTheme.import_theme(@url, Discourse.system_user, private_key: @options[:private_key], branch: @options[:branch]) + @theme = + RemoteTheme.import_theme( + @url, + Discourse.system_user, + private_key: @options[:private_key], + branch: @options[:branch], + ) @theme.set_default! if @options.fetch(:default, false) add_component_to_all_themes end @@ -76,9 +83,13 @@ class ThemesInstallTask def add_component_to_all_themes return if (!@options.fetch(:add_to_all_themes, false) || !@theme.component) - Theme.where(component: false).each do |parent_theme| - next if ChildTheme.where(parent_theme_id: parent_theme.id, child_theme_id: @theme.id).exists? - parent_theme.add_relative_theme!(:child, @theme) - end + Theme + .where(component: false) + .each do |parent_theme| + if ChildTheme.where(parent_theme_id: parent_theme.id, child_theme_id: @theme.id).exists? + next + end + parent_theme.add_relative_theme!(:child, @theme) + end end end diff --git a/app/services/topic_bookmarkable.rb b/app/services/topic_bookmarkable.rb index 5a5276f64a..f79af4c07c 100644 --- a/app/services/topic_bookmarkable.rb +++ b/app/services/topic_bookmarkable.rb @@ -19,28 +19,29 @@ class TopicBookmarkable < BaseBookmarkable topics = topic_bookmarks.map(&:bookmarkable) topic_user_lookup = TopicUser.lookup_for(guardian.user, topics) - topics.each do |topic| - topic.user_data = topic_user_lookup[topic.id] - end + topics.each { |topic| topic.user_data = topic_user_lookup[topic.id] } end def self.list_query(user, guardian) topics = Topic.listable_topics.secured(guardian) pms = Topic.private_messages_for_user(user) - topic_bookmarks = user - .bookmarks_of_type("Topic") - .joins("INNER JOIN topics ON topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic'") - .joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id") - .where("topic_users.user_id = ?", user.id) + topic_bookmarks = + user + .bookmarks_of_type("Topic") + .joins( + "INNER JOIN topics ON topics.id = bookmarks.bookmarkable_id AND bookmarks.bookmarkable_type = 'Topic'", + ) + .joins("LEFT JOIN topic_users ON topic_users.topic_id = topics.id") + .where("topic_users.user_id = ?", user.id) guardian.filter_allowed_categories(topic_bookmarks.merge(topics.or(pms))) end def self.search_query(bookmarks, query, ts_query, &bookmarkable_search) bookmarkable_search.call( - bookmarks - .joins("LEFT JOIN posts ON posts.topic_id = topics.id AND posts.post_number = 1") - .joins("LEFT JOIN post_search_data ON post_search_data.post_id = posts.id"), - "#{ts_query} @@ post_search_data.search_data" + bookmarks.joins( + "LEFT JOIN posts ON posts.topic_id = topics.id AND posts.post_number = 1", + ).joins("LEFT JOIN post_search_data ON post_search_data.post_id = posts.id"), + "#{ts_query} @@ post_search_data.search_data", ) end @@ -51,8 +52,8 @@ class TopicBookmarkable < BaseBookmarkable post_number: 1, data: { title: bookmark.bookmarkable.title, - bookmarkable_url: bookmark.bookmarkable.first_post.url - } + bookmarkable_url: bookmark.bookmarkable.first_post.url, + }, ) end diff --git a/app/services/topic_status_updater.rb b/app/services/topic_status_updater.rb index d5e1d95d44..7008c3223a 100644 --- a/app/services/topic_status_updater.rb +++ b/app/services/topic_status_updater.rb @@ -1,167 +1,176 @@ # frozen_string_literal: true -TopicStatusUpdater = Struct.new(:topic, :user) do - def update!(status, enabled, opts = {}) - status = Status.new(status, enabled) +TopicStatusUpdater = + Struct.new(:topic, :user) do + def update!(status, enabled, opts = {}) + status = Status.new(status, enabled) - @topic_timer = topic.public_topic_timer + @topic_timer = topic.public_topic_timer - updated = nil - Topic.transaction do - updated = change(status, opts) - if updated - highest_post_number = topic.highest_post_number - create_moderator_post_for(status, opts) - update_read_state_for(status, highest_post_number) + updated = nil + Topic.transaction do + updated = change(status, opts) + if updated + highest_post_number = topic.highest_post_number + create_moderator_post_for(status, opts) + update_read_state_for(status, highest_post_number) + end end + + updated end - updated - end + private - private + def change(status, opts = {}) + result = true - def change(status, opts = {}) - result = true - - if status.pinned? || status.pinned_globally? - topic.update_pinned(status.enabled?, status.pinned_globally?, opts[:until]) - elsif status.autoclosed? - rc = Topic.where(id: topic.id, closed: !status.enabled?).update_all(closed: status.enabled?) - topic.closed = status.enabled? - result = false if rc == 0 - else - rc = Topic.where(:id => topic.id, status.name => !status.enabled) - .update_all(status.name => status.enabled?) - - topic.public_send("#{status.name}=", status.enabled?) - result = false if rc == 0 - end - - if status.manually_closing_topic? - DiscourseEvent.trigger(:topic_closed, topic) - end - - if status.visible? && status.disabled? - UserProfile.remove_featured_topic_from_all_profiles(topic) - end - - if status.visible? && result - topic.update_category_topic_count_by(status.enabled? ? 1 : -1) - UserStatCountUpdater.public_send(status.enabled? ? :increment! : :decrement!, topic.first_post) - end - - if @topic_timer - if status.manually_closing_topic? || status.closing_topic? - topic.delete_topic_timer(TopicTimer.types[:close]) - topic.delete_topic_timer(TopicTimer.types[:silent_close]) - elsif status.manually_opening_topic? || status.opening_topic? - topic.delete_topic_timer(TopicTimer.types[:open]) - topic.inherit_auto_close_from_category - end - end - - # remove featured topics if we close/archive/make them invisible. Previously we used - # to run the whole featuring logic but that could be very slow and have concurrency - # errors on large sites with many autocloses and topics being created. - if ((status.enabled? && (status.autoclosed? || status.closed? || status.archived?)) || - (status.disabled? && status.visible?)) - CategoryFeaturedTopic.where(topic_id: topic.id).delete_all - end - - result - end - - def create_moderator_post_for(status, opts) - message = opts[:message] - topic.add_moderator_post(user, message || message_for(status), options_for(status, opts)) - topic.reload - end - - def update_read_state_for(status, old_highest_read) - if status.autoclosed? && status.enabled? - # let's pretend all the people that read up to the autoclose message - # actually read the topic - PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number) - end - end - - def message_for(status) - if status.autoclosed? - locale_key = status.locale_key.dup - locale_key << "_lastpost" if @topic_timer&.based_on_last_post - message_for_autoclosed(locale_key) - end - end - - def message_for_autoclosed(locale_key) - num_minutes = - if @topic_timer&.based_on_last_post - (@topic_timer.duration_minutes || 0).minutes.to_i - elsif @topic_timer&.created_at - Time.zone.now - @topic_timer.created_at + if status.pinned? || status.pinned_globally? + topic.update_pinned(status.enabled?, status.pinned_globally?, opts[:until]) + elsif status.autoclosed? + rc = Topic.where(id: topic.id, closed: !status.enabled?).update_all(closed: status.enabled?) + topic.closed = status.enabled? + result = false if rc == 0 else - Time.zone.now - topic.created_at + rc = + Topic.where(:id => topic.id, status.name => !status.enabled).update_all( + status.name => status.enabled?, + ) + + topic.public_send("#{status.name}=", status.enabled?) + result = false if rc == 0 end - # all of the results above are in seconds, this brings them - # back to the actual minutes integer - num_minutes = (num_minutes / 1.minute).round + DiscourseEvent.trigger(:topic_closed, topic) if status.manually_closing_topic? - if num_minutes.minutes >= 2.days - I18n.t("#{locale_key}_days", count: (num_minutes.minutes / 1.day).round) - else - num_hours = (num_minutes.minutes / 1.hour).round - if num_hours >= 2 - I18n.t("#{locale_key}_hours", count: num_hours) + if status.visible? && status.disabled? + UserProfile.remove_featured_topic_from_all_profiles(topic) + end + + if status.visible? && result + topic.update_category_topic_count_by(status.enabled? ? 1 : -1) + UserStatCountUpdater.public_send( + status.enabled? ? :increment! : :decrement!, + topic.first_post, + ) + end + + if @topic_timer + if status.manually_closing_topic? || status.closing_topic? + topic.delete_topic_timer(TopicTimer.types[:close]) + topic.delete_topic_timer(TopicTimer.types[:silent_close]) + elsif status.manually_opening_topic? || status.opening_topic? + topic.delete_topic_timer(TopicTimer.types[:open]) + topic.inherit_auto_close_from_category + end + end + + # remove featured topics if we close/archive/make them invisible. Previously we used + # to run the whole featuring logic but that could be very slow and have concurrency + # errors on large sites with many autocloses and topics being created. + if ( + (status.enabled? && (status.autoclosed? || status.closed? || status.archived?)) || + (status.disabled? && status.visible?) + ) + CategoryFeaturedTopic.where(topic_id: topic.id).delete_all + end + + result + end + + def create_moderator_post_for(status, opts) + message = opts[:message] + topic.add_moderator_post(user, message || message_for(status), options_for(status, opts)) + topic.reload + end + + def update_read_state_for(status, old_highest_read) + if status.autoclosed? && status.enabled? + # let's pretend all the people that read up to the autoclose message + # actually read the topic + PostTiming.pretend_read(topic.id, old_highest_read, topic.highest_post_number) + end + end + + def message_for(status) + if status.autoclosed? + locale_key = status.locale_key.dup + locale_key << "_lastpost" if @topic_timer&.based_on_last_post + message_for_autoclosed(locale_key) + end + end + + def message_for_autoclosed(locale_key) + num_minutes = + if @topic_timer&.based_on_last_post + (@topic_timer.duration_minutes || 0).minutes.to_i + elsif @topic_timer&.created_at + Time.zone.now - @topic_timer.created_at + else + Time.zone.now - topic.created_at + end + + # all of the results above are in seconds, this brings them + # back to the actual minutes integer + num_minutes = (num_minutes / 1.minute).round + + if num_minutes.minutes >= 2.days + I18n.t("#{locale_key}_days", count: (num_minutes.minutes / 1.day).round) else - I18n.t("#{locale_key}_minutes", count: num_minutes) + num_hours = (num_minutes.minutes / 1.hour).round + if num_hours >= 2 + I18n.t("#{locale_key}_hours", count: num_hours) + else + I18n.t("#{locale_key}_minutes", count: num_minutes) + end end end + + def options_for(status, opts = {}) + { + bump: status.opening_topic?, + post_type: Post.types[:small_action], + silent: opts[:silent], + action_code: status.action_code, + } + end + + Status = + Struct.new(:name, :enabled) do + %w[pinned_globally pinned autoclosed closed visible archived].each do |status| + define_method("#{status}?") { name == status } + end + + def enabled? + enabled + end + + def disabled? + !enabled? + end + + def action_code + "#{name}.#{enabled? ? "enabled" : "disabled"}" + end + + def locale_key + "topic_statuses.#{action_code.tr(".", "_")}" + end + + def opening_topic? + (closed? || autoclosed?) && disabled? + end + + def closing_topic? + (closed? || autoclosed?) && enabled? + end + + def manually_closing_topic? + closed? && enabled? + end + + def manually_opening_topic? + closed? && disabled? + end + end end - - def options_for(status, opts = {}) - { bump: status.opening_topic?, - post_type: Post.types[:small_action], - silent: opts[:silent], - action_code: status.action_code } - end - - Status = Struct.new(:name, :enabled) do - %w(pinned_globally pinned autoclosed closed visible archived).each do |status| - define_method("#{status}?") { name == status } - end - - def enabled? - enabled - end - - def disabled? - !enabled? - end - - def action_code - "#{name}.#{enabled? ? 'enabled' : 'disabled'}" - end - - def locale_key - "topic_statuses.#{action_code.tr('.', '_')}" - end - - def opening_topic? - (closed? || autoclosed?) && disabled? - end - - def closing_topic? - (closed? || autoclosed?) && enabled? - end - - def manually_closing_topic? - closed? && enabled? - end - - def manually_opening_topic? - closed? && disabled? - end - end -end diff --git a/app/services/topic_timestamp_changer.rb b/app/services/topic_timestamp_changer.rb index a8d5059c4d..c9f1d8b552 100644 --- a/app/services/topic_timestamp_changer.rb +++ b/app/services/topic_timestamp_changer.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true class TopicTimestampChanger - class InvalidTimestampError < StandardError; end + class InvalidTimestampError < StandardError + end def initialize(timestamp:, topic: nil, topic_id: nil) @topic = topic || Topic.with_deleted.find(topic_id) @@ -46,11 +47,7 @@ class TopicTimestampChanger end def update_topic(last_posted_at) - @topic.update( - created_at: @timestamp, - updated_at: @timestamp, - last_posted_at: last_posted_at - ) + @topic.update(created_at: @timestamp, updated_at: @timestamp, last_posted_at: last_posted_at) end def update_post(post, timestamp) diff --git a/app/services/tracked_topics_updater.rb b/app/services/tracked_topics_updater.rb index 018303af0b..1fc74ce207 100644 --- a/app/services/tracked_topics_updater.rb +++ b/app/services/tracked_topics_updater.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TrackedTopicsUpdater - def initialize(user_id, threshold) @id = user_id @threshold = threshold @@ -12,8 +11,14 @@ class TrackedTopicsUpdater if @threshold < 0 topic_users.update_all(notification_level: TopicUser.notification_levels[:regular]) else - topic_users.update_all(["notification_level = CASE WHEN total_msecs_viewed < ? THEN ? ELSE ? END", - @threshold, TopicUser.notification_levels[:regular], TopicUser.notification_levels[:tracking]]) + topic_users.update_all( + [ + "notification_level = CASE WHEN total_msecs_viewed < ? THEN ? ELSE ? END", + @threshold, + TopicUser.notification_levels[:regular], + TopicUser.notification_levels[:tracking], + ], + ) end end end diff --git a/app/services/trust_level_granter.rb b/app/services/trust_level_granter.rb index db284b5dc8..9d95d117a5 100644 --- a/app/services/trust_level_granter.rb +++ b/app/services/trust_level_granter.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class TrustLevelGranter - def initialize(trust_level, user) @trust_level, @user = trust_level, user end diff --git a/app/services/user_action_manager.rb b/app/services/user_action_manager.rb index ae460b6235..61a207687d 100644 --- a/app/services/user_action_manager.rb +++ b/app/services/user_action_manager.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserActionManager - def self.disable @disabled = true end @@ -10,8 +9,7 @@ class UserActionManager @disabled = false end - [:notification, :post, :topic, :post_action].each do |type| - self.class_eval(<<~RUBY) + %i[notification post topic post_action].each { |type| self.class_eval(<<~RUBY) } def self.#{type}_created(*args) return if @disabled #{type}_rows(*args).each { |row| UserAction.log_action!(row) } @@ -21,9 +19,8 @@ class UserActionManager #{type}_rows(*args).each { |row| UserAction.remove_action!(row) } end RUBY - end -private + private def self.topic_rows(topic) # no action to log here, this can happen if a user is deleted @@ -36,22 +33,28 @@ private acting_user_id: topic.user_id, target_topic_id: topic.id, target_post_id: -1, - created_at: topic.created_at + created_at: topic.created_at, } - UserAction.remove_action!(row.merge( - action_type: topic.private_message? ? UserAction::NEW_TOPIC : UserAction::NEW_PRIVATE_MESSAGE - )) + UserAction.remove_action!( + row.merge( + action_type: + topic.private_message? ? UserAction::NEW_TOPIC : UserAction::NEW_PRIVATE_MESSAGE, + ), + ) rows = [row] if topic.private_message? - topic.topic_allowed_users.reject { |a| a.user_id == topic.user_id }.each do |ta| - row = row.dup - row[:user_id] = ta.user_id - row[:action_type] = UserAction::GOT_PRIVATE_MESSAGE - rows << row - end + topic + .topic_allowed_users + .reject { |a| a.user_id == topic.user_id } + .each do |ta| + row = row.dup + row[:user_id] = ta.user_id + row[:action_type] = UserAction::GOT_PRIVATE_MESSAGE + rows << row + end end rows end @@ -66,7 +69,7 @@ private acting_user_id: post.user_id, target_post_id: post.id, target_topic_id: post.topic_id, - created_at: post.created_at + created_at: post.created_at, } rows = [row] @@ -76,7 +79,13 @@ private post.topic.topic_allowed_users.each do |ta| row = row.dup row[:user_id] = ta.user_id - row[:action_type] = ta.user_id == post.user_id ? UserAction::NEW_PRIVATE_MESSAGE : UserAction::GOT_PRIVATE_MESSAGE + row[:action_type] = ( + if ta.user_id == post.user_id + UserAction::NEW_PRIVATE_MESSAGE + else + UserAction::GOT_PRIVATE_MESSAGE + end + ) rows << row end end @@ -100,13 +109,15 @@ private # skip any invalid items, eg failed to save post and so on return [] unless action && post && user && post.id - [{ - action_type: action, - user_id: user.id, - acting_user_id: acting_user_id || post.user_id, - target_topic_id: post.topic_id, - target_post_id: post.id - }] + [ + { + action_type: action, + user_id: user.id, + acting_user_id: acting_user_id || post.user_id, + target_topic_id: post.topic_id, + target_post_id: post.id, + }, + ] end def self.post_action_rows(post_action) @@ -121,11 +132,13 @@ private acting_user_id: post_action.user_id, target_post_id: post_action.post_id, target_topic_id: post.topic_id, - created_at: post_action.created_at + created_at: post_action.created_at, } - post_action.is_like? ? - [row, row.merge(action_type: UserAction::WAS_LIKED, user_id: post.user_id)] : + if post_action.is_like? + [row, row.merge(action_type: UserAction::WAS_LIKED, user_id: post.user_id)] + else [row] + end end end diff --git a/app/services/user_activator.rb b/app/services/user_activator.rb index 4c0d14ab62..6d45907fbf 100644 --- a/app/services/user_activator.rb +++ b/app/services/user_activator.rb @@ -39,7 +39,6 @@ class UserActivator LoginActivator end end - end class ApprovalActivator < UserActivator @@ -69,7 +68,7 @@ class LoginActivator < UserActivator def activate log_on_user(user) - user.enqueue_welcome_message('welcome_user') + user.enqueue_welcome_message("welcome_user") success_message end diff --git a/app/services/user_anonymizer.rb b/app/services/user_anonymizer.rb index 970d1fff42..6f1b317c6c 100644 --- a/app/services/user_anonymizer.rb +++ b/app/services/user_anonymizer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserAnonymizer - attr_reader :user_history # opts: @@ -55,7 +54,7 @@ class UserAnonymizer bio_raw: nil, bio_cooked: nil, profile_background_upload: nil, - card_background_upload: nil + card_background_upload: nil, ) end @@ -70,15 +69,19 @@ class UserAnonymizer @user_history = log_action end - UsernameChanger.update_username(user_id: @user.id, - old_username: @prev_username, - new_username: @user.username, - avatar_template: @user.avatar_template) + UsernameChanger.update_username( + user_id: @user.id, + old_username: @prev_username, + new_username: @user.username, + avatar_template: @user.avatar_template, + ) - Jobs.enqueue(:anonymize_user, - user_id: @user.id, - prev_email: @prev_email, - anonymize_ip: @opts[:anonymize_ip]) + Jobs.enqueue( + :anonymize_user, + user_id: @user.id, + prev_email: @prev_email, + anonymize_ip: @opts[:anonymize_ip], + ) DiscourseEvent.trigger(:user_anonymized, user: @user, opts: @opts) @user @@ -88,7 +91,7 @@ class UserAnonymizer def make_anon_username 100.times do - new_username = "anon#{(SecureRandom.random_number * 100000000).to_i}" + new_username = "anon#{(SecureRandom.random_number * 100_000_000).to_i}" return new_username unless User.where(username_lower: new_username).exists? end raise "Failed to generate an anon username" diff --git a/app/services/user_authenticator.rb b/app/services/user_authenticator.rb index c5cd920cf1..73dbe04e2a 100644 --- a/app/services/user_authenticator.rb +++ b/app/services/user_authenticator.rb @@ -1,8 +1,12 @@ # frozen_string_literal: true class UserAuthenticator - - def initialize(user, session, authenticator_finder: Users::OmniauthCallbacksController, require_password: true) + def initialize( + user, + session, + authenticator_finder: Users::OmniauthCallbacksController, + require_password: true + ) @user = user @session = session if session&.dig(:authentication) && session[:authentication].is_a?(Hash) @@ -61,5 +65,4 @@ class UserAuthenticator def authenticator_name @auth_result&.authenticator_name end - end diff --git a/app/services/user_destroyer.rb b/app/services/user_destroyer.rb index ed91b77422..0936d5cffa 100644 --- a/app/services/user_destroyer.rb +++ b/app/services/user_destroyer.rb @@ -2,19 +2,19 @@ # Responsible for destroying a User record class UserDestroyer - - class PostsExistError < RuntimeError; end + class PostsExistError < RuntimeError + end def initialize(actor) @actor = actor - raise Discourse::InvalidParameters.new('acting user is nil') unless @actor && @actor.is_a?(User) + raise Discourse::InvalidParameters.new("acting user is nil") unless @actor && @actor.is_a?(User) @guardian = Guardian.new(actor) end # Returns false if the user failed to be deleted. # Returns a frozen instance of the User if the delete succeeded. def destroy(user, opts = {}) - raise Discourse::InvalidParameters.new('user is nil') unless user && user.is_a?(User) + raise Discourse::InvalidParameters.new("user is nil") unless user && user.is_a?(User) raise PostsExistError if !opts[:delete_posts] && user.posts.joins(:topic).count != 0 @guardian.ensure_can_delete_user!(user) @@ -26,7 +26,6 @@ class UserDestroyer result = nil optional_transaction(open_transaction: opts[:transaction]) do - UserSecurityKey.where(user_id: user.id).delete_all Bookmark.where(user_id: user.id).delete_all Draft.where(user_id: user.id).delete_all @@ -44,17 +43,15 @@ class UserDestroyer delete_posts(user, category_topic_ids, opts) end - user.post_actions.find_each do |post_action| - post_action.remove_act!(Discourse.system_user) - end + user.post_actions.find_each { |post_action| post_action.remove_act!(Discourse.system_user) } # Add info about the user to staff action logs UserHistory.staff_action_records( - Discourse.system_user, acting_user: user.username - ).update_all([ - "details = CONCAT(details, ?)", - "\nuser_id: #{user.id}\nusername: #{user.username}" - ]) + Discourse.system_user, + acting_user: user.username, + ).update_all( + ["details = CONCAT(details, ?)", "\nuser_id: #{user.id}\nusername: #{user.username}"], + ) # keep track of emails used user_emails = user.user_emails.pluck(:email) @@ -76,22 +73,26 @@ class UserDestroyer Post.unscoped.where(user_id: result.id).update_all(user_id: nil) # If this user created categories, fix those up: - Category.where(user_id: result.id).each do |c| - c.user_id = Discourse::SYSTEM_USER_ID - c.save! - if topic = Topic.unscoped.find_by(id: c.topic_id) - topic.recover! - topic.user_id = Discourse::SYSTEM_USER_ID - topic.save! + Category + .where(user_id: result.id) + .each do |c| + c.user_id = Discourse::SYSTEM_USER_ID + c.save! + if topic = Topic.unscoped.find_by(id: c.topic_id) + topic.recover! + topic.user_id = Discourse::SYSTEM_USER_ID + topic.save! + end end - end - Invite.where(email: user_emails).each do |invite| - # invited_users will be removed by dependent destroy association when user is destroyed - invite.invited_groups.destroy_all - invite.topic_invites.destroy_all - invite.destroy - end + Invite + .where(email: user_emails) + .each do |invite| + # invited_users will be removed by dependent destroy association when user is destroyed + invite.invited_groups.destroy_all + invite.topic_invites.destroy_all + invite.destroy + end unless opts[:quiet] if @actor == user @@ -101,7 +102,9 @@ class UserDestroyer deleted_by = @actor end StaffActionLogger.new(deleted_by).log_user_deletion(user, opts.slice(:context)) - Rails.logger.warn("User destroyed without context from: #{caller_locations(14, 1)[0]}") if opts.slice(:context).blank? + if opts.slice(:context).blank? + Rails.logger.warn("User destroyed without context from: #{caller_locations(14, 1)[0]}") + end end MessageBus.publish "/logout/#{result.id}", result.id, user_ids: [result.id] end @@ -118,16 +121,22 @@ class UserDestroyer protected def block_external_urls(user) - TopicLink.where(user: user, internal: false).find_each do |link| - next if Oneboxer.engine(link.url) != Onebox::Engine::AllowlistedGenericOnebox - ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match! - end + TopicLink + .where(user: user, internal: false) + .find_each do |link| + next if Oneboxer.engine(link.url) != Onebox::Engine::AllowlistedGenericOnebox + ScreenedUrl.watch(link.url, link.domain, ip_address: user.ip_address)&.record_match! + end end def agree_with_flags(user) - ReviewableFlaggedPost.where(target_created_by: user).find_each do |reviewable| - reviewable.perform(@actor, :agree_and_keep) if reviewable.actions_for(@guardian).has?(:agree_and_keep) - end + ReviewableFlaggedPost + .where(target_created_by: user) + .find_each do |reviewable| + if reviewable.actions_for(@guardian).has?(:agree_and_keep) + reviewable.perform(@actor, :agree_and_keep) + end + end end def delete_posts(user, category_topic_ids, opts) @@ -146,7 +155,10 @@ class UserDestroyer def prepare_for_destroy(user) PostAction.where(user_id: user.id).delete_all - UserAction.where('user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id', user_id: user.id).delete_all + UserAction.where( + "user_id = :user_id OR target_user_id = :user_id OR acting_user_id = :user_id", + user_id: user.id, + ).delete_all PostTiming.where(user_id: user.id).delete_all TopicViewItem.where(user_id: user.id).delete_all TopicUser.where(user_id: user.id).delete_all @@ -156,10 +168,9 @@ class UserDestroyer def optional_transaction(open_transaction: true) if open_transaction - User.transaction { yield } + User.transaction { yield } else yield end end - end diff --git a/app/services/user_merger.rb b/app/services/user_merger.rb index a17089200d..96af036a46 100644 --- a/app/services/user_merger.rb +++ b/app/services/user_merger.rb @@ -33,23 +33,35 @@ class UserMerger def update_username return if @source_user.username == @target_user.username - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_username") }, user_ids: [@acting_user.id] if @acting_user - UsernameChanger.update_username(user_id: @source_user.id, - old_username: @source_user.username, - new_username: @target_user.username, - avatar_template: @target_user.avatar_template, - asynchronous: false) + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.updating_username") }, + user_ids: [@acting_user.id] + end + UsernameChanger.update_username( + user_id: @source_user.id, + old_username: @source_user.username, + new_username: @target_user.username, + avatar_template: @target_user.avatar_template, + asynchronous: false, + ) end def move_posts - posts = Post.with_deleted - .where(user_id: @source_user.id) - .order(:topic_id, :post_number) - .pluck(:topic_id, :id) + posts = + Post + .with_deleted + .where(user_id: @source_user.id) + .order(:topic_id, :post_number) + .pluck(:topic_id, :id) return if posts.count == 0 - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.changing_post_ownership") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.changing_post_ownership") }, + user_ids: [@acting_user.id] + end last_topic_id = nil post_ids = [] @@ -73,12 +85,16 @@ class UserMerger post_ids: post_ids, new_owner: @target_user, acting_user: Discourse.system_user, - skip_revision: true + skip_revision: true, ).change_owner! end def merge_given_daily_likes - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.merging_given_daily_likes") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.merging_given_daily_likes") }, + user_ids: [@acting_user.id] + end sql = <<~SQL INSERT INTO given_daily_likes AS g (user_id, likes_given, given_date, limit_reached) @@ -107,15 +123,21 @@ class UserMerger source_user_id: @source_user.id, target_user_id: @target_user.id, max_likes_per_day: SiteSetting.max_likes_per_day, - action_type_id: PostActionType.types[:like] + action_type_id: PostActionType.types[:like], ) end def merge_post_timings - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.merging_post_timings") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.merging_post_timings") }, + user_ids: [@acting_user.id] + end - update_user_id(:post_timings, conditions: ["x.topic_id = y.topic_id", - "x.post_number = y.post_number"]) + update_user_id( + :post_timings, + conditions: ["x.topic_id = y.topic_id", "x.post_number = y.post_number"], + ) sql = <<~SQL UPDATE post_timings AS t SET msecs = LEAST(t.msecs::bigint + s.msecs, 2^31 - 1) @@ -128,7 +150,11 @@ class UserMerger end def merge_user_visits - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.merging_user_visits") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.merging_user_visits") }, + user_ids: [@acting_user.id] + end update_user_id(:user_visits, conditions: "x.visited_at = y.visited_at") @@ -146,17 +172,27 @@ class UserMerger end def update_site_settings - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_site_settings") }, user_ids: [@acting_user.id] if @acting_user - - SiteSetting.all_settings(include_hidden: true).each do |setting| - if setting[:type] == "username" && setting[:value] == @source_user.username - SiteSetting.set_and_log(setting[:setting], @target_user.username) - end + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.updating_site_settings") }, + user_ids: [@acting_user.id] end + + SiteSetting + .all_settings(include_hidden: true) + .each do |setting| + if setting[:type] == "username" && setting[:value] == @source_user.username + SiteSetting.set_and_log(setting[:setting], @target_user.username) + end + end end def update_user_stats - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_user_stats") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.updating_user_stats") }, + user_ids: [@acting_user.id] + end # topics_entered DB.exec(<<~SQL, target_user_id: @target_user.id) @@ -212,7 +248,11 @@ class UserMerger end def merge_user_attributes - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.merging_user_attributes") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.merging_user_attributes") }, + user_ids: [@acting_user.id] + end DB.exec(<<~SQL, source_user_id: @source_user.id, target_user_id: @target_user.id) UPDATE users AS t @@ -255,7 +295,11 @@ class UserMerger end def update_user_ids - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.updating_user_ids") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.updating_user_ids") }, + user_ids: [@acting_user.id] + end Category.where(user_id: @source_user.id).update_all(user_id: @target_user.id) @@ -278,23 +322,44 @@ class UserMerger IncomingEmail.where(user_id: @source_user.id).update_all(user_id: @target_user.id) IncomingLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id) - IncomingLink.where(current_user_id: @source_user.id).update_all(current_user_id: @target_user.id) + IncomingLink.where(current_user_id: @source_user.id).update_all( + current_user_id: @target_user.id, + ) InvitedUser.where(user_id: @source_user.id).update_all(user_id: @target_user.id) - Invite.with_deleted.where(invited_by_id: @source_user.id).update_all(invited_by_id: @target_user.id) - Invite.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) + Invite + .with_deleted + .where(invited_by_id: @source_user.id) + .update_all(invited_by_id: @target_user.id) + Invite + .with_deleted + .where(deleted_by_id: @source_user.id) + .update_all(deleted_by_id: @target_user.id) update_user_id(:muted_users, conditions: "x.muted_user_id = y.muted_user_id") - update_user_id(:muted_users, user_id_column_name: "muted_user_id", conditions: "x.user_id = y.user_id") + update_user_id( + :muted_users, + user_id_column_name: "muted_user_id", + conditions: "x.user_id = y.user_id", + ) update_user_id(:ignored_users, conditions: "x.ignored_user_id = y.ignored_user_id") - update_user_id(:ignored_users, user_id_column_name: "ignored_user_id", conditions: "x.user_id = y.user_id") + update_user_id( + :ignored_users, + user_id_column_name: "ignored_user_id", + conditions: "x.user_id = y.user_id", + ) Notification.where(user_id: @source_user.id).update_all(user_id: @target_user.id) - update_user_id(:post_actions, conditions: ["x.post_id = y.post_id", - "x.post_action_type_id = y.post_action_type_id", - "x.targets_topic = y.targets_topic"]) + update_user_id( + :post_actions, + conditions: [ + "x.post_id = y.post_id", + "x.post_action_type_id = y.post_action_type_id", + "x.targets_topic = y.targets_topic", + ], + ) PostAction.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) PostAction.where(deferred_by_id: @source_user.id).update_all(deferred_by_id: @target_user.id) @@ -303,13 +368,24 @@ class UserMerger PostRevision.where(user_id: @source_user.id).update_all(user_id: @target_user.id) - Post.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) - Post.with_deleted.where(last_editor_id: @source_user.id).update_all(last_editor_id: @target_user.id) + Post + .with_deleted + .where(deleted_by_id: @source_user.id) + .update_all(deleted_by_id: @target_user.id) + Post + .with_deleted + .where(last_editor_id: @source_user.id) + .update_all(last_editor_id: @target_user.id) Post.with_deleted.where(locked_by_id: @source_user.id).update_all(locked_by_id: @target_user.id) - Post.with_deleted.where(reply_to_user_id: @source_user.id).update_all(reply_to_user_id: @target_user.id) + Post + .with_deleted + .where(reply_to_user_id: @source_user.id) + .update_all(reply_to_user_id: @target_user.id) Reviewable.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id) - ReviewableHistory.where(created_by_id: @source_user.id).update_all(created_by_id: @target_user.id) + ReviewableHistory.where(created_by_id: @source_user.id).update_all( + created_by_id: @target_user.id, + ) SearchLog.where(user_id: @source_user.id).update_all(user_id: @target_user.id) @@ -319,22 +395,36 @@ class UserMerger update_user_id(:topic_allowed_users, conditions: "x.topic_id = y.topic_id") - TopicEmbed.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) + TopicEmbed + .with_deleted + .where(deleted_by_id: @source_user.id) + .update_all(deleted_by_id: @target_user.id) TopicLink.where(user_id: @source_user.id).update_all(user_id: @target_user.id) TopicLinkClick.where(user_id: @source_user.id).update_all(user_id: @target_user.id) - TopicTimer.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) + TopicTimer + .with_deleted + .where(deleted_by_id: @source_user.id) + .update_all(deleted_by_id: @target_user.id) - update_user_id(:topic_timers, conditions: ["x.status_type = y.status_type", - "x.topic_id = y.topic_id", - "y.deleted_at IS NULL"]) + update_user_id( + :topic_timers, + conditions: [ + "x.status_type = y.status_type", + "x.topic_id = y.topic_id", + "y.deleted_at IS NULL", + ], + ) update_user_id(:topic_users, conditions: "x.topic_id = y.topic_id") update_user_id(:topic_views, conditions: "x.topic_id = y.topic_id") - Topic.with_deleted.where(deleted_by_id: @source_user.id).update_all(deleted_by_id: @target_user.id) + Topic + .with_deleted + .where(deleted_by_id: @source_user.id) + .update_all(deleted_by_id: @target_user.id) UnsubscribeKey.where(user_id: @source_user.id).update_all(user_id: @target_user.id) @@ -342,29 +432,46 @@ class UserMerger update_user_id(:user_archived_messages, conditions: "x.topic_id = y.topic_id") - update_user_id(:user_actions, - user_id_column_name: "user_id", - conditions: ["x.action_type = y.action_type", - "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id", - "x.target_post_id IS NOT DISTINCT FROM y.target_post_id", - "(x.acting_user_id IN (:source_user_id, :target_user_id) OR x.acting_user_id IS NOT DISTINCT FROM y.acting_user_id)"]) - update_user_id(:user_actions, - user_id_column_name: "acting_user_id", - conditions: ["x.action_type = y.action_type", - "x.user_id = y.user_id", - "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id", - "x.target_post_id IS NOT DISTINCT FROM y.target_post_id"]) + update_user_id( + :user_actions, + user_id_column_name: "user_id", + conditions: [ + "x.action_type = y.action_type", + "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id", + "x.target_post_id IS NOT DISTINCT FROM y.target_post_id", + "(x.acting_user_id IN (:source_user_id, :target_user_id) OR x.acting_user_id IS NOT DISTINCT FROM y.acting_user_id)", + ], + ) + update_user_id( + :user_actions, + user_id_column_name: "acting_user_id", + conditions: [ + "x.action_type = y.action_type", + "x.user_id = y.user_id", + "x.target_topic_id IS NOT DISTINCT FROM y.target_topic_id", + "x.target_post_id IS NOT DISTINCT FROM y.target_post_id", + ], + ) - update_user_id(:user_badges, conditions: ["x.badge_id = y.badge_id", - "x.seq = y.seq", - "x.post_id IS NOT DISTINCT FROM y.post_id"]) + update_user_id( + :user_badges, + conditions: [ + "x.badge_id = y.badge_id", + "x.seq = y.seq", + "x.post_id IS NOT DISTINCT FROM y.post_id", + ], + ) UserBadge.where(granted_by_id: @source_user.id).update_all(granted_by_id: @target_user.id) update_user_id(:user_custom_fields, conditions: "x.name = y.name") if @target_user.human? - update_user_id(:user_emails, conditions: "x.email = y.email OR y.primary = false", updates: '"primary" = false') + update_user_id( + :user_emails, + conditions: "x.email = y.email OR y.primary = false", + updates: '"primary" = false', + ) end UserExport.where(user_id: @source_user.id).update_all(user_id: @target_user.id) @@ -372,7 +479,9 @@ class UserMerger UserHistory.where(target_user_id: @source_user.id).update_all(target_user_id: @target_user.id) UserHistory.where(acting_user_id: @source_user.id).update_all(acting_user_id: @target_user.id) - UserProfileView.where(user_profile_id: @source_user.id).update_all(user_profile_id: @target_user.id) + UserProfileView.where(user_profile_id: @source_user.id).update_all( + user_profile_id: @target_user.id, + ) UserProfileView.where(user_id: @source_user.id).update_all(user_id: @target_user.id) UserWarning.where(user_id: @source_user.id).update_all(user_id: @target_user.id) @@ -382,14 +491,18 @@ class UserMerger end def delete_source_user - ::MessageBus.publish '/merge_user', { message: I18n.t("admin.user.merge_user.deleting_source_user") }, user_ids: [@acting_user.id] if @acting_user + if @acting_user + ::MessageBus.publish "/merge_user", + { message: I18n.t("admin.user.merge_user.deleting_source_user") }, + user_ids: [@acting_user.id] + end @source_user.reload @source_user.skip_email_validation = true @source_user.update( admin: false, - email: "#{@source_user.username}_#{SecureRandom.hex}@no-email.invalid" + email: "#{@source_user.username}_#{SecureRandom.hex}@no-email.invalid", ) UserDestroyer.new(Discourse.system_user).destroy(@source_user, quiet: true) diff --git a/app/services/user_notification_renderer.rb b/app/services/user_notification_renderer.rb index e66b7d9dec..673a53dec2 100644 --- a/app/services/user_notification_renderer.rb +++ b/app/services/user_notification_renderer.rb @@ -9,9 +9,10 @@ class UserNotificationRenderer < ActionView::Base def self.render(*args) LOCK.synchronize do - @instance ||= UserNotificationRenderer.with_empty_template_cache.with_view_paths( - Rails.configuration.paths["app/views"] - ) + @instance ||= + UserNotificationRenderer.with_empty_template_cache.with_view_paths( + Rails.configuration.paths["app/views"], + ) @instance.render(*args) end end diff --git a/app/services/user_notification_schedule_processor.rb b/app/services/user_notification_schedule_processor.rb index 7b924b22d4..6a340ca14e 100644 --- a/app/services/user_notification_schedule_processor.rb +++ b/app/services/user_notification_schedule_processor.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserNotificationScheduleProcessor - attr_accessor :schedule, :user, :timezone_name def initialize(schedule) @@ -38,10 +37,11 @@ class UserNotificationScheduleProcessor user.do_not_disturb_timings.find_or_create_by(previous_timing.attributes.except("id")) end - next_timing = user.do_not_disturb_timings.new( - starts_at: utc_time_at_minute(local_time, end_minute), - scheduled: true - ) + next_timing = + user.do_not_disturb_timings.new( + starts_at: utc_time_at_minute(local_time, end_minute), + scheduled: true, + ) save_timing_and_continue(local_time, next_timing, days) else save_timing_and_continue(local_time, previous_timing, days) @@ -53,14 +53,13 @@ class UserNotificationScheduleProcessor def find_previous_timing(local_time) # Try and find a previously scheduled dnd timing that we can extend if the # ends_at is at the previous midnight. fallback to a new timing if not. - previous = user.do_not_disturb_timings.find_by( - ends_at: (local_time - 1.day).end_of_day.utc, - scheduled: true - ) - previous || user.do_not_disturb_timings.new( - starts_at: local_time.beginning_of_day.utc, - scheduled: true - ) + previous = + user.do_not_disturb_timings.find_by( + ends_at: (local_time - 1.day).end_of_day.utc, + scheduled: true, + ) + previous || + user.do_not_disturb_timings.new(starts_at: local_time.beginning_of_day.utc, scheduled: true) end def save_timing_and_continue(local_time, timing, days) @@ -78,7 +77,15 @@ class UserNotificationScheduleProcessor def utc_time_at_minute(base_time, total_minutes) hour = total_minutes / 60 minute = total_minutes % 60 - Time.new(base_time.year, base_time.month, base_time.day, hour, minute, 0, base_time.formatted_offset).utc + Time.new( + base_time.year, + base_time.month, + base_time.day, + hour, + minute, + 0, + base_time.formatted_offset, + ).utc end def transform_wday(wday) diff --git a/app/services/user_silencer.rb b/app/services/user_silencer.rb index 10c3b1c74a..50ae7b4ab7 100644 --- a/app/services/user_silencer.rb +++ b/app/services/user_silencer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserSilencer - attr_reader :user_history def initialize(user, by_user = nil, opts = {}) @@ -29,11 +28,7 @@ class UserSilencer if @user.save message_type = @opts[:message] || :silenced_by_staff - details = StaffMessageFormat.new( - :silence, - @opts[:reason], - @opts[:message_body] - ).format + details = StaffMessageFormat.new(:silence, @opts[:reason], @opts[:message_body]).format context = "#{message_type}: #{@opts[:reason]}" @@ -41,10 +36,7 @@ class UserSilencer log_params = { context: context, details: details } log_params[:post_id] = @opts[:post_id].to_i if @opts[:post_id] - @user_history = StaffActionLogger.new(@by_user).log_silence_user( - @user, - log_params - ) + @user_history = StaffActionLogger.new(@by_user).log_silence_user(@user, log_params) end silence_message_params = {} @@ -58,7 +50,7 @@ class UserSilencer post_id: @opts[:post_id], silenced_till: @user.silenced_till, silenced_at: DateTime.now, - silence_message_params: silence_message_params + silence_message_params: silence_message_params, ) silence_message_params.merge!(post_alert_options: { skip_send_email: true }) @@ -73,8 +65,20 @@ class UserSilencer def hide_posts return unless @user.trust_level == TrustLevel[0] - Post.where(user_id: @user.id).where("created_at > ?", 24.hours.ago).update_all(["hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", Post.hidden_reasons[:new_user_spam_threshold_reached]]) - topic_ids = Post.where(user_id: @user.id, post_number: 1).where("created_at > ?", 24.hours.ago).pluck(:topic_id) + Post + .where(user_id: @user.id) + .where("created_at > ?", 24.hours.ago) + .update_all( + [ + "hidden = true, hidden_reason_id = COALESCE(hidden_reason_id, ?)", + Post.hidden_reasons[:new_user_spam_threshold_reached], + ], + ) + topic_ids = + Post + .where(user_id: @user.id, post_number: 1) + .where("created_at > ?", 24.hours.ago) + .pluck(:topic_id) Topic.where(id: topic_ids).update_all(visible: false) unless topic_ids.empty? end @@ -86,5 +90,4 @@ class UserSilencer StaffActionLogger.new(@by_user).log_unsilence_user(@user) if @by_user end end - end diff --git a/app/services/user_stat_count_updater.rb b/app/services/user_stat_count_updater.rb index dac3e086b4..ed55e620ac 100644 --- a/app/services/user_stat_count_updater.rb +++ b/app/services/user_stat_count_updater.rb @@ -12,11 +12,11 @@ class UserStatCountUpdater def set!(user_stat:, count:, count_column:) return if user_stat.blank? - return if ![:post_count, :topic_count].include?(count_column) + return if !%i[post_count topic_count].include?(count_column) if SiteSetting.verbose_user_stat_count_logging && count < 0 Rails.logger.warn( - "Attempted to insert negative count into UserStat##{count_column} for user #{user_stat.user_id}, using 0 instead. Caller:\n #{caller[0..10].join("\n")}" + "Attempted to insert negative count into UserStat##{count_column} for user #{user_stat.user_id}, using 0 instead. Caller:\n #{caller[0..10].join("\n")}", ) end @@ -47,7 +47,7 @@ class UserStatCountUpdater if action == :decrement! && stat.public_send(column) < 1 if SiteSetting.verbose_user_stat_count_logging Rails.logger.warn( - "Attempted to insert negative count into UserStat##{column} for post with id '#{post.id}'. Caller:\n #{caller[0..10].join("\n")}" + "Attempted to insert negative count into UserStat##{column} for post with id '#{post.id}'. Caller:\n #{caller[0..10].join("\n")}", ) end diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index d537b7d0e9..511755628f 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -1,66 +1,65 @@ # frozen_string_literal: true class UserUpdater - CATEGORY_IDS = { watched_first_post_category_ids: :watching_first_post, watched_category_ids: :watching, tracked_category_ids: :tracking, regular_category_ids: :regular, - muted_category_ids: :muted + muted_category_ids: :muted, } TAG_NAMES = { watching_first_post_tags: :watching_first_post, watched_tags: :watching, tracked_tags: :tracking, - muted_tags: :muted + muted_tags: :muted, } - OPTION_ATTR = [ - :mailing_list_mode, - :mailing_list_mode_frequency, - :email_digests, - :email_level, - :email_messages_level, - :external_links_in_new_tab, - :enable_quoting, - :enable_defer, - :color_scheme_id, - :dark_scheme_id, - :dynamic_favicon, - :automatically_unpin_topics, - :digest_after_minutes, - :new_topic_duration_minutes, - :auto_track_topics_after_msecs, - :notification_level_when_replying, - :email_previous_replies, - :email_in_reply_to, - :like_notification_frequency, - :include_tl0_in_digests, - :theme_ids, - :allow_private_messages, - :enable_allowed_pm_users, - :homepage_id, - :hide_profile_and_presence, - :text_size, - :title_count_mode, - :timezone, - :skip_new_user_tips, - :seen_popups, - :default_calendar, - :sidebar_list_destination, - :bookmark_auto_delete_preference + OPTION_ATTR = %i[ + mailing_list_mode + mailing_list_mode_frequency + email_digests + email_level + email_messages_level + external_links_in_new_tab + enable_quoting + enable_defer + color_scheme_id + dark_scheme_id + dynamic_favicon + automatically_unpin_topics + digest_after_minutes + new_topic_duration_minutes + auto_track_topics_after_msecs + notification_level_when_replying + email_previous_replies + email_in_reply_to + like_notification_frequency + include_tl0_in_digests + theme_ids + allow_private_messages + enable_allowed_pm_users + homepage_id + hide_profile_and_presence + text_size + title_count_mode + timezone + skip_new_user_tips + seen_popups + default_calendar + sidebar_list_destination + bookmark_auto_delete_preference ] - NOTIFICATION_SCHEDULE_ATTRS = -> { + NOTIFICATION_SCHEDULE_ATTRS = -> do attrs = [:enabled] 7.times do |n| attrs.push("day_#{n}_start_time".to_sym) attrs.push("day_#{n}_end_time".to_sym) end { user_notification_schedule: attrs } - }.call + end.call def initialize(actor, user) @user = user @@ -70,7 +69,9 @@ class UserUpdater def update(attributes = {}) user_profile = user.user_profile - user_profile.dismissed_banner_key = attributes[:dismissed_banner_key] if attributes[:dismissed_banner_key].present? + user_profile.dismissed_banner_key = attributes[:dismissed_banner_key] if attributes[ + :dismissed_banner_key + ].present? unless SiteSetting.enable_discourse_connect && SiteSetting.discourse_connect_overrides_bio user_profile.bio_raw = attributes.fetch(:bio_raw) { user_profile.bio_raw } end @@ -83,56 +84,52 @@ class UserUpdater user_profile.website = format_url(attributes.fetch(:website) { user_profile.website }) end - if attributes[:profile_background_upload_url] == "" || !guardian.can_upload_profile_header?(user) + if attributes[:profile_background_upload_url] == "" || + !guardian.can_upload_profile_header?(user) user_profile.profile_background_upload_id = nil elsif upload = Upload.get_from_url(attributes[:profile_background_upload_url]) user_profile.profile_background_upload_id = upload.id end - if attributes[:card_background_upload_url] == "" || !guardian.can_upload_user_card_background?(user) + if attributes[:card_background_upload_url] == "" || + !guardian.can_upload_user_card_background?(user) user_profile.card_background_upload_id = nil elsif upload = Upload.get_from_url(attributes[:card_background_upload_url]) user_profile.card_background_upload_id = upload.id end if attributes[:user_notification_schedule] - user_notification_schedule = user.user_notification_schedule || UserNotificationSchedule.new(user: user) + user_notification_schedule = + user.user_notification_schedule || UserNotificationSchedule.new(user: user) user_notification_schedule.assign_attributes(attributes[:user_notification_schedule]) end old_user_name = user.name.present? ? user.name : "" - if guardian.can_edit_name?(user) - user.name = attributes.fetch(:name) { user.name } - end + user.name = attributes.fetch(:name) { user.name } if guardian.can_edit_name?(user) user.locale = attributes.fetch(:locale) { user.locale } user.date_of_birth = attributes.fetch(:date_of_birth) { user.date_of_birth } - if attributes[:title] && - attributes[:title] != user.title && - guardian.can_grant_title?(user, attributes[:title]) + if attributes[:title] && attributes[:title] != user.title && + guardian.can_grant_title?(user, attributes[:title]) user.title = attributes[:title] end - if SiteSetting.user_selected_primary_groups && - attributes[:primary_group_id] && - attributes[:primary_group_id] != user.primary_group_id && - guardian.can_use_primary_group?(user, attributes[:primary_group_id]) - + if SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] && + attributes[:primary_group_id] != user.primary_group_id && + guardian.can_use_primary_group?(user, attributes[:primary_group_id]) user.primary_group_id = attributes[:primary_group_id] - elsif SiteSetting.user_selected_primary_groups && - attributes[:primary_group_id] && - attributes[:primary_group_id].blank? - + elsif SiteSetting.user_selected_primary_groups && attributes[:primary_group_id] && + attributes[:primary_group_id].blank? user.primary_group_id = nil end - if attributes[:flair_group_id] && - attributes[:flair_group_id] != user.flair_group_id && - (attributes[:flair_group_id].blank? || - guardian.can_use_flair_group?(user, attributes[:flair_group_id])) - + if attributes[:flair_group_id] && attributes[:flair_group_id] != user.flair_group_id && + ( + attributes[:flair_group_id].blank? || + guardian.can_use_flair_group?(user, attributes[:flair_group_id]) + ) user.flair_group_id = attributes[:flair_group_id] end @@ -145,7 +142,7 @@ class UserUpdater TAG_NAMES.each do |attribute, level| if attributes.has_key?(attribute) - TagUser.batch_set(user, level, attributes[attribute]&.split(',') || []) + TagUser.batch_set(user, level, attributes[attribute]&.split(",") || []) end end end @@ -165,7 +162,8 @@ class UserUpdater end if attributes.key?(:text_size) - user.user_option.text_size_seq += 1 if user.user_option.text_size.to_s != attributes[:text_size] + user.user_option.text_size_seq += 1 if user.user_option.text_size.to_s != + attributes[:text_size] end OPTION_ATTR.each do |attribute| @@ -173,7 +171,7 @@ class UserUpdater save_options = true if [true, false].include?(user.user_option.public_send(attribute)) - val = attributes[attribute].to_s == 'true' + val = attributes[attribute].to_s == "true" user.user_option.public_send("#{attribute}=", val) else user.user_option.public_send("#{attribute}=", attributes[attribute]) @@ -189,16 +187,12 @@ class UserUpdater user.user_option.email_digests = false if user.user_option.mailing_list_mode fields = attributes[:custom_fields] - if fields.present? - user.custom_fields = user.custom_fields.merge(fields) - end + user.custom_fields = user.custom_fields.merge(fields) if fields.present? saved = nil User.transaction do - if attributes.key?(:muted_usernames) - update_muted_users(attributes[:muted_usernames]) - end + update_muted_users(attributes[:muted_usernames]) if attributes.key?(:muted_usernames) if attributes.key?(:allowed_pm_usernames) update_allowed_pm_users(attributes[:allowed_pm_usernames]) @@ -213,11 +207,17 @@ class UserUpdater end if attributes.key?(:sidebar_category_ids) - SidebarSectionLinksUpdater.update_category_section_links(user, category_ids: attributes[:sidebar_category_ids]) + SidebarSectionLinksUpdater.update_category_section_links( + user, + category_ids: attributes[:sidebar_category_ids], + ) end if attributes.key?(:sidebar_tag_names) && SiteSetting.tagging_enabled - SidebarSectionLinksUpdater.update_tag_section_links(user, tag_names: attributes[:sidebar_tag_names]) + SidebarSectionLinksUpdater.update_tag_section_links( + user, + tag_names: attributes[:sidebar_tag_names], + ) end if SiteSetting.enable_user_status? @@ -225,17 +225,16 @@ class UserUpdater end name_changed = user.name_changed? - saved = (!save_options || user.user_option.save) && - (user_notification_schedule.nil? || user_notification_schedule.save) && - user_profile.save && - user.save + saved = + (!save_options || user.user_option.save) && + (user_notification_schedule.nil? || user_notification_schedule.save) && + user_profile.save && user.save if saved && (name_changed && old_user_name.casecmp(attributes.fetch(:name)) != 0) - StaffActionLogger.new(@actor).log_name_change( user.id, old_user_name, - attributes.fetch(:name) { '' } + attributes.fetch(:name) { "" }, ) end rescue Addressable::URI::InvalidURIError => e @@ -245,16 +244,14 @@ class UserUpdater if saved if user_notification_schedule - user_notification_schedule.enabled ? - user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) : + if user_notification_schedule.enabled + user_notification_schedule.create_do_not_disturb_timings(delete_existing: true) + else user_notification_schedule.destroy_scheduled_timings + end end if attributes.key?(:seen_popups) || attributes.key?(:skip_new_user_tips) - MessageBus.publish( - '/user-tips', - user.user_option.seen_popups, - user_ids: [user.id] - ) + MessageBus.publish("/user-tips", user.user_option.seen_popups, user_ids: [user.id]) end DiscourseEvent.trigger(:user_updated, user) end @@ -269,7 +266,7 @@ class UserUpdater if desired_ids.empty? MutedUser.where(user_id: user.id).destroy_all else - MutedUser.where('user_id = ? AND muted_user_id not in (?)', user.id, desired_ids).destroy_all + MutedUser.where("user_id = ? AND muted_user_id not in (?)", user.id, desired_ids).destroy_all # SQL is easier here than figuring out how to do the same in AR DB.exec(<<~SQL, now: Time.now, user_id: user.id, desired_ids: desired_ids) @@ -290,7 +287,11 @@ class UserUpdater if desired_ids.empty? AllowedPmUser.where(user_id: user.id).destroy_all else - AllowedPmUser.where('user_id = ? AND allowed_pm_user_id not in (?)', user.id, desired_ids).destroy_all + AllowedPmUser.where( + "user_id = ? AND allowed_pm_user_id not in (?)", + user.id, + desired_ids, + ).destroy_all # SQL is easier here than figuring out how to do the same in AR DB.exec(<<~SQL, now: Time.zone.now, user_id: user.id, desired_ids: desired_ids) @@ -305,7 +306,11 @@ class UserUpdater def updated_associated_accounts(associations) associations.each do |association| - user_associated_account = UserAssociatedAccount.find_or_initialize_by(user_id: user.id, provider_name: association[:provider_name]) + user_associated_account = + UserAssociatedAccount.find_or_initialize_by( + user_id: user.id, + provider_name: association[:provider_name], + ) if association[:provider_uid].present? user_associated_account.update!(provider_uid: association[:provider_uid]) else @@ -329,7 +334,10 @@ class UserUpdater sso = SingleSignOnRecord.find_or_initialize_by(user_id: user.id) if external_id.present? - sso.update!(external_id: discourse_connect[:external_id], last_payload: "external_id=#{discourse_connect[:external_id]}") + sso.update!( + external_id: discourse_connect[:external_id], + last_payload: "external_id=#{discourse_connect[:external_id]}", + ) else sso.destroy! end diff --git a/app/services/username_changer.rb b/app/services/username_changer.rb index bb2f7becc0..10f7d66c22 100644 --- a/app/services/username_changer.rb +++ b/app/services/username_changer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UsernameChanger - def initialize(user, new_username, actor = nil) @user = user @old_username = user.username @@ -37,23 +36,33 @@ class UsernameChanger StaffActionLogger.new(@actor).log_username_change(@user, @old_username, @new_username) end - UsernameChanger.update_username(user_id: @user.id, - old_username: @old_username, - new_username: @new_username, - avatar_template: @user.avatar_template_url, - asynchronous: asynchronous) if run_update_job + if run_update_job + UsernameChanger.update_username( + user_id: @user.id, + old_username: @old_username, + new_username: @new_username, + avatar_template: @user.avatar_template_url, + asynchronous: asynchronous, + ) + end return true end false end - def self.update_username(user_id:, old_username:, new_username:, avatar_template:, asynchronous: true) + def self.update_username( + user_id:, + old_username:, + new_username:, + avatar_template:, + asynchronous: true + ) args = { user_id: user_id, old_username: old_username, new_username: new_username, - avatar_template: avatar_template + avatar_template: avatar_template, } if asynchronous diff --git a/app/services/username_checker_service.rb b/app/services/username_checker_service.rb index e724e2bca2..3540d00715 100644 --- a/app/services/username_checker_service.rb +++ b/app/services/username_checker_service.rb @@ -17,11 +17,8 @@ class UsernameCheckerService end def check_username_availability(username, email) - available = User.username_available?( - username, - email, - allow_reserved_username: @allow_reserved_username - ) + available = + User.username_available?(username, email, allow_reserved_username: @allow_reserved_username) if available { available: true, is_developer: is_developer?(email) } @@ -31,11 +28,11 @@ class UsernameCheckerService end def is_developer?(value) - Rails.configuration.respond_to?(:developer_emails) && Rails.configuration.developer_emails.include?(value) + Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(value) end def self.is_developer?(email) UsernameCheckerService.new.is_developer?(email) end - end diff --git a/app/services/web_hook_emitter.rb b/app/services/web_hook_emitter.rb index d8ac652e1c..738026e48d 100644 --- a/app/services/web_hook_emitter.rb +++ b/app/services/web_hook_emitter.rb @@ -15,17 +15,13 @@ class WebHookEmitter request: { write_timeout: REQUEST_TIMEOUT, read_timeout: REQUEST_TIMEOUT, - open_timeout: REQUEST_TIMEOUT + open_timeout: REQUEST_TIMEOUT, }, } - if !@webhook.verify_certificate - connection_opts[:ssl] = { verify: false } - end + connection_opts[:ssl] = { verify: false } if !@webhook.verify_certificate - conn = Faraday.new(nil, connection_opts) do |f| - f.adapter FinalDestination::FaradayAdapter - end + conn = Faraday.new(nil, connection_opts) { |f| f.adapter FinalDestination::FaradayAdapter } start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) error = nil @@ -36,17 +32,15 @@ class WebHookEmitter error = e end duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - start - event_update_args = { - headers: MultiJson.dump(headers), - duration: duration, - } + event_update_args = { headers: MultiJson.dump(headers), duration: duration } if response event_update_args[:response_headers] = MultiJson.dump(response.headers) event_update_args[:response_body] = response.body event_update_args[:status] = response.status else event_update_args[:status] = -1 - if error.is_a?(Faraday::Error) && error.wrapped_exception.is_a?(FinalDestination::SSRFDetector::DisallowedIpError) + if error.is_a?(Faraday::Error) && + error.wrapped_exception.is_a?(FinalDestination::SSRFDetector::DisallowedIpError) error = I18n.t("webhooks.payload_url.blocked_or_internal") end event_update_args[:response_headers] = MultiJson.dump(error: error) diff --git a/app/services/wildcard_domain_checker.rb b/app/services/wildcard_domain_checker.rb index 89c6453882..2b2e983a48 100644 --- a/app/services/wildcard_domain_checker.rb +++ b/app/services/wildcard_domain_checker.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true module WildcardDomainChecker - def self.check_domain(domain, external_domain) - escaped_domain = domain[0] == "*" ? Regexp.escape(domain).sub("\\*", '\S*') : Regexp.escape(domain) - domain_regex = Regexp.new("\\A#{escaped_domain}\\z", 'i') + escaped_domain = + domain[0] == "*" ? Regexp.escape(domain).sub("\\*", '\S*') : Regexp.escape(domain) + domain_regex = Regexp.new("\\A#{escaped_domain}\\z", "i") external_domain.match(domain_regex) end - end diff --git a/app/services/wildcard_url_checker.rb b/app/services/wildcard_url_checker.rb index f90defe15d..5f9621c27b 100644 --- a/app/services/wildcard_url_checker.rb +++ b/app/services/wildcard_url_checker.rb @@ -5,7 +5,7 @@ module WildcardUrlChecker return false if !valid_url?(url_to_check) escaped_url = Regexp.escape(url).sub("\\*", '\S*') - url_regex = Regexp.new("\\A#{escaped_url}\\z", 'i') + url_regex = Regexp.new("\\A#{escaped_url}\\z", "i") url_to_check.match?(url_regex) end diff --git a/app/services/word_watcher.rb b/app/services/word_watcher.rb index 3967457e38..90529705c4 100644 --- a/app/services/word_watcher.rb +++ b/app/services/word_watcher.rb @@ -34,17 +34,18 @@ class WordWatcher def self.get_cached_words(action) if cache_enabled? - Discourse.cache.fetch(word_matcher_regexp_key(action), expires_in: 1.day) do - words_for_action(action).presence - end + Discourse + .cache + .fetch(word_matcher_regexp_key(action), expires_in: 1.day) do + words_for_action(action).presence + end else words_for_action(action).presence end end def self.serializable_word_matcher_regexp(action) - word_matcher_regexp_list(action) - .map { |r| { r.source => { case_sensitive: !r.casefold? } } } + word_matcher_regexp_list(action).map { |r| { r.source => { case_sensitive: !r.casefold? } } } end # This regexp is run in miniracer, and the client JS app @@ -64,9 +65,7 @@ class WordWatcher grouped_words[group_key] << word end - regexps = grouped_words - .select { |_, w| w.present? } - .transform_values { |w| w.join("|") } + regexps = grouped_words.select { |_, w| w.present? }.transform_values { |w| w.join("|") } if !SiteSetting.watched_words_regular_expressions? regexps.transform_values! do |regexp| @@ -75,8 +74,7 @@ class WordWatcher end end - regexps - .map { |c, regexp| Regexp.new(regexp, c == :case_sensitive ? nil : Regexp::IGNORECASE) } + regexps.map { |c, regexp| Regexp.new(regexp, c == :case_sensitive ? nil : Regexp::IGNORECASE) } rescue RegexpError raise if raise_errors [] # Admin will be alerted via admin_dashboard_data.rb @@ -113,7 +111,7 @@ class WordWatcher regexps = word_matcher_regexp_list(:censor) return html if regexps.blank? - doc = Nokogiri::HTML5::fragment(html) + doc = Nokogiri::HTML5.fragment(html) doc.traverse do |node| regexps.each do |regexp| node.content = censor_text_with_regexp(node.content, regexp) if node.text? @@ -150,9 +148,7 @@ class WordWatcher end def self.clear_cache! - WatchedWord.actions.each do |a, i| - Discourse.cache.delete word_matcher_regexp_key(a) - end + WatchedWord.actions.each { |a, i| Discourse.cache.delete word_matcher_regexp_key(a) } end def requires_approval? @@ -188,13 +184,15 @@ class WordWatcher if SiteSetting.watched_words_regular_expressions? set = Set.new - @raw.scan(regexp).each do |m| - if Array === m - set.add(m.find(&:present?)) - elsif String === m - set.add(m) + @raw + .scan(regexp) + .each do |m| + if Array === m + set.add(m.find(&:present?)) + elsif String === m + set.add(m) + end end - end matches = set.to_a else @@ -214,9 +212,10 @@ class WordWatcher end def word_matches?(word, case_sensitive: false) - Regexp - .new(WordWatcher.word_to_regexp(word, whole: true), case_sensitive ? nil : Regexp::IGNORECASE) - .match?(@raw) + Regexp.new( + WordWatcher.word_to_regexp(word, whole: true), + case_sensitive ? nil : Regexp::IGNORECASE, + ).match?(@raw) end def self.replace_text_with_regexp(text, regexp, replacement) diff --git a/app/views/users_email/show_confirm_new_email.html.erb b/app/views/users_email/show_confirm_new_email.html.erb index 59c1e9362d..071dc73130 100644 --- a/app/views/users_email/show_confirm_new_email.html.erb +++ b/app/views/users_email/show_confirm_new_email.html.erb @@ -4,7 +4,7 @@ <%= t 'change_email.confirmed' %>

    - "><%= t('change_email.please_continue', site_name: SiteSetting.title) %> + "><%= t('change_email.please_continue', site_name: SiteSetting.title) %>

    <% elsif @error %>
    diff --git a/config/application.rb b/config/application.rb index 1c69f3d99c..0c31d3a327 100644 --- a/config/application.rb +++ b/config/application.rb @@ -5,12 +5,12 @@ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.7.0") exit 1 end -require File.expand_path('../boot', __FILE__) -require 'active_record/railtie' -require 'action_controller/railtie' -require 'action_view/railtie' -require 'action_mailer/railtie' -require 'sprockets/railtie' +require File.expand_path("../boot", __FILE__) +require "active_record/railtie" +require "action_controller/railtie" +require "action_view/railtie" +require "action_mailer/railtie" +require "sprockets/railtie" if !Rails.env.production? recommended = File.read(".ruby-version.sample").strip @@ -20,66 +20,59 @@ if !Rails.env.production? end # Plugin related stuff -require_relative '../lib/plugin' -require_relative '../lib/discourse_event' -require_relative '../lib/discourse_plugin_registry' +require_relative "../lib/plugin" +require_relative "../lib/discourse_event" +require_relative "../lib/discourse_plugin_registry" -require_relative '../lib/plugin_gem' +require_relative "../lib/plugin_gem" # Global config -require_relative '../app/models/global_setting' +require_relative "../app/models/global_setting" GlobalSetting.configure! if GlobalSetting.load_plugins? # Support for plugins to register custom setting providers. They can do this # by having a file, `register_provider.rb` in their root that will be run # at this point. - Dir.glob(File.join(File.dirname(__FILE__), '../plugins', '*', "register_provider.rb")) do |p| + Dir.glob(File.join(File.dirname(__FILE__), "../plugins", "*", "register_provider.rb")) do |p| require p end end GlobalSetting.load_defaults -if GlobalSetting.try(:cdn_url).present? && GlobalSetting.cdn_url !~ /^https?:\/\// +if GlobalSetting.try(:cdn_url).present? && GlobalSetting.cdn_url !~ %r{^https?://} STDERR.puts "WARNING: Your CDN URL does not begin with a protocol like `https://` - this is probably not going to work" end -if ENV['SKIP_DB_AND_REDIS'] == '1' +if ENV["SKIP_DB_AND_REDIS"] == "1" GlobalSetting.skip_db = true GlobalSetting.skip_redis = true end -if !GlobalSetting.skip_db? - require 'rails_failover/active_record' -end +require "rails_failover/active_record" if !GlobalSetting.skip_db? -if !GlobalSetting.skip_redis? - require 'rails_failover/redis' -end +require "rails_failover/redis" if !GlobalSetting.skip_redis? -require 'pry-rails' if Rails.env.development? -require 'pry-byebug' if Rails.env.development? +require "pry-rails" if Rails.env.development? +require "pry-byebug" if Rails.env.development? -require 'discourse_fonts' +require "discourse_fonts" -require_relative '../lib/ember_cli' +require_relative "../lib/ember_cli" if defined?(Bundler) bundler_groups = [:default] if !Rails.env.production? - bundler_groups = bundler_groups.concat(Rails.groups( - assets: %w(development test profile) - )) + bundler_groups = bundler_groups.concat(Rails.groups(assets: %w[development test profile])) end Bundler.require(*bundler_groups) end -require_relative '../lib/require_dependency_backward_compatibility' +require_relative "../lib/require_dependency_backward_compatibility" module Discourse class Application < Rails::Application - def config.database_configuration if Rails.env.production? GlobalSetting.database_config @@ -91,18 +84,23 @@ module Discourse # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. - require 'discourse' - require 'js_locale_helper' + require "discourse" + require "js_locale_helper" # tiny file needed by site settings - require 'highlight_js' + require "highlight_js" config.load_defaults 6.1 config.active_record.cache_versioning = false # our custom cache class doesn’t support this config.action_controller.forgery_protection_origin_check = false config.active_record.belongs_to_required_by_default = false config.active_record.legacy_connection_handling = true - config.active_record.yaml_column_permitted_classes = [Hash, HashWithIndifferentAccess, Time, Symbol] + config.active_record.yaml_column_permitted_classes = [ + Hash, + HashWithIndifferentAccess, + Time, + Symbol, + ] # we skip it cause we configure it in the initializer # the railtie for message_bus would insert it in the @@ -111,7 +109,8 @@ module Discourse config.skip_multisite_middleware = true config.skip_rails_failover_active_record_middleware = true - multisite_config_path = ENV['DISCOURSE_MULTISITE_CONFIG_PATH'] || GlobalSetting.multisite_config_path + multisite_config_path = + ENV["DISCOURSE_MULTISITE_CONFIG_PATH"] || GlobalSetting.multisite_config_path config.multisite_config_path = File.absolute_path(multisite_config_path, Rails.root) # Custom directories with classes and modules you want to be autoloadable. @@ -129,14 +128,14 @@ module Discourse # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - config.time_zone = 'UTC' + config.time_zone = "UTC" # auto-load locales in plugins # NOTE: we load both client & server locales since some might be used by PrettyText config.i18n.load_path += Dir["#{Rails.root}/plugins/*/config/locales/*.yml"] # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = 'utf-8' + config.encoding = "utf-8" # see: http://stackoverflow.com/questions/11894180/how-does-one-correctly-add-custom-sql-dml-in-migrations/11894420#11894420 config.active_record.schema_format = :sql @@ -145,7 +144,7 @@ module Discourse config.active_record.use_schema_cache_dump = false # per https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet - config.pbkdf2_iterations = 64000 + config.pbkdf2_iterations = 64_000 config.pbkdf2_algorithm = "sha256" # rack lock is nothing but trouble, get rid of it @@ -160,32 +159,39 @@ module Discourse # supports etags (post 1.7) config.middleware.delete Rack::ETag - if !(Rails.env.development? || ENV['SKIP_ENFORCE_HOSTNAME'] == "1") - require 'middleware/enforce_hostname' + if !(Rails.env.development? || ENV["SKIP_ENFORCE_HOSTNAME"] == "1") + require "middleware/enforce_hostname" config.middleware.insert_after Rack::MethodOverride, Middleware::EnforceHostname end - require 'content_security_policy/middleware' - config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, ContentSecurityPolicy::Middleware + require "content_security_policy/middleware" + config.middleware.swap ActionDispatch::ContentSecurityPolicy::Middleware, + ContentSecurityPolicy::Middleware - require 'middleware/discourse_public_exceptions' + require "middleware/discourse_public_exceptions" config.exceptions_app = Middleware::DiscoursePublicExceptions.new(Rails.public_path) - require 'discourse_js_processor' - require 'discourse_sourcemapping_url_processor' + require "discourse_js_processor" + require "discourse_sourcemapping_url_processor" - Sprockets.register_mime_type 'application/javascript', extensions: ['.js', '.es6', '.js.es6'], charset: :unicode - Sprockets.register_postprocessor 'application/javascript', DiscourseJsProcessor + Sprockets.register_mime_type "application/javascript", + extensions: %w[.js .es6 .js.es6], + charset: :unicode + Sprockets.register_postprocessor "application/javascript", DiscourseJsProcessor Discourse::Application.initializer :prepend_ember_assets do |app| # Needs to be in its own initializer so it runs after the append_assets_path initializer defined by Sprockets - app.config.assets.paths.unshift "#{app.config.root}/app/assets/javascripts/discourse/dist/assets" - Sprockets.unregister_postprocessor 'application/javascript', Sprockets::Rails::SourcemappingUrlProcessor - Sprockets.register_postprocessor 'application/javascript', DiscourseSourcemappingUrlProcessor + app + .config + .assets + .paths.unshift "#{app.config.root}/app/assets/javascripts/discourse/dist/assets" + Sprockets.unregister_postprocessor "application/javascript", + Sprockets::Rails::SourcemappingUrlProcessor + Sprockets.register_postprocessor "application/javascript", DiscourseSourcemappingUrlProcessor end - require 'discourse_redis' - require 'logster/redis_store' + require "discourse_redis" + require "logster/redis_store" # Use redis for our cache config.cache_store = DiscourseRedis.new_redis_store Discourse.redis = DiscourseRedis.new @@ -198,7 +204,7 @@ module Discourse # our setup does not use rack cache and instead defers to nginx config.action_dispatch.rack_cache = nil - require 'auth' + require "auth" if GlobalSetting.relative_url_root.present? config.relative_url_root = GlobalSetting.relative_url_root @@ -207,38 +213,28 @@ module Discourse if Rails.env.test? && GlobalSetting.load_plugins? Discourse.activate_plugins! elsif GlobalSetting.load_plugins? - Plugin.initialization_guard do - Discourse.activate_plugins! - end + Plugin.initialization_guard { Discourse.activate_plugins! } end # Use discourse-fonts gem to symlink fonts and generate .scss file - fonts_path = File.join(config.root, 'public/fonts') + fonts_path = File.join(config.root, "public/fonts") Discourse::Utils.atomic_ln_s(DiscourseFonts.path_for_fonts, fonts_path) - require 'stylesheet/manager' - require 'svg_sprite' + require "stylesheet/manager" + require "svg_sprite" config.after_initialize do # Load plugins - Plugin.initialization_guard do - Discourse.plugins.each(&:notify_after_initialize) - end + Plugin.initialization_guard { Discourse.plugins.each(&:notify_after_initialize) } # we got to clear the pool in case plugins connect ActiveRecord::Base.connection_handler.clear_active_connections! end - if ENV['RBTRACE'] == "1" - require 'rbtrace' - end + require "rbtrace" if ENV["RBTRACE"] == "1" - if ENV['RAILS_QUERY_LOG_TAGS'] == "1" - config.active_record.query_log_tags_enabled = true - end + config.active_record.query_log_tags_enabled = true if ENV["RAILS_QUERY_LOG_TAGS"] == "1" - config.generators do |g| - g.test_framework :rspec, fixture: false - end + config.generators { |g| g.test_framework :rspec, fixture: false } end end diff --git a/config/boot.rb b/config/boot.rb index 6866506f24..bf5d61d56d 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,48 +1,49 @@ # frozen_string_literal: true -if ENV['DISCOURSE_DUMP_HEAP'] == "1" - require 'objspace' +if ENV["DISCOURSE_DUMP_HEAP"] == "1" + require "objspace" ObjectSpace.trace_object_allocations_start end -require 'rubygems' +require "rubygems" # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", __FILE__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) -if (ENV['DISABLE_BOOTSNAP'] != '1') +if (ENV["DISABLE_BOOTSNAP"] != "1") begin - require 'bootsnap' + require "bootsnap" rescue LoadError # not a strong requirement end - if defined? Bootsnap + if defined?(Bootsnap) Bootsnap.setup( - cache_dir: 'tmp/cache', # Path to your cache - load_path_cache: true, # Should we optimize the LOAD_PATH with a cache? - compile_cache_iseq: true, # Should compile Ruby code into ISeq cache? - compile_cache_yaml: false # Skip YAML cache for now, cause we were seeing issues with it + cache_dir: "tmp/cache", # Path to your cache + load_path_cache: true, # Should we optimize the LOAD_PATH with a cache? + compile_cache_iseq: true, # Should compile Ruby code into ISeq cache? + compile_cache_yaml: false, # Skip YAML cache for now, cause we were seeing issues with it ) end end # Parallel spec system -if ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER'] - if ENV['TEST_ENV_NUMBER'] == '' +if ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"] + if ENV["TEST_ENV_NUMBER"] == "" n = 1 else - n = ENV['TEST_ENV_NUMBER'].to_i + n = ENV["TEST_ENV_NUMBER"].to_i end - port = 10000 + n + port = 10_000 + n STDERR.puts "Setting up parallel test mode - starting Redis #{n} on port #{port}" `rm -rf tmp/test_data_#{n} && mkdir -p tmp/test_data_#{n}/redis` - pid = Process.spawn("redis-server --dir tmp/test_data_#{n}/redis --port #{port}", out: "/dev/null") + pid = + Process.spawn("redis-server --dir tmp/test_data_#{n}/redis --port #{port}", out: "/dev/null") ENV["DISCOURSE_REDIS_PORT"] = port.to_s ENV["RAILS_DB"] = "discourse_test_#{n}" diff --git a/config/cloud/cloud66/files/production.rb b/config/cloud/cloud66/files/production.rb index f527db5d6a..2814996b68 100644 --- a/config/cloud/cloud66/files/production.rb +++ b/config/cloud/cloud66/files/production.rb @@ -7,7 +7,7 @@ Discourse::Application.configure do config.cache_classes = true # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false + config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) @@ -23,19 +23,20 @@ Discourse::Application.configure do config.assets.digest = true # Specifies the header that your server uses for sending files - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for nginx # you may use other configuration here for mail eg: sendgrid config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { - address: ENV['SMTP_ADDRESS'], - port: ENV['SMTP_PORT'], - domain: ENV['SMTP_DOMAIN'], - user_name: ENV['SMTP_USERNAME'], - password: ENV['SMTP_PASSWORD'], - authentication: 'plain', - enable_starttls_auto: true } + address: ENV["SMTP_ADDRESS"], + port: ENV["SMTP_PORT"], + domain: ENV["SMTP_DOMAIN"], + user_name: ENV["SMTP_USERNAME"], + password: ENV["SMTP_PASSWORD"], + authentication: "plain", + enable_starttls_auto: true, + } #config.action_mailer.delivery_method = :sendmail #config.action_mailer.sendmail_settings = {arguments: '-i'} @@ -59,5 +60,4 @@ Discourse::Application.configure do # Discourse strongly recommend you use a CDN. # For origin pull cdns all you need to do is register an account and configure # config.action_controller.asset_host = "http://YOUR_CDN_HERE" - end diff --git a/config/environment.rb b/config/environment.rb index 7bb5d95ae9..90ea3b4fa9 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -7,6 +7,7 @@ require_relative "application" Rails.application.initialize! # When in "dev" mode, ensure we won't be sending any emails -if Rails.env.development? && ActionMailer::Base.smtp_settings.slice(:address, :port) != { address: "localhost", port: 1025 } +if Rails.env.development? && + ActionMailer::Base.smtp_settings.slice(:address, :port) != { address: "localhost", port: 1025 } fail "In development mode, you should be using mailhog otherwise you might end up sending thousands of digest emails" end diff --git a/config/environments/development.rb b/config/environments/development.rb index 3e6496f1a9..62b3dd2843 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,7 +16,7 @@ Discourse::Application.configure do config.active_record.use_schema_cache_dump = true # Show full error reports and disable caching - config.consider_all_requests_local = true + config.consider_all_requests_local = true config.action_controller.perform_caching = false config.action_controller.asset_host = GlobalSetting.cdn_url @@ -32,27 +32,23 @@ Discourse::Application.configure do config.assets.debug = false - config.public_file_server.headers = { - 'Access-Control-Allow-Origin' => '*' - } + config.public_file_server.headers = { "Access-Control-Allow-Origin" => "*" } # Raise an error on page load if there are pending migrations config.active_record.migration_error = :page_load - config.watchable_dirs['lib'] = [:rb] + config.watchable_dirs["lib"] = [:rb] # we recommend you use mailhog https://github.com/mailhog/MailHog config.action_mailer.smtp_settings = { address: "localhost", port: 1025 } config.action_mailer.raise_delivery_errors = true - config.log_level = ENV['DISCOURSE_DEV_LOG_LEVEL'] if ENV['DISCOURSE_DEV_LOG_LEVEL'] + config.log_level = ENV["DISCOURSE_DEV_LOG_LEVEL"] if ENV["DISCOURSE_DEV_LOG_LEVEL"] - if ENV['RAILS_VERBOSE_QUERY_LOGS'] == "1" - config.active_record.verbose_query_logs = true - end + config.active_record.verbose_query_logs = true if ENV["RAILS_VERBOSE_QUERY_LOGS"] == "1" if defined?(BetterErrors) - BetterErrors::Middleware.allow_ip! ENV['TRUSTED_IP'] if ENV['TRUSTED_IP'] + BetterErrors::Middleware.allow_ip! ENV["TRUSTED_IP"] if ENV["TRUSTED_IP"] if defined?(Unicorn) && ENV["UNICORN_WORKERS"].to_i != 1 # BetterErrors doesn't work with multiple unicorn workers. Disable it to avoid confusion @@ -60,51 +56,44 @@ Discourse::Application.configure do end end - if !ENV["DISABLE_MINI_PROFILER"] - config.load_mini_profiler = true - end + config.load_mini_profiler = true if !ENV["DISABLE_MINI_PROFILER"] - if hosts = ENV['DISCOURSE_DEV_HOSTS'] + if hosts = ENV["DISCOURSE_DEV_HOSTS"] Discourse.deprecate("DISCOURSE_DEV_HOSTS is deprecated. Use RAILS_DEVELOPMENT_HOSTS instead.") config.hosts.concat(hosts.split(",")) end - require 'middleware/turbo_dev' + require "middleware/turbo_dev" config.middleware.insert 0, Middleware::TurboDev - require 'middleware/missing_avatars' + require "middleware/missing_avatars" config.middleware.insert 1, Middleware::MissingAvatars config.enable_anon_caching = false - if RUBY_ENGINE == "ruby" - require 'rbtrace' - end + require "rbtrace" if RUBY_ENGINE == "ruby" if emails = GlobalSetting.developer_emails config.developer_emails = emails.split(",").map(&:downcase).map(&:strip) end - if ENV["DISCOURSE_SKIP_CSS_WATCHER"] != "1" && (defined?(Rails::Server) || defined?(Puma) || defined?(Unicorn)) - require 'stylesheet/watcher' + if ENV["DISCOURSE_SKIP_CSS_WATCHER"] != "1" && + (defined?(Rails::Server) || defined?(Puma) || defined?(Unicorn)) + require "stylesheet/watcher" STDERR.puts "Starting CSS change watcher" @watcher = Stylesheet::Watcher.watch end config.after_initialize do - if ENV["RAILS_COLORIZE_LOGGING"] == "1" - config.colorize_logging = true - end + config.colorize_logging = true if ENV["RAILS_COLORIZE_LOGGING"] == "1" if ENV["RAILS_VERBOSE_QUERY_LOGS"] == "1" ActiveRecord::LogSubscriber.backtrace_cleaner.add_silencer do |line| - line =~ /lib\/freedom_patches/ + line =~ %r{lib/freedom_patches} end end - if ENV["RAILS_DISABLE_ACTIVERECORD_LOGS"] == "1" - ActiveRecord::Base.logger = nil - end + ActiveRecord::Base.logger = nil if ENV["RAILS_DISABLE_ACTIVERECORD_LOGS"] == "1" - if ENV['BULLET'] + if ENV["BULLET"] Bullet.enable = true Bullet.rails_logger = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index f5a5df8ff5..5b6cd92456 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -8,7 +8,7 @@ Discourse::Application.configure do config.eager_load = true # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false + config.consider_all_requests_local = false config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) @@ -34,19 +34,19 @@ Discourse::Application.configure do authentication: GlobalSetting.smtp_authentication, enable_starttls_auto: GlobalSetting.smtp_enable_start_tls, open_timeout: GlobalSetting.smtp_open_timeout, - read_timeout: GlobalSetting.smtp_read_timeout + read_timeout: GlobalSetting.smtp_read_timeout, } - settings[:openssl_verify_mode] = GlobalSetting.smtp_openssl_verify_mode if GlobalSetting.smtp_openssl_verify_mode + settings[ + :openssl_verify_mode + ] = GlobalSetting.smtp_openssl_verify_mode if GlobalSetting.smtp_openssl_verify_mode - if GlobalSetting.smtp_force_tls - settings[:tls] = true - end + settings[:tls] = true if GlobalSetting.smtp_force_tls config.action_mailer.smtp_settings = settings.compact else config.action_mailer.delivery_method = :sendmail - config.action_mailer.sendmail_settings = { arguments: '-i' } + config.action_mailer.sendmail_settings = { arguments: "-i" } end # Send deprecation notices to registered listeners diff --git a/config/environments/profile.rb b/config/environments/profile.rb index d8f55a44f5..05e526c573 100644 --- a/config/environments/profile.rb +++ b/config/environments/profile.rb @@ -11,7 +11,7 @@ Discourse::Application.configure do config.log_level = :info # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false + config.consider_all_requests_local = false config.action_controller.perform_caching = true # in profile mode we serve static assets @@ -27,7 +27,7 @@ Discourse::Application.configure do config.assets.digest = true # Specifies the header that your server uses for sending files - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for nginx # we recommend you use mailhog https://github.com/mailhog/MailHog config.action_mailer.smtp_settings = { address: "localhost", port: 1025 } @@ -39,9 +39,7 @@ Discourse::Application.configure do config.load_mini_profiler = false # we don't need full logster support, but need to keep it working - config.after_initialize do - Logster.logger = Rails.logger - end + config.after_initialize { Logster.logger = Rails.logger } # for profiling with perftools # config.middleware.use ::Rack::PerftoolsProfiler, default_printer: 'gif' diff --git a/config/environments/test.rb b/config/environments/test.rb index 85b20535d5..e80e4f7a64 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -46,21 +46,22 @@ Discourse::Application.configure do config.eager_load = false - if ENV['RAILS_ENABLE_TEST_LOG'] + if ENV["RAILS_ENABLE_TEST_LOG"] config.logger = Logger.new(STDOUT) - config.log_level = ENV['RAILS_TEST_LOG_LEVEL'].present? ? ENV['RAILS_TEST_LOG_LEVEL'].to_sym : :info + config.log_level = + ENV["RAILS_TEST_LOG_LEVEL"].present? ? ENV["RAILS_TEST_LOG_LEVEL"].to_sym : :info else config.logger = Logger.new(nil) config.log_level = :fatal end - if defined? RspecErrorTracker + if defined?(RspecErrorTracker) config.middleware.insert_after ActionDispatch::Flash, RspecErrorTracker end config.after_initialize do SiteSetting.defaults.tap do |s| - s.set_regardless_of_locale(:s3_upload_bucket, 'bucket') + s.set_regardless_of_locale(:s3_upload_bucket, "bucket") s.set_regardless_of_locale(:min_post_length, 5) s.set_regardless_of_locale(:min_first_post_length, 5) s.set_regardless_of_locale(:min_personal_message_post_length, 10) @@ -73,7 +74,7 @@ Discourse::Application.configure do s.set_regardless_of_locale(:allow_uncategorized_topics, true) # disable plugins - if ENV['LOAD_PLUGINS'] == '1' + if ENV["LOAD_PLUGINS"] == "1" s.set_regardless_of_locale(:discourse_narrative_bot_enabled, false) end end diff --git a/config/initializers/000-development_reload_warnings.rb b/config/initializers/000-development_reload_warnings.rb index ce0260a8ee..bc1f74718f 100644 --- a/config/initializers/000-development_reload_warnings.rb +++ b/config/initializers/000-development_reload_warnings.rb @@ -8,25 +8,35 @@ if Rails.env.development? && !Rails.configuration.cache_classes && Discourse.run *Dir["#{Rails.root}/app/*"].reject { |path| path.end_with? "/assets" }, "#{Rails.root}/config", "#{Rails.root}/lib", - "#{Rails.root}/plugins" + "#{Rails.root}/plugins", ] - Listen.to(*paths, only: /\.rb$/) do |modified, added, removed| - supervisor_pid = UNICORN_DEV_SUPERVISOR_PID - auto_restart = supervisor_pid && ENV["AUTO_RESTART"] != "0" + Listen + .to(*paths, only: /\.rb$/) do |modified, added, removed| + supervisor_pid = UNICORN_DEV_SUPERVISOR_PID + auto_restart = supervisor_pid && ENV["AUTO_RESTART"] != "0" - files = modified + added + removed + files = modified + added + removed - not_autoloaded = files.filter_map do |file| - autoloaded = Rails.autoloaders.main.autoloads.key? file - Pathname.new(file).relative_path_from(Rails.root) if !autoloaded + not_autoloaded = + files.filter_map do |file| + autoloaded = Rails.autoloaders.main.autoloads.key? file + Pathname.new(file).relative_path_from(Rails.root) if !autoloaded + end + + if not_autoloaded.length > 0 + message = + ( + if auto_restart + "Restarting server..." + else + "Server restart required. Automate this by setting AUTO_RESTART=1." + end + ) + STDERR.puts "[DEV]: Edited files which are not autoloaded. #{message}" + STDERR.puts not_autoloaded.map { |path| "- #{path}".indent(7) }.join("\n") + Process.kill("USR2", supervisor_pid) if auto_restart + end end - - if not_autoloaded.length > 0 - message = auto_restart ? "Restarting server..." : "Server restart required. Automate this by setting AUTO_RESTART=1." - STDERR.puts "[DEV]: Edited files which are not autoloaded. #{message}" - STDERR.puts not_autoloaded.map { |path| "- #{path}".indent(7) }.join("\n") - Process.kill("USR2", supervisor_pid) if auto_restart - end - end.start + .start end diff --git a/config/initializers/000-mini_sql.rb b/config/initializers/000-mini_sql.rb index 15bbb7de06..53a9b7aa94 100644 --- a/config/initializers/000-mini_sql.rb +++ b/config/initializers/000-mini_sql.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -require 'mini_sql_multisite_connection' +require "mini_sql_multisite_connection" ::DB = MiniSqlMultisiteConnection.instance diff --git a/config/initializers/000-post_migration.rb b/config/initializers/000-post_migration.rb index 31d5370e97..2029d9a9cd 100644 --- a/config/initializers/000-post_migration.rb +++ b/config/initializers/000-post_migration.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true unless Discourse.skip_post_deployment_migrations? - ActiveRecord::Tasks::DatabaseTasks.migrations_paths << Rails.root.join( - Discourse::DB_POST_MIGRATE_PATH - ).to_s + ActiveRecord::Tasks::DatabaseTasks.migrations_paths << Rails + .root + .join(Discourse::DB_POST_MIGRATE_PATH) + .to_s end diff --git a/config/initializers/000-trace_pg_connections.rb b/config/initializers/000-trace_pg_connections.rb index ccfa760008..6a04daf399 100644 --- a/config/initializers/000-trace_pg_connections.rb +++ b/config/initializers/000-trace_pg_connections.rb @@ -15,31 +15,32 @@ # Warning: this could create some very large files! if ENV["TRACE_PG_CONNECTIONS"] - PG::Connection.prepend(Module.new do - TRACE_DIR = "tmp/pgtrace" + PG::Connection.prepend( + Module.new do + TRACE_DIR = "tmp/pgtrace" - def initialize(*args) - super(*args).tap do - next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server? - FileUtils.mkdir_p(TRACE_DIR) - @trace_filename = "#{TRACE_DIR}/#{Process.pid}_#{self.object_id}.txt" - trace File.new(@trace_filename, "w") + def initialize(*args) + super(*args).tap do + next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server? + FileUtils.mkdir_p(TRACE_DIR) + @trace_filename = "#{TRACE_DIR}/#{Process.pid}_#{self.object_id}.txt" + trace File.new(@trace_filename, "w") + end + @access_log_mutex = Mutex.new + @accessor_thread = nil end - @access_log_mutex = Mutex.new - @accessor_thread = nil - end - def close - super.tap do - next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server? - File.delete(@trace_filename) + def close + super.tap do + next if ENV["TRACE_PG_CONNECTIONS"] == "SIDEKIQ" && !Sidekiq.server? + File.delete(@trace_filename) + end end - end - def log_access(&blk) - @access_log_mutex.synchronize do - if !@accessor_thread.nil? - Rails.logger.error <<~TEXT + def log_access(&blk) + @access_log_mutex.synchronize do + if !@accessor_thread.nil? + Rails.logger.error <<~TEXT PG Clash: A connection is being accessed from two locations #{@accessor_thread} was using the connection. Backtrace: @@ -51,37 +52,38 @@ if ENV["TRACE_PG_CONNECTIONS"] #{Thread.current&.backtrace&.join("\n")} TEXT - if ENV["ON_PG_CLASH"] == "byebug" - require "byebug" - byebug # rubocop:disable Lint/Debugger + if ENV["ON_PG_CLASH"] == "byebug" + require "byebug" + byebug # rubocop:disable Lint/Debugger + end end + @accessor_thread = Thread.current end - @accessor_thread = Thread.current + yield + ensure + @access_log_mutex.synchronize { @accessor_thread = nil } end - yield - ensure - @access_log_mutex.synchronize do - @accessor_thread = nil - end - end - - end) + end, + ) class PG::Connection - LOG_ACCESS_METHODS = [:exec, :sync_exec, :async_exec, - :sync_exec_params, :async_exec_params, - :sync_prepare, :async_prepare, - :sync_exec_prepared, :async_exec_prepared, - ] + LOG_ACCESS_METHODS = %i[ + exec + sync_exec + async_exec + sync_exec_params + async_exec_params + sync_prepare + async_prepare + sync_exec_prepared + async_exec_prepared + ] LOG_ACCESS_METHODS.each do |method| new_method = "#{method}_without_logging".to_sym alias_method new_method, method - define_method(method) do |*args, &blk| - log_access { send(new_method, *args, &blk) } - end + define_method(method) { |*args, &blk| log_access { send(new_method, *args, &blk) } } end end - end diff --git a/config/initializers/000-zeitwerk.rb b/config/initializers/000-zeitwerk.rb index 130ba2d5c7..13131cbb01 100644 --- a/config/initializers/000-zeitwerk.rb +++ b/config/initializers/000-zeitwerk.rb @@ -11,7 +11,7 @@ module DiscourseInflector def self.camelize(basename, abspath) return basename.camelize if abspath.ends_with?("onceoff.rb") - return 'Jobs' if abspath.ends_with?("jobs/base.rb") + return "Jobs" if abspath.ends_with?("jobs/base.rb") @overrides[basename] || basename.camelize end @@ -26,27 +26,29 @@ Rails.autoloaders.each do |autoloader| # We have filenames that do not follow Zeitwerk's camelization convention. Maintain an inflections for these files # for now until we decide to fix them one day. autoloader.inflector.inflect( - 'canonical_url' => 'CanonicalURL', - 'clean_up_unmatched_ips' => 'CleanUpUnmatchedIPs', - 'homepage_constraint' => 'HomePageConstraint', - 'ip_addr' => 'IPAddr', - 'onpdiff' => 'ONPDiff', - 'pop3_polling_enabled_setting_validator' => 'POP3PollingEnabledSettingValidator', - 'version' => 'Discourse', - 'onceoff' => 'Jobs', - 'regular' => 'Jobs', - 'scheduled' => 'Jobs', - 'google_oauth2_authenticator' => 'GoogleOAuth2Authenticator', - 'omniauth_strategies' => 'OmniAuthStrategies', - 'csrf_token_verifier' => 'CSRFTokenVerifier', - 'html' => 'HTML', - 'json' => 'JSON', - 'ssrf_detector' => 'SSRFDetector', - 'http' => 'HTTP', + "canonical_url" => "CanonicalURL", + "clean_up_unmatched_ips" => "CleanUpUnmatchedIPs", + "homepage_constraint" => "HomePageConstraint", + "ip_addr" => "IPAddr", + "onpdiff" => "ONPDiff", + "pop3_polling_enabled_setting_validator" => "POP3PollingEnabledSettingValidator", + "version" => "Discourse", + "onceoff" => "Jobs", + "regular" => "Jobs", + "scheduled" => "Jobs", + "google_oauth2_authenticator" => "GoogleOAuth2Authenticator", + "omniauth_strategies" => "OmniAuthStrategies", + "csrf_token_verifier" => "CSRFTokenVerifier", + "html" => "HTML", + "json" => "JSON", + "ssrf_detector" => "SSRFDetector", + "http" => "HTTP", ) end -Rails.autoloaders.main.ignore("lib/tasks", - "lib/generators", - "lib/freedom_patches", - "lib/i18n/backend", - "lib/unicorn_logstash_patch.rb") +Rails.autoloaders.main.ignore( + "lib/tasks", + "lib/generators", + "lib/freedom_patches", + "lib/i18n/backend", + "lib/unicorn_logstash_patch.rb", +) diff --git a/config/initializers/001-redis.rb b/config/initializers/001-redis.rb index d61f467b28..1384e44467 100644 --- a/config/initializers/001-redis.rb +++ b/config/initializers/001-redis.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -if Rails.env.development? && ENV['DISCOURSE_FLUSH_REDIS'] +if Rails.env.development? && ENV["DISCOURSE_FLUSH_REDIS"] puts "Flushing redis (development mode)" Discourse.redis.flushdb end begin - if Gem::Version.new(Discourse.redis.info['redis_version']) < Gem::Version.new("6.2.0") + if Gem::Version.new(Discourse.redis.info["redis_version"]) < Gem::Version.new("6.2.0") STDERR.puts "Discourse requires Redis 6.2.0 or up" exit 1 end diff --git a/config/initializers/002-rails_failover.rb b/config/initializers/002-rails_failover.rb index 3f1463fdd8..276e6d951b 100644 --- a/config/initializers/002-rails_failover.rb +++ b/config/initializers/002-rails_failover.rb @@ -14,16 +14,12 @@ if defined?(RailsFailover::Redis) Discourse.request_refresh! MessageBus.keepalive_interval = message_bus_keepalive_interval - ObjectSpace.each_object(DistributedCache) do |cache| - cache.clear - end + ObjectSpace.each_object(DistributedCache) { |cache| cache.clear } SiteSetting.refresh! end - if Rails.logger.respond_to? :chained - RailsFailover::Redis.logger = Rails.logger.chained.first - end + RailsFailover::Redis.logger = Rails.logger.chained.first if Rails.logger.respond_to? :chained end if defined?(RailsFailover::ActiveRecord) @@ -73,10 +69,7 @@ if defined?(RailsFailover::ActiveRecord) end RailsFailover::ActiveRecord.register_force_reading_role_callback do - Discourse.redis.exists?( - Discourse::PG_READONLY_MODE_KEY, - Discourse::PG_FORCE_READONLY_MODE_KEY - ) + Discourse.redis.exists?(Discourse::PG_READONLY_MODE_KEY, Discourse::PG_FORCE_READONLY_MODE_KEY) rescue => e if !e.is_a?(Redis::CannotConnectError) Rails.logger.warn "#{e.class} #{e.message}: #{e.backtrace.join("\n")}" diff --git a/config/initializers/004-message_bus.rb b/config/initializers/004-message_bus.rb index 46a531e3f7..2fa74be763 100644 --- a/config/initializers/004-message_bus.rb +++ b/config/initializers/004-message_bus.rb @@ -30,7 +30,8 @@ def setup_message_bus_env(env) extra_headers = { "Access-Control-Allow-Origin" => Discourse.base_url_no_prefix, "Access-Control-Allow-Methods" => "GET, POST", - "Access-Control-Allow-Headers" => "X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Present", + "Access-Control-Allow-Headers" => + "X-SILENCE-LOGGER, X-Shared-Session-Key, Dont-Chunk, Discourse-Present", "Access-Control-Max-Age" => "7200", } @@ -40,7 +41,7 @@ def setup_message_bus_env(env) rescue Discourse::InvalidAccess => e # this is bad we need to remove the cookie if e.opts[:delete_cookie].present? - extra_headers['Set-Cookie'] = '_t=del; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT' + extra_headers["Set-Cookie"] = "_t=del; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT" end rescue => e Discourse.warn_exception(e, message: "Unexpected error in Message Bus", env: env) @@ -51,23 +52,22 @@ def setup_message_bus_env(env) raise Discourse::InvalidAccess if !user_id && SiteSetting.login_required is_admin = !!(user && user.admin?) - group_ids = if is_admin - # special rule, admin is allowed access to all groups - Group.pluck(:id) - elsif user - user.groups.pluck('groups.id') - end + group_ids = + if is_admin + # special rule, admin is allowed access to all groups + Group.pluck(:id) + elsif user + user.groups.pluck("groups.id") + end - if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] - extra_headers['Discourse-Logged-Out'] = '1' - end + extra_headers["Discourse-Logged-Out"] = "1" if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] hash = { extra_headers: extra_headers, user_id: user_id, group_ids: group_ids, is_admin: is_admin, - site_id: RailsMultisite::ConnectionManagement.current_db + site_id: RailsMultisite::ConnectionManagement.current_db, } env["__mb"] = hash end @@ -99,7 +99,7 @@ MessageBus.on_middleware_error do |env, e| if Discourse::InvalidAccess === e [403, {}, ["Invalid Access"]] elsif RateLimiter::LimitExceeded === e - [429, { 'Retry-After' => e.available_in.to_s }, [e.description]] + [429, { "Retry-After" => e.available_in.to_s }, [e.description]] end end @@ -119,8 +119,9 @@ end MessageBus.backend_instance.max_backlog_size = GlobalSetting.message_bus_max_backlog_size MessageBus.backend_instance.clear_every = GlobalSetting.message_bus_clear_every -MessageBus.long_polling_enabled = GlobalSetting.enable_long_polling.nil? ? true : GlobalSetting.enable_long_polling -MessageBus.long_polling_interval = GlobalSetting.long_polling_interval || 25000 +MessageBus.long_polling_enabled = + GlobalSetting.enable_long_polling.nil? ? true : GlobalSetting.enable_long_polling +MessageBus.long_polling_interval = GlobalSetting.long_polling_interval || 25_000 if Rails.env == "test" || $0 =~ /rake$/ # disable keepalive in testing diff --git a/config/initializers/005-site_settings.rb b/config/initializers/005-site_settings.rb index 9286affcbf..c05c4709f7 100644 --- a/config/initializers/005-site_settings.rb +++ b/config/initializers/005-site_settings.rb @@ -6,7 +6,7 @@ Discourse.git_version if GlobalSetting.skip_redis? - require 'site_settings/local_process_provider' + require "site_settings/local_process_provider" Rails.cache = Discourse.cache Rails.application.config.to_prepare do SiteSetting.provider = SiteSettings::LocalProcessProvider.new @@ -19,7 +19,8 @@ Rails.application.config.to_prepare do begin SiteSetting.refresh! - unless String === SiteSetting.push_api_secret_key && SiteSetting.push_api_secret_key.length == 32 + unless String === SiteSetting.push_api_secret_key && + SiteSetting.push_api_secret_key.length == 32 SiteSetting.push_api_secret_key = SecureRandom.hex end rescue ActiveRecord::StatementInvalid diff --git a/config/initializers/006-ensure_login_hint.rb b/config/initializers/006-ensure_login_hint.rb index 06debac756..830ed10d7f 100644 --- a/config/initializers/006-ensure_login_hint.rb +++ b/config/initializers/006-ensure_login_hint.rb @@ -5,17 +5,15 @@ return if GlobalSetting.skip_db? Rails.application.config.to_prepare do # Some sanity checking so we don't count on an unindexed column on boot begin - if ActiveRecord::Base.connection.table_exists?(:users) && - User.limit(20).count < 20 && - User.where(admin: true).human_users.count == 0 - + if ActiveRecord::Base.connection.table_exists?(:users) && User.limit(20).count < 20 && + User.where(admin: true).human_users.count == 0 notice = if GlobalSetting.developer_emails.blank? "Congratulations, you installed Discourse! Unfortunately, no administrator emails were defined during setup, so finalizing the configuration may be difficult." else emails = GlobalSetting.developer_emails.split(",") if emails.length > 1 - emails = emails[0..-2].join(', ') << " or #{emails[-1]} " + emails = emails[0..-2].join(", ") << " or #{emails[-1]} " else emails = emails[0] end diff --git a/config/initializers/006-mini_profiler.rb b/config/initializers/006-mini_profiler.rb index 9642894352..2972a98110 100644 --- a/config/initializers/006-mini_profiler.rb +++ b/config/initializers/006-mini_profiler.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true # If Mini Profiler is included via gem -if Rails.configuration.respond_to?(:load_mini_profiler) && Rails.configuration.load_mini_profiler && RUBY_ENGINE == "ruby" - require 'rack-mini-profiler' - require 'stackprof' +if Rails.configuration.respond_to?(:load_mini_profiler) && Rails.configuration.load_mini_profiler && + RUBY_ENGINE == "ruby" + require "rack-mini-profiler" + require "stackprof" begin - require 'memory_profiler' + require "memory_profiler" rescue => e STDERR.put "#{e} failed to require mini profiler" end @@ -20,55 +21,56 @@ if defined?(Rack::MiniProfiler) && defined?(Rack::MiniProfiler::Config) # raw_connection means results are not namespaced # # namespacing gets complex, cause mini profiler is in the rack chain way before multisite - Rack::MiniProfiler.config.storage_instance = Rack::MiniProfiler::RedisStore.new( - connection: DiscourseRedis.new(nil, namespace: false) - ) + Rack::MiniProfiler.config.storage_instance = + Rack::MiniProfiler::RedisStore.new(connection: DiscourseRedis.new(nil, namespace: false)) Rack::MiniProfiler.config.snapshot_every_n_requests = GlobalSetting.mini_profiler_snapshots_period - Rack::MiniProfiler.config.snapshots_transport_destination_url = GlobalSetting.mini_profiler_snapshots_transport_url - Rack::MiniProfiler.config.snapshots_transport_auth_key = GlobalSetting.mini_profiler_snapshots_transport_auth_key + Rack::MiniProfiler.config.snapshots_transport_destination_url = + GlobalSetting.mini_profiler_snapshots_transport_url + Rack::MiniProfiler.config.snapshots_transport_auth_key = + GlobalSetting.mini_profiler_snapshots_transport_auth_key Rack::MiniProfiler.config.skip_paths = [ - /^\/message-bus/, - /^\/extra-locales/, - /topics\/timings/, + %r{^/message-bus}, + %r{^/extra-locales}, + %r{topics/timings}, /assets/, - /\/user_avatar\//, - /\/letter_avatar\//, - /\/letter_avatar_proxy\//, - /\/highlight-js\//, - /\/svg-sprite\//, + %r{/user_avatar/}, + %r{/letter_avatar/}, + %r{/letter_avatar_proxy/}, + %r{/highlight-js/}, + %r{/svg-sprite/}, /qunit/, - /srv\/status/, + %r{srv/status}, /commits-widget/, - /^\/cdn_asset/, - /^\/logs/, - /^\/site_customizations/, - /^\/uploads/, - /^\/secure-media-uploads/, - /^\/secure-uploads/, - /^\/javascripts\//, - /^\/images\//, - /^\/stylesheets\//, - /^\/favicon\/proxied/, - /^\/theme-javascripts/ + %r{^/cdn_asset}, + %r{^/logs}, + %r{^/site_customizations}, + %r{^/uploads}, + %r{^/secure-media-uploads}, + %r{^/secure-uploads}, + %r{^/javascripts/}, + %r{^/images/}, + %r{^/stylesheets/}, + %r{^/favicon/proxied}, + %r{^/theme-javascripts}, ] # we DO NOT WANT mini-profiler loading on anything but real desktops and laptops # so let's rule out all handheld, tablet, and mobile devices - Rack::MiniProfiler.config.pre_authorize_cb = lambda do |env| - env['HTTP_USER_AGENT'] !~ /iPad|iPhone|Android/ - end + Rack::MiniProfiler.config.pre_authorize_cb = + lambda { |env| env["HTTP_USER_AGENT"] !~ /iPad|iPhone|Android/ } # without a user provider our results will use the ip address for namespacing # with a load balancer in front this becomes really bad as some results can # be stored associated with ip1 as the user and retrieved using ip2 causing 404s - Rack::MiniProfiler.config.user_provider = lambda do |env| - request = Rack::Request.new(env) - id = request.cookies["_t"] || request.ip || "unknown" - id = id.to_s - # some security, lets not have these tokens floating about - Digest::MD5.hexdigest(id) - end + Rack::MiniProfiler.config.user_provider = + lambda do |env| + request = Rack::Request.new(env) + id = request.cookies["_t"] || request.ip || "unknown" + id = id.to_s + # some security, lets not have these tokens floating about + Digest::MD5.hexdigest(id) + end # Cookie path should be set to the base path so Discourse's session cookie path # does not get clobbered. @@ -77,15 +79,15 @@ if defined?(Rack::MiniProfiler) && defined?(Rack::MiniProfiler::Config) Rack::MiniProfiler.config.position = "right" Rack::MiniProfiler.config.backtrace_ignores ||= [] - Rack::MiniProfiler.config.backtrace_ignores << /lib\/rack\/message_bus.rb/ - Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/silence_logger/ - Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/quiet_logger/ + Rack::MiniProfiler.config.backtrace_ignores << %r{lib/rack/message_bus.rb} + Rack::MiniProfiler.config.backtrace_ignores << %r{config/initializers/silence_logger} + Rack::MiniProfiler.config.backtrace_ignores << %r{config/initializers/quiet_logger} - Rack::MiniProfiler.config.backtrace_includes = [/^\/?(app|config|lib|test|plugins)/] + Rack::MiniProfiler.config.backtrace_includes = [%r{^/?(app|config|lib|test|plugins)}] Rack::MiniProfiler.config.max_traces_to_show = 100 if Rails.env.development? - Rack::MiniProfiler.counter_method(Redis::Client, :call) { 'redis' } + Rack::MiniProfiler.counter_method(Redis::Client, :call) { "redis" } # Rack::MiniProfiler.counter_method(ActiveRecord::QueryMethods, 'build_arel') # Rack::MiniProfiler.counter_method(Array, 'uniq') # require "#{Rails.root}/vendor/backports/notification" @@ -115,10 +117,11 @@ if defined?(Rack::MiniProfiler) && defined?(Rack::MiniProfiler::Config) end if ENV["PRINT_EXCEPTIONS"] - trace = TracePoint.new(:raise) do |tp| - puts tp.raised_exception - puts tp.raised_exception.backtrace.join("\n") - puts - end + trace = + TracePoint.new(:raise) do |tp| + puts tp.raised_exception + puts tp.raised_exception.backtrace.join("\n") + puts + end trace.enable end diff --git a/config/initializers/008-rack-cors.rb b/config/initializers/008-rack-cors.rb index b03fb2568f..d499a38cc8 100644 --- a/config/initializers/008-rack-cors.rb +++ b/config/initializers/008-rack-cors.rb @@ -6,18 +6,17 @@ class Discourse::Cors def initialize(app, options = nil) @app = app if GlobalSetting.enable_cors && GlobalSetting.cors_origin.present? - @global_origins = GlobalSetting.cors_origin.split(',').map { |x| x.strip.chomp('/') } + @global_origins = GlobalSetting.cors_origin.split(",").map { |x| x.strip.chomp("/") } end end def call(env) - cors_origins = @global_origins || [] - cors_origins += SiteSetting.cors_origins.split('|') if SiteSetting.cors_origins.present? + cors_origins += SiteSetting.cors_origins.split("|") if SiteSetting.cors_origins.present? cors_origins = cors_origins.presence - if env['REQUEST_METHOD'] == ('OPTIONS') && env['HTTP_ACCESS_CONTROL_REQUEST_METHOD'] - return [200, Discourse::Cors.apply_headers(cors_origins, env, {}), []] + if env["REQUEST_METHOD"] == ("OPTIONS") && env["HTTP_ACCESS_CONTROL_REQUEST_METHOD"] + return 200, Discourse::Cors.apply_headers(cors_origins, env, {}), [] end env[Discourse::Cors::ORIGINS_ENV] = cors_origins if cors_origins @@ -31,21 +30,24 @@ class Discourse::Cors end def self.apply_headers(cors_origins, env, headers) - request_method = env['REQUEST_METHOD'] + request_method = env["REQUEST_METHOD"] - if env['REQUEST_PATH'] =~ /\/(javascripts|assets)\// && Discourse.is_cdn_request?(env, request_method) + if env["REQUEST_PATH"] =~ %r{/(javascripts|assets)/} && + Discourse.is_cdn_request?(env, request_method) Discourse.apply_cdn_headers(headers) elsif cors_origins origin = nil - if origin = env['HTTP_ORIGIN'] + if origin = env["HTTP_ORIGIN"] origin = nil unless cors_origins.include?(origin) end - headers['Access-Control-Allow-Origin'] = origin || cors_origins[0] - headers['Access-Control-Allow-Headers'] = 'Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization' - headers['Access-Control-Allow-Credentials'] = 'true' - headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS, DELETE' - headers['Access-Control-Max-Age'] = '7200' + headers["Access-Control-Allow-Origin"] = origin || cors_origins[0] + headers[ + "Access-Control-Allow-Headers" + ] = "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization" + headers["Access-Control-Allow-Credentials"] = "true" + headers["Access-Control-Allow-Methods"] = "POST, PUT, GET, OPTIONS, DELETE" + headers["Access-Control-Max-Age"] = "7200" end headers diff --git a/config/initializers/012-web_hook_events.rb b/config/initializers/012-web_hook_events.rb index fa08cd3aea..520f4ecd6f 100644 --- a/config/initializers/012-web_hook_events.rb +++ b/config/initializers/012-web_hook_events.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true -%i( - topic_recovered -).each do |event| - DiscourseEvent.on(event) do |topic, _| - WebHook.enqueue_topic_hooks(event, topic) - end +%i[topic_recovered].each do |event| + DiscourseEvent.on(event) { |topic, _| WebHook.enqueue_topic_hooks(event, topic) } end DiscourseEvent.on(:topic_status_updated) do |topic, status| @@ -16,18 +12,12 @@ DiscourseEvent.on(:topic_created) do |topic, _, _| WebHook.enqueue_topic_hooks(:topic_created, topic) end -%i( - post_created - post_recovered -).each do |event| - DiscourseEvent.on(event) do |post, _, _| - WebHook.enqueue_post_hooks(event, post) - end +%i[post_created post_recovered].each do |event| + DiscourseEvent.on(event) { |post, _, _| WebHook.enqueue_post_hooks(event, post) } end DiscourseEvent.on(:post_edited) do |post, topic_changed| unless post.topic&.trashed? - # if we are editing the OP and the topic is changed, do not send # the post_edited event -- this event is sent separately because # when we update the OP in the UI we send two API calls in this order: @@ -42,49 +32,30 @@ DiscourseEvent.on(:post_edited) do |post, topic_changed| end end -%i( +%i[ user_logged_out user_created user_logged_in user_approved user_updated user_confirmed_email -).each do |event| - DiscourseEvent.on(event) do |user| - WebHook.enqueue_object_hooks(:user, user, event) - end +].each do |event| + DiscourseEvent.on(event) { |user| WebHook.enqueue_object_hooks(:user, user, event) } end -%i( - group_created - group_updated -).each do |event| - DiscourseEvent.on(event) do |group| - WebHook.enqueue_object_hooks(:group, group, event) - end +%i[group_created group_updated].each do |event| + DiscourseEvent.on(event) { |group| WebHook.enqueue_object_hooks(:group, group, event) } end -%i( - category_created - category_updated -).each do |event| - DiscourseEvent.on(event) do |category| - WebHook.enqueue_object_hooks(:category, category, event) - end +%i[category_created category_updated].each do |event| + DiscourseEvent.on(event) { |category| WebHook.enqueue_object_hooks(:category, category, event) } end -%i( - tag_created - tag_updated -).each do |event| - DiscourseEvent.on(event) do |tag| - WebHook.enqueue_object_hooks(:tag, tag, event, TagSerializer) - end +%i[tag_created tag_updated].each do |event| + DiscourseEvent.on(event) { |tag| WebHook.enqueue_object_hooks(:tag, tag, event, TagSerializer) } end -%i( - user_badge_granted -).each do |event| +%i[user_badge_granted].each do |event| # user_badge_revoked DiscourseEvent.on(event) do |badge, user_id| ub = UserBadge.find_by(badge: badge, user_id: user_id) @@ -92,30 +63,43 @@ end end end -%i( - reviewable_created - reviewable_score_updated -).each do |event| +%i[reviewable_created reviewable_score_updated].each do |event| DiscourseEvent.on(event) do |reviewable| WebHook.enqueue_object_hooks(:reviewable, reviewable, event, reviewable.serializer) end end DiscourseEvent.on(:reviewable_transitioned_to) do |status, reviewable| - WebHook.enqueue_object_hooks(:reviewable, reviewable, :reviewable_transitioned_to, reviewable.serializer) + WebHook.enqueue_object_hooks( + :reviewable, + reviewable, + :reviewable_transitioned_to, + reviewable.serializer, + ) end DiscourseEvent.on(:notification_created) do |notification| - WebHook.enqueue_object_hooks(:notification, notification, :notification_created, NotificationSerializer) + WebHook.enqueue_object_hooks( + :notification, + notification, + :notification_created, + NotificationSerializer, + ) end DiscourseEvent.on(:user_added_to_group) do |user, group, options| group_user = GroupUser.find_by(user: user, group: group) - WebHook.enqueue_object_hooks(:group_user, group_user, :user_added_to_group, WebHookGroupUserSerializer) + WebHook.enqueue_object_hooks( + :group_user, + group_user, + :user_added_to_group, + WebHookGroupUserSerializer, + ) end DiscourseEvent.on(:user_promoted) do |payload| - user_id, new_trust_level, old_trust_level = payload.values_at(:user_id, :new_trust_level, :old_trust_level) + user_id, new_trust_level, old_trust_level = + payload.values_at(:user_id, :new_trust_level, :old_trust_level) next if new_trust_level < old_trust_level @@ -130,8 +114,13 @@ DiscourseEvent.on(:like_created) do |post_action| category_id = topic&.category_id tag_ids = topic&.tag_ids - WebHook.enqueue_object_hooks(:like, - post_action, :post_liked, WebHookLikeSerializer, - group_ids: group_ids, category_id: category_id, tag_ids: tag_ids + WebHook.enqueue_object_hooks( + :like, + post_action, + :post_liked, + WebHookLikeSerializer, + group_ids: group_ids, + category_id: category_id, + tag_ids: tag_ids, ) end diff --git a/config/initializers/013-excon_defaults.rb b/config/initializers/013-excon_defaults.rb index 40389051db..302dbda499 100644 --- a/config/initializers/013-excon_defaults.rb +++ b/config/initializers/013-excon_defaults.rb @@ -1,4 +1,4 @@ # frozen_string_literal: true -require 'excon' +require "excon" Excon::DEFAULTS[:omit_default_port] = true diff --git a/config/initializers/014-track-setting-changes.rb b/config/initializers/014-track-setting-changes.rb index ae4705bb21..410b8091d7 100644 --- a/config/initializers/014-track-setting-changes.rb +++ b/config/initializers/014-track-setting-changes.rb @@ -6,10 +6,11 @@ DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value| # Enabling `must_approve_users` on an existing site is odd, so we assume that the # existing users are approved. if name == :must_approve_users && new_value == true - - User.where(approved: false) + User + .where(approved: false) .joins("LEFT JOIN reviewables r ON r.target_id = users.id") - .where(r: { id: nil }).update_all(approved: true) + .where(r: { id: nil }) + .update_all(approved: true) end if name == :emoji_set @@ -19,31 +20,28 @@ DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value| after = "/images/emoji/#{new_value}/" Scheduler::Defer.later("Fix Emoji Links") do - DB.exec("UPDATE posts SET cooked = REPLACE(cooked, :before, :after) WHERE cooked LIKE :like", + DB.exec( + "UPDATE posts SET cooked = REPLACE(cooked, :before, :after) WHERE cooked LIKE :like", before: before, after: after, - like: "%#{before}%" + like: "%#{before}%", ) end end - Stylesheet::Manager.clear_color_scheme_cache! if [:base_font, :heading_font].include?(name) + Stylesheet::Manager.clear_color_scheme_cache! if %i[base_font heading_font].include?(name) - Report.clear_cache(:storage_stats) if [:backup_location, :s3_backup_bucket].include?(name) + Report.clear_cache(:storage_stats) if %i[backup_location s3_backup_bucket].include?(name) if name == :slug_generation_method - Scheduler::Defer.later("Null topic slug") do - Topic.update_all(slug: nil) - end + Scheduler::Defer.later("Null topic slug") { Topic.update_all(slug: nil) } end - Jobs.enqueue(:update_s3_inventory) if [:enable_s3_inventory, :s3_upload_bucket].include?(name) + Jobs.enqueue(:update_s3_inventory) if %i[enable_s3_inventory s3_upload_bucket].include?(name) SvgSprite.expire_cache if name.to_s.include?("_icon") - if SiteIconManager::WATCHED_SETTINGS.include?(name) - SiteIconManager.ensure_optimized! - end + SiteIconManager.ensure_optimized! if SiteIconManager::WATCHED_SETTINGS.include?(name) # Make sure medium and high priority thresholds were calculated. if name == :reviewable_low_priority_threshold && Reviewable.min_score_for_priority(:medium) > 0 diff --git a/config/initializers/099-anon-cache.rb b/config/initializers/099-anon-cache.rb index 32bca43206..ee5e240d93 100644 --- a/config/initializers/099-anon-cache.rb +++ b/config/initializers/099-anon-cache.rb @@ -9,7 +9,7 @@ enabled = Rails.env.production? end -if !ENV['DISCOURSE_DISABLE_ANON_CACHE'] && enabled +if !ENV["DISCOURSE_DISABLE_ANON_CACHE"] && enabled # in an ideal world this is position 0, but mobile detection uses ... session and request and params Rails.configuration.middleware.insert_after ActionDispatch::Flash, Middleware::AnonymousCache end diff --git a/config/initializers/099-drain_pool.rb b/config/initializers/099-drain_pool.rb index e69de29bb2..8b13789179 100644 --- a/config/initializers/099-drain_pool.rb +++ b/config/initializers/099-drain_pool.rb @@ -0,0 +1 @@ + diff --git a/config/initializers/100-i18n.rb b/config/initializers/100-i18n.rb index 4165bd75b6..473e2f4933 100644 --- a/config/initializers/100-i18n.rb +++ b/config/initializers/100-i18n.rb @@ -2,8 +2,8 @@ # order: after 02-freedom_patches.rb -require 'i18n/backend/discourse_i18n' -require 'i18n/backend/fallback_locale_list' +require "i18n/backend/discourse_i18n" +require "i18n/backend/fallback_locale_list" # Requires the `translate_accelerator.rb` freedom patch to be loaded Rails.application.reloader.to_prepare do @@ -11,7 +11,7 @@ Rails.application.reloader.to_prepare do I18n.fallbacks = I18n::Backend::FallbackLocaleList.new I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) } I18n.reload! - I18n.init_accelerator!(overrides_enabled: ENV['DISABLE_TRANSLATION_OVERRIDES'] != '1') + I18n.init_accelerator!(overrides_enabled: ENV["DISABLE_TRANSLATION_OVERRIDES"] != "1") unless Rails.env.test? MessageBus.subscribe("/i18n-flush") do diff --git a/config/initializers/100-logster.rb b/config/initializers/100-logster.rb index 5047a01a63..8bbbc2a7a1 100644 --- a/config/initializers/100-logster.rb +++ b/config/initializers/100-logster.rb @@ -2,9 +2,7 @@ if GlobalSetting.skip_redis? Rails.application.reloader.to_prepare do - if Rails.logger.respond_to? :chained - Rails.logger = Rails.logger.chained.first - end + Rails.logger = Rails.logger.chained.first if Rails.logger.respond_to? :chained end return end @@ -39,9 +37,7 @@ if Rails.env.production? # https://github.com/rails/rails/blob/f2caed1e/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb#L39-L42 /^ActionController::RoutingError \(No route matches/, /^ActionDispatch::Http::MimeNegotiation::InvalidType/, - /^PG::Error: ERROR:\s+duplicate key/, - /^ActionController::UnknownFormat/, /^ActionController::UnknownHttpMethod/, /^AbstractController::ActionNotFound/, @@ -51,29 +47,21 @@ if Rails.env.production? # Column: # /(?m).*?Line: (?:\D|0).*?Column: (?:\D|0)/, - # suppress empty JS errors (covers MSIE 9, etc) /^(Syntax|Script) error.*Line: (0|1)\b/m, - # CSRF errors are not providing enough data # suppress unconditionally for now /^Can't verify CSRF token authenticity.$/, - # Yandex bot triggers this JS error a lot /^Uncaught ReferenceError: I18n is not defined/, - # related to browser plugins somehow, we don't care /Error calling method on NPObject/, - # 404s can be dealt with elsewhere /^ActiveRecord::RecordNotFound/, - # bad asset requested, no need to log /^ActionController::BadRequest/, - # we can't do anything about invalid parameters /Rack::QueryParser::InvalidParameterError/, - # we handle this cleanly in the message bus middleware # no point logging to logster /RateLimiter::LimitExceeded.*/m, @@ -98,33 +86,37 @@ store.redis_raw_connection = redis.without_namespace severities = [Logger::WARN, Logger::ERROR, Logger::FATAL, Logger::UNKNOWN] RailsMultisite::ConnectionManagement.each_connection do - error_rate_per_minute = SiteSetting.alert_admins_if_errors_per_minute rescue 0 + error_rate_per_minute = + begin + SiteSetting.alert_admins_if_errors_per_minute + rescue StandardError + 0 + end if (error_rate_per_minute || 0) > 0 store.register_rate_limit_per_minute(severities, error_rate_per_minute) do |rate| - MessageBus.publish("/logs_error_rate_exceeded", - { - rate: rate, - duration: 'minute', - publish_at: Time.current.to_i - }, - group_ids: [Group::AUTO_GROUPS[:admins]] - ) + MessageBus.publish( + "/logs_error_rate_exceeded", + { rate: rate, duration: "minute", publish_at: Time.current.to_i }, + group_ids: [Group::AUTO_GROUPS[:admins]], + ) end end - error_rate_per_hour = SiteSetting.alert_admins_if_errors_per_hour rescue 0 + error_rate_per_hour = + begin + SiteSetting.alert_admins_if_errors_per_hour + rescue StandardError + 0 + end if (error_rate_per_hour || 0) > 0 store.register_rate_limit_per_hour(severities, error_rate_per_hour) do |rate| - MessageBus.publish("/logs_error_rate_exceeded", - { - rate: rate, - duration: 'hour', - publish_at: Time.current.to_i, - }, - group_ids: [Group::AUTO_GROUPS[:admins]] - ) + MessageBus.publish( + "/logs_error_rate_exceeded", + { rate: rate, duration: "hour", publish_at: Time.current.to_i }, + group_ids: [Group::AUTO_GROUPS[:admins]], + ) end end end @@ -137,13 +129,13 @@ if Rails.configuration.multisite end Logster.config.project_directories = [ - { path: Rails.root.to_s, url: "https://github.com/discourse/discourse", main_app: true } + { path: Rails.root.to_s, url: "https://github.com/discourse/discourse", main_app: true }, ] Discourse.plugins.each do |plugin| next if !plugin.metadata.url Logster.config.project_directories << { path: "#{Rails.root.to_s}/plugins/#{plugin.directory_name}", - url: plugin.metadata.url + url: plugin.metadata.url, } end diff --git a/config/initializers/100-onebox_options.rb b/config/initializers/100-onebox_options.rb index 3d2e4a2f05..70f0a468e8 100644 --- a/config/initializers/100-onebox_options.rb +++ b/config/initializers/100-onebox_options.rb @@ -6,13 +6,13 @@ Rails.application.config.to_prepare do twitter_client: TwitterApi, redirect_limit: 3, user_agent: "Discourse Forum Onebox v#{Discourse::VERSION::STRING}", - allowed_ports: [80, 443, SiteSetting.port.to_i] + allowed_ports: [80, 443, SiteSetting.port.to_i], } else Onebox.options = { twitter_client: TwitterApi, redirect_limit: 3, - user_agent: "Discourse Forum Onebox v#{Discourse::VERSION::STRING}" + user_agent: "Discourse Forum Onebox v#{Discourse::VERSION::STRING}", } end end diff --git a/config/initializers/100-push-notifications.rb b/config/initializers/100-push-notifications.rb index 056e4bb56e..1261c465c6 100644 --- a/config/initializers/100-push-notifications.rb +++ b/config/initializers/100-push-notifications.rb @@ -3,13 +3,11 @@ return if GlobalSetting.skip_db? Rails.application.config.to_prepare do - require 'web-push' + require "web-push" def generate_vapid_key? - SiteSetting.vapid_public_key.blank? || - SiteSetting.vapid_private_key.blank? || - SiteSetting.vapid_public_key_bytes.blank? || - SiteSetting.vapid_base_url != Discourse.base_url + SiteSetting.vapid_public_key.blank? || SiteSetting.vapid_private_key.blank? || + SiteSetting.vapid_public_key_bytes.blank? || SiteSetting.vapid_base_url != Discourse.base_url end SiteSetting.vapid_base_url = Discourse.base_url if SiteSetting.vapid_base_url.blank? @@ -19,15 +17,12 @@ Rails.application.config.to_prepare do SiteSetting.vapid_public_key = vapid_key.public_key SiteSetting.vapid_private_key = vapid_key.private_key - SiteSetting.vapid_public_key_bytes = Base64.urlsafe_decode64(SiteSetting.vapid_public_key).bytes.join("|") + SiteSetting.vapid_public_key_bytes = + Base64.urlsafe_decode64(SiteSetting.vapid_public_key).bytes.join("|") SiteSetting.vapid_base_url = Discourse.base_url - if ActiveRecord::Base.connection.table_exists?(:push_subscriptions) - PushSubscription.delete_all - end + PushSubscription.delete_all if ActiveRecord::Base.connection.table_exists?(:push_subscriptions) end - DiscourseEvent.on(:user_logged_out) do |user| - PushNotificationPusher.clear_subscriptions(user) - end + DiscourseEvent.on(:user_logged_out) { |user| PushNotificationPusher.clear_subscriptions(user) } end diff --git a/config/initializers/100-quiet_logger.rb b/config/initializers/100-quiet_logger.rb index f73826f108..ef15649986 100644 --- a/config/initializers/100-quiet_logger.rb +++ b/config/initializers/100-quiet_logger.rb @@ -1,30 +1,23 @@ # frozen_string_literal: true -Rails.application.config.assets.configure do |env| - env.logger = Logger.new('/dev/null') -end +Rails.application.config.assets.configure { |env| env.logger = Logger.new("/dev/null") } module DiscourseRackQuietAssetsLogger def call(env) override = false - if (env['PATH_INFO'].index("/assets/") == 0) || - (env['PATH_INFO'].index("/stylesheets") == 0) || - (env['PATH_INFO'].index("/svg-sprite") == 0) || - (env['PATH_INFO'].index("/manifest") == 0) || - (env['PATH_INFO'].index("/service-worker") == 0) || - (env['PATH_INFO'].index("mini-profiler-resources") == 0) || - (env['PATH_INFO'].index("/srv/status") == 0) + if (env["PATH_INFO"].index("/assets/") == 0) || (env["PATH_INFO"].index("/stylesheets") == 0) || + (env["PATH_INFO"].index("/svg-sprite") == 0) || + (env["PATH_INFO"].index("/manifest") == 0) || + (env["PATH_INFO"].index("/service-worker") == 0) || + (env["PATH_INFO"].index("mini-profiler-resources") == 0) || + (env["PATH_INFO"].index("/srv/status") == 0) if ::Logster::Logger === Rails.logger override = true Rails.logger.override_level = Logger::ERROR end end - super(env).tap do - if override - Rails.logger.override_level = nil - end - end + super(env).tap { Rails.logger.override_level = nil if override } end end diff --git a/config/initializers/100-session_store.rb b/config/initializers/100-session_store.rb index b2ce5d47b4..b06e28dd63 100644 --- a/config/initializers/100-session_store.rb +++ b/config/initializers/100-session_store.rb @@ -4,8 +4,15 @@ Rails.application.config.session_store( :discourse_cookie_store, - key: '_forum_session', - path: (Rails.application.config.relative_url_root.nil?) ? '/' : Rails.application.config.relative_url_root + key: "_forum_session", + path: + ( + if (Rails.application.config.relative_url_root.nil?) + "/" + else + Rails.application.config.relative_url_root + end + ), ) Rails.application.config.to_prepare do diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index 79422e0b79..3ce1927722 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -1,22 +1,17 @@ # frozen_string_literal: true require "sidekiq/pausable" -require 'sidekiq_logster_reporter' +require "sidekiq_logster_reporter" -Sidekiq.configure_client do |config| - config.redis = Discourse.sidekiq_redis_config -end +Sidekiq.configure_client { |config| config.redis = Discourse.sidekiq_redis_config } Sidekiq.configure_server do |config| config.redis = Discourse.sidekiq_redis_config - config.server_middleware do |chain| - chain.add Sidekiq::Pausable - end + config.server_middleware { |chain| chain.add Sidekiq::Pausable } end if Sidekiq.server? - module Sidekiq class CLI private @@ -34,13 +29,17 @@ if Sidekiq.server? # warm up AR RailsMultisite::ConnectionManagement.safe_each_connection do (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end end scheduler_hostname = ENV["UNICORN_SCHEDULER_HOSTNAME"] - if !scheduler_hostname || scheduler_hostname.split(',').include?(Discourse.os_hostname) + if !scheduler_hostname || scheduler_hostname.split(",").include?(Discourse.os_hostname) begin MiniScheduler.start(workers: GlobalSetting.mini_scheduler_workers) rescue MiniScheduler::DistributedMutex::Timeout @@ -57,9 +56,11 @@ else # Instead, this patch adds a dedicated logger instance and patches # the #add method to forward messages to Rails.logger. Sidekiq.logger = Logger.new(nil) - Sidekiq.logger.define_singleton_method(:add) do |severity, message = nil, progname = nil, &blk| - Rails.logger.add(severity, message, progname, &blk) - end + Sidekiq + .logger + .define_singleton_method(:add) do |severity, message = nil, progname = nil, &blk| + Rails.logger.add(severity, message, progname, &blk) + end end Sidekiq.error_handlers.clear @@ -69,28 +70,20 @@ Sidekiq.strict_args! Rails.application.config.to_prepare do # Ensure that scheduled jobs are loaded before mini_scheduler is configured. - if Rails.env.development? - Dir.glob("#{Rails.root}/app/jobs/scheduled/*.rb") do |f| - require(f) - end - end + Dir.glob("#{Rails.root}/app/jobs/scheduled/*.rb") { |f| require(f) } if Rails.env.development? MiniScheduler.configure do |config| config.redis = Discourse.redis - config.job_exception_handler do |ex, context| - Discourse.handle_job_exception(ex, context) - end + config.job_exception_handler { |ex, context| Discourse.handle_job_exception(ex, context) } - config.job_ran do |stat| - DiscourseEvent.trigger(:scheduled_job_ran, stat) - end + config.job_ran { |stat| DiscourseEvent.trigger(:scheduled_job_ran, stat) } config.skip_schedule { Sidekiq.paused? } config.before_sidekiq_web_request do RailsMultisite::ConnectionManagement.establish_connection( - db: RailsMultisite::ConnectionManagement::DEFAULT + db: RailsMultisite::ConnectionManagement::DEFAULT, ) end end diff --git a/config/initializers/100-silence_logger.rb b/config/initializers/100-silence_logger.rb index 3a900d24e3..e01b29816f 100644 --- a/config/initializers/100-silence_logger.rb +++ b/config/initializers/100-silence_logger.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class SilenceLogger < Rails::Rack::Logger - PATH_INFO = 'PATH_INFO' - HTTP_X_SILENCE_LOGGER = 'HTTP_X_SILENCE_LOGGER' + PATH_INFO = "PATH_INFO" + HTTP_X_SILENCE_LOGGER = "HTTP_X_SILENCE_LOGGER" def initialize(app, opts = {}) @app = app @@ -17,11 +17,9 @@ class SilenceLogger < Rails::Rack::Logger path_info = env[PATH_INFO] override = false - if env[HTTP_X_SILENCE_LOGGER] || - @opts[:silenced].include?(path_info) || - path_info.start_with?('/logs') || - path_info.start_with?('/user_avatar') || - path_info.start_with?('/letter_avatar') + if env[HTTP_X_SILENCE_LOGGER] || @opts[:silenced].include?(path_info) || + path_info.start_with?("/logs") || path_info.start_with?("/user_avatar") || + path_info.start_with?("/letter_avatar") if ::Logster::Logger === Rails.logger override = true Rails.logger.override_level = Logger::WARN @@ -35,10 +33,10 @@ class SilenceLogger < Rails::Rack::Logger end end -silenced = [ - "/mini-profiler-resources/results", - "/mini-profiler-resources/includes.js", - "/mini-profiler-resources/includes.css", - "/mini-profiler-resources/jquery.tmpl.js" +silenced = %w[ + /mini-profiler-resources/results + /mini-profiler-resources/includes.js + /mini-profiler-resources/includes.css + /mini-profiler-resources/jquery.tmpl.js ] Rails.configuration.middleware.swap Rails::Rack::Logger, SilenceLogger, silenced: silenced diff --git a/config/initializers/100-verify_config.rb b/config/initializers/100-verify_config.rb index c2e7c63e66..5d779b6aac 100644 --- a/config/initializers/100-verify_config.rb +++ b/config/initializers/100-verify_config.rb @@ -3,8 +3,7 @@ # Check that the app is configured correctly. Raise some helpful errors if something is wrong. if defined?(Rails::Server) && Rails.env.production? # Only run these checks when starting up a production server - - if ['localhost', 'production.localhost'].include?(Discourse.current_hostname) + if %w[localhost production.localhost].include?(Discourse.current_hostname) puts <<~TEXT Discourse.current_hostname = '#{Discourse.current_hostname}' @@ -18,7 +17,7 @@ if defined?(Rails::Server) && Rails.env.production? # Only run these checks when raise "Invalid host_names in database.yml" end - if !Dir.glob(File.join(Rails.root, 'public', 'assets', 'application*.js')).present? + if !Dir.glob(File.join(Rails.root, "public", "assets", "application*.js")).present? puts <<~TEXT Assets have not been precompiled. Please run the following command diff --git a/config/initializers/100-wrap_parameters.rb b/config/initializers/100-wrap_parameters.rb index 85b2d84061..26c901dede 100644 --- a/config/initializers/100-wrap_parameters.rb +++ b/config/initializers/100-wrap_parameters.rb @@ -6,11 +6,7 @@ # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] -end +ActiveSupport.on_load(:action_controller) { wrap_parameters format: [:json] } # Disable root element in JSON by default. -ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false -end +ActiveSupport.on_load(:active_record) { self.include_root_in_json = false } diff --git a/config/initializers/101-lograge.rb b/config/initializers/101-lograge.rb index 311c64c753..3baac8b4b4 100644 --- a/config/initializers/101-lograge.rb +++ b/config/initializers/101-lograge.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true Rails.application.config.to_prepare do - if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || (ENV["ENABLE_LOGRAGE"] == "1") - require 'lograge' + if (Rails.env.production? && SiteSetting.logging_provider == "lograge") || + (ENV["ENABLE_LOGRAGE"] == "1") + require "lograge" if Rails.configuration.multisite Rails.logger.formatter = ActiveSupport::Logger::SimpleFormatter.new @@ -11,20 +12,20 @@ Rails.application.config.to_prepare do Rails.application.configure do config.lograge.enabled = true - Lograge.ignore(lambda do |event| - # this is our hijack magic status, - # no point logging this cause we log again - # direct from hijack - event.payload[:status] == 418 - end) + Lograge.ignore( + lambda do |event| + # this is our hijack magic status, + # no point logging this cause we log again + # direct from hijack + event.payload[:status] == 418 + end, + ) config.lograge.custom_payload do |controller| begin username = begin - if controller.respond_to?(:current_user) - controller.current_user&.username - end + controller.current_user&.username if controller.respond_to?(:current_user) rescue Discourse::InvalidAccess, Discourse::ReadOnly, ActiveRecord::ReadOnlyError nil end @@ -36,78 +37,77 @@ Rails.application.config.to_prepare do nil end - { - ip: ip, - username: username - } + { ip: ip, username: username } rescue => e - Rails.logger.warn("Failed to append custom payload: #{e.message}\n#{e.backtrace.join("\n")}") + Rails.logger.warn( + "Failed to append custom payload: #{e.message}\n#{e.backtrace.join("\n")}", + ) {} end end - config.lograge.custom_options = lambda do |event| - begin - exceptions = %w(controller action format id) + config.lograge.custom_options = + lambda do |event| + begin + exceptions = %w[controller action format id] - params = event.payload[:params].except(*exceptions) + params = event.payload[:params].except(*exceptions) - if (file = params[:file]) && file.respond_to?(:headers) - params[:file] = file.headers + if (file = params[:file]) && file.respond_to?(:headers) + params[:file] = file.headers + end + + if (files = params[:files]) && files.respond_to?(:map) + params[:files] = files.map { |f| f.respond_to?(:headers) ? f.headers : f } + end + + output = { + params: params.to_query, + database: RailsMultisite::ConnectionManagement.current_db, + } + + if data = (Thread.current[:_method_profiler] || event.payload[:timings]) + sql = data[:sql] + + if sql + output[:db] = sql[:duration] * 1000 + output[:db_calls] = sql[:calls] + end + + redis = data[:redis] + + if redis + output[:redis] = redis[:duration] * 1000 + output[:redis_calls] = redis[:calls] + end + + net = data[:net] + + if net + output[:net] = net[:duration] * 1000 + output[:net_calls] = net[:calls] + end + end + + output + rescue RateLimiter::LimitExceeded + # no idea who this is, but they are limited + {} + rescue => e + Rails.logger.warn( + "Failed to append custom options: #{e.message}\n#{e.backtrace.join("\n")}", + ) + {} end - - if (files = params[:files]) && files.respond_to?(:map) - params[:files] = files.map do |f| - f.respond_to?(:headers) ? f.headers : f - end - end - - output = { - params: params.to_query, - database: RailsMultisite::ConnectionManagement.current_db, - } - - if data = (Thread.current[:_method_profiler] || event.payload[:timings]) - sql = data[:sql] - - if sql - output[:db] = sql[:duration] * 1000 - output[:db_calls] = sql[:calls] - end - - redis = data[:redis] - - if redis - output[:redis] = redis[:duration] * 1000 - output[:redis_calls] = redis[:calls] - end - - net = data[:net] - - if net - output[:net] = net[:duration] * 1000 - output[:net_calls] = net[:calls] - end - end - - output - rescue RateLimiter::LimitExceeded - # no idea who this is, but they are limited - {} - rescue => e - Rails.logger.warn("Failed to append custom options: #{e.message}\n#{e.backtrace.join("\n")}") - {} end - end if ENV["LOGSTASH_URI"] config.lograge.formatter = Lograge::Formatters::Logstash.new - require 'discourse_logstash_logger' + require "discourse_logstash_logger" - config.lograge.logger = DiscourseLogstashLogger.logger( - uri: ENV['LOGSTASH_URI'], type: :rails - ) + config.lograge.logger = + DiscourseLogstashLogger.logger(uri: ENV["LOGSTASH_URI"], type: :rails) # Remove ActiveSupport::Logger from the chain and replace with Lograge's # logger diff --git a/config/initializers/200-first_middlewares.rb b/config/initializers/200-first_middlewares.rb index 4617bd519d..ea011a6f8a 100644 --- a/config/initializers/200-first_middlewares.rb +++ b/config/initializers/200-first_middlewares.rb @@ -11,13 +11,11 @@ Rails.configuration.middleware.unshift(MessageBus::Rack::Middleware) # no reason to track this in development, that is 300+ redis calls saved per # page view (we serve all assets out of thin in development) -if Rails.env != 'development' || ENV['TRACK_REQUESTS'] - require 'middleware/request_tracker' +if Rails.env != "development" || ENV["TRACK_REQUESTS"] + require "middleware/request_tracker" Rails.configuration.middleware.unshift Middleware::RequestTracker - if GlobalSetting.enable_performance_http_headers - MethodProfiler.ensure_discourse_instrumentation! - end + MethodProfiler.ensure_discourse_instrumentation! if GlobalSetting.enable_performance_http_headers end if Rails.env.test? @@ -30,23 +28,27 @@ if Rails.env.test? super(env) end end - Rails.configuration.middleware.unshift TestMultisiteMiddleware, RailsMultisite::DiscoursePatches.config + Rails.configuration.middleware.unshift TestMultisiteMiddleware, + RailsMultisite::DiscoursePatches.config elsif Rails.configuration.multisite assets_hostnames = GlobalSetting.cdn_hostnames if assets_hostnames.empty? - assets_hostnames = - Discourse::Application.config.database_configuration[Rails.env]["host_names"] + assets_hostnames = Discourse::Application.config.database_configuration[Rails.env]["host_names"] end RailsMultisite::ConnectionManagement.asset_hostnames = assets_hostnames # Multisite needs to be first, because the request tracker and message bus rely on it - Rails.configuration.middleware.unshift RailsMultisite::Middleware, RailsMultisite::DiscoursePatches.config + Rails.configuration.middleware.unshift RailsMultisite::Middleware, + RailsMultisite::DiscoursePatches.config Rails.configuration.middleware.delete ActionDispatch::Executor if defined?(RailsFailover::ActiveRecord) && Rails.configuration.active_record_rails_failover - Rails.configuration.middleware.insert_after(RailsMultisite::Middleware, RailsFailover::ActiveRecord::Middleware) + Rails.configuration.middleware.insert_after( + RailsMultisite::Middleware, + RailsFailover::ActiveRecord::Middleware, + ) end if Rails.env.development? @@ -57,5 +59,8 @@ elsif Rails.configuration.multisite end end elsif defined?(RailsFailover::ActiveRecord) && Rails.configuration.active_record_rails_failover - Rails.configuration.middleware.insert_before(MessageBus::Rack::Middleware, RailsFailover::ActiveRecord::Middleware) + Rails.configuration.middleware.insert_before( + MessageBus::Rack::Middleware, + RailsFailover::ActiveRecord::Middleware, + ) end diff --git a/config/initializers/400-deprecations.rb b/config/initializers/400-deprecations.rb index 39373182d1..29d636f584 100644 --- a/config/initializers/400-deprecations.rb +++ b/config/initializers/400-deprecations.rb @@ -2,18 +2,32 @@ if !GlobalSetting.skip_redis? if GlobalSetting.respond_to?(:redis_slave_host) && GlobalSetting.redis_slave_host.present? - Discourse.deprecate("redis_slave_host is deprecated, use redis_replica_host instead", drop_from: "2.8") + Discourse.deprecate( + "redis_slave_host is deprecated, use redis_replica_host instead", + drop_from: "2.8", + ) end if GlobalSetting.respond_to?(:redis_slave_port) && GlobalSetting.redis_slave_port.present? - Discourse.deprecate("redis_slave_port is deprecated, use redis_replica_port instead", drop_from: "2.8") + Discourse.deprecate( + "redis_slave_port is deprecated, use redis_replica_port instead", + drop_from: "2.8", + ) end - if GlobalSetting.respond_to?(:message_bus_redis_slave_host) && GlobalSetting.message_bus_redis_slave_host.present? - Discourse.deprecate("message_bus_redis_slave_host is deprecated, use message_bus_redis_replica_host", drop_from: "2.8") + if GlobalSetting.respond_to?(:message_bus_redis_slave_host) && + GlobalSetting.message_bus_redis_slave_host.present? + Discourse.deprecate( + "message_bus_redis_slave_host is deprecated, use message_bus_redis_replica_host", + drop_from: "2.8", + ) end - if GlobalSetting.respond_to?(:message_bus_redis_slave_port) && GlobalSetting.message_bus_redis_slave_port.present? - Discourse.deprecate("message_bus_redis_slave_port is deprecated, use message_bus_redis_replica_port", drop_from: "2.8") + if GlobalSetting.respond_to?(:message_bus_redis_slave_port) && + GlobalSetting.message_bus_redis_slave_port.present? + Discourse.deprecate( + "message_bus_redis_slave_port is deprecated, use message_bus_redis_replica_port", + drop_from: "2.8", + ) end end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 98564a17cf..191f6acc28 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -17,11 +17,13 @@ Rails.application.config.assets.paths << "#{Rails.root}/public/javascripts" # folder are already added. # explicitly precompile any images in plugins ( /assets/images ) path -Rails.application.config.assets.precompile += [lambda do |filename, path| - path =~ /assets\/images/ && !%w(.js .css).include?(File.extname(filename)) -end] +Rails.application.config.assets.precompile += [ + lambda do |filename, path| + path =~ %r{assets/images} && !%w[.js .css].include?(File.extname(filename)) + end, +] -Rails.application.config.assets.precompile += %w{ +Rails.application.config.assets.precompile += %w[ discourse.js vendor.js admin.js @@ -49,15 +51,21 @@ Rails.application.config.assets.precompile += %w{ embed-application.js scripts/discourse-test-listen-boot scripts/discourse-boot -} +] -Rails.application.config.assets.precompile += EmberCli.assets.map { |name| name.sub('.js', '.map') } +Rails.application.config.assets.precompile += EmberCli.assets.map { |name| name.sub(".js", ".map") } # Precompile all available locales unless GlobalSetting.try(:omit_base_locales) - Dir.glob("#{Rails.root}/app/assets/javascripts/locales/*.js.erb").each do |file| - Rails.application.config.assets.precompile << "locales/#{file.match(/([a-z_A-Z]+\.js)\.erb$/)[1]}" - end + Dir + .glob("#{Rails.root}/app/assets/javascripts/locales/*.js.erb") + .each do |file| + Rails + .application + .config + .assets + .precompile << "locales/#{file.match(/([a-z_A-Z]+\.js)\.erb$/)[1]}" + end end # out of the box sprockets 3 grabs loose files that are hanging in assets, @@ -65,18 +73,16 @@ end Rails.application.config.assets.precompile.delete(Sprockets::Railtie::LOOSE_APP_ASSETS) # We don't want application from node_modules, only from the root -Rails.application.config.assets.precompile.delete(/(?:\/|\\|\A)application\.(css|js)$/) -Rails.application.config.assets.precompile += ['application.js'] +Rails.application.config.assets.precompile.delete(%r{(?:/|\\|\A)application\.(css|js)$}) +Rails.application.config.assets.precompile += ["application.js"] start_path = ::Rails.root.join("app/assets").to_s -exclude = ['.es6', '.hbs', '.hbr', '.js', '.css', '.lock', '.json', '.log', '.html', ''] +exclude = [".es6", ".hbs", ".hbr", ".js", ".css", ".lock", ".json", ".log", ".html", ""] Rails.application.config.assets.precompile << lambda do |logical_path, filename| - filename.start_with?(start_path) && - !filename.include?("/node_modules/") && - !filename.include?("/dist/") && - !exclude.include?(File.extname(logical_path)) + filename.start_with?(start_path) && !filename.include?("/node_modules/") && + !filename.include?("/dist/") && !exclude.include?(File.extname(logical_path)) end -Discourse.find_plugin_js_assets(include_disabled: true).each do |file| - Rails.application.config.assets.precompile << "#{file}.js" -end +Discourse + .find_plugin_js_assets(include_disabled: true) + .each { |file| Rails.application.config.assets.precompile << "#{file}.js" } diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index af4c108bf6..44f7fffd74 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -3,13 +3,13 @@ # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. -Rails.application.config.filter_parameters += [ - :password, - :pop3_polling_password, - :api_key, - :s3_secret_access_key, - :twitter_consumer_secret, - :facebook_app_secret, - :github_client_secret, - :second_factor_token, +Rails.application.config.filter_parameters += %i[ + password + pop3_polling_password + api_key + s3_secret_access_key + twitter_consumer_secret + facebook_app_secret + github_client_secret + second_factor_token ] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index 7ee8ffa85f..fdbf91e62f 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -102,5 +102,5 @@ Rails.application.config.action_dispatch.default_headers = { "X-Content-Type-Options" => "nosniff", "X-Download-Options" => "noopen", "X-Permitted-Cross-Domain-Policies" => "none", - "Referrer-Policy" => "strict-origin-when-cross-origin" + "Referrer-Policy" => "strict-origin-when-cross-origin", } diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index c5ba5a8fb8..a6f421687c 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -443,7 +443,6 @@ ar: remove_reminder_keep_bookmark: "إزالة التذكير والاحتفاظ بالإشارة المرجعية" created_with_reminder: "لقد وضعت إشارة مرجعية على هذا المنشور وضبطت تذكيرًا في %{date}. ‏%{name}" created_with_reminder_generic: "لقد وضعت إشارة مرجعية على هذا المنشور وضبطت تذكيرًا في %{date}. ‏%{name}" - remove: "إزالة الإشارة المرجعية" delete: "حذف الإشارة المرجعية" confirm_delete: "هل تريد بالتأكيد حذف هذه الإشارة المرجعية؟ سيتم حذف التذكير أيضًا." confirm_clear: "هل تريد بالتأكيد مسح كل إشاراتك المرجعية من هذا الموضوع؟" @@ -533,7 +532,6 @@ ar: enable: "تفعيل" disable: "إيقاف" continue: "متابعة" - undo: "تراجع" switch_to_anon: "دخول وضع التخفي" switch_from_anon: "الخروج من وضع التخفي" banner: @@ -886,6 +884,7 @@ ar: denied: "تم الرفض" undone: "تم التراجع عن الطلب" handle: "التعامل مع طلب العضوية" + undo: "تراجع" manage: title: "إدارة" name: "الاسم" @@ -1279,7 +1278,6 @@ ar: perm_denied_expl: "لقد رفضت منح الإذن بإرسال الإشعارات. يمكنك السماح بالإشعارات في إعدادات المتصفح." disable: "إيقاف الإشعارات" enable: "تفعيل الإشعارات" - each_browser_note: 'ملاحظة: عليك تغيير هذا الإعداد في كل متصفح تستخدمه. سيتم إيقاف جميع الإشعارات في وضع "عدم الإزعاج"، بغض النظر عن هذا الإعداد.' consent_prompt: "هل تريد تلقي إشعارات فورية عند رد الأشخاص على منشوراتك؟" dismiss: "تجاهل" dismiss_notifications: "تجاهل الكل" @@ -3724,7 +3722,6 @@ ar: create_for_topic: "إنشاء إشارة مرجعية للموضوع" edit: "تعديل الإشارة المرجعية" edit_for_topic: "تعديل الإشارة المرجعية للموضوع" - created: "تاريخ الإنشاء" updated: "تاريخ التحديث" name: "الاسم" name_placeholder: "ما استخدام هذه الإشارة المرجعية؟" @@ -4062,7 +4059,6 @@ ar: category_title: "الفئة" history_capped_revisions: "السجل، آخر 100 مراجعة" history: "السجل" - changed_by: "بواسطة %{author}" raw_email: title: "البريد الوارد" not_available: "غير متوفرة!" @@ -4561,7 +4557,6 @@ ar: few: "%{count} جديدة" many: "%{count} جديدًا" other: "%{count} جديدة" - toggle_section: "تبديل القسم" more: "المزيد" all_categories: "كل الفئات" all_tags: "كل الوسوم" @@ -4570,7 +4565,6 @@ ar: header_link_text: "نبذة" messages: header_link_text: "الرسائل" - header_action_title: "إنشاء رسالة شخصية" links: inbox: "صندوق الوارد" sent: "المُرسَلة" @@ -4587,7 +4581,6 @@ ar: none: "لم تضف أي وسوم." click_to_get_started: "انقر هنا للبدء." header_link_text: "الوسوم" - header_action_title: "تعديل وسوم الشريط الجانبي" configure_defaults: "ضبط الإعدادات الافتراضية" categories: links: @@ -4597,11 +4590,9 @@ ar: none: "لم تضف أي فئات." click_to_get_started: "انقر هنا للبدء." header_link_text: "الفئات" - header_action_title: "تعديل فئات الشريط الجانبي" configure_defaults: "ضبط الإعدادات الافتراضية" community: header_link_text: "المجتمع" - header_action_title: "إنشاء موضوع جديد" links: about: content: "نبذة" @@ -4616,10 +4607,8 @@ ar: content: "الأسئلة الشائعة" groups: content: "المجموعات" - title: "كل المجموعات" users: content: "المستخدمون" - title: "جميع المستخدمين" my_posts: content: "منشوراتي" draft_count: @@ -4631,7 +4620,6 @@ ar: other: "%{count} مسودة" review: content: "المراجعة" - title: "مراجعة" pending_count: "بقي %{count}" welcome_topic_banner: title: "إنشاء موضوعك الترحيبي" @@ -5094,7 +5082,6 @@ ar: button_title: "إرسال الدعوات" customize: title: "تخصيص" - long_title: "تخصيصات الموقع" preview: "معاينة" explain_preview: "معاينة الموقع بعد تفعيل هذه السمة" save: "حفظ" diff --git a/config/locales/client.be.yml b/config/locales/client.be.yml index f41b0e6cf0..ac4f163b92 100644 --- a/config/locales/client.be.yml +++ b/config/locales/client.be.yml @@ -163,7 +163,6 @@ be: unbookmark: "Націсніце каб выдаліць усе закладкі ў гэтай тэме" bookmarks: not_bookmarked: "закладка гэты пост" - remove: "выдаліць закладку" save: "Захаваць" no_user_bookmarks: "У вас няма закладак паведамлення; закладкі дазваляюць хутка спасылацца на канкрэтныя пасады." search: "Пошук" @@ -181,7 +180,6 @@ be: enable: "Уключыць" disable: "Адключыць" continue: "Працягнуць" - undo: "Адмяніць" switch_to_anon: "Увайсці як ананім" switch_from_anon: "Пакінуць ананімны прагляд" banner: @@ -297,6 +295,7 @@ be: requests: reason: "прычына" handle: "звяртацца з просьбай аб членстве" + undo: "Адмяніць" manage: name: "Імя" full_name: "поўнае Імя" @@ -1105,7 +1104,6 @@ be: side_by_side_markdown: title: "Паказаць непасрэдныя адрозненні побач" bookmarks: - created: "створаны" name: "Імя" options: "Налады" category: @@ -1212,7 +1210,6 @@ be: many: "карыстальнікі" other: "карыстальнікі" category_title: "Катэгорыя" - changed_by: "%{Author}" categories_list: "Спіс катэгорый" filters: latest: @@ -1373,14 +1370,12 @@ be: content: "Пытанні і адказы" groups: content: "Групы" - title: "Усе групы" users: content: "карыстальнікаў" my_posts: content: "мае паведамленні" review: content: "Патрабуе перагляду" - title: "патрабуе перагляду" admin_js: admin: moderator: "Мадэратар" @@ -1550,7 +1545,6 @@ be: button_title: "Адправіць запрашэнні" customize: title: "Кастамізаваць" - long_title: "Site Customizations" preview: "preview" save: "Save" new: "Новая" @@ -1590,9 +1584,9 @@ be: colors: title: "<......" copy_name_prefix: "капіяваць з" - undo: "адмяніць" + undo: "Адмяніць" undo_title: "Адмяніць вашыя змены гэтага колеру з часу апошняга захавання." - revert: "вярнуць" + revert: "Вярнуць" primary: name: "асноўнай" secondary: diff --git a/config/locales/client.bg.yml b/config/locales/client.bg.yml index 64737493cb..4ef80402aa 100644 --- a/config/locales/client.bg.yml +++ b/config/locales/client.bg.yml @@ -278,7 +278,6 @@ bg: not_bookmarked: "добавете тази публикация в Отметки" remove_reminder_keep_bookmark: "Премахнете напомнянето и запазете отметката" created_with_reminder: "Отбелязахте тази публикация с напомняне %{date}. %{name}" - remove: "Премахнете отметката" delete: "Изтрийте отметката" confirm_delete: "Наистина ли искате да изтриете тази отметка? Напомнянето, свързано с нея, също ще бъде отменено." confirm_clear: "Наистина ли искате да премахните всичките отметки от тази тема?" @@ -341,7 +340,6 @@ bg: enable: "Позволи" disable: "Деактивиране" continue: "Напред" - undo: "Отмени" switch_to_anon: "Влез в Анонимен режим. " switch_from_anon: "Излез от Анонимен режим." banner: @@ -622,6 +620,7 @@ bg: denied: "отказан" undone: "заявката е отменена" handle: "обработва заявка за членство" + undo: "Отмени" manage: title: "Управление" name: "Име" @@ -986,7 +985,6 @@ bg: perm_denied_expl: "Вие сте забранили известията. Моля разрешете ги чрез настройките на браузъра си." disable: "Деактивиране на Известията" enable: "Активиране на Известията" - each_browser_note: 'Забележка: Трябва да промените тази настройка на всеки браузър, който използвате. Всички известия ще бъдат деактивирани, когато сте в „не безпокойте“, независимо от тази настройка.' consent_prompt: "Искате ли известявания в реално време, когато хората отговарят на вашите публикации?" dismiss: "Отмени" dismiss_notifications: "Отмени всички" @@ -2568,7 +2566,6 @@ bg: button: "HTML" bookmarks: edit: "Редактиране на отметката" - created: "Създадени" name: "Име" name_placeholder: "За какво е тази отметка?" set_reminder: "Напомни ми" @@ -2754,7 +2751,6 @@ bg: one: "потребител" other: "потребители" category_title: "Категория" - changed_by: "от %{author}" raw_email: title: "Входяща поща" not_available: "Не е наличен!" @@ -3015,14 +3011,12 @@ bg: content: "FAQ" groups: content: "Групи" - title: "Всички групи" users: content: "Потребители" my_posts: content: "Моите публикации" review: content: "Преглед" - title: "преглед" admin_js: type_to_filter: "филтрирай..." admin: @@ -3219,7 +3213,6 @@ bg: button_title: "Изпрати поканите" customize: title: "Персонализация" - long_title: "Персонализация на сайта" preview: "преглед" save: "Запази" new: "Нов" @@ -3263,9 +3256,9 @@ bg: colors: title: "Цветове" copy_name_prefix: "Копие на" - undo: "отмени" + undo: "Отмени" undo_title: "Върнете промените направени на този цвят до последно запазените." - revert: "върни" + revert: "Върни" primary: name: "главен" description: "Повече от текстовете, икони и граници " diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 4783fe546c..a7536d0ad9 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -289,7 +289,6 @@ bs_BA: create: "Stvaranje zabilješke" edit: "Izmijeni zabilješku" not_bookmarked: "zabilježi objavu" - remove: "Ukloni zabilješku" delete: "Izbriši zabilješku" confirm_delete: "Jeste li sigurni da želite izbrisati ovu zabilješku? Podsjetnik će takođe biti izbrisan." confirm_clear: "Jeste li sigurni da želite ukloniti sve vaše zabilješke iz ove teme?" @@ -349,7 +348,6 @@ bs_BA: enable: "Omogući" disable: "Onemogući" continue: "Nastavi" - undo: "Vrati nazad" switch_to_anon: "Uđi u privatni način rada" switch_from_anon: "Izađi iz privatnog načina rada" banner: @@ -613,6 +611,7 @@ bs_BA: denied: "odbijeno" undone: "zahtjev izbrsan" handle: "obradi zahtjev za članstvo" + undo: "Vrati nazad" manage: title: "Uredi" name: "Ime" @@ -2471,7 +2470,6 @@ bs_BA: bookmarks: create: "Stvaranje zabilješke" edit: "Izmijeni zabilješku" - created: "Stvoreno" updated: "Ažurirano" name: "Ime" name_placeholder: "Za što služi ova zabilješka?" @@ -2713,7 +2711,6 @@ bs_BA: few: "korisnika" other: "korisnika" category_title: "Kategorija" - changed_by: "od %{author}" raw_email: title: "Dolazna email" not_available: "Nije dostupno!" @@ -3091,14 +3088,12 @@ bs_BA: content: "Česta pitanja" groups: content: "Grupe" - title: "Sve grupe" users: content: "Users" my_posts: content: "Moje objave" review: content: "Pregled" - title: "pregled" admin_js: type_to_filter: "kucaj da sortiraš..." admin: @@ -3455,7 +3450,6 @@ bs_BA: button_title: "Pošalji pozivnice" customize: title: "Customize" - long_title: "Site Customizations" preview: "preview" explain_preview: "Pogledajte lokaciju sa omogućenom ovom temom" save: "Save" @@ -3613,9 +3607,9 @@ bs_BA: new_name: "Nova paleta boja" copy_name_prefix: "Copy of" delete_confirm: "Izbrisati ovu paletu boja?" - undo: "undo" + undo: "Vrati nazad" undo_title: "Undo your changes to this color since the last time it was saved." - revert: "vrati" + revert: "Vrati nazad" revert_title: "Vrati boju na standardnu boju Discourse palete." primary: name: "primary" diff --git a/config/locales/client.ca.yml b/config/locales/client.ca.yml index 418379e580..7ce685788c 100644 --- a/config/locales/client.ca.yml +++ b/config/locales/client.ca.yml @@ -285,7 +285,6 @@ ca: not_bookmarked: "marca aquesta publicació com a preferit" remove_reminder_keep_bookmark: "Elimina el recordatori i mantén el marcador" created_with_reminder: "Heu marcat aquesta publicació amb un recordatori de %{date}. %{name}" - remove: "Elimina preferit" delete: "Suprimeix el marcador" confirm_delete: "Esteu segur que voleu suprimir aquest marcador? El recordatori també se suprimirà." confirm_clear: "Esteu segur que voleu eliminar tots els preferits d'aquest tema?" @@ -346,7 +345,6 @@ ca: enable: "Activa" disable: "Desactiva" continue: "Continua" - undo: "Desfés" switch_to_anon: "Entra en el mode anònim" switch_from_anon: "Surt del mode anònim" banner: @@ -615,6 +613,7 @@ ca: denied: "denegat" undone: "sol·licitud revertida" handle: "gestiona la sol·licitud d'afiliació" + undo: "Desfés" manage: title: "Gestiona" name: "Nom" @@ -2378,7 +2377,6 @@ ca: title: "Mostra la part HTML del correu electrònic" button: "HTML" bookmarks: - created: "Creat" updated: "Actualitzat" name: "Nom" options: "Opcions" @@ -2606,7 +2604,6 @@ ca: one: "usuari" other: "usuaris" category_title: "Categoria" - changed_by: "per %{author}" raw_email: title: "Correu entrant" not_available: "No disponible!" @@ -2923,14 +2920,12 @@ ca: content: "PMF" groups: content: "Grups" - title: "Tots els grups" users: content: "Usuaris" my_posts: content: "Les meves publicacions" review: content: "Revisa" - title: "revisa" admin_js: type_to_filter: "escriu per a filtrar..." admin: @@ -3284,7 +3279,6 @@ ca: button_title: "Envia invitacions" customize: title: "Personalitza" - long_title: "Personalitzacions del lloc web" preview: "previsualització" explain_preview: "Mostra el lloc web amb aquesta aparença activada" save: "Desa" @@ -3445,9 +3439,9 @@ ca: new_name: "Nova paleta de colors" copy_name_prefix: "Còpia de" delete_confirm: "Voleu suprimir aquesta paleta de colors?" - undo: "desfés" + undo: "Desfés" undo_title: "Desfà els canvis a aquest color des de la darrera vegada que va ser desat." - revert: "reverteix" + revert: "Reverteix" revert_title: "Restableix aquest color a la paleta de colors predeterminada de Discourse." primary: name: "primari" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index fa6def0c8f..43fcbfd6c9 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -345,7 +345,6 @@ cs: remove_reminder_keep_bookmark: "Odebrat připomenutí a ponechat záložku" created_with_reminder: "Tento příspěvek jste označili záložkou s připomenutím %{date}. %{name}" created_with_reminder_generic: "Toto jste si přidali do záložek s připomenutím %{date}. %{name}" - remove: "Odstranit záložku" delete: "Smazat záložku" confirm_delete: "Opravdu chcete tuto záložku smazat? Připomenutí bude také smazáno." confirm_clear: "Opravdu chcete odstranit všechny své záložky z tohoto tématu?" @@ -425,7 +424,6 @@ cs: enable: "Zapnout" disable: "Vypnout" continue: "Pokračovat" - undo: "Zpět" switch_to_anon: "Vstoupit do anonymního módu" switch_from_anon: "Opustit anonymní mód" banner: @@ -643,6 +641,7 @@ cs: requests: reason: "Reason" accepted: "přijato" + undo: "Zpět" manage: title: "Spravovat" name: "Název" @@ -2373,7 +2372,6 @@ cs: title: "Zobrazit html část emailu" button: "HTML" bookmarks: - created: "Vytvořený" name: "Jméno" options: "Možnosti" category: @@ -2600,7 +2598,6 @@ cs: many: "uživatelů" other: "uživatelů" category_title: "Kategorie" - changed_by: "od uživatele %{author}" raw_email: title: "Příchozí email" not_available: "Není k dispozici!" @@ -2920,7 +2917,6 @@ cs: content: "FAQ" groups: content: "Skupiny" - title: "Všechny skupiny" users: content: "Uživatelé" my_posts: @@ -3201,7 +3197,6 @@ cs: button_title: "Poslat pozvánky" customize: title: "Přizpůsobení" - long_title: "Přizpůsobení webu" preview: "náhled" explain_preview: "Náhled stránky s povoleným motivem" save: "Uložit" @@ -3300,9 +3295,9 @@ cs: colors: title: "Barvy" copy_name_prefix: "Kopie" - undo: "zpět" + undo: "Zpět" undo_title: "Vrať svoje změny této barvy od doby kdy byla posledně uložena." - revert: "vrátit" + revert: "Vrátit" primary: name: "primární" description: "Většina textu, ikon a okrajů." diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 9ed4ea7700..e6a5058f5d 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -289,7 +289,6 @@ da: remove_reminder_keep_bookmark: "Fjern påmindelse og behold bogmærke" created_with_reminder: "Du har sat et bogmærke med en påmindelse for dette indlæg %{date}. %{name}" created_with_reminder_generic: "Du har bogmærket dette med en påmindelse %{date}. %{name}" - remove: "Fjern bogmærke" delete: "Slet Bogmærke" confirm_delete: "Er du sikker på, at du vil slette dette bogmærke? Påmindelsen vil også blive slettet." confirm_clear: "Er du sikker på, at du vil rydde alle dine bogmærker fra dette emne?" @@ -357,7 +356,6 @@ da: enable: "Aktivér" disable: "Deaktivér" continue: "Fortsæt" - undo: "Fortryd" switch_to_anon: "Gå i anonym tilstand" switch_from_anon: "Afslut anonym tilstand" banner: @@ -651,6 +649,7 @@ da: denied: "Nægtet" undone: "anmodning fortrudt" handle: "håndter medlemskabsanmodning" + undo: "Fortryd" manage: title: "Administrér" name: "Navn" @@ -1014,7 +1013,6 @@ da: perm_denied_expl: "Du nægtede adgang for notifikationer. Tillad notifikationer via indstillingerne i din browser." disable: "Deaktiver notifikationer" enable: "Aktiver notifikationer" - each_browser_note: 'Bemærk: Du skal ændre denne indstilling i hver browser, du bruger. Alle meddelelser deaktiveres, når de er i "forstyr ikke", uanset denne indstilling.' consent_prompt: "Ønsker du live notifikationer, når folk svarer på dine indlæg?" dismiss: "Ignorer Alle" dismiss_notifications: "Skjul Alle" @@ -2917,7 +2915,6 @@ da: create_for_topic: "Opret bogmærke til emnet" edit: "Rediger bogmærke" edit_for_topic: "Rediger bogmærke til emnet" - created: "Oprettet" updated: "Opdateret" name: "Navn" name_placeholder: "Hvad er dette bogmærke til?" @@ -3205,7 +3202,6 @@ da: category_title: "Kategori" history_capped_revisions: "Historik, seneste 100 revisioner" history: "Historik" - changed_by: "af %{author}" raw_email: title: "Indgående e-mail" not_available: "Ikke tilgængelig!" @@ -3635,7 +3631,6 @@ da: content: "OSS" groups: content: "Grupper" - title: "Alle grupper" users: content: "Brugere" my_posts: @@ -3643,7 +3638,6 @@ da: title_drafts: "Mine uopslåede kladder" review: content: "Anmeldelse" - title: "anmeldelse" until: "Indtil:" admin_js: type_to_filter: "skriv for at filtrere…" @@ -4062,7 +4056,6 @@ da: button_title: "Send Invitationer" customize: title: "Tilpasning" - long_title: "Tilpasning af site" preview: "forhåndsvisning" explain_preview: "Se sitet med dette tema slået til" save: "Gem" @@ -4263,9 +4256,9 @@ da: new_name: "Ny farvepalet" copy_name_prefix: "Kopi af" delete_confirm: "Slet denne farvepalet?" - undo: "fortryd" + undo: "Fortryd" undo_title: "Fortryd dine ændringer til denne farve, siden sidste gang den blev gemt." - revert: "gendan" + revert: "Gendan" revert_title: "Nulstil denne farve til Discourses standard farvepalet." primary: name: "Primær" diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 2e03584510..a72bdc2647 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -315,7 +315,6 @@ de: remove_reminder_keep_bookmark: "Erinnerung entfernen und Lesezeichen behalten" created_with_reminder: "Du wirst %{date} an dieses Lesezeichen erinnert. %{name}" created_with_reminder_generic: "Du wirst am %{date} an dieses Lesezeichen erinnert. %{name}" - remove: "Lesezeichen entfernen" delete: "Lesezeichen löschen" confirm_delete: "Möchtest du dieses Lesezeichen wirklich löschen? Die Erinnerung wird ebenfalls gelöscht." confirm_clear: "Möchtest du wirklich alle deine Lesezeichen in diesem Thema löschen?" @@ -385,7 +384,6 @@ de: enable: "Aktivieren" disable: "Deaktivieren" continue: "Weiter" - undo: "Rückgängig machen" switch_to_anon: "Anonymen Modus aktivieren" switch_from_anon: "Anonymen Modus deaktivieren" banner: @@ -682,6 +680,7 @@ de: denied: "abgelehnt" undone: "Anfrage zurückgenommen" handle: "Mitgliedschaftsanfrage bearbeiten" + undo: "Rückgängig machen" manage: title: "Verwalten" name: "Name" @@ -1008,6 +1007,7 @@ de: notification_schedule: title: "Zeitplan für Benachrichtigungen" label: "Aktiviere den benutzerdefinierten Zeitplan für Benachrichtigungen" + tip: "Außerhalb dieser Zeiten werden deine Benachrichtigungen pausiert." midnight: "Mitternacht" none: "Keine" monday: "Montag" @@ -1051,7 +1051,6 @@ de: perm_denied_expl: "Du hast das Anzeigen von Benachrichtigungen verboten. Aktiviere die Benachrichtigungen über deine Browser-Einstellungen." disable: "Benachrichtigungen deaktivieren" enable: "Benachrichtigungen aktivieren" - each_browser_note: 'Hinweis: Du musst diese Einstellung in jedem von dir verwendeten Browser ändern. Alle Benachrichtigungen werden deaktiviert, wenn der „Nicht stören“-Modus verwendet wird, unabhängig von dieser Einstellung.' consent_prompt: "Möchtest du Live-Benachrichtigungen erhalten, wenn jemand auf deine Beiträge antwortet?" dismiss: "Alles gelesen" dismiss_notifications: "Alles gelesen" @@ -1663,6 +1662,7 @@ de: save: "Speichern" set_custom_status: "Benutzerdefinierten Status festlegen" what_are_you_doing: "Was machst du gerade?" + pause_notifications: "Benachrichtigungen pausieren" remove_status: "Status entfernen" user_tips: primary: "Verstanden!" @@ -3120,7 +3120,6 @@ de: create_for_topic: "Lesezeichen für Thema erstellen" edit: "Lesezeichen bearbeiten" edit_for_topic: "Lesezeichen für Thema bearbeiten" - created: "Erstellt" updated: "Aktualisiert" name: "Name" name_placeholder: "Wofür dient dieses Lesezeichen?" @@ -3422,7 +3421,6 @@ de: category_title: "Kategorie" history_capped_revisions: "Verlauf, letzte 100 Überarbeitungen" history: "Verlauf" - changed_by: "von %{author}" raw_email: title: "Eingegangene E-Mail" not_available: "Nicht verfügbar!" @@ -3788,6 +3786,8 @@ de: enabled: "Der abgesicherte Modus ist aktiviert. Schließe das Browserfenster, um ihn zu verlassen." image_removed: "(Bild entfernt)" pause_notifications: + title: "Benachrichtigungen pausieren für..." + label: "Benachrichtigungen pausieren" remaining: "%{remaining} verbleibend" options: half_hour: "30 Minuten" @@ -3837,7 +3837,6 @@ de: new_count: one: "%{count} neu" other: "%{count} neu" - toggle_section: "Abschnitt umschalten" more: "Mehr" all_categories: "Alle Kategorien" all_tags: "Alle Schlagwörter" @@ -3846,7 +3845,6 @@ de: header_link_text: "Über uns" messages: header_link_text: "Nachrichten" - header_action_title: "eine persönliche Nachricht erstellen" links: inbox: "Posteingang" sent: "Gesendet" @@ -3863,7 +3861,6 @@ de: none: "Du hast keine Schlagwörter hinzugefügt." click_to_get_started: "Klicke hier, um zu beginnen." header_link_text: "Schlagwörter" - header_action_title: "bearbeite die Schlagwörter in deiner Seitenleiste" configure_defaults: "Standardeinstellungen konfigurieren" categories: links: @@ -3873,11 +3870,9 @@ de: none: "Du hast keine Kategorien hinzugefügt." click_to_get_started: "Klicke hier, um zu beginnen." header_link_text: "Kategorien" - header_action_title: "bearbeite die Kategorien in deiner Seitenleiste" configure_defaults: "Standardeinstellungen konfigurieren" community: header_link_text: "Community" - header_action_title: "ein neues Thema erstellen" links: about: content: "Über uns" @@ -3892,10 +3887,8 @@ de: content: "FAQ" groups: content: "Gruppen" - title: "Alle Gruppen" users: content: "Benutzer" - title: "Alle Benutzer" my_posts: content: "Meine Beiträge" draft_count: @@ -3903,7 +3896,6 @@ de: other: "%{count} Entwürfe" review: content: "Überprüfen" - title: "überprüfen" pending_count: "%{count} ausstehend" welcome_topic_banner: title: "Erstelle dein Willkommensthema" @@ -4350,7 +4342,6 @@ de: button_title: "Einladungen versenden" customize: title: "Anpassen" - long_title: "Website-Anpassungen" preview: "Vorschau" explain_preview: "Website mit diesem Theme anschauen" save: "Speichern" @@ -4555,9 +4546,9 @@ de: new_name: "Neue Farbpalette" copy_name_prefix: "Kopie von" delete_confirm: "Diese Farbpalette löschen?" - undo: "rückgängig machen" + undo: "Rückgängig machen" undo_title: "Die seit dem letzten Speichern an dieser Farbe vorgenommenen Änderungen rückgängig machen." - revert: "verwerfen" + revert: "Verwerfen" revert_title: "Diese Farbe auf den Standardwert aus der Farbpalette zurücksetzen." primary: name: "erste" @@ -4654,7 +4645,7 @@ de: last_seen_user: "Zuletzt gesehener Benutzer:" no_result: "Für diese Zusammenfassung wurden keine Ergebnisse gefunden." reply_key: "Antwort-Schlüssel" - post_link_with_smtp: "Post- und SMTP-Details" + post_link_with_smtp: "Beitrag & SMTP-Details" skipped_reason: "Grund des Überspringens" incoming_emails: from_address: "Von" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index 8a640c579d..4384c94a80 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -265,7 +265,6 @@ el: edit: "Επεξεργασία σελιδοδείκτη" not_bookmarked: "προσθέστε σελιδοδείκτη σε αυτήν την ανάρτηση" created_with_reminder: "Έχετε προσθέσει σελιδοδείκτη σε αυτήν την ανάρτηση με υπενθύμιση %{date}. %{name}" - remove: "Αφαίρεση σελιδοδείκτη" delete: "Διαγραφή σελιδοδείκτη" confirm_delete: "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον σελιδοδείκτη; Η υπενθύμιση θα διαγραφεί επίσης." confirm_clear: "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλους τους σελιδοδείκτες σας από αυτό το θέμα;" @@ -321,7 +320,6 @@ el: enable: "Ενεργοποίηση" disable: "Απενεργοποίηση" continue: "Συνεχίστε" - undo: "Αναίρεση" switch_to_anon: "Έναρξη Κατάστασης Ανωνυμίας" switch_from_anon: "Τερματισμός Κατάστασης Ανωνυμίας" banner: @@ -577,6 +575,7 @@ el: denied: "μη αποδεκτό" undone: "αναίρεση αιτήματος" handle: "χειριστείτε το αίτημα συμμετοχής" + undo: "Αναίρεση" manage: title: "Διαχειριστείτε" name: "Όνομα" @@ -2472,7 +2471,6 @@ el: bookmarks: create: "Δημιουργία σελιδοδείκτη" edit: "Επεξεργασία σελιδοδείκτη" - created: "Δημιουργήθηκε" updated: "Ενημερώθηκε" name: "Όνομα" name_placeholder: "Σε τι χρησιμεύει αυτός ο σελιδοδείκτης;" @@ -2715,7 +2713,6 @@ el: one: "χρήστης" other: "χρήστες" category_title: "Κατηγορία" - changed_by: "του/της %{author}" raw_email: title: "Εισερχόμενα email" not_available: "Μη διαθέσιμο!" @@ -3080,14 +3077,12 @@ el: content: "Συχνές ερωτήσεις" groups: content: "Ομάδες" - title: "Όλες οι ομάδες" users: content: "Χρήστες" my_posts: content: "Οι αναρτήσεις μου" review: content: "Ανασκόπηση" - title: "ανασκόπηση" admin_js: type_to_filter: "γράψε εδώ για φιλτράρισμα..." admin: @@ -3467,7 +3462,6 @@ el: button_title: "Αποστολή Προσκλήσεων" customize: title: "Προσαρμογή" - long_title: "Προσαρμογές Ιστότοπου" preview: "προεπισκόπηση " explain_preview: "Δες την ιστοσελίδα με αυτό το θέμα ενεργό" save: "Αποθήκευση" @@ -3644,9 +3638,9 @@ el: new_name: "Νέα παλέτα χρωμάτων" copy_name_prefix: "Αντίγραφο του" delete_confirm: "Διαγραφή αυτής της παλέτας χρωμάτων;" - undo: "αναίρεση" + undo: "Αναίρεση" undo_title: "Να αναιρεθούν οι αλλαγές που έγιναν σε αυτό το χρώμα από την τελευταία αποθήκευσή του και έπειτα." - revert: "απόρριψη" + revert: "Επαναφορά" revert_title: "Επαναφορά αυτού του χρώματος στην προεπιλεγμένη παλέτα χρωμάτων του Discourse." primary: name: "κύριο" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4aa447355c..a807ded6f4 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -346,7 +346,6 @@ en: remove_reminder_keep_bookmark: "Remove reminder and keep bookmark" created_with_reminder: "You've bookmarked this post with a reminder %{date}. %{name}" created_with_reminder_generic: "You've bookmarked this with a reminder %{date}. %{name}" - remove: "Remove Bookmark" delete: "Delete Bookmark" confirm_delete: "Are you sure you want to delete this bookmark? The reminder will also be deleted." confirm_clear: "Are you sure you want to clear all your bookmarks from this topic?" @@ -1122,7 +1121,7 @@ en: perm_denied_expl: "You denied permission for notifications. Allow notifications via your browser settings." disable: "Disable Notifications" enable: "Enable Notifications" - each_browser_note: 'Note: You have to change this setting on every browser you use. All notifications will be disabled when in "do not disturb", regardless of this setting.' + each_browser_note: 'Note: You have to change this setting on every browser you use. All notifications will be disabled if you pause notifications from user menu, regardless of this setting.' consent_prompt: "Do you want live notifications when people reply to your posts?" dismiss: "Dismiss" dismiss_notifications: "Dismiss All" @@ -4903,7 +4902,6 @@ en: customize: title: "Customize" - long_title: "Site Customizations" preview: "preview" explain_preview: "See the site with this theme enabled" save: "Save" diff --git a/config/locales/client.en_GB.yml b/config/locales/client.en_GB.yml index bcab0e6c2a..2cdd3aa67f 100644 --- a/config/locales/client.en_GB.yml +++ b/config/locales/client.en_GB.yml @@ -106,7 +106,6 @@ en_GB: button_title: "Send invitations" customize: title: "Customise" - long_title: "Site customisations" color: "Colour" theme: customize_desc: "Customise:" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index ae782b5860..445470f110 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -315,7 +315,6 @@ es: remove_reminder_keep_bookmark: "Quitar recordatorio y mantener marcador" created_with_reminder: "Has marcado esta publicación con un recordatorio %{date}. %{name}" created_with_reminder_generic: "Has añadido esto a marcadores con un recordatorio %{date}. %{name}" - remove: "Eliminar marcador" delete: "Eliminar marcador" confirm_delete: "¿Quieres borrar este marcador? El recordatorio también se borrará." confirm_clear: "¿Seguro que quieres eliminar todos tus marcadores en este tema?" @@ -385,7 +384,6 @@ es: enable: "Activar" disable: "Desactivar" continue: "Continuar" - undo: "Deshacer" switch_to_anon: "Entrar en modo anónimo" switch_from_anon: "Salir del modo anónimo" banner: @@ -682,6 +680,7 @@ es: denied: "denegado" undone: "solicitud deshecha" handle: "atender solicitud de membresía" + undo: "Deshacer" manage: title: "Gestionar" name: "Nombre" @@ -1051,7 +1050,6 @@ es: perm_denied_expl: "Has denegado el permiso para las notificaciones. Configura tu navegador para permitir notificaciones. " disable: "Desactivar notificaciones" enable: "Activar notificaciones" - each_browser_note: 'Nota: hay que cambiar este ajuste en cada navegador que uses. Todas las notificaciones serán desactivadas en el modo «no molestar», independientemente de este ajuste.' consent_prompt: "¿Quieres recibir notificaciones en tiempo real cuando alguien responda a tus mensajes?" dismiss: "Descartar" dismiss_notifications: "Descartar todo" @@ -3120,7 +3118,6 @@ es: create_for_topic: "Añadir este tema a marcadores" edit: "Editar marcador" edit_for_topic: "Editar el marcador de este tema" - created: "Creado" updated: "Actualizado" name: "Nombre" name_placeholder: "¿Para qué es este marcador?" @@ -3422,7 +3419,6 @@ es: category_title: "Categoría" history_capped_revisions: "Últimas 100 ediciones" history: "Historial" - changed_by: "por %{author}" raw_email: title: "Correo electrónico entrante" not_available: "¡No disponible!" @@ -3837,7 +3833,6 @@ es: new_count: one: "%{count} nuevo" other: "%{count} nuevos" - toggle_section: "mostrar/ocultar sección" more: "Más" all_categories: "Todas las categorías" all_tags: "Todas las etiquetas" @@ -3846,7 +3841,6 @@ es: header_link_text: "Acerca de" messages: header_link_text: "Mensajes" - header_action_title: "crear un mensaje personal" links: inbox: "Bandeja de entrada" sent: "Enviados" @@ -3863,7 +3857,6 @@ es: none: "No has añadido ninguna etiqueta." click_to_get_started: "Haz clic aquí para empezar." header_link_text: "Etiquetas" - header_action_title: "editar las etiquetas de tu barra lateral" configure_defaults: "Configurar valores predeterminados" categories: links: @@ -3873,11 +3866,9 @@ es: none: "No has añadido ninguna categoría." click_to_get_started: "Haz clic aquí para empezar." header_link_text: "Categorías" - header_action_title: "editar las categorías de tu barra lateral" configure_defaults: "Configurar valores predeterminados" community: header_link_text: "Comunidad" - header_action_title: "crear un nuevo tema" links: about: content: "Acerca de" @@ -3892,10 +3883,8 @@ es: content: "Preguntas frecuentes" groups: content: "Grupos" - title: "Todos los grupos" users: content: "Usuarios" - title: "Todos los usuarios" my_posts: content: "Mis publicaciones" draft_count: @@ -3903,7 +3892,6 @@ es: other: "%{count} borradores" review: content: "Revisión" - title: "revisión" pending_count: "%{count} pendiente(s)" welcome_topic_banner: title: "Crea tu tema de bienvenida" @@ -4349,7 +4337,6 @@ es: button_title: "Enviar invitaciones" customize: title: "Personalizar" - long_title: "Personalizaciones del sitio" preview: "vista previa" explain_preview: "Ver el sitio con este tema activado" save: "Guardar" @@ -4554,9 +4541,9 @@ es: new_name: "Nueva paleta de color" copy_name_prefix: "Copia de" delete_confirm: "¿Eliminar esta paleta de color?" - undo: "deshacer" + undo: "Deshacer" undo_title: "Deshacer los cambios realizados a este color desde la última vez que se guardó." - revert: "revertir" + revert: "Revertir" revert_title: "Restablecer este color a la paleta de color por defecto de Discourse." primary: name: "principal" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 9c6ef51d95..2107eb1783 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -253,7 +253,6 @@ et: edit: "Muuda järjehoidjat" not_bookmarked: "lisa sellele postitusele järjehoidja" created_with_reminder: "Olete selle postituse järjehoidjatesse lisanud meeldetuletusega %{date}. %{name}" - remove: "Eemalda järjehoidja" delete: "Kustuta järjehoidja" confirm_delete: "Kas soovid selle järjehoidja kustutada? Kustutatakse ka meeldetuletus." confirm_clear: "Kas soovid kindlasti sellest teemast kustutada kõik oma järjehoidjad?" @@ -305,7 +304,6 @@ et: enable: "Võimalda" disable: "Tõkesta" continue: "Jätka" - undo: "Ennista" switch_to_anon: "Sisene anonüümsesse režiimi" switch_from_anon: "Välju anonüümsest režiimist" banner: @@ -486,6 +484,7 @@ et: requests: reason: "Põhjus" accepted: "aktsepteeritud" + undo: "Ennista" manage: title: "Halda" name: "Nimi" @@ -2058,7 +2057,6 @@ et: button: "HTML" bookmarks: edit: "Muuda järjehoidjat" - created: "Loodud" name: "Nimi" options: "Võimalused" actions: @@ -2284,7 +2282,6 @@ et: one: "kasutaja" other: "kasutajad" category_title: "Foorum" - changed_by: "autor %{author}" raw_email: title: "Sissetulevad e-kirjad" not_available: "Pole saadaval!" @@ -2560,14 +2557,12 @@ et: content: "KKK" groups: content: "Grupid" - title: "Kõik grupid" users: content: "Kasutajad" my_posts: content: "Minu" review: content: "Vaata üle" - title: "vaata üle" admin_js: type_to_filter: "filtreerimiseks trüki..." admin: @@ -2839,7 +2834,6 @@ et: button_title: "Saada kutsed" customize: title: "Kohanda" - long_title: "Saidi kohandused" preview: "eelvaade" save: "Salvesta" new: "Uus" @@ -2915,9 +2909,9 @@ et: long_title: "Värvipaletid" copy_name_prefix: "Koopia sellest" delete_confirm: "Kas kustutada see vävripalett?" - undo: "ennista" + undo: "Ennista" undo_title: "Ennista selle värvistiku muudatused alates viimasest salvestamisest." - revert: "võta tagasi" + revert: "Võta tagasi" primary: name: "primaarne" description: "Enamik tekste, ikoone ja ääri." diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 64a7561563..b11a153e6c 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -289,7 +289,6 @@ fa_IR: not_bookmarked: "این نوشته را نشانک بزنید" remove_reminder_keep_bookmark: "حذف یادآوری و نگه داشتن نشانک" created_with_reminder: "شما این نوشته را با یادآوری %{date} نشانک کردید. %{name}" - remove: "پاک کردن نشانک" delete: "حذف نشانک" confirm_delete: "آیا مطمئن هستید که می‌خواهید این نشانک را حذف کنید؟ یادآوری هم حذف خواهد شد." confirm_clear: "آیا مطمئنید می‌خواهید همه‌ی نشانک‌های خود را از این موضوع پاک کنید؟" @@ -356,7 +355,6 @@ fa_IR: enable: "فعال کردن" disable: "ازکاراندازی" continue: "ادامه" - undo: "بی‌اثر کردن" switch_to_anon: "ورود به حالت ناشناس" switch_from_anon: "خروج از حالت ناشناس" banner: @@ -653,6 +651,7 @@ fa_IR: denied: "رد شده" undone: "بازگرداندن درخواست" handle: "رسیدگی به درخواست عضویت" + undo: "بی‌اثر کردن" manage: title: "مدیریت" name: "نام" @@ -991,6 +990,7 @@ fa_IR: perm_denied_expl: "شما دسترسی دریافت اعلان را بسته‌اید. در تنظیمات مرورگر خود آن‌را فعال کنید." disable: "غیرفعال کردن آگاه‌سازی" enable: "فعال کردن آگاه‌سازی" + each_browser_note: 'توجه: شما باید این تنظیمات را در هر مرورگری که استفاده می‌کنید، تغییر دهید. اگر آگاه‌سازی‌ها را از منوی کاربر متوقف کنید، صرف‌نظر از این تنظیم، همه آگاه‌سازی‌ها غیرفعال می‌شوند.' consent_prompt: "آیا مایلید وقتی دیگران به شما پاسخ می‌دهند، آگاه‌سازی زنده دریافت کنید؟" dismiss: "نخواستیم" dismiss_notifications: "پنهان کردن همه" @@ -2788,7 +2788,6 @@ fa_IR: create_for_topic: "ایجاد نشانک برای موضوع" edit: "ویرایش نشانک" edit_for_topic: "ویرایش نشانک برای موضوع" - created: "ساخته شده" updated: "به روز شده" name: "نام" name_placeholder: "این نشانک برای چی است؟" @@ -3023,7 +3022,6 @@ fa_IR: one: "کاربران" other: "کاربران" category_title: "دسته" - changed_by: "توسط %{author}" raw_email: title: "ایمیل دریافتی" not_available: "در دسترس نیست!" @@ -3369,22 +3367,24 @@ fa_IR: none: "شما هنوز هیچ برچسبی اضافه نکرده‌اید." click_to_get_started: "برای شروع اینجا را کلیک کنید." header_link_text: "برچسب" - header_action_title: "برچسب‌های نوار کناری خود را ویرایش کنید" + header_action_title: "ويرايش برچسب‌های نوار کناری خود" configure_defaults: "تنظیمات پیش‌فرض" categories: none: "شما هنوز هیچ دسته‌بندی اضافه نکرده‌اید." click_to_get_started: "برای شروع اینجا را کلیک کنید." header_link_text: "دسته‌بندی‌ها" - header_action_title: "دسته‌بندی‌های نوار کناری خود را ویرایش کنید" + header_action_title: "ويرايش دسته‌بندی‌های نوار کناری خود" configure_defaults: "تنظیمات پیش‌فرض" community: header_link_text: "انجمن" - header_action_title: "ایجاد موضوع جدید" + header_action_title: "ایجاد موضوع" links: about: content: "درباره" + title: "جزئیات بیشتر در مورد این سایت" admin: content: "مدیر کل" + title: "تنظیمات و گزارش‌های سایت" badges: content: "نشان‌ها" everything: @@ -3392,12 +3392,13 @@ fa_IR: title: "همه موضوعات" faq: content: "پرسش‌های متداول" + title: "راهنمای استفاده از این سایت" groups: content: "گروه‌ها" - title: "همه گروه‌ها" + title: "فهرست گروه‌های کاربری موجود" users: content: "کاربران" - title: "همه کاربران" + title: "فهرست همه‌ی کاربران" my_posts: content: "نوشته‌های من" draft_count: @@ -3405,7 +3406,6 @@ fa_IR: other: "%{count} پیش‌نویس" review: content: "بازنگری" - title: "بازنگری" pending_count: "%{count} در انتظار" until: "تا وقتی که:" admin_js: @@ -3743,7 +3743,6 @@ fa_IR: button_title: "ارسال دعوتنامه‌ها" customize: title: "سفارشی‌سازی" - long_title: "سفارشی‌سازی‌های سایت" preview: "پیش‌نمایش" explain_preview: "نمایش سایت با این قالب" save: "ذخیره" @@ -3877,7 +3876,7 @@ fa_IR: colors: title: "رنگ‌ها" copy_name_prefix: "کپی از" - undo: "خنثی کردن" + undo: "بی‌اثر کردن" undo_title: "برگشت دادن رنگ دخیره شده خود به آخرین رنگی که ذخیره شده است" revert: "برگشت" primary: diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index b165c5142f..f98d01409f 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -315,7 +315,6 @@ fi: remove_reminder_keep_bookmark: "Poista muistutus ja säilytä kirjanmerkki" created_with_reminder: "Olet kirjanmerkinnyt tämän viestin muistutuksen kera %{date}. %{name}" created_with_reminder_generic: "Olet lisännyt tämän kirjanmerkkeihin muistutuksella %{date}. %{name}" - remove: "Poista kirjanmerkki" delete: "Poista kirjanmerkki" confirm_delete: "Oletko varma, että haluat poistaa tämän kirjanmerkin? Muistutus poistetaan myös." confirm_clear: "Haluatko varmasti poistaa kaikki kirjanmerkkisi tästä ketjusta?" @@ -385,7 +384,6 @@ fi: enable: "Ota käyttöön" disable: "Poista käytöstä" continue: "Jatka" - undo: "Kumoa" switch_to_anon: "Siirry anonyymitilaan" switch_from_anon: "Poistu anonyymitilasta" banner: @@ -682,6 +680,7 @@ fi: denied: "hylättiin" undone: "pyyntö peruttu" handle: "käsittele jäsenhakemus" + undo: "Kumoa" manage: title: "Hallitse" name: "Nimi" @@ -1051,7 +1050,6 @@ fi: perm_denied_expl: "Olet kieltänyt ilmoitukset. Salli ilmoitukset selaimesi asetuksista." disable: "Poista ilmoitukset käytöstä" enable: "Näytä ilmoituksia" - each_browser_note: 'Huomautus: Tätä asetusta on muutettava jokaisessa käyttämässäsi selaimessa. Kaikki ilmoitukset poistetaan käytöstä, kun ”älä häiritse” -tila on käytössä riippumatta tästä asetuksesta.' consent_prompt: "Haluatko reaaliaikaisia ilmoituksia, kun ihmiset vastaavat viesteihisi?" dismiss: "Kuittaa" dismiss_notifications: "Kuittaa kaikki" @@ -3121,7 +3119,6 @@ fi: create_for_topic: "Luo kirjanmerkki ketjulle" edit: "Muokkaa kirjanmerkkiä" edit_for_topic: "Muokkaa ketjun kirjanmerkkiä" - created: "Luotu" updated: "Päivitetty" name: "Nimi" name_placeholder: "Mitä varten kirjanmerkki on?" @@ -3423,7 +3420,6 @@ fi: category_title: "Alue" history_capped_revisions: "Historia, viimeiset 100 versiota" history: "Historia" - changed_by: "käyttäjältä %{author}" raw_email: title: "Saapuva sähköposti" not_available: "Ei käytettävissä!" @@ -3838,7 +3834,6 @@ fi: new_count: one: "%{count} uusi" other: "%{count} uutta" - toggle_section: "vaihda osa" more: "Lisää" all_categories: "Kaikki alueet" all_tags: "Kaikki tunnisteet" @@ -3847,7 +3842,6 @@ fi: header_link_text: "Tietoa" messages: header_link_text: "Viestit" - header_action_title: "luo yksityisviesti" links: inbox: "Postilaatikko" sent: "Lähetetyt" @@ -3864,7 +3858,6 @@ fi: none: "Et ole lisännyt tunnisteita." click_to_get_started: "Aloita klikkaamalla tätä." header_link_text: "Tunnisteet" - header_action_title: "muokkaa sivupalkin tunnisteitasi" configure_defaults: "Määritä oletukset" categories: links: @@ -3874,11 +3867,9 @@ fi: none: "Et ole lisännyt alueita." click_to_get_started: "Aloita klikkaamalla tätä." header_link_text: "Alueet" - header_action_title: "muokkaa sivupalkin alueitasi" configure_defaults: "Määritä oletukset" community: header_link_text: "Yhteisö" - header_action_title: "aloita uusi ketju" links: about: content: "Tietoa" @@ -3893,10 +3884,8 @@ fi: content: "UKK" groups: content: "Ryhmät" - title: "Kaikki ryhmät" users: content: "Käyttäjät" - title: "Kaikki käyttäjät" my_posts: content: "Viestini" draft_count: @@ -3904,7 +3893,6 @@ fi: other: "%{count} luonnosta" review: content: "Käsittele" - title: "käsittele" pending_count: "%{count} odottaa" welcome_topic_banner: title: "Luo tervetuloketjusi" @@ -4350,7 +4338,6 @@ fi: button_title: "Lähetä kutsuja" customize: title: "Mukauta" - long_title: "Sivuston mukautukset" preview: "esikatselu" explain_preview: "Näe miltä sivusto näyttää tällä teemalla" save: "Tallenna" @@ -4555,9 +4542,9 @@ fi: new_name: "Uusi väripaletti" copy_name_prefix: "Kopio" delete_confirm: "Poistetaanko tämä väripaletti?" - undo: "kumoa" + undo: "Kumoa" undo_title: "Kumoa muutoksesi tähän väriin viimeisimmän tallennuskerran jälkeen." - revert: "palauta" + revert: "Palauta" revert_title: "Nollaa tämä väri vastaamaan Discoursen oletusväripaletin väriä." primary: name: "ensisijainen" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index d5bb432a79..515ad29fdc 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -315,7 +315,6 @@ fr: remove_reminder_keep_bookmark: "Supprimer le rappel et conserver le signet" created_with_reminder: "Vous avez mis un signet à ce message avec un rappel pour le %{date}. %{name}" created_with_reminder_generic: "Vous avez mis ceci en signet avec un rappel pour le %{date}. %{name}" - remove: "Retirer le signet" delete: "Supprimer le signet" confirm_delete: "Voulez-vous vraiment supprimer ce signet ? Le rappel sera également supprimé." confirm_clear: "Voulez-vous vraiment retirer tous les signets de ce sujet ?" @@ -385,7 +384,6 @@ fr: enable: "Activer" disable: "Désactiver" continue: "Continuer" - undo: "Annuler" switch_to_anon: "Activer le mode anonyme" switch_from_anon: "Quitter le mode anonyme" banner: @@ -682,6 +680,7 @@ fr: denied: "refusé" undone: "demande annulée" handle: "gérer une demande d'adhésion" + undo: "Annuler" manage: title: "Gérer" name: "Nom" @@ -1051,7 +1050,6 @@ fr: perm_denied_expl: "Vous n'avez pas autorisé les notifications. Autorisez-les à partir des paramètres de votre navigateur." disable: "Désactiver les notifications" enable: "Activer les notifications" - each_browser_note: 'Remarque : vous devez modifier ce paramètre sur chaque navigateur que vous utiliserez. Toutes les notifications seront désactivées lorsque vous serez en mode « Ne pas déranger », quel que soit la valeur de ce paramètre.' consent_prompt: "Souhaitez-vous recevoir des notifications en temps réel en cas de réponse à vos messages ?" dismiss: "Vu" dismiss_notifications: "Tout vu" @@ -3120,7 +3118,6 @@ fr: create_for_topic: "Créer un signet associé à ce sujet" edit: "Modifier le signet" edit_for_topic: "Modifier le signet associé à ce sujet" - created: "Créé" updated: "Mis à jour" name: "Nom" name_placeholder: "À quoi correspond ce signet ?" @@ -3422,7 +3419,6 @@ fr: category_title: "Catégorie" history_capped_revisions: "Historique des 100 dernières modifications" history: "Historique" - changed_by: "par %{author}" raw_email: title: "E-mail entrant" not_available: "Indisponible !" @@ -3837,7 +3833,6 @@ fr: new_count: one: "%{count} nouveau" other: "%{count} nouveaux" - toggle_section: "basculer la section" more: "Plus" all_categories: "Toutes les catégories" all_tags: "Toutes les étiquettes" @@ -3846,7 +3841,6 @@ fr: header_link_text: "À propos" messages: header_link_text: "Messages" - header_action_title: "créer un message direct" links: inbox: "Boîte de réception" sent: "Envoyés" @@ -3863,7 +3857,6 @@ fr: none: "Vous n'avez ajouté aucune étiquette." click_to_get_started: "Cliquez ici pour commencer." header_link_text: "Étiquettes" - header_action_title: "modifier les étiquettes de votre barre latérale" configure_defaults: "Configurer les valeurs par défaut" categories: links: @@ -3873,11 +3866,9 @@ fr: none: "Vous n'avez ajouté aucune catégorie." click_to_get_started: "Cliquez ici pour commencer." header_link_text: "Catégories" - header_action_title: "modifier les catégories de votre barre latérale" configure_defaults: "Configurer les valeurs par défaut" community: header_link_text: "Communauté" - header_action_title: "créer un nouveau sujet" links: about: content: "À propos" @@ -3892,10 +3883,8 @@ fr: content: "FAQ" groups: content: "Groupes" - title: "Tous les groupes" users: content: "Utilisateurs" - title: "Tous les utilisateurs" my_posts: content: "Mes messages" draft_count: @@ -3903,7 +3892,6 @@ fr: other: "%{count} brouillons" review: content: "À examiner" - title: "à examiner" pending_count: "%{count} en attente" welcome_topic_banner: title: "Créez votre sujet de bienvenue" @@ -4350,7 +4338,6 @@ fr: button_title: "Envoyer des invitations" customize: title: "Personnaliser" - long_title: "Personnalisations du site" preview: "prévisualiser" explain_preview: "Voir le site avec ce thème activé" save: "Enregistrer" @@ -4555,9 +4542,9 @@ fr: new_name: "Nouvelle palette de couleurs" copy_name_prefix: "Copie de" delete_confirm: "Supprimer cette palette de couleurs ?" - undo: "annuler" + undo: "Annuler" undo_title: "Annuler les modifications que vous avez apportées à cette couleur depuis son dernier enregistrement." - revert: "rétablir" + revert: "Rétablir" revert_title: "Rétablir la couleur de la palette par défaut de Discourse." primary: name: "primaire" diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 80f7ece0db..de173728ab 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -270,7 +270,6 @@ gl: edit: "Editar marcador" not_bookmarked: "marcar esta publicación" created_with_reminder: "Fixou como marcador esta publicación cun recordatorio %{date}. %{name}" - remove: "Retirar marcador" delete: "Eliminar marcador" confirm_delete: "Confirma a eliminación deste marcador? O recordatorio tamén se eliminará." confirm_clear: "Confirma o borrado de todos os marcadores deste tema?" @@ -330,7 +329,6 @@ gl: enable: "Activar" disable: "Desactivar" continue: "Continuar" - undo: "Desfacer" switch_to_anon: "Entrar no modo anónimo" switch_from_anon: "Saír do modo anónimo" banner: @@ -615,6 +613,7 @@ gl: denied: "rexeitado" undone: "petición retirada" handle: "xestionar a petición de incorporación" + undo: "Desfacer" manage: title: "Xestionar" name: "Nome" @@ -952,7 +951,6 @@ gl: perm_denied_expl: "Denegou o permiso para recibir notificacións no navegador. Permita as notificacións na configuración do navegador." disable: "Desactivar as notificacións" enable: "Activar as notificacións" - each_browser_note: 'Nota: Ten que cambiar este axuste en todos os navegadores que use. Desactivaranse todas as notificacións cando estea en «non molestar», independentemente deste axuste.' consent_prompt: "Quere recibir notificacións ao vivo cando a xente lea as súas publicacións?" dismiss: "Desbotar" dismiss_notifications: "Desbotar todo" @@ -2719,7 +2717,6 @@ gl: bookmarks: create: "Crear marcador" edit: "Editar marcador" - created: "Creado" updated: "Actualizado" name: "Nome" name_placeholder: "Para que serve este marcador?" @@ -2998,7 +2995,6 @@ gl: one: "usuario" other: "usuarios" category_title: "Categoría" - changed_by: "de %{author}" raw_email: title: "Correo entrante" not_available: "Non dispoñíbel!" @@ -3369,14 +3365,12 @@ gl: content: "PMF" groups: content: "Grupos" - title: "Todos os grupos" users: content: "Usuarios" my_posts: content: "As miñas publicacións" review: content: "Revisar" - title: "revisar" admin_js: type_to_filter: "escriba para filtrar..." admin: @@ -3772,7 +3766,6 @@ gl: button_title: "Enviar convites" customize: title: "Personalizar" - long_title: "Personalización do sitio" preview: "visualizar" explain_preview: "Ver o sitio con este tema activado" save: "Gardar" @@ -3966,9 +3959,9 @@ gl: new_name: "Nova paleta de cores" copy_name_prefix: "Copiar de" delete_confirm: "Eliminar esta paleta de cores?" - undo: "desfacer" + undo: "Desfacer" undo_title: "Desfai os teus cambio nesta cor desde a última vez que a gardaches." - revert: "reverter" + revert: "Reverter" revert_title: "Restabelece esta cor na paleta de cores predeterminada de Discourse." primary: name: "primario" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 38e803a547..acae120830 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -380,7 +380,6 @@ he: remove_reminder_keep_bookmark: "הסרת תזכורת ושמירת הסימנייה" created_with_reminder: "סימנת את הפוסט הזה עם תזכורת %{date}. %{name}" created_with_reminder_generic: "סימנת את זה עם תזכורת %{date}. %{name}" - remove: "הסרה מהסימניות" delete: "מחיקת סימנייה" confirm_delete: "למחוק את הסימנייה הזאת? גם התזכורת תימחק." confirm_clear: "לנקות את כל הסימניות מנושא זה?" @@ -460,7 +459,6 @@ he: enable: "הפעלה" disable: "השבתה" continue: "המשך" - undo: "לבטל פעולה" switch_to_anon: "מעבר למצב אלמוני" switch_from_anon: "יציאה ממצב אלמוני" banner: @@ -785,6 +783,7 @@ he: denied: "נדחה" undone: "הבקשה נמשכה" handle: "טיפול בבקשות חברות" + undo: "לבטל פעולה" manage: title: "ניהול" name: "שם" @@ -1167,7 +1166,7 @@ he: perm_denied_expl: "דחית הרשאה לקבלת התראות. יש לאפשר התראות בהגדרות הדפדפן שלך." disable: "השבתת התראות" enable: "הפעלת התראות" - each_browser_note: 'הערה: עליך לשנות הגדרה זו בכל דפדפן שבו אתה משתמש. כל ההודעות יושבתו במצב "אל תפריע", ללא קשר להגדרה זו.' + each_browser_note: 'הערה: עליך לשנות הגדרה זו בכל דפדפן שמשמש אותך. כל ההודעות יושבתו אם בחרת להשהות התראות מתפריט המשתמש, ללא קשר להגדרה זו.' consent_prompt: "לקבל התראות חיות כשמתקבלות תגובות לפוסטים שלך?" dismiss: "דחה" dismiss_notifications: "להתעלם מהכול" @@ -3440,7 +3439,6 @@ he: create_for_topic: "יצירת סימנייה לנושא" edit: "עריכת סימנייה" edit_for_topic: "עריכת סימנייה לנושא" - created: "נוצר" updated: "עודכן" name: "שם" name_placeholder: "עבור מה הסימנייה?" @@ -3761,7 +3759,6 @@ he: category_title: "קטגוריה" history_capped_revisions: "היסטוריה, 100 התיקונים האחרונים" history: "היסטוריה" - changed_by: "מאת %{author}" raw_email: title: "דוא״ל נכנס" not_available: "לא זמין!" @@ -4262,25 +4259,29 @@ he: configure_defaults: "הגדרת ברירות מחדל" community: header_link_text: "קהילה" - header_action_title: "יצירת נושא חדש" + header_action_title: "יצירת נושא" links: about: content: "על אודות" + title: "פרטים נוספים על האתר הזה" admin: content: "הנהלה" + title: "הגדרות האתר ודוחות" badges: content: "עיטורים" + title: "כל העיטורים שאפשר לקבל" everything: content: "הכול" title: "כל הנושאים" faq: content: "שאלות נפוצות" + title: "הנחיות לשימוש באתר הזה" groups: content: "קבוצות" - title: "כל הקבוצות" + title: "רשימת קבוצות משתמשים זמינות" users: content: "משתמשים" - title: "כל המשתמשים" + title: "רשימת כל המשתמשים" my_posts: content: "הפוסטים שלי" title: "הפעילות האחרונה שלי בנושא" @@ -4292,7 +4293,7 @@ he: other: "%{count} טיוטות" review: content: "סקירה" - title: "סקירה" + title: "פוסטים מסומנים ופריטים אחרים בתור" pending_count: "%{count} ממתינים" welcome_topic_banner: title: "כאן ניתן ליצור את נושא קבלת הפנים שלך" @@ -4449,6 +4450,9 @@ he: other: "ל־%{count} משתמשים יש את כתובות הדוא״ל בשם התחום החדש ויתווספו לקבוצה." automatic_membership_associated_groups: "משתמשים שהם חברים בקבוצה בשירות שמופיע כאן יתווספו לקבוצה הזאת אוטומטית עם כניסתם לשירות." primary_group: "קבע כקבוצה ראשית באופן אוטומטי" + alert: + primary_group: "מכיוון שזו קבוצה עיקרית, ייעשה שימוש בשם ‚%{group_name}’ במחלקות CSS שחשופות לכולם." + flair_group: "מכיוון שבקבוצה זאת יש סמלון לחברים, השם ‚%{group_name}’ יהיה גלוי לכולם." name_placeholder: "שם קבוצה, ללא רווחים, לפי הכללים של שמות משתמשים" primary: "קבוצה ראשית" no_primary: "(אין קבוצה ראשית)" @@ -4756,7 +4760,6 @@ he: button_title: "משלוח הזמנות" customize: title: "התאמה אישית" - long_title: "התאמה של האתר" preview: "תצוגה מקדימה" explain_preview: "הצגת האתר עם ערכת העיצוב הזאת מופעלת" save: "שמור" @@ -4963,9 +4966,9 @@ he: new_name: "לוח צבעים חדש" copy_name_prefix: "העתק של" delete_confirm: "למחוק לוח צבעים זה?" - undo: "ביטול (Unfo)" + undo: "לבטל פעולה" undo_title: "ביטול השינויים לצבע זה מאז הפעם שעברה שהוא נשמר." - revert: "לחזור" + revert: "להחזיר" revert_title: "לאפס את הצבע הזה ללוח הצבעים כברירת מחדל של Discourse." primary: name: "ראשי" diff --git a/config/locales/client.hr.yml b/config/locales/client.hr.yml index ec6ec9e9aa..1b13ea2b8c 100644 --- a/config/locales/client.hr.yml +++ b/config/locales/client.hr.yml @@ -344,7 +344,6 @@ hr: remove_reminder_keep_bookmark: "Ukloni podsjetnik i zadrži oznaku" created_with_reminder: "Označili ste ovu objavu s podsjetnikom %{date}. %{name}" created_with_reminder_generic: "Označili ste ovo s podsjetnikom %{date}. %{name}" - remove: "Ukloni zabilješku" delete: "Izbriši oznaku" confirm_delete: "Jeste li sigurni da želite izbrisati ovu oznaku? Podsjetnik će također biti izbrisan." confirm_clear: "Jeste li sigurni da želite ukloniti sve vaše zabilježbe iz ove teme?" @@ -419,7 +418,6 @@ hr: enable: "Omogući" disable: "Onemogući" continue: "Nastavi" - undo: "Vrati na prethodno" switch_to_anon: "Uđi u anonimni mod" switch_from_anon: "Napusti neimenovani način" banner: @@ -726,6 +724,7 @@ hr: denied: "odbijen" undone: "zahtjev poništen" handle: "obradi zahtjev za članstvo" + undo: "Vrati na prethodno" manage: title: "Upravljanje" name: "Ime" @@ -1098,7 +1097,6 @@ hr: perm_denied_expl: "Odbili ste dopuštenje za obavijesti. Dopusti obavijesti putem postavki preglednika." disable: "Isključi obavijesti" enable: "Uključi obavijesti" - each_browser_note: 'Napomena: Ovu postavku morate promijeniti na svakom pregledniku kojeg koristite. Sve obavijesti bit će onemogućene kada su u "ne ometaj", bez obzira na ovu postavku.' consent_prompt: "Želite li obavijesti (uživo) kada ljudi odgovaraju na vaše postove?" dismiss: "Skloni" dismiss_notifications: "Skloni sve" @@ -3142,7 +3140,6 @@ hr: create_for_topic: "Napravite oznaku za temu" edit: "Uredi oznaku" edit_for_topic: "Uredi oznaku za temu" - created: "Stvoreno" updated: "Ažurirano" name: "Ime i prezime" name_placeholder: "Čemu služi ova oznaka?" @@ -3450,7 +3447,6 @@ hr: category_title: "Kategorija" history_capped_revisions: "Povijest, posljednjih 100 izmjena" history: "Povijest" - changed_by: "od %{author}" raw_email: title: "Dolazna e-pošta" not_available: "Nije dostupno!" @@ -3898,7 +3894,6 @@ hr: header_link_text: "Kategorije" community: header_link_text: "Zajednica" - header_action_title: "otvori novu temu" links: about: content: "O nama" @@ -3913,14 +3908,12 @@ hr: content: "ČPP" groups: content: "Grupe" - title: "Sve grupe" users: content: "Korisnika" my_posts: content: "Moje objave" review: content: "Osvrt" - title: "osvrt" admin_js: type_to_filter: "upiši za filtriranje" admin: @@ -4362,7 +4355,6 @@ hr: button_title: "Pošalji pozivnice" customize: title: "Prilagodba" - long_title: "Prilagodba stranice" preview: "predprikaz" explain_preview: "Pogledajte web-mjesto s omogućenom ovom temom" save: "Spremi" @@ -4563,9 +4555,9 @@ hr: new_name: "Nova paleta boja" copy_name_prefix: "Kopija" delete_confirm: "Izbrisati ovu paletu boja?" - undo: "poništi" + undo: "Vrati na prethodno" undo_title: "Poništi promjene ovoj boji od zadnjeg puta kad su spremljene." - revert: "vrati" + revert: "Vrati nazad" revert_title: "Vratite ovu boju na zadanu paletu boja diskursa." primary: name: "primarna" diff --git a/config/locales/client.hu.yml b/config/locales/client.hu.yml index 11bdd5727d..165a1a7266 100644 --- a/config/locales/client.hu.yml +++ b/config/locales/client.hu.yml @@ -315,7 +315,6 @@ hu: remove_reminder_keep_bookmark: "Emlékeztető eltávolítása és könyvjelző megtartása" created_with_reminder: "Könyvjelzőzte ezt a bejegyzést, és emlékeztetőt állított be ekkorra: %{date}. %{name}" created_with_reminder_generic: "Könyvjelzőzte ezt, és emlékeztetőt állított be ekkorra: %{date}. %{name}" - remove: "Könyvjelző eltávolítása" delete: "Könyvjelző törlése" confirm_delete: "Biztos, hogy törli ezt a könyvjelzőt? Az emlékeztető is törlődni fog." confirm_clear: "Biztos, hogy törli az összes könyvjelzőjét ebből a témából?" @@ -385,7 +384,6 @@ hu: enable: "Engedélyezés" disable: "Letiltás" continue: "Folytatás" - undo: "Visszavonás" switch_to_anon: "Anonim módba lépés" switch_from_anon: "Kilépés az anonim módból" banner: @@ -682,6 +680,7 @@ hu: denied: "tiltva" undone: "kérés visszavonva" handle: "tagsági kérés kezelése" + undo: "Visszavonás" manage: title: "Kezelés" name: "Név" @@ -1051,7 +1050,7 @@ hu: perm_denied_expl: "Letiltotta a figyelmeztetéseket. Engedélyezze őket a böngésző beállításaiban." disable: "Értesítések kikapcsolása" enable: "Értesítések bekapcsolása" - each_browser_note: 'Megjegyzés: Ezt a beállítást minden használt böngészőben meg kell változtatnia. Az összes értesítés letiltásra került ettől a beállítástól függetlenül, ha a profilját „Ne zavarjanak” állapotba teszi.' + each_browser_note: 'Megjegyzés: Ezt a beállítást minden használt böngészőben módosítania kell. Ettől a beállítástól függetlenül minden értesítés le lesz tiltva, ha szünetelteti az értesítéseket a felhasználói menüből.' consent_prompt: "Szeretne élő értesítéseket kapni, ha valaki válaszol a hozzászólásaira?" dismiss: "Elvetés" dismiss_notifications: "Összes elvetése" @@ -3041,7 +3040,6 @@ hu: bookmarks: create: "Könyvjelző létrehozása" edit: "Könyvjelző szerkesztése" - created: "Létrehozott" updated: "Frissítve" name: "Név" name_placeholder: "Mire való ez a könyvjelző?" @@ -3269,7 +3267,6 @@ hu: other: "felhasználók" category_title: "Kategória" history: "Idővonal" - changed_by: "szerző %{author}" raw_email: title: "Bejövő email" not_available: "Nem elérhető!" @@ -3618,7 +3615,7 @@ hu: header_link_text: "Névjegy" messages: header_link_text: "Üzenetek" - header_action_title: "új személyes üzenet létrehozása" + header_action_title: "Személyes üzenet létrehozása" links: inbox: "Beérkezett üzenetek" sent: "Elküldött" @@ -3635,7 +3632,6 @@ hu: none: "Még nem adott hozzá címkéket." click_to_get_started: "Kattintson ide a kezdéshez." header_link_text: "Címkék" - header_action_title: "szerkessze az oldalsávja címkéit" configure_defaults: "Alapértelmezések beállítása" categories: links: @@ -3645,36 +3641,39 @@ hu: none: "Még nem adott hozzá kategóriákat." click_to_get_started: "Kattintson ide a kezdéshez." header_link_text: "Kategóriák" - header_action_title: "szerkessze az oldalsávja kategóriáit" configure_defaults: "Alapértelmezések beállítása" community: header_link_text: "Közösség" - header_action_title: "új téma létrehozása" + header_action_title: "Téma létrehozása" links: about: content: "Névjegy" + title: "További részletek erről az oldalról" admin: content: "Adminisztrátor" + title: "Oldalbeállítások és jelentések" badges: content: "Jelvények" + title: "Az összes megszerezhető jelvény" everything: content: "Összes" title: "Minden téma" faq: content: "GYIK" + title: "Útmutató az oldal használatához" groups: content: "Csoportok" - title: "Összes csoport" + title: "Az elérhető felhasználói csoportok listája" users: content: "Felhasználók" - title: "Minden felhasználó" + title: "Az összes felhasználó listája" my_posts: content: "Saját bejegyzéseim" title: "Legutóbbi téma tevékenységem" title_drafts: "Nem közzétett piszkozataim" review: content: "Áttekintés" - title: "áttekintés" + title: "Megjelölt bejegyzések és egyéb sorban álló elemek" pending_count: "%{count} függőben" welcome_topic_banner: title: "Hozzon létre üdvözlő témát" @@ -4055,7 +4054,6 @@ hu: button_title: "Meghívók küldése" customize: title: "Személyre szabás" - long_title: "Oldal testreszabása" preview: "előnézet" explain_preview: "Az oldal megtekintése ezzel a témával" save: "Mentés" diff --git a/config/locales/client.hy.yml b/config/locales/client.hy.yml index f5f13055ce..c283adcaa5 100644 --- a/config/locales/client.hy.yml +++ b/config/locales/client.hy.yml @@ -259,7 +259,6 @@ hy: create: "Ստեղծել էջանշան" edit: "Խմբագրել էջանշանը" not_bookmarked: "Էջանշել այս գրառումը" - remove: "Հեռացնել էջանշանը" delete: "Ջնջել Էջանշանը" confirm_delete: "Դուք համոզվա՞ծ եք, որ ցանկանում եք ջնջել այս էջանշանը: Բոլոր կապակցված հիշեցումները կջնջվեն: " confirm_clear: "Դուք համոզվա՞ծ եք, որ ցանկանում եք հեռացնել այս թեմայի բոլոր էջանշանները:" @@ -311,7 +310,6 @@ hy: enable: "Միացնել" disable: "Անջատել" continue: "Շարունակել" - undo: "Ետարկել" switch_to_anon: "Սկսել Անանուն Ռեժիմը" switch_from_anon: "Ավարտել Անանուն Ռեժիմը" banner: @@ -546,6 +544,7 @@ hy: denied: "Մերժվել" undone: "հայցը չեղարկել" handle: "Մշակել հարցումը " + undo: "Ետարկել" manage: title: "Կառավարել" name: "Անուն" @@ -2316,7 +2315,6 @@ hy: bookmarks: create: "Ստեղծել էջանշան" edit: "Խմբագրել էջանշանը" - created: "Ստեղծված" updated: "Թարմացված" name: "Անուն" name_placeholder: "Ինչի՞ համար է այս էջանշանը: " @@ -2555,7 +2553,6 @@ hy: one: "օգտատեր" other: "օգտատերեր" category_title: "Կատեգորիա" - changed_by: "%{author}-ի կողմից" raw_email: title: "Մուտքային Էլ. նամակ" not_available: "Հասանելի չէ!" @@ -2901,14 +2898,12 @@ hy: content: "ՀՏՀ" groups: content: "Խմբեր" - title: "Բոլոր խմբերը" users: content: "Օգտատերեր" my_posts: content: "Իմ Գրառումները" review: content: "Վերանայում" - title: "վերանայում" admin_js: type_to_filter: "գրեք ֆիլտրելու համար..." admin: @@ -3270,7 +3265,6 @@ hy: button_title: "Ուղարկել Հրավերներ" customize: title: "Անհատականացնել" - long_title: "Կայքի Անհատականացումներ" preview: "նախադիտում" explain_preview: "Դիտել այս կայքը՝ այս թեման միացված վիճակում" save: "Պահպանել" @@ -3441,9 +3435,9 @@ hy: new_name: "Նոր Գունապնակ" copy_name_prefix: "Կրկնօրինակ՝ " delete_confirm: "Ջնջե՞լ այս գունապնակը:" - undo: "ետարկել" + undo: "Ետարկել" undo_title: "Ետարկել այս գույնի Ձեր փոփոխությունները՝ սկսած դրա վերջին պահպանման պահից:" - revert: "վերադարձնել" + revert: "Հետադարձել" revert_title: "Վերահաստատել այս գույնը Discourse-ի լռելյայն գունապնակում:" primary: name: "հիմնական" diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index b7f9e1bf20..55f0067e5c 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -278,7 +278,6 @@ id: remove_reminder_keep_bookmark: "Hapus pengingat dan simpan penanda" created_with_reminder: "Anda telah menandai kiriman ini dengan pengingat %{date}. %{name}" created_with_reminder_generic: "Anda telah menandai kiriman ini dengan pengingat %{date}. %{name}" - remove: "Hilangkan penandaan" delete: "Hapus Bookmark" confirm_delete: "Yakin ingin menghapus penanda ini? Pengingat juga akan dihapus." confirm_clear: "Yakin ingin menghapus semua penandaan Anda dari topik ini?" @@ -343,7 +342,6 @@ id: enable: "Aktifkan" disable: "Nonaktifkan" continue: "Lanjutkan" - undo: "Batalkan perintah" switch_to_anon: "Masuki Mode Anonim" switch_from_anon: "Keluar Mode Anonim" banner: @@ -604,6 +602,7 @@ id: accepted: "Diterima" deny: "Menolak" denied: "ditolak" + undo: "Batalkan perintah" manage: title: "Mengelola" name: "Nama" @@ -1634,7 +1633,6 @@ id: controls: first: "Revisi pertama" bookmarks: - created: "Dibuat" name: "Nama" options: "Pilihan" category: @@ -1822,7 +1820,6 @@ id: content: "Pengguna" review: content: "Ulasan" - title: "ulasan" admin_js: type_to_filter: "ketik untuk memfilter..." admin: @@ -1935,8 +1932,8 @@ id: body_tag: text: "Konten" colors: - undo: "batalkan perintah" - revert: "dibalik" + undo: "Batalkan perintah" + revert: "Dibalik" email: title: "Surel" settings: "Pengaturan" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index cae91fcff1..3b169081ee 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -315,7 +315,6 @@ it: remove_reminder_keep_bookmark: "Rimuovi il promemoria e mantieni il segnalibro" created_with_reminder: "Hai aggiunto ai segnalibri questo messaggio con un promemoria %{date}. %{name}" created_with_reminder_generic: "L'hai aggiunto ai segnalibri con un promemoria per il %{date}. %{name}" - remove: "Rimuovi Segnalibro" delete: "Cancella Segnalibro" confirm_delete: "Vuoi eliminare questo segnalibro? Anche il promemoria verrà eliminato." confirm_clear: "Vuoi rimuovere tutti i segnalibri da questo argomento?" @@ -385,7 +384,6 @@ it: enable: "Attiva" disable: "Disattiva" continue: "Continua" - undo: "Annulla" switch_to_anon: "Avvia Modalità Anonima" switch_from_anon: "Esci da Modalità Anonima" banner: @@ -682,6 +680,7 @@ it: denied: "negato" undone: "richiesta annullata" handle: "gestisci le richieste di iscrizione" + undo: "Annulla" manage: title: "Gestisci" name: "Nome" @@ -1051,7 +1050,6 @@ it: perm_denied_expl: "Hai negato il permesso per le notifiche. Autorizza le notifiche tramite le impostazioni del tuo browser." disable: "Disabilita Notifiche" enable: "Abilita Notifiche" - each_browser_note: 'Nota: devi modificare questa impostazione su ogni browser che utilizzi. Tutte le notifiche verranno disabilitate quando l''utente è in stato "non disturbare", indipendentemente da questa impostazione.' consent_prompt: "Desideri ricevere notifiche in tempo reale quando qualcuno risponde a un tuo messaggio?" dismiss: "Ignora" dismiss_notifications: "Ignora tutti" @@ -3120,7 +3118,6 @@ it: create_for_topic: "Crea un segnalibro per quest'argomento" edit: "Modifica Segnalibro" edit_for_topic: "Modifica il segnalibro per quest'argomento" - created: "Creazione" updated: "Aggiornato" name: "Nome" name_placeholder: "A cosa serve questo segnalibro?" @@ -3422,7 +3419,6 @@ it: category_title: "Categoria" history_capped_revisions: "Cronologia, ultime 100 revisioni" history: "Cronologia" - changed_by: "da %{author}" raw_email: title: "Email In Arrivo" not_available: "Non disponibile!" @@ -3837,7 +3833,6 @@ it: new_count: one: "%{count} nuovo" other: "%{count} nuovi" - toggle_section: "attiva/disattiva sezione" more: "Altro" all_categories: "Tutte le categorie" all_tags: "Tutte le etichette" @@ -3846,7 +3841,6 @@ it: header_link_text: "Informazioni" messages: header_link_text: "Messaggi" - header_action_title: "crea un messaggio personale" links: inbox: "Posta in arrivo" sent: "Inviati" @@ -3863,7 +3857,6 @@ it: none: "Non hai aggiunto nessuna etichetta." click_to_get_started: "Clicca qui per iniziare." header_link_text: "Etichette" - header_action_title: "modifica le etichette nella barra laterale" configure_defaults: "Configura impostazioni predefinite" categories: links: @@ -3873,11 +3866,9 @@ it: none: "Non hai aggiunto nessuna categoria." click_to_get_started: "Clicca qui per iniziare." header_link_text: "Categorie" - header_action_title: "modifica le categorie nella barra laterale" configure_defaults: "Configura impostazioni predefinite" community: header_link_text: "Community" - header_action_title: "crea un nuovo argomento" links: about: content: "Informazioni" @@ -3892,10 +3883,8 @@ it: content: "FAQ" groups: content: "Gruppi" - title: "Tutti i gruppi" users: content: "Utenti" - title: "Tutti gli utenti" my_posts: content: "I miei Messaggi" draft_count: @@ -3903,7 +3892,6 @@ it: other: "%{count} bozze" review: content: "Revisiona" - title: "revisiona" pending_count: "%{count} in attesa" welcome_topic_banner: title: "Crea il tuo argomento di benvenuto" @@ -4350,7 +4338,6 @@ it: button_title: "Manda Inviti" customize: title: "Personalizza" - long_title: "Personalizzazioni Sito" preview: "anteprima" explain_preview: "Guarda il sito con questo tema abilitato" save: "Salva" @@ -4555,9 +4542,9 @@ it: new_name: "Nuova tavolozza dei colori" copy_name_prefix: "Copia di" delete_confirm: "Eliminare questa tavolozza dei colori?" - undo: "annulla" + undo: "Annulla" undo_title: "Annulla le modifiche effettuate a questo colore dall'ultimo salvataggio." - revert: "ripristina" + revert: "Ripristina" revert_title: "Reimposta questo colore alla tavolozza dei colori predefiniti di Discourse." primary: name: "primario" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index f3a789a70c..456811fa38 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -283,7 +283,6 @@ ja: remove_reminder_keep_bookmark: "リマインダーを削除してブックマークを保持" created_with_reminder: "%{date} のリマインダー付きでこの投稿をブックマークしました。%{name}" created_with_reminder_generic: "%{date} のリマインダー付きでこれをブックマークしました。%{name}" - remove: "ブックマークを削除" delete: "ブックマークを削除" confirm_delete: "このブックマークを削除してもよろしいですか?リマインダーも削除されます。" confirm_clear: "このトピックのすべてのブックマークをクリアしてもよろしいですか?" @@ -348,7 +347,6 @@ ja: enable: "有効化" disable: "無効化" continue: "続行" - undo: "元に戻す" switch_to_anon: "匿名モードを開始" switch_from_anon: "匿名モードを終了" banner: @@ -631,6 +629,7 @@ ja: denied: "拒否" undone: "リクエストの取り消し" handle: "メンバーシップリクエストを処理する" + undo: "元に戻す" manage: title: "管理" name: "名前" @@ -994,7 +993,6 @@ ja: perm_denied_expl: "通知へのアクセスが拒否されました。ブラウザの設定から通知を許可してください。" disable: "通知を無効にする" enable: "通知を有効にする" - each_browser_note: '注意: 使用するすべてのブラウザでこの設定を変更する必要があります。「おやすみモード」では、この設定に関係なくすべての通知が無効になります。' consent_prompt: "あなたの投稿に返信があったときライブ通知しますか?" dismiss: "閉じる" dismiss_notifications: "すべて閉じる" @@ -2970,7 +2968,6 @@ ja: create_for_topic: "トピックのブックマークを作成する" edit: "ブックマークを編集" edit_for_topic: "トピックのブックマークを編集する" - created: "作成" updated: "更新" name: "名前" name_placeholder: "これは何のブックマークですか?" @@ -3263,7 +3260,6 @@ ja: category_title: "カテゴリ" history_capped_revisions: "履歴、直近の 100 回のレビジョン" history: "履歴" - changed_by: "%{author}" raw_email: title: "受信メール" not_available: "利用できません!" @@ -3657,7 +3653,6 @@ ja: other: "未読 %{count}" new_count: other: "新規 %{count}" - toggle_section: "セクションの切り替え" more: "もっと" all_categories: "すべてのカテゴリ" all_tags: "すべてのタグ" @@ -3666,7 +3661,6 @@ ja: header_link_text: "サイト情報" messages: header_link_text: "メッセージ" - header_action_title: "個人メッセージを作成" links: inbox: "受信トレイ" sent: "送信済み" @@ -3683,7 +3677,6 @@ ja: none: "追加したタグはまだありません" click_to_get_started: "ここをクリックして開始してください。" header_link_text: "タグ" - header_action_title: "サイドバーのタグを編集" configure_defaults: "デフォルトの構成" categories: links: @@ -3693,11 +3686,9 @@ ja: none: "追加したカテゴリはありません" click_to_get_started: "ここをクリックして開始してください。" header_link_text: "カテゴリ" - header_action_title: "サイドバーのカテゴリを編集" configure_defaults: "デフォルトの構成" community: header_link_text: "コミュニティー" - header_action_title: "新しいトピックを作成" links: about: content: "サイト情報" @@ -3712,17 +3703,14 @@ ja: content: "FAQ" groups: content: "グループ" - title: "すべてのグループ" users: content: "ユーザー" - title: "すべてのユーザー" my_posts: content: "自分の投稿" draft_count: other: "%{count} 下書き" review: content: "レビュー" - title: "レビュー" pending_count: "保留中 %{count}" welcome_topic_banner: title: "ウェルカムトピックを作成しましょう" @@ -4165,7 +4153,6 @@ ja: button_title: "招待を送信" customize: title: "カスタマイズ" - long_title: "サイトのカスタマイズ" preview: "プレビュー" explain_preview: "このテーマを有効にした状態でサイトを見る" save: "保存" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 22a54d50cc..696db1a595 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -148,6 +148,7 @@ ko: banner: enabled: "%{when}에 배너를 만들었습니다. 사용자가 닫을 때까지 모든 페이지의 상단에 나타납니다." disabled: "%{when}에 이 배너를 제거했습니다. 더 이상 모든 페이지의 상단에 표시되지 않습니다." + forwarded: "위의 이메일을 전달했습니다." topic_admin_menu: "주제 관리" skip_to_main_content: "메인 콘텐츠로 건너뛰기" emails_are_disabled: "관리자에 의해 모든 이메일의 발신이 비활성화되었습니다. 어떤 종류의 이메일 알림도 전송되지 않습니다." @@ -162,6 +163,7 @@ ko: themes: default_description: "디폴트" broken_theme_alert: "테마/구성 요소에 오류가 있어 사이트가 작동하지 않을 수 있습니다." + error_caused_by: "'%{name}' 테마에서 오류가 발생하였습니다. 여기를 클릭해 업데이트, 재설정 혹은 테마 사용을 중지하세요." only_admins: "(이 메시지는 사이트 관리자에게만 표시됩니다)" s3: regions: @@ -199,6 +201,7 @@ ko: delete: "삭제하기" generic_error: "죄송합니다. 오류가 발생했습니다." generic_error_with_reason: "오류가 발생했습니다. %{error}" + multiple_errors: "복수의 오류가 발생하였습니다: %{errors}" sign_up: "회원가입" log_in: "로그인" age: "나이" @@ -277,7 +280,6 @@ ko: not_bookmarked: "이 게시물 북마크하기" remove_reminder_keep_bookmark: "미리 알림 제거 및 북마크 유지" created_with_reminder: "%{date} 미리 알림과 함께 이 게시물을 북마크했습니다. %{name}" - remove: "북마크 제거" delete: "북마크 삭제" confirm_delete: "이 북마크를 삭제할까요? 미리 알림도 삭제됩니다." confirm_clear: "이 주제의 모든 북마크를 지울까요?" @@ -341,7 +343,6 @@ ko: enable: "활성화" disable: "비활성화" continue: "계속하기" - undo: "실행 취소" switch_to_anon: "익명 모드 시작" switch_from_anon: "익명 모드 종료" banner: @@ -529,6 +530,8 @@ ko: relative: "상대적" time_shortcut: now: "지금" + in_one_hour: "한 시간 내에" + in_two_hours: "두 시간 내에" later_today: "오늘 중" next_business_day: "다음 영업일" tomorrow: "내일" @@ -593,6 +596,8 @@ ko: reset_to_default: "디폴트로 리셋" group: all: "모든 그룹" + sort: + label: "%{criteria}로 정렬" group_histories: actions: change_group_setting: "그룹 설정 변경" @@ -619,6 +624,7 @@ ko: denied: "거부됨" undone: "요청 실행 취소" handle: "멤버십 요청 처리" + undo: "실행 취소" manage: title: "관리" name: "이름" @@ -919,6 +925,7 @@ ko: all: "모두" read: "읽음" unread: "읽지 않음" + unseen: "읽지 않음" ignore_duration_title: "사용자 무시" ignore_duration_username: "사용자명" ignore_duration_when: "기간:" @@ -979,7 +986,6 @@ ko: perm_denied_expl: "알림 권한을 거부한 상태입니다. 브라우저 설정에서 알림을 허용해 주세요." disable: "알림 비활성화" enable: "알림 활성화" - each_browser_note: '참고: 사용하는 모든 브라우저에서 이 설정을 변경해야 합니다. 이 설정에 관계없이 ''방해 금지''에 있는 경우 모든 알림이 비활성화됩니다.' consent_prompt: "내 게시물에 댓글이 달리면 실시간으로 알림을 받을까요?" dismiss: "해제" dismiss_notifications: "모두 해제" @@ -2356,6 +2362,7 @@ ko: show_links: "이 주제에 링크 표시" collapse_details: "주제 상세 정보 접기" expand_details: "주제 상세 정보 펼치기" + read_more_in_category: "더 읽고 싶으신가요? %{categoryLink}의 다른 토픽을 살펴보거나 최신 토픽을 확인해 보세요." unread_indicator: "아직 이 주제의 마지막 게시물을 읽은 회원이 없습니다." suggest_create_topic: 새 대화를 시작할 준비가 되었나요? jump_reply_up: 이전 댓글로 이동 @@ -2862,7 +2869,6 @@ ko: create_for_topic: "주제에 대한 북마크 만들기" edit: "북마크 편집" edit_for_topic: "주제에 대한 북마크 편집" - created: "생성일" updated: "업데이트일" name: "이름" name_placeholder: "이 북마크는 무엇인가요?" @@ -3146,7 +3152,6 @@ ko: category_title: "카테고리" history_capped_revisions: "히스토리, 최근 100개의 수정 버전" history: "히스토리" - changed_by: "작성자: %{author}" raw_email: title: "수신 이메일" not_available: "Raw 이메일이 가능하지 않습니다." @@ -3547,7 +3552,6 @@ ko: none: "태그를 추가하지 않았습니다." click_to_get_started: "시작하려면 여기를 클릭하십시오." header_link_text: "태그" - header_action_title: "사이드바 태그 편집" configure_defaults: "기본값 구성" categories: links: @@ -3555,10 +3559,8 @@ ko: content: "카테고리 추가" click_to_get_started: "시작하려면 여기를 클릭하십시오." header_link_text: "카테고리" - header_action_title: "사이드바 카테고리 편집" community: header_link_text: "커뮤니티" - header_action_title: "새글 쓰기" links: about: content: "소개" @@ -3573,17 +3575,14 @@ ko: content: "자주하는 질문" groups: content: "그룹" - title: "모든 그룹" users: content: "사용자들" - title: "모든 사용자" my_posts: content: "내 글" draft_count: other: "초안 %{count}" review: content: "검토" - title: "검토" welcome_topic_banner: title: "환영 글 만들기" button_title: "편집 시작" @@ -4011,7 +4010,6 @@ ko: button_title: "초대 전송" customize: title: "사용자 지정" - long_title: "사이트 사용자 지정" preview: "미리 보기" explain_preview: "이 테마 적용한 사이트 보기" save: "저장" @@ -4210,7 +4208,7 @@ ko: new_name: "새 색상 팔레트" copy_name_prefix: "다음의 사본" delete_confirm: "이 색상 팔레트를 삭제할까요?" - undo: "실행 복귀" + undo: "실행 취소" undo_title: "마지막 저장 이후 색상 변경사항을 실행 취소합니다." revert: "되돌리기" revert_title: "이 색상을 Discourse의 디폴트 색상 팔레트로 리셋합니다." diff --git a/config/locales/client.lt.yml b/config/locales/client.lt.yml index 6d5f4eec1d..f80713d935 100644 --- a/config/locales/client.lt.yml +++ b/config/locales/client.lt.yml @@ -335,7 +335,6 @@ lt: not_bookmarked: "pažymėkite šį įrašą" remove_reminder_keep_bookmark: "Pašalinkite priminimą ir išsaugokite žymę" created_with_reminder: "Jūs pažymėjote šį įrašą priminimu %{date}. %{name}" - remove: "Pašalinti Žymę" delete: "Ištrinti žymę" confirm_delete: "Ar tikrai norite ištrinti šią žymę? Priminimas taip pat bus ištrintas." confirm_clear: "Ar tikrai norite išvalyti visas savo žymes iš šios temos?" @@ -392,7 +391,6 @@ lt: enable: "Įjungti" disable: "Išjungti" continue: "Tęsti" - undo: "Anuliuoti" switch_to_anon: "Pasirinkti slaptumo būseną" switch_from_anon: "Išjunkti slaptumo būseną" banner: @@ -656,6 +654,7 @@ lt: denied: "atmesti" undone: "prašymas anuliuotas" handle: "tvarkyti narystės užklausą" + undo: "Anuliuoti" manage: title: "Redaguoti" name: "Vardas" @@ -1008,7 +1007,6 @@ lt: perm_denied_expl: "Užklausa atmesta nes jūs to neleidote. Jaigu norite gauti perspėjimus, tai galite jūsų naršyklės nustatymuose." disable: "Išjungti Pranešimus" enable: "Galimi Pranešimai" - each_browser_note: 'Pastaba: šį nustatymą turite pakeisti kiekvienoje naudojamoje naršyklėje. Visi pranešimai bus išjungti, kai yra „netrukdyti“ režimas, neatsižvelgiant į šį nustatymą.' consent_prompt: "Ar norite gauti tiesioginius pranešimus, kai žmonės atsakys į jūsų įrašus?" dismiss: "Praleisti" dismiss_notifications: "Atmesti visus" @@ -2819,7 +2817,6 @@ lt: create_for_topic: "Sukurti žymę temai" edit: "Redaguoti žymę" edit_for_topic: "Redaguoti temos žymę" - created: "Sukurta" updated: "Atnaujinta" name: "Vardas" name_placeholder: "Kam skirta ši žymė?" @@ -3106,7 +3103,6 @@ lt: category_title: "Kategorija" history_capped_revisions: "Istorija, paskutiniai 100 peržiūrų" history: "Istorija" - changed_by: "pagal %{author}" raw_email: title: "Įeinantis paštas" not_available: "Nėra galimybių!" @@ -3510,7 +3506,6 @@ lt: header_link_text: "Kategorijos" community: header_link_text: "Bendruomenė" - header_action_title: "sukurti naują temą" links: about: content: "Apie" @@ -3525,14 +3520,12 @@ lt: content: "DUK" groups: content: "Grupės" - title: "Visos grupės" users: content: "Vartotojai" my_posts: content: "Mano Įrašai" review: content: "Peržiūra" - title: "peržiūra" admin_js: type_to_filter: "įrašyk kažką dėl filtro..." admin: @@ -3917,7 +3910,6 @@ lt: button_title: "Siųsti pakvietimą" customize: title: "Pakeisti" - long_title: "Puslapio Keitimas" preview: "peržiūra" explain_preview: "Peržiūrėkite svetainę, kurioje įjungta ši tema" save: "Išsaugoti" @@ -4087,9 +4079,9 @@ lt: new_name: "Nauja spalvų paletė" copy_name_prefix: "Kopija" delete_confirm: "Ištrinti šią spalvų paletę?" - undo: "anuliuoti" + undo: "Anuliuoti" undo_title: "Anuliuoti šios spalvos pakeitimus į paskutinį kartą išsaugotus." - revert: "grąžinti" + revert: "Grąžinti" revert_title: "Iš naujo nustatykite šią spalvą į numatytąją „Discourse“ spalvų paletę." primary: name: "pirminė" diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 0375ecd7cf..eff9c32bcf 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -285,7 +285,6 @@ lv: created: "Jūs esat atzīmējis šo ziņu ar grāmatzīmi. %{name}" not_bookmarked: "atzīmējiet šo ziņu ar grāmatzīmi" created_with_reminder: "Jūs atzīmējāt šo ziņu ar grāmatzīmi, atgādinot %{date}. %{name}" - remove: "Noņemt grāmatzīmi" delete: "Dzēst grāmatzīmi" confirm_delete: "Vai tiešām vēlaties dzēst šo grāmatzīmi? Atgādinājums arī tiks dzēsts." confirm_clear: "Vai tiešām vēlaties notīrīt visas grāmatzīmes no šīs tēmas?" @@ -331,7 +330,6 @@ lv: enable: "Ieslēgt" disable: "Atslēgt" continue: "Turpināt" - undo: "Atsaukt" switch_to_anon: "Ieiet anonīmajā režīmā" switch_from_anon: "Iziet no anonīmā režīma" banner: @@ -588,6 +586,7 @@ lv: denied: "atteikts" undone: "pieprasījums atsaukts" handle: "izskatīt reģistrācijas pieprasījumu" + undo: "Atsaukt" manage: title: "Pārvaldīt" name: "Vārds" @@ -932,7 +931,6 @@ lv: perm_denied_expl: "Jūs neatļāvāt paziņojumus. Atļaujiet paziņojumus jūsu pārlūkprogrammas iestatījumos." disable: "Atslēgt paziņojumus" enable: "Ieslēgt paziņojumus" - each_browser_note: 'Piezīme. Šis iestatījums ir jāmaina katrā izmantotajā pārlūkprogrammā. Neatkarīgi no šī iestatījuma visi paziņojumi tiks atspējoti režīmā “netraucēt”.' consent_prompt: "Vai vēlaties saņemt tiešraides paziņojumus, kad cilvēki atbild uz jūsu ziņām?" dismiss: "Nerādīt" dismiss_notifications: "Vairs nerādīt visus" @@ -2345,7 +2343,6 @@ lv: title: "Parādīt e-pasta html daļu" button: "HTML" bookmarks: - created: "Radīts" name: "Vārds" options: "Iespējas" category: @@ -2545,7 +2542,6 @@ lv: one: "lietotājs" other: "lietotāji" category_title: "Sadaļa" - changed_by: "%{author}" raw_email: title: "Ienākošie e-pasti" not_available: "Nav pieejams!" @@ -2825,14 +2821,12 @@ lv: content: "BUJ" groups: content: "Grupas" - title: "Visas grupas" users: content: "Lietotāji" my_posts: content: "Mani ieraksti" review: content: "Pārskats" - title: "pārskats" admin_js: type_to_filter: "ievadiet, lai atlasītu..." admin: @@ -3065,7 +3059,6 @@ lv: button_title: "Nosūtīt ielūgumus" customize: title: "Pielāgot" - long_title: "Vietnes pielāgojumi" preview: "priekšskatījums" explain_preview: "Aplūkot vietni ar ieslēgtu šo dizainu" save: "Saglabāt" @@ -3131,8 +3124,8 @@ lv: body_tag: text: "Ķermenis" colors: - undo: "atsaukt" - revert: "atgriezt" + undo: "Atsaukt" + revert: "Atgriezt" success: name: "veiksmīgi" love: diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index ba312679e5..1b6c7481d7 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -278,7 +278,6 @@ nb_NO: edit: "Rediger bokmerke" not_bookmarked: "bokmerk dette innlegget" created_with_reminder: "Du har bokmerket dette innlegget med en påminnelse %{date} %{name}" - remove: "Fjern bokmerke" delete: "Slett bokmerke" confirm_delete: "Er du sikker på at du vil slette denne bokmerket? Påminnelsen vil også bli slettet." confirm_clear: "Er du sikker på at du vil fjerne alle favoritter fra dette emnet?" @@ -341,7 +340,6 @@ nb_NO: enable: "Aktiver" disable: "Deaktiver" continue: "Fortsett" - undo: "Angre" switch_to_anon: "Start inkognitomodus" switch_from_anon: "Avslutt inkognitomodus" banner: @@ -632,6 +630,7 @@ nb_NO: denied: "nektet" undone: "forespørsel, angret" handle: "Behandler forespørsel om medlemskap" + undo: "Angre" manage: title: "Behandle" name: "Navn" @@ -980,7 +979,6 @@ nb_NO: perm_denied_expl: "Du tillot ikke varsler. Tillat varsler via innstillingene i din nettleser." disable: "Slå av varslinger" enable: "Slå på varslinger" - each_browser_note: 'Merk: Denne innstillingen må endres for hver nettleser som brukes. Alle varsler vil bli deaktivert når du er i "ikke forstyrr", uavhengig av denne innstillingen.' consent_prompt: "Ønsker du å motta skrivebordsvarsler når andre svarer på dine innlegg?" dismiss: "Avslå" dismiss_notifications: "Forkast alle" @@ -2760,7 +2758,6 @@ nb_NO: bookmarks: create: "Opprett bokmerke" edit: "Rediger bokmerke" - created: "Opprettet" updated: "Oppdatert" name: "Navn" name_placeholder: "Hva er bokmerket etter?" @@ -3042,7 +3039,6 @@ nb_NO: other: "brukere" category_title: "Kategori" history_capped_revisions: "Historikk, siste 100 revisjoner" - changed_by: "av %{author}" raw_email: title: "Innkommende e-post" not_available: "Ikke tilgjengelig!" @@ -3433,14 +3429,12 @@ nb_NO: content: "O-S-S" groups: content: "Grupper" - title: "Alle grupper" users: content: "Brukere" my_posts: content: "Mine innlegg" review: content: "Gjennomgang" - title: "gjennomgang" admin_js: type_to_filter: "skriv for å filtrere…" admin: @@ -3792,7 +3786,6 @@ nb_NO: button_title: "Send invitasjoner" customize: title: "Tilpasse" - long_title: "Nettstedstilpasninger" preview: "forhåndsvisning" explain_preview: "Se siden iført denne drakten" save: "Lagre" @@ -3931,9 +3924,9 @@ nb_NO: colors: title: "Farger" copy_name_prefix: "Kopi av" - undo: "angre" + undo: "Angre" undo_title: "Fjern endringer av denne fargen siden sist den ble lagret." - revert: "gå tilbake" + revert: "Reverser" primary: name: "primær" description: "Det meste av tekst, ikoner og kanter." diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 1bfbc32e08..7792e8e4ba 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -315,7 +315,6 @@ nl: remove_reminder_keep_bookmark: "Herinnering verwijderen en bladwijzer behouden" created_with_reminder: "Je hebt een bladwijzer voor dit bericht gemaakt met een herinnering voor %{date}. %{name}" created_with_reminder_generic: "Je hebt een bladwijzer hiervoor gemaakt met een herinnering voor %{date}. %{name}" - remove: "Bladwijzer verwijderen" delete: "Bladwijzer verwijderen" confirm_delete: "Weet je zeker dat je deze bladwijzer wilt verwijderen? De herinnering wordt ook verwijderd." confirm_clear: "Weet je zeker dat je alle bladwijzers van dit topic wilt verwijderen?" @@ -385,7 +384,6 @@ nl: enable: "Inschakelen" disable: "Uitschakelen" continue: "Doorgaan" - undo: "Ongedaan maken" switch_to_anon: "Anonieme modus starten" switch_from_anon: "Anonieme modus verlaten" banner: @@ -682,6 +680,7 @@ nl: denied: "geweigerd" undone: "verzoek ongedaan gemaakt" handle: "lidmaatschapsverzoek behandelen" + undo: "Ongedaan maken" manage: title: "Beheren" name: "Naam" @@ -1051,7 +1050,6 @@ nl: perm_denied_expl: "Je hebt toestemming voor meldingen geweigerd. Sta meldingen toe via je browserinstellingen." disable: "Meldingen uitschakelen" enable: "Meldingen inschakelen" - each_browser_note: 'Opmerking: je moet deze instelling wijzigen in elke browser die je gebruikt. Alle meldingen worden uitgeschakeld wanneer je op ''niet storen'' staat, ongeacht deze instelling.' consent_prompt: "Wil je live meldingen ontvangen als mensen antwoorden op je berichten?" dismiss: "Negeren" dismiss_notifications: "Alles negeren" @@ -3133,7 +3131,6 @@ nl: create_for_topic: "Bladwijzer maken voor topic" edit: "Bladwijzer bewerken" edit_for_topic: "Bladwijzer voor topic bewerken" - created: "Gemaakt" updated: "Bijgewerkt" name: "Naam" name_placeholder: "Waar is deze bladwijzer voor?" @@ -3436,7 +3433,6 @@ nl: category_title: "Categorie" history_capped_revisions: "Geschiedenis, laatste 100 herzieningen" history: "Geschiedenis" - changed_by: "van %{author}" raw_email: title: "Inkomende e-mail" not_available: "Niet beschikbaar!" @@ -3853,7 +3849,6 @@ nl: new_count: one: "%{count} nieuw" other: "%{count} nieuw" - toggle_section: "sectie schakelen" more: "Meer" all_categories: "Alle categorieën" all_tags: "Alle tags" @@ -3862,7 +3857,6 @@ nl: header_link_text: "Over" messages: header_link_text: "Berichten" - header_action_title: "maak een persoonlijk bericht" links: inbox: "Inbox" sent: "Verzonden" @@ -3879,7 +3873,6 @@ nl: none: "Je hebt geen tags toegevoegd." click_to_get_started: "Klik hier om te beginnen." header_link_text: "Tags" - header_action_title: "bewerk je zijbalktags" configure_defaults: "Standaardinstellingen configureren" categories: links: @@ -3889,11 +3882,9 @@ nl: none: "Je hebt geen categorieën toegevoegd." click_to_get_started: "Klik hier om te beginnen." header_link_text: "Categorieën" - header_action_title: "bewerk je zijbalkcategorieën" configure_defaults: "Standaardinstellingen configureren" community: header_link_text: "Community" - header_action_title: "maak een nieuw topic" links: about: content: "Over" @@ -3908,10 +3899,8 @@ nl: content: "FAQ" groups: content: "Groepen" - title: "Alle groepen" users: content: "Gebruikers" - title: "Alle gebruikers" my_posts: content: "Mijn berichten" title: "Mijn recente topicactiviteit" @@ -3921,7 +3910,6 @@ nl: other: "%{count} concepten" review: content: "Beoordelen" - title: "beoordelen" pending_count: "%{count} wachtend" welcome_topic_banner: title: "Maak je welkomsttopic" @@ -4375,7 +4363,6 @@ nl: button_title: "Uitnodigingen sturen" customize: title: "Aanpassen" - long_title: "Websiteaanpassingen" preview: "voorbeeld" explain_preview: "De website bekijken met dit thema ingeschakeld" save: "Opslaan" @@ -4580,9 +4567,9 @@ nl: new_name: "Nieuw kleurenpalet" copy_name_prefix: "Kopie van" delete_confirm: "Dit kleurenpalet verwijderen?" - undo: "ongedaan maken" + undo: "Ongedaan maken" undo_title: "Je wijzigingen aan deze kleur sinds de laatste keer dat deze is opgeslagen ongedaan maken." - revert: "herstellen" + revert: "Herstellen" revert_title: "Deze kleur herstellen naar het standaardkleurenpalet van Discourse." primary: name: "primair" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 52521a654f..01c80d213f 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -380,7 +380,6 @@ pl_PL: remove_reminder_keep_bookmark: "Usuń przypomnienie i zachowaj zakładkę" created_with_reminder: "Dodano post do zakładek z przypomnieniem %{date}. %{name}" created_with_reminder_generic: "Dodałeś to do zakładek z przypomnieniem %{date}. %{name}" - remove: "Usuń zakładkę" delete: "Usuń zakładkę" confirm_delete: "Czy na pewno chcesz usunąć tę zakładkę? Przypomnienie również zostanie usunięte." confirm_clear: "Czy na pewno chcesz usunąć wszystkie swoje zakładki ustawione w tym temacie?" @@ -460,7 +459,6 @@ pl_PL: enable: "Włącz" disable: "Wyłącz" continue: "Kontynuuj" - undo: "Cofnij" switch_to_anon: "Włącz tryb anonimowy" switch_from_anon: "Zakończ tryb anonimowy" banner: @@ -785,6 +783,7 @@ pl_PL: denied: "odrzucony" undone: "prośba cofnięta" handle: "obsłuż prośby o członkostwo" + undo: "Cofnij" manage: title: "Zarządzaj" name: "Nazwa" @@ -1167,7 +1166,6 @@ pl_PL: perm_denied_expl: "Odmówiłeś/łaś dostępu dla powiadomień. Pozwól na powiadomienia w ustawieniach przeglądarki." disable: "Wyłącz powiadomienia" enable: "Włącz powiadomienia" - each_browser_note: 'Uwaga: musisz zmienić to ustawienie w każdej przeglądarce, której używasz. Wszystkie powiadomienia zostaną wyłączone w trybie „nie przeszkadzać”, niezależnie od tego ustawienia.' consent_prompt: "Czy chcesz otrzymywać natychmiastowe powiadomienia, gdy ktoś odpowiada na twoje posty?" dismiss: "Odrzuć" dismiss_notifications: "Odrzuć wszystkie" @@ -3449,7 +3447,6 @@ pl_PL: create_for_topic: "Utwórz zakładkę do tematu" edit: "Edytuj zakładkę" edit_for_topic: "Edytuj zakładkę dla tematu" - created: "Utworzono" updated: "Zaktualizowane" name: "Imię" name_placeholder: "Do czego ma służyć ta zakładka?" @@ -3767,7 +3764,6 @@ pl_PL: category_title: "Kategoria" history_capped_revisions: "Historia, ostatnie 100 wersji" history: "Historia" - changed_by: "przez %{author}" raw_email: title: "Email przychodzący" not_available: "Niedostępne!" @@ -4226,7 +4222,6 @@ pl_PL: few: "%{count} nowe" many: "%{count} nowych" other: "%{count} nowy" - toggle_section: "przełącz sekcję" more: "Więcej" all_categories: "Wszystkie kategorie" all_tags: "Wszystkie tagi" @@ -4235,7 +4230,6 @@ pl_PL: header_link_text: "O stronie" messages: header_link_text: "Wiadomości" - header_action_title: "utwórz osobistą wiadomość" links: inbox: "Skrzynka odbiorcza" sent: "Wysłane" @@ -4252,7 +4246,6 @@ pl_PL: none: "Nie dodałeś żadnych tagów." click_to_get_started: "Kliknij tutaj, aby rozpocząć." header_link_text: "Etykiety" - header_action_title: "edytuj tagi paska bocznego" configure_defaults: "Skonfiguruj ustawienia domyślne" categories: links: @@ -4262,11 +4255,9 @@ pl_PL: none: "Nie dodałeś żadnych kategorii." click_to_get_started: "Kliknij tutaj, aby rozpocząć." header_link_text: "Kategorie" - header_action_title: "edytuj kategorie paska bocznego" configure_defaults: "Skonfiguruj ustawienia domyślne" community: header_link_text: "Społeczność" - header_action_title: "utwórz nowy temat" links: about: content: "O stronie" @@ -4281,10 +4272,8 @@ pl_PL: content: "FAQ" groups: content: "Grupy" - title: "Wszystkie grupy" users: content: "Użytkownicy" - title: "Wszyscy użytkownicy" my_posts: content: "Wysłane" title: "Moja ostatnia aktywność w temacie" @@ -4296,7 +4285,6 @@ pl_PL: other: "%{count} szkice" review: content: "Sprawdź" - title: "sprawdź" pending_count: "%{count} oczekujących" welcome_topic_banner: title: "Stwórz swój temat powitalny" @@ -4759,7 +4747,6 @@ pl_PL: button_title: "Wysyłanie zaproszeń" customize: title: "Wygląd" - long_title: "Personalizacja strony" preview: "podgląd" explain_preview: "Zobacz tę stronę z włączonym stylem" save: "Zapisz" @@ -4966,9 +4953,9 @@ pl_PL: new_name: "Nowa paleta kolorów" copy_name_prefix: "Kopia" delete_confirm: "Usunąć tę paletę kolorów?" - undo: "cofnij" + undo: "Cofnij" undo_title: "Cofnij zmiany tego koloru od ostatniego zapisu" - revert: "przywróć" + revert: "Przywróć" revert_title: "Zresetuj ten kolor do domyślnej wartości palety Discourse." primary: name: "podstawowy" diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index bbc5b51fc2..c87d064baa 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -310,7 +310,6 @@ pt: not_bookmarked: "adicionar esta mensagem aos marcadores" remove_reminder_keep_bookmark: "Remover lembrete e manter marcador" created_with_reminder: "Você marcou este post com um lembrete %{date}. %{name}" - remove: "Remover Marcador" delete: "Remover Marcador" confirm_delete: "Tem certeza de que deseja remover este marcador? O lembrete também será excluído." confirm_clear: "De certeza que pretende remover todos os marcadores deste tópico?" @@ -372,7 +371,6 @@ pt: enable: "Ativar " disable: "Desativar" continue: "Continuar" - undo: "Desfazer" switch_to_anon: "Entrar em modo Anónimo" switch_from_anon: "Sair de modo Anónimo" banner: @@ -667,6 +665,7 @@ pt: denied: "rejeitado" undone: "pedido desfeito" handle: "gerir o pedido de adesão" + undo: "Desfazer" manage: title: "Gerir" name: "Nome" @@ -1027,7 +1026,6 @@ pt: perm_denied_expl: "Negou a permissão para as notificações. Autorize as notificações através das configurações do seu navegador." disable: "Desativar Notificações" enable: "Ativar Notificações" - each_browser_note: 'Nota: Terá de alterar esta configuração em cada navegador que usar. Todas as notificações serão desativadas quando estiver em "Não perturbar", independentemente desta configuração.' consent_prompt: "Quer receber notificações instantâneas quando outras pessoas responderem às suas publicações?" dismiss: "Marcar como visto" dismiss_notifications: "Marcar Visto Tudo" @@ -2883,7 +2881,6 @@ pt: bookmarks: create: "Criar favorito" edit: "Editar favorito" - created: "Criado" updated: "Atualização" name: "Nome" name_placeholder: "Para que serve este favorito?" @@ -3141,7 +3138,6 @@ pt: other: "utilizadores" category_title: "Categoria" history: "Histórico" - changed_by: "por %{author}" raw_email: not_available: "Indisponível!" categories_list: "Lista de Categorias" @@ -3517,14 +3513,12 @@ pt: content: "FAQ" groups: content: "Grupos" - title: "Todos os grupos" users: content: "Utilizadores" my_posts: content: "As Minhas publicações" review: content: "Revisão" - title: "revisão" admin_js: type_to_filter: "digite para filtrar..." admin: @@ -3886,7 +3880,6 @@ pt: button_title: "Enviar Convites" customize: title: "Personalizar" - long_title: "Personalizações do Sítio" preview: "pré-visualização" explain_preview: "Veja o site com este tema ativado" save: "Guardar" @@ -4029,9 +4022,9 @@ pt: new_name: "Nova Paleta de Cores" copy_name_prefix: "Cópia de" delete_confirm: "Excluir esta paleta de cores?" - undo: "desfazer" + undo: "Desfazer" undo_title: "Desfazer as alterações a esta cor desde a última gravação." - revert: "reverter" + revert: "Reverter" revert_title: "Redefina essa cor para a paleta de cores padrão do Discourse." primary: name: "primária" diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index b6d9d71e53..cab81e385a 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -315,7 +315,6 @@ pt_BR: remove_reminder_keep_bookmark: "Remover lembrete e manter favorito" created_with_reminder: "Você marcou esta postagem com um lembrete em %{date}. %{name}" created_with_reminder_generic: "Você marcou isso como lembrete em %{date}. %{name}" - remove: "Remover favorito" delete: "Excluir favorito" confirm_delete: "Tem certeza de que deseja excluir este favorito? O lembrete também será excluído." confirm_clear: "Você tem certeza de que deseja apagar todos os seus favoritos deste tópico?" @@ -385,7 +384,6 @@ pt_BR: enable: "Ativar" disable: "Desativar" continue: "Continuar" - undo: "Desfazer" switch_to_anon: "Entrar no Modo anônimo" switch_from_anon: "Sair do Modo anônimo" banner: @@ -682,6 +680,7 @@ pt_BR: denied: "negado" undone: "solicitação desfeita" handle: "tratar solicitação de associação" + undo: "Desfazer" manage: title: "Gerenciar" name: "Nome" @@ -1051,7 +1050,6 @@ pt_BR: perm_denied_expl: "Você negou permissão para notificações. Permita as notificações nas configurações do seu navegador." disable: "Desativar notificações" enable: "Ativar notificações" - each_browser_note: 'Observação: é preciso alterar esta configuração em cada navegador utilizado. Todas as notificações serão desativadas no modo "Não incomodar", seja qual for a configuração.' consent_prompt: "Você quer notificações em tempo real quando as pessoas responderem às suas postagens?" dismiss: "Descartar" dismiss_notifications: "Descartar tudo" @@ -3121,7 +3119,6 @@ pt_BR: create_for_topic: "Criar favorito para o tópico" edit: "Editar favorito" edit_for_topic: "Editar favorito para o tópico" - created: "Criado" updated: "Atualizado" name: "Nome" name_placeholder: "Para que serve este favorito?" @@ -3423,7 +3420,6 @@ pt_BR: category_title: "Categoria" history_capped_revisions: "Histórico, últimas 100 revisões" history: "Histórico" - changed_by: "de %{author}" raw_email: title: "E-mails recebidos" not_available: "Não disponível!" @@ -3838,7 +3834,6 @@ pt_BR: new_count: one: "%{count} nova" other: "%{count} novas" - toggle_section: "alternar seção" more: "Mais" all_categories: "Todas as categorias" all_tags: "Todas as etiquetas" @@ -3847,7 +3842,6 @@ pt_BR: header_link_text: "Sobre" messages: header_link_text: "Mensagens" - header_action_title: "criar uma mensagem pessoal" links: inbox: "Caixa de entrada" sent: "Enviadas" @@ -3864,7 +3858,6 @@ pt_BR: none: "Você não adicionou nenhuma etiqueta." click_to_get_started: "Clique aqui para começar." header_link_text: "Etiquetas" - header_action_title: "editar as suas etiquetas da barra lateral" configure_defaults: "Configurar padrões" categories: links: @@ -3874,11 +3867,9 @@ pt_BR: none: "Você não adicionou nenhuma categoria." click_to_get_started: "Clique aqui para começar." header_link_text: "Categorias" - header_action_title: "editar suas categorias de barra lateral" configure_defaults: "Configurar padrões" community: header_link_text: "Comunidade" - header_action_title: "criar um novo tópico" links: about: content: "Sobre" @@ -3893,10 +3884,8 @@ pt_BR: content: "FAQ" groups: content: "Grupos" - title: "Todos os grupos" users: content: "Usuários(as)" - title: "Todos(as) os(as) usuários(as)" my_posts: content: "Minhas postagens" draft_count: @@ -3904,7 +3893,6 @@ pt_BR: other: "%{count} rascunhos" review: content: "Revisar" - title: "revisar" pending_count: "%{count} pendente" welcome_topic_banner: title: "Crie seu Tópico de Boas-Vindas" @@ -4351,7 +4339,6 @@ pt_BR: button_title: "Enviar convites" customize: title: "Personalizar" - long_title: "Personalizações do site" preview: "pré-visualização" explain_preview: "Veja o site com este tema ativado" save: "Salvar" @@ -4556,9 +4543,9 @@ pt_BR: new_name: "Nova paleta de cores" copy_name_prefix: "Copiar de" delete_confirm: "Excluir esta paleta de cores?" - undo: "desfazer" + undo: "Desfazer" undo_title: "Desfaça suas alterações nesta cor desde a última vez que foi salva." - revert: "reverter" + revert: "Reverter" revert_title: "Redefina esta cor na paleta de cores padrão do Discourse." primary: name: "primário(a)" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index da6bd92cf0..9895fa1c45 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -309,7 +309,6 @@ ro: not_bookmarked: "marchează ca semn de carte acest post" remove_reminder_keep_bookmark: "Șterge memento-ul și păstrează semnul de carte" created_with_reminder: "Ai marcat această postare cu un memento %{date}. %{name}" - remove: "Șterge semn de carte" delete: "Șterge semn de carte" confirm_delete: "Sunteţi sigur că doriţi să ştergeţi acest semn de carte? Mementoul va fi de asemenea şters." confirm_clear: "Sigur doriți să ștergeți toate marcajele din acest subiect?" @@ -382,7 +381,6 @@ ro: enable: "Activează" disable: "Dezactivează" continue: "Continuă" - undo: "Anulează acțiunea" switch_to_anon: "Intrați în Modul anonim" switch_from_anon: "Ieșiți din Modul anonim" banner: @@ -673,6 +671,7 @@ ro: denied: "respinse" undone: "cerere retrasă" handle: "gestionează cererea de înscriere" + undo: "Anulează acțiunea" manage: title: "Gestionează" name: "Nume" @@ -971,7 +970,6 @@ ro: perm_denied_expl: "Ai blocat notificările. Activează notificările din setările browser-ului." disable: "Dezactivează notificări" enable: "Activează notificările" - each_browser_note: 'Notă: trebuie să modifici această setare în fiecare browser pe care îl utilizezi. Toate notificările vor fi dezactivate când se află în modul „nu deranja”, indiferent de această setare.' consent_prompt: "Vrei să primești notificări atunci când cineva îți răspunde?" dismiss: "Înlătură" dismiss_notifications: "Elimină tot" @@ -2415,7 +2413,6 @@ ro: bookmarks: create: "Creare semn de carte" edit: "Editare semn de carte" - created: "Creat" updated: "Actualizat" name: "Nume" name_placeholder: "Pentru ce este acest semn de carte?" @@ -2642,7 +2639,6 @@ ro: few: "utilizatori" other: "de utilizatori" category_title: "Categorie" - changed_by: "de %{author}" raw_email: not_available: "Indisponibil!" categories_list: "Listă categorii" @@ -2961,14 +2957,12 @@ ro: content: "Întrebări frecvente" groups: content: "Grupuri" - title: "Toate grupurile" users: content: "Utilizatori" my_posts: content: "Postările mele" review: content: "Revizuire" - title: "revizuire" admin_js: type_to_filter: "tastează pentru a filtra..." admin: @@ -3229,7 +3223,6 @@ ro: button_title: "Trimite invitații" customize: title: "Personalizare" - long_title: "Personalizările site-ului" preview: "previzualizare" explain_preview: "Vizualizează site-ul cu această temă activată" save: "Salvare" @@ -3303,9 +3296,9 @@ ro: colors: title: "Culori" copy_name_prefix: "Copie a" - undo: "revenire" + undo: "Anulează acțiunea" undo_title: "Revino asupra schimbărilor aduse acestei culori, de ultima oară când a fost salvată și până acum." - revert: "revenire" + revert: "Revenire la starea inițială" primary: name: "primar" description: "Majoritatea textului, iconițe și margini." diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 651a0f315d..9aa24f4798 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -377,7 +377,6 @@ ru: remove_reminder_keep_bookmark: "Удалить напоминание, но оставить закладку" created_with_reminder: "Вы добавили эту запись в закладки с напоминанием %{date}: «%{name}»" created_with_reminder_generic: "Добавлено в закладки с напоминанием %{date}: «%{name}»" - remove: "Удаление закладки" delete: "Удалить закладку" confirm_delete: "Действительно удалить эту закладку? Напоминание также будет удалено." confirm_clear: "Действительно удалить все ваши закладки из этой темы?" @@ -457,7 +456,6 @@ ru: enable: "Включить" disable: "Отключить" continue: "Продолжить" - undo: "Отменить" switch_to_anon: "Войти в анонимный режим" switch_from_anon: "Выйти из анонимного режима" banner: @@ -782,6 +780,7 @@ ru: denied: "отказано" undone: "запрос отменён" handle: "обрабатывать запрос на вступление" + undo: "Отменить" manage: title: "Управление" name: "Имя" @@ -1163,7 +1162,6 @@ ru: perm_denied_expl: "Вы запретили уведомления в браузере. Разрешите уведомления в настройках браузера." disable: "Отключить уведомления" enable: "Включить уведомления" - each_browser_note: 'Примечание: этот параметр необходимо изменить в каждом используемом браузере. В режиме «Не беспокоить» все уведомления, касающиеся форума, будут отключены, вне зависимости от значения этого параметра.' consent_prompt: "Вы хотите получать уведомления в реальном времени, когда пользователи отвечают на ваши записи?" dismiss: "Пометить прочитанными" dismiss_notifications: "Отклонить всё" @@ -3406,7 +3404,6 @@ ru: create_for_topic: "Создать закладку для темы" edit: "Изменить закладку" edit_for_topic: "Изменить закладку для темы" - created: "Создана" updated: "Обновлена" name: "Название" name_placeholder: "Для чего эта закладка?" @@ -3725,7 +3722,6 @@ ru: category_title: "Категория" history_capped_revisions: "История, последние 100 изменений" history: "История" - changed_by: "автором %{author}" raw_email: title: "Входящее письмо" not_available: "Недоступно!" @@ -4182,7 +4178,6 @@ ru: few: "%{count} новые" many: "%{count} новых" other: "%{count} новой" - toggle_section: "свернуть / развернуть раздел" more: "Ещё" all_categories: "Все категории" all_tags: "Все теги" @@ -4191,7 +4186,6 @@ ru: header_link_text: "Информация" messages: header_link_text: "Личные сообщения" - header_action_title: "создать личное сообщение" links: inbox: "Входящие" sent: "Отправленные" @@ -4204,17 +4198,14 @@ ru: none: "Вы не добавили ни одного тега." click_to_get_started: "Нажмите здесь, чтобы начать." header_link_text: "Теги" - header_action_title: "Редактировать теги боковой панели" configure_defaults: "Настроить значения по умолчанию" categories: none: "Вы не добавили ни одной категории." click_to_get_started: "Нажмите здесь, чтобы начать." header_link_text: "Категории" - header_action_title: "Редактировать категории боковой панели" configure_defaults: "Настроить значения по умолчанию" community: header_link_text: "Сообщество" - header_action_title: "Cоздать новую тему" links: about: content: "О форуме" @@ -4229,10 +4220,8 @@ ru: content: "Ответы на вопросы" groups: content: "Группы" - title: "Все группы" users: content: "Пользователи" - title: "Все пользователи" my_posts: content: "Мои записи" draft_count: @@ -4242,7 +4231,6 @@ ru: other: "%{count} черновика" review: content: "Очередь проверки" - title: "очередь проверки записей" pending_count: "В ожидании: %{count}" welcome_topic_banner: title: "Создать приветственную тему" @@ -4696,7 +4684,6 @@ ru: button_title: "Отправить приглашения" customize: title: "Оформление" - long_title: "Стили и заголовки" preview: "предпросмотр" explain_preview: "Предпросмотр сайта с новым оформлением" save: "Сохранить" @@ -4905,7 +4892,7 @@ ru: delete_confirm: "Удалить эту цветовую палитру?" undo: "Отменить" undo_title: "Отменить изменения этого цвета с момента последнего сохранения." - revert: "Восстановить" + revert: "Вернуть" revert_title: "Сброс этого цвета на значение по умолчанию." primary: name: "Первичный" diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index ad1b038ab4..3f5b13f5cf 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -288,7 +288,6 @@ sk: unbookmark: "Kliknutím odstánite všetky záložky v tejto téme" bookmarks: not_bookmarked: "Vytvor záložku na tento príspevok" - remove: "Odstrániť záložku" confirm_clear: "Ste si istý, že chcete odstrániť všetky záložky z tejto témy?" save: "Uložiť" search: "Hľadať" @@ -338,7 +337,6 @@ sk: enable: "Zapnúť" disable: "Vypnúť" continue: "Pokračovať" - undo: "Späť" switch_to_anon: "Vstúpiť do anonymného režimu" switch_from_anon: "Opustiť anonymný režim" banner: @@ -472,6 +470,7 @@ sk: usernames_placeholder: "používateľské mená" requests: reason: "Dôvod" + undo: "Späť" manage: title: "Spravovať" name: "Názov" @@ -1843,7 +1842,6 @@ sk: text_part: button: "Text" bookmarks: - created: "Vytvorené" name: "Meno" options: "Možnosti" category: @@ -2032,7 +2030,6 @@ sk: many: "používateľov" other: "používateľov" category_title: "Kategória" - changed_by: "od %{author}" raw_email: not_available: "Nedostupné!" categories_list: "Zoznam kategórií" @@ -2312,7 +2309,6 @@ sk: content: "Časté otázky" groups: content: "Skupiny" - title: "Všetky skupiny" users: content: "Používatelia" my_posts: @@ -2530,7 +2526,6 @@ sk: button_title: "Poslať pozvánky" customize: title: "Upraviť" - long_title: "Úpravy webu" preview: "náhľad" save: "Uložiť" new: "Nový" @@ -2594,9 +2589,9 @@ sk: colors: title: "Farby" copy_name_prefix: "Kópia" - undo: "späť" + undo: "Späť" undo_title: "Zrušiť zmeny farby a vrátiť sa k predchádzajucej uloženej verzii. " - revert: "vrátiť zmeny" + revert: "Vrátiť zmeny" primary: name: "primárny" description: "Väčšina textov, ikony, a okraje." diff --git a/config/locales/client.sl.yml b/config/locales/client.sl.yml index 04067f74ed..2b016d0bf9 100644 --- a/config/locales/client.sl.yml +++ b/config/locales/client.sl.yml @@ -317,7 +317,6 @@ sl: created: "To objavo ste dodali med zaznamke. %{name}" not_bookmarked: "zaznamuj prispevek" created_with_reminder: "To objavo ste zaznamovali z opomnikom %{date}. %{name}" - remove: "Odstrani zaznamek" delete: "Odstrani zaznamek" confirm_delete: "Ali ste prepričani, da želite odstraniti ta zaznamek? Opomnik bo prav tako odstranjen." confirm_clear: "Ste prepričani, da želite odstraniti vse svoje zaznamke iz te teme?" @@ -383,7 +382,6 @@ sl: enable: "Omogoči" disable: "Onemogoči" continue: "Nadaljuj" - undo: "Razveljavi" switch_to_anon: "Vklopi anonimni način" switch_from_anon: "Izklopi anonimni način" banner: @@ -635,6 +633,7 @@ sl: denied: "zavrnjeno" undone: "zahtevek umaknjen" handle: "obravnavaj zahteve za članstvo" + undo: "Razveljavi" manage: title: "Upravljaj" name: "Ime skupine" @@ -981,7 +980,6 @@ sl: perm_denied_expl: "Zavrnili ste dovoljenje za obvestila. Omogočite obvestila v nastavitvah vašega brskalnika." disable: "Onemogoči obvestila" enable: "Omogoči obvestila" - each_browser_note: 'Opomba: To nastavitev morate spremeniti v vsakem brskalniku, ki ga uporabljate. Vsa obvestila bodo onemogočena, če imate možnost »ne moti«, ne glede na to nastavitev.' consent_prompt: "Ali hočete obvestila v brskalniku, ko prejmete odgovore na vaše prispevke?" dismiss: "Opusti" dismiss_notifications: "Opusti vse" @@ -2738,7 +2736,6 @@ sl: title: "Prikaži HTML obliko e-sporočila" button: "HTML" bookmarks: - created: "Ustvarjeno" name: "Polno ime" options: "Možnosti" filtered_replies: @@ -2996,7 +2993,6 @@ sl: few: "uporabniki" other: "uporabnikov" category_title: "Kategorija" - changed_by: "od %{author}" raw_email: title: "Dohodno e-sporočilo" not_available: "Ni na voljo!" @@ -3403,14 +3399,12 @@ sl: content: "Pravila skupnosti" groups: content: "Skupine" - title: "Vse skupine" users: content: "Uporabniki" my_posts: content: "Moji prispevki" review: content: "Pregled" - title: "pregled" admin_js: type_to_filter: "vnesite za filter..." admin: @@ -3723,8 +3717,8 @@ sl: colors: title: "Barve" copy_name_prefix: "Kopija od" - undo: "razveljavi" - revert: "povrni" + undo: "Razveljavi" + revert: "Povrni" primary: name: "primarni" highlight: diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 49d62a68a8..6d6ed9841a 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -224,7 +224,6 @@ sq: unbookmark: "Kliko për të hequr të preferuarat nga kjo temë" bookmarks: not_bookmarked: "shto postimin tek të preferuarat" - remove: "Hiqeni nga të prefereruarat" save: "Ruaj" search: "Kërko" drafts: @@ -239,7 +238,6 @@ sq: uploaded: "U ngarkua!" enable: "Aktivizo" disable: "Çaktivizo" - undo: "Çbëj" switch_to_anon: "Filloni sesionin anonim" switch_from_anon: "Shkëputu nga sesioni anonim" banner: @@ -354,6 +352,7 @@ sq: groups: requests: reason: "Arsye" + undo: "Çbëj" manage: name: "Emri" full_name: "Emri i plotë" @@ -1525,7 +1524,6 @@ sq: html_part: button: "HTML" bookmarks: - created: "Krijuar" name: "Emri" options: "Opsione" category: @@ -1701,7 +1699,6 @@ sq: one: "përdorues" other: "përdorues" category_title: "Kategoria" - changed_by: "nga %{author}" raw_email: not_available: "Not available!" categories_list: "Lista Kategorive" @@ -1969,7 +1966,6 @@ sq: content: "Pyetje të shpeshta" groups: content: "Grupet" - title: "Të gjitha grupet" users: content: "Users" my_posts: @@ -2168,7 +2164,6 @@ sq: button_title: "Dërgo ftesa" customize: title: "Personalizo" - long_title: "Personalizimet" preview: "parashiko" save: "Ruaj" new: "E Re" @@ -2213,9 +2208,9 @@ sq: colors: title: "Ngjyrat" copy_name_prefix: "Kopje e" - undo: "rikthe" + undo: "Çbëj" undo_title: "Anullo ndryshimet e bëra në këtë skemë, rikthe versionin e ruajtur herën e fundit. " - revert: "rikthe" + revert: "Rikthe" primary: name: "parësor" description: "Shumica e tekstit, ikonave dhe bordurave. " diff --git a/config/locales/client.sr.yml b/config/locales/client.sr.yml index 8378e23944..1f53b9cdcd 100644 --- a/config/locales/client.sr.yml +++ b/config/locales/client.sr.yml @@ -308,7 +308,6 @@ sr: remove_reminder_keep_bookmark: "Uklonite podsetnik i sačuvajte obeleživač" created_with_reminder: "Obeležili ste ovu poruku sa podsetnikom %{date}. %{name}" created_with_reminder_generic: "Obeležili ste ovu poruku sa podsetnikom %{date}. %{name}" - remove: "Obriši marker" delete: "Ukloni obeleživač" confirm_delete: "Da li ste sigurni da želite da uklonite ovaj obeleživač? Podsetnik će takođe biti uklonjen." confirm_clear: "Da li ste sigurni da želite da uklonite sve obeleživače sa ove teme?" @@ -370,7 +369,6 @@ sr: enable: "Omogući" disable: "Onemogući" continue: "Nastavi" - undo: "Vrati na prethodno" switch_to_anon: "Uključi Anonimni režim" switch_from_anon: "Izađi iz Anonimnog režima" banner: @@ -530,6 +528,7 @@ sr: accepted: "prihvaćen" deny: "Odbij" denied: "odbijen" + undo: "Vrati na prethodno" manage: name: "Ime foruma" full_name: "Ime" @@ -1471,7 +1470,6 @@ sr: side_by_side_markdown: title: "Prikaži razlike u sirovom izvoru jednu nasuprot druge." bookmarks: - created: "Napravljeno" name: "Ime foruma" options: "Opcije" category: @@ -1616,7 +1614,6 @@ sr: few: "korisnici" other: "korisnici" category_title: "Kategorija" - changed_by: "od %{author}" raw_email: not_available: "Nije dostupno!" categories_list: "Lista Kategorija" @@ -2000,7 +1997,6 @@ sr: button_text: "Izvoz" customize: title: "Prilagođavanje" - long_title: "Prilagođavanje Stranice" preview: "Prikaz" save: "Sačuvaj" new: "Novo" @@ -2034,9 +2030,9 @@ sr: colors: title: "Boje" copy_name_prefix: "Kopija od" - undo: "Poništi" + undo: "Vrati na prethodno" undo_title: "Poništi promene ovoj boji od zadnjeg puta kad su sačuvane." - revert: "vrati" + revert: "Vrati nazad" primary: name: "primarna" description: "Većina teksta, ikonica i granica." diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index b44e745ae7..65b67bbc17 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -316,7 +316,6 @@ sv: remove_reminder_keep_bookmark: "Ta bort påminnelsen och behåll bokmärket" created_with_reminder: "Du har bokmärkt detta inlägg med en påminnelse %{date}. %{name}" created_with_reminder_generic: "Du har bokmärkt detta med en påminnelse %{date}. %{name}" - remove: "Ta bort bokmärke" delete: "Radera bokmärke" confirm_delete: "Är du säker på att du vill radera det här bokmärket? Påminnelsen kommer också att raderas." confirm_clear: "Är du säker på att du vill radera alla dina bokmärken från ämnet?" @@ -386,7 +385,6 @@ sv: enable: "Aktivera" disable: "Inaktivera" continue: "Fortsätt" - undo: "Ångra" switch_to_anon: "Starta anonymt läge" switch_from_anon: "Avsluta anonymt läge" banner: @@ -683,6 +681,7 @@ sv: denied: "nekad" undone: "förfrågan återkallad" handle: "hantera begäran om medlemskap" + undo: "Ångra" manage: title: "Hantera" name: "Namn" @@ -1053,7 +1052,7 @@ sv: perm_denied_expl: "Du nekade tillåtelse för aviseringar. Tillåt aviseringar via din webbläsares inställningar." disable: "Inaktivera aviseringar" enable: "Aktivera aviseringar" - each_browser_note: 'Obs! Du måste ändra den här inställningen i varje webbläsare du använder. Alla meddelanden kommer att inaktiveras när du är i ”stör ej”, oavsett denna inställning.' + each_browser_note: 'Obs: Du måste ändra den här inställningen i varje webbläsare du använder. Alla aviseringar kommer att inaktiveras om du pausar aviseringar från användarmenyn, oavsett denna inställning.' consent_prompt: "Vill du ha realtidsaviseringar när personer svarar på dina inlägg?" dismiss: "Avfärda" dismiss_notifications: "Avfärda alla" @@ -3260,7 +3259,6 @@ sv: create_for_topic: "Skapa bokmärke för ämne" edit: "Redigera bokmärke" edit_for_topic: "Redigera bokmärke för ämne" - created: "Skapade" updated: "Uppdaterade" name: "Namn" name_placeholder: "Vad används detta bokmärke till?" @@ -3563,7 +3561,6 @@ sv: category_title: "Kategori" history_capped_revisions: "Historik, senaste 100 versionerna" history: "Historik" - changed_by: "av %{author}" raw_email: title: "Inkommande e-post" not_available: "Ej tillgänglig!" @@ -3982,7 +3979,7 @@ sv: new_count: one: "%{count} ny" other: "%{count} nya" - toggle_section: "växla avsnitt" + toggle_section: "Växla avsnitt" more: "Mer" all_categories: "Alla kategorier" all_tags: "Alla taggar" @@ -3991,7 +3988,7 @@ sv: header_link_text: "Om" messages: header_link_text: "Meddelanden" - header_action_title: "skapa ett personligt meddelande" + header_action_title: "Skapa ett personligt meddelande" links: inbox: "Inkorg" sent: "Skickat" @@ -4008,7 +4005,7 @@ sv: none: "Du har inte lagt till några taggar." click_to_get_started: "Klicka här för att komma igång." header_link_text: "Taggar" - header_action_title: "redigera sidofältets taggar" + header_action_title: "Redigera ditt sidofälts taggar" configure_defaults: "Konfigurera standardvärden" categories: links: @@ -4018,29 +4015,33 @@ sv: none: "Du har inte lagt till några kategorier." click_to_get_started: "Klicka här för att komma igång." header_link_text: "Kategorier" - header_action_title: "redigera sidofältets kategorier" + header_action_title: "Redigera ditt sidofälts kategorier" configure_defaults: "Konfigurera standardvärden" community: header_link_text: "Community" - header_action_title: "skapa ett nytt ämne" + header_action_title: "Skapa ett ämne" links: about: content: "Om" + title: "Mer information om denna webbplats" admin: content: "Admin" + title: "Webbplatsinställningar och rapporter" badges: content: "Utmärkelser" + title: "Alla utmärkelser som kan förtjänas" everything: content: "Allting" title: "Alla ämnen" faq: content: "Vanliga frågor och svar" + title: "Riktlinjer för att använda denna webbplats" groups: content: "Grupper" - title: "Alla grupper" + title: "Lista över tillgängliga användargrupper" users: content: "Användare" - title: "Alla användare" + title: "Lista över alla användare" my_posts: content: "Mina inlägg" title: "Min senaste ämnesaktivitet" @@ -4050,7 +4051,7 @@ sv: other: "%{count} utkast" review: content: "Granska" - title: "granska" + title: "Flaggade inlägg och andra objekt i kö" pending_count: "%{count} väntande" welcome_topic_banner: title: "Skapa ditt välkomstämne" @@ -4510,7 +4511,6 @@ sv: button_title: "Skicka inbjudningar" customize: title: "Anpassa" - long_title: "Webbplatsanpassningar" preview: "förhandsgranska" explain_preview: "Visa sidan med detta tema aktiverat" save: "Spara" @@ -4715,9 +4715,9 @@ sv: new_name: "Ny färgpalett" copy_name_prefix: "Kopia av" delete_confirm: "Radera den här färgpaletten?" - undo: "ångra" + undo: "Ångra" undo_title: "Återställ ändringarna för den här färgen till den senast sparade versionen." - revert: "återställ" + revert: "Återställ" revert_title: "Återställ den här färgen till Discourse-standardfärgspaletten." primary: name: "primär" diff --git a/config/locales/client.sw.yml b/config/locales/client.sw.yml index c17fb97129..a984f674b4 100644 --- a/config/locales/client.sw.yml +++ b/config/locales/client.sw.yml @@ -228,7 +228,6 @@ sw: unbookmark: "Bofya kuondoa mialamisho yote kwenye mada hii" bookmarks: not_bookmarked: "alamisha chapisho hili" - remove: "Ondoa Alamisho" confirm_clear: "Una uhakika unataka kuondoa mialamisho ya mada hii?" save: "hifadhi" search: "Tafuta" @@ -263,7 +262,6 @@ sw: enable: "Ruhusu" disable: "Zuia" continue: "Endelea" - undo: "Tendua" switch_to_anon: "Ingia Hali-tumizi Isiyojulikana" switch_from_anon: "Ondoka kwenye Hali-tumizi Isiyojulikana" banner: @@ -391,6 +389,7 @@ sw: requests: reason: "Sababu" accepted: "imeruhusiwa" + undo: "Tendua" manage: title: "Simamia" name: "Jina" @@ -1857,7 +1856,6 @@ sw: title: "Onyesha sehemu yenye html kwenye barua pepe." button: "HTML" bookmarks: - created: "Imetengenezwa" name: "Jina" options: "Chaguo" category: @@ -2025,7 +2023,6 @@ sw: one: "Mtumiaji" other: "Watumiaji" category_title: "Kategoria" - changed_by: "na %{author}" raw_email: title: "Barua Pepe Iliyopokelewa" not_available: "Haipatikani!" @@ -2289,7 +2286,6 @@ sw: content: "FAQ" groups: content: "Vikundi" - title: "Vikundi vyote" users: content: "Watumiaji" my_posts: @@ -2563,7 +2559,6 @@ sw: button_title: "Tuma Mialiko" customize: title: "Geuza Kukufaa" - long_title: "Mabadiliko Maalum ya Tovuti" preview: "kihakiki" explain_preview: "Tembelea tovuti inayotumia mandhari hii." save: "Hifadhi" @@ -2652,9 +2647,9 @@ sw: colors: title: "Rangi" copy_name_prefix: "Nakala ya" - undo: "tendua" + undo: "Tendua" undo_title: "tendua madiliko kwenye rangi hii kutoka mara ya mwisho ilivyohifadhiwa." - revert: "rejea" + revert: "Rudisha Nyuma" primary: name: "msingi" description: "Neno, ikoni, na mipaka mingi." diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 6e940275fa..441dc5a36b 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -166,7 +166,6 @@ te: unbookmark: "ఈ అంశంపై అన్ని పేజీకలను తొలగించడానికి నొక్కండి" bookmarks: not_bookmarked: "ఈ టపాకు పేజీక ఉంచు" - remove: "పేజీక తొలగించండి" save: "భద్రపరచు" search: "వెతుకు" drafts: @@ -181,7 +180,6 @@ te: uploaded: "ఎగుమతైంది!" enable: "చేతనం" disable: "అచేతనం" - undo: "రద్దు" banner: edit: "సవరించు" choose_topic: @@ -260,6 +258,7 @@ te: groups: requests: reason: "కారణం" + undo: "రద్దు" manage: name: "పేరు" full_name: "పూర్తి పేరు" @@ -1067,7 +1066,6 @@ te: side_by_side_markdown: title: "ముడి మూల తేడాను పక్కపక్కన చూపు" bookmarks: - created: "సృష్టించిన" name: "పేరు" options: "ఎంపికలు" category: @@ -1200,7 +1198,6 @@ te: one: "వాడుకరి" other: "వాడుకరులు" category_title: "వర్గం" - changed_by: " %{author} రాసిన" raw_email: not_available: "అందుబాటులో లేదు!" categories_list: "వర్గాల జాబితా" @@ -1554,7 +1551,6 @@ te: button_title: "ఆహ్వానాలు పంపు" customize: title: "కస్టమైజ్" - long_title: "సైట్ కస్టమైజేషనులు" preview: "మునుజూపు" save: "భద్రపరుచు" new: "కొత్త" diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index c1ab8dee96..336033fa66 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -265,7 +265,6 @@ th: create: "สร้างบุ๊กมาร์ก" edit: "แก้ไขบุ๊กมาร์ก" not_bookmarked: "บุ๊กมาร์กโพสต์นี้" - remove: "ลบบุ๊กมาร์ก" delete: "ลบบุ๊กมาร์ก" confirm_delete: "คุณแน่ใจหรือไม่ว่าต้องการลบบุ๊กมาร์กนี้ การเตือนความจำจะถูกลบออกด้วย" confirm_clear: "คุณแน่ใจหรือว่าต้องการล้างบุ๊กมาร์กทั้งหมดจากกระทู้นี้" @@ -311,7 +310,6 @@ th: enable: "เปิดใช้งาน" disable: "ปิดใช้งาน" continue: "ดำเนินการต่อ" - undo: "เลิกทำ" switch_to_anon: "เข้าสู่โหมดไม่ระบุชื่อ" switch_from_anon: "ออกจากโหมดไม่ระบุชื่อ" banner: @@ -506,6 +504,7 @@ th: accepted: "ยอมรับแล้ว" deny: "ปฏิเสธ" denied: "ปฏิเสธแล้ว" + undo: "เลิกทำ" manage: title: "จัดการ" name: "ชื่อ" @@ -2040,7 +2039,6 @@ th: bookmarks: create: "สร้างบุ๊กมาร์ก" edit: "แก้ไขบุ๊กมาร์ก" - created: "สร้าง" updated: "อัปเดตแล้ว" name: "ชื่อ" name_placeholder: "บุ๊กมาร์กนี้สำหรับสิ่งใด" @@ -2203,7 +2201,6 @@ th: users_lowercase: other: "ผู้ใช้" category_title: "หมวดหมู่" - changed_by: "โดย %{author}" raw_email: not_available: "ไม่พร้อม!" categories_list: "รายชื่อหมวดหมู่" @@ -2475,7 +2472,6 @@ th: content: "คำถามที่พบบ่อย" groups: content: "กลุ่ม" - title: "กลุ่มทั้งหมด" users: content: "ผู้ใช้" my_posts: @@ -2484,7 +2480,6 @@ th: other: "%{count}แบบร่าง" review: content: "รีวิว" - title: "รีวิว" admin_js: type_to_filter: "พิมพ์เพื่อกรอง..." admin: diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 32c2228774..7ea2053253 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -316,7 +316,6 @@ tr_TR: remove_reminder_keep_bookmark: "Anımsatıcıyı kaldır ve yer imini tut" created_with_reminder: "Bu gönderiyi %{date} tarihli bir anımsatıcı ile yer imlerine eklediniz. %{name}" created_with_reminder_generic: "Bunu %{date} tarihli bir anımsatıcıyla yer imlerine eklediniz. %{name}" - remove: "Yer İmini Kaldır" delete: "Yer İmini Sil" confirm_delete: "Bu yer imini silmek istediğinizden emin misiniz? Anımsatıcı da silinir." confirm_clear: "Bu konudaki tüm yer imlerinizi silmek istediğinize emin misiniz?" @@ -386,7 +385,6 @@ tr_TR: enable: "Etkinleştir" disable: "Devre dışı bırak" continue: "Devam et" - undo: "Geri Al" switch_to_anon: "Anonim Moda Gir" switch_from_anon: "Anonim Moddan Çık" banner: @@ -683,6 +681,7 @@ tr_TR: denied: "reddedildi" undone: "talep reddedildi" handle: "üyelik talebini işle" + undo: "Geri Al" manage: title: "Yönet" name: "Ad" @@ -1053,7 +1052,6 @@ tr_TR: perm_denied_expl: "Bildirim izinlerini reddettiniz. Bildirimlere tarayıcı ayarlarınızdan izin verebilirsiniz." disable: "Bildirimleri Devre Dışı Bırak" enable: "Bildirimleri Etkinleştir" - each_browser_note: 'Not: Kullandığınız her tarayıcıda bu ayarı değiştirmeniz gerekir. Bu ayardan bağımsız olarak, "rahatsız etmeyin" modundayken tüm bildirimler devre dışı bırakılır.' consent_prompt: "Gönderilerinize yanıt verildiğinde canlı bildirim almak ister misiniz?" dismiss: "Kapat" dismiss_notifications: "Tümünü Kapat" @@ -3260,7 +3258,6 @@ tr_TR: create_for_topic: "Konu için yer imi oluştur" edit: "Yer imini düzenle" edit_for_topic: "Bu konunun yer imini düzenle" - created: "Oluşturuldu" updated: "Güncellendi" name: "Ad" name_placeholder: "Bu yer imi ne için?" @@ -3563,7 +3560,6 @@ tr_TR: category_title: "Kategori" history_capped_revisions: "Geçmiş, son 100 revizyon" history: "Geçmiş" - changed_by: "Yazan: %{author}" raw_email: title: "Gelen E-posta" not_available: "Kullanılamıyor!" @@ -3982,7 +3978,6 @@ tr_TR: new_count: one: "%{count} yeni" other: "%{count} yeni" - toggle_section: "bölümü değiştir" more: "Daha fazla" all_categories: "Tüm kategoriler" all_tags: "Tüm etiketler" @@ -3991,7 +3986,6 @@ tr_TR: header_link_text: "Hakkında" messages: header_link_text: "Mesajlar" - header_action_title: "kişisel mesaj oluştur" links: inbox: "Gelen Kutusu" sent: "Gönderilen" @@ -4008,7 +4002,6 @@ tr_TR: none: "Etiket eklemediniz." click_to_get_started: "Başlamak için buraya tıklayın." header_link_text: "Etiketler" - header_action_title: "kenar çubuğu etiketlerinizi düzenleyin" configure_defaults: "Varsayılanları yapılandır" categories: links: @@ -4018,11 +4011,9 @@ tr_TR: none: "Kategori eklemediniz." click_to_get_started: "Başlamak için buraya tıklayın." header_link_text: "Kategoriler" - header_action_title: "kenar çubuğu kategorilerinizi düzenleyin" configure_defaults: "Varsayılanları yapılandır" community: header_link_text: "Topluluk" - header_action_title: "yeni bir konu oluştur" links: about: content: "Hakkında" @@ -4037,10 +4028,8 @@ tr_TR: content: "SSS" groups: content: "Gruplar" - title: "Tüm gruplar" users: content: "Kullanıcılar" - title: "Tüm kullanıcılar" my_posts: content: "Gönderilerim" title: "Son konu etkinliğim" @@ -4050,7 +4039,6 @@ tr_TR: other: "%{count} taslak" review: content: "İncele" - title: "incele" pending_count: "%{count} beklemede" welcome_topic_banner: title: "Karşılama Konunuzu oluşturun" @@ -4507,7 +4495,6 @@ tr_TR: button_title: "Davet Gönder" customize: title: "Özelleştir" - long_title: "Site Özelleştirmeleri" preview: "ön izleme" explain_preview: "Bu temanın etkin olduğu siteye bakın" save: "Kaydet" @@ -4712,9 +4699,9 @@ tr_TR: new_name: "Yeni Renk Paleti" copy_name_prefix: "Şunun kopyası:" delete_confirm: "Bu renk paleti silinsin mi?" - undo: "geri al" + undo: "Geri Al" undo_title: "Bu rengin son kaydedilmesinden bu yana yaptığınız değişiklikleri geri alın." - revert: "eski haline getir" + revert: "Eski Haline Getir" revert_title: "Bu rengi Discourse'un varsayılan renk paletine sıfırlayın." primary: name: "birincil" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 9c679c0489..3b259f110d 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -379,7 +379,6 @@ uk: remove_reminder_keep_bookmark: "Вилучити нагадування, але зберегти закладку" created_with_reminder: "Ви додали цей допис у закладку із нагадуванням %{date}. %{name}" created_with_reminder_generic: "Ви створили закладку із нагадуванням %{date}. %{name}" - remove: "Вилучити закладку" delete: "Видалити закладку" confirm_delete: "Ви впевнені, що хочете видалити цю закладку? Нагадування також буде видалено." confirm_clear: "Ви впевнені, що хочете вилучити всі закладки з цієї теми?" @@ -459,7 +458,6 @@ uk: enable: "Увімкнути" disable: "Вимкнути" continue: "Продовжити" - undo: "Скасувати" switch_to_anon: "Увійти в анонімний режим" switch_from_anon: "Полишити анонімний режим" banner: @@ -784,6 +782,7 @@ uk: denied: "відмовлено" undone: "запит скасовано" handle: "обробляти запит на вступ" + undo: "Скасувати" manage: title: "Керувати" name: "Імʼя" @@ -1165,7 +1164,6 @@ uk: perm_denied_expl: "Ви заборонили сповіщення. Дозвольте сповіщення у налаштуваннях свого оглядача." disable: "Вимкнути сповіщення" enable: "Увімкнути сповіщення" - each_browser_note: 'Примітка. Вам потрібно змінити це налаштування в кожному оглядачі, який ви використовуєте. У режимі «не турбувати» всі сповіщення буде вимкнено незалежно від цього налаштування.' consent_prompt: "Хочете отримувати сповіщення в реальному часі, коли люди відповідають на ваші дописи?" dismiss: "Відкинути" dismiss_notifications: "Відхилити всі" @@ -3424,7 +3422,6 @@ uk: create_for_topic: "Створіть закладку для теми" edit: "Редагувати закладку" edit_for_topic: "Редагувати закладку для теми" - created: "Створено" updated: "Оновлено" name: "Назва" name_placeholder: "Для чого ця закладка?" @@ -3744,7 +3741,6 @@ uk: category_title: "Розділ" history_capped_revisions: "Історія та останні 100 змін" history: "Історія" - changed_by: "%{author}" raw_email: title: "Вхідне повідомлення" not_available: "Не доступно!" @@ -4201,7 +4197,6 @@ uk: few: "%{count} нових" many: "%{count} нова" other: "%{count} нова" - toggle_section: "Переключити розділ" more: "Більше" all_categories: "Всі розділи" all_tags: "Всі теґи" @@ -4210,7 +4205,6 @@ uk: header_link_text: "Про" messages: header_link_text: "Повідомлення" - header_action_title: "створити особисте повідомлення" links: inbox: "Вхідні" sent: "Надіслано" @@ -4227,7 +4221,6 @@ uk: none: "Ви не додали жодного теґу." click_to_get_started: "Натисніть тут, щоб почати." header_link_text: "Теґи" - header_action_title: "Редагувати теґи бічної панелі" configure_defaults: "Налаштувати типові значення" categories: links: @@ -4237,11 +4230,9 @@ uk: none: "Ви не додали жодного розділу." click_to_get_started: "Натисніть тут, щоб почати." header_link_text: "Розділи" - header_action_title: "редагувати розділи бічної панелі" configure_defaults: "Налаштувати типові значення" community: header_link_text: "Спільнота" - header_action_title: "створити нову тему" links: about: content: "Про" @@ -4256,10 +4247,8 @@ uk: content: "Часті запитання" groups: content: "Групи" - title: "Усі групи" users: content: "Користувачі" - title: "Всі користувачі" my_posts: content: "Мої дописи" draft_count: @@ -4269,7 +4258,6 @@ uk: other: "%{count} чернеток" review: content: "Переглянути" - title: "переглянути" pending_count: "%{count} очікує на розгляд" welcome_topic_banner: title: "Створіть тему привітання" @@ -4724,7 +4712,6 @@ uk: button_title: "Надіслати запрошення" customize: title: "Customize" - long_title: "Site Customizations" preview: "preview" explain_preview: "Попередній перегляд сайту з активованим стилем" save: "Зберегти" @@ -4931,7 +4918,7 @@ uk: new_name: "Нова Колірна Палітра" copy_name_prefix: "Копіювати з" delete_confirm: "видалити цю колірну палітру?" - undo: "скасувати" + undo: "Скасувати" undo_title: "Скасувати ваші зміни цього кольору з часу останнього збереження." revert: "Повернути" revert_title: "Скиньте цей колір на колірну палітру Discourse за замовчуванням." diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 6a51af7b0b..e57e066fe6 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -291,7 +291,6 @@ ur: not_bookmarked: "اِس پوسٹ کو بُک مارک کریں" remove_reminder_keep_bookmark: "یاد دہانی کو ہٹا دیں اور بک مارک کو بچا کر رکھیں" created_with_reminder: "آپ نے اس پوسٹ کو ایک یاد دہانی %{date}, %{name} کے ساتھ بک مارک کیا ہے۔" - remove: "بُک مارک ہٹائیں" delete: "بک مارک مٹائیں" confirm_delete: "کیا آپ واقعی اس بُک مارک کو مٹانا چاہتے ہیں؟ یاد دہانی بھی مٹادی جائے گی۔" confirm_clear: "کیا آپ کو یقین ہے کہ آپ اِس ٹاپک سے اپنے تمام بُک مارکس ہٹانا چاہتے ہیں؟" @@ -360,7 +359,6 @@ ur: enable: "فعال کریں" disable: "غیر فعال کریں" continue: "جاری رکھی" - undo: "کالعدم کریں" switch_to_anon: "گمنام موڈ میں داخل ہوں" switch_from_anon: "گمنام موڈ سے باہر نکلیں" banner: @@ -652,6 +650,7 @@ ur: denied: "انکار کر دیا" undone: "درخواست کالعدم" handle: "رکنیت کی درخواست سے نمٹیں" + undo: "کالعدم کریں" manage: title: "مَینَیج" name: "نام" @@ -1018,7 +1017,6 @@ ur: perm_denied_expl: "آپ نے اطلاعات کے لئے اجازت دینے سے انکار کر دیا۔ اپنے براؤزر کی سیٹِنگ سے اطلاعات کی اجازت دیں۔" disable: "اطلاعات غیر فعال کریں" enable: "اطلاعات فعال کریں" - each_browser_note: 'نوٹ: آپ کو اپنے استعمال کردہ ہر براؤزر پر اس ترتیب کو تبدیل کرنا ہوگا۔ اس ترتیب سے قطع نظر "ڈسٹرب نہ کریں" میں ہونے پر تمام اطلاعات کو غیر فعال کر دیا جائے گا۔' consent_prompt: "جب لوگ آپ کی پوسٹس کا جواب دیں تو کیا آپ لائیو اطلاعات چاہتے ہیں؟" dismiss: "بر خاست کریں" dismiss_notifications: "سب بر خاست کریں" @@ -2924,7 +2922,6 @@ ur: create_for_topic: "موضوع کے لیے بُک مارک بنائیں" edit: "بک مارک میں ترمیم" edit_for_topic: "موضوع کے لیے بک مارک میں ترمیم" - created: "بنایا گیا" updated: "اپ ڈیٹ کیا" name: "نام" name_placeholder: "یہ بک مارک کس لیے ہے؟" @@ -3220,7 +3217,6 @@ ur: category_title: "زمرہ" history_capped_revisions: "تاریخی، آخری 100 ترمیم" history: "تاریخی" - changed_by: "%{author} کی طرف سے" raw_email: title: "آنے والی اِیمیل" not_available: "دستیاب نہیں ہے!" @@ -3653,14 +3649,12 @@ ur: content: "عمومی سوالات" groups: content: "گروپس" - title: "تمام گروپس" users: content: "صارفین" my_posts: content: "میری پوسٹ" review: content: "جائزہ لیں" - title: "جائزہ لیں" admin_js: type_to_filter: "فِلٹر کرنے کے لئے ٹائپ کریں..." admin: @@ -4093,7 +4087,6 @@ ur: button_title: "دعوت بھیجیں" customize: title: "مرضی کے مطابق بنائیں" - long_title: "ویب سائٹ کو اپنی مرضی کے حساب سے بنانے کیلئے اختیارات" preview: "پیشگی دیکھیں" explain_preview: "اِس تِھیم کے ساتھ سائٹ ملاحظہ کریں" save: "محفوظ کریں" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index 98f6c48820..33d4c35905 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -281,7 +281,6 @@ vi: remove_reminder_keep_bookmark: "Xóa lời nhắc và giữ lại dấu trang" created_with_reminder: "Bạn đã đánh dấu bài đăng này với lời nhắc %{date}. %{name}" created_with_reminder_generic: "Bạn đã đánh dấu trang này với lời nhắc %{date}. %{name}" - remove: "Xóa dấu trang" delete: "Xóa chỉ mục" confirm_delete: "Bạn có chắc chắn muốn xóa dấu trang này không? Lời nhắc cũng sẽ bị xóa." confirm_clear: "Bạn có chắc muốn xóa toàn bộ đánh dấu trong chủ đề này?" @@ -346,7 +345,6 @@ vi: enable: "Kích hoạt" disable: "Vô hiệu hóa" continue: "Tiếp tục" - undo: "Hoàn tác" switch_to_anon: "Vào chế độ Ẩn danh" switch_from_anon: "Thoát chế độ Ẩn danh" banner: @@ -627,6 +625,7 @@ vi: denied: "Đã từ chối" undone: "yêu cầu bị hủy" handle: "xử lý yêu cầu thành viên" + undo: "Hoàn tác" manage: title: "Quản lý" name: "Tên" @@ -984,7 +983,6 @@ vi: perm_denied_expl: "Bạn đã từ chối nhận thông báo, để nhận lại bạn cần thiết lập trình duyệt." disable: "Khóa Notification" enable: "Cho phép Notification" - each_browser_note: 'Lưu ý: Bạn phải thay đổi cài đặt này trên mọi trình duyệt bạn sử dụng. Tất cả thông báo sẽ bị tắt khi ở chế độ "không làm phiền", bất kể cài đặt này là gì.' consent_prompt: "Bạn có muốn thông báo trực tiếp khi mọi người trả lời bài đăng của bạn không?" dismiss: "Hủy bỏ" dismiss_notifications: "Bỏ qua tất cả" @@ -2898,7 +2896,6 @@ vi: create_for_topic: "Tạo dấu trang cho chủ đề" edit: "Chỉnh sửa dấu trang" edit_for_topic: "Chỉnh sửa dấu trang cho chủ đề" - created: "Tạo bởi" updated: "Đã cập nhật" name: "Tên" name_placeholder: "Đánh dấu này là gì?" @@ -3186,7 +3183,6 @@ vi: category_title: "Danh mục" history_capped_revisions: "Lịch sử, 100 bản sửa đổi gần đây nhất" history: "Lịch sử" - changed_by: "bởi %{author}" raw_email: title: "Email đến" not_available: "Không sẵn sàng!" @@ -3575,7 +3571,6 @@ vi: other: "%{count} chưa đọc" new_count: other: "%{count} mới" - toggle_section: "phần chuyển đổi" more: "Thêm" all_categories: "Tất cả danh mục" sections: @@ -3583,7 +3578,6 @@ vi: header_link_text: "Giới thiệu" messages: header_link_text: "Tin nhắn" - header_action_title: "tạo một tin nhắn cá nhân" links: inbox: "Hộp thư" sent: "Đã gửi" @@ -3596,15 +3590,12 @@ vi: none: "Bạn chưa thêm bất kỳ thẻ nào." click_to_get_started: "Bấm vào đây để bắt đầu." header_link_text: "Thẻ" - header_action_title: "chỉnh sửa thẻ thanh bên của bạn" categories: none: "Bạn chưa thêm bất kỳ danh mục nào." click_to_get_started: "Bấm vào đây để bắt đầu." header_link_text: "Thư mục" - header_action_title: "chỉnh sửa danh mục thanh bên của bạn" community: header_link_text: "Cộng đồng" - header_action_title: "tạo một chủ đề mới" links: about: content: "Giới thiệu" @@ -3619,17 +3610,14 @@ vi: content: "Câu hỏi thường gặp" groups: content: "Nhóm" - title: "Các nhóm" users: content: "Người dùng" - title: "Tất cả Thành viên" my_posts: content: "Bài viết của tôi" draft_count: other: "%{count} bản nháp" review: content: "Review" - title: "review" until: "Cho đến khi:" admin_js: type_to_filter: "gõ để lọc..." @@ -4066,7 +4054,6 @@ vi: button_title: "Gửi Lời Mời" customize: title: "Tùy biến" - long_title: "Tùy biến trang" preview: "xem trước" explain_preview: "Xem trang web đã bật chủ đề này" save: "Lưu" @@ -4263,9 +4250,9 @@ vi: new_name: "Bảng màu mới" copy_name_prefix: "Bản sao của" delete_confirm: "Xóa bảng màu này?" - undo: "hoàn tác" + undo: "Hoàn tác" undo_title: "Hoàn tác thay đổi của bạn vơ" - revert: "phục hồi" + revert: "Phục hồi" revert_title: "Đặt lại màu này thành bảng màu mặc định của Discourse." primary: name: "chính" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 73b340b7b3..cfe5b6e540 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -283,7 +283,6 @@ zh_CN: remove_reminder_keep_bookmark: "移除提醒并保留书签" created_with_reminder: "您已将此帖子加入书签,并于 %{date}设置了一个提醒。%{name}" created_with_reminder_generic: "您已将此加入书签并设置了一个提醒 %{date}。%{name}" - remove: "移除书签" delete: "删除书签" confirm_delete: "确定要删除此书签吗?提醒也将一并删除。" confirm_clear: "确定要清除此话题中的所有书签吗?" @@ -348,7 +347,6 @@ zh_CN: enable: "启用" disable: "禁用" continue: "继续" - undo: "撤消" switch_to_anon: "进入匿名模式" switch_from_anon: "退出匿名模式" banner: @@ -631,6 +629,7 @@ zh_CN: denied: "已拒绝" undone: "请求已撤消" handle: "处理成员资格请求" + undo: "撤消" manage: title: "管理" name: "名称" @@ -994,7 +993,6 @@ zh_CN: perm_denied_expl: "您拒绝了通知的权限。通过您的浏览器设置允许通知。" disable: "禁用通知" enable: "启用通知" - each_browser_note: '注意:您必须在您使用的每个浏览器上更改此设置。在“勿扰”模式下,所有通知都将被禁用,无论此设置如何。' consent_prompt: "当其他人回复您的帖子时是否接收实时通知?" dismiss: "忽略" dismiss_notifications: "全部忽略" @@ -2970,7 +2968,6 @@ zh_CN: create_for_topic: "为话题创建书签" edit: "编辑书签" edit_for_topic: "编辑话题的书签" - created: "已创建" updated: "更新" name: "名称" name_placeholder: "此书签有什么用?" @@ -3263,7 +3260,6 @@ zh_CN: category_title: "类别" history_capped_revisions: "历史记录,最近 100 次修订" history: "历史记录" - changed_by: "作者 %{author}" raw_email: title: "传入电子邮件" not_available: "不可用!" @@ -3657,7 +3653,6 @@ zh_CN: other: "%{count} 未读" new_count: other: "%{count} 新" - toggle_section: "切换版块" more: "更多" all_categories: "所有类别" all_tags: "所有标签" @@ -3666,7 +3661,6 @@ zh_CN: header_link_text: "关于" messages: header_link_text: "消息" - header_action_title: "创建个人消息" links: inbox: "收件箱" sent: "已发送" @@ -3683,7 +3677,6 @@ zh_CN: none: "您还没有添加任何标签。" click_to_get_started: "点击此处开始。" header_link_text: "标签" - header_action_title: "编辑您的边栏标签" configure_defaults: "配置默认值" categories: links: @@ -3693,11 +3686,9 @@ zh_CN: none: "您还没有添加任何类别。" click_to_get_started: "点击此处开始。" header_link_text: "类别" - header_action_title: "编辑您的边栏类别" configure_defaults: "配置默认值" community: header_link_text: "社区" - header_action_title: "创建新话题" links: about: content: "关于" @@ -3712,17 +3703,14 @@ zh_CN: content: "常见问题解答" groups: content: "群组" - title: "所有群组" users: content: "用户" - title: "所有用户" my_posts: content: "我的帖子" draft_count: other: "%{count} 个草稿" review: content: "审核" - title: "审核" pending_count: "%{count} 待处理" welcome_topic_banner: title: "创建您的欢迎话题" @@ -4165,7 +4153,6 @@ zh_CN: button_title: "发送邀请" customize: title: "自定义" - long_title: "站点自定义" preview: "预览" explain_preview: "查看启用此主题的站点" save: "保存" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 792f551990..690e925bab 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -279,7 +279,6 @@ zh_TW: not_bookmarked: "將此貼文加入書籤" remove_reminder_keep_bookmark: "刪除提醒並保留書籤" created_with_reminder: "您已使用提醒 %{date}將此帖子添加為書籤。 %{name}" - remove: "移除書籤" delete: "刪除書籤" confirm_delete: "您確定要刪除這個書籤嗎?提醒也會被刪除。" confirm_clear: "確定要移除該話題上的所有書籤嗎?" @@ -344,7 +343,6 @@ zh_TW: enable: "啟用" disable: "停用" continue: "繼續" - undo: "復原" switch_to_anon: "進入匿名模式" switch_from_anon: "離開匿名模式" banner: @@ -610,6 +608,7 @@ zh_TW: denied: "被拒絕" undone: "請求未完成" handle: "處理會員申請" + undo: "復原" manage: title: "管理" name: "名稱" @@ -2352,7 +2351,6 @@ zh_TW: button: "HTML" bookmarks: edit: "編輯書籤" - created: "已建立" name: "名稱" options: "選項" actions: @@ -2573,7 +2571,6 @@ zh_TW: users_lowercase: other: "使用者" category_title: "分類" - changed_by: "作者 %{author}" raw_email: title: "寄來的郵件" not_available: "不可使用" @@ -2847,7 +2844,6 @@ zh_TW: other: "%{count} 個未讀" new_count: other: "%{count} 近期" - toggle_section: "切換選擇" more: "更多" all_categories: "所有分類" all_tags: "所有標籤" @@ -2856,7 +2852,6 @@ zh_TW: header_link_text: "關於" messages: header_link_text: "訊息" - header_action_title: "寫一則個人訊息" links: inbox: "收件匣" sent: "送出" @@ -2869,17 +2864,14 @@ zh_TW: none: "您尚未新增任何標籤。" click_to_get_started: "按一下這裡開始。" header_link_text: "標籤" - header_action_title: "編輯側選單的標籤" configure_defaults: "設定預設值" categories: none: "您尚未新增任何類別。" click_to_get_started: "按一下這裡開始。" header_link_text: "分類" - header_action_title: "編輯側選單的分類" configure_defaults: "設定預設值" community: header_link_text: "社群" - header_action_title: "開啟新話題" links: about: content: "關於" @@ -2894,17 +2886,14 @@ zh_TW: content: "常見問答" groups: content: "群組" - title: "所有群組" users: content: "使用者" - title: "所有使用者" my_posts: content: "我的貼文" draft_count: other: "%{count} 草稿" review: content: "審核" - title: "審核" pending_count: "剩餘 %{count}" welcome_topic_banner: title: "建立您的歡迎主題" @@ -3233,7 +3222,6 @@ zh_TW: button_title: "送出邀請" customize: title: "客製化" - long_title: "網站客製化" preview: "預覽" explain_preview: "以此佈景主題預覽網站" save: "儲存" @@ -3390,7 +3378,7 @@ zh_TW: delete_confirm: "刪除此調色盤?" undo: "復原" undo_title: "復原你前次對此顏色所做的修改" - revert: "回復" + revert: "恢復" revert_title: "將此顏色重置為Discourse的預設調色。" primary: name: "一級" diff --git a/config/locales/plurals.rb b/config/locales/plurals.rb index 4ffacf4408..babf8cf131 100644 --- a/config/locales/plurals.rb +++ b/config/locales/plurals.rb @@ -3,6 +3,7 @@ # source: https://github.com/svenfuchs/i18n/blob/master/test/test_data/locales/plurals.rb +# stree-ignore { af: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| n == 1 ? :one : :other } } } }, am: { i18n: { plural: { keys: [:one, :other], rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } } }, diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index ceebb27298..0f7ef9095c 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -971,10 +971,6 @@ ar: other: "منذ حوالي %{count} عام تقريبًا" password_reset: no_token: 'عذرًا! لم يعُد الرابط الذي استخدمته يعمل. يمكنك تسجيل الدخول الآن. إذا نسيت كلمة مرورك، يمكنك طلب رابط لإعادة تعيينها.' - choose_new: "اختيار كلمة مرور جديدة" - choose: "اختيار كلمة مرور" - update: "تحديث كلمة المرور" - save: "ضبط كلمة المرور" title: "إعادة ضبط كلمة المرور" success: "لقد غيَّرت كلمة مرورك وسجَّلت الدخول بنجاح." success_unapproved: "لقد غيَّرت كلمة مرورك بنجاح." diff --git a/config/locales/server.be.yml b/config/locales/server.be.yml index 7bcdb5ca91..5e9b79a74c 100644 --- a/config/locales/server.be.yml +++ b/config/locales/server.be.yml @@ -414,9 +414,6 @@ be: many: "%{Count} дзён таму" other: " %{Count} дзён таму" password_reset: - choose_new: "Абярыце новы пароль" - choose: "% Менш, чым {лік} хвілін таму........................................................." - save: "ўсталяваць пароль" title: "скінуць пароль" user_auth_tokens: browser: diff --git a/config/locales/server.bg.yml b/config/locales/server.bg.yml index 0129608aa7..a28459ebc6 100644 --- a/config/locales/server.bg.yml +++ b/config/locales/server.bg.yml @@ -356,10 +356,6 @@ bg: one: "преди почти %{count} година" other: "преди почти %{count} години" password_reset: - choose_new: "Избери нова парола" - choose: "Избери парола" - update: "Обновете паролата" - save: "Задайте парола" title: "Смяна на паролата" success: "Вие успешно променихте вашата парола и в момента сте логнати." success_unapproved: "Вие успешно променихте вашата парола." diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 4137487fc1..2dd5932355 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -310,10 +310,6 @@ bs_BA: few: "prije %{count} mjeseca" other: "prije %{count} mjeseci" password_reset: - choose_new: "Izaberite novu šifru" - choose: "Izaberite šifru" - update: "Obnovi Šifru" - save: "Namjesti Šifru" title: "Resetuj Šifru" success: "Uspješno ste promjenili šifru i sada se možete ulogovati." success_unapproved: "Uspješno ste promjenili šifru." diff --git a/config/locales/server.ca.yml b/config/locales/server.ca.yml index b696bcfcda..29effe2d60 100644 --- a/config/locales/server.ca.yml +++ b/config/locales/server.ca.yml @@ -585,10 +585,6 @@ ca: one: "fa gairebé %{count} any" other: "fa gairebé %{count} any" password_reset: - choose_new: "Trieu una contrasenya nova" - choose: "Trieu una contrasenya" - update: "Actualitza la contrasenya" - save: "Estableix la contrasenya" title: "Reinicia la contrasenya" success: "Heu canviat la contrasenya satisfactòriament i ara heu iniciat la sessió." success_unapproved: "Heu canviat la contrasenya satisfactòriament." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index cbe3619bd0..64265281c5 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -526,10 +526,6 @@ cs: many: "téměř před %{count} roky" other: "téměř před %{count} roky" password_reset: - choose_new: "Vyberte si nové heslo" - choose: "Vybrat heslo" - update: "změnit heslo" - save: "Nastavit heslo" title: "Resetovat heslo" success: "Heslo bylo úspěšně změněno a nyní jste přihlášeni." success_unapproved: "Heslo bylo úšpěšně změněno." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 3b1c5ae9f7..d152cf4910 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -700,10 +700,6 @@ da: one: "næsten %{count} år siden" other: "næsten %{count} år siden" password_reset: - choose_new: "Vælg en ny adgangskode" - choose: "Vælg en adgangskode" - update: "Opdatér Adgangskode" - save: "Gem Adgangskode" title: "Nulstil Adgangskode" success: "Din adgangskode er ændret og du er nu logget ind." success_unapproved: "Din adgangskode er nu ændret." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index f7139d827f..2e6affb035 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -767,10 +767,6 @@ de: other: "vor fast %{count} Jahren" password_reset: no_token: 'Ups! Der Link, den du benutzt hast, funktioniert nicht mehr. Du kannst dich jetzt anmelden. Falls du dein Passwort vergessen hast, kannst du einen Link anfordern, um es zurückzusetzen.' - choose_new: "Wähle ein neues Passwort" - choose: "Wähle ein Passwort" - update: "Passwort aktualisieren" - save: "Passwort festlegen" title: "Passwort zurücksetzen" success: "Dein Passwort wurde erfolgreich geändert und du bist nun angemeldet." success_unapproved: "Dein Passwort wurde erfolgreich geändert." diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index b781b94d2a..a1afb75b4b 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -543,10 +543,6 @@ el: one: "σχεδόν %{count} χρόνο πριν" other: "σχεδόν %{count} χρόνια πριν" password_reset: - choose_new: "Επιλέξτε νέο κωδικό πρόσβασης" - choose: "Επιλέξτε έναν κωδικό πρόσβασης" - update: "Ενημέρωση κωδικού πρόσβασης" - save: "Θέσε Κωδικό Πρόσβασης" title: "Επαναφορά κωδικού πρόσβασης" success: "Αλλάξατε επιτυχώς τον κωδικό πρόσβασής σας και έχετε συνδεθεί." success_unapproved: "Αλλάξατε επιτυχώς τον κωδικό πρόσβασής σας" diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 1da9326255..a79d6b31c1 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -767,10 +767,6 @@ es: other: "hace casi %{count} años" password_reset: no_token: '¡Uy! El enlace que has utilizado ya no funciona. Ahora puedes Iniciar sesión. Si has olvidado tu contraseña, puedes solicitar un enlace para restablecerla.' - choose_new: "Escoge una nueva contraseña" - choose: "Escoge una contraseña" - update: "Actualizar contraseña" - save: "Establecer contraseña" title: "Restablecer contraseña" success: "Has cambiado tu contraseña correctamente y ahora has iniciado sesión." success_unapproved: "Has cambiado tu contraseña correctamente." diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index 864e18fb05..a1c6e6a603 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -399,10 +399,6 @@ et: one: "peaaegu %{count} aasta tagasi" other: "peaaegu %{count} aastat tagasi" password_reset: - choose_new: "Vali uus parool" - choose: "Vali parool" - update: "Uuenda parooli" - save: "Määra Parool" title: "Uuenda Parool" success: "Sinu parooli muutmine õnnestus ja oled nüüd sisse logitud." success_unapproved: "Sinu parooli muutmine õnnestus." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 113f4a4ff9..5af24e850c 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -501,10 +501,6 @@ fa_IR: one: "تقریبا %{count} سال قبل" other: "تقریبا %{count} سال قبل" password_reset: - choose_new: "رمز‌عبور جدید را وارد کنید" - choose: "رمز‌عبور وارد کنید" - update: "به‌روز کردن رمز‌عبور" - save: "تنظیم رمز‌عبور" title: "بازیابی رمز‌عبور" success: "شما با موفقیت رمز‌عبورتان را تغییر دادید و الان وارد سیستم هستید. " success_unapproved: "شما با موفقیت رمز‌عبورتان را تغییر دادید." @@ -629,7 +625,6 @@ fa_IR: unwatch_category: "موضوعات دسته‌بندی %{category} را مشاهده نکنید" mailing_list_mode: "غیرفعال سازی حالت فهرست ایمیل" different_user_description: "در حال حاضر با نام کاربری متفاوتی با آن که برایتان ایمیل شده است وارد شده‌اید. لطفا خارج شوید یا در حالت ناشناس وارد شده و دوباره تلاش کنید." - not_found_description: با عرض پوزش، نتوانستیم آن اشتراک را پیدا کنیم. ممکن است پیوند ایمیل شما خیلی قدیمی باشد و منقضی شده باشد؟ log_out: "خروج" digest_frequency: never: "هیچوقت" diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 95a15a31a7..45999a4a7a 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -768,10 +768,6 @@ fi: other: "lähes %{count} vuotta sitten" password_reset: no_token: 'Käyttämäsi linkki ei enää toimi. Voit kirjautua sisään nyt. Jos olet unohtanut salasanasi, voit pyytää linkkiä sen palauttamiseksi.' - choose_new: "Valitse uusi salasana" - choose: "Valitse salasana" - update: "Päivitä salasana" - save: "Aseta salasana" title: "Palauta salasana" success: "Salasanan vaihto onnistui, ja olet nyt kirjautunut sisään." success_unapproved: "Salasana vaihdettu onnistuneesti." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 8191479655..5d758fb463 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -767,10 +767,6 @@ fr: other: "il y a presque %{count} ans" password_reset: no_token: 'Oups ! Le lien que vous avez utilisé ne fonctionne plus. Vous pouvez vous connecter maintenant. Si vous avez oublié votre mot de passe, vous pouvez demander un lien pour le réinitialiser.' - choose_new: "Choisir un nouveau mot de passe" - choose: "Choisir un mot de passe" - update: "Mettre à jour le mot de passe" - save: "Définir le mot de passe" title: "Réinitialiser le mot de passe" success: "Vous avez modifié votre mot de passe avec succès et vous êtes maintenant connecté(e)." success_unapproved: "Vous avez modifié votre mot de passe avec succès." diff --git a/config/locales/server.gl.yml b/config/locales/server.gl.yml index 97b9ad460b..0379303fc8 100644 --- a/config/locales/server.gl.yml +++ b/config/locales/server.gl.yml @@ -690,10 +690,6 @@ gl: one: "hai case %{count} ano" other: "hai case %{count} anos" password_reset: - choose_new: "Elixir un novo contrasinal" - choose: "Elixir un contrasinal" - update: "Actualizar o contrasinal" - save: "Estabelecer o contrasinal" title: "Restabelecer contrasinal" success: "Cambiou correctamente o seu contrasinal e agora iniciou sesión." success_unapproved: "Cambiou correctamente o seu contrasinal." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 197c496919..08eca8cbef 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -873,10 +873,6 @@ he: other: "לפני %{count} שנים כמעט" password_reset: no_token: 'אוי ויי! הקישור בו השתמשת לא פעיל עוד. אפשר להיכנס כעת. אם שכחת את הסיסמה שלך, אפשר לבקש קישור כדי לאפס אותה.' - choose_new: "נא לבחור בסיסמה חדשה" - choose: "בחירת סיסמה" - update: "עדכון סיסמה" - save: "הגדרת סיסמה" title: "איפוס סיסמה" success: "החלפת את הסיסמה בהצלחה וכעת נכנסת לאתר." success_unapproved: "החלפת את הסיסמה בהצלחה." @@ -1072,7 +1068,7 @@ he: mailing_list_mode: "כבו את מצב ״רשימת תפוצה״" all: "לא לשלוח לי הודעות דוא״ל מאת %{sitename}" different_user_description: "נכנסת כמשתמש אחר מזה שאליו נשלחה הודעת דוא״ל. נא להתנתק או לעבור למצב אלמוני ולנסות שוב." - not_found_description: לא הצלחנו למצוא את המינוי הזה, עמך הסליחה. יכול להיות שהקישור בהודעה שקיבלת ישן מדי או שתוקפו פג?" + not_found_description: "לא הצלחנו למצוא את המינוי הזה, עמך הסליחה. יכול להיות שהקישור בהודעה שקיבלת ישן מדי או שתוקפו פג?" user_not_found_description: "לא הצלחנו למצוא משתמש למינוי הזה, עמך הסליחה. כנראה שניסית לבטל את המינוי לחשבון שלא קיים עוד." log_out: "התנתקות" submit: "שמירת העדפות" diff --git a/config/locales/server.hr.yml b/config/locales/server.hr.yml index 1b49d2bf7d..bbcf8a7c46 100644 --- a/config/locales/server.hr.yml +++ b/config/locales/server.hr.yml @@ -501,10 +501,6 @@ hr: few: "prije skoro %{count} godina" other: "prije skoro %{count} godina" password_reset: - choose_new: "Izaberite novu lozinku" - choose: "Izaberite lozinku" - update: "Ažurirajte zaporku" - save: "Postavite zaporku" title: "Ponovo postavite zaporku" success: "Uspiješno ste promijenili zaporku i sad ste prijavljeni." success_unapproved: "Uspiješno ste promijenili zaporku." diff --git a/config/locales/server.hu.yml b/config/locales/server.hu.yml index 80ceafa997..73774d3aaf 100644 --- a/config/locales/server.hu.yml +++ b/config/locales/server.hu.yml @@ -536,10 +536,6 @@ hu: one: "majdnem %{count} éve" other: "majdnem %{count} éve" password_reset: - choose_new: "Válasszon új jelszót" - choose: "Válasszon jelszót" - update: "Jelszó frissítése" - save: "Jelszó beállítása" title: "Jelszó visszaállítása" success: "Sikeresen megváltoztatta a jelszavát és bejelentkezett." success_unapproved: "Sikeresen megváltoztatta a jelszavát!" @@ -656,7 +652,7 @@ hu: mailing_list_mode: "Levelezőlista mód kikapcsolása" all: "Ne küldjön nekem semmilyen e-mailt a %{sitename} oldalról" different_user_description: "Jelenleg más felhasználóként van bejelentkezve, mint amit e-mailben küldtünk. Kérjük, jelentkezz ki, vagy lépj be anonim módban, és próbálkozz újra." - not_found_description: Sajnáljuk, de nem találtuk ezt az előfizetést. Lehetséges, hogy az e-mailben található link túl régi és lejárt?" + not_found_description: "Sajnáljuk, de nem találtuk ezt az előfizetést. Lehetséges, hogy az e-mailben található link túl régi és lejárt?" user_not_found_description: "Sajnáljuk, nem találtunk felhasználót ehhez az előfizetéshez. Valószínűleg megpróbál leiratkozni egy olyan fiókról, amely már nem létezik." log_out: "Kijelentkezés" submit: "Beállítások mentése" diff --git a/config/locales/server.hy.yml b/config/locales/server.hy.yml index ada4b8b60b..bfb5c14547 100644 --- a/config/locales/server.hy.yml +++ b/config/locales/server.hy.yml @@ -534,10 +534,6 @@ hy: one: "գրեթե %{count} տարի առաջ" other: "գրեթե %{count} տարի առաջ" password_reset: - choose_new: "Ընտրել նոր գաղտնաբառ" - choose: "Ընտրել գաղտնաբառ" - update: "Թարմացնել Գաղտնաբառը" - save: "Սահմանել Գաղտնաբառ" title: "Վերականգնել Գաղտնաբառը" success: "Դուք հաջողությամբ փոփոխել եք Ձեր գաղտնաբառը և այժմ մուտքագրված եք:" success_unapproved: "Դուք հաջողությամբ փոփոխել եք Ձեր գաղտնաբառը:" diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index c83cab5383..1ba355b143 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -322,10 +322,6 @@ id: almost_x_years: other: "hampir %{count} tahun yang lalu" password_reset: - choose_new: "Pilih kata sandi baru" - choose: "Pilih kata sandi" - update: "Perbaharui Kata Sandi" - save: "Atur Kata Sandi" title: "Atur Ulang Kata Sandi" success: "Anda telah mengubah kata sandi dengan sukses dan sekarang telah login." success_unapproved: "Anda telah mengubah kata sandi dengan sukses." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 28fbe41b97..a8ccf7fc02 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -767,10 +767,6 @@ it: other: "quasi %{count} anni fa" password_reset: no_token: 'Ops! Il link che hai utilizzato non funziona più. Puoi Accedere adesso. Se hai dimenticato la password, puoi richiedere un link per reimpostarla.' - choose_new: "Scegli una nuova password" - choose: "Scegli una password" - update: "Aggiorna Password" - save: "Imposta Password" title: "Reimposta Password" success: "La tua password è stata cambiata con successo e ora sei connesso." success_unapproved: "La tua password è stata cambiata con successo." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 0c62a332a1..09ce09d2fe 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -717,10 +717,6 @@ ja: other: "ほぼ %{count} 年前" password_reset: no_token: 'おっとっと!使用したリンクは機能しなくなっています。今すぐログインしましょう。パスワードを忘れた場合は、リセットするためのリンクをリクエストできます。' - choose_new: "新しいパスワードを選択する" - choose: "パスワードを選択する" - update: "パスワードを更新" - save: "パスワードを設定" title: "パスワードをリセット" success: "パスワードを変更し、ログインしました。" success_unapproved: "パスワードを変更しました。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 3f6592bb6e..d683b7f471 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -684,10 +684,6 @@ ko: almost_x_years: other: "약 %{count}년 전" password_reset: - choose_new: "새 비밀번호를 입력하세요" - choose: "비밀번호를 입력하세요" - update: "비밀번호 업데이트" - save: "비밀번호 설정" title: "비밀번호 리셋" success: "비밀번호를 변경했으며, 로그인되었습니다." success_unapproved: "비밀번호를 변경했습니다." diff --git a/config/locales/server.lt.yml b/config/locales/server.lt.yml index dda62bee6a..3be96d5943 100644 --- a/config/locales/server.lt.yml +++ b/config/locales/server.lt.yml @@ -680,10 +680,6 @@ lt: many: "beveik prieš %{count} metų" other: "beveik prieš %{count} metų" password_reset: - choose_new: "Pasirinkite naują slaptažodį" - choose: "Pasirinkite slaptažodį" - update: "Atnaujinti slaptažodį" - save: "Nustatykite slaptažodį" title: "Keisti slaptažodį" success: "Jūs sėkmingai pasikeitėte savo slaptažodį ir dabar esate prisijungęs." success_unapproved: "Jūs sėkmingai pasikeitėte savo slaptažodį." diff --git a/config/locales/server.lv.yml b/config/locales/server.lv.yml index 779ab778ed..0caabfe70c 100644 --- a/config/locales/server.lv.yml +++ b/config/locales/server.lv.yml @@ -183,9 +183,6 @@ lv: one: "pirms %{count} dienas" other: "pirms %{count} dienām" password_reset: - choose_new: "Izvēlēties jaunu paroli" - choose: "Izvēlēties paroli" - save: "Iestatīt paroli" title: "Atjaunot paroli" change_email: please_continue: "Turpināt %{site_name}" diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 6472f83f8f..066c082928 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -524,10 +524,6 @@ nb_NO: one: "nesten %{count} år siden" other: "nesten %{count} år siden" password_reset: - choose_new: "Velg et nytt passord" - choose: "Velg et passord" - update: "Oppdater passord" - save: "Sett passord" title: "Tilbakestill passord" success: "Du har endret passordet og er nå logget inn." success_unapproved: "Du har endret ditt passord." diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index bf152f1797..13f9e5f934 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -770,10 +770,6 @@ nl: other: "bijna %{count} jaar geleden" password_reset: no_token: 'Oeps! De link die je hebt gebruikt werkt niet meer. Je kunt je nu aanmelden . Als je je wachtwoord bent vergeten, kun je een link verzoeken om het te herstellen.' - choose_new: "Kies een nieuw wachtwoord" - choose: "Kies een wachtwoord" - update: "Wachtwoord bijwerken" - save: "Wachtwoord instellen" title: "Wachtwoord herstellen" success: "Je hebt je wachtwoord gewijzigd en bent nu aangemeld." success_unapproved: "Je hebt je wachtwoord gewijzigd." @@ -967,7 +963,7 @@ nl: mailing_list_mode: "Mailinglijstmodus uitschakelen" all: "Stuur me geen e-mail van %{sitename}" different_user_description: "Je bent momenteel aangemeld als een andere gebruiker dan die we een e-mail hebben gestuurd. Meld je af of open de anonieme modus en probeer het opnieuw." - not_found_description: Sorry, we konden dat abonnement niet vinden. Het is mogelijk dat de link in je e-mail te oud is en verlopen is?" + not_found_description: "Sorry, we konden dat abonnement niet vinden. Het is mogelijk dat de link in je e-mail te oud is en verlopen is?" user_not_found_description: "Sorry, we konden geen gebruiker vinden voor dit abonnement. Je probeert waarschijnlijk een account op te zeggen dat niet meer bestaat." log_out: "Afmelden" submit: "Voorkeuren opslaan" diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 1bfa8ded3d..4ba9e8c5d0 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -870,10 +870,6 @@ pl_PL: other: "prawie %{count} lat temu" password_reset: no_token: 'Ups! Link, którego użyłeś, już nie działa. Możesz zalogować się teraz. Jeśli nie pamiętasz swojego hasła, możesz poprosić o łącze, aby je zresetować.' - choose_new: "Wprowadź nowe hasło" - choose: "Wprowadź hasło" - update: "Aktualizuj hasło" - save: "Ustaw hasło" title: "Zresetuj hasło" success: "Twoje hasło zostało pomyślnie zmienione, zalogowano." success_unapproved: "Zmieniłeś(-a) swoje hasło." @@ -1067,7 +1063,6 @@ pl_PL: mailing_list_mode: "Wyłącz tryb listy mailingowej" all: "Nie wysyłaj mi żadnych wiadomości z %{sitename}" different_user_description: "Jesteś zalogowany jako inny użytkownik niż ten do którego wysłano wysłaliśmy emaila. proszę wyloguj się, lub przejść do trybu incognito i spróbuj ponownie." - not_found_description: Przykro nam, nie udało nam się znaleźć tej subskrypcji. Możliwe, że link w twoim mailu jest zbyt stary i stracił ważność?". user_not_found_description: "Przepraszamy, nie mogliśmy znaleźć użytkownika dla tej subskrypcji. Prawdopodobnie próbujesz zrezygnować z subskrypcji z konta, które już nie istnieje." log_out: "Wyloguj" submit: "Zapisz ustawienia" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index eec2826f71..4fba865cf4 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -591,10 +591,6 @@ pt: one: "há quase %{count} ano atrás" other: "há quase %{count} anos atrás" password_reset: - choose_new: "Escolha uma nova palavra-passe" - choose: "Escolha uma palavra-passe" - update: "Atualizar Palavra-passe" - save: "Definir Palavra-passe" title: "Redefinir Palavra-passe" success: "A sua palavra-passe foi alterada com sucesso e está agora ligado." success_unapproved: "Palavra-passe modificada com sucesso." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 14e6da1558..dd1339f1ae 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -768,10 +768,6 @@ pt_BR: other: "há quase %{count} anos" password_reset: no_token: 'Ops! O link que você usou não funciona mais. Agora você poderá Log In. Se tiver esquecido sua senha, você poderá solicitar um link para redefini-la.' - choose_new: "Escolha uma nova senha" - choose: "Escolha uma senha" - update: "Atualizar senha" - save: "Definir senha" title: "Redefinir senha" success: "Sua senha foi modificada com êxito e você já entrou com a sua conta." success_unapproved: "Senha modificada com êxito." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 3d4bb318eb..9260789c49 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -452,10 +452,6 @@ ro: few: "cu aproape %{count} ani în urmă" other: "cu aproape %{count} de ani în urmă" password_reset: - choose_new: "Alege o parolă nouă" - choose: "Alege o parolă" - update: "Actualizează parola" - save: "Setează parola" title: "Resetează parola" success: "Ți-ai schimbat cu succes parola și acum ești autentificat." success_unapproved: "Ți-ai schimbat cu succes parola." diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 11a5db9fd4..fc1e2bfedb 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -864,10 +864,6 @@ ru: many: "почти %{count} лет назад" other: "почти %{count} года назад" password_reset: - choose_new: "Введите новый пароль" - choose: "Введите пароль" - update: "Обновить пароль" - save: "Установить пароль" title: "Сбросить пароль" success: "Пароль изменен, вы вошли на сайт." success_unapproved: "Пароль изменен." @@ -1163,7 +1159,7 @@ ru: description: "Аккаунты, зарегистрированные за выбранный период." new_contributors: title: "Новые участники" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество новых участников" description: "Количество пользователей, которые создали свою первую запись за выбранный период." trust_level_growth: @@ -1181,7 +1177,7 @@ ru: page_view_crawler: "Поисковые роботы" page_view_anon: "Анонимные пользователи" page_view_logged_in: "Вошедшие пользователи" - yaxis: "День" + yaxis: "Дата" description: "Статистика просмотра страниц с учётом зарегистрированных и анонимных пользователей, а также поисковых роботов." labels: post: Запись @@ -1195,12 +1191,12 @@ ru: yaxis: "Дата" dau_by_mau: title: "DAU/MAU" - xaxis: "День" + xaxis: "Дата" yaxis: "DAU/MAU" - description: "Количество участников, которые вошли за последний день, поделённое на количество участников, которые вошли за прошедший месяц — — возвращает %, который указывает «удержание» сообщества. Стремитесь к значению > 30%." + description: "Количество участников, которые вошедших за последний день, поделённое на количество участников, вошедших за прошедший месяц. Возвращает %, который указывает «удержание» сообщества. Стремитесь к значению > 30%." daily_engaged_users: title: "Ежедневно вовлечённые пользователи" - xaxis: "День" + xaxis: "Дата" yaxis: "Вовлечённые пользователи" description: "Количество пользователей, которые ставили лайки или публиковали записи за последний день." profile_views: @@ -1225,12 +1221,12 @@ ru: description: "Количество новых лайков." flags: title: "Жалобы" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество жалоб" description: "Количество новых жалоб." bookmarks: title: "Закладки" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество новых закладок" description: "Количество новых тем и записей, добавленных в закладки." users_by_trust_level: @@ -1257,78 +1253,78 @@ ru: title: Популярные поисковые запросы labels: term: Запрос - searches: Поиски + searches: Поисковые запросы click_through: CTR description: "Самые популярные поисковые запросы с коэффициентами кликов." emails: title: "Отправленные письма" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество электронных писем" description: "Количество новых отправленных писем." user_to_user_private_messages: title: "Личные сообщения (исключая ответы)" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество сообщений" description: "Количество новых личных сообщений без учёта ответов." user_to_user_private_messages_with_replies: title: "Личные сообщения (включая ответы)" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество сообщений" description: "Количество всех новых личных сообщений с учётом ответов." system_private_messages: title: "Системные сообщения" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество сообщений" description: "Количество личных сообщений, автоматически отправленных системой." moderator_warning_private_messages: title: "Предупреждения модераторов" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество предупреждений" description: "Количество предупреждений, отправленных модераторами в личных сообщениях." notify_moderators_private_messages: title: "Жалобы модераторам" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество сообщений" - description: "Количество конфиденциальных жалоб, направленных модераторам посредством пометки." + description: "Количество конфиденциальных жалоб, направленных модераторам." notify_user_private_messages: title: "Жалобы пользователям" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество сообщений" - description: "Количество конфиденциальных жалоб, направленных пользователям посредством пометки." + description: "Количество конфиденциальных жалоб, направленных пользователям." top_referrers: title: "Топ распространителей" xaxis: "Пользователь" - num_clicks: "Кликов" + num_clicks: "Переходов" num_topics: "Тем" labels: user: "Пользователь" - num_clicks: "Кликов" + num_clicks: "Переходов" num_topics: "Тем" description: "Пользователи, отсортированные по количеству кликов по ссылкам, которыми они поделились." top_traffic_sources: title: "Топ источников трафика" xaxis: "Домен" - num_clicks: "Кликов" + num_clicks: "Переходов" num_topics: "Тем" - num_users: "Пользователи" + num_users: "Пользователей" labels: domain: Домен - num_clicks: Кликов + num_clicks: Переходов num_topics: Тем description: "Внешние источники, с которых зафиксировано максимальное количество переходов на этот сайт." top_referred_topics: title: "Топ тем, на которые ссылаются" labels: - num_clicks: "Кликов" + num_clicks: "Переходов" topic: "Тема" description: "Темы, которые получили наибольшее количество кликов из внешних источников." page_view_anon_reqs: title: "Анонимные просмотры" - xaxis: "День" + xaxis: "Дата" yaxis: "Анонимные просмотры страниц" description: "Количество новых просмотров пользователями, не авторизованными в системе." page_view_logged_in_reqs: - title: "Просмотры авторизованными пользователями" + title: "Авторизованные пользователи" xaxis: "Дата" yaxis: "Просмотры авторизованными пользователями" description: "Количество новых просмотров авторизованными пользователями." @@ -1339,12 +1335,12 @@ ru: description: "Общее количество просмотров страниц поисковыми роботами за период." page_view_total_reqs: title: "Просмотры" - xaxis: "День" + xaxis: "Дата" yaxis: "Всего просмотров страниц" description: "Количество новых просмотров с учётом всех посетителей." page_view_logged_in_mobile_reqs: title: "Просмотры авторизованными пользователями" - xaxis: "День" + xaxis: "Дата" yaxis: "Просмотры авторизованными пользователями с мобильных устройств" description: "Количество новых просмотров страниц авторизованными пользователями с мобильных устройств." page_view_anon_mobile_reqs: @@ -1354,41 +1350,41 @@ ru: description: "Количество новых просмотров с мобильных устройств посетителями, которые не вошли в систему." http_background_reqs: title: "Фоновые" - xaxis: "День" + xaxis: "Дата" yaxis: "Запросы на отслеживание и обновления в режиме реального времени" http_2xx_reqs: title: "Статус HTTP 2xx (OK)" - xaxis: "День" + xaxis: "Дата" yaxis: "Успешные запросы (статус 2xx)" http_3xx_reqs: title: "HTTP 3xx (переадресация)" - xaxis: "День" + xaxis: "Дата" yaxis: "Запросы с переадресацией (статус 3xx)" http_4xx_reqs: title: "HTTP 4xx (ошибка клиента)" - xaxis: "День" + xaxis: "Дата" yaxis: "Запросы с ошибкой клиента (статус 4xx)" http_5xx_reqs: title: "HTTP 5xx (ошибка сервера)" - xaxis: "День" + xaxis: "Дата" yaxis: "Запросы с ошибкой сервера (статус 5xx)" http_total_reqs: title: "Всего" - xaxis: "День" + xaxis: "Дата" yaxis: "Всего запросов" time_to_first_response: title: "Время до первого ответа" - xaxis: "День" + xaxis: "Дата" yaxis: "Среднее время (в часах)" description: "Среднее время (в часах) до первого ответа на новые темы." topics_with_no_response: title: "Темы без ответов" - xaxis: "День" + xaxis: "Дата" yaxis: "Всего" description: "Количество созданных новых тем, которые не получили ответа." mobile_visits: title: "Посещения с мобильных устройств" - xaxis: "День" + xaxis: "Дата" yaxis: "Количество визитов" description: "Количество уникальных пользователей, посетивших сайт с мобильных устройств." web_crawlers: @@ -1435,31 +1431,31 @@ ru: labels: user: Пользователь qtt_like: Получено лайков - description: "Топ 10 пользователей, получившие больше всего лайков" + description: "Топ-10 пользователей, получившие больше всего лайков" top_users_by_likes_received_from_inferior_trust_level: title: "Топ пользователей по количеству лайков, полученных от пользователей с более низким уровнем доверия" labels: user: Пользователь trust_level: Уровень доверия qtt_like: Получено лайков - description: "Топ-10 пользователей с более высоким уровнем доверия, получившие симпатии от пользователей с более низким уровнем доверия." + description: "Топ-10 пользователей с более высоким уровнем доверия, получивших лайки от пользователей с более низким уровнем доверия." top_users_by_likes_received_from_a_variety_of_people: - title: "Топ пользователей по количеству лайков, полученных от самых разных пользователей." + title: "Топ пользователей по количеству лайков, полученных от различных пользователей." labels: user: Пользователь qtt_like: Получено лайков - description: "Топ 10 пользователей по количеству лайков, полученных от самых разных пользователей." + description: "Топ-10 пользователей по количеству лайков, полученных от различных пользователей." dashboard: group_email_credentials_warning: 'Возникла проблема с учётными данными электронной почты для группы %{group_full_name}. Электронная почта не будет отправляться из почтового ящика группы, пока эта проблема не будет решена. %{error}' rails_env_warning: "Сервер работает в режиме %{env}." - host_names_warning: "Файл config/database.yml использует имя хоста localhost по умолчанию. Поменяйте его на имя хоста сайта." + host_names_warning: "Файл config/database.yml использует имя хоста localhost по умолчанию. Измените его на имя хоста сайта." sidekiq_warning: 'Планировщик заданий Sidekiq не запущен. Многие задачи, в том числе отправка писем, должны выполняться им асинхронно. Должен быть запущен хотя бы один процесс sidekiq. Подробнее о Sidekiq — здесь.' queue_size_warning: "Большое количество заданий в очереди: %{queue_size}. Это может привести к проблемам с процессами Sidekiq — придётся добавить больше рабочих процессов Sidekiq." memory_warning: "Сервер использует менее 1 ГБ памяти. Рекомендовано использовать минимум 1 ГБ." google_oauth2_config_warning: 'Сервер настроен так, чтобы разрешать регистрацию и вход с помощью Google OAuth2 (enable_google_oauth2_logins), но идентификатор клиента и значения секретного кода клиента не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' - facebook_config_warning: 'Сервер настроен так, чтобы разрешать регистрацию и вход с помощью Facebook (enable_facebook_logins), но идентификатор приложения и секретные значения приложения не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' - twitter_config_warning: 'Сервер настроен так, чтобы разрешать регистрацию и вход с помощью Twitter (enable_twitter_logins), но ключ и секретные значения не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' - github_config_warning: 'Сервер настроен, так, чтобы разрешать регистрацию и вход с помощью GitHub (enable_github_logins), но идентификатор клиента и секретные значения не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' + facebook_config_warning: 'Сервер настроен так, чтобы разрешать регистрацию и вход с помощью Facebook (enable_facebook_logins), но идентификатор приложения и значения ключа приложения не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' + twitter_config_warning: 'Сервер настроен так, чтобы разрешать регистрацию и вход с помощью Twitter (enable_twitter_logins), но ключ и значения ключа не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' + github_config_warning: 'Сервер настроен, так, чтобы разрешать регистрацию и вход с помощью GitHub (enable_github_logins), но идентификатор клиента и значения ключа не заданы. Зайдите в настройки сайта и измените параметры. Подробнее — в этом руководстве.' s3_config_warning: 'Сервер настроен на загрузку файлов в S3, но как минимум один из следующих параметров не задан: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile или s3_upload_bucket. Зайдите в настройки сайта и измените параметры. Подробнее — на странице Как настроить загрузку изображений в S3.' s3_backup_config_warning: 'Сервер настроен на загрузку резервных копий в S3, но как минимум один из следующих параметров не задан: s3_access_key_id, s3_secret_access_key, s3_use_iam_profile или s3_backup_bucket. Зайдите в настройки сайта и измените параметры. Подробнее — на странице Как настроить загрузку изображений в S3.' s3_cdn_warning: 'Сервер настроен на загрузку файлов в S3, но CDN не настроена. Это может привести к увеличению затрат на хранение данных и снижению производительности сайта. Детали см. в теме Using Object Storage for Uploads.' @@ -1481,24 +1477,24 @@ ru: watched_word_regexp_error: "Недопустимое регулярное выражение для контролируемых слов типа «%{action}». Проверьте параметры контролируемых слов или отключите в настройках сайта пункт «Контролируемые слова представлены регулярными выражениями»." site_settings: allow_bulk_invite: "Разрешить массовые приглашения путём загрузки CSV-файла" - disabled: "отключён" + disabled: "отключено" display_local_time_in_user_card: "Отображать местное время в карточке пользователя." censored_words: "Слова, которые будут автоматически заменены на ■■■■." delete_old_hidden_posts: "Автоматически удалять записи, скрытые дольше 30 дней." default_locale: "Язык по умолчанию для этого экземпляра Discourse. Текст сгенерированных системой категорий и тем можно изменить на вкладке Оформление / Текст." allow_user_locale: "Разрешить пользователям выбирать язык интерфейса" - set_locale_from_accept_language_header: "Задавать язык интерфейса для анонимных пользователей по языковым настройка браузера" + set_locale_from_accept_language_header: "задавать язык интерфейса для анонимных пользователей по языковым настройка браузера" support_mixed_text_direction: "Поддерживать смешанные направления текста слева направо и справа налево." min_post_length: "Минимум символов в одной записи" min_first_post_length: "Минимум символов в первой записи (текст темы)" - min_personal_message_post_length: "Минимум символов в личном сообщении" - max_post_length: "Максимум символов в одной записи" + min_personal_message_post_length: "Минимум символов в сообщении" + max_post_length: "Максимум символов в записи" topic_featured_link_enabled: "Разрешать публиковать ссылку на избранную тему." show_topic_featured_link_in_digest: "Показывать ссылку на избранную тему в дайджесте по эл. почте." min_topic_views_for_delete_confirm: "Минимальное количество просмотров темы, при котором появляется всплывающее окно с запросом подтверждения при её удалении" min_topic_title_length: "Минимум символов в названии темы" max_topic_title_length: "Максимум символов в названии темы" - min_personal_message_title_length: "Минимум символов в заголовке личного сообщения" + min_personal_message_title_length: "Минимум символов в заголовке сообщения" max_emojis_in_title: "Максимальное количество эмодзи в названии темы" min_search_term_length: "Минимальное количество символов в поисковом запросе" search_tokenize_chinese: "Принудительный поиск для токенизации китайского языка даже на некитайских сайтах" @@ -1513,82 +1509,82 @@ ru: category_search_priority_high_weight: "Вес, применяемый к ранжированию для высокого приоритета поиска категории." allow_uncategorized_topics: "Разрешить создание тем без выбора категории. ВНИМАНИЕ! Если в разделе «Без категории» есть какие-либо темы, их нужно переместить в соответствующие категории, прежде чем отключать этот параметр." allow_duplicate_topic_titles: "Разрешать создание тем с одинаковыми названиями." - allow_duplicate_topic_titles_category: "Разрешить создание тем с одинаковыми названиями, если они создаются в разных категориях. (Параметр allow_duplicate_topic_titles должен быть отключён)." + allow_duplicate_topic_titles_category: "Разрешить создание тем с одинаковыми названиями, если они создаются в разных категориях. (Параметр «allow_duplicate_topic_titles» должен быть отключён)." unique_posts_mins: "Минимальное количество минут между созданием записей с одинаковым контентом" - educate_until_posts: "Количество первых сообщений новых пользователей, при создании которых необходимо показывать всплывающую подсказку с советами для новичков." + educate_until_posts: "Количество первых записей новых пользователей, при создании которых необходимо показывать всплывающую подсказку с советами для новичков." crawl_images: "Скачивать картинки с других ресурсов для автоматического определения их размеров." - download_remote_images_to_local: "Конвертировать удалённые (hotlinked) изображения в локальные изображения; это позволит сохранить содержимое, даже если в будущем изображения будут удалены со стороннего сайта." - download_remote_images_threshold: "Минимально доступное место на диске (в процентах), необходимое для хранения скачанных картинок." + download_remote_images_to_local: "Конвертировать удалённые (hotlinked) изображения в локальные изображения; это позволит сохранить контент, даже если в будущем изображения будут удалены со стороннего сайта." + download_remote_images_threshold: "Минимально доступное место на диске (в процентах), необходимое для хранения скачанных изображений." disabled_image_download_domains: "Домены, с которых не нужно скачивать картинки. Список с разделителем-чертой." - block_hotlinked_media: "Запретить пользователям размещать в своих сообщениях удалённый (hotlinked) мультимедийный контент. Подобный контент, если он не был загружен при включённом параметре «download_remote_images_to_local», будет заменён ссылкой." + block_hotlinked_media: "Запретить пользователям размещать в своих записях удалённый (hotlinked) мультимедийный контент. Подобный контент, если он не был загружен при включённом параметре «download_remote_images_to_local», будет заменён ссылкой." block_hotlinked_media_exceptions: "Список базовых URL-адресов, на которые не распространяется настройка параметра «block_hotlinked_media». В начале адреса укажите протокол (например, https://example.com)." - editing_grace_period: "Количество секунд после отправки сообщения, в течении которых правка сообщения не будет записываться в историю правок (т.н. льготный период редактирования)." - editing_grace_period_max_diff: "Максимальное количество изменённых символов, допустимое в льготном периоде редактирования. При превышении этого значения все изменения будут записаны в историю правок (уровень доверия 0 и 1)." - editing_grace_period_max_diff_high_trust: "Максимальное количество изменённых символов, допустимое в льготном периоде редактирования. При превышении этого значения все изменения будут записаны в историю правок (уровень доверия 2 и выше)." - staff_edit_locks_post: "Сообщения будут недоступны для редактирования, если они редактировались персоналом" - post_edit_time_limit: "Пользователи с уровнем доверия 0 или 1 могут изменять своё сообщение в течение указанного здесь количества минут после его публикации. Если значение установлено в 0, то этот период не ограничен." - tl2_post_edit_time_limit: "Пользователи с уровнем доверия 2 и выше могут изменять своё сообщение в течение указанного здесь количества минут после его публикации. Если значение установлено в 0, то этот период не ограничен." - edit_history_visible_to_public: "Разрешить обычным пользователям просматривать историю редактирования сообщений. В противном случае история редактирования будет доступна только персоналу." - delete_removed_posts_after: "Сообщения, удалённые автором, будут автоматически удаляться через указанное здесь количество часов. Если значение установлено в 0, то сообщения будут удаляться немедленно." + editing_grace_period: "Количество секунд после отправки записи, в течении которых правка не будет записываться в историю правок." + editing_grace_period_max_diff: "Максимальное количество изменённых символов, допустимое в нерегистрируемый период редактирования. При превышении этого значения все изменения будут записаны в историю правок (уровень доверия 0 и 1)." + editing_grace_period_max_diff_high_trust: "Максимальное количество изменённых символов, допустимое в нерегистрируемый период редактирования. При превышении этого значения все изменения будут записаны в историю правок (уровень доверия 2 и выше)." + staff_edit_locks_post: "Записи будут недоступны для редактирования, если они редактировались персоналом" + post_edit_time_limit: "Пользователи с уровнем доверия 0 или 1 могут изменять свою запись в течение указанного количества минут после публикации. Если значение установлено в 0, то этот период не ограничен." + tl2_post_edit_time_limit: "Пользователи с уровнем доверия 2 и выше могут изменять свою запись в течение указанного количества минут после публикации. Если значение установлено в 0, то этот период не ограничен." + edit_history_visible_to_public: "Разрешить обычным пользователям просматривать историю редактирования записи. В противном случае история редактирования будет доступна только персоналу." + delete_removed_posts_after: "Записи, удалённые автором, будут автоматически удаляться через указанное здесь количество часов. Если значение установлено в 0, то записи будут удаляться немедленно." notify_users_after_responses_deleted_on_flagged_post: "Отправлять уведомление пользователям после удаления темы, в которой они отвечали, поскольку на эту тему поступила жалоба." - responsive_post_image_sizes: "Изменять размер превью в лайтбоксе, чтобы использовать экраны с высоким разрешением при указанных соотношениях пикселей. Удалите все значения, если необходимо отключить адаптивный дизайн." - fixed_category_positions: "Отображать разделы в строго указанном порядке. В противном случае сортировка категорий будет зависеть от степени активности в них." - fixed_category_positions_on_create: "Сохранять порядок отображения категорий в диалоговом окне создания темы (должен быть включён параметр fixed_category_positions)." - add_rel_nofollow_to_user_content: 'Добавлять «rel nofollow» для всех ссылок за исключением внутренних (включая родительский домен). Если вы измените эту настройку, то необходимо перезаписать все сообщения, выполнив команду «rake posts:rebake»' - exclude_rel_nofollow_domains: "Список доменов, где атрибут «nofollow» не следует добавлять в ссылки. Например, сайт example.com также автоматически разрешит sub.example.com. Как минимум вы должны добавить домен этого сайта, чтобы поисковые роботы могли найти весь контент. Если другие части вашего сайта находятся в других доменах, добавьте и их тоже." - post_excerpt_maxlength: "Максимальная длина резюме сообщения." - topic_excerpt_maxlength: "Максимальная длина фрагмента / резюме темы, созданного из первого сообщения темы." + responsive_post_image_sizes: "Изменять размер предпросмотра в лайтбоксе, чтобы использовать экраны с высоким разрешением при указанных соотношениях пикселей. Если необходимо отключить адаптивный дизайн, удалите все значения." + fixed_category_positions: "Отображать категории в строго указанном порядке. В противном случае сортировка категорий будет зависеть от степени активности в них." + fixed_category_positions_on_create: "Сохранять порядок отображения категорий в диалоговом окне создания темы (должен быть включён параметр «fixed_category_positions»)." + add_rel_nofollow_to_user_content: 'Добавлять «rel nofollow» для всех ссылок за исключением внутренних (включая родительский домен). Если вы измените эту настройку, то необходимо обновить все записи командой «rake posts:rebake».' + exclude_rel_nofollow_domains: "Список доменов, где атрибут «nofollow» не следует добавлять в ссылки. Например, сайт example.com также автоматически разрешит sub.example.com. Как минимум нужно добавить домен этого сайта, чтобы поисковые роботы могли находить его контент. Если другие части сайта находятся в других доменах, добавьте их тоже." + post_excerpt_maxlength: "Максимальная длина сводки записи." + topic_excerpt_maxlength: "Максимальная длина фрагмента (сводки) темы, созданных из первой записи темы." show_pinned_excerpt_mobile: "Показывать фрагменты закреплённых тем в мобильном представлении." - show_pinned_excerpt_desktop: "Показывать фрагменты закреплённых тем в настольных устройствах." - post_onebox_maxlength: "Максимальная длина сообщения в режиме умной вставки." + show_pinned_excerpt_desktop: "Показывать фрагменты закреплённых тем на настольных устройствах." + post_onebox_maxlength: "Максимальная длина записи в режиме умной вставки." blocked_onebox_domains: "Список доменов, которые не будут преобразовываться в умную вставку, например wikipedia.org\n(подстановочные знаки «*» и «?» не поддерживаются)." block_onebox_on_redirect: "Не использовать умную вставку, если с URL-адреса идёт переадресация." allowed_inline_onebox_domains: "Список доменов, контент с которых будет преобразовываться в умную вставку, если ссылка указана без заголовка." enable_inline_onebox_on_all_domains: "Игнорировать параметр `inline_onebox_domain_whitelist` и разрешить умную вставку для всех доменов." - force_custom_user_agent_hosts: "Хосты, для которых можно использовать пользовательский User-Agent умных вставок для всех запросов. (Особенно полезно для хостов, которые ограничивают доступ пользовательским агентам)." + force_custom_user_agent_hosts: "Хосты, для которых можно использовать пользовательский User-Agent умных вставок для всех запросов. (Особенно полезно для хостов, которые ограничивают доступ пользовательским агентам.)" max_oneboxes_per_post: "Максимальное количество умных вставок в записи." - facebook_app_access_token: "Токен, созданный на основе идентификатора и секрета вашего приложения Facebook. Используется для создания умных вставок для Instagram." - logo: "Логотип в левом верхнем углу вашего сайта. Используйте широкое прямоугольное изображение с высотой 120 и соотношением сторон более чем 3:1. Если оставить поле пустым, будет показан текст заголовка сайта." - logo_small: "Уменьшенный логотип в левом верхнем углу вашего сайта, отображаемый при прокрутке вниз. Используйте квадратное изображение 120 × 120. Если оставить поле пустым, будет отображаться значок главной страницы." + facebook_app_access_token: "Токен, созданный на основе идентификатора и ключа приложения Facebook. Используется для создания умных вставок для Instagram." + logo: "Логотип в левом верхнем углу сайта. Используйте широкое прямоугольное изображение с высотой 120 и соотношением сторон более чем 3:1. Если оставить поле пустым, будет показан текст заголовка сайта." + logo_small: "Уменьшенный логотип в левом верхнем углу сайта, отображаемый при прокрутке вниз. Используйте квадратное изображение 120 × 120. Если оставить поле пустым, будет отображаться значок главной страницы." digest_logo: "Альтернативный логотип, используемый в верхней части страницы, отображающей письма со сводками. Используйте широкое прямоугольное изображение. Не используйте изображение в формате SVG. Если оставить поле пустым, будет использоваться логотип из настройки `logo`." mobile_logo: "Логотип, используемый в мобильной версии сайта. Используйте широкое прямоугольное изображение с высотой 120 и соотношением сторон более чем 3:1. Если оставить поле пустым, будет использоваться логотип из настройки `logo`." - logo_dark: "Dark scheme alternative for the «logo» site setting." - logo_small_dark: "Dark scheme alternative for the «logo small» site setting." - mobile_logo_dark: "Dark scheme alternative for the «mobile logo» site setting." - large_icon: "Изображение, используемое в качестве основы для других значков метаданных. В идеале должно быть больше 512 х 512. Если оставить поле пустым, будет использоваться уменьшенный логотип." - manifest_icon: "Изображение, используемое в качестве логотипа на Android-устройствах. Размер будет автоматически изменён до 512 × 512. Если оставить поле пустым, будет использоваться большой значок." - manifest_screenshots: "Скриншоты, демонстрирующие особенности и функциональные возможности вашего приложения, отображаемые на странице с запросом на установку. Все изображения должны быть загружены локально и иметь одинаковые размеры." - favicon: "Значок сайта, см. https://ru.wikipedia.org/wiki/Favicon. Для корректного отображения она должен быть в формате PNG. Размер будет автоматически изменён до 32x32. Если оставить поле пустым, будет использоваться большой значок." - apple_touch_icon: "Значок, используемый на сенсорных устройствах Apple. Размер будет автоматически изменён до 180x180. Если оставить поле пустым, будет использоваться большой значок." - opengraph_image: "Стандартное изображение opengraph, используемое, если на странице нет другого подходящего изображения. Если оставить поле пустым, будет использоваться большой значок." - twitter_summary_large_image: "Сводная карточка Twitter с большим изображением (должна быть не менее 280 пикселей в ширину, не менее 150 пикселей в высоту и не может быть в формате «svg»). Если оставить поле пустым, обычные метаданные карточки генерируются с использованием технологии opengraph image (если исходное изображение также не в формате «svg»)." + logo_dark: "Альтернативное значение настройки сайта «logo» для тёмной схемы." + logo_small_dark: "Альтернативное значение настройки сайта «logo small» для тёмной схемы." + mobile_logo_dark: "Альтернативное значение настройки сайта «mobile logo» для тёмной схемы." + large_icon: "Изображение, используемое в качестве основы для других значков метаданных. В идеале должно быть больше 512 х 512. Если оставить поле пустым, будет использоваться «logo_small»." + manifest_icon: "Изображение, используемое в качестве логотипа на Android-устройствах. Размер будет автоматически изменён до 512 × 512. Если оставить поле пустым, будет использоваться «large_icon»." + manifest_screenshots: "Скриншоты, демонстрирующие особенности и функциональные возможности приложения, отображаемые на странице с запросом на установку. Все изображения должны быть загружены локально и иметь одинаковые размеры." + favicon: "Значок сайта, см. https://ru.wikipedia.org/wiki/Favicon. Для корректного отображения должен быть в формате PNG. Размер будет автоматически изменён до 32x32. Если оставить поле пустым, будет использоваться «large_icon»." + apple_touch_icon: "Значок, используемый на сенсорных устройствах Apple. Размер будет автоматически изменён до 180x180. Если оставить поле пустым, будет использоваться «large_icon»." + opengraph_image: "Стандартное изображение opengraph, используемое, если на странице нет другого подходящего изображения. Если оставить поле пустым, будет использоваться «large_icon»." + twitter_summary_large_image: "Сводная карточка Twitter с большим изображением (должна быть не менее 280 пикселей в ширину, не менее 150 пикселей в высоту и не может быть в формате «svg»). Если оставить поле пустым, обычные метаданные карточки генерируются с использованием «opengraph_image» (если исходное изображение также не в формате «svg»)." notification_email: "Отправитель: эл. почта, используемая при отправке всех системных писем. Для успешной отправки писем указанный здесь домен должен иметь правильно настроенные SPF, DKIM и reverse PTR." email_custom_headers: "Разделённый вертикальной чертой список дополнительных заголовков в почтовых сообщениях" email_subject: "Настраиваемый формат темы для стандартных писем. См. тему https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801." detailed_404: "Предоставление пользователям более подробной информации о том, почему они не могут получить доступ к определённой теме. Примечание: это менее безопасно, поскольку пользователи будут знать, ссылается ли URL-адрес на допустимую тему." - enforce_second_factor: "Принудительное включение двухфакторной аутентификации. Выберите «all», чтобы применить его ко всем пользователям. Выберите «staff», чтобы принудительно использовать её только сотрудниками." - force_https: "Принудительное использование протокола HTTPS. ВНИМАНИЕ: НЕ ВКЛЮЧАЙТЕ этот параметр, пока не убедитесь, что HTTPS полностью настроен и работает абсолютно везде! Вы проверили настройки сети, доступность аккаунтов в социальных сетях, доступность внешних логотипов и зависимостей, чтобы убедиться, что они корректно работают при использовании HTTPS?" + enforce_second_factor: "Принудительное включение двухфакторной аутентификации. «All» — применить ко всем пользователям. «Staff» — принудительно использовать только для персонала." + force_https: "Принудительное использование протокола HTTPS. ВНИМАНИЕ: НЕ ВКЛЮЧАЙТЕ этот параметр, пока не убедитесь, что HTTPS полностью настроен и работает абсолютно везде! Настройки CDN, вход через соцсети, внешние логотипы и зависимости должны корректно работать через HTTPS." same_site_cookies: "Использовать одни и те же файлы cookie, они предотвращают межсайтовую подделку запроса в поддерживаемых браузерах (Lax или Strict). Предупреждение: Strict будет работать только на сайтах, которые поддерживают принудительный вход в систему и используют внешнюю аутентификацию." - summary_score_threshold: "Минимальная оценка сообщения, необходимая для его включения в сводку по теме" - summary_posts_required: "Минимальное количество сообщений, необходимое для получения сводки по теме. Изменения этого параметра будут применены к темам за прошедшую неделю." + summary_score_threshold: "Минимальная оценка записи, необходимая для включения её в сводку по теме" + summary_posts_required: "Минимальное количество записей, необходимое для получения сводки по теме. Изменения этого параметра будут применены к темам за прошедшую неделю." summary_likes_required: "Минимальное количество лайков, необходимое для получения сводки по теме. Изменения этого параметра будут применены к темам за прошедшую неделю." - summary_percent_filter: "При получении сводки по теме отображать % обсуждаемых сообщений" - summary_max_results: "Максимальное количество сообщений, отображаемых в сводке по теме" + summary_percent_filter: "Процент самых обсуждаемых записей для сводки по теме" + summary_max_results: "Максимальное количество записей, отображаемых в сводке по теме" summary_timeline_button: "Отображать кнопку «Сводка» рядом со шкалой времени" - enable_personal_messages: "УСТАРЕЛО: вместо этого параметра используйте параметр «personal message enabled groups» . Разрешать пользователям уровня доверия 1 (настраивается через минимальный уровень доверия для отправки сообщений) создавать сообщения и отвечать на них. Обратите внимание, что сотрудники всегда могут отправлять сообщения, несмотря на настроенный уровень доверия." - personal_message_enabled_groups: "Разрешить пользователям в этих группах создавать сообщения и отвечать на сообщения. Группы уровней доверия включают в себя все уровни доверия выше указанного числа, например, выбор 1-го уровня доверия также позволит пользователям 2, 3, и 4-го уровня доверия отправлять личные сообщения. Заметьте, что сотрудники форума всегда могут отправлять личные сообщения, независимо от того, какой уровень доверия им назначен." - enable_system_message_replies: "Позволять пользователям отвечать на системные сообщения, даже если личные сообщения отключены" - enable_chunked_encoding: "Разрешить фрагментацию сообщений (chunked encoding) на сервере. Эта функция работает в большинстве случаев, однако некоторые прокси могут буферизировать контент, вызывая задержку ответов" - long_polling_base_url: "Базовый URL, используемый для long polling (при использовании CDN для раздачи динамического контента установите в этом параметре адрес origin pull), например: http://origin.site.com" - polling_interval: "Если не используется long polling, как часто следует вошедшим клиентам опрашивать сервер, в миллисекундах" - anon_polling_interval: "Как часто следует анонимным клиентам опрашивать сервер, в миллисекундах" - background_polling_interval: "Как часто следует клиентам опрашивать сервер, в миллисекундах (когда окно находится в фоновом режиме)" - hide_post_sensitivity: "Вероятность того, что сообщение, на которое пожаловались, будет скрыто" + enable_personal_messages: "УСТАРЕЛО: вместо этого параметра используйте параметр «personal message enabled groups». Разрешать пользователям уровня доверия 1 (настраивается через минимальный уровень доверия для отправки сообщений) создавать сообщения и отвечать на них. Персонал может отправлять сообщения независимо от уровня доверия." + personal_message_enabled_groups: "Разрешить пользователям в этих группах создавать сообщения и отвечать на сообщения. Группы уровней доверия включают в себя все уровни доверия выше указанного числа, например, выбор 1-го уровня также позволит пользователям 2, 3, и 4-го уровня доверия отправлять личные сообщения. Персонал может отправлять сообщения независимо от уровня доверия." + enable_system_message_replies: "Разрешить пользователям отвечать на системные сообщения, даже если личные сообщения отключены" + enable_chunked_encoding: "Разрешить фрагментацию сообщений («chunked encoding») на сервере. Эта функция работает в большинстве случаев, однако некоторые прокси могут буферизировать контент, вызывая задержку ответов." + long_polling_base_url: "Базовый URL, используемый для «long polling» (при использовании CDN для раздачи динамического контента установите в этом параметре адрес «origin pull»), например: http://origin.site.com" + polling_interval: "Если не используется «long polling», как часто следует вошедшим клиентам опрашивать сервер (в миллисекундах)" + anon_polling_interval: "Как часто следует анонимным клиентам опрашивать сервер (в миллисекундах)" + background_polling_interval: "Как часто следует клиентам опрашивать сервер, в миллисекундах (когда окно в фоновом режиме)" + hide_post_sensitivity: "Вероятность того, что запись, на которую пожаловались, будет скрыта" silence_new_user_sensitivity: "Вероятность того, что новый пользователь, оценённый как спамер, будет заблокирован" auto_close_topic_sensitivity: "Вероятность того, что тема, на которую пожаловались, будет автоматически закрыта" - cooldown_minutes_after_hiding_posts: "Количество минут, в течение которых сообщение, скрытое из-за жалоб, не может быть отредактировано автором" - max_topics_in_first_day: "Максимальное количество тем, которое пользователь может создать в течение 24 часов с момента создания своего первого сообщения" - max_replies_in_first_day: "Максимальное количество ответов, которое пользователь может сделать в течение 24 часов с момента создания своего первого сообщения" + cooldown_minutes_after_hiding_posts: "Количество минут, в течение которых запись, скрытая из-за жалоб, не может быть отредактирована автором" + max_topics_in_first_day: "Максимальное количество тем, которое пользователь может создать в течение 24 часов с момента создания своего первой записи" + max_replies_in_first_day: "Максимальное количество ответов, которое пользователь может отправить в течение 24 часов с момента создания своей первой записи" tl2_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для уровня доверия 2 (участник), умножив его на указанное здесь число" tl3_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для уровня доверия 3 (активный пользователь), умножив его на указанное здесь число" tl4_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для уровня доверия 4 (лидер), умножив его на указанное здесь число" @@ -1598,9 +1594,9 @@ ru: tl2_additional_flags_per_day_multiplier: "Увеличить лимит жалоб в день для уровня доверия 2 (участник), умножив его на указанное здесь число" tl3_additional_flags_per_day_multiplier: "Увеличить лимит жалоб в день для уровня доверия 3 (активный пользователь), умножив его на указанное здесь число" tl4_additional_flags_per_day_multiplier: "Увеличить лимит жалоб в день для уровня доверия 4 (лидер), умножив его на указанное здесь число" - num_users_to_silence_new_user: "Если сообщения нового пользователя получают num_spam_flags_to_silence_new_user жалоб от указанного здесь количества пользователей, скрыть все его сообщения и предотвратить будущие публикации. Для отключения этого параметра установите значение в 0." - num_tl3_flags_to_silence_new_user: "Если сообщения нового пользователя получают указанное здесь количество жалоб от num_tl3_users_to_silence_new_user разных пользователей уровня доверия 3, скрыть все его сообщения и предотвратить будущие публикации. Для отключения этого параметра установите значение в 0." - num_tl3_users_to_silence_new_user: "Если сообщения нового пользователя получают num_tl3_flags_to_silence_new_user жалоб от указанного здесь количества пользователей уровня доверия 3, скрыть все его сообщения и предотвратить будущие публикации. Для отключения этого параметра установите значение в 0." + num_users_to_silence_new_user: "Если записи нового пользователя получат «num_spam_flags_to_silence_new_user» жалоб от указанного здесь количества пользователей, то все его записи будут скрыты и он не сможет больше публиковать. Для отключения этого параметра установите значение в 0." + num_tl3_flags_to_silence_new_user: "Если записи нового пользователя получают указанное здесь количество жалоб от «num_tl3_users_to_silence_new_user» разных пользователей уровня доверия 3, то все его записи будут скрыты и он не сможет больше публиковать. Для отключения этого параметра установите значение в 0." + num_tl3_users_to_silence_new_user: "Если сообщения нового пользователя получают «num_tl3_flags_to_silence_new_user» жалоб от указанного здесь количества пользователей уровня доверия 3, то все его записи будут скрыты и он не сможет больше публиковать. Для отключения этого параметра установите значение в 0." notify_mods_when_user_silenced: "Отправлять сообщение всем модераторам, если пользователь автоматически заблокирован." flag_sockpuppets: "Если новый пользователь отвечает на тему с того же IP-адреса, что и пользователь, создавший тему, пометить обе его публикации как потенциальный спам." traditional_markdown_linebreaks: "Использовать стандартный способ переноса строки в Markdown: строка должна заканчиваться двумя пробелами." @@ -1608,160 +1604,160 @@ ru: enable_markdown_linkify: "Автоматически отображать текст, который выглядит как ссылка, в виде ссылки: www.example.com и https://example.com будут автоматически отображены в виде ссылок" markdown_linkify_tlds: "Список доменов верхнего уровня, которые автоматически обрабатываются как ссылки" markdown_typographer_quotation_marks: "Список пар замены двойных и одинарных кавычек" - post_undo_action_window_mins: "Количество минут, в течение которых пользователь может отменить действия, связанные с сообщениями (лайк, жалоба на запись и т. д.)" + post_undo_action_window_mins: "Количество минут, в течение которых пользователь может отменить действия, связанные с записями (лайк, жалоба и т. д.)" must_approve_users: "Персонал должен подтвердить регистрацию новых пользователей перед тем, как им будет разрешён доступ к сайту." invite_code: "Пользователь должен ввести этот код для регистрации аккаунта (без учёта регистра)" - approve_suspect_users: "Добавлять подозрительных пользователей в очередь премодерации. Пользователи с подозрительной активностью имеют доступ к своему профилю, но не могут читать сообщения." - review_every_post: "Проверять каждое сообщение. ПРЕДУПРЕЖДЕНИЕ! НЕ РЕКОМЕНДУЕТСЯ ВКЛЮЧАТЬ ЭТОТ ПАРАМЕТР НА ЗАГРУЖЕННЫХ САЙТАХ." + approve_suspect_users: "Добавлять подозрительных пользователей в очередь проверки. Подозрительными считаются пользователи, которые указали информацию о себе, но ничего не читали." + review_every_post: "Проверять каждую запись. ВНИМАНИЕ! НЕ РЕКОМЕНДУЕТСЯ ВКЛЮЧАТЬ ЭТОТ ПАРАМЕТР НА ЗАГРУЖЕННЫХ САЙТАХ." pending_users_reminder_delay_minutes: "Уведомлять модераторов, если новые пользователи ждут одобрения больше, чем указанное здесь количество минут. Установите значение в «-1» для отключения уведомлений." - persistent_sessions: "Пользователи остаются авторизованными после закрытия веб-браузера" + persistent_sessions: "Пользователи остаются авторизованными после закрытия браузера" maximum_session_age: "Пользователь остаётся в системе в течение указанного здесь количества часов с момента последнего посещения" ga_version: "Используемая версия Google Universal Analytics: v3 (analytics.js), v4 (gtag)" ga_universal_tracking_code: "Идентификатор отслеживания Google Universal Analytics, например: UA-12345678-9; см. https://google.com/analytics" ga_universal_domain_name: "Доменное имя для Google Universal Analytics, например: mysite.com; см. https://google.com/analytics" ga_universal_auto_link_domains: "Включить междоменное отслеживание Google Universal Analytics. К исходящим ссылкам на эти домены будет добавлен идентификатор клиента. См. Как настроить междоменное отслеживание." - gtm_container_id: "Идентификатор менеджера тегов Google, например: GTM-ABCDEF.
    Примечание. Сторонние скрипты, загруженные GTM, могут быть внесены в белый список в «скрипте политики безопасности контента src»." - enable_escaped_fragments: "Обратитесь к Google Ajax-Crawling API, если веб-сканер не обнаружен. См. https://developers.google.com/webmasters/ajax-crawling/docs/learn-more." - moderators_manage_categories_and_groups: "Разрешать модераторам создавать разделы / группы и управлять ими" - moderators_change_post_ownership: "Разрешать модераторам менять владельца сообщения" - cors_origins: "Разрешённые источники для кросс-доменных запросов (CORS). Каждый источник должен содержать http:// или https://. Для включения CORS переменная окружения DISCOURSE_ENABLE_CORS должна быть установлена в значение «true»." - use_admin_ip_allowlist: "Администраторы могут войти в систему только в том случае, если их IP-адрес указан в белом списке (Админка > Логи > IP-адреса)." + gtm_container_id: "Идентификатор менеджера тегов Google, например: GTM-ABCDEF.
    Примечание. Сторонние скрипты, загруженные менеджером тегов, могут быть внесены в белый список в при помощи директивы `script-src` политики защиты контента (CSP)." + enable_escaped_fragments: "Если поисковый робот не обнаружен, переключаться на Google Ajax-Crawling API. См. https://developers.google.com/webmasters/ajax-crawling/docs/learn-more." + moderators_manage_categories_and_groups: "Разрешить модераторам создавать категории и группы и управлять ими" + moderators_change_post_ownership: "Разрешить модераторам менять владельца записи" + cors_origins: "Разрешённые источники для междоменных запросов (CORS). Каждый источник должен содержать http:// или https://. Для включения CORS переменная окружения DISCOURSE_ENABLE_CORS должна быть установлена в значение «true»." + use_admin_ip_allowlist: "Администраторы могут войти в систему только в том случае, если их IP-адрес в белом списке (Администрирование > Журналы > IP-адреса)." blocked_ip_blocks: "Список локальных диапазонов IP-адресов, которые никогда не должны сканироваться Discourse" allowed_internal_hosts: "Список внутренних хостов, которые Discourse может безопасно сканировать для преобразования ссылок в умные вставки и для других целей" allowed_onebox_iframes: "Список встроенных во фреймы доменов, контент с которых будет разрешено встраивать в OneBox. `*` — все механизмы OneBox по умолчанию." - allowed_iframes: "Список iframe src, которым Discourse может разрешить встраивание в сообщения." - allowed_crawler_user_agents: "Перечень поисковых ботов, которым должен быть разрешён доступ к сайту. ПРЕДУПРЕЖДЕНИЕ! УСТАНОВКА ЭТОГО ПАРАМЕТРА ЗАПРЕТИТ ДОСТУП ВСЕМ ПОИСКОВЫМ БОТАМ, НЕ УКАЗАННЫМ В ПЕРЕЧНЕ!" - blocked_crawler_user_agents: "Уникальное нечувствительное к регистру слово в строке User-Agent, идентифицирующее поисковых ботов, которым запрещён доступ к сайту. Не применяется, если определён белый список." - slow_down_crawler_user_agents: 'User agents, скорость работы которых должна быть ограничена в соответствии с настройкой «slow down crawler rate». Каждое значение должно состоять не менее чем из 3 символов.' - slow_down_crawler_rate: "Если указано значение slow_down_crawler_user_agents (количество секунд между запросами), то это значение будет применяться ко всем веб-сканерам" + allowed_iframes: "Список префиксов доменов iframe src, которым Discourse может разрешить встраивание в записи." + allowed_crawler_user_agents: "Перечень поисковых роботов, которым должен быть разрешён доступ к сайту. ВНИМАНИЕ! УСТАНОВКА ЭТОГО ПАРАМЕТРА ЗАПРЕТИТ ДОСТУП ВСЕМ ПОИСКОВЫМ БОТАМ, НЕ УКАЗАННЫМ В ПЕРЕЧНЕ!" + blocked_crawler_user_agents: "Уникальное нечувствительное к регистру слово в строке User-Agent, идентифицирующее поисковых роботов, которым запрещён доступ к сайту. Не применяется, если определён белый список." + slow_down_crawler_user_agents: 'Значения User Agent, для которых скорость работы должна быть ограничена в соответствии с настройкой «slow down crawler rate». Каждое значение должно состоять не менее чем из трех символов.' + slow_down_crawler_rate: "Если указано значение «slow_down_crawler_user_agents» (количество секунд между запросами), то это значение будет применяться ко всем поисковым роботам" content_security_policy: "Включить политику безопасности контента (CSP)" content_security_policy_report_only: "Включить только отчёт о политике безопасности контента (CSP)" content_security_policy_collect_reports: "Включить отчёты о нарушениях CSP в /csp_reports" - content_security_policy_frame_ancestors: "Ограничить через CSP число тех, кто может встроить этот сайт в iframes. Перечень разрешённых хостов настраивается на закладке Встраивание" + content_security_policy_frame_ancestors: "Ограничить через CSP число тех, кто может встроить этот сайт в iframes. Перечень разрешённых хостов настраивается на закладке Встраивание." content_security_policy_script_src: "Дополнительные источники сценариев в белом списке. Текущий хост и CDN включены по умолчанию. См. Mitigate XSS Attacks with Content Security Policy." invalidate_inactive_admin_email_after_days: "Аккаунты администраторов, которые не посещали сайт в течение указанного здесь количества дней, должны будут повторно подтвердить свой адрес электронной почты перед входом в систему. Для отключения параметра установите это значение в 0." - top_menu: "Укажите, какие элементы навигации должны располагаться на главной странице и в какой последовательности. Пример: latest|new|unread|categories|top|read|posted|bookmarks" - post_menu: "Укажите, какие элементы должны отображаться под сообщением и в какой последовательности. Пример: like|edit|flag|delete|share|bookmark|reply" - post_menu_hidden_items: "Пункты меню действий над сообщением, которые по умолчанию спрятаны и появляются по нажатию на кнопку «Показать ещё»." + top_menu: "Укажите, какие элементы навигации должны располагаться на главной странице и в какой последовательности. Пример: latest|new|unread|categories|top|read|posted|bookmarks." + post_menu: "Укажите, какие элементы должны отображаться под записью и в какой последовательности. Пример: like|edit|flag|delete|share|bookmark|reply." + post_menu_hidden_items: "Пункты меню действий над записью, которые по умолчанию скрыты и появляются по нажатию на кнопку «Показать ещё»." share_links: "Укажите, какие элементы должны отображаться в окне «Поделиться» и в какой последовательности." - allow_username_in_share_links: "Разрешить включать имена пользователей в общие ссылки. Может быть полезно для награждения за привлечение уникальных посетителей." + allow_username_in_share_links: "Разрешить включать имена пользователей в ссылки доступа. Может быть полезно для награждения за привлечение уникальных посетителей." site_contact_username: "Все автоматические сообщения будут отправляться от имени указанного здесь пользователя. Если оставить поле пустым, то будет использован системный пользователь по умолчанию — System." site_contact_group_name: "Допустимое имя группы для приглашения ко всем автоматическим сообщениям." send_welcome_message: "Отправлять всем новым пользователям приветственное сообщение с коротким описанием возможностей форума." send_tl1_welcome_message: "Отправлять всем новым пользователям с уровнем доверия 1 приветственное сообщение." send_tl2_promotion_message: "Отправлять новым пользователям с уровня доверия 2 сообщение о повышении." - suppress_reply_directly_below: "Не показывать разворачиваемое количество ответов на запись, если есть всего лишь один ответ непосредственно под сообщением." - suppress_reply_directly_above: "Не показывать разворачиваемый блок «в ответ на» для сообщения, если есть всего лишь одно сообщение непосредственно выше." - remove_full_quote: "Автоматически удалять цитату, если (а) цитата расположена в начале сообщения, (б) всё сообщение состоит из цитаты и (в) цитата взята из непосредственно предшествующего сообщения. Подробнее см. тему Removal of full quotes from direct replies" - suppress_reply_when_quoting: "Не показывать разворачиваемый блок «в ответ на», если сообщение уже содержит цитату." + suppress_reply_directly_below: "Не показывать разворачиваемое количество ответов на запись, если есть всего лишь один ответ непосредственно под записью." + suppress_reply_directly_above: "Не показывать разворачиваемый блок «в ответ на» для записи, если есть всего лишь одна запись непосредственно выше." + remove_full_quote: "Автоматически удалять цитату, если (а) она расположена в начале записи, (б) цитируется вся запись и (в) цитата взята из непосредственно предшествующей записи. Подробнее см. тему Removal of full quotes from direct replies." + suppress_reply_when_quoting: "Не показывать разворачиваемый блок «в ответ на», если запись уже содержит цитату." max_reply_history: "Максимальное число разворачивающихся ответов в блоке «в ответ на»" topics_per_period_in_top_summary: "Количество тем в разделе «Обсуждаемые», отображаемое по умолчанию." - topics_per_period_in_top_page: "Количество тем в разделе «Обсуждаемые», отображаемое дополнительно при скроллинге до конца страницы." - redirect_users_to_top_page: "Автоматически перенаправлять новых и давно отсутствовавших пользователей в секцию «Обсуждаемые»." + topics_per_period_in_top_page: "Количество тем в разделе «Обсуждаемые», отображаемое дополнительно при прокрутке до конца страницы." + redirect_users_to_top_page: "Автоматически перенаправлять новых и давно отсутствовавших пользователей в раздел «Обсуждаемые»." top_page_default_timeframe: "Стандартные периоды времени для отображения тем в разделе «Обсуждаемые»." - moderators_view_emails: "Разрешать модераторам просматривать электронные сообщения пользователей" - prioritize_username_in_ux: "Показывать имя пользователя первым на странице пользователя, карточке пользователя и сообщениях (когда отключённое имя показывается первым)" - enable_rich_text_paste: "Включить автоматическое преобразование HTML в Markdown при вставке текста в редактор. (Экспериментально)" + moderators_view_emails: "Разрешать модераторам просматривать электронные письма пользователей" + prioritize_username_in_ux: "Показывать имя пользователя первым на странице пользователя, карточке пользователя и записях (когда отключённое имя показывается первым)" + enable_rich_text_paste: "Включить автоматическое преобразование HTML в Markdown при вставке текста в редактор. (Экспериментальная функция.)" send_old_credential_reminder_days: "Напомнить о старых учётных данных через указанное количество дней" email_token_valid_hours: "Ссылка на восстановление пароля / активацию учётной записи будет действовать в течение указанного здесь количества часов." - enable_badges: "Включить систему значков" - max_favorite_badges: "Максимальное количество значков, которое может выбрать пользователь" + enable_badges: "Включить систему наград" + max_favorite_badges: "Максимальное количество наград, которое может выбрать пользователь" whispers_allowed_groups: "Разрешать участникам указанных групп создавать скрытые сообщения в темах." - allow_index_in_robots_txt: "Укажите в файле robots.txt, что этот сайт может быть проиндексирован поисковыми системами. В исключительных случаях вы можете навсегда переопределить файл robots.txt." - blocked_email_domains: "Перечень почтовых доменов, разделённых вертикальной чертой, с которых запрещена регистрация аккаунтов. Пример: mailinator.com|trashmail.net" - allowed_email_domains: "Перечень почтовых доменов, разделённых вертикальной чертой, с которых ДОЛЖНА производиться регистрация аккаунтов. ВНИМАНИЕ: Пользователям других почтовых доменов регистрация будет недоступна!" - normalize_emails: "Проверять нормализованную электронную почту на уникальность. При нормализации из адреса электронной почты удаляются все точки из имени пользователя и символ «+»." + allow_index_in_robots_txt: "Укажите в файле robots.txt, что этот сайт может быть проиндексирован поисковыми системами. В исключительных случаях можно навсегда переопределить файл robots.txt." + blocked_email_domains: "Перечень почтовых доменов, разделённых вертикальной чертой, с которых запрещена регистрация аккаунтов. Пример: mailinator.com|trashmail.net." + allowed_email_domains: "Перечень почтовых доменов, разделённых вертикальной чертой, с которых ДОЛЖНА производиться регистрация аккаунтов. ВНИМАНИЕ: пользователям других почтовых доменов регистрация будет недоступна!" + normalize_emails: "Проверять нормализованную электронную почту на уникальность. При нормализации из адреса электронной почты удаляются точки из имени пользователя и символы между «+» и «@»." auto_approve_email_domains: "Пользователи с адресами электронной почты из этого списка доменов будут автоматически одобрены." - hide_email_address_taken: "Не сообщать пользователям о существовании учётной записи с указанным адресом электронной почты при регистрации или при восстановлении забытого пароля . Запрашивать полный адрес электронной почты при восстановлении забытого пароля." - log_out_strict: "Когда пользователь выходит из системы, закрывать все сессии на всех устройствах пользователя." + hide_email_address_taken: "Не сообщать пользователям о существовании аккаунтом с указанным адресом электронной почты при регистрации или восстановлении забытого пароля. Запрашивать полный адрес электронной почты при восстановлении забытого пароля." + log_out_strict: "Когда пользователь выходит из системы, закрывать все сеансы на всех устройствах пользователя." version_checks: "Проверять обновления на Discourse Hub и отображать сообщения о новых версиях в панели администратора" - new_version_emails: "Отправлять сообщение на адрес contact_email, когда будут доступны новые версии Discourse." + new_version_emails: "Отправлять сообщение на адрес «contact_email», когда будут доступны новые версии Discourse." invite_expiry_days: "Количество дней, в течение которых высланные приглашённому пользователю ключи являются действительными." invite_only: "Все новые пользователи должны явно приглашаться доверенными пользователями или сотрудниками. Публичная регистрация отключена." - login_required: "Требовать авторизации для доступа к сайту, анонимный доступ запрещён." - min_username_length: "Минимальная длина имени пользователя в символах. ВНИМАНИЕ: если у каких-либо существующих пользователей или групп имена будут короче этого значения, ваш сайт не будет работать корректно!" - max_username_length: "Максимальная длина имени пользователя в символах. ВНИМАНИЕ: если у существующих пользователей или групп имена будут длиннее этого значения, ваш не будет работать корректно!" + login_required: "Требовать авторизации для доступа к сайту. Анонимный доступ запрещён." + min_username_length: "Минимальная длина имени пользователя в символах. ВНИМАНИЕ: если у существующих пользователей или групп имена будут короче этого значения, сайт не будет работать корректно!" + max_username_length: "Максимальная длина имени пользователя в символах. ВНИМАНИЕ: если у существующих пользователей или групп имена будут длиннее этого значения, сайт не будет работать корректно!" unicode_usernames: "Разрешать буквы и цифры Unicode в именах пользователей и названиях групп." allowed_unicode_username_characters: "Регулярное выражение, позволяющее использовать только некоторые символы Юникода в именах пользователей. Буквы и цифры ASCII всегда будут разрешены и могут не включаться в белый список." reserved_usernames: "Имена, которые нельзя использовать при регистрации на форуме. Подстановочный знак «*» — совпадение с любым символом ноль или более раз." - min_password_length: "Минимальная длина пароля" + min_password_length: "Минимальная длина пароля." min_admin_password_length: "Минимальная длина пароля для администратора." - password_unique_characters: "Минимальное количество уникальных символов, которое должен иметь пароль." - block_common_passwords: "Не позволять использовать пароли из списка 10 000 самых часто используемых паролей." - auth_skip_create_confirm: При регистрации через внешнюю авторизацию пропустить всплывающее окно создания учётной записи. Лучше всего использовать вместе с параметрами auth_overrides_email, auth_overrides_username и auth_overrides_name. - auth_immediately: "Автоматически перенаправлять на внешнюю систему входа без вмешательства пользователя. Настройка сработает только в том случае, если параметр login_required установлен в «true», и используется только один внешний метод аутентификации." + password_unique_characters: "Минимальное количество уникальных символов в пароле." + block_common_passwords: "Не разрешать пароли из списка 10 000 самых часто используемых." + auth_skip_create_confirm: При регистрации через внешнюю авторизацию пропустить всплывающее окно создания аккаунта. Лучше всего использовать вместе с параметрами «auth_overrides_email», «auth_overrides_username» и «auth_overrides_name». + auth_immediately: "Автоматически перенаправлять на внешнюю систему входа без вмешательства пользователя. Настройка сработает только в том случае, если параметр «login_required» установлен в «true», и используется только один внешний метод аутентификации." enable_discourse_connect: "Включить вход через DiscourseConnect (прежнее название — «Discourse SSO») (ВНИМАНИЕ: АДРЕСА ЭЛЕКТРОННОЙ ПОЧТЫ ПОЛЬЗОВАТЕЛЕЙ *ДОЛЖНЫ* ПРОВЕРЯТЬСЯ НА ВНЕШНЕМ САЙТЕ!)" verbose_discourse_connect_logging: "Записывать подробную диагностику, связанную с DiscourseConnect, в файл журнала" - enable_discourse_connect_provider: "Использовать протокол единого входа DiscourseConnect (прежнее название — «Discourse SSO»). Необходимо настроить параметр «discourse_connect_provider_secrets»" + enable_discourse_connect_provider: "Использовать протокол единого входа DiscourseConnect (прежнее название — «Discourse SSO»). Необходимо настроить параметр «discourse_connect_provider_secrets»." discourse_connect_url: "URL-адрес DiscourseConnect (должен содержать http:// или https://)" - discourse_connect_secret: "Секретный набор символов, используемый для проверки подлинности зашифрованного входа с помощью DiscourseConnect, убедитесь, что это 10 или более символов" - discourse_connect_provider_secrets: "Список секретных доменов, которые используют DiscourseConnect. Убедитесь, что это 10 или более символов. Может быть использован символ звёздочки для соответствия любому домену или части домена (пример: *.example.com)" + discourse_connect_secret: "Секретный набор символов, используемый для проверки подлинности зашифрованного входа с помощью DiscourseConnect. Минимум 10 символов." + discourse_connect_provider_secrets: "Список секретных доменов, которые используют DiscourseConnect. Минимум 10 символов. Символ «*» — любой домен или часть домена (пример: *.example.com)." discourse_connect_overrides_bio: "Перезаписывать в профиле информацию о пользователе с последующим запретом на её редактирование" - discourse_connect_overrides_groups: "Перезаписывать все группы, указанные вручную, группами, указанными в соответствующем групповом атрибуте (ВНИМАНИЕ: Если группы не указаны, то список групп пользователя будет очищен)" - auth_overrides_email: "Перезаписывать локальную электронную почту электронной почтой внешнего сайта при каждом входе в систему и запретить её редактирование. Относится ко всем провайдерам аутентификации. (ВНИМАНИЕ: из-за этого могут возникать расхождения)" - auth_overrides_username: "Перезаписывать локальное имя пользователя пользователем внешнего сайта при каждом входе в систему и запретить его редактирование. Относится ко всем провайдерам аутентификации. (ВНИМАНИЕ: из-за этого могут возникать расхождения)" - auth_overrides_name: "Перезаписывать локальное полное имя пользователя полным именем пользователем внешнего сайта при каждом входе в систему и запретить его редактирование. Относится ко всем провайдерам аутентификации." + discourse_connect_overrides_groups: "Перезаписывать все группы, указанные вручную, группами, указанными в соответствующем групповом атрибуте (ВНИМАНИЕ: если группы не указаны, то список групп пользователя будет очищен)" + auth_overrides_email: "Перезаписывать локальный адрес электронной почты адресом внешнего сайта при каждом входе в систему и запретить редактирование. Относится ко всем провайдерам аутентификации. (ВНИМАНИЕ: из-за этого могут возникать расхождения.)" + auth_overrides_username: "Перезаписывать локальное имя пользователя пользователем внешнего сайта при каждом входе в систему и запретить редактирование. Относится ко всем провайдерам аутентификации. (ВНИМАНИЕ: могут возникать расхождения из-за требований к имени пользователя.)" + auth_overrides_name: "Перезаписывать локальное полное имя полным именем внешнего сайта при каждом входе в систему и запретить редактирование. Относится ко всем провайдерам аутентификации." discourse_connect_overrides_avatar: "Перезаписывать локальный аватар значением, используемым в DiscourseConnect. Если этот параметр включен, пользователям не будет разрешено загружать аватары на Discourse." discourse_connect_overrides_location: "Перезаписывать расположение пользователя данными с DiscourseConnect и запретить их изменение." discourse_connect_overrides_website: "Перезаписывать веб-сайт пользователя данными с DiscourseConnect и запретить их изменение." discourse_connect_overrides_profile_background: "Перезаписывать фон шапки профиля значением, используемым в DiscourseConnect." discourse_connect_overrides_card_background: "Перезаписывать фон карточки пользователя значением, используемым в DiscourseConnect." discourse_connect_not_approved_url: "Перенаправлять неподтверждённые аккаунты DiscourseConnect на этот URL" - discourse_connect_allows_all_return_paths: "Не вводить ограничения для параметра return_paths на доменное имя, предоставляемое DiscourseConnect (по умолчанию возврат должен осуществляться на текущий сайт)" + discourse_connect_allows_all_return_paths: "Не вводить ограничения для параметра «return_paths» на доменное имя, предоставляемое DiscourseConnect (по умолчанию возврат должен осуществляться на текущий сайт)" enable_local_logins: "Включить локальные аккаунты на основе имени пользователя и пароля. ВНИМАНИЕ: если параметр отключён, вы не сможете войти в систему, если ранее не был настроен хотя бы один альтернативный метод входа." enable_local_logins_via_email: "Разрешать пользователям запрашивать получение ссылки для входа в систему по электронной почте." - allow_new_registrations: "Разрешать регистрацию новых пользователей. Отключите этот параметр, если вы хотите запретить кому-либо создавать новые аккаунты." + allow_new_registrations: "Разрешать регистрацию новых пользователей. Отключите этот параметр, если вы хотите запретить создавать новые аккаунты." enable_signup_cta: "Показывать уведомление возвращающимся на форум анонимным пользователям, предложив им зарегистрировать аккаунт." enable_google_oauth2_logins: "Включить аутентификацию Google Oauth2. Это метод аутентификации, который в настоящее время поддерживает Google. Требуется ключ и секрет. См. тему Configuring Google login for Discourse." - google_oauth2_client_id: "Client ID для Google-приложения." - google_oauth2_client_secret: "Client secret для Google-приложения." - google_oauth2_prompt: "Необязательный список строковых значений, разделенных пробелами, который указывает, запрашивает ли сервер авторизации у пользователя повторную аутентификацию и согласие. См. этот раздел для указания возможных значений." - google_oauth2_hd: "Необязательный домен, указанный в G Suite, доступ к которому будет ограничен. См. этот раздел для получения дополнительной информации." - google_oauth2_hd_groups: "(экспериментально) Получение групп Google пользователей в размещенном домене при аутентификации. Полученные группы Google можно использовать для предоставления автоматического членства в группе Discourse (см. настройки группы). Для получения дополнительной информации см. https://meta.discourse.org/t/226850." - google_oauth2_hd_groups_service_account_admin_email: "Адрес электронной почты, принадлежащий учётной записи администратора Google Workspace. Будет использоваться вместе со служебной учетной записью для получения информации о группе." + google_oauth2_client_id: "Идентификатор клиента для приложения Google" + google_oauth2_client_secret: "Секрет клиента для приложения Google" + google_oauth2_prompt: "Необязательный список строковых значений, разделенных пробелами, который указывает, запрашивает ли сервер авторизации у пользователя повторную аутентификацию и согласие. Возможные значения см. в этом разделе." + google_oauth2_hd: "Необязательный домен, указанный в G Suite, доступ к которому будет ограничен. Подробнее см. в этом разделе." + google_oauth2_hd_groups: "(Экспериментальная функция.) Получение групп Google пользователей в размещенном домене при аутентификации. Полученные группы Google можно использовать для предоставления автоматического членства в группе Discourse (см. настройки группы). Подробнее: https://meta.discourse.org/t/226850." + google_oauth2_hd_groups_service_account_admin_email: "Адрес электронной почты, принадлежащий аккаунту администратора Google Workspace. Будет использоваться вместе со служебным аккаунтом для получения информации о группе." google_oauth2_hd_groups_service_account_json: "Ключевая информация в формате JSON для служебного аккаунта. Будет использоваться для получения информации о группе." - enable_twitter_logins: "Включить аутентификацию Twitter, требуются twitter_consumer_key и twitter_consumer_secret. См. тему Configuring Twitter login (and rich embeds) for Discourse." - twitter_consumer_key: "Ключ пользователя для аутентификации в Twitter, зарегистрированный в https://developer.twitter.com/apps" - twitter_consumer_secret: "Секретный номер для проверки подлинности Twitter, зарегистрированный в https://developer.twitter.com/apps" - enable_facebook_logins: "Включите аутентификацию Facebook, требуются facebook_app_id и facebook_app_secret. См. Настройка входа в Facebook для Discourse." - facebook_app_id: "Идентификатор приложения для аутентификации и обмена Facebook, зарегистрирован на https://developers.facebook.com/apps" - facebook_app_secret: "Секрет приложения для аутентификации Facebook, зарегистрированный по адресу https://developers.facebook.com/apps" - enable_github_logins: "Включить аутентификацию GitHub, требуются github_client_id и github_client_secret. См. тему Configuring GitHub login for Discourse." - github_client_id: "Идентификатор клиента для аутентификации GitHub, зарегистрированный в https://github.com/settings/developers" - github_client_secret: "Секрет клиента для аутентификации GitHub, зарегистрированный по адресу https://github.com/settings/developers" + enable_twitter_logins: "Включить аутентификацию Twitter, требуются «twitter_consumer_key» и «twitter_consumer_secret». См. тему Configuring Twitter login (and rich embeds) for Discourse." + twitter_consumer_key: "Ключ пользователя для аутентификации в Twitter, зарегистрированный в https://developer.twitter.com/apps." + twitter_consumer_secret: "Секретный номер для проверки подлинности Twitter, зарегистрированный в https://developer.twitter.com/apps." + enable_facebook_logins: "Включить аутентификацию Facebook, требуются «facebook_app_id» и «facebook_app_secret». См. раздел Настройка входа в Facebook для Discourse." + facebook_app_id: "Идентификатор приложения для аутентификации и публикации на Facebook, зарегистрированный на https://developers.facebook.com/apps." + facebook_app_secret: "Секрет приложения для аутентификации Facebook, зарегистрированный по адресу https://developers.facebook.com/apps." + enable_github_logins: "Включить аутентификацию GitHub, требуются «github_client_id» и «github_client_secret». См. тему Настройка входа на GitHub для Discourse." + github_client_id: "Идентификатор клиента для аутентификации GitHub, зарегистрированный на https://github.com/settings/developers." + github_client_secret: "Секрет клиента для аутентификации GitHub, зарегистрированный на https://github.com/settings/developers." enable_discord_logins: "Разрешать пользователям проходить аутентификацию с использованием Discord?" - discord_client_id: 'Discord Client ID (Нужен ID? Посетите портал разработчиков Discord)' - discord_secret: "Discord Secret Key" - discord_trusted_guilds: 'Разрешать вход в систему через Discord только членам этих гильдий. Используйте числовой идентификатор гильдии. Для получения дополнительной информации ознакомьтесь с этими инструкциями. Оставьте поле пустым, если необходимо разрешить все гильдии.' + discord_client_id: 'Идентификатор клиента Discord (его можно получить на портале разработчиков Discord)' + discord_secret: "Секретный ключ Discord" + discord_trusted_guilds: 'Разрешать вход в систему через Discord только членам этих гильдий. Используйте числовой идентификатор гильдии. Подробнее — в здесь. Чтобы разрешить все гильдии, оставьте поле пустым.' enable_backups: "Разрешить администраторам создавать резервные копии форума" - allow_restore: "Разрешить восстановление, которое может заменить ВСЕ данные сайта. Оставьте опцию выключенной, если не планируете делать восстановление из резервной копии" + allow_restore: "Разрешить восстановление, которое может заменить ВСЕ данные сайта. Оставьте опцию выключенной, если не планируете делать восстановление из резервной копии." automatic_backups_enabled: "Запускать автоматическое создание резервных копий с указанной в настройках периодичностью" backup_frequency: "Периодичность создания резервных копий (в днях)." s3_backup_bucket: "Адрес папки удалённого сервера для резервных копий. ВНИМАНИЕ! Убедитесь, что место назначения защищено от посторонних." s3_endpoint: "Конечная точка для резервного копирования может быть изменена на другую S3-совместимую службу, такую как DigitalOcean Spaces или Minio. ВНИМАНИЕ! Оставьте поле пустым, если используется AWS S3." s3_configure_tombstone_policy: "Включить политику автоматического удаления загрузок, помеченных как «удалённые». ВАЖНО! Если этот параметр отключён, удалённые загрузки будут фактически занимать место на диске." s3_disable_cleanup: "Не допускать удаление старых резервных копий из S3, когда резервных копий больше, чем максимально допустимо." - enable_s3_inventory: "Создавать отчёты и проверять загрузку с помощью инвентаря Amazon S3. ВАЖНО: требуется правильные настройки S3 (access key id и secret access key)." + enable_s3_inventory: "Создавать отчёты и проверять загрузку с помощью инвентаря Amazon S3. ВАЖНО: требуются действительные учётные данные S3 (идентификатор ключа доступа и секретный ключ доступа)." backup_time_of_day: "Время создания резервной копии (UTC)." backup_with_uploads: "Сохранять в резервной копии все загружаемые файлы. В противном случае будет сохраняться только база данных." - backup_location: "Место, где хранятся резервные копии. ВАЖНО: S3 требует действительные учётные данные S3, указанные в настройках." + backup_location: "Место, где хранятся резервные копии. ВАЖНО: для S3 нужно указать действительные учётные данные в настройках файлов." backup_gzip_compression_level_for_uploads: "Уровень gzip-сжатия для загружаемых файлов." include_thumbnails_in_backups: "Включать создаваемые эскизы в резервные копии. Отключение этого параметра сделает резервные копии меньше, но потребует обновления всех сообщений после восстановления." active_user_rate_limit_secs: "Частота обновления поля «last_seen_at», в секундах" verbose_localization: "Показывать ключи используемых строк в интерфейсе для перевода на другой язык" - previous_visit_timeout_hours: "Как долго должно длиться посещение сайта, чтобы мы посчитали его «предыдущим посещением», в часах" + previous_visit_timeout_hours: "Какое время должно пройти, чтобы посещение посчитали «предыдущим», в часах" top_topics_formula_log_views_multiplier: "значение множителя просмотров журнала (n) в формуле топ-тем: `log(views_count) * (n) + op_likes_count * 0.5 + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" - top_topics_formula_first_post_likes_multiplier: "значение первого поста множитель лайков (n) в формуле топ-тем: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" - top_topics_formula_least_likes_per_post_multiplier: "значение наименьшего количества лайков на множитель поста (n) в формуле топ-тем: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`" + top_topics_formula_first_post_likes_multiplier: "значение множителя лайков (n) для первой записи в формуле топ-тем: `log(views_count) * 2 + op_likes_count * (n) + LEAST(likes_count / posts_count, 3) + 10 + log(posts_count)`" + top_topics_formula_least_likes_per_post_multiplier: "значение множителя наименьшего количества лайков на запись (n) в формуле топ-тем: `log(views_count) * 2 + op_likes_count * 0.5 + LEAST(likes_count / posts_count, (n)) + 10 + log(posts_count)`" enable_safe_mode: "Разрешать пользователям входить в безопасный режим для отладки плагинов." rate_limit_create_topic: "Пользователи могут создавать новую тему после создания предыдущей только по прошествии указанного здесь количества секунд." rate_limit_create_post: "Пользователи могут создавать новое сообщение после создания предыдущего только по прошествии указанного здесь количества секунд." rate_limit_new_user_create_topic: "Новички могут создавать новую тему после создания предыдущей только по прошествии указанного здесь количества секунд." - rate_limit_new_user_create_post: "Новички могут создавать новое сообщение после создания предыдущего только по прошествии указанного здесь количества секунд." + rate_limit_new_user_create_post: "Новички могут создавать новую запись после создания предыдущей только по прошествии указанного здесь количества секунд." max_likes_per_day: "Максимальное количество лайков, которое пользователь может поставить за один день." max_flags_per_day: "Максимальное количество жалоб, которое пользователь может подать за один день." max_bookmarks_per_day: "Максимальное количество закладок, которое пользователь может создать за один день." - max_edits_per_day: "Максимальное количество редактирований, которое пользователь может выполнить за один день." + max_edits_per_day: "Максимальное количество правок, которое пользователь может выполнить за один день." max_topics_per_day: "Максимальное количество тем, которое пользователь может создать за один день." max_personal_messages_per_day: "Максимальное количество новых личных сообщений, которые пользователь может создать за день." max_invites_per_day: "Максимальное количество приглашений, которое пользователь может отправить за один день." @@ -1769,8 +1765,8 @@ ru: max_topic_invitations_per_minute: "Максимальное количество приглашений в тему, которое может отправить пользователь в течение одной минуты." max_logins_per_ip_per_hour: "Максимальное количество входов в систему с одного IP-адреса в течение часа" max_logins_per_ip_per_minute: "Максимальное количество входов в систему с одного IP-адреса в течение минуты" - max_post_deletions_per_minute: "Максимальное количество сообщений, которые пользователь может удалить в минуту. Установите значение в 0, чтобы отключить удаление сообщений." - max_post_deletions_per_day: "Максимальное количество сообщений, которые пользователь может удалить за день. Установите значение в 0, чтобы отключить удаление сообщений." + max_post_deletions_per_minute: "Максимальное количество записей, которые пользователь может удалить в минуту. Чтобы отключить удаление записей, установите значение в 0." + max_post_deletions_per_day: "Максимальное количество записей, которые пользователь может удалить за день. Чтобы отключить удаление записей, установите значение в 0." invite_link_max_redemptions_limit: "Максимум использований пригласительных ссылок." invite_link_max_redemptions_limit_users: "Максимальное допустимое количество использований пригласительных ссылок, созданных пользователями." alert_admins_if_errors_per_minute: "Количество ошибок в минуту, необходимое для предупреждения администратора. Для отключения этого параметра установите значение в 0. ВНИМАНИЕ: требуется перезапуск." @@ -1781,29 +1777,29 @@ ru: suggested_topics_max_days_old: "Похожие темы не должны быть старше указанного здесь количества дней." suggested_topics_unread_max_days_old: "Похожие непрочитанные темы не должны быть старше указанного здесь количества дней." clean_up_uploads: "Удалять неиспользуемые загрузки для предотвращения хранения нелегального контента. ВНИМАНИЕ: рекомендуется сделать резервную копию папки /uploads перед включением этой настройки." - clean_orphan_uploads_grace_period_hours: "Период (в часах) после которого неопубликованные вложения удаляются." - purge_deleted_uploads_grace_period_days: "Период (в днях) после которого удалённые вложения не могут быть восстановлены." - purge_unactivated_users_grace_period_days: "Количество дней до удаления пользователя, который не активировал аккаунт. Установите 0, чтобы никогда не удалять неактивированных пользователей." - enable_s3_uploads: "Размещать загружаемые файлы на Amazon S3. ВАЖНО: требуется правильные настройки S3 (access key id и secret access key)." + clean_orphan_uploads_grace_period_hours: "Период (в часах), после которого неопубликованные вложения удаляются." + purge_deleted_uploads_grace_period_days: "Период (в днях), после которого удалённые вложения не могут быть восстановлены." + purge_unactivated_users_grace_period_days: "Количество дней до удаления пользователя, который не активировал аккаунт. Чтобы никогда не удалять неактивированных пользователей, установите 0." + enable_s3_uploads: "Размещать загружаемые файлы на Amazon S3. ВАЖНО: требуется правильные учетные данные S3 (идентификатор ключа доступа и секретный ключ доступа)." s3_use_iam_profile: 'Использовать профиль экземпляра AWS EC2 для предоставления доступа к корзине S3. ПРИМЕЧАНИЕ: для включения этого параметра необходимо, чтобы Discourse работал в соответствующим образом настроенном экземпляре EC2 и перезаписывал настройки «s3 access key id» и «s3 secret access key».' s3_upload_bucket: "Название корзины Amazon S3, куда будут загружаться файлы. ВНИМАНИЕ: название должно быть в нижнем регистре, без пробелов, без подчёркиваний." s3_access_key_id: "Идентификатор ключа доступа Amazon S3, который будет использоваться для загрузки изображений, вложений и резервных копий." s3_secret_access_key: "Секретный ключ доступа Amazon S3, который будет использоваться для загрузки изображений, вложений и резервных копий." s3_region: "Название региона Amazon S3, которое будет использоваться для загрузки изображений и резервных копий." - s3_cdn_url: "URL-адрес CDN, используемый для всех ресурсов s3 (например, https://cdn.где-то.com). ВНИМАНИЕ: после изменения этого параметра вы должны обновить все старые сообщения." - avatar_sizes: "Перечень автоматически создаваемых размеров аватара." + s3_cdn_url: "URL-адрес CDN, используемый для всех ресурсов s3 (например, https://cdn.где-то.com). ВНИМАНИЕ: после изменения этого параметра нужно обновить все старые записи." + avatar_sizes: "Перечень размеров автоматически создаваемых аватаров." external_system_avatars_enabled: "Использовать внешний сервис для загрузки аватаров." external_system_avatars_url: "URL внешнего сервиса загрузки аватаров. Допустимые замены: {username} {first_letter} {color} {size}" external_emoji_url: "URL-адрес внешнего сервиса эмодзи. Оставьте поле пустым, если внешний сервис не требуется." use_site_small_logo_as_system_avatar: "Использовать логотип сайта вместо аватара пользователя системы. Требуется наличие логотипа." - restrict_letter_avatar_colors: "Список 6-значных шестнадцатеричных значений цвета, которые будут использоваться для фона буквенного аватара." + restrict_letter_avatar_colors: "Список шестизначных шестнадцатеричных значений цвета, которые будут использоваться для фона буквенного аватара." enable_listing_suspended_users_on_search: "Разрешить обычным пользователям находить замороженных пользователей." - selectable_avatars_mode: "Разрешить пользователям выбирать аватар из списка selectable_avatars и ограничивать загрузку пользовательских аватаров выбранным уровнем доверия." + selectable_avatars_mode: "Разрешить пользователям выбирать аватар из списка «selectable_avatars» и ограничивать загрузку своих аватаров выбранным уровнем доверия." selectable_avatars: "Список доступных для выбора аватаров." allow_all_attachments_for_group_messages: "Разрешать все типы почтовых вложений для групповых сообщений." - png_to_jpg_quality: "Качество преобразованного файла JPG (1 — низкое качество, 99 — лучшее качество, 100 — отключить параметр)." - recompress_original_jpg_quality: "Качество загружемых файлов JPG (1 — низкое качество, 99 — лучшее качество, 100 — отключить параметр)." - image_preview_jpg_quality: "Качество файлов JPG при изменении их размера (1 — низкое качество, 99 — лучшее качество, 100 — отключить параметр)." + png_to_jpg_quality: "Качество преобразованного файла JPG (1 — низкое, 99 — лучшее, 100 — отключить параметр)." + recompress_original_jpg_quality: "Качество загружаемых файлов JPG (1 — низкое, 99 — лучшее, 100 — отключить параметр)." + image_preview_jpg_quality: "Качество файлов JPG при изменении их размера (1 — низкое, 99 — лучшее, 100 — отключить параметр)." allow_staff_to_upload_any_file_in_pm: "Разрешать сотрудникам загружать любые файлы в личных сообщениях." strip_image_metadata: "Удалять метаданные из изображения." composer_media_optimization_image_enabled: "Включить оптимизацию загружаемых файлов изображений на стороне клиента." @@ -1812,28 +1808,28 @@ ru: composer_media_optimization_image_resize_width_target: "Для изображений с шириной больше, чем указано в параметре `composer_media_optimization_image_dimensions_resize_threshold`, ширина будет изменена до указанного здесь значения. Значение должно быть больше или равно параметру `composer_media_optimization_image_dimensions_resize_threshold`." composer_media_optimization_image_encode_quality: "Качество JPEG-кодирования, используемое в процессе оптимизации." min_ratio_to_crop: "Отношение, используемое для обрезки высоких изображений. Введите необходимое отношение ширины к высоте." - simultaneous_uploads: "Максимальное количество файлов, которые можно вставить в редактор сообщения." + simultaneous_uploads: "Максимальное количество файлов, которые можно вставить в редактор." default_invitee_trust_level: "Уровень доверия для приглашённых пользователей (от 0 до 4)." - default_trust_level: "Уровень доверия по умолчанию для всех новых пользователей (0-4). ПРЕДУПРЕЖДЕНИЕ! Повышая уровень доверия для новых пользователей, вы можете столкнуться с большим количеством спама." + default_trust_level: "Уровень доверия по умолчанию для всех новых пользователей (0-4). ВНИМАНИЕ! Повышая уровень доверия для новых пользователей, вы можете столкнуться с большим количеством спама." tl1_requires_topics_entered: "Количество тем, в которых должен побывать новый пользователь для продвижения до уровня доверия 1." - tl1_requires_read_posts: "Количество сообщений, которые должен прочитать новый пользователь для продвижения до уровня доверия 1." - tl1_requires_time_spent_mins: "Количество минут, которое должен потратить новый пользователь на чтение сообщений для продвижения до уровня доверия 1." + tl1_requires_read_posts: "Количество записей, которые должен прочитать новый пользователь для продвижения до уровня доверия 1." + tl1_requires_time_spent_mins: "Количество минут, которое должен потратить новый пользователь на чтение записей для продвижения до уровня доверия 1." tl2_requires_topics_entered: "Количество тем, в которых должен побывать пользователь для продвижения до уровня доверия 2." - tl2_requires_read_posts: "Количество сообщений, которые должен прочитать новый пользователь для продвижения до уровня доверия 2." - tl2_requires_time_spent_mins: "Количество минут, которое должен потратить новый пользователь на чтение сообщений для продвижения до уровня доверия 2." + tl2_requires_read_posts: "Количество записей, которые должен прочитать пользователь для продвижения до уровня доверия 2." + tl2_requires_time_spent_mins: "Количество минут, которое должен потратить пользователь на чтение записей для продвижения до уровня доверия 2." tl2_requires_days_visited: "Количество дней, на протяжении которых пользователь должен посещать форум для продвижения до уровня доверия 2." tl2_requires_likes_received: "Количество лайков, которое должен получить пользователь для продвижения до уровня доверия 2." tl2_requires_likes_given: "Количество лайков, которое должен поставить пользователь для продвижения до уровня доверия 2." tl2_requires_topic_reply_count: "Количество тем, в которых пользователь должен ответить для продвижения до уровня доверия 2." tl3_time_period: "Непрерывный период времени, необходимый для повышения до уровня доверия 3 (в днях)" - tl3_requires_days_visited: "Минимальное количество дней, которое требуется пользователю для посещения сайта за указанный непрерывный период времени (tl3_time_period), чтобы получить право на повышение до уровня доверия 3. Установите значение больше, чем tl3_time_period, если необходимо отключить повышение до уровня доверия 3. (Допустимые значения: 0 и выше.)" + tl3_requires_days_visited: "Минимальное количество дней, которое требуется пользователю для посещения сайта за указанный непрерывный период времени (tl3_time_period), чтобы получить право на повышение до уровня доверия 3. Если необходимо отключить повышение до уровня доверия 3, установите значение больше, чем «tl3_time_period». (Допустимые значения: 0 и выше.)" tl3_requires_topics_replied_to: "Минимальное количество тем, на которые пользователь должен ответить за указанный непрерывный период времени (tl3_time_period), чтобы претендовать на повышение до уровня доверия 3. (Допустимые значения: 0 и выше.)" tl3_requires_topics_viewed: "Процент созданных тем за указанный непрерывный период времени (tl3_time_period), который пользователь должен иметь для повышения уровня доверия до 3. (Допустимые значения: от 0 до 100.)" - tl3_requires_topics_viewed_cap: "Максимально необходимое количество тем, просматриваемых за непрерывный период времени, указанный в настройке tl3_time_period." - tl3_requires_posts_read: "Процент созданных сообщений за указанный непрерывный период времени (tl3_time_period), который пользователь должен иметь для повышения уровня доверия до 3. (Допустимые значения: от 0 до 100.)" - tl3_requires_posts_read_cap: "Максимально необходимое количество тем, прочитанных за указанный непрерывный период времени (tl3_time_period)." + tl3_requires_topics_viewed_cap: "Максимально необходимое количество тем, просматриваемых за непрерывный период времени, указанный в настройке «tl3_time_period»." + tl3_requires_posts_read: "Процент созданных записей за указанный непрерывный период времени (tl3_time_period), который пользователь должен иметь для повышения уровня доверия до 3. (Допустимые значения: от 0 до 100.)" + tl3_requires_posts_read_cap: "Максимально необходимое количество записей, прочитанных за указанный непрерывный период времени (tl3_time_period)." tl3_requires_topics_viewed_all_time: "Минимальное количество тем, которые пользователь должен просмотреть, чтобы претендовать на уровень доверия 3." - tl3_requires_posts_read_all_time: "Минимальное количество сообщений, которые пользователь должен прочитать, чтобы претендовать на уровень доверия 3." + tl3_requires_posts_read_all_time: "Минимальное количество записей, которые пользователь должен прочитать, чтобы претендовать на уровень доверия 3." tl3_requires_max_flagged: "Максимальное количество жалоб от различных пользователей за указанный непрерывный период времени (tl3_time_period), чтобы пользователь мог претендовать на повышение до уровня доверия 3. (Допустимые значения: 0 и выше.)" tl3_promotion_min_duration: "Минимальное количество дней, в течении которых пользователь с уровнем доверия 3 не может быть понижен до уровня доверия 2." tl3_requires_likes_given: "Минимальное количество лайков, которое необходимо поставить за указанный непрерывный период времени (tl3_time_period), чтобы претендовать на уровень доверия 3." @@ -1842,148 +1838,148 @@ ru: trusted_users_can_edit_others: "Разрешать пользователям с высоким уровнем доверия редактировать контент других пользователей." min_trust_to_create_topic: "Минимальный уровень доверия для создания новой темы." allow_flagging_staff: "Разрешать пользователям жаловаться на сообщения персонала." - min_trust_to_edit_wiki_post: "Минимальный уровень доверия, требуемый для редактирования вики-сообщения." - min_trust_to_edit_post: "Минимальный уровень доверия, требуемый для редактирования сообщений." - min_trust_to_allow_self_wiki: "Минимальный уровень доверия, требуемый для создания своего вики-сообщения." - min_trust_to_send_messages: "УСТАРЕЛО: вместо этого параметра используйте параметр «personal message enabled groups» . Минимальный уровень доверия, необходимый для создания личных сообщений." + min_trust_to_edit_wiki_post: "Минимальный уровень доверия, требуемый для редактирования вики-записи." + min_trust_to_edit_post: "Минимальный уровень доверия, требуемый для редактирования записей." + min_trust_to_allow_self_wiki: "Минимальный уровень доверия, требуемый для создания вики-записи." + min_trust_to_send_messages: "УСТАРЕЛО: вместо этого параметра используйте параметр «personal message enabled groups». Минимальный уровень доверия, необходимый для создания личных сообщений." min_trust_to_send_email_messages: "Минимальный уровень доверия, необходимый для отправки личных сообщений по электронной почте." min_trust_to_flag_posts: "Минимальный уровень доверия, необходимый для создания жалобы на запись." - min_trust_to_post_links: "Минимальный уровень доверия, необходимый для включения ссылок в сообщения" - min_trust_to_post_embedded_media: "Минимальный уровень доверия, необходимый для встраивания медиафайлов в сообщение" + min_trust_to_post_links: "Минимальный уровень доверия, необходимый для включения ссылок в записи" + min_trust_to_post_embedded_media: "Минимальный уровень доверия, необходимый для встраивания медиафайлов в запись" min_trust_level_to_allow_profile_background: "Минимальный уровень доверия, необходимый для загрузки шапки профиля" min_trust_level_to_allow_user_card_background: "Минимальный уровень доверия, необходимый для загрузки фона карточки пользователя" min_trust_level_to_allow_invite: "Минимальный уровень доверия, необходимый для приглашения пользователей" min_trust_level_to_allow_ignore: "Минимальный уровень доверия, необходимый для игнорирования пользователей" allowed_link_domains: "Домены, на которые пользователи могут ссылаться, даже если у них нет соответствующего уровня доверия для публикации ссылок" - newuser_max_links: "Максимальное количество ссылок, которое новый пользователь может вставлять в сообщение." - newuser_max_embedded_media: "Максимальное количество медиафайлов, которое новый пользователь может добавить в сообщение." - newuser_max_attachments: "Максимальное количество вложений, которое новый пользователь может прикрепить к сообщению." - newuser_max_mentions_per_post: "Максимальное число упоминаний других пользователей (@name), которое новый пользователь может сделать в одном сообщении." + newuser_max_links: "Максимальное количество ссылок, которое новый пользователь может вставлять в запись." + newuser_max_embedded_media: "Максимальное количество медиафайлов, которое новый пользователь может добавить в запись." + newuser_max_attachments: "Максимальное количество вложений, которое новый пользователь может прикрепить к записи." + newuser_max_mentions_per_post: "Максимальное число упоминаний других пользователей, которое новый пользователь может сделать в одной записи." newuser_max_replies_per_topic: "Максимальное количество ответов нового пользователя в одной теме до того, как кто-нибудь напишет ответ." - max_mentions_per_post: "Максимальное число упоминаний других пользователей (@name), которое любой пользователь может сделать в одном сообщении." + max_mentions_per_post: "Максимальное число упоминаний других пользователей, которое любой пользователь может сделать в одной записи." max_users_notified_per_group_mention: "Максимальное количество пользователей, которые могут получать уведомления при упоминании группы (при достижении порогового значения уведомления создаваться не будут)" enable_mentions: "Разрешать пользователям упоминать других пользователей." here_mention: "Название для переменной @here, позволяющее пользователям с соответствующим уровнем доверия уведомлять до «max_here_mentioned» пользователей, участвующих в теме. Не должно быть существующим именем пользователя." max_here_mentioned: "Максимальное количество пользователей для упоминания при использовании @here." min_trust_level_for_here_mention: "Минимальный уровень доверия, при котором допускается использование @here." - create_thumbnails: "Создавать миниатюры слишком больших картинок в сообщениях, показывать большие оригиналы картинок в отдельно открывающемся окне." - email_time_window_mins: "Подождать указанное здесь количество минут перед отправкой почтовых уведомлений, чтобы дать автору возможность перечитать и отредактировать только что созданное сообщение." - personal_email_time_window_seconds: "Подождать указанное здесь количество секунд перед отправкой письма с личным сообщением, чтобы дать автору возможность перечитать и отредактировать только что созданное сообщение." + create_thumbnails: "Создавать миниатюры слишком больших изображений в записях, показывать большие оригиналы в отдельно открывающемся окне." + email_time_window_mins: "Подождать указанное здесь количество минут перед отправкой почтовых уведомлений, чтобы дать автору возможность перечитать и отредактировать только что созданную запись." + personal_email_time_window_seconds: "Подождать указанное здесь количество секунд перед отправкой письма с уведомлением о личном сообщении, чтобы дать автору возможность перечитать и отредактировать только что созданное сообщение." email_posts_context: "Количество предыдущих ответов, которое необходимо включать в почтовые уведомления в качестве контекста." - flush_timings_secs: "Частота, с которой оправляются метки времени на сервер, в секундах" + flush_timings_secs: "Частота, с которой оправляются метки времени на сервер, в секундах." title_max_word_length: "Максимально допустимая длина слов в заголовке темы." - title_min_entropy: "Минимальная энтропия, требуемая для названия темы. (Энтропия — количество уникальных символов, причём некоторые русские буквы могут считаться за 2 символа, а не за 1, как английские)." - body_min_entropy: "Минимальная энтропия, требуемая для текста новой темы. Энтропия — количество уникальных символов, причём некоторые русские буквы могут считаться за 2 символа, а не за 1, как английские)." - allow_uppercase_posts: "Разрешать создавать названия тем или сообщений заглавными буквами." - max_consecutive_replies: "Максимальное количество сообщений, которые пользователь может создать ПОДРЯД в теме, после чего у него не будет возможности добавить ещё один ответ." - enable_filtered_replies_view: 'Кнопка ответов отображает только текущее сообщение и ответы на него, свернув все остальные сообщения.' + title_min_entropy: "Минимальная энтропия, требуемая для названия темы. (Количество уникальных символов, причём некоторые неанглийские буквы могут считаться за 2 символа, а не за 1.)" + body_min_entropy: "Минимальная энтропия, требуемая для текста новой записи. (Количество уникальных символов, причём некоторые неанглийские буквы могут считаться за 2 символа, а не за 1.)" + allow_uppercase_posts: "Разрешать создавать названия тем и текст записей заглавными буквами." + max_consecutive_replies: "Максимальное количество записей, которые пользователь может создать подряд в теме, после чего у него не будет возможности добавить ещё один ответ." + enable_filtered_replies_view: 'Кнопка ответов отображает только текущую запись и ответы на нее, остальные записи сворачиваются.' title_fancy_entities: "В заголовках тем преобразовывать обычные символы ASCII и пунктуацию SmartyPants в объекты HTML" min_title_similar_length: "Минимальная длина названия темы, при которой название будет проверено на наличие похожих тем." - desktop_category_page_style: "Визуальный стиль для секции «Разделы»." + desktop_category_page_style: "Визуальный стиль для страницы «Категории»." category_colors: "Список шестнадцатеричных кодов палитры цветов, разрешённых в категориях." category_style: "Стили для выделения категорий." default_dark_mode_color_scheme_id: "Цветовая схема, используемая в тёмном режиме." dark_mode_none: "Нет" - max_image_size_kb: "Максимальный размер загружаемого изображения в кБ. Этот параметр должен быть настроен в nginx (client_max_body_size), apache или прокси. Размер изображений, превышающий указанное здесь значение, будет изменён при загрузке." - max_attachment_size_kb: "Максимальный размер загружаемых файлов в кБ. Этот параметр должен быть настроен в nginx (client_max_body_size), apache или прокси." - authorized_extensions: "Список расширений файлов, разрешённых к загрузке. Укажите символ «*» для загрузки любых типов файлов." - authorized_extensions_for_staff: "Список расширений файлов, разрешенных для загрузки сотрудникам, в дополнение к списку, определённому в настройке `authorized_extensions`. Укажите символ «*» для загрузки любых типов файлов." + max_image_size_kb: "Максимальный размер загружаемого изображения в кБ. Этот параметр должен также быть настроен в nginx (client_max_body_size), apache или прокси. Размер изображений, превышающий указанное значение «client_max_body_size», будет изменён при загрузке." + max_attachment_size_kb: "Максимальный размер загружаемых файлов в кБ. Этот параметр должен также быть настроен в nginx (client_max_body_size), apache или прокси." + authorized_extensions: "Список расширений файлов, разрешённых к загрузке. Символ «*» разрешает загрузку любых типов файлов." + authorized_extensions_for_staff: "Список расширений файлов, разрешенных для загрузки сотрудникам, в дополнение к списку, определённому в настройке `authorized_extensions`. Символ «*» разрешает загрузку любых типов файлов." theme_authorized_extensions: "Список расширений файлов, разрешённых для загрузки тем оформления. Укажите символ «*» для загрузки любых типов файлов." max_similar_results: "Количество похожих тем, показываемых пользователю во время создания новой темы. Сравнение выполняется на основании названия и текста темы." max_image_megapixels: "Максимум мегапикселей в изображении. Изображения с большим количеством мегапикселей будут отклонены." - title_prettify: "Предотвращать распространённые опечатки и ошибки в названии тем, включая печать в верхнем регистре, использование строчного символа в начале названия, множественные знаки «!» и «?», лишние точки в конце предложения и т. д." + title_prettify: "Предотвращать распространённые опечатки и ошибки в названии тем, включая набор в верхнем регистре, использование строчного символа в начале названия, множественные знаки «!» и «?», лишние точки в конце предложения и т. д." title_remove_extraneous_space: "Удалять пробелы перед точкой в конце предложения." automatic_topic_heat_values: 'Автоматически обновлять количество просмотров и лайков тем, основываясь на активности сайта.' - topic_views_heat_low: "После указанного здесь количества просмотров, цифра просмотров слегка подсвечивается." - topic_views_heat_medium: "После указанного здесь количества просмотров, цифра просмотров умеренно подсвечивается." - topic_views_heat_high: "После указанного здесь количества просмотров, цифра просмотров значительно подсвечивается." + topic_views_heat_low: "После указанного здесь количества просмотров цифра просмотров слегка подсвечивается." + topic_views_heat_medium: "После указанного здесь количества просмотров цифра просмотров умеренно подсвечивается." + topic_views_heat_high: "После указанного здесь количества просмотров цифра просмотров значительно подсвечивается." cold_age_days_low: "Количество дней, по истечению которых дата последней активности в теме станет немного тусклее." cold_age_days_medium: "Количество дней, по истечению которых дата последней активности в теме станет значительно тусклее." cold_age_days_high: "Количество дней, по истечению которых дата последней активности в теме станет совсем тусклой." - history_hours_low: "Сообщение, отредактированное в течение этого количества часов, имеет слегка подсвеченный индикатор редактирования." - history_hours_medium: "Сообщение, отредактированное в течение этого количества часов, имеет умеренно подсвеченный индикатор редактирования." - history_hours_high: "Сообщение, отредактированное в течение этого количества часов, имеет значительно подсвеченный индикатор редактирования." + history_hours_low: "Запись, отредактированная в течение этого количества часов, имеет слегка подсвеченный индикатор редактирования." + history_hours_medium: "Запись, отредактированная в течение этого количества часов, имеет умеренно подсвеченный индикатор редактирования." + history_hours_high: "Запись, отредактированная в течение этого количества часов, имеет значительно подсвеченный индикатор редактирования." topic_post_like_heat_low: "Количество лайков в записи, после которого счётчик количества ответов в теме будет слегка подсвечен." topic_post_like_heat_medium: "Количество лайков в записи, после которого счётчик количества ответов в теме будет умеренно подсвечен." topic_post_like_heat_high: "Количество лайков в записи, после которого счётчик количества ответов в теме будет сильно подсвечен." faq_url: "Укажите полный URL к странице FAQ, если хотите использовать собственную версию." - tos_url: "Укажите полный URL к документу с условиями предоставления услуг, если хотите использовать собственную версию." + tos_url: "Укажите полный URL к документу с условиями использования, если хотите использовать собственную версию." privacy_policy_url: "Укажите полный URL к документу с условиями политики конфиденциальности, если хотите использовать собственную версию." newuser_spam_host_threshold: "Максимальное количество ссылок на один и тот же ресурс, публикуемых новыми пользователями в записях (в количестве `newuser_spam_host_threshold`), прежде чем они будут расцениваться как спам." - allowed_spam_host_domains: "Перечень доменов, исключённых из тестирования на спам. У новых пользователей не будет ограничений на создание сообщений со ссылками на эти домены." + allowed_spam_host_domains: "Перечень доменов, исключённых из проверки на спам. У новых пользователей не будет ограничений на создание записей со ссылками на эти домены." staff_like_weight: "Весовой коэффициент для лайков, выдаваемых персоналом (для остальных пользователей он равен 1)." - topic_view_duration_hours: "Считать все просмотры темы как один просмотр, если просмотры происходят с одного IP-адреса в течение указанного здесь количества часов." - user_profile_view_duration_hours: "Считать все просмотры профиля пользователя как один просмотр, если просмотры происходят с одного IP-адреса в течение указанного здесь количества часов." + topic_view_duration_hours: "Считать все просмотры темы как один, если они выполнены с одного IP-адреса в течение указанного здесь количества часов." + user_profile_view_duration_hours: "Считать все просмотры профиля пользователя как один, если они выполнены с одного IP-адреса в течение указанного здесь количества часов." levenshtein_distance_spammer_emails: "Количество символов, на которое могут различаться сообщения, если проводится нечёткое сравнение текста при проверке писем на спам." - max_new_accounts_per_registration_ip: "Если обнаружено указанное здесь количество аккаунтов с уровнем доверия 0, использующих общий IP-адрес (и ни одна учётная запись не принадлежит сотрудникам или пользователям с уровнем доверия 2 и выше), прекратить регистрацию новых аккаунтов с этого IP-адреса. Для отключения параметра установите значение в 0." + max_new_accounts_per_registration_ip: "Если обнаружено указанное здесь количество аккаунтов с уровнем доверия 0, использующих общий IP-адрес (и ни один аккаунт не принадлежит сотрудникам или пользователям с уровнем доверия 2 и выше), прекратить регистрацию новых аккаунтов с этого IP-адреса. Для отключения параметра установите значение в 0." min_ban_entries_for_roll_up: "Создавать новую запись запрета подсети, если в списке адресов есть указанное здесь количество записей." max_age_unmatched_emails: "Удалять отфильтрованные письма после указанного здесь количества дней." max_age_unmatched_ips: "Удалять отфильтрованные IP-адреса после указанного здесь количества дней." - num_flaggers_to_close_topic: "Минимальное количество человек, жалующихся на тему, которое требуется для автоматической блокировки темы и отправки её на премодерацию" - num_hours_to_close_topic: "Тема блокируется на указанное здесь количество часов, необходимых для её модерации." + num_flaggers_to_close_topic: "Минимальное количество человек, жалующихся на тему, которое требуется для автоматической блокировки и отправки темы на проверку" + num_hours_to_close_topic: "Тема блокируется на указанное здесь количество часов, необходимых для её проверки." auto_respond_to_flag_actions: "Включить автоматический ответ при отклонении жалобы." - min_first_post_typing_time: "Минимальное количество времени в миллисекундах, которое пользователь должен потратить на набор текста первого сообщения. Если порог не соблюдён, сообщение автоматически попадает на премодерацию. Установите 0, если необходимо отключить этот параметр (не рекомендуется)." + min_first_post_typing_time: "Минимальное количество времени в миллисекундах, которое пользователь должен потратить на набор текста первой записи. Если порог не соблюдён, запись автоматически попадает на проверку. Чтобы отключить этот параметр (не рекомендуется), установите 0." auto_silence_fast_typers_on_first_post: "Автоматически блокировать пользователей, которые не соответствуют параметру `min_first_post_typing_time`" auto_silence_fast_typers_max_trust_level: "Максимальный уровень доверия для автоматической блокировки пользователей, если зафиксирован быстрый набор текста" - auto_silence_first_post_regex: "Не учитывающее регистр текста регулярное выражение, которое (если в тексте первого сообщения пользователя будет найдено совпадение) заблокирует пользователя и отправит его сообщение на премодерацию. Пример: выражение `raging|a[bc]a` заблокирует все сообщения, содержащие raging, aba или aca. Правило применяется только к первому сообщению пользователя. ЭТА НАСТРОЙКА ЯВЛЯЕТСЯ УСТАРЕВШЕЙ, вместо неё используйте функцию блокировки первых сообщений в разделе «Админка->Логи->Контролируемые слова»." - reviewable_claiming: "Нужно ли резервировать проверяемый контент за конкретным сотрудником, прежде чем отправить контент на премодерацию?" - reviewable_default_topics: "Показывать модерируемый контент, сгруппированный по темам" - reviewable_default_visibility: "Не показывать модерируемый контент, если он не соответствует этому приоритету" + auto_silence_first_post_regex: "Регулярное выражение (без учета регистра), которое отправит первую запись пользователя на проверку и заблокирует его, если в тексте записи будет найдено совпадение. Пример: выражение `raging|a[bc]a` заблокирует все записи, содержащие «raging», «aba» или «aca». Правило применяется только к первой записи пользователя. ЭТА НАСТРОЙКА ЯВЛЯЕТСЯ УСТАРЕВШЕЙ — используйте функцию «Контролируемые слова»." + reviewable_claiming: "Нужно ли резервировать проверяемый контент за конкретным сотрудником, прежде чем отправить контент на проверку?" + reviewable_default_topics: "Показывать проверяемый контент, сгруппированный по темам" + reviewable_default_visibility: "Не показывать проверяемый контент, если он не соответствует этому приоритету" reviewable_low_priority_threshold: "Фильтр приоритета скрывает проверяемые объекты, которые не соответствуют выбранному фильтру, только если не используется фильтр «любой»." - high_trust_flaggers_auto_hide_posts: "Сообщения нового пользователя автоматически скрываются, если они помечаются как спам пользователем с уровнем доверия 3 и выше" + high_trust_flaggers_auto_hide_posts: "Записи нового пользователя автоматически скрываются, если они помечаются как спам пользователем с уровнем доверия 3 и выше" cooldown_hours_until_reflag: "Количество часов, в течение которых пользователи не смогут повторно пожаловаться на запись" - slow_mode_prevents_editing: "Запрещать редактирование сообщений в замедленном режиме по прошествии льготного периода редактирования?" + slow_mode_prevents_editing: "Запрещать редактирование записей в замедленном режиме по прошествии периода «editing_grace_period»?" reply_by_email_enabled: "Разрешать отвечать в темах с помощью электронных писем." reply_by_email_address: "Шаблон для ответа по электронной почте в формате: %%{reply_key}@reply.example.com или replies+%%{reply_key}@example.com" alternative_reply_by_email_addresses: "Список альтернативных шаблонов для ответа, применяемый на основе входящих адресов электронной почты. Пример: %%{reply_key}@reply.example.com|replies+1%%{reply_key}@example.com" incoming_email_prefer_html: "Использовать HTML вместо обычного текста для входящих писем." strip_incoming_email_lines: "Удалять начальные и конечные пробелы из каждой строки входящих писем." - disable_emails: "Запретить отправлять любые электронные письма. Выберите «да», чтобы отключить электронную почту для всех пользователей. Выберите «non-staff», чтобы отключить электронную почту для пользователей, не являющихся персоналом." + disable_emails: "Запретить отправлять электронные письма. Выберите «yes», чтобы отключить электронную почту для всех пользователей. Выберите «non-staff», чтобы отключить электронную почту для пользователей, не являющихся персоналом." strip_images_from_short_emails: "Удалять картинки из коротких писем — размером менее 2800 байт" short_email_length: "Письма будут считаться короткими, если их размер не превышает указанное здесь количество байт" display_name_on_email_from: "Отображать в поле «От» полные имена отправителей электронных писем" unsubscribe_via_email: "Разрешать пользователям отказываться от подписки на электронные письма, отправив электронное письмо, содержащее «unsubscribe» в теме или теле письма" unsubscribe_via_email_footer: "Отображать ссылку на отписку под текстом отправляемого письма" - delete_email_logs_after_days: "Удалять записи из журнала эл. почты спустя указанное количество дней. Укажите 0, если необходимо хранить записи неограниченное количество времени." - disallow_reply_by_email_after_days: "Запретить ответ по электронной почте через указанное количество дней. Укажите 0, если необходимо хранить записи неограниченное количество времени." - max_emails_per_day_per_user: "Максимальное количество эл. писем, отправляемых пользователям в течение дня. Укажите 0, если необходимо отключить это ограничение." + delete_email_logs_after_days: "Удалять записи из журнала эл. почты спустя указанное количество дней. 0 — хранить записи неограниченное количество времени." + disallow_reply_by_email_after_days: "Запретить ответ по электронной почте через указанное количество дней. 0 — запрет не нужен." + max_emails_per_day_per_user: "Максимальное количество писем, отправляемых пользователям в течение дня. 0 — отключить ограничение." enable_staged_users: "Автоматически создавать сымитированных пользователей при обработке входящих писем." maximum_staged_users_per_email: "Максимальное количество сымитированных пользователей, создаваемых при обработке входящих писем." maximum_recipients_per_new_group_email: "Блокировать входящие письма со слишком большим количеством получателей." auto_generated_allowlist: "Перечень адресов электронной почты, которые не будут проверяться на автоматически сгенерированный контент. Пример: foo@bar.com|discourse@bar.com" - block_auto_generated_emails: "Блокировать входящие эл. письма, идентифицированные как автоматически созданные." - ignore_by_title: "Игнорировать входящие эл. письма по их заголовку." + block_auto_generated_emails: "Блокировать входящие письма, идентифицированные как автоматически созданные." + ignore_by_title: "Игнорировать входящие письма по заголовку." mailgun_api_key: "Секретный ключ API Mailgun, используемый для проверки сообщений вебхука." soft_bounce_score: "Количество возвратов, добавляемых пользователю при непостоянных возвратах писем." hard_bounce_score: "Количество возвратов, добавляемых пользователю при постоянных возвратах писем." - bounce_score_threshold: "Максимальное количество возвратов недоставленных писем, после которого мы перестанем отправлять пользователю электронную почту." - reset_bounce_score_after_days: "Автоматически сбрасывать счётчик отказов после указанного здесь количества дней." + bounce_score_threshold: "Максимальное количество возвратов недоставленных писем, после которого мы перестанем отправлять пользователю письма." + reset_bounce_score_after_days: "Автоматически сбрасывать счётчик возвратов после указанного здесь количества дней." blocked_attachment_content_types: "Перечень ключевых слов, используемых для добавления вложений в черный список в зависимости от типа контента." blocked_attachment_filenames: "Список ключевых слов, используемых для добавления вложений в черный список на основе имени файла." forwarded_emails_behaviour: "Как обрабатывать переадресованное письмо в Discourse" always_show_trimmed_content: "Всегда показывать обрезанную часть входящих писем. ВНИМАНИЕ: может раскрыть адреса электронной почты." - trim_incoming_emails: "Обрезать часть входящих писем, если они не актуальны." - private_email: "Не включать контент из сообщений или тем в заголовок или текст письма. ПРИМЕЧАНИЕ: в этом случае также отключается дайджест по эл. почте." + trim_incoming_emails: "Обрезать часть входящих писем, которая не актуальна." + private_email: "Не включать контент из записей и тем в заголовок и текст письма. ПРИМЕЧАНИЕ: в этом случае также отключается дайджест по эл. почте." email_total_attachment_size_limit_kb: "Максимальный общий размер файлов в килобайтах, прикрепляемых к исходящим письмам. Установите 0, если необходимо отключить отправку вложений." - post_excerpts_in_emails: "В уведомлениях по электронной почте всегда отправлять выдержки вместо полных сообщений" + post_excerpts_in_emails: "В уведомлениях по электронной почте всегда отправлять выдержки вместо полных записей" raw_email_max_length: "Максимальное количество символов, сохраняющихся в сообщениях входящей электронной почты." raw_rejected_email_max_length: "Максимальное количество символов, сохраняющихся в отклонённых сообщениях входящей электронной почты." delete_rejected_email_after_days: "Удалять отклонённые письма старше указанного здесь количества дней." - require_change_email_confirmation: "Требовать от обычных пользователей подтверждения старого адреса электронной почты перед его изменением. Не относится к сотрудникам, им всегда необходимо подтверждать старый адрес электронной почты." + require_change_email_confirmation: "Требовать от обычных пользователей подтверждения старого адреса электронной почты перед его изменением. Не относится к сотрудникам: им всегда необходимо подтверждать старый адрес электронной почты." manual_polling_enabled: "Отправлять электронную почту, используя API для ответов." pop3_polling_enabled: "Загружать ответы на форум в виде писем с аккаунта POP3." pop3_polling_ssl: "Использовать SSL при подключении к POP3-серверу. (Рекомендуется.)" - pop3_polling_openssl_verify: "Проверять сертификат сервера TLS (по умолчанию: включено)" - pop3_polling_period_mins: "Период в минутах между проверками аккаунта POP3 на наличие электронных писем. ВНИМАНИЕ: требуется перезапуск." + pop3_polling_openssl_verify: "Проверять сертификат сервера TLS (по умолчанию включено)" + pop3_polling_period_mins: "Период в минутах между проверками аккаунта POP3 на наличие писем. ВНИМАНИЕ: требуется перезапуск." pop3_polling_port: "Порт аккаунта POP3 для загрузки эл. писем." pop3_polling_host: "Хост аккаунта POP3 для загрузки эл. писем." - pop3_polling_username: "Имя пользователя, или логин, аккаунта POP3 для загрузки эл. писем." + pop3_polling_username: "Имя пользователя (логин) аккаунта POP3 для загрузки эл. писем." pop3_polling_password: "Пароль аккаунта POP3 для загрузки эл. писем." - pop3_polling_delete_from_server: "Удалять электронную почту с сервера. ПРИМЕЧАНИЕ: при отключении этого параметра необходимо вручную очищать почтовый ящик" + pop3_polling_delete_from_server: "Удалять электронную почту с сервера. ПРИМЕЧАНИЕ: при отключении этого параметра необходимо вручную очищать почтовый ящик." log_mail_processing_failures: "Записывать все ошибки обработки электронной почты в файл журнала" email_in: 'Разрешать пользователям публиковать новые темы по электронной почте (требуется автоматическая или ручная проверка почтового ящика — POP3). Настройте адреса на вкладке «Настройки» каждой категории.' email_in_min_trust: "Минимальный уровень доверия, требуемый для создания новых тем через электронные письма." - email_in_authserv_id: "Идентификатор службы, выполняющей проверку подлинности входящих писем. См. тему https://meta.discourse.org/t/134358 для получения более подробной информации." + email_in_authserv_id: "Идентификатор службы, выполняющей проверку подлинности входящих писем. Подробнее — в теме https://meta.discourse.org/t/134358." email_in_spam_header: "Заголовок электронного письма для обнаружения спама." enable_imap: "Включить IMAP для синхронизации групповых сообщений." enable_imap_write: "Включить двустороннюю синхронизацию IMAP. При выключенном параметре все операции записи в аккаунтах IMAP отключены." @@ -1991,26 +1987,26 @@ ru: enable_smtp: "Включить SMTP для отправки групповых уведомлений." imap_polling_period_mins: "Период в минутах между проверкой аккаунтов IMAP на наличие писем." imap_polling_old_emails: "Максимальное количество старых (обработанных) электронных писем, которые будут обновляться каждый раз при опросе окна IMAP (укажите 0 для обновления всех старых писем)." - imap_polling_new_emails: "Максимальное количество новых электронных писем (необработанных), которые будут обновляться каждый раз при опросе окна IMAP." + imap_polling_new_emails: "Максимальное количество новых писем (необработанных) для обновления при опросе почтового ящика IMAP." imap_batch_import_email: "Минимальное количество новых писем, которые запускают режим импорта (отключает почтовые оповещения)." - email_prefix: "[Метка], используемая в заголовках писем. Если не указано, по-умолчанию будет использовано значение из настройки «title»." - email_site_title: "Заголовок сайта, используемый в автоматических письмах. Если заголовок не настроен, то используется стандартное значение «title». Если ваш заголовок содержит символы, запрещённые в поле «От», будет использовано это значение." - find_related_post_with_key: "Использовать только «ответный ключ», чтобы найти ответ на запись. ВНИМАНИЕ: отключение этого параметра позволяет выдавать себя за другого пользователя на основании адреса электронной почты." - minimum_topics_similar: "Сколько тем должно существовать, прежде чем похожие темы будут представлены при составлении новых тем." - relative_date_duration: "Количество дней после создания сообщения, в течении которых его дата будет отображаться в относительном виде (7д), а не в абсолютном (20 Фев)." - delete_user_max_post_age: "Запретить удаление пользователей, чьё первое сообщение было создано более указанного здесь количества дней." - delete_all_posts_max: "Максимальное количество сообщений, которое может быть удалено за один раз через кнопку «Удалить все сообщения». Если у пользователя сообщений больше этого числа, сообщения не могут быть удалены за одно нажатие и в этом случае пользователь не может быть удалён." - delete_user_self_max_post_count: "Максимальное количество сообщений, которое пользователь может иметь при удалении собственного аккаунта. Установите это значение в -1, если необходимо отключить возможность удаления собственного аккаунта." - username_change_period: "Максимальное количество дней после регистрации, в течение которых пользователи могут менять свой псевдоним (установите это значение в 0, если необходимо запретить изменение псевдонимов)." - email_editable: "Позволять пользователям изменять адрес электронной почты после регистрации." - logout_redirect: "Адрес страницы для перенаправления после выхода из аккаунта (например: https://примерная_ссылка.ком/выход)" + email_prefix: "[Метка], используемая в темах писем. Если не указано, по умолчанию будет использовано значение из настройки «title»." + email_site_title: "Заголовок сайта, используемый в автоматических письмах. Если заголовок не настроен, то используется стандартное значение «title». Если заголовок содержит символы, запрещённые в поле «От», будет использовано это значение." + find_related_post_with_key: "Для ответа на запись использовать только «ответный ключ». ВНИМАНИЕ: отключение этого параметра позволяет выдавать себя за другого пользователя на основании адреса электронной почты." + minimum_topics_similar: "Сколько тем должно существовать, прежде чем при создании новых тем будут предлагаться похожие темы." + relative_date_duration: "Количество дней после создания записи, в течении которых ее дата будет отображаться в относительном виде (7 дней), а не в абсолютном (20 февраля)." + delete_user_max_post_age: "Запретить удаление пользователей, чья первая запись старше указанного здесь количества дней." + delete_all_posts_max: "Максимальное количество записей, которое может быть удалено за один раз кнопкой «Удалить все записи». Если у пользователя записей больше, они не будут удалены за одно нажатие, — в этом случае пользователь не может быть удалён." + delete_user_self_max_post_count: "Максимальное количество записей, которое пользователь может иметь при удалении собственного аккаунта. Установите это значение в -1, если необходимо отключить возможность удаления собственного аккаунта." + username_change_period: "Максимальное количество дней после регистрации, в течение которых можно менять свое имя пользователя (установите это значение в 0, если необходимо запретить изменение имени пользователя)." + email_editable: "Разрешить пользователям изменять адрес электронной почты после регистрации." + logout_redirect: "Адрес страницы для перенаправления после выхода из аккаунта (например: https://example.com/logout)" allow_uploaded_avatars: "Разрешать пользователям загружать свои собственные аватары." - default_avatars: "URL для аватара, который будет использован по умолчанию для новых пользователей, пока они его не поменяют." + default_avatars: "URL для аватара, который будет использован по умолчанию для новых пользователей, пока они его не сменят." automatically_download_gravatars: "Скачивать Gravatar пользователя при создании аккаунта или изменении адреса электронной почты." - digest_topics: "Максимальное количество популярных тем, отображаемых в дайджесте по эл. почте." - digest_posts: "Максимальное количество популярных сообщений, отображаемых в дайджесте по эл. почте." - digest_other_topics: "Максимальное количество тем для отображения в разделе дайджеста по эл. почте «Новые темы и разделы, за которыми вы следите»." - digest_min_excerpt_length: "Минимальная длина отрывка сообщения в дайджесте по эл. почте (количество символов)." + digest_topics: "Максимальное количество популярных тем, отображаемых в сводке по эл. почте." + digest_posts: "Максимальное количество популярных записей, отображаемых в сводке по эл. почте." + digest_other_topics: "Максимальное количество тем для отображения в разделе сводки по эл. почте «Новые темы и категории, за которыми вы следите»." + digest_min_excerpt_length: "Минимальная длина выдержки записи в сводке по эл. почте (количество символов)." suppress_digest_email_after_days: "Не рассылать письма со сводками для пользователей, не появлявшихся на форуме более указанного здесь количества дней." digest_suppress_categories: "Не отображать содержимое этих категорий в письмах со сводками." disable_digest_emails: "Отключить письма со сводками для всех пользователей." @@ -2028,67 +2024,67 @@ ru: group_in_subject: "Установить переменную %%{optional_pm} в теме электронного письма в соответствии с именем первой группы в личном кабинете, см. тему Настройка формата темы для стандартных электронных писем" allow_anonymous_posting: "Разрешать пользователям переключаться в анонимный режим." anonymous_posting_min_trust_level: "Минимальный уровень доверия для возможности создавать темы от имени анонимного пользователя." - anonymous_account_duration_minutes: "Защита от слишком частого создания новых аккаунтов. Пример: если значение установлено в 600, это означает, что должно пройти не менее 600 минут с момента последнего сообщения пользователя И его выхода из системы, после чего он сможет создать новый аккаунт." + anonymous_account_duration_minutes: "Защита от слишком частого создания новых аккаунтов. Пример: если значение установлено в 600, это означает, что должно пройти не менее 600 минут с момента последней записи пользователя и его выхода из системы, после чего он сможет создать новый аккаунт." hide_user_profiles_from_public: "Отключить отображение карточек пользователей, профилей и списка участников форума для анонимных пользователей." allow_users_to_hide_profile: "Разрешить пользователям скрывать свой профиль и присутствие на форуме" allow_featured_topic_on_user_profiles: "Разрешать пользователям размещать ссылку на избранную тему в карточке пользователя и в профиле." show_inactive_accounts: "Разрешать авторизованным пользователям просматривать профили неактивных аккаунтов." hide_suspension_reasons: "Не отображать публично причины заморозки в профиле пользователей." - log_personal_messages_views: "Регистрировать просмотры личных сообщений администратором для других пользователей / групп." + log_personal_messages_views: "Регистрировать просмотры личных сообщений администратором для других пользователей (групп)." ignored_users_count_message_threshold: "Уведомлять модераторов, если определённого пользователя игнорируют указанное здесь количество пользователей." - ignored_users_message_gap_days: "Количество дней ожидания, прежде чем снова уведомить модераторов о пользователе, которого игнорировали многие другие пользователи." - clean_up_inactive_users_after_days: "Количество дней до удаления неактивного пользователя ( с уровнем доверия 0 и без единого сообщения). Для отключения удаления установите это значение в 0." - clean_up_unused_staged_users_after_days: "Количество дней до удаления неиспользуемого сымитированного пользователя (без каких-либо сообщений). Для отключения удаления установите это значение в 0." + ignored_users_message_gap_days: "Количество дней ожидания, прежде чем модераторы будут снова уведомлены о пользователе, которого игнорировали многие другие пользователи." + clean_up_inactive_users_after_days: "Количество дней до удаления неактивного пользователя (с уровнем доверия 0 и без единой записи). Для отключения удаления установите это значение в 0." + clean_up_unused_staged_users_after_days: "Количество дней до удаления неиспользуемого сымитированного пользователя (без каких-либо записей). Для отключения удаления установите это значение в 0." user_selected_primary_groups: "Разрешать пользователям устанавливать свою основную группу" max_notifications_per_user: "Максимальное количество уведомлений на пользователя. Если это число будет превышено, старые уведомления будут еженедельно удаляться. Для отключения параметра установите это значение в 0." allowed_user_website_domains: "Сайт пользователя будет проверен по этим доменам. Список с разделителем-чертой." - allow_profile_backgrounds: "Разрешать пользователям загружать фоновые картинки для своих страниц профиля." - sequential_replies_threshold: "Количество сообщений, которые пользователь может опубликовать подряд в теме, прежде чем ему придёт напоминание о слишком большом количестве последовательных ответов в одной и той же теме." - get_a_room_threshold: "Количество сообщений, которые пользователь может опубликовать подряд в теме одному и тому же лицу, прежде чем ему придёт напоминание о слишком большом количестве последовательных ответов в одной и той же теме." + allow_profile_backgrounds: "Разрешать пользователям загружать фоновые изображения для своих страниц профиля." + sequential_replies_threshold: "Количество записей, которые пользователь может опубликовать подряд в теме, прежде чем ему придёт напоминание о слишком большом количестве последовательных ответов в одной и той же теме." + get_a_room_threshold: "Количество записей, которые пользователь может опубликовать подряд в теме одному и тому же лицу, прежде чем ему придёт напоминание о слишком большом количестве последовательных ответов в одной и той же теме." enable_mobile_theme: "Мобильные устройства используют специальную тему с возможностью переключения в обычный вид. Отключите данную настройку, если вы хотите использовать специально адаптированный стиль для мобильных устройств." - dominating_topic_minimum_percent: "Какой процент сообщений в одной теме могут составлять сообщения одного пользователя, прежде чем ему будет показано предупреждение о слишком большой активности в теме." + dominating_topic_minimum_percent: "Какой процент записей в одной теме могут составлять записи одного пользователя, прежде чем ему будет показано предупреждение о слишком большой активности в теме." disable_avatar_education_message: "Отключить обучающее сообщение при смене аватара." - pm_warn_user_last_seen_months_ago: "При создании нового личного сообщения предупреждать автора сообщения, если получатель сообщения не был замечен на форуме более указанного здесь количества месяцев назад." - suppress_uncategorized_badge: "Не показывать награду в списках тем для тех тем, которые находятся в разделе РАЗНОЕ." + pm_warn_user_last_seen_months_ago: "При создании нового личного сообщения предупреждать автора сообщения, если получатель не заходил на форум более указанного здесь количества месяцев." + suppress_uncategorized_badge: "Не показывать награду в списках тем для тех тем, которые находятся в разделе «Без категории»." header_dropdown_category_count: "Максимальное количество категорий, отображаемых в выпадающем меню заголовка." permalink_normalizations: "Применять указанное регулярное выражение перед поиском соответствий в постоянных ссылках, например, выражение: /(topic.*)\\?.*/\\1 удалит подстроку запроса из ссылок. Использование: регулярное выражение + строка. Используйте \\1 и т. д. для доступа к захватам." - global_notice: "Показывать глобальное постоянно отображаемое объявление со СРОЧНЫМИ / АВАРИЙНЫМИ сообщениями всем посетителям. Для скрытия объявления — удалите его содержание (разрешено использование HTML)." + global_notice: "Показывать глобальный постоянно отображаемый баннер со СРОЧНЫМИ (ЧРЕЗВЫЧАЙНЫМИ) сообщениями всем посетителям. Для скрытия баннера удалите его контент (разрешено использование HTML)." disable_system_edit_notifications: "Отключить уведомления системы, если включена настройка «download_remote_images_to_local»." - disable_category_edit_notifications: "Не уведомлять при перемещении тем в другой раздел." + disable_category_edit_notifications: "Не уведомлять при перемещении тем в другую категорию." disable_tags_edit_notifications: "Не уведомлять при изменении тегов темы." notification_consolidation_threshold: "Максимальное количество уведомлений о полученных лайках или запросах на вступление в группу, после которого уведомления будут объединяться в одно. Для отключения параметра установите это значение в 0." - likes_notification_consolidation_window_mins: "Количество минут, по прошествии которых уведомления о полученных лайках будут объединяться в одно уведомление, если достигнуто пороговое значение, которое настраивается в параметре `SiteSetting.notification_consolidation_threshold`." + likes_notification_consolidation_window_mins: "Количество минут, по прошествии которых уведомления о полученных лайках будут объединяться в одно, если достигнуто пороговое значение (настраивается в параметре `SiteSetting.notification_consolidation_threshold`)." automatically_unpin_topics: "Автоматически откреплять полностью прочтённые темы." read_time_word_count: "Количество слов, читаемых за минуту, для расчёта предполагаемого времени чтения." - topic_page_title_includes_category: "Элемент заголовка темы включает название раздела." + topic_page_title_includes_category: "Элемент заголовка темы включает в себя название категории." native_app_install_banner_ios: "Отображать баннер DiscourseHub на устройствах iOS для обычных пользователей (уровень доверия 1 и выше)." native_app_install_banner_android: "Отображать баннер DiscourseHub на устройствах Android для обычных пользователей (уровень доверия 1 и выше)." app_association_android: "Содержимое конечной точки .well-known/assetlinks.json, используемой для Google Digital Asset Links API." app_association_ios: "Содержимое конечной точки apple-app-site-association, используемой для создания универсальных ссылок между этим сайтом и приложениями iOS." - share_anonymized_statistics: "Делиться анонимной статистикой использования." - auto_handle_queued_age: "По истечении указанного здесь количества дней автоматически обрабатывать записи, ожидающие модерации. Жалобы будут игнорироваться. Сообщения в очереди на премодерацию будут отклоняться. Установите значение в 0, если необходимо отключить этот параметр." - penalty_step_hours: "Стандартная продолжительность санкций для заблокированных или замороженных пользователей, указанная в часах. Первое нарушение по умолчанию принимает первое значение, второе нарушение — второе значение и т. д." + share_anonymized_statistics: "Передавать анонимную статистику использования." + auto_handle_queued_age: "По истечении указанного здесь количества дней автоматически обрабатывать записи, ожидающие проверки. Жалобы будут игнорироваться. Записи в очереди на проверку будут отклоняться. Установите значение в 0, если необходимо отключить этот параметр." + penalty_step_hours: "Стандартная продолжительность санкций для заблокированных и замороженных пользователей, указанная в часах. Первое нарушение по умолчанию принимает первое значение, второе нарушение — второе значение и т. д." svg_icon_subset: "Добавить дополнительные значки из коллекции FontAwesome 5, которые вы хотели бы включить в свои ресурсы. Используйте префикс «fa-» для solid-значков, «far-» для regular-значков и «fab-» для brand-значков." max_prints_per_hour_per_user: "Максимальное количество распечаток страницы в час для пользователя. Для отключения параметра установите значение в 0." - full_name_required: "Обязательно указывать ПОЛНОЕ имя в профиле пользователя." + full_name_required: "Обязательно указывать полное имя в профиле пользователя." enable_names: "Отображать полное имя пользователя в его профиле, карточке пользователя и в письмах. Отключите этот параметр, если необходимо полностью скрыть полное имя." - display_name_on_posts: "Отображать полные имена пользователей в их сообщениях в дополнение к их @псевдониму." - show_time_gap_days: "Если между двумя сообщениями в теме прошло указанное здесь количество дней, отображать этот промежуток времени текстом между этими сообщениями." - short_progress_text_threshold: "После достижения в теме указанного здесь количества сообщений, индикатор сообщений будет отображать только текущий номер сообщения. Если вы измените ширину индикатора, вам может потребоваться изменение этого значения." - warn_reviving_old_topic_age: "Отображать предупреждение, когда кто-то пытается ответить в тему, в которой предыдущий ответ был опубликован более указанного здесь количества дней назад. Для отключения параметра установите это значение в 0." + display_name_on_posts: "Отображать полные имена пользователей в их записях в дополнение к @имени_пользователя." + show_time_gap_days: "Если между двумя записями в теме прошло указанное здесь количество дней, отображать этот промежуток времени текстом." + short_progress_text_threshold: "После достижения в теме указанного здесь количества записей индикатор будет отображать только текущий номер записи. Если изменить ширину индикатора, может потребоваться изменить и это значение." + warn_reviving_old_topic_age: "Отображать предупреждение, когда кто-то пытается ответить в теме, где предыдущий ответ был опубликован более указанного здесь количества дней назад. Для отключения параметра установите это значение в 0." autohighlight_all_code: "Принудительно использовать подсветку кода для всех отформатированных блоков кода, даже когда явно не указан язык." - highlighted_languages: "Включить правила подсветки синтаксиса. (Предупреждение: включение слишком большого количества языков может повлиять на производительность), см. https://highlightjs.org/static/demo для демонстрации" + highlighted_languages: "Включить правила подсветки синтаксиса. (Предупреждение: включение слишком большого количества языков может повлиять на производительность.) Демонстрацию см. здесь: https://highlightjs.org/static/demo." show_copy_button_on_codeblocks: "Отображать кнопку в блоке кода для копирования содержимого блока в буфер обмена." embed_any_origin: "Разрешать встраиваемый контент независимо от происхождения. Требуется для мобильных приложений со статическим HTML." - embed_topics_list: "Поддержка HTML встраивания списков тем" + embed_topics_list: "Поддержка встраивания HTML в списках тем" embed_set_canonical_url: "Установить канонический URL для встроенных тем в URL встроенного содержимого." - embed_truncate: "Обрезать встроенные сообщения." + embed_truncate: "Обрезать встроенные записи." embed_unlisted: "Импортированные темы будут отображаться в списке тем только после добавления в них ответа." - embed_support_markdown: "Поддержка форматирования Markdown для встроенных сообщений." + embed_support_markdown: "Поддержка форматирования Markdown для встроенных записей." allowed_embed_selectors: "Разделённый запятыми перечень элементов CSS, которые разрешены для встраивания." allowed_href_schemes: "Схемы, разрешённые в ссылках помимо http и https." - embed_post_limit: "Максимальное количество встроенных сообщений." - embed_username_required: "Для создания темы требуется псевдоним пользователя." + embed_post_limit: "Максимальное количество встроенных записей." + embed_username_required: "Для создания темы требуется имя пользователя." notify_about_flags_after: "Отправлять личное сообщение модератору, если есть жалобы, не обработанные в течение указанного здесь количества часов. Для отключения параметра установите это значение в 0." show_create_topics_notice: "Если общее количество тем на сайте меньше 5, показывать персоналу сообщение с просьбой создать новые темы." delete_drafts_older_than_n_days: "Удалять черновики, которые старше указанного здесь количества дней." diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 1e66aa55e7..03a3f960f0 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -463,10 +463,6 @@ sk: many: "pred takmer %{count} rokmi" other: "pred takmer %{count} rokmi" password_reset: - choose_new: "Napíš nové heslo" - choose: "Napíš heslo" - update: "Aktualizujte heslo" - save: "Nastavte heslo" title: "Obnoviť heslo" success: "Heslo bolo úspešne zmenené a ste prihlasený do systému. " success_unapproved: "Heslo bolo úspešne zmenené." diff --git a/config/locales/server.sl.yml b/config/locales/server.sl.yml index 6a6ea595fc..b1862f6972 100644 --- a/config/locales/server.sl.yml +++ b/config/locales/server.sl.yml @@ -458,10 +458,6 @@ sl: few: "skoraj %{count} leta nazaj" other: "skoraj %{count} let nazaj" password_reset: - choose_new: "Vnesite novo geslo" - choose: "Vnesite geslo" - update: "Spremenite geslo" - save: "Nastavi geslo" title: "Zamenjaj geslo" success: "Uspešno ste zamenjali geslo in ste sedaj prijavljeni." success_unapproved: "Uspešno ste zamenjali geslo." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index df054c4518..aa17ac067c 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -300,10 +300,6 @@ sq: one: "pothuajse %{count} vit më parë" other: "pothuajse %{count} vite më parë" password_reset: - choose_new: "Zgjidhni një fjalëkalim të ri" - choose: "Zgjidhni një fjalëkalim të ri" - update: "Rifresko Fjalëkalimin" - save: "Vendos Fjalëkalim" title: "Rivendos Fjalëkalimin" success: "Ndryshimi i fjalëkalimit u krye me sukses dhe tashmë ju jeni futur në faqe." success_unapproved: "Ndryshimi i fjalëkalimit u krye me sukses." diff --git a/config/locales/server.sr.yml b/config/locales/server.sr.yml index e68e8091ea..698c12ab9f 100644 --- a/config/locales/server.sr.yml +++ b/config/locales/server.sr.yml @@ -215,7 +215,6 @@ sr: few: "pre %{count} mesec dana" other: "pre %{count} mesec dana" password_reset: - save: "Postavi Šifru" title: "Resetujte Šifru" change_email: max_secondary_emails_error: "Dosegli ste maksimalni dozvoljeni broj sekundarne e-pošte." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index a99e703238..bea6eadc90 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -771,10 +771,6 @@ sv: other: "nästan %{count} år sedan" password_reset: no_token: 'Hoppsan! Länken du använde fungerar inte längre. Du kan logga in nu. Om du har glömt ditt lösenord kan du begära en länk för att återställa det.' - choose_new: "Välj ett nytt lösenord" - choose: "Välj ett lösenord" - update: "Uppdatera lösenord" - save: "Välj lösenord" title: "Återställ lösenord" success: "Du har lyckats med att byta ditt lösenord och är nu inloggad." success_unapproved: "Du lyckades med att byta ditt lösenord." @@ -970,7 +966,7 @@ sv: mailing_list_mode: "Stäng av utskicksläge" all: "Skicka mig inga fler e-postmeddelanden från %{sitename}" different_user_description: "Du är för tillfället inloggad som en annan användare än den som vi skickade e-post till. Logga ut, eller aktivera anonymt läge, och försök igen." - not_found_description: Tyvärr kunde vi inte hitta den avprenumerationen. Kan länken i din e-post vara för gammal och ha löpt ut?" + not_found_description: "Tyvärr kunde vi inte hitta den avprenumerationen. Kan länken i din e-post vara för gammal och ha löpt ut?" user_not_found_description: "Tyvärr kunde vi inte hitta någon användare för den här prenumerationen. Du försöker troligen avprenumerera ett konto som inte längre finns." log_out: "Logga ut" submit: "Spara inställningar" diff --git a/config/locales/server.sw.yml b/config/locales/server.sw.yml index 11480c7340..2b11d9fff1 100644 --- a/config/locales/server.sw.yml +++ b/config/locales/server.sw.yml @@ -406,10 +406,6 @@ sw: one: "siku %{count} iliyopita" other: "siku %{count} zilizopita" password_reset: - choose_new: "Chagua nywila" - choose: "Chagua nywila" - update: "Sasisha Nywila" - save: "Seti Nywila" title: "Weka Upya Nywila" success: "Umebadilisha nywila yako kwa mafanikio na sasa umeingia." success_unapproved: "Umebadilisha nywila yako kwa mafanikio." diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index dd23443f7d..b434d6b3e0 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -275,8 +275,6 @@ te: one: "అటో ఇటో ఒక సంవత్సరం వెనుక" other: "అటోఇటో %{count} సంవత్సరాల ముందు" password_reset: - update: "సంకేతపదం ఉన్నతీకరించండి" - save: "సంకేతపదం అమర్చండి" title: "సంకేతపదం రీసెట్ చెయ్యండి" success: "మీరు విజయవంతంగా మీ సంకేతపదం మార్చారు ఇంకా ఇప్పుడు లాగిన్ అయ్యారు కూడా." success_unapproved: "మీరు విజయవంతంగా సంకేతపదం మార్చారు." diff --git a/config/locales/server.th.yml b/config/locales/server.th.yml index 6b57f17916..5fbbe64b01 100644 --- a/config/locales/server.th.yml +++ b/config/locales/server.th.yml @@ -159,9 +159,6 @@ th: x_months: other: "%{count}เดือนที่แล้ว" password_reset: - choose_new: "เลือกรหัสผู้ใช้ใหม่" - choose: "เลือกรหัสผู้ใช้" - save: "ตั้งรหัสผ่าน" title: "รีเซ็ทรหัสผ่าน" change_email: please_continue: "ดำเนินการต่อไปยัง %{site_name}" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index c69bc2d828..005964ae69 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -764,10 +764,6 @@ tr_TR: other: "neredeyse %{count} yıl önce" password_reset: no_token: 'Hay aksi! Kullandığınız bağlantı artık çalışmıyor. Şimdi Giriş yapabilirsiniz. Şifrenizi unuttuysanız sıfırlamak için bağlantı talep edebilirsiniz.' - choose_new: "Yeni bir şifre seçin" - choose: "Şifre seçin" - update: "Şifreyi güncelleyin" - save: "Şifre Belirleyin" title: "Şifreyi Sıfırlayın" success: "Şifrenizi başarıyla değiştirdiniz ve giriş yaptınız." success_unapproved: "Şifrenizi başarıyla değiştirdiniz." @@ -963,7 +959,7 @@ tr_TR: mailing_list_mode: "Posta listesi modunu kapat" all: "Bana %{sitename} adresinden herhangi bir posta gönderme." different_user_description: "Şu anda e-posta gönderdiğimiz kullanıcıdan farklı bir kullanıcı olarak giriş yapmış durumdasınız. Lütfen çıkış yapın veya anonim moda girip tekrar deneyin." - not_found_description: Üzgünüz, bu aboneliği bulamadık. E-postanızdaki bağlantı çok eski ve süresi dolmuş olabilir mi?" + not_found_description: "Üzgünüz, bu aboneliği bulamadık. E-postanızdaki bağlantı çok eski ve süresi dolmuş olabilir mi?" user_not_found_description: "Üzgünüz, bu abonelik için bir kullanıcı bulamadık. Muhtemelen artık var olmayan bir hesabın aboneliğini iptal etmeye çalışıyorsunuz." log_out: "Çıkış Yap" submit: "Tercihleri kaydet" diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 6486bcd646..2034cf16b4 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -869,10 +869,6 @@ uk: other: "майже %{count} років тому" password_reset: no_token: 'Упс! Посилання, яке ви використовували, більше не працює. Ви можете Увійти зараз. Якщо ви забули пароль, ви можете запросити посилання, щоб скинути його.' - choose_new: "Виберіть новий пароль" - choose: "Виберіть пароль" - update: "Оновити пароль" - save: "Встановити пароль" title: "Скинути пароль" success: "Ви успішно змінили свій пароль і зараз Ви в системі." success_unapproved: "Ви успішно змінили свій пароль." diff --git a/config/locales/server.ur.yml b/config/locales/server.ur.yml index 23979e09fa..c823dde48e 100644 --- a/config/locales/server.ur.yml +++ b/config/locales/server.ur.yml @@ -734,10 +734,6 @@ ur: one: "تقریباً %{count} سال قبل" other: "تقریباً %{count} سال قبل" password_reset: - choose_new: "نیا پاسورڈ منتخب کریں" - choose: "پاسورڈ منتخب کریں" - update: "پاسورڈ اَپ ڈیٹ کریں" - save: "پاسورڈ رکھیں" title: "پاسورڈ رِی سَیٹ کریں" success: "آپ نے کامیابی سے اپنا پاسورڈ تبدیل کر لیا اور اب آپ لاگ ان ہوے وے ہیں۔" success_unapproved: "آپ نے کامیابی سے اپنا پاسورڈ تبدیل کر لیا۔" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index b558c02026..756bb2074f 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -605,10 +605,6 @@ vi: almost_x_years: other: "gần %{count} năm trước" password_reset: - choose_new: "Chọn một mật khẩu mới" - choose: "Chọn một mật khẩu" - update: "Cập nhật mật khẩu" - save: "Nhập mật khẩu" title: "Thiết lập lại mật khẩu" success: "Bạn đã thay đổi mật khẩu thành công và đã được đăng nhập." success_unapproved: "Bạn đã thay đổi mật khẩu thành công." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 103ba05214..8d104d29b7 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -717,10 +717,6 @@ zh_CN: other: "将近 %{count} 年前" password_reset: no_token: '糟糕!您使用的链接不再有效。您现在可以登录。如果您忘记了密码,可以请求链接来重置密码。' - choose_new: "选择一个新密码" - choose: "选择一个密码" - update: "更新密码" - save: "设置密码" title: "重置密码" success: "您已成功更改密码,现已登录。" success_unapproved: "您已成功更改密码。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 8004cfe149..faac3a894b 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -541,10 +541,6 @@ zh_TW: almost_x_years: other: "約 %{count} 年前" password_reset: - choose_new: "選擇一個新密碼" - choose: "選擇一個密碼" - update: "更新密碼" - save: "設定密碼" title: "重設密碼" success: "密碼修改成功,現在已為你登入。" success_unapproved: "你的密碼已成功修改。" diff --git a/config/puma.rb b/config/puma.rb index 0fea9a2487..c349148f57 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -if ENV['RAILS_ENV'] == 'production' - +if ENV["RAILS_ENV"] == "production" # First, you need to change these below to your situation. - APP_ROOT = ENV["APP_ROOT"] || '/home/discourse/discourse' + APP_ROOT = ENV["APP_ROOT"] || "/home/discourse/discourse" num_workers = ENV["NUM_WEBS"].to_i > 0 ? ENV["NUM_WEBS"].to_i : 4 # Second, you can choose how many threads that you are going to run at same time. @@ -16,5 +15,4 @@ if ENV['RAILS_ENV'] == 'production' pidfile "#{APP_ROOT}/tmp/pids/puma.pid" state_path "#{APP_ROOT}/tmp/pids/puma.state" preload_app! - end diff --git a/config/routes.rb b/config/routes.rb index baf501a6f9..abdf514ef8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,16 +4,27 @@ require "sidekiq/web" require "mini_scheduler/web" # The following constants have been replaced with `RouteFormat` and are deprecated. -USERNAME_ROUTE_FORMAT = /[%\w.\-]+?/ unless defined? USERNAME_ROUTE_FORMAT -BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined? BACKUP_ROUTE_FORMAT +USERNAME_ROUTE_FORMAT = /[%\w.\-]+?/ unless defined?(USERNAME_ROUTE_FORMAT) +BACKUP_ROUTE_FORMAT = /.+\.(sql\.gz|tar\.gz|tgz)/i unless defined?(BACKUP_ROUTE_FORMAT) Discourse::Application.routes.draw do - def patch(*) end # Disable PATCH requests + def patch(*) + end # Disable PATCH requests - scope path: nil, constraints: { format: /(json|html|\*\/\*)/ } do - relative_url_root = (defined?(Rails.configuration.relative_url_root) && Rails.configuration.relative_url_root) ? Rails.configuration.relative_url_root + '/' : '/' + scope path: nil, constraints: { format: %r{(json|html|\*/\*)} } do + relative_url_root = + ( + if ( + defined?(Rails.configuration.relative_url_root) && + Rails.configuration.relative_url_root + ) + Rails.configuration.relative_url_root + "/" + else + "/" + end + ) - match "/404", to: "exceptions#not_found", via: [:get, :post] + match "/404", to: "exceptions#not_found", via: %i[get post] get "/404-body" => "exceptions#not_found_body" get "/bootstrap" => "bootstrap#index" @@ -22,8 +33,8 @@ Discourse::Application.routes.draw do end post "webhooks/aws" => "webhooks#aws" - post "webhooks/mailgun" => "webhooks#mailgun" - post "webhooks/mailjet" => "webhooks#mailjet" + post "webhooks/mailgun" => "webhooks#mailgun" + post "webhooks/mailjet" => "webhooks#mailjet" post "webhooks/mandrill" => "webhooks#mandrill" get "webhooks/mandrill" => "webhooks#mandrill_head" post "webhooks/postmark" => "webhooks#postmark" @@ -32,7 +43,7 @@ Discourse::Application.routes.draw do scope path: nil, format: true, constraints: { format: :xml } do resources :sitemap, only: [:index] - get "/sitemap_:page" => "sitemap#page", page: /[1-9][0-9]*/ + get "/sitemap_:page" => "sitemap#page", :page => /[1-9][0-9]*/ get "/sitemap_recent" => "sitemap#recent" get "/news" => "sitemap#news" end @@ -43,15 +54,13 @@ Discourse::Application.routes.draw do mount Logster::Web => "/logs" else # only allow sidekiq in master site - mount Sidekiq::Web => "/sidekiq", constraints: AdminConstraint.new(require_master: true) - mount Logster::Web => "/logs", constraints: AdminConstraint.new + mount Sidekiq::Web => "/sidekiq", :constraints => AdminConstraint.new(require_master: true) + mount Logster::Web => "/logs", :constraints => AdminConstraint.new end end resources :about do - collection do - get "live_post_counts" - end + collection { get "live_post_counts" } end get "finish-installation" => "finish_installation#index" @@ -76,25 +85,23 @@ Discourse::Application.routes.draw do get "emoji" end - get "site/basic-info" => 'site#basic_info' - get "site/statistics" => 'site#statistics' + get "site/basic-info" => "site#basic_info" + get "site/statistics" => "site#statistics" get "srv/status" => "forums#status" get "wizard" => "wizard#index" - get 'wizard/steps' => 'steps#index' - get 'wizard/steps/:id' => "wizard#index" - put 'wizard/steps/:id' => "steps#update" + get "wizard/steps" => "steps#index" + get "wizard/steps/:id" => "wizard#index" + put "wizard/steps/:id" => "steps#update" namespace :admin, constraints: StaffConstraint.new do get "" => "admin#index" - get 'plugins' => 'plugins#index' + get "plugins" => "plugins#index" resources :site_settings, constraints: AdminConstraint.new do - collection do - get "category/:id" => "site_settings#index" - end + collection { get "category/:id" => "site_settings#index" } put "user_count" => "site_settings#user_count" end @@ -111,13 +118,11 @@ Discourse::Application.routes.draw do end end resources :groups, except: [:create], constraints: AdminConstraint.new do - collection do - put "automatic_membership_count" => "groups#automatic_membership_count" - end + collection { put "automatic_membership_count" => "groups#automatic_membership_count" } end - get "groups/:type" => "groups#show", constraints: AdminConstraint.new - get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new + get "groups/:type" => "groups#show", :constraints => AdminConstraint.new + get "groups/:type/:id" => "groups#show", :constraints => AdminConstraint.new resources :users, id: RouteFormat.username, except: [:show] do collection do @@ -145,8 +150,8 @@ Discourse::Application.routes.draw do put "trust_level" put "trust_level_lock" put "primary_group" - post "groups" => "users#add_group", constraints: AdminConstraint.new - delete "groups/:group_id" => "users#remove_group", constraints: AdminConstraint.new + post "groups" => "users#add_group", :constraints => AdminConstraint.new + delete "groups/:group_id" => "users#remove_group", :constraints => AdminConstraint.new get "badges" get "leader_requirements" => "users#tl3_requirements" get "tl3_requirements" @@ -156,12 +161,16 @@ Discourse::Application.routes.draw do put "disable_second_factor" delete "sso_record" end - get "users/:id.json" => 'users#show', defaults: { format: 'json' } - get 'users/:id/:username' => 'users#show', constraints: { username: RouteFormat.username }, as: :user_show - get 'users/:id/:username/badges' => 'users#show' - get 'users/:id/:username/tl3_requirements' => 'users#show' + get "users/:id.json" => "users#show", :defaults => { format: "json" } + get "users/:id/:username" => "users#show", + :constraints => { + username: RouteFormat.username, + }, + :as => :user_show + get "users/:id/:username/badges" => "users#show" + get "users/:id/:username/tl3_requirements" => "users#show" - post "users/sync_sso" => "users#sync_sso", constraints: AdminConstraint.new + post "users/sync_sso" => "users#sync_sso", :constraints => AdminConstraint.new resources :impersonate, constraints: AdminConstraint.new @@ -186,28 +195,29 @@ Discourse::Application.routes.draw do end scope "/logs" do - resources :staff_action_logs, only: [:index] - get 'staff_action_logs/:id/diff' => 'staff_action_logs#diff' - resources :screened_emails, only: [:index, :destroy] - resources :screened_ip_addresses, only: [:index, :create, :update, :destroy] - resources :screened_urls, only: [:index] - resources :search_logs, only: [:index] - get 'search_logs/term/' => 'search_logs#term' + resources :staff_action_logs, only: [:index] + get "staff_action_logs/:id/diff" => "staff_action_logs#diff" + resources :screened_emails, only: %i[index destroy] + resources :screened_ip_addresses, only: %i[index create update destroy] + resources :screened_urls, only: [:index] + resources :search_logs, only: [:index] + get "search_logs/term/" => "search_logs#term" end get "/logs" => "staff_action_logs#index" # alias - get '/logs/watched_words', to: redirect(relative_url_root + 'admin/customize/watched_words') - get '/logs/watched_words/*path', to: redirect(relative_url_root + 'admin/customize/watched_words/%{path}') + get "/logs/watched_words", to: redirect(relative_url_root + "admin/customize/watched_words") + get "/logs/watched_words/*path", + to: redirect(relative_url_root + "admin/customize/watched_words/%{path}") - get "customize" => "color_schemes#index", constraints: AdminConstraint.new - get "customize/themes" => "themes#index", constraints: AdminConstraint.new - get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new - get "customize/colors/:id" => "color_schemes#index", constraints: AdminConstraint.new - get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new - get "customize/embedding" => "embedding#show", constraints: AdminConstraint.new - put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new + get "customize" => "color_schemes#index", :constraints => AdminConstraint.new + get "customize/themes" => "themes#index", :constraints => AdminConstraint.new + get "customize/colors" => "color_schemes#index", :constraints => AdminConstraint.new + get "customize/colors/:id" => "color_schemes#index", :constraints => AdminConstraint.new + get "customize/permalinks" => "permalinks#index", :constraints => AdminConstraint.new + get "customize/embedding" => "embedding#show", :constraints => AdminConstraint.new + put "customize/embedding" => "embedding#update", :constraints => AdminConstraint.new resources :themes, constraints: AdminConstraint.new do member do @@ -225,33 +235,42 @@ Discourse::Application.routes.draw do resources :user_fields, constraints: AdminConstraint.new resources :emojis, constraints: AdminConstraint.new - get 'themes/:id/:target/:field_name/edit' => 'themes#index' - get 'themes/:id' => 'themes#index' + get "themes/:id/:target/:field_name/edit" => "themes#index" + get "themes/:id" => "themes#index" get "themes/:id/export" => "themes#export" # They have periods in their URLs often: - get 'site_texts' => 'site_texts#index' - get 'site_texts/:id.json' => 'site_texts#show', constraints: { id: /[\w.\-\+\%\&]+/i } - get 'site_texts/:id' => 'site_texts#show', constraints: { id: /[\w.\-\+\%\&]+/i } - put 'site_texts/:id.json' => 'site_texts#update', constraints: { id: /[\w.\-\+\%\&]+/i } - put 'site_texts/:id' => 'site_texts#update', constraints: { id: /[\w.\-\+\%\&]+/i } - delete 'site_texts/:id.json' => 'site_texts#revert', constraints: { id: /[\w.\-\+\%\&]+/i } - delete 'site_texts/:id' => 'site_texts#revert', constraints: { id: /[\w.\-\+\%\&]+/i } + get "site_texts" => "site_texts#index" + get "site_texts/:id.json" => "site_texts#show", :constraints => { id: /[\w.\-\+\%\&]+/i } + get "site_texts/:id" => "site_texts#show", :constraints => { id: /[\w.\-\+\%\&]+/i } + put "site_texts/:id.json" => "site_texts#update", :constraints => { id: /[\w.\-\+\%\&]+/i } + put "site_texts/:id" => "site_texts#update", :constraints => { id: /[\w.\-\+\%\&]+/i } + delete "site_texts/:id.json" => "site_texts#revert", + :constraints => { + id: /[\w.\-\+\%\&]+/i, + } + delete "site_texts/:id" => "site_texts#revert", :constraints => { id: /[\w.\-\+\%\&]+/i } - get 'reseed' => 'site_texts#get_reseed_options' - post 'reseed' => 'site_texts#reseed' + get "reseed" => "site_texts#get_reseed_options" + post "reseed" => "site_texts#reseed" - get 'email_templates' => 'email_templates#index' - get 'email_templates/(:id)' => 'email_templates#show', constraints: { id: /[0-9a-z_.]+/ } - put 'email_templates/(:id)' => 'email_templates#update', constraints: { id: /[0-9a-z_.]+/ } - delete 'email_templates/(:id)' => 'email_templates#revert', constraints: { id: /[0-9a-z_.]+/ } + get "email_templates" => "email_templates#index" + get "email_templates/(:id)" => "email_templates#show", :constraints => { id: /[0-9a-z_.]+/ } + put "email_templates/(:id)" => "email_templates#update", + :constraints => { + id: /[0-9a-z_.]+/, + } + delete "email_templates/(:id)" => "email_templates#revert", + :constraints => { + id: /[0-9a-z_.]+/, + } - get 'robots' => 'robots_txt#show' - put 'robots.json' => 'robots_txt#update' - delete 'robots.json' => 'robots_txt#reset' + get "robots" => "robots_txt#show" + put "robots.json" => "robots_txt#update" + delete "robots.json" => "robots_txt#reset" - resource :email_style, only: [:show, :update] - get 'email_style/:field' => 'email_styles#show', constraints: { field: /html|css/ } + resource :email_style, only: %i[show update] + get "email_style/:field" => "email_styles#show", :constraints => { field: /html|css/ } end resources :embeddable_hosts, constraints: AdminConstraint.new @@ -259,7 +278,7 @@ Discourse::Application.routes.draw do resources :permalinks, constraints: AdminConstraint.new scope "/customize" do - resources :watched_words, only: [:index, :create, :update, :destroy] do + resources :watched_words, only: %i[index create update destroy] do collection do get "action/:id" => "watched_words#index" get "action/:id/download" => "watched_words#download" @@ -280,17 +299,13 @@ Discourse::Application.routes.draw do put "dashboard/mark-new-features-as-seen" => "dashboard#mark_new_features_as_seen" resources :dashboard, only: [:index] do - collection do - get "problems" - end + collection { get "problems" } end resources :api, only: [:index], constraints: AdminConstraint.new do collection do - resources :keys, controller: 'api', only: [:index, :show, :update, :create, :destroy] do - collection do - get 'scopes' => 'api#scopes' - end + resources :keys, controller: "api", only: %i[index show update create destroy] do + collection { get "scopes" => "api#scopes" } member do post "revoke" => "api#revoke_key" @@ -299,26 +314,27 @@ Discourse::Application.routes.draw do end resources :web_hooks - get 'web_hook_events/:id' => 'web_hooks#list_events', as: :web_hook_events - get 'web_hooks/:id/events/bulk' => 'web_hooks#bulk_events' - post 'web_hooks/:web_hook_id/events/:event_id/redeliver' => 'web_hooks#redeliver_event' - post 'web_hooks/:id/ping' => 'web_hooks#ping' + get "web_hook_events/:id" => "web_hooks#list_events", :as => :web_hook_events + get "web_hooks/:id/events/bulk" => "web_hooks#bulk_events" + post "web_hooks/:web_hook_id/events/:event_id/redeliver" => "web_hooks#redeliver_event" + post "web_hooks/:id/ping" => "web_hooks#ping" end end - resources :backups, only: [:index, :create], constraints: AdminConstraint.new do + resources :backups, only: %i[index create], constraints: AdminConstraint.new do member do - get "" => "backups#show", constraints: { id: RouteFormat.backup } - put "" => "backups#email", constraints: { id: RouteFormat.backup } - delete "" => "backups#destroy", constraints: { id: RouteFormat.backup } - post "restore" => "backups#restore", constraints: { id: RouteFormat.backup } + get "" => "backups#show", :constraints => { id: RouteFormat.backup } + put "" => "backups#email", :constraints => { id: RouteFormat.backup } + delete "" => "backups#destroy", :constraints => { id: RouteFormat.backup } + post "restore" => "backups#restore", :constraints => { id: RouteFormat.backup } end collection do # multipart uploads - post "create-multipart" => "backups#create_multipart", format: :json - post "complete-multipart" => "backups#complete_multipart", format: :json - post "abort-multipart" => "backups#abort_multipart", format: :json - post "batch-presign-multipart-parts" => "backups#batch_presign_multipart_parts", format: :json + post "create-multipart" => "backups#create_multipart", :format => :json + post "complete-multipart" => "backups#complete_multipart", :format => :json + post "abort-multipart" => "backups#abort_multipart", :format => :json + post "batch-presign-multipart-parts" => "backups#batch_presign_multipart_parts", + :format => :json get "logs" => "backups#logs" get "status" => "backups#status" @@ -340,39 +356,41 @@ Discourse::Application.routes.draw do post "preview" => "badges#preview" end end - end # admin namespace - get "email/unsubscribe/:key" => "email#unsubscribe", as: "email_unsubscribe" - get "email/unsubscribed" => "email#unsubscribed", as: "email_unsubscribed" - post "email/unsubscribe/:key" => "email#perform_unsubscribe", as: "email_perform_unsubscribe" + get "email/unsubscribe/:key" => "email#unsubscribe", :as => "email_unsubscribe" + get "email/unsubscribed" => "email#unsubscribed", :as => "email_unsubscribed" + post "email/unsubscribe/:key" => "email#perform_unsubscribe", :as => "email_perform_unsubscribe" get "extra-locales/:bundle" => "extra_locales#show" - resources :session, id: RouteFormat.username, only: [:create, :destroy, :become] do - if !Rails.env.production? - get 'become' - end + resources :session, id: RouteFormat.username, only: %i[create destroy become] do + get "become" if !Rails.env.production? - collection do - post "forgot_password" - end + collection { post "forgot_password" } end get "review" => "reviewables#index" # For ember app - get "review/:reviewable_id" => "reviewables#show", constraints: { reviewable_id: /\d+/ } - get "review/:reviewable_id/explain" => "reviewables#explain", constraints: { reviewable_id: /\d+/ } + get "review/:reviewable_id" => "reviewables#show", :constraints => { reviewable_id: /\d+/ } + get "review/:reviewable_id/explain" => "reviewables#explain", + :constraints => { + reviewable_id: /\d+/, + } get "review/count" => "reviewables#count" get "review/topics" => "reviewables#topics" get "review/settings" => "reviewables#settings" - get "review/user-menu-list" => "reviewables#user_menu_list", format: :json + get "review/user-menu-list" => "reviewables#user_menu_list", :format => :json put "review/settings" => "reviewables#settings" - put "review/:reviewable_id/perform/:action_id" => "reviewables#perform", constraints: { - reviewable_id: /\d+/, - action_id: /[a-z\_]+/ - } - put "review/:reviewable_id" => "reviewables#update", constraints: { reviewable_id: /\d+/ } - delete "review/:reviewable_id" => "reviewables#destroy", constraints: { reviewable_id: /\d+/ } + put "review/:reviewable_id/perform/:action_id" => "reviewables#perform", + :constraints => { + reviewable_id: /\d+/, + action_id: /[a-z\_]+/, + } + put "review/:reviewable_id" => "reviewables#update", :constraints => { reviewable_id: /\d+/ } + delete "review/:reviewable_id" => "reviewables#destroy", + :constraints => { + reviewable_id: /\d+/, + } resources :reviewable_claimed_topics @@ -384,8 +402,8 @@ Discourse::Application.routes.draw do get "session/hp" => "session#get_honeypot_value" get "session/email-login/:token" => "session#email_login_info" post "session/email-login/:token" => "session#email_login" - get "session/otp/:token" => "session#one_time_password", constraints: { token: /[0-9a-f]+/ } - post "session/otp/:token" => "session#one_time_password", constraints: { token: /[0-9a-f]+/ } + get "session/otp/:token" => "session#one_time_password", :constraints => { token: /[0-9a-f]+/ } + post "session/otp/:token" => "session#one_time_password", :constraints => { token: /[0-9a-f]+/ } get "session/2fa" => "session#second_factor_auth_show" post "session/2fa" => "session#second_factor_auth_perform" if Rails.env.test? @@ -398,30 +416,31 @@ Discourse::Application.routes.draw do resources :static post "login" => "static#enter" - get "login" => "static#show", id: "login" - get "password-reset" => "static#show", id: "password_reset" - get "faq" => "static#show", id: "faq" - get "tos" => "static#show", id: "tos", as: 'tos' - get "privacy" => "static#show", id: "privacy", as: 'privacy' - get "signup" => "static#show", id: "signup" - get "login-preferences" => "static#show", id: "login" + get "login" => "static#show", :id => "login" + get "password-reset" => "static#show", :id => "password_reset" + get "faq" => "static#show", :id => "faq" + get "tos" => "static#show", :id => "tos", :as => "tos" + get "privacy" => "static#show", :id => "privacy", :as => "privacy" + get "signup" => "static#show", :id => "signup" + get "login-preferences" => "static#show", :id => "login" - %w{guidelines rules conduct}.each do |faq_alias| - get faq_alias => "static#show", id: "guidelines", as: faq_alias + %w[guidelines rules conduct].each do |faq_alias| + get faq_alias => "static#show", :id => "guidelines", :as => faq_alias end - get "my/*path", to: 'users#my_redirect' - get ".well-known/change-password", to: redirect(relative_url_root + 'my/preferences/security', status: 302) + get "my/*path", to: "users#my_redirect" + get ".well-known/change-password", + to: redirect(relative_url_root + "my/preferences/security", status: 302) - get "user-cards" => "users#cards", format: :json - get "directory-columns" => "directory_columns#index", format: :json - get "edit-directory-columns" => "edit_directory_columns#index", format: :json - put "edit-directory-columns" => "edit_directory_columns#update", format: :json + get "user-cards" => "users#cards", :format => :json + get "directory-columns" => "directory_columns#index", :format => :json + get "edit-directory-columns" => "edit_directory_columns#index", :format => :json + put "edit-directory-columns" => "edit_directory_columns#update", :format => :json - %w{users u}.each_with_index do |root_path, index| - get "#{root_path}" => "users#index", constraints: { format: 'html' } + %w[users u].each_with_index do |root_path, index| + get "#{root_path}" => "users#index", :constraints => { format: "html" } - resources :users, except: [:index, :new, :show, :update, :destroy], path: root_path do + resources :users, except: %i[index new show update destroy], path: root_path do collection do get "check_username" get "check_email" @@ -431,8 +450,10 @@ Discourse::Application.routes.draw do post "#{root_path}/second_factors" => "users#list_second_factors" put "#{root_path}/second_factor" => "users#update_second_factor" - post "#{root_path}/create_second_factor_security_key" => "users#create_second_factor_security_key" - post "#{root_path}/register_second_factor_security_key" => "users#register_second_factor_security_key" + post "#{root_path}/create_second_factor_security_key" => + "users#create_second_factor_security_key" + post "#{root_path}/register_second_factor_security_key" => + "users#register_second_factor_security_key" put "#{root_path}/security_key" => "users#update_security_key" post "#{root_path}/create_second_factor_totp" => "users#create_second_factor_totp" post "#{root_path}/enable_second_factor_totp" => "users#enable_second_factor_totp" @@ -446,19 +467,40 @@ Discourse::Application.routes.draw do put "#{root_path}/admin-login" => "users#admin_login" post "#{root_path}/toggle-anon" => "users#toggle_anon" post "#{root_path}/read-faq" => "users#read_faq" - get "#{root_path}/recent-searches" => "users#recent_searches", constraints: { format: 'json' } - delete "#{root_path}/recent-searches" => "users#reset_recent_searches", constraints: { format: 'json' } + get "#{root_path}/recent-searches" => "users#recent_searches", + :constraints => { + format: "json", + } + delete "#{root_path}/recent-searches" => "users#reset_recent_searches", + :constraints => { + format: "json", + } get "#{root_path}/search/users" => "users#search_users" - get({ "#{root_path}/account-created/" => "users#account_created" }.merge(index == 1 ? { as: :users_account_created } : { as: :old_account_created })) + get( + { "#{root_path}/account-created/" => "users#account_created" }.merge( + index == 1 ? { as: :users_account_created } : { as: :old_account_created }, + ), + ) get "#{root_path}/account-created/resent" => "users#account_created" get "#{root_path}/account-created/edit-email" => "users#account_created" - get({ "#{root_path}/password-reset/:token" => "users#password_reset_show" }.merge(index == 1 ? { as: :password_reset_token } : {})) - get "#{root_path}/confirm-email-token/:token" => "users#confirm_email_token", constraints: { format: 'json' } + get( + { "#{root_path}/password-reset/:token" => "users#password_reset_show" }.merge( + index == 1 ? { as: :password_reset_token } : {}, + ), + ) + get "#{root_path}/confirm-email-token/:token" => "users#confirm_email_token", + :constraints => { + format: "json", + } put "#{root_path}/password-reset/:token" => "users#password_reset_update" get "#{root_path}/activate-account/:token" => "users#activate_account" - put({ "#{root_path}/activate-account/:token" => "users#perform_account_activation" }.merge(index == 1 ? { as: 'perform_activate_account' } : {})) + put( + { "#{root_path}/activate-account/:token" => "users#perform_account_activation" }.merge( + index == 1 ? { as: "perform_activate_account" } : {}, + ), + ) get "#{root_path}/confirm-old-email/:token" => "users_email#show_confirm_old_email" put "#{root_path}/confirm-old-email" => "users_email#confirm_old_email" @@ -466,105 +508,415 @@ Discourse::Application.routes.draw do get "#{root_path}/confirm-new-email/:token" => "users_email#show_confirm_new_email" put "#{root_path}/confirm-new-email" => "users_email#confirm_new_email" - get({ - "#{root_path}/confirm-admin/:token" => "users#confirm_admin", - constraints: { token: /[0-9a-f]+/ } - }.merge(index == 1 ? { as: 'confirm_admin' } : {})) - post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", constraints: { token: /[0-9a-f]+/ } - get "#{root_path}/:username/private-messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/messages" => "user_actions#private_messages", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } - get "#{root_path}/:username/messages/group/:group_name/:filter" => "user_actions#private_messages", constraints: { username: RouteFormat.username, group_name: RouteFormat.username } - get "#{root_path}/:username/messages/tags/:tag_id" => "list#private_messages_tag", constraints: { username: RouteFormat.username } - get "#{root_path}/:username.json" => "users#show", constraints: { username: RouteFormat.username }, defaults: { format: :json } - get({ "#{root_path}/:username" => "users#show", constraints: { username: RouteFormat.username } }.merge(index == 1 ? { as: 'user' } : {})) - put "#{root_path}/:username" => "users#update", constraints: { username: RouteFormat.username }, defaults: { format: :json } - get "#{root_path}/:username/emails" => "users#check_emails", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/sso-email" => "users#check_sso_email", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/sso-payload" => "users#check_sso_payload", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/email" => "users_email#index", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/account" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/security" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/profile" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/emails" => "users#preferences", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", format: :json, constraints: { username: RouteFormat.username } - delete "#{root_path}/:username/preferences/email" => "users#destroy_email", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/notifications" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/tracking" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/categories" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/users" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/tags" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/interface" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/sidebar" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/apps" => "users#preferences", constraints: { username: RouteFormat.username } - post "#{root_path}/:username/preferences/email" => "users_email#create", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/email" => "users_email#update", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/badge_title" => "users#preferences", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/username" => "users#preferences", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/username" => "users#username", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/second-factor" => "users#preferences", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/preferences/second-factor-backup" => "users#preferences", constraints: { username: RouteFormat.username } - delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", constraints: { username: RouteFormat.username } - post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account", constraints: { username: RouteFormat.username } - post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/staff-info" => "users#staff_info", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/summary" => "users#summary", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/notification_level" => "users#notification_level", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/invited" => "users#invited", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/invited/:filter" => "users#invited", constraints: { username: RouteFormat.username } + get( + { + "#{root_path}/confirm-admin/:token" => "users#confirm_admin", + :constraints => { + token: /[0-9a-f]+/, + }, + }.merge(index == 1 ? { as: "confirm_admin" } : {}), + ) + post "#{root_path}/confirm-admin/:token" => "users#confirm_admin", + :constraints => { + token: /[0-9a-f]+/, + } + get "#{root_path}/:username/private-messages" => "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/private-messages/:filter" => "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/messages" => "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/messages/:filter" => "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/messages/group/:group_name" => "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + group_name: RouteFormat.username, + } + get "#{root_path}/:username/messages/group/:group_name/:filter" => + "user_actions#private_messages", + :constraints => { + username: RouteFormat.username, + group_name: RouteFormat.username, + } + get "#{root_path}/:username/messages/tags/:tag_id" => "list#private_messages_tag", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username.json" => "users#show", + :constraints => { + username: RouteFormat.username, + }, + :defaults => { + format: :json, + } + get( + { + "#{root_path}/:username" => "users#show", + :constraints => { + username: RouteFormat.username, + }, + }.merge(index == 1 ? { as: "user" } : {}), + ) + put "#{root_path}/:username" => "users#update", + :constraints => { + username: RouteFormat.username, + }, + :defaults => { + format: :json, + } + get "#{root_path}/:username/emails" => "users#check_emails", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/sso-email" => "users#check_sso_email", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/sso-payload" => "users#check_sso_payload", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/email" => "users_email#index", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/account" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/security" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/profile" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/emails" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/primary-email" => "users#update_primary_email", + :format => :json, + :constraints => { + username: RouteFormat.username, + } + delete "#{root_path}/:username/preferences/email" => "users#destroy_email", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/notifications" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/tracking" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/categories" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/users" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/tags" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/interface" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/sidebar" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/apps" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + post "#{root_path}/:username/preferences/email" => "users_email#create", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/email" => "users_email#update", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/badge_title" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/badge_title" => "users#badge_title", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/username" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/username" => "users#username", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/second-factor" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/preferences/second-factor-backup" => "users#preferences", + :constraints => { + username: RouteFormat.username, + } + delete "#{root_path}/:username/preferences/user_image" => "users#destroy_user_image", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/avatar/pick" => "users#pick_avatar", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/preferences/avatar/select" => "users#select_avatar", + :constraints => { + username: RouteFormat.username, + } + post "#{root_path}/:username/preferences/revoke-account" => "users#revoke_account", + :constraints => { + username: RouteFormat.username, + } + post "#{root_path}/:username/preferences/revoke-auth-token" => "users#revoke_auth_token", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/staff-info" => "users#staff_info", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/summary" => "users#summary", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/notification_level" => "users#notification_level", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/invited" => "users#invited", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/invited/:filter" => "users#invited", + :constraints => { + username: RouteFormat.username, + } post "#{root_path}/action/send_activation_email" => "users#send_activation_email" - get "#{root_path}/:username/summary" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", format: :rss, constraints: { username: RouteFormat.username } - get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", format: :rss, constraints: { username: RouteFormat.username } - get "#{root_path}/:username/activity.json" => "posts#user_posts_feed", format: :json, constraints: { username: RouteFormat.username } - get "#{root_path}/:username/activity" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/activity/:filter" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/badges" => "users#badges", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/bookmarks" => "users#bookmarks", constraints: { username: RouteFormat.username, format: /(json|ics)/ } - get "#{root_path}/:username/user-menu-bookmarks" => "users#user_menu_bookmarks", constraints: { username: RouteFormat.username, format: :json } - get "#{root_path}/:username/user-menu-private-messages" => "users#user_menu_messages", constraints: { username: RouteFormat.username, format: :json } - get "#{root_path}/:username/notifications" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/notifications/:filter" => "users#show", constraints: { username: RouteFormat.username } - delete "#{root_path}/:username" => "users#destroy", constraints: { username: RouteFormat.username } - get "#{root_path}/by-external/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ } - get "#{root_path}/by-external/:external_provider/:external_id" => "users#show", constraints: { external_id: /[^\/]+/ } - get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/private-message-topic-tracking-state" => "users#private_message_topic_tracking_state", constraints: { username: RouteFormat.username } + get "#{root_path}/:username/summary" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/activity/topics.rss" => "list#user_topics_feed", + :format => :rss, + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/activity.rss" => "posts#user_posts_feed", + :format => :rss, + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/activity.json" => "posts#user_posts_feed", + :format => :json, + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/activity" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/activity/:filter" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/badges" => "users#badges", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/bookmarks" => "users#bookmarks", + :constraints => { + username: RouteFormat.username, + format: /(json|ics)/, + } + get "#{root_path}/:username/user-menu-bookmarks" => "users#user_menu_bookmarks", + :constraints => { + username: RouteFormat.username, + format: :json, + } + get "#{root_path}/:username/user-menu-private-messages" => "users#user_menu_messages", + :constraints => { + username: RouteFormat.username, + format: :json, + } + get "#{root_path}/:username/notifications" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/notifications/:filter" => "users#show", + :constraints => { + username: RouteFormat.username, + } + delete "#{root_path}/:username" => "users#destroy", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/by-external/:external_id" => "users#show", + :constraints => { + external_id: %r{[^/]+}, + } + get "#{root_path}/by-external/:external_provider/:external_id" => "users#show", + :constraints => { + external_id: %r{[^/]+}, + } + get "#{root_path}/:username/flagged-posts" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/deleted-posts" => "users#show", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/private-message-topic-tracking-state" => + "users#private_message_topic_tracking_state", + :constraints => { + username: RouteFormat.username, + } get "#{root_path}/:username/profile-hidden" => "users#profile_hidden" - put "#{root_path}/:username/feature-topic" => "users#feature_topic", constraints: { username: RouteFormat.username } - put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic", constraints: { username: RouteFormat.username } - get "#{root_path}/:username/card.json" => "users#show_card", format: :json, constraints: { username: RouteFormat.username } + put "#{root_path}/:username/feature-topic" => "users#feature_topic", + :constraints => { + username: RouteFormat.username, + } + put "#{root_path}/:username/clear-featured-topic" => "users#clear_featured_topic", + :constraints => { + username: RouteFormat.username, + } + get "#{root_path}/:username/card.json" => "users#show_card", + :format => :json, + :constraints => { + username: RouteFormat.username, + } end - get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json } - get "user-badges/:username" => "user_badges#username", constraints: { username: RouteFormat.username } + get "user-badges/:username.json" => "user_badges#username", + :constraints => { + username: RouteFormat.username, + }, + :defaults => { + format: :json, + } + get "user-badges/:username" => "user_badges#username", + :constraints => { + username: RouteFormat.username, + } - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: { username: RouteFormat.username } - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username, format: :png } - get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: RouteFormat.username, format: :png } + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", + :constraints => { + username: RouteFormat.username, + } + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", + :constraints => { + hostname: /[\w\.-]+/, + size: /\d+/, + username: RouteFormat.username, + format: :png, + } + get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", + :constraints => { + hostname: /[\w\.-]+/, + size: /\d+/, + username: RouteFormat.username, + format: :png, + } - get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => "user_avatars#show_proxy_letter", constraints: { format: :png } + get "letter_avatar_proxy/:version/letter/:letter/:color/:size.png" => + "user_avatars#show_proxy_letter", + :constraints => { + format: :png, + } - get "svg-sprite/:hostname/svg-:theme_id-:version.js" => "svg_sprite#show", constraints: { hostname: /[\w\.-]+/, version: /\h{40}/, theme_id: /([0-9]+)?/, format: :js } - get "svg-sprite/search/:keyword" => "svg_sprite#search", format: false, constraints: { keyword: /[-a-z0-9\s\%]+/ } - get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search", defaults: { format: :json } - get "svg-sprite/:hostname/icon(/:color)/:name.svg" => "svg_sprite#svg_icon", constraints: { hostname: /[\w\.-]+/, name: /[-a-z0-9\s\%]+/, color: /(\h{3}{1,2})/, format: :svg } + get "svg-sprite/:hostname/svg-:theme_id-:version.js" => "svg_sprite#show", + :constraints => { + hostname: /[\w\.-]+/, + version: /\h{40}/, + theme_id: /([0-9]+)?/, + format: :js, + } + get "svg-sprite/search/:keyword" => "svg_sprite#search", + :format => false, + :constraints => { + keyword: /[-a-z0-9\s\%]+/, + } + get "svg-sprite/picker-search" => "svg_sprite#icon_picker_search", + :defaults => { + format: :json, + } + get "svg-sprite/:hostname/icon(/:color)/:name.svg" => "svg_sprite#svg_icon", + :constraints => { + hostname: /[\w\.-]+/, + name: /[-a-z0-9\s\%]+/, + color: /(\h{3}{1,2})/, + format: :svg, + } - get "highlight-js/:hostname/:version.js" => "highlight_js#show", constraints: { hostname: /[\w\.-]+/, format: :js } + get "highlight-js/:hostname/:version.js" => "highlight_js#show", + :constraints => { + hostname: /[\w\.-]+/, + format: :js, + } - get "stylesheets/:name" => "stylesheets#show_source_map", constraints: { name: /[-a-z0-9_]+/, format: /css\.map/ }, format: true - get "stylesheets/:name" => "stylesheets#show", constraints: { name: /[-a-z0-9_]+/, format: "css" }, format: true - get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", constraints: { format: :json } - get "theme-javascripts/:digest" => "theme_javascripts#show", constraints: { digest: /\h{40}/, format: :js }, format: true - get "theme-javascripts/:digest" => "theme_javascripts#show_map", constraints: { digest: /\h{40}/, format: :map }, format: true + get "stylesheets/:name" => "stylesheets#show_source_map", + :constraints => { + name: /[-a-z0-9_]+/, + format: /css\.map/, + }, + :format => true + get "stylesheets/:name" => "stylesheets#show", + :constraints => { + name: /[-a-z0-9_]+/, + format: "css", + }, + :format => true + get "color-scheme-stylesheet/:id(/:theme_id)" => "stylesheets#color_scheme", + :constraints => { + format: :json, + } + get "theme-javascripts/:digest" => "theme_javascripts#show", + :constraints => { + digest: /\h{40}/, + format: :js, + }, + :format => true + get "theme-javascripts/:digest" => "theme_javascripts#show_map", + :constraints => { + digest: /\h{40}/, + format: :map, + }, + :format => true get "theme-javascripts/tests/:theme_id-:digest.js" => "theme_javascripts#show_tests" post "uploads/lookup-metadata" => "uploads#metadata" @@ -572,65 +924,115 @@ Discourse::Application.routes.draw do post "uploads/lookup-urls" => "uploads#lookup_urls" # direct to s3 uploads - post "uploads/generate-presigned-put" => "uploads#generate_presigned_put", format: :json - post "uploads/complete-external-upload" => "uploads#complete_external_upload", format: :json + post "uploads/generate-presigned-put" => "uploads#generate_presigned_put", :format => :json + post "uploads/complete-external-upload" => "uploads#complete_external_upload", :format => :json # multipart uploads - post "uploads/create-multipart" => "uploads#create_multipart", format: :json - post "uploads/complete-multipart" => "uploads#complete_multipart", format: :json - post "uploads/abort-multipart" => "uploads#abort_multipart", format: :json - post "uploads/batch-presign-multipart-parts" => "uploads#batch_presign_multipart_parts", format: :json + post "uploads/create-multipart" => "uploads#create_multipart", :format => :json + post "uploads/complete-multipart" => "uploads#complete_multipart", :format => :json + post "uploads/abort-multipart" => "uploads#abort_multipart", :format => :json + post "uploads/batch-presign-multipart-parts" => "uploads#batch_presign_multipart_parts", + :format => :json # used to download original images - get "uploads/:site/:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, sha: /\h{40}/, extension: /[a-z0-9\._]+/i } - get "uploads/short-url/:base62(.:extension)" => "uploads#show_short", constraints: { site: /\w+/, base62: /[a-zA-Z0-9]+/, extension: /[a-zA-Z0-9\._-]+/i }, as: :upload_short + get "uploads/:site/:sha(.:extension)" => "uploads#show", + :constraints => { + site: /\w+/, + sha: /\h{40}/, + extension: /[a-z0-9\._]+/i, + } + get "uploads/short-url/:base62(.:extension)" => "uploads#show_short", + :constraints => { + site: /\w+/, + base62: /[a-zA-Z0-9]+/, + extension: /[a-zA-Z0-9\._-]+/i, + }, + :as => :upload_short # used to download attachments - get "uploads/:site/original/:tree:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, tree: /([a-z0-9]+\/)+/i, sha: /\h{40}/, extension: /[a-z0-9\._]+/i } + get "uploads/:site/original/:tree:sha(.:extension)" => "uploads#show", + :constraints => { + site: /\w+/, + tree: %r{([a-z0-9]+/)+}i, + sha: /\h{40}/, + extension: /[a-z0-9\._]+/i, + } if Rails.env.test? - get "uploads/:site/test_:index/original/:tree:sha(.:extension)" => "uploads#show", constraints: { site: /\w+/, index: /\d+/, tree: /([a-z0-9]+\/)+/i, sha: /\h{40}/, extension: /[a-z0-9\._]+/i } + get "uploads/:site/test_:index/original/:tree:sha(.:extension)" => "uploads#show", + :constraints => { + site: /\w+/, + index: /\d+/, + tree: %r{([a-z0-9]+/)+}i, + sha: /\h{40}/, + extension: /[a-z0-9\._]+/i, + } end # used to download attachments (old route) - get "uploads/:site/:id/:sha" => "uploads#show", constraints: { site: /\w+/, id: /\d+/, sha: /\h{16}/, format: /.*/ } + get "uploads/:site/:id/:sha" => "uploads#show", + :constraints => { + site: /\w+/, + id: /\d+/, + sha: /\h{16}/, + format: /.*/, + } # NOTE: secure-media-uploads is the old form, all new URLs generated for # secure uploads will be secure-uploads, this is left in for backwards # compat without needing to rebake all posts for each site. - get "secure-media-uploads/*path(.:extension)" => "uploads#_show_secure_deprecated", constraints: { extension: /[a-z0-9\._]+/i } - get "secure-uploads/*path(.:extension)" => "uploads#show_secure", constraints: { extension: /[a-z0-9\._]+/i } + get "secure-media-uploads/*path(.:extension)" => "uploads#_show_secure_deprecated", + :constraints => { + extension: /[a-z0-9\._]+/i, + } + get "secure-uploads/*path(.:extension)" => "uploads#show_secure", + :constraints => { + extension: /[a-z0-9\._]+/i, + } - get "posts" => "posts#latest", id: "latest_posts", constraints: { format: /(json|rss)/ } - get "private-posts" => "posts#latest", id: "private_posts", constraints: { format: /(json|rss)/ } + get "posts" => "posts#latest", :id => "latest_posts", :constraints => { format: /(json|rss)/ } + get "private-posts" => "posts#latest", + :id => "private_posts", + :constraints => { + format: /(json|rss)/, + } get "posts/by_number/:topic_id/:post_number" => "posts#by_number" get "posts/by-date/:topic_id/:date" => "posts#by_date" get "posts/:id/reply-history" => "posts#reply_history" - get "posts/:id/reply-ids" => "posts#reply_ids" + get "posts/:id/reply-ids" => "posts#reply_ids" get "posts/:id/reply-ids/all" => "posts#all_reply_ids" - get "posts/:username/deleted" => "posts#deleted_posts", constraints: { username: RouteFormat.username } - get "posts/:username/flagged" => "posts#flagged_posts", constraints: { username: RouteFormat.username } - get "posts/:username/pending" => "posts#pending", constraints: { username: RouteFormat.username } + get "posts/:username/deleted" => "posts#deleted_posts", + :constraints => { + username: RouteFormat.username, + } + get "posts/:username/flagged" => "posts#flagged_posts", + :constraints => { + username: RouteFormat.username, + } + get "posts/:username/pending" => "posts#pending", + :constraints => { + username: RouteFormat.username, + } - %w{groups g}.each do |root_path| + %w[groups g].each do |root_path| resources :groups, id: RouteFormat.username, path: root_path do - get "posts.rss" => "groups#posts_feed", format: :rss - get "mentions.rss" => "groups#mentions_feed", format: :rss + get "posts.rss" => "groups#posts_feed", :format => :rss + get "mentions.rss" => "groups#mentions_feed", :format => :rss - get 'members' - get 'posts' - get 'mentions' - get 'counts' - get 'mentionable' - get 'messageable' - get 'logs' => 'groups#histories' - post 'test_email_settings' + get "members" + get "posts" + get "mentions" + get "counts" + get "mentionable" + get "messageable" + get "logs" => "groups#histories" + post "test_email_settings" collection do - get "check-name" => 'groups#check_name' - get 'custom/new' => 'groups#new', constraints: StaffConstraint.new + get "check-name" => "groups#check_name" + get "custom/new" => "groups#new", :constraints => StaffConstraint.new get "search" => "groups#search" end member do - %w{ + %w[ activity activity/:filter requests @@ -646,9 +1048,7 @@ Discourse::Application.routes.draw do manage/categories manage/tags manage/logs - }.each do |path| - get path => 'groups#show' - end + ].each { |path| get path => "groups#show" } get "permissions" => "groups#permissions" put "members" => "groups#add_members" @@ -665,8 +1065,8 @@ Discourse::Application.routes.draw do resources :associated_groups, only: %i[index], constraints: AdminConstraint.new # aliases so old API code works - delete "admin/groups/:id/members" => "groups#remove_member", constraints: AdminConstraint.new - put "admin/groups/:id/members" => "groups#add_members", constraints: AdminConstraint.new + delete "admin/groups/:id/members" => "groups#remove_member", :constraints => AdminConstraint.new + put "admin/groups/:id/members" => "groups#add_members", :constraints => AdminConstraint.new resources :posts do delete "bookmark", to: "posts#destroy_bookmark" @@ -678,10 +1078,10 @@ Discourse::Application.routes.draw do put "notice" get "replies" get "revisions/latest" => "posts#latest_revision" - get "revisions/:revision" => "posts#revisions", constraints: { revision: /\d+/ } - put "revisions/:revision/hide" => "posts#hide_revision", constraints: { revision: /\d+/ } - put "revisions/:revision/show" => "posts#show_revision", constraints: { revision: /\d+/ } - put "revisions/:revision/revert" => "posts#revert", constraints: { revision: /\d+/ } + get "revisions/:revision" => "posts#revisions", :constraints => { revision: /\d+/ } + put "revisions/:revision/hide" => "posts#hide_revision", :constraints => { revision: /\d+/ } + put "revisions/:revision/show" => "posts#show_revision", :constraints => { revision: /\d+/ } + put "revisions/:revision/revert" => "posts#revert", :constraints => { revision: /\d+/ } put "recover" collection do delete "destroy_many" @@ -695,23 +1095,29 @@ Discourse::Application.routes.draw do resources :notifications, except: :show do collection do - put 'mark-read' => 'notifications#mark_read' + put "mark-read" => "notifications#mark_read" # creating an alias cause the api was extended to mark a single notification # this allows us to cleanly target it - put 'read' => 'notifications#mark_read' + put "read" => "notifications#mark_read" end end - match "/auth/failure", to: "users/omniauth_callbacks#failure", via: [:get, :post] + match "/auth/failure", to: "users/omniauth_callbacks#failure", via: %i[get post] get "/auth/:provider", to: "users/omniauth_callbacks#confirm_request" - match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: [:get, :post] - get "/associate/:token", to: "users/associate_accounts#connect_info", constraints: { token: /\h{32}/ } - post "/associate/:token", to: "users/associate_accounts#connect", constraints: { token: /\h{32}/ } + match "/auth/:provider/callback", to: "users/omniauth_callbacks#complete", via: %i[get post] + get "/associate/:token", + to: "users/associate_accounts#connect_info", + constraints: { + token: /\h{32}/, + } + post "/associate/:token", + to: "users/associate_accounts#connect", + constraints: { + token: /\h{32}/, + } resources :clicks do - collection do - post "track" - end + collection { post "track" } end get "excerpt" => "excerpt#show" @@ -727,17 +1133,17 @@ Discourse::Application.routes.draw do resources :user_actions resources :badges, only: [:index] - get "/badges/:id(/:slug)" => "badges#show", constraints: { format: /(json|html|rss)/ } - resources :user_badges, only: [:index, :create, :destroy] do - put "toggle_favorite" => "user_badges#toggle_favorite", constraints: { format: :json } + get "/badges/:id(/:slug)" => "badges#show", :constraints => { format: /(json|html|rss)/ } + resources :user_badges, only: %i[index create destroy] do + put "toggle_favorite" => "user_badges#toggle_favorite", :constraints => { format: :json } end - get '/c', to: redirect(relative_url_root + 'categories') + get "/c", to: redirect(relative_url_root + "categories") - resources :categories, except: [:show, :new, :edit] + resources :categories, except: %i[show new edit] post "categories/reorder" => "categories#reorder" - scope path: 'category/:category_id' do + scope path: "category/:category_id" do post "/move" => "categories#move" post "/notifications" => "categories#set_notifications" put "/slug" => "categories#update_slug" @@ -752,11 +1158,14 @@ Discourse::Application.routes.draw do get "c/:id/visible_groups" => "categories#visible_groups" get "c/*category_slug/find_by_slug" => "categories#find_by_slug" - get "c/*category_slug/edit(/:tab)" => "categories#find_by_slug", constraints: { format: 'html' } - get "/new-category" => "categories#show", constraints: { format: 'html' } + get "c/*category_slug/edit(/:tab)" => "categories#find_by_slug", + :constraints => { + format: "html", + } + get "/new-category" => "categories#show", :constraints => { format: "html" } - get "c/*category_slug_path_with_id.rss" => "list#category_feed", format: :rss - scope path: 'c/*category_slug_path_with_id' do + get "c/*category_slug_path_with_id.rss" => "list#category_feed", :format => :rss + scope path: "c/*category_slug_path_with_id" do get "/none" => "list#category_none_latest" TopTopic.periods.each do |period| @@ -765,12 +1174,16 @@ Discourse::Application.routes.draw do end Discourse.filters.each do |filter| - get "/none/l/#{filter}" => "list#category_none_#{filter}", as: "category_none_#{filter}" - get "/l/#{filter}" => "list#category_#{filter}", as: "category_#{filter}" + get "/none/l/#{filter}" => "list#category_none_#{filter}", :as => "category_none_#{filter}" + get "/l/#{filter}" => "list#category_#{filter}", :as => "category_#{filter}" end - get "/all" => "list#category_default", as: "category_all", constraints: { format: 'html' } - get "/" => "list#category_default", as: "category_default" + get "/all" => "list#category_default", + :as => "category_all", + :constraints => { + format: "html", + } + get "/" => "list#category_default", :as => "category_default" end get "hashtags" => "hashtags#lookup" @@ -783,12 +1196,10 @@ Discourse::Application.routes.draw do end Discourse.anonymous_filters.each do |filter| - get "#{filter}.rss" => "list##{filter}_feed", format: :rss + get "#{filter}.rss" => "list##{filter}_feed", :format => :rss end - Discourse.filters.each do |filter| - get "#{filter}" => "list##{filter}" - end + Discourse.filters.each { |filter| get "#{filter}" => "list##{filter}" } get "search/query" => "search#query" get "search" => "search#show" @@ -796,7 +1207,7 @@ Discourse::Application.routes.draw do # Topics resource get "t/:id" => "topics#show" - put "t/:topic_id" => "topics#update", constraints: { topic_id: /\d+/ } + put "t/:topic_id" => "topics#update", :constraints => { topic_id: /\d+/ } delete "t/:id" => "topics#destroy" put "t/:id/archive-message" => "topics#archive_message" put "t/:id/move-to-inbox" => "topics#move_to_inbox" @@ -805,89 +1216,191 @@ Discourse::Application.routes.draw do put "t/:id/shared-draft" => "topics#update_shared_draft" put "t/:id/reset-bump-date" => "topics#reset_bump_date" put "topics/bulk" - put "topics/reset-new" => 'topics#reset_new' - put "topics/pm-reset-new" => 'topics#private_message_reset_new' + put "topics/reset-new" => "topics#reset_new" + put "topics/pm-reset-new" => "topics#private_message_reset_new" post "topics/timings" - get 'topics/similar_to' => 'similar_topics#index' + get "topics/similar_to" => "similar_topics#index" resources :similar_topics get "topics/feature_stats" scope "/topics", username: RouteFormat.username do - get "created-by/:username" => "list#topics_by", as: "topics_by", defaults: { format: :json } - get "private-messages/:username" => "list#private_messages", as: "topics_private_messages", defaults: { format: :json } - get "private-messages-sent/:username" => "list#private_messages_sent", as: "topics_private_messages_sent", defaults: { format: :json } - get "private-messages-archive/:username" => "list#private_messages_archive", as: "topics_private_messages_archive", defaults: { format: :json } - get "private-messages-unread/:username" => "list#private_messages_unread", as: "topics_private_messages_unread", defaults: { format: :json } - get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag", as: "topics_private_messages_tag", defaults: { format: :json } - get "private-messages-new/:username" => "list#private_messages_new", as: "topics_private_messages_new", defaults: { format: :json } - get "private-messages-warnings/:username" => "list#private_messages_warnings", as: "topics_private_messages_warnings", defaults: { format: :json } - get "groups/:group_name" => "list#group_topics", as: "group_topics", group_name: RouteFormat.username + get "created-by/:username" => "list#topics_by", + :as => "topics_by", + :defaults => { + format: :json, + } + get "private-messages/:username" => "list#private_messages", + :as => "topics_private_messages", + :defaults => { + format: :json, + } + get "private-messages-sent/:username" => "list#private_messages_sent", + :as => "topics_private_messages_sent", + :defaults => { + format: :json, + } + get "private-messages-archive/:username" => "list#private_messages_archive", + :as => "topics_private_messages_archive", + :defaults => { + format: :json, + } + get "private-messages-unread/:username" => "list#private_messages_unread", + :as => "topics_private_messages_unread", + :defaults => { + format: :json, + } + get "private-messages-tags/:username/:tag_id.json" => "list#private_messages_tag", + :as => "topics_private_messages_tag", + :defaults => { + format: :json, + } + get "private-messages-new/:username" => "list#private_messages_new", + :as => "topics_private_messages_new", + :defaults => { + format: :json, + } + get "private-messages-warnings/:username" => "list#private_messages_warnings", + :as => "topics_private_messages_warnings", + :defaults => { + format: :json, + } + get "groups/:group_name" => "list#group_topics", + :as => "group_topics", + :group_name => RouteFormat.username scope "/private-messages-group/:username", group_name: RouteFormat.username do - get ":group_name.json" => "list#private_messages_group", as: "topics_private_messages_group" - get ":group_name/archive.json" => "list#private_messages_group_archive", as: "topics_private_messages_group_archive" - get ":group_name/new.json" => "list#private_messages_group_new", as: "topics_private_messages_group_new" - get ":group_name/unread.json" => "list#private_messages_group_unread", as: "topics_private_messages_group_unread" + get ":group_name.json" => "list#private_messages_group", + :as => "topics_private_messages_group" + get ":group_name/archive.json" => "list#private_messages_group_archive", + :as => "topics_private_messages_group_archive" + get ":group_name/new.json" => "list#private_messages_group_new", + :as => "topics_private_messages_group_new" + get ":group_name/unread.json" => "list#private_messages_group_unread", + :as => "topics_private_messages_group_unread" end end - get 'embed/topics' => 'embed#topics' - get 'embed/comments' => 'embed#comments' - get 'embed/count' => 'embed#count' - get 'embed/info' => 'embed#info' + get "embed/topics" => "embed#topics" + get "embed/comments" => "embed#comments" + get "embed/count" => "embed#count" + get "embed/info" => "embed#info" get "new-topic" => "new_topic#index" get "new-message" => "new_topic#index" # Topic routes get "t/id_for/:slug" => "topics#id_for_slug" - get "t/external_id/:external_id" => "topics#show_by_external_id", format: :json, constraints: { external_id: /[\w-]+/ } - get "t/:slug/:topic_id/print" => "topics#show", format: :html, print: 'true', constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ } - get "t/:topic_id/wordpress" => "topics#wordpress", constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id/summary" => "topics#show", defaults: { summary: true }, constraints: { topic_id: /\d+/ } - get "t/:topic_id/summary" => "topics#show", constraints: { topic_id: /\d+/ } - put "t/:slug/:topic_id" => "topics#update", constraints: { topic_id: /\d+/ } - put "t/:slug/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ } - put "t/:topic_id/star" => "topics#star", constraints: { topic_id: /\d+/ } - put "t/:slug/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ } - put "t/:topic_id/status" => "topics#status", constraints: { topic_id: /\d+/ } - put "t/:topic_id/clear-pin" => "topics#clear_pin", constraints: { topic_id: /\d+/ } - put "t/:topic_id/re-pin" => "topics#re_pin", constraints: { topic_id: /\d+/ } - put "t/:topic_id/mute" => "topics#mute", constraints: { topic_id: /\d+/ } - put "t/:topic_id/unmute" => "topics#unmute", constraints: { topic_id: /\d+/ } - post "t/:topic_id/timer" => "topics#timer", constraints: { topic_id: /\d+/ } - put "t/:topic_id/make-banner" => "topics#make_banner", constraints: { topic_id: /\d+/ } - put "t/:topic_id/remove-banner" => "topics#remove_banner", constraints: { topic_id: /\d+/ } - put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", constraints: { topic_id: /\d+/ } - put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", constraints: { topic_id: /\d+/ } - put "t/:topic_id/recover" => "topics#recover", constraints: { topic_id: /\d+/ } - get "t/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ } - get "t/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id.rss" => "topics#feed", format: :rss, constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id" => "topics#show", constraints: { topic_id: /\d+/ } - get "t/:slug/:topic_id/:post_number" => "topics#show", constraints: { topic_id: /\d+/, post_number: /\d+/ } - get "t/:slug/:topic_id/last" => "topics#show", post_number: 99999999, constraints: { topic_id: /\d+/ } - get "t/:topic_id/posts" => "topics#posts", constraints: { topic_id: /\d+/ }, format: :json - get "t/:topic_id/post_ids" => "topics#post_ids", constraints: { topic_id: /\d+/ }, format: :json - get "t/:topic_id/excerpts" => "topics#excerpts", constraints: { topic_id: /\d+/ }, format: :json - post "t/:topic_id/timings" => "topics#timings", constraints: { topic_id: /\d+/ } - post "t/:topic_id/invite" => "topics#invite", constraints: { topic_id: /\d+/ } - post "t/:topic_id/invite-group" => "topics#invite_group", constraints: { topic_id: /\d+/ } - post "t/:topic_id/move-posts" => "topics#move_posts", constraints: { topic_id: /\d+/ } - post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: { topic_id: /\d+/ } - post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: { topic_id: /\d+/ } - put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: { topic_id: /\d+/ } - delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: { topic_id: /\d+/ } - put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ } - put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: { topic_id: /\d+/ } - put "t/:topic_id/tags" => "topics#update_tags", constraints: { topic_id: /\d+/ } - put "t/:topic_id/slow_mode" => "topics#set_slow_mode", constraints: { topic_id: /\d+/ } + get "t/external_id/:external_id" => "topics#show_by_external_id", + :format => :json, + :constraints => { + external_id: /[\w-]+/, + } + get "t/:slug/:topic_id/print" => "topics#show", + :format => :html, + :print => "true", + :constraints => { + topic_id: /\d+/, + } + get "t/:slug/:topic_id/wordpress" => "topics#wordpress", :constraints => { topic_id: /\d+/ } + get "t/:topic_id/wordpress" => "topics#wordpress", :constraints => { topic_id: /\d+/ } + get "t/:slug/:topic_id/moderator-liked" => "topics#moderator_liked", + :constraints => { + topic_id: /\d+/, + } + get "t/:slug/:topic_id/summary" => "topics#show", + :defaults => { + summary: true, + }, + :constraints => { + topic_id: /\d+/, + } + get "t/:topic_id/summary" => "topics#show", :constraints => { topic_id: /\d+/ } + put "t/:slug/:topic_id" => "topics#update", :constraints => { topic_id: /\d+/ } + put "t/:slug/:topic_id/star" => "topics#star", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/star" => "topics#star", :constraints => { topic_id: /\d+/ } + put "t/:slug/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/status" => "topics#status", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/clear-pin" => "topics#clear_pin", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/re-pin" => "topics#re_pin", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/mute" => "topics#mute", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/unmute" => "topics#unmute", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/timer" => "topics#timer", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/make-banner" => "topics#make_banner", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/remove-banner" => "topics#remove_banner", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/remove-allowed-user" => "topics#remove_allowed_user", + :constraints => { + topic_id: /\d+/, + } + put "t/:topic_id/remove-allowed-group" => "topics#remove_allowed_group", + :constraints => { + topic_id: /\d+/, + } + put "t/:topic_id/recover" => "topics#recover", :constraints => { topic_id: /\d+/ } + get "t/:topic_id/:post_number" => "topics#show", + :constraints => { + topic_id: /\d+/, + post_number: /\d+/, + } + get "t/:topic_id/last" => "topics#show", + :post_number => 99_999_999, + :constraints => { + topic_id: /\d+/, + } + get "t/:slug/:topic_id.rss" => "topics#feed", + :format => :rss, + :constraints => { + topic_id: /\d+/, + } + get "t/:slug/:topic_id" => "topics#show", :constraints => { topic_id: /\d+/ } + get "t/:slug/:topic_id/:post_number" => "topics#show", + :constraints => { + topic_id: /\d+/, + post_number: /\d+/, + } + get "t/:slug/:topic_id/last" => "topics#show", + :post_number => 99_999_999, + :constraints => { + topic_id: /\d+/, + } + get "t/:topic_id/posts" => "topics#posts", :constraints => { topic_id: /\d+/ }, :format => :json + get "t/:topic_id/post_ids" => "topics#post_ids", + :constraints => { + topic_id: /\d+/, + }, + :format => :json + get "t/:topic_id/excerpts" => "topics#excerpts", + :constraints => { + topic_id: /\d+/, + }, + :format => :json + post "t/:topic_id/timings" => "topics#timings", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/invite" => "topics#invite", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/invite-group" => "topics#invite_group", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/move-posts" => "topics#move_posts", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/merge-topic" => "topics#merge_topic", :constraints => { topic_id: /\d+/ } + post "t/:topic_id/change-owner" => "topics#change_post_owners", + :constraints => { + topic_id: /\d+/, + } + put "t/:topic_id/change-timestamp" => "topics#change_timestamps", + :constraints => { + topic_id: /\d+/, + } + delete "t/:topic_id/timings" => "topics#destroy_timings", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/bookmark" => "topics#bookmark", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", + :constraints => { + topic_id: /\d+/, + } + put "t/:topic_id/tags" => "topics#update_tags", :constraints => { topic_id: /\d+/ } + put "t/:topic_id/slow_mode" => "topics#set_slow_mode", :constraints => { topic_id: /\d+/ } - post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ } + post "t/:topic_id/notifications" => "topics#set_notifications", + :constraints => { + topic_id: /\d+/, + } get "p/:post_id(/:user_id)" => "posts#short_link" get "/posts/:id/cooked" => "posts#cooked" @@ -897,7 +1410,7 @@ Discourse::Application.routes.draw do get "raw/:topic_id(/:post_number)" => "posts#markdown_num" resources :invites, except: [:show] - get "/invites/:id" => "invites#show", constraints: { format: :html } + get "/invites/:id" => "invites#show", :constraints => { format: :html } put "/invites/:id" => "invites#update" post "invites/upload_csv" => "invites#upload_csv" @@ -905,13 +1418,11 @@ Discourse::Application.routes.draw do post "invites/reinvite" => "invites#resend_invite" post "invites/reinvite-all" => "invites#resend_all_invites" delete "invites" => "invites#destroy" - put "invites/show/:id" => "invites#perform_accept_invitation", as: 'perform_accept_invite' + put "invites/show/:id" => "invites#perform_accept_invitation", :as => "perform_accept_invite" get "invites/retrieve" => "invites#retrieve" resources :export_csv do - collection do - post "export_entity" => "export_csv#export_entity" - end + collection { post "export_entity" => "export_csv#export_entity" } end get "onebox" => "onebox#show" @@ -921,95 +1432,129 @@ Discourse::Application.routes.draw do get "message-bus/poll" => "message_bus#poll" - resources :drafts, only: [:index, :create, :show, :destroy] + resources :drafts, only: %i[index create show destroy] - get "/service-worker.js" => "static#service_worker_asset", format: :js - if service_worker_asset = Rails.application.assets_manifest.assets['service-worker.js'] + get "/service-worker.js" => "static#service_worker_asset", :format => :js + if service_worker_asset = Rails.application.assets_manifest.assets["service-worker.js"] # https://developers.google.com/web/fundamentals/codelabs/debugging-service-workers/ # Normally the browser will wait until a user closes all tabs that contain the # current site before updating to a new Service Worker. # Support the old Service Worker path to avoid routing error filling up the # logs. - get service_worker_asset => "static#service_worker_asset", format: :js + get service_worker_asset => "static#service_worker_asset", :format => :js end - get "cdn_asset/:site/*path" => "static#cdn_asset", format: false, constraints: { format: /.*/ } - get "brotli_asset/*path" => "static#brotli_asset", format: false, constraints: { format: /.*/ } + get "cdn_asset/:site/*path" => "static#cdn_asset", + :format => false, + :constraints => { + format: /.*/, + } + get "brotli_asset/*path" => "static#brotli_asset", + :format => false, + :constraints => { + format: /.*/, + } - get "favicon/proxied" => "static#favicon", format: false + get "favicon/proxied" => "static#favicon", :format => false get "robots.txt" => "robots_txt#index" get "robots-builder.json" => "robots_txt#builder" get "offline.html" => "offline#index" - get "manifest.webmanifest" => "metadata#manifest", as: :manifest + get "manifest.webmanifest" => "metadata#manifest", :as => :manifest get "manifest.json" => "metadata#manifest" get ".well-known/assetlinks.json" => "metadata#app_association_android" - get "apple-app-site-association" => "metadata#app_association_ios", format: false - get "opensearch" => "metadata#opensearch", constraints: { format: :xml } + get "apple-app-site-association" => "metadata#app_association_ios", :format => false + get "opensearch" => "metadata#opensearch", :constraints => { format: :xml } - scope '/tag/:tag_id' do + scope "/tag/:tag_id" do constraints format: :json do - get '/' => 'tags#show', as: 'tag_show' - get '/info' => 'tags#info' - get '/notifications' => 'tags#notifications' - put '/notifications' => 'tags#update_notifications' - put '/' => 'tags#update' - delete '/' => 'tags#destroy' - post '/synonyms' => 'tags#create_synonyms' - delete '/synonyms/:synonym_id' => 'tags#destroy_synonym' + get "/" => "tags#show", :as => "tag_show" + get "/info" => "tags#info" + get "/notifications" => "tags#notifications" + put "/notifications" => "tags#update_notifications" + put "/" => "tags#update" + delete "/" => "tags#destroy" + post "/synonyms" => "tags#create_synonyms" + delete "/synonyms/:synonym_id" => "tags#destroy_synonym" Discourse.filters.each do |filter| - get "/l/#{filter}" => "tags#show_#{filter}", as: "tag_show_#{filter}" + get "/l/#{filter}" => "tags#show_#{filter}", :as => "tag_show_#{filter}" end end constraints format: :rss do - get '/' => 'tags#tag_feed' + get "/" => "tags#tag_feed" end end scope "/tags" do - get '/' => 'tags#index' - get '/filter/list' => 'tags#index' - get '/filter/search' => 'tags#search' - get '/personal_messages/:username' => 'tags#personal_messages', constraints: { username: RouteFormat.username } - post '/upload' => 'tags#upload' - get '/unused' => 'tags#list_unused' - delete '/unused' => 'tags#destroy_unused' + get "/" => "tags#index" + get "/filter/list" => "tags#index" + get "/filter/search" => "tags#search" + get "/personal_messages/:username" => "tags#personal_messages", + :constraints => { + username: RouteFormat.username, + } + post "/upload" => "tags#upload" + get "/unused" => "tags#list_unused" + delete "/unused" => "tags#destroy_unused" - constraints(tag_id: /[^\/]+?/, format: /json|rss/) do - scope path: '/c/*category_slug_path_with_id' do + constraints(tag_id: %r{[^/]+?}, format: /json|rss/) do + scope path: "/c/*category_slug_path_with_id" do Discourse.filters.each do |filter| - get "/none/:tag_id/l/#{filter}" => "tags#show_#{filter}", as: "tag_category_none_show_#{filter}", defaults: { no_subcategories: true } - get "/all/:tag_id/l/#{filter}" => "tags#show_#{filter}", as: "tag_category_all_show_#{filter}", defaults: { no_subcategories: false } + get "/none/:tag_id/l/#{filter}" => "tags#show_#{filter}", + :as => "tag_category_none_show_#{filter}", + :defaults => { + no_subcategories: true, + } + get "/all/:tag_id/l/#{filter}" => "tags#show_#{filter}", + :as => "tag_category_all_show_#{filter}", + :defaults => { + no_subcategories: false, + } end - get '/none/:tag_id' => 'tags#show', as: 'tag_category_none_show', defaults: { no_subcategories: true } - get '/all/:tag_id' => 'tags#show', as: 'tag_category_all_show', defaults: { no_subcategories: false } + get "/none/:tag_id" => "tags#show", + :as => "tag_category_none_show", + :defaults => { + no_subcategories: true, + } + get "/all/:tag_id" => "tags#show", + :as => "tag_category_all_show", + :defaults => { + no_subcategories: false, + } Discourse.filters.each do |filter| - get "/:tag_id/l/#{filter}" => "tags#show_#{filter}", as: "tag_category_show_#{filter}" + get "/:tag_id/l/#{filter}" => "tags#show_#{filter}", + :as => "tag_category_show_#{filter}" end - get '/:tag_id' => 'tags#show', as: 'tag_category_show' + get "/:tag_id" => "tags#show", :as => "tag_category_show" end - get '/intersection/:tag_id/*additional_tag_ids' => 'tags#show', as: 'tag_intersection' + get "/intersection/:tag_id/*additional_tag_ids" => "tags#show", :as => "tag_intersection" end - get '*tag_id', to: redirect(relative_url_root + 'tag/%{tag_id}') + get "*tag_id", to: redirect(relative_url_root + "tag/%{tag_id}") end resources :tag_groups, constraints: StaffConstraint.new, except: [:edit] - get '/tag_groups/filter/search' => 'tag_groups#search', format: :json + get "/tag_groups/filter/search" => "tag_groups#search", :format => :json Discourse.filters.each do |filter| - root to: "list##{filter}", constraints: HomePageConstraint.new("#{filter}"), as: "list_#{filter}" + root to: "list##{filter}", + constraints: HomePageConstraint.new("#{filter}"), + as: "list_#{filter}" end # special case for categories - root to: "categories#index", constraints: HomePageConstraint.new("categories"), as: "categories_index" + root to: "categories#index", + constraints: HomePageConstraint.new("categories"), + as: "categories_index" - root to: 'finish_installation#index', constraints: HomePageConstraint.new("finish_installation"), as: 'installation_redirect' + root to: "finish_installation#index", + constraints: HomePageConstraint.new("finish_installation"), + as: "installation_redirect" get "/user-api-key/new" => "user_api_keys#new" post "/user-api-key" => "user_api_keys#create" @@ -1019,7 +1564,7 @@ Discourse::Application.routes.draw do post "/user-api-key/otp" => "user_api_keys#create_otp" get "/safe-mode" => "safe_mode#index" - post "/safe-mode" => "safe_mode#enter", as: "safe_mode_enter" + post "/safe-mode" => "safe_mode#enter", :as => "safe_mode_enter" get "/theme-qunit" => "qunit#theme" @@ -1028,7 +1573,7 @@ Discourse::Application.routes.draw do resources :csp_reports, only: [:create] - get "/permalink-check", to: 'permalinks#check' + get "/permalink-check", to: "permalinks#check" post "/do-not-disturb" => "do_not_disturb#create" delete "/do-not-disturb" => "do_not_disturb#destroy" @@ -1040,6 +1585,6 @@ Discourse::Application.routes.draw do put "user-status" => "user_status#set" delete "user-status" => "user_status#clear" - get "*url", to: 'permalinks#show', constraints: PermalinkConstraint.new + get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new end end diff --git a/config/site_settings.yml b/config/site_settings.yml index 6c7e37640b..599e0de852 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1549,6 +1549,10 @@ files: default: false client: true hidden: true + composer_ios_media_optimisation_image_enabled: + default: false + client: true + hidden: true trust: default_trust_level: diff --git a/config/spring.rb b/config/spring.rb index 68b1b41a33..8d12edf4a8 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -7,7 +7,5 @@ # spring binstub rails # spring binstub rake # spring binstub rspec -Spring.after_fork do - Discourse.after_fork -end +Spring.after_fork { Discourse.after_fork } Spring::Commands::Rake.environment_matchers["spec"] = "test" diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index e69979adfe..2e0b22f48f 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -3,9 +3,9 @@ # See http://unicorn.bogomips.org/Unicorn/Configurator.html if (ENV["LOGSTASH_UNICORN_URI"] || "").length > 0 - require_relative '../lib/discourse_logstash_logger' - require_relative '../lib/unicorn_logstash_patch' - logger DiscourseLogstashLogger.logger(uri: ENV['LOGSTASH_UNICORN_URI'], type: :unicorn) + require_relative "../lib/discourse_logstash_logger" + require_relative "../lib/unicorn_logstash_patch" + logger DiscourseLogstashLogger.logger(uri: ENV["LOGSTASH_UNICORN_URI"], type: :unicorn) end discourse_path = File.expand_path(File.expand_path(File.dirname(__FILE__)) + "/../") @@ -16,11 +16,10 @@ worker_processes (ENV["UNICORN_WORKERS"] || 3).to_i working_directory discourse_path # listen "#{discourse_path}/tmp/sockets/unicorn.sock" -listen ENV["UNICORN_LISTENER"] || "#{(ENV["UNICORN_BIND_ALL"] ? "" : "127.0.0.1:")}#{(ENV["UNICORN_PORT"] || 3000).to_i}" +listen ENV["UNICORN_LISTENER"] || + "#{(ENV["UNICORN_BIND_ALL"] ? "" : "127.0.0.1:")}#{(ENV["UNICORN_PORT"] || 3000).to_i}" -if !File.exist?("#{discourse_path}/tmp/pids") - FileUtils.mkdir_p("#{discourse_path}/tmp/pids") -end +FileUtils.mkdir_p("#{discourse_path}/tmp/pids") if !File.exist?("#{discourse_path}/tmp/pids") # feel free to point this anywhere accessible on the filesystem pid (ENV["UNICORN_PID_PATH"] || "#{discourse_path}/tmp/pids/unicorn.pid") @@ -52,7 +51,6 @@ check_client_connection false initialized = false before_fork do |server, worker| - unless initialized Discourse.preload_rails! @@ -67,7 +65,7 @@ before_fork do |server, worker| initialized = true - supervisor = ENV['UNICORN_SUPERVISOR_PID'].to_i + supervisor = ENV["UNICORN_SUPERVISOR_PID"].to_i if supervisor > 0 Thread.new do while true @@ -80,14 +78,12 @@ before_fork do |server, worker| end end - sidekiqs = ENV['UNICORN_SIDEKIQS'].to_i + sidekiqs = ENV["UNICORN_SIDEKIQS"].to_i if sidekiqs > 0 server.logger.info "starting #{sidekiqs} supervised sidekiqs" - require 'demon/sidekiq' - Demon::Sidekiq.after_fork do - DiscourseEvent.trigger(:sidekiq_fork_started) - end + require "demon/sidekiq" + Demon::Sidekiq.after_fork { DiscourseEvent.trigger(:sidekiq_fork_started) } Demon::Sidekiq.start(sidekiqs) @@ -98,13 +94,14 @@ before_fork do |server, worker| # Trap USR1, so we can re-issue to sidekiq workers # but chain the default unicorn implementation as well - old_handler = Signal.trap("USR1") do - Demon::Sidekiq.kill("USR1") - old_handler.call - end + old_handler = + Signal.trap("USR1") do + Demon::Sidekiq.kill("USR1") + old_handler.call + end end - if ENV['DISCOURSE_ENABLE_EMAIL_SYNC_DEMON'] == 'true' + if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true" server.logger.info "starting up EmailSync demon" Demon::EmailSync.start Signal.trap("SIGTSTP") do @@ -119,13 +116,13 @@ before_fork do |server, worker| end class ::Unicorn::HttpServer - alias :master_sleep_orig :master_sleep + alias master_sleep_orig master_sleep def max_sidekiq_rss - rss = `ps -eo rss,args | grep sidekiq | grep -v grep | awk '{print $1}'` - .split("\n") - .map(&:to_i) - .max + rss = + `ps -eo rss,args | grep sidekiq | grep -v grep | awk '{print $1}'`.split("\n") + .map(&:to_i) + .max rss ||= 0 @@ -133,18 +130,24 @@ before_fork do |server, worker| end def max_allowed_sidekiq_rss - [ENV['UNICORN_SIDEKIQ_MAX_RSS'].to_i, 500].max.megabytes + [ENV["UNICORN_SIDEKIQ_MAX_RSS"].to_i, 500].max.megabytes end def force_kill_rogue_sidekiq info = `ps -eo pid,rss,args | grep sidekiq | grep -v grep | awk '{print $1,$2}'` - info.split("\n").each do |row| - pid, mem = row.split(" ").map(&:to_i) - if pid > 0 && (mem * 1024) > max_allowed_sidekiq_rss - Rails.logger.warn "Detected rogue Sidekiq pid #{pid} mem #{mem * 1024}, killing" - Process.kill("KILL", pid) rescue nil + info + .split("\n") + .each do |row| + pid, mem = row.split(" ").map(&:to_i) + if pid > 0 && (mem * 1024) > max_allowed_sidekiq_rss + Rails.logger.warn "Detected rogue Sidekiq pid #{pid} mem #{mem * 1024}, killing" + begin + Process.kill("KILL", pid) + rescue StandardError + nil + end + end end - end end def check_sidekiq_heartbeat @@ -152,13 +155,15 @@ before_fork do |server, worker| @sidekiq_next_heartbeat_check ||= Time.now.to_i + @sidekiq_heartbeat_interval if @sidekiq_next_heartbeat_check < Time.now.to_i - last_heartbeat = Jobs::RunHeartbeat.last_heartbeat restart = false sidekiq_rss = max_sidekiq_rss if sidekiq_rss > max_allowed_sidekiq_rss - Rails.logger.warn("Sidekiq is consuming too much memory (using: %0.2fM) for '%s', restarting" % [(sidekiq_rss.to_f / 1.megabyte), ENV["DISCOURSE_HOSTNAME"]]) + Rails.logger.warn( + "Sidekiq is consuming too much memory (using: %0.2fM) for '%s', restarting" % + [(sidekiq_rss.to_f / 1.megabyte), ENV["DISCOURSE_HOSTNAME"]], + ) restart = true end @@ -185,16 +190,18 @@ before_fork do |server, worker| email_sync_pids = Demon::EmailSync.demons.map { |uid, demon| demon.pid } return 0 if email_sync_pids.empty? - rss = `ps -eo pid,rss,args | grep '#{email_sync_pids.join('|')}' | grep -v grep | awk '{print $2}'` - .split("\n") - .map(&:to_i) - .max + rss = + `ps -eo pid,rss,args | grep '#{email_sync_pids.join("|")}' | grep -v grep | awk '{print $2}'`.split( + "\n", + ) + .map(&:to_i) + .max (rss || 0) * 1024 end def max_allowed_email_sync_rss - [ENV['UNICORN_EMAIL_SYNC_MAX_RSS'].to_i, 500].max.megabytes + [ENV["UNICORN_EMAIL_SYNC_MAX_RSS"].to_i, 500].max.megabytes end def check_email_sync_heartbeat @@ -207,16 +214,22 @@ before_fork do |server, worker| restart = false # Restart process if it does not respond anymore - last_heartbeat_ago = Time.now.to_i - Discourse.redis.get(Demon::EmailSync::HEARTBEAT_KEY).to_i + last_heartbeat_ago = + Time.now.to_i - Discourse.redis.get(Demon::EmailSync::HEARTBEAT_KEY).to_i if last_heartbeat_ago > Demon::EmailSync::HEARTBEAT_INTERVAL.to_i - STDERR.puts("EmailSync heartbeat test failed (last heartbeat was #{last_heartbeat_ago}s ago), restarting") + STDERR.puts( + "EmailSync heartbeat test failed (last heartbeat was #{last_heartbeat_ago}s ago), restarting", + ) restart = true end # Restart process if memory usage is too high email_sync_rss = max_email_sync_rss if email_sync_rss > max_allowed_email_sync_rss - STDERR.puts("EmailSync is consuming too much memory (using: %0.2fM) for '%s', restarting" % [(email_sync_rss.to_f / 1.megabyte), ENV["DISCOURSE_HOSTNAME"]]) + STDERR.puts( + "EmailSync is consuming too much memory (using: %0.2fM) for '%s', restarting" % + [(email_sync_rss.to_f / 1.megabyte), ENV["DISCOURSE_HOSTNAME"]], + ) restart = true end @@ -224,25 +237,22 @@ before_fork do |server, worker| end def master_sleep(sec) - sidekiqs = ENV['UNICORN_SIDEKIQS'].to_i + sidekiqs = ENV["UNICORN_SIDEKIQS"].to_i if sidekiqs > 0 Demon::Sidekiq.ensure_running check_sidekiq_heartbeat end - if ENV['DISCOURSE_ENABLE_EMAIL_SYNC_DEMON'] == 'true' + if ENV["DISCOURSE_ENABLE_EMAIL_SYNC_DEMON"] == "true" Demon::EmailSync.ensure_running check_email_sync_heartbeat end - DiscoursePluginRegistry.demon_processes.each do |demon_class| - demon_class.ensure_running - end + DiscoursePluginRegistry.demon_processes.each { |demon_class| demon_class.ensure_running } master_sleep_orig(sec) end end - end Discourse.redis.close diff --git a/db/api_test_seeds.rb b/db/api_test_seeds.rb index 17c3256e97..d8d4aa074a 100644 --- a/db/api_test_seeds.rb +++ b/db/api_test_seeds.rb @@ -1,5 +1,14 @@ # frozen_string_literal: true -user = User.where(username: "test_user").first_or_create(name: "Test User", email: "test_user@example.com", password: SecureRandom.hex, username: "test_user", approved: true, active: true, admin: true) +user = + User.where(username: "test_user").first_or_create( + name: "Test User", + email: "test_user@example.com", + password: SecureRandom.hex, + username: "test_user", + approved: true, + active: true, + admin: true, + ) UserAuthToken.generate!(user_id: user.id) -ApiKey.create(key: 'test_d7fd0429940', user_id: user.id, created_by_id: user.id) +ApiKey.create(key: "test_d7fd0429940", user_id: user.id, created_by_id: user.id) diff --git a/db/fixtures/001_refresh.rb b/db/fixtures/001_refresh.rb index 3e0d603b93..b7e737c632 100644 --- a/db/fixtures/001_refresh.rb +++ b/db/fixtures/001_refresh.rb @@ -12,7 +12,11 @@ class SeedData::Refresher # Not that reset_column_information is not thread safe so we have to be careful # not to run it concurrently within the same process. ActiveRecord::Base.connection.tables.each do |table| - table.classify.constantize.reset_column_information rescue nil + begin + table.classify.constantize.reset_column_information + rescue StandardError + nil + end end @refreshed = true diff --git a/db/fixtures/002_groups.rb b/db/fixtures/002_groups.rb index d9cc999beb..b7c04101c7 100644 --- a/db/fixtures/002_groups.rb +++ b/db/fixtures/002_groups.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true Group.ensure_automatic_groups! -if g = Group.find_by(name: 'trust_level_5', id: 15) +if g = Group.find_by(name: "trust_level_5", id: 15) g.destroy! end -Group.where(name: 'everyone').update_all(visibility_level: Group.visibility_levels[:staff]) +Group.where(name: "everyone").update_all(visibility_level: Group.visibility_levels[:staff]) diff --git a/db/fixtures/003_post_action_types.rb b/db/fixtures/003_post_action_types.rb index c6a4949573..10870f90dc 100644 --- a/db/fixtures/003_post_action_types.rb +++ b/db/fixtures/003_post_action_types.rb @@ -2,16 +2,16 @@ PostActionType.seed do |s| s.id = PostActionType.types[:like] - s.name_key = 'like' + s.name_key = "like" s.is_flag = false - s.icon = 'heart' + s.icon = "heart" s.position = 2 end if PostActionType.types[:off_topic] PostActionType.seed do |s| s.id = PostActionType.types[:off_topic] - s.name_key = 'off_topic' + s.name_key = "off_topic" s.is_flag = true s.position = 3 end @@ -20,7 +20,7 @@ end if PostActionType.types[:inappropriate] PostActionType.seed do |s| s.id = PostActionType.types[:inappropriate] - s.name_key = 'inappropriate' + s.name_key = "inappropriate" s.is_flag = true s.position = 4 end @@ -29,7 +29,7 @@ end if PostActionType.types[:spam] PostActionType.seed do |s| s.id = PostActionType.types[:spam] - s.name_key = 'spam' + s.name_key = "spam" s.is_flag = true s.position = 6 end @@ -38,7 +38,7 @@ end if PostActionType.types[:notify_user] PostActionType.seed do |s| s.id = PostActionType.types[:notify_user] - s.name_key = 'notify_user' + s.name_key = "notify_user" s.is_flag = true s.position = 7 end @@ -47,7 +47,7 @@ end if PostActionType.types[:notify_moderators] PostActionType.seed do |s| s.id = PostActionType.types[:notify_moderators] - s.name_key = 'notify_moderators' + s.name_key = "notify_moderators" s.is_flag = true s.position = 8 end diff --git a/db/fixtures/006_badges.rb b/db/fixtures/006_badges.rb index c0b3b76dfc..1642abe64a 100644 --- a/db/fixtures/006_badges.rb +++ b/db/fixtures/006_badges.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'badge_queries' +require "badge_queries" BadgeGrouping.seed do |g| g.id = BadgeGrouping::GettingStarted @@ -45,9 +45,9 @@ SQL [ [Badge::BasicUser, "Basic User", BadgeType::Bronze], - [Badge::Member, "Member", BadgeType::Bronze], - [Badge::Regular, "Regular", BadgeType::Silver], - [Badge::Leader, "Leader", BadgeType::Gold], + [Badge::Member, "Member", BadgeType::Bronze], + [Badge::Regular, "Regular", BadgeType::Silver], + [Badge::Leader, "Leader", BadgeType::Gold], ].each do |id, name, type| Badge.seed do |b| b.id = id @@ -179,9 +179,9 @@ Badge.seed do |b| end [ - [Badge::NiceShare, "Nice Share", BadgeType::Bronze, 25], - [Badge::GoodShare, "Good Share", BadgeType::Silver, 300], - [Badge::GreatShare, "Great Share", BadgeType::Gold, 1000], + [Badge::NiceShare, "Nice Share", BadgeType::Bronze, 25], + [Badge::GoodShare, "Good Share", BadgeType::Silver, 300], + [Badge::GreatShare, "Great Share", BadgeType::Gold, 1000], ].each do |id, name, level, count| Badge.seed do |b| b.id = id @@ -246,12 +246,12 @@ Badge.seed do |b| end [ - [Badge::NicePost, "Nice Post", BadgeType::Bronze, false], - [Badge::GoodPost, "Good Post", BadgeType::Silver, false], - [Badge::GreatPost, "Great Post", BadgeType::Gold, false], - [Badge::NiceTopic, "Nice Topic", BadgeType::Bronze, true], - [Badge::GoodTopic, "Good Topic", BadgeType::Silver, true], - [Badge::GreatTopic, "Great Topic", BadgeType::Gold, true], + [Badge::NicePost, "Nice Post", BadgeType::Bronze, false], + [Badge::GoodPost, "Good Post", BadgeType::Silver, false], + [Badge::GreatPost, "Great Post", BadgeType::Gold, false], + [Badge::NiceTopic, "Nice Topic", BadgeType::Bronze, true], + [Badge::GoodTopic, "Good Topic", BadgeType::Silver, true], + [Badge::GreatTopic, "Great Topic", BadgeType::Gold, true], ].each do |id, name, type, topic| Badge.seed do |b| b.id = id @@ -281,9 +281,9 @@ Badge.seed do |b| end [ - [Badge::PopularLink, "Popular Link", BadgeType::Bronze, 50], - [Badge::HotLink, "Hot Link", BadgeType::Silver, 300], - [Badge::FamousLink, "Famous Link", BadgeType::Gold, 1000], + [Badge::PopularLink, "Popular Link", BadgeType::Bronze, 50], + [Badge::HotLink, "Hot Link", BadgeType::Silver, 300], + [Badge::FamousLink, "Famous Link", BadgeType::Gold, 1000], ].each do |id, name, level, count| Badge.seed do |b| b.id = id @@ -302,8 +302,8 @@ end [ [Badge::Appreciated, "Appreciated", BadgeType::Bronze, 1, 20], - [Badge::Respected, "Respected", BadgeType::Silver, 2, 100], - [Badge::Admired, "Admired", BadgeType::Gold, 5, 300], + [Badge::Respected, "Respected", BadgeType::Silver, 2, 100], + [Badge::Admired, "Admired", BadgeType::Gold, 5, 300], ].each do |id, name, level, like_count, post_count| Badge.seed do |b| b.id = id @@ -319,9 +319,9 @@ end end [ - [Badge::ThankYou, "Thank You", BadgeType::Bronze, 20, 10], - [Badge::GivesBack, "Gives Back", BadgeType::Silver, 100, 100], - [Badge::Empathetic, "Empathetic", BadgeType::Gold, 500, 1000] + [Badge::ThankYou, "Thank You", BadgeType::Bronze, 20, 10], + [Badge::GivesBack, "Gives Back", BadgeType::Silver, 100, 100], + [Badge::Empathetic, "Empathetic", BadgeType::Gold, 500, 1000], ].each do |id, name, level, count, ratio| Badge.seed do |b| b.id = id @@ -337,9 +337,9 @@ end end [ - [Badge::OutOfLove, "Out of Love", BadgeType::Bronze, 1], - [Badge::HigherLove, "Higher Love", BadgeType::Silver, 5], - [Badge::CrazyInLove, "Crazy in Love", BadgeType::Gold, 20], + [Badge::OutOfLove, "Out of Love", BadgeType::Bronze, 1], + [Badge::HigherLove, "Higher Love", BadgeType::Silver, 5], + [Badge::CrazyInLove, "Crazy in Love", BadgeType::Gold, 20], ].each do |id, name, level, count| Badge.seed do |b| b.id = id @@ -422,7 +422,7 @@ end [ [Badge::Enthusiast, "Enthusiast", BadgeType::Bronze, 10], [Badge::Aficionado, "Aficionado", BadgeType::Silver, 100], - [Badge::Devotee, "Devotee", BadgeType::Gold, 365], + [Badge::Devotee, "Devotee", BadgeType::Gold, 365], ].each do |id, name, level, days| Badge.seed do |b| b.id = id @@ -437,9 +437,11 @@ end end end -Badge.where("NOT system AND id < 100").each do |badge| - new_id = [Badge.maximum(:id) + 1, 100].max - old_id = badge.id - badge.update_columns(id: new_id) - UserBadge.where(badge_id: old_id).update_all(badge_id: new_id) -end +Badge + .where("NOT system AND id < 100") + .each do |badge| + new_id = [Badge.maximum(:id) + 1, 100].max + old_id = badge.id + badge.update_columns(id: new_id) + UserBadge.where(badge_id: old_id).update_all(badge_id: new_id) + end diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index b0484fd444..d362926bb4 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -30,7 +30,7 @@ end UserOption.where(user_id: -1).update_all( email_messages_level: UserOption.email_level_types[:never], - email_level: UserOption.email_level_types[:never] + email_level: UserOption.email_level_types[:never], ) Group.user_trust_level_change!(-1, TrustLevel[4]) @@ -44,22 +44,25 @@ if ENV["SMOKE"] == "1" ue.user_id = 0 end - smoke_user = User.seed do |u| - u.id = 0 - u.name = "smoke_user" - u.username = "smoke_user" - u.username_lower = "smoke_user" - u.password = "P4ssw0rd" - u.active = true - u.approved = true - u.approved_at = Time.now - u.trust_level = TrustLevel[3] - end.first + smoke_user = + User + .seed do |u| + u.id = 0 + u.name = "smoke_user" + u.username = "smoke_user" + u.username_lower = "smoke_user" + u.password = "P4ssw0rd" + u.active = true + u.approved = true + u.approved_at = Time.now + u.trust_level = TrustLevel[3] + end + .first UserOption.where(user_id: smoke_user.id).update_all( email_digests: false, email_messages_level: UserOption.email_level_types[:never], - email_level: UserOption.email_level_types[:never] + email_level: UserOption.email_level_types[:never], ) EmailToken.where(user_id: smoke_user.id).update_all(confirmed: true) diff --git a/db/fixtures/500_categories.rb b/db/fixtures/500_categories.rb index 060969b72e..a743b2d0ad 100644 --- a/db/fixtures/500_categories.rb +++ b/db/fixtures/500_categories.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'seed_data/categories' +require "seed_data/categories" -if !Rails.env.test? - SeedData::Categories.with_default_locale.create -end +SeedData::Categories.with_default_locale.create if !Rails.env.test? diff --git a/db/fixtures/600_themes.rb b/db/fixtures/600_themes.rb index 483214303f..ce01f8eb1c 100644 --- a/db/fixtures/600_themes.rb +++ b/db/fixtures/600_themes.rb @@ -10,23 +10,28 @@ if !Theme.exists? { name: I18n.t("color_schemes.wcag_dark"), base_scheme_id: "WCAG Dark" }, { name: I18n.t("color_schemes.dracula"), base_scheme_id: "Dracula" }, { name: I18n.t("color_schemes.solarized_light"), base_scheme_id: "Solarized Light" }, - { name: I18n.t("color_schemes.solarized_dark"), base_scheme_id: "Solarized Dark" } + { name: I18n.t("color_schemes.solarized_dark"), base_scheme_id: "Solarized Dark" }, ] color_schemes.each do |cs| scheme = ColorScheme.find_by(base_scheme_id: cs[:base_scheme_id]) - scheme ||= ColorScheme.create_from_base(name: cs[:name], via_wizard: true, base_scheme_id: cs[:base_scheme_id], user_selectable: true) + scheme ||= + ColorScheme.create_from_base( + name: cs[:name], + via_wizard: true, + base_scheme_id: cs[:base_scheme_id], + user_selectable: true, + ) end - name = I18n.t('color_schemes.default_theme_name') + name = I18n.t("color_schemes.default_theme_name") default_theme = Theme.create!(name: name, user_id: -1) default_theme.set_default! - if SiteSetting.default_dark_mode_color_scheme_id == SiteSetting.defaults[:default_dark_mode_color_scheme_id] + if SiteSetting.default_dark_mode_color_scheme_id == + SiteSetting.defaults[:default_dark_mode_color_scheme_id] dark_scheme_id = ColorScheme.where(base_scheme_id: "Dark").pluck_first(:id) - if dark_scheme_id.present? - SiteSetting.default_dark_mode_color_scheme_id = dark_scheme_id - end + SiteSetting.default_dark_mode_color_scheme_id = dark_scheme_id if dark_scheme_id.present? end end diff --git a/db/fixtures/990_topics.rb b/db/fixtures/990_topics.rb index fd29715bee..6c036ac273 100644 --- a/db/fixtures/990_topics.rb +++ b/db/fixtures/990_topics.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true if !Rails.env.test? - require 'seed_data/topics' + require "seed_data/topics" topics_exist = Topic.where(<<~SQL).exists? id NOT IN ( diff --git a/db/migrate/20000225050318_add_schema_migration_details.rb b/db/migrate/20000225050318_add_schema_migration_details.rb index 55e6abf22b..5a857485db 100644 --- a/db/migrate/20000225050318_add_schema_migration_details.rb +++ b/db/migrate/20000225050318_add_schema_migration_details.rb @@ -19,11 +19,13 @@ class AddSchemaMigrationDetails < ActiveRecord::Migration[4.2] add_index :schema_migration_details, [:version] - execute("INSERT INTO schema_migration_details(version, created_at) + execute( + "INSERT INTO schema_migration_details(version, created_at) SELECT version, current_timestamp FROM schema_migrations ORDER BY version - ") + ", + ) end def down diff --git a/db/migrate/20120311164326_create_posts.rb b/db/migrate/20120311164326_create_posts.rb index 3dd129cd94..4aadc9913f 100644 --- a/db/migrate/20120311164326_create_posts.rb +++ b/db/migrate/20120311164326_create_posts.rb @@ -11,6 +11,6 @@ class CreatePosts < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :posts, [:forum_thread_id, :created_at] + add_index :posts, %i[forum_thread_id created_at] end end diff --git a/db/migrate/20120311210245_create_sites.rb b/db/migrate/20120311210245_create_sites.rb index 4246a6ee7e..6778ee32fd 100644 --- a/db/migrate/20120311210245_create_sites.rb +++ b/db/migrate/20120311210245_create_sites.rb @@ -2,7 +2,7 @@ class CreateSites < ActiveRecord::Migration[4.2] def change - create_table :sites do |t| + create_table :sites do |t| t.string :title, limit: 100, null: false t.timestamps null: false end diff --git a/db/migrate/20120423142820_fix_post_indices.rb b/db/migrate/20120423142820_fix_post_indices.rb index 2d1042663c..19e7d69122 100644 --- a/db/migrate/20120423142820_fix_post_indices.rb +++ b/db/migrate/20120423142820_fix_post_indices.rb @@ -2,12 +2,12 @@ class FixPostIndices < ActiveRecord::Migration[4.2] def up - remove_index :posts, [:forum_thread_id, :created_at] - add_index :posts, [:forum_thread_id, :post_number] + remove_index :posts, %i[forum_thread_id created_at] + add_index :posts, %i[forum_thread_id post_number] end def down - remove_index :posts, [:forum_thread_id, :post_number] - add_index :posts, [:forum_thread_id, :created_at] + remove_index :posts, %i[forum_thread_id post_number] + add_index :posts, %i[forum_thread_id created_at] end end diff --git a/db/migrate/20120425145456_add_display_username_to_users.rb b/db/migrate/20120425145456_add_display_username_to_users.rb index e88c4ee0ca..1eefd12487 100644 --- a/db/migrate/20120425145456_add_display_username_to_users.rb +++ b/db/migrate/20120425145456_add_display_username_to_users.rb @@ -13,5 +13,4 @@ class AddDisplayUsernameToUsers < ActiveRecord::Migration[4.2] execute "UPDATE users SET username = display_username" remove_column :users, :display_username end - end diff --git a/db/migrate/20120427154330_create_vestal_versions.rb b/db/migrate/20120427154330_create_vestal_versions.rb index 364592430f..ae88fd2411 100644 --- a/db/migrate/20120427154330_create_vestal_versions.rb +++ b/db/migrate/20120427154330_create_vestal_versions.rb @@ -5,18 +5,18 @@ class CreateVestalVersions < ActiveRecord::Migration[4.2] create_table :versions do |t| t.belongs_to :versioned, polymorphic: true t.belongs_to :user, polymorphic: true - t.string :user_name - t.text :modifications + t.string :user_name + t.text :modifications t.integer :number t.integer :reverted_from - t.string :tag + t.string :tag t.timestamps null: false end change_table :versions do |t| - t.index [:versioned_id, :versioned_type] - t.index [:user_id, :user_type] + t.index %i[versioned_id versioned_type] + t.index %i[user_id user_type] t.index :user_name t.index :number t.index :tag diff --git a/db/migrate/20120502183240_add_created_by_to_forum_threads.rb b/db/migrate/20120502183240_add_created_by_to_forum_threads.rb index 62f76731b3..90c3e67883 100644 --- a/db/migrate/20120502183240_add_created_by_to_forum_threads.rb +++ b/db/migrate/20120502183240_add_created_by_to_forum_threads.rb @@ -13,5 +13,4 @@ class AddCreatedByToForumThreads < ActiveRecord::Migration[4.2] def down remove_column :forum_threads, :user_id end - end diff --git a/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb b/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb index 174e4f99d9..9d1701d9b8 100644 --- a/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb +++ b/db/migrate/20120502192121_add_last_post_user_id_to_forum_threads.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AddLastPostUserIdToForumThreads < ActiveRecord::Migration[4.2] - def up add_column :forum_threads, :last_post_user_id, :integer @@ -14,5 +13,4 @@ class AddLastPostUserIdToForumThreads < ActiveRecord::Migration[4.2] def down remove_column :forum_threads, :last_post_user_id end - end diff --git a/db/migrate/20120507144132_create_expressions.rb b/db/migrate/20120507144132_create_expressions.rb index 8c2aba4cc9..5f7fdbb5c6 100644 --- a/db/migrate/20120507144132_create_expressions.rb +++ b/db/migrate/20120507144132_create_expressions.rb @@ -10,6 +10,9 @@ class CreateExpressions < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :expressions, [:parent_id, :parent_type, :expression_type_id, :user_id], unique: true, name: "expressions_pk" + add_index :expressions, + %i[parent_id parent_type expression_type_id user_id], + unique: true, + name: "expressions_pk" end end diff --git a/db/migrate/20120507144222_create_expression_types.rb b/db/migrate/20120507144222_create_expression_types.rb index 70630349fb..3e83a0551d 100644 --- a/db/migrate/20120507144222_create_expression_types.rb +++ b/db/migrate/20120507144222_create_expression_types.rb @@ -9,6 +9,6 @@ class CreateExpressionTypes < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :expression_types, [:site_id, :name], unique: true + add_index :expression_types, %i[site_id name], unique: true end end diff --git a/db/migrate/20120514144549_add_reply_count_to_posts.rb b/db/migrate/20120514144549_add_reply_count_to_posts.rb index 8209c8281f..d0264b4270 100644 --- a/db/migrate/20120514144549_add_reply_count_to_posts.rb +++ b/db/migrate/20120514144549_add_reply_count_to_posts.rb @@ -11,5 +11,4 @@ class AddReplyCountToPosts < ActiveRecord::Migration[4.2] def down remove_column :posts, :reply_count end - end diff --git a/db/migrate/20120518200115_create_read_posts.rb b/db/migrate/20120518200115_create_read_posts.rb index c17305159a..377220f1fe 100644 --- a/db/migrate/20120518200115_create_read_posts.rb +++ b/db/migrate/20120518200115_create_read_posts.rb @@ -9,11 +9,10 @@ class CreateReadPosts < ActiveRecord::Migration[4.2] t.column :seen, :integer, null: false end - add_index :read_posts, [:forum_thread_id, :user_id, :page], unique: true + add_index :read_posts, %i[forum_thread_id user_id page], unique: true end def down drop_table :read_posts end - end diff --git a/db/migrate/20120519182212_create_last_read_posts.rb b/db/migrate/20120519182212_create_last_read_posts.rb index 3b9e47b84f..23f0720701 100644 --- a/db/migrate/20120519182212_create_last_read_posts.rb +++ b/db/migrate/20120519182212_create_last_read_posts.rb @@ -9,6 +9,6 @@ class CreateLastReadPosts < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :last_read_posts, [:user_id, :forum_thread_id], unique: true + add_index :last_read_posts, %i[user_id forum_thread_id], unique: true end end diff --git a/db/migrate/20120523180723_create_views.rb b/db/migrate/20120523180723_create_views.rb index 78da199478..a1ff9258a5 100644 --- a/db/migrate/20120523180723_create_views.rb +++ b/db/migrate/20120523180723_create_views.rb @@ -3,14 +3,14 @@ class CreateViews < ActiveRecord::Migration[4.2] def change create_table :views, id: false do |t| - t.integer :parent_id, null: false - t.string :parent_type, limit: 50, null: false - t.integer :ip, limit: 8, null: false + t.integer :parent_id, null: false + t.string :parent_type, limit: 50, null: false + t.integer :ip, limit: 8, null: false t.datetime :viewed_at, null: false - t.integer :user_id, null: true + t.integer :user_id, null: true end - add_index :views, [:parent_id, :parent_type] - add_index :views, [:parent_id, :parent_type, :ip, :viewed_at], unique: true, name: "unique_views" + add_index :views, %i[parent_id parent_type] + add_index :views, %i[parent_id parent_type ip viewed_at], unique: true, name: "unique_views" end end diff --git a/db/migrate/20120525194845_add_avg_time_to_forum_threads.rb b/db/migrate/20120525194845_add_avg_time_to_forum_threads.rb index 0d4533e910..95e2e4780a 100644 --- a/db/migrate/20120525194845_add_avg_time_to_forum_threads.rb +++ b/db/migrate/20120525194845_add_avg_time_to_forum_threads.rb @@ -10,5 +10,4 @@ class AddAvgTimeToForumThreads < ActiveRecord::Migration[4.2] def down remove_column :forum_threads, :avg_time end - end diff --git a/db/migrate/20120529175956_create_uploads.rb b/db/migrate/20120529175956_create_uploads.rb index 02d7cc39b9..a3756d5d96 100644 --- a/db/migrate/20120529175956_create_uploads.rb +++ b/db/migrate/20120529175956_create_uploads.rb @@ -5,16 +5,15 @@ class CreateUploads < ActiveRecord::Migration[4.2] create_table :uploads do |t| t.integer :user_id, null: false t.integer :forum_thread_id, null: false - t.string :original_filename, null: false + t.string :original_filename, null: false t.integer :filesize, null: false t.integer :width, null: true t.integer :height, null: true - t.string :url, null: false + t.string :url, null: false t.timestamps null: false end add_index :uploads, :forum_thread_id add_index :uploads, :user_id end - end diff --git a/db/migrate/20120529202707_create_stars.rb b/db/migrate/20120529202707_create_stars.rb index 99655810df..5c8df40d49 100644 --- a/db/migrate/20120529202707_create_stars.rb +++ b/db/migrate/20120529202707_create_stars.rb @@ -3,12 +3,12 @@ class CreateStars < ActiveRecord::Migration[4.2] def change create_table :stars, id: false do |t| - t.integer :parent_id, null: false - t.string :parent_type, limit: 50, null: false - t.integer :user_id, null: true + t.integer :parent_id, null: false + t.string :parent_type, limit: 50, null: false + t.integer :user_id, null: true t.timestamps null: false end - add_index :stars, [:parent_id, :parent_type, :user_id] + add_index :stars, %i[parent_id parent_type user_id] end end diff --git a/db/migrate/20120530150726_create_forum_thread_user.rb b/db/migrate/20120530150726_create_forum_thread_user.rb index 2597499387..7196aac944 100644 --- a/db/migrate/20120530150726_create_forum_thread_user.rb +++ b/db/migrate/20120530150726_create_forum_thread_user.rb @@ -3,17 +3,17 @@ class CreateForumThreadUser < ActiveRecord::Migration[4.2] def up create_table :forum_thread_users, id: false do |t| - t.integer :user_id, null: false - t.integer :forum_thread_id, null: false - t.boolean :starred, null: false, default: false - t.boolean :posted, null: false, default: false - t.integer :last_read_post_number, null: false, default: 1 + t.integer :user_id, null: false + t.integer :forum_thread_id, null: false + t.boolean :starred, null: false, default: false + t.boolean :posted, null: false, default: false + t.integer :last_read_post_number, null: false, default: 1 t.timestamps null: false end execute "DELETE FROM read_posts" - add_index :forum_thread_users, [:forum_thread_id, :user_id], unique: true + add_index :forum_thread_users, %i[forum_thread_id user_id], unique: true drop_table :stars drop_table :last_read_posts @@ -23,13 +23,13 @@ class CreateForumThreadUser < ActiveRecord::Migration[4.2] drop_table :forum_thread_users create_table :stars, id: false do |t| - t.integer :parent_id, null: false - t.string :parent_type, limit: 50, null: false - t.integer :user_id, null: true + t.integer :parent_id, null: false + t.string :parent_type, limit: 50, null: false + t.integer :user_id, null: true t.timestamps null: false end - add_index :stars, [:parent_id, :parent_type, :user_id] + add_index :stars, %i[parent_id parent_type user_id] create_table :last_read_posts do |t| t.integer :user_id, null: false @@ -38,6 +38,6 @@ class CreateForumThreadUser < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :last_read_posts, [:user_id, :forum_thread_id], unique: true + add_index :last_read_posts, %i[user_id forum_thread_id], unique: true end end diff --git a/db/migrate/20120530212912_create_forum_thread_links.rb b/db/migrate/20120530212912_create_forum_thread_links.rb index de8b5e9962..f974247bf6 100644 --- a/db/migrate/20120530212912_create_forum_thread_links.rb +++ b/db/migrate/20120530212912_create_forum_thread_links.rb @@ -6,8 +6,8 @@ class CreateForumThreadLinks < ActiveRecord::Migration[4.2] t.integer :forum_thread_id, null: false t.integer :post_id, null: false t.integer :user_id, null: false - t.string :url, limit: 500, null: false - t.string :domain, limit: 100, null: false + t.string :url, limit: 500, null: false + t.string :domain, limit: 100, null: false t.boolean :internal, null: false, default: false t.integer :link_forum_thread_id, null: true t.timestamps null: false diff --git a/db/migrate/20120615180517_create_bookmarks.rb b/db/migrate/20120615180517_create_bookmarks.rb index 49433756eb..fa1ffca61c 100644 --- a/db/migrate/20120615180517_create_bookmarks.rb +++ b/db/migrate/20120615180517_create_bookmarks.rb @@ -8,6 +8,6 @@ class CreateBookmarks < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :bookmarks, [:user_id, :post_id], unique: true + add_index :bookmarks, %i[user_id post_id], unique: true end end diff --git a/db/migrate/20120618212349_create_post_timings.rb b/db/migrate/20120618212349_create_post_timings.rb index b8ea4a2858..c4297820ea 100644 --- a/db/migrate/20120618212349_create_post_timings.rb +++ b/db/migrate/20120618212349_create_post_timings.rb @@ -9,7 +9,7 @@ class CreatePostTimings < ActiveRecord::Migration[4.2] t.integer :msecs, null: false end - add_index :post_timings, [:thread_id, :post_number] - add_index :post_timings, [:thread_id, :post_number, :user_id], unique: true + add_index :post_timings, %i[thread_id post_number] + add_index :post_timings, %i[thread_id post_number user_id], unique: true end end diff --git a/db/migrate/20120618214856_create_message_bus.rb b/db/migrate/20120618214856_create_message_bus.rb index 929fb0fea5..a71c312eb4 100644 --- a/db/migrate/20120618214856_create_message_bus.rb +++ b/db/migrate/20120618214856_create_message_bus.rb @@ -11,5 +11,4 @@ class CreateMessageBus < ActiveRecord::Migration[4.2] add_index :message_bus, [:created_at] end - end diff --git a/db/migrate/20120619150807_fix_post_timings.rb b/db/migrate/20120619150807_fix_post_timings.rb index c2ff90805a..49bd065b3b 100644 --- a/db/migrate/20120619150807_fix_post_timings.rb +++ b/db/migrate/20120619150807_fix_post_timings.rb @@ -2,12 +2,14 @@ class FixPostTimings < ActiveRecord::Migration[4.2] def up - remove_index :post_timings, [:thread_id, :post_number] - remove_index :post_timings, [:thread_id, :post_number, :user_id] + remove_index :post_timings, %i[thread_id post_number] + remove_index :post_timings, %i[thread_id post_number user_id] rename_column :post_timings, :thread_id, :forum_thread_id - add_index :post_timings, [:forum_thread_id, :post_number], name: 'post_timings_summary' - add_index :post_timings, [:forum_thread_id, :post_number, :user_id], unique: true, name: 'post_timings_unique' - + add_index :post_timings, %i[forum_thread_id post_number], name: "post_timings_summary" + add_index :post_timings, + %i[forum_thread_id post_number user_id], + unique: true, + name: "post_timings_unique" end def down diff --git a/db/migrate/20120619172714_add_post_number_to_bookmarks.rb b/db/migrate/20120619172714_add_post_number_to_bookmarks.rb index 6976191f42..d411d601f2 100644 --- a/db/migrate/20120619172714_add_post_number_to_bookmarks.rb +++ b/db/migrate/20120619172714_add_post_number_to_bookmarks.rb @@ -11,6 +11,6 @@ class AddPostNumberToBookmarks < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :bookmarks, [:user_id, :forum_thread_id, :post_number], unique: true + add_index :bookmarks, %i[user_id forum_thread_id post_number], unique: true end end diff --git a/db/migrate/20120622200242_create_notifications.rb b/db/migrate/20120622200242_create_notifications.rb index b6f8a320b7..d43b29dfb2 100644 --- a/db/migrate/20120622200242_create_notifications.rb +++ b/db/migrate/20120622200242_create_notifications.rb @@ -10,6 +10,6 @@ class CreateNotifications < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :notifications, [:user_id, :created_at] + add_index :notifications, %i[user_id created_at] end end diff --git a/db/migrate/20120625145714_add_seen_notification_id_to_users.rb b/db/migrate/20120625145714_add_seen_notification_id_to_users.rb index 75b975dd7c..ffae6a811d 100644 --- a/db/migrate/20120625145714_add_seen_notification_id_to_users.rb +++ b/db/migrate/20120625145714_add_seen_notification_id_to_users.rb @@ -2,7 +2,6 @@ class AddSeenNotificationIdToUsers < ActiveRecord::Migration[4.2] def change - execute "TRUNCATE TABLE notifications" add_column :users, :seen_notificaiton_id, :integer, default: 0, null: false diff --git a/db/migrate/20120629143908_rename_expression_type_id.rb b/db/migrate/20120629143908_rename_expression_type_id.rb index 630cc4afae..78958d82df 100644 --- a/db/migrate/20120629143908_rename_expression_type_id.rb +++ b/db/migrate/20120629143908_rename_expression_type_id.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class RenameExpressionTypeId < ActiveRecord::Migration[4.2] - def up add_column :expression_types, :expression_index, :integer execute "UPDATE expression_types SET expression_index = id" remove_column :expression_types, :id - add_index :expression_types, [:site_id, :expression_index], unique: true + add_index :expression_types, %i[site_id expression_index], unique: true end def down diff --git a/db/migrate/20120629150253_denormalize_expressions.rb b/db/migrate/20120629150253_denormalize_expressions.rb index eb0e386ec1..e81c38ba55 100644 --- a/db/migrate/20120629150253_denormalize_expressions.rb +++ b/db/migrate/20120629150253_denormalize_expressions.rb @@ -2,7 +2,6 @@ class DenormalizeExpressions < ActiveRecord::Migration[4.2] def change - # Denormalizing this makes our queries so, so, so much nicer add_column :posts, :expression1_count, :integer, null: false, default: 0 @@ -22,5 +21,4 @@ class DenormalizeExpressions < ActiveRecord::Migration[4.2] execute "update forum_threads set expression#{i}_count = (select sum(expression#{i}_count) from posts where forum_thread_id = forum_threads.id)" end end - end diff --git a/db/migrate/20120629151243_make_expressions_less_generic.rb b/db/migrate/20120629151243_make_expressions_less_generic.rb index 030d5bb2bd..3ac9d1c471 100644 --- a/db/migrate/20120629151243_make_expressions_less_generic.rb +++ b/db/migrate/20120629151243_make_expressions_less_generic.rb @@ -6,7 +6,10 @@ class MakeExpressionsLessGeneric < ActiveRecord::Migration[4.2] rename_column :expressions, :expression_type_id, :expression_index remove_column :expressions, :parent_type - add_index :expressions, [:post_id, :expression_index, :user_id], unique: true, name: 'unique_by_user' + add_index :expressions, + %i[post_id expression_index user_id], + unique: true, + name: "unique_by_user" end def down @@ -14,5 +17,4 @@ class MakeExpressionsLessGeneric < ActiveRecord::Migration[4.2] rename_column :expressions, :expression_index, :expression_type_id add_column :expressions, :parent_type, :string, null: true end - end diff --git a/db/migrate/20120629182637_create_incoming_links.rb b/db/migrate/20120629182637_create_incoming_links.rb index 70a1ffa2e9..9b61bb774b 100644 --- a/db/migrate/20120629182637_create_incoming_links.rb +++ b/db/migrate/20120629182637_create_incoming_links.rb @@ -12,6 +12,6 @@ class CreateIncomingLinks < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :incoming_links, [:site_id, :forum_thread_id, :post_number], name: 'incoming_index' + add_index :incoming_links, %i[site_id forum_thread_id post_number], name: "incoming_index" end end diff --git a/db/migrate/20120702211427_create_replies.rb b/db/migrate/20120702211427_create_replies.rb index a252a11501..ef792918b1 100644 --- a/db/migrate/20120702211427_create_replies.rb +++ b/db/migrate/20120702211427_create_replies.rb @@ -8,7 +8,7 @@ class CreateReplies < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :post_replies, [:post_id, :reply_id], unique: true + add_index :post_replies, %i[post_id reply_id], unique: true execute "INSERT INTO post_replies (post_id, reply_id, created_at, updated_at) SELECT p2.id, p.id, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP diff --git a/db/migrate/20120712150500_create_categories.rb b/db/migrate/20120712150500_create_categories.rb index 3f5ef230fd..f94bbcf50e 100644 --- a/db/migrate/20120712150500_create_categories.rb +++ b/db/migrate/20120712150500_create_categories.rb @@ -4,7 +4,7 @@ class CreateCategories < ActiveRecord::Migration[4.2] def up create_table :categories do |t| t.string :name, limit: 50, null: false - t.string :color, limit: 6, null: false, default: '0088CC' + t.string :color, limit: 6, null: false, default: "0088CC" t.integer :forum_thread_id, null: true t.integer :top1_forum_thread_id, null: true t.integer :top2_forum_thread_id, null: true @@ -26,5 +26,4 @@ class CreateCategories < ActiveRecord::Migration[4.2] def down drop_table :categories end - end diff --git a/db/migrate/20120712151934_add_category_id_to_forum_threads.rb b/db/migrate/20120712151934_add_category_id_to_forum_threads.rb index d9feac5477..df8dca72da 100644 --- a/db/migrate/20120712151934_add_category_id_to_forum_threads.rb +++ b/db/migrate/20120712151934_add_category_id_to_forum_threads.rb @@ -16,5 +16,4 @@ class AddCategoryIdToForumThreads < ActiveRecord::Migration[4.2] remove_column :forum_threads, :category_id add_column :forum_threads, :tag, :string, limit: 20 end - end diff --git a/db/migrate/20120713201324_create_category_featured_threads.rb b/db/migrate/20120713201324_create_category_featured_threads.rb index 3008224433..38df7f177f 100644 --- a/db/migrate/20120713201324_create_category_featured_threads.rb +++ b/db/migrate/20120713201324_create_category_featured_threads.rb @@ -8,6 +8,9 @@ class CreateCategoryFeaturedThreads < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :category_featured_threads, [:category_id, :forum_thread_id], unique: true, name: 'cat_featured_threads' + add_index :category_featured_threads, + %i[category_id forum_thread_id], + unique: true, + name: "cat_featured_threads" end end diff --git a/db/migrate/20120718044955_create_user_open_ids.rb b/db/migrate/20120718044955_create_user_open_ids.rb index 19e0c0e797..aa9133dd1b 100644 --- a/db/migrate/20120718044955_create_user_open_ids.rb +++ b/db/migrate/20120718044955_create_user_open_ids.rb @@ -10,6 +10,5 @@ class CreateUserOpenIds < ActiveRecord::Migration[4.2] end add_index :user_open_ids, [:url] - end end diff --git a/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb b/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb index 355e4c42b3..db77582327 100644 --- a/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb +++ b/db/migrate/20120719004636_add_email_hashed_password_name_salt_to_users.rb @@ -17,7 +17,6 @@ class AddEmailHashedPasswordNameSaltToUsers < ActiveRecord::Migration[4.2] add_column :users, :activation_key, :string, limit: 32 add_column :user_open_ids, :active, :boolean, null: false - end def down diff --git a/db/migrate/20120720162422_add_forum_id_to_categories.rb b/db/migrate/20120720162422_add_forum_id_to_categories.rb index 8868e76785..d9213e5d1e 100644 --- a/db/migrate/20120720162422_add_forum_id_to_categories.rb +++ b/db/migrate/20120720162422_add_forum_id_to_categories.rb @@ -10,5 +10,4 @@ class AddForumIdToCategories < ActiveRecord::Migration[4.2] def down remove_column :categories, :forum_id end - end diff --git a/db/migrate/20120726201830_add_invisible_to_forum_thread.rb b/db/migrate/20120726201830_add_invisible_to_forum_thread.rb index faad592120..50d29688ea 100644 --- a/db/migrate/20120726201830_add_invisible_to_forum_thread.rb +++ b/db/migrate/20120726201830_add_invisible_to_forum_thread.rb @@ -10,5 +10,4 @@ class AddInvisibleToForumThread < ActiveRecord::Migration[4.2] remove_column :forum_threads, :invisible change_column :categories, :excerpt, :string, limit: 250, null: true end - end diff --git a/db/migrate/20120727150428_rename_invisible.rb b/db/migrate/20120727150428_rename_invisible.rb index 68986b1cad..b06238881b 100644 --- a/db/migrate/20120727150428_rename_invisible.rb +++ b/db/migrate/20120727150428_rename_invisible.rb @@ -2,10 +2,8 @@ class RenameInvisible < ActiveRecord::Migration[4.2] def change - add_column :forum_threads, :visible, :boolean, default: true, null: false execute "UPDATE forum_threads SET visible = CASE WHEN invisible THEN false ELSE true END" remove_column :forum_threads, :invisible - end end diff --git a/db/migrate/20120807223020_create_actions.rb b/db/migrate/20120807223020_create_actions.rb index 0fe02dd09a..2a1843b706 100644 --- a/db/migrate/20120807223020_create_actions.rb +++ b/db/migrate/20120807223020_create_actions.rb @@ -3,7 +3,6 @@ class CreateActions < ActiveRecord::Migration[4.2] def change create_table :actions do |t| - # I elected for multiple ids as opposed to using :as cause it makes the table # thinner, and the joining semantics much simpler (a simple multiple left join will do) # @@ -20,7 +19,7 @@ class CreateActions < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :actions, [:user_id, :action_type] + add_index :actions, %i[user_id action_type] add_index :actions, [:acting_user_id] end end diff --git a/db/migrate/20120809020415_remove_site_id.rb b/db/migrate/20120809020415_remove_site_id.rb index 93c242f897..55beb92040 100644 --- a/db/migrate/20120809020415_remove_site_id.rb +++ b/db/migrate/20120809020415_remove_site_id.rb @@ -2,23 +2,23 @@ class RemoveSiteId < ActiveRecord::Migration[4.2] def up - drop_table 'sites' - remove_index 'incoming_links', name: "incoming_index" - add_index "incoming_links", ["forum_thread_id", "post_number"], name: "incoming_index" - remove_column 'incoming_links', 'site_id' - remove_index 'users', name: 'index_users_on_site_id' - remove_column 'users', 'site_id' + drop_table "sites" + remove_index "incoming_links", name: "incoming_index" + add_index "incoming_links", %w[forum_thread_id post_number], name: "incoming_index" + remove_column "incoming_links", "site_id" + remove_index "users", name: "index_users_on_site_id" + remove_column "users", "site_id" - remove_index 'expression_types', name: 'index_expression_types_on_site_id_and_expression_index' - remove_index 'expression_types', name: 'index_expression_types_on_site_id_and_name' - remove_column 'expression_types', 'site_id' + remove_index "expression_types", name: "index_expression_types_on_site_id_and_expression_index" + remove_index "expression_types", name: "index_expression_types_on_site_id_and_name" + remove_column "expression_types", "site_id" add_index "expression_types", ["expression_index"], unique: true add_index "expression_types", ["name"], unique: true - drop_table 'forums' + drop_table "forums" end def down - raise 'not reversable' + raise "not reversable" end end diff --git a/db/migrate/20120809030647_remove_forum_id.rb b/db/migrate/20120809030647_remove_forum_id.rb index e13cfade6c..084d76c8c0 100644 --- a/db/migrate/20120809030647_remove_forum_id.rb +++ b/db/migrate/20120809030647_remove_forum_id.rb @@ -2,11 +2,11 @@ class RemoveForumId < ActiveRecord::Migration[4.2] def up - remove_column 'forum_threads', 'forum_id' - remove_column 'categories', 'forum_id' + remove_column "forum_threads", "forum_id" + remove_column "categories", "forum_id" end def down - raise 'not reversible' + raise "not reversible" end end diff --git a/db/migrate/20120809053414_correct_indexing_on_posts.rb b/db/migrate/20120809053414_correct_indexing_on_posts.rb index 2600244583..d2b5e49ae6 100644 --- a/db/migrate/20120809053414_correct_indexing_on_posts.rb +++ b/db/migrate/20120809053414_correct_indexing_on_posts.rb @@ -13,11 +13,10 @@ from ) as c where pp.id = c.id and pp.post_number <> c.real_number" - remove_index "posts", ["forum_thread_id", "post_number"] + remove_index "posts", %w[forum_thread_id post_number] # this needs to be unique if it is not we can not use post_number to identify a post - add_index "posts", ["forum_thread_id", "post_number"], unique: true - + add_index "posts", %w[forum_thread_id post_number], unique: true end def down diff --git a/db/migrate/20120809154750_remove_index_for_now.rb b/db/migrate/20120809154750_remove_index_for_now.rb index 44a2c1189e..daef0e2fe2 100644 --- a/db/migrate/20120809154750_remove_index_for_now.rb +++ b/db/migrate/20120809154750_remove_index_for_now.rb @@ -2,12 +2,12 @@ class RemoveIndexForNow < ActiveRecord::Migration[4.2] def up - remove_index "posts", ["forum_thread_id", "post_number"] - add_index "posts", ["forum_thread_id", "post_number"], unique: false + remove_index "posts", %w[forum_thread_id post_number] + add_index "posts", %w[forum_thread_id post_number], unique: false end def down - remove_index "posts", ["forum_thread_id", "post_number"] - add_index "posts", ["forum_thread_id", "post_number"], unique: true + remove_index "posts", %w[forum_thread_id post_number] + add_index "posts", %w[forum_thread_id post_number], unique: true end end diff --git a/db/migrate/20120809174649_create_post_actions.rb b/db/migrate/20120809174649_create_post_actions.rb index e437e40b54..a76ea9ede9 100644 --- a/db/migrate/20120809174649_create_post_actions.rb +++ b/db/migrate/20120809174649_create_post_actions.rb @@ -13,9 +13,8 @@ class CreatePostActions < ActiveRecord::Migration[4.2] add_index :post_actions, ["post_id"] # no support for this till rails 4 - execute 'create unique index idx_unique_actions on - post_actions(user_id, post_action_type_id, post_id) where deleted_at is null' - + execute "create unique index idx_unique_actions on + post_actions(user_id, post_action_type_id, post_id) where deleted_at is null" end def down drop_table :post_actions diff --git a/db/migrate/20120810064839_rename_actions_to_user_actions.rb b/db/migrate/20120810064839_rename_actions_to_user_actions.rb index 051e2e9fef..631fdfb8e0 100644 --- a/db/migrate/20120810064839_rename_actions_to_user_actions.rb +++ b/db/migrate/20120810064839_rename_actions_to_user_actions.rb @@ -2,6 +2,6 @@ class RenameActionsToUserActions < ActiveRecord::Migration[4.2] def change - rename_table 'actions', 'user_actions' + rename_table "actions", "user_actions" end end diff --git a/db/migrate/20120812235417_retire_expressions.rb b/db/migrate/20120812235417_retire_expressions.rb index a5494ccd2c..7f0d683be1 100644 --- a/db/migrate/20120812235417_retire_expressions.rb +++ b/db/migrate/20120812235417_retire_expressions.rb @@ -2,7 +2,7 @@ class RetireExpressions < ActiveRecord::Migration[4.2] def up - execute 'insert into post_actions (post_action_type_id, user_id, post_id, created_at, updated_at) + execute "insert into post_actions (post_action_type_id, user_id, post_id, created_at, updated_at) select case when expression_index=1 then 3 @@ -10,10 +10,10 @@ select when expression_index=3 then 2 end - , user_id, post_id, created_at, updated_at from expressions' + , user_id, post_id, created_at, updated_at from expressions" - drop_table 'expressions' - drop_table 'expression_types' + drop_table "expressions" + drop_table "expression_types" end def down diff --git a/db/migrate/20120813004347_rename_expression_columns_in_forum_thread.rb b/db/migrate/20120813004347_rename_expression_columns_in_forum_thread.rb index 43958a8622..f42ce2539a 100644 --- a/db/migrate/20120813004347_rename_expression_columns_in_forum_thread.rb +++ b/db/migrate/20120813004347_rename_expression_columns_in_forum_thread.rb @@ -2,11 +2,10 @@ class RenameExpressionColumnsInForumThread < ActiveRecord::Migration[4.2] def change - rename_column 'forum_threads', 'expression1_count', 'off_topic_count' - rename_column 'forum_threads', 'expression2_count', 'offensive_count' - rename_column 'forum_threads', 'expression3_count', 'like_count' - remove_column 'forum_threads', 'expression4_count' - remove_column 'forum_threads', 'expression5_count' - + rename_column "forum_threads", "expression1_count", "off_topic_count" + rename_column "forum_threads", "expression2_count", "offensive_count" + rename_column "forum_threads", "expression3_count", "like_count" + remove_column "forum_threads", "expression4_count" + remove_column "forum_threads", "expression5_count" end end diff --git a/db/migrate/20120813042912_rename_expression_columns_in_posts.rb b/db/migrate/20120813042912_rename_expression_columns_in_posts.rb index 90ac91d415..4db02e8143 100644 --- a/db/migrate/20120813042912_rename_expression_columns_in_posts.rb +++ b/db/migrate/20120813042912_rename_expression_columns_in_posts.rb @@ -2,10 +2,10 @@ class RenameExpressionColumnsInPosts < ActiveRecord::Migration[4.2] def change - rename_column 'posts', 'expression1_count', 'off_topic_count' - rename_column 'posts', 'expression2_count', 'offensive_count' - rename_column 'posts', 'expression3_count', 'like_count' - remove_column 'posts', 'expression4_count' - remove_column 'posts', 'expression5_count' + rename_column "posts", "expression1_count", "off_topic_count" + rename_column "posts", "expression2_count", "offensive_count" + rename_column "posts", "expression3_count", "like_count" + remove_column "posts", "expression4_count" + remove_column "posts", "expression5_count" end end diff --git a/db/migrate/20120815004411_add_unique_index_to_forum_thread_links.rb b/db/migrate/20120815004411_add_unique_index_to_forum_thread_links.rb index ffbfd6d278..c8b53bc59b 100644 --- a/db/migrate/20120815004411_add_unique_index_to_forum_thread_links.rb +++ b/db/migrate/20120815004411_add_unique_index_to_forum_thread_links.rb @@ -2,7 +2,6 @@ class AddUniqueIndexToForumThreadLinks < ActiveRecord::Migration[4.2] def change - execute "DELETE FROM forum_thread_links USING forum_thread_links ftl2 WHERE ftl2.forum_thread_id = forum_thread_links.forum_thread_id AND ftl2.post_id = forum_thread_links.post_id @@ -10,6 +9,9 @@ class AddUniqueIndexToForumThreadLinks < ActiveRecord::Migration[4.2] AND ftl2.id < forum_thread_links.id" # Add the unique index - add_index :forum_thread_links, [:forum_thread_id, :post_id, :url], unique: true, name: 'unique_post_links' + add_index :forum_thread_links, + %i[forum_thread_id post_id url], + unique: true, + name: "unique_post_links" end end diff --git a/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb b/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb index 0b71524f63..bee131ef9a 100644 --- a/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb +++ b/db/migrate/20120816050526_add_unique_constraint_to_user_actions.rb @@ -2,6 +2,9 @@ class AddUniqueConstraintToUserActions < ActiveRecord::Migration[4.2] def change - add_index :user_actions, ['action_type', 'user_id', 'target_forum_thread_id', 'target_post_id', 'acting_user_id'], name: "idx_unique_rows", unique: true + add_index :user_actions, + %w[action_type user_id target_forum_thread_id target_post_id acting_user_id], + name: "idx_unique_rows", + unique: true end end diff --git a/db/migrate/20120816205538_add_starred_at_to_forum_thread_user.rb b/db/migrate/20120816205538_add_starred_at_to_forum_thread_user.rb index 8421b8b544..f510672f1d 100644 --- a/db/migrate/20120816205538_add_starred_at_to_forum_thread_user.rb +++ b/db/migrate/20120816205538_add_starred_at_to_forum_thread_user.rb @@ -3,20 +3,21 @@ class AddStarredAtToForumThreadUser < ActiveRecord::Migration[4.2] def up add_column :forum_thread_users, :starred_at, :datetime - DB.exec 'update forum_thread_users f set starred_at = COALESCE(created_at, ?) + DB.exec "update forum_thread_users f set starred_at = COALESCE(created_at, ?) from ( select f1.forum_thread_id, f1.user_id, t.created_at from forum_thread_users f1 left join forum_threads t on f1.forum_thread_id = t.id ) x - where x.forum_thread_id = f.forum_thread_id and x.user_id = f.user_id', [DateTime.now] + where x.forum_thread_id = f.forum_thread_id and x.user_id = f.user_id", + [DateTime.now] # probably makes sense to move this out to forum_thread_actions - execute 'alter table forum_thread_users add constraint test_starred_at check(starred = false or starred_at is not null)' + execute "alter table forum_thread_users add constraint test_starred_at check(starred = false or starred_at is not null)" end def down - execute 'alter table forum_thread_users drop constraint test_starred_at' + execute "alter table forum_thread_users drop constraint test_starred_at" remove_column :forum_thread_users, :starred_at end end diff --git a/db/migrate/20120824171908_create_category_featured_users.rb b/db/migrate/20120824171908_create_category_featured_users.rb index 5a57b77214..9d896b46d8 100644 --- a/db/migrate/20120824171908_create_category_featured_users.rb +++ b/db/migrate/20120824171908_create_category_featured_users.rb @@ -8,6 +8,6 @@ class CreateCategoryFeaturedUsers < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :category_featured_users, [:category_id, :user_id], unique: true + add_index :category_featured_users, %i[category_id user_id], unique: true end end diff --git a/db/migrate/20120828204624_create_post_onebox_renders.rb b/db/migrate/20120828204624_create_post_onebox_renders.rb index 4eadbfbd23..6e6baa2c1a 100644 --- a/db/migrate/20120828204624_create_post_onebox_renders.rb +++ b/db/migrate/20120828204624_create_post_onebox_renders.rb @@ -7,6 +7,6 @@ class CreatePostOneboxRenders < ActiveRecord::Migration[4.2] t.references :onebox_render, null: false t.timestamps null: false end - add_index :post_onebox_renders, [:post_id, :onebox_render_id], unique: true + add_index :post_onebox_renders, %i[post_id onebox_render_id], unique: true end end diff --git a/db/migrate/20120918205931_add_sub_tag_to_forum_threads.rb b/db/migrate/20120918205931_add_sub_tag_to_forum_threads.rb index 2b2d4e9911..74c223d0b7 100644 --- a/db/migrate/20120918205931_add_sub_tag_to_forum_threads.rb +++ b/db/migrate/20120918205931_add_sub_tag_to_forum_threads.rb @@ -3,6 +3,6 @@ class AddSubTagToForumThreads < ActiveRecord::Migration[4.2] def change add_column :forum_threads, :sub_tag, :string - add_index :forum_threads, [:category_id, :sub_tag, :bumped_at] + add_index :forum_threads, %i[category_id sub_tag bumped_at] end end diff --git a/db/migrate/20120919152846_add_has_best_of_to_forum_threads.rb b/db/migrate/20120919152846_add_has_best_of_to_forum_threads.rb index c37187cef4..a8a624cc83 100644 --- a/db/migrate/20120919152846_add_has_best_of_to_forum_threads.rb +++ b/db/migrate/20120919152846_add_has_best_of_to_forum_threads.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true class AddHasBestOfToForumThreads < ActiveRecord::Migration[4.2] - def change add_column :forum_threads, :has_best_of, :boolean, default: false, null: false change_column :posts, :score, :float end - end diff --git a/db/migrate/20120921055428_add_twitter_user_info.rb b/db/migrate/20120921055428_add_twitter_user_info.rb index 8f27148c11..b88eece069 100644 --- a/db/migrate/20120921055428_add_twitter_user_info.rb +++ b/db/migrate/20120921055428_add_twitter_user_info.rb @@ -2,7 +2,7 @@ class AddTwitterUserInfo < ActiveRecord::Migration[4.2] def change - create_table :twitter_user_infos do |t| + create_table :twitter_user_infos do |t| t.integer :user_id, null: false t.string :screen_name, null: false t.integer :twitter_user_id, null: false diff --git a/db/migrate/20120921155050_create_archetypes.rb b/db/migrate/20120921155050_create_archetypes.rb index d8f7954bed..33626ca358 100644 --- a/db/migrate/20120921155050_create_archetypes.rb +++ b/db/migrate/20120921155050_create_archetypes.rb @@ -18,5 +18,4 @@ class CreateArchetypes < ActiveRecord::Migration[4.2] remove_column :forum_threads, :archetype_id drop_table :archetypes end - end diff --git a/db/migrate/20121011155904_create_email_logs.rb b/db/migrate/20121011155904_create_email_logs.rb index 9d04908bb8..cd59f899cd 100644 --- a/db/migrate/20121011155904_create_email_logs.rb +++ b/db/migrate/20121011155904_create_email_logs.rb @@ -10,6 +10,6 @@ class CreateEmailLogs < ActiveRecord::Migration[4.2] end add_index :email_logs, :created_at, order: { created_at: :desc } - add_index :email_logs, [:user_id, :created_at], order: { created_at: :desc } + add_index :email_logs, %i[user_id created_at], order: { created_at: :desc } end end diff --git a/db/migrate/20121017162924_convert_archetypes.rb b/db/migrate/20121017162924_convert_archetypes.rb index 891c413315..8adce8204a 100644 --- a/db/migrate/20121017162924_convert_archetypes.rb +++ b/db/migrate/20121017162924_convert_archetypes.rb @@ -2,7 +2,7 @@ class ConvertArchetypes < ActiveRecord::Migration[4.2] def up - add_column :forum_threads, :archetype, :string, default: 'regular', null: false + add_column :forum_threads, :archetype, :string, default: "regular", null: false execute "UPDATE forum_threads SET archetype = a.name_key FROM archetypes AS a WHERE a.id = forum_threads.archetype_id" remove_column :forum_threads, :archetype_id diff --git a/db/migrate/20121018103721_rename_forum_thread_tables.rb b/db/migrate/20121018103721_rename_forum_thread_tables.rb index cdcba0994f..1b72b75cec 100644 --- a/db/migrate/20121018103721_rename_forum_thread_tables.rb +++ b/db/migrate/20121018103721_rename_forum_thread_tables.rb @@ -2,39 +2,39 @@ class RenameForumThreadTables < ActiveRecord::Migration[4.2] def change - rename_table 'forum_threads', 'topics' - rename_table 'forum_thread_link_clicks', 'topic_link_clicks' - rename_table 'forum_thread_links', 'topic_links' - rename_table 'forum_thread_users', 'topic_users' - rename_table 'category_featured_threads', 'category_featured_topics' + rename_table "forum_threads", "topics" + rename_table "forum_thread_link_clicks", "topic_link_clicks" + rename_table "forum_thread_links", "topic_links" + rename_table "forum_thread_users", "topic_users" + rename_table "category_featured_threads", "category_featured_topics" - rename_column 'categories', 'forum_thread_id', 'topic_id' - rename_column 'categories', 'top1_forum_thread_id', 'top1_topic_id' - rename_column 'categories', 'top2_forum_thread_id', 'top2_topic_id' - rename_column 'categories', 'forum_thread_count', 'topic_count' - rename_column 'categories', 'threads_year', 'topics_year' - rename_column 'categories', 'threads_month', 'topics_month' - rename_column 'categories', 'threads_week', 'topics_week' + rename_column "categories", "forum_thread_id", "topic_id" + rename_column "categories", "top1_forum_thread_id", "top1_topic_id" + rename_column "categories", "top2_forum_thread_id", "top2_topic_id" + rename_column "categories", "forum_thread_count", "topic_count" + rename_column "categories", "threads_year", "topics_year" + rename_column "categories", "threads_month", "topics_month" + rename_column "categories", "threads_week", "topics_week" - rename_column 'category_featured_topics', 'forum_thread_id', 'topic_id' + rename_column "category_featured_topics", "forum_thread_id", "topic_id" - rename_column 'topic_link_clicks', 'forum_thread_link_id', 'topic_link_id' + rename_column "topic_link_clicks", "forum_thread_link_id", "topic_link_id" - rename_column 'topic_links', 'forum_thread_id', 'topic_id' - rename_column 'topic_links', 'link_forum_thread_id', 'link_topic_id' + rename_column "topic_links", "forum_thread_id", "topic_id" + rename_column "topic_links", "link_forum_thread_id", "link_topic_id" - rename_column 'topic_users', 'forum_thread_id', 'topic_id' + rename_column "topic_users", "forum_thread_id", "topic_id" - rename_column 'incoming_links', 'forum_thread_id', 'topic_id' + rename_column "incoming_links", "forum_thread_id", "topic_id" - rename_column 'notifications', 'forum_thread_id', 'topic_id' + rename_column "notifications", "forum_thread_id", "topic_id" - rename_column 'post_timings', 'forum_thread_id', 'topic_id' + rename_column "post_timings", "forum_thread_id", "topic_id" - rename_column 'posts', 'forum_thread_id', 'topic_id' + rename_column "posts", "forum_thread_id", "topic_id" - rename_column 'user_actions', 'target_forum_thread_id', 'target_topic_id' + rename_column "user_actions", "target_forum_thread_id", "target_topic_id" - rename_column 'uploads', 'forum_thread_id', 'topic_id' + rename_column "uploads", "forum_thread_id", "topic_id" end end diff --git a/db/migrate/20121018133039_create_topic_allowed_users.rb b/db/migrate/20121018133039_create_topic_allowed_users.rb index e926ae8fbc..f6a996c4bd 100644 --- a/db/migrate/20121018133039_create_topic_allowed_users.rb +++ b/db/migrate/20121018133039_create_topic_allowed_users.rb @@ -8,7 +8,7 @@ class CreateTopicAllowedUsers < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :topic_allowed_users, [:topic_id, :user_id], unique: true - add_index :topic_allowed_users, [:user_id, :topic_id], unique: true + add_index :topic_allowed_users, %i[topic_id user_id], unique: true + add_index :topic_allowed_users, %i[user_id topic_id], unique: true end end diff --git a/db/migrate/20121106015500_drop_avatar_url_from_users.rb b/db/migrate/20121106015500_drop_avatar_url_from_users.rb index 1f27171727..c8664801cd 100644 --- a/db/migrate/20121106015500_drop_avatar_url_from_users.rb +++ b/db/migrate/20121106015500_drop_avatar_url_from_users.rb @@ -12,6 +12,6 @@ class DropAvatarUrlFromUsers < ActiveRecord::Migration[4.2] end def down - add_column :users, :avatar_url, :string, null: false, default: '' + add_column :users, :avatar_url, :string, null: false, default: "" end end diff --git a/db/migrate/20121121202035_create_invites.rb b/db/migrate/20121121202035_create_invites.rb index 46096435ab..3c36b8540e 100644 --- a/db/migrate/20121121202035_create_invites.rb +++ b/db/migrate/20121121202035_create_invites.rb @@ -12,6 +12,6 @@ class CreateInvites < ActiveRecord::Migration[4.2] end add_index :invites, :invite_key, unique: true - add_index :invites, [:email, :invited_by_id], unique: true + add_index :invites, %i[email invited_by_id], unique: true end end diff --git a/db/migrate/20121121205215_create_topic_invites.rb b/db/migrate/20121121205215_create_topic_invites.rb index 5ff307a5af..f0156b3435 100644 --- a/db/migrate/20121121205215_create_topic_invites.rb +++ b/db/migrate/20121121205215_create_topic_invites.rb @@ -8,7 +8,7 @@ class CreateTopicInvites < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :topic_invites, [:topic_id, :invite_id], unique: true + add_index :topic_invites, %i[topic_id invite_id], unique: true add_index :topic_invites, :invite_id end end diff --git a/db/migrate/20121123054127_make_post_number_distinct.rb b/db/migrate/20121123054127_make_post_number_distinct.rb index 0270daec47..c8dfba51dd 100644 --- a/db/migrate/20121123054127_make_post_number_distinct.rb +++ b/db/migrate/20121123054127_make_post_number_distinct.rb @@ -2,8 +2,8 @@ class MakePostNumberDistinct < ActiveRecord::Migration[4.2] def up - - DB.exec('update posts p + DB.exec( + "update posts p set post_number = calc from ( @@ -20,7 +20,8 @@ from ) ) as X -where calc <> p.post_number and X.id = p.id') +where calc <> p.post_number and X.id = p.id", + ) end def down diff --git a/db/migrate/20121123063630_create_user_visits.rb b/db/migrate/20121123063630_create_user_visits.rb index 994ea22d52..85f9eaca8b 100644 --- a/db/migrate/20121123063630_create_user_visits.rb +++ b/db/migrate/20121123063630_create_user_visits.rb @@ -7,6 +7,6 @@ class CreateUserVisits < ActiveRecord::Migration[4.2] t.date :visited_at, null: false end - add_index :user_visits, [:user_id, :visited_at], unique: true + add_index :user_visits, %i[user_id visited_at], unique: true end end diff --git a/db/migrate/20121130010400_create_drafts.rb b/db/migrate/20121130010400_create_drafts.rb index 6d5bfe1723..5ae257ff72 100644 --- a/db/migrate/20121130010400_create_drafts.rb +++ b/db/migrate/20121130010400_create_drafts.rb @@ -8,6 +8,6 @@ class CreateDrafts < ActiveRecord::Migration[4.2] t.text :data, null: false t.timestamps null: false end - add_index :drafts, [:user_id, :draft_key] + add_index :drafts, %i[user_id draft_key] end end diff --git a/db/migrate/20121204183855_fix_link_post_id.rb b/db/migrate/20121204183855_fix_link_post_id.rb index 2fe158b607..bc99f99baa 100644 --- a/db/migrate/20121204183855_fix_link_post_id.rb +++ b/db/migrate/20121204183855_fix_link_post_id.rb @@ -4,22 +4,21 @@ class FixLinkPostId < ActiveRecord::Migration[4.2] def up to_remove = [] - TopicLink.where('internal = TRUE AND link_post_id IS NULL').each do |tl| - - begin - parsed = URI.parse(tl.url) - route = Rails.application.routes.recognize_path(parsed.path) - if route[:topic_id].present? - post = Post.find_by(topic_id: route[:topic_id], post_number: (route[:post_number] || 1)) - tl.update_column(:link_post_id, post.id) if post.present? + TopicLink + .where("internal = TRUE AND link_post_id IS NULL") + .each do |tl| + begin + parsed = URI.parse(tl.url) + route = Rails.application.routes.recognize_path(parsed.path) + if route[:topic_id].present? + post = Post.find_by(topic_id: route[:topic_id], post_number: (route[:post_number] || 1)) + tl.update_column(:link_post_id, post.id) if post.present? + end + rescue ActionController::RoutingError + to_remove << tl.id end - - rescue ActionController::RoutingError - to_remove << tl.id end - end - TopicLink.where("id in (?)", to_remove).delete_all end diff --git a/db/migrate/20121216230719_add_override_default_style_to_site_customization.rb b/db/migrate/20121216230719_add_override_default_style_to_site_customization.rb index 4bab4d6e00..2dd399adb0 100644 --- a/db/migrate/20121216230719_add_override_default_style_to_site_customization.rb +++ b/db/migrate/20121216230719_add_override_default_style_to_site_customization.rb @@ -3,6 +3,6 @@ class AddOverrideDefaultStyleToSiteCustomization < ActiveRecord::Migration[4.2] def change add_column :site_customizations, :override_default_style, :boolean, default: false, null: false - add_column :site_customizations, :stylesheet_baked, :text, default: '', null: false + add_column :site_customizations, :stylesheet_baked, :text, default: "", null: false end end diff --git a/db/migrate/20121224095139_create_draft_sequence.rb b/db/migrate/20121224095139_create_draft_sequence.rb index fbd32d4a84..ae996eb710 100644 --- a/db/migrate/20121224095139_create_draft_sequence.rb +++ b/db/migrate/20121224095139_create_draft_sequence.rb @@ -7,6 +7,6 @@ class CreateDraftSequence < ActiveRecord::Migration[4.2] t.string :draft_key, null: false t.integer :sequence, null: false end - add_index :draft_sequences, [:user_id, :draft_key], unique: true + add_index :draft_sequences, %i[user_id draft_key], unique: true end end diff --git a/db/migrate/20130115043603_oops_unwatch_a_boat_of_watched_stuff.rb b/db/migrate/20130115043603_oops_unwatch_a_boat_of_watched_stuff.rb index d793092f44..43af27d0c4 100644 --- a/db/migrate/20130115043603_oops_unwatch_a_boat_of_watched_stuff.rb +++ b/db/migrate/20130115043603_oops_unwatch_a_boat_of_watched_stuff.rb @@ -2,6 +2,6 @@ class OopsUnwatchABoatOfWatchedStuff < ActiveRecord::Migration[4.2] def change - execute 'update topic_users set notification_level = 1 where notifications_reason_id is null and notification_level = 2' + execute "update topic_users set notification_level = 1 where notifications_reason_id is null and notification_level = 2" end end diff --git a/db/migrate/20130120222728_fix_search.rb b/db/migrate/20130120222728_fix_search.rb index 48e775fb7e..8e449bbd85 100644 --- a/db/migrate/20130120222728_fix_search.rb +++ b/db/migrate/20130120222728_fix_search.rb @@ -2,16 +2,16 @@ class FixSearch < ActiveRecord::Migration[4.2] def up - execute 'drop index idx_search_thread' - execute 'drop index idx_search_user' + execute "drop index idx_search_thread" + execute "drop index idx_search_user" - execute 'create table posts_search (id integer not null primary key, search_data tsvector)' - execute 'create table users_search (id integer not null primary key, search_data tsvector)' - execute 'create table categories_search (id integer not null primary key, search_data tsvector)' + execute "create table posts_search (id integer not null primary key, search_data tsvector)" + execute "create table users_search (id integer not null primary key, search_data tsvector)" + execute "create table categories_search (id integer not null primary key, search_data tsvector)" - execute 'create index idx_search_post on posts_search using gin(search_data) ' - execute 'create index idx_search_user on users_search using gin(search_data) ' - execute 'create index idx_search_category on categories_search using gin(search_data) ' + execute "create index idx_search_post on posts_search using gin(search_data) " + execute "create index idx_search_user on users_search using gin(search_data) " + execute "create index idx_search_category on categories_search using gin(search_data) " end def down diff --git a/db/migrate/20130121231352_add_tracking_to_topic_users.rb b/db/migrate/20130121231352_add_tracking_to_topic_users.rb index 78173e3b6b..5ebe50af33 100644 --- a/db/migrate/20130121231352_add_tracking_to_topic_users.rb +++ b/db/migrate/20130121231352_add_tracking_to_topic_users.rb @@ -2,9 +2,9 @@ class AddTrackingToTopicUsers < ActiveRecord::Migration[4.2] def up - execute 'update topic_users set notification_level = 3 where notification_level = 2' + execute "update topic_users set notification_level = 3 where notification_level = 2" end def down - execute 'update topic_users set notification_level = 2 where notification_level = 3' + execute "update topic_users set notification_level = 2 where notification_level = 3" end end diff --git a/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb b/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb index 10bc0fe209..fc6eea34b6 100644 --- a/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb +++ b/db/migrate/20130122232825_add_auto_track_after_seconds_and_banning_and_dob_to_user.rb @@ -12,11 +12,11 @@ class AddAutoTrackAfterSecondsAndBanningAndDobToUser < ActiveRecord::Migration[4 add_column :topic_users, :total_msecs_viewed, :integer, null: false, default: 0 - execute 'update topic_users set total_msecs_viewed = + execute "update topic_users set total_msecs_viewed = ( select coalesce(sum(msecs) ,0) from post_timings t where topic_users.topic_id = t.topic_id and topic_users.user_id = t.user_id - )' + )" end end diff --git a/db/migrate/20130123070909_auto_track_all_topics_replied_to.rb b/db/migrate/20130123070909_auto_track_all_topics_replied_to.rb index 35ff0e57e1..d0ec9ebd5c 100644 --- a/db/migrate/20130123070909_auto_track_all_topics_replied_to.rb +++ b/db/migrate/20130123070909_auto_track_all_topics_replied_to.rb @@ -2,14 +2,14 @@ class AutoTrackAllTopicsRepliedTo < ActiveRecord::Migration[4.2] def up - execute 'update topic_users set notification_level = 2, notifications_reason_id = 4 + execute "update topic_users set notification_level = 2, notifications_reason_id = 4 from posts p where notification_level = 1 and notifications_reason_id is null and p.topic_id = topic_users.topic_id and p.user_id = topic_users.user_id - ' + " end def down diff --git a/db/migrate/20130125031122_correct_index_on_post_action.rb b/db/migrate/20130125031122_correct_index_on_post_action.rb index 3f947d4b42..3237a635e3 100644 --- a/db/migrate/20130125031122_correct_index_on_post_action.rb +++ b/db/migrate/20130125031122_correct_index_on_post_action.rb @@ -3,6 +3,9 @@ class CorrectIndexOnPostAction < ActiveRecord::Migration[4.2] def change remove_index "post_actions", name: "idx_unique_actions" - add_index "post_actions", ["user_id", "post_action_type_id", "post_id", "deleted_at"], name: "idx_unique_actions", unique: true + add_index "post_actions", + %w[user_id post_action_type_id post_id deleted_at], + name: "idx_unique_actions", + unique: true end end diff --git a/db/migrate/20130127213646_remove_trust_levels.rb b/db/migrate/20130127213646_remove_trust_levels.rb index 2154d2ac0a..0e7fea2b71 100644 --- a/db/migrate/20130127213646_remove_trust_levels.rb +++ b/db/migrate/20130127213646_remove_trust_levels.rb @@ -11,5 +11,4 @@ class RemoveTrustLevels < ActiveRecord::Migration[4.2] remove_column :users, :moderator add_column :users, :flag_level, :integer, null: false, default: 0 end - end diff --git a/db/migrate/20130130154611_remove_index_from_views.rb b/db/migrate/20130130154611_remove_index_from_views.rb index 4af6cede79..bfa93c0575 100644 --- a/db/migrate/20130130154611_remove_index_from_views.rb +++ b/db/migrate/20130130154611_remove_index_from_views.rb @@ -7,7 +7,7 @@ class RemoveIndexFromViews < ActiveRecord::Migration[4.2] end def down - add_index "views", ["parent_id", "parent_type", "ip", "viewed_at"], name: "unique_views", unique: true + add_index "views", %w[parent_id parent_type ip viewed_at], name: "unique_views", unique: true change_column :views, :viewed_at, :timestamp end end diff --git a/db/migrate/20130204000159_add_ip_address_to_users.rb b/db/migrate/20130204000159_add_ip_address_to_users.rb index 6ac3e4b929..9fab5fefa7 100644 --- a/db/migrate/20130204000159_add_ip_address_to_users.rb +++ b/db/migrate/20130204000159_add_ip_address_to_users.rb @@ -2,9 +2,9 @@ class AddIpAddressToUsers < ActiveRecord::Migration[4.2] def up - execute 'alter table users add column ip_address inet' + execute "alter table users add column ip_address inet" end def down - execute 'alter table users drop column ip_address' + execute "alter table users drop column ip_address" end end diff --git a/db/migrate/20130213021450_remove_topic_response_actions.rb b/db/migrate/20130213021450_remove_topic_response_actions.rb index 2709734c8e..cf425a2abd 100644 --- a/db/migrate/20130213021450_remove_topic_response_actions.rb +++ b/db/migrate/20130213021450_remove_topic_response_actions.rb @@ -8,7 +8,7 @@ class RemoveTopicResponseActions < ActiveRecord::Migration[4.2] # # There is an open question about we should keep stuff in the user stream on the user page, even if a topic is unwatched # Eg: I am not watching a topic I created, when somebody responds to the topic should I be notified on the user page? - execute 'delete from user_actions where action_type = 8' + execute "delete from user_actions where action_type = 8" end def down diff --git a/db/migrate/20130221215017_add_description_to_categories.rb b/db/migrate/20130221215017_add_description_to_categories.rb index f7b3071e6c..3f6a2a2fee 100644 --- a/db/migrate/20130221215017_add_description_to_categories.rb +++ b/db/migrate/20130221215017_add_description_to_categories.rb @@ -13,23 +13,22 @@ class AddDescriptionToCategories < ActiveRecord::Migration[4.2] # some ancient installs may have bad category descriptions # attempt to fix if !DB.query_single("SELECT 1 FROM categories limit 1").empty? - # Reaching into post revisor is not ideal here, but this code # should almost never run, so bypass it Discourse.reset_active_record_cache - Category.order('id').each do |c| - post = c.topic.ordered_posts.first - PostRevisor.new(post).update_category_description - end + Category + .order("id") + .each do |c| + post = c.topic.ordered_posts.first + PostRevisor.new(post).update_category_description + end Discourse.reset_active_record_cache end - end def down remove_column :categories, :description end - end diff --git a/db/migrate/20130226015336_add_github_user_info.rb b/db/migrate/20130226015336_add_github_user_info.rb index edcfacea25..3f0d956e6a 100644 --- a/db/migrate/20130226015336_add_github_user_info.rb +++ b/db/migrate/20130226015336_add_github_user_info.rb @@ -2,7 +2,7 @@ class AddGithubUserInfo < ActiveRecord::Migration[4.2] def change - create_table :github_user_infos do |t| + create_table :github_user_infos do |t| t.integer :user_id, null: false t.string :screen_name, null: false t.integer :github_user_id, null: false diff --git a/db/migrate/20130314093434_add_foreground_color_to_categories.rb b/db/migrate/20130314093434_add_foreground_color_to_categories.rb index 1784d7785d..72508e2411 100644 --- a/db/migrate/20130314093434_add_foreground_color_to_categories.rb +++ b/db/migrate/20130314093434_add_foreground_color_to_categories.rb @@ -2,6 +2,6 @@ class AddForegroundColorToCategories < ActiveRecord::Migration[4.2] def change - add_column :categories, :text_color, :string, limit: 6, null: false, default: 'FFFFFF' + add_column :categories, :text_color, :string, limit: 6, null: false, default: "FFFFFF" end end diff --git a/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb b/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb index 62aec7d531..0eab860322 100644 --- a/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb +++ b/db/migrate/20130319122248_add_reply_to_user_id_to_post.rb @@ -4,12 +4,12 @@ class AddReplyToUserIdToPost < ActiveRecord::Migration[4.2] def up # caching this column makes the topic page WAY faster add_column :posts, :reply_to_user_id, :integer - execute 'UPDATE posts p SET reply_to_user_id = ( + execute "UPDATE posts p SET reply_to_user_id = ( SELECT u.id from users u JOIN posts p2 ON p2.user_id = u.id AND p2.post_number = p.reply_to_post_number AND p2.topic_id = p.topic_id - )' + )" end def down diff --git a/db/migrate/20130322183614_add_percent_rank_to_posts.rb b/db/migrate/20130322183614_add_percent_rank_to_posts.rb index 93e861cee5..cfa2a8c5f5 100644 --- a/db/migrate/20130322183614_add_percent_rank_to_posts.rb +++ b/db/migrate/20130322183614_add_percent_rank_to_posts.rb @@ -9,6 +9,5 @@ class AddPercentRankToPosts < ActiveRecord::Migration[4.2] OVER (PARTITION BY topic_id ORDER BY SCORE DESC) FROM posts) AS x WHERE x.id = posts.id" - end end diff --git a/db/migrate/20130328162943_create_hot_topics.rb b/db/migrate/20130328162943_create_hot_topics.rb index e574da1aea..2bdd873dba 100644 --- a/db/migrate/20130328162943_create_hot_topics.rb +++ b/db/migrate/20130328162943_create_hot_topics.rb @@ -9,6 +9,6 @@ class CreateHotTopics < ActiveRecord::Migration[4.2] end add_index :hot_topics, :topic_id, unique: true - add_index :hot_topics, :score, order: 'desc' + add_index :hot_topics, :score, order: "desc" end end diff --git a/db/migrate/20130404232558_add_user_extras.rb b/db/migrate/20130404232558_add_user_extras.rb index 8fc2c776af..6e0401ec33 100644 --- a/db/migrate/20130404232558_add_user_extras.rb +++ b/db/migrate/20130404232558_add_user_extras.rb @@ -2,7 +2,6 @@ class AddUserExtras < ActiveRecord::Migration[4.2] def up - # NOTE: our user table is getting bloated, we probably want to split it for performance # put lesser used columns into a user_extras table and frequently used ones here. add_column :users, :likes_given, :integer, null: false, default: 0 @@ -27,7 +26,7 @@ FROM ( WHERE X.user_id = u.id SQL - execute < 0") + result = + execute( + "SELECT count(*) FROM site_settings where name='access_password' and char_length(value) > 0", + ) if result[0] && result[0]["count"].to_i > (0) execute "DELETE FROM site_settings where name='access_password'" SiteSetting.login_required = true diff --git a/db/migrate/20130731163035_add_report_index_to_incoming_links.rb b/db/migrate/20130731163035_add_report_index_to_incoming_links.rb index 6f6e5711ef..adfe13d8dc 100644 --- a/db/migrate/20130731163035_add_report_index_to_incoming_links.rb +++ b/db/migrate/20130731163035_add_report_index_to_incoming_links.rb @@ -2,7 +2,7 @@ class AddReportIndexToIncomingLinks < ActiveRecord::Migration[4.2] def change - add_index :incoming_links, [:created_at, :user_id] - add_index :incoming_links, [:created_at, :domain] + add_index :incoming_links, %i[created_at user_id] + add_index :incoming_links, %i[created_at domain] end end diff --git a/db/migrate/20130809160751_fix_seen_notification_ids.rb b/db/migrate/20130809160751_fix_seen_notification_ids.rb index b002ec486f..feda358e66 100644 --- a/db/migrate/20130809160751_fix_seen_notification_ids.rb +++ b/db/migrate/20130809160751_fix_seen_notification_ids.rb @@ -2,7 +2,6 @@ class FixSeenNotificationIds < ActiveRecord::Migration[4.2] def up - # There was an error where `seen_notification_id` was being updated incorrectly. # This tries to fix some of the bad data. execute "UPDATE users SET diff --git a/db/migrate/20130809204732_add_filter_indexes_to_staff_action_logs.rb b/db/migrate/20130809204732_add_filter_indexes_to_staff_action_logs.rb index 2a559646bd..479a4305b4 100644 --- a/db/migrate/20130809204732_add_filter_indexes_to_staff_action_logs.rb +++ b/db/migrate/20130809204732_add_filter_indexes_to_staff_action_logs.rb @@ -2,8 +2,8 @@ class AddFilterIndexesToStaffActionLogs < ActiveRecord::Migration[4.2] def change - add_index :staff_action_logs, [:action, :id] - add_index :staff_action_logs, [:staff_user_id, :id] - add_index :staff_action_logs, [:target_user_id, :id] + add_index :staff_action_logs, %i[action id] + add_index :staff_action_logs, %i[staff_user_id id] + add_index :staff_action_logs, %i[target_user_id id] end end diff --git a/db/migrate/20130816024250_create_oauth2_user_infos.rb b/db/migrate/20130816024250_create_oauth2_user_infos.rb index daea47c31f..efcaea23ea 100644 --- a/db/migrate/20130816024250_create_oauth2_user_infos.rb +++ b/db/migrate/20130816024250_create_oauth2_user_infos.rb @@ -11,6 +11,6 @@ class CreateOauth2UserInfos < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :oauth2_user_infos, [:uid, :provider], unique: true + add_index :oauth2_user_infos, %i[uid provider], unique: true end end diff --git a/db/migrate/20130820174431_add_subject_index_to_staff_action_logs.rb b/db/migrate/20130820174431_add_subject_index_to_staff_action_logs.rb index b9aae2d469..9111b296c6 100644 --- a/db/migrate/20130820174431_add_subject_index_to_staff_action_logs.rb +++ b/db/migrate/20130820174431_add_subject_index_to_staff_action_logs.rb @@ -2,6 +2,6 @@ class AddSubjectIndexToStaffActionLogs < ActiveRecord::Migration[4.2] def change - add_index :staff_action_logs, [:subject, :id] + add_index :staff_action_logs, %i[subject id] end end diff --git a/db/migrate/20130822213513_add_ip_address_to_screening_tables.rb b/db/migrate/20130822213513_add_ip_address_to_screening_tables.rb index f226f18b2f..1ac091bacd 100644 --- a/db/migrate/20130822213513_add_ip_address_to_screening_tables.rb +++ b/db/migrate/20130822213513_add_ip_address_to_screening_tables.rb @@ -3,6 +3,6 @@ class AddIpAddressToScreeningTables < ActiveRecord::Migration[4.2] def change add_column :screened_emails, :ip_address, :inet - add_column :screened_urls, :ip_address, :inet + add_column :screened_urls, :ip_address, :inet end end diff --git a/db/migrate/20130823201420_drop_defaults_on_email_digest_columns_of_users.rb b/db/migrate/20130823201420_drop_defaults_on_email_digest_columns_of_users.rb index f1f3560dd4..1abbc0d126 100644 --- a/db/migrate/20130823201420_drop_defaults_on_email_digest_columns_of_users.rb +++ b/db/migrate/20130823201420_drop_defaults_on_email_digest_columns_of_users.rb @@ -2,13 +2,13 @@ class DropDefaultsOnEmailDigestColumnsOfUsers < ActiveRecord::Migration[4.2] def up - change_column_default :users, :email_digests, nil - change_column :users, :digest_after_days, :integer, default: nil, null: true + change_column_default :users, :email_digests, nil + change_column :users, :digest_after_days, :integer, default: nil, null: true end def down - change_column_default :users, :email_digests, true + change_column_default :users, :email_digests, true change_column_default :users, :digest_after_days, 7 - change_column :users, :digest_after_days, :integer, default: 7, null: false + change_column :users, :digest_after_days, :integer, default: 7, null: false end end diff --git a/db/migrate/20130826011521_create_plugin_store_rows.rb b/db/migrate/20130826011521_create_plugin_store_rows.rb index f74a9037d9..c8190d750f 100644 --- a/db/migrate/20130826011521_create_plugin_store_rows.rb +++ b/db/migrate/20130826011521_create_plugin_store_rows.rb @@ -12,6 +12,6 @@ class CreatePluginStoreRows < ActiveRecord::Migration[4.2] table.text :value end - add_index :plugin_store_rows, [:plugin_name, :key], unique: true + add_index :plugin_store_rows, %i[plugin_name key], unique: true end end diff --git a/db/migrate/20130903154323_allow_null_user_id_on_posts.rb b/db/migrate/20130903154323_allow_null_user_id_on_posts.rb index 0798b0debf..27d474618b 100644 --- a/db/migrate/20130903154323_allow_null_user_id_on_posts.rb +++ b/db/migrate/20130903154323_allow_null_user_id_on_posts.rb @@ -8,7 +8,7 @@ class AllowNullUserIdOnPosts < ActiveRecord::Migration[4.2] end def down - add_column :posts, :nuked_user, :boolean, default: false + add_column :posts, :nuked_user, :boolean, default: false change_column :posts, :user_id, :integer, null: false end end diff --git a/db/migrate/20130906171631_add_index_to_uploads.rb b/db/migrate/20130906171631_add_index_to_uploads.rb index ad15cd3bc9..293f81b620 100644 --- a/db/migrate/20130906171631_add_index_to_uploads.rb +++ b/db/migrate/20130906171631_add_index_to_uploads.rb @@ -2,6 +2,6 @@ class AddIndexToUploads < ActiveRecord::Migration[4.2] def change - add_index :uploads, [:id, :url] + add_index :uploads, %i[id url] end end diff --git a/db/migrate/20130910040235_index_topics_for_front_page.rb b/db/migrate/20130910040235_index_topics_for_front_page.rb index 746e7946fa..1717580d6e 100644 --- a/db/migrate/20130910040235_index_topics_for_front_page.rb +++ b/db/migrate/20130910040235_index_topics_for_front_page.rb @@ -2,9 +2,8 @@ class IndexTopicsForFrontPage < ActiveRecord::Migration[4.2] def change - add_index :topics, [:deleted_at, :visible, :archetype, :id] + add_index :topics, %i[deleted_at visible archetype id] # covering index for join - add_index :topics, [:id, :deleted_at] + add_index :topics, %i[id deleted_at] end - end diff --git a/db/migrate/20130910220317_rename_staff_action_logs_to_user_history.rb b/db/migrate/20130910220317_rename_staff_action_logs_to_user_history.rb index acd5f730c7..34894ba326 100644 --- a/db/migrate/20130910220317_rename_staff_action_logs_to_user_history.rb +++ b/db/migrate/20130910220317_rename_staff_action_logs_to_user_history.rb @@ -2,16 +2,16 @@ class RenameStaffActionLogsToUserHistory < ActiveRecord::Migration[4.2] def up - remove_index :staff_action_logs, [:staff_user_id, :id] + remove_index :staff_action_logs, %i[staff_user_id id] rename_table :staff_action_logs, :user_histories rename_column :user_histories, :staff_user_id, :acting_user_id - add_index :user_histories, [:acting_user_id, :action, :id] + add_index :user_histories, %i[acting_user_id action id] end def down - remove_index :user_histories, [:acting_user_id, :action, :id] + remove_index :user_histories, %i[acting_user_id action id] rename_table :user_histories, :staff_action_logs rename_column :staff_action_logs, :acting_user_id, :staff_user_id - add_index :staff_action_logs, [:staff_user_id, :id] + add_index :staff_action_logs, %i[staff_user_id id] end end diff --git a/db/migrate/20130911182437_create_user_stats.rb b/db/migrate/20130911182437_create_user_stats.rb index 9b0794e8aa..9a89a9254d 100644 --- a/db/migrate/20130911182437_create_user_stats.rb +++ b/db/migrate/20130911182437_create_user_stats.rb @@ -13,5 +13,4 @@ class CreateUserStats < ActiveRecord::Migration[4.2] def down drop_table :user_stats end - end diff --git a/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb b/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb index 4c0d553592..26401ad9c0 100644 --- a/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb +++ b/db/migrate/20131002070347_add_user_id_parent_type_index_on_views.rb @@ -2,6 +2,6 @@ class AddUserIdParentTypeIndexOnViews < ActiveRecord::Migration[4.2] def change - add_index :views, [:user_id, :parent_type, :parent_id] + add_index :views, %i[user_id parent_type parent_id] end end diff --git a/db/migrate/20131003061137_move_columns_to_user_stats.rb b/db/migrate/20131003061137_move_columns_to_user_stats.rb index 2faa132426..202c4d2fb1 100644 --- a/db/migrate/20131003061137_move_columns_to_user_stats.rb +++ b/db/migrate/20131003061137_move_columns_to_user_stats.rb @@ -10,7 +10,7 @@ class MoveColumnsToUserStats < ActiveRecord::Migration[4.2] add_column :user_stats, :likes_received, :integer, default: 0, null: false add_column :user_stats, :topic_reply_count, :integer, default: 0, null: false - execute 'UPDATE user_stats s + execute "UPDATE user_stats s SET topics_entered = u.topics_entered, time_read = u.time_read, days_visited = u.days_visited, @@ -19,7 +19,7 @@ class MoveColumnsToUserStats < ActiveRecord::Migration[4.2] likes_received = u.likes_received, topic_reply_count = u.topic_reply_count FROM users u WHERE u.id = s.user_id - ' + " remove_column :users, :topics_entered remove_column :users, :time_read @@ -39,7 +39,7 @@ class MoveColumnsToUserStats < ActiveRecord::Migration[4.2] add_column :users, :likes_received, :integer add_column :users, :topic_reply_count, :integer - execute 'UPDATE users s + execute "UPDATE users s SET topics_entered = u.topics_entered, time_read = u.time_read, days_visited = u.days_visited, @@ -48,7 +48,7 @@ class MoveColumnsToUserStats < ActiveRecord::Migration[4.2] likes_received = u.likes_received, topic_reply_count = u.topic_reply_count FROM user_stats u WHERE s.id = u.user_id - ' + " remove_column :user_stats, :topics_entered remove_column :user_stats, :time_read diff --git a/db/migrate/20131014203951_backfill_post_upload_reverse_index.rb b/db/migrate/20131014203951_backfill_post_upload_reverse_index.rb index 0c5f4bb3e6..1d3b1d4702 100644 --- a/db/migrate/20131014203951_backfill_post_upload_reverse_index.rb +++ b/db/migrate/20131014203951_backfill_post_upload_reverse_index.rb @@ -1,19 +1,20 @@ # frozen_string_literal: true class BackfillPostUploadReverseIndex < ActiveRecord::Migration[4.2] - def up # clean the reverse index execute "TRUNCATE TABLE post_uploads" # fill the reverse index up - Post.select([:id, :cooked]).find_each do |post| - doc = Nokogiri::HTML5::fragment(post.cooked) - # images - doc.search("img").each { |img| add_to_reverse_index(img['src'], post.id) } - # thumbnails and/or attachments - doc.search("a").each { |a| add_to_reverse_index(a['href'], post.id) } - end + Post + .select(%i[id cooked]) + .find_each do |post| + doc = Nokogiri::HTML5.fragment(post.cooked) + # images + doc.search("img").each { |img| add_to_reverse_index(img["src"], post.id) } + # thumbnails and/or attachments + doc.search("a").each { |a| add_to_reverse_index(a["href"], post.id) } + end end def add_to_reverse_index(url, post_id) @@ -72,5 +73,4 @@ class BackfillPostUploadReverseIndex < ActiveRecord::Migration[4.2] def is_s3_avatar?(url) url.starts_with?(s3_avatar_base_url) end - end diff --git a/db/migrate/20131015131652_create_post_details.rb b/db/migrate/20131015131652_create_post_details.rb index 4371c0efb7..e17c30d491 100644 --- a/db/migrate/20131015131652_create_post_details.rb +++ b/db/migrate/20131015131652_create_post_details.rb @@ -4,13 +4,13 @@ class CreatePostDetails < ActiveRecord::Migration[4.2] def change create_table :post_details do |t| t.belongs_to :post - t.string :key - t.string :value, size: 512 - t.text :extra + t.string :key + t.string :value, size: 512 + t.text :extra t.timestamps null: false end - add_index :post_details, [:post_id, :key], unique: true + add_index :post_details, %i[post_id key], unique: true end end diff --git a/db/migrate/20131022045114_add_uncategorized_category.rb b/db/migrate/20131022045114_add_uncategorized_category.rb index cb362fb0c3..170ccf264e 100644 --- a/db/migrate/20131022045114_add_uncategorized_category.rb +++ b/db/migrate/20131022045114_add_uncategorized_category.rb @@ -2,14 +2,12 @@ class AddUncategorizedCategory < ActiveRecord::Migration[4.2] def up - result = execute "SELECT 1 FROM categories WHERE lower(name) = 'uncategorized'" - name = +'Uncategorized' - if result.count > 0 - name << SecureRandom.hex - end + name = +"Uncategorized" + name << SecureRandom.hex if result.count > 0 - result = execute "INSERT INTO categories + result = + execute "INSERT INTO categories (name,color,slug,description,text_color, user_id, created_at, updated_at, position) VALUES ('#{name}', '0088CC', 'uncategorized', '', 'FFFFFF', -1, now(), now(), 0 ) RETURNING id @@ -24,7 +22,6 @@ class AddUncategorizedCategory < ActiveRecord::Migration[4.2] execute "UPDATE topics SET category_id = #{category_id} WHERE archetype = 'regular' AND category_id IS NULL" execute "ALTER table topics ADD CONSTRAINT has_category_id CHECK (category_id IS NOT NULL OR archetype <> 'regular')" - end def down diff --git a/db/migrate/20131022151218_create_api_keys.rb b/db/migrate/20131022151218_create_api_keys.rb index 1eed1250ea..78f0d934e0 100644 --- a/db/migrate/20131022151218_create_api_keys.rb +++ b/db/migrate/20131022151218_create_api_keys.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class CreateApiKeys < ActiveRecord::Migration[4.2] - def up create_table :api_keys do |t| t.string :key, limit: 64, null: false @@ -20,5 +19,4 @@ class CreateApiKeys < ActiveRecord::Migration[4.2] def down raise ActiveRecord::IrreversibleMigration.new end - end diff --git a/db/migrate/20131107154900_rename_banned_to_suspended.rb b/db/migrate/20131107154900_rename_banned_to_suspended.rb index 09d0a9f7e4..374de38fa4 100644 --- a/db/migrate/20131107154900_rename_banned_to_suspended.rb +++ b/db/migrate/20131107154900_rename_banned_to_suspended.rb @@ -2,7 +2,7 @@ class RenameBannedToSuspended < ActiveRecord::Migration[4.2] def change - rename_column :users, :banned_at, :suspended_at + rename_column :users, :banned_at, :suspended_at rename_column :users, :banned_till, :suspended_till end end diff --git a/db/migrate/20131114185225_add_participant_count_to_topics.rb b/db/migrate/20131114185225_add_participant_count_to_topics.rb index 214d83d628..84a4e0b1f4 100644 --- a/db/migrate/20131114185225_add_participant_count_to_topics.rb +++ b/db/migrate/20131114185225_add_participant_count_to_topics.rb @@ -11,5 +11,4 @@ class AddParticipantCountToTopics < ActiveRecord::Migration[4.2] def down remove_column :topics, :participant_count end - end diff --git a/db/migrate/20131120055018_move_emoji_to_new_location.rb b/db/migrate/20131120055018_move_emoji_to_new_location.rb index b564a7326d..1d62f47eba 100644 --- a/db/migrate/20131120055018_move_emoji_to_new_location.rb +++ b/db/migrate/20131120055018_move_emoji_to_new_location.rb @@ -2,10 +2,14 @@ class MoveEmojiToNewLocation < ActiveRecord::Migration[4.2] def up - execute("update posts set cooked = regexp_replace(cooked, '\(]*)assets\/emoji\/', '\\1plugins\/emoji\/images\/' , 'g') where cooked like '%emoji%'") + execute( + "update posts set cooked = regexp_replace(cooked, '\(]*)assets\/emoji\/', '\\1plugins\/emoji\/images\/' , 'g') where cooked like '%emoji%'", + ) end def down - execute("update posts set cooked = regexp_replace(cooked, '\(]*)plugins\/emoji\/images\/', '\\1assets\/emoji\/' , 'g') where cooked like '%emoji%'") + execute( + "update posts set cooked = regexp_replace(cooked, '\(]*)plugins\/emoji\/images\/', '\\1assets\/emoji\/' , 'g') where cooked like '%emoji%'", + ) end end diff --git a/db/migrate/20131209091702_create_post_revisions.rb b/db/migrate/20131209091702_create_post_revisions.rb index 9e97b05bf1..a9decbfe3d 100644 --- a/db/migrate/20131209091702_create_post_revisions.rb +++ b/db/migrate/20131209091702_create_post_revisions.rb @@ -17,7 +17,7 @@ class CreatePostRevisions < ActiveRecord::Migration[4.2] change_table :post_revisions do |t| t.index :post_id - t.index [:post_id, :number] + t.index %i[post_id number] end end diff --git a/db/migrate/20131209091742_create_topic_revisions.rb b/db/migrate/20131209091742_create_topic_revisions.rb index f4e53afc99..a48c568ffc 100644 --- a/db/migrate/20131209091742_create_topic_revisions.rb +++ b/db/migrate/20131209091742_create_topic_revisions.rb @@ -17,7 +17,7 @@ class CreateTopicRevisions < ActiveRecord::Migration[4.2] change_table :topic_revisions do |t| t.index :topic_id - t.index [:topic_id, :number] + t.index %i[topic_id number] end end diff --git a/db/migrate/20131210181901_migrate_word_counts.rb b/db/migrate/20131210181901_migrate_word_counts.rb index aa99889163..861547925c 100644 --- a/db/migrate/20131210181901_migrate_word_counts.rb +++ b/db/migrate/20131210181901_migrate_word_counts.rb @@ -4,32 +4,34 @@ class MigrateWordCounts < ActiveRecord::Migration[4.2] disable_ddl_transaction! def up - post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } + post_ids = + execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r["id"].to_i } while post_ids.length > 0 3.times do begin - execute "UPDATE posts SET word_count = COALESCE(array_length(regexp_split_to_array(raw, ' '),1), 0) WHERE id IN (#{post_ids.join(',')})" + execute "UPDATE posts SET word_count = COALESCE(array_length(regexp_split_to_array(raw, ' '),1), 0) WHERE id IN (#{post_ids.join(",")})" break rescue PG::Error # Deadlock. Try again, up to 3 times. end end - post_ids = execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } + post_ids = + execute("SELECT id FROM posts WHERE word_count IS NULL LIMIT 500").map { |r| r["id"].to_i } end - topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } + topic_ids = + execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r["id"].to_i } while topic_ids.length > 0 3.times do begin - execute "UPDATE topics SET word_count = COALESCE((SELECT SUM(COALESCE(posts.word_count, 0)) FROM posts WHERE posts.topic_id = topics.id), 0) WHERE topics.id IN (#{topic_ids.join(',')})" + execute "UPDATE topics SET word_count = COALESCE((SELECT SUM(COALESCE(posts.word_count, 0)) FROM posts WHERE posts.topic_id = topics.id), 0) WHERE topics.id IN (#{topic_ids.join(",")})" break rescue PG::Error # Deadlock. Try again, up to 3 times. end end - topic_ids = execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r['id'].to_i } + topic_ids = + execute("SELECT id FROM topics WHERE word_count IS NULL LIMIT 500").map { |r| r["id"].to_i } end - end - end diff --git a/db/migrate/20131210234530_rename_version_column.rb b/db/migrate/20131210234530_rename_version_column.rb index c524f13c8e..4349f524ee 100644 --- a/db/migrate/20131210234530_rename_version_column.rb +++ b/db/migrate/20131210234530_rename_version_column.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class RenameVersionColumn < ActiveRecord::Migration[4.2] - def change add_column :posts, :version, :integer, default: 1, null: false execute "UPDATE posts SET version = cached_version" remove_column :posts, :cached_version end - end diff --git a/db/migrate/20131223171005_create_top_topics.rb b/db/migrate/20131223171005_create_top_topics.rb index 298f618b97..ec1b05882a 100644 --- a/db/migrate/20131223171005_create_top_topics.rb +++ b/db/migrate/20131223171005_create_top_topics.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class CreateTopTopics < ActiveRecord::Migration[4.2] - PERIODS = [:yearly, :monthly, :weekly, :daily] - SORT_ORDERS = [:posts, :views, :likes] + PERIODS = %i[yearly monthly weekly daily] + SORT_ORDERS = %i[posts views likes] def change create_table :top_topics, force: true do |t| @@ -13,16 +13,14 @@ class CreateTopTopics < ActiveRecord::Migration[4.2] t.integer "#{period}_#{sort}_count".to_sym, null: false, default: 0 end end - end add_index :top_topics, :topic_id, unique: true PERIODS.each do |period| SORT_ORDERS.each do |sort| - add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: 'desc' + add_index :top_topics, "#{period}_#{sort}_count".to_sym, order: "desc" end end - end end diff --git a/db/migrate/20131227164338_add_scores_to_top_topics.rb b/db/migrate/20131227164338_add_scores_to_top_topics.rb index 040adc1127..addeb688a2 100644 --- a/db/migrate/20131227164338_add_scores_to_top_topics.rb +++ b/db/migrate/20131227164338_add_scores_to_top_topics.rb @@ -2,7 +2,7 @@ class AddScoresToTopTopics < ActiveRecord::Migration[4.2] def change - [:daily, :weekly, :monthly, :yearly].each do |period| + %i[daily weekly monthly yearly].each do |period| add_column :top_topics, "#{period}_score".to_sym, :float end end diff --git a/db/migrate/20140206195001_add_defaults_to_category_posts_and_topics_fields.rb b/db/migrate/20140206195001_add_defaults_to_category_posts_and_topics_fields.rb index c676407c07..c4b506b694 100644 --- a/db/migrate/20140206195001_add_defaults_to_category_posts_and_topics_fields.rb +++ b/db/migrate/20140206195001_add_defaults_to_category_posts_and_topics_fields.rb @@ -2,12 +2,12 @@ class AddDefaultsToCategoryPostsAndTopicsFields < ActiveRecord::Migration[4.2] def change - change_column_default :categories, :posts_week, 0 - change_column_default :categories, :posts_month, 0 - change_column_default :categories, :posts_year, 0 + change_column_default :categories, :posts_week, 0 + change_column_default :categories, :posts_month, 0 + change_column_default :categories, :posts_year, 0 - change_column_default :categories, :topics_week, 0 + change_column_default :categories, :topics_week, 0 change_column_default :categories, :topics_month, 0 - change_column_default :categories, :topics_year, 0 + change_column_default :categories, :topics_year, 0 end end diff --git a/db/migrate/20140211230222_move_cas_settings.rb b/db/migrate/20140211230222_move_cas_settings.rb index 1923408e48..07f038b58d 100644 --- a/db/migrate/20140211230222_move_cas_settings.rb +++ b/db/migrate/20140211230222_move_cas_settings.rb @@ -6,9 +6,9 @@ class MoveCasSettings < ActiveRecord::Migration[4.2] #convert the data over to be used by the plugin. cas_hostname = SiteSetting.find_by(name: "cas_hostname") cas_sso_hostname = SiteSetting.find_by(name: "cas_sso_hostname") - if cas_hostname && ! cas_sso_hostname + if cas_hostname && !cas_sso_hostname #convert the setting over for use by the plugin - cas_hostname.update_attribute(:name, 'cas_sso_hostname') + cas_hostname.update_attribute(:name, "cas_sso_hostname") elsif cas_hostname && cas_sso_hostname #copy the setting over for use by the plugin and delete the original setting cas_sso_hostname.update_attribute(:value, cas_hostname.value) @@ -17,9 +17,9 @@ class MoveCasSettings < ActiveRecord::Migration[4.2] cas_domainname = SiteSetting.find_by(name: "cas_domainname") cas_sso_email_domain = SiteSetting.find_by(name: "cas_sso_email_domain") - if cas_domainname && ! cas_sso_email_domain + if cas_domainname && !cas_sso_email_domain #convert the setting over for use by the plugin - cas_domainname.update_attribute(:name, 'cas_sso_email_domain') + cas_domainname.update_attribute(:name, "cas_sso_email_domain") elsif cas_domainname && cas_sso_email_domain #copy the setting over for use by the plugin and delete the original setting cas_sso_email_domain.update_attribute(:value, cas_domainname.value) @@ -27,12 +27,9 @@ class MoveCasSettings < ActiveRecord::Migration[4.2] end cas_logins = SiteSetting.find_by(name: "cas_logins") - if cas_logins - cas_logins.destroy - end - - #remove the unused table - drop_table :cas_user_infos + cas_logins.destroy if cas_logins + #remove the unused table + drop_table :cas_user_infos end end diff --git a/db/migrate/20140214151255_add_skipped_to_email_logs.rb b/db/migrate/20140214151255_add_skipped_to_email_logs.rb index 8af4e016b2..19e111084d 100644 --- a/db/migrate/20140214151255_add_skipped_to_email_logs.rb +++ b/db/migrate/20140214151255_add_skipped_to_email_logs.rb @@ -4,6 +4,6 @@ class AddSkippedToEmailLogs < ActiveRecord::Migration[4.2] def change add_column :email_logs, :skipped, :boolean, default: :false add_column :email_logs, :skipped_reason, :string - add_index :email_logs, [:skipped, :created_at] + add_index :email_logs, %i[skipped created_at] end end diff --git a/db/migrate/20140220160510_rename_site_settings.rb b/db/migrate/20140220160510_rename_site_settings.rb index 7424d1f4b2..05e05e8dcd 100644 --- a/db/migrate/20140220160510_rename_site_settings.rb +++ b/db/migrate/20140220160510_rename_site_settings.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RenameSiteSettings < ActiveRecord::Migration[4.2] - def up execute "UPDATE site_settings SET name = 'allow_restore' WHERE name = 'allow_import'" execute "UPDATE site_settings SET name = 'topics_per_period_in_top_summary' WHERE name = 'topics_per_period_in_summary'" @@ -11,5 +10,4 @@ class RenameSiteSettings < ActiveRecord::Migration[4.2] execute "UPDATE site_settings SET name = 'allow_import' WHERE name = 'allow_restore'" execute "UPDATE site_settings SET name = 'topics_per_period_in_summary' WHERE name = 'topics_per_period_in_top_summary'" end - end diff --git a/db/migrate/20140305100909_create_user_badges.rb b/db/migrate/20140305100909_create_user_badges.rb index 5ccca851e8..84d0f83a98 100644 --- a/db/migrate/20140305100909_create_user_badges.rb +++ b/db/migrate/20140305100909_create_user_badges.rb @@ -9,6 +9,6 @@ class CreateUserBadges < ActiveRecord::Migration[4.2] t.integer :granted_by_id, null: false end - add_index :user_badges, [:badge_id, :user_id], unique: true + add_index :user_badges, %i[badge_id user_id], unique: true end end diff --git a/db/migrate/20140306223522_move_topic_revisions_to_post_revisions.rb b/db/migrate/20140306223522_move_topic_revisions_to_post_revisions.rb index 0d7dbaa522..68a437e445 100644 --- a/db/migrate/20140306223522_move_topic_revisions_to_post_revisions.rb +++ b/db/migrate/20140306223522_move_topic_revisions_to_post_revisions.rb @@ -12,7 +12,7 @@ class MoveTopicRevisionsToPostRevisions < ActiveRecord::Migration[4.2] SQL - execute < 0 AND id <= 100').find_each do |badge| - new_id = badge.id + max_badge_id + 100 - UserBadge.where(badge_id: badge.id).update_all badge_id: new_id - badge.update_column :id, new_id - end + max_badge_id = Badge.order("id DESC").limit(1).first.try(:id) + Badge + .where("id > 0 AND id <= 100") + .find_each do |badge| + new_id = badge.id + max_badge_id + 100 + UserBadge.where(badge_id: badge.id).update_all badge_id: new_id + badge.update_column :id, new_id + end end def down diff --git a/db/migrate/20140520062826_add_multiple_award_to_badges.rb b/db/migrate/20140520062826_add_multiple_award_to_badges.rb index 483bf62e3c..29e3ac1aa3 100644 --- a/db/migrate/20140520062826_add_multiple_award_to_badges.rb +++ b/db/migrate/20140520062826_add_multiple_award_to_badges.rb @@ -6,13 +6,13 @@ class AddMultipleAwardToBadges < ActiveRecord::Migration[4.2] reversible do |dir| dir.up do - remove_index :user_badges, column: [:badge_id, :user_id] - add_index :user_badges, [:badge_id, :user_id] + remove_index :user_badges, column: %i[badge_id user_id] + add_index :user_badges, %i[badge_id user_id] end dir.down do - remove_index :user_badges, column: [:badge_id, :user_id] - add_index :user_badges, [:badge_id, :user_id], unique: true + remove_index :user_badges, column: %i[badge_id user_id] + add_index :user_badges, %i[badge_id user_id], unique: true end end end diff --git a/db/migrate/20140521220115_google_openid_default_has_changed.rb b/db/migrate/20140521220115_google_openid_default_has_changed.rb index fc6993e2b4..13bcb681a0 100644 --- a/db/migrate/20140521220115_google_openid_default_has_changed.rb +++ b/db/migrate/20140521220115_google_openid_default_has_changed.rb @@ -5,14 +5,18 @@ class GoogleOpenidDefaultHasChanged < ActiveRecord::Migration[4.2] users_count_query = DB.query_single("SELECT count(*) FROM users") if users_count_query.first.to_i > 1 # This is an existing site. - result = DB.query_single("SELECT count(*) FROM site_settings WHERE name = 'enable_google_logins'") + result = + DB.query_single("SELECT count(*) FROM site_settings WHERE name = 'enable_google_logins'") if result.first.to_i == 0 # The old default was true, so add a row to keep it that way. execute "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) VALUES ('enable_google_logins', 5, 't', now(), now())" end # Don't enable the new Google setting on an existing site. - result = DB.query_single("SELECT count(*) FROM site_settings WHERE name = 'enable_google_oauth2_logins'") + result = + DB.query_single( + "SELECT count(*) FROM site_settings WHERE name = 'enable_google_oauth2_logins'", + ) if result.first.to_i == 0 execute "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) VALUES ('enable_google_oauth2_logins', 5, 'f', now(), now())" end diff --git a/db/migrate/20140526185749_change_category_uniquness_contstraint.rb b/db/migrate/20140526185749_change_category_uniquness_contstraint.rb index 27e5a0ac1d..59b6e909e4 100644 --- a/db/migrate/20140526185749_change_category_uniquness_contstraint.rb +++ b/db/migrate/20140526185749_change_category_uniquness_contstraint.rb @@ -2,7 +2,7 @@ class ChangeCategoryUniqunessContstraint < ActiveRecord::Migration[4.2] def change - remove_index :categories, name: 'index_categories_on_name' - add_index :categories, [:parent_category_id, :name], unique: true + remove_index :categories, name: "index_categories_on_name" + add_index :categories, %i[parent_category_id name], unique: true end end diff --git a/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb b/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb index aaa17e901f..f28f4bc43a 100644 --- a/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb +++ b/db/migrate/20140530002535_remove_system_avatars_from_user_avatars.rb @@ -11,7 +11,7 @@ class RemoveSystemAvatarsFromUserAvatars < ActiveRecord::Migration[4.2] # normally we dont reach into the object model, but we have to here. # otherwise we will wait a real long time for uploads to go away skip = -1 - while skip = destroy_system_avatar_batch(skip) do + while skip = destroy_system_avatar_batch(skip) puts "Destroyed up to id: #{skip}" end @@ -22,20 +22,21 @@ class RemoveSystemAvatarsFromUserAvatars < ActiveRecord::Migration[4.2] def destroy_system_avatar_batch(skip) initial = skip - Upload.where('id IN (SELECT system_upload_id FROM user_avatars) AND id > ?', skip) + Upload + .where("id IN (SELECT system_upload_id FROM user_avatars) AND id > ?", skip) .order(:id) .limit(500) .each do |upload| - skip = upload.id - begin - upload.destroy - rescue - Rails.logger.warn "Could not destroy system avatar #{upload.id}" + skip = upload.id + begin + upload.destroy + rescue StandardError + Rails.logger.warn "Could not destroy system avatar #{upload.id}" + end end - end skip == initial ? nil : skip - rescue + rescue StandardError Rails.logger.warn "Could not destroy system avatars, skipping" nil end diff --git a/db/migrate/20140604145431_disable_external_auths_by_default.rb b/db/migrate/20140604145431_disable_external_auths_by_default.rb index dd8363f265..19ff7599f0 100644 --- a/db/migrate/20140604145431_disable_external_auths_by_default.rb +++ b/db/migrate/20140604145431_disable_external_auths_by_default.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DisableExternalAuthsByDefault < ActiveRecord::Migration[4.2] - def enable_setting_if_default(name) result = DB.query_single("SELECT count(*) count FROM site_settings WHERE name = '#{name}'") if result.first.to_i == 0 @@ -13,10 +12,10 @@ class DisableExternalAuthsByDefault < ActiveRecord::Migration[4.2] users_count_query = DB.query_single("SELECT count(*) FROM users") if users_count_query.first.to_i > 1 # existing site, so keep settings as they are - enable_setting_if_default 'enable_yahoo_logins' - enable_setting_if_default 'enable_google_oauth2_logins' - enable_setting_if_default 'enable_twitter_logins' - enable_setting_if_default 'enable_facebook_logins' + enable_setting_if_default "enable_yahoo_logins" + enable_setting_if_default "enable_google_oauth2_logins" + enable_setting_if_default "enable_twitter_logins" + enable_setting_if_default "enable_facebook_logins" end end diff --git a/db/migrate/20140618001820_dont_auto_muto_topics.rb b/db/migrate/20140618001820_dont_auto_muto_topics.rb index ded7399691..0dafdd14d1 100644 --- a/db/migrate/20140618001820_dont_auto_muto_topics.rb +++ b/db/migrate/20140618001820_dont_auto_muto_topics.rb @@ -3,10 +3,10 @@ class DontAutoMutoTopics < ActiveRecord::Migration[4.2] def change # muting all new topics was a mistake, revert it - execute 'DELETE FROM topic_users WHERE notification_level = 0 and notifications_reason_id =7 AND first_visited_at IS NULL' + execute "DELETE FROM topic_users WHERE notification_level = 0 and notifications_reason_id =7 AND first_visited_at IS NULL" - execute 'UPDATE topic_users SET notification_level = 1, + execute "UPDATE topic_users SET notification_level = 1, notifications_reason_id = NULL - WHERE notification_level = 0 AND notifications_reason_id =7' + WHERE notification_level = 0 AND notifications_reason_id =7" end end diff --git a/db/migrate/20140623195618_fix_categories_constraint.rb b/db/migrate/20140623195618_fix_categories_constraint.rb index 5591e14b11..91fcff7c4b 100644 --- a/db/migrate/20140623195618_fix_categories_constraint.rb +++ b/db/migrate/20140623195618_fix_categories_constraint.rb @@ -2,7 +2,7 @@ class FixCategoriesConstraint < ActiveRecord::Migration[4.2] def change - remove_index :categories, name: 'index_categories_on_parent_category_id_and_name' + remove_index :categories, name: "index_categories_on_parent_category_id_and_name" # Remove any previous duplicates execute "DELETE FROM categories WHERE id IN (SELECT id FROM (SELECT id, row_number() over (partition BY parent_category_id, name ORDER BY id) AS rnum FROM categories) t WHERE t.rnum > 1)" diff --git a/db/migrate/20140705081453_index_user_badges.rb b/db/migrate/20140705081453_index_user_badges.rb index 1f7d28bfaa..79cc45a4c8 100644 --- a/db/migrate/20140705081453_index_user_badges.rb +++ b/db/migrate/20140705081453_index_user_badges.rb @@ -2,12 +2,12 @@ class IndexUserBadges < ActiveRecord::Migration[4.2] def change - execute 'DELETE FROM user_badges USING user_badges ub2 + execute "DELETE FROM user_badges USING user_badges ub2 WHERE user_badges.badge_id = ub2.badge_id AND user_badges.user_id = ub2.user_id AND user_badges.post_id IS NOT NULL AND user_badges.id < ub2.id - ' - add_index :user_badges, [:badge_id, :user_id, :post_id], unique: true, where: 'post_id IS NOT NULL' + " + add_index :user_badges, %i[badge_id user_id post_id], unique: true, where: "post_id IS NOT NULL" end end diff --git a/db/migrate/20140711193923_remove_email_in_address_setting.rb b/db/migrate/20140711193923_remove_email_in_address_setting.rb index 22d5dbbe75..b1f64f92a2 100644 --- a/db/migrate/20140711193923_remove_email_in_address_setting.rb +++ b/db/migrate/20140711193923_remove_email_in_address_setting.rb @@ -2,9 +2,14 @@ class RemoveEmailInAddressSetting < ActiveRecord::Migration[4.2] def up - uncat_id = DB.query_single("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'").first - cat_id_r = DB.query_single("SELECT value FROM site_settings WHERE name = 'email_in_category'").first - email_r = DB.query_single("SELECT value FROM site_settings WHERE name = 'email_in_address'").first + uncat_id = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'", + ).first + cat_id_r = + DB.query_single("SELECT value FROM site_settings WHERE name = 'email_in_category'").first + email_r = + DB.query_single("SELECT value FROM site_settings WHERE name = 'email_in_address'").first if email_r category_id = uncat_id["value"].to_i category_id = cat_id_r["value"].to_i if cat_id_r @@ -12,7 +17,9 @@ class RemoveEmailInAddressSetting < ActiveRecord::Migration[4.2] DB.exec("UPDATE categories SET email_in = ? WHERE id = ?", email, category_id) end - DB.exec("DELETE FROM site_settings WHERE name = 'email_in_category' OR name = 'email_in_address'") + DB.exec( + "DELETE FROM site_settings WHERE name = 'email_in_category' OR name = 'email_in_address'", + ) end def down diff --git a/db/migrate/20140715013018_correct_post_number_index.rb b/db/migrate/20140715013018_correct_post_number_index.rb index 9d0e0f80fb..01d74b1c7a 100644 --- a/db/migrate/20140715013018_correct_post_number_index.rb +++ b/db/migrate/20140715013018_correct_post_number_index.rb @@ -2,7 +2,6 @@ class CorrectPostNumberIndex < ActiveRecord::Migration[4.2] def change - begin a = execute <3, :inappropriate=>4, :notify_moderators=>7, :spam=>8} flag_ids = "3,4,7,8" - x = execute "DELETE FROM post_actions pa + x = + execute "DELETE FROM post_actions pa USING post_actions pa2 WHERE pa.post_action_type_id IN (#{flag_ids}) AND pa2.post_action_type_id IN (#{flag_ids}) AND @@ -34,10 +33,11 @@ class CorrectPostActionIndex < ActiveRecord::Migration[4.2] puts add_index :post_actions, - ["user_id", "post_id", "targets_topic"], - name: "idx_unique_flags", - unique: true, - where: "deleted_at IS NULL AND + %w[user_id post_id targets_topic], + name: "idx_unique_flags", + unique: true, + where: + "deleted_at IS NULL AND disagreed_at IS NULL AND deferred_at IS NULL AND post_action_type_id IN (#{flag_ids})" diff --git a/db/migrate/20140905055251_rename_trust_level_badges.rb b/db/migrate/20140905055251_rename_trust_level_badges.rb index a62419c6fe..2f8ede11f4 100644 --- a/db/migrate/20140905055251_rename_trust_level_badges.rb +++ b/db/migrate/20140905055251_rename_trust_level_badges.rb @@ -1,22 +1,21 @@ # frozen_string_literal: true class RenameTrustLevelBadges < ActiveRecord::Migration[4.2] - def rename(id, old, new) execute "UPDATE badges SET name = '#{new}' WHERE name = '#{old}' AND id = #{id}" - rescue + rescue StandardError puts "#{new} badge is already in use, skipping rename" end def up - rename 2, 'Regular User', 'Member' - rename 3, 'Leader', 'Regular' - rename 4, 'Elder', 'Leader' + rename 2, "Regular User", "Member" + rename 3, "Leader", "Regular" + rename 4, "Elder", "Leader" end def down - rename 2, 'Member', 'Regular User' - rename 3, 'Regular', 'Leader' - rename 4, 'Leader', 'Elder' + rename 2, "Member", "Regular User" + rename 3, "Regular", "Leader" + rename 4, "Leader", "Elder" end end diff --git a/db/migrate/20140910130155_create_topic_user_index.rb b/db/migrate/20140910130155_create_topic_user_index.rb index ff75229e2c..b72971ab10 100644 --- a/db/migrate/20140910130155_create_topic_user_index.rb +++ b/db/migrate/20140910130155_create_topic_user_index.rb @@ -3,6 +3,6 @@ class CreateTopicUserIndex < ActiveRecord::Migration[4.2] def change # seems to be the most effective for joining into topics - add_index :topic_users, [:user_id, :topic_id], unique: true + add_index :topic_users, %i[user_id topic_id], unique: true end end diff --git a/db/migrate/20140929204155_migrate_tos_setting.rb b/db/migrate/20140929204155_migrate_tos_setting.rb index 3ee0ccbd8c..7085399ab9 100644 --- a/db/migrate/20140929204155_migrate_tos_setting.rb +++ b/db/migrate/20140929204155_migrate_tos_setting.rb @@ -4,15 +4,15 @@ class MigrateTosSetting < ActiveRecord::Migration[4.2] def up res = execute("SELECT * FROM site_settings WHERE name = 'tos_accept_required' AND value = 't'") if res.present? && res.cmd_tuples > 0 - label = 'Terms of Service' + label = "Terms of Service" res = execute("SELECT value FROM site_texts WHERE text_type = 'tos_signup_form_message'") - if res.present? && res.cmd_tuples == 1 - label = res[0]['value'] - end + label = res[0]["value"] if res.present? && res.cmd_tuples == 1 label = PG::Connection.escape_string(label) - execute("INSERT INTO user_fields (name, field_type, editable) VALUES ('#{label}', 'confirm', false)") + execute( + "INSERT INTO user_fields (name, field_type, editable) VALUES ('#{label}', 'confirm', false)", + ) end end end diff --git a/db/migrate/20141211114517_fix_emoji_path.rb b/db/migrate/20141211114517_fix_emoji_path.rb index 3538a644f0..bd474b2fbd 100644 --- a/db/migrate/20141211114517_fix_emoji_path.rb +++ b/db/migrate/20141211114517_fix_emoji_path.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class FixEmojiPath < ActiveRecord::Migration[4.2] - BASE_URL = '/plugins/emoji/images/' + BASE_URL = "/plugins/emoji/images/" def up execute <<-SQL diff --git a/db/migrate/20141222224220_fix_emoji_path_take2.rb b/db/migrate/20141222224220_fix_emoji_path_take2.rb index 9bf3482ac4..c3de4618cf 100644 --- a/db/migrate/20141222224220_fix_emoji_path_take2.rb +++ b/db/migrate/20141222224220_fix_emoji_path_take2.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class FixEmojiPathTake2 < ActiveRecord::Migration[4.2] - OLD_URL = '/plugins/emoji/images/' - NEW_URL = '/images/emoji/' + OLD_URL = "/plugins/emoji/images/" + NEW_URL = "/images/emoji/" def up execute <<-SQL diff --git a/db/migrate/20150106215342_remove_stars.rb b/db/migrate/20150106215342_remove_stars.rb index d1eca4b7c4..ec391a5c11 100644 --- a/db/migrate/20150106215342_remove_stars.rb +++ b/db/migrate/20150106215342_remove_stars.rb @@ -13,9 +13,9 @@ class RemoveStars < ActiveRecord::Migration[4.2] pa.post_action_type_id = 1 WHERE pa.post_id IS NULL AND tu.starred SQL - puts "#{r.cmd_tuples} stars were converted to bookmarks!" + puts "#{r.cmd_tuples} stars were converted to bookmarks!" - execute < 0 - ttl = "'#{ttl.seconds.ago.strftime('%Y-%m-%d %H:%M:%S')}'" + ttl = "'#{ttl.seconds.ago.strftime("%Y-%m-%d %H:%M:%S")}'" else ttl = "CURRENT_TIMESTAMP" end Discourse.redis.del(key) - key.gsub!('temporary_key:', '') + key.gsub!("temporary_key:", "") user_id ? "('#{key}', #{user_id}, #{ttl}, #{ttl})" : nil end temp_keys.compact! if temp_keys.present? - execute "INSERT INTO digest_unsubscribe_keys (key, user_id, created_at, updated_at) VALUES #{temp_keys.join(', ')}" + execute "INSERT INTO digest_unsubscribe_keys (key, user_id, created_at, updated_at) VALUES #{temp_keys.join(", ")}" end end - rescue + rescue StandardError # If anything goes wrong, continue with other migrations end diff --git a/db/migrate/20150224004420_add_pinned_indexes.rb b/db/migrate/20150224004420_add_pinned_indexes.rb index 4dc48416f9..929ce87e26 100644 --- a/db/migrate/20150224004420_add_pinned_indexes.rb +++ b/db/migrate/20150224004420_add_pinned_indexes.rb @@ -2,7 +2,7 @@ class AddPinnedIndexes < ActiveRecord::Migration[4.2] def change - add_index :topics, :pinned_globally, where: 'pinned_globally' - add_index :topics, :pinned_at, where: 'pinned_at IS NOT NULL' + add_index :topics, :pinned_globally, where: "pinned_globally" + add_index :topics, :pinned_at, where: "pinned_at IS NOT NULL" end end diff --git a/db/migrate/20150301224250_create_suggested_for_index.rb b/db/migrate/20150301224250_create_suggested_for_index.rb index 2a664199f3..6fcb20e129 100644 --- a/db/migrate/20150301224250_create_suggested_for_index.rb +++ b/db/migrate/20150301224250_create_suggested_for_index.rb @@ -2,7 +2,8 @@ class CreateSuggestedForIndex < ActiveRecord::Migration[4.2] def change - add_index :topics, [:created_at, :visible], - where: "deleted_at IS NULL AND archetype <> 'private_message'" + add_index :topics, + %i[created_at visible], + where: "deleted_at IS NULL AND archetype <> 'private_message'" end end diff --git a/db/migrate/20150306050437_add_all_time_and_op_likes_to_top_topics.rb b/db/migrate/20150306050437_add_all_time_and_op_likes_to_top_topics.rb index d1db8f0e22..de76497b51 100644 --- a/db/migrate/20150306050437_add_all_time_and_op_likes_to_top_topics.rb +++ b/db/migrate/20150306050437_add_all_time_and_op_likes_to_top_topics.rb @@ -3,7 +3,7 @@ class AddAllTimeAndOpLikesToTopTopics < ActiveRecord::Migration[4.2] def change add_column :top_topics, :all_score, :float, default: 0 - [:daily, :weekly, :monthly, :yearly].each do |period| + %i[daily weekly monthly yearly].each do |period| column = "#{period}_op_likes_count" add_column :top_topics, column, :integer, default: 0, null: false add_index :top_topics, [column] diff --git a/db/migrate/20150323234856_add_muted_users.rb b/db/migrate/20150323234856_add_muted_users.rb index 82a102245d..b55665d7f6 100644 --- a/db/migrate/20150323234856_add_muted_users.rb +++ b/db/migrate/20150323234856_add_muted_users.rb @@ -8,7 +8,7 @@ class AddMutedUsers < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :muted_users, [:user_id, :muted_user_id], unique: true - add_index :muted_users, [:muted_user_id, :user_id], unique: true + add_index :muted_users, %i[user_id muted_user_id], unique: true + add_index :muted_users, %i[muted_user_id user_id], unique: true end end diff --git a/db/migrate/20150325190959_create_queued_posts.rb b/db/migrate/20150325190959_create_queued_posts.rb index 4a2c199cb2..b11aa56c71 100644 --- a/db/migrate/20150325190959_create_queued_posts.rb +++ b/db/migrate/20150325190959_create_queued_posts.rb @@ -3,20 +3,20 @@ class CreateQueuedPosts < ActiveRecord::Migration[4.2] def change create_table :queued_posts, force: true do |t| - t.string :queue, null: false - t.integer :state, null: false - t.integer :user_id, null: false - t.text :raw, null: false - t.json :post_options, null: false - t.integer :topic_id - t.integer :approved_by_id - t.timestamp :approved_at - t.integer :rejected_by_id - t.timestamp :rejected_at + t.string :queue, null: false + t.integer :state, null: false + t.integer :user_id, null: false + t.text :raw, null: false + t.json :post_options, null: false + t.integer :topic_id + t.integer :approved_by_id + t.timestamp :approved_at + t.integer :rejected_by_id + t.timestamp :rejected_at t.timestamps null: false end - add_index :queued_posts, [:queue, :state, :created_at], name: 'by_queue_status' - add_index :queued_posts, [:topic_id, :queue, :state, :created_at], name: 'by_queue_status_topic' + add_index :queued_posts, %i[queue state created_at], name: "by_queue_status" + add_index :queued_posts, %i[topic_id queue state created_at], name: "by_queue_status_topic" end end diff --git a/db/migrate/20150422160235_add_link_post_id_index_on_topic_links.rb b/db/migrate/20150422160235_add_link_post_id_index_on_topic_links.rb index 7cb6fd73dd..38b558b7c5 100644 --- a/db/migrate/20150422160235_add_link_post_id_index_on_topic_links.rb +++ b/db/migrate/20150422160235_add_link_post_id_index_on_topic_links.rb @@ -2,6 +2,6 @@ class AddLinkPostIdIndexOnTopicLinks < ActiveRecord::Migration[4.2] def change - add_index :topic_links, [:link_post_id, :reflection] + add_index :topic_links, %i[link_post_id reflection] end end diff --git a/db/migrate/20150505044154_add_stylesheet_cache.rb b/db/migrate/20150505044154_add_stylesheet_cache.rb index 9af3cbbd0a..73d0361045 100644 --- a/db/migrate/20150505044154_add_stylesheet_cache.rb +++ b/db/migrate/20150505044154_add_stylesheet_cache.rb @@ -9,6 +9,6 @@ class AddStylesheetCache < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :stylesheet_cache, [:target, :digest], unique: true + add_index :stylesheet_cache, %i[target digest], unique: true end end diff --git a/db/migrate/20150513094042_add_index_on_post_actions.rb b/db/migrate/20150513094042_add_index_on_post_actions.rb index fa7eafff3d..5aead212ca 100644 --- a/db/migrate/20150513094042_add_index_on_post_actions.rb +++ b/db/migrate/20150513094042_add_index_on_post_actions.rb @@ -2,6 +2,6 @@ class AddIndexOnPostActions < ActiveRecord::Migration[4.2] def change - add_index :post_actions, [:user_id, :post_action_type_id], where: 'deleted_at IS NULL' + add_index :post_actions, %i[user_id post_action_type_id], where: "deleted_at IS NULL" end end diff --git a/db/migrate/20150514023016_add_unread_notifications_index.rb b/db/migrate/20150514023016_add_unread_notifications_index.rb index 237258b0a9..b21ebc43d0 100644 --- a/db/migrate/20150514023016_add_unread_notifications_index.rb +++ b/db/migrate/20150514023016_add_unread_notifications_index.rb @@ -2,6 +2,9 @@ class AddUnreadNotificationsIndex < ActiveRecord::Migration[4.2] def change - add_index :notifications, [:user_id, :notification_type], where: 'not read', name: 'idx_notifications_speedup_unread_count' + add_index :notifications, + %i[user_id notification_type], + where: "not read", + name: "idx_notifications_speedup_unread_count" end end diff --git a/db/migrate/20150514043155_add_user_actions_all_index.rb b/db/migrate/20150514043155_add_user_actions_all_index.rb index bfffe4f620..e549cc5c5a 100644 --- a/db/migrate/20150514043155_add_user_actions_all_index.rb +++ b/db/migrate/20150514043155_add_user_actions_all_index.rb @@ -2,6 +2,8 @@ class AddUserActionsAllIndex < ActiveRecord::Migration[4.2] def change - add_index :user_actions, [:user_id, :created_at, :action_type], name: 'idx_user_actions_speed_up_user_all' + add_index :user_actions, + %i[user_id created_at action_type], + name: "idx_user_actions_speed_up_user_all" end end diff --git a/db/migrate/20150617080349_add_index_on_post_notifications.rb b/db/migrate/20150617080349_add_index_on_post_notifications.rb index 1d4b980a0b..dba012c162 100644 --- a/db/migrate/20150617080349_add_index_on_post_notifications.rb +++ b/db/migrate/20150617080349_add_index_on_post_notifications.rb @@ -2,6 +2,6 @@ class AddIndexOnPostNotifications < ActiveRecord::Migration[4.2] def change - add_index :notifications, [:user_id, :topic_id, :post_number] + add_index :notifications, %i[user_id topic_id post_number] end end diff --git a/db/migrate/20150617234511_add_staff_index_to_users.rb b/db/migrate/20150617234511_add_staff_index_to_users.rb index 7d0384b73e..f07aca8dec 100644 --- a/db/migrate/20150617234511_add_staff_index_to_users.rb +++ b/db/migrate/20150617234511_add_staff_index_to_users.rb @@ -2,7 +2,7 @@ class AddStaffIndexToUsers < ActiveRecord::Migration[4.2] def change - add_index :users, [:id], name: 'idx_users_admin', where: 'admin' - add_index :users, [:id], name: 'idx_users_moderator', where: 'moderator' + add_index :users, [:id], name: "idx_users_admin", where: "admin" + add_index :users, [:id], name: "idx_users_moderator", where: "moderator" end end diff --git a/db/migrate/20150707163251_add_reports_index_to_user_visits.rb b/db/migrate/20150707163251_add_reports_index_to_user_visits.rb index 803da8331b..49ce5de310 100644 --- a/db/migrate/20150707163251_add_reports_index_to_user_visits.rb +++ b/db/migrate/20150707163251_add_reports_index_to_user_visits.rb @@ -2,10 +2,10 @@ class AddReportsIndexToUserVisits < ActiveRecord::Migration[4.2] def up - add_index :user_visits, [:visited_at, :mobile] + add_index :user_visits, %i[visited_at mobile] end def down - remove_index :user_visits, [:visited_at, :mobile] + remove_index :user_visits, %i[visited_at mobile] end end diff --git a/db/migrate/20150724165259_add_index_to_post_custom_fields.rb b/db/migrate/20150724165259_add_index_to_post_custom_fields.rb index 984eb33974..d140f0dbb1 100644 --- a/db/migrate/20150724165259_add_index_to_post_custom_fields.rb +++ b/db/migrate/20150724165259_add_index_to_post_custom_fields.rb @@ -11,6 +11,5 @@ SQL execute < 0 - category_id = category_row[0]['id'].to_i - end + category_id = category_row[0]["id"].to_i if category_row.cmd_tuples > 0 if category_id == 0 - category_id = execute("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'")[0]['value'].to_i + category_id = + execute("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'")[0][ + "value" + ].to_i end embeddable_hosts = execute("SELECT value FROM site_settings WHERE name = 'embeddable_hosts'") if embeddable_hosts && embeddable_hosts.cmd_tuples > 0 - val = embeddable_hosts[0]['value'] + val = embeddable_hosts[0]["value"] if val.present? records = val.split("\n") if records.present? diff --git a/db/migrate/20150914021445_create_user_profile_views.rb b/db/migrate/20150914021445_create_user_profile_views.rb index 37399c6f2c..2c1414cf16 100644 --- a/db/migrate/20150914021445_create_user_profile_views.rb +++ b/db/migrate/20150914021445_create_user_profile_views.rb @@ -11,7 +11,15 @@ class CreateUserProfileViews < ActiveRecord::Migration[4.2] add_index :user_profile_views, :user_profile_id add_index :user_profile_views, :user_id - add_index :user_profile_views, [:viewed_at, :ip_address, :user_profile_id], where: "user_id IS NULL", unique: true, name: 'unique_profile_view_ip' - add_index :user_profile_views, [:viewed_at, :user_id, :user_profile_id], where: "user_id IS NOT NULL", unique: true, name: 'unique_profile_view_user' + add_index :user_profile_views, + %i[viewed_at ip_address user_profile_id], + where: "user_id IS NULL", + unique: true, + name: "unique_profile_view_ip" + add_index :user_profile_views, + %i[viewed_at user_id user_profile_id], + where: "user_id IS NOT NULL", + unique: true, + name: "unique_profile_view_user" end end diff --git a/db/migrate/20150918004206_add_user_id_group_id_index_to_group_users.rb b/db/migrate/20150918004206_add_user_id_group_id_index_to_group_users.rb index be517bebb0..6c9c900dce 100644 --- a/db/migrate/20150918004206_add_user_id_group_id_index_to_group_users.rb +++ b/db/migrate/20150918004206_add_user_id_group_id_index_to_group_users.rb @@ -2,6 +2,6 @@ class AddUserIdGroupIdIndexToGroupUsers < ActiveRecord::Migration[4.2] def change - add_index :group_users, [:user_id, :group_id], unique: true + add_index :group_users, %i[user_id group_id], unique: true end end diff --git a/db/migrate/20151113205046_create_translation_overrides.rb b/db/migrate/20151113205046_create_translation_overrides.rb index 3b7f68fd2d..933ccfc023 100644 --- a/db/migrate/20151113205046_create_translation_overrides.rb +++ b/db/migrate/20151113205046_create_translation_overrides.rb @@ -9,6 +9,6 @@ class CreateTranslationOverrides < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :translation_overrides, [:locale, :translation_key], unique: true + add_index :translation_overrides, %i[locale translation_key], unique: true end end diff --git a/db/migrate/20151201035631_add_group_mentions.rb b/db/migrate/20151201035631_add_group_mentions.rb index 8ebe2d5364..97c34613a0 100644 --- a/db/migrate/20151201035631_add_group_mentions.rb +++ b/db/migrate/20151201035631_add_group_mentions.rb @@ -8,7 +8,7 @@ class AddGroupMentions < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :group_mentions, [:post_id, :group_id], unique: true - add_index :group_mentions, [:group_id, :post_id], unique: true + add_index :group_mentions, %i[post_id group_id], unique: true + add_index :group_mentions, %i[group_id post_id], unique: true end end diff --git a/db/migrate/20151218232200_add_unique_index_to_category_users.rb b/db/migrate/20151218232200_add_unique_index_to_category_users.rb index a255ea2deb..13e440e859 100644 --- a/db/migrate/20151218232200_add_unique_index_to_category_users.rb +++ b/db/migrate/20151218232200_add_unique_index_to_category_users.rb @@ -10,14 +10,18 @@ DELETE FROM category_users cu USING category_users cu1 cu.id < cu1.id SQL - add_index :category_users, [:user_id, :category_id, :notification_level], - name: 'idx_category_users_u1', unique: true - add_index :category_users, [:category_id, :user_id, :notification_level], - name: 'idx_category_users_u2', unique: true + add_index :category_users, + %i[user_id category_id notification_level], + name: "idx_category_users_u1", + unique: true + add_index :category_users, + %i[category_id user_id notification_level], + name: "idx_category_users_u2", + unique: true end def down - remove_index :category_users, name: 'idx_category_users_u1' - remove_index :category_users, name: 'idx_category_users_u2' + remove_index :category_users, name: "idx_category_users_u1" + remove_index :category_users, name: "idx_category_users_u2" end end diff --git a/db/migrate/20151219045559_add_has_messages_to_groups.rb b/db/migrate/20151219045559_add_has_messages_to_groups.rb index 82d064f374..2acead6aca 100644 --- a/db/migrate/20151219045559_add_has_messages_to_groups.rb +++ b/db/migrate/20151219045559_add_has_messages_to_groups.rb @@ -8,7 +8,6 @@ class AddHasMessagesToGroups < ActiveRecord::Migration[4.2] UPDATE groups g SET has_messages = true WHERE exists(SELECT group_id FROM topic_allowed_groups WHERE group_id = g.id) SQL - end def down diff --git a/db/migrate/20151220232725_add_user_archived_messages_group_archived_messages.rb b/db/migrate/20151220232725_add_user_archived_messages_group_archived_messages.rb index 3110c726f8..cd0fe69b18 100644 --- a/db/migrate/20151220232725_add_user_archived_messages_group_archived_messages.rb +++ b/db/migrate/20151220232725_add_user_archived_messages_group_archived_messages.rb @@ -8,7 +8,7 @@ class AddUserArchivedMessagesGroupArchivedMessages < ActiveRecord::Migration[4.2 t.timestamps null: false end - add_index :user_archived_messages, [:user_id, :topic_id], unique: true + add_index :user_archived_messages, %i[user_id topic_id], unique: true create_table :group_archived_messages do |t| t.integer :group_id, null: false @@ -16,6 +16,6 @@ class AddUserArchivedMessagesGroupArchivedMessages < ActiveRecord::Migration[4.2 t.timestamps null: false end - add_index :group_archived_messages, [:group_id, :topic_id], unique: true + add_index :group_archived_messages, %i[group_id topic_id], unique: true end end diff --git a/db/migrate/20160108051129_fix_incorrect_user_history.rb b/db/migrate/20160108051129_fix_incorrect_user_history.rb index 5e2928d258..e7062993b2 100644 --- a/db/migrate/20160108051129_fix_incorrect_user_history.rb +++ b/db/migrate/20160108051129_fix_incorrect_user_history.rb @@ -16,17 +16,24 @@ class FixIncorrectUserHistory < ActiveRecord::Migration[4.2] (action = 19 AND target_user_id IS NULL AND details IS NOT NULL) SQL - first_wrong_id = execute("SELECT min(id) FROM user_histories WHERE #{condition}").values[0][0].to_i - last_wrong_id = execute("SELECT max(id) FROM user_histories WHERE #{condition}").values[0][0].to_i + first_wrong_id = + execute("SELECT min(id) FROM user_histories WHERE #{condition}").values[0][0].to_i + last_wrong_id = + execute("SELECT max(id) FROM user_histories WHERE #{condition}").values[0][0].to_i if first_wrong_id < last_wrong_id - msg = "Correcting user history records from id: #{first_wrong_id} to #{last_wrong_id} (see: https://meta.discourse.org/t/old-user-suspension-reasons-have-gone-missing/3730)" + msg = + "Correcting user history records from id: #{first_wrong_id} to #{last_wrong_id} (see: https://meta.discourse.org/t/old-user-suspension-reasons-have-gone-missing/3730)" - execute("UPDATE user_histories SET action = action - 1 - WHERE action > 5 AND id >= #{first_wrong_id} AND id <= #{last_wrong_id}") + execute( + "UPDATE user_histories SET action = action - 1 + WHERE action > 5 AND id >= #{first_wrong_id} AND id <= #{last_wrong_id}", + ) - execute("INSERT INTO user_histories(action, acting_user_id, details, created_at, updated_at) - VALUES (22, -1, '#{msg}', current_timestamp, current_timestamp)") + execute( + "INSERT INTO user_histories(action, acting_user_id, details, created_at, updated_at) + VALUES (22, -1, '#{msg}', current_timestamp, current_timestamp)", + ) end end diff --git a/db/migrate/20160110053003_archive_system_messages_with_no_replies.rb b/db/migrate/20160110053003_archive_system_messages_with_no_replies.rb index 8c88c345e1..dcc9361a8b 100644 --- a/db/migrate/20160110053003_archive_system_messages_with_no_replies.rb +++ b/db/migrate/20160110053003_archive_system_messages_with_no_replies.rb @@ -18,7 +18,6 @@ class ArchiveSystemMessagesWithNoReplies < ActiveRecord::Migration[4.2] p.topic_id IS NOT NULL AND p.post_number = 1 SQL - end def down diff --git a/db/migrate/20160112025852_remove_users_from_topic_allowed_users.rb b/db/migrate/20160112025852_remove_users_from_topic_allowed_users.rb index 1dde0a2edd..cbfb1d9050 100644 --- a/db/migrate/20160112025852_remove_users_from_topic_allowed_users.rb +++ b/db/migrate/20160112025852_remove_users_from_topic_allowed_users.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RemoveUsersFromTopicAllowedUsers < ActiveRecord::Migration[4.2] - # historically we added admins automatically to a message if they # responded, despite them being in the group the message is targeted at # this causes inbox bloat for pretty much no reason diff --git a/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb b/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb index 6e9d7aa45a..ebf1688dc0 100644 --- a/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb +++ b/db/migrate/20160215075528_add_unread_pm_index_to_notifications.rb @@ -3,6 +3,9 @@ class AddUnreadPmIndexToNotifications < ActiveRecord::Migration[4.2] def change # create index idxtmp on notifications(user_id, id) where notification_type = 6 AND NOT read - add_index :notifications, [:user_id, :id], unique: true, where: 'notification_type = 6 AND NOT read' + add_index :notifications, + %i[user_id id], + unique: true, + where: "notification_type = 6 AND NOT read" end end diff --git a/db/migrate/20160225050317_add_user_options.rb b/db/migrate/20160225050317_add_user_options.rb index ec1c17536e..acdef7fd10 100644 --- a/db/migrate/20160225050317_add_user_options.rb +++ b/db/migrate/20160225050317_add_user_options.rb @@ -2,7 +2,6 @@ class AddUserOptions < ActiveRecord::Migration[4.2] def up - create_table :user_options, id: false do |t| t.integer :user_id, null: false t.boolean :email_always, null: false, default: false diff --git a/db/migrate/20160225095306_add_email_in_reply_to_to_user_options.rb b/db/migrate/20160225095306_add_email_in_reply_to_to_user_options.rb index 7a8be0e9a2..aeccb1840f 100644 --- a/db/migrate/20160225095306_add_email_in_reply_to_to_user_options.rb +++ b/db/migrate/20160225095306_add_email_in_reply_to_to_user_options.rb @@ -4,7 +4,7 @@ class AddEmailInReplyToToUserOptions < ActiveRecord::Migration[4.2] def up add_column :user_options, :email_in_reply_to, :boolean, null: false, default: true change_column :user_options, :email_previous_replies, :integer, default: 2, null: false - execute 'UPDATE user_options SET email_previous_replies = 2' + execute "UPDATE user_options SET email_previous_replies = 2" end def down diff --git a/db/migrate/20160302063432_rebuild_directory_item_with_index.rb b/db/migrate/20160302063432_rebuild_directory_item_with_index.rb index 0b2836bc71..3503836afb 100644 --- a/db/migrate/20160302063432_rebuild_directory_item_with_index.rb +++ b/db/migrate/20160302063432_rebuild_directory_item_with_index.rb @@ -4,11 +4,11 @@ class RebuildDirectoryItemWithIndex < ActiveRecord::Migration[4.2] def up remove_index :directory_items, [:period_type] execute "TRUNCATE TABLE directory_items RESTART IDENTITY" - add_index :directory_items, [:period_type, :user_id], unique: true + add_index :directory_items, %i[period_type user_id], unique: true end def down - remove_index :directory_items, [:period_type, :user_id] + remove_index :directory_items, %i[period_type user_id] add_index :directory_items, [:period_type] end end diff --git a/db/migrate/20160317174357_create_given_daily_likes.rb b/db/migrate/20160317174357_create_given_daily_likes.rb index 755256efcc..34b93d52ba 100644 --- a/db/migrate/20160317174357_create_given_daily_likes.rb +++ b/db/migrate/20160317174357_create_given_daily_likes.rb @@ -5,16 +5,14 @@ class CreateGivenDailyLikes < ActiveRecord::Migration[4.2] create_table :given_daily_likes, id: false, force: true do |t| t.integer :user_id, null: false t.integer :likes_given, null: false - t.date :given_date, null: false + t.date :given_date, null: false t.boolean :limit_reached, null: false, default: false end - add_index :given_daily_likes, [:user_id, :given_date], unique: true - add_index :given_daily_likes, [:limit_reached, :user_id] + add_index :given_daily_likes, %i[user_id given_date], unique: true + add_index :given_daily_likes, %i[limit_reached user_id] max_likes_rows = execute("SELECT value FROM site_settings WHERE name = 'max_likes_per_day'") - if max_likes_rows && max_likes_rows.cmd_tuples > 0 - max_likes = max_likes_rows[0]['value'].to_i - end + max_likes = max_likes_rows[0]["value"].to_i if max_likes_rows && max_likes_rows.cmd_tuples > 0 max_likes ||= 50 execute "INSERT INTO given_daily_likes (user_id, likes_given, limit_reached, given_date) diff --git a/db/migrate/20160405172827_create_user_firsts.rb b/db/migrate/20160405172827_create_user_firsts.rb index 8a5789f29c..886faf1135 100644 --- a/db/migrate/20160405172827_create_user_firsts.rb +++ b/db/migrate/20160405172827_create_user_firsts.rb @@ -9,6 +9,6 @@ class CreateUserFirsts < ActiveRecord::Migration[4.2] t.datetime :created_at, null: false end - add_index :user_firsts, [:user_id, :first_type], unique: true + add_index :user_firsts, %i[user_id first_type], unique: true end end diff --git a/db/migrate/20160407160756_remove_user_firsts.rb b/db/migrate/20160407160756_remove_user_firsts.rb index d3c7d51b95..e09b77250c 100644 --- a/db/migrate/20160407160756_remove_user_firsts.rb +++ b/db/migrate/20160407160756_remove_user_firsts.rb @@ -3,7 +3,7 @@ class RemoveUserFirsts < ActiveRecord::Migration[4.2] def up drop_table(:user_firsts) if table_exists?(:user_firsts) - rescue + rescue StandardError # continues with other migrations if we can't delete that table nil end diff --git a/db/migrate/20160503205953_create_tags.rb b/db/migrate/20160503205953_create_tags.rb index b04a91ff09..cfa6a0200b 100644 --- a/db/migrate/20160503205953_create_tags.rb +++ b/db/migrate/20160503205953_create_tags.rb @@ -3,27 +3,33 @@ class CreateTags < ActiveRecord::Migration[4.2] def change create_table :tags do |t| - t.string :name, null: false - t.integer :topic_count, null: false, default: 0 + t.string :name, null: false + t.integer :topic_count, null: false, default: 0 t.timestamps null: false end create_table :topic_tags do |t| t.references :topic, null: false - t.references :tag, null: false + t.references :tag, null: false t.timestamps null: false end create_table :tag_users do |t| - t.references :tag, null: false + t.references :tag, null: false t.references :user, null: false - t.integer :notification_level, null: false + t.integer :notification_level, null: false t.timestamps null: false end add_index :tags, :name, unique: true - add_index :topic_tags, [:topic_id, :tag_id], unique: true - add_index :tag_users, [:user_id, :tag_id, :notification_level], name: "idx_tag_users_ix1", unique: true - add_index :tag_users, [:tag_id, :user_id, :notification_level], name: "idx_tag_users_ix2", unique: true + add_index :topic_tags, %i[topic_id tag_id], unique: true + add_index :tag_users, + %i[user_id tag_id notification_level], + name: "idx_tag_users_ix1", + unique: true + add_index :tag_users, + %i[tag_id user_id notification_level], + name: "idx_tag_users_ix2", + unique: true end end diff --git a/db/migrate/20160520022627_shorten_topic_custom_fields_index.rb b/db/migrate/20160520022627_shorten_topic_custom_fields_index.rb index 8918d25472..c3371cf426 100644 --- a/db/migrate/20160520022627_shorten_topic_custom_fields_index.rb +++ b/db/migrate/20160520022627_shorten_topic_custom_fields_index.rb @@ -3,12 +3,13 @@ class ShortenTopicCustomFieldsIndex < ActiveRecord::Migration[4.2] def up remove_index :topic_custom_fields, :value - add_index :topic_custom_fields, [:value, :name], - name: 'topic_custom_fields_value_key_idx', - where: 'value IS NOT NULL AND char_length(value) < 400' + add_index :topic_custom_fields, + %i[value name], + name: "topic_custom_fields_value_key_idx", + where: "value IS NOT NULL AND char_length(value) < 400" end def down - remove_index :topic_custom_fields, :value, name: 'topic_custom_fields_value_key_idx' + remove_index :topic_custom_fields, :value, name: "topic_custom_fields_value_key_idx" add_index :topic_custom_fields, :value end end diff --git a/db/migrate/20160527015355_correct_mailing_list_mode_frequency.rb b/db/migrate/20160527015355_correct_mailing_list_mode_frequency.rb index f19cdb0d38..7d1f9135ba 100644 --- a/db/migrate/20160527015355_correct_mailing_list_mode_frequency.rb +++ b/db/migrate/20160527015355_correct_mailing_list_mode_frequency.rb @@ -4,7 +4,7 @@ class CorrectMailingListModeFrequency < ActiveRecord::Migration[4.2] def up # historically mailing list mode was for every message # keep working the same way for all old users - execute 'UPDATE user_options SET mailing_list_mode_frequency = 1 where mailing_list_mode' + execute "UPDATE user_options SET mailing_list_mode_frequency = 1 where mailing_list_mode" end def down diff --git a/db/migrate/20160527191614_create_category_tags.rb b/db/migrate/20160527191614_create_category_tags.rb index 9675a4ce1e..0a52730f06 100644 --- a/db/migrate/20160527191614_create_category_tags.rb +++ b/db/migrate/20160527191614_create_category_tags.rb @@ -4,11 +4,11 @@ class CreateCategoryTags < ActiveRecord::Migration[4.2] def change create_table :category_tags do |t| t.references :category, null: false - t.references :tag, null: false + t.references :tag, null: false t.timestamps null: false end - add_index :category_tags, [:category_id, :tag_id], name: "idx_category_tags_ix1", unique: true - add_index :category_tags, [:tag_id, :category_id], name: "idx_category_tags_ix2", unique: true + add_index :category_tags, %i[category_id tag_id], name: "idx_category_tags_ix1", unique: true + add_index :category_tags, %i[tag_id category_id], name: "idx_category_tags_ix2", unique: true end end diff --git a/db/migrate/20160602164008_create_tag_groups.rb b/db/migrate/20160602164008_create_tag_groups.rb index 13e2cfdafd..685fdbc9be 100644 --- a/db/migrate/20160602164008_create_tag_groups.rb +++ b/db/migrate/20160602164008_create_tag_groups.rb @@ -3,17 +3,17 @@ class CreateTagGroups < ActiveRecord::Migration[4.2] def change create_table :tag_groups do |t| - t.string :name, null: false + t.string :name, null: false t.integer :tag_count, null: false, default: 0 t.timestamps null: false end create_table :tag_group_memberships do |t| - t.references :tag, null: false + t.references :tag, null: false t.references :tag_group, null: false t.timestamps null: false end - add_index :tag_group_memberships, [:tag_group_id, :tag_id], unique: true + add_index :tag_group_memberships, %i[tag_group_id tag_id], unique: true end end diff --git a/db/migrate/20160606204319_create_category_tag_groups.rb b/db/migrate/20160606204319_create_category_tag_groups.rb index e6c8a9db29..ed7b3678c3 100644 --- a/db/migrate/20160606204319_create_category_tag_groups.rb +++ b/db/migrate/20160606204319_create_category_tag_groups.rb @@ -3,11 +3,14 @@ class CreateCategoryTagGroups < ActiveRecord::Migration[4.2] def change create_table :category_tag_groups do |t| - t.references :category, null: false + t.references :category, null: false t.references :tag_group, null: false t.timestamps null: false end - add_index :category_tag_groups, [:category_id, :tag_group_id], name: "idx_category_tag_groups_ix1", unique: true + add_index :category_tag_groups, + %i[category_id tag_group_id], + name: "idx_category_tag_groups_ix1", + unique: true end end diff --git a/db/migrate/20160719002225_add_deleted_post_index_to_posts.rb b/db/migrate/20160719002225_add_deleted_post_index_to_posts.rb index df3139d70c..0ee9f6050d 100644 --- a/db/migrate/20160719002225_add_deleted_post_index_to_posts.rb +++ b/db/migrate/20160719002225_add_deleted_post_index_to_posts.rb @@ -2,6 +2,9 @@ class AddDeletedPostIndexToPosts < ActiveRecord::Migration[4.2] def change - add_index :posts, [:topic_id, :post_number], where: 'deleted_at IS NOT NULL', name: 'idx_posts_deleted_posts' + add_index :posts, + %i[topic_id post_number], + where: "deleted_at IS NOT NULL", + name: "idx_posts_deleted_posts" end end diff --git a/db/migrate/20160815210156_add_flair_url_to_groups.rb b/db/migrate/20160815210156_add_flair_url_to_groups.rb index b678f03d06..02b7cb6991 100644 --- a/db/migrate/20160815210156_add_flair_url_to_groups.rb +++ b/db/migrate/20160815210156_add_flair_url_to_groups.rb @@ -2,7 +2,7 @@ class AddFlairUrlToGroups < ActiveRecord::Migration[4.2] def change - add_column :groups, :flair_url, :string + add_column :groups, :flair_url, :string add_column :groups, :flair_bg_color, :string end end diff --git a/db/migrate/20160905082248_create_web_hooks.rb b/db/migrate/20160905082248_create_web_hooks.rb index f98f2c0e0b..ff88d731be 100644 --- a/db/migrate/20160905082248_create_web_hooks.rb +++ b/db/migrate/20160905082248_create_web_hooks.rb @@ -3,11 +3,11 @@ class CreateWebHooks < ActiveRecord::Migration[4.2] def change create_table :web_hooks do |t| - t.string :payload_url, null: false + t.string :payload_url, null: false t.integer :content_type, default: 1, null: false t.integer :last_delivery_status, default: 1, null: false t.integer :status, default: 1, null: false - t.string :secret, default: '' + t.string :secret, default: "" t.boolean :wildcard_web_hook, default: false, null: false t.boolean :verify_certificate, default: true, null: false t.boolean :active, default: false, null: false diff --git a/db/migrate/20160905084502_create_web_hook_events.rb b/db/migrate/20160905084502_create_web_hook_events.rb index a82721308f..d4d8b6dd8f 100644 --- a/db/migrate/20160905084502_create_web_hook_events.rb +++ b/db/migrate/20160905084502_create_web_hook_events.rb @@ -4,12 +4,12 @@ class CreateWebHookEvents < ActiveRecord::Migration[4.2] def change create_table :web_hook_events do |t| t.belongs_to :web_hook, null: false, index: true - t.string :headers - t.text :payload - t.integer :status, default: 0 - t.string :response_headers - t.text :response_body - t.integer :duration, default: 0 + t.string :headers + t.text :payload + t.integer :status, default: 0 + t.string :response_headers + t.text :response_body + t.integer :duration, default: 0 t.timestamps null: false end diff --git a/db/migrate/20160905085445_create_join_table_web_hooks_web_hook_event_types.rb b/db/migrate/20160905085445_create_join_table_web_hooks_web_hook_event_types.rb index f28ef31671..84db92b31b 100644 --- a/db/migrate/20160905085445_create_join_table_web_hooks_web_hook_event_types.rb +++ b/db/migrate/20160905085445_create_join_table_web_hooks_web_hook_event_types.rb @@ -4,8 +4,9 @@ class CreateJoinTableWebHooksWebHookEventTypes < ActiveRecord::Migration[4.2] def change create_join_table :web_hooks, :web_hook_event_types - add_index :web_hook_event_types_hooks, [:web_hook_event_type_id, :web_hook_id], - name: 'idx_web_hook_event_types_hooks_on_ids', - unique: true + add_index :web_hook_event_types_hooks, + %i[web_hook_event_type_id web_hook_id], + name: "idx_web_hook_event_types_hooks_on_ids", + unique: true end end diff --git a/db/migrate/20160905091958_create_join_table_web_hooks_groups.rb b/db/migrate/20160905091958_create_join_table_web_hooks_groups.rb index 17e6b2a12d..4c8aaed95b 100644 --- a/db/migrate/20160905091958_create_join_table_web_hooks_groups.rb +++ b/db/migrate/20160905091958_create_join_table_web_hooks_groups.rb @@ -3,6 +3,6 @@ class CreateJoinTableWebHooksGroups < ActiveRecord::Migration[4.2] def change create_join_table :web_hooks, :groups - add_index :groups_web_hooks, [:web_hook_id, :group_id], unique: true + add_index :groups_web_hooks, %i[web_hook_id group_id], unique: true end end diff --git a/db/migrate/20160905092148_create_join_table_web_hooks_categories.rb b/db/migrate/20160905092148_create_join_table_web_hooks_categories.rb index 328aa3c25a..7a43d80740 100644 --- a/db/migrate/20160905092148_create_join_table_web_hooks_categories.rb +++ b/db/migrate/20160905092148_create_join_table_web_hooks_categories.rb @@ -3,6 +3,6 @@ class CreateJoinTableWebHooksCategories < ActiveRecord::Migration[4.2] def change create_join_table :web_hooks, :categories - add_index :categories_web_hooks, [:web_hook_id, :category_id], unique: true + add_index :categories_web_hooks, %i[web_hook_id category_id], unique: true end end diff --git a/db/migrate/20161205001727_add_topic_columns_back.rb b/db/migrate/20161205001727_add_topic_columns_back.rb index 7e3ba35069..e533a4b649 100644 --- a/db/migrate/20161205001727_add_topic_columns_back.rb +++ b/db/migrate/20161205001727_add_topic_columns_back.rb @@ -1,23 +1,22 @@ # frozen_string_literal: true class AddTopicColumnsBack < ActiveRecord::Migration[4.2] - # This really sucks big time, we have no use for these columns yet can not remove them # if we remove them then sites will be down during migration def up add_column :topics, :bookmark_count, :int - add_column :topics, :off_topic_count, :int - add_column :topics, :illegal_count, :int - add_column :topics, :inappropriate_count, :int - add_column :topics, :notify_user_count, :int + add_column :topics, :off_topic_count, :int + add_column :topics, :illegal_count, :int + add_column :topics, :inappropriate_count, :int + add_column :topics, :notify_user_count, :int end def down remove_column :topics, :bookmark_count - remove_column :topics, :off_topic_count - remove_column :topics, :illegal_count - remove_column :topics, :inappropriate_count - remove_column :topics, :notify_user_count + remove_column :topics, :off_topic_count + remove_column :topics, :illegal_count + remove_column :topics, :inappropriate_count + remove_column :topics, :notify_user_count end end diff --git a/db/migrate/20170124181409_add_user_auth_tokens.rb b/db/migrate/20170124181409_add_user_auth_tokens.rb index 0aabc3b6c1..9ca46e6897 100644 --- a/db/migrate/20170124181409_add_user_auth_tokens.rb +++ b/db/migrate/20170124181409_add_user_auth_tokens.rb @@ -11,18 +11,17 @@ class AddUserAuthTokens < ActiveRecord::Migration[4.2] SQL drop_table :user_auth_tokens - end def up create_table :user_auth_tokens do |t| t.integer :user_id, null: false - t.string :auth_token, null: false - t.string :prev_auth_token, null: false - t.string :user_agent + t.string :auth_token, null: false + t.string :prev_auth_token, null: false + t.string :user_agent t.boolean :auth_token_seen, default: false, null: false t.boolean :legacy, default: false, null: false - t.inet :client_ip + t.inet :client_ip t.datetime :rotated_at, null: false t.timestamps null: false end diff --git a/db/migrate/20170221204204_add_show_subcategory_list_to_categories.rb b/db/migrate/20170221204204_add_show_subcategory_list_to_categories.rb index 3fe4c73d8c..4bc76152cc 100644 --- a/db/migrate/20170221204204_add_show_subcategory_list_to_categories.rb +++ b/db/migrate/20170221204204_add_show_subcategory_list_to_categories.rb @@ -4,7 +4,10 @@ class AddShowSubcategoryListToCategories < ActiveRecord::Migration[4.2] def up add_column :categories, :show_subcategory_list, :boolean, default: false - result = execute("select count(1) from site_settings where name = 'show_subcategory_list' and value = 't'") + result = + execute( + "select count(1) from site_settings where name = 'show_subcategory_list' and value = 't'", + ) if result[0] && result[0]["count"].to_i > (0) execute "UPDATE categories SET show_subcategory_list = true WHERE parent_category_id IS NULL" end diff --git a/db/migrate/20170227211458_add_featured_topics_to_categories.rb b/db/migrate/20170227211458_add_featured_topics_to_categories.rb index abcc021e77..011c4b9a0b 100644 --- a/db/migrate/20170227211458_add_featured_topics_to_categories.rb +++ b/db/migrate/20170227211458_add_featured_topics_to_categories.rb @@ -4,7 +4,10 @@ class AddFeaturedTopicsToCategories < ActiveRecord::Migration[4.2] def up add_column :categories, :num_featured_topics, :integer, default: 3 - result = execute("select value from site_settings where name = 'category_featured_topics' and value != '3'") + result = + execute( + "select value from site_settings where name = 'category_featured_topics' and value != '3'", + ) if result.count > 0 && result[0]["value"].to_i > 0 execute "UPDATE categories SET num_featured_topics = #{result[0]["value"].to_i}" end diff --git a/db/migrate/20170303070706_add_index_to_topic_view_items.rb b/db/migrate/20170303070706_add_index_to_topic_view_items.rb index 164505e9f1..667752ea08 100644 --- a/db/migrate/20170303070706_add_index_to_topic_view_items.rb +++ b/db/migrate/20170303070706_add_index_to_topic_view_items.rb @@ -2,6 +2,6 @@ class AddIndexToTopicViewItems < ActiveRecord::Migration[4.2] def change - add_index :topic_views, [:user_id, :viewed_at] + add_index :topic_views, %i[user_id viewed_at] end end diff --git a/db/migrate/20170308201552_add_subcategory_list_style_to_categories.rb b/db/migrate/20170308201552_add_subcategory_list_style_to_categories.rb index a2e9110597..9a79f38833 100644 --- a/db/migrate/20170308201552_add_subcategory_list_style_to_categories.rb +++ b/db/migrate/20170308201552_add_subcategory_list_style_to_categories.rb @@ -2,9 +2,16 @@ class AddSubcategoryListStyleToCategories < ActiveRecord::Migration[4.2] def up - add_column :categories, :subcategory_list_style, :string, limit: 50, default: 'rows_with_featured_topics' + add_column :categories, + :subcategory_list_style, + :string, + limit: 50, + default: "rows_with_featured_topics" - result = execute("select value from site_settings where name = 'desktop_category_page_style' and value != 'categories_with_featured_topics'") + result = + execute( + "select value from site_settings where name = 'desktop_category_page_style' and value != 'categories_with_featured_topics'", + ) if result.count > 0 execute "UPDATE categories SET subcategory_list_style = 'rows' WHERE parent_category_id IS NULL" end diff --git a/db/migrate/20170313192741_add_themes.rb b/db/migrate/20170313192741_add_themes.rb index 54c735e718..0e3a342174 100644 --- a/db/migrate/20170313192741_add_themes.rb +++ b/db/migrate/20170313192741_add_themes.rb @@ -14,27 +14,23 @@ class AddThemes < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :child_themes, [:parent_theme_id, :child_theme_id], unique: true - add_index :child_themes, [:child_theme_id, :parent_theme_id], unique: true + add_index :child_themes, %i[parent_theme_id child_theme_id], unique: true + add_index :child_themes, %i[child_theme_id parent_theme_id], unique: true # versioning in color scheme table was very confusing, remove it execute "DELETE FROM color_schemes WHERE versioned_id IS NOT NULL" remove_column :color_schemes, :versioned_id - enabled_theme_count = execute("SELECT count(*) FROM themes WHERE enabled") - .to_a[0]["count"].to_i + enabled_theme_count = execute("SELECT count(*) FROM themes WHERE enabled").to_a[0]["count"].to_i - enabled_scheme_id = execute("SELECT id FROM color_schemes WHERE enabled") - .to_a[0]&.fetch("id") + enabled_scheme_id = execute("SELECT id FROM color_schemes WHERE enabled").to_a[0]&.fetch("id") - theme_key, theme_id = - execute("SELECT key, id FROM themes WHERE enabled").to_a[0]&.values + theme_key, theme_id = execute("SELECT key, id FROM themes WHERE enabled").to_a[0]&.values if (enabled_theme_count == 0 && enabled_scheme_id) || enabled_theme_count > 1 - puts "Creating a new default theme!" - theme_key = '7e202ef2-6666-47d5-98d8-a9c8d15e57dd' + theme_key = "7e202ef2-6666-47d5-98d8-a9c8d15e57dd" sql = < 1 - execute < 1 INSERT INTO child_themes(parent_theme_id, child_theme_id, created_at, updated_at) SELECT #{theme_id.to_i}, id, created_at, updated_at FROM themes WHERE enabled SQL - end if enabled_scheme_id execute "UPDATE themes SET color_scheme_id=#{enabled_scheme_id.to_i} WHERE id=#{theme_id.to_i}" diff --git a/db/migrate/20170322191305_add_default_top_period_to_categories.rb b/db/migrate/20170322191305_add_default_top_period_to_categories.rb index 92480da3dc..8847a7fd00 100644 --- a/db/migrate/20170322191305_add_default_top_period_to_categories.rb +++ b/db/migrate/20170322191305_add_default_top_period_to_categories.rb @@ -2,6 +2,6 @@ class AddDefaultTopPeriodToCategories < ActiveRecord::Migration[4.2] def change - add_column :categories, :default_top_period, :string, limit: 20, default: 'all' + add_column :categories, :default_top_period, :string, limit: 20, default: "all" end end diff --git a/db/migrate/20170328163918_break_up_themes_table.rb b/db/migrate/20170328163918_break_up_themes_table.rb index 498542d0b6..ad2c2d03ee 100644 --- a/db/migrate/20170328163918_break_up_themes_table.rb +++ b/db/migrate/20170328163918_break_up_themes_table.rb @@ -11,7 +11,7 @@ class BreakUpThemesTable < ActiveRecord::Migration[4.2] t.timestamps null: false end - add_index :theme_fields, [:theme_id, :target, :name], unique: true + add_index :theme_fields, %i[theme_id target name], unique: true [ [0, "embedded_scss", "embedded_scss"], @@ -27,7 +27,6 @@ class BreakUpThemesTable < ActiveRecord::Migration[4.2] [1, "footer", "footer"], [2, "mobile_footer", "footer"], ].each do |target, value, name| - execute < 0' + execute "UPDATE groups set visible = false where visibility_level > 0" end end diff --git a/db/migrate/20170713164357_create_search_logs.rb b/db/migrate/20170713164357_create_search_logs.rb index 8fafc76c65..8d4fc6dec3 100644 --- a/db/migrate/20170713164357_create_search_logs.rb +++ b/db/migrate/20170713164357_create_search_logs.rb @@ -5,7 +5,7 @@ class CreateSearchLogs < ActiveRecord::Migration[4.2] create_table :search_logs do |t| t.string :term, null: false t.integer :user_id, null: true - t.inet :ip_address, null: false + t.inet :ip_address, null: false t.integer :clicked_topic_id, null: true t.integer :search_type, null: false t.datetime :created_at, null: false diff --git a/db/migrate/20170717084947_create_user_emails.rb b/db/migrate/20170717084947_create_user_emails.rb index 3876893a69..e15a1f6aa4 100644 --- a/db/migrate/20170717084947_create_user_emails.rb +++ b/db/migrate/20170717084947_create_user_emails.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class CreateUserEmails < ActiveRecord::Migration[4.2] def up @@ -12,7 +12,7 @@ class CreateUserEmails < ActiveRecord::Migration[4.2] end add_index :user_emails, :user_id - add_index :user_emails, [:user_id, :primary], unique: true + add_index :user_emails, %i[user_id primary], unique: true execute "CREATE UNIQUE INDEX index_user_emails_on_email ON user_emails (lower(email));" diff --git a/db/migrate/20170818191909_split_alias_levels.rb b/db/migrate/20170818191909_split_alias_levels.rb index 68c6ef041c..e02c639df3 100644 --- a/db/migrate/20170818191909_split_alias_levels.rb +++ b/db/migrate/20170818191909_split_alias_levels.rb @@ -5,7 +5,7 @@ class SplitAliasLevels < ActiveRecord::Migration[4.2] add_column :groups, :messageable_level, :integer, default: 0 add_column :groups, :mentionable_level, :integer, default: 0 - execute 'UPDATE groups SET messageable_level = alias_level, mentionable_level = alias_level' + execute "UPDATE groups SET messageable_level = alias_level, mentionable_level = alias_level" end def down diff --git a/db/migrate/20170823173427_create_tag_search_data.rb b/db/migrate/20170823173427_create_tag_search_data.rb index 87db88bcfe..c43c4d8163 100644 --- a/db/migrate/20170823173427_create_tag_search_data.rb +++ b/db/migrate/20170823173427_create_tag_search_data.rb @@ -4,15 +4,15 @@ class CreateTagSearchData < ActiveRecord::Migration[4.2] def up create_table :tag_search_data, primary_key: :tag_id do |t| t.tsvector "search_data" - t.text "raw_data" - t.text "locale" - t.integer "version", default: 0 + t.text "raw_data" + t.text "locale" + t.integer "version", default: 0 end - execute 'create index idx_search_tag on tag_search_data using gin(search_data)' + execute "create index idx_search_tag on tag_search_data using gin(search_data)" end def down - execute 'drop index idx_search_tag' + execute "drop index idx_search_tag" drop_table :tag_search_data end end diff --git a/db/migrate/20170824172615_add_slug_index_on_topic.rb b/db/migrate/20170824172615_add_slug_index_on_topic.rb index f033b1e5ab..92592b69ca 100644 --- a/db/migrate/20170824172615_add_slug_index_on_topic.rb +++ b/db/migrate/20170824172615_add_slug_index_on_topic.rb @@ -2,10 +2,10 @@ class AddSlugIndexOnTopic < ActiveRecord::Migration[4.2] def up - execute 'CREATE INDEX idxTopicSlug ON topics(slug) WHERE deleted_at IS NULL AND slug IS NOT NULL' + execute "CREATE INDEX idxTopicSlug ON topics(slug) WHERE deleted_at IS NULL AND slug IS NOT NULL" end def down - execute 'DROP INDEX idxTopicSlug' + execute "DROP INDEX idxTopicSlug" end end diff --git a/db/migrate/20171110174413_rename_blocked_silence.rb b/db/migrate/20171110174413_rename_blocked_silence.rb index d118509655..7338259848 100644 --- a/db/migrate/20171110174413_rename_blocked_silence.rb +++ b/db/migrate/20171110174413_rename_blocked_silence.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RenameBlockedSilence < ActiveRecord::Migration[5.1] - def setting(old, new) execute "UPDATE site_settings SET name='#{new}' where name='#{old}'" end diff --git a/db/migrate/20171113214725_add_time_read_to_user_visits.rb b/db/migrate/20171113214725_add_time_read_to_user_visits.rb index ef3bd037bc..3359f2323d 100644 --- a/db/migrate/20171113214725_add_time_read_to_user_visits.rb +++ b/db/migrate/20171113214725_add_time_read_to_user_visits.rb @@ -3,11 +3,11 @@ class AddTimeReadToUserVisits < ActiveRecord::Migration[5.1] def up add_column :user_visits, :time_read, :integer, null: false, default: 0 # in seconds - add_index :user_visits, [:user_id, :visited_at, :time_read] + add_index :user_visits, %i[user_id visited_at time_read] end def down - remove_index :user_visits, [:user_id, :visited_at, :time_read] + remove_index :user_visits, %i[user_id visited_at time_read] remove_column :user_visits, :time_read end end diff --git a/db/migrate/20171220181249_change_user_emails_primary_index.rb b/db/migrate/20171220181249_change_user_emails_primary_index.rb index 2c1187f7da..dc9863e39e 100644 --- a/db/migrate/20171220181249_change_user_emails_primary_index.rb +++ b/db/migrate/20171220181249_change_user_emails_primary_index.rb @@ -2,12 +2,12 @@ class ChangeUserEmailsPrimaryIndex < ActiveRecord::Migration[5.1] def up - remove_index :user_emails, [:user_id, :primary] - add_index :user_emails, [:user_id, :primary], unique: true, where: '"primary"' + remove_index :user_emails, %i[user_id primary] + add_index :user_emails, %i[user_id primary], unique: true, where: '"primary"' end def down - remove_index :user_emails, [:user_id, :primary] - add_index :user_emails, [:user_id, :primary], unique: true + remove_index :user_emails, %i[user_id primary] + add_index :user_emails, %i[user_id primary], unique: true end end diff --git a/db/migrate/20180131052859_rename_private_personal.rb b/db/migrate/20180131052859_rename_private_personal.rb index d475f33a99..1952bf5854 100644 --- a/db/migrate/20180131052859_rename_private_personal.rb +++ b/db/migrate/20180131052859_rename_private_personal.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class RenamePrivatePersonal < ActiveRecord::Migration[5.1] - def setting(old, new) execute "UPDATE site_settings SET name='#{new}' where name='#{old}'" end diff --git a/db/migrate/20180207163946_create_category_tag_stats.rb b/db/migrate/20180207163946_create_category_tag_stats.rb index 7168250573..b5560a5831 100644 --- a/db/migrate/20180207163946_create_category_tag_stats.rb +++ b/db/migrate/20180207163946_create_category_tag_stats.rb @@ -8,7 +8,7 @@ class CreateCategoryTagStats < ActiveRecord::Migration[5.1] t.integer :topic_count, default: 0, null: false end - add_index :category_tag_stats, [:category_id, :topic_count] - add_index :category_tag_stats, [:category_id, :tag_id], unique: true + add_index :category_tag_stats, %i[category_id topic_count] + add_index :category_tag_stats, %i[category_id tag_id], unique: true end end diff --git a/db/migrate/20180308071922_drop_raise_read_only_function.rb b/db/migrate/20180308071922_drop_raise_read_only_function.rb index 1f303e7c15..39bdf14e47 100644 --- a/db/migrate/20180308071922_drop_raise_read_only_function.rb +++ b/db/migrate/20180308071922_drop_raise_read_only_function.rb @@ -2,9 +2,7 @@ class DropRaiseReadOnlyFunction < ActiveRecord::Migration[5.1] def up - DB.exec( - "DROP FUNCTION IF EXISTS raise_read_only() CASCADE;" - ) + DB.exec("DROP FUNCTION IF EXISTS raise_read_only() CASCADE;") end def down diff --git a/db/migrate/20180320190339_create_web_crawler_requests.rb b/db/migrate/20180320190339_create_web_crawler_requests.rb index 291e4fbf75..21b667e61d 100644 --- a/db/migrate/20180320190339_create_web_crawler_requests.rb +++ b/db/migrate/20180320190339_create_web_crawler_requests.rb @@ -8,6 +8,6 @@ class CreateWebCrawlerRequests < ActiveRecord::Migration[5.1] t.integer :count, null: false, default: 0 end - add_index :web_crawler_requests, [:date, :user_agent], unique: true + add_index :web_crawler_requests, %i[date user_agent], unique: true end end diff --git a/db/migrate/20180323154826_create_tag_group_permissions.rb b/db/migrate/20180323154826_create_tag_group_permissions.rb index 57bf1a018a..d12721c789 100644 --- a/db/migrate/20180323154826_create_tag_group_permissions.rb +++ b/db/migrate/20180323154826_create_tag_group_permissions.rb @@ -3,7 +3,7 @@ class CreateTagGroupPermissions < ActiveRecord::Migration[5.1] def change create_table :tag_group_permissions do |t| - t.references :tag_group, null: false + t.references :tag_group, null: false t.references :group, null: false t.integer :permission_type, default: 1, null: false t.timestamps null: false diff --git a/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb b/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb index fd7920b067..68b1483bce 100644 --- a/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb +++ b/db/migrate/20180327062911_add_post_custom_fields_akismet_index.rb @@ -7,8 +7,9 @@ class AddPostCustomFieldsAkismetIndex < ActiveRecord::Migration[5.1] def change - add_index :post_custom_fields, [:post_id], - name: 'idx_post_custom_fields_akismet', - where: "name = 'AKISMET_STATE' AND value = 'needs_review'" + add_index :post_custom_fields, + [:post_id], + name: "idx_post_custom_fields_akismet", + where: "name = 'AKISMET_STATE' AND value = 'needs_review'" end end diff --git a/db/migrate/20180420141134_remove_staff_tags_setting.rb b/db/migrate/20180420141134_remove_staff_tags_setting.rb index cf7300a204..eba9cc41aa 100644 --- a/db/migrate/20180420141134_remove_staff_tags_setting.rb +++ b/db/migrate/20180420141134_remove_staff_tags_setting.rb @@ -12,14 +12,15 @@ class RemoveStaffTagsSetting < ActiveRecord::Migration[5.1] result = execute("SELECT value FROM site_settings WHERE name = 'staff_tags'").to_a if result.length > 0 - if tags = result[0]['value']&.split('|') - tag_group = execute( - "INSERT INTO tag_groups (name, created_at, updated_at) + if tags = result[0]["value"]&.split("|") + tag_group = + execute( + "INSERT INTO tag_groups (name, created_at, updated_at) VALUES ('staff tags', now(), now()) - RETURNING id" - ) + RETURNING id", + ) - tag_group_id = tag_group[0]['id'] + tag_group_id = tag_group[0]["id"] execute( "INSERT INTO tag_group_permissions @@ -28,17 +29,17 @@ class RemoveStaffTagsSetting < ActiveRecord::Migration[5.1] (#{tag_group_id}, #{Group::AUTO_GROUPS[:everyone]}, #{TagGroupPermission.permission_types[:readonly]}, now(), now()), (#{tag_group_id}, #{Group::AUTO_GROUPS[:staff]}, - #{TagGroupPermission.permission_types[:full]}, now(), now())" + #{TagGroupPermission.permission_types[:full]}, now(), now())", ) tags.each do |tag_name| tag = execute("SELECT id FROM tags WHERE name = '#{tag_name}'").to_a - if tag[0] && tag[0]['id'] + if tag[0] && tag[0]["id"] execute( "INSERT INTO tag_group_memberships (tag_id, tag_group_id, created_at, updated_at) VALUES - (#{tag[0]['id']}, #{tag_group_id}, now(), now())" + (#{tag[0]["id"]}, #{tag_group_id}, now(), now())", ) end end diff --git a/db/migrate/20180521175611_change_indexes_topic_view_item.rb b/db/migrate/20180521175611_change_indexes_topic_view_item.rb index bd125ed15d..60d70a4576 100644 --- a/db/migrate/20180521175611_change_indexes_topic_view_item.rb +++ b/db/migrate/20180521175611_change_indexes_topic_view_item.rb @@ -3,14 +3,14 @@ class ChangeIndexesTopicViewItem < ActiveRecord::Migration[5.1] def up remove_index :topic_views, - column: [:ip_address, :topic_id], - name: :ip_address_topic_id_topic_views, - unique: true + column: %i[ip_address topic_id], + name: :ip_address_topic_id_topic_views, + unique: true remove_index :topic_views, - column: [:user_id, :topic_id], - name: :user_id_topic_id_topic_views, - unique: true + column: %i[user_id topic_id], + name: :user_id_topic_id_topic_views, + unique: true end def down diff --git a/db/migrate/20180521191418_allow_null_ip_user_profile_view.rb b/db/migrate/20180521191418_allow_null_ip_user_profile_view.rb index c7bdb05a16..a45e882ed3 100644 --- a/db/migrate/20180521191418_allow_null_ip_user_profile_view.rb +++ b/db/migrate/20180521191418_allow_null_ip_user_profile_view.rb @@ -10,16 +10,17 @@ class AllowNullIpUserProfileView < ActiveRecord::Migration[5.1] end remove_index :user_profile_views, - column: [:viewed_at, :ip_address, :user_profile_id], - name: :unique_profile_view_ip, - unique: true + column: %i[viewed_at ip_address user_profile_id], + name: :unique_profile_view_ip, + unique: true remove_index :user_profile_views, - column: [:viewed_at, :user_id, :user_profile_id], - name: :unique_profile_view_user, - unique: true - add_index :user_profile_views, [:viewed_at, :user_id, :ip_address, :user_profile_id], - name: :unique_profile_view_user_or_ip, - unique: true + column: %i[viewed_at user_id user_profile_id], + name: :unique_profile_view_user, + unique: true + add_index :user_profile_views, + %i[viewed_at user_id ip_address user_profile_id], + name: :unique_profile_view_user_or_ip, + unique: true end def down diff --git a/db/migrate/20180621013807_add_index_topic_id_percent_rank_on_posts.rb b/db/migrate/20180621013807_add_index_topic_id_percent_rank_on_posts.rb index aaacaa0846..a1467e15e9 100644 --- a/db/migrate/20180621013807_add_index_topic_id_percent_rank_on_posts.rb +++ b/db/migrate/20180621013807_add_index_topic_id_percent_rank_on_posts.rb @@ -2,10 +2,10 @@ class AddIndexTopicIdPercentRankOnPosts < ActiveRecord::Migration[5.2] def up - add_index :posts, [:topic_id, :percent_rank], order: { percent_rank: :asc } + add_index :posts, %i[topic_id percent_rank], order: { percent_rank: :asc } end def down - remove_index :posts, [:topic_id, :percent_rank] + remove_index :posts, %i[topic_id percent_rank] end end diff --git a/db/migrate/20180706054922_drop_key_column_from_themes.rb b/db/migrate/20180706054922_drop_key_column_from_themes.rb index 3df4da749d..8027a2d7bd 100644 --- a/db/migrate/20180706054922_drop_key_column_from_themes.rb +++ b/db/migrate/20180706054922_drop_key_column_from_themes.rb @@ -2,7 +2,7 @@ class DropKeyColumnFromThemes < ActiveRecord::Migration[5.2] def up - add_column :user_options, :theme_ids, :integer, array: true, null: false, default: [] + add_column :user_options, :theme_ids, :integer, array: true, null: false, default: [] execute( "UPDATE user_options AS uo @@ -12,14 +12,14 @@ class DropKeyColumnFromThemes < ActiveRecord::Migration[5.2] INNER JOIN user_options ON themes.key = user_options.theme_key WHERE user_options.user_id = uo.user_id - ) WHERE uo.theme_key IN (SELECT key FROM themes)" + ) WHERE uo.theme_key IN (SELECT key FROM themes)", ) execute( "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) SELECT 'default_theme_id', 3, id, now(), now() FROM themes - WHERE key = (SELECT value FROM site_settings WHERE name = 'default_theme_key')" + WHERE key = (SELECT value FROM site_settings WHERE name = 'default_theme_key')", ) execute("DELETE FROM site_settings WHERE name = 'default_theme_key'") diff --git a/db/migrate/20180710075119_add_index_topic_id_sort_order_on_posts.rb b/db/migrate/20180710075119_add_index_topic_id_sort_order_on_posts.rb index 90baa4d673..083d721580 100644 --- a/db/migrate/20180710075119_add_index_topic_id_sort_order_on_posts.rb +++ b/db/migrate/20180710075119_add_index_topic_id_sort_order_on_posts.rb @@ -2,6 +2,6 @@ class AddIndexTopicIdSortOrderOnPosts < ActiveRecord::Migration[5.2] def change - add_index :posts, [:topic_id, :sort_order], order: { sort_order: :asc } + add_index :posts, %i[topic_id sort_order], order: { sort_order: :asc } end end diff --git a/db/migrate/20180710172959_disallow_multi_levels_theme_components.rb b/db/migrate/20180710172959_disallow_multi_levels_theme_components.rb index ee672de5c0..99caa7e55e 100644 --- a/db/migrate/20180710172959_disallow_multi_levels_theme_components.rb +++ b/db/migrate/20180710172959_disallow_multi_levels_theme_components.rb @@ -3,34 +3,44 @@ class DisallowMultiLevelsThemeComponents < ActiveRecord::Migration[5.2] def up @handled = [] - top_parents = DB.query(" + top_parents = + DB.query( + " SELECT parent_theme_id, child_theme_id FROM child_themes WHERE parent_theme_id NOT IN (SELECT child_theme_id FROM child_themes) - ") + ", + ) - top_parents.each do |top_parent| - migrate_child(top_parent, top_parent) - end + top_parents.each { |top_parent| migrate_child(top_parent, top_parent) } if @handled.size > 0 - execute(" + execute( + " DELETE FROM child_themes WHERE parent_theme_id NOT IN (#{top_parents.map(&:parent_theme_id).join(", ")}) - ") + ", + ) end - execute(" + execute( + " UPDATE themes SET user_selectable = false FROM child_themes WHERE themes.id = child_themes.child_theme_id AND themes.user_selectable = true - ") + ", + ) - default = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_theme_id'").first + default = + DB.query_single("SELECT value FROM site_settings WHERE name = 'default_theme_id'").first if default - default_child = DB.query("SELECT 1 AS one FROM child_themes WHERE child_theme_id = ?", default.to_i).present? + default_child = + DB.query( + "SELECT 1 AS one FROM child_themes WHERE child_theme_id = ?", + default.to_i, + ).present? execute("DELETE FROM site_settings WHERE name = 'default_theme_id'") if default_child end end @@ -43,28 +53,39 @@ class DisallowMultiLevelsThemeComponents < ActiveRecord::Migration[5.2] def migrate_child(parent, top_parent) unless already_exists?(top_parent.parent_theme_id, parent.child_theme_id) - execute(" + execute( + " INSERT INTO child_themes (parent_theme_id, child_theme_id, created_at, updated_at) VALUES (#{top_parent.parent_theme_id}, #{parent.child_theme_id}, now(), now()) - ") + ", + ) end @handled << [top_parent.parent_theme_id, parent.parent_theme_id, parent.child_theme_id] - children = DB.query(" + children = + DB.query( + " SELECT parent_theme_id, child_theme_id FROM child_themes - WHERE parent_theme_id = :child", child: parent.child_theme_id - ) + WHERE parent_theme_id = :child", + child: parent.child_theme_id, + ) children.each do |child| - unless @handled.include?([top_parent.parent_theme_id, child.parent_theme_id, child.child_theme_id]) + unless @handled.include?( + [top_parent.parent_theme_id, child.parent_theme_id, child.child_theme_id], + ) migrate_child(child, top_parent) end end end def already_exists?(parent, child) - DB.query("SELECT 1 AS one FROM child_themes WHERE child_theme_id = :child AND parent_theme_id = :parent", child: child, parent: parent).present? + DB.query( + "SELECT 1 AS one FROM child_themes WHERE child_theme_id = :child AND parent_theme_id = :parent", + child: child, + parent: parent, + ).present? end end diff --git a/db/migrate/20180716072125_alter_bounce_key_on_email_logs.rb b/db/migrate/20180716072125_alter_bounce_key_on_email_logs.rb index 736c0074a5..8ebef173ee 100644 --- a/db/migrate/20180716072125_alter_bounce_key_on_email_logs.rb +++ b/db/migrate/20180716072125_alter_bounce_key_on_email_logs.rb @@ -2,7 +2,7 @@ class AlterBounceKeyOnEmailLogs < ActiveRecord::Migration[5.2] def up - change_column :email_logs, :bounce_key, 'uuid USING bounce_key::uuid' + change_column :email_logs, :bounce_key, "uuid USING bounce_key::uuid" end def down diff --git a/db/migrate/20180716140323_add_uniq_ip_or_user_id_topic_views.rb b/db/migrate/20180716140323_add_uniq_ip_or_user_id_topic_views.rb index 7bb452f01f..0462da362a 100644 --- a/db/migrate/20180716140323_add_uniq_ip_or_user_id_topic_views.rb +++ b/db/migrate/20180716140323_add_uniq_ip_or_user_id_topic_views.rb @@ -4,14 +4,16 @@ class AddUniqIpOrUserIdTopicViews < ActiveRecord::Migration[5.2] disable_ddl_transaction! def change - unless index_exists?(:topic_views, [:user_id, :ip_address, :topic_id], - name: :uniq_ip_or_user_id_topic_views - ) - - add_index :topic_views, [:user_id, :ip_address, :topic_id], - name: :uniq_ip_or_user_id_topic_views, - unique: true, - algorithm: :concurrently + unless index_exists?( + :topic_views, + %i[user_id ip_address topic_id], + name: :uniq_ip_or_user_id_topic_views, + ) + add_index :topic_views, + %i[user_id ip_address topic_id], + name: :uniq_ip_or_user_id_topic_views, + unique: true, + algorithm: :concurrently end end end diff --git a/db/migrate/20180717084758_alter_reply_key_on_email_logs.rb b/db/migrate/20180717084758_alter_reply_key_on_email_logs.rb index 26dd91944f..fe9ef02f65 100644 --- a/db/migrate/20180717084758_alter_reply_key_on_email_logs.rb +++ b/db/migrate/20180717084758_alter_reply_key_on_email_logs.rb @@ -2,7 +2,7 @@ class AlterReplyKeyOnEmailLogs < ActiveRecord::Migration[5.2] def up - change_column :email_logs, :reply_key, 'uuid USING reply_key::uuid' + change_column :email_logs, :reply_key, "uuid USING reply_key::uuid" end def down diff --git a/db/migrate/20180718062728_create_post_reply_keys.rb b/db/migrate/20180718062728_create_post_reply_keys.rb index ddc622175b..92e64e64f6 100644 --- a/db/migrate/20180718062728_create_post_reply_keys.rb +++ b/db/migrate/20180718062728_create_post_reply_keys.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class CreatePostReplyKeys < ActiveRecord::Migration[5.2] def up @@ -46,7 +46,7 @@ class CreatePostReplyKeys < ActiveRecord::Migration[5.2] execute(sql) - add_index :post_reply_keys, [:user_id, :post_id], unique: true + add_index :post_reply_keys, %i[user_id post_id], unique: true end def down diff --git a/db/migrate/20180719103905_alter_indexes_on_email_logs.rb b/db/migrate/20180719103905_alter_indexes_on_email_logs.rb index 4633de0dca..15a4f0fbe4 100644 --- a/db/migrate/20180719103905_alter_indexes_on_email_logs.rb +++ b/db/migrate/20180719103905_alter_indexes_on_email_logs.rb @@ -3,12 +3,12 @@ class AlterIndexesOnEmailLogs < ActiveRecord::Migration[5.2] def change remove_index :email_logs, - name: "index_email_logs_on_user_id_and_created_at", - column: [:user_id, :created_at] + name: "index_email_logs_on_user_id_and_created_at", + column: %i[user_id created_at] add_index :email_logs, :user_id - remove_index :email_logs, [:skipped, :created_at] - add_index :email_logs, [:skipped, :bounced, :created_at] + remove_index :email_logs, %i[skipped created_at] + add_index :email_logs, %i[skipped bounced created_at] end end diff --git a/db/migrate/20180720054856_create_skipped_email_logs.rb b/db/migrate/20180720054856_create_skipped_email_logs.rb index 78cfe6c77c..0b20e08504 100644 --- a/db/migrate/20180720054856_create_skipped_email_logs.rb +++ b/db/migrate/20180720054856_create_skipped_email_logs.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class CreateSkippedEmailLogs < ActiveRecord::Migration[5.2] def change @@ -44,6 +44,6 @@ class CreateSkippedEmailLogs < ActiveRecord::Migration[5.2] execute(sql) - Migration::ColumnDropper.mark_readonly('email_logs', 'skipped_reason') + Migration::ColumnDropper.mark_readonly("email_logs", "skipped_reason") end end diff --git a/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb b/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb index 9e95744180..c0983714c3 100644 --- a/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb +++ b/db/migrate/20180727042448_drop_reply_key_skipped_skipped_reason_from_email_logs.rb @@ -2,9 +2,9 @@ class DropReplyKeySkippedSkippedReasonFromEmailLogs < ActiveRecord::Migration[5.2] def up - remove_index :email_logs, [:skipped, :bounced, :created_at] - remove_index :email_logs, name: 'idx_email_logs_user_created_filtered' - add_index :email_logs, [:user_id, :created_at] + remove_index :email_logs, %i[skipped bounced created_at] + remove_index :email_logs, name: "idx_email_logs_user_created_filtered" + add_index :email_logs, %i[user_id created_at] end def down diff --git a/db/migrate/20180803085321_add_index_email_logs_on_bounced.rb b/db/migrate/20180803085321_add_index_email_logs_on_bounced.rb index d0b640a430..7fb3ed5251 100644 --- a/db/migrate/20180803085321_add_index_email_logs_on_bounced.rb +++ b/db/migrate/20180803085321_add_index_email_logs_on_bounced.rb @@ -3,6 +3,6 @@ class AddIndexEmailLogsOnBounced < ActiveRecord::Migration[5.2] def change add_index :email_logs, :bounced - remove_index :email_logs, [:user_id, :created_at] + remove_index :email_logs, %i[user_id created_at] end end diff --git a/db/migrate/20180812150839_add_user_api_keys_last_used_at.rb b/db/migrate/20180812150839_add_user_api_keys_last_used_at.rb index 09cd56971a..0203582f95 100644 --- a/db/migrate/20180812150839_add_user_api_keys_last_used_at.rb +++ b/db/migrate/20180812150839_add_user_api_keys_last_used_at.rb @@ -2,6 +2,10 @@ class AddUserApiKeysLastUsedAt < ActiveRecord::Migration[5.2] def change - add_column :user_api_keys, :last_used_at, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :user_api_keys, + :last_used_at, + :datetime, + null: false, + default: -> { "CURRENT_TIMESTAMP" } end end diff --git a/db/migrate/20180813074843_add_component_to_themes.rb b/db/migrate/20180813074843_add_component_to_themes.rb index 9bd08859c0..6f7a68104b 100644 --- a/db/migrate/20180813074843_add_component_to_themes.rb +++ b/db/migrate/20180813074843_add_component_to_themes.rb @@ -4,22 +4,28 @@ class AddComponentToThemes < ActiveRecord::Migration[5.2] def up add_column :themes, :component, :boolean, null: false, default: false - execute(" + execute( + " UPDATE themes SET component = true, color_scheme_id = NULL, user_selectable = false WHERE id IN (SELECT child_theme_id FROM child_themes) - ") + ", + ) - execute(" + execute( + " UPDATE site_settings SET value = -1 WHERE name = 'default_theme_id' AND value::integer IN (SELECT id FROM themes WHERE component) - ") + ", + ) - execute(" + execute( + " DELETE FROM child_themes WHERE parent_theme_id IN (SELECT id FROM themes WHERE component) - ") + ", + ) end def down diff --git a/db/migrate/20180917024729_remove_superfluous_columns.rb b/db/migrate/20180917024729_remove_superfluous_columns.rb index a2307345d0..cc2f87a31d 100644 --- a/db/migrate/20180917024729_remove_superfluous_columns.rb +++ b/db/migrate/20180917024729_remove_superfluous_columns.rb @@ -1,80 +1,63 @@ # frozen_string_literal: true -require 'migration/column_dropper' -require 'badge_posts_view_manager' +require "migration/column_dropper" +require "badge_posts_view_manager" class RemoveSuperfluousColumns < ActiveRecord::Migration[5.2] DROPPED_COLUMNS ||= { - user_profiles: %i{ - card_image_badge_id - }, - categories: %i{ - logo_url - background_url - suppress_from_homepage - }, - groups: %i{ - visible - public - alias_level - }, - theme_fields: %i{target}, - user_stats: %i{first_topic_unread_at}, - topics: %i{ - auto_close_at - auto_close_user_id - auto_close_started_at - auto_close_based_on_last_post - auto_close_hours - inappropriate_count - bookmark_count - off_topic_count - illegal_count - notify_user_count - last_unread_at - vote_count - }, - users: %i{ - email - email_always - mailing_list_mode - email_digests - email_direct - email_private_messages - external_links_in_new_tab - enable_quoting - dynamic_favicon - disable_jump_reply - edit_history_public - automatically_unpin_topics - digest_after_days - auto_track_topics_after_msecs - new_topic_duration_minutes - last_redirected_to_top_at - auth_token - auth_token_updated_at - blocked - silenced - trust_level_locked - }, - user_auth_tokens: %i{legacy}, - user_options: %i{theme_key}, - themes: %i{key}, - email_logs: %i{ - topic_id - reply_key - skipped - skipped_reason - }, - posts: %i{vote_count} + user_profiles: %i[card_image_badge_id], + categories: %i[logo_url background_url suppress_from_homepage], + groups: %i[visible public alias_level], + theme_fields: %i[target], + user_stats: %i[first_topic_unread_at], + topics: %i[ + auto_close_at + auto_close_user_id + auto_close_started_at + auto_close_based_on_last_post + auto_close_hours + inappropriate_count + bookmark_count + off_topic_count + illegal_count + notify_user_count + last_unread_at + vote_count + ], + users: %i[ + email + email_always + mailing_list_mode + email_digests + email_direct + email_private_messages + external_links_in_new_tab + enable_quoting + dynamic_favicon + disable_jump_reply + edit_history_public + automatically_unpin_topics + digest_after_days + auto_track_topics_after_msecs + new_topic_duration_minutes + last_redirected_to_top_at + auth_token + auth_token_updated_at + blocked + silenced + trust_level_locked + ], + user_auth_tokens: %i[legacy], + user_options: %i[theme_key], + themes: %i[key], + email_logs: %i[topic_id reply_key skipped skipped_reason], + posts: %i[vote_count], } def up BadgePostsViewManager.drop! - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } DB.exec "DROP FUNCTION IF EXISTS first_unread_topic_for(int)" diff --git a/db/migrate/20180917034056_remove_superfluous_tables.rb b/db/migrate/20180917034056_remove_superfluous_tables.rb index f652452683..5dfcb8ce95 100644 --- a/db/migrate/20180917034056_remove_superfluous_tables.rb +++ b/db/migrate/20180917034056_remove_superfluous_tables.rb @@ -1,18 +1,12 @@ # frozen_string_literal: true -require 'migration/table_dropper' +require "migration/table_dropper" class RemoveSuperfluousTables < ActiveRecord::Migration[5.2] - DROPPED_TABLES ||= %i{ - category_featured_users - versions - topic_status_updates - } + DROPPED_TABLES ||= %i[category_featured_users versions topic_status_updates] def up - DROPPED_TABLES.each do |table| - Migration::TableDropper.execute_drop(table) - end + DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) } end def down diff --git a/db/migrate/20180920042415_create_user_uploads.rb b/db/migrate/20180920042415_create_user_uploads.rb index 5e592d00fc..5481d67c41 100644 --- a/db/migrate/20180920042415_create_user_uploads.rb +++ b/db/migrate/20180920042415_create_user_uploads.rb @@ -8,7 +8,7 @@ class CreateUserUploads < ActiveRecord::Migration[5.2] t.datetime :created_at, null: false end - add_index :user_uploads, [:upload_id, :user_id], unique: true + add_index :user_uploads, %i[upload_id user_id], unique: true execute <<~SQL INSERT INTO user_uploads(upload_id, user_id, created_at) diff --git a/db/migrate/20180928105835_add_index_to_tags.rb b/db/migrate/20180928105835_add_index_to_tags.rb index b964a33095..54a3ab0702 100644 --- a/db/migrate/20180928105835_add_index_to_tags.rb +++ b/db/migrate/20180928105835_add_index_to_tags.rb @@ -11,7 +11,7 @@ class AddIndexToTags < ActiveRecord::Migration[5.2] WHERE EXISTS(SELECT * FROM tags t WHERE lower(t.name) = lower(tags.name) AND t.id < tags.id) SQL - add_index :tags, 'lower(name)', unique: true + add_index :tags, "lower(name)", unique: true end def down raise ActiveRecord::IrreversibleMigration diff --git a/db/migrate/20181012123001_drop_group_locked_trust_level_from_user.rb b/db/migrate/20181012123001_drop_group_locked_trust_level_from_user.rb index 3b3bd6eba3..5084d9c904 100644 --- a/db/migrate/20181012123001_drop_group_locked_trust_level_from_user.rb +++ b/db/migrate/20181012123001_drop_group_locked_trust_level_from_user.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class DropGroupLockedTrustLevelFromUser < ActiveRecord::Migration[5.2] - DROPPED_COLUMNS ||= { - posts: %i{group_locked_trust_level} - } + DROPPED_COLUMNS ||= { posts: %i[group_locked_trust_level] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20181031165343_add_flag_stats_to_user.rb b/db/migrate/20181031165343_add_flag_stats_to_user.rb index 22c5cb5f76..fa60e60955 100644 --- a/db/migrate/20181031165343_add_flag_stats_to_user.rb +++ b/db/migrate/20181031165343_add_flag_stats_to_user.rb @@ -18,7 +18,7 @@ class AddFlagStatsToUser < ActiveRecord::Migration[5.2] SUM(CASE WHEN pa.deferred_at IS NOT NULL THEN 1 ELSE 0 END) as flags_ignored FROM post_actions AS pa INNER JOIN users AS u ON u.id = pa.user_id - WHERE pa.post_action_type_id IN (#{PostActionType.notify_flag_types.values.join(', ')}) + WHERE pa.post_action_type_id IN (#{PostActionType.notify_flag_types.values.join(", ")}) AND pa.user_id > 0 GROUP BY u.id ) AS x diff --git a/db/migrate/20181108115009_create_user_associated_accounts.rb b/db/migrate/20181108115009_create_user_associated_accounts.rb index 6dd4e00d80..5057096ff8 100644 --- a/db/migrate/20181108115009_create_user_associated_accounts.rb +++ b/db/migrate/20181108115009_create_user_associated_accounts.rb @@ -14,7 +14,13 @@ class CreateUserAssociatedAccounts < ActiveRecord::Migration[5.2] t.timestamps end - add_index :user_associated_accounts, [:provider_name, :provider_uid], unique: true, name: 'associated_accounts_provider_uid' - add_index :user_associated_accounts, [:provider_name, :user_id], unique: true, name: 'associated_accounts_provider_user' + add_index :user_associated_accounts, + %i[provider_name provider_uid], + unique: true, + name: "associated_accounts_provider_uid" + add_index :user_associated_accounts, + %i[provider_name user_id], + unique: true, + name: "associated_accounts_provider_user" end end diff --git a/db/migrate/20181204193426_create_join_table_web_hooks_tags.rb b/db/migrate/20181204193426_create_join_table_web_hooks_tags.rb index c6c34b6d63..89fc78c644 100644 --- a/db/migrate/20181204193426_create_join_table_web_hooks_tags.rb +++ b/db/migrate/20181204193426_create_join_table_web_hooks_tags.rb @@ -3,7 +3,7 @@ class CreateJoinTableWebHooksTags < ActiveRecord::Migration[5.2] def change create_join_table :web_hooks, :tags do |t| - t.index [:web_hook_id, :tag_id], name: 'web_hooks_tags', unique: true + t.index %i[web_hook_id tag_id], name: "web_hooks_tags", unique: true end end end diff --git a/db/migrate/20181220115844_add_smtp_and_imap_to_groups.rb b/db/migrate/20181220115844_add_smtp_and_imap_to_groups.rb index 42f79a4b52..7c4cdf4f74 100644 --- a/db/migrate/20181220115844_add_smtp_and_imap_to_groups.rb +++ b/db/migrate/20181220115844_add_smtp_and_imap_to_groups.rb @@ -10,7 +10,7 @@ class AddSmtpAndImapToGroups < ActiveRecord::Migration[5.2] add_column :groups, :imap_port, :integer add_column :groups, :imap_ssl, :boolean - add_column :groups, :imap_mailbox_name, :string, default: '', null: false + add_column :groups, :imap_mailbox_name, :string, default: "", null: false add_column :groups, :imap_uid_validity, :integer, default: 0, null: false add_column :groups, :imap_last_uid, :integer, default: 0, null: false diff --git a/db/migrate/20181221121805_create_theme_translation_override.rb b/db/migrate/20181221121805_create_theme_translation_override.rb index bcf4190172..65ca084c99 100644 --- a/db/migrate/20181221121805_create_theme_translation_override.rb +++ b/db/migrate/20181221121805_create_theme_translation_override.rb @@ -10,7 +10,9 @@ class CreateThemeTranslationOverride < ActiveRecord::Migration[5.2] t.timestamps null: false t.index :theme_id - t.index [:theme_id, :locale, :translation_key], unique: true, name: 'theme_translation_overrides_unique' + t.index %i[theme_id locale translation_key], + unique: true, + name: "theme_translation_overrides_unique" end end end diff --git a/db/migrate/20190103060819_force_rebake_on_posts_with_images.rb b/db/migrate/20190103060819_force_rebake_on_posts_with_images.rb index 535ea684a6..7d9f622b0f 100644 --- a/db/migrate/20190103060819_force_rebake_on_posts_with_images.rb +++ b/db/migrate/20190103060819_force_rebake_on_posts_with_images.rb @@ -2,7 +2,6 @@ class ForceRebakeOnPostsWithImages < ActiveRecord::Migration[5.2] def up - # commit message has more info: # Picking up changes with pngquant, placeholder image, new image magick, retina images diff --git a/db/migrate/20190103065652_remove_uploaded_meta_id_from_category.rb b/db/migrate/20190103065652_remove_uploaded_meta_id_from_category.rb index 9aba22fa06..24c4501dff 100644 --- a/db/migrate/20190103065652_remove_uploaded_meta_id_from_category.rb +++ b/db/migrate/20190103065652_remove_uploaded_meta_id_from_category.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class RemoveUploadedMetaIdFromCategory < ActiveRecord::Migration[5.2] - DROPPED_COLUMNS ||= { - categories: %i{uploaded_meta_id} - } + DROPPED_COLUMNS ||= { categories: %i[uploaded_meta_id] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20190103160533_create_reviewables.rb b/db/migrate/20190103160533_create_reviewables.rb index 79d13f889e..564446abc4 100644 --- a/db/migrate/20190103160533_create_reviewables.rb +++ b/db/migrate/20190103160533_create_reviewables.rb @@ -36,8 +36,8 @@ class CreateReviewables < ActiveRecord::Migration[5.2] end add_index :reviewables, :status - add_index :reviewables, [:status, :type] - add_index :reviewables, [:status, :score] - add_index :reviewables, [:type, :target_id], unique: true + add_index :reviewables, %i[status type] + add_index :reviewables, %i[status score] + add_index :reviewables, %i[type target_id], unique: true end end diff --git a/db/migrate/20190103185626_create_reviewable_users.rb b/db/migrate/20190103185626_create_reviewable_users.rb index e1306be9fd..574f47fcfc 100644 --- a/db/migrate/20190103185626_create_reviewable_users.rb +++ b/db/migrate/20190103185626_create_reviewable_users.rb @@ -3,7 +3,9 @@ class CreateReviewableUsers < ActiveRecord::Migration[5.2] def up # Create reviewables for approved users - if DB.query_single("SELECT 1 FROM site_settings WHERE name = 'must_approve_users' AND value = 't'").first + if DB.query_single( + "SELECT 1 FROM site_settings WHERE name = 'must_approve_users' AND value = 't'", + ).first execute(<<~SQL) INSERT INTO reviewables ( type, diff --git a/db/migrate/20190106041015_add_topic_id_index_to_user_histories.rb b/db/migrate/20190106041015_add_topic_id_index_to_user_histories.rb index d3b6d6366d..4b9f435a12 100644 --- a/db/migrate/20190106041015_add_topic_id_index_to_user_histories.rb +++ b/db/migrate/20190106041015_add_topic_id_index_to_user_histories.rb @@ -2,6 +2,6 @@ class AddTopicIdIndexToUserHistories < ActiveRecord::Migration[5.2] def change - add_index :user_histories, [:topic_id, :target_user_id, :action] + add_index :user_histories, %i[topic_id target_user_id action] end end diff --git a/db/migrate/20190110212005_create_reviewable_histories.rb b/db/migrate/20190110212005_create_reviewable_histories.rb index 6bf9ae5b32..02663d0969 100644 --- a/db/migrate/20190110212005_create_reviewable_histories.rb +++ b/db/migrate/20190110212005_create_reviewable_histories.rb @@ -7,7 +7,7 @@ class CreateReviewableHistories < ActiveRecord::Migration[5.2] t.integer :reviewable_history_type, null: false t.integer :status, null: false t.integer :created_by_id, null: false - t.json :edited, null: true + t.json :edited, null: true t.timestamps end diff --git a/db/migrate/20190130163001_migrate_reviewable_flagged_posts.rb b/db/migrate/20190130163001_migrate_reviewable_flagged_posts.rb index a2c1866869..286a77d693 100644 --- a/db/migrate/20190130163001_migrate_reviewable_flagged_posts.rb +++ b/db/migrate/20190130163001_migrate_reviewable_flagged_posts.rb @@ -2,7 +2,6 @@ class MigrateReviewableFlaggedPosts < ActiveRecord::Migration[5.2] def up - # for the migration we'll do 1.0 + trust_level and not take into account user flagging accuracy # It should be good enough for old flags whose scores are not as important as pending flags. execute(<<~SQL) diff --git a/db/migrate/20190205104116_drop_unused_auth_tables.rb b/db/migrate/20190205104116_drop_unused_auth_tables.rb index 99fa6f7585..e21ea59bfc 100644 --- a/db/migrate/20190205104116_drop_unused_auth_tables.rb +++ b/db/migrate/20190205104116_drop_unused_auth_tables.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/table_dropper' +require "migration/table_dropper" class DropUnusedAuthTables < ActiveRecord::Migration[5.2] def change diff --git a/db/migrate/20190208144706_drop_unused_auth_tables_again.rb b/db/migrate/20190208144706_drop_unused_auth_tables_again.rb index 6fb208c6c8..fc910b24d9 100644 --- a/db/migrate/20190208144706_drop_unused_auth_tables_again.rb +++ b/db/migrate/20190208144706_drop_unused_auth_tables_again.rb @@ -1,17 +1,12 @@ # frozen_string_literal: true -require 'migration/table_dropper' +require "migration/table_dropper" class DropUnusedAuthTablesAgain < ActiveRecord::Migration[5.2] - DROPPED_TABLES ||= %i{ - facebook_user_infos - twitter_user_infos - } + DROPPED_TABLES ||= %i[facebook_user_infos twitter_user_infos] def up - DROPPED_TABLES.each do |table| - Migration::TableDropper.execute_drop(table) - end + DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) } end def down diff --git a/db/migrate/20190225133654_add_ignored_users_table.rb b/db/migrate/20190225133654_add_ignored_users_table.rb index d2f164a984..15a440c191 100644 --- a/db/migrate/20190225133654_add_ignored_users_table.rb +++ b/db/migrate/20190225133654_add_ignored_users_table.rb @@ -8,7 +8,7 @@ class AddIgnoredUsersTable < ActiveRecord::Migration[5.2] t.timestamps null: false end - add_index :ignored_users, [:user_id, :ignored_user_id], unique: true - add_index :ignored_users, [:ignored_user_id, :user_id], unique: true + add_index :ignored_users, %i[user_id ignored_user_id], unique: true + add_index :ignored_users, %i[ignored_user_id user_id], unique: true end end diff --git a/db/migrate/20190312194528_drop_email_user_options_columns.rb b/db/migrate/20190312194528_drop_email_user_options_columns.rb index 1b4203b175..1367ba3871 100644 --- a/db/migrate/20190312194528_drop_email_user_options_columns.rb +++ b/db/migrate/20190312194528_drop_email_user_options_columns.rb @@ -1,20 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class DropEmailUserOptionsColumns < ActiveRecord::Migration[5.2] - DROPPED_COLUMNS ||= { - user_options: %i{ - email_direct - email_private_messages - email_always - }, - } + DROPPED_COLUMNS ||= { user_options: %i[email_direct email_private_messages email_always] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20190313134642_migrate_default_user_email_options.rb b/db/migrate/20190313134642_migrate_default_user_email_options.rb index 961789b92a..3048456aed 100644 --- a/db/migrate/20190313134642_migrate_default_user_email_options.rb +++ b/db/migrate/20190313134642_migrate_default_user_email_options.rb @@ -5,13 +5,18 @@ class MigrateDefaultUserEmailOptions < ActiveRecord::Migration[5.2] # see UserOption.email_level_types # always = 0, only_while_away: 1, never: 2 - email_always = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_email_always'").first - email_direct = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_email_direct'").first - email_personal_messages = DB.query_single("SELECT value FROM site_settings WHERE name = 'default_email_personal_messages'").first + email_always = + DB.query_single("SELECT value FROM site_settings WHERE name = 'default_email_always'").first + email_direct = + DB.query_single("SELECT value FROM site_settings WHERE name = 'default_email_direct'").first + email_personal_messages = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'default_email_personal_messages'", + ).first default_email_level = nil - default_email_level = 0 if email_direct != 'f' && email_always == 't' - default_email_level = 2 if email_direct == 'f' + default_email_level = 0 if email_direct != "f" && email_always == "t" + default_email_level = 2 if email_direct == "f" unless default_email_level.nil? execute "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) @@ -19,8 +24,8 @@ class MigrateDefaultUserEmailOptions < ActiveRecord::Migration[5.2] end default_email_messages_level = nil - default_email_messages_level = 0 if email_personal_messages != 'f' && email_always == 't' - default_email_messages_level = 2 if email_personal_messages == 'f' + default_email_messages_level = 0 if email_personal_messages != "f" && email_always == "t" + default_email_messages_level = 2 if email_personal_messages == "f" unless default_email_messages_level.nil? execute "INSERT INTO site_settings (name, data_type, value, created_at, updated_at) diff --git a/db/migrate/20190313171338_add_indexes_to_reviewables.rb b/db/migrate/20190313171338_add_indexes_to_reviewables.rb index 245ad2a916..1ad7c2cbbb 100644 --- a/db/migrate/20190313171338_add_indexes_to_reviewables.rb +++ b/db/migrate/20190313171338_add_indexes_to_reviewables.rb @@ -3,11 +3,11 @@ class AddIndexesToReviewables < ActiveRecord::Migration[5.2] def up remove_index :reviewables, :status - add_index :reviewables, [:status, :created_at] + add_index :reviewables, %i[status created_at] end def down - remove_index :reviewables, [:status, :created_at] + remove_index :reviewables, %i[status created_at] add_index :reviewables, :status end end diff --git a/db/migrate/20190315174428_migrate_flag_history.rb b/db/migrate/20190315174428_migrate_flag_history.rb index dbeefd3c11..4fa90d30e5 100644 --- a/db/migrate/20190315174428_migrate_flag_history.rb +++ b/db/migrate/20190315174428_migrate_flag_history.rb @@ -2,7 +2,6 @@ class MigrateFlagHistory < ActiveRecord::Migration[5.2] def up - # Migrate Created History execute(<<~SQL) INSERT INTO reviewable_histories ( diff --git a/db/migrate/20190320091323_add_index_post_action_type_id_disagreed_at_on_post_actions.rb b/db/migrate/20190320091323_add_index_post_action_type_id_disagreed_at_on_post_actions.rb index 6e4344236f..53606a1d0a 100644 --- a/db/migrate/20190320091323_add_index_post_action_type_id_disagreed_at_on_post_actions.rb +++ b/db/migrate/20190320091323_add_index_post_action_type_id_disagreed_at_on_post_actions.rb @@ -2,7 +2,6 @@ class AddIndexPostActionTypeIdDisagreedAtOnPostActions < ActiveRecord::Migration[5.2] def change - add_index :post_actions, [:post_action_type_id, :disagreed_at], - where: "(disagreed_at IS NULL)" + add_index :post_actions, %i[post_action_type_id disagreed_at], where: "(disagreed_at IS NULL)" end end diff --git a/db/migrate/20190321072029_add_index_method_enabled_on_user_second_factors.rb b/db/migrate/20190321072029_add_index_method_enabled_on_user_second_factors.rb index 2f915d0c10..1afb271a22 100644 --- a/db/migrate/20190321072029_add_index_method_enabled_on_user_second_factors.rb +++ b/db/migrate/20190321072029_add_index_method_enabled_on_user_second_factors.rb @@ -2,6 +2,6 @@ class AddIndexMethodEnabledOnUserSecondFactors < ActiveRecord::Migration[5.2] def change - add_index :user_second_factors, [:method, :enabled] + add_index :user_second_factors, %i[method enabled] end end diff --git a/db/migrate/20190327205525_require_reviewable_scores.rb b/db/migrate/20190327205525_require_reviewable_scores.rb index 916aed0939..b3cb534800 100644 --- a/db/migrate/20190327205525_require_reviewable_scores.rb +++ b/db/migrate/20190327205525_require_reviewable_scores.rb @@ -2,7 +2,12 @@ class RequireReviewableScores < ActiveRecord::Migration[5.2] def up - min_score = DB.query_single("SELECT value FROM site_settings WHERE name = 'min_score_default_visibility'")[0].to_f + min_score = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'min_score_default_visibility'", + )[ + 0 + ].to_f min_score = 1.0 if (min_score < 1.0) execute(<<~SQL) diff --git a/db/migrate/20190402024053_add_first_unread_at_to_user_stats.rb b/db/migrate/20190402024053_add_first_unread_at_to_user_stats.rb index df0631443d..adecf40d15 100644 --- a/db/migrate/20190402024053_add_first_unread_at_to_user_stats.rb +++ b/db/migrate/20190402024053_add_first_unread_at_to_user_stats.rb @@ -6,7 +6,11 @@ class AddFirstUnreadAtToUserStats < ActiveRecord::Migration[5.2] def up # so we can rerun this if the index creation fails out of ddl if !column_exists?(:user_stats, :first_unread_at) - add_column :user_stats, :first_unread_at, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :user_stats, + :first_unread_at, + :datetime, + null: false, + default: -> { "CURRENT_TIMESTAMP" } end execute <<~SQL @@ -19,19 +23,35 @@ class AddFirstUnreadAtToUserStats < ActiveRecord::Migration[5.2] # since DDL transactions are disabled we got to check # this could potentially fail half way and we want it to recover if !index_exists?( - :topics, - # the big list of columns here is not really needed, but ... why not - [:updated_at, :visible, :highest_staff_post_number, :highest_post_number, :category_id, :created_at, :id], - name: 'index_topics_on_updated_at_public' - ) + :topics, + # the big list of columns here is not really needed, but ... why not + %i[ + updated_at + visible + highest_staff_post_number + highest_post_number + category_id + created_at + id + ], + name: "index_topics_on_updated_at_public", + ) # this is quite a big index to carry, but we need it to optimise home page initial load # by covering all these columns we are able to quickly retrieve the set of topics that were # updated in the last N days. We perform a ranged lookup and selectivity may vary a lot add_index :topics, - [:updated_at, :visible, :highest_staff_post_number, :highest_post_number, :category_id, :created_at, :id], - algorithm: :concurrently, - name: 'index_topics_on_updated_at_public', - where: "(topics.archetype <> 'private_message') AND (topics.deleted_at IS NULL)" + %i[ + updated_at + visible + highest_staff_post_number + highest_post_number + category_id + created_at + id + ], + algorithm: :concurrently, + name: "index_topics_on_updated_at_public", + where: "(topics.archetype <> 'private_message') AND (topics.deleted_at IS NULL)" end end diff --git a/db/migrate/20190405044140_add_index_action_type_created_at_on_user_actions.rb b/db/migrate/20190405044140_add_index_action_type_created_at_on_user_actions.rb index c04f43dbec..d0ac774de7 100644 --- a/db/migrate/20190405044140_add_index_action_type_created_at_on_user_actions.rb +++ b/db/migrate/20190405044140_add_index_action_type_created_at_on_user_actions.rb @@ -2,6 +2,6 @@ class AddIndexActionTypeCreatedAtOnUserActions < ActiveRecord::Migration[5.2] def change - add_index :user_actions, [:action_type, :created_at] + add_index :user_actions, %i[action_type created_at] end end diff --git a/db/migrate/20190408072550_add_index_id_baked_version_on_posts.rb b/db/migrate/20190408072550_add_index_id_baked_version_on_posts.rb index 6035119b47..08d22cbf68 100644 --- a/db/migrate/20190408072550_add_index_id_baked_version_on_posts.rb +++ b/db/migrate/20190408072550_add_index_id_baked_version_on_posts.rb @@ -2,8 +2,6 @@ class AddIndexIdBakedVersionOnPosts < ActiveRecord::Migration[5.2] def change - add_index :posts, [:id, :baked_version], - order: { id: :desc }, - where: "(deleted_at IS NULL)" + add_index :posts, %i[id baked_version], order: { id: :desc }, where: "(deleted_at IS NULL)" end end diff --git a/db/migrate/20190408082101_add_search_data_indexes.rb b/db/migrate/20190408082101_add_search_data_indexes.rb index bb20a59530..cadc7b0845 100644 --- a/db/migrate/20190408082101_add_search_data_indexes.rb +++ b/db/migrate/20190408082101_add_search_data_indexes.rb @@ -2,7 +2,7 @@ class AddSearchDataIndexes < ActiveRecord::Migration[5.2] def change - add_index :topic_search_data, [:topic_id, :version, :locale] - add_index :post_search_data, [:post_id, :version, :locale] + add_index :topic_search_data, %i[topic_id version locale] + add_index :post_search_data, %i[post_id version locale] end end diff --git a/db/migrate/20190409054736_add_index_for_rebake_old_on_posts.rb b/db/migrate/20190409054736_add_index_for_rebake_old_on_posts.rb index 900ea7de38..76b84ff84b 100644 --- a/db/migrate/20190409054736_add_index_for_rebake_old_on_posts.rb +++ b/db/migrate/20190409054736_add_index_for_rebake_old_on_posts.rb @@ -9,11 +9,14 @@ class AddIndexForRebakeOldOnPosts < ActiveRecord::Migration[5.2] end if !index_exists?(:posts, :index_for_rebake_old) - add_index :posts, :id, - order: { id: :desc }, - where: "(baked_version IS NULL OR baked_version < 2) AND deleted_at IS NULL", - name: :index_for_rebake_old, - algorithm: :concurrently + add_index :posts, + :id, + order: { + id: :desc, + }, + where: "(baked_version IS NULL OR baked_version < 2) AND deleted_at IS NULL", + name: :index_for_rebake_old, + algorithm: :concurrently end end diff --git a/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb b/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb index 02187168b8..27b0c86df7 100644 --- a/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb +++ b/db/migrate/20190412161430_add_created_by_index_to_reviewables.rb @@ -2,6 +2,6 @@ class AddCreatedByIndexToReviewables < ActiveRecord::Migration[5.2] def change - add_index :reviewables, [:topic_id, :status, :created_by_id] + add_index :reviewables, %i[topic_id status created_by_id] end end diff --git a/db/migrate/20190414162753_rename_post_notices.rb b/db/migrate/20190414162753_rename_post_notices.rb index 997fb5f907..098de36045 100644 --- a/db/migrate/20190414162753_rename_post_notices.rb +++ b/db/migrate/20190414162753_rename_post_notices.rb @@ -2,8 +2,16 @@ class RenamePostNotices < ActiveRecord::Migration[5.2] def up - add_index :post_custom_fields, :post_id, unique: true, name: "index_post_custom_fields_on_notice_type", where: "name = 'notice_type'" - add_index :post_custom_fields, :post_id, unique: true, name: "index_post_custom_fields_on_notice_args", where: "name = 'notice_args'" + add_index :post_custom_fields, + :post_id, + unique: true, + name: "index_post_custom_fields_on_notice_type", + where: "name = 'notice_type'" + add_index :post_custom_fields, + :post_id, + unique: true, + name: "index_post_custom_fields_on_notice_args", + where: "name = 'notice_args'" # Split site setting `min_post_notice_tl` into `new_user_notice_tl` and `returning_user_notice_tl`. execute <<~SQL diff --git a/db/migrate/20190418113814_add_unique_index_to_group_requests.rb b/db/migrate/20190418113814_add_unique_index_to_group_requests.rb index 54b0e85c66..cc266dad40 100644 --- a/db/migrate/20190418113814_add_unique_index_to_group_requests.rb +++ b/db/migrate/20190418113814_add_unique_index_to_group_requests.rb @@ -3,6 +3,6 @@ class AddUniqueIndexToGroupRequests < ActiveRecord::Migration[5.2] def change execute "DELETE FROM group_requests WHERE id NOT IN (SELECT MIN(id) FROM group_requests GROUP BY group_id, user_id)" - add_index :group_requests, [:group_id, :user_id], unique: true + add_index :group_requests, %i[group_id user_id], unique: true end end diff --git a/db/migrate/20190424065841_add_post_image_indexes.rb b/db/migrate/20190424065841_add_post_image_indexes.rb index 496358bd45..25027b8bde 100644 --- a/db/migrate/20190424065841_add_post_image_indexes.rb +++ b/db/migrate/20190424065841_add_post_image_indexes.rb @@ -2,13 +2,7 @@ class AddPostImageIndexes < ActiveRecord::Migration[5.2] def change - - %w{ - large_images - broken_images - downloaded_images - }.each do |field| - + %w[large_images broken_images downloaded_images].each do |field| execute <<~SQL DELETE FROM post_custom_fields f WHERE name = '#{field}' AND id > ( @@ -17,10 +11,11 @@ class AddPostImageIndexes < ActiveRecord::Migration[5.2] ) SQL - add_index :post_custom_fields, [:post_id], - name: "post_custom_field_#{field}_idx", - unique: true, - where: "name = '#{field}'" + add_index :post_custom_fields, + [:post_id], + name: "post_custom_field_#{field}_idx", + unique: true, + where: "name = '#{field}'" end end end diff --git a/db/migrate/20190426011148_add_upload_foreign_keys_to_user_profiles.rb b/db/migrate/20190426011148_add_upload_foreign_keys_to_user_profiles.rb index c0918769b2..8ef54c9190 100644 --- a/db/migrate/20190426011148_add_upload_foreign_keys_to_user_profiles.rb +++ b/db/migrate/20190426011148_add_upload_foreign_keys_to_user_profiles.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class AddUploadForeignKeysToUserProfiles < ActiveRecord::Migration[5.2] def up - %i{profile_background card_background}.each do |column| + %i[profile_background card_background].each do |column| Migration::ColumnDropper.mark_readonly(:user_profiles, column) end diff --git a/db/migrate/20190426074404_add_missing_user_destroyer_indexes.rb b/db/migrate/20190426074404_add_missing_user_destroyer_indexes.rb index 9ef1e93ae3..91430e287a 100644 --- a/db/migrate/20190426074404_add_missing_user_destroyer_indexes.rb +++ b/db/migrate/20190426074404_add_missing_user_destroyer_indexes.rb @@ -3,9 +3,9 @@ class AddMissingUserDestroyerIndexes < ActiveRecord::Migration[5.2] def change # these indexes are required to make deletions of users fast - add_index :user_actions, [:target_user_id], where: 'target_user_id IS NOT NULL' + add_index :user_actions, [:target_user_id], where: "target_user_id IS NOT NULL" add_index :post_actions, [:user_id] - add_index :user_uploads, [:user_id, :upload_id] + add_index :user_uploads, %i[user_id upload_id] add_index :user_auth_token_logs, [:user_id] add_index :topic_links, [:user_id] end diff --git a/db/migrate/20190426123026_add_incoming_email_by_user_id_index.rb b/db/migrate/20190426123026_add_incoming_email_by_user_id_index.rb index ecbe8381a9..dd886d8b63 100644 --- a/db/migrate/20190426123026_add_incoming_email_by_user_id_index.rb +++ b/db/migrate/20190426123026_add_incoming_email_by_user_id_index.rb @@ -2,6 +2,6 @@ class AddIncomingEmailByUserIdIndex < ActiveRecord::Migration[5.2] def change - add_index :incoming_emails, [:user_id], where: 'user_id IS NOT NULL' + add_index :incoming_emails, [:user_id], where: "user_id IS NOT NULL" end end diff --git a/db/migrate/20190502223613_add_bounce_key_index_on_email_logs.rb b/db/migrate/20190502223613_add_bounce_key_index_on_email_logs.rb index 83ccf913d2..8e948a5edb 100644 --- a/db/migrate/20190502223613_add_bounce_key_index_on_email_logs.rb +++ b/db/migrate/20190502223613_add_bounce_key_index_on_email_logs.rb @@ -2,6 +2,6 @@ class AddBounceKeyIndexOnEmailLogs < ActiveRecord::Migration[5.2] def change - add_index :email_logs, [:bounce_key], unique: true, where: 'bounce_key IS NOT NULL' + add_index :email_logs, [:bounce_key], unique: true, where: "bounce_key IS NOT NULL" end end diff --git a/db/migrate/20190508193900_add_missing_uploads_ignored_index_to_post_custom_fields.rb b/db/migrate/20190508193900_add_missing_uploads_ignored_index_to_post_custom_fields.rb index 1f6978225c..e9a03468f0 100644 --- a/db/migrate/20190508193900_add_missing_uploads_ignored_index_to_post_custom_fields.rb +++ b/db/migrate/20190508193900_add_missing_uploads_ignored_index_to_post_custom_fields.rb @@ -2,6 +2,10 @@ class AddMissingUploadsIgnoredIndexToPostCustomFields < ActiveRecord::Migration[5.2] def change - add_index :post_custom_fields, :post_id, unique: true, where: "name = 'missing uploads ignored'", name: "index_post_id_where_missing_uploads_ignored" + add_index :post_custom_fields, + :post_id, + unique: true, + where: "name = 'missing uploads ignored'", + name: "index_post_id_where_missing_uploads_ignored" end end diff --git a/db/migrate/20190529002752_add_unique_constraint_to_shadow_accounts.rb b/db/migrate/20190529002752_add_unique_constraint_to_shadow_accounts.rb index 78a8a3f4f7..d87c25696f 100644 --- a/db/migrate/20190529002752_add_unique_constraint_to_shadow_accounts.rb +++ b/db/migrate/20190529002752_add_unique_constraint_to_shadow_accounts.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AddUniqueConstraintToShadowAccounts < ActiveRecord::Migration[5.2] - def up create_table :anonymous_users do |t| t.integer :user_id, null: false @@ -10,7 +9,7 @@ class AddUniqueConstraintToShadowAccounts < ActiveRecord::Migration[5.2] t.timestamps t.index [:user_id], unique: true - t.index [:master_user_id], unique: true, where: 'active' + t.index [:master_user_id], unique: true, where: "active" end rows = DB.exec <<~SQL @@ -24,9 +23,7 @@ class AddUniqueConstraintToShadowAccounts < ActiveRecord::Migration[5.2] ) SQL - if rows > 0 - STDERR.puts "Removed #{rows} duplicate shadow users" - end + STDERR.puts "Removed #{rows} duplicate shadow users" if rows > 0 rows = DB.exec <<~SQL INSERT INTO anonymous_users(user_id, master_user_id, created_at, updated_at, active) @@ -43,9 +40,7 @@ class AddUniqueConstraintToShadowAccounts < ActiveRecord::Migration[5.2] WHERE name = 'master_id' AND a.user_id IS NULL SQL - if rows > 0 - STDERR.puts "Migrated #{rows} anon users to new structure" - end + STDERR.puts "Migrated #{rows} anon users to new structure" if rows > 0 DB.exec <<~SQL DELETE FROM user_custom_fields diff --git a/db/migrate/20190621095105_remove_notification_level_from_category_user_indexes.rb b/db/migrate/20190621095105_remove_notification_level_from_category_user_indexes.rb index 587e2cf4f9..81bb15edd1 100644 --- a/db/migrate/20190621095105_remove_notification_level_from_category_user_indexes.rb +++ b/db/migrate/20190621095105_remove_notification_level_from_category_user_indexes.rb @@ -9,22 +9,30 @@ DELETE FROM category_users cu USING category_users cu1 cu.notification_level < cu1.notification_level SQL - remove_index :category_users, name: 'idx_category_users_u1' - remove_index :category_users, name: 'idx_category_users_u2' + remove_index :category_users, name: "idx_category_users_u1" + remove_index :category_users, name: "idx_category_users_u2" - add_index :category_users, [:user_id, :category_id], - name: 'idx_category_users_user_id_category_id', unique: true - add_index :category_users, [:category_id, :user_id], - name: 'idx_category_users_category_id_user_id', unique: true + add_index :category_users, + %i[user_id category_id], + name: "idx_category_users_user_id_category_id", + unique: true + add_index :category_users, + %i[category_id user_id], + name: "idx_category_users_category_id_user_id", + unique: true end def down - remove_index :category_users, name: 'idx_category_users_user_id_category_id' - remove_index :category_users, name: 'idx_category_users_category_id_user_id' + remove_index :category_users, name: "idx_category_users_user_id_category_id" + remove_index :category_users, name: "idx_category_users_category_id_user_id" - add_index :category_users, [:user_id, :category_id, :notification_level], - name: 'idx_category_users_u1', unique: true - add_index :category_users, [:category_id, :user_id, :notification_level], - name: 'idx_category_users_u2', unique: true + add_index :category_users, + %i[user_id category_id notification_level], + name: "idx_category_users_u1", + unique: true + add_index :category_users, + %i[category_id user_id notification_level], + name: "idx_category_users_u2", + unique: true end end diff --git a/db/migrate/20190716124050_remove_via_email_from_invite.rb b/db/migrate/20190716124050_remove_via_email_from_invite.rb index 29e91f39ed..f0d8b4779d 100644 --- a/db/migrate/20190716124050_remove_via_email_from_invite.rb +++ b/db/migrate/20190716124050_remove_via_email_from_invite.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class RemoveViaEmailFromInvite < ActiveRecord::Migration[5.2] - DROPPED_COLUMNS ||= { - invites: %i{via_email} - } + DROPPED_COLUMNS ||= { invites: %i[via_email] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20190716173854_add_secure_to_uploads.rb b/db/migrate/20190716173854_add_secure_to_uploads.rb index 837cff98f6..e2c79aad32 100644 --- a/db/migrate/20190716173854_add_secure_to_uploads.rb +++ b/db/migrate/20190716173854_add_secure_to_uploads.rb @@ -4,20 +4,18 @@ class AddSecureToUploads < ActiveRecord::Migration[5.2] def up add_column :uploads, :secure, :boolean, default: false, null: false - prevent_anons_from_downloading_files = \ - DB.query_single("SELECT value FROM site_settings WHERE name = 'prevent_anons_from_downloading_files'").first == 't' + prevent_anons_from_downloading_files = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'prevent_anons_from_downloading_files'", + ).first == "t" - if prevent_anons_from_downloading_files - execute( - <<-SQL + execute(<<-SQL) if prevent_anons_from_downloading_files UPDATE uploads SET secure = 't' WHERE id IN ( SELECT DISTINCT(uploads.id) FROM uploads INNER JOIN post_uploads ON post_uploads.upload_id = uploads.id WHERE LOWER(original_filename) NOT SIMILAR TO '%\.(jpg|jpeg|png|gif|svg|ico)' ) SQL - ) - end end def down diff --git a/db/migrate/20190717133743_migrate_group_list_site_settings.rb b/db/migrate/20190717133743_migrate_group_list_site_settings.rb index 6a81fe17b0..acd7bb8acd 100644 --- a/db/migrate/20190717133743_migrate_group_list_site_settings.rb +++ b/db/migrate/20190717133743_migrate_group_list_site_settings.rb @@ -10,7 +10,7 @@ class MigrateGroupListSiteSettings < ActiveRecord::Migration[5.2] end def migrate_value(from, to) - cast_type = from == :id ? '::int[]' : '' + cast_type = from == :id ? "::int[]" : "" DB.exec <<~SQL UPDATE site_settings SET value = COALESCE(array_to_string( diff --git a/db/migrate/20190904104533_create_user_security_keys.rb b/db/migrate/20190904104533_create_user_security_keys.rb index acccab9685..c9bf5ed9b7 100644 --- a/db/migrate/20190904104533_create_user_security_keys.rb +++ b/db/migrate/20190904104533_create_user_security_keys.rb @@ -19,8 +19,6 @@ class CreateUserSecurityKeys < ActiveRecord::Migration[5.2] end def down - if table_exists?(:user_security_keys) - drop_table(:user_security_keys) - end + drop_table(:user_security_keys) if table_exists?(:user_security_keys) end end diff --git a/db/migrate/20190917100006_add_enabled_index_to_user_security_key.rb b/db/migrate/20190917100006_add_enabled_index_to_user_security_key.rb index 14b9c027c2..8cc5be7958 100644 --- a/db/migrate/20190917100006_add_enabled_index_to_user_security_key.rb +++ b/db/migrate/20190917100006_add_enabled_index_to_user_security_key.rb @@ -2,6 +2,6 @@ class AddEnabledIndexToUserSecurityKey < ActiveRecord::Migration[6.0] def change - add_index :user_security_keys, [:factor_type, :enabled] + add_index :user_security_keys, %i[factor_type enabled] end end diff --git a/db/migrate/20191008124357_add_unique_index_categories_on_slug.rb b/db/migrate/20191008124357_add_unique_index_categories_on_slug.rb index 7c9dad67e3..e5aeecc1d9 100644 --- a/db/migrate/20191008124357_add_unique_index_categories_on_slug.rb +++ b/db/migrate/20191008124357_add_unique_index_categories_on_slug.rb @@ -4,8 +4,8 @@ class AddUniqueIndexCategoriesOnSlug < ActiveRecord::Migration[6.0] def change add_index( :categories, - 'COALESCE(parent_category_id, -1), slug', - name: 'unique_index_categories_on_slug' + "COALESCE(parent_category_id, -1), slug", + name: "unique_index_categories_on_slug", ) end end diff --git a/db/migrate/20191011131041_migrate_decompressed_file_max_size_mb.rb b/db/migrate/20191011131041_migrate_decompressed_file_max_size_mb.rb index f3db85da39..6e8c551fdc 100644 --- a/db/migrate/20191011131041_migrate_decompressed_file_max_size_mb.rb +++ b/db/migrate/20191011131041_migrate_decompressed_file_max_size_mb.rb @@ -2,16 +2,17 @@ class MigrateDecompressedFileMaxSizeMb < ActiveRecord::Migration[6.0] def up - current_value = DB.query_single("SELECT value FROM site_settings WHERE name ='decompressed_file_max_size_mb' ").first + current_value = + DB.query_single( + "SELECT value FROM site_settings WHERE name ='decompressed_file_max_size_mb' ", + ).first - if current_value && current_value != '1000' - DB.exec <<~SQL + DB.exec <<~SQL if current_value && current_value != "1000" INSERT INTO site_settings (name, data_type, value, created_at, updated_at) VALUES ('decompressed_theme_max_file_size_mb', 3, #{current_value}, current_timestamp, current_timestamp), ('decompressed_backup_max_file_size_mb', 3, #{current_value}, current_timestamp, current_timestamp) SQL - end end def down diff --git a/db/migrate/20191016124059_fix_category_slugs_index.rb b/db/migrate/20191016124059_fix_category_slugs_index.rb index 58b67b6fe6..ca26c0a95f 100644 --- a/db/migrate/20191016124059_fix_category_slugs_index.rb +++ b/db/migrate/20191016124059_fix_category_slugs_index.rb @@ -2,14 +2,14 @@ class FixCategorySlugsIndex < ActiveRecord::Migration[6.0] def change - remove_index(:categories, name: 'unique_index_categories_on_slug') + remove_index(:categories, name: "unique_index_categories_on_slug") add_index( :categories, - 'COALESCE(parent_category_id, -1), slug', - name: 'unique_index_categories_on_slug', + "COALESCE(parent_category_id, -1), slug", + name: "unique_index_categories_on_slug", where: "slug != ''", - unique: true + unique: true, ) end end diff --git a/db/migrate/20191017044811_add_draft_backup_tables.rb b/db/migrate/20191017044811_add_draft_backup_tables.rb index 6177426c50..16902029f0 100644 --- a/db/migrate/20191017044811_add_draft_backup_tables.rb +++ b/db/migrate/20191017044811_add_draft_backup_tables.rb @@ -2,7 +2,6 @@ class AddDraftBackupTables < ActiveRecord::Migration[6.0] def change - create_table :backup_draft_topics do |t| t.integer :user_id, null: false t.integer :topic_id, null: false @@ -16,7 +15,7 @@ class AddDraftBackupTables < ActiveRecord::Migration[6.0] t.timestamps end - add_index :backup_draft_posts, [:user_id, :key], unique: true + add_index :backup_draft_posts, %i[user_id key], unique: true add_index :backup_draft_posts, [:post_id], unique: true add_index :backup_draft_topics, [:user_id], unique: true diff --git a/db/migrate/20191022155215_add_index_to_oauth2_user_info.rb b/db/migrate/20191022155215_add_index_to_oauth2_user_info.rb index 4c330ddae4..3228e5589b 100644 --- a/db/migrate/20191022155215_add_index_to_oauth2_user_info.rb +++ b/db/migrate/20191022155215_add_index_to_oauth2_user_info.rb @@ -2,6 +2,6 @@ class AddIndexToOauth2UserInfo < ActiveRecord::Migration[6.0] def change - add_index :oauth2_user_infos, [:user_id, :provider] + add_index :oauth2_user_infos, %i[user_id provider] end end diff --git a/db/migrate/20191025005204_amend_oauth2_user_info_index.rb b/db/migrate/20191025005204_amend_oauth2_user_info_index.rb index 15dcf001d3..e9e1891313 100644 --- a/db/migrate/20191025005204_amend_oauth2_user_info_index.rb +++ b/db/migrate/20191025005204_amend_oauth2_user_info_index.rb @@ -4,6 +4,6 @@ class AmendOauth2UserInfoIndex < ActiveRecord::Migration[6.0] def up # remove old index which may have been unique execute "DROP INDEX index_oauth2_user_infos_on_user_id_and_provider" - add_index :oauth2_user_infos, [:user_id, :provider] + add_index :oauth2_user_infos, %i[user_id provider] end end diff --git a/db/migrate/20191030112559_add_index_to_notifications.rb b/db/migrate/20191030112559_add_index_to_notifications.rb index 69d36d2f4d..48fa67fdbb 100644 --- a/db/migrate/20191030112559_add_index_to_notifications.rb +++ b/db/migrate/20191030112559_add_index_to_notifications.rb @@ -4,8 +4,8 @@ class AddIndexToNotifications < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - if !index_exists?(:notifications, [:topic_id, :post_number]) - add_index :notifications, [:topic_id, :post_number], algorithm: :concurrently + if !index_exists?(:notifications, %i[topic_id post_number]) + add_index :notifications, %i[topic_id post_number], algorithm: :concurrently end end diff --git a/db/migrate/20191031052711_add_granted_title_badge_id_to_user_profile.rb b/db/migrate/20191031052711_add_granted_title_badge_id_to_user_profile.rb index f76ea34ef4..c37627a95c 100644 --- a/db/migrate/20191031052711_add_granted_title_badge_id_to_user_profile.rb +++ b/db/migrate/20191031052711_add_granted_title_badge_id_to_user_profile.rb @@ -2,7 +2,13 @@ class AddGrantedTitleBadgeIdToUserProfile < ActiveRecord::Migration[6.0] def up - add_reference :user_profiles, :granted_title_badge, foreign_key: { to_table: :badges }, index: true, null: true + add_reference :user_profiles, + :granted_title_badge, + foreign_key: { + to_table: :badges, + }, + index: true, + null: true # update all the regular badge derived titles based # on the normal badge name diff --git a/db/migrate/20191101001705_add_banner_index_to_topics.rb b/db/migrate/20191101001705_add_banner_index_to_topics.rb index ecafad00de..7da2048e7e 100644 --- a/db/migrate/20191101001705_add_banner_index_to_topics.rb +++ b/db/migrate/20191101001705_add_banner_index_to_topics.rb @@ -2,6 +2,10 @@ class AddBannerIndexToTopics < ActiveRecord::Migration[6.0] def change # this speeds up the process for finding banners on the site - add_index :topics, [:id], name: 'index_topics_on_id_filtered_banner', where: "archetype = 'banner' AND deleted_at IS NULL", unique: true + add_index :topics, + [:id], + name: "index_topics_on_id_filtered_banner", + where: "archetype = 'banner' AND deleted_at IS NULL", + unique: true end end diff --git a/db/migrate/20191107025140_add_index_to_last_seen_at_on_category_users.rb b/db/migrate/20191107025140_add_index_to_last_seen_at_on_category_users.rb index e26839625d..59abd284b3 100644 --- a/db/migrate/20191107025140_add_index_to_last_seen_at_on_category_users.rb +++ b/db/migrate/20191107025140_add_index_to_last_seen_at_on_category_users.rb @@ -4,12 +4,12 @@ class AddIndexToLastSeenAtOnCategoryUsers < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - if !index_exists?(:category_users, [:user_id, :last_seen_at]) - add_index :category_users, [:user_id, :last_seen_at], algorithm: :concurrently + if !index_exists?(:category_users, %i[user_id last_seen_at]) + add_index :category_users, %i[user_id last_seen_at], algorithm: :concurrently end end def down - remove_index :category_users, [:user_id, :last_seen_at] + remove_index :category_users, %i[user_id last_seen_at] end end diff --git a/db/migrate/20191107190330_remove_suppress_from_latest_from_category.rb b/db/migrate/20191107190330_remove_suppress_from_latest_from_category.rb index 7d047c17c0..fb836afa63 100644 --- a/db/migrate/20191107190330_remove_suppress_from_latest_from_category.rb +++ b/db/migrate/20191107190330_remove_suppress_from_latest_from_category.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true class RemoveSuppressFromLatestFromCategory < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - categories: %i{suppress_from_latest} - } + DROPPED_COLUMNS ||= { categories: %i[suppress_from_latest] } def up ids = DB.query_single("SELECT id::text FROM categories WHERE suppress_from_latest = TRUE") if ids.present? - muted_ids = DB.query_single("SELECT value from site_settings WHERE name = 'default_categories_muted'").first + muted_ids = + DB.query_single( + "SELECT value from site_settings WHERE name = 'default_categories_muted'", + ).first ids += muted_ids.split("|") if muted_ids.present? ids.uniq! @@ -37,9 +38,7 @@ class RemoveSuppressFromLatestFromCategory < ActiveRecord::Migration[6.0] end end - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20191108000414_add_unique_index_to_drafts.rb b/db/migrate/20191108000414_add_unique_index_to_drafts.rb index f50d1b7c49..2b34a8c2ca 100644 --- a/db/migrate/20191108000414_add_unique_index_to_drafts.rb +++ b/db/migrate/20191108000414_add_unique_index_to_drafts.rb @@ -2,7 +2,6 @@ class AddUniqueIndexToDrafts < ActiveRecord::Migration[6.0] def up - execute <<~SQL DELETE FROM drafts d1 USING ( @@ -17,8 +16,8 @@ class AddUniqueIndexToDrafts < ActiveRecord::Migration[6.0] d1.id <> d2.id SQL - remove_index :drafts, [:user_id, :draft_key] - add_index :drafts, [:user_id, :draft_key], unique: true + remove_index :drafts, %i[user_id draft_key] + add_index :drafts, %i[user_id draft_key], unique: true end def down diff --git a/db/migrate/20191120015344_add_timezone_to_user_options.rb b/db/migrate/20191120015344_add_timezone_to_user_options.rb index 8284842cf6..57c17ebfc0 100644 --- a/db/migrate/20191120015344_add_timezone_to_user_options.rb +++ b/db/migrate/20191120015344_add_timezone_to_user_options.rb @@ -3,14 +3,12 @@ class AddTimezoneToUserOptions < ActiveRecord::Migration[6.0] def up add_column :user_options, :timezone, :string - execute( - <<-SQL + execute(<<-SQL) UPDATE user_options SET timezone = ucf.value FROM user_custom_fields AS ucf WHERE ucf.user_id = user_options.user_id AND ucf.name = 'timezone' SQL - ) end def down diff --git a/db/migrate/20191128222140_add_unique_index_to_developers.rb b/db/migrate/20191128222140_add_unique_index_to_developers.rb index d839b15770..2c7e99df04 100644 --- a/db/migrate/20191128222140_add_unique_index_to_developers.rb +++ b/db/migrate/20191128222140_add_unique_index_to_developers.rb @@ -15,7 +15,7 @@ class AddUniqueIndexToDevelopers < ActiveRecord::Migration[6.0] d1.id <> d2.id SQL - add_index :developers, %i(user_id), unique: true + add_index :developers, %i[user_id], unique: true end def down diff --git a/db/migrate/20191129144706_drop_unused_google_instagram_auth_tables.rb b/db/migrate/20191129144706_drop_unused_google_instagram_auth_tables.rb index f1b3df2e60..f6df758fc1 100644 --- a/db/migrate/20191129144706_drop_unused_google_instagram_auth_tables.rb +++ b/db/migrate/20191129144706_drop_unused_google_instagram_auth_tables.rb @@ -1,17 +1,12 @@ # frozen_string_literal: true -require 'migration/table_dropper' +require "migration/table_dropper" class DropUnusedGoogleInstagramAuthTables < ActiveRecord::Migration[6.0] - DROPPED_TABLES ||= %i{ - google_user_infos - instagram_user_infos - } + DROPPED_TABLES ||= %i[google_user_infos instagram_user_infos] def up - DROPPED_TABLES.each do |table| - Migration::TableDropper.execute_drop(table) - end + DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) } end def down diff --git a/db/migrate/20191205100434_create_standalone_bookmarks_table.rb b/db/migrate/20191205100434_create_standalone_bookmarks_table.rb index 74c636a881..10a5d75480 100644 --- a/db/migrate/20191205100434_create_standalone_bookmarks_table.rb +++ b/db/migrate/20191205100434_create_standalone_bookmarks_table.rb @@ -13,7 +13,7 @@ class CreateStandaloneBookmarksTable < ActiveRecord::Migration[6.0] t.timestamps end - add_index :bookmarks, [:user_id, :post_id], unique: true + add_index :bookmarks, %i[user_id post_id], unique: true end def down diff --git a/db/migrate/20191211170000_add_hashed_api_key.rb b/db/migrate/20191211170000_add_hashed_api_key.rb index 2811239c5d..385d5f29be 100644 --- a/db/migrate/20191211170000_add_hashed_api_key.rb +++ b/db/migrate/20191211170000_add_hashed_api_key.rb @@ -4,12 +4,10 @@ class AddHashedApiKey < ActiveRecord::Migration[6.0] add_column(:api_keys, :key_hash, :string) add_column(:api_keys, :truncated_key, :string) - execute( - <<~SQL + execute(<<~SQL) UPDATE api_keys SET truncated_key = LEFT(key, 4) SQL - ) batch_size = 500 begin diff --git a/db/migrate/20191217035630_populate_topic_id_on_bookmarks.rb b/db/migrate/20191217035630_populate_topic_id_on_bookmarks.rb index 1918c185ac..c7d85193b9 100644 --- a/db/migrate/20191217035630_populate_topic_id_on_bookmarks.rb +++ b/db/migrate/20191217035630_populate_topic_id_on_bookmarks.rb @@ -2,9 +2,10 @@ class PopulateTopicIdOnBookmarks < ActiveRecord::Migration[6.0] def up - Bookmark.where(topic_id: nil).includes(:post).find_each do |bookmark| - bookmark.update_column(:topic_id, bookmark.post.topic_id) - end + Bookmark + .where(topic_id: nil) + .includes(:post) + .find_each { |bookmark| bookmark.update_column(:topic_id, bookmark.post.topic_id) } end def down diff --git a/db/migrate/20191219112000_remove_key_from_api_keys.rb b/db/migrate/20191219112000_remove_key_from_api_keys.rb index 75e93d3c95..c9dcb70f17 100644 --- a/db/migrate/20191219112000_remove_key_from_api_keys.rb +++ b/db/migrate/20191219112000_remove_key_from_api_keys.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true class RemoveKeyFromApiKeys < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - api_keys: %i{key} - } + DROPPED_COLUMNS ||= { api_keys: %i[key] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20191230055237_add_access_control_columns_to_upload.rb b/db/migrate/20191230055237_add_access_control_columns_to_upload.rb index 87f1a213ef..717da9b39f 100644 --- a/db/migrate/20191230055237_add_access_control_columns_to_upload.rb +++ b/db/migrate/20191230055237_add_access_control_columns_to_upload.rb @@ -2,7 +2,13 @@ class AddAccessControlColumnsToUpload < ActiveRecord::Migration[6.0] def up - add_reference :uploads, :access_control_post, foreign_key: { to_table: :posts }, index: true, null: true + add_reference :uploads, + :access_control_post, + foreign_key: { + to_table: :posts, + }, + index: true, + null: true add_column :uploads, :original_sha1, :string, null: true add_index :uploads, :original_sha1 end diff --git a/db/migrate/20200117141138_update_post_reply_indexes.rb b/db/migrate/20200117141138_update_post_reply_indexes.rb index 0ba728edcc..12190d2a7a 100644 --- a/db/migrate/20200117141138_update_post_reply_indexes.rb +++ b/db/migrate/20200117141138_update_post_reply_indexes.rb @@ -6,16 +6,16 @@ class UpdatePostReplyIndexes < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - if !index_exists?(:post_replies, [:post_id, :reply_post_id]) - add_index :post_replies, [:post_id, :reply_post_id], unique: true, algorithm: :concurrently + if !index_exists?(:post_replies, %i[post_id reply_post_id]) + add_index :post_replies, %i[post_id reply_post_id], unique: true, algorithm: :concurrently end if !index_exists?(:post_replies, [:reply_post_id]) add_index :post_replies, [:reply_post_id], algorithm: :concurrently end - if index_exists?(:post_replies, [:post_id, :reply_id]) - remove_index :post_replies, column: [:post_id, :reply_id], algorithm: :concurrently + if index_exists?(:post_replies, %i[post_id reply_id]) + remove_index :post_replies, column: %i[post_id reply_id], algorithm: :concurrently end if index_exists?(:post_replies, [:reply_id]) diff --git a/db/migrate/20200120131338_drop_unused_columns.rb b/db/migrate/20200120131338_drop_unused_columns.rb index 096f3ee4a6..9e0cd64c71 100644 --- a/db/migrate/20200120131338_drop_unused_columns.rb +++ b/db/migrate/20200120131338_drop_unused_columns.rb @@ -2,19 +2,12 @@ class DropUnusedColumns < ActiveRecord::Migration[6.0] DROPPED_COLUMNS ||= { - post_replies: %i{ - reply_id - }, - user_profiles: %i{ - card_background - profile_background - } + post_replies: %i[reply_id], + user_profiles: %i[card_background profile_background], } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20200311135425_clear_approved_users_from_the_review_queue.rb b/db/migrate/20200311135425_clear_approved_users_from_the_review_queue.rb index 349fea7c7b..4f1e500209 100644 --- a/db/migrate/20200311135425_clear_approved_users_from_the_review_queue.rb +++ b/db/migrate/20200311135425_clear_approved_users_from_the_review_queue.rb @@ -11,12 +11,12 @@ class ClearApprovedUsersFromTheReviewQueue < ActiveRecord::Migration[6.0] SQL system_user_id = Discourse::SYSTEM_USER_ID - scores = reviewables.map do |id| - "(#{id}, 1, #{Reviewable.statuses[:approved]}, #{system_user_id}, NOW(), NOW())" - end + scores = + reviewables.map do |id| + "(#{id}, 1, #{Reviewable.statuses[:approved]}, #{system_user_id}, NOW(), NOW())" + end - if scores.present? - DB.exec <<~SQL + DB.exec <<~SQL if scores.present? INSERT INTO reviewable_histories ( reviewable_id, reviewable_history_type, @@ -25,9 +25,8 @@ class ClearApprovedUsersFromTheReviewQueue < ActiveRecord::Migration[6.0] created_at, updated_at ) - VALUES #{scores.join(',') << ';'} + VALUES #{scores.join(",") << ";"} SQL - end end def down diff --git a/db/migrate/20200403100259_add_key_hash_to_user_api_key.rb b/db/migrate/20200403100259_add_key_hash_to_user_api_key.rb index 138cf28b55..01f9abb4c0 100644 --- a/db/migrate/20200403100259_add_key_hash_to_user_api_key.rb +++ b/db/migrate/20200403100259_add_key_hash_to_user_api_key.rb @@ -6,9 +6,10 @@ class AddKeyHashToUserApiKey < ActiveRecord::Migration[6.0] batch_size = 500 loop do - rows = DB - .query("SELECT id, key FROM user_api_keys WHERE key_hash IS NULL LIMIT #{batch_size}") - .map { |row| { id: row.id, key_hash: Digest::SHA256.hexdigest(row.key) } } + rows = + DB + .query("SELECT id, key FROM user_api_keys WHERE key_hash IS NULL LIMIT #{batch_size}") + .map { |row| { id: row.id, key_hash: Digest::SHA256.hexdigest(row.key) } } break if rows.size == 0 diff --git a/db/migrate/20200408121312_remove_key_from_user_api_key.rb b/db/migrate/20200408121312_remove_key_from_user_api_key.rb index a6147e5d58..e2b0602fb8 100644 --- a/db/migrate/20200408121312_remove_key_from_user_api_key.rb +++ b/db/migrate/20200408121312_remove_key_from_user_api_key.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class RemoveKeyFromUserApiKey < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - user_api_keys: %i{key} - } + DROPPED_COLUMNS ||= { user_api_keys: %i[key] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20200409033412_create_bookmarks_from_post_action_bookmarks.rb b/db/migrate/20200409033412_create_bookmarks_from_post_action_bookmarks.rb index 57b1885e35..bae9cb2e6e 100644 --- a/db/migrate/20200409033412_create_bookmarks_from_post_action_bookmarks.rb +++ b/db/migrate/20200409033412_create_bookmarks_from_post_action_bookmarks.rb @@ -9,8 +9,7 @@ class CreateBookmarksFromPostActionBookmarks < ActiveRecord::Migration[6.0] # post action type id 1 is :bookmark. we do not need to OFFSET here for # paging because the WHERE bookmarks.id IS NULL clause handles this effectively, # because we do not get bookmarks back that have already been inserted - post_action_bookmarks = DB.query( - <<~SQL, type_id: 1 + post_action_bookmarks = DB.query(<<~SQL, type_id: 1) SELECT post_actions.id, post_actions.post_id, posts.topic_id, post_actions.user_id FROM post_actions INNER JOIN posts ON posts.id = post_actions.post_id @@ -20,7 +19,6 @@ class CreateBookmarksFromPostActionBookmarks < ActiveRecord::Migration[6.0] WHERE bookmarks.id IS NULL AND post_action_type_id = :type_id AND post_actions.deleted_at IS NULL AND posts.deleted_at IS NULL LIMIT 2000 SQL - ) break if post_action_bookmarks.count.zero? post_action_bookmarks.each do |pab| @@ -48,12 +46,10 @@ class CreateBookmarksFromPostActionBookmarks < ActiveRecord::Migration[6.0] # the above LEFT JOIN but best to be safe knowing this # won't blow up # - DB.exec( - <<~SQL + DB.exec(<<~SQL) INSERT INTO bookmarks (topic_id, post_id, user_id, created_at, updated_at) VALUES #{bookmarks_to_create.join(",\n")} ON CONFLICT DO NOTHING SQL - ) end end diff --git a/db/migrate/20200415140830_drop_automatic_membership_retroactive_from_group.rb b/db/migrate/20200415140830_drop_automatic_membership_retroactive_from_group.rb index 4e7b779d6b..23a740aa27 100644 --- a/db/migrate/20200415140830_drop_automatic_membership_retroactive_from_group.rb +++ b/db/migrate/20200415140830_drop_automatic_membership_retroactive_from_group.rb @@ -1,16 +1,10 @@ # frozen_string_literal: true class DropAutomaticMembershipRetroactiveFromGroup < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - groups: %i{ - automatic_membership_retroactive - } - } + DROPPED_COLUMNS ||= { groups: %i[automatic_membership_retroactive] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20200417183143_add_created_at_to_badge_user.rb b/db/migrate/20200417183143_add_created_at_to_badge_user.rb index bbfd6066e9..ae7349d00a 100644 --- a/db/migrate/20200417183143_add_created_at_to_badge_user.rb +++ b/db/migrate/20200417183143_add_created_at_to_badge_user.rb @@ -2,8 +2,8 @@ class AddCreatedAtToBadgeUser < ActiveRecord::Migration[6.0] def up add_column :user_badges, :created_at, :datetime, null: true - execute 'UPDATE user_badges SET created_at = granted_at WHERE created_at IS NULL' - change_column :user_badges, :created_at, :datetime, null: false, default: 'current_timestamp' + execute "UPDATE user_badges SET created_at = granted_at WHERE created_at IS NULL" + change_column :user_badges, :created_at, :datetime, null: false, default: "current_timestamp" end def down diff --git a/db/migrate/20200428014005_correct_topic_user_bookmarked_boolean.rb b/db/migrate/20200428014005_correct_topic_user_bookmarked_boolean.rb index 9d6f8ea029..cc690f50a3 100644 --- a/db/migrate/20200428014005_correct_topic_user_bookmarked_boolean.rb +++ b/db/migrate/20200428014005_correct_topic_user_bookmarked_boolean.rb @@ -4,20 +4,16 @@ class CorrectTopicUserBookmarkedBoolean < ActiveRecord::Migration[6.0] def up # if the relation exists then we set to bookmarked because # at least 1 bookmark for the user + topic exists - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE topic_users SET bookmarked = true FROM bookmarks AS b WHERE NOT topic_users.bookmarked AND topic_users.topic_id = b.topic_id AND topic_users.user_id = b.user_id SQL - ) - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE topic_users SET bookmarked = false WHERE topic_users.bookmarked AND (SELECT COUNT(*) FROM bookmarks WHERE topic_id = topic_users.topic_id AND user_id = topic_users.user_id) = 0 SQL - ) end def down diff --git a/db/migrate/20200428102014_add_bulk_invite_link_to_invites.rb b/db/migrate/20200428102014_add_bulk_invite_link_to_invites.rb index 57636b6dcb..855d469a11 100644 --- a/db/migrate/20200428102014_add_bulk_invite_link_to_invites.rb +++ b/db/migrate/20200428102014_add_bulk_invite_link_to_invites.rb @@ -6,7 +6,8 @@ class AddBulkInviteLinkToInvites < ActiveRecord::Migration[6.0] add_column :invites, :redemption_count, :integer, null: false, default: 0 add_column :invites, :expires_at, :datetime, null: true - invite_expiry_days = DB.query_single("SELECT value FROM site_settings WHERE name = 'invite_expiry_days'").first + invite_expiry_days = + DB.query_single("SELECT value FROM site_settings WHERE name = 'invite_expiry_days'").first invite_expiry_days = 30 if invite_expiry_days.blank? execute <<~SQL UPDATE invites SET expires_at = updated_at + INTERVAL '#{invite_expiry_days} days' diff --git a/db/migrate/20200429095034_add_topic_thumbnail_information.rb b/db/migrate/20200429095034_add_topic_thumbnail_information.rb index 3efb4c3a02..1de2f764a7 100644 --- a/db/migrate/20200429095034_add_topic_thumbnail_information.rb +++ b/db/migrate/20200429095034_add_topic_thumbnail_information.rb @@ -4,7 +4,6 @@ class AddTopicThumbnailInformation < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - # tables are huge ... avoid holding on to large number of locks by doing one at a time execute <<~SQL ALTER TABLE posts @@ -36,7 +35,10 @@ class AddTopicThumbnailInformation < ActiveRecord::Migration[6.0] t.integer :max_height, null: false end - add_index :topic_thumbnails, [:upload_id, :max_width, :max_height], name: :unique_topic_thumbnails, unique: true + add_index :topic_thumbnails, + %i[upload_id max_width max_height], + name: :unique_topic_thumbnails, + unique: true end end diff --git a/db/migrate/20200430072846_create_invited_users.rb b/db/migrate/20200430072846_create_invited_users.rb index fa3f843e60..5c7592e7f2 100644 --- a/db/migrate/20200430072846_create_invited_users.rb +++ b/db/migrate/20200430072846_create_invited_users.rb @@ -10,6 +10,6 @@ class CreateInvitedUsers < ActiveRecord::Migration[6.0] end add_index :invited_users, :invite_id - add_index :invited_users, [:user_id, :invite_id], unique: true, where: 'user_id IS NOT NULL' + add_index :invited_users, %i[user_id invite_id], unique: true, where: "user_id IS NOT NULL" end end diff --git a/db/migrate/20200506044956_migrate_at_desktop_bookmark_reminders.rb b/db/migrate/20200506044956_migrate_at_desktop_bookmark_reminders.rb index add607b31d..8245ba5fe4 100644 --- a/db/migrate/20200506044956_migrate_at_desktop_bookmark_reminders.rb +++ b/db/migrate/20200506044956_migrate_at_desktop_bookmark_reminders.rb @@ -3,12 +3,10 @@ class MigrateAtDesktopBookmarkReminders < ActiveRecord::Migration[6.0] def up # reminder_type 0 is at_desktop, which is no longer valid - DB.exec( - <<~SQL, now: Time.zone.now + DB.exec(<<~SQL, now: Time.zone.now) UPDATE bookmarks SET reminder_type = NULL, reminder_at = NULL, updated_at = :now WHERE reminder_type = 0 SQL - ) end def down diff --git a/db/migrate/20200507234409_ensure_bookmark_delete_when_reminder_sent_not_null.rb b/db/migrate/20200507234409_ensure_bookmark_delete_when_reminder_sent_not_null.rb index 5052675f96..483226578e 100644 --- a/db/migrate/20200507234409_ensure_bookmark_delete_when_reminder_sent_not_null.rb +++ b/db/migrate/20200507234409_ensure_bookmark_delete_when_reminder_sent_not_null.rb @@ -2,7 +2,9 @@ class EnsureBookmarkDeleteWhenReminderSentNotNull < ActiveRecord::Migration[6.0] def change - DB.exec("UPDATE bookmarks SET delete_when_reminder_sent = false WHERE delete_when_reminder_sent IS NULL") + DB.exec( + "UPDATE bookmarks SET delete_when_reminder_sent = false WHERE delete_when_reminder_sent IS NULL", + ) change_column_null :bookmarks, :delete_when_reminder_sent, false end end diff --git a/db/migrate/20200511043818_add_more_flair_columns_to_group.rb b/db/migrate/20200511043818_add_more_flair_columns_to_group.rb index 70c1b53043..3d2098549a 100644 --- a/db/migrate/20200511043818_add_more_flair_columns_to_group.rb +++ b/db/migrate/20200511043818_add_more_flair_columns_to_group.rb @@ -7,29 +7,23 @@ class AddMoreFlairColumnsToGroup < ActiveRecord::Migration[6.0] reversible do |dir| dir.up do - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE groups SET flair_icon = REPLACE(REPLACE(flair_url, 'fas fa-', ''), ' fa-', '-') WHERE flair_url LIKE 'fa%' SQL - ) - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE groups g1 SET flair_upload_id = u.id FROM groups g2 INNER JOIN uploads u ON g2.flair_url ~ CONCAT('\/', u.sha1, '[\.\w]*') WHERE g1.id = g2.id SQL - ) - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE groups SET flair_url = NULL WHERE flair_icon IS NOT NULL OR flair_upload_id IS NOT NULL SQL - ) end end end diff --git a/db/migrate/20200513185052_drop_topic_reply_count.rb b/db/migrate/20200513185052_drop_topic_reply_count.rb index 5bae3435c6..4b5a57f6f7 100644 --- a/db/migrate/20200513185052_drop_topic_reply_count.rb +++ b/db/migrate/20200513185052_drop_topic_reply_count.rb @@ -1,16 +1,10 @@ # frozen_string_literal: true class DropTopicReplyCount < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - user_stats: %i{ - topic_reply_count - } - } + DROPPED_COLUMNS ||= { user_stats: %i[topic_reply_count] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20200517140915_update_deprecated_icon_names.rb b/db/migrate/20200517140915_update_deprecated_icon_names.rb index 74c07f7c6e..4655bbe23e 100644 --- a/db/migrate/20200517140915_update_deprecated_icon_names.rb +++ b/db/migrate/20200517140915_update_deprecated_icon_names.rb @@ -10,7 +10,10 @@ class UpdateDeprecatedIconNames < ActiveRecord::Migration[6.0] end def migrate_value(dir) - icons = File.open("#{Rails.root}/db/migrate/20200517140915_fa4_renames.json", "r:UTF-8") { |f| JSON.parse(f.read) } + icons = + File.open("#{Rails.root}/db/migrate/20200517140915_fa4_renames.json", "r:UTF-8") do |f| + JSON.parse(f.read) + end icons.each do |key, value| from = dir == "up" ? key : value @@ -26,7 +29,6 @@ class UpdateDeprecatedIconNames < ActiveRecord::Migration[6.0] SET flair_icon = '#{to}' WHERE flair_icon = '#{from}' OR flair_icon = 'fa-#{from}' SQL - end end end diff --git a/db/migrate/20200524181959_add_default_list_filter_to_categories.rb b/db/migrate/20200524181959_add_default_list_filter_to_categories.rb index 4c43b5ccd3..1a90e90780 100644 --- a/db/migrate/20200524181959_add_default_list_filter_to_categories.rb +++ b/db/migrate/20200524181959_add_default_list_filter_to_categories.rb @@ -2,6 +2,6 @@ class AddDefaultListFilterToCategories < ActiveRecord::Migration[6.0] def change - add_column :categories, :default_list_filter, :string, limit: 20, default: 'all' + add_column :categories, :default_list_filter, :string, limit: 20, default: "all" end end diff --git a/db/migrate/20200525072638_remove_none_tags.rb b/db/migrate/20200525072638_remove_none_tags.rb index ed50eea490..da52e651bf 100644 --- a/db/migrate/20200525072638_remove_none_tags.rb +++ b/db/migrate/20200525072638_remove_none_tags.rb @@ -4,9 +4,13 @@ class RemoveNoneTags < ActiveRecord::Migration[6.0] def up none_tag_id = DB.query_single("SELECT id FROM tags WHERE lower(name) = 'none'").first if none_tag_id.present? - [:tag_users, :topic_tags, :category_tag_stats, :category_tags, :tag_group_memberships].each do |table_name| - execute "DELETE FROM #{table_name} WHERE tag_id = #{none_tag_id}" - end + %i[ + tag_users + topic_tags + category_tag_stats + category_tags + tag_group_memberships + ].each { |table_name| execute "DELETE FROM #{table_name} WHERE tag_id = #{none_tag_id}" } execute "DELETE FROM tags WHERE id = #{none_tag_id} OR target_tag_id = #{none_tag_id}" end end diff --git a/db/migrate/20200601130900_correct_schema_discrepancies.rb b/db/migrate/20200601130900_correct_schema_discrepancies.rb index 4d76e95352..882824e47b 100644 --- a/db/migrate/20200601130900_correct_schema_discrepancies.rb +++ b/db/migrate/20200601130900_correct_schema_discrepancies.rb @@ -4,7 +4,7 @@ class CorrectSchemaDiscrepancies < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - timestamp_columns = %w{ + timestamp_columns = %w[ category_tag_groups.created_at category_tag_groups.updated_at category_tags.created_at @@ -65,9 +65,9 @@ class CorrectSchemaDiscrepancies < ActiveRecord::Migration[6.0] web_hook_events.updated_at web_hooks.created_at web_hooks.updated_at - } + ] - char_limit_columns = %w{ + char_limit_columns = %w[ badge_groupings.name badge_types.name badges.icon @@ -136,30 +136,25 @@ class CorrectSchemaDiscrepancies < ActiveRecord::Migration[6.0] user_profiles.website users.name users.title - } + ] - float_default_columns = %w{ + float_default_columns = %w[ top_topics.all_score top_topics.daily_score top_topics.monthly_score top_topics.weekly_score top_topics.yearly_score - } + ] - other_default_columns = %w{ - categories.color - topic_search_data.topic_id - } + other_default_columns = %w[categories.color topic_search_data.topic_id] - lookup_sql = ( - timestamp_columns + - char_limit_columns + - float_default_columns + - other_default_columns - ).map do |ref| - table, column = ref.split(".") - "(table_name='#{table}' AND column_name='#{column}')" - end.join(" OR ") + lookup_sql = + (timestamp_columns + char_limit_columns + float_default_columns + other_default_columns) + .map do |ref| + table, column = ref.split(".") + "(table_name='#{table}' AND column_name='#{column}')" + end + .join(" OR ") raw_info = DB.query_hash <<~SQL SELECT table_name, column_name, is_nullable, character_maximum_length, column_default @@ -172,9 +167,7 @@ class CorrectSchemaDiscrepancies < ActiveRecord::Migration[6.0] schema_hash = {} - raw_info.each do |row| - schema_hash["#{row["table_name"]}.#{row["column_name"]}"] = row - end + raw_info.each { |row| schema_hash["#{row["table_name"]}.#{row["column_name"]}"] = row } # In the past, rails changed the default behavior for timestamp columns # This only affects older discourse installations @@ -232,11 +225,9 @@ class CorrectSchemaDiscrepancies < ActiveRecord::Migration[6.0] # Older sites have a default value like nextval('topic_search_data_topic_id_seq'::regclass) # Modern sites do not. This is likely caused by another historical change in rails - if schema_hash["topic_search_data.topic_id"]["column_default"] != nil - DB.exec <<~SQL + DB.exec <<~SQL if schema_hash["topic_search_data.topic_id"]["column_default"] != nil ALTER TABLE topic_search_data ALTER COLUMN topic_id SET DEFAULT NULL SQL - end end def down diff --git a/db/migrate/20200602153813_migrate_invite_redeemed_data_to_invited_users.rb b/db/migrate/20200602153813_migrate_invite_redeemed_data_to_invited_users.rb index 7f38286d4f..5f7922536b 100644 --- a/db/migrate/20200602153813_migrate_invite_redeemed_data_to_invited_users.rb +++ b/db/migrate/20200602153813_migrate_invite_redeemed_data_to_invited_users.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class MigrateInviteRedeemedDataToInvitedUsers < ActiveRecord::Migration[6.0] def up - %i{user_id redeemed_at}.each do |column| + %i[user_id redeemed_at].each do |column| Migration::ColumnDropper.mark_readonly(:invites, column) end diff --git a/db/migrate/20200707154522_fix_topic_like_count.rb b/db/migrate/20200707154522_fix_topic_like_count.rb index 95dd170792..8bbb36678c 100644 --- a/db/migrate/20200707154522_fix_topic_like_count.rb +++ b/db/migrate/20200707154522_fix_topic_like_count.rb @@ -2,7 +2,11 @@ class FixTopicLikeCount < ActiveRecord::Migration[6.0] def up - return if DB.query_single("SELECT * FROM site_settings WHERE name = 'enable_whispers' AND value = 't'").empty? + if DB.query_single( + "SELECT * FROM site_settings WHERE name = 'enable_whispers' AND value = 't'", + ).empty? + return + end DB.exec(<<~SQL, whisper: Post.types[:whisper]) UPDATE topics SET like_count = tbl.like_count diff --git a/db/migrate/20200708051009_cap_bookmark_name_at_100_characters.rb b/db/migrate/20200708051009_cap_bookmark_name_at_100_characters.rb index 1428778531..eb25419cf5 100644 --- a/db/migrate/20200708051009_cap_bookmark_name_at_100_characters.rb +++ b/db/migrate/20200708051009_cap_bookmark_name_at_100_characters.rb @@ -2,7 +2,9 @@ class CapBookmarkNameAt100Characters < ActiveRecord::Migration[6.0] def up - DB.exec("UPDATE bookmarks SET name = LEFT(name, 100) WHERE name IS NOT NULL AND name <> LEFT(name, 100)") + DB.exec( + "UPDATE bookmarks SET name = LEFT(name, 100) WHERE name IS NOT NULL AND name <> LEFT(name, 100)", + ) change_column :bookmarks, :name, :string, limit: 100 end diff --git a/db/migrate/20200709032247_allowlist_and_blocklist_site_settings.rb b/db/migrate/20200709032247_allowlist_and_blocklist_site_settings.rb index 7145c9fe81..c69e58f0e1 100644 --- a/db/migrate/20200709032247_allowlist_and_blocklist_site_settings.rb +++ b/db/migrate/20200709032247_allowlist_and_blocklist_site_settings.rb @@ -2,22 +2,18 @@ class AllowlistAndBlocklistSiteSettings < ActiveRecord::Migration[6.0] def up - SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |old_key, new_key| - DB.exec <<~SQL + SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair { |old_key, new_key| DB.exec <<~SQL } INSERT INTO site_settings(name, data_type, value, created_at, updated_at) SELECT '#{new_key}', data_type, value, created_at, updated_At FROM site_settings WHERE name = '#{old_key}' SQL - end end def down - SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |_old_key, new_key| - DB.exec <<~SQL + SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair { |_old_key, new_key| DB.exec <<~SQL } DELETE FROM site_settings WHERE name = '#{new_key}' SQL - end end end diff --git a/db/migrate/20200717193118_create_allowed_pm_users.rb b/db/migrate/20200717193118_create_allowed_pm_users.rb index 5c368eccf2..30e52f18fd 100644 --- a/db/migrate/20200717193118_create_allowed_pm_users.rb +++ b/db/migrate/20200717193118_create_allowed_pm_users.rb @@ -7,7 +7,7 @@ class CreateAllowedPmUsers < ActiveRecord::Migration[6.0] t.timestamps null: false end - add_index :allowed_pm_users, [:user_id, :allowed_pm_user_id], unique: true - add_index :allowed_pm_users, [:allowed_pm_user_id, :user_id], unique: true + add_index :allowed_pm_users, %i[user_id allowed_pm_user_id], unique: true + add_index :allowed_pm_users, %i[allowed_pm_user_id user_id], unique: true end end diff --git a/db/migrate/20200724060632_remove_deprecated_allowlist_settings.rb b/db/migrate/20200724060632_remove_deprecated_allowlist_settings.rb index 38b87c9ac2..038c43d723 100644 --- a/db/migrate/20200724060632_remove_deprecated_allowlist_settings.rb +++ b/db/migrate/20200724060632_remove_deprecated_allowlist_settings.rb @@ -2,22 +2,18 @@ class RemoveDeprecatedAllowlistSettings < ActiveRecord::Migration[6.0] def up - SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |old_key, _new_key| - DB.exec <<~SQL + SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair { |old_key, _new_key| DB.exec <<~SQL } DELETE FROM site_settings WHERE name = '#{old_key}' SQL - end end def down - SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair do |old_key, new_key| - DB.exec <<~SQL + SiteSetting::ALLOWLIST_DEPRECATED_SITE_SETTINGS.each_pair { |old_key, new_key| DB.exec <<~SQL } INSERT INTO site_settings(name, data_type, value, created_at, updated_at) SELECT '#{old_key}', data_type, value, created_at, updated_At FROM site_settings WHERE name = '#{new_key}' SQL - end end end diff --git a/db/migrate/20200728000854_duplicate_allowed_paths_from_path_whitelist.rb b/db/migrate/20200728000854_duplicate_allowed_paths_from_path_whitelist.rb index 17dd89960e..5f8a1b5d28 100644 --- a/db/migrate/20200728000854_duplicate_allowed_paths_from_path_whitelist.rb +++ b/db/migrate/20200728000854_duplicate_allowed_paths_from_path_whitelist.rb @@ -7,14 +7,12 @@ class DuplicateAllowedPathsFromPathWhitelist < ActiveRecord::Migration[6.0] end if column_exists?(:embeddable_hosts, :path_whitelist) - Migration::ColumnDropper.mark_readonly('embeddable_hosts', 'path_whitelist') + Migration::ColumnDropper.mark_readonly("embeddable_hosts", "path_whitelist") - if column_exists?(:embeddable_hosts, :allowed_paths) - DB.exec <<~SQL + DB.exec <<~SQL if column_exists?(:embeddable_hosts, :allowed_paths) UPDATE embeddable_hosts SET allowed_paths = path_whitelist SQL - end end end diff --git a/db/migrate/20200728004302_drop_path_whitelist_from_embeddable_hosts.rb b/db/migrate/20200728004302_drop_path_whitelist_from_embeddable_hosts.rb index 56771b4c07..1a85e38c8e 100644 --- a/db/migrate/20200728004302_drop_path_whitelist_from_embeddable_hosts.rb +++ b/db/migrate/20200728004302_drop_path_whitelist_from_embeddable_hosts.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropPathWhitelistFromEmbeddableHosts < ActiveRecord::Migration[6.0] - DROPPED_COLUMNS ||= { - embeddable_hosts: %i{path_whitelist} - } + DROPPED_COLUMNS ||= { embeddable_hosts: %i[path_whitelist] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20200730205554_create_group_default_tracking.rb b/db/migrate/20200730205554_create_group_default_tracking.rb index 1a1ac25a99..fc727f7b71 100644 --- a/db/migrate/20200730205554_create_group_default_tracking.rb +++ b/db/migrate/20200730205554_create_group_default_tracking.rb @@ -9,9 +9,9 @@ class CreateGroupDefaultTracking < ActiveRecord::Migration[6.0] end add_index :group_category_notification_defaults, - [:group_id, :category_id], - unique: true, - name: :idx_group_category_notification_defaults_unique + %i[group_id category_id], + unique: true, + name: :idx_group_category_notification_defaults_unique create_table :group_tag_notification_defaults do |t| t.integer :group_id, null: false @@ -20,8 +20,8 @@ class CreateGroupDefaultTracking < ActiveRecord::Migration[6.0] end add_index :group_tag_notification_defaults, - [:group_id, :tag_id], - unique: true, - name: :idx_group_tag_notification_defaults_unique + %i[group_id tag_id], + unique: true, + name: :idx_group_tag_notification_defaults_unique end end diff --git a/db/migrate/20200810194943_change_selectable_avatars_site_setting.rb b/db/migrate/20200810194943_change_selectable_avatars_site_setting.rb index a756615008..8e389843cb 100644 --- a/db/migrate/20200810194943_change_selectable_avatars_site_setting.rb +++ b/db/migrate/20200810194943_change_selectable_avatars_site_setting.rb @@ -2,7 +2,8 @@ class ChangeSelectableAvatarsSiteSetting < ActiveRecord::Migration[6.0] def up - selectable_avatars = execute("SELECT value FROM site_settings WHERE name = 'selectable_avatars'") + selectable_avatars = + execute("SELECT value FROM site_settings WHERE name = 'selectable_avatars'") return if selectable_avatars.cmd_tuples == 0 # Keep old site setting value as a backup @@ -15,22 +16,28 @@ class ChangeSelectableAvatarsSiteSetting < ActiveRecord::Migration[6.0] # Extract SHA1s from URLs and then use them for upload ID lookups urls = [] sha1s = [] - selectable_avatars.first["value"].split("\n").each do |url| - match = url.match(/(\/original\/\dX[\/\.\w]*\/(\h+)[\.\w]*)/) - if match.present? - urls << match[1] - sha1s << match[2] - else - STDERR.puts "Could not extract a SHA1 from #{url}" + selectable_avatars.first["value"] + .split("\n") + .each do |url| + match = url.match(%r{(/original/\dX[/\.\w]*/(\h+)[\.\w]*)}) + if match.present? + urls << match[1] + sha1s << match[2] + else + STDERR.puts "Could not extract a SHA1 from #{url}" + end end - end # Ensure at least one URL or SHA1 exists so the query below can be valid return if urls.size == 0 && sha1s.size == 0 uploads_query = [] - uploads_query << "url IN (#{urls.map { |url| ActiveRecord::Base.connection.quote(url) }.join(',')})" if urls.size > 0 - uploads_query << "sha1 IN (#{sha1s.map { |sha1| ActiveRecord::Base.connection.quote(sha1) }.join(',')})" if sha1s.size > 0 + if urls.size > 0 + uploads_query << "url IN (#{urls.map { |url| ActiveRecord::Base.connection.quote(url) }.join(",")})" + end + if sha1s.size > 0 + uploads_query << "sha1 IN (#{sha1s.map { |sha1| ActiveRecord::Base.connection.quote(sha1) }.join(",")})" + end uploads_query = "SELECT DISTINCT id FROM uploads WHERE #{uploads_query.join(" OR ")}" upload_ids = execute(uploads_query).map { |row| row["id"] } @@ -46,6 +53,8 @@ class ChangeSelectableAvatarsSiteSetting < ActiveRecord::Migration[6.0] def down execute("DELETE FROM site_settings WHERE name = 'selectable_avatars'") - execute("UPDATE site_settings SET name = 'selectable_avatars' WHERE name = 'selectable_avatars_urls'") + execute( + "UPDATE site_settings SET name = 'selectable_avatars' WHERE name = 'selectable_avatars_urls'", + ) end end diff --git a/db/migrate/20200818084329_update_private_message_on_post_search_data.rb b/db/migrate/20200818084329_update_private_message_on_post_search_data.rb index cab2c49ebf..1d42b1dec6 100644 --- a/db/migrate/20200818084329_update_private_message_on_post_search_data.rb +++ b/db/migrate/20200818084329_update_private_message_on_post_search_data.rb @@ -5,7 +5,6 @@ class UpdatePrivateMessageOnPostSearchData < ActiveRecord::Migration[6.0] disable_ddl_transaction! def update_private_message_flag - sql = <<~SQL UPDATE post_search_data SET private_message = X.private_message diff --git a/db/migrate/20200819030609_migrate_user_topic_timers_to_bookmark_reminders.rb b/db/migrate/20200819030609_migrate_user_topic_timers_to_bookmark_reminders.rb index 2c9ddbc6b6..8699051eda 100644 --- a/db/migrate/20200819030609_migrate_user_topic_timers_to_bookmark_reminders.rb +++ b/db/migrate/20200819030609_migrate_user_topic_timers_to_bookmark_reminders.rb @@ -15,9 +15,8 @@ class MigrateUserTopicTimersToBookmarkReminders < ActiveRecord::Migration[6.0] return if topic_timers_to_migrate.empty? - topic_timer_tuples = topic_timers_to_migrate.map do |tt| - "(#{tt.user_id}, #{tt.topic_id})" - end.join(", ") + topic_timer_tuples = + topic_timers_to_migrate.map { |tt| "(#{tt.user_id}, #{tt.topic_id})" }.join(", ") existing_bookmarks = DB.query(<<~SQL) SELECT bookmarks.id, reminder_at, reminder_type, @@ -29,12 +28,12 @@ class MigrateUserTopicTimersToBookmarkReminders < ActiveRecord::Migration[6.0] new_bookmarks = [] topic_timers_to_migrate.each do |tt| - bookmark = existing_bookmarks.find do |bm| - - # we only care about existing topic-level bookmarks here - # because topic timers are (funnily enough) topic-level - bm.topic_id == tt.topic_id && bm.user_id == tt.user_id && bm.post_number == 1 - end + bookmark = + existing_bookmarks.find do |bm| + # we only care about existing topic-level bookmarks here + # because topic timers are (funnily enough) topic-level + bm.topic_id == tt.topic_id && bm.user_id == tt.user_id && bm.post_number == 1 + end if !bookmark # create one @@ -45,7 +44,7 @@ class MigrateUserTopicTimersToBookmarkReminders < ActiveRecord::Migration[6.0] DB.exec( "UPDATE bookmarks SET reminder_at = :reminder_at, reminder_type = 6 WHERE id = :bookmark_id", reminder_at: tt.execute_at, - bookmark_id: bookmark.id + bookmark_id: bookmark.id, ) end @@ -69,7 +68,7 @@ class MigrateUserTopicTimersToBookmarkReminders < ActiveRecord::Migration[6.0] "UPDATE topic_timers SET deleted_at = :deleted_at, deleted_by_id = :deleted_by WHERE ID IN (:ids)", ids: topic_timers_to_migrate_ids, deleted_at: Time.zone.now, - deleted_by: Discourse.system_user + deleted_by: Discourse.system_user, ) end diff --git a/db/migrate/20200820174703_add_partial_target_id_index_to_reviewables.rb b/db/migrate/20200820174703_add_partial_target_id_index_to_reviewables.rb index 55d2bd81ac..f2148411a0 100644 --- a/db/migrate/20200820174703_add_partial_target_id_index_to_reviewables.rb +++ b/db/migrate/20200820174703_add_partial_target_id_index_to_reviewables.rb @@ -4,6 +4,10 @@ class AddPartialTargetIdIndexToReviewables < ActiveRecord::Migration[6.0] disable_ddl_transaction! def change - add_index :reviewables, [:target_id], where: "target_type = 'Post'", algorithm: :concurrently, name: "index_reviewables_on_target_id_where_post_type_eq_post" + add_index :reviewables, + [:target_id], + where: "target_type = 'Post'", + algorithm: :concurrently, + name: "index_reviewables_on_target_id_where_post_type_eq_post" end end diff --git a/db/migrate/20200902054531_add_first_unread_pm_a_to_group_user.rb b/db/migrate/20200902054531_add_first_unread_pm_a_to_group_user.rb index 8cfb624a11..89e0fbdb56 100644 --- a/db/migrate/20200902054531_add_first_unread_pm_a_to_group_user.rb +++ b/db/migrate/20200902054531_add_first_unread_pm_a_to_group_user.rb @@ -2,7 +2,11 @@ class AddFirstUnreadPmAToGroupUser < ActiveRecord::Migration[6.0] def up - add_column :group_users, :first_unread_pm_at, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :group_users, + :first_unread_pm_at, + :datetime, + null: false, + default: -> { "CURRENT_TIMESTAMP" } execute <<~SQL UPDATE group_users gu diff --git a/db/migrate/20200902082203_add_first_unread_pm_at_to_user_stats.rb b/db/migrate/20200902082203_add_first_unread_pm_at_to_user_stats.rb index 6f4c8e812a..f7cb270419 100644 --- a/db/migrate/20200902082203_add_first_unread_pm_at_to_user_stats.rb +++ b/db/migrate/20200902082203_add_first_unread_pm_at_to_user_stats.rb @@ -2,7 +2,11 @@ class AddFirstUnreadPmAtToUserStats < ActiveRecord::Migration[6.0] def up - add_column :user_stats, :first_unread_pm_at, :datetime, null: false, default: -> { 'CURRENT_TIMESTAMP' } + add_column :user_stats, + :first_unread_pm_at, + :datetime, + null: false, + default: -> { "CURRENT_TIMESTAMP" } execute <<~SQL UPDATE user_stats us diff --git a/db/migrate/20200910051633_change_uploads_verified_to_integer.rb b/db/migrate/20200910051633_change_uploads_verified_to_integer.rb index 785817b966..18c19ee1e0 100644 --- a/db/migrate/20200910051633_change_uploads_verified_to_integer.rb +++ b/db/migrate/20200910051633_change_uploads_verified_to_integer.rb @@ -5,15 +5,13 @@ class ChangeUploadsVerifiedToInteger < ActiveRecord::Migration[6.0] add_column :uploads, :verification_status, :integer, null: false, default: 1 Migration::ColumnDropper.mark_readonly(:uploads, :verified) - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE uploads SET verification_status = CASE WHEN verified THEN 2 WHEN NOT verified THEN 3 ELSE 1 END SQL - ) end def down diff --git a/db/migrate/20200916085541_create_user_ip_address_histories.rb b/db/migrate/20200916085541_create_user_ip_address_histories.rb index d9d50b8aed..908fd414cc 100644 --- a/db/migrate/20200916085541_create_user_ip_address_histories.rb +++ b/db/migrate/20200916085541_create_user_ip_address_histories.rb @@ -9,7 +9,7 @@ class CreateUserIpAddressHistories < ActiveRecord::Migration[6.0] t.timestamps end - add_index :user_ip_address_histories, [:user_id, :ip_address], unique: true + add_index :user_ip_address_histories, %i[user_id ip_address], unique: true end def down diff --git a/db/migrate/20201006021020_add_requested_by_to_email_change_request.rb b/db/migrate/20201006021020_add_requested_by_to_email_change_request.rb index 6753a18975..8a76deba1d 100644 --- a/db/migrate/20201006021020_add_requested_by_to_email_change_request.rb +++ b/db/migrate/20201006021020_add_requested_by_to_email_change_request.rb @@ -4,7 +4,9 @@ class AddRequestedByToEmailChangeRequest < ActiveRecord::Migration[6.0] def up add_column :email_change_requests, :requested_by_user_id, :integer, null: true - DB.exec("CREATE INDEX IF NOT EXISTS idx_email_change_requests_on_requested_by ON email_change_requests(requested_by_user_id)") + DB.exec( + "CREATE INDEX IF NOT EXISTS idx_email_change_requests_on_requested_by ON email_change_requests(requested_by_user_id)", + ) end def down diff --git a/db/migrate/20201027110546_create_linked_topics.rb b/db/migrate/20201027110546_create_linked_topics.rb index 0f3c8497d6..162e623a1c 100644 --- a/db/migrate/20201027110546_create_linked_topics.rb +++ b/db/migrate/20201027110546_create_linked_topics.rb @@ -10,7 +10,7 @@ class CreateLinkedTopics < ActiveRecord::Migration[6.0] t.timestamps end - add_index :linked_topics, [:topic_id, :original_topic_id], unique: true - add_index :linked_topics, [:topic_id, :sequence], unique: true + add_index :linked_topics, %i[topic_id original_topic_id], unique: true + add_index :linked_topics, %i[topic_id sequence], unique: true end end diff --git a/db/migrate/20201105190351_move_post_notices_to_json.rb b/db/migrate/20201105190351_move_post_notices_to_json.rb index 82322ab7b6..b586cb6406 100644 --- a/db/migrate/20201105190351_move_post_notices_to_json.rb +++ b/db/migrate/20201105190351_move_post_notices_to_json.rb @@ -21,7 +21,11 @@ class MovePostNoticesToJson < ActiveRecord::Migration[6.0] execute "DELETE FROM post_custom_fields WHERE name = 'notice_type' OR name = 'notice_args'" - add_index :post_custom_fields, :post_id, unique: true, name: "index_post_custom_fields_on_notice", where: "name = 'notice'" + add_index :post_custom_fields, + :post_id, + unique: true, + name: "index_post_custom_fields_on_notice", + where: "name = 'notice'" remove_index :post_custom_fields, name: "index_post_custom_fields_on_notice_type" remove_index :post_custom_fields, name: "index_post_custom_fields_on_notice_args" @@ -44,8 +48,16 @@ class MovePostNoticesToJson < ActiveRecord::Migration[6.0] execute "DELETE FROM post_custom_fields WHERE name = 'notice'" - add_index :post_custom_fields, :post_id, unique: true, name: "index_post_custom_fields_on_notice_type", where: "name = 'notice_type'" - add_index :post_custom_fields, :post_id, unique: true, name: "index_post_custom_fields_on_notice_args", where: "name = 'notice_args'" + add_index :post_custom_fields, + :post_id, + unique: true, + name: "index_post_custom_fields_on_notice_type", + where: "name = 'notice_type'" + add_index :post_custom_fields, + :post_id, + unique: true, + name: "index_post_custom_fields_on_notice_args", + where: "name = 'notice_args'" remove_index :index_post_custom_fields_on_notice end diff --git a/db/migrate/20201109170951_migrate_github_user_infos.rb b/db/migrate/20201109170951_migrate_github_user_infos.rb index b93d9a4d4b..5bab9989b6 100644 --- a/db/migrate/20201109170951_migrate_github_user_infos.rb +++ b/db/migrate/20201109170951_migrate_github_user_infos.rb @@ -29,14 +29,12 @@ class MigrateGithubUserInfos < ActiveRecord::Migration[6.0] FROM github_user_infos SQL - if maintain_ids - execute <<~SQL + execute <<~SQL if maintain_ids SELECT setval( pg_get_serial_sequence('user_associated_accounts', 'id'), (select greatest(max(id), 1) from user_associated_accounts) ); SQL - end end def down diff --git a/db/migrate/20201110110952_drop_github_user_infos.rb b/db/migrate/20201110110952_drop_github_user_infos.rb index 7fbbb78b95..275daeb035 100644 --- a/db/migrate/20201110110952_drop_github_user_infos.rb +++ b/db/migrate/20201110110952_drop_github_user_infos.rb @@ -1,14 +1,12 @@ # frozen_string_literal: true -require 'migration/table_dropper' +require "migration/table_dropper" class DropGithubUserInfos < ActiveRecord::Migration[6.0] - DROPPED_TABLES ||= %i{ github_user_infos } + DROPPED_TABLES ||= %i[github_user_infos] def up - DROPPED_TABLES.each do |table| - Migration::TableDropper.execute_drop(table) - end + DROPPED_TABLES.each { |table| Migration::TableDropper.execute_drop(table) } end def down diff --git a/db/migrate/20201117212328_set_category_slug_to_lower.rb b/db/migrate/20201117212328_set_category_slug_to_lower.rb index 8d7d275645..924de41bcc 100644 --- a/db/migrate/20201117212328_set_category_slug_to_lower.rb +++ b/db/migrate/20201117212328_set_category_slug_to_lower.rb @@ -2,7 +2,7 @@ class SetCategorySlugToLower < ActiveRecord::Migration[6.0] def up - remove_index(:categories, name: 'unique_index_categories_on_slug') + remove_index(:categories, name: "unique_index_categories_on_slug") categories = DB.query("SELECT id, name, slug, parent_category_id FROM categories") old_slugs = categories.map { |c| [c.id, c.slug] }.to_h @@ -10,23 +10,26 @@ class SetCategorySlugToLower < ActiveRecord::Migration[6.0] # Resolve duplicate tags by replacing mixed case slugs with new ones # extracted from category names - slugs = categories - .filter { |category| category.slug.present? } - .group_by { |category| [category.parent_category_id, category.slug.downcase] } - .map { |slug, cats| [slug, cats.size] } - .to_h + slugs = + categories + .filter { |category| category.slug.present? } + .group_by { |category| [category.parent_category_id, category.slug.downcase] } + .map { |slug, cats| [slug, cats.size] } + .to_h categories.each do |category| old_parent_and_slug = [category.parent_category_id, category.slug.downcase] - next if category.slug.blank? || - category.slug == category.slug.downcase || - slugs[old_parent_and_slug] <= 1 + if category.slug.blank? || category.slug == category.slug.downcase || + slugs[old_parent_and_slug] <= 1 + next + end - new_slug = category.name.parameterize.tr("_", "-").squeeze('-').gsub(/\A-+|-+\z/, '')[0..255] - new_slug = '' if (new_slug =~ /[^\d]/).blank? + new_slug = category.name.parameterize.tr("_", "-").squeeze("-").gsub(/\A-+|-+\z/, "")[0..255] + new_slug = "" if (new_slug =~ /[^\d]/).blank? new_parent_and_slug = [category.parent_category_id, new_slug] - next if new_slug.blank? || - (slugs[new_parent_and_slug].present? && slugs[new_parent_and_slug] > 0) + if new_slug.blank? || (slugs[new_parent_and_slug].present? && slugs[new_parent_and_slug] > 0) + next + end updates[category.id] = category.slug = new_slug slugs[old_parent_and_slug] -= 1 @@ -34,52 +37,52 @@ class SetCategorySlugToLower < ActiveRecord::Migration[6.0] end # Reset left conflicting slugs - slugs = categories - .filter { |category| category.slug.present? } - .group_by { |category| [category.parent_category_id, category.slug.downcase] } - .map { |slug, cats| [slug, cats.size] } - .to_h + slugs = + categories + .filter { |category| category.slug.present? } + .group_by { |category| [category.parent_category_id, category.slug.downcase] } + .map { |slug, cats| [slug, cats.size] } + .to_h categories.each do |category| old_parent_and_slug = [category.parent_category_id, category.slug.downcase] - next if category.slug.blank? || - category.slug == category.slug.downcase || - slugs[old_parent_and_slug] <= 1 + if category.slug.blank? || category.slug == category.slug.downcase || + slugs[old_parent_and_slug] <= 1 + next + end - updates[category.id] = category.slug = '' + updates[category.id] = category.slug = "" slugs[old_parent_and_slug] -= 1 end # Update all category slugs - updates.each do |id, slug| - execute <<~SQL + updates.each { |id, slug| execute <<~SQL } UPDATE categories SET slug = '#{PG::Connection.escape_string(slug)}' WHERE id = #{id} -- #{PG::Connection.escape_string(old_slugs[id])} SQL - end # Ensure all slugs are lowercase execute "UPDATE categories SET slug = LOWER(slug)" add_index( :categories, - 'COALESCE(parent_category_id, -1), LOWER(slug)', - name: 'unique_index_categories_on_slug', + "COALESCE(parent_category_id, -1), LOWER(slug)", + name: "unique_index_categories_on_slug", where: "slug != ''", - unique: true + unique: true, ) end def down - remove_index(:categories, name: 'unique_index_categories_on_slug') + remove_index(:categories, name: "unique_index_categories_on_slug") add_index( :categories, - 'COALESCE(parent_category_id, -1), slug', - name: 'unique_index_categories_on_slug', + "COALESCE(parent_category_id, -1), slug", + name: "unique_index_categories_on_slug", where: "slug != ''", - unique: true + unique: true, ) end end diff --git a/db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb b/db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb index 5d3f6e6d01..30b79f8394 100644 --- a/db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb +++ b/db/migrate/20210125100452_migrate_search_data_after_default_locale_rename.rb @@ -4,7 +4,7 @@ class MigrateSearchDataAfterDefaultLocaleRename < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - %w{category tag topic user}.each { |model| fix_search_data(model) } + %w[category tag topic user].each { |model| fix_search_data(model) } end def down diff --git a/db/migrate/20210131221311_create_dismissed_topic_users_table.rb b/db/migrate/20210131221311_create_dismissed_topic_users_table.rb index 699e05deed..d0a9e45cfe 100644 --- a/db/migrate/20210131221311_create_dismissed_topic_users_table.rb +++ b/db/migrate/20210131221311_create_dismissed_topic_users_table.rb @@ -7,6 +7,6 @@ class CreateDismissedTopicUsersTable < ActiveRecord::Migration[6.0] t.integer :topic_id t.datetime :created_at end - add_index :dismissed_topic_users, %i(user_id topic_id), unique: true + add_index :dismissed_topic_users, %i[user_id topic_id], unique: true end end diff --git a/db/migrate/20210201034048_move_category_last_seen_at_to_new_table.rb b/db/migrate/20210201034048_move_category_last_seen_at_to_new_table.rb index 2e61a42aa7..7221b3542c 100644 --- a/db/migrate/20210201034048_move_category_last_seen_at_to_new_table.rb +++ b/db/migrate/20210201034048_move_category_last_seen_at_to_new_table.rb @@ -21,13 +21,16 @@ class MoveCategoryLastSeenAtToNewTable < ActiveRecord::Migration[6.0] ORDER BY topics.created_at DESC LIMIT :max_new_topics SQL - sql = DB.sql_fragment(sql, - now: DateTime.now, - last_visit: User::NewTopicDuration::LAST_VISIT, - always: User::NewTopicDuration::ALWAYS, - default_duration: SiteSetting.default_other_new_topic_duration_minutes, - min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, - max_new_topics: SiteSetting.max_new_topics) + sql = + DB.sql_fragment( + sql, + now: DateTime.now, + last_visit: User::NewTopicDuration::LAST_VISIT, + always: User::NewTopicDuration::ALWAYS, + default_duration: SiteSetting.default_other_new_topic_duration_minutes, + min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, + max_new_topics: SiteSetting.max_new_topics, + ) DB.exec(sql) end diff --git a/db/migrate/20210203031628_add_duration_minutes_to_topic_timer.rb b/db/migrate/20210203031628_add_duration_minutes_to_topic_timer.rb index 3df0d3ad08..02ae77d0e0 100644 --- a/db/migrate/20210203031628_add_duration_minutes_to_topic_timer.rb +++ b/db/migrate/20210203031628_add_duration_minutes_to_topic_timer.rb @@ -6,8 +6,12 @@ class AddDurationMinutesToTopicTimer < ActiveRecord::Migration[6.0] # 7 is delete_replies type, this duration is measured in days, the other # duration is measured in hours - DB.exec("UPDATE topic_timers SET duration_minutes = (duration * 60 * 24) WHERE duration_minutes != duration AND status_type = 7 AND duration IS NOT NULL") - DB.exec("UPDATE topic_timers SET duration_minutes = (duration * 60) WHERE duration_minutes != duration AND status_type != 7 AND duration IS NOT NULL") + DB.exec( + "UPDATE topic_timers SET duration_minutes = (duration * 60 * 24) WHERE duration_minutes != duration AND status_type = 7 AND duration IS NOT NULL", + ) + DB.exec( + "UPDATE topic_timers SET duration_minutes = (duration * 60) WHERE duration_minutes != duration AND status_type != 7 AND duration IS NOT NULL", + ) end def down diff --git a/db/migrate/20210204135429_rename_sso_site_settings.rb b/db/migrate/20210204135429_rename_sso_site_settings.rb index a793e9fb80..2d2ef12072 100644 --- a/db/migrate/20210204135429_rename_sso_site_settings.rb +++ b/db/migrate/20210204135429_rename_sso_site_settings.rb @@ -2,39 +2,37 @@ class RenameSsoSiteSettings < ActiveRecord::Migration[6.0] RENAME_SETTINGS = [ - ['enable_sso', 'enable_discourse_connect'], - ['sso_allows_all_return_paths', 'discourse_connect_allows_all_return_paths'], - ['enable_sso_provider', 'enable_discourse_connect_provider'], - ['verbose_sso_logging', 'verbose_discourse_connect_logging'], - ['sso_url', 'discourse_connect_url'], - ['sso_secret', 'discourse_connect_secret'], - ['sso_provider_secrets', 'discourse_connect_provider_secrets'], - ['sso_overrides_groups', 'discourse_connect_overrides_groups'], - ['sso_overrides_bio', 'discourse_connect_overrides_bio'], - ['sso_overrides_email', 'auth_overrides_email'], - ['sso_overrides_username', 'auth_overrides_username'], - ['sso_overrides_name', 'auth_overrides_name'], - ['sso_overrides_avatar', 'discourse_connect_overrides_avatar'], - ['sso_overrides_profile_background', 'discourse_connect_overrides_profile_background'], - ['sso_overrides_location', 'discourse_connect_overrides_location'], - ['sso_overrides_website', 'discourse_connect_overrides_website'], - ['sso_overrides_card_background', 'discourse_connect_overrides_card_background'], - ['external_auth_skip_create_confirm', 'auth_skip_create_confirm'], - ['external_auth_immediately', 'auth_immediately'] + %w[enable_sso enable_discourse_connect], + %w[sso_allows_all_return_paths discourse_connect_allows_all_return_paths], + %w[enable_sso_provider enable_discourse_connect_provider], + %w[verbose_sso_logging verbose_discourse_connect_logging], + %w[sso_url discourse_connect_url], + %w[sso_secret discourse_connect_secret], + %w[sso_provider_secrets discourse_connect_provider_secrets], + %w[sso_overrides_groups discourse_connect_overrides_groups], + %w[sso_overrides_bio discourse_connect_overrides_bio], + %w[sso_overrides_email auth_overrides_email], + %w[sso_overrides_username auth_overrides_username], + %w[sso_overrides_name auth_overrides_name], + %w[sso_overrides_avatar discourse_connect_overrides_avatar], + %w[sso_overrides_profile_background discourse_connect_overrides_profile_background], + %w[sso_overrides_location discourse_connect_overrides_location], + %w[sso_overrides_website discourse_connect_overrides_website], + %w[sso_overrides_card_background discourse_connect_overrides_card_background], + %w[external_auth_skip_create_confirm auth_skip_create_confirm], + %w[external_auth_immediately auth_immediately], ] def up # Copying the rows so that things keep working during deploy # They will be dropped in post_migrate/20210219171329_drop_old_sso_site_settings - RENAME_SETTINGS.each do |old_name, new_name| - execute <<~SQL + RENAME_SETTINGS.each { |old_name, new_name| execute <<~SQL } INSERT INTO site_settings (name, data_type, value, created_at, updated_at) SELECT '#{new_name}', data_type, value, created_at, updated_at FROM site_settings WHERE name = '#{old_name}' SQL - end end def down diff --git a/db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb b/db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb index 22ecc3094f..14baafee7c 100644 --- a/db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb +++ b/db/migrate/20210207232853_fix_topic_timer_duration_minutes.rb @@ -4,8 +4,12 @@ class FixTopicTimerDurationMinutes < ActiveRecord::Migration[6.0] DB.exec("DELETE FROM topic_timers WHERE status_type = 7 AND duration > 20 * 365") DB.exec("DELETE FROM topic_timers WHERE status_type != 7 AND duration > 20 * 365 * 24") - DB.exec("UPDATE topic_timers SET duration_minutes = (duration * 60 * 24) WHERE duration_minutes IS NULL AND status_type = 7 AND duration IS NOT NULL") - DB.exec("UPDATE topic_timers SET duration_minutes = (duration * 60) WHERE duration_minutes IS NULL AND status_type != 7 AND duration IS NOT NULL") + DB.exec( + "UPDATE topic_timers SET duration_minutes = (duration * 60 * 24) WHERE duration_minutes IS NULL AND status_type = 7 AND duration IS NOT NULL", + ) + DB.exec( + "UPDATE topic_timers SET duration_minutes = (duration * 60) WHERE duration_minutes IS NULL AND status_type != 7 AND duration IS NOT NULL", + ) end def down diff --git a/db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb b/db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb index 3232bacb6f..0eae24bae2 100644 --- a/db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb +++ b/db/migrate/20210215231312_fix_group_flair_avatar_upload_security_and_acls.rb @@ -4,12 +4,11 @@ class FixGroupFlairAvatarUploadSecurityAndAcls < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - upload_ids = DB.query_single(<<~SQL + upload_ids = DB.query_single(<<~SQL) SELECT flair_upload_id FROM groups WHERE flair_upload_id IS NOT NULL SQL - ) if upload_ids.any? reason = "group_flair fixup migration" @@ -19,7 +18,8 @@ class FixGroupFlairAvatarUploadSecurityAndAcls < ActiveRecord::Migration[6.0] SQL if Discourse.store.external? - uploads = Upload.where(id: upload_ids, secure: false).where("updated_at = security_last_changed_at") + uploads = + Upload.where(id: upload_ids, secure: false).where("updated_at = security_last_changed_at") uploads.each do |upload| Discourse.store.update_upload_ACL(upload) upload.touch diff --git a/db/migrate/20210218022739_move_new_since_to_new_table_again.rb b/db/migrate/20210218022739_move_new_since_to_new_table_again.rb index f468d09ff7..bc102f5c76 100644 --- a/db/migrate/20210218022739_move_new_since_to_new_table_again.rb +++ b/db/migrate/20210218022739_move_new_since_to_new_table_again.rb @@ -47,19 +47,20 @@ class MoveNewSinceToNewTableAgain < ActiveRecord::Migration[6.0] ON CONFLICT DO NOTHING SQL - DB.exec(sql, - now: DateTime.now, - last_visit: User::NewTopicDuration::LAST_VISIT, - always: User::NewTopicDuration::ALWAYS, - default_duration: SiteSetting.default_other_new_topic_duration_minutes, - min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, - private_message: Archetype.private_message, - min_id: min_id, - max_id: max_id, - max_new_topics: SiteSetting.max_new_topics) + DB.exec( + sql, + now: DateTime.now, + last_visit: User::NewTopicDuration::LAST_VISIT, + always: User::NewTopicDuration::ALWAYS, + default_duration: SiteSetting.default_other_new_topic_duration_minutes, + min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime, + private_message: Archetype.private_message, + min_id: min_id, + max_id: max_id, + max_new_topics: SiteSetting.max_new_topics, + ) offset += BATCH_SIZE - end end diff --git a/db/migrate/20210308195916_add_unique_index_to_invited_groups.rb b/db/migrate/20210308195916_add_unique_index_to_invited_groups.rb index c3f8b60769..4804d59b64 100644 --- a/db/migrate/20210308195916_add_unique_index_to_invited_groups.rb +++ b/db/migrate/20210308195916_add_unique_index_to_invited_groups.rb @@ -10,6 +10,6 @@ class AddUniqueIndexToInvitedGroups < ActiveRecord::Migration[6.0] AND a.group_id = b.group_id SQL - add_index :invited_groups, [:group_id, :invite_id], unique: true + add_index :invited_groups, %i[group_id invite_id], unique: true end end diff --git a/db/migrate/20210311070755_add_image_upload_id_to_badges.rb b/db/migrate/20210311070755_add_image_upload_id_to_badges.rb index c9d15ee030..d1be9ee2a9 100644 --- a/db/migrate/20210311070755_add_image_upload_id_to_badges.rb +++ b/db/migrate/20210311070755_add_image_upload_id_to_badges.rb @@ -3,9 +3,7 @@ class AddImageUploadIdToBadges < ActiveRecord::Migration[6.0] def change add_column :badges, :image_upload_id, :integer - reversible do |dir| - dir.up do - DB.exec <<~SQL + reversible { |dir| dir.up { DB.exec <<~SQL } } UPDATE badges b1 SET image_upload_id = u.id FROM ( @@ -19,7 +17,5 @@ class AddImageUploadIdToBadges < ActiveRecord::Migration[6.0] WHERE b1.id = b2.id SQL - end - end end end diff --git a/db/migrate/20210315173137_set_disable_mailing_list_mode.rb b/db/migrate/20210315173137_set_disable_mailing_list_mode.rb index 83abd1d93d..d0b35f5040 100644 --- a/db/migrate/20210315173137_set_disable_mailing_list_mode.rb +++ b/db/migrate/20210315173137_set_disable_mailing_list_mode.rb @@ -3,13 +3,11 @@ class SetDisableMailingListMode < ActiveRecord::Migration[6.0] def up result = execute "SELECT COUNT(*) FROM user_options WHERE mailing_list_mode" - if result.first['count'] > 0 - execute <<~SQL + execute <<~SQL if result.first["count"] > 0 INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES('disable_mailing_list_mode', 5, 'f', NOW(), NOW()) ON CONFLICT (name) DO NOTHING SQL - end end def down diff --git a/db/migrate/20210406060434_fix_topic_user_bookmarked_sync_issues_again.rb b/db/migrate/20210406060434_fix_topic_user_bookmarked_sync_issues_again.rb index 41aedc5554..94c0895b72 100644 --- a/db/migrate/20210406060434_fix_topic_user_bookmarked_sync_issues_again.rb +++ b/db/migrate/20210406060434_fix_topic_user_bookmarked_sync_issues_again.rb @@ -4,20 +4,16 @@ class FixTopicUserBookmarkedSyncIssuesAgain < ActiveRecord::Migration[6.0] disable_ddl_transaction! def up - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE topic_users SET bookmarked = true FROM bookmarks AS b WHERE NOT topic_users.bookmarked AND topic_users.topic_id = b.topic_id AND topic_users.user_id = b.user_id SQL - ) - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE topic_users SET bookmarked = false WHERE topic_users.bookmarked AND (SELECT COUNT(*) FROM bookmarks WHERE topic_id = topic_users.topic_id AND user_id = topic_users.user_id) = 0 SQL - ) end def down diff --git a/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb b/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb index 85fd823813..13c74377d7 100644 --- a/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb +++ b/db/migrate/20210414013318_add_category_setting_allow_unlimited_owner_edits_op.rb @@ -2,6 +2,10 @@ class AddCategorySettingAllowUnlimitedOwnerEditsOp < ActiveRecord::Migration[6.0] def change - add_column :categories, :allow_unlimited_owner_edits_on_first_post, :boolean, default: false, null: false + add_column :categories, + :allow_unlimited_owner_edits_on_first_post, + :boolean, + default: false, + null: false end end diff --git a/db/migrate/20210527114834_set_tagging_enabled.rb b/db/migrate/20210527114834_set_tagging_enabled.rb index 73e9fa7447..8e2dd92db4 100644 --- a/db/migrate/20210527114834_set_tagging_enabled.rb +++ b/db/migrate/20210527114834_set_tagging_enabled.rb @@ -10,13 +10,11 @@ class SetTaggingEnabled < ActiveRecord::Migration[6.1] SQL # keep tagging disabled for existing sites - if result.first['created_at'].to_datetime < 1.hour.ago - execute <<~SQL + execute <<~SQL if result.first["created_at"].to_datetime < 1.hour.ago INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES('tagging_enabled', 5, 'f', NOW(), NOW()) ON CONFLICT (name) DO NOTHING SQL - end end def down diff --git a/db/migrate/20210527131318_create_directory_columns.rb b/db/migrate/20210527131318_create_directory_columns.rb index 81fa5421d1..ec1a31d96e 100644 --- a/db/migrate/20210527131318_create_directory_columns.rb +++ b/db/migrate/20210527131318_create_directory_columns.rb @@ -9,10 +9,10 @@ class CreateDirectoryColumns < ActiveRecord::Migration[6.1] t.boolean :automatic, null: false t.boolean :enabled, null: false t.integer :position, null: false - t.datetime :created_at, default: -> { 'CURRENT_TIMESTAMP' } + t.datetime :created_at, default: -> { "CURRENT_TIMESTAMP" } end - add_index :directory_columns, [:enabled, :position, :user_field_id], name: "directory_column_index" + add_index :directory_columns, %i[enabled position user_field_id], name: "directory_column_index" create_automatic_columns end @@ -22,8 +22,7 @@ class CreateDirectoryColumns < ActiveRecord::Migration[6.1] end def create_automatic_columns - DB.exec( - <<~SQL + DB.exec(<<~SQL) INSERT INTO directory_columns ( name, automatic, enabled, automatic_position, position, icon ) @@ -36,6 +35,5 @@ class CreateDirectoryColumns < ActiveRecord::Migration[6.1] ( 'posts_read', true, true, 6, 6, NULL ), ( 'days_visited', true, true, 7, 7, NULL ); SQL - ) end end diff --git a/db/migrate/20210528003603_fix_badge_image_avatar_upload_security_and_acls.rb b/db/migrate/20210528003603_fix_badge_image_avatar_upload_security_and_acls.rb index e41c2f6d3d..8bd5e95091 100644 --- a/db/migrate/20210528003603_fix_badge_image_avatar_upload_security_and_acls.rb +++ b/db/migrate/20210528003603_fix_badge_image_avatar_upload_security_and_acls.rb @@ -4,13 +4,12 @@ class FixBadgeImageAvatarUploadSecurityAndAcls < ActiveRecord::Migration[6.1] disable_ddl_transaction! def up - upload_ids = DB.query_single(<<~SQL + upload_ids = DB.query_single(<<~SQL) SELECT image_upload_id FROM badges INNER JOIN uploads ON uploads.id = badges.image_upload_id WHERE image_upload_id IS NOT NULL AND uploads.secure SQL - ) if upload_ids.any? reason = "badge_image fixup migration" diff --git a/db/migrate/20210601002145_rename_trust_level_translations.rb b/db/migrate/20210601002145_rename_trust_level_translations.rb index b5cb17fd74..a974455d4d 100644 --- a/db/migrate/20210601002145_rename_trust_level_translations.rb +++ b/db/migrate/20210601002145_rename_trust_level_translations.rb @@ -4,22 +4,18 @@ class RenameTrustLevelTranslations < ActiveRecord::Migration[6.1] KEYS = %w[newuser basic member regular leader] def up - KEYS.each do |key| - execute <<~SQL + KEYS.each { |key| execute <<~SQL } UPDATE translation_overrides SET translation_key = 'js.trust_levels.names.#{key}' WHERE translation_key = 'trust_levels.#{key}.title' SQL - end end def down - KEYS.each do |key| - execute <<~SQL + KEYS.each { |key| execute <<~SQL } UPDATE translation_overrides SET translation_key = 'trust_levels.#{key}.title' WHERE translation_key = 'js.trust_levels.names.#{key}' SQL - end end end diff --git a/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb b/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb index bb149e7eeb..d7f36dbc8b 100644 --- a/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb +++ b/db/migrate/20210618135229_reintroduce_type_to_directory_columns.rb @@ -8,12 +8,10 @@ class ReintroduceTypeToDirectoryColumns < ActiveRecord::Migration[6.1] add_column :directory_columns, :type, :integer, default: 0, null: false end - DB.exec( - <<~SQL + DB.exec(<<~SQL) UPDATE directory_columns SET type = CASE WHEN automatic THEN 0 ELSE 1 END; SQL - ) end def down diff --git a/db/migrate/20210621002201_add_columns_to_email_log_to_match_incoming_for_smtp_imap.rb b/db/migrate/20210621002201_add_columns_to_email_log_to_match_incoming_for_smtp_imap.rb index b9fea5f543..0b1d699824 100644 --- a/db/migrate/20210621002201_add_columns_to_email_log_to_match_incoming_for_smtp_imap.rb +++ b/db/migrate/20210621002201_add_columns_to_email_log_to_match_incoming_for_smtp_imap.rb @@ -7,7 +7,7 @@ class AddColumnsToEmailLogToMatchIncomingForSmtpImap < ActiveRecord::Migration[6 add_column :email_logs, :raw, :text, null: true add_column :email_logs, :topic_id, :integer, null: true - add_index :email_logs, :topic_id, where: 'topic_id IS NOT NULL' + add_index :email_logs, :topic_id, where: "topic_id IS NOT NULL" end def down diff --git a/db/migrate/20210621103509_add_bannered_until.rb b/db/migrate/20210621103509_add_bannered_until.rb index 6a48cf416e..96baba8305 100644 --- a/db/migrate/20210621103509_add_bannered_until.rb +++ b/db/migrate/20210621103509_add_bannered_until.rb @@ -4,6 +4,6 @@ class AddBanneredUntil < ActiveRecord::Migration[6.1] def change add_column :topics, :bannered_until, :datetime, null: true - add_index :topics, :bannered_until, where: 'bannered_until IS NOT NULL' + add_index :topics, :bannered_until, where: "bannered_until IS NOT NULL" end end diff --git a/db/migrate/20210621190335_migrate_pending_users_reminder_delay_setting.rb b/db/migrate/20210621190335_migrate_pending_users_reminder_delay_setting.rb index a1cd158c9d..dd633617eb 100644 --- a/db/migrate/20210621190335_migrate_pending_users_reminder_delay_setting.rb +++ b/db/migrate/20210621190335_migrate_pending_users_reminder_delay_setting.rb @@ -2,7 +2,10 @@ class MigratePendingUsersReminderDelaySetting < ActiveRecord::Migration[6.1] def up - setting_value = DB.query_single("SELECT value FROM site_settings WHERE name = 'pending_users_reminder_delay'").first + setting_value = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'pending_users_reminder_delay'", + ).first if setting_value.present? new_value = setting_value.to_i diff --git a/db/migrate/20210624023831_remove_highest_seen_post_number_from_topic_users.rb b/db/migrate/20210624023831_remove_highest_seen_post_number_from_topic_users.rb index dbf2cdc3e2..da77919333 100644 --- a/db/migrate/20210624023831_remove_highest_seen_post_number_from_topic_users.rb +++ b/db/migrate/20210624023831_remove_highest_seen_post_number_from_topic_users.rb @@ -1,16 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class RemoveHighestSeenPostNumberFromTopicUsers < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS = { - topic_users: %i{highest_seen_post_number} - } + DROPPED_COLUMNS = { topic_users: %i[highest_seen_post_number] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20210628035905_drop_duration_column_from_topic_timers.rb b/db/migrate/20210628035905_drop_duration_column_from_topic_timers.rb index f8e8ee2c73..dc231c2ca1 100644 --- a/db/migrate/20210628035905_drop_duration_column_from_topic_timers.rb +++ b/db/migrate/20210628035905_drop_duration_column_from_topic_timers.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropDurationColumnFromTopicTimers < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - topic_timers: %i{duration} - } + DROPPED_COLUMNS ||= { topic_timers: %i[duration] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20210706091905_drop_disable_jump_reply_column_from_user_options.rb b/db/migrate/20210706091905_drop_disable_jump_reply_column_from_user_options.rb index e9efd44ab2..59b105435b 100644 --- a/db/migrate/20210706091905_drop_disable_jump_reply_column_from_user_options.rb +++ b/db/migrate/20210706091905_drop_disable_jump_reply_column_from_user_options.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropDisableJumpReplyColumnFromUserOptions < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - user_options: %i{disable_jump_reply} - } + DROPPED_COLUMNS ||= { user_options: %i[disable_jump_reply] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20210709053030_drop_uploads_verified.rb b/db/migrate/20210709053030_drop_uploads_verified.rb index bbf6a7dde5..5f999b3519 100644 --- a/db/migrate/20210709053030_drop_uploads_verified.rb +++ b/db/migrate/20210709053030_drop_uploads_verified.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropUploadsVerified < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - uploads: %i{verified} - } + DROPPED_COLUMNS ||= { uploads: %i[verified] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20210824203421_remove_post_timings_summary_index.rb b/db/migrate/20210824203421_remove_post_timings_summary_index.rb index 856a89e7d0..cd9f4c625c 100644 --- a/db/migrate/20210824203421_remove_post_timings_summary_index.rb +++ b/db/migrate/20210824203421_remove_post_timings_summary_index.rb @@ -2,6 +2,9 @@ class RemovePostTimingsSummaryIndex < ActiveRecord::Migration[6.1] def change - remove_index :post_timings, column: [:topic_id, :post_number], name: :post_timings_summary, if_exists: true + remove_index :post_timings, + column: %i[topic_id post_number], + name: :post_timings_summary, + if_exists: true end end diff --git a/db/migrate/20210913032326_add_for_topic_to_bookmarks.rb b/db/migrate/20210913032326_add_for_topic_to_bookmarks.rb index 84524f570e..796d5fd4d7 100644 --- a/db/migrate/20210913032326_add_for_topic_to_bookmarks.rb +++ b/db/migrate/20210913032326_add_for_topic_to_bookmarks.rb @@ -3,9 +3,7 @@ class AddForTopicToBookmarks < ActiveRecord::Migration[6.1] def change add_column :bookmarks, :for_topic, :boolean, default: false, null: false - add_index :bookmarks, [:user_id, :post_id, :for_topic], unique: true - if index_exists?(:bookmarks, [:user_id, :post_id]) - remove_index :bookmarks, [:user_id, :post_id] - end + add_index :bookmarks, %i[user_id post_id for_topic], unique: true + remove_index :bookmarks, %i[user_id post_id] if index_exists?(:bookmarks, %i[user_id post_id]) end end diff --git a/db/migrate/20210915222124_drop_reminder_type_index_for_bookmarks.rb b/db/migrate/20210915222124_drop_reminder_type_index_for_bookmarks.rb index 06e9fb010e..f10471face 100644 --- a/db/migrate/20210915222124_drop_reminder_type_index_for_bookmarks.rb +++ b/db/migrate/20210915222124_drop_reminder_type_index_for_bookmarks.rb @@ -2,14 +2,10 @@ class DropReminderTypeIndexForBookmarks < ActiveRecord::Migration[6.1] def up - if index_exists?(:bookmarks, [:reminder_type]) - remove_index :bookmarks, [:reminder_type] - end + remove_index :bookmarks, [:reminder_type] if index_exists?(:bookmarks, [:reminder_type]) end def down - if !index_exists?(:bookmarks, [:reminder_type]) - add_index :bookmarks, :reminder_type - end + add_index :bookmarks, :reminder_type if !index_exists?(:bookmarks, [:reminder_type]) end end diff --git a/db/migrate/20210920044353_add_default_calendar_to_user_options.rb b/db/migrate/20210920044353_add_default_calendar_to_user_options.rb index 0bbc5a3c73..169e792083 100644 --- a/db/migrate/20210920044353_add_default_calendar_to_user_options.rb +++ b/db/migrate/20210920044353_add_default_calendar_to_user_options.rb @@ -3,6 +3,6 @@ class AddDefaultCalendarToUserOptions < ActiveRecord::Migration[6.1] def change add_column :user_options, :default_calendar, :integer, default: 0, null: false - add_index :user_options, [:user_id, :default_calendar] + add_index :user_options, %i[user_id default_calendar] end end diff --git a/db/migrate/20210929215543_add_token_hash_to_email_token.rb b/db/migrate/20210929215543_add_token_hash_to_email_token.rb index b691b3cedf..ca2ad4dd21 100644 --- a/db/migrate/20210929215543_add_token_hash_to_email_token.rb +++ b/db/migrate/20210929215543_add_token_hash_to_email_token.rb @@ -5,9 +5,10 @@ class AddTokenHashToEmailToken < ActiveRecord::Migration[6.1] add_column :email_tokens, :token_hash, :string loop do - rows = DB - .query("SELECT id, token FROM email_tokens WHERE token_hash IS NULL LIMIT 500") - .map { |row| { id: row.id, token_hash: Digest::SHA256.hexdigest(row.token) } } + rows = + DB + .query("SELECT id, token FROM email_tokens WHERE token_hash IS NULL LIMIT 500") + .map { |row| { id: row.id, token_hash: Digest::SHA256.hexdigest(row.token) } } break if rows.size == 0 diff --git a/db/migrate/20211106085344_create_associated_groups.rb b/db/migrate/20211106085344_create_associated_groups.rb index 72c441fe5f..ffdad1f29f 100644 --- a/db/migrate/20211106085344_create_associated_groups.rb +++ b/db/migrate/20211106085344_create_associated_groups.rb @@ -10,6 +10,9 @@ class CreateAssociatedGroups < ActiveRecord::Migration[6.1] t.timestamps end - add_index :associated_groups, %i[provider_name provider_id], unique: true, name: 'associated_groups_provider_id' + add_index :associated_groups, + %i[provider_name provider_id], + unique: true, + name: "associated_groups_provider_id" end end diff --git a/db/migrate/20211106085527_create_user_associated_groups.rb b/db/migrate/20211106085527_create_user_associated_groups.rb index 73464b66f4..25ae89ec8d 100644 --- a/db/migrate/20211106085527_create_user_associated_groups.rb +++ b/db/migrate/20211106085527_create_user_associated_groups.rb @@ -8,6 +8,9 @@ class CreateUserAssociatedGroups < ActiveRecord::Migration[6.1] t.timestamps end - add_index :user_associated_groups, %i[user_id associated_group_id], unique: true, name: 'index_user_associated_groups' + add_index :user_associated_groups, + %i[user_id associated_group_id], + unique: true, + name: "index_user_associated_groups" end end diff --git a/db/migrate/20211106085605_create_group_associated_groups.rb b/db/migrate/20211106085605_create_group_associated_groups.rb index 281adb11b1..384231495d 100644 --- a/db/migrate/20211106085605_create_group_associated_groups.rb +++ b/db/migrate/20211106085605_create_group_associated_groups.rb @@ -8,6 +8,9 @@ class CreateGroupAssociatedGroups < ActiveRecord::Migration[6.1] t.timestamps end - add_index :group_associated_groups, %i[group_id associated_group_id], unique: true, name: 'index_group_associated_groups' + add_index :group_associated_groups, + %i[group_id associated_group_id], + unique: true, + name: "index_group_associated_groups" end end diff --git a/db/migrate/20211123144714_add_recent_searches.rb b/db/migrate/20211123144714_add_recent_searches.rb index 0e586f6afb..43ffe229e2 100644 --- a/db/migrate/20211123144714_add_recent_searches.rb +++ b/db/migrate/20211123144714_add_recent_searches.rb @@ -4,6 +4,6 @@ class AddRecentSearches < ActiveRecord::Migration[6.1] def change add_column :user_options, :oldest_search_log_date, :datetime - add_index :search_logs, [:user_id, :created_at], where: 'user_id IS NOT NULL' + add_index :search_logs, %i[user_id created_at], where: "user_id IS NOT NULL" end end diff --git a/db/migrate/20211124161346_queue_internal_onebox_rebake.rb b/db/migrate/20211124161346_queue_internal_onebox_rebake.rb index 85af7f1cee..a58d527182 100644 --- a/db/migrate/20211124161346_queue_internal_onebox_rebake.rb +++ b/db/migrate/20211124161346_queue_internal_onebox_rebake.rb @@ -4,12 +4,10 @@ class QueueInternalOneboxRebake < ActiveRecord::Migration[6.1] def up # Prior to this fix, internal oneboxes were bypassing the CDN for avatar URLs. # If a site has a CDN, queue up a rebake in the background - if GlobalSetting.cdn_url - execute <<~SQL + execute <<~SQL if GlobalSetting.cdn_url UPDATE posts SET baked_version = 0 WHERE cooked LIKE '%src="/user_avatar/%' SQL - end end def down diff --git a/db/migrate/20211201221028_migrate_email_to_normalized_email.rb b/db/migrate/20211201221028_migrate_email_to_normalized_email.rb index 2839c8f2fd..d84d7cf4d4 100644 --- a/db/migrate/20211201221028_migrate_email_to_normalized_email.rb +++ b/db/migrate/20211201221028_migrate_email_to_normalized_email.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true class MigrateEmailToNormalizedEmail < ActiveRecord::Migration[6.1] - # minimize locking on user_email table disable_ddl_transaction! def up - min, max = DB.query_single "SELECT MIN(id), MAX(id) FROM user_emails" # scaling is needed to compensate for "holes" where records were deleted # and pathological cases where for some reason id 100_000_000 and 0 exist @@ -29,7 +27,6 @@ class MigrateEmailToNormalizedEmail < ActiveRecord::Migration[6.1] low_id = min bounds.each do |high_id| - # using execute cause MiniSQL is not logging at the moment # to_i is not needed, but specified so it is explicit there is no SQL injection execute <<~SQL @@ -41,7 +38,6 @@ class MigrateEmailToNormalizedEmail < ActiveRecord::Migration[6.1] low_id = high_id end - end def down diff --git a/db/migrate/20211206160212_drop_token_from_email_tokens.rb b/db/migrate/20211206160212_drop_token_from_email_tokens.rb index 2e65cdb4a5..f42cd01ddc 100644 --- a/db/migrate/20211206160212_drop_token_from_email_tokens.rb +++ b/db/migrate/20211206160212_drop_token_from_email_tokens.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropTokenFromEmailTokens < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - email_tokens: %i{token} - } + DROPPED_COLUMNS ||= { email_tokens: %i[token] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20211213060445_email_tokens_token_to_nullable.rb b/db/migrate/20211213060445_email_tokens_token_to_nullable.rb index 1207ae1825..16f542f01c 100644 --- a/db/migrate/20211213060445_email_tokens_token_to_nullable.rb +++ b/db/migrate/20211213060445_email_tokens_token_to_nullable.rb @@ -9,16 +9,14 @@ class EmailTokensTokenToNullable < ActiveRecord::Migration[6.1] # drifted on main begin Migration::SafeMigrate.disable! - if DB.query_single(<<~SQL).length > 0 + execute <<~SQL if DB.query_single(<<~SQL).length > 0 + ALTER TABLE email_tokens ALTER COLUMN token DROP NOT NULL + SQL SELECT 1 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema='public' AND table_name = 'email_tokens' AND column_name = 'token' SQL - execute <<~SQL - ALTER TABLE email_tokens ALTER COLUMN token DROP NOT NULL - SQL - end ensure Migration::SafeMigrate.enable! end diff --git a/db/migrate/20211224010204_drop_old_bookmark_columns.rb b/db/migrate/20211224010204_drop_old_bookmark_columns.rb index 6dc3e8354c..48793b0f2a 100644 --- a/db/migrate/20211224010204_drop_old_bookmark_columns.rb +++ b/db/migrate/20211224010204_drop_old_bookmark_columns.rb @@ -1,19 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class DropOldBookmarkColumns < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - bookmarks: %i{ - topic_id - reminder_type - } - } + DROPPED_COLUMNS ||= { bookmarks: %i[topic_id reminder_type] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/migrate/20211224111749_not_null_notification_level_in_category_users.rb b/db/migrate/20211224111749_not_null_notification_level_in_category_users.rb index 8638bb447b..be401dd3d5 100644 --- a/db/migrate/20211224111749_not_null_notification_level_in_category_users.rb +++ b/db/migrate/20211224111749_not_null_notification_level_in_category_users.rb @@ -2,9 +2,7 @@ class NotNullNotificationLevelInCategoryUsers < ActiveRecord::Migration[6.1] def change - up_only do - execute("DELETE FROM category_users WHERE notification_level IS NULL") - end + up_only { execute("DELETE FROM category_users WHERE notification_level IS NULL") } change_column_null :category_users, :notification_level, false end end diff --git a/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb b/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb index 22b19c60db..29f9a07812 100644 --- a/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb +++ b/db/migrate/20220130192155_set_use_email_for_username_and_name_suggestions_on_existing_sites.rb @@ -10,13 +10,11 @@ class SetUseEmailForUsernameAndNameSuggestionsOnExistingSites < ActiveRecord::Mi SQL # make setting enabled for existing sites - if result.first['created_at'].to_datetime < 1.hour.ago - execute <<~SQL + execute <<~SQL if result.first["created_at"].to_datetime < 1.hour.ago INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES('use_email_for_username_and_name_suggestions', 5, 't', NOW(), NOW()) ON CONFLICT (name) DO NOTHING SQL - end end def down diff --git a/db/migrate/20220202225716_add_external_id_to_topics.rb b/db/migrate/20220202225716_add_external_id_to_topics.rb index 6ee7c3edb5..2e04ceddc7 100644 --- a/db/migrate/20220202225716_add_external_id_to_topics.rb +++ b/db/migrate/20220202225716_add_external_id_to_topics.rb @@ -3,6 +3,6 @@ class AddExternalIdToTopics < ActiveRecord::Migration[6.1] def change add_column :topics, :external_id, :string, null: true - add_index :topics, :external_id, unique: true, where: 'external_id IS NOT NULL' + add_index :topics, :external_id, unique: true, where: "external_id IS NOT NULL" end end diff --git a/db/migrate/20220302163246_update_avatar_service_domain.rb b/db/migrate/20220302163246_update_avatar_service_domain.rb index b602c501a5..dd9624250f 100644 --- a/db/migrate/20220302163246_update_avatar_service_domain.rb +++ b/db/migrate/20220302163246_update_avatar_service_domain.rb @@ -2,7 +2,10 @@ class UpdateAvatarServiceDomain < ActiveRecord::Migration[6.1] def up - existing_value = DB.query_single("SELECT value FROM site_settings WHERE name = 'external_system_avatars_url'")&.[](0) + existing_value = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'external_system_avatars_url'", + )&.[](0) if existing_value&.include?("avatars.discourse.org") new_value = DB.query_single(<<~SQL)&.[](0) diff --git a/db/migrate/20220304162250_enable_unaccent_extension.rb b/db/migrate/20220304162250_enable_unaccent_extension.rb index d6f6e7f770..5991caac1c 100644 --- a/db/migrate/20220304162250_enable_unaccent_extension.rb +++ b/db/migrate/20220304162250_enable_unaccent_extension.rb @@ -2,6 +2,6 @@ class EnableUnaccentExtension < ActiveRecord::Migration[6.1] def change - enable_extension 'unaccent' + enable_extension "unaccent" end end diff --git a/db/migrate/20220308201942_create_upload_references.rb b/db/migrate/20220308201942_create_upload_references.rb index 84891f80a5..3f928526b3 100644 --- a/db/migrate/20220308201942_create_upload_references.rb +++ b/db/migrate/20220308201942_create_upload_references.rb @@ -8,6 +8,9 @@ class CreateUploadReferences < ActiveRecord::Migration[6.1] t.timestamps end - add_index :upload_references, [:upload_id, :target_type, :target_id], unique: true, name: 'index_upload_references_on_upload_and_target' + add_index :upload_references, + %i[upload_id target_type target_id], + unique: true, + name: "index_upload_references_on_upload_and_target" end end diff --git a/db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb b/db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb index 922d611d5f..fc7b34d11e 100644 --- a/db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb +++ b/db/migrate/20220322024216_add_bookmark_polymorphic_columns.rb @@ -10,8 +10,11 @@ class AddBookmarkPolymorphicColumns < ActiveRecord::Migration[6.1] add_column :bookmarks, :bookmarkable_type, :string end - if !index_exists?(:bookmarks, [:user_id, :bookmarkable_type, :bookmarkable_id]) - add_index :bookmarks, [:user_id, :bookmarkable_type, :bookmarkable_id], name: "idx_bookmarks_user_polymorphic_unique", unique: true + if !index_exists?(:bookmarks, %i[user_id bookmarkable_type bookmarkable_id]) + add_index :bookmarks, + %i[user_id bookmarkable_type bookmarkable_id], + name: "idx_bookmarks_user_polymorphic_unique", + unique: true end end end diff --git a/db/migrate/20220401130745_create_category_required_tag_groups.rb b/db/migrate/20220401130745_create_category_required_tag_groups.rb index dd054a9e11..b6e3193922 100644 --- a/db/migrate/20220401130745_create_category_required_tag_groups.rb +++ b/db/migrate/20220401130745_create_category_required_tag_groups.rb @@ -10,7 +10,10 @@ class CreateCategoryRequiredTagGroups < ActiveRecord::Migration[6.1] t.timestamps end - add_index :category_required_tag_groups, [:category_id, :tag_group_id], name: "idx_category_required_tag_groups", unique: true + add_index :category_required_tag_groups, + %i[category_id tag_group_id], + name: "idx_category_required_tag_groups", + unique: true execute <<~SQL INSERT INTO category_required_tag_groups diff --git a/db/migrate/20220428094026_create_post_hotlinked_media.rb b/db/migrate/20220428094026_create_post_hotlinked_media.rb index ff31138ccd..27354aa495 100644 --- a/db/migrate/20220428094026_create_post_hotlinked_media.rb +++ b/db/migrate/20220428094026_create_post_hotlinked_media.rb @@ -3,16 +3,12 @@ class CreatePostHotlinkedMedia < ActiveRecord::Migration[6.1] def change reversible do |dir| - dir.up do - execute <<~SQL + dir.up { execute <<~SQL } CREATE TYPE hotlinked_media_status AS ENUM('downloaded', 'too_large', 'download_failed', 'upload_create_failed') SQL - end - dir.down do - execute <<~SQL + dir.down { execute <<~SQL } DROP TYPE hotlinked_media_status SQL - end end create_table :post_hotlinked_media do |t| diff --git a/db/migrate/20220506221447_set_pm_tags_allowed_for_groups_default.rb b/db/migrate/20220506221447_set_pm_tags_allowed_for_groups_default.rb index e7a36eec4d..c28b832e11 100644 --- a/db/migrate/20220506221447_set_pm_tags_allowed_for_groups_default.rb +++ b/db/migrate/20220506221447_set_pm_tags_allowed_for_groups_default.rb @@ -2,10 +2,10 @@ class SetPmTagsAllowedForGroupsDefault < ActiveRecord::Migration[7.0] def up - # if the old SiteSetting of `allow_staff_to_tag_pms` was set to true, update the new SiteSetting of # `pm_tags_allowed_for_groups` default to include the staff group - allow_staff_to_tag_pms = DB.query_single("SELECT value FROM site_settings WHERE name = 'allow_staff_to_tag_pms'").first + allow_staff_to_tag_pms = + DB.query_single("SELECT value FROM site_settings WHERE name = 'allow_staff_to_tag_pms'").first # Dynamically sets the default value if allow_staff_to_tag_pms == "t" diff --git a/db/migrate/20220628031850_create_sidebar_section_links.rb b/db/migrate/20220628031850_create_sidebar_section_links.rb index 3bd1bad37f..ab34ecb5f3 100644 --- a/db/migrate/20220628031850_create_sidebar_section_links.rb +++ b/db/migrate/20220628031850_create_sidebar_section_links.rb @@ -5,11 +5,14 @@ class CreateSidebarSectionLinks < ActiveRecord::Migration[7.0] create_table :sidebar_section_links do |t| t.integer :user_id, null: false t.integer :linkable_id, null: false - t.string :linkable_type, null: false + t.string :linkable_type, null: false t.timestamps end - add_index :sidebar_section_links, [:user_id, :linkable_type, :linkable_id], unique: true, name: 'idx_unique_sidebar_section_links' + add_index :sidebar_section_links, + %i[user_id linkable_type linkable_id], + unique: true, + name: "idx_unique_sidebar_section_links" end end diff --git a/db/migrate/20220726164831_fix_delete_rejected_email_after_days_site_setting.rb b/db/migrate/20220726164831_fix_delete_rejected_email_after_days_site_setting.rb index 89ce57c1d8..70b6d9641d 100644 --- a/db/migrate/20220726164831_fix_delete_rejected_email_after_days_site_setting.rb +++ b/db/migrate/20220726164831_fix_delete_rejected_email_after_days_site_setting.rb @@ -1,8 +1,14 @@ # frozen_string_literal: true class FixDeleteRejectedEmailAfterDaysSiteSetting < ActiveRecord::Migration[6.1] def up - delete_rejected_email_after_days = DB.query_single("SELECT value FROM site_settings WHERE name = 'delete_rejected_email_after_days'").first - delete_email_logs_after_days = DB.query_single("SELECT value FROM site_settings WHERE name = 'delete_email_logs_after_days'").first + delete_rejected_email_after_days = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'delete_rejected_email_after_days'", + ).first + delete_email_logs_after_days = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'delete_email_logs_after_days'", + ).first # These settings via the sql query return nil if they are using their default values unless delete_email_logs_after_days @@ -10,14 +16,14 @@ class FixDeleteRejectedEmailAfterDaysSiteSetting < ActiveRecord::Migration[6.1] end # Only update if the setting is not using the default and it is lower than 'delete_email_logs_after_days' - if delete_rejected_email_after_days != nil && delete_rejected_email_after_days.to_i < delete_email_logs_after_days.to_i + if delete_rejected_email_after_days != nil && + delete_rejected_email_after_days.to_i < delete_email_logs_after_days.to_i execute <<~SQL UPDATE site_settings SET value = #{delete_email_logs_after_days.to_i} WHERE name = 'delete_rejected_email_after_days' SQL end - end def down diff --git a/db/migrate/20220727085001_create_index_on_reviewables_score_desc_created_at_desc.rb b/db/migrate/20220727085001_create_index_on_reviewables_score_desc_created_at_desc.rb index b2cc0ec199..cdf780428f 100644 --- a/db/migrate/20220727085001_create_index_on_reviewables_score_desc_created_at_desc.rb +++ b/db/migrate/20220727085001_create_index_on_reviewables_score_desc_created_at_desc.rb @@ -4,9 +4,12 @@ class CreateIndexOnReviewablesScoreDescCreatedAtDesc < ActiveRecord::Migration[7 def change add_index( :reviewables, - [:score, :created_at], - order: { score: :desc, created_at: :desc }, - name: 'idx_reviewables_score_desc_created_at_desc' + %i[score created_at], + order: { + score: :desc, + created_at: :desc, + }, + name: "idx_reviewables_score_desc_created_at_desc", ) end end diff --git a/db/migrate/20220825054405_fill_personal_message_enabled_groups_based_on_deprecated_settings.rb b/db/migrate/20220825054405_fill_personal_message_enabled_groups_based_on_deprecated_settings.rb index cc10befd0f..28241e035d 100644 --- a/db/migrate/20220825054405_fill_personal_message_enabled_groups_based_on_deprecated_settings.rb +++ b/db/migrate/20220825054405_fill_personal_message_enabled_groups_based_on_deprecated_settings.rb @@ -2,11 +2,19 @@ class FillPersonalMessageEnabledGroupsBasedOnDeprecatedSettings < ActiveRecord::Migration[7.0] def up - enable_personal_messages_raw = DB.query_single("SELECT value FROM site_settings WHERE name = 'enable_personal_messages'").first - enable_personal_messages = enable_personal_messages_raw.blank? || enable_personal_messages_raw == 't' + enable_personal_messages_raw = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'enable_personal_messages'", + ).first + enable_personal_messages = + enable_personal_messages_raw.blank? || enable_personal_messages_raw == "t" - min_trust_to_send_messages_raw = DB.query_single("SELECT value FROM site_settings WHERE name = 'min_trust_to_send_messages'").first - min_trust_to_send_messages = (min_trust_to_send_messages_raw.blank? ? 1 : min_trust_to_send_messages_raw).to_i + min_trust_to_send_messages_raw = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'min_trust_to_send_messages'", + ).first + min_trust_to_send_messages = + (min_trust_to_send_messages_raw.blank? ? 1 : min_trust_to_send_messages_raw).to_i # default to TL1, Group::AUTO_GROUPS[:trust_level_1] is 11 personal_message_enabled_groups = "11" @@ -17,15 +25,13 @@ class FillPersonalMessageEnabledGroupsBasedOnDeprecatedSettings < ActiveRecord:: end # only allow staff if the setting was previously disabled, Group::AUTO_GROUPS[:staff] is 3 - if !enable_personal_messages - personal_message_enabled_groups = "3" - end + personal_message_enabled_groups = "3" if !enable_personal_messages # data_type 20 is group_list DB.exec( "INSERT INTO site_settings(name, value, data_type, created_at, updated_at) VALUES('personal_message_enabled_groups', :setting, '20', NOW(), NOW())", - setting: personal_message_enabled_groups + setting: personal_message_enabled_groups, ) end diff --git a/db/migrate/20220927065328_set_secure_uploads_settings_based_on_secure_media_equivalent.rb b/db/migrate/20220927065328_set_secure_uploads_settings_based_on_secure_media_equivalent.rb index 037968a534..1fa50a9219 100644 --- a/db/migrate/20220927065328_set_secure_uploads_settings_based_on_secure_media_equivalent.rb +++ b/db/migrate/20220927065328_set_secure_uploads_settings_based_on_secure_media_equivalent.rb @@ -2,32 +2,36 @@ class SetSecureUploadsSettingsBasedOnSecureMediaEquivalent < ActiveRecord::Migration[7.0] def up - secure_media_enabled = DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media'") + secure_media_enabled = + DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media'") - if secure_media_enabled.present? && secure_media_enabled[0] == "t" - execute <<~SQL + execute <<~SQL if secure_media_enabled.present? && secure_media_enabled[0] == "t" INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES ('secure_uploads', 5, 't', now(), now()) SQL - end - secure_media_allow_embed_images_in_emails = DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media_allow_embed_images_in_emails'") + secure_media_allow_embed_images_in_emails = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'secure_media_allow_embed_images_in_emails'", + ) - if secure_media_allow_embed_images_in_emails.present? && secure_media_allow_embed_images_in_emails[0] == "t" + if secure_media_allow_embed_images_in_emails.present? && + secure_media_allow_embed_images_in_emails[0] == "t" execute <<~SQL INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES ('secure_uploads_allow_embed_images_in_emails', 5, 't', now(), now()) SQL end - secure_media_max_email_embed_image_size_kb = DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media_max_email_embed_image_size_kb'") + secure_media_max_email_embed_image_size_kb = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'secure_media_max_email_embed_image_size_kb'", + ) - if secure_media_max_email_embed_image_size_kb.present? - execute <<~SQL + execute <<~SQL if secure_media_max_email_embed_image_size_kb.present? INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES ('secure_uploads_max_email_embed_image_size_kb', 3, '#{secure_media_max_email_embed_image_size_kb[0]}', now(), now()) SQL - end end def down diff --git a/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb b/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb index 979b8a6b56..82827c5537 100644 --- a/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb +++ b/db/migrate/20220927171707_disable_allow_uncategorized_new_sites.rb @@ -10,13 +10,11 @@ class DisableAllowUncategorizedNewSites < ActiveRecord::Migration[7.0] SQL # keep allow uncategorized for existing sites - if result.first['created_at'].to_datetime < 1.hour.ago - execute <<~SQL + execute <<~SQL if result.first["created_at"].to_datetime < 1.hour.ago INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES('allow_uncategorized_topics', 5, 't', NOW(), NOW()) ON CONFLICT (name) DO NOTHING SQL - end end def down diff --git a/db/migrate/20221017223309_fix_general_category_id.rb b/db/migrate/20221017223309_fix_general_category_id.rb index f51f8f0a49..f2243d89a2 100644 --- a/db/migrate/20221017223309_fix_general_category_id.rb +++ b/db/migrate/20221017223309_fix_general_category_id.rb @@ -6,9 +6,11 @@ class FixGeneralCategoryId < ActiveRecord::Migration[7.0] def up - general_category_id = DB.query_single("SELECT value FROM site_settings WHERE name = 'general_category_id'") + general_category_id = + DB.query_single("SELECT value FROM site_settings WHERE name = 'general_category_id'") return if general_category_id.blank? || general_category_id[0].to_i < 0 - matching_category_id = DB.query_single("SELECT id FROM categories WHERE id = #{general_category_id[0]}") + matching_category_id = + DB.query_single("SELECT id FROM categories WHERE id = #{general_category_id[0]}") # If the general_category_id has been set to something other than the default and there isn't a matching # category to go with it we should set it back to the default. diff --git a/db/migrate/20221110175456_populate_default_composer_category.rb b/db/migrate/20221110175456_populate_default_composer_category.rb index 812e9ba0ae..22692d3ea1 100644 --- a/db/migrate/20221110175456_populate_default_composer_category.rb +++ b/db/migrate/20221110175456_populate_default_composer_category.rb @@ -5,14 +5,16 @@ class PopulateDefaultComposerCategory < ActiveRecord::Migration[7.0] def up - general_category_id = DB.query_single("SELECT value FROM site_settings WHERE name = 'general_category_id'") + general_category_id = + DB.query_single("SELECT value FROM site_settings WHERE name = 'general_category_id'") return if general_category_id.blank? || general_category_id[0].to_i < 0 - default_composer_category = DB.query_single("SELECT value FROM site_settings where name = 'default_composer_category'") + default_composer_category = + DB.query_single("SELECT value FROM site_settings where name = 'default_composer_category'") return if !default_composer_category.blank? DB.exec( "INSERT INTO site_settings(name, value, data_type, created_at, updated_at) VALUES('default_composer_category', :setting, '16', NOW(), NOW())", - setting: general_category_id[0].to_i + setting: general_category_id[0].to_i, ) end diff --git a/db/migrate/20221205225450_migrate_sidebar_site_settings.rb b/db/migrate/20221205225450_migrate_sidebar_site_settings.rb index fc6a054f4d..11e96430be 100644 --- a/db/migrate/20221205225450_migrate_sidebar_site_settings.rb +++ b/db/migrate/20221205225450_migrate_sidebar_site_settings.rb @@ -2,32 +2,32 @@ class MigrateSidebarSiteSettings < ActiveRecord::Migration[7.0] def up - previous_enable_experimental_sidebar_hamburger = DB.query_single( - "SELECT value FROM site_settings WHERE name = 'enable_experimental_sidebar_hamburger'" - )[0] + previous_enable_experimental_sidebar_hamburger = + DB.query_single( + "SELECT value FROM site_settings WHERE name = 'enable_experimental_sidebar_hamburger'", + )[ + 0 + ] - previous_enable_sidebar = DB.query_single( - "SELECT value FROM site_settings WHERE name = 'enable_sidebar'" - )[0] + previous_enable_sidebar = + DB.query_single("SELECT value FROM site_settings WHERE name = 'enable_sidebar'")[0] value = case [previous_enable_experimental_sidebar_hamburger, previous_enable_sidebar] - when ['t', 't'], ['t', nil] + when %w[t t], ["t", nil] "sidebar" - when ['t', 'f'] + when %w[t f] "header dropdown" - when ['f', 't'], ['f', 'f'], ['f', nil] + when %w[f t], %w[f f], ["f", nil] "legacy" - when [nil, 't'], [nil, 'f'], [nil, nil] + when [nil, "t"], [nil, "f"], [nil, nil] nil end - if value - execute(<<~SQL) + execute(<<~SQL) if value INSERT INTO site_settings (name, data_type, value, created_at, updated_at) VALUES ('navigation_menu', 8, '#{value}', now(), now()) SQL - end end def down diff --git a/db/migrate/20221212225921_enable_sidebar_and_chat.rb b/db/migrate/20221212225921_enable_sidebar_and_chat.rb index 0edf7a46da..83b365dd49 100644 --- a/db/migrate/20221212225921_enable_sidebar_and_chat.rb +++ b/db/migrate/20221212225921_enable_sidebar_and_chat.rb @@ -10,7 +10,7 @@ class EnableSidebarAndChat < ActiveRecord::Migration[7.0] SQL # keep sidebar legacy and chat disabled for for existing sites - if result.first['created_at'].to_datetime < 1.hour.ago + if result.first["created_at"].to_datetime < 1.hour.ago execute <<~SQL INSERT INTO site_settings(name, data_type, value, created_at, updated_at) VALUES('chat_enabled', 5, 'f', NOW(), NOW()) diff --git a/db/post_migrate/20220214224506_reset_custom_emoji_post_bakes_version_secure_fix.rb b/db/post_migrate/20220214224506_reset_custom_emoji_post_bakes_version_secure_fix.rb index 90cf8d1793..1a2c3accd6 100644 --- a/db/post_migrate/20220214224506_reset_custom_emoji_post_bakes_version_secure_fix.rb +++ b/db/post_migrate/20220214224506_reset_custom_emoji_post_bakes_version_secure_fix.rb @@ -2,14 +2,13 @@ class ResetCustomEmojiPostBakesVersionSecureFix < ActiveRecord::Migration[6.1] def up - secure_media_enabled = DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media'") + secure_media_enabled = + DB.query_single("SELECT value FROM site_settings WHERE name = 'secure_media'") - if secure_media_enabled.present? && secure_media_enabled[0] == "t" - execute <<~SQL + execute <<~SQL if secure_media_enabled.present? && secure_media_enabled[0] == "t" UPDATE posts SET baked_version = 0 WHERE cooked LIKE '%emoji emoji-custom%' AND cooked LIKE '%secure-media-uploads%' SQL - end end def down diff --git a/db/post_migrate/20220302171443_rebake_old_avatar_service_urls.rb b/db/post_migrate/20220302171443_rebake_old_avatar_service_urls.rb index 84e70066b7..63a65e8ce6 100644 --- a/db/post_migrate/20220302171443_rebake_old_avatar_service_urls.rb +++ b/db/post_migrate/20220302171443_rebake_old_avatar_service_urls.rb @@ -16,12 +16,10 @@ class RebakeOldAvatarServiceUrls < ActiveRecord::Migration[6.1] AND created_at > NOW() - INTERVAL '1 month' SQL - if recently_changed - execute <<~SQL + execute <<~SQL if recently_changed UPDATE posts SET baked_version = 0 WHERE cooked LIKE '%avatars.discourse.org%' SQL - end end def down diff --git a/db/post_migrate/20220325064954_make_some_bookmark_columns_nullable.rb b/db/post_migrate/20220325064954_make_some_bookmark_columns_nullable.rb index 90905d34af..dd2d3b4592 100644 --- a/db/post_migrate/20220325064954_make_some_bookmark_columns_nullable.rb +++ b/db/post_migrate/20220325064954_make_some_bookmark_columns_nullable.rb @@ -10,7 +10,9 @@ class MakeSomeBookmarkColumnsNullable < ActiveRecord::Migration[6.1] def down DB.exec("UPDATE bookmarks SET post_id = bookmarkable_id WHERE bookmarkable_type = 'Post'") - DB.exec("UPDATE bookmarks SET post_id = (SELECT id FROM posts WHERE topic_id = bookmarkable_id AND post_number = 1), for_topic = TRUE WHERE bookmarkable_type = 'Topic'") + DB.exec( + "UPDATE bookmarks SET post_id = (SELECT id FROM posts WHERE topic_id = bookmarkable_id AND post_number = 1), for_topic = TRUE WHERE bookmarkable_type = 'Topic'", + ) change_column_null :bookmarks, :post_id, false execute "ALTER TABLE bookmarks DROP CONSTRAINT enforce_post_id_or_bookmarkable" end diff --git a/db/post_migrate/20220401140745_drop_category_required_tag_group_columns.rb b/db/post_migrate/20220401140745_drop_category_required_tag_group_columns.rb index fb7abd769c..b203db0d17 100644 --- a/db/post_migrate/20220401140745_drop_category_required_tag_group_columns.rb +++ b/db/post_migrate/20220401140745_drop_category_required_tag_group_columns.rb @@ -1,17 +1,10 @@ # frozen_string_literal: true class DropCategoryRequiredTagGroupColumns < ActiveRecord::Migration[6.1] - DROPPED_COLUMNS ||= { - categories: %i{ - required_tag_group_id - min_tags_from_required_group - } - } + DROPPED_COLUMNS ||= { categories: %i[required_tag_group_id min_tags_from_required_group] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/post_migrate/20220621164914_drop_flair_url_from_groups.rb b/db/post_migrate/20220621164914_drop_flair_url_from_groups.rb index 8f2f57f114..c66cfd036f 100644 --- a/db/post_migrate/20220621164914_drop_flair_url_from_groups.rb +++ b/db/post_migrate/20220621164914_drop_flair_url_from_groups.rb @@ -1,14 +1,10 @@ # frozen_string_literal: true class DropFlairUrlFromGroups < ActiveRecord::Migration[7.0] - DROPPED_COLUMNS ||= { - groups: %i{flair_url} - } + DROPPED_COLUMNS ||= { groups: %i[flair_url] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/db/post_migrate/20221026035440_security_log_out_invite_redemption_invited_users.rb b/db/post_migrate/20221026035440_security_log_out_invite_redemption_invited_users.rb index e87a27e3b0..9f627d10ff 100644 --- a/db/post_migrate/20221026035440_security_log_out_invite_redemption_invited_users.rb +++ b/db/post_migrate/20221026035440_security_log_out_invite_redemption_invited_users.rb @@ -3,7 +3,12 @@ class SecurityLogOutInviteRedemptionInvitedUsers < ActiveRecord::Migration[7.0] def up # 20220606061813 was added shortly before the vulnerability was introduced - vulnerable_since = DB.query_single("SELECT created_at FROM schema_migration_details WHERE version='20220606061813'")[0] + vulnerable_since = + DB.query_single( + "SELECT created_at FROM schema_migration_details WHERE version='20220606061813'", + )[ + 0 + ] DB.exec(<<~SQL, vulnerable_since: vulnerable_since) DELETE FROM user_auth_tokens diff --git a/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb b/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb index a0ad3fd3d1..7860e1546f 100644 --- a/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb +++ b/db/post_migrate/20221108032233_drop_old_bookmark_columns_v2.rb @@ -1,19 +1,12 @@ # frozen_string_literal: true -require 'migration/column_dropper' +require "migration/column_dropper" class DropOldBookmarkColumnsV2 < ActiveRecord::Migration[7.0] - DROPPED_COLUMNS ||= { - bookmarks: %i{ - post_id - for_topic - } - } + DROPPED_COLUMNS ||= { bookmarks: %i[post_id for_topic] } def up - DROPPED_COLUMNS.each do |table, columns| - Migration::ColumnDropper.execute_drop(table, columns) - end + DROPPED_COLUMNS.each { |table, columns| Migration::ColumnDropper.execute_drop(table, columns) } end def down diff --git a/lib/action_dispatch/session/discourse_cookie_store.rb b/lib/action_dispatch/session/discourse_cookie_store.rb index 91a32f4545..58da5c8f5d 100644 --- a/lib/action_dispatch/session/discourse_cookie_store.rb +++ b/lib/action_dispatch/session/discourse_cookie_store.rb @@ -9,9 +9,7 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::C def set_cookie(request, session_id, cookie) if Hash === cookie - if SiteSetting.force_https - cookie[:secure] = true - end + cookie[:secure] = true if SiteSetting.force_https unless SiteSetting.same_site_cookies == "Disabled" cookie[:same_site] = SiteSetting.same_site_cookies end diff --git a/lib/admin_confirmation.rb b/lib/admin_confirmation.rb index fa73f66de7..06b7bc775c 100644 --- a/lib/admin_confirmation.rb +++ b/lib/admin_confirmation.rb @@ -17,10 +17,7 @@ class AdminConfirmation @token = SecureRandom.hex Discourse.redis.setex("admin-confirmation:#{@target_user.id}", 3.hours.to_i, @token) - payload = { - target_user_id: @target_user.id, - performed_by: @performed_by.id - } + payload = { target_user_id: @target_user.id, performed_by: @performed_by.id } Discourse.redis.setex("admin-confirmation-token:#{@token}", 3.hours.to_i, payload.to_json) Jobs.enqueue( @@ -28,7 +25,7 @@ class AdminConfirmation to_address: @performed_by.email, target_email: @target_user.email, target_username: @target_user.username, - token: @token + token: @token, ) end @@ -51,8 +48,8 @@ class AdminConfirmation return nil unless json parsed = JSON.parse(json) - target_user = User.find(parsed['target_user_id'].to_i) - performed_by = User.find(parsed['performed_by'].to_i) + target_user = User.find(parsed["target_user_id"].to_i) + performed_by = User.find(parsed["performed_by"].to_i) ac = AdminConfirmation.new(target_user, performed_by) ac.token = token diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 7146eedfee..521cb0c2be 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminConstraint - def initialize(options = {}) @require_master = options[:require_master] end @@ -19,5 +18,4 @@ class AdminConstraint def custom_admin_check(request) true end - end diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 9ecbb8f19a..49564110bf 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminUserIndexQuery - def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels) @params = params @query = initialize_query_with_order(klass) @@ -11,24 +10,22 @@ class AdminUserIndexQuery attr_reader :params, :trust_levels SORTABLE_MAPPING = { - 'created' => 'created_at', - 'last_emailed' => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'seen' => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'username' => 'username', - 'email' => 'email', - 'trust_level' => 'trust_level', - 'days_visited' => 'user_stats.days_visited', - 'posts_read' => 'user_stats.posts_read_count', - 'topics_viewed' => 'user_stats.topics_entered', - 'posts' => 'user_stats.post_count', - 'read_time' => 'user_stats.time_read' + "created" => "created_at", + "last_emailed" => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "seen" => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "username" => "username", + "email" => "email", + "trust_level" => "trust_level", + "days_visited" => "user_stats.days_visited", + "posts_read" => "user_stats.posts_read_count", + "topics_viewed" => "user_stats.topics_entered", + "posts" => "user_stats.post_count", + "read_time" => "user_stats.time_read", } def find_users(limit = 100) page = params[:page].to_i - 1 - if page < 0 - page = 0 - end + page = 0 if page < 0 find_users_query.limit(limit).offset(page * limit) end @@ -37,7 +34,13 @@ class AdminUserIndexQuery end def custom_direction - Discourse.deprecate(":ascending is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') if params[:ascending] + if params[:ascending] + Discourse.deprecate( + ":ascending is deprecated please use :asc instead", + output_in_test: true, + drop_from: "2.9.0", + ) + end asc = params[:asc] || params[:ascending] asc.present? && asc ? "ASC" : "DESC" end @@ -47,7 +50,7 @@ class AdminUserIndexQuery custom_order = params[:order] if custom_order.present? && - without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, '')] + without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, "")] order << "#{without_dir} #{custom_direction}" end @@ -61,13 +64,9 @@ class AdminUserIndexQuery order << "users.username" end - query = klass - .includes(:totps) - .order(order.reject(&:blank?).join(",")) + query = klass.includes(:totps).order(order.reject(&:blank?).join(",")) - unless params[:stats].present? && params[:stats] == false - query = query.includes(:user_stat) - end + query = query.includes(:user_stat) unless params[:stats].present? && params[:stats] == false query = query.joins(:primary_email) if params[:show_emails] == "true" @@ -77,32 +76,44 @@ class AdminUserIndexQuery def filter_by_trust levels = trust_levels.map { |key, _| key.to_s } if levels.include?(params[:query]) - @query.where('trust_level = ?', trust_levels[params[:query].to_sym]) + @query.where("trust_level = ?", trust_levels[params[:query].to_sym]) end end def filter_by_query_classification case params[:query] - when 'staff' then @query.where("admin or moderator") - when 'admins' then @query.where(admin: true) - when 'moderators' then @query.where(moderator: true) - when 'silenced' then @query.silenced - when 'suspended' then @query.suspended - when 'pending' then @query.not_suspended.where(approved: false, active: true) - when 'staged' then @query.where(staged: true) + when "staff" + @query.where("admin or moderator") + when "admins" + @query.where(admin: true) + when "moderators" + @query.where(moderator: true) + when "silenced" + @query.silenced + when "suspended" + @query.suspended + when "pending" + @query.not_suspended.where(approved: false, active: true) + when "staged" + @query.where(staged: true) end end def filter_by_search if params[:email].present? - return @query.joins(:primary_email).where('user_emails.email = ?', params[:email].downcase) + return @query.joins(:primary_email).where("user_emails.email = ?", params[:email].downcase) end filter = params[:filter] if filter.present? filter = filter.strip - if ip = IPAddr.new(filter) rescue nil - @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) + if ip = + begin + IPAddr.new(filter) + rescue StandardError + nil + end + @query.where("ip_address <<= :ip OR registration_ip_address <<= :ip", ip: ip.to_cidr_s) else @query.filter_by_username_or_email(filter) end @@ -111,14 +122,12 @@ class AdminUserIndexQuery def filter_by_ip if params[:ip].present? - @query.where('ip_address = :ip OR registration_ip_address = :ip', ip: params[:ip].strip) + @query.where("ip_address = :ip OR registration_ip_address = :ip", ip: params[:ip].strip) end end def filter_exclude - if params[:exclude].present? - @query.where('users.id != ?', params[:exclude]) - end + @query.where("users.id != ?", params[:exclude]) if params[:exclude].present? end # this might not be needed in rails 4 ? @@ -134,5 +143,4 @@ class AdminUserIndexQuery append filter_by_search @query end - end diff --git a/lib/age_words.rb b/lib/age_words.rb index 814cc219e5..c9bfbfec28 100644 --- a/lib/age_words.rb +++ b/lib/age_words.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module AgeWords - def self.age_words(secs) if secs.blank? "—" @@ -10,5 +9,4 @@ module AgeWords FreedomPatches::Rails4.distance_of_time_in_words(now, now + secs) end end - end diff --git a/lib/archetype.rb b/lib/archetype.rb index e3fc8e5354..a9380d59d9 100644 --- a/lib/archetype.rb +++ b/lib/archetype.rb @@ -11,22 +11,19 @@ class Archetype end def attributes - { - id: @id, - options: @options - } + { id: @id, options: @options } end def self.default - 'regular' + "regular" end def self.private_message - 'private_message' + "private_message" end def self.banner - 'banner' + "banner" end def self.list @@ -40,8 +37,7 @@ class Archetype end # default archetypes - register 'regular' - register 'private_message' - register 'banner' - + register "regular" + register "private_message" + register "banner" end diff --git a/lib/auth.rb b/lib/auth.rb index f501d90157..5380c826d0 100644 --- a/lib/auth.rb +++ b/lib/auth.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -module Auth; end +module Auth +end -require 'auth/auth_provider' -require 'auth/result' -require 'auth/authenticator' -require 'auth/managed_authenticator' -require 'auth/facebook_authenticator' -require 'auth/github_authenticator' -require 'auth/twitter_authenticator' -require 'auth/google_oauth2_authenticator' -require 'auth/discord_authenticator' +require "auth/auth_provider" +require "auth/result" +require "auth/authenticator" +require "auth/managed_authenticator" +require "auth/facebook_authenticator" +require "auth/github_authenticator" +require "auth/twitter_authenticator" +require "auth/google_oauth2_authenticator" +require "auth/discord_authenticator" diff --git a/lib/auth/auth_provider.rb b/lib/auth/auth_provider.rb index 09f0f5f39a..20e0e6cfbc 100644 --- a/lib/auth/auth_provider.rb +++ b/lib/auth/auth_provider.rb @@ -8,32 +8,60 @@ class Auth::AuthProvider end def self.auth_attributes - [:authenticator, :pretty_name, :title, :message, :frame_width, :frame_height, - :pretty_name_setting, :title_setting, :enabled_setting, :full_screen_login, :full_screen_login_setting, - :custom_url, :background_color, :icon] + %i[ + authenticator + pretty_name + title + message + frame_width + frame_height + pretty_name_setting + title_setting + enabled_setting + full_screen_login + full_screen_login_setting + custom_url + background_color + icon + ] end attr_accessor(*auth_attributes) def enabled_setting=(val) - Discourse.deprecate("(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", + drop_from: "2.9.0", + ) @enabled_setting = val end def background_color=(val) - Discourse.deprecate("(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", + drop_from: "2.9.0", + ) end def full_screen_login=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def full_screen_login_setting=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def message=(val) - Discourse.deprecate("(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", + drop_from: "2.9.0", + ) end def name @@ -47,5 +75,4 @@ class Auth::AuthProvider def can_revoke authenticator.can_revoke? end - end diff --git a/lib/auth/current_user_provider.rb b/lib/auth/current_user_provider.rb index 31880b6dc6..f12e992de3 100644 --- a/lib/auth/current_user_provider.rb +++ b/lib/auth/current_user_provider.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -module Auth; end +module Auth +end class Auth::CurrentUserProvider - # do all current user initialization here def initialize(env) raise NotImplementedError diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 3318b062da..91bacb78a7 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require_relative '../route_matcher' +require_relative "../route_matcher" # You may have seen references to v0 and v1 of our auth cookie in the codebase # and you're not sure how they differ, so here is an explanation: @@ -23,7 +23,6 @@ require_relative '../route_matcher' # We'll drop support for v0 after Discourse 2.9 is released. class Auth::DefaultCurrentUserProvider - CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER" USER_TOKEN_KEY ||= "_DISCOURSE_USER_TOKEN" API_KEY ||= "api_key" @@ -37,7 +36,7 @@ class Auth::DefaultCurrentUserProvider USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID" API_KEY_ENV ||= "_DISCOURSE_API" USER_API_KEY_ENV ||= "_DISCOURSE_USER_API" - TOKEN_COOKIE ||= ENV['DISCOURSE_TOKEN_COOKIE'] || "_t" + TOKEN_COOKIE ||= ENV["DISCOURSE_TOKEN_COOKIE"] || "_t" PATH_INFO ||= "PATH_INFO" COOKIE_ATTEMPTS_PER_MIN ||= 10 BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN" @@ -59,30 +58,20 @@ class Auth::DefaultCurrentUserProvider "badges#show", "tags#tag_feed", "tags#show", - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "list##{f}_feed" }, - *[:all, :yearly, :quarterly, :monthly, :weekly, :daily].map { |p| "list#top_#{p}_feed" }, - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "tags#show_#{f}" } + *%i[latest unread new read posted bookmarks].map { |f| "list##{f}_feed" }, + *%i[all yearly quarterly monthly weekly daily].map { |p| "list#top_#{p}_feed" }, + *%i[latest unread new read posted bookmarks].map { |f| "tags#show_#{f}" }, ], - formats: :rss - ), - RouteMatcher.new( - methods: :get, - actions: "users#bookmarks", - formats: :ics - ), - RouteMatcher.new( - methods: :post, - actions: "admin/email#handle_mail", - formats: nil + formats: :rss, ), + RouteMatcher.new(methods: :get, actions: "users#bookmarks", formats: :ics), + RouteMatcher.new(methods: :post, actions: "admin/email#handle_mail", formats: nil), ] def self.find_v0_auth_cookie(request) cookie = request.cookies[TOKEN_COOKIE] - if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE - cookie - end + cookie if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE end def self.find_v1_auth_cookie(env) @@ -111,12 +100,10 @@ class Auth::DefaultCurrentUserProvider return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY) # bypass if we have the shared session header - if shared_key = @env['HTTP_X_SHARED_SESSION_KEY'] + if shared_key = @env["HTTP_X_SHARED_SESSION_KEY"] uid = Discourse.redis.get("shared_session_key_#{shared_key}") user = nil - if uid - user = User.find_by(id: uid.to_i) - end + user = User.find_by(id: uid.to_i) if uid @env[CURRENT_USER_KEY] = user return user end @@ -130,28 +117,27 @@ class Auth::DefaultCurrentUserProvider user_api_key ||= request[PARAMETER_USER_API_KEY] end - if !@env.blank? && request[API_KEY] && api_parameter_allowed? - api_key ||= request[API_KEY] - end + api_key ||= request[API_KEY] if !@env.blank? && request[API_KEY] && api_parameter_allowed? auth_token = find_auth_token current_user = nil if auth_token - limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60) + limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN, 60) if limiter.can_perform? - @env[USER_TOKEN_KEY] = @user_token = begin - UserAuthToken.lookup( - auth_token, - seen: true, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip - ) - rescue ActiveRecord::ReadOnlyError - nil - end + @env[USER_TOKEN_KEY] = @user_token = + begin + UserAuthToken.lookup( + auth_token, + seen: true, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + ) + rescue ActiveRecord::ReadOnlyError + nil + end current_user = @user_token.try(:user) end @@ -161,14 +147,10 @@ class Auth::DefaultCurrentUserProvider begin limiter.performed! rescue RateLimiter::LimitExceeded - raise Discourse::InvalidAccess.new( - 'Invalid Access', - nil, - delete_cookie: TOKEN_COOKIE - ) + raise Discourse::InvalidAccess.new("Invalid Access", nil, delete_cookie: TOKEN_COOKIE) end end - elsif @env['HTTP_DISCOURSE_LOGGED_IN'] + elsif @env["HTTP_DISCOURSE_LOGGED_IN"] @env[BAD_TOKEN] = true end @@ -177,10 +159,10 @@ class Auth::DefaultCurrentUserProvider current_user = lookup_api_user(api_key, request) if !current_user raise Discourse::InvalidAccess.new( - I18n.t('invalid_api_credentials'), - nil, - custom_message: "invalid_api_credentials" - ) + I18n.t("invalid_api_credentials"), + nil, + custom_message: "invalid_api_credentials", + ) end raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active admin_api_key_limiter.performed! if !Rails.env.profile? @@ -191,12 +173,13 @@ class Auth::DefaultCurrentUserProvider if user_api_key @hashed_user_api_key = ApiKey.hash_key(user_api_key) - user_api_key_obj = UserApiKey - .active - .joins(:user) - .where(key_hash: @hashed_user_api_key) - .includes(:user, :scopes) - .first + user_api_key_obj = + UserApiKey + .active + .joins(:user) + .where(key_hash: @hashed_user_api_key) + .includes(:user, :scopes) + .first raise Discourse::InvalidAccess unless user_api_key_obj @@ -208,18 +191,14 @@ class Auth::DefaultCurrentUserProvider current_user = user_api_key_obj.user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active - if can_write? - user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) - end + user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) if can_write? @env[USER_API_KEY_ENV] = true end # keep this rule here as a safeguard # under no conditions to suspended or inactive accounts get current_user - if current_user && (current_user.suspended? || !current_user.active) - current_user = nil - end + current_user = nil if current_user && (current_user.suspended? || !current_user.active) if current_user && should_update_last_seen? ip = request.ip @@ -247,31 +226,40 @@ class Auth::DefaultCurrentUserProvider if !is_user_api? && @user_token && @user_token.user == user rotated_at = @user_token.rotated_at - needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + needs_rotation = + ( + if @user_token.auth_token_seen + rotated_at < UserAuthToken::ROTATE_TIME.ago + else + rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + end + ) if needs_rotation - if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'], - client_ip: @request.ip, - path: @env['REQUEST_PATH']) + if @user_token.rotate!( + user_agent: @env["HTTP_USER_AGENT"], + client_ip: @request.ip, + path: @env["REQUEST_PATH"], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) DiscourseEvent.trigger(:user_session_refreshed, user) end end end - if !user && cookie_jar.key?(TOKEN_COOKIE) - cookie_jar.delete(TOKEN_COOKIE) - end + cookie_jar.delete(TOKEN_COOKIE) if !user && cookie_jar.key?(TOKEN_COOKIE) end def log_on_user(user, session, cookie_jar, opts = {}) - @env[USER_TOKEN_KEY] = @user_token = UserAuthToken.generate!( - user_id: user.id, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip, - staff: user.staff?, - impersonate: opts[:impersonate]) + @env[USER_TOKEN_KEY] = @user_token = + UserAuthToken.generate!( + user_id: user.id, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + staff: user.staff?, + impersonate: opts[:impersonate], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) user.unstage! @@ -288,23 +276,19 @@ class Auth::DefaultCurrentUserProvider token: unhashed_auth_token, user_id: user.id, trust_level: user.trust_level, - issued_at: Time.zone.now.to_i + issued_at: Time.zone.now.to_i, } - if SiteSetting.persistent_sessions - expires = SiteSetting.maximum_session_age.hours.from_now - end + expires = SiteSetting.maximum_session_age.hours.from_now if SiteSetting.persistent_sessions - if SiteSetting.same_site_cookies != "Disabled" - same_site = SiteSetting.same_site_cookies - end + same_site = SiteSetting.same_site_cookies if SiteSetting.same_site_cookies != "Disabled" cookie_jar.encrypted[TOKEN_COOKIE] = { value: data, httponly: true, secure: SiteSetting.force_https, expires: expires, - same_site: same_site + same_site: same_site, } end @@ -313,10 +297,8 @@ class Auth::DefaultCurrentUserProvider # for signup flow, since all admin emails are stored in # DISCOURSE_DEVELOPER_EMAILS for self-hosters. def make_developer_admin(user) - if user.active? && - !user.admin && - Rails.configuration.respond_to?(:developer_emails) && - Rails.configuration.developer_emails.include?(user.email) + if user.active? && !user.admin && Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(user.email) user.admin = true user.save Group.refresh_automatic_groups!(:staff, :admins) @@ -347,7 +329,7 @@ class Auth::DefaultCurrentUserProvider @user_token.destroy end - cookie_jar.delete('authentication_data') + cookie_jar.delete("authentication_data") cookie_jar.delete(TOKEN_COOKIE) end @@ -384,9 +366,7 @@ class Auth::DefaultCurrentUserProvider if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME] - if !api_key.request_allowed?(@env) - return nil - end + return nil if !api_key.request_allowed?(@env) user = if api_key.user @@ -395,7 +375,8 @@ class Auth::DefaultCurrentUserProvider User.find_by(username_lower: api_username.downcase) elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"] User.find_by(id: user_id.to_i) - elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] + elsif external_id = + header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user) end @@ -435,52 +416,48 @@ class Auth::DefaultCurrentUserProvider limit = GlobalSetting.max_admin_api_reqs_per_minute.to_i if GlobalSetting.respond_to?(:max_admin_api_reqs_per_key_per_minute) - Discourse.deprecate("DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", drop_from: '2.9.0') - limit = [ - GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, - limit - ].max + Discourse.deprecate( + "DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", + drop_from: "2.9.0", + ) + limit = [GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, limit].max end - @admin_api_key_limiter = RateLimiter.new( - nil, - "admin_api_min", - limit, - 60, - error_code: "admin_api_key_rate_limit" - ) + @admin_api_key_limiter = + RateLimiter.new(nil, "admin_api_min", limit, 60, error_code: "admin_api_key_rate_limit") end def user_api_key_limiter_60_secs - @user_api_key_limiter_60_secs ||= RateLimiter.new( - nil, - "user_api_min_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_minute, - 60, - error_code: "user_api_key_limiter_60_secs" - ) + @user_api_key_limiter_60_secs ||= + RateLimiter.new( + nil, + "user_api_min_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_minute, + 60, + error_code: "user_api_key_limiter_60_secs", + ) end def user_api_key_limiter_1_day - @user_api_key_limiter_1_day ||= RateLimiter.new( - nil, - "user_api_day_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_day, - 86400, - error_code: "user_api_key_limiter_1_day" - ) + @user_api_key_limiter_1_day ||= + RateLimiter.new( + nil, + "user_api_day_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_day, + 86_400, + error_code: "user_api_key_limiter_1_day", + ) end def find_auth_token return @auth_token if defined?(@auth_token) - @auth_token = begin - if v0 = self.class.find_v0_auth_cookie(@request) - v0 - elsif v1 = self.class.find_v1_auth_cookie(@env) - if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i - v1[:token] + @auth_token = + begin + if v0 = self.class.find_v0_auth_cookie(@request) + v0 + elsif v1 = self.class.find_v1_auth_cookie(@env) + v1[:token] if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i end end - end end end diff --git a/lib/auth/discord_authenticator.rb b/lib/auth/discord_authenticator.rb index 2629e6d369..56d8923cd4 100644 --- a/lib/auth/discord_authenticator.rb +++ b/lib/auth/discord_authenticator.rb @@ -2,35 +2,34 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator class DiscordStrategy < OmniAuth::Strategies::OAuth2 - option :name, 'discord' - option :scope, 'identify email guilds' + option :name, "discord" + option :scope, "identify email guilds" option :client_options, - site: 'https://discord.com/api', - authorize_url: 'oauth2/authorize', - token_url: 'oauth2/token' + site: "https://discord.com/api", + authorize_url: "oauth2/authorize", + token_url: "oauth2/token" option :authorize_options, %i[scope permissions] - uid { raw_info['id'] } + uid { raw_info["id"] } info do { - name: raw_info['username'], - email: raw_info['verified'] ? raw_info['email'] : nil, - image: "https://cdn.discordapp.com/avatars/#{raw_info['id']}/#{raw_info['avatar']}" + name: raw_info["username"], + email: raw_info["verified"] ? raw_info["email"] : nil, + image: "https://cdn.discordapp.com/avatars/#{raw_info["id"]}/#{raw_info["avatar"]}", } end - extra do - { - 'raw_info' => raw_info - } - end + extra { { "raw_info" => raw_info } } def raw_info - @raw_info ||= access_token.get('users/@me').parsed. - merge(guilds: access_token.get('users/@me/guilds').parsed) + @raw_info ||= + access_token + .get("users/@me") + .parsed + .merge(guilds: access_token.get("users/@me/guilds").parsed) end def callback_url @@ -39,7 +38,7 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator end def name - 'discord' + "discord" end def enabled? @@ -48,23 +47,26 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider DiscordStrategy, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.discord_client_id - strategy.options[:client_secret] = SiteSetting.discord_secret - } - end + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.discord_client_id + strategy.options[:client_secret] = SiteSetting.discord_secret + } + end def after_authenticate(auth_token, existing_account: nil) allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|") if allowed_guild_ids.length > 0 - user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g['id'] } + user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g["id"] } if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds - return Auth::Result.new.tap do |auth_result| - auth_result.failed = true - auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") - end + return( + Auth::Result.new.tap do |auth_result| + auth_result.failed = true + auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") + end + ) end end diff --git a/lib/auth/facebook_authenticator.rb b/lib/auth/facebook_authenticator.rb index 48a57cd9ae..18924c5fc0 100644 --- a/lib/auth/facebook_authenticator.rb +++ b/lib/auth/facebook_authenticator.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator - AVATAR_SIZE ||= 480 def name @@ -14,15 +13,19 @@ class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :facebook, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.facebook_app_id - strategy.options[:client_secret] = SiteSetting.facebook_app_secret - strategy.options[:info_fields] = 'name,first_name,last_name,email' - strategy.options[:image_size] = { width: AVATAR_SIZE, height: AVATAR_SIZE } - strategy.options[:secure_image_url] = true - }, - scope: "email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.facebook_app_id + strategy.options[:client_secret] = SiteSetting.facebook_app_secret + strategy.options[:info_fields] = "name,first_name,last_name,email" + strategy.options[:image_size] = { + width: AVATAR_SIZE, + height: AVATAR_SIZE, + } + strategy.options[:secure_image_url] = true + }, + scope: "email" end # facebook doesn't return unverified email addresses so it's safe to assume diff --git a/lib/auth/github_authenticator.rb b/lib/auth/github_authenticator.rb index 865cb21e0c..03f2913f27 100644 --- a/lib/auth/github_authenticator.rb +++ b/lib/auth/github_authenticator.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'has_errors' +require "has_errors" class Auth::GithubAuthenticator < Auth::ManagedAuthenticator - def name "github" end @@ -50,12 +49,13 @@ class Auth::GithubAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :github, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.github_client_id - strategy.options[:client_secret] = SiteSetting.github_client_secret - }, - scope: "user:email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.github_client_id + strategy.options[:client_secret] = SiteSetting.github_client_secret + }, + scope: "user:email" end # the omniauth-github gem only picks up the primary email if it's verified: diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index ba35b1fecd..3e7c4c4afb 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -22,47 +22,46 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) options = { - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.google_oauth2_client_id - strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret + setup: + lambda do |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.google_oauth2_client_id + strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret - if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? - strategy.options[:hd] = google_oauth2_hd - end + if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? + strategy.options[:hd] = google_oauth2_hd + end - if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? - strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") - end + if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? + strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") + end - # All the data we need for the `info` and `credentials` auth hash - # are obtained via the user info API, not the JWT. Using and verifying - # the JWT can fail due to clock skew, so let's skip it completely. - # https://github.com/zquestz/omniauth-google-oauth2/pull/392 - strategy.options[:skip_jwt] = true - } + # All the data we need for the `info` and `credentials` auth hash + # are obtained via the user info API, not the JWT. Using and verifying + # the JWT can fail due to clock skew, so let's skip it completely. + # https://github.com/zquestz/omniauth-google-oauth2/pull/392 + strategy.options[:skip_jwt] = true + end, } omniauth.provider :google_oauth2, options end def after_authenticate(auth_token, existing_account: nil) groups = provides_groups? ? raw_groups(auth_token.uid) : nil - if groups - auth_token.extra[:raw_groups] = groups - end + auth_token.extra[:raw_groups] = groups if groups result = super if groups - result.associated_groups = groups.map { |group| group.with_indifferent_access.slice(:id, :name) } + result.associated_groups = + groups.map { |group| group.with_indifferent_access.slice(:id, :name) } end result end def provides_groups? - SiteSetting.google_oauth2_hd.present? && - SiteSetting.google_oauth2_hd_groups && + SiteSetting.google_oauth2_hd.present? && SiteSetting.google_oauth2_hd_groups && SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? && SiteSetting.google_oauth2_hd_groups_service_account_json.present? end @@ -77,20 +76,20 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator return if client.nil? loop do - params = { - userKey: uid - } + params = { userKey: uid } params[:pageToken] = page_token if page_token response = client.get(groups_url, params: params, raise_errors: false) if response.status == 200 response = response.parsed - groups.push(*response['groups']) - page_token = response['nextPageToken'] + groups.push(*response["groups"]) + page_token = response["nextPageToken"] break if page_token.nil? else - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}", + ) break end end @@ -107,26 +106,35 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator scope: GROUPS_SCOPE, iat: Time.now.to_i, exp: Time.now.to_i + 60, - sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email + sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email, } headers = { "alg" => "RS256", "typ" => "JWT" } key = OpenSSL::PKey::RSA.new(service_account_info["private_key"]) - encoded_jwt = ::JWT.encode(payload, key, 'RS256', headers) + encoded_jwt = ::JWT.encode(payload, key, "RS256", headers) - client = OAuth2::Client.new( - SiteSetting.google_oauth2_client_id, - SiteSetting.google_oauth2_client_secret, - site: OAUTH2_BASE_URL - ) + client = + OAuth2::Client.new( + SiteSetting.google_oauth2_client_id, + SiteSetting.google_oauth2_client_secret, + site: OAUTH2_BASE_URL, + ) - token_response = client.request(:post, '/token', body: { - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: encoded_jwt - }, raise_errors: false) + token_response = + client.request( + :post, + "/token", + body: { + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: encoded_jwt, + }, + raise_errors: false, + ) if token_response.status != 200 - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}", + ) return end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index 41f36a611f..bb163d1a71 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -56,28 +56,27 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_authenticate(auth_token, existing_account: nil) # Try and find an association for this account - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) # Reconnecting to existing account - if can_connect_existing_user? && existing_account && (association.user.nil? || existing_account.id != association.user_id) + if can_connect_existing_user? && existing_account && + (association.user.nil? || existing_account.id != association.user_id) association.user = existing_account end # Matching an account by email - if match_by_email && - association.user.nil? && - (user = find_user_by_email(auth_token)) - + if match_by_email && association.user.nil? && (user = find_user_by_email(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end # Matching an account by username - if match_by_username && - association.user.nil? && - SiteSetting.username_change_period.zero? && - (user = find_user_by_username(auth_token)) - + if match_by_username && association.user.nil? && SiteSetting.username_change_period.zero? && + (user = find_user_by_username(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -100,7 +99,14 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result = Auth::Result.new info = auth_token[:info] result.email = info[:email] - result.name = (info[:first_name] && info[:last_name]) ? "#{info[:first_name]} #{info[:last_name]}" : info[:name] + result.name = + ( + if (info[:first_name] && info[:last_name]) + "#{info[:first_name]} #{info[:last_name]}" + else + info[:name] + end + ) if result.name.present? && result.name == result.email # Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration) # We add some generic protection here, so that users don't accidently make their email addresses public @@ -109,10 +115,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.username = info[:nickname] result.email_valid = primary_email_verified?(auth_token) if result.email.present? result.overrides_email = always_update_user_email? - result.extra_data = { - provider: auth_token[:provider], - uid: auth_token[:uid] - } + result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] } result.user = association.user result @@ -120,7 +123,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_create_account(user, auth_result) auth_token = auth_result[:extra_data] - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) association.user = user association.save! @@ -132,16 +139,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def find_user_by_email(auth_token) email = auth_token.dig(:info, :email) - if email && primary_email_verified?(auth_token) - User.find_by_email(email) - end + User.find_by_email(email) if email && primary_email_verified?(auth_token) end def find_user_by_username(auth_token) username = auth_token.dig(:info, :nickname) - if username - User.find_by_username(username) - end + User.find_by_username(username) if username end def retrieve_avatar(user, url) @@ -158,7 +161,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator if bio || location profile = user.user_profile - profile.bio_raw = bio unless profile.bio_raw.present? + profile.bio_raw = bio unless profile.bio_raw.present? profile.location = location unless profile.location.present? profile.save end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 397a373f80..8e83fea8ba 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -1,48 +1,48 @@ # frozen_string_literal: true class Auth::Result - ATTRIBUTES = [ - :user, - :name, - :username, - :email, - :email_valid, - :extra_data, - :awaiting_activation, - :awaiting_approval, - :authenticated, - :authenticator_name, - :requires_invite, - :not_allowed_from_ip_address, - :admin_not_allowed_from_ip_address, - :skip_email_validation, - :destination_url, - :omniauth_disallow_totp, - :failed, - :failed_reason, - :failed_code, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + ATTRIBUTES = %i[ + user + name + username + email + email_valid + extra_data + awaiting_activation + awaiting_approval + authenticated + authenticator_name + requires_invite + not_allowed_from_ip_address + admin_not_allowed_from_ip_address + skip_email_validation + destination_url + omniauth_disallow_totp + failed + failed_reason + failed_code + associated_groups + overrides_email + overrides_username + overrides_name ] attr_accessor *ATTRIBUTES # These are stored in the session during # account creation. The user cannot read or modify them - SESSION_ATTRIBUTES = [ - :email, - :username, - :email_valid, - :name, - :authenticator_name, - :extra_data, - :skip_email_validation, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + SESSION_ATTRIBUTES = %i[ + email + username + email_valid + name + authenticator_name + extra_data + skip_email_validation + associated_groups + overrides_email + overrides_username + overrides_name ] def [](key) @@ -59,9 +59,7 @@ class Auth::Result end def email_valid=(val) - if !val.in? [true, false, nil] - raise ArgumentError, "email_valid should be boolean or nil" - end + raise ArgumentError, "email_valid should be boolean or nil" if !val.in? [true, false, nil] @email_valid = !!val end @@ -83,14 +81,14 @@ class Auth::Result def apply_user_attributes! change_made = false - if (SiteSetting.auth_overrides_username? || overrides_username) && (resolved_username = resolve_username).present? + if (SiteSetting.auth_overrides_username? || overrides_username) && + (resolved_username = resolve_username).present? change_made = UsernameChanger.override(user, resolved_username) end - if (SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")) && - email_valid && - email.present? && - user.email != Email.downcase(email) + if ( + SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid") + ) && email_valid && email.present? && user.email != Email.downcase(email) user.email = email change_made = true end @@ -109,11 +107,12 @@ class Auth::Result associated_groups.uniq.each do |associated_group| begin - associated_group = AssociatedGroup.find_or_create_by( - name: associated_group[:name], - provider_id: associated_group[:id], - provider_name: extra_data[:provider] - ) + associated_group = + AssociatedGroup.find_or_create_by( + name: associated_group[:name], + provider_id: associated_group[:id], + provider_name: extra_data[:provider], + ) rescue ActiveRecord::RecordNotUnique retry end @@ -135,22 +134,12 @@ class Auth::Result end def to_client_hash - if requires_invite - return { requires_invite: true } - end + return { requires_invite: true } if requires_invite - if user&.suspended? - return { - suspended: true, - suspended_message: user.suspended_message - } - end + return { suspended: true, suspended_message: user.suspended_message } if user&.suspended? if omniauth_disallow_totp - return { - omniauth_disallow_totp: !!omniauth_disallow_totp, - email: email - } + return { omniauth_disallow_totp: !!omniauth_disallow_totp, email: email } end if user @@ -159,7 +148,7 @@ class Auth::Result awaiting_activation: !!awaiting_activation, awaiting_approval: !!awaiting_approval, not_allowed_from_ip_address: !!not_allowed_from_ip_address, - admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address + admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address, } result[:destination_url] = destination_url if authenticated && destination_url.present? @@ -173,7 +162,7 @@ class Auth::Result auth_provider: authenticator_name, email_valid: !!email_valid, can_edit_username: can_edit_username, - can_edit_name: can_edit_name + can_edit_name: can_edit_name, } result[:destination_url] = destination_url if destination_url.present? @@ -190,9 +179,7 @@ class Auth::Result def staged_user return @staged_user if defined?(@staged_user) - if email.present? && email_valid - @staged_user = User.where(staged: true).find_by_email(email) - end + @staged_user = User.where(staged: true).find_by_email(email) if email.present? && email_valid end def username_suggester_attributes diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb index 35f990bb66..5817f617e3 100644 --- a/lib/auth/twitter_authenticator.rb +++ b/lib/auth/twitter_authenticator.rb @@ -17,11 +17,12 @@ class Auth::TwitterAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :twitter, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key - strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret - } + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key + strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret + } end # twitter doesn't return unverfied email addresses in the API diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb index bc78371f5c..4a0128f4cc 100644 --- a/lib/autospec/base_runner.rb +++ b/lib/autospec/base_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class BaseRunner - # used when starting the runner - preloading happens here def start(opts = {}) end @@ -32,7 +30,5 @@ module Autospec # used to stop the runner def stop end - end - end diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index a6b26aff7f..5dee7dd42c 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -3,11 +3,15 @@ require "rspec/core/formatters/base_text_formatter" require "parallel_tests/rspec/logger_base" -module Autospec; end +module Autospec +end class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter - - RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump + RSpec::Core::Formatters.register self, + :example_passed, + :example_pending, + :example_failed, + :start_dump RSPEC_RESULT = "./tmp/rspec_result" @@ -19,15 +23,15 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter end def example_passed(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap(".", :success) end def example_pending(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("*", :pending) end def example_failed(notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("F", :failure) @fail_file.puts(notification.example.location + " ") @fail_file.flush end @@ -40,5 +44,4 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter @fail_file.close super(filename) end - end diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index e912887d2e..87dded1963 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -7,7 +7,8 @@ require "autospec/reload_css" require "autospec/base_runner" require "socket_server" -module Autospec; end +module Autospec +end class Autospec::Manager def self.run(opts = {}) @@ -25,7 +26,10 @@ class Autospec::Manager end def run - Signal.trap("HUP") { stop_runners; exit } + Signal.trap("HUP") do + stop_runners + exit + end Signal.trap("INT") do begin @@ -47,7 +51,6 @@ class Autospec::Manager STDIN.gets process_queue end - rescue => e fail(e, "failed in run") ensure @@ -71,16 +74,16 @@ class Autospec::Manager @queue.reject! { |_, s, _| s == "spec" } - if current_runner - @queue.concat [['spec', 'spec', current_runner]] - end + @queue.concat [["spec", "spec", current_runner]] if current_runner @runners.each do |runner| - @queue.concat [['spec', 'spec', runner]] unless @queue.any? { |_, s, r| s == "spec" && r == runner } + unless @queue.any? { |_, s, r| s == "spec" && r == runner } + @queue.concat [["spec", "spec", runner]] + end end end - [:start, :stop, :abort].each do |verb| + %i[start stop abort].each do |verb| define_method("#{verb}_runners") do puts "@@@@@@@@@@@@ #{verb}_runners" if @debug @runners.each(&verb) @@ -89,11 +92,7 @@ class Autospec::Manager def start_service_queue puts "@@@@@@@@@@@@ start_service_queue" if @debug - Thread.new do - while true - thread_loop - end - end + Thread.new { thread_loop while true } end # the main loop, will run the specs in the queue till one fails or the queue is empty @@ -176,9 +175,7 @@ class Autospec::Manager Dir[root_path + "/plugins/*"].each do |f| next if !File.directory? f resolved = File.realpath(f) - if resolved != f - map[resolved] = f - end + map[resolved] = f if resolved != f end map end @@ -188,9 +185,7 @@ class Autospec::Manager resolved = file @reverse_map ||= reverse_symlink_map @reverse_map.each do |location, discourse_location| - if file.start_with?(location) - resolved = discourse_location + file[location.length..-1] - end + resolved = discourse_location + file[location.length..-1] if file.start_with?(location) end resolved @@ -199,9 +194,7 @@ class Autospec::Manager def listen_for_changes puts "@@@@@@@@@@@@ listen_for_changes" if @debug - options = { - ignore: /^lib\/autospec/, - } + options = { ignore: %r{^lib/autospec} } if @opts[:force_polling] options[:force_polling] = true @@ -210,14 +203,14 @@ class Autospec::Manager path = root_path - if ENV['VIM_AUTOSPEC'] + if ENV["VIM_AUTOSPEC"] STDERR.puts "Using VIM file listener" socket_path = (Rails.root + "tmp/file_change.sock").to_s FileUtils.rm_f(socket_path) server = SocketServer.new(socket_path) server.start do |line| - file, line = line.split(' ') + file, line = line.split(" ") file = reverse_symlink(file) file = file.sub(Rails.root.to_s + "/", "") # process_change can acquire a mutex and block @@ -235,20 +228,20 @@ class Autospec::Manager end # to speed up boot we use a thread - ["spec", "lib", "app", "config", "test", "vendor", "plugins"].each do |watch| - + %w[spec lib app config test vendor plugins].each do |watch| puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug Thread.new do begin - listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _| - paths = [modified, added].flatten - paths.compact! - paths.map! do |long| - long = reverse_symlink(long) - long[(path.length + 1)..-1] + listener = + Listen.to("#{path}/#{watch}", options) do |modified, added, _| + paths = [modified, added].flatten + paths.compact! + paths.map! do |long| + long = reverse_symlink(long) + long[(path.length + 1)..-1] + end + process_change(paths) end - process_change(paths) - end listener.start sleep rescue => e @@ -257,7 +250,6 @@ class Autospec::Manager end end end - end def process_change(files) @@ -285,13 +277,9 @@ class Autospec::Manager hit = true spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file with_line = spec - if spec == file && line - with_line = spec + ":" << line.to_s - end + with_line = spec + ":" << line.to_s if spec == file && line if File.exist?(spec) || Dir.exist?(spec) - if with_line != spec - specs << [file, spec, runner] - end + specs << [file, spec, runner] if with_line != spec specs << [file, with_line, runner] end end @@ -329,9 +317,7 @@ class Autospec::Manager focus = @queue.shift @queue.unshift([file, spec, runner]) unless spec.include?(":") && focus[1].include?(spec.split(":")[0]) - if focus[1].include?(spec) || file != spec - @queue.unshift(focus) - end + @queue.unshift(focus) if focus[1].include?(spec) || file != spec end else @queue.unshift([file, spec, runner]) diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb index 8f24cdec98..78258bed17 100644 --- a/lib/autospec/reload_css.rb +++ b/lib/autospec/reload_css.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -module Autospec; end +module Autospec +end class Autospec::ReloadCss - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -30,7 +30,7 @@ class Autospec::ReloadCss if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ } # todo connect to dev instead? ActiveRecord::Base.establish_connection - [:desktop, :mobile].each do |style| + %i[desktop mobile].each do |style| s = DiscourseStylesheets.new(style) s.compile paths << "public" + s.stylesheet_relpath_no_digest @@ -44,10 +44,9 @@ class Autospec::ReloadCss p = p.sub(/\.sass\.erb/, "") p = p.sub(/\.sass/, "") p = p.sub(/\.scss/, "") - p = p.sub(/^app\/assets\/stylesheets/, "assets") + p = p.sub(%r{^app/assets/stylesheets}, "assets") { name: p, hash: hash || SecureRandom.hex } end message_bus.publish "/file-change", paths end - end diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb index b4ebbf3ca2..408b6fe79e 100644 --- a/lib/autospec/rspec_runner.rb +++ b/lib/autospec/rspec_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class RspecRunner < BaseRunner - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -13,26 +11,28 @@ module Autospec end # Discourse specific - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^spec/support/.+\.rb$}) { "spec" } - watch("app/controllers/application_controller.rb") { "spec/requests" } + watch(%r{^spec/support/.+\.rb$}) { "spec" } + watch("app/controllers/application_controller.rb") { "spec/requests" } - watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } + watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } - watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } + watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) do + "spec/components/pretty_text_spec.rb" + end watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } watch(%r{^plugins/.*/spec/.*\.rb}) - watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } - watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } + watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } + watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } watch(%r{^(plugins/.*)/lib/(.*)\.rb}) { |m| "#{m[1]}/spec/lib/#{m[2]}_spec.rb" } RELOADERS = Set.new @@ -50,11 +50,9 @@ module Autospec def failed_specs specs = [] - path = './tmp/rspec_result' + path = "./tmp/rspec_result" specs = File.readlines(path) if File.exist?(path) specs end - end - end diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index 62fcbc21a0..dcf88e4443 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -3,7 +3,6 @@ require "autospec/rspec_runner" module Autospec - class SimpleRunner < RspecRunner def initialize @mutex = Mutex.new @@ -12,36 +11,29 @@ module Autospec def run(specs) puts "Running Rspec: #{specs}" # kill previous rspec instance - @mutex.synchronize do - self.abort - end + @mutex.synchronize { self.abort } # we use our custom rspec formatter - args = [ - "-r", "#{File.dirname(__FILE__)}/formatter.rb", - "-f", "Autospec::Formatter" - ] + args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter"] - command = begin - line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line - multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files - if ENV["PARALLEL_SPEC"] == '1' && multiple_files && !line_specified - "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" - else - "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + command = + begin + line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line + multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files + if ENV["PARALLEL_SPEC"] == "1" && multiple_files && !line_specified + "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" + else + "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + end end - end # launch rspec Dir.chdir(Rails.root) do # rubocop:disable Discourse/NoChdir because this is not part of the app env = { "RAILS_ENV" => "test" } - if specs.split(' ').any? { |s| s =~ /^(.\/)?plugins/ } + if specs.split(" ").any? { |s| s =~ %r{^(./)?plugins} } env["LOAD_PLUGINS"] = "1" puts "Loading plugins while running specs" end - pid = - @mutex.synchronize do - @pid = Process.spawn(env, command) - end + pid = @mutex.synchronize { @pid = Process.spawn(env, command) } _, status = Process.wait2(pid) @@ -51,7 +43,11 @@ module Autospec def abort if pid = @pid - Process.kill("TERM", pid) rescue nil + begin + Process.kill("TERM", pid) + rescue StandardError + nil + end wait_for_done(pid) pid = nil end @@ -66,16 +62,26 @@ module Autospec def wait_for_done(pid) i = 3000 - while (i > 0 && Process.getpgid(pid) rescue nil) + while ( + begin + i > 0 && Process.getpgid(pid) + rescue StandardError + nil + end + ) sleep 0.001 i -= 1 end - if (Process.getpgid(pid) rescue nil) + if ( + begin + Process.getpgid(pid) + rescue StandardError + nil + end + ) STDERR.puts "Terminating rspec #{pid} by force cause it refused graceful termination" Process.kill("KILL", pid) end end - end - end diff --git a/lib/backup_restore.rb b/lib/backup_restore.rb index 291567a6f0..b8c7fa7784 100644 --- a/lib/backup_restore.rb +++ b/lib/backup_restore.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module BackupRestore - - class OperationRunningError < RuntimeError; end + class OperationRunningError < RuntimeError + end VERSION_PREFIX = "v" DUMP_FILE = "dump.sql.gz" @@ -22,9 +22,7 @@ module BackupRestore def self.rollback! raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running? - if can_rollback? - move_tables_between_schemas("backup", "public") - end + move_tables_between_schemas("backup", "public") if can_rollback? end def self.cancel! @@ -58,7 +56,7 @@ module BackupRestore { is_operation_running: is_operation_running?, can_rollback: can_rollback?, - allow_restore: Rails.env.development? || SiteSetting.allow_restore + allow_restore: Rails.env.development? || SiteSetting.allow_restore, } end @@ -133,7 +131,7 @@ module BackupRestore config["backup_port"] || config["port"], config["username"] || username || ENV["USER"] || "postgres", config["password"] || password, - config["database"] + config["database"], ) end @@ -194,7 +192,11 @@ module BackupRestore end def self.backup_tables_count - DB.query_single("SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'").first.to_i + DB + .query_single( + "SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'", + ) + .first + .to_i end - end diff --git a/lib/backup_restore/backup_file_handler.rb b/lib/backup_restore/backup_file_handler.rb index 483d6f6473..645fad9f64 100644 --- a/lib/backup_restore/backup_file_handler.rb +++ b/lib/backup_restore/backup_file_handler.rb @@ -69,15 +69,22 @@ module BackupRestore path_transformation = case tar_implementation when :gnu - ['--transform', 's|var/www/discourse/public/uploads/|uploads/|'] + %w[--transform s|var/www/discourse/public/uploads/|uploads/|] when :bsd - ['-s', '|var/www/discourse/public/uploads/|uploads/|'] + %w[-s |var/www/discourse/public/uploads/|uploads/|] end log "Unzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'tar', '--extract', '--gzip', '--file', @archive_path, '--directory', @tmp_directory, - *path_transformation, failure_message: "Failed to decompress archive." + "tar", + "--extract", + "--gzip", + "--file", + @archive_path, + "--directory", + @tmp_directory, + *path_transformation, + failure_message: "Failed to decompress archive.", ) end @@ -86,15 +93,19 @@ module BackupRestore if @is_archive # for compatibility with backups from Discourse v1.5 and below old_dump_path = File.join(@tmp_directory, OLD_DUMP_FILENAME) - File.exist?(old_dump_path) ? old_dump_path : File.join(@tmp_directory, BackupRestore::DUMP_FILE) + if File.exist?(old_dump_path) + old_dump_path + else + File.join(@tmp_directory, BackupRestore::DUMP_FILE) + end else File.join(@tmp_directory, @filename) end - if File.extname(@db_dump_path) == '.gz' + if File.extname(@db_dump_path) == ".gz" log "Extracting dump file..." Compression::Gzip.new.decompress(@tmp_directory, @db_dump_path, available_size) - @db_dump_path.delete_suffix!('.gz') + @db_dump_path.delete_suffix!(".gz") end @db_dump_path @@ -105,17 +116,18 @@ module BackupRestore end def tar_implementation - @tar_version ||= begin - tar_version = Discourse::Utils.execute_command('tar', '--version') + @tar_version ||= + begin + tar_version = Discourse::Utils.execute_command("tar", "--version") - if tar_version.include?("GNU tar") - :gnu - elsif tar_version.include?("bsdtar") - :bsd - else - raise "Unknown tar implementation: #{tar_version}" + if tar_version.include?("GNU tar") + :gnu + elsif tar_version.include?("bsdtar") + :bsd + else + raise "Unknown tar implementation: #{tar_version}" + end end - end end end end diff --git a/lib/backup_restore/backup_store.rb b/lib/backup_restore/backup_store.rb index 65a3acaa95..5afcec27bf 100644 --- a/lib/backup_restore/backup_store.rb +++ b/lib/backup_restore/backup_store.rb @@ -37,9 +37,7 @@ module BackupRestore return unless cleanup_allowed? return if (backup_files = files).size <= SiteSetting.maximum_backups - backup_files[SiteSetting.maximum_backups..-1].each do |file| - delete_file(file.filename) - end + backup_files[SiteSetting.maximum_backups..-1].each { |file| delete_file(file.filename) } reset_cache end @@ -74,7 +72,7 @@ module BackupRestore used_bytes: used_bytes, free_bytes: free_bytes, count: files.size, - last_backup_taken_at: latest_file&.last_modified + last_backup_taken_at: latest_file&.last_modified, } end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index 83c10911b3..2483f5bb7b 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -4,7 +4,6 @@ require "mini_mime" require "file_store/s3_store" module BackupRestore - class Backuper attr_reader :success @@ -84,7 +83,11 @@ module BackupRestore @dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE) @archive_directory = BackupRestore::LocalBackupStore.base_directory(db: @current_db) filename = @filename_override || "#{get_parameterized_title}-#{@timestamp}" - @archive_basename = File.join(@archive_directory, "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}") + @archive_basename = + File.join( + @archive_directory, + "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}", + ) @backup_filename = if @with_uploads @@ -119,9 +122,18 @@ module BackupRestore BackupMetadata.delete_all BackupMetadata.create!(name: "base_url", value: Discourse.base_url) BackupMetadata.create!(name: "cdn_url", value: Discourse.asset_host) - BackupMetadata.create!(name: "s3_base_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil) - BackupMetadata.create!(name: "s3_cdn_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil) - BackupMetadata.create!(name: "db_name", value: RailsMultisite::ConnectionManagement.current_db) + BackupMetadata.create!( + name: "s3_base_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil, + ) + BackupMetadata.create!( + name: "s3_cdn_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil, + ) + BackupMetadata.create!( + name: "db_name", + value: RailsMultisite::ConnectionManagement.current_db, + ) BackupMetadata.create!(name: "multisite", value: Rails.configuration.multisite) end @@ -132,7 +144,7 @@ module BackupRestore pg_dump_running = true Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) while pg_dump_running message = logs.pop.strip log(message) unless message.blank? @@ -159,23 +171,24 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to pg_dump (if any) - "pg_dump", # the pg_dump command - "--schema=public", # only public schema - "-T public.pg_*", # exclude tables and views whose name starts with "pg_" + [ + password_argument, # pass the password to pg_dump (if any) + "pg_dump", # the pg_dump command + "--schema=public", # only public schema + "-T public.pg_*", # exclude tables and views whose name starts with "pg_" "--file='#{@dump_filename}'", # output to the dump.sql file - "--no-owner", # do not output commands to set ownership of objects - "--no-privileges", # prevent dumping of access privileges - "--verbose", # specifies verbose mode - "--compress=4", # Compression level of 4 - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument, # the username to connect as (if any) - db_conf.database # the name of the database to dump + "--no-owner", # do not output commands to set ownership of objects + "--no-privileges", # prevent dumping of access privileges + "--verbose", # specifies verbose mode + "--compress=4", # Compression level of 4 + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) + db_conf.database, # the name of the database to dump ].join(" ") end @@ -185,8 +198,10 @@ module BackupRestore archive_filename = File.join(@archive_directory, @backup_filename) Discourse::Utils.execute_command( - 'mv', @dump_filename, archive_filename, - failure_message: "Failed to move database dump file." + "mv", + @dump_filename, + archive_filename, + failure_message: "Failed to move database dump file.", ) remove_tmp_directory @@ -198,17 +213,29 @@ module BackupRestore tar_filename = "#{@archive_basename}.tar" log "Making sure archive does not already exist..." - Discourse::Utils.execute_command('rm', '-f', tar_filename) - Discourse::Utils.execute_command('rm', '-f', "#{tar_filename}.gz") + Discourse::Utils.execute_command("rm", "-f", tar_filename) + Discourse::Utils.execute_command("rm", "-f", "#{tar_filename}.gz") log "Creating empty archive..." - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, '--files-from', '/dev/null') + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + "--files-from", + "/dev/null", + ) log "Archiving data dump..." Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', '--file', tar_filename, File.basename(@dump_filename), + "tar", + "--append", + "--dereference", + "--file", + tar_filename, + File.basename(@dump_filename), failure_message: "Failed to archive data dump.", - chdir: File.dirname(@dump_filename) + chdir: File.dirname(@dump_filename), ) add_local_uploads_to_archive(tar_filename) @@ -218,8 +245,10 @@ module BackupRestore log "Gzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'gzip', "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", tar_filename, - failure_message: "Failed to gzip archive." + "gzip", + "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", + tar_filename, + failure_message: "Failed to gzip archive.", ) end @@ -244,14 +273,21 @@ module BackupRestore if SiteSetting.include_thumbnails_in_backups exclude_optimized = "" else - optimized_path = File.join(upload_directory, 'optimized') + optimized_path = File.join(upload_directory, "optimized") exclude_optimized = "--exclude=#{optimized_path}" end Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', exclude_optimized, '--file', tar_filename, upload_directory, - failure_message: "Failed to archive uploads.", success_status_codes: [0, 1], - chdir: File.join(Rails.root, "public") + "tar", + "--append", + "--dereference", + exclude_optimized, + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to archive uploads.", + success_status_codes: [0, 1], + chdir: File.join(Rails.root, "public"), ) else log "No local uploads found. Skipping archiving of local uploads..." @@ -287,9 +323,14 @@ module BackupRestore log "Appending uploads to archive..." Discourse::Utils.execute_command( - 'tar', '--append', '--file', tar_filename, upload_directory, - failure_message: "Failed to append uploads to archive.", success_status_codes: [0, 1], - chdir: @tmp_directory + "tar", + "--append", + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to append uploads to archive.", + success_status_codes: [0, 1], + chdir: @tmp_directory, ) log "No uploads found on S3. Skipping archiving of uploads stored on S3..." if count == 0 @@ -327,9 +368,7 @@ module BackupRestore logs = Discourse::Utils.logs_markdown(@logs, user: @user) post = SystemMessage.create_from_system_user(@user, status, logs: logs) - if @user.id == Discourse::SYSTEM_USER_ID - post.topic.invite_group(@user, Group[:admins]) - end + post.topic.invite_group(@user, Group[:admins]) if @user.id == Discourse::SYSTEM_USER_ID rescue => ex log "Something went wrong while notifying user.", ex end @@ -399,7 +438,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "backup", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) @@ -416,5 +460,4 @@ module BackupRestore end end end - end diff --git a/lib/backup_restore/database_restorer.rb b/lib/backup_restore/database_restorer.rb index ae3ac7deaa..2a3bc6985c 100644 --- a/lib/backup_restore/database_restorer.rb +++ b/lib/backup_restore/database_restorer.rb @@ -46,9 +46,7 @@ module BackupRestore end def self.drop_backup_schema - if backup_schema_dropable? - ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) - end + ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) if backup_schema_dropable? end def self.core_migration_files @@ -65,13 +63,14 @@ module BackupRestore last_line = nil psql_running = true - log_thread = Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) - while psql_running || !logs.empty? - message = logs.pop.strip - log(message) if message.present? + log_thread = + Thread.new do + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) + while psql_running || !logs.empty? + message = logs.pop.strip + log(message) if message.present? + end end - end IO.popen(restore_dump_command) do |pipe| begin @@ -89,7 +88,9 @@ module BackupRestore logs << "" log_thread.join - raise DatabaseRestoreError.new("psql failed: #{last_line}") if Process.last_status&.exitstatus != 0 + if Process.last_status&.exitstatus != 0 + raise DatabaseRestoreError.new("psql failed: #{last_line}") + end end # Removes unwanted SQL added by certain versions of pg_dump and modifies @@ -99,7 +100,7 @@ module BackupRestore "DROP SCHEMA", # Discourse <= v1.5 "CREATE SCHEMA", # PostgreSQL 11+ "COMMENT ON SCHEMA", # PostgreSQL 11+ - "SET default_table_access_method" # PostgreSQL 12 + "SET default_table_access_method", # PostgreSQL 12 ].join("|") command = "sed -E '/^(#{unwanted_sql})/d' #{@db_dump_path}" @@ -117,18 +118,19 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? - username_argument = "--username=#{db_conf.username}" if db_conf.username.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to psql (if any) - "psql", # the psql command + [ + password_argument, # pass the password to psql (if any) + "psql", # the psql command "--dbname='#{db_conf.database}'", # connect to database *dbname* - "--single-transaction", # all or nothing (also runs COPY commands faster) - "--variable=ON_ERROR_STOP=1", # stop on first error - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument # the username to connect as (if any) + "--single-transaction", # all or nothing (also runs COPY commands faster) + "--variable=ON_ERROR_STOP=1", # stop on first error + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) ].compact.join(" ") end @@ -136,21 +138,22 @@ module BackupRestore log "Migrating the database..." log Discourse::Utils.execute_command( - { - "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", - "SKIP_OPTIMIZE_ICONS" => "1", - "DISABLE_TRANSLATION_OVERRIDES" => "1" - }, - "rake", "db:migrate", - failure_message: "Failed to migrate database.", - chdir: Rails.root - ) + { + "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", + "SKIP_OPTIMIZE_ICONS" => "1", + "DISABLE_TRANSLATION_OVERRIDES" => "1", + }, + "rake", + "db:migrate", + failure_message: "Failed to migrate database.", + chdir: Rails.root, + ) end def reconnect_database log "Reconnecting to the database..." - RailsMultisite::ConnectionManagement::reload if RailsMultisite::ConnectionManagement::instance - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.reload if RailsMultisite::ConnectionManagement.instance + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) end def create_missing_discourse_functions @@ -179,10 +182,12 @@ module BackupRestore end end - existing_function_names = Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } + existing_function_names = + Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } all_readonly_table_columns.each do |table_name, column_name| - function_name = Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) + function_name = + Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) if !existing_function_names.include?(function_name) Migration::BaseDropper.create_readonly_function(table_name, column_name) diff --git a/lib/backup_restore/local_backup_store.rb b/lib/backup_restore/local_backup_store.rb index fd7f16b9f6..77ccfcce09 100644 --- a/lib/backup_restore/local_backup_store.rb +++ b/lib/backup_restore/local_backup_store.rb @@ -12,7 +12,12 @@ module BackupRestore end def self.chunk_path(identifier, filename, chunk_number) - File.join(LocalBackupStore.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") + File.join( + LocalBackupStore.base_directory, + "tmp", + identifier, + "#{filename}.part#{chunk_number}", + ) end def initialize(opts = {}) @@ -39,7 +44,7 @@ module BackupRestore def download_file(filename, destination, failure_message = "") path = path_from_filename(filename) - Discourse::Utils.execute_command('cp', path, destination, failure_message: failure_message) + Discourse::Utils.execute_command("cp", path, destination, failure_message: failure_message) end private @@ -59,7 +64,7 @@ module BackupRestore filename: File.basename(path), size: File.size(path), last_modified: File.mtime(path).utc, - source: include_download_source ? path : nil + source: include_download_source ? path : nil, ) end diff --git a/lib/backup_restore/logger.rb b/lib/backup_restore/logger.rb index fe777d389b..f9c0eab642 100644 --- a/lib/backup_restore/logger.rb +++ b/lib/backup_restore/logger.rb @@ -32,7 +32,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "restore", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) diff --git a/lib/backup_restore/meta_data_handler.rb b/lib/backup_restore/meta_data_handler.rb index 02bc0e98bf..0996d8866b 100644 --- a/lib/backup_restore/meta_data_handler.rb +++ b/lib/backup_restore/meta_data_handler.rb @@ -28,8 +28,10 @@ module BackupRestore log " Restored version: #{metadata[:version]}" if metadata[:version] > @current_version - raise MigrationRequiredError.new("You're trying to restore a more recent version of the schema. " \ - "You should migrate first!") + raise MigrationRequiredError.new( + "You're trying to restore a more recent version of the schema. " \ + "You should migrate first!", + ) end metadata diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index 00bedab241..8caf5dafec 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -65,8 +65,8 @@ module BackupRestore after_restore_hook rescue Compression::Strategy::ExtractFailed - log 'ERROR: The uncompressed file is too big. Consider increasing the hidden ' \ - '"decompressed_backup_max_file_size_mb" setting.' + log "ERROR: The uncompressed file is too big. Consider increasing the hidden " \ + '"decompressed_backup_max_file_size_mb" setting.' @database_restorer.rollback rescue SystemExit log "Restore process was cancelled!" @@ -118,10 +118,10 @@ module BackupRestore DiscourseEvent.trigger(:site_settings_restored) - if @disable_emails && SiteSetting.disable_emails == 'no' + if @disable_emails && SiteSetting.disable_emails == "no" log "Disabling outgoing emails for non-staff users..." user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + SiteSetting.set_and_log(:disable_emails, "non-staff", user) end end @@ -152,7 +152,7 @@ module BackupRestore post = SystemMessage.create_from_system_user(user, status, logs: logs) else log "Could not send notification to '#{@user_info[:username]}' " \ - "(#{@user_info[:email]}), because the user does not exist." + "(#{@user_info[:email]}), because the user does not exist." end rescue => ex log "Something went wrong while notifying user.", ex diff --git a/lib/backup_restore/s3_backup_store.rb b/lib/backup_restore/s3_backup_store.rb index 591eb2ed06..10e2798642 100644 --- a/lib/backup_restore/s3_backup_store.rb +++ b/lib/backup_restore/s3_backup_store.rb @@ -4,8 +4,11 @@ module BackupRestore class S3BackupStore < BackupStore UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 6.hours.to_i - delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts, - :complete_multipart, to: :s3_helper + delegate :abort_multipart, + :presign_multipart_part, + :list_multipart_parts, + :complete_multipart, + to: :s3_helper def initialize(opts = {}) @s3_options = S3Helper.s3_options(SiteSetting) @@ -13,7 +16,7 @@ module BackupRestore end def s3_helper - @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, '', @s3_options.clone) + @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, "", @s3_options.clone) end def remote? @@ -57,11 +60,17 @@ module BackupRestore presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS) rescue Aws::Errors::ServiceError => e - Rails.logger.warn("Failed to generate upload URL for S3: #{e.message.presence || e.class.name}") + Rails.logger.warn( + "Failed to generate upload URL for S3: #{e.message.presence || e.class.name}", + ) raise StorageError.new(e.message.presence || e.class.name) end - def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {}) + def signed_url_for_temporary_upload( + file_name, + expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, + metadata: {} + ) obj = object_from_path(file_name) raise BackupFileExists.new if obj.exists? key = temporary_upload_path(file_name) @@ -71,8 +80,8 @@ module BackupRestore expires_in: expires_in, opts: { metadata: metadata, - acl: "private" - } + acl: "private", + }, ) end @@ -84,7 +93,7 @@ module BackupRestore folder_prefix = s3_helper.s3_bucket_folder_path.nil? ? "" : s3_helper.s3_bucket_folder_path if Rails.env.test? - folder_prefix = File.join(folder_prefix, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + folder_prefix = File.join(folder_prefix, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") end folder_prefix @@ -105,7 +114,10 @@ module BackupRestore s3_helper.copy( existing_external_upload_key, File.join(s3_helper.s3_bucket_folder_path, original_filename), - options: { acl: "private", apply_metadata_to_destination: true } + options: { + acl: "private", + apply_metadata_to_destination: true, + }, ) s3_helper.delete_object(existing_external_upload_key) end @@ -120,9 +132,7 @@ module BackupRestore objects = [] s3_helper.list.each do |obj| - if obj.key.match?(file_regex) - objects << create_file_from_object(obj) - end + objects << create_file_from_object(obj) if obj.key.match?(file_regex) end objects @@ -137,7 +147,7 @@ module BackupRestore filename: File.basename(obj.key), size: obj.size, last_modified: obj.last_modified, - source: include_download_source ? presigned_url(obj, :get, expires) : nil + source: include_download_source ? presigned_url(obj, :get, expires) : nil, ) end @@ -154,16 +164,17 @@ module BackupRestore end def file_regex - @file_regex ||= begin - path = s3_helper.s3_bucket_folder_path || "" + @file_regex ||= + begin + path = s3_helper.s3_bucket_folder_path || "" - if path.present? - path = "#{path}/" unless path.end_with?("/") - path = Regexp.quote(path) + if path.present? + path = "#{path}/" unless path.end_with?("/") + path = Regexp.quote(path) + end + + %r{^#{path}[^/]*\.t?gz$}i end - - /^#{path}[^\/]*\.t?gz$/i - end end def free_bytes diff --git a/lib/backup_restore/system_interface.rb b/lib/backup_restore/system_interface.rb index c0d209e538..31bd4eb944 100644 --- a/lib/backup_restore/system_interface.rb +++ b/lib/backup_restore/system_interface.rb @@ -98,9 +98,7 @@ module BackupRestore def flush_redis redis = Discourse.redis - redis.scan_each(match: "*") do |key| - redis.del(key) unless key == SidekiqPauser::PAUSED_KEY - end + redis.scan_each(match: "*") { |key| redis.del(key) unless key == SidekiqPauser::PAUSED_KEY } end def clear_sidekiq_queues diff --git a/lib/backup_restore/uploads_restorer.rb b/lib/backup_restore/uploads_restorer.rb index 7409c38f56..8b6c13e17d 100644 --- a/lib/backup_restore/uploads_restorer.rb +++ b/lib/backup_restore/uploads_restorer.rb @@ -11,11 +11,12 @@ module BackupRestore def self.s3_regex_string(s3_base_url) clean_url = s3_base_url.sub(S3_ENDPOINT_REGEX, ".s3.amazonaws.com") - regex_string = clean_url - .split(".s3.amazonaws.com") - .map { |s| Regexp.escape(s) } - .insert(1, S3_ENDPOINT_REGEX.source) - .join("") + regex_string = + clean_url + .split(".s3.amazonaws.com") + .map { |s| Regexp.escape(s) } + .insert(1, S3_ENDPOINT_REGEX.source) + .join("") [regex_string, clean_url] end @@ -25,12 +26,16 @@ module BackupRestore end def restore(tmp_directory) - upload_directories = Dir.glob(File.join(tmp_directory, "uploads", "*")) - .reject { |path| File.basename(path).start_with?("PaxHeaders") } + upload_directories = + Dir + .glob(File.join(tmp_directory, "uploads", "*")) + .reject { |path| File.basename(path).start_with?("PaxHeaders") } if upload_directories.count > 1 - raise UploadsRestoreError.new("Could not find uploads, because the uploads " \ - "directory contains multiple folders.") + raise UploadsRestoreError.new( + "Could not find uploads, because the uploads " \ + "directory contains multiple folders.", + ) end @tmp_uploads_path = upload_directories.first @@ -55,7 +60,9 @@ module BackupRestore if !store.respond_to?(:copy_from) # a FileStore implementation from a plugin might not support this method, so raise a helpful error store_name = Discourse.store.class.name - raise UploadsRestoreError.new("The current file store (#{store_name}) does not support restoring uploads.") + raise UploadsRestoreError.new( + "The current file store (#{store_name}) does not support restoring uploads.", + ) end log "Restoring uploads, this may take a while..." @@ -89,13 +96,17 @@ module BackupRestore remap(old_base_url, Discourse.base_url) end - current_s3_base_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil - if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && old_s3_base_url != current_s3_base_url + current_s3_base_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil + if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && + old_s3_base_url != current_s3_base_url remap_s3("#{old_s3_base_url}/", uploads_folder) end - current_s3_cdn_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil - if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && old_s3_cdn_url != current_s3_cdn_url + current_s3_cdn_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil + if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && + old_s3_cdn_url != current_s3_cdn_url base_url = current_s3_cdn_url || Discourse.base_url remap("#{old_s3_cdn_url}/", UrlHelper.schemaless("#{base_url}#{uploads_folder}")) @@ -113,10 +124,7 @@ module BackupRestore remap(old_host, new_host) if old_host != new_host end - if @previous_db_name != @current_db_name - remap("/uploads/#{@previous_db_name}/", upload_path) - end - + remap("/uploads/#{@previous_db_name}/", upload_path) if @previous_db_name != @current_db_name rescue => ex log "Something went wrong while remapping uploads.", ex end @@ -130,7 +138,12 @@ module BackupRestore if old_s3_base_url.include?("amazonaws.com") from_regex, from_clean_url = self.class.s3_regex_string(old_s3_base_url) log "Remapping with regex from '#{from_clean_url}' to '#{uploads_folder}'" - DbHelper.regexp_replace(from_regex, uploads_folder, verbose: true, excluded_tables: ["backup_metadata"]) + DbHelper.regexp_replace( + from_regex, + uploads_folder, + verbose: true, + excluded_tables: ["backup_metadata"], + ) else remap(old_s3_base_url, uploads_folder) end @@ -141,13 +154,15 @@ module BackupRestore DB.exec("TRUNCATE TABLE optimized_images") SiteIconManager.ensure_optimized! - User.where("uploaded_avatar_id IS NOT NULL").find_each do |user| - Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) - end + User + .where("uploaded_avatar_id IS NOT NULL") + .find_each do |user| + Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) + end end def rebake_posts_with_uploads - log 'Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed.' + log "Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed." log 'You can expedite the process by manually running "rake posts:rebake_uncooked_posts"' DB.exec(<<~SQL) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 0f8a4c4832..4eda809873 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -173,7 +173,7 @@ module BadgeQueries <<~SQL SELECT p.user_id, p.id post_id, current_timestamp granted_at FROM badge_posts p - WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND + WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1"} AND p.like_count >= #{count.to_i} AND (:backfill OR p.id IN (:post_ids) ) SQL end @@ -271,5 +271,4 @@ module BadgeQueries WHERE "rank" = 1 SQL end - end diff --git a/lib/bookmark_query.rb b/lib/bookmark_query.rb index f62ce591fa..3a54a19580 100644 --- a/lib/bookmark_query.rb +++ b/lib/bookmark_query.rb @@ -11,9 +11,7 @@ class BookmarkQuery def self.preload(bookmarks, object) preload_polymorphic_associations(bookmarks, object.guardian) - if @preload - @preload.each { |preload| preload.call(bookmarks, object) } - end + @preload.each { |preload| preload.call(bookmarks, object) } if @preload end # These polymorphic associations are loaded to make the UserBookmarkListSerializer's @@ -42,24 +40,27 @@ class BookmarkQuery ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil - queries = Bookmark.registered_bookmarkables.map do |bookmarkable| - interim_results = bookmarkable.perform_list_query(@user, @guardian) + queries = + Bookmark + .registered_bookmarkables + .map do |bookmarkable| + interim_results = bookmarkable.perform_list_query(@user, @guardian) - # this could occur if there is some security reason that the user cannot - # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark - # on a topic and that topic was moved into a private category - next if interim_results.blank? + # this could occur if there is some security reason that the user cannot + # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark + # on a topic and that topic was moved into a private category + next if interim_results.blank? - if search_term.present? - interim_results = bookmarkable.perform_search_query( - interim_results, search_term_wildcard, ts_query - ) - end + if search_term.present? + interim_results = + bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query) + end - # this is purely to make the query easy to read and debug, otherwise it's - # all mashed up into a massive ball in MiniProfiler :) - "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" - end.compact + # this is purely to make the query easy to read and debug, otherwise it's + # all mashed up into a massive ball in MiniProfiler :) + "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" + end + .compact # same for interim results being blank, the user might have been locked out # from all their various bookmarks, in which case they will see nothing and @@ -68,17 +69,16 @@ class BookmarkQuery union_sql = queries.join("\n\nUNION\n\n") results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks") - results = results.order( - "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), + results = + results.order( + "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), bookmarks.reminder_at ASC, - bookmarks.updated_at DESC" - ) + bookmarks.updated_at DESC", + ) @count = results.count - if @page.positive? - results = results.offset(@page * @params[:per_page]) - end + results = results.offset(@page * @params[:per_page]) if @page.positive? if updated_results = blk&.call(results) results = updated_results diff --git a/lib/bookmark_reminder_notification_handler.rb b/lib/bookmark_reminder_notification_handler.rb index 13f5b2966e..247cc4637d 100644 --- a/lib/bookmark_reminder_notification_handler.rb +++ b/lib/bookmark_reminder_notification_handler.rb @@ -28,12 +28,10 @@ class BookmarkReminderNotificationHandler def clear_reminder Rails.logger.debug( - "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}" + "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}", ) - if bookmark.auto_clear_reminder_when_reminder_sent? - bookmark.reminder_at = nil - end + bookmark.reminder_at = nil if bookmark.auto_clear_reminder_when_reminder_sent? bookmark.clear_reminder! end diff --git a/lib/browser_detection.rb b/lib/browser_detection.rb index e2f8d46a88..7cfbaffe3b 100644 --- a/lib/browser_detection.rb +++ b/lib/browser_detection.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module BrowserDetection - def self.browser(user_agent) case user_agent when /Edg/i @@ -66,5 +65,4 @@ module BrowserDetection :unknown end end - end diff --git a/lib/cache.rb b/lib/cache.rb index ca814c9e2b..296519b17e 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -15,12 +15,11 @@ # this makes it harder to reason about the API class Cache - # nothing is cached for longer than 1 day EVER # there is no reason to have data older than this clogging redis # it is dangerous cause if we rename keys we will be stuck with # pointless data - MAX_CACHE_AGE = 1.day unless defined? MAX_CACHE_AGE + MAX_CACHE_AGE = 1.day unless defined?(MAX_CACHE_AGE) attr_reader :namespace @@ -47,9 +46,7 @@ class Cache end def clear - keys.each do |k| - redis.del(k) - end + keys.each { |k| redis.del(k) } end def normalize_key(key) @@ -80,9 +77,7 @@ class Cache key = normalize_key(name) raw = nil - if !force - raw = redis.get(key) - end + raw = redis.get(key) if !force if raw begin @@ -96,7 +91,8 @@ class Cache val end elsif force - raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block." + raise ArgumentError, + "Missing block: Calling `Cache#fetch` with `force: true` requires a block." else read(name) end @@ -105,7 +101,7 @@ class Cache protected def log_first_exception(e) - if !defined? @logged_a_warning + if !defined?(@logged_a_warning) @logged_a_warning = true Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}") end @@ -129,5 +125,4 @@ class Cache redis.setex(key, expiry, dumped) true end - end diff --git a/lib/canonical_url.rb b/lib/canonical_url.rb index df66c37a6c..bc9b6e2d10 100644 --- a/lib/canonical_url.rb +++ b/lib/canonical_url.rb @@ -2,7 +2,7 @@ module CanonicalURL module ControllerExtensions - ALLOWED_CANONICAL_PARAMS = %w(page) + ALLOWED_CANONICAL_PARAMS = %w[page] def canonical_url(url_for_options = {}) case url_for_options @@ -14,14 +14,15 @@ module CanonicalURL end def default_canonical - @default_canonical ||= begin - canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" - allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } - if allowed_params.present? - canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + @default_canonical ||= + begin + canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" + allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } + if allowed_params.present? + canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + end + canonical end - canonical - end end def self.included(base) @@ -31,7 +32,7 @@ module CanonicalURL module Helpers def canonical_link_tag(url = nil) - tag('link', rel: 'canonical', href: url || @canonical_url || default_canonical) + tag("link", rel: "canonical", href: url || @canonical_url || default_canonical) end end end diff --git a/lib/category_badge.rb b/lib/category_badge.rb index fdfd8035c5..9cf422e588 100644 --- a/lib/category_badge.rb +++ b/lib/category_badge.rb @@ -1,23 +1,26 @@ # frozen_string_literal: true module CategoryBadge - def self.category_stripe(color, classes) - style = color ? "style='background-color: ##{color};'" : '' + style = color ? "style='background-color: ##{color};'" : "" "" end - def self.inline_category_stripe(color, styles = '', insert_blank = false) - "#{insert_blank ? ' ' : ''}" + def self.inline_category_stripe(color, styles = "", insert_blank = false) + "#{insert_blank ? " " : ""}" end def self.inline_badge_wrapper_style(category) style = case (SiteSetting.category_style || :box).to_sym - when :bar then 'line-height: 1.25; margin-right: 5px;' - when :box then "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" - when :bullet then 'line-height: 1; margin-right: 10px;' - when :none then '' + when :bar + "line-height: 1.25; margin-right: 5px;" + when :box + "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" + when :bullet + "line-height: 1; margin-right: 10px;" + when :none + "" end " style='font-size: 0.857em; white-space: nowrap; display: inline-block; position: relative; #{style}'" @@ -34,73 +37,88 @@ module CategoryBadge extra_classes = "#{opts[:extra_classes]} #{SiteSetting.category_style}" - result = +'' + result = +"" # parent span unless category.parent_category_id.nil? || opts[:hide_parent] parent_category = Category.find_by(id: category.parent_category_id) - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 1px;', true) - when :box - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 0 1px;', true) - when :bullet - inline_category_stripe(parent_category.color, 'display: inline-block; width: 5px; height: 10px; line-height: 1;') - when :none - '' - end - else - category_stripe(parent_category.color, 'badge-category-parent-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 1px;", + true, + ) + when :box + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 0 1px;", + true, + ) + when :bullet + inline_category_stripe( + parent_category.color, + "display: inline-block; width: 5px; height: 10px; line-height: 1;", + ) + when :none + "" end + else + category_stripe(parent_category.color, "badge-category-parent-bg") + end end # sub parent or main category span - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(category.color, 'display: inline-block; padding: 1px;', true) - when :box - '' - when :bullet - inline_category_stripe(category.color, "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;") - when :none - '' - end - else - category_stripe(category.color, 'badge-category-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe(category.color, "display: inline-block; padding: 1px;", true) + when :box + "" + when :bullet + inline_category_stripe( + category.color, + "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;", + ) + when :none + "" end + else + category_stripe(category.color, "badge-category-bg") + end # category name - class_names = 'badge-category clear-badge' - description = category.description_text ? "title='#{category.description_text}'" : '' - category_url = opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url + class_names = "badge-category clear-badge" + description = category.description_text ? "title='#{category.description_text}'" : "" + category_url = + opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url extra_span_classes = if opts[:inline_style] case (SiteSetting.category_style || :box).to_sym when :bar - 'color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;' + "color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;" when :box "color: ##{category.text_color}; padding: 0 5px;" when :bullet - 'color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;' + "color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;" when :none - '' - end + 'max-width: 150px; overflow: hidden; text-overflow: ellipsis;' + "" + end + "max-width: 150px; overflow: hidden; text-overflow: ellipsis;" elsif (SiteSetting.category_style).to_sym == :box "color: ##{category.text_color}" else - '' + "" end result << "" - result << ERB::Util.html_escape(category.name) << '' + result << ERB::Util.html_escape(category.name) << "" - result = "#{result}" + result = + "#{result}" result.html_safe end diff --git a/lib/chrome_installed_checker.rb b/lib/chrome_installed_checker.rb index 0576eb7401..2f8c2824cc 100644 --- a/lib/chrome_installed_checker.rb +++ b/lib/chrome_installed_checker.rb @@ -3,13 +3,17 @@ require "rbconfig" class ChromeInstalledChecker - class ChromeError < StandardError; end - class ChromeVersionError < ChromeError; end - class ChromeNotInstalled < ChromeError; end - class ChromeVersionTooLow < ChromeError; end + class ChromeError < StandardError + end + class ChromeVersionError < ChromeError + end + class ChromeNotInstalled < ChromeError + end + class ChromeVersionTooLow < ChromeError + end def self.run - if RbConfig::CONFIG['host_os'][/darwin|mac os/] + if RbConfig::CONFIG["host_os"][/darwin|mac os/] binary = "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" elsif system("command -v google-chrome-stable >/dev/null;") binary = "google-chrome-stable" @@ -18,15 +22,15 @@ class ChromeInstalledChecker binary ||= "chromium" if system("command -v chromium >/dev/null;") if !binary - raise ChromeNotInstalled.new("Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html") + raise ChromeNotInstalled.new( + "Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html", + ) end version = `\"#{binary}\" --version` version_match = version.match(/[\d\.]+/) - if !version_match - raise ChromeError.new("Can't get the #{binary} version") - end + raise ChromeError.new("Can't get the #{binary} version") if !version_match if Gem::Version.new(version_match[0]) < Gem::Version.new("59") raise ChromeVersionTooLow.new("Chrome 59 or higher is required") diff --git a/lib/comment_migration.rb b/lib/comment_migration.rb index bcfcdfd10e..94062378c2 100644 --- a/lib/comment_migration.rb +++ b/lib/comment_migration.rb @@ -28,24 +28,27 @@ class CommentMigration < ActiveRecord::Migration[4.2] end def down - replace_nils(comments_up).deep_merge(comments_down).each do |table| - table[1].each do |column| - table_name = table[0] - column_name = column[0] - comment = column[1] + replace_nils(comments_up) + .deep_merge(comments_down) + .each do |table| + table[1].each do |column| + table_name = table[0] + column_name = column[0] + comment = column[1] - if column_name == :_table - DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment - puts " COMMENT ON TABLE #{table_name}" - else - DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment - puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + if column_name == :_table + DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment + puts " COMMENT ON TABLE #{table_name}" + else + DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment + puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + end end end - end end private + def replace_nils(hash) hash.each do |key, value| if Hash === value diff --git a/lib/common_passwords.rb b/lib/common_passwords.rb index 901a007b01..5b4aec28e8 100644 --- a/lib/common_passwords.rb +++ b/lib/common_passwords.rb @@ -12,9 +12,8 @@ # Discourse.redis.without_namespace.del CommonPasswords::LIST_KEY class CommonPasswords - - PASSWORD_FILE = File.join(Rails.root, 'lib', 'common_passwords', '10-char-common-passwords.txt') - LIST_KEY = 'discourse-common-passwords' + PASSWORD_FILE = File.join(Rails.root, "lib", "common_passwords", "10-char-common-passwords.txt") + LIST_KEY = "discourse-common-passwords" @mutex = Mutex.new @@ -32,9 +31,7 @@ class CommonPasswords end def self.password_list - @mutex.synchronize do - load_passwords unless redis.scard(LIST_KEY) > 0 - end + @mutex.synchronize { load_passwords unless redis.scard(LIST_KEY) > 0 } RedisPasswordList.new end @@ -49,5 +46,4 @@ class CommonPasswords # tolerate this so we don't block signups Rails.logger.error "Common passwords file #{PASSWORD_FILE} is not found! Common password checking is skipped." end - end diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 510048d1db..878727f20c 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ComposerMessagesFinder - def initialize(user, details) @user = user @details = details @@ -29,26 +28,30 @@ class ComposerMessagesFinder if creating_topic? count = @user.created_topic_count - education_key = 'education.new-topic' + education_key = "education.new-topic" else count = @user.post_count - education_key = 'education.new-reply' + education_key = "education.new-reply" end if count < SiteSetting.educate_until_posts - return { - id: 'education', - templateName: 'education', - wait_for_typing: true, - body: PrettyText.cook( - I18n.t( - education_key, - education_posts_text: I18n.t('education.until_posts', count: SiteSetting.educate_until_posts), - site_name: SiteSetting.title, - base_path: Discourse.base_path - ) - ) - } + return( + { + id: "education", + templateName: "education", + wait_for_typing: true, + body: + PrettyText.cook( + I18n.t( + education_key, + education_posts_text: + I18n.t("education.until_posts", count: SiteSetting.educate_until_posts), + site_name: SiteSetting.title, + base_path: Discourse.base_path, + ), + ), + } + ) end nil @@ -59,35 +62,55 @@ class ComposerMessagesFinder return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id]) { - id: 'too_many_replies', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) + id: "too_many_replies", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.too_many_replies", + newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic, + ), + ), } end # Should a user be contacted to update their avatar? def check_avatar_notification - # A user has to be basic at least to be considered for an avatar notification return unless @user.has_trust_level?(TrustLevel[1]) # We don't notify users who have avatars or who have been notified already. - return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + return + end # Do not notify user if any of the following is true: # - "disable avatar education message" is enabled # - "sso overrides avatar" is enabled # - "allow uploaded avatars" is disabled - return if SiteSetting.disable_avatar_education_message || SiteSetting.discourse_connect_overrides_avatar || !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + if SiteSetting.disable_avatar_education_message || + SiteSetting.discourse_connect_overrides_avatar || + !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + return + end # If we got this far, log that we've nagged them about the avatar - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_avatar], + target_user_id: @user.id, + ) # Return the message { - id: 'avatar', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture")) + id: "avatar", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.avatar", + profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture", + ), + ), } end @@ -96,39 +119,45 @@ class ComposerMessagesFinder return unless educate_reply?(:notified_about_sequential_replies) # Count the posts made by this user in the last day - recent_posts_user_ids = Post.where(topic_id: @details[:topic_id]) - .where("created_at > ?", 1.day.ago) - .where(post_type: Post.types[:regular]) - .order('created_at desc') - .limit(SiteSetting.sequential_replies_threshold) - .pluck(:user_id) + recent_posts_user_ids = + Post + .where(topic_id: @details[:topic_id]) + .where("created_at > ?", 1.day.ago) + .where(post_type: Post.types[:regular]) + .order("created_at desc") + .limit(SiteSetting.sequential_replies_threshold) + .pluck(:user_id) # Did we get back as many posts as we asked for, and are they all by the current user? - return if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || - recent_posts_user_ids.detect { |u| u != @user.id } + if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || + recent_posts_user_ids.detect { |u| u != @user.id } + return + end # If we got this far, log that we've nagged them about the sequential replies - UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_sequential_replies], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'sequential_replies', - templateName: 'education', + id: "sequential_replies", + templateName: "education", wait_for_typing: true, - extraClass: 'education-message', + extraClass: "education-message", hide_if_whisper: true, - body: PrettyText.cook(I18n.t('education.sequential_replies')) + body: PrettyText.cook(I18n.t("education.sequential_replies")), } end def check_dominating_topic return unless educate_reply?(:notified_about_dominating_topic) - return if @topic.blank? || - @topic.user_id == @user.id || - @topic.posts_count < SiteSetting.summary_posts_required || - @topic.private_message? + if @topic.blank? || @topic.user_id == @user.id || + @topic.posts_count < SiteSetting.summary_posts_required || @topic.private_message? + return + end posts_by_user = @user.posts.where(topic_id: @topic.id).count @@ -136,16 +165,18 @@ class ComposerMessagesFinder return if ratio < (SiteSetting.dominating_topic_minimum_percent.to_f / 100.0) # Log the topic notification - UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_dominating_topic], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'dominating_topic', - templateName: 'dominating-topic', + id: "dominating_topic", + templateName: "dominating-topic", wait_for_typing: true, - extraClass: 'education-message dominating-topic-message', - body: PrettyText.cook(I18n.t('education.dominating_topic')) + extraClass: "education-message dominating-topic-message", + body: PrettyText.cook(I18n.t("education.dominating_topic")), } end @@ -157,73 +188,85 @@ class ComposerMessagesFinder reply_to_user_id = Post.where(id: @details[:post_id]).pluck(:user_id)[0] # Users's last x posts in the topic - last_x_replies = @topic. - posts. - where(user_id: @user.id). - order('created_at desc'). - limit(SiteSetting.get_a_room_threshold). - pluck(:reply_to_user_id). - find_all { |uid| uid != @user.id && uid == reply_to_user_id } + last_x_replies = + @topic + .posts + .where(user_id: @user.id) + .order("created_at desc") + .limit(SiteSetting.get_a_room_threshold) + .pluck(:reply_to_user_id) + .find_all { |uid| uid != @user.id && uid == reply_to_user_id } return unless last_x_replies.size == SiteSetting.get_a_room_threshold - return unless @topic.posts.count('distinct user_id') >= min_users_posted + return unless @topic.posts.count("distinct user_id") >= min_users_posted - UserHistory.create!(action: UserHistory.actions[:notified_about_get_a_room], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_get_a_room], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) reply_username = User.where(id: last_x_replies[0]).pluck_first(:username) { - id: 'get_a_room', - templateName: 'get-a-room', + id: "get_a_room", + templateName: "get-a-room", wait_for_typing: true, reply_username: reply_username, - extraClass: 'education-message get-a-room', - body: PrettyText.cook( - I18n.t( - 'education.get_a_room', - count: SiteSetting.get_a_room_threshold, - reply_username: reply_username, - base_path: Discourse.base_path - ) - ) + extraClass: "education-message get-a-room", + body: + PrettyText.cook( + I18n.t( + "education.get_a_room", + count: SiteSetting.get_a_room_threshold, + reply_username: reply_username, + base_path: Discourse.base_path, + ), + ), } end def check_reviving_old_topic return unless replying? - return if @topic.nil? || - SiteSetting.warn_reviving_old_topic_age < 1 || - @topic.last_posted_at.nil? || - @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + if @topic.nil? || SiteSetting.warn_reviving_old_topic_age < 1 || @topic.last_posted_at.nil? || + @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + return + end { - id: 'reviving_old', - templateName: 'education', + id: "reviving_old", + templateName: "education", wait_for_typing: false, - extraClass: 'education-message', - body: PrettyText.cook( - I18n.t( - 'education.reviving_old_topic', - time_ago: FreedomPatches::Rails4.time_ago_in_words(@topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose') - ) - ) + extraClass: "education-message", + body: + PrettyText.cook( + I18n.t( + "education.reviving_old_topic", + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + @topic.last_posted_at, + false, + scope: :"datetime.distance_in_words_verbose", + ), + ), + ), } end def self.user_not_seen_in_a_while(usernames) - User.where(username_lower: usernames).where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago).pluck(:username).sort + User + .where(username_lower: usernames) + .where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago) + .pluck(:username) + .sort end private def educate_reply?(type) - replying? && - @details[:topic_id] && - (@topic.present? && !@topic.private_message?) && - (@user.post_count >= SiteSetting.educate_until_posts) && - !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) + replying? && @details[:topic_id] && (@topic.present? && !@topic.private_message?) && + (@user.post_count >= SiteSetting.educate_until_posts) && + !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) end def creating_topic? @@ -237,5 +280,4 @@ class ComposerMessagesFinder def editing_post? @details[:composer_action] == "edit" end - end diff --git a/lib/compression/engine.rb b/lib/compression/engine.rb index c098c0b668..732164639d 100644 --- a/lib/compression/engine.rb +++ b/lib/compression/engine.rb @@ -9,12 +9,13 @@ module Compression Compression::Zip.new, Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]), Compression::Gzip.new, - Compression::Tar.new + Compression::Tar.new, ] end def self.engine_for(filename, strategies: default_strategies) - strategy = strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } + strategy = + strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } new(strategy) end diff --git a/lib/compression/gzip.rb b/lib/compression/gzip.rb index 4d0b7c51e1..c668b088f7 100644 --- a/lib/compression/gzip.rb +++ b/lib/compression/gzip.rb @@ -3,12 +3,17 @@ module Compression class Gzip < Strategy def extension - '.gz' + ".gz" end def compress(path, target_name) gzip_target = sanitize_path("#{path}/#{target_name}") - Discourse::Utils.execute_command('gzip', '-5', gzip_target, failure_message: "Failed to gzip file.") + Discourse::Utils.execute_command( + "gzip", + "-5", + gzip_target, + failure_message: "Failed to gzip file.", + ) "#{gzip_target}.gz" end @@ -23,7 +28,8 @@ module Compression true end - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) gzip = Zlib::GzipReader.open(compressed_file_path) @@ -32,7 +38,7 @@ module Compression def build_entry_path(dest_path, _, compressed_file_path) basename = File.basename(compressed_file_path) - basename.gsub!(/#{Regexp.escape(extension)}$/, '') + basename.gsub!(/#{Regexp.escape(extension)}$/, "") File.join(dest_path, basename) end @@ -44,12 +50,11 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end # Change this later. - ::File.open(entry_path, 'wb') do |os| - buf = ''.dup + ::File.open(entry_path, "wb") do |os| + buf = "".dup while (buf = entry.read(chunk_size)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/pipeline.rb b/lib/compression/pipeline.rb index 1ea9f17ddb..39a03f9cda 100644 --- a/lib/compression/pipeline.rb +++ b/lib/compression/pipeline.rb @@ -7,25 +7,27 @@ module Compression end def extension - @strategies.reduce('') { |ext, strategy| ext += strategy.extension } + @strategies.reduce("") { |ext, strategy| ext += strategy.extension } end def compress(path, target_name) current_target = target_name - @strategies.reduce('') do |compressed_path, strategy| + @strategies.reduce("") do |compressed_path, strategy| compressed_path = strategy.compress(path, current_target) - current_target = compressed_path.split('/').last + current_target = compressed_path.split("/").last compressed_path end end def decompress(dest_path, compressed_file_path, max_size) - @strategies.reverse.reduce(compressed_file_path) do |to_decompress, strategy| - next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) - FileUtils.rm_rf(to_decompress) - next_compressed_file - end + @strategies + .reverse + .reduce(compressed_file_path) do |to_decompress, strategy| + next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) + FileUtils.rm_rf(to_decompress) + next_compressed_file + end end end end diff --git a/lib/compression/strategy.rb b/lib/compression/strategy.rb index 8bb54f87d4..d4a05a578c 100644 --- a/lib/compression/strategy.rb +++ b/lib/compression/strategy.rb @@ -18,9 +18,7 @@ module Compression entries_of(compressed_file).each do |entry| entry_path = build_entry_path(sanitized_dest_path, entry, sanitized_compressed_file_path) - if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) - next - end + next if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) FileUtils.mkdir_p(File.dirname(entry_path)) if is_file?(entry) @@ -45,10 +43,10 @@ module Compression filename.strip.tap do |name| # NOTE: File.basename doesn't work right with Windows paths on Unix # get only the filename, not the whole path - name.sub! /\A.*(\\|\/)/, '' + name.sub! %r{\A.*(\\|/)}, "" # Finally, replace all non alphanumeric, underscore # or periods with underscore - name.gsub! /[^\w\.\-]/, '_' + name.gsub! /[^\w\.\-]/, "_" end end @@ -75,7 +73,7 @@ module Compression raise DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| while (buf = entry.read(chunk_size)) remaining_size -= buf.size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/tar.rb b/lib/compression/tar.rb index 580a8ebdd5..3346d1e764 100644 --- a/lib/compression/tar.rb +++ b/lib/compression/tar.rb @@ -1,23 +1,31 @@ # frozen_string_literal: true -require 'rubygems/package' +require "rubygems/package" module Compression class Tar < Strategy def extension - '.tar' + ".tar" end def compress(path, target_name) tar_filename = sanitize_filename("#{target_name}.tar") - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, target_name, failure_message: "Failed to tar file.") + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + target_name, + failure_message: "Failed to tar file.", + ) sanitize_path("#{path}/#{tar_filename}") end private - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) file_stream = IO.new(IO.sysopen(compressed_file_path)) diff --git a/lib/compression/zip.rb b/lib/compression/zip.rb index 63c6b92729..dbf132bf88 100644 --- a/lib/compression/zip.rb +++ b/lib/compression/zip.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'zip' +require "zip" module Compression class Zip < Strategy def extension - '.zip' + ".zip" end def compress(path, target_name) @@ -15,7 +15,7 @@ module Compression ::Zip::File.open(zip_filename, ::Zip::File::CREATE) do |zipfile| if File.directory?(absolute_path) entries = Dir.entries(absolute_path) - %w[. ..] - write_entries(entries, absolute_path, '', zipfile) + write_entries(entries, absolute_path, "", zipfile) else put_into_archive(absolute_path, zipfile, target_name) end @@ -47,15 +47,14 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| entry.get_input_stream do |is| entry.set_extra_attributes_on_path(entry_path) - buf = ''.dup + buf = "".dup while (buf = is.sysread(chunk_size, buf)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? @@ -70,7 +69,7 @@ module Compression # A helper method to make the recursion work. def write_entries(entries, base_path, path, zipfile) entries.each do |e| - zipfile_path = path == '' ? e : File.join(path, e) + zipfile_path = path == "" ? e : File.join(path, e) disk_file_path = File.join(base_path, zipfile_path) if File.directory? disk_file_path diff --git a/lib/configurable_urls.rb b/lib/configurable_urls.rb index f20321ff6d..c196b74b12 100644 --- a/lib/configurable_urls.rb +++ b/lib/configurable_urls.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ConfigurableUrls - def faq_path SiteSetting.faq_url.blank? ? "#{Discourse.base_path}/faq" : SiteSetting.faq_url end @@ -11,7 +10,10 @@ module ConfigurableUrls end def privacy_path - SiteSetting.privacy_policy_url.blank? ? "#{Discourse.base_path}/privacy" : SiteSetting.privacy_policy_url + if SiteSetting.privacy_policy_url.blank? + "#{Discourse.base_path}/privacy" + else + SiteSetting.privacy_policy_url + end end - end diff --git a/lib/content_buffer.rb b/lib/content_buffer.rb index 997b57d795..6d6895811f 100644 --- a/lib/content_buffer.rb +++ b/lib/content_buffer.rb @@ -3,7 +3,6 @@ # this class is used to track changes to an arbitrary buffer class ContentBuffer - def initialize(initial_content) @initial_content = initial_content @lines = @initial_content.split("\n") @@ -17,7 +16,6 @@ class ContentBuffer text = transform[:text] if transform[:operation] == :delete - # fix first line l = @lines[start_row] @@ -32,16 +30,13 @@ class ContentBuffer @lines[start_row] = l # remove middle lines - (finish_row - start_row).times do - l = @lines.delete_at start_row + 1 - end + (finish_row - start_row).times { l = @lines.delete_at start_row + 1 } # fix last line @lines[start_row] << @lines[finish_row][finish_col - 1..-1] end if transform[:operation] == :insert - @lines[start_row].insert(start_col, text) split = @lines[start_row].split("\n") @@ -56,7 +51,6 @@ class ContentBuffer @lines.insert(i, "") unless @lines.length > i @lines[i] = split[-1] + @lines[i] end - end end diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb index 0cfd309a4b..107dc0437d 100644 --- a/lib/content_security_policy.rb +++ b/lib/content_security_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'content_security_policy/builder' -require 'content_security_policy/extension' +require "content_security_policy/builder" +require "content_security_policy/extension" class ContentSecurityPolicy class << self diff --git a/lib/content_security_policy/builder.rb b/lib/content_security_policy/builder.rb index 4f5dbfb913..e23f55111e 100644 --- a/lib/content_security_policy/builder.rb +++ b/lib/content_security_policy/builder.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy/default' +require "content_security_policy/default" class ContentSecurityPolicy class Builder @@ -33,7 +33,9 @@ class ContentSecurityPolicy def <<(extension) return unless valid_extension?(extension) - extension.each { |directive, sources| extend_directive(normalize_directive(directive), sources) } + extension.each do |directive, sources| + extend_directive(normalize_directive(directive), sources) + end end def build @@ -53,7 +55,7 @@ class ContentSecurityPolicy private def normalize_directive(directive) - directive.to_s.gsub('-', '_').to_sym + directive.to_s.gsub("-", "_").to_sym end def normalize_source(source) diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index ef67672b05..6b147079c7 100644 --- a/lib/content_security_policy/default.rb +++ b/lib/content_security_policy/default.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Default @@ -7,16 +7,19 @@ class ContentSecurityPolicy def initialize(base_url:) @base_url = base_url - @directives = {}.tap do |directives| - directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https - directives[:base_uri] = [:self] - directives[:object_src] = [:none] - directives[:script_src] = script_src - directives[:worker_src] = worker_src - directives[:report_uri] = report_uri if SiteSetting.content_security_policy_collect_reports - directives[:frame_ancestors] = frame_ancestors if restrict_embed? - directives[:manifest_src] = ["'self'"] - end + @directives = + {}.tap do |directives| + directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https + directives[:base_uri] = [:self] + directives[:object_src] = [:none] + directives[:script_src] = script_src + directives[:worker_src] = worker_src + directives[ + :report_uri + ] = report_uri if SiteSetting.content_security_policy_collect_reports + directives[:frame_ancestors] = frame_ancestors if restrict_embed? + directives[:manifest_src] = ["'self'"] + end end private @@ -27,27 +30,34 @@ class ContentSecurityPolicy SCRIPT_ASSET_DIRECTORIES = [ # [dir, can_use_s3_cdn, can_use_cdn, for_worker] - ['/assets/', true, true, true], - ['/brotli_asset/', true, true, true], - ['/extra-locales/', false, false, false], - ['/highlight-js/', false, true, false], - ['/javascripts/', false, true, true], - ['/plugins/', false, true, true], - ['/theme-javascripts/', false, true, false], - ['/svg-sprite/', false, true, false], + ["/assets/", true, true, true], + ["/brotli_asset/", true, true, true], + ["/extra-locales/", false, false, false], + ["/highlight-js/", false, true, false], + ["/javascripts/", false, true, true], + ["/plugins/", false, true, true], + ["/theme-javascripts/", false, true, false], + ["/svg-sprite/", false, true, false], ] - def script_assets(base = base_url, s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, cdn = GlobalSetting.cdn_url, worker: false) - SCRIPT_ASSET_DIRECTORIES.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| - next if worker && !for_worker - if can_use_s3_cdn && s3_cdn - s3_cdn + dir - elsif can_use_cdn && cdn - cdn + Discourse.base_path + dir - else - base + dir + def script_assets( + base = base_url, + s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, + cdn = GlobalSetting.cdn_url, + worker: false + ) + SCRIPT_ASSET_DIRECTORIES + .map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| + next if worker && !for_worker + if can_use_s3_cdn && s3_cdn + s3_cdn + dir + elsif can_use_cdn && cdn + cdn + Discourse.base_path + dir + else + base + dir + end end - end.compact + .compact end def script_src @@ -55,7 +65,7 @@ class ContentSecurityPolicy "#{base_url}/logs/", "#{base_url}/sidekiq/", "#{base_url}/mini-profiler-resources/", - *script_assets + *script_assets, ].tap do |sources| sources << :report_sample if SiteSetting.content_security_policy_collect_reports sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev @@ -67,23 +77,25 @@ class ContentSecurityPolicy end # we need analytics.js still as gtag/js is a script wrapper for it - sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present? - sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + if SiteSetting.ga_universal_tracking_code.present? + sources << "https://www.google-analytics.com/analytics.js" + end + if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + sources << "https://www.googletagmanager.com/gtag/js" + end if SiteSetting.gtm_container_id.present? - sources << 'https://www.googletagmanager.com/gtm.js' + sources << "https://www.googletagmanager.com/gtm.js" sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'" end - if SiteSetting.splash_screen - sources << "'#{SplashScreenHelper.fingerprint}'" - end + sources << "'#{SplashScreenHelper.fingerprint}'" if SiteSetting.splash_screen end end def worker_src [ "'self'", # For service worker - *script_assets(worker: true) + *script_assets(worker: true), ] end @@ -92,15 +104,11 @@ class ContentSecurityPolicy end def frame_ancestors - [ - "'self'", - *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" } - ] + ["'self'", *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }] end def restrict_embed? - SiteSetting.content_security_policy_frame_ancestors && - !SiteSetting.embed_any_origin + SiteSetting.content_security_policy_frame_ancestors && !SiteSetting.embed_any_origin end end end diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb index 51b59acda3..150e004862 100644 --- a/lib/content_security_policy/extension.rb +++ b/lib/content_security_policy/extension.rb @@ -4,12 +4,12 @@ class ContentSecurityPolicy extend self def site_setting_extension - { script_src: SiteSetting.content_security_policy_script_src.split('|') } + { script_src: SiteSetting.content_security_policy_script_src.split("|") } end def path_specific_extension(path_info) {}.tap do |obj| - for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info) + for_qunit_route = !Rails.env.production? && %w[/qunit /wizard/qunit].include?(path_info) for_qunit_route ||= "/theme-qunit" == path_info obj[:script_src] = :unsafe_eval if for_qunit_route end @@ -23,7 +23,7 @@ class ContentSecurityPolicy end end - THEME_SETTING = 'extend_content_security_policy' + THEME_SETTING = "extend_content_security_policy" def theme_extensions(theme_id) key = "theme_extensions_#{theme_id}" @@ -37,47 +37,55 @@ class ContentSecurityPolicy private def cache - @cache ||= DistributedCache.new('csp_extensions') + @cache ||= DistributedCache.new("csp_extensions") end def find_theme_extensions(theme_id) extensions = [] theme_ids = Theme.transform_ids(theme_id) - Theme.where(id: theme_ids).find_each do |theme| - theme.cached_settings.each do |setting, value| - extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + Theme + .where(id: theme_ids) + .find_each do |theme| + theme.cached_settings.each do |setting, value| + extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + end end - end - extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions) - - html_fields = ThemeField.where( - theme_id: theme_ids, - target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, - name: ThemeField.html_fields + extensions << build_theme_extension( + ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions, ) + html_fields = + ThemeField.where( + theme_id: theme_ids, + target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, + name: ThemeField.html_fields, + ) + auto_script_src_extension = { script_src: [] } html_fields.each(&:ensure_baked!) doc = html_fields.map(&:value_baked).join("\n") - Nokogiri::HTML5.fragment(doc).css('script[src]').each do |node| - src = node['src'] - uri = URI(src) + Nokogiri::HTML5 + .fragment(doc) + .css("script[src]") + .each do |node| + src = node["src"] + uri = URI(src) - next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) - next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) - next if uri.path.nil? # Ignore raw hosts + next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) + next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) + next if uri.path.nil? # Ignore raw hosts - uri.query = nil # CSP should not include query part of url + uri.query = nil # CSP should not include query part of url - uri_string = uri.to_s.sub(/^\/\//, '') # Protocol-less CSP should not have // at beginning of URL + uri_string = uri.to_s.sub(%r{^//}, "") # Protocol-less CSP should not have // at beginning of URL - auto_script_src_extension[:script_src] << uri_string - rescue URI::Error - # Ignore invalid URI - end + auto_script_src_extension[:script_src] << uri_string + rescue URI::Error + # Ignore invalid URI + end extensions << auto_script_src_extension @@ -87,7 +95,7 @@ class ContentSecurityPolicy def build_theme_extension(entries) {}.tap do |extension| entries.each do |entry| - directive, source = entry.split(':', 2).map(&:strip) + directive, source = entry.split(":", 2).map(&:strip) extension[directive] ||= [] extension[directive] << source diff --git a/lib/content_security_policy/middleware.rb b/lib/content_security_policy/middleware.rb index 54ec0f5a65..79ec083427 100644 --- a/lib/content_security_policy/middleware.rb +++ b/lib/content_security_policy/middleware.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Middleware @@ -19,8 +19,16 @@ class ContentSecurityPolicy theme_id = env[:resolved_theme_id] - headers['Content-Security-Policy'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy - headers['Content-Security-Policy-Report-Only'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only + headers["Content-Security-Policy"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy + headers["Content-Security-Policy-Report-Only"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy_report_only response end @@ -30,7 +38,7 @@ class ContentSecurityPolicy delegate :policy, to: :ContentSecurityPolicy def html_response?(headers) - headers['Content-Type'] && headers['Content-Type'] =~ /html/ + headers["Content-Type"] && headers["Content-Type"] =~ /html/ end end end diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 4d8df118e6..01463bb776 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -7,7 +7,7 @@ class CookedPostProcessor include CookedProcessorMixin LIGHTBOX_WRAPPER_CSS_CLASS = "lightbox-wrapper" - GIF_SOURCES_REGEXP = /(giphy|tenor)\.com\// + GIF_SOURCES_REGEXP = %r{(giphy|tenor)\.com/} attr_reader :cooking_options, :doc @@ -61,25 +61,27 @@ class CookedPostProcessor return if @post.user.blank? || !Guardian.new.can_see?(@post) BadgeGranter.grant(Badge.find(Badge::FirstEmoji), @post.user, post_id: @post.id) if has_emoji? - BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) if @has_oneboxes - BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) if @post.is_reply_by_email? + if @has_oneboxes + BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) + end + if @post.is_reply_by_email? + BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) + end end def post_process_quotes - @doc.css("aside.quote").each do |q| - post_number = q['data-post'] - topic_id = q['data-topic'] - if topic_id && post_number - comparer = QuoteComparer.new( - topic_id.to_i, - post_number.to_i, - q.css('blockquote').text - ) + @doc + .css("aside.quote") + .each do |q| + post_number = q["data-post"] + topic_id = q["data-topic"] + if topic_id && post_number + comparer = QuoteComparer.new(topic_id.to_i, post_number.to_i, q.css("blockquote").text) - q['class'] = ((q['class'] || '') + " quote-post-not-found").strip if comparer.missing? - q['class'] = ((q['class'] || '') + " quote-modified").strip if comparer.modified? + q["class"] = ((q["class"] || "") + " quote-post-not-found").strip if comparer.missing? + q["class"] = ((q["class"] || "") + " quote-modified").strip if comparer.modified? + end end - end end def remove_full_quote_on_direct_reply @@ -87,66 +89,68 @@ class CookedPostProcessor return if @post.post_number == 1 return if @doc.xpath("aside[contains(@class, 'quote')]").size != 1 - previous = Post - .where("post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", @post.post_number, @post.topic_id, Post.types[:regular]) - .order("post_number DESC") - .limit(1) - .pluck(:cooked) - .first + previous = + Post + .where( + "post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", + @post.post_number, + @post.topic_id, + Post.types[:regular], + ) + .order("post_number DESC") + .limit(1) + .pluck(:cooked) + .first return if previous.blank? - previous_text = Nokogiri::HTML5::fragment(previous).text.strip + previous_text = Nokogiri::HTML5.fragment(previous).text.strip quoted_text = @doc.css("aside.quote:first-child blockquote").first&.text&.strip || "" return if previous_text.gsub(/(\s){2,}/, '\1') != quoted_text.gsub(/(\s){2,}/, '\1') - quote_regexp = /\A\s*\[quote.+\[\/quote\]/im + quote_regexp = %r{\A\s*\[quote.+\[/quote\]}im quoteless_raw = @post.raw.sub(quote_regexp, "").strip return if @post.raw.strip == quoteless_raw PostRevisor.new(@post).revise!( Discourse.system_user, - { - raw: quoteless_raw, - edit_reason: I18n.t(:removed_direct_reply_full_quotes) - }, + { raw: quoteless_raw, edit_reason: I18n.t(:removed_direct_reply_full_quotes) }, skip_validations: true, - bypass_bump: true + bypass_bump: true, ) end def extract_images # all images with a src attribute @doc.css("img[src], img[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]") - - # minus data images - @doc.css("img[src^='data']") - - # minus emojis - @doc.css("img.emoji") + # minus data images + @doc.css("img[src^='data']") - + # minus emojis + @doc.css("img.emoji") end def extract_images_for_post # all images with a src attribute @doc.css("img[src]") - - # minus emojis - @doc.css("img.emoji") - - # minus images inside quotes - @doc.css(".quote img") - - # minus onebox site icons - @doc.css("img.site-icon") - - # minus onebox avatars - @doc.css("img.onebox-avatar") - - @doc.css("img.onebox-avatar-inline") - - # minus github onebox profile images - @doc.css(".onebox.githubfolder img") + # minus emojis + @doc.css("img.emoji") - + # minus images inside quotes + @doc.css(".quote img") - + # minus onebox site icons + @doc.css("img.site-icon") - + # minus onebox avatars + @doc.css("img.onebox-avatar") - @doc.css("img.onebox-avatar-inline") - + # minus github onebox profile images + @doc.css(".onebox.githubfolder img") end def convert_to_link!(img) w, h = img["width"].to_i, img["height"].to_i - user_width, user_height = (w > 0 && h > 0 && [w, h]) || - get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) + user_width, user_height = + (w > 0 && h > 0 && [w, h]) || get_size_from_attributes(img) || + get_size_from_image_sizes(img["src"], @opts[:image_sizes]) limit_size!(img) @@ -155,7 +159,7 @@ class CookedPostProcessor upload = Upload.get_from_url(src) - original_width, original_height = nil + original_width, original_height = nil if (upload.present?) original_width = upload.width || 0 @@ -172,12 +176,17 @@ class CookedPostProcessor img.add_class("animated") end - return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height + if original_width <= SiteSetting.max_image_width && + original_height <= SiteSetting.max_image_height + return + end - user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0 + user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && + user_height.to_i <= 0 width, height = user_width, user_height - crop = SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop + crop = + SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop if crop width, height = ImageSizer.crop(width, height) @@ -200,7 +209,7 @@ class CookedPostProcessor return if upload.animated? - if img.ancestors('.onebox, .onebox-body, .quote').blank? && !img.classes.include?("onebox") + if img.ancestors(".onebox, .onebox-body, .quote").blank? && !img.classes.include?("onebox") add_lightbox!(img, original_width, original_height, upload, cropped: crop) end @@ -211,7 +220,7 @@ class CookedPostProcessor def each_responsive_ratio SiteSetting .responsive_post_image_sizes - .split('|') + .split("|") .map(&:to_f) .sort .each { |r| yield r if r > 1 } @@ -239,13 +248,16 @@ class CookedPostProcessor srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x" end - img["srcset"] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? + img[ + "srcset" + ] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? end else img["src"] = upload.url end - if !@disable_dominant_color && (color = upload.dominant_color(calculate_if_missing: true).presence) + if !@disable_dominant_color && + (color = upload.dominant_color(calculate_if_missing: true).presence) img["data-dominant-color"] = color end end @@ -261,9 +273,7 @@ class CookedPostProcessor a = create_link_node("lightbox", src) img.add_next_sibling(a) - if upload - a["data-download-href"] = Discourse.store.download_url(upload) - end + a["data-download-href"] = Discourse.store.download_url(upload) if upload a.add_child(img) @@ -309,48 +319,55 @@ class CookedPostProcessor @post.update_column(:image_upload_id, upload.id) # post if @post.is_first_post? # topic @post.topic.update_column(:image_upload_id, upload.id) - extra_sizes = ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes + extra_sizes = + ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes @post.topic.generate_thumbnails!(extra_sizes: extra_sizes) end else @post.update_column(:image_upload_id, nil) if @post.image_upload_id - @post.topic.update_column(:image_upload_id, nil) if @post.topic.image_upload_id && @post.is_first_post? + if @post.topic.image_upload_id && @post.is_first_post? + @post.topic.update_column(:image_upload_id, nil) + end nil end end def optimize_urls - %w{href data-download-href}.each do |selector| - @doc.css("a[#{selector}]").each do |a| - a[selector] = UrlHelper.cook_url(a[selector].to_s) - end + %w[href data-download-href].each do |selector| + @doc.css("a[#{selector}]").each { |a| a[selector] = UrlHelper.cook_url(a[selector].to_s) } end - %w{src}.each do |selector| - @doc.css("img[#{selector}]").each do |img| - custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) - img[selector] = UrlHelper.cook_url( - img[selector].to_s, secure: @post.with_secure_uploads? && !custom_emoji - ) - end + %w[src].each do |selector| + @doc + .css("img[#{selector}]") + .each do |img| + custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) + img[selector] = UrlHelper.cook_url( + img[selector].to_s, + secure: @post.with_secure_uploads? && !custom_emoji, + ) + end end end def remove_user_ids - @doc.css("a[href]").each do |a| - uri = begin - URI(a["href"]) - rescue URI::Error - next + @doc + .css("a[href]") + .each do |a| + uri = + begin + URI(a["href"]) + rescue URI::Error + next + end + next if uri.hostname != Discourse.current_hostname + + query = Rack::Utils.parse_nested_query(uri.query) + next if !query.delete("u") + + uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence + a["href"] = uri.to_s end - next if uri.hostname != Discourse.current_hostname - - query = Rack::Utils.parse_nested_query(uri.query) - next if !query.delete("u") - - uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence - a["href"] = uri.to_s - end end def enforce_nofollow @@ -369,13 +386,14 @@ class CookedPostProcessor def process_hotlinked_image(img) @hotlinked_map ||= @post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h - normalized_src = PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) + normalized_src = + PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) info = @hotlinked_map[normalized_src] still_an_image = true if info&.too_large? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_large_image_placeholder!(img) else img.remove @@ -383,7 +401,7 @@ class CookedPostProcessor still_an_image = false elsif info&.download_failed? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_broken_image_placeholder!(img) else img.remove @@ -399,28 +417,29 @@ class CookedPostProcessor end def add_blocked_hotlinked_media_placeholders - @doc.css([ - "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", - "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", - ].join(',')).each do |el| - src = el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || - el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(',')&.first&.split(' ')&.first + @doc + .css( + [ + "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", + "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", + ].join(","), + ) + .each do |el| + src = + el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || + el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(",")&.first&.split(" ")&.first - if el.name == "img" - add_blocked_hotlinked_image_placeholder!(el) - next + if el.name == "img" + add_blocked_hotlinked_image_placeholder!(el) + next + end + + el = el.parent if %w[video audio].include?(el.parent.name) + + el = el.parent if el.parent.classes.include?("video-container") + + add_blocked_hotlinked_media_placeholder!(el, src) end - - if ["video", "audio"].include?(el.parent.name) - el = el.parent - end - - if el.parent.classes.include?("video-container") - el = el.parent - end - - add_blocked_hotlinked_media_placeholder!(el, src) - end end def is_svg?(img) @@ -431,6 +450,6 @@ class CookedPostProcessor nil end - File.extname(path) == '.svg' if path + File.extname(path) == ".svg" if path end end diff --git a/lib/cooked_processor_mixin.rb b/lib/cooked_processor_mixin.rb index 68add8893f..4c2fba4c17 100644 --- a/lib/cooked_processor_mixin.rb +++ b/lib/cooked_processor_mixin.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CookedProcessorMixin - def post_process_oneboxes limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size oneboxes = {} @@ -14,7 +13,7 @@ module CookedProcessorMixin if skip_onebox if is_onebox - element.remove_class('onebox') + element.remove_class("onebox") else remove_inline_onebox_loading_class(element) end @@ -26,11 +25,13 @@ module CookedProcessorMixin map[url] = true if is_onebox - onebox = Oneboxer.onebox(url, - invalidate_oneboxes: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + onebox = + Oneboxer.onebox( + url, + invalidate_oneboxes: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) @has_oneboxes = true if onebox.present? onebox @@ -56,7 +57,7 @@ module CookedProcessorMixin # and wrap in a div limit_size!(img) - next if img["class"]&.include?('onebox-avatar') + next if img["class"]&.include?("onebox-avatar") parent = parent&.parent if parent&.name == "a" parent_class = parent && parent["class"] @@ -84,12 +85,18 @@ module CookedProcessorMixin if width < 64 && height < 64 img["class"] = img["class"].to_s + " onebox-full-image" else - img.delete('width') - img.delete('height') - new_parent = img.add_next_sibling("
    ") + img.delete("width") + img.delete("height") + new_parent = + img.add_next_sibling( + "
    ", + ) new_parent.first.add_child(img) end - elsif (parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || parent_class&.include?("scale-images")) && width > 0 && height > 0 + elsif ( + parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || + parent_class&.include?("scale-images") + ) && width > 0 && height > 0 img.remove_attribute("width") img.remove_attribute("height") parent["class"] = "aspect-image-full-size" @@ -98,16 +105,18 @@ module CookedProcessorMixin end if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content - @doc.css(".onebox-body a[rel], .onebox a[rel]").each do |a| - rel_values = a['rel'].split(' ').map(&:downcase) - rel_values.delete('nofollow') - rel_values.delete('ugc') - if rel_values.blank? - a.remove_attribute("rel") - else - a["rel"] = rel_values.join(' ') + @doc + .css(".onebox-body a[rel], .onebox a[rel]") + .each do |a| + rel_values = a["rel"].split(" ").map(&:downcase) + rel_values.delete("nofollow") + rel_values.delete("ugc") + if rel_values.blank? + a.remove_attribute("rel") + else + a["rel"] = rel_values.join(" ") + end end - end end end @@ -116,9 +125,9 @@ module CookedProcessorMixin # 1) the width/height attributes # 2) the dimension from the preview (image_sizes) # 3) the dimension of the original image (HTTP request) - w, h = get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || - get_size(img["src"]) + w, h = + get_size_from_attributes(img) || get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || + get_size(img["src"]) # limit the size of the thumbnail img["width"], img["height"] = ImageSizer.resize(w, h) @@ -126,7 +135,7 @@ module CookedProcessorMixin def get_size_from_attributes(img) w, h = img["width"].to_i, img["height"].to_i - return [w, h] unless w <= 0 || h <= 0 + return w, h unless w <= 0 || h <= 0 # if only width or height are specified attempt to scale image if w > 0 || h > 0 w = w.to_f @@ -149,9 +158,9 @@ module CookedProcessorMixin return unless image_sizes.present? image_sizes.each do |image_size| url, size = image_size[0], image_size[1] - if url && src && url.include?(src) && - size && size["width"].to_i > 0 && size["height"].to_i > 0 - return [size["width"], size["height"]] + if url && src && url.include?(src) && size && size["width"].to_i > 0 && + size["height"].to_i > 0 + return size["width"], size["height"] end end nil @@ -165,7 +174,7 @@ module CookedProcessorMixin return @size_cache[url] if @size_cache.has_key?(url) absolute_url = url - absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ /^\/[^\/]/ + absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ %r{^/[^/]} return unless absolute_url @@ -186,14 +195,13 @@ module CookedProcessorMixin else @size_cache[url] = FastImage.size(absolute_url) end - rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError # FastImage.size raises BufError for some gifs, leave it. end def is_valid_image_url?(url) uri = URI.parse(url) - %w(http https).include? uri.scheme + %w[http https].include? uri.scheme rescue URI::Error end @@ -217,9 +225,12 @@ module CookedProcessorMixin "help", I18n.t( "upload.placeholders.too_large_humanized", - max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_image_size_kb.kilobytes) - ) - ) + max_size: + ActiveSupport::NumberHelper.number_to_human_size( + SiteSetting.max_image_size_kb.kilobytes, + ), + ), + ), ) # Only if the image is already linked @@ -227,7 +238,7 @@ module CookedProcessorMixin parent = placeholder.parent parent.add_next_sibling(placeholder) - if parent.name == 'a' && parent["href"].present? + if parent.name == "a" && parent["href"].present? if url == parent["href"] parent.remove else @@ -295,12 +306,13 @@ module CookedProcessorMixin end def process_inline_onebox(element) - inline_onebox = InlineOneboxer.lookup( - element.attributes["href"].value, - invalidate: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + inline_onebox = + InlineOneboxer.lookup( + element.attributes["href"].value, + invalidate: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) if title = inline_onebox&.dig(:title) element.children = CGI.escapeHTML(title) diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index 0b90dc0acb..f926d3455d 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -4,7 +4,7 @@ module CrawlerDetection WAYBACK_MACHINE_URL = "archive.org" def self.to_matcher(string, type: nil) - escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|') + escaped = string.split("|").map { |agent| Regexp.escape(agent) }.join("|") if type == :real && Rails.env == "test" # we need this bypass so we properly render views @@ -15,18 +15,33 @@ module CrawlerDetection end def self.crawler?(user_agent, via_header = nil) - return true if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || via_header&.include?(WAYBACK_MACHINE_URL) + if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || + via_header&.include?(WAYBACK_MACHINE_URL) + return true + end # this is done to avoid regenerating regexes @non_crawler_matchers ||= {} @matchers ||= {} - possibly_real = (@non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher(SiteSetting.non_crawler_user_agents, type: :real)) + possibly_real = + ( + @non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher( + SiteSetting.non_crawler_user_agents, + type: :real, + ) + ) if user_agent.match?(possibly_real) - known_bots = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) + known_bots = + (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) if user_agent.match?(known_bots) - bypass = (@matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher(SiteSetting.crawler_check_bypass_agents)) + bypass = + ( + @matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher( + SiteSetting.crawler_check_bypass_agents, + ) + ) !user_agent.match?(bypass) else false @@ -34,30 +49,40 @@ module CrawlerDetection else true end - end def self.show_browser_update?(user_agent) return false if SiteSetting.browser_update_user_agents.blank? @browser_update_matchers ||= {} - matcher = @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher(SiteSetting.browser_update_user_agents) + matcher = + @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher( + SiteSetting.browser_update_user_agents, + ) user_agent.match?(matcher) end # Given a user_agent that returns true from crawler?, should its request be allowed? def self.allow_crawler?(user_agent) - return true if SiteSetting.allowed_crawler_user_agents.blank? && - SiteSetting.blocked_crawler_user_agents.blank? + if SiteSetting.allowed_crawler_user_agents.blank? && + SiteSetting.blocked_crawler_user_agents.blank? + return true + end @allowlisted_matchers ||= {} @blocklisted_matchers ||= {} if SiteSetting.allowed_crawler_user_agents.present? - allowlisted = @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher(SiteSetting.allowed_crawler_user_agents) + allowlisted = + @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher( + SiteSetting.allowed_crawler_user_agents, + ) !user_agent.nil? && user_agent.match?(allowlisted) else - blocklisted = @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher(SiteSetting.blocked_crawler_user_agents) + blocklisted = + @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher( + SiteSetting.blocked_crawler_user_agents, + ) user_agent.nil? || !user_agent.match?(blocklisted) end end diff --git a/lib/csrf_token_verifier.rb b/lib/csrf_token_verifier.rb index 56ed911a7f..8736b749df 100644 --- a/lib/csrf_token_verifier.rb +++ b/lib/csrf_token_verifier.rb @@ -2,7 +2,8 @@ # Provides a way to check a CSRF token outside of a controller class CSRFTokenVerifier - class InvalidCSRFToken < StandardError; end + class InvalidCSRFToken < StandardError + end include ActiveSupport::Configurable include ActionController::RequestForgeryProtection @@ -18,9 +19,7 @@ class CSRFTokenVerifier def call(env) @request = ActionDispatch::Request.new(env.dup) - unless verified_request? - raise InvalidCSRFToken - end + raise InvalidCSRFToken unless verified_request? end public :form_authenticity_token diff --git a/lib/current_user.rb b/lib/current_user.rb index cf84adfb27..fdf7843198 100644 --- a/lib/current_user.rb +++ b/lib/current_user.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CurrentUser - def self.has_auth_cookie?(env) Discourse.current_user_provider.new(env).has_auth_cookie? end @@ -45,5 +44,4 @@ module CurrentUser def current_user_provider @current_user_provider ||= Discourse.current_user_provider.new(request.env) end - end diff --git a/lib/db_helper.rb b/lib/db_helper.rb index 6bc79b044e..db9a9a0df8 100644 --- a/lib/db_helper.rb +++ b/lib/db_helper.rb @@ -3,7 +3,6 @@ require "migration/base_dropper" class DbHelper - REMAP_SQL ||= <<~SQL SELECT table_name::text, column_name::text, character_maximum_length FROM information_schema.columns @@ -19,24 +18,33 @@ class DbHelper WHERE trigger_name LIKE '%_readonly' SQL - TRUNCATABLE_COLUMNS ||= [ - 'topic_links.url' - ] + TRUNCATABLE_COLUMNS ||= ["topic_links.url"] - def self.remap(from, to, anchor_left: false, anchor_right: false, excluded_tables: [], verbose: false) - like = "#{anchor_left ? '' : "%"}#{from}#{anchor_right ? '' : "%"}" + def self.remap( + from, + to, + anchor_left: false, + anchor_right: false, + excluded_tables: [], + verbose: false + ) + like = "#{anchor_left ? "" : "%"}#{from}#{anchor_right ? "" : "%"}" text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REPLACE(\"#{column[:name]}\", :from, :to)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REPLACE(\"#{column[:name]}\", :from, :to)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" - end.join(" OR ") + where = + columns + .map { |column| "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" } + .join(" OR ") rows = DB.exec(<<~SQL, from: from, to: to, like: like) UPDATE \"#{table}\" @@ -50,19 +58,32 @@ class DbHelper finish! end - def self.regexp_replace(pattern, replacement, flags: "gi", match: "~*", excluded_tables: [], verbose: false) + def self.regexp_replace( + pattern, + replacement, + flags: "gi", + match: "~*", + excluded_tables: [], + verbose: false + ) text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" - end.join(" OR ") + where = + columns + .map do |column| + "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" + end + .join(" OR ") rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match) UPDATE \"#{table}\" @@ -78,23 +99,25 @@ class DbHelper def self.find(needle, anchor_left: false, anchor_right: false, excluded_tables: []) found = {} - like = "#{anchor_left ? '' : "%"}#{needle}#{anchor_right ? '' : "%"}" + like = "#{anchor_left ? "" : "%"}#{needle}#{anchor_right ? "" : "%"}" - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) + DB + .query(REMAP_SQL) + .each do |r| + next if excluded_tables.include?(r.table_name) - rows = DB.query(<<~SQL, like: like) + rows = DB.query(<<~SQL, like: like) SELECT \"#{r.column_name}\" FROM \"#{r.table_name}\" - WHERE \""#{r.column_name}"\" LIKE :like + WHERE \"#{r.column_name}\" LIKE :like SQL - if rows.size > 0 - found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| - row.public_send(r.column_name) + if rows.size > 0 + found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| + row.public_send(r.column_name) + end end end - end found end @@ -112,16 +135,21 @@ class DbHelper triggers = DB.query(TRIGGERS_SQL).map(&:trigger_name).to_set text_columns = Hash.new { |h, k| h[k] = [] } - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name)) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + DB + .query(REMAP_SQL) + .each do |r| + if excluded_tables.include?(r.table_name) || + triggers.include?( + Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name), + ) || triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + next + end - text_columns[r.table_name] << { - name: r.column_name, - max_length: r.character_maximum_length - } - end + text_columns[r.table_name] << { + name: r.column_name, + max_length: r.character_maximum_length, + } + end text_columns end diff --git a/lib/demon/base.rb b/lib/demon/base.rb index 397fb5d0b0..c49265a4cf 100644 --- a/lib/demon/base.rb +++ b/lib/demon/base.rb @@ -1,26 +1,22 @@ # frozen_string_literal: true -module Demon; end +module Demon +end # intelligent fork based demonizer class Demon::Base - def self.demons @demons end def self.start(count = 1, verbose: false) @demons ||= {} - count.times do |i| - (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start - end + count.times { |i| (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start } end def self.stop return unless @demons - @demons.values.each do |demon| - demon.stop - end + @demons.values.each { |demon| demon.stop } end def self.restart @@ -32,16 +28,12 @@ class Demon::Base end def self.ensure_running - @demons.values.each do |demon| - demon.ensure_running - end + @demons.values.each { |demon| demon.ensure_running } end def self.kill(signal) return unless @demons - @demons.values.each do |demon| - demon.kill(signal) - end + @demons.values.each { |demon| demon.kill(signal) } end attr_reader :pid, :parent_pid, :started, :index @@ -83,18 +75,27 @@ class Demon::Base if @pid Process.kill(stop_signal, @pid) - wait_for_stop = lambda { - timeout = @stop_timeout + wait_for_stop = + lambda do + timeout = @stop_timeout - while alive? && timeout > 0 - timeout -= (@stop_timeout / 10.0) - sleep(@stop_timeout / 10.0) - Process.waitpid(@pid, Process::WNOHANG) rescue -1 + while alive? && timeout > 0 + timeout -= (@stop_timeout / 10.0) + sleep(@stop_timeout / 10.0) + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end + end + + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end end - Process.waitpid(@pid, Process::WNOHANG) rescue -1 - } - wait_for_stop.call if alive? @@ -118,7 +119,12 @@ class Demon::Base return end - dead = Process.waitpid(@pid, Process::WNOHANG) rescue -1 + dead = + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end if dead STDERR.puts "Detected dead worker #{@pid}, restarting..." @pid = nil @@ -141,21 +147,20 @@ class Demon::Base end def run - @pid = fork do - Process.setproctitle("discourse #{self.class.prefix}") - monitor_parent - establish_app - after_fork - end + @pid = + fork do + Process.setproctitle("discourse #{self.class.prefix}") + monitor_parent + establish_app + after_fork + end write_pid_file end def already_running? if File.exist? pid_file pid = File.read(pid_file).to_i - if Demon::Base.alive?(pid) - return pid - end + return pid if Demon::Base.alive?(pid) end nil @@ -164,24 +169,20 @@ class Demon::Base def self.alive?(pid) Process.kill(0, pid) true - rescue + rescue StandardError false end private def verbose(msg) - if @verbose - puts msg - end + puts msg if @verbose end def write_pid_file verbose("writing pid file #{pid_file} for #{@pid}") FileUtils.mkdir_p(@rails_root + "tmp/pids") - File.open(pid_file, 'w') do |f| - f.write(@pid) - end + File.open(pid_file, "w") { |f| f.write(@pid) } end def delete_pid_file diff --git a/lib/demon/email_sync.rb b/lib/demon/email_sync.rb index 12fedc5724..93a627c033 100644 --- a/lib/demon/email_sync.rb +++ b/lib/demon/email_sync.rb @@ -36,15 +36,20 @@ class Demon::EmailSync < ::Demon::Base status = nil idle = false - while @running && group.reload.imap_mailbox_name.present? do + while @running && group.reload.imap_mailbox_name.present? ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group) - status = syncer.process( - idle: syncer.can_idle? && status && status[:remaining] == 0, - old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, - ) + status = + syncer.process( + idle: syncer.can_idle? && status && status[:remaining] == 0, + old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, + ) if !syncer.can_idle? && status[:remaining] == 0 - ImapSyncLog.debug("Going to sleep for group #{group.name} in db #{db} to wait for new emails", group, db: false) + ImapSyncLog.debug( + "Going to sleep for group #{group.name} in db #{db} to wait for new emails", + group, + db: false, + ) # Thread goes into sleep for a bit so it is better to return any # connection back to the pool. @@ -66,11 +71,7 @@ class Demon::EmailSync < ::Demon::Base # synchronization primitives available anyway). @running = false - @sync_data.each do |db, sync_data| - sync_data.each do |_, data| - kill_and_disconnect!(data) - end - end + @sync_data.each { |db, sync_data| sync_data.each { |_, data| kill_and_disconnect!(data) } } exit 0 end @@ -89,9 +90,9 @@ class Demon::EmailSync < ::Demon::Base @sync_data = {} @sync_lock = Mutex.new - trap('INT') { kill_threads } - trap('TERM') { kill_threads } - trap('HUP') { kill_threads } + trap("INT") { kill_threads } + trap("TERM") { kill_threads } + trap("HUP") { kill_threads } while @running Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL) @@ -101,9 +102,7 @@ class Demon::EmailSync < ::Demon::Base @sync_data.filter! do |db, sync_data| next true if all_dbs.include?(db) - sync_data.each do |_, data| - kill_and_disconnect!(data) - end + sync_data.each { |_, data| kill_and_disconnect!(data) } false end @@ -121,7 +120,10 @@ class Demon::EmailSync < ::Demon::Base next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected? if !groups[group_id] - ImapSyncLog.warn("Killing thread for group because mailbox is no longer synced", group_id) + ImapSyncLog.warn( + "Killing thread for group because mailbox is no longer synced", + group_id, + ) else ImapSyncLog.warn("Thread for group is dead", group_id) end @@ -133,12 +135,13 @@ class Demon::EmailSync < ::Demon::Base # Spawn new threads for groups that are now synchronized. groups.each do |group_id, group| if !@sync_data[db][group_id] - ImapSyncLog.debug("Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", group, db: false) + ImapSyncLog.debug( + "Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", + group, + db: false, + ) - @sync_data[db][group_id] = { - thread: start_thread(db, group), - syncer: nil - } + @sync_data[db][group_id] = { thread: start_thread(db, group), syncer: nil } end end end diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb index babf5a5e7e..78f0782046 100644 --- a/lib/demon/rails_autospec.rb +++ b/lib/demon/rails_autospec.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::RailsAutospec < Demon::Base - def self.prefix "rails-autospec" end @@ -17,15 +16,10 @@ class Demon::RailsAutospec < Demon::Base def after_fork require "rack" ENV["RAILS_ENV"] = "test" - Rack::Server.start( - config: "config.ru", - AccessLog: [], - Port: ENV["TEST_SERVER_PORT"] || 60099, - ) + Rack::Server.start(config: "config.ru", AccessLog: [], Port: ENV["TEST_SERVER_PORT"] || 60_099) rescue => e STDERR.puts e.message STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index ed7556d299..1c1776d80c 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::Sidekiq < ::Demon::Base - def self.prefix "sidekiq" end @@ -26,7 +25,7 @@ class Demon::Sidekiq < ::Demon::Base Demon::Sidekiq.after_fork&.call puts "Loading Sidekiq in process id #{Process.pid}" - require 'sidekiq/cli' + require "sidekiq/cli" cli = Sidekiq::CLI.instance # Unicorn uses USR1 to indicate that log files have been rotated @@ -38,10 +37,10 @@ class Demon::Sidekiq < ::Demon::Base options = ["-c", GlobalSetting.sidekiq_workers.to_s] - [['critical', 8], ['default', 4], ['low', 2], ['ultra_low', 1]].each do |queue_name, weight| + [["critical", 8], ["default", 4], ["low", 2], ["ultra_low", 1]].each do |queue_name, weight| custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"] - if !custom_queue_hostname || custom_queue_hostname.split(',').include?(Discourse.os_hostname) + if !custom_queue_hostname || custom_queue_hostname.split(",").include?(Discourse.os_hostname) options << "-q" options << "#{queue_name},#{weight}" end @@ -49,7 +48,7 @@ class Demon::Sidekiq < ::Demon::Base # Sidekiq not as high priority as web, in this environment it is forked so a web is very # likely running - Discourse::Utils.execute_command('renice', '-n', '5', '-p', Process.pid.to_s) + Discourse::Utils.execute_command("renice", "-n", "5", "-p", Process.pid.to_s) cli.parse(options) load Rails.root + "config/initializers/100-sidekiq.rb" @@ -59,5 +58,4 @@ class Demon::Sidekiq < ::Demon::Base STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/directory_helper.rb b/lib/directory_helper.rb index a41b117a39..80b2865a85 100644 --- a/lib/directory_helper.rb +++ b/lib/directory_helper.rb @@ -1,24 +1,23 @@ # frozen_string_literal: true module DirectoryHelper - def tmp_directory(prefix) directory_cache[prefix] ||= begin - f = File.join(Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) + f = File.join(Rails.root, "tmp", Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) FileUtils.mkdir_p(f) unless Dir[f].present? f end end def remove_tmp_directory(prefix) - tmp_directory_name = directory_cache[prefix] || '' + tmp_directory_name = directory_cache[prefix] || "" directory_cache.delete(prefix) FileUtils.rm_rf(tmp_directory_name) if Dir[tmp_directory_name].present? end private + def directory_cache @directory_cache ||= {} end - end diff --git a/lib/discourse.rb b/lib/discourse.rb index 68c2e6f210..acb8d2060d 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'cache' -require 'open3' -require 'plugin/instance' -require 'version' +require "cache" +require "open3" +require "plugin/instance" +require "version" module Discourse DB_POST_MIGRATE_PATH ||= "db/post_migrate" REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME" class Utils - URI_REGEXP ||= URI.regexp(%w{http https}) + URI_REGEXP ||= URI.regexp(%w[http https]) # Usage: # Discourse::Utils.execute_command("pwd", chdir: 'mydirectory') @@ -22,7 +22,9 @@ module Discourse runner = CommandRunner.new(**args) if block_given? - raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present? + if command.present? + raise RuntimeError.new("Cannot pass command and block to execute_command") + end yield runner else runner.exec(*command) @@ -33,33 +35,32 @@ module Discourse logs.join("\n") end - def self.logs_markdown(logs, user:, filename: 'log.txt') + def self.logs_markdown(logs, user:, filename: "log.txt") # Reserve 250 characters for the rest of the text max_logs_length = SiteSetting.max_post_length - 250 pretty_logs = Discourse::Utils.pretty_logs(logs) # If logs are short, try to inline them - if pretty_logs.size < max_logs_length - return <<~TEXT + return <<~TEXT if pretty_logs.size < max_logs_length ```text #{pretty_logs} ``` TEXT - end # Try to create an upload for the logs - upload = Dir.mktmpdir do |dir| - File.write(File.join(dir, filename), pretty_logs) - zipfile = Compression::Zip.new.compress(dir, filename) - File.open(zipfile) do |file| - UploadCreator.new( - file, - File.basename(zipfile), - type: 'backup_logs', - for_export: 'true' - ).create_for(user.id) + upload = + Dir.mktmpdir do |dir| + File.write(File.join(dir, filename), pretty_logs) + zipfile = Compression::Zip.new.compress(dir, filename) + File.open(zipfile) do |file| + UploadCreator.new( + file, + File.basename(zipfile), + type: "backup_logs", + for_export: "true", + ).create_for(user.id) + end end - end if upload.persisted? return UploadMarkdown.new(upload).attachment_markdown @@ -82,8 +83,8 @@ module Discourse rescue Errno::ENOENT end - FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) - temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) + FileUtils.mkdir_p(File.join(Rails.root, "tmp")) + temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex) File.open(temp_destination, "w") do |fd| fd.write(contents) @@ -101,9 +102,9 @@ module Discourse rescue Errno::ENOENT, Errno::EINVAL end - FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) - temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) - execute_command('ln', '-s', source, temp_destination) + FileUtils.mkdir_p(File.join(Rails.root, "tmp")) + temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex) + execute_command("ln", "-s", source, temp_destination) FileUtils.mv(temp_destination, destination) nil @@ -127,13 +128,22 @@ module Discourse end def exec(*command, **exec_params) - raise RuntimeError.new("Cannot specify same parameters at block and command level") if (@init_params.keys & exec_params.keys).present? + if (@init_params.keys & exec_params.keys).present? + raise RuntimeError.new("Cannot specify same parameters at block and command level") + end execute_command(*command, **@init_params.merge(exec_params)) end private - def execute_command(*command, timeout: nil, failure_message: "", success_status_codes: [0], chdir: ".", unsafe_shell: false) + def execute_command( + *command, + timeout: nil, + failure_message: "", + success_status_codes: [0], + chdir: ".", + unsafe_shell: false + ) env = nil env = command.shift if command[0].is_a?(Hash) @@ -156,11 +166,11 @@ module Discourse if !status.exited? || !success_status_codes.include?(status.exitstatus) failure_message = "#{failure_message}\n" if !failure_message.blank? raise CommandError.new( - "#{caller[0]}: #{failure_message}#{stderr}", - stdout: stdout, - stderr: stderr, - status: status - ) + "#{caller[0]}: #{failure_message}#{stderr}", + stdout: stdout, + stderr: stderr, + status: status, + ) end stdout @@ -195,33 +205,32 @@ module Discourse # mini_scheduler direct reporting if Hash === job job_class = job["class"] - if job_class - job_exception_stats[job_class] += 1 - end + job_exception_stats[job_class] += 1 if job_class end # internal reporting - if job.class == Class && ::Jobs::Base > job - job_exception_stats[job] += 1 - end + job_exception_stats[job] += 1 if job.class == Class && ::Jobs::Base > job cm = RailsMultisite::ConnectionManagement - parent_logger.handle_exception(ex, { - current_db: cm.current_db, - current_hostname: cm.current_hostname - }.merge(context)) + parent_logger.handle_exception( + ex, + { current_db: cm.current_db, current_hostname: cm.current_hostname }.merge(context), + ) raise ex if Rails.env.test? end # Expected less matches than what we got in a find - class TooManyMatches < StandardError; end + class TooManyMatches < StandardError + end # When they try to do something they should be logged in for - class NotLoggedIn < StandardError; end + class NotLoggedIn < StandardError + end # When the input is somehow bad - class InvalidParameters < StandardError; end + class InvalidParameters < StandardError + end # When they don't have permission to do something class InvalidAccess < StandardError @@ -249,7 +258,13 @@ module Discourse attr_reader :original_path attr_reader :custom_message - def initialize(msg = nil, status: 404, check_permalinks: false, original_path: nil, custom_message: nil) + def initialize( + msg = nil, + status: 404, + check_permalinks: false, + original_path: nil, + custom_message: nil + ) super(msg) @status = status @@ -260,27 +275,33 @@ module Discourse end # When a setting is missing - class SiteSettingMissing < StandardError; end + class SiteSettingMissing < StandardError + end # When ImageMagick is missing - class ImageMagickMissing < StandardError; end + class ImageMagickMissing < StandardError + end # When read-only mode is enabled - class ReadOnly < StandardError; end + class ReadOnly < StandardError + end # Cross site request forgery - class CSRF < StandardError; end + class CSRF < StandardError + end - class Deprecation < StandardError; end + class Deprecation < StandardError + end - class ScssError < StandardError; end + class ScssError < StandardError + end def self.filters - @filters ||= [:latest, :unread, :new, :unseen, :top, :read, :posted, :bookmarks] + @filters ||= %i[latest unread new unseen top read posted bookmarks] end def self.anonymous_filters - @anonymous_filters ||= [:latest, :top, :categories] + @anonymous_filters ||= %i[latest top categories] end def self.top_menu_items @@ -288,7 +309,7 @@ module Discourse end def self.anonymous_top_menu_items - @anonymous_top_menu_items ||= Discourse.anonymous_filters + [:categories, :top] + @anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top] end PIXEL_RATIOS ||= [1, 1.5, 2, 3] @@ -297,26 +318,28 @@ module Discourse # TODO: should cache these when we get a notification system for site settings set = Set.new - SiteSetting.avatar_sizes.split("|").map(&:to_i).each do |size| - PIXEL_RATIOS.each do |pixel_ratio| - set << (size * pixel_ratio).to_i - end - end + SiteSetting + .avatar_sizes + .split("|") + .map(&:to_i) + .each { |size| PIXEL_RATIOS.each { |pixel_ratio| set << (size * pixel_ratio).to_i } } set end def self.activate_plugins! @plugins = [] - Plugin::Instance.find_all("#{Rails.root}/plugins").each do |p| - v = p.metadata.required_version || Discourse::VERSION::STRING - if Discourse.has_needed_version?(Discourse::VERSION::STRING, v) - p.activate! - @plugins << p - else - STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})" + Plugin::Instance + .find_all("#{Rails.root}/plugins") + .each do |p| + v = p.metadata.required_version || Discourse::VERSION::STRING + if Discourse.has_needed_version?(Discourse::VERSION::STRING, v) + p.activate! + @plugins << p + else + STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})" + end end - end DiscourseEvent.trigger(:after_plugin_activation) end @@ -360,9 +383,7 @@ module Discourse def self.apply_asset_filters(plugins, type, request) filter_opts = asset_filter_options(type, request) - plugins.select do |plugin| - plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } - end + plugins.select { |plugin| plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } } end def self.asset_filter_options(type, request) @@ -385,20 +406,24 @@ module Discourse targets << :desktop if args[:desktop_view] targets.each do |target| - assets += plugins.find_all do |plugin| - plugin.css_asset_exists?(target) - end.map do |plugin| - target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}" - end + assets += + plugins + .find_all { |plugin| plugin.css_asset_exists?(target) } + .map do |plugin| + target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}" + end end assets end def self.find_plugin_js_assets(args) - plugins = self.find_plugins(args).select do |plugin| - plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists? - end + plugins = + self + .find_plugins(args) + .select do |plugin| + plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists? + end plugins = apply_asset_filters(plugins, :js, args[:request]) @@ -413,25 +438,33 @@ module Discourse end def self.assets_digest - @assets_digest ||= begin - digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) + @assets_digest ||= + begin + digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) - channel = "/global/asset-version" - message = MessageBus.last_message(channel) + channel = "/global/asset-version" + message = MessageBus.last_message(channel) - unless message && message.data == digest - MessageBus.publish channel, digest + MessageBus.publish channel, digest unless message && message.data == digest + digest end - digest - end end BUILTIN_AUTH ||= [ - Auth::AuthProvider.new(authenticator: Auth::FacebookAuthenticator.new, frame_width: 580, frame_height: 400, icon: "fab-facebook"), - Auth::AuthProvider.new(authenticator: Auth::GoogleOAuth2Authenticator.new, frame_width: 850, frame_height: 500), # Custom icon implemented in client + Auth::AuthProvider.new( + authenticator: Auth::FacebookAuthenticator.new, + frame_width: 580, + frame_height: 400, + icon: "fab-facebook", + ), + Auth::AuthProvider.new( + authenticator: Auth::GoogleOAuth2Authenticator.new, + frame_width: 850, + frame_height: 500, + ), # Custom icon implemented in client Auth::AuthProvider.new(authenticator: Auth::GithubAuthenticator.new, icon: "fab-github"), Auth::AuthProvider.new(authenticator: Auth::TwitterAuthenticator.new, icon: "fab-twitter"), - Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord") + Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord"), ] def self.auth_providers @@ -439,7 +472,7 @@ module Discourse end def self.enabled_auth_providers - auth_providers.select { |provider| provider.authenticator.enabled? } + auth_providers.select { |provider| provider.authenticator.enabled? } end def self.authenticators @@ -449,17 +482,18 @@ module Discourse end def self.enabled_authenticators - authenticators.select { |authenticator| authenticator.enabled? } + authenticators.select { |authenticator| authenticator.enabled? } end def self.cache - @cache ||= begin - if GlobalSetting.skip_redis? - ActiveSupport::Cache::MemoryStore.new - else - Cache.new + @cache ||= + begin + if GlobalSetting.skip_redis? + ActiveSupport::Cache::MemoryStore.new + else + Cache.new + end end - end end # hostname of the server, operating system level @@ -467,15 +501,15 @@ module Discourse def self.os_hostname @os_hostname ||= begin - require 'socket' + require "socket" Socket.gethostname rescue => e - warn_exception(e, message: 'Socket.gethostname is not working') + warn_exception(e, message: "Socket.gethostname is not working") begin `hostname`.strip rescue => e - warn_exception(e, message: 'hostname command is not working') - 'unknown_host' + warn_exception(e, message: "hostname command is not working") + "unknown_host" end end end @@ -501,12 +535,12 @@ module Discourse def self.current_hostname_with_port default_port = SiteSetting.force_https? ? 443 : 80 result = +"#{current_hostname}" - result << ":#{SiteSetting.port}" if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port - - if Rails.env.development? && SiteSetting.port.blank? - result << ":#{ENV["UNICORN_PORT"] || 3000}" + if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port + result << ":#{SiteSetting.port}" end + result << ":#{ENV["UNICORN_PORT"] || 3000}" if Rails.env.development? && SiteSetting.port.blank? + result end @@ -520,16 +554,18 @@ module Discourse def self.route_for(uri) unless uri.is_a?(URI) - uri = begin - URI(uri) - rescue ArgumentError, URI::Error - end + uri = + begin + URI(uri) + rescue ArgumentError, URI::Error + end end return unless uri path = +(uri.path || "") - if !uri.host || (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path)) + if !uri.host || + (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path)) path.slice!(Discourse.base_path) return Rails.application.routes.recognize_path(path) end @@ -543,21 +579,21 @@ module Discourse alias_method :base_url_no_path, :base_url_no_prefix end - READONLY_MODE_KEY_TTL ||= 60 - READONLY_MODE_KEY ||= 'readonly_mode' - PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres' - PG_READONLY_MODE_KEY_TTL ||= 300 - USER_READONLY_MODE_KEY ||= 'readonly_mode:user' - PG_FORCE_READONLY_MODE_KEY ||= 'readonly_mode:postgres_force' + READONLY_MODE_KEY_TTL ||= 60 + READONLY_MODE_KEY ||= "readonly_mode" + PG_READONLY_MODE_KEY ||= "readonly_mode:postgres" + PG_READONLY_MODE_KEY_TTL ||= 300 + USER_READONLY_MODE_KEY ||= "readonly_mode:user" + PG_FORCE_READONLY_MODE_KEY ||= "readonly_mode:postgres_force" # Psuedo readonly mode, where staff can still write - STAFF_WRITES_ONLY_MODE_KEY ||= 'readonly_mode:staff_writes_only' + STAFF_WRITES_ONLY_MODE_KEY ||= "readonly_mode:staff_writes_only" READONLY_KEYS ||= [ READONLY_MODE_KEY, PG_READONLY_MODE_KEY, USER_READONLY_MODE_KEY, - PG_FORCE_READONLY_MODE_KEY + PG_FORCE_READONLY_MODE_KEY, ] def self.enable_readonly_mode(key = READONLY_MODE_KEY) @@ -565,7 +601,9 @@ module Discourse Sidekiq.pause!("pg_failover") if !Sidekiq.paused? end - if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?(key) + if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?( + key, + ) Discourse.redis.set(key, 1) else ttl = @@ -595,15 +633,13 @@ module Discourse unless @threads[key]&.alive? @threads[key] = Thread.new do - while @dbs.size > 0 do + while @dbs.size > 0 sleep ttl / 2 @mutex.synchronize do @dbs.each do |db| RailsMultisite::ConnectionManagement.with_connection(db) do - if !Discourse.redis.expire(key, ttl) - @dbs.delete(db) - end + @dbs.delete(db) if !Discourse.redis.expire(key, ttl) end end end @@ -653,7 +689,7 @@ module Discourse # Shared between processes def self.postgres_last_read_only - @postgres_last_read_only ||= DistributedCache.new('postgres_last_read_only', namespace: false) + @postgres_last_read_only ||= DistributedCache.new("postgres_last_read_only", namespace: false) end # Per-process @@ -698,39 +734,43 @@ module Discourse # This is better than `MessageBus.publish "/file-change", ["refresh"]` because # it spreads the refreshes out over a time period if user_ids - MessageBus.publish("/refresh_client", 'clobber', user_ids: user_ids) + MessageBus.publish("/refresh_client", "clobber", user_ids: user_ids) else - MessageBus.publish('/global/asset-version', 'clobber') + MessageBus.publish("/global/asset-version", "clobber") end end def self.git_version - @git_version ||= begin - git_cmd = 'git rev-parse HEAD' - self.try_git(git_cmd, Discourse::VERSION::STRING) - end + @git_version ||= + begin + git_cmd = "git rev-parse HEAD" + self.try_git(git_cmd, Discourse::VERSION::STRING) + end end def self.git_branch - @git_branch ||= begin - git_cmd = 'git rev-parse --abbrev-ref HEAD' - self.try_git(git_cmd, 'unknown') - end + @git_branch ||= + begin + git_cmd = "git rev-parse --abbrev-ref HEAD" + self.try_git(git_cmd, "unknown") + end end def self.full_version - @full_version ||= begin - git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null' - self.try_git(git_cmd, 'unknown') - end + @full_version ||= + begin + git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null' + self.try_git(git_cmd, "unknown") + end end def self.last_commit_date - @last_commit_date ||= begin - git_cmd = 'git log -1 --format="%ct"' - seconds = self.try_git(git_cmd, nil) - seconds.nil? ? nil : DateTime.strptime(seconds, '%s') - end + @last_commit_date ||= + begin + git_cmd = 'git log -1 --format="%ct"' + seconds = self.try_git(git_cmd, nil) + seconds.nil? ? nil : DateTime.strptime(seconds, "%s") + end end def self.try_git(git_cmd, default_value) @@ -738,20 +778,21 @@ module Discourse begin version_value = `#{git_cmd}`.strip - rescue + rescue StandardError version_value = default_value end - if version_value.empty? - version_value = default_value - end + version_value = default_value if version_value.empty? version_value end # Either returns the site_contact_username user or the first admin. def self.site_contact_user - user = User.find_by(username_lower: SiteSetting.site_contact_username.downcase) if SiteSetting.site_contact_username.present? + user = + User.find_by( + username_lower: SiteSetting.site_contact_username.downcase, + ) if SiteSetting.site_contact_username.present? user ||= (system_user || User.admins.real.order(:id).first) end @@ -765,10 +806,10 @@ module Discourse def self.store if SiteSetting.Upload.enable_s3_uploads - @s3_store_loaded ||= require 'file_store/s3_store' + @s3_store_loaded ||= require "file_store/s3_store" FileStore::S3Store.new else - @local_store_loaded ||= require 'file_store/local_store' + @local_store_loaded ||= require "file_store/local_store" FileStore::LocalStore.new end end @@ -805,15 +846,15 @@ module Discourse Discourse.cache.reconnect Logster.store.redis.reconnect # shuts down all connections in the pool - Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! } + Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! } # re-establish Sidekiq.redis = sidekiq_redis_config # in case v8 was initialized we want to make sure it is nil PrettyText.reset_context - DiscourseJsProcessor::Transpiler.reset_context if defined? DiscourseJsProcessor::Transpiler - JsLocaleHelper.reset_context if defined? JsLocaleHelper + DiscourseJsProcessor::Transpiler.reset_context if defined?(DiscourseJsProcessor::Transpiler) + JsLocaleHelper.reset_context if defined?(JsLocaleHelper) # warm up v8 after fork, that way we do not fork a v8 context # it may cause issues if bg threads in a v8 isolate randomly stop @@ -831,7 +872,7 @@ module Discourse # you can use Discourse.warn when you want to report custom environment # with the error, this helps with grouping def self.warn(message, env = nil) - append = env ? (+" ") << env.map { |k, v|"#{k}: #{v}" }.join(" ") : "" + append = env ? (+" ") << env.map { |k, v| "#{k}: #{v}" }.join(" ") : "" if !(Logster::Logger === Rails.logger) Rails.logger.warn("#{message}#{append}") @@ -839,9 +880,7 @@ module Discourse end loggers = [Rails.logger] - if Rails.logger.chained - loggers.concat(Rails.logger.chained) - end + loggers.concat(Rails.logger.chained) if Rails.logger.chained logster_env = env @@ -849,9 +888,7 @@ module Discourse logster_env = Logster::Message.populate_from_env(old_env) # a bit awkward by try to keep the new params - env.each do |k, v| - logster_env[k] = v - end + env.each { |k, v| logster_env[k] = v } end loggers.each do |logger| @@ -860,12 +897,7 @@ module Discourse next end - logger.store.report( - ::Logger::Severity::WARN, - "discourse", - message, - env: logster_env - ) + logger.store.report(::Logger::Severity::WARN, "discourse", message, env: logster_env) end if old_env @@ -881,7 +913,6 @@ module Discourse # report a warning maintaining backtrack for logster def self.warn_exception(e, message: "", env: nil) if Rails.logger.respond_to? :add_with_opts - env ||= {} env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db @@ -891,16 +922,23 @@ module Discourse "#{message} : #{e.class.name} : #{e}", "discourse-exception", backtrace: e.backtrace.join("\n"), - env: env + env: env, ) else # no logster ... fallback Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}") end - rescue + rescue StandardError STDERR.puts "Failed to report exception #{e} #{message}" end + def self.capture_exceptions(message: "", env: nil) + yield + rescue Exception => e + Discourse.warn_exception(e, message: message, env: env) + nil + end + def self.deprecate(warning, drop_from: nil, since: nil, raise_error: false, output_in_test: false) location = caller_locations[1].yield_self { |l| "#{l.path}:#{l.lineno}:in \`#{l.label}\`" } warning = ["Deprecation notice:", warning] @@ -909,17 +947,11 @@ module Discourse warning << "\nAt #{location}" warning = warning.join(" ") - if raise_error - raise Deprecation.new(warning) - end + raise Deprecation.new(warning) if raise_error - if Rails.env == "development" - STDERR.puts(warning) - end + STDERR.puts(warning) if Rails.env == "development" - if output_in_test && Rails.env == "test" - STDERR.puts(warning) - end + STDERR.puts(warning) if output_in_test && Rails.env == "test" digest = Digest::MD5.hexdigest(warning) redis_key = "deprecate-notice-#{digest}" @@ -935,7 +967,7 @@ module Discourse warning end - SIDEKIQ_NAMESPACE ||= 'sidekiq' + SIDEKIQ_NAMESPACE ||= "sidekiq" def self.sidekiq_redis_config conf = GlobalSetting.redis_config.dup @@ -951,7 +983,8 @@ module Discourse def self.reset_active_record_cache_if_needed(e) last_cache_reset = Discourse.last_ar_cache_reset - if e && e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) + if e && e.message =~ /UndefinedColumn/ && + (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago) Rails.logger.warn "Clearing Active Record cache, this can happen if schema changed while site is running or in a multisite various databases are running different schemas. Consider running rake multisite:migrate." Discourse.last_ar_cache_reset = Time.zone.now Discourse.reset_active_record_cache @@ -961,7 +994,11 @@ module Discourse def self.reset_active_record_cache ActiveRecord::Base.connection.query_cache.clear (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.reset_column_information rescue nil + begin + table.classify.constantize.reset_column_information + rescue StandardError + nil + end end nil end @@ -971,7 +1008,7 @@ module Discourse end def self.skip_post_deployment_migrations? - ['1', 'true'].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) + %w[1 true].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) end # this is used to preload as much stuff as possible prior to forking @@ -985,7 +1022,11 @@ module Discourse # load up all models and schema (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end # ensure we have a full schema cache in case we missed something above @@ -1024,29 +1065,27 @@ module Discourse end [ - Thread.new { + Thread.new do # router warm up - Rails.application.routes.recognize_path('abc') rescue nil - }, - Thread.new { + begin + Rails.application.routes.recognize_path("abc") + rescue StandardError + nil + end + end, + Thread.new do # preload discourse version Discourse.git_version Discourse.git_branch Discourse.full_version - }, - Thread.new { - require 'actionview_precompiler' + end, + Thread.new do + require "actionview_precompiler" ActionviewPrecompiler.precompile - }, - Thread.new { - LetterAvatar.image_magick_version - }, - Thread.new { - SvgSprite.core_svgs - }, - Thread.new { - EmberCli.script_chunks - } + end, + Thread.new { LetterAvatar.image_magick_version }, + Thread.new { SvgSprite.core_svgs }, + Thread.new { EmberCli.script_chunks }, ].each(&:join) ensure @preloaded_rails = true @@ -1055,10 +1094,10 @@ module Discourse mattr_accessor :redis def self.is_parallel_test? - ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER'] + ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"] end - CDN_REQUEST_METHODS ||= ["GET", "HEAD", "OPTIONS"] + CDN_REQUEST_METHODS ||= %w[GET HEAD OPTIONS] def self.is_cdn_request?(env, request_method) return unless CDN_REQUEST_METHODS.include?(request_method) @@ -1071,8 +1110,8 @@ module Discourse end def self.apply_cdn_headers(headers) - headers['Access-Control-Allow-Origin'] = '*' - headers['Access-Control-Allow-Methods'] = CDN_REQUEST_METHODS.join(", ") + headers["Access-Control-Allow-Origin"] = "*" + headers["Access-Control-Allow-Methods"] = CDN_REQUEST_METHODS.join(", ") headers end @@ -1091,8 +1130,12 @@ module Discourse end def self.anonymous_locale(request) - locale = HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie - locale ||= HttpLanguageParser.parse(request.env["HTTP_ACCEPT_LANGUAGE"]) if SiteSetting.set_locale_from_accept_language_header + locale = + HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie + locale ||= + HttpLanguageParser.parse( + request.env["HTTP_ACCEPT_LANGUAGE"], + ) if SiteSetting.set_locale_from_accept_language_header locale end end diff --git a/lib/discourse_connect_base.rb b/lib/discourse_connect_base.rb index 3a503ddcbe..b5e04d8f21 100644 --- a/lib/discourse_connect_base.rb +++ b/lib/discourse_connect_base.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true class DiscourseConnectBase + class ParseError < RuntimeError + end - class ParseError < RuntimeError; end - - ACCESSORS = %i{ + ACCESSORS = %i[ add_groups - admin moderator + admin + moderator avatar_force_update avatar_url bio @@ -31,11 +32,11 @@ class DiscourseConnectBase title username website - } + ] FIXNUMS = [] - BOOLS = %i{ + BOOLS = %i[ admin avatar_force_update confirmed_2fa @@ -46,7 +47,7 @@ class DiscourseConnectBase require_2fa require_activation suppress_welcome_message - } + ] def self.nonce_expiry_time @nonce_expiry_time ||= 10.minutes @@ -80,9 +81,11 @@ class DiscourseConnectBase decoded_hash = Rack::Utils.parse_query(decoded) if sso.sign(parsed["sso"]) != parsed["sig"] - diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" - if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m - raise ParseError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" + diags = + "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" + if parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m + raise ParseError, + "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" else raise ParseError, "Bad signature for payload #{diags}" end @@ -91,9 +94,7 @@ class DiscourseConnectBase ACCESSORS.each do |k| val = decoded_hash[k.to_s] val = val.to_i if FIXNUMS.include? k - if BOOLS.include? k - val = ["true", "false"].include?(val) ? val == "true" : nil - end + val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k sso.public_send("#{k}=", val) end @@ -137,12 +138,12 @@ class DiscourseConnectBase def to_url(base_url = nil) base = "#{base_url || sso_url}" - "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" + "#{base}#{base.include?("?") ? "&" : "?"}#{payload}" end def payload(secret = nil) payload = Base64.strict_encode64(unsigned_payload) - "sso=#{CGI::escape(payload)}&sig=#{sign(payload, secret)}" + "sso=#{CGI.escape(payload)}&sig=#{sign(payload, secret)}" end def unsigned_payload @@ -157,9 +158,7 @@ class DiscourseConnectBase payload[k] = val end - @custom_fields&.each do |k, v| - payload["custom.#{k}"] = v.to_s - end + @custom_fields&.each { |k, v| payload["custom.#{k}"] = v.to_s } payload end diff --git a/lib/discourse_connect_provider.rb b/lib/discourse_connect_provider.rb index 218d081b10..76f46989df 100644 --- a/lib/discourse_connect_provider.rb +++ b/lib/discourse_connect_provider.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class DiscourseConnectProvider < DiscourseConnectBase - class BlankSecret < RuntimeError; end - class BlankReturnUrl < RuntimeError; end + class BlankSecret < RuntimeError + end + class BlankReturnUrl < RuntimeError + end def self.parse(payload, sso_secret = nil, **init_kwargs) parsed_payload = Rack::Utils.parse_query(payload) @@ -15,11 +17,16 @@ class DiscourseConnectProvider < DiscourseConnectBase if sso_secret.blank? begin host = URI.parse(return_sso_url).host - Rails.logger.warn("SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings") + Rails.logger.warn( + "SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings", + ) rescue StandardError => e # going for StandardError cause URI::Error may not be enough, eg it parses to something not # responding to host - Discourse.warn_exception(e, message: "SSO failed; invalid or missing return_sso_url in SSO payload") + Discourse.warn_exception( + e, + message: "SSO failed; invalid or missing return_sso_url in SSO payload", + ) end raise BlankSecret @@ -31,7 +38,7 @@ class DiscourseConnectProvider < DiscourseConnectBase def self.lookup_return_sso_url(parsed_payload) decoded = Base64.decode64(parsed_payload["sso"]) decoded_hash = Rack::Utils.parse_query(decoded) - decoded_hash['return_sso_url'] + decoded_hash["return_sso_url"] end def self.lookup_sso_secret(return_sso_url, parsed_payload) @@ -39,21 +46,23 @@ class DiscourseConnectProvider < DiscourseConnectBase return_url_host = URI.parse(return_sso_url).host - provider_secrets = SiteSetting - .discourse_connect_provider_secrets - .split("\n") - .map { |row| row.split("|", 2) } - .sort_by { |k, _| k } - .reverse + provider_secrets = + SiteSetting + .discourse_connect_provider_secrets + .split("\n") + .map { |row| row.split("|", 2) } + .sort_by { |k, _| k } + .reverse first_domain_match = nil - pair = provider_secrets.find do |domain, configured_secret| - if WildcardDomainChecker.check_domain(domain, return_url_host) - first_domain_match ||= configured_secret - sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + pair = + provider_secrets.find do |domain, configured_secret| + if WildcardDomainChecker.check_domain(domain, return_url_host) + first_domain_match ||= configured_secret + sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + end end - end # falls back to a secret which will fail to validate in DiscourseConnectBase # this ensures error flow is correct diff --git a/lib/discourse_dev/category.rb b/lib/discourse_dev/category.rb index b13c43d758..7c1fe2500c 100644 --- a/lib/discourse_dev/category.rb +++ b/lib/discourse_dev/category.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Category < Record - def initialize super(::Category, DiscourseDev.config.category[:count]) @parent_category_ids = ::Category.where(parent_category_id: nil).pluck(:id) @@ -29,7 +28,7 @@ module DiscourseDev description: Faker::Lorem.paragraph, user_id: ::Discourse::SYSTEM_USER_ID, color: Faker::Color.hex_color.last(6), - parent_category_id: parent_category_id + parent_category_id: parent_category_id, } end diff --git a/lib/discourse_dev/config.rb b/lib/discourse_dev/config.rb index eb4a1d160b..f83eebe880 100644 --- a/lib/discourse_dev/config.rb +++ b/lib/discourse_dev/config.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rails' -require 'highline/import' +require "rails" +require "highline/import" module DiscourseDev class Config @@ -63,10 +63,11 @@ module DiscourseDev if settings.present? email = settings[:email] || "new_user@example.com" - new_user = ::User.create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email) - ) + new_user = + ::User.create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + ) new_user.email_tokens.update_all confirmed: true new_user.activate end @@ -88,15 +89,14 @@ module DiscourseDev def create_admin_user_from_settings(settings) email = settings[:email] - admin = ::User.with_email(email).first_or_create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email), - password: settings[:password] - ) + admin = + ::User.with_email(email).first_or_create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + password: settings[:password], + ) admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate end @@ -107,10 +107,7 @@ module DiscourseDev password = ask("Password (optional, press ENTER to skip): ") username = UserNameSuggester.suggest(email) - admin = ::User.new( - email: email, - username: username - ) + admin = ::User.new(email: email, username: username) if password.present? admin.password = password @@ -122,7 +119,7 @@ module DiscourseDev saved = admin.save if saved - File.open(file_path, 'a') do | file| + File.open(file_path, "a") do |file| file.puts("admin:") file.puts(" username: #{admin.username}") file.puts(" email: #{admin.email}") @@ -137,9 +134,7 @@ module DiscourseDev admin.save admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate diff --git a/lib/discourse_dev/group.rb b/lib/discourse_dev/group.rb index c346b0c5fa..91a0ffcb7e 100644 --- a/lib/discourse_dev/group.rb +++ b/lib/discourse_dev/group.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Group < Record - def initialize super(::Group, DiscourseDev.config.group[:count]) end diff --git a/lib/discourse_dev/post.rb b/lib/discourse_dev/post.rb index ef0e072daa..350696a7d8 100644 --- a/lib/discourse_dev/post.rb +++ b/lib/discourse_dev/post.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Post < Record - attr_reader :topic def initialize(topic, count) @@ -28,7 +27,7 @@ module DiscourseDev raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), created_at: Faker::Time.between(from: topic.last_posted_at, to: DateTime.now), skip_validations: true, - skip_guardian: true + skip_guardian: true, } end @@ -44,13 +43,20 @@ module DiscourseDev def generate_likes(post) user_ids = [post.user_id] - Faker::Number.between(from: 0, to: @max_likes_count).times do - user = self.user - next if user_ids.include?(user.id) + Faker::Number + .between(from: 0, to: @max_likes_count) + .times do + user = self.user + next if user_ids.include?(user.id) - PostActionCreator.new(user, post, PostActionType.types[:like], created_at: Faker::Time.between(from: post.created_at, to: DateTime.now)).perform - user_ids << user.id - end + PostActionCreator.new( + user, + post, + PostActionType.types[:like], + created_at: Faker::Time.between(from: post.created_at, to: DateTime.now), + ).perform + user_ids << user.id + end end def user @@ -90,13 +96,14 @@ module DiscourseDev count.times do |i| begin user = User.random - reply = Faker::DiscourseMarkdown.with_user(user.id) do - { - topic_id: topic.id, - raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), - skip_validations: true - } - end + reply = + Faker::DiscourseMarkdown.with_user(user.id) do + { + topic_id: topic.id, + raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), + skip_validations: true, + } + end PostCreator.new(user, reply).create! rescue ActiveRecord::RecordNotSaved => e puts e @@ -109,6 +116,5 @@ module DiscourseDev def self.random super(::Post) end - end end diff --git a/lib/discourse_dev/post_revision.rb b/lib/discourse_dev/post_revision.rb index 7c1f1da644..0c282e9540 100644 --- a/lib/discourse_dev/post_revision.rb +++ b/lib/discourse_dev/post_revision.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class PostRevision < Record - def initialize super(::PostRevision, DiscourseDev.config.post_revisions[:count]) end diff --git a/lib/discourse_dev/record.rb b/lib/discourse_dev/record.rb index 3e2767ef62..b1b84097df 100644 --- a/lib/discourse_dev/record.rb +++ b/lib/discourse_dev/record.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'discourse_dev' -require 'rails' -require 'faker' +require "discourse_dev" +require "rails" +require "faker" module DiscourseDev class Record @@ -12,11 +12,12 @@ module DiscourseDev attr_reader :model, :type def initialize(model, count = DEFAULT_COUNT) - @@initialized ||= begin - Faker::Discourse.unique.clear - RateLimiter.disable - true - end + @@initialized ||= + begin + Faker::Discourse.unique.clear + RateLimiter.disable + true + end @model = model @type = model.to_s.downcase.to_sym @@ -40,11 +41,9 @@ module DiscourseDev if current_count >= @count puts "Already have #{current_count} #{type} records" - Rake.application.top_level_tasks.each do |task_name| - Rake::Task[task_name].reenable - end + Rake.application.top_level_tasks.each { |task_name| Rake::Task[task_name].reenable } - Rake::Task['dev:repopulate'].invoke + Rake::Task["dev:repopulate"].invoke return elsif current_count > 0 @count -= current_count @@ -74,7 +73,9 @@ module DiscourseDev end def self.random(model, use_existing_records: true) - model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") if !use_existing_records && model.new.respond_to?(:custom_fields) + if !use_existing_records && model.new.respond_to?(:custom_fields) + model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") + end count = model.count raise "#{:type} records are not yet populated" if count == 0 diff --git a/lib/discourse_dev/tag.rb b/lib/discourse_dev/tag.rb index 987d8656e9..96c06ad6e1 100644 --- a/lib/discourse_dev/tag.rb +++ b/lib/discourse_dev/tag.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Tag < Record - def initialize super(::Tag, DiscourseDev.config.tag[:count]) end @@ -24,9 +23,7 @@ module DiscourseDev end def data - { - name: Faker::Discourse.unique.tag, - } + { name: Faker::Discourse.unique.tag } end end end diff --git a/lib/discourse_dev/topic.rb b/lib/discourse_dev/topic.rb index c08af628eb..9c6baaafd5 100644 --- a/lib/discourse_dev/topic.rb +++ b/lib/discourse_dev/topic.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Topic < Record - def initialize(private_messages: false, recipient: nil, ignore_current_count: false) @settings = DiscourseDev.config.topic @private_messages = private_messages @@ -33,15 +32,9 @@ module DiscourseDev end if @category - merge_attributes = { - category: @category.id, - tags: tags - } + merge_attributes = { category: @category.id, tags: tags } else - merge_attributes = { - archetype: "private_message", - target_usernames: [@recipient] - } + merge_attributes = { archetype: "private_message", target_usernames: [@recipient] } end { @@ -51,9 +44,11 @@ module DiscourseDev topic_opts: { import_mode: true, views: Faker::Number.between(from: 1, to: max_views), - custom_fields: { dev_sample: true } + custom_fields: { + dev_sample: true, + }, }, - skip_validations: true + skip_validations: true, }.merge(merge_attributes) end @@ -61,7 +56,10 @@ module DiscourseDev if current_count < I18n.t("faker.discourse.topics").count Faker::Discourse.unique.topic else - Faker::Lorem.unique.sentence(word_count: 5, supplemental: true, random_words_to_add: 4).chomp(".") + Faker::Lorem + .unique + .sentence(word_count: 5, supplemental: true, random_words_to_add: 4) + .chomp(".") end end @@ -70,9 +68,9 @@ module DiscourseDev @tags = [] - Faker::Number.between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)).times do - @tags << Faker::Discourse.tag - end + Faker::Number + .between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)) + .times { @tags << Faker::Discourse.tag } @tags.uniq end @@ -92,7 +90,11 @@ module DiscourseDev if override = @settings.dig(:replies, :overrides).find { |o| o[:title] == topic_data[:title] } reply_count = override[:count] else - reply_count = Faker::Number.between(from: @settings.dig(:replies, :min), to: @settings.dig(:replies, :max)) + reply_count = + Faker::Number.between( + from: @settings.dig(:replies, :min), + to: @settings.dig(:replies, :max), + ) end topic = post.topic @@ -123,9 +125,7 @@ module DiscourseDev end def delete_unwanted_sidekiq_jobs - Sidekiq::ScheduledSet.new.each do |job| - job.delete if job.item["class"] == "Jobs::UserEmail" - end + Sidekiq::ScheduledSet.new.each { |job| job.delete if job.item["class"] == "Jobs::UserEmail" } end end end diff --git a/lib/discourse_diff.rb b/lib/discourse_diff.rb index 3887abe134..e31a699f85 100644 --- a/lib/discourse_diff.rb +++ b/lib/discourse_diff.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DiscourseDiff - MAX_DIFFERENCE = 200 def initialize(before, after) @@ -9,8 +8,8 @@ class DiscourseDiff @after = after before_html = tokenize_html_blocks(@before) after_html = tokenize_html_blocks(@after) - before_markdown = tokenize_line(CGI::escapeHTML(@before)) - after_markdown = tokenize_line(CGI::escapeHTML(@after)) + before_markdown = tokenize_line(CGI.escapeHTML(@before)) + after_markdown = tokenize_line(CGI.escapeHTML(@after)) @block_by_block_diff = ONPDiff.new(before_html, after_html).paragraph_diff @line_by_line_diff = ONPDiff.new(before_markdown, after_markdown).short_diff @@ -21,7 +20,8 @@ class DiscourseDiff inline = [] while i < @block_by_block_diff.size op_code = @block_by_block_diff[i][1] - if op_code == :common then inline << @block_by_block_diff[i][0] + if op_code == :common + inline << @block_by_block_diff[i][0] else if op_code == :delete opposite_op_code = :add @@ -36,7 +36,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff inline << generate_inline_html(diff) i += 1 else @@ -73,7 +77,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff deleted, inserted = generate_side_by_side_html(diff) left << deleted right << inserted @@ -109,9 +117,13 @@ class DiscourseDiff end if i + 1 < @line_by_line_diff.size && @line_by_line_diff[i + 1][1] == opposite_op_code - before_tokens, after_tokens = tokenize_markdown(@line_by_line_diff[first][0]), tokenize_markdown(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_markdown(@line_by_line_diff[first][0]), + tokenize_markdown(@line_by_line_diff[second][0]) if (before_tokens.size - after_tokens.size).abs > MAX_DIFFERENCE - before_tokens, after_tokens = tokenize_line(@line_by_line_diff[first][0]), tokenize_line(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_line(@line_by_line_diff[first][0]), + tokenize_line(@line_by_line_diff[second][0]) end diff = ONPDiff.new(before_tokens, after_tokens).short_diff deleted, inserted = generate_side_by_side_markdown(diff) @@ -178,7 +190,7 @@ class DiscourseDiff def add_class_or_wrap_in_tags(html_or_text, klass) result = html_or_text.dup index_of_next_chevron = result.index(">") - if result.size > 0 && result[0] == '<' && index_of_next_chevron + if result.size > 0 && result[0] == "<" && index_of_next_chevron index_of_class = result.index("class=") if index_of_class.nil? || index_of_class > index_of_next_chevron # we do not have a class for the current tag @@ -202,9 +214,12 @@ class DiscourseDiff inline = [] diff.each do |d| case d[1] - when :common then inline << d[0] - when :delete then inline << add_class_or_wrap_in_tags(d[0], "del") - when :add then inline << add_class_or_wrap_in_tags(d[0], "ins") + when :common + inline << d[0] + when :delete + inline << add_class_or_wrap_in_tags(d[0], "del") + when :add + inline << add_class_or_wrap_in_tags(d[0], "ins") end end inline @@ -217,8 +232,10 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << add_class_or_wrap_in_tags(d[0], "del") - when :add then inserted << add_class_or_wrap_in_tags(d[0], "ins") + when :delete + deleted << add_class_or_wrap_in_tags(d[0], "del") + when :add + inserted << add_class_or_wrap_in_tags(d[0], "ins") end end [deleted, inserted] @@ -231,15 +248,16 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << "#{d[0]}" - when :add then inserted << "#{d[0]}" + when :delete + deleted << "#{d[0]}" + when :add + inserted << "#{d[0]}" end end [deleted, inserted] end class HtmlTokenizer < Nokogiri::XML::SAX::Document - attr_accessor :tokens def initialize @@ -253,23 +271,21 @@ class DiscourseDiff me.tokens end - USELESS_TAGS = %w{html body} + USELESS_TAGS = %w[html body] def start_element(name, attributes = []) return if USELESS_TAGS.include?(name) - attrs = attributes.map { |a| " #{a[0]}=\"#{CGI::escapeHTML(a[1])}\"" }.join + attrs = attributes.map { |a| " #{a[0]}=\"#{CGI.escapeHTML(a[1])}\"" }.join @tokens << "<#{name}#{attrs}>" end - AUTOCLOSING_TAGS = %w{area base br col embed hr img input meta} + AUTOCLOSING_TAGS = %w[area base br col embed hr img input meta] def end_element(name) return if USELESS_TAGS.include?(name) || AUTOCLOSING_TAGS.include?(name) @tokens << "" end def characters(string) - @tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI::escapeHTML(x) } + @tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI.escapeHTML(x) } end - end - end diff --git a/lib/discourse_event.rb b/lib/discourse_event.rb index c2db7d1002..43c8548765 100644 --- a/lib/discourse_event.rb +++ b/lib/discourse_event.rb @@ -3,21 +3,23 @@ # This is meant to be used by plugins to trigger and listen to events # So we can execute code when things happen. class DiscourseEvent - # Defaults to a hash where default values are empty sets. def self.events @events ||= Hash.new { |hash, key| hash[key] = Set.new } end - def self.trigger(event_name, *params) - events[event_name].each do |event| - event.call(*params) - end + def self.trigger(event_name, *args, **kwargs) + events[event_name].each { |event| event.call(*args, **kwargs) } end def self.on(event_name, &block) if event_name == :site_setting_saved - Discourse.deprecate("The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", since: "2.3.0beta8", drop_from: "2.4", raise_error: true) + Discourse.deprecate( + "The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", + since: "2.3.0beta8", + drop_from: "2.4", + raise_error: true, + ) end events[event_name] << block end diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 31d4e8a2f0..4b5b36d481 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true module DiscourseHub - STATS_FETCHED_AT_KEY = "stats_fetched_at" def self.version_check_payload - default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch }) + default_payload = { installed_version: Discourse::VERSION::STRING }.merge!( + Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch }, + ) default_payload.merge!(get_payload) end def self.discourse_version_check - get('/version_check', version_check_payload) + get("/version_check", version_check_payload) end def self.stats_fetched_at=(time_with_zone) @@ -18,7 +19,11 @@ module DiscourseHub end def self.get_payload - SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago ? About.fetch_cached_stats.symbolize_keys : {} + if SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago + About.fetch_cached_stats.symbolize_keys + else + {} + end end def self.get(rel_url, params = {}) @@ -40,27 +45,39 @@ module DiscourseHub def self.singular_action(action, rel_url, params = {}) connect_opts = connect_opts(params) - JSON.parse(Excon.public_send(action, - "#{hub_base_url}#{rel_url}", - { - headers: { 'Referer' => referer, 'Accept' => accepts.join(', ') }, - query: params, - omit_default_port: true - }.merge(connect_opts) - ).body) + JSON.parse( + Excon.public_send( + action, + "#{hub_base_url}#{rel_url}", + { + headers: { + "Referer" => referer, + "Accept" => accepts.join(", "), + }, + query: params, + omit_default_port: true, + }.merge(connect_opts), + ).body, + ) end def self.collection_action(action, rel_url, params = {}) connect_opts = connect_opts(params) - response = Excon.public_send(action, - "#{hub_base_url}#{rel_url}", - { - body: JSON[params], - headers: { 'Referer' => referer, 'Accept' => accepts.join(', '), "Content-Type" => "application/json" }, - omit_default_port: true - }.merge(connect_opts) - ) + response = + Excon.public_send( + action, + "#{hub_base_url}#{rel_url}", + { + body: JSON[params], + headers: { + "Referer" => referer, + "Accept" => accepts.join(", "), + "Content-Type" => "application/json", + }, + omit_default_port: true, + }.merge(connect_opts), + ) if (status = response.status) != 200 Rails.logger.warn(response_status_log_message(rel_url, status)) @@ -87,14 +104,14 @@ module DiscourseHub def self.hub_base_url if Rails.env.production? - ENV['HUB_BASE_URL'] || 'https://api.discourse.org/api' + ENV["HUB_BASE_URL"] || "https://api.discourse.org/api" else - ENV['HUB_BASE_URL'] || 'http://local.hub:3000/api' + ENV["HUB_BASE_URL"] || "http://local.hub:3000/api" end end def self.accepts - ['application/json', 'application/vnd.discoursehub.v1'] + %w[application/json application/vnd.discoursehub.v1] end def self.referer @@ -105,5 +122,4 @@ module DiscourseHub t = Discourse.redis.get(STATS_FETCHED_AT_KEY) t ? Time.zone.at(t.to_i) : 1.year.ago end - end diff --git a/lib/discourse_ip_info.rb b/lib/discourse_ip_info.rb index 7f7116c47d..2949f5858c 100644 --- a/lib/discourse_ip_info.rb +++ b/lib/discourse_ip_info.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'maxminddb' -require 'resolv' +require "maxminddb" +require "resolv" class DiscourseIpInfo include Singleton @@ -11,13 +11,13 @@ class DiscourseIpInfo end def open_db(path) - @loc_mmdb = mmdb_load(File.join(path, 'GeoLite2-City.mmdb')) - @asn_mmdb = mmdb_load(File.join(path, 'GeoLite2-ASN.mmdb')) + @loc_mmdb = mmdb_load(File.join(path, "GeoLite2-City.mmdb")) + @asn_mmdb = mmdb_load(File.join(path, "GeoLite2-ASN.mmdb")) @cache = LruRedux::ThreadSafeCache.new(2000) end def self.path - @path ||= File.join(Rails.root, 'vendor', 'data') + @path ||= File.join(Rails.root, "vendor", "data") end def self.mmdb_path(name) @@ -25,7 +25,6 @@ class DiscourseIpInfo end def self.mmdb_download(name) - if GlobalSetting.maxmind_license_key.blank? STDERR.puts "MaxMind IP database updates require a license" STDERR.puts "Please set DISCOURSE_MAXMIND_LICENSE_KEY to one you generated at https://www.maxmind.com" @@ -34,41 +33,29 @@ class DiscourseIpInfo FileUtils.mkdir_p(path) - url = "https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz" + url = + "https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz" - gz_file = FileHelper.download( - url, - max_file_size: 100.megabytes, - tmp_file_name: "#{name}.gz", - validate_uri: false, - follow_redirect: false - ) + gz_file = + FileHelper.download( + url, + max_file_size: 100.megabytes, + tmp_file_name: "#{name}.gz", + validate_uri: false, + follow_redirect: false, + ) filename = File.basename(gz_file.path) dir = "#{Dir.tmpdir}/#{SecureRandom.hex}" - Discourse::Utils.execute_command( - "mkdir", "-p", dir - ) + Discourse::Utils.execute_command("mkdir", "-p", dir) - Discourse::Utils.execute_command( - "cp", - gz_file.path, - "#{dir}/#{filename}" - ) + Discourse::Utils.execute_command("cp", gz_file.path, "#{dir}/#{filename}") - Discourse::Utils.execute_command( - "tar", - "-xzvf", - "#{dir}/#{filename}", - chdir: dir - ) - - Dir["#{dir}/**/*.mmdb"].each do |f| - FileUtils.mv(f, mmdb_path(name)) - end + Discourse::Utils.execute_command("tar", "-xzvf", "#{dir}/#{filename}", chdir: dir) + Dir["#{dir}/**/*.mmdb"].each { |f| FileUtils.mv(f, mmdb_path(name)) } ensure FileUtils.rm_r(dir, force: true) if dir gz_file&.close! @@ -96,7 +83,8 @@ class DiscourseIpInfo if result&.found? ret[:country] = result.country.name(locale) || result.country.name ret[:country_code] = result.country.iso_code - ret[:region] = result.subdivisions.most_specific.name(locale) || result.subdivisions.most_specific.name + ret[:region] = result.subdivisions.most_specific.name(locale) || + result.subdivisions.most_specific.name ret[:city] = result.city.name(locale) || result.city.name ret[:latitude] = result.location.latitude ret[:longitude] = result.location.longitude @@ -104,13 +92,18 @@ class DiscourseIpInfo # used by plugins or API to locate users more accurately ret[:geoname_ids] = [ - result.continent.geoname_id, result.country.geoname_id, result.city.geoname_id, - *result.subdivisions.map(&:geoname_id) + result.continent.geoname_id, + result.country.geoname_id, + result.city.geoname_id, + *result.subdivisions.map(&:geoname_id), ] ret[:geoname_ids].compact! end rescue => e - Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.") + Discourse.warn_exception( + e, + message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.", + ) end end @@ -123,7 +116,10 @@ class DiscourseIpInfo ret[:organization] = result["autonomous_system_organization"] end rescue => e - Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.") + Discourse.warn_exception( + e, + message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.", + ) end end @@ -142,10 +138,13 @@ class DiscourseIpInfo def get(ip, locale: :en, resolve_hostname: false) ip = ip.to_s - locale = locale.to_s.sub('_', '-') + locale = locale.to_s.sub("_", "-") - @cache["#{ip}-#{locale}-#{resolve_hostname}"] ||= - lookup(ip, locale: locale, resolve_hostname: resolve_hostname) + @cache["#{ip}-#{locale}-#{resolve_hostname}"] ||= lookup( + ip, + locale: locale, + resolve_hostname: resolve_hostname, + ) end def self.open_db(path) diff --git a/lib/discourse_js_processor.rb b/lib/discourse_js_processor.rb index 11f1ee2767..ae167ffe57 100644 --- a/lib/discourse_js_processor.rb +++ b/lib/discourse_js_processor.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true -require 'execjs' -require 'mini_racer' +require "execjs" +require "mini_racer" class DiscourseJsProcessor - class TranspileError < StandardError; end + class TranspileError < StandardError + end DISCOURSE_COMMON_BABEL_PLUGINS = [ - 'proposal-optional-chaining', - ['proposal-decorators', { legacy: true } ], - 'transform-template-literals', - 'proposal-class-properties', - 'proposal-class-static-block', - 'proposal-private-property-in-object', - 'proposal-private-methods', - 'proposal-numeric-separator', - 'proposal-logical-assignment-operators', - 'proposal-nullish-coalescing-operator', - 'proposal-json-strings', - 'proposal-optional-catch-binding', - 'transform-parameters', - 'proposal-async-generator-functions', - 'proposal-object-rest-spread', - 'proposal-export-namespace-from', + "proposal-optional-chaining", + ["proposal-decorators", { legacy: true }], + "transform-template-literals", + "proposal-class-properties", + "proposal-class-static-block", + "proposal-private-property-in-object", + "proposal-private-methods", + "proposal-numeric-separator", + "proposal-logical-assignment-operators", + "proposal-nullish-coalescing-operator", + "proposal-json-strings", + "proposal-optional-catch-binding", + "transform-parameters", + "proposal-async-generator-functions", + "proposal-object-rest-spread", + "proposal-export-namespace-from", ] def self.plugin_transpile_paths @@ -33,22 +34,22 @@ class DiscourseJsProcessor end def self.call(input) - root_path = input[:load_path] || '' - logical_path = (input[:filename] || '').sub(root_path, '').gsub(/\.(js|es6).*$/, '').sub(/^\//, '') + root_path = input[:load_path] || "" + logical_path = + (input[:filename] || "").sub(root_path, "").gsub(/\.(js|es6).*$/, "").sub(%r{^/}, "") data = input[:data] - if should_transpile?(input[:filename]) - data = transpile(data, root_path, logical_path) - end + data = transpile(data, root_path, logical_path) if should_transpile?(input[:filename]) # add sourceURL until we can do proper source maps if !Rails.env.production? && !ember_cli?(input[:filename]) - plugin_name = root_path[/\/plugins\/([\w-]+)\/assets/, 1] - source_url = if plugin_name - "plugins/#{plugin_name}/assets/javascripts/#{logical_path}" - else - logical_path - end + plugin_name = root_path[%r{/plugins/([\w-]+)/assets}, 1] + source_url = + if plugin_name + "plugins/#{plugin_name}/assets/javascripts/#{logical_path}" + else + logical_path + end data = "eval(#{data.inspect} + \"\\n//# sourceURL=#{source_url}\");\n" end @@ -62,7 +63,7 @@ class DiscourseJsProcessor end def self.should_transpile?(filename) - filename ||= '' + filename ||= "" # skip ember cli return false if ember_cli?(filename) @@ -73,7 +74,7 @@ class DiscourseJsProcessor # For .js check the path... return false unless filename.end_with?(".js") || filename.end_with?(".js.erb") - relative_path = filename.sub(Rails.root.to_s, '').sub(/^\/*/, '') + relative_path = filename.sub(Rails.root.to_s, "").sub(%r{^/*}, "") js_root = "app/assets/javascripts" test_root = "test/javascripts" @@ -81,26 +82,27 @@ class DiscourseJsProcessor return false if relative_path.start_with?("#{js_root}/locales/") return false if relative_path.start_with?("#{js_root}/plugins/") - return true if %w( - start-discourse - onpopstate-handler - google-tag-manager - google-universal-analytics-v3 - google-universal-analytics-v4 - activate-account - auto-redirect - embed-application - app-boot - ).any? { |f| relative_path == "#{js_root}/#{f}.js" } + if %w[ + start-discourse + onpopstate-handler + google-tag-manager + google-universal-analytics-v3 + google-universal-analytics-v4 + activate-account + auto-redirect + embed-application + app-boot + ].any? { |f| relative_path == "#{js_root}/#{f}.js" } + return true + end return true if plugin_transpile_paths.any? { |prefix| relative_path.start_with?(prefix) } - !!(relative_path =~ /^#{js_root}\/[^\/]+\// || - relative_path =~ /^#{test_root}\/[^\/]+\//) + !!(relative_path =~ %r{^#{js_root}/[^/]+/} || relative_path =~ %r{^#{test_root}/[^/]+/}) end def self.skip_module?(data) - !!(data.present? && data =~ /^\/\/ discourse-skip-module$/) + !!(data.present? && data =~ %r{^// discourse-skip-module$}) end class Transpiler @@ -113,19 +115,17 @@ class DiscourseJsProcessor def self.load_file_in_context(ctx, path, wrap_in_module: nil) contents = File.read("#{Rails.root}/app/assets/javascripts/#{path}") - if wrap_in_module - contents = <<~JS + contents = <<~JS if wrap_in_module define(#{wrap_in_module.to_json}, ["exports", "require", "module"], function(exports, require, module){ #{contents} }); JS - end ctx.eval(contents, filename: path) end def self.create_new_context # timeout any eval that takes longer than 15 seconds - ctx = MiniRacer::Context.new(timeout: 15000, ensure_gc_after_idle: 2000) + ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000) # General shims ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) }) @@ -158,10 +158,26 @@ class DiscourseJsProcessor # Template Compiler load_file_in_context(ctx, "node_modules/ember-source/dist/ember-template-compiler.js") - load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", wrap_in_module: "babel-plugin-ember-template-compilation/index") - load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser") - load_file_in_context(ctx, "node_modules/babel-import-util/src/index.js", wrap_in_module: "babel-import-util") - load_file_in_context(ctx, "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", wrap_in_module: "colocated-babel-plugin") + load_file_in_context( + ctx, + "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", + wrap_in_module: "babel-plugin-ember-template-compilation/index", + ) + load_file_in_context( + ctx, + "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", + wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser", + ) + load_file_in_context( + ctx, + "node_modules/babel-import-util/src/index.js", + wrap_in_module: "babel-import-util", + ) + load_file_in_context( + ctx, + "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", + wrap_in_module: "colocated-babel-plugin", + ) # Widget HBS compiler widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js") @@ -170,32 +186,44 @@ class DiscourseJsProcessor #{widget_hbs_compiler_source} }); JS - widget_hbs_compiler_transpiled = ctx.call("rawBabelTransform", widget_hbs_compiler_source, { - ast: false, - moduleId: 'widget-hbs-compiler', - plugins: DISCOURSE_COMMON_BABEL_PLUGINS - }) + widget_hbs_compiler_transpiled = + ctx.call( + "rawBabelTransform", + widget_hbs_compiler_source, + { ast: false, moduleId: "widget-hbs-compiler", plugins: DISCOURSE_COMMON_BABEL_PLUGINS }, + ) ctx.eval(widget_hbs_compiler_transpiled, filename: "widget-hbs-compiler.js") # Raw HBS compiler - load_file_in_context(ctx, "node_modules/handlebars/dist/handlebars.js", wrap_in_module: "handlebars") - - raw_hbs_transpiled = ctx.call( - "rawBabelTransform", - File.read("#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js"), - { - ast: false, - moduleId: "raw-handlebars", - plugins: [ - ['transform-modules-amd', { noInterop: true }], - *DISCOURSE_COMMON_BABEL_PLUGINS - ] - } + load_file_in_context( + ctx, + "node_modules/handlebars/dist/handlebars.js", + wrap_in_module: "handlebars", ) + + raw_hbs_transpiled = + ctx.call( + "rawBabelTransform", + File.read( + "#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js", + ), + { + ast: false, + moduleId: "raw-handlebars", + plugins: [ + ["transform-modules-amd", { noInterop: true }], + *DISCOURSE_COMMON_BABEL_PLUGINS, + ], + }, + ) ctx.eval(raw_hbs_transpiled, filename: "raw-handlebars.js") # Theme template AST transformation plugins - load_file_in_context(ctx, "discourse-js-processor.js", wrap_in_module: "discourse-js-processor") + load_file_in_context( + ctx, + "discourse-js-processor.js", + wrap_in_module: "discourse-js-processor", + ) # Make interfaces available via `v8.call` ctx.eval <<~JS @@ -262,10 +290,10 @@ class DiscourseJsProcessor { skip_module: @skip_module, moduleId: module_name(root_path, logical_path), - filename: logical_path || 'unknown', + filename: logical_path || "unknown", themeId: theme_id, - commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS - } + commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS, + }, ) end @@ -274,15 +302,16 @@ class DiscourseJsProcessor root_base = File.basename(Rails.root) # If the resource is a plugin, use the plugin name as a prefix - if root_path =~ /(.*\/#{root_base}\/plugins\/[^\/]+)\// + if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/} plugin_path = "#{Regexp.last_match[1]}/plugin.rb" plugin = Discourse.plugins.find { |p| p.path == plugin_path } - path = "discourse/plugins/#{plugin.name}/#{logical_path.sub(/javascripts\//, '')}" if plugin + path = + "discourse/plugins/#{plugin.name}/#{logical_path.sub(%r{javascripts/}, "")}" if plugin end # We need to strip the app subdirectory to replicate how ember-cli works. - path || logical_path&.gsub('app/', '')&.gsub('addon/', '')&.gsub('admin/addon', 'admin') + path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin") end def compile_raw_template(source, theme_id: nil) diff --git a/lib/discourse_logstash_logger.rb b/lib/discourse_logstash_logger.rb index ee1b739133..15225d3384 100644 --- a/lib/discourse_logstash_logger.rb +++ b/lib/discourse_logstash_logger.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true -require 'logstash-logger' +require "logstash-logger" class DiscourseLogstashLogger def self.logger(uri:, type:) # See Discourse.os_hostname - hostname = begin - require 'socket' - Socket.gethostname - rescue => e - `hostname`.chomp - end + hostname = + begin + require "socket" + Socket.gethostname + rescue => e + `hostname`.chomp + end LogStashLogger.new( uri: uri, sync: true, - customize_event: ->(event) { - event['hostname'] = hostname - event['severity_name'] = event['severity'] - event['severity'] = Object.const_get("Logger::Severity::#{event['severity']}") - event['type'] = type - event['pid'] = Process.pid - }, + customize_event: ->(event) do + event["hostname"] = hostname + event["severity_name"] = event["severity"] + event["severity"] = Object.const_get("Logger::Severity::#{event["severity"]}") + event["type"] = type + event["pid"] = Process.pid + end, ) end end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index b6bcbb9821..9f0fd9e608 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -4,7 +4,6 @@ # A class that handles interaction between a plugin and the Discourse App. # class DiscoursePluginRegistry - # Plugins often need to be able to register additional handlers, data, or # classes that will be used by core classes. This should be used if you # need to control which type the registry is, and if it doesn't need to @@ -24,9 +23,7 @@ class DiscoursePluginRegistry instance_variable_set(:"@#{register_name}", type.new) end - define_method(register_name) do - self.class.public_send(register_name) - end + define_method(register_name) { self.class.public_send(register_name) } end # Plugins often need to add values to a list, and we need to filter those @@ -45,10 +42,7 @@ class DiscoursePluginRegistry define_singleton_method(register_name) do unfiltered = public_send(:"_raw_#{register_name}") - unfiltered - .filter { |v| v[:plugin].enabled? } - .map { |v| v[:value] } - .uniq + unfiltered.filter { |v| v[:plugin].enabled? }.map { |v| v[:value] }.uniq end define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin| @@ -158,9 +152,7 @@ class DiscoursePluginRegistry next if each_options[:admin] end - Dir.glob("#{root}/**/*.#{ext}") do |f| - yield f - end + Dir.glob("#{root}/**/*.#{ext}") { |f| yield f } end end @@ -227,7 +219,7 @@ class DiscoursePluginRegistry def self.seed_paths result = SeedFu.fixture_paths.dup - unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1" + unless Rails.env.test? && ENV["LOAD_PLUGINS"] != "1" seed_path_builders.each { |b| result += b.call } end result.uniq @@ -239,7 +231,7 @@ class DiscoursePluginRegistry VENDORED_CORE_PRETTY_TEXT_MAP = { "moment.js" => "vendor/assets/javascripts/moment.js", - "moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js" + "moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js", } def self.core_asset_for_name(name) asset = VENDORED_CORE_PRETTY_TEXT_MAP[name] @@ -248,16 +240,12 @@ class DiscoursePluginRegistry end def self.reset! - @@register_names.each do |name| - instance_variable_set(:"@#{name}", nil) - end + @@register_names.each { |name| instance_variable_set(:"@#{name}", nil) } end def self.reset_register!(register_name) found_register = @@register_names.detect { |name| name == register_name } - if found_register - instance_variable_set(:"@#{found_register}", nil) - end + instance_variable_set(:"@#{found_register}", nil) if found_register end end diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index 4c661cc9af..e47e9734c5 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -46,15 +46,103 @@ class DiscourseRedis end # Proxy key methods through, but prefix the keys with the namespace - [:append, :blpop, :brpop, :brpoplpush, :decr, :decrby, :expire, :expireat, :get, :getbit, :getrange, :getset, - :hdel, :hexists, :hget, :hgetall, :hincrby, :hincrbyfloat, :hkeys, :hlen, :hmget, :hmset, :hset, :hsetnx, :hvals, :incr, - :incrby, :incrbyfloat, :lindex, :linsert, :llen, :lpop, :lpush, :lpushx, :lrange, :lrem, :lset, :ltrim, - :mapped_hmset, :mapped_hmget, :mapped_mget, :mapped_mset, :mapped_msetnx, :move, :mset, - :msetnx, :persist, :pexpire, :pexpireat, :psetex, :pttl, :rename, :renamenx, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :sadd?, :scard, - :sdiff, :set, :setbit, :setex, :setnx, :setrange, :sinter, :sismember, :smembers, :sort, :spop, :srandmember, :srem, :srem?, :strlen, - :sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank, - :zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore, - :dump, :restore].each do |m| + %i[ + append + blpop + brpop + brpoplpush + decr + decrby + expire + expireat + get + getbit + getrange + getset + hdel + hexists + hget + hgetall + hincrby + hincrbyfloat + hkeys + hlen + hmget + hmset + hset + hsetnx + hvals + incr + incrby + incrbyfloat + lindex + linsert + llen + lpop + lpush + lpushx + lrange + lrem + lset + ltrim + mapped_hmset + mapped_hmget + mapped_mget + mapped_mset + mapped_msetnx + move + mset + msetnx + persist + pexpire + pexpireat + psetex + pttl + rename + renamenx + rpop + rpoplpush + rpush + rpushx + sadd + sadd? + scard + sdiff + set + setbit + setex + setnx + setrange + sinter + sismember + smembers + sort + spop + srandmember + srem + srem? + strlen + sunion + ttl + type + watch + zadd + zcard + zcount + zincrby + zrange + zrangebyscore + zrank + zrem + zremrangebyrank + zremrangebyscore + zrevrange + zrevrangebyscore + zrevrank + zrangebyscore + dump + restore + ].each do |m| define_method m do |*args, **kwargs| args[0] = "#{namespace}:#{args[0]}" if @namespace DiscourseRedis.ignore_readonly { @redis.public_send(m, *args, **kwargs) } @@ -72,7 +160,7 @@ class DiscourseRedis end def mget(*args) - args.map! { |a| "#{namespace}:#{a}" } if @namespace + args.map! { |a| "#{namespace}:#{a}" } if @namespace DiscourseRedis.ignore_readonly { @redis.mget(*args) } end @@ -86,14 +174,13 @@ class DiscourseRedis def scan_each(options = {}, &block) DiscourseRedis.ignore_readonly do - match = options[:match].presence || '*' + match = options[:match].presence || "*" - options[:match] = - if @namespace - "#{namespace}:#{match}" - else - match - end + options[:match] = if @namespace + "#{namespace}:#{match}" + else + match + end if block @redis.scan_each(**options) do |key| @@ -101,17 +188,19 @@ class DiscourseRedis block.call(key) end else - @redis.scan_each(**options).map do |key| - key = remove_namespace(key) if @namespace - key - end + @redis + .scan_each(**options) + .map do |key| + key = remove_namespace(key) if @namespace + key + end end end end def keys(pattern = nil) DiscourseRedis.ignore_readonly do - pattern = pattern || '*' + pattern = pattern || "*" pattern = "#{namespace}:#{pattern}" if @namespace keys = @redis.keys(pattern) @@ -125,9 +214,7 @@ class DiscourseRedis end def delete_prefixed(prefix) - DiscourseRedis.ignore_readonly do - keys("#{prefix}*").each { |k| Discourse.redis.del(k) } - end + DiscourseRedis.ignore_readonly { keys("#{prefix}*").each { |k| Discourse.redis.del(k) } } end def reconnect diff --git a/lib/discourse_sourcemapping_url_processor.rb b/lib/discourse_sourcemapping_url_processor.rb index 7b6ae82d56..29095863c5 100644 --- a/lib/discourse_sourcemapping_url_processor.rb +++ b/lib/discourse_sourcemapping_url_processor.rb @@ -6,7 +6,8 @@ class DiscourseSourcemappingUrlProcessor < Sprockets::Rails::SourcemappingUrlProcessor def self.sourcemap_asset_path(sourcemap_logical_path, context:) result = super(sourcemap_logical_path, context: context) - if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) || sourcemap_logical_path.start_with?("plugins/") + if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) || + sourcemap_logical_path.start_with?("plugins/") # If the original sourcemap reference is relative, keep it relative result = File.basename(result) end diff --git a/lib/discourse_tagging.rb b/lib/discourse_tagging.rb index e68b9032cd..8a8c2d7f87 100644 --- a/lib/discourse_tagging.rb +++ b/lib/discourse_tagging.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module DiscourseTagging - TAGS_FIELD_NAME ||= "tags" TAGS_FILTER_REGEXP ||= /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<> TAGS_STAFF_CACHE_KEY ||= "staff_tag_names" @@ -22,9 +21,11 @@ module DiscourseTagging tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || [] if !tag_names.empty? - Tag.where_name(tag_names).joins(:target_tag).includes(:target_tag).each do |tag| - tag_names[tag_names.index(tag.name)] = tag.target_tag.name - end + Tag + .where_name(tag_names) + .joins(:target_tag) + .includes(:target_tag) + .each { |tag| tag_names[tag_names.index(tag.name)] = tag.target_tag.name } end # tags currently on the topic @@ -45,9 +46,7 @@ module DiscourseTagging # If this user has explicit permission to use certain tags, # we need to ensure those tags are removed from the list of # restricted tags - if permitted_tags.present? - readonly_tags = readonly_tags - permitted_tags - end + readonly_tags = readonly_tags - permitted_tags if permitted_tags.present? # visible, but not usable, tags this user is trying to use disallowed_tags = new_tag_names & readonly_tags @@ -55,13 +54,19 @@ module DiscourseTagging disallowed_tags += new_tag_names & hidden_tags if disallowed_tags.present? - topic.errors.add(:base, I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" "))) + topic.errors.add( + :base, + I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" ")), + ) return false end removed_readonly_tags = removed_tag_names & readonly_tags if removed_readonly_tags.present? - topic.errors.add(:base, I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" "))) + topic.errors.add( + :base, + I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" ")), + ) return false end @@ -73,50 +78,61 @@ module DiscourseTagging if tag_names.present? # guardian is explicitly nil cause we don't want to strip all # staff tags that already passed validation - tags = filter_allowed_tags( - nil, # guardian - for_topic: true, - category: category, - selected_tags: tag_names, - only_tag_names: tag_names - ) + tags = + filter_allowed_tags( + nil, # guardian + for_topic: true, + category: category, + selected_tags: tag_names, + only_tag_names: tag_names, + ) # keep existent tags that current user cannot use tags += Tag.where(name: old_tag_names & tag_names) tags = Tag.where(id: tags.map(&:id)).all.to_a if tags.size > 0 - if tags.size < tag_names.size && (category.nil? || category.allow_global_tags || (category.tags.count == 0 && category.tag_groups.count == 0)) + if tags.size < tag_names.size && + ( + category.nil? || category.allow_global_tags || + (category.tags.count == 0 && category.tag_groups.count == 0) + ) tag_names.each do |name| - unless Tag.where_name(name).exists? - tags << Tag.create(name: name) - end + tags << Tag.create(name: name) unless Tag.where_name(name).exists? end end # add missing mandatory parent tags tag_ids = tags.map(&:id) - parent_tags_map = DB.query(" + parent_tags_map = + DB + .query( + " SELECT tgm.tag_id, tg.parent_tag_id FROM tag_groups tg INNER JOIN tag_group_memberships tgm ON tgm.tag_group_id = tg.id WHERE tg.parent_tag_id IS NOT NULL AND tgm.tag_id IN (?) - ", tag_ids).inject({}) do |h, v| - h[v.tag_id] ||= [] - h[v.tag_id] << v.parent_tag_id - h - end + ", + tag_ids, + ) + .inject({}) do |h, v| + h[v.tag_id] ||= [] + h[v.tag_id] << v.parent_tag_id + h + end - missing_parent_tag_ids = parent_tags_map.map do |_, parent_tag_ids| - (tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil - end.compact.uniq + missing_parent_tag_ids = + parent_tags_map + .map do |_, parent_tag_ids| + (tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil + end + .compact + .uniq - unless missing_parent_tag_ids.empty? - tags = tags + Tag.where(id: missing_parent_tag_ids).all - end + tags = tags + Tag.where(id: missing_parent_tag_ids).all unless missing_parent_tag_ids.empty? return false unless validate_min_required_tags_for_category(guardian, topic, category, tags) return false unless validate_required_tags_from_group(guardian, topic, category, tags) @@ -137,7 +153,9 @@ module DiscourseTagging DiscourseEvent.trigger( :topic_tags_changed, - topic, old_tag_names: old_tag_names, new_tag_names: topic.tags.map(&:name) + topic, + old_tag_names: old_tag_names, + new_tag_names: topic.tags.map(&:name), ) return true @@ -146,12 +164,12 @@ module DiscourseTagging end def self.validate_min_required_tags_for_category(guardian, model, category, tags = []) - if !guardian.is_staff? && - category && - category.minimum_required_tags > 0 && - tags.length < category.minimum_required_tags - - model.errors.add(:base, I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags)) + if !guardian.is_staff? && category && category.minimum_required_tags > 0 && + tags.length < category.minimum_required_tags + model.errors.add( + :base, + I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags), + ) false else true @@ -164,17 +182,17 @@ module DiscourseTagging success = true category.category_required_tag_groups.each do |crtg| if tags.length < crtg.min_count || - crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count - + crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count success = false - model.errors.add(:base, + model.errors.add( + :base, I18n.t( "tags.required_tags_from_group", count: crtg.min_count, tag_group_name: crtg.tag_group.name, - tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", ") - ) + tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", "), + ), ) end end @@ -189,24 +207,28 @@ module DiscourseTagging tags_restricted_to_categories = Hash.new { |h, k| h[k] = Set.new } query = Tag.where(name: tags) - query.joins(tag_groups: :categories).pluck(:name, 'categories.id').each do |(tag, cat_id)| - tags_restricted_to_categories[tag] << cat_id - end - query.joins(:categories).pluck(:name, 'categories.id').each do |(tag, cat_id)| - tags_restricted_to_categories[tag] << cat_id - end + query + .joins(tag_groups: :categories) + .pluck(:name, "categories.id") + .each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id } + query + .joins(:categories) + .pluck(:name, "categories.id") + .each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id } - unallowed_tags = tags_restricted_to_categories.keys.select do |tag| - !tags_restricted_to_categories[tag].include?(category.id) - end + unallowed_tags = + tags_restricted_to_categories.keys.select do |tag| + !tags_restricted_to_categories[tag].include?(category.id) + end if unallowed_tags.present? - msg = I18n.t( - "tags.forbidden.restricted_tags_cannot_be_used_in_category", - count: unallowed_tags.size, - tags: unallowed_tags.sort.join(", "), - category: category.name - ) + msg = + I18n.t( + "tags.forbidden.restricted_tags_cannot_be_used_in_category", + count: unallowed_tags.size, + tags: unallowed_tags.sort.join(", "), + category: category.name, + ) model.errors.add(:base, msg) return false end @@ -214,12 +236,13 @@ module DiscourseTagging if !category.allow_global_tags && category.has_restricted_tags? unrestricted_tags = tags - tags_restricted_to_categories.keys if unrestricted_tags.present? - msg = I18n.t( - "tags.forbidden.category_does_not_allow_tags", - count: unrestricted_tags.size, - tags: unrestricted_tags.sort.join(", "), - category: category.name - ) + msg = + I18n.t( + "tags.forbidden.category_does_not_allow_tags", + count: unrestricted_tags.size, + tags: unrestricted_tags.sort.join(", "), + category: category.name, + ) model.errors.add(:base, msg) return false end @@ -280,7 +303,8 @@ module DiscourseTagging def self.filter_allowed_tags(guardian, opts = {}) selected_tag_ids = opts[:selected_tags] ? Tag.where_name(opts[:selected_tags]).pluck(:id) : [] category = opts[:category] - category_has_restricted_tags = category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false + category_has_restricted_tags = + category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false # If guardian is nil, it means the caller doesn't want tags to be filtered # based on guardian rules. Use the same rules as for staff users. @@ -288,9 +312,7 @@ module DiscourseTagging builder_params = {} - unless selected_tag_ids.empty? - builder_params[:selected_tag_ids] = selected_tag_ids - end + builder_params[:selected_tag_ids] = selected_tag_ids unless selected_tag_ids.empty? sql = +"WITH #{TAG_GROUP_RESTRICTIONS_SQL}, #{CATEGORY_RESTRICTIONS_SQL}" if (opts[:for_input] || opts[:for_topic]) && filter_for_non_staff @@ -301,13 +323,14 @@ module DiscourseTagging outer_join = category.nil? || category.allow_global_tags || !category_has_restricted_tags - distinct_clause = if opts[:order_popularity] - "DISTINCT ON (topic_count, name)" - elsif opts[:order_search_results] && opts[:term].present? - "DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)" - else - "" - end + distinct_clause = + if opts[:order_popularity] + "DISTINCT ON (topic_count, name)" + elsif opts[:order_search_results] && opts[:term].present? + "DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)" + else + "" + end sql << <<~SQL SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description, @@ -336,16 +359,20 @@ module DiscourseTagging # parent tag requirements if opts[:for_input] builder.where( - builder_params[:selected_tag_ids] ? - "tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)" : - "tgm_id IS NULL OR parent_tag_id IS NULL" + ( + if builder_params[:selected_tag_ids] + "tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)" + else + "tgm_id IS NULL OR parent_tag_id IS NULL" + end + ), ) end if category && category_has_restricted_tags builder.where( category.allow_global_tags ? "category_id = ? OR category_id IS NULL" : "category_id = ?", - category.id + category.id, ) elsif category || opts[:for_input] || opts[:for_topic] # tags not restricted to any categories @@ -354,7 +381,9 @@ module DiscourseTagging if filter_for_non_staff && (opts[:for_input] || opts[:for_topic]) # exclude staff-only tag groups - builder.where("tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)") + builder.where( + "tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)", + ) end term = opts[:term] @@ -380,7 +409,8 @@ module DiscourseTagging # - and no search term has been included required_tag_ids = nil required_category_tag_group = nil - if opts[:for_input] && category&.category_required_tag_groups.present? && (filter_for_non_staff || term.blank?) + if opts[:for_input] && category&.category_required_tag_groups.present? && + (filter_for_non_staff || term.blank?) category.category_required_tag_groups.each do |crtg| group_tags = crtg.tag_group.tags.pluck(:id) next if (group_tags & selected_tag_ids).size >= crtg.min_count @@ -426,22 +456,18 @@ module DiscourseTagging if !one_tag_per_group_ids.empty? builder.where( "tag_group_id IS NULL OR tag_group_id NOT IN (?) OR id IN (:selected_tag_ids)", - one_tag_per_group_ids + one_tag_per_group_ids, ) end end - if opts[:exclude_synonyms] - builder.where("target_tag_id IS NULL") - end + builder.where("target_tag_id IS NULL") if opts[:exclude_synonyms] if opts[:exclude_has_synonyms] builder.where("id NOT IN (SELECT target_tag_id FROM tags WHERE target_tag_id IS NOT NULL)") end - if opts[:excluded_tag_names]&.any? - builder.where("name NOT IN (?)", opts[:excluded_tag_names]) - end + builder.where("name NOT IN (?)", opts[:excluded_tag_names]) if opts[:excluded_tag_names]&.any? if opts[:limit] if required_tag_ids && term.blank? @@ -465,7 +491,7 @@ module DiscourseTagging if required_category_tag_group context[:required_tag_group] = { name: required_category_tag_group.tag_group.name, - min_count: required_category_tag_group.min_count + min_count: required_category_tag_group.min_count, } end [result, context] @@ -480,21 +506,15 @@ module DiscourseTagging else # Visible tags either have no permissions or have allowable permissions Tag - .where.not( - id: - TagGroupMembership - .joins(tag_group: :tag_group_permissions) - .select(:tag_id) - ) + .where.not(id: TagGroupMembership.joins(tag_group: :tag_group_permissions).select(:tag_id)) .or( - Tag - .where( - id: - TagGroupPermission - .joins(tag_group: :tag_group_memberships) - .where(group_id: permitted_group_ids_query(guardian)) - .select('tag_group_memberships.tag_id'), - ) + Tag.where( + id: + TagGroupPermission + .joins(tag_group: :tag_group_memberships) + .where(group_id: permitted_group_ids_query(guardian)) + .select("tag_group_memberships.tag_id"), + ), ) end end @@ -509,21 +529,18 @@ module DiscourseTagging def self.permitted_group_ids_query(guardian = nil) if guardian&.authenticated? - Group - .from( - Group.sanitize_sql( - ["(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups", Group::AUTO_GROUPS[:everyone]] - ) - ) - .select(:id) + Group.from( + Group.sanitize_sql( + [ + "(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups", + Group::AUTO_GROUPS[:everyone], + ], + ), + ).select(:id) else - Group - .from( - Group.sanitize_sql( - ["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]] - ) - ) - .select(:id) + Group.from( + Group.sanitize_sql(["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]]), + ).select(:id) end end @@ -535,9 +552,11 @@ module DiscourseTagging def self.readonly_tag_names(guardian = nil) return [] if guardian&.is_staff? - query = Tag.joins(tag_groups: :tag_group_permissions) - .where('tag_group_permissions.permission_type = ?', - TagGroupPermission.permission_types[:readonly]) + query = + Tag.joins(tag_groups: :tag_group_permissions).where( + "tag_group_permissions.permission_type = ?", + TagGroupPermission.permission_types[:readonly], + ) query.pluck(:name) end @@ -545,14 +564,12 @@ module DiscourseTagging # explicit permissions to use these tags def self.permitted_tag_names(guardian = nil) query = - Tag - .joins(tag_groups: :tag_group_permissions) - .where( - tag_group_permissions: { - group_id: permitted_group_ids(guardian), - permission_type: TagGroupPermission.permission_types[:full], - }, - ) + Tag.joins(tag_groups: :tag_group_permissions).where( + tag_group_permissions: { + group_id: permitted_group_ids(guardian), + permission_type: TagGroupPermission.permission_types[:full], + }, + ) query.pluck(:name).uniq end @@ -586,15 +603,14 @@ module DiscourseTagging tag = tag.dup tag.downcase! if SiteSetting.force_lowercase_tags tag.strip! - tag.gsub!(/[[:space:]]+/, '-') - tag.gsub!(/[^[:word:][:punct:]]+/, '') - tag.squeeze!('-') - tag.gsub!(TAGS_FILTER_REGEXP, '') + tag.gsub!(/[[:space:]]+/, "-") + tag.gsub!(/[^[:word:][:punct:]]+/, "") + tag.squeeze!("-") + tag.gsub!(TAGS_FILTER_REGEXP, "") tag[0...SiteSetting.max_tag_length] end def self.tags_for_saving(tags_arg, guardian, opts = {}) - return [] unless guardian.can_tag_topics? && tags_arg.present? tag_names = Tag.where_name(tags_arg).pluck(:name) @@ -609,21 +625,23 @@ module DiscourseTagging end def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {}) - tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || [] + tag_names = + DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || + [] if taggable.tags.pluck(:name).sort != tag_names.sort taggable.tags = Tag.where_name(tag_names).all - new_tag_names = taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : [] + new_tag_names = + taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : [] taggable.tags << Tag.where(target_tag_id: taggable.tags.map(&:id)).all - new_tag_names.each do |name| - taggable.tags << Tag.create(name: name) - end + new_tag_names.each { |name| taggable.tags << Tag.create(name: name) } end end # Returns true if all were added successfully, or an Array of the # tags that failed to be added, with errors on each Tag. def self.add_or_create_synonyms_by_name(target_tag, synonym_names) - tag_names = DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || [] + tag_names = + DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || [] tag_names -= [target_tag.name] existing = Tag.where_name(tag_names).all target_tag.synonyms << existing @@ -642,6 +660,6 @@ module DiscourseTagging def self.muted_tags(user) return [] unless user - TagUser.lookup(user, :muted).joins(:tag).pluck('tags.name') + TagUser.lookup(user, :muted).joins(:tag).pluck("tags.name") end end diff --git a/lib/discourse_updates.rb b/lib/discourse_updates.rb index 5fea6c0724..d20870b966 100644 --- a/lib/discourse_updates.rb +++ b/lib/discourse_updates.rb @@ -1,13 +1,11 @@ # frozen_string_literal: true module DiscourseUpdates - class << self - def check_version attrs = { installed_version: Discourse::VERSION::STRING, - installed_sha: (Discourse.git_version == 'unknown' ? nil : Discourse.git_version), + installed_sha: (Discourse.git_version == "unknown" ? nil : Discourse.git_version), installed_describe: Discourse.full_version, git_branch: Discourse.git_branch, updated_at: updated_at, @@ -17,7 +15,7 @@ module DiscourseUpdates attrs.merge!( latest_version: latest_version, critical_updates: critical_updates_available?, - missing_versions_count: missing_versions_count + missing_versions_count: missing_versions_count, ) end @@ -25,19 +23,24 @@ module DiscourseUpdates # replace -commit_count with +commit_count if version_info.installed_describe =~ /-(\d+)-/ - version_info.installed_describe = version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}") + version_info.installed_describe = + version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}") end if SiteSetting.version_checks? is_stale_data = - (version_info.missing_versions_count == 0 && version_info.latest_version != version_info.installed_version) || - (version_info.missing_versions_count != 0 && version_info.latest_version == version_info.installed_version) + ( + version_info.missing_versions_count == 0 && + version_info.latest_version != version_info.installed_version + ) || + ( + version_info.missing_versions_count != 0 && + version_info.latest_version == version_info.installed_version + ) # Handle cases when version check data is old so we report something that makes sense - if version_info.updated_at.nil? || # never performed a version check - last_installed_version != Discourse::VERSION::STRING || # upgraded since the last version check - is_stale_data - + if version_info.updated_at.nil? || last_installed_version != Discourse::VERSION::STRING || # never performed a version check # upgraded since the last version check + is_stale_data Jobs.enqueue(:version_check, all_sites: true) version_info.version_check_pending = true @@ -48,9 +51,8 @@ module DiscourseUpdates end version_info.stale_data = - version_info.version_check_pending || - (updated_at && updated_at < 48.hours.ago) || - is_stale_data + version_info.version_check_pending || (updated_at && updated_at < 48.hours.ago) || + is_stale_data end version_info @@ -82,7 +84,7 @@ module DiscourseUpdates end def critical_updates_available? - (Discourse.redis.get(critical_updates_available_key) || false) == 'true' + (Discourse.redis.get(critical_updates_available_key) || false) == "true" end def critical_updates_available=(arg) @@ -110,7 +112,7 @@ module DiscourseUpdates # store the list in redis version_keys = [] versions[0, 5].each do |v| - key = "#{missing_versions_key_prefix}:#{v['version']}" + key = "#{missing_versions_key_prefix}:#{v["version"]}" Discourse.redis.mapped_hmset key, v version_keys << key end @@ -140,11 +142,21 @@ module DiscourseUpdates end def new_features - entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + entries = + begin + JSON.parse(Discourse.redis.get(new_features_key)) + rescue StandardError + nil + end return nil if entries.nil? entries.select! do |item| - item["discourse_version"].nil? || Discourse.has_needed_version?(current_version, item["discourse_version"]) rescue nil + begin + item["discourse_version"].nil? || + Discourse.has_needed_version?(current_version, item["discourse_version"]) + rescue StandardError + nil + end end entries.sort_by { |item| Time.zone.parse(item["created_at"]).to_i }.reverse @@ -170,7 +182,12 @@ module DiscourseUpdates end def mark_new_features_as_seen(user_id) - entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil + entries = + begin + JSON.parse(Discourse.redis.get(new_features_key)) + rescue StandardError + nil + end return nil if entries.nil? last_seen = entries.max_by { |x| x["created_at"] } Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"]) @@ -204,39 +221,39 @@ module DiscourseUpdates private def last_installed_version_key - 'last_installed_version' + "last_installed_version" end def latest_version_key - 'discourse_latest_version' + "discourse_latest_version" end def critical_updates_available_key - 'critical_updates_available' + "critical_updates_available" end def missing_versions_count_key - 'missing_versions_count' + "missing_versions_count" end def updated_at_key - 'last_version_check_at' + "last_version_check_at" end def missing_versions_list_key - 'missing_versions' + "missing_versions" end def missing_versions_key_prefix - 'missing_version' + "missing_version" end def new_features_endpoint - 'https://meta.discourse.org/new-features.json' + "https://meta.discourse.org/new-features.json" end def new_features_key - 'new_features' + "new_features" end def new_features_last_seen_key(user_id) diff --git a/lib/disk_space.rb b/lib/disk_space.rb index 9903164e3c..c7ee672cc0 100644 --- a/lib/disk_space.rb +++ b/lib/disk_space.rb @@ -18,13 +18,13 @@ class DiskSpace end def self.free(path) - output = Discourse::Utils.execute_command('df', '-Pk', path) + output = Discourse::Utils.execute_command("df", "-Pk", path) size_line = output.split("\n")[1] size_line.split(/\s+/)[3].to_i * 1024 end def self.percent_free(path) - output = Discourse::Utils.execute_command('df', '-P', path) + output = Discourse::Utils.execute_command("df", "-P", path) size_line = output.split("\n")[1] size_line.split(/\s+/)[4].to_i end diff --git a/lib/distributed_cache.rb b/lib/distributed_cache.rb index 89e6058d11..80fa7bf1f8 100644 --- a/lib/distributed_cache.rb +++ b/lib/distributed_cache.rb @@ -1,23 +1,16 @@ # frozen_string_literal: true -require 'message_bus/distributed_cache' +require "message_bus/distributed_cache" class DistributedCache < MessageBus::DistributedCache def initialize(key, manager: nil, namespace: true) - super( - key, - manager: manager, - namespace: namespace, - app_version: Discourse.git_version - ) + super(key, manager: manager, namespace: namespace, app_version: Discourse.git_version) end # Defer setting of the key in the cache for performance critical path to avoid # waiting on MessageBus to publish the message which involves writing to Redis. def defer_set(k, v) - Scheduler::Defer.later("#{@key}_set") do - self[k] = v - end + Scheduler::Defer.later("#{@key}_set") { self[k] = v } end def defer_get_set(k, &block) diff --git a/lib/distributed_mutex.rb b/lib/distributed_mutex.rb index b456244a6d..f7eb3b4e51 100644 --- a/lib/distributed_mutex.rb +++ b/lib/distributed_mutex.rb @@ -31,11 +31,7 @@ class DistributedMutex LUA def self.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk) - self.new( - key, - redis: redis, - validity: validity - ).synchronize(&blk) + self.new(key, redis: redis, validity: validity).synchronize(&blk) end def initialize(key, redis: nil, validity: DEFAULT_VALIDITY) @@ -58,7 +54,9 @@ class DistributedMutex ensure current_time = redis.time[0] if current_time > expire_time - warn("held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs") + warn( + "held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs", + ) end unlocked = UNLOCK_SCRIPT.eval(redis, [prefixed_key], [expire_time.to_s]) diff --git a/lib/edit_rate_limiter.rb b/lib/edit_rate_limiter.rb index d9769f5262..b39bcb5a8c 100644 --- a/lib/edit_rate_limiter.rb +++ b/lib/edit_rate_limiter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rate_limiter' +require "rate_limiter" class EditRateLimiter < RateLimiter def initialize(user) limit = SiteSetting.max_edits_per_day diff --git a/lib/email.rb b/lib/email.rb index 8993c2a416..ec2061c29a 100644 --- a/lib/email.rb +++ b/lib/email.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mail' +require "mail" module Email # See https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml#smtp-enhanced-status-codes-1 @@ -21,19 +21,19 @@ module Email def self.obfuscate(email) return email if !Email.is_valid?(email) - first, _, last = email.rpartition('@') + first, _, last = email.rpartition("@") # Obfuscate each last part, except tld - last = last.split('.') + last = last.split(".") tld = last.pop last.map! { |part| obfuscate_part(part) } last << tld - "#{obfuscate_part(first)}@#{last.join('.')}" + "#{obfuscate_part(first)}@#{last.join(".")}" end def self.cleanup_alias(name) - name ? name.gsub(/[:<>,"]/, '') : name + name ? name.gsub(/[:<>,"]/, "") : name end def self.extract_parts(raw) diff --git a/lib/email/authentication_results.rb b/lib/email/authentication_results.rb index aa8f5ad2ef..05bea8df78 100644 --- a/lib/email/authentication_results.rb +++ b/lib/email/authentication_results.rb @@ -2,12 +2,7 @@ module Email class AuthenticationResults - VERDICT = Enum.new( - :gray, - :pass, - :fail, - start: 0, - ) + VERDICT = Enum.new(:gray, :pass, :fail, start: 0) def initialize(headers) @authserv_id = SiteSetting.email_in_authserv_id @@ -16,11 +11,10 @@ module Email end def results - @results ||= Array(@headers).map do |header| - parse_header(header.to_s) - end.filter do |result| - @authserv_id.blank? || @authserv_id == result[:authserv_id] - end + @results ||= + Array(@headers) + .map { |header| parse_header(header.to_s) } + .filter { |result| @authserv_id.blank? || @authserv_id == result[:authserv_id] } end def action @@ -55,7 +49,8 @@ module Email end end end - verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? && verdict == VERDICT[:pass] + verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? && + verdict == VERDICT[:pass] verdict end @@ -67,10 +62,11 @@ module Email authres_version = /\d+#{cfws}?/ no_result = /#{cfws}?;#{cfws}?none/ keyword = /([a-zA-Z0-9-]*[a-zA-Z0-9])/ - authres_payload = /\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/ + authres_payload = + /\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/ method_version = authres_version - method = /#{keyword}\s*(?:#{cfws}?\/#{cfws}?#{method_version})?/ + method = %r{#{keyword}\s*(?:#{cfws}?/#{cfws}?#{method_version})?} result = keyword methodspec = /#{cfws}?#{method}#{cfws}?=#{cfws}?#{result}/ reasonspec = /reason#{cfws}?=#{cfws}?#{value}/ @@ -87,27 +83,21 @@ module Email if resinfo_val resinfo_scan = resinfo_val.scan(resinfo) - parsed_resinfo = resinfo_scan.map do |x| - { - method: x[2], - result: x[8], - reason: x[12] || x[13], - props: x[-1].scan(propspec).map do |y| - { - ptype: y[0], - property: y[4], - pvalue: y[8] || y[9] - } - end - } - end + parsed_resinfo = + resinfo_scan.map do |x| + { + method: x[2], + result: x[8], + reason: x[12] || x[13], + props: + x[-1] + .scan(propspec) + .map { |y| { ptype: y[0], property: y[4], pvalue: y[8] || y[9] } }, + } + end end - { - authserv_id: parsed_authserv_id, - resinfo: parsed_resinfo - } + { authserv_id: parsed_authserv_id, resinfo: parsed_resinfo } end - end end diff --git a/lib/email/build_email_helper.rb b/lib/email/build_email_helper.rb index 80677942a0..51ddd1f97a 100644 --- a/lib/email/build_email_helper.rb +++ b/lib/email/build_email_helper.rb @@ -5,11 +5,11 @@ module Email def build_email(*builder_args) builder = Email::MessageBuilder.new(*builder_args) headers(builder.header_args) if builder.header_args.present? - mail(builder.build_args).tap { |message| + mail(builder.build_args).tap do |message| if message && h = builder.html_part message.html_part = h end - } + end end end end diff --git a/lib/email/cleaner.rb b/lib/email/cleaner.rb index 52170a9659..16668dd3fb 100644 --- a/lib/email/cleaner.rb +++ b/lib/email/cleaner.rb @@ -4,7 +4,7 @@ module Email class Cleaner def initialize(mail, remove_attachments: true, truncate: true, rejected: false) @mail = Mail.new(mail) - @mail.charset = 'UTF-8' + @mail.charset = "UTF-8" @remove_attachments = remove_attachments @truncate = truncate @rejected = rejected @@ -17,13 +17,16 @@ module Email end def self.delete_rejected! - IncomingEmail.delete_by('rejection_message IS NOT NULL AND created_at < ?', SiteSetting.delete_rejected_email_after_days.days.ago) + IncomingEmail.delete_by( + "rejection_message IS NOT NULL AND created_at < ?", + SiteSetting.delete_rejected_email_after_days.days.ago, + ) end private def truncate! - parts.each { |part| part.body = part.body.decoded.truncate(truncate_limit, omission: '') } + parts.each { |part| part.body = part.body.decoded.truncate(truncate_limit, omission: "") } end def parts diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 9308705cd6..396085955a 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -6,7 +6,7 @@ module Email class MessageBuilder attr_reader :template_args - ALLOW_REPLY_BY_EMAIL_HEADER = 'X-Discourse-Allow-Reply-By-Email' + ALLOW_REPLY_BY_EMAIL_HEADER = "X-Discourse-Allow-Reply-By-Email" def initialize(to, opts = nil) @to = to @@ -21,30 +21,44 @@ module Email }.merge!(@opts) if @template_args[:url].present? - @template_args[:header_instructions] ||= I18n.t('user_notifications.header_instructions', @template_args) + @template_args[:header_instructions] ||= I18n.t( + "user_notifications.header_instructions", + @template_args, + ) if @opts[:include_respond_instructions] == false - @template_args[:respond_instructions] = '' - @template_args[:respond_instructions] = I18n.t('user_notifications.pm_participants', @template_args) if @opts[:private_reply] + @template_args[:respond_instructions] = "" + @template_args[:respond_instructions] = I18n.t( + "user_notifications.pm_participants", + @template_args, + ) if @opts[:private_reply] else if @opts[:only_reply_by_email] string = +"user_notifications.only_reply_by_email" string << "_pm" if @opts[:private_reply] else - string = allow_reply_by_email? ? +"user_notifications.reply_by_email" : +"user_notifications.visit_link_to_respond" + string = + ( + if allow_reply_by_email? + +"user_notifications.reply_by_email" + else + +"user_notifications.visit_link_to_respond" + end + ) string << "_pm" if @opts[:private_reply] end @template_args[:respond_instructions] = "---\n" + I18n.t(string, @template_args) end if @opts[:add_unsubscribe_link] - unsubscribe_string = if @opts[:mailing_list_mode] - "unsubscribe_mailing_list" - elsif SiteSetting.unsubscribe_via_email_footer - "unsubscribe_link_and_mail" - else - "unsubscribe_link" - end + unsubscribe_string = + if @opts[:mailing_list_mode] + "unsubscribe_mailing_list" + elsif SiteSetting.unsubscribe_via_email_footer + "unsubscribe_link_and_mail" + else + "unsubscribe_link" + end @template_args[:unsubscribe_instructions] = I18n.t(unsubscribe_string, @template_args) end end @@ -52,26 +66,60 @@ module Email def subject if @opts[:template] && - TranslationOverride.exists?(locale: I18n.locale, translation_key: "#{@opts[:template]}.subject_template") - augmented_template_args = @template_args.merge({ - site_name: @template_args[:email_prefix], - optional_re: @opts[:add_re_to_subject] ? I18n.t('subject_re') : '', - optional_pm: @opts[:private_reply] ? @template_args[:subject_pm] : '', - optional_cat: @template_args[:show_category_in_subject] ? "[#{@template_args[:show_category_in_subject]}] " : '', - optional_tags: @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : '', - topic_title: @template_args[:topic_title] ? @template_args[:topic_title] : '', - }) + TranslationOverride.exists?( + locale: I18n.locale, + translation_key: "#{@opts[:template]}.subject_template", + ) + augmented_template_args = + @template_args.merge( + { + site_name: @template_args[:email_prefix], + optional_re: @opts[:add_re_to_subject] ? I18n.t("subject_re") : "", + optional_pm: @opts[:private_reply] ? @template_args[:subject_pm] : "", + optional_cat: + ( + if @template_args[:show_category_in_subject] + "[#{@template_args[:show_category_in_subject]}] " + else + "" + end + ), + optional_tags: + ( + if @template_args[:show_tags_in_subject] + "#{@template_args[:show_tags_in_subject]} " + else + "" + end + ), + topic_title: @template_args[:topic_title] ? @template_args[:topic_title] : "", + }, + ) subject = I18n.t("#{@opts[:template]}.subject_template", augmented_template_args) elsif @opts[:use_site_subject] subject = String.new(SiteSetting.email_subject) subject.gsub!("%{site_name}", @template_args[:email_prefix]) - subject.gsub!("%{optional_re}", @opts[:add_re_to_subject] ? I18n.t('subject_re') : '') - subject.gsub!("%{optional_pm}", @opts[:private_reply] ? @template_args[:subject_pm] : '') - subject.gsub!("%{optional_cat}", @template_args[:show_category_in_subject] ? "[#{@template_args[:show_category_in_subject]}] " : '') - subject.gsub!("%{optional_tags}", @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : '') - subject.gsub!("%{topic_title}", @template_args[:topic_title]) if @template_args[:topic_title] # must be last for safety + subject.gsub!("%{optional_re}", @opts[:add_re_to_subject] ? I18n.t("subject_re") : "") + subject.gsub!("%{optional_pm}", @opts[:private_reply] ? @template_args[:subject_pm] : "") + subject.gsub!( + "%{optional_cat}", + ( + if @template_args[:show_category_in_subject] + "[#{@template_args[:show_category_in_subject]}] " + else + "" + end + ), + ) + subject.gsub!( + "%{optional_tags}", + @template_args[:show_tags_in_subject] ? "#{@template_args[:show_tags_in_subject]} " : "", + ) + if @template_args[:topic_title] + subject.gsub!("%{topic_title}", @template_args[:topic_title]) + end # must be last for safety elsif @opts[:use_topic_title_subject] - subject = @opts[:add_re_to_subject] ? I18n.t('subject_re') : '' + subject = @opts[:add_re_to_subject] ? I18n.t("subject_re") : "" subject = "#{subject}#{@template_args[:topic_title]}" elsif @opts[:template] subject = I18n.t("#{@opts[:template]}.subject_template", @template_args) @@ -85,34 +133,40 @@ module Email return unless html_override = @opts[:html_override] if @template_args[:unsubscribe_instructions].present? - unsubscribe_instructions = PrettyText.cook(@template_args[:unsubscribe_instructions], sanitize: false).html_safe + unsubscribe_instructions = + PrettyText.cook(@template_args[:unsubscribe_instructions], sanitize: false).html_safe html_override.gsub!("%{unsubscribe_instructions}", unsubscribe_instructions) else html_override.gsub!("%{unsubscribe_instructions}", "") end if @template_args[:header_instructions].present? - header_instructions = PrettyText.cook(@template_args[:header_instructions], sanitize: false).html_safe + header_instructions = + PrettyText.cook(@template_args[:header_instructions], sanitize: false).html_safe html_override.gsub!("%{header_instructions}", header_instructions) else html_override.gsub!("%{header_instructions}", "") end if @template_args[:respond_instructions].present? - respond_instructions = PrettyText.cook(@template_args[:respond_instructions], sanitize: false).html_safe + respond_instructions = + PrettyText.cook(@template_args[:respond_instructions], sanitize: false).html_safe html_override.gsub!("%{respond_instructions}", respond_instructions) else html_override.gsub!("%{respond_instructions}", "") end - html = UserNotificationRenderer.render( - template: 'layouts/email_template', - format: :html, - locals: { html_body: html_override.html_safe } - ) + html = + UserNotificationRenderer.render( + template: "layouts/email_template", + format: :html, + locals: { + html_body: html_override.html_safe, + }, + ) Mail::Part.new do - content_type 'text/html; charset=UTF-8' + content_type "text/html; charset=UTF-8" body html end end @@ -139,14 +193,18 @@ module Email to: @to, subject: subject, body: body, - charset: 'UTF-8', + charset: "UTF-8", from: from_value, cc: @opts[:cc], - bcc: @opts[:bcc] + bcc: @opts[:bcc], } - args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[:delivery_method_options] - args[:delivery_method_options] = (args[:delivery_method_options] || {}).merge(return_response: true) + args[:delivery_method_options] = @opts[:delivery_method_options] if @opts[ + :delivery_method_options + ] + args[:delivery_method_options] = (args[:delivery_method_options] || {}).merge( + return_response: true, + ) args end @@ -154,33 +212,42 @@ module Email def header_args result = {} if @opts[:add_unsubscribe_link] - unsubscribe_url = @template_args[:unsubscribe_url].presence || @template_args[:user_preferences_url] - result['List-Unsubscribe'] = "<#{unsubscribe_url}>" + unsubscribe_url = + @template_args[:unsubscribe_url].presence || @template_args[:user_preferences_url] + result["List-Unsubscribe"] = "<#{unsubscribe_url}>" end - result['X-Discourse-Post-Id'] = @opts[:post_id].to_s if @opts[:post_id] - result['X-Discourse-Topic-Id'] = @opts[:topic_id].to_s if @opts[:topic_id] + result["X-Discourse-Post-Id"] = @opts[:post_id].to_s if @opts[:post_id] + result["X-Discourse-Topic-Id"] = @opts[:topic_id].to_s if @opts[:topic_id] + + # at this point these have been filtered by the recipient's guardian for visibility, + # see UserNotifications#send_notification_email + result["X-Discourse-Tags"] = @template_args[:show_tags_in_subject] if @opts[ + :show_tags_in_subject + ] + result["X-Discourse-Category"] = @template_args[:show_category_in_subject] if @opts[ + :show_category_in_subject + ] # please, don't send us automatic responses... - result['X-Auto-Response-Suppress'] = 'All' + result["X-Auto-Response-Suppress"] = "All" if !allow_reply_by_email? # This will end up being the notification_email, which is a # noreply address. - result['Reply-To'] = from_value + result["Reply-To"] = from_value else - # The only reason we use from address for reply to is for group # SMTP emails, where the person will be replying to the group's # email_username. if !@opts[:use_from_address_for_reply_to] result[ALLOW_REPLY_BY_EMAIL_HEADER] = true - result['Reply-To'] = reply_by_email_address + result["Reply-To"] = reply_by_email_address else # No point in adding a reply-to header if it is going to be identical # to the from address/alias. If the from option is not present, then # the default reply-to address is used. - result['Reply-To'] = from_value if from_value != alias_email(@opts[:from]) + result["Reply-To"] = from_value if from_value != alias_email(@opts[:from]) end end @@ -189,23 +256,24 @@ module Email def self.custom_headers(string) result = {} - string.split('|').each { |item| - header = item.split(':', 2) - if header.length == 2 - name = header[0].strip - value = header[1].strip - result[name] = value if name.length > 0 && value.length > 0 - end - } unless string.nil? + string + .split("|") + .each do |item| + header = item.split(":", 2) + if header.length == 2 + name = header[0].strip + value = header[1].strip + result[name] = value if name.length > 0 && value.length > 0 + end + end unless string.nil? result end protected def allow_reply_by_email? - SiteSetting.reply_by_email_enabled? && - reply_by_email_address.present? && - @opts[:allow_reply_by_email] + SiteSetting.reply_by_email_enabled? && reply_by_email_address.present? && + @opts[:allow_reply_by_email] end def private_reply? @@ -233,9 +301,10 @@ module Email end def alias_email(source) - return source if @opts[:from_alias].blank? && - SiteSetting.email_site_title.blank? && - SiteSetting.title.blank? + if @opts[:from_alias].blank? && SiteSetting.email_site_title.blank? && + SiteSetting.title.blank? + return source + end if @opts[:from_alias].present? %Q|"#{Email.cleanup_alias(@opts[:from_alias])}" <#{source}>| @@ -250,7 +319,5 @@ module Email from_alias = Email.site_title %Q|"#{Email.cleanup_alias(from_alias)}" <#{source}>| end - end - end diff --git a/lib/email/message_id_service.rb b/lib/email/message_id_service.rb index fe2af54343..4ba6b0d737 100644 --- a/lib/email/message_id_service.rb +++ b/lib/email/message_id_service.rb @@ -49,9 +49,8 @@ module Email # inbound email sent to Discourse and handled by Email::Receiver, # this is the only case where we want to use the original Message-ID # because we want to maintain threading in the original mail client. - if use_incoming_email_if_present && - incoming_email&.message_id.present? && - incoming_email&.created_via == IncomingEmail.created_via_types[:handle_mail] + if use_incoming_email_if_present && incoming_email&.message_id.present? && + incoming_email&.created_via == IncomingEmail.created_via_types[:handle_mail] return "<#{first_post.incoming_email.message_id}>" end @@ -100,12 +99,26 @@ module Email # TODO (martin) 2023-01-01 We should remove these backwards-compatible # formats for the Message-ID and solely use the discourse/post/999@host # format. - topic_ids = message_ids.map { |message_id| message_id[message_id_topic_id_regexp, 1] }.compact.map(&:to_i) - post_ids = message_ids.map { |message_id| message_id[message_id_post_id_regexp, 1] }.compact.map(&:to_i) + topic_ids = + message_ids + .map { |message_id| message_id[message_id_topic_id_regexp, 1] } + .compact + .map(&:to_i) + post_ids = + message_ids + .map { |message_id| message_id[message_id_post_id_regexp, 1] } + .compact + .map(&:to_i) - post_ids << message_ids.map { |message_id| message_id[message_id_discourse_regexp, 1] }.compact.map(&:to_i) + post_ids << message_ids + .map { |message_id| message_id[message_id_discourse_regexp, 1] } + .compact + .map(&:to_i) - post_ids << Post.where(outbound_message_id: message_ids).or(Post.where(topic_id: topic_ids, post_number: 1)).pluck(:id) + post_ids << Post + .where(outbound_message_id: message_ids) + .or(Post.where(topic_id: topic_ids, post_number: 1)) + .pluck(:id) post_ids << EmailLog.where(message_id: message_ids).pluck(:post_id) post_ids << IncomingEmail.where(message_id: message_ids).pluck(:post_id) @@ -151,11 +164,15 @@ module Email end def message_id_clean(message_id) - message_id.present? && is_message_id_rfc?(message_id) ? message_id.gsub(/^<|>$/, "") : message_id + if message_id.present? && is_message_id_rfc?(message_id) + message_id.gsub(/^<|>$/, "") + else + message_id + end end def is_message_id_rfc?(message_id) - message_id.start_with?('<') && message_id.include?('@') && message_id.end_with?('>') + message_id.start_with?("<") && message_id.include?("@") && message_id.end_with?(">") end def host diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 4d9f5085d4..9c807fc2b8 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Email - class Processor attr_reader :receiver @@ -19,7 +18,11 @@ module Email @receiver = Email::Receiver.new(@mail, @opts) @receiver.process! rescue RateLimiter::LimitExceeded - @opts[:retry_on_rate_limit] ? Jobs.enqueue(:process_email, mail: @mail, source: @opts[:source]) : raise + if @opts[:retry_on_rate_limit] + Jobs.enqueue(:process_email, mail: @mail, source: @opts[:source]) + else + raise + end rescue => e return handle_bounce(e) if @receiver.is_bounce? @@ -37,39 +40,70 @@ module Email def handle_bounce(e) # never reply to bounced emails log_email_process_failure(@mail, e) - set_incoming_email_rejection_message(@receiver.incoming_email, I18n.t("emails.incoming.errors.bounced_email_error")) + set_incoming_email_rejection_message( + @receiver.incoming_email, + I18n.t("emails.incoming.errors.bounced_email_error"), + ) end def handle_failure(mail_string, e) - message_template = case e - when Email::Receiver::NoSenderDetectedError then return nil - when Email::Receiver::FromReplyByAddressError then return nil - when Email::Receiver::EmptyEmailError then :email_reject_empty - when Email::Receiver::NoBodyDetectedError then :email_reject_empty - when Email::Receiver::UserNotFoundError then :email_reject_user_not_found - when Email::Receiver::ScreenedEmailError then :email_reject_screened_email - when Email::Receiver::EmailNotAllowed then :email_reject_not_allowed_email - when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated - when Email::Receiver::InactiveUserError then :email_reject_inactive_user - when Email::Receiver::SilencedUserError then :email_reject_silenced_user - when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address - when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed - when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level - when Email::Receiver::ReplyUserNotMatchingError then :email_reject_reply_user_not_matching - when Email::Receiver::TopicNotFoundError then :email_reject_topic_not_found - when Email::Receiver::TopicClosedError then :email_reject_topic_closed - when Email::Receiver::InvalidPost then :email_reject_invalid_post - when Email::Receiver::TooShortPost then :email_reject_post_too_short - when Email::Receiver::UnsubscribeNotAllowed then :email_reject_invalid_post - when ActiveRecord::Rollback then :email_reject_invalid_post - when Email::Receiver::InvalidPostAction then :email_reject_invalid_post_action - when Discourse::InvalidAccess then :email_reject_invalid_access - when Email::Receiver::OldDestinationError then :email_reject_old_destination - when Email::Receiver::ReplyNotAllowedError then :email_reject_reply_not_allowed - when Email::Receiver::ReplyToDigestError then :email_reject_reply_to_digest - when Email::Receiver::TooManyRecipientsError then :email_reject_too_many_recipients - else :email_reject_unrecognized_error - end + message_template = + case e + when Email::Receiver::NoSenderDetectedError + return nil + when Email::Receiver::FromReplyByAddressError + return nil + when Email::Receiver::EmptyEmailError + :email_reject_empty + when Email::Receiver::NoBodyDetectedError + :email_reject_empty + when Email::Receiver::UserNotFoundError + :email_reject_user_not_found + when Email::Receiver::ScreenedEmailError + :email_reject_screened_email + when Email::Receiver::EmailNotAllowed + :email_reject_not_allowed_email + when Email::Receiver::AutoGeneratedEmailError + :email_reject_auto_generated + when Email::Receiver::InactiveUserError + :email_reject_inactive_user + when Email::Receiver::SilencedUserError + :email_reject_silenced_user + when Email::Receiver::BadDestinationAddress + :email_reject_bad_destination_address + when Email::Receiver::StrangersNotAllowedError + :email_reject_strangers_not_allowed + when Email::Receiver::InsufficientTrustLevelError + :email_reject_insufficient_trust_level + when Email::Receiver::ReplyUserNotMatchingError + :email_reject_reply_user_not_matching + when Email::Receiver::TopicNotFoundError + :email_reject_topic_not_found + when Email::Receiver::TopicClosedError + :email_reject_topic_closed + when Email::Receiver::InvalidPost + :email_reject_invalid_post + when Email::Receiver::TooShortPost + :email_reject_post_too_short + when Email::Receiver::UnsubscribeNotAllowed + :email_reject_invalid_post + when ActiveRecord::Rollback + :email_reject_invalid_post + when Email::Receiver::InvalidPostAction + :email_reject_invalid_post_action + when Discourse::InvalidAccess + :email_reject_invalid_access + when Email::Receiver::OldDestinationError + :email_reject_old_destination + when Email::Receiver::ReplyNotAllowedError + :email_reject_reply_not_allowed + when Email::Receiver::ReplyToDigestError + :email_reject_reply_to_digest + when Email::Receiver::TooManyRecipientsError + :email_reject_too_many_recipients + else + :email_reject_unrecognized_error + end template_args = {} client_message = nil @@ -85,7 +119,7 @@ module Email end if message_template == :email_reject_unrecognized_error - msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" + msg = "Unrecognized error type (#{e.class}: #{e.message}) when processing incoming email" msg += "\n\nBacktrace:\n#{e.backtrace.map { |l| " #{l}" }.join("\n")}" msg += "\n\nMail:\n#{mail_string}" @@ -109,7 +143,8 @@ module Email template_args[:destination] = message.to template_args[:site_name] = SiteSetting.title - client_message = RejectionMailer.send_rejection(message_template, message.from, template_args) + client_message = + RejectionMailer.send_rejection(message_template, message.from, template_args) # only send one rejection email per day to the same email address if can_send_rejection_email?(message.from, message_template) @@ -138,7 +173,7 @@ module Email if incoming_email incoming_email.update!( rejection_message: message, - raw: Email::Cleaner.new(incoming_email.raw, rejected: true).execute + raw: Email::Cleaner.new(incoming_email.raw, rejected: true).execute, ) end end @@ -148,7 +183,5 @@ module Email Rails.logger.warn("Email can not be processed: #{exception}\n\n#{mail_string}") end end - end - end diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index ab318439ba..05b51c7eb0 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -7,31 +7,56 @@ module Email # If you add a new error, you need to # * add it to Email::Processor#handle_failure() # * add text to server.en.yml (parent key: "emails.incoming.errors") - class ProcessingError < StandardError; end - class EmptyEmailError < ProcessingError; end - class ScreenedEmailError < ProcessingError; end - class UserNotFoundError < ProcessingError; end - class AutoGeneratedEmailError < ProcessingError; end - class BouncedEmailError < ProcessingError; end - class NoBodyDetectedError < ProcessingError; end - class NoSenderDetectedError < ProcessingError; end - class FromReplyByAddressError < ProcessingError; end - class InactiveUserError < ProcessingError; end - class SilencedUserError < ProcessingError; end - class BadDestinationAddress < ProcessingError; end - class StrangersNotAllowedError < ProcessingError; end - class ReplyNotAllowedError < ProcessingError; end - class InsufficientTrustLevelError < ProcessingError; end - class ReplyUserNotMatchingError < ProcessingError; end - class TopicNotFoundError < ProcessingError; end - class TopicClosedError < ProcessingError; end - class InvalidPost < ProcessingError; end - class TooShortPost < ProcessingError; end - class InvalidPostAction < ProcessingError; end - class UnsubscribeNotAllowed < ProcessingError; end - class EmailNotAllowed < ProcessingError; end - class OldDestinationError < ProcessingError; end - class ReplyToDigestError < ProcessingError; end + class ProcessingError < StandardError + end + class EmptyEmailError < ProcessingError + end + class ScreenedEmailError < ProcessingError + end + class UserNotFoundError < ProcessingError + end + class AutoGeneratedEmailError < ProcessingError + end + class BouncedEmailError < ProcessingError + end + class NoBodyDetectedError < ProcessingError + end + class NoSenderDetectedError < ProcessingError + end + class FromReplyByAddressError < ProcessingError + end + class InactiveUserError < ProcessingError + end + class SilencedUserError < ProcessingError + end + class BadDestinationAddress < ProcessingError + end + class StrangersNotAllowedError < ProcessingError + end + class ReplyNotAllowedError < ProcessingError + end + class InsufficientTrustLevelError < ProcessingError + end + class ReplyUserNotMatchingError < ProcessingError + end + class TopicNotFoundError < ProcessingError + end + class TopicClosedError < ProcessingError + end + class InvalidPost < ProcessingError + end + class TooShortPost < ProcessingError + end + class InvalidPostAction < ProcessingError + end + class UnsubscribeNotAllowed < ProcessingError + end + class EmailNotAllowed < ProcessingError + end + class OldDestinationError < ProcessingError + end + class ReplyToDigestError < ProcessingError + end class TooManyRecipientsError < ProcessingError attr_reader :recipients_count @@ -120,7 +145,7 @@ module Email imap_uid_validity: @opts[:imap_uid_validity], imap_uid: @opts[:imap_uid], imap_group_id: @opts[:imap_group_id], - imap_sync: false + imap_sync: false, ) incoming_email @@ -133,9 +158,7 @@ module Email def create_incoming_email cc_addresses = Array.wrap(@mail.cc) - if has_been_forwarded? && embedded_email&.cc - cc_addresses.concat(embedded_email.cc) - end + cc_addresses.concat(embedded_email.cc) if has_been_forwarded? && embedded_email&.cc IncomingEmail.create( message_id: @message_id, raw: Email::Cleaner.new(@raw_email).execute, @@ -147,7 +170,7 @@ module Email imap_uid: @opts[:imap_uid], imap_group_id: @opts[:imap_group_id], imap_sync: false, - created_via: IncomingEmail.created_via_types[@opts[:source] || :unknown] + created_via: IncomingEmail.created_via_types[@opts[:source] || :unknown], ) end @@ -199,13 +222,15 @@ module Email end end - create_reply(user: user, - raw: body, - elided: elided, - post: post, - topic: post.topic, - skip_validations: user.staged?, - bounce: is_bounce?) + create_reply( + user: user, + raw: body, + elided: elided, + post: post, + topic: post.topic, + skip_validations: user.staged?, + bounce: is_bounce?, + ) else first_exception = nil @@ -232,7 +257,9 @@ module Email end end - raise ReplyToDigestError if EmailLog.where(email_type: "digest", message_id: @mail.in_reply_to).exists? + if EmailLog.where(email_type: "digest", message_id: @mail.in_reply_to).exists? + raise ReplyToDigestError + end raise BadDestinationAddress end end @@ -247,7 +274,7 @@ module Email def get_all_recipients(mail) recipients = Set.new - %i(to cc bcc).each do |field| + %i[to cc bcc].each do |field| next if mail[field].blank? mail[field].each do |address_field| @@ -270,10 +297,7 @@ module Email mail_error_statuses = Array.wrap(@mail.error_status) if email_log.present? - email_log.update_columns( - bounced: true, - bounce_error_code: mail_error_statuses.first - ) + email_log.update_columns(bounced: true, bounce_error_code: mail_error_statuses.first) post = email_log.post topic = email_log.topic end @@ -293,13 +317,15 @@ module Email body, elided = select_body body ||= "" - create_reply(user: @from_user, - raw: body, - elided: elided, - post: post, - topic: topic, - skip_validations: true, - bounce: true) + create_reply( + user: @from_user, + raw: body, + elided: elided, + post: post, + topic: topic, + skip_validations: true, + bounce: true, + ) end end @@ -311,10 +337,11 @@ module Email end def bounce_key - @bounce_key ||= begin - verp = all_destinations.select { |to| to[/\+verp-\h{32}@/] }.first - verp && verp[/\+verp-(\h{32})@/, 1] - end + @bounce_key ||= + begin + verp = all_destinations.select { |to| to[/\+verp-\h{32}@/] }.first + verp && verp[/\+verp-(\h{32})@/, 1] + end end def email_log @@ -329,13 +356,19 @@ module Email range = (old_bounce_score + 1..new_bounce_score) user.user_stat.bounce_score = new_bounce_score - user.user_stat.reset_bounce_score_after = SiteSetting.reset_bounce_score_after_days.days.from_now + user.user_stat.reset_bounce_score_after = + SiteSetting.reset_bounce_score_after_days.days.from_now user.user_stat.save! if range === SiteSetting.bounce_score_threshold # NOTE: we check bounce_score before sending emails # So log we revoked the email... - reason = I18n.t("user.email.revoked", email: user.email, date: user.user_stat.reset_bounce_score_after) + reason = + I18n.t( + "user.email.revoked", + email: user.email, + date: user.user_stat.reset_bounce_score_after, + ) StaffActionLogger.new(Discourse.system_user).log_revoke_email(user, reason) # ... and PM the user SystemMessage.create_from_system_user(user, :email_revoked) @@ -344,20 +377,24 @@ module Email end def is_auto_generated? - return false if SiteSetting.auto_generated_allowlist.split('|').include?(@from_email) + return false if SiteSetting.auto_generated_allowlist.split("|").include?(@from_email) @mail[:precedence].to_s[/list|junk|bulk|auto_reply/i] || - @mail[:from].to_s[/(mailer[\-_]?daemon|post[\-_]?master|no[\-_]?reply)@/i] || - @mail[:subject].to_s[/^\s*(Auto:|Automatic reply|Autosvar|Automatisk svar|Automatisch antwoord|Abwesenheitsnotiz|Risposta Non al computer|Automatisch antwoord|Auto Response|Respuesta automática|Fuori sede|Out of Office|Frånvaro|Réponse automatique)/i] || - @mail.header.to_s[/auto[\-_]?(response|submitted|replied|reply|generated|respond)|holidayreply|machinegenerated/i] + @mail[:from].to_s[/(mailer[\-_]?daemon|post[\-_]?master|no[\-_]?reply)@/i] || + @mail[:subject].to_s[ + /^\s*(Auto:|Automatic reply|Autosvar|Automatisk svar|Automatisch antwoord|Abwesenheitsnotiz|Risposta Non al computer|Automatisch antwoord|Auto Response|Respuesta automática|Fuori sede|Out of Office|Frånvaro|Réponse automatique)/i + ] || + @mail.header.to_s[ + /auto[\-_]?(response|submitted|replied|reply|generated|respond)|holidayreply|machinegenerated/i + ] end def is_spam? case SiteSetting.email_in_spam_header - when 'X-Spam-Flag' + when "X-Spam-Flag" @mail[:x_spam_flag].to_s[/YES/i] - when 'X-Spam-Status' + when "X-Spam-Status" @mail[:x_spam_status].to_s[/^Yes, /i] - when 'X-SES-Spam-Verdict' + when "X-SES-Spam-Verdict" @mail[:x_ses_spam_verdict].to_s[/FAIL/i] else false @@ -394,53 +431,62 @@ module Email text_content_type ||= "" converter_opts = { format_flowed: !!(text_content_type =~ /format\s*=\s*["']?flowed["']?/i), - delete_flowed_space: !!(text_content_type =~ /DelSp\s*=\s*["']?yes["']?/i) + delete_flowed_space: !!(text_content_type =~ /DelSp\s*=\s*["']?yes["']?/i), } text = PlainTextToMarkdown.new(text, converter_opts).to_markdown elided_text = PlainTextToMarkdown.new(elided_text, converter_opts).to_markdown end end - markdown, elided_markdown = if html.present? - # use the first html extracter that matches - if html_extracter = HTML_EXTRACTERS.select { |_, r| html[r] }.min_by { |_, r| html =~ r } - doc = Nokogiri::HTML5.fragment(html) - self.public_send(:"extract_from_#{html_extracter[0]}", doc) - else - markdown = HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown - markdown = trim_discourse_markers(markdown) - trim_reply_and_extract_elided(markdown) + markdown, elided_markdown = + if html.present? + # use the first html extracter that matches + if html_extracter = HTML_EXTRACTERS.select { |_, r| html[r] }.min_by { |_, r| html =~ r } + doc = Nokogiri::HTML5.fragment(html) + self.public_send(:"extract_from_#{html_extracter[0]}", doc) + else + markdown = + HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown + markdown = trim_discourse_markers(markdown) + trim_reply_and_extract_elided(markdown) + end end - end - text_format = Receiver::formats[:plaintext] + text_format = Receiver.formats[:plaintext] if text.blank? || (SiteSetting.incoming_email_prefer_html && markdown.present?) - text, elided_text, text_format = markdown, elided_markdown, Receiver::formats[:markdown] + text, elided_text, text_format = markdown, elided_markdown, Receiver.formats[:markdown] end if SiteSetting.strip_incoming_email_lines && text.present? in_code = nil - text = text.lines.map! do |line| - stripped = line.strip << "\n" + text = + text + .lines + .map! do |line| + stripped = line.strip << "\n" - # Do not strip list items. - next line if (stripped[0] == '*' || stripped[0] == '-' || stripped[0] == '+') && stripped[1] == ' ' + # Do not strip list items. + if (stripped[0] == "*" || stripped[0] == "-" || stripped[0] == "+") && + stripped[1] == " " + next line + end - # Match beginning and ending of code blocks. - if !in_code && stripped[0..2] == '```' - in_code = '```' - elsif in_code == '```' && stripped[0..2] == '```' - in_code = nil - elsif !in_code && stripped[0..4] == '[code' - in_code = '[code]' - elsif in_code == '[code]' && stripped[0..6] == '[/code]' - in_code = nil - end + # Match beginning and ending of code blocks. + if !in_code && stripped[0..2] == "```" + in_code = "```" + elsif in_code == "```" && stripped[0..2] == "```" + in_code = nil + elsif !in_code && stripped[0..4] == "[code" + in_code = "[code]" + elsif in_code == "[code]" && stripped[0..6] == "[/code]" + in_code = nil + end - # Strip only lines outside code blocks. - in_code ? line : stripped - end.join + # Strip only lines outside code blocks. + in_code ? line : stripped + end + .join end [text, elided_text, text_format] @@ -448,7 +494,8 @@ module Email def to_markdown(html, elided_html) markdown = HtmlToMarkdown.new(html, keep_img_tags: true, keep_cid_imgs: true).to_markdown - elided_markdown = HtmlToMarkdown.new(elided_html, keep_img_tags: true, keep_cid_imgs: true).to_markdown + elided_markdown = + HtmlToMarkdown.new(elided_html, keep_img_tags: true, keep_cid_imgs: true).to_markdown [EmailReplyTrimmer.trim(markdown), elided_markdown] end @@ -481,7 +528,10 @@ module Email def extract_from_word(doc) # Word (?) keeps the content in the 'WordSection1' class and uses

    tags # When there's something else (,
    , etc..) there's high chance it's a signature or forwarded email - elided = doc.css(".WordSection1 > :not(p):not(ul):first-of-type, .WordSection1 > :not(p):not(ul):first-of-type ~ *").remove + elided = + doc.css( + ".WordSection1 > :not(p):not(ul):first-of-type, .WordSection1 > :not(p):not(ul):first-of-type ~ *", + ).remove to_markdown(doc.at(".WordSection1").to_html, elided.to_html) end @@ -502,9 +552,12 @@ module Email def extract_from_mozilla(doc) # Mozilla (Thunderbird ?) properly identifies signature and forwarded emails # Remove them and anything that comes after - elided = doc.css("*[class^='moz-cite'], *[class^='moz-cite'] ~ *, " \ - "*[class^='moz-signature'], *[class^='moz-signature'] ~ *, " \ - "*[class^='moz-forward'], *[class^='moz-forward'] ~ *").remove + elided = + doc.css( + "*[class^='moz-cite'], *[class^='moz-cite'] ~ *, " \ + "*[class^='moz-signature'], *[class^='moz-signature'] ~ *, " \ + "*[class^='moz-forward'], *[class^='moz-forward'] ~ *", + ).remove to_markdown(doc.to_html, elided.to_html) end @@ -533,14 +586,19 @@ module Email end def trim_reply_and_extract_elided(text) - return [text, ""] if @opts[:skip_trimming] || !SiteSetting.trim_incoming_emails + return text, "" if @opts[:skip_trimming] || !SiteSetting.trim_incoming_emails EmailReplyTrimmer.trim(text, true) end def fix_charset(mail_part) return nil if mail_part.blank? || mail_part.body.blank? - string = mail_part.body.decoded rescue nil + string = + begin + mail_part.body.decoded + rescue StandardError + nil + end return nil if string.blank? @@ -572,23 +630,32 @@ module Email end def previous_replies_regex - strings = I18n.available_locales.map do |locale| - I18n.with_locale(locale) { I18n.t("user_notifications.previous_discussion") } - end.uniq + strings = + I18n + .available_locales + .map do |locale| + I18n.with_locale(locale) { I18n.t("user_notifications.previous_discussion") } + end + .uniq - @previous_replies_regex ||= /^--[- ]\n\*(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\*\n/im + @previous_replies_regex ||= + /^--[- ]\n\*(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\*\n/im end def reply_above_line_regex - strings = I18n.available_locales.map do |locale| - I18n.with_locale(locale) { I18n.t("user_notifications.reply_above_line") } - end.uniq + strings = + I18n + .available_locales + .map do |locale| + I18n.with_locale(locale) { I18n.t("user_notifications.reply_above_line") } + end + .uniq @reply_above_line_regex ||= /\n(?:#{strings.map { |x| Regexp.escape(x) }.join("|")})\n/im end def trim_discourse_markers(reply) - return '' if reply.blank? + return "" if reply.blank? reply = reply.split(previous_replies_regex)[0] reply.split(reply_above_line_regex)[0] end @@ -598,11 +665,9 @@ module Email if email_log.present? email = email_log.to_address || email_log.user&.email - return [email, email_log.user&.username] + return email, email_log.user&.username elsif mail.bounced? - Array.wrap(mail.final_recipient).each do |from| - return extract_from_address_and_name(from) - end + Array.wrap(mail.final_recipient).each { |from| return extract_from_address_and_name(from) } end return unless mail[:from] @@ -613,7 +678,7 @@ module Email if has_been_forwarded? if mail[:from].to_s =~ group_incoming_emails_regex && embedded_email[:from].errors.blank? from_address, from_display_name = extract_from_fields_from_header(embedded_email, :from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end end @@ -621,18 +686,21 @@ module Email # been forwarded via Google Groups, which is why we are checking the # X-Original-From header too. In future we may want to use the Reply-To # header in more cases. - if mail['X-Original-From'].present? + if mail["X-Original-From"].present? if mail[:reply_to] && mail[:reply_to].errors.blank? - from_address, from_display_name = extract_from_fields_from_header( - mail, :reply_to, comparison_headers: ['X-Original-From'] - ) - return [from_address, from_display_name] if from_address + from_address, from_display_name = + extract_from_fields_from_header( + mail, + :reply_to, + comparison_headers: ["X-Original-From"], + ) + return from_address, from_display_name if from_address end end if mail[:from].errors.blank? from_address, from_display_name = extract_from_fields_from_header(mail, :from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end return extract_from_address_and_name(mail.from) if mail.from.is_a? String @@ -640,7 +708,7 @@ module Email if mail.from.is_a? Mail::AddressContainer mail.from.each do |from| from_address, from_display_name = extract_from_address_and_name(from) - return [from_address, from_display_name] if from_address + return from_address, from_display_name if from_address end end @@ -665,7 +733,7 @@ module Email next if comparison_failed next if !from_address&.include?("@") - return [from_address&.downcase, from_display_name&.strip] + return from_address&.downcase, from_display_name&.strip end [nil, nil] @@ -674,7 +742,7 @@ module Email def extract_from_address_and_name(value) if value[";"] from_display_name, from_address = value.split(";") - return [from_address&.strip&.downcase, from_display_name&.strip] + return from_address&.strip&.downcase, from_display_name&.strip end if value[/<[^>]+>/] @@ -708,12 +776,13 @@ module Email username = UserNameSuggester.sanitize_username(display_name) if display_name.present? begin - user = User.create!( - email: email, - username: UserNameSuggester.suggest(username.presence || email), - name: display_name.presence || User.suggest_name(email), - staged: true - ) + user = + User.create!( + email: email, + username: UserNameSuggester.suggest(username.presence || email), + name: display_name.presence || User.suggest_name(email), + staged: true, + ) @created_staged_users << user rescue PG::UniqueViolation, ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid raise if raise_on_failed_create @@ -740,19 +809,19 @@ module Email end def destinations - @destinations ||= all_destinations - .map { |d| Email::Receiver.check_address(d, is_bounce?) } - .reject(&:blank?) + @destinations ||= + all_destinations.map { |d| Email::Receiver.check_address(d, is_bounce?) }.reject(&:blank?) end def sent_to_mailinglist_mirror? - @sent_to_mailinglist_mirror ||= begin - destinations.each do |destination| - return true if destination.is_a?(Category) && destination.mailinglist_mirror? - end + @sent_to_mailinglist_mirror ||= + begin + destinations.each do |destination| + return true if destination.is_a?(Category) && destination.mailinglist_mirror? + end - false - end + false + end end def self.check_address(address, include_verp = false) @@ -778,9 +847,10 @@ module Email end def process_destination(destination, user, body, elided) - return if SiteSetting.forwarded_emails_behaviour != "hide" && - has_been_forwarded? && - process_forwarded_email(destination, user) + if SiteSetting.forwarded_emails_behaviour != "hide" && has_been_forwarded? && + process_forwarded_email(destination, user) + return + end return if is_bounce? && !destination.is_a?(PostReplyKey) @@ -788,19 +858,24 @@ module Email user ||= stage_from_user create_group_post(destination, user, body, elided) elsif destination.is_a?(Category) - raise StrangersNotAllowedError if (user.nil? || user.staged?) && !destination.email_in_allow_strangers + if (user.nil? || user.staged?) && !destination.email_in_allow_strangers + raise StrangersNotAllowedError + end user ||= stage_from_user - raise InsufficientTrustLevelError if !user.has_trust_level?(SiteSetting.email_in_min_trust) && !sent_to_mailinglist_mirror? - - create_topic(user: user, - raw: body, - elided: elided, - title: subject, - category: destination.id, - skip_validations: user.staged?) + if !user.has_trust_level?(SiteSetting.email_in_min_trust) && !sent_to_mailinglist_mirror? + raise InsufficientTrustLevelError + end + create_topic( + user: user, + raw: body, + elided: elided, + title: subject, + category: destination.id, + skip_validations: user.staged?, + ) elsif destination.is_a?(PostReplyKey) # We don't stage new users for emails to reply addresses, exit if user is nil raise BadDestinationAddress if user.blank? @@ -809,16 +884,19 @@ module Email raise ReplyNotAllowedError if !Guardian.new(user).can_create_post?(post&.topic) if destination.user_id != user.id && !forwarded_reply_key?(destination, user) - raise ReplyUserNotMatchingError, "post_reply_key.user_id => #{destination.user_id.inspect}, user.id => #{user.id.inspect}" + raise ReplyUserNotMatchingError, + "post_reply_key.user_id => #{destination.user_id.inspect}, user.id => #{user.id.inspect}" end - create_reply(user: user, - raw: body, - elided: elided, - post: post, - topic: post&.topic, - skip_validations: user.staged?, - bounce: is_bounce?) + create_reply( + user: user, + raw: body, + elided: elided, + post: post, + topic: post&.topic, + skip_validations: user.staged?, + bounce: is_bounce?, + ) end end @@ -839,11 +917,14 @@ module Email # there will be a corresponding EmailLog record, so we can use that as the # reply post if it exists if Email::MessageIdService.discourse_generated_message_id?(mail.in_reply_to) - post_id_from_email_log = EmailLog.where(message_id: mail.in_reply_to) - .addressed_to_user(user) - .order(created_at: :desc) - .limit(1) - .pluck(:post_id).last + post_id_from_email_log = + EmailLog + .where(message_id: mail.in_reply_to) + .addressed_to_user(user) + .order(created_at: :desc) + .limit(1) + .pluck(:post_id) + .last post_ids << post_id_from_email_log if post_id_from_email_log end @@ -851,14 +932,16 @@ module Email too_old_for_group_smtp = (destination_too_old?(target_post) && group.smtp_enabled) if target_post.blank? || too_old_for_group_smtp - create_topic(user: user, - raw: new_group_topic_body(body, target_post, too_old_for_group_smtp), - elided: elided, - title: subject, - archetype: Archetype.private_message, - target_group_names: [group.name], - is_group_message: true, - skip_validations: true) + create_topic( + user: user, + raw: new_group_topic_body(body, target_post, too_old_for_group_smtp), + elided: elided, + title: subject, + archetype: Archetype.private_message, + target_group_names: [group.name], + is_group_message: true, + skip_validations: true, + ) else # This must be done for the unknown user (who is staged) to # be allowed to post a reply in the topic. @@ -866,39 +949,47 @@ module Email target_post.topic.topic_allowed_users.find_or_create_by!(user_id: user.id) end - create_reply(user: user, - raw: body, - elided: elided, - post: target_post, - topic: target_post.topic, - skip_validations: true) + create_reply( + user: user, + raw: body, + elided: elided, + post: target_post, + topic: target_post.topic, + skip_validations: true, + ) end end def new_group_topic_body(body, target_post, too_old_for_group_smtp) return body if !too_old_for_group_smtp - body + "\n\n----\n\n" + I18n.t( - "emails.incoming.continuing_old_discussion", - url: target_post.topic.url, - title: target_post.topic.title, - count: SiteSetting.disallow_reply_by_email_after_days - ) + body + "\n\n----\n\n" + + I18n.t( + "emails.incoming.continuing_old_discussion", + url: target_post.topic.url, + title: target_post.topic.title, + count: SiteSetting.disallow_reply_by_email_after_days, + ) end def forwarded_reply_key?(post_reply_key, user) - incoming_emails = IncomingEmail - .joins(:post) - .where('posts.topic_id = ?', post_reply_key.post.topic_id) - .addressed_to(post_reply_key.reply_key) - .addressed_to_user(user) - .pluck(:to_addresses, :cc_addresses) + incoming_emails = + IncomingEmail + .joins(:post) + .where("posts.topic_id = ?", post_reply_key.post.topic_id) + .addressed_to(post_reply_key.reply_key) + .addressed_to_user(user) + .pluck(:to_addresses, :cc_addresses) incoming_emails.each do |to_addresses, cc_addresses| - next unless contains_email_address_of_user?(to_addresses, user) || - contains_email_address_of_user?(cc_addresses, user) + unless contains_email_address_of_user?(to_addresses, user) || + contains_email_address_of_user?(cc_addresses, user) + next + end - return true if contains_reply_by_email_address(to_addresses, post_reply_key.reply_key) || - contains_reply_by_email_address(cc_addresses, post_reply_key.reply_key) + if contains_reply_by_email_address(to_addresses, post_reply_key.reply_key) || + contains_reply_by_email_address(cc_addresses, post_reply_key.reply_key) + return true + end end false @@ -914,10 +1005,12 @@ module Email def contains_reply_by_email_address(addresses, reply_key) return false if addresses.blank? - addresses.split(";").each do |address| - match = Email::Receiver.reply_by_email_address_regex.match(address) - return true if match && match.captures&.include?(reply_key) - end + addresses + .split(";") + .each do |address| + match = Email::Receiver.reply_by_email_address_regex.match(address) + return true if match && match.captures&.include?(reply_key) + end false end @@ -934,13 +1027,14 @@ module Email end def embedded_email - @embedded_email ||= if embedded_email_raw.present? - mail = Mail.new(embedded_email_raw) - Email::Validator.ensure_valid_address_lists!(mail) - mail - else - nil - end + @embedded_email ||= + if embedded_email_raw.present? + mail = Mail.new(embedded_email_raw) + Email::Validator.ensure_valid_address_lists!(mail) + mail + else + nil + end end def process_forwarded_email(destination, user) @@ -955,30 +1049,40 @@ module Email end end - def forwarded_email_create_topic(destination: , user: , raw: , title: , date: nil, embedded_user: nil) + def forwarded_email_create_topic( + destination:, + user:, + raw:, + title:, + date: nil, + embedded_user: nil + ) if destination.is_a?(Group) topic_user = embedded_user&.call || user - create_topic(user: topic_user, - raw: raw, - title: title, - archetype: Archetype.private_message, - target_usernames: [user.username], - target_group_names: [destination.name], - is_group_message: true, - skip_validations: true, - created_at: date) - + create_topic( + user: topic_user, + raw: raw, + title: title, + archetype: Archetype.private_message, + target_usernames: [user.username], + target_group_names: [destination.name], + is_group_message: true, + skip_validations: true, + created_at: date, + ) elsif destination.is_a?(Category) return false if user.staged? && !destination.email_in_allow_strangers return false if !user.has_trust_level?(SiteSetting.email_in_min_trust) topic_user = embedded_user&.call || user - create_topic(user: topic_user, - raw: raw, - title: title, - category: destination.id, - skip_validations: topic_user.staged?, - created_at: date) + create_topic( + user: topic_user, + raw: raw, + title: title, + category: destination.id, + skip_validations: topic_user.staged?, + created_at: date, + ) else false end @@ -994,29 +1098,40 @@ module Email return false if email.blank? || !email["@"] - post = forwarded_email_create_topic(destination: destination, - user: user, - raw: try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s, - title: embedded.subject.presence || subject, - date: embedded.date, - embedded_user: lambda { find_or_create_user(email, display_name) }) + post = + forwarded_email_create_topic( + destination: destination, + user: user, + raw: try_to_encode(embedded.decoded, "UTF-8").presence || embedded.to_s, + title: embedded.subject.presence || subject, + date: embedded.date, + embedded_user: lambda { find_or_create_user(email, display_name) }, + ) return false unless post if post.topic # mark post as seen for the forwarder - PostTiming.record_timing(user_id: user.id, topic_id: post.topic_id, post_number: post.post_number, msecs: 5000) + PostTiming.record_timing( + user_id: user.id, + topic_id: post.topic_id, + post_number: post.post_number, + msecs: 5000, + ) # create reply when available if @before_embedded.present? post_type = Post.types[:regular] - post_type = Post.types[:whisper] if post.topic.private_message? && destination.usernames[user.username] + post_type = Post.types[:whisper] if post.topic.private_message? && + destination.usernames[user.username] - create_reply(user: user, - raw: @before_embedded, - post: post, - topic: post.topic, - post_type: post_type, - skip_validations: user.staged?) + create_reply( + user: user, + raw: @before_embedded, + post: post, + topic: post.topic, + post_type: post_type, + skip_validations: user.staged?, + ) else if @forwarded_by_user post.topic.topic_allowed_users.find_or_create_by!(user_id: @forwarded_by_user.id) @@ -1050,15 +1165,28 @@ module Email [/quote] MD - return true if forwarded_email_create_topic(destination: destination, user: user, raw: raw, title: subject) + if forwarded_email_create_topic( + destination: destination, + user: user, + raw: raw, + title: subject, + ) + true + end end def self.reply_by_email_address_regex(extract_reply_key = true, include_verp = false) reply_addresses = [SiteSetting.reply_by_email_address] - reply_addresses << (SiteSetting.alternative_reply_by_email_addresses.presence || "").split("|") + reply_addresses << (SiteSetting.alternative_reply_by_email_addresses.presence || "").split( + "|", + ) - if include_verp && SiteSetting.reply_by_email_address.present? && SiteSetting.reply_by_email_address["+"] - reply_addresses << SiteSetting.reply_by_email_address.sub("%{reply_key}", "verp-%{reply_key}") + if include_verp && SiteSetting.reply_by_email_address.present? && + SiteSetting.reply_by_email_address["+"] + reply_addresses << SiteSetting.reply_by_email_address.sub( + "%{reply_key}", + "verp-%{reply_key}", + ) end reply_addresses.flatten! @@ -1074,17 +1202,22 @@ module Email end def group_incoming_emails_regex - @group_incoming_emails_regex = Regexp.union( - DB.query_single(<<~SQL).map { |e| e.split("|") }.flatten.compact_blank.uniq + @group_incoming_emails_regex = + Regexp.union(DB.query_single(<<~SQL).map { |e| e.split("|") }.flatten.compact_blank.uniq) SELECT CONCAT(incoming_email, '|', email_username) FROM groups WHERE incoming_email IS NOT NULL OR email_username IS NOT NULL SQL - ) end def category_email_in_regex - @category_email_in_regex ||= Regexp.union Category.pluck(:email_in).select(&:present?).map { |e| e.split("|") }.flatten.uniq + @category_email_in_regex ||= + Regexp.union Category + .pluck(:email_in) + .select(&:present?) + .map { |e| e.split("|") } + .flatten + .uniq end def find_related_post(force: false) @@ -1108,21 +1241,19 @@ module Email if Array === references references elsif references.present? - references.split(/[\s,]/).map do |r| - Email::MessageIdService.message_id_clean(r) - end + references.split(/[\s,]/).map { |r| Email::MessageIdService.message_id_clean(r) } end end def likes - @likes ||= Set.new ["+1", "<3", "❤", I18n.t('post_action_types.like.title').downcase] + @likes ||= Set.new ["+1", "<3", "❤", I18n.t("post_action_types.like.title").downcase] end def subscription_action_for(body, subject) return unless SiteSetting.unsubscribe_via_email return if sent_to_mailinglist_mirror? - if ([subject, body].compact.map(&:to_s).map(&:downcase) & ['unsubscribe']).any? + if ([subject, body].compact.map(&:to_s).map(&:downcase) & ["unsubscribe"]).any? :confirm_unsubscribe end end @@ -1132,9 +1263,7 @@ module Email end def create_topic(options = {}) - if options[:archetype] == Archetype.private_message - enable_email_pm_setting(options[:user]) - end + enable_email_pm_setting(options[:user]) if options[:archetype] == Archetype.private_message create_post_with_attachments(options) end @@ -1151,26 +1280,36 @@ module Email NotificationLevels.topic_levels[:tracking] when "watch" NotificationLevels.topic_levels[:watching] - else nil + else + nil end end def create_reply(options = {}) raise TopicNotFoundError if options[:topic].nil? || options[:topic].trashed? - raise BouncedEmailError if options[:bounce] && options[:topic].archetype != Archetype.private_message + if options[:bounce] && options[:topic].archetype != Archetype.private_message + raise BouncedEmailError + end options[:post] = nil if options[:post]&.trashed? - enable_email_pm_setting(options[:user]) if options[:topic].archetype == Archetype.private_message + if options[:topic].archetype == Archetype.private_message + enable_email_pm_setting(options[:user]) + end if post_action_type = post_action_for(options[:raw]) create_post_action(options[:user], options[:post], post_action_type) elsif notification_level = notification_level_for(options[:raw]) - TopicUser.change(options[:user].id, options[:post].topic_id, notification_level: notification_level) + TopicUser.change( + options[:user].id, + options[:post].topic_id, + notification_level: notification_level, + ) else raise TopicClosedError if options[:topic].closed? options[:topic_id] = options[:topic].id options[:reply_to_post_number] = options[:post]&.post_number - options[:is_group_message] = options[:topic].private_message? && options[:topic].allowed_groups.exists? + options[:is_group_message] = options[:topic].private_message? && + options[:topic].allowed_groups.exists? create_post_with_attachments(options) end end @@ -1182,21 +1321,20 @@ module Email def is_allowed?(attachment) attachment.content_type !~ SiteSetting.blocked_attachment_content_types_regex && - attachment.filename !~ SiteSetting.blocked_attachment_filenames_regex + attachment.filename !~ SiteSetting.blocked_attachment_filenames_regex end def attachments - @attachments ||= begin - attachments = @mail.attachments.select { |attachment| is_allowed?(attachment) } - attachments << @mail if @mail.attachment? && is_allowed?(@mail) + @attachments ||= + begin + attachments = @mail.attachments.select { |attachment| is_allowed?(attachment) } + attachments << @mail if @mail.attachment? && is_allowed?(@mail) - @mail.parts.each do |part| - attachments << part if part.attachment? && is_allowed?(part) + @mail.parts.each { |part| attachments << part if part.attachment? && is_allowed?(part) } + + attachments.uniq! + attachments end - - attachments.uniq! - attachments - end end def create_post_with_attachments(options = {}) @@ -1223,10 +1361,13 @@ module Email if raw[attachment.url] raw.sub!(attachment.url, upload.url) - InlineUploads.match_img(raw, uploads: { upload.url => upload }) do |match, src, replacement, _| - if src == upload.url - raw = raw.sub(match, replacement) - end + InlineUploads.match_img( + raw, + uploads: { + upload.url => upload, + }, + ) do |match, src, replacement, _| + raw = raw.sub(match, replacement) if src == upload.url end elsif raw[/\[image:[^\]]*\]/i] raw.sub!(/\[image:[^\]]*\]/i, UploadMarkdown.new(upload).to_markdown) @@ -1238,13 +1379,15 @@ module Email end else rejected_attachments << upload - raw << "\n\n#{I18n.t('emails.incoming.missing_attachment', filename: upload.original_filename)}\n\n" + raw << "\n\n#{I18n.t("emails.incoming.missing_attachment", filename: upload.original_filename)}\n\n" end ensure tmp&.close! end end - notify_about_rejected_attachment(rejected_attachments) if rejected_attachments.present? && !user.staged? + if rejected_attachments.present? && !user.staged? + notify_about_rejected_attachment(rejected_attachments) + end raw end @@ -1262,19 +1405,21 @@ module Email former_title: message.subject, destination: message.to, site_name: SiteSetting.title, - rejected_errors: errors.join("\n") + rejected_errors: errors.join("\n"), } - client_message = RejectionMailer.send_rejection(:email_reject_attachment, message.from, template_args) + client_message = + RejectionMailer.send_rejection(:email_reject_attachment, message.from, template_args) Email::Sender.new(client_message, :email_reject_attachment).send end def add_elided_to_raw!(options) - is_private_message = options[:archetype] == Archetype.private_message || - options[:topic].try(:private_message?) + is_private_message = + options[:archetype] == Archetype.private_message || options[:topic].try(:private_message?) # only add elided part in messages - if options[:elided].present? && (SiteSetting.always_show_trimmed_content || is_private_message) + if options[:elided].present? && + (SiteSetting.always_show_trimmed_content || is_private_message) options[:raw] << Email::Receiver.elided_html(options[:elided]) options[:elided] = "" end @@ -1303,7 +1448,11 @@ module Email user = options.delete(:user) if options[:bounce] - options[:raw] = I18n.t("system_messages.email_bounced", email: user.email, raw: options[:raw]) + options[:raw] = I18n.t( + "system_messages.email_bounced", + email: user.email, + raw: options[:raw], + ) user = Discourse.system_user options[:post_type] = Post.types[:whisper] end @@ -1316,16 +1465,16 @@ module Email result = NewPostManager.new(user, options).perform errors = result.errors.full_messages - if errors.any? do |message| + if errors.any? { |message| message.include?(I18n.t("activerecord.attributes.post.raw").strip) && - message.include?(I18n.t("errors.messages.too_short", count: SiteSetting.min_post_length).strip) - end + message.include?( + I18n.t("errors.messages.too_short", count: SiteSetting.min_post_length).strip, + ) + } raise TooShortPost end - if result.errors.present? - raise InvalidPost, errors.join("\n") - end + raise InvalidPost, errors.join("\n") if result.errors.present? if result.post IncomingEmail.transaction do @@ -1343,11 +1492,16 @@ module Email # Alert the people involved in the topic now that the incoming email # has been linked to the post. - PostJobsEnqueuer.new(result.post, result.post.topic, options[:topic_id].blank?, - import_mode: options[:import_mode], - post_alert_options: options[:post_alert_options] - ).enqueue_jobs - DiscourseEvent.trigger(:topic_created, result.post.topic, options, user) if result.post.is_first_post? + PostJobsEnqueuer.new( + result.post, + result.post.topic, + options[:topic_id].blank?, + import_mode: options[:import_mode], + post_alert_options: options[:post_alert_options], + ).enqueue_jobs + if result.post.is_first_post? + DiscourseEvent.trigger(:topic_created, result.post.topic, options, user) + end DiscourseEvent.trigger(:post_created, result.post, options, user) end @@ -1355,8 +1509,8 @@ module Email end def self.elided_html(elided) - html = +"\n\n" << "
    " << "\n" - html << "···" << "\n\n" + html = +"\n\n" << "
    " << "\n" + html << "···" << "\n\n" html << elided << "\n\n" html << "
    " << "\n" html @@ -1365,7 +1519,7 @@ module Email def add_other_addresses(post, sender, mail_object) max_staged_users_post = nil - %i(to cc bcc).each do |d| + %i[to cc bcc].each do |d| next if mail_object[d].blank? mail_object[d].each do |address_field| @@ -1379,16 +1533,31 @@ module Email user = User.find_by_email(email) # cap number of staged users created per email - if (!user || user.staged) && @staged_users.count >= SiteSetting.maximum_staged_users_per_email - max_staged_users_post ||= post.topic.add_moderator_post(sender, I18n.t("emails.incoming.maximum_staged_user_per_email_reached"), import_mode: @opts[:import_mode]) + if (!user || user.staged) && + @staged_users.count >= SiteSetting.maximum_staged_users_per_email + max_staged_users_post ||= + post.topic.add_moderator_post( + sender, + I18n.t("emails.incoming.maximum_staged_user_per_email_reached"), + import_mode: @opts[:import_mode], + ) next end user = find_or_create_user(email, display_name, user: user) if user && can_invite?(post.topic, user) post.topic.topic_allowed_users.create!(user_id: user.id) - TopicUser.auto_notification_for_staging(user.id, post.topic_id, TopicUser.notification_reasons[:auto_watch]) - post.topic.add_small_action(sender, "invited_user", user.username, import_mode: @opts[:import_mode]) + TopicUser.auto_notification_for_staging( + user.id, + post.topic_id, + TopicUser.notification_reasons[:auto_watch], + ) + post.topic.add_small_action( + sender, + "invited_user", + user.username, + import_mode: @opts[:import_mode], + ) end end rescue ActiveRecord::RecordInvalid, EmailNotAllowed @@ -1400,13 +1569,15 @@ module Email def should_invite?(email) email !~ Email::Receiver.reply_by_email_address_regex && - email !~ group_incoming_emails_regex && - email !~ category_email_in_regex + email !~ group_incoming_emails_regex && email !~ category_email_in_regex end def can_invite?(topic, user) !topic.topic_allowed_users.where(user_id: user.id).exists? && - !topic.topic_allowed_groups.where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id).exists? + !topic + .topic_allowed_groups + .where("group_id IN (SELECT group_id FROM group_users WHERE user_id = ?)", user.id) + .exists? end def send_subscription_mail(action, user) @@ -1419,26 +1590,21 @@ module Email end def stage_sender_user(email, display_name) - find_or_create_user!(email, display_name).tap do |u| - log_and_validate_user(u) - end + find_or_create_user!(email, display_name).tap { |u| log_and_validate_user(u) } end def delete_created_staged_users @created_staged_users.each do |user| - if @incoming_email.user&.id == user.id - @incoming_email.update_columns(user_id: nil) - end + @incoming_email.update_columns(user_id: nil) if @incoming_email.user&.id == user.id - if user.posts.count == 0 - UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) - end + UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) if user.posts.count == 0 end end def enable_email_pm_setting(user) # ensure user PM emails are enabled (since user is posting via email) - if !user.staged && user.user_option.email_messages_level == UserOption.email_level_types[:never] + if !user.staged && + user.user_option.email_messages_level == UserOption.email_level_types[:never] user.user_option.update!(email_messages_level: UserOption.email_level_types[:always]) end end diff --git a/lib/email/renderer.rb b/lib/email/renderer.rb index feeb63c411..a375176e2c 100644 --- a/lib/email/renderer.rb +++ b/lib/email/renderer.rb @@ -2,7 +2,6 @@ module Email class Renderer - def initialize(message, opts = nil) @message = message @opts = opts || {} @@ -10,26 +9,30 @@ module Email def text return @text if @text - @text = (+(@message.text_part ? @message.text_part : @message).body.to_s).force_encoding('UTF-8') + @text = + (+(@message.text_part ? @message.text_part : @message).body.to_s).force_encoding("UTF-8") @text = CGI.unescapeHTML(@text) end def html - style = if @message.html_part - Email::Styles.new(@message.html_part.body.to_s, @opts) - else - unstyled = UserNotificationRenderer.render( - template: 'layouts/email_template', - format: :html, - locals: { html_body: PrettyText.cook(text).html_safe } - ) - Email::Styles.new(unstyled, @opts) - end + style = + if @message.html_part + Email::Styles.new(@message.html_part.body.to_s, @opts) + else + unstyled = + UserNotificationRenderer.render( + template: "layouts/email_template", + format: :html, + locals: { + html_body: PrettyText.cook(text).html_safe, + }, + ) + Email::Styles.new(unstyled, @opts) + end style.format_basic style.format_html style.to_html end - end end diff --git a/lib/email/sender.rb b/lib/email/sender.rb index c31df8b76a..1f3c59efd0 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -8,11 +8,11 @@ # # It also adds an HTML part for the plain text body # -require 'uri' -require 'net/smtp' +require "uri" +require "net/smtp" SMTP_CLIENT_ERRORS = [Net::SMTPFatalError, Net::SMTPSyntaxError] -BYPASS_DISABLE_TYPES = %w( +BYPASS_DISABLE_TYPES = %w[ admin_login test_message new_version @@ -20,11 +20,10 @@ BYPASS_DISABLE_TYPES = %w( invite_password_instructions download_backup_message admin_confirmation_message -) +] module Email class Sender - def initialize(message, email_type, user = nil) @message = message @message_attachments_index = {} @@ -35,33 +34,40 @@ module Email def send bypass_disable = BYPASS_DISABLE_TYPES.include?(@email_type.to_s) - if SiteSetting.disable_emails == "yes" && !bypass_disable + return if SiteSetting.disable_emails == "yes" && !bypass_disable + + return if ActionMailer::Base::NullMail === @message + if ActionMailer::Base::NullMail === + ( + begin + @message.message + rescue StandardError + nil + end + ) return end - return if ActionMailer::Base::NullMail === @message - return if ActionMailer::Base::NullMail === (@message.message rescue nil) - - return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank? + return skip(SkippedEmailLog.reason_types[:sender_message_blank]) if @message.blank? return skip(SkippedEmailLog.reason_types[:sender_message_to_blank]) if @message.to.blank? if SiteSetting.disable_emails == "non-staff" && !bypass_disable return unless find_user&.staff? end - return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) if to_address.end_with?(".invalid") + if to_address.end_with?(".invalid") + return skip(SkippedEmailLog.reason_types[:sender_message_to_invalid]) + end if @message.text_part if @message.text_part.body.to_s.blank? return skip(SkippedEmailLog.reason_types[:sender_text_part_body_blank]) end else - if @message.body.to_s.blank? - return skip(SkippedEmailLog.reason_types[:sender_body_blank]) - end + return skip(SkippedEmailLog.reason_types[:sender_body_blank]) if @message.body.to_s.blank? end - @message.charset = 'UTF-8' + @message.charset = "UTF-8" opts = {} @@ -70,50 +76,58 @@ module Email if @message.html_part @message.html_part.body = renderer.html else - @message.html_part = Mail::Part.new do - content_type 'text/html; charset=UTF-8' - body renderer.html - end + @message.html_part = + Mail::Part.new do + content_type "text/html; charset=UTF-8" + body renderer.html + end end # Fix relative (ie upload) HTML links in markdown which do not work well in plain text emails. # These are the links we add when a user uploads a file or image. # Ideally we would parse general markdown into plain text, but that is almost an intractable problem. url_prefix = Discourse.base_url - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/([^<]*)<\/a>/, '[\2|attachment](' + url_prefix + '\1)') - @message.parts[0].body = @message.parts[0].body.to_s.gsub(/]*)>/, '![](' + url_prefix + '\1)') + @message.parts[0].body = + @message.parts[0].body.to_s.gsub( + %r{([^<]*)}, + '[\2|attachment](' + url_prefix + '\1)', + ) + @message.parts[0].body = + @message.parts[0].body.to_s.gsub( + %r{]*)>}, + "![](" + url_prefix + '\1)', + ) - @message.text_part.content_type = 'text/plain; charset=UTF-8' + @message.text_part.content_type = "text/plain; charset=UTF-8" user_id = @user&.id # Set up the email log - email_log = EmailLog.new( - email_type: @email_type, - to_address: to_address, - user_id: user_id - ) + email_log = EmailLog.new(email_type: @email_type, to_address: to_address, user_id: user_id) if cc_addresses.any? email_log.cc_addresses = cc_addresses.join(";") email_log.cc_user_ids = User.with_email(cc_addresses).pluck(:id) end - if bcc_addresses.any? - email_log.bcc_addresses = bcc_addresses.join(";") - end + email_log.bcc_addresses = bcc_addresses.join(";") if bcc_addresses.any? host = Email::Sender.host_for(Discourse.base_url) - post_id = header_value('X-Discourse-Post-Id') - topic_id = header_value('X-Discourse-Topic-Id') + post_id = header_value("X-Discourse-Post-Id") + topic_id = header_value("X-Discourse-Topic-Id") reply_key = get_reply_key(post_id, user_id) from_address = @message.from&.first - smtp_group_id = from_address.blank? ? nil : Group.where( - email_username: from_address, smtp_enabled: true - ).pluck_first(:id) + smtp_group_id = + ( + if from_address.blank? + nil + else + Group.where(email_username: from_address, smtp_enabled: true).pluck_first(:id) + end + ) # always set a default Message ID from the host - @message.header['Message-ID'] = Email::MessageIdService.generate_default + @message.header["Message-ID"] = Email::MessageIdService.generate_default if topic_id.present? && post_id.present? post = Post.find_by(id: post_id, topic_id: topic_id) @@ -130,12 +144,14 @@ module Email # See https://www.ietf.org/rfc/rfc2919.txt for the List-ID # specification. if topic&.category && !topic.category.uncategorized? - list_id = "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{host}>" + list_id = + "#{SiteSetting.title} | #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{host}>" # subcategory case if !topic.category.parent_category_id.nil? parent_category_name = Category.find_by(id: topic.category.parent_category_id).name - list_id = "#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(' ', '-')}.#{parent_category_name.downcase.tr(' ', '-')}.#{host}>" + list_id = + "#{SiteSetting.title} | #{parent_category_name} #{topic.category.name} <#{topic.category.name.downcase.tr(" ", "-")}.#{parent_category_name.downcase.tr(" ", "-")}.#{host}>" end else list_id = "#{SiteSetting.title} <#{host}>" @@ -148,16 +164,15 @@ module Email # conversation between the group and a small handful of people # directly contacting the group, often just one person. if !smtp_group_id - # https://www.ietf.org/rfc/rfc3834.txt - @message.header['Precedence'] = 'list' - @message.header['List-ID'] = list_id + @message.header["Precedence"] = "list" + @message.header["List-ID"] = list_id if topic if SiteSetting.private_email? - @message.header['List-Archive'] = "#{Discourse.base_url}#{topic.slugless_url}" + @message.header["List-Archive"] = "#{Discourse.base_url}#{topic.slugless_url}" else - @message.header['List-Archive'] = topic.url + @message.header["List-Archive"] = topic.url end end end @@ -176,61 +191,59 @@ module Email email_log.topic_id = topic_id if topic_id.present? if reply_key.present? - @message.header['Reply-To'] = header_value('Reply-To').gsub!("%{reply_key}", reply_key) + @message.header["Reply-To"] = header_value("Reply-To").gsub!("%{reply_key}", reply_key) @message.header[Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER] = nil end - MessageBuilder.custom_headers(SiteSetting.email_custom_headers).each do |key, _| - # Any custom headers added via MessageBuilder that are doubled up here - # with values that we determine should be set to the last value, which is - # the one we determined. Our header values should always override the email_custom_headers. - # - # While it is valid via RFC5322 to have more than one value for certain headers, - # we just want to keep it to one, especially in cases where the custom value - # would conflict with our own. - # - # See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and - # https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132 - custom_header = @message.header[key] - if custom_header.is_a?(Array) - our_value = custom_header.last.value + MessageBuilder + .custom_headers(SiteSetting.email_custom_headers) + .each do |key, _| + # Any custom headers added via MessageBuilder that are doubled up here + # with values that we determine should be set to the last value, which is + # the one we determined. Our header values should always override the email_custom_headers. + # + # While it is valid via RFC5322 to have more than one value for certain headers, + # we just want to keep it to one, especially in cases where the custom value + # would conflict with our own. + # + # See https://datatracker.ietf.org/doc/html/rfc5322#section-3.6 and + # https://github.com/mikel/mail/blob/8ef377d6a2ca78aa5bd7f739813f5a0648482087/lib/mail/header.rb#L109-L132 + custom_header = @message.header[key] + if custom_header.is_a?(Array) + our_value = custom_header.last.value - # Must be set to nil first otherwise another value is just added - # to the array of values for the header. - @message.header[key] = nil - @message.header[key] = our_value - end + # Must be set to nil first otherwise another value is just added + # to the array of values for the header. + @message.header[key] = nil + @message.header[key] = our_value + end - value = header_value(key) + value = header_value(key) - # Remove Auto-Submitted header for group private message emails, it does - # not make sense there and may hurt deliverability. - # - # From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml: - # - # > Indicates that a message was generated by an automatic process, and is not a direct response to another message. - if key.downcase == "auto-submitted" && smtp_group_id - @message.header[key] = nil - end + # Remove Auto-Submitted header for group private message emails, it does + # not make sense there and may hurt deliverability. + # + # From https://www.iana.org/assignments/auto-submitted-keywords/auto-submitted-keywords.xhtml: + # + # > Indicates that a message was generated by an automatic process, and is not a direct response to another message. + @message.header[key] = nil if key.downcase == "auto-submitted" && smtp_group_id - # Replace reply_key in custom headers or remove - if value&.include?('%{reply_key}') - # Delete old header first or else the same header will be added twice - @message.header[key] = nil - if reply_key.present? - @message.header[key] = value.gsub!('%{reply_key}', reply_key) + # Replace reply_key in custom headers or remove + if value&.include?("%{reply_key}") + # Delete old header first or else the same header will be added twice + @message.header[key] = nil + @message.header[key] = value.gsub!("%{reply_key}", reply_key) if reply_key.present? end end - end # pass the original message_id when using mailjet/mandrill/sparkpost case ActionMailer::Base.smtp_settings[:address] when /\.mailjet\.com/ - @message.header['X-MJ-CustomID'] = @message.message_id + @message.header["X-MJ-CustomID"] = @message.message_id when "smtp.mandrillapp.com" - merge_json_x_header('X-MC-Metadata', message_id: @message.message_id) + merge_json_x_header("X-MC-Metadata", message_id: @message.message_id) when "smtp.sparkpostmail.com" - merge_json_x_header('X-MSYS-API', metadata: { message_id: @message.message_id }) + merge_json_x_header("X-MSYS-API", metadata: { message_id: @message.message_id }) end # Parse the HTML again so we can make any final changes before @@ -239,8 +252,8 @@ module Email # Suppress images from short emails if SiteSetting.strip_images_from_short_emails && - @message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length && - @message.html_part.body =~ /]+>/ + @message.html_part.body.to_s.bytesize <= SiteSetting.short_email_length && + @message.html_part.body =~ /]+>/ style.strip_avatars_and_emojis end @@ -291,23 +304,26 @@ module Email end def to_address - @to_address ||= begin - to = @message.try(:to) - to = to.first if Array === to - to.presence || "no_email_found" - end + @to_address ||= + begin + to = @message.try(:to) + to = to.first if Array === to + to.presence || "no_email_found" + end end def cc_addresses - @cc_addresses ||= begin - @message.try(:cc) || [] - end + @cc_addresses ||= + begin + @message.try(:cc) || [] + end end def bcc_addresses - @bcc_addresses ||= begin - @message.try(:bcc) || [] - end + @bcc_addresses ||= + begin + @message.try(:bcc) || [] + end end def self.host_for(base_url) @@ -333,7 +349,7 @@ module Email optimized_1X = original_upload.optimized_images.first if FileHelper.is_supported_image?(original_upload.original_filename) && - !should_attach_image?(original_upload, optimized_1X) + !should_attach_image?(original_upload, optimized_1X) next end @@ -341,11 +357,12 @@ module Email next if email_size + attached_upload.filesize > max_email_size begin - path = if attached_upload.local? - Discourse.store.path_for(attached_upload) - else - Discourse.store.download(attached_upload).path - end + path = + if attached_upload.local? + Discourse.store.path_for(attached_upload) + else + Discourse.store.download(attached_upload).path + end @message_attachments_index[original_upload.sha1] = @message.attachments.size @message.attachments[original_upload.original_filename] = File.read(path) @@ -357,8 +374,8 @@ module Email env: { post_id: post.id, upload_id: original_upload.id, - filename: original_upload.original_filename - } + filename: original_upload.original_filename, + }, ) end end @@ -368,7 +385,10 @@ module Email def should_attach_image?(upload, optimized_1X = nil) return if !SiteSetting.secure_uploads_allow_embed_images_in_emails || !upload.secure? - return if (optimized_1X&.filesize || upload.filesize) > SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes + if (optimized_1X&.filesize || upload.filesize) > + SiteSetting.secure_uploads_max_email_embed_image_size_kb.kilobytes + return + end true end @@ -391,8 +411,7 @@ module Email # def fix_parts_after_attachments! has_attachments = @message.attachments.present? - has_alternative_renderings = - @message.html_part.present? && @message.text_part.present? + has_alternative_renderings = @message.html_part.present? && @message.text_part.present? if has_attachments && has_alternative_renderings @message.content_type = "multipart/mixed" @@ -403,15 +422,16 @@ module Email text_part = @message.text_part @message.text_part = nil - content = Mail::Part.new do - content_type "multipart/alternative" + content = + Mail::Part.new do + content_type "multipart/alternative" - # we have to re-specify the charset and give the part the decoded body - # here otherwise the parts will get encoded with US-ASCII which makes - # a bunch of characters not render correctly in the email - part content_type: "text/html; charset=utf-8", body: html_part.body.decoded - part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded - end + # we have to re-specify the charset and give the part the decoded body + # here otherwise the parts will get encoded with US-ASCII which makes + # a bunch of characters not render correctly in the email + part content_type: "text/html; charset=utf-8", body: html_part.body.decoded + part content_type: "text/plain; charset=utf-8", body: text_part.body.decoded + end @message.parts.unshift(content) end @@ -437,7 +457,7 @@ module Email email_type: @email_type, to_address: to_address, user_id: @user&.id, - reason_type: reason_type + reason_type: reason_type, } attributes[:custom_reason] = custom_reason if custom_reason @@ -445,7 +465,12 @@ module Email end def merge_json_x_header(name, value) - data = JSON.parse(@message.header[name].to_s) rescue nil + data = + begin + JSON.parse(@message.header[name].to_s) + rescue StandardError + nil + end data ||= {} data.merge!(value) # /!\ @message.header is not a standard ruby hash. @@ -460,12 +485,12 @@ module Email def get_reply_key(post_id, user_id) # ALLOW_REPLY_BY_EMAIL_HEADER is only added if we are _not_ sending # via group SMTP and if reply by email site settings are configured - return if !user_id || !post_id || !header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present? + if !user_id || !post_id || + !header_value(Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER).present? + return + end - PostReplyKey.create_or_find_by!( - post_id: post_id, - user_id: user_id - ).reply_key + PostReplyKey.create_or_find_by!(post_id: post_id, user_id: user_id).reply_key end def self.bounceable_reply_address? @@ -514,7 +539,9 @@ module Email # # https://meta.discourse.org/t/discourse-email-messages-are-incorrectly-threaded/233499 def add_identification_field_headers(topic, post) - @message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing(post.id).first + @message.header["Message-ID"] = Email::MessageIdService.generate_or_use_existing( + post.id, + ).first if post.post_number > 1 op_message_id = Email::MessageIdService.generate_or_use_existing(topic.first_post.id).first @@ -523,11 +550,12 @@ module Email # Whenever we reply to a post directly _or_ quote a post, a PostReply # record is made, with the reply_post_id referencing the newly created # post, and the post_id referencing the post that was quoted or replied to. - referenced_posts = Post - .joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ") - .where("post_replies.reply_post_id = ?", post.id) - .order(id: :desc) - .to_a + referenced_posts = + Post + .joins("INNER JOIN post_replies ON post_replies.post_id = posts.id ") + .where("post_replies.reply_post_id = ?", post.id) + .order(id: :desc) + .to_a ## # No referenced posts means that we are just creating a new post not @@ -543,7 +571,8 @@ module Email # every directly replied to post can go into In-Reply-To. # # We want to make sure all of the outbound_message_ids are already filled here. - in_reply_to_message_ids = MessageIdService.generate_or_use_existing(referenced_posts.map(&:id)) + in_reply_to_message_ids = + MessageIdService.generate_or_use_existing(referenced_posts.map(&:id)) @message.header["In-Reply-To"] = in_reply_to_message_ids most_recent_post_message_id = in_reply_to_message_ids.last @@ -559,7 +588,9 @@ module Email parent_message_ids = MessageIdService.generate_or_use_existing(reply_tree.values.flatten) @message.header["References"] = [ - op_message_id, parent_message_ids, most_recent_post_message_id + op_message_id, + parent_message_ids, + most_recent_post_message_id, ].flatten.uniq end end diff --git a/lib/email/styles.rb b/lib/email/styles.rb index cd561eb8b5..ee240b981e 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -7,7 +7,8 @@ module Email class Styles MAX_IMAGE_DIMENSION = 400 - ONEBOX_IMAGE_BASE_STYLE = "max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;" + ONEBOX_IMAGE_BASE_STYLE = + "max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;" ONEBOX_IMAGE_THUMBNAIL_STYLE = "width: 60px;" ONEBOX_INLINE_AVATAR_STYLE = "width: 20px; height: 20px; float: none; vertical-align: middle;" @@ -29,12 +30,12 @@ module Email end def add_styles(node, new_styles) - existing = node['style'] + existing = node["style"] if existing.present? # merge styles - node['style'] = "#{new_styles}; #{existing}" + node["style"] = "#{new_styles}; #{existing}" else - node['style'] = new_styles + node["style"] = new_styles end end @@ -47,12 +48,12 @@ module Email if !css.blank? # there is a minor race condition here, CssParser could be # loaded by ::CssParser::Parser not loaded - require 'css_parser' unless defined?(::CssParser::Parser) + require "css_parser" unless defined?(::CssParser::Parser) parser = ::CssParser::Parser.new(import: false) parser.load_string!(css) parser.each_selector do |selector, value| - @custom_styles[selector] ||= +'' + @custom_styles[selector] ||= +"" @custom_styles[selector] << value end end @@ -67,118 +68,148 @@ module Email @fragment.css('svg, img[src$=".svg"]').remove # images - @fragment.css('img').each do |img| - next if img['class'] == 'site-logo' + @fragment + .css("img") + .each do |img| + next if img["class"] == "site-logo" - if (img['class'] && img['class']['emoji']) || (img['src'] && img['src'][/\/_?emoji\//]) - img['width'] = img['height'] = 20 - else - # use dimensions of original iPhone screen for 'too big, let device rescale' - if img['width'].to_i > (320) || img['height'].to_i > (480) - img['width'] = img['height'] = 'auto' + if (img["class"] && img["class"]["emoji"]) || (img["src"] && img["src"][%r{/_?emoji/}]) + img["width"] = img["height"] = 20 + else + # use dimensions of original iPhone screen for 'too big, let device rescale' + if img["width"].to_i > (320) || img["height"].to_i > (480) + img["width"] = img["height"] = "auto" + end + end + + if img["src"] + # ensure all urls are absolute + img["src"] = "#{Discourse.base_url}#{img["src"]}" if img["src"][%r{^/[^/]}] + # ensure no schemaless urls + img["src"] = "#{uri.scheme}:#{img["src"]}" if img["src"][%r{^//}] end end - if img['src'] - # ensure all urls are absolute - img['src'] = "#{Discourse.base_url}#{img['src']}" if img['src'][/^\/[^\/]/] - # ensure no schemaless urls - img['src'] = "#{uri.scheme}:#{img['src']}" if img['src'][/^\/\//] - end - end - # add max-width to big images - big_images = @fragment.css('img[width="auto"][height="auto"]') - - @fragment.css('aside.onebox img') - - @fragment.css('img.site-logo, img.emoji') - big_images.each do |img| - add_styles(img, 'max-width: 100%;') if img['style'] !~ /max-width/ - end + big_images = + @fragment.css('img[width="auto"][height="auto"]') - @fragment.css("aside.onebox img") - + @fragment.css("img.site-logo, img.emoji") + big_images.each { |img| add_styles(img, "max-width: 100%;") if img["style"] !~ /max-width/ } # topic featured link - @fragment.css('a.topic-featured-link').each do |e| - e['style'] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);" - end + @fragment + .css("a.topic-featured-link") + .each do |e| + e[ + "style" + ] = "color:#858585;padding:2px 8px;border:1px solid #e6e6e6;border-radius:2px;box-shadow:0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);" + end # attachments - @fragment.css('a.attachment').each do |a| - # ensure all urls are absolute - if a['href'] =~ /^\/[^\/]/ - a['href'] = "#{Discourse.base_url}#{a['href']}" - end + @fragment + .css("a.attachment") + .each do |a| + # ensure all urls are absolute + a["href"] = "#{Discourse.base_url}#{a["href"]}" if a["href"] =~ %r{^/[^/]} - # ensure no schemaless urls - if a['href'] && a['href'].starts_with?("//") - a['href'] = "#{uri.scheme}:#{a['href']}" + # ensure no schemaless urls + a["href"] = "#{uri.scheme}:#{a["href"]}" if a["href"] && a["href"].starts_with?("//") end - end end def onebox_styles # Links to other topics - style('aside.quote', 'padding: 12px 25px 2px 12px; margin-bottom: 10px;') - style('aside.quote div.info-line', 'color: #666; margin: 10px 0') - style('aside.quote .avatar', 'margin-right: 5px; width:20px; height:20px; vertical-align:middle;') - style('aside.quote', 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;') + style("aside.quote", "padding: 12px 25px 2px 12px; margin-bottom: 10px;") + style("aside.quote div.info-line", "color: #666; margin: 10px 0") + style( + "aside.quote .avatar", + "margin-right: 5px; width:20px; height:20px; vertical-align:middle;", + ) + style("aside.quote", "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin: 0;") - style('blockquote', 'border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;') + style( + "blockquote", + "border-left: 5px solid #e9e9e9; background-color: #f8f8f8; margin-left: 0; padding: 12px;", + ) # Oneboxes - style('aside.onebox', "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;") - style('aside.onebox header img.site-icon', "width: 16px; height: 16px; margin-right: 3px;") - style('aside.onebox header a[href]', "color: #222222; text-decoration: none;") - style('aside.onebox .onebox-body', "clear: both") - style('aside.onebox .onebox-body img:not(.onebox-avatar-inline)', ONEBOX_IMAGE_BASE_STYLE) - style('aside.onebox .onebox-body img.thumbnail', ONEBOX_IMAGE_THUMBNAIL_STYLE) - style('aside.onebox .onebox-body h3, aside.onebox .onebox-body h4', "font-size: 1.17em; margin: 10px 0;") - style('.onebox-metadata', "color: #919191") - style('.github-info', "margin-top: 10px;") - style('.github-info .added', "color: #090;") - style('.github-info .removed', "color: #e45735;") - style('.github-info div', "display: inline; margin-right: 10px;") - style('.github-icon-container', 'float: left;') - style('.github-icon-container *', 'fill: #646464; width: 40px; height: 40px;') - style('.github-body-container', 'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;') - style('.onebox-avatar-inline', ONEBOX_INLINE_AVATAR_STYLE) + style( + "aside.onebox", + "border: 5px solid #e9e9e9; padding: 12px 25px 12px 12px; margin-bottom: 10px;", + ) + style("aside.onebox header img.site-icon", "width: 16px; height: 16px; margin-right: 3px;") + style("aside.onebox header a[href]", "color: #222222; text-decoration: none;") + style("aside.onebox .onebox-body", "clear: both") + style("aside.onebox .onebox-body img:not(.onebox-avatar-inline)", ONEBOX_IMAGE_BASE_STYLE) + style("aside.onebox .onebox-body img.thumbnail", ONEBOX_IMAGE_THUMBNAIL_STYLE) + style( + "aside.onebox .onebox-body h3, aside.onebox .onebox-body h4", + "font-size: 1.17em; margin: 10px 0;", + ) + style(".onebox-metadata", "color: #919191") + style(".github-info", "margin-top: 10px;") + style(".github-info .added", "color: #090;") + style(".github-info .removed", "color: #e45735;") + style(".github-info div", "display: inline; margin-right: 10px;") + style(".github-icon-container", "float: left;") + style(".github-icon-container *", "fill: #646464; width: 40px; height: 40px;") + style( + ".github-body-container", + 'font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; margin-top: 1em !important;', + ) + style(".onebox-avatar-inline", ONEBOX_INLINE_AVATAR_STYLE) - @fragment.css('.github-body-container .excerpt').remove + @fragment.css(".github-body-container .excerpt").remove - @fragment.css('aside.quote blockquote > p').each do |p| - p['style'] = 'padding: 0;' - end + @fragment.css("aside.quote blockquote > p").each { |p| p["style"] = "padding: 0;" } # Convert all `aside.quote` tags to `blockquote`s - @fragment.css('aside.quote').each do |n| - original_node = n.dup - original_node.search('div.quote-controls').remove - blockquote = original_node.css('blockquote').inner_html.strip.start_with?("#{original_node.css('blockquote').inner_html}

    " - n.inner_html = original_node.css('div.title').inner_html + blockquote - n.name = "blockquote" - end + @fragment + .css("aside.quote") + .each do |n| + original_node = n.dup + original_node.search("div.quote-controls").remove + blockquote = + ( + if original_node.css("blockquote").inner_html.strip.start_with?("#{original_node.css("blockquote").inner_html}

    " + end + ) + n.inner_html = original_node.css("div.title").inner_html + blockquote + n.name = "blockquote" + end # Finally, convert all `aside` tags to `div`s - @fragment.css('aside, article, header').each do |n| - n.name = "div" - end + @fragment.css("aside, article, header").each { |n| n.name = "div" } # iframes can't go in emails, so replace them with clickable links - @fragment.css('iframe').each do |i| - begin - # sometimes, iframes are blocklisted... - if i["src"].blank? - i.remove - next - end + @fragment + .css("iframe") + .each do |i| + begin + # sometimes, iframes are blocklisted... + if i["src"].blank? + i.remove + next + end - src_uri = i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i['src']) - # If an iframe is protocol relative, use SSL when displaying it - display_src = "#{src_uri.scheme || 'https'}://#{src_uri.host}#{src_uri.path}#{src_uri.query.nil? ? '' : '?' + src_uri.query}#{src_uri.fragment.nil? ? '' : '#' + src_uri.fragment}" - i.replace(Nokogiri::HTML5.fragment("

    #{CGI.escapeHTML(display_src)}

    ")) - rescue URI::Error - # If the URL is weird, remove the iframe - i.remove + src_uri = + i["data-original-href"].present? ? URI(i["data-original-href"]) : URI(i["src"]) + # If an iframe is protocol relative, use SSL when displaying it + display_src = + "#{src_uri.scheme || "https"}://#{src_uri.host}#{src_uri.path}#{src_uri.query.nil? ? "" : "?" + src_uri.query}#{src_uri.fragment.nil? ? "" : "#" + src_uri.fragment}" + i.replace( + Nokogiri::HTML5.fragment( + "

    #{CGI.escapeHTML(display_src)}

    ", + ), + ) + rescue URI::Error + # If the URL is weird, remove the iframe + i.remove + end end - end end def format_html @@ -189,67 +220,93 @@ module Email reset_tables html_lang = SiteSetting.default_locale.sub("_", "-") - style('html', nil, lang: html_lang, 'xml:lang' => html_lang) - style('body', "line-height: 1.4; text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };") - style('body', nil, dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr') + style("html", nil, :lang => html_lang, "xml:lang" => html_lang) + style("body", "line-height: 1.4; text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};") + style("body", nil, dir: Rtl.new(nil).enabled? ? "rtl" : "ltr") - style('.with-dir', - "text-align:#{ Rtl.new(nil).enabled? ? 'right' : 'left' };", - dir: Rtl.new(nil).enabled? ? 'rtl' : 'ltr' + style( + ".with-dir", + "text-align:#{Rtl.new(nil).enabled? ? "right" : "left"};", + dir: Rtl.new(nil).enabled? ? "rtl" : "ltr", ) - style('blockquote > :first-child', 'margin-top: 0;') - style('blockquote > :last-child', 'margin-bottom: 0;') - style('blockquote > p', 'padding: 0;') + style("blockquote > :first-child", "margin-top: 0;") + style("blockquote > :last-child", "margin-bottom: 0;") + style("blockquote > p", "padding: 0;") - style('.with-accent-colors', "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};") - style('h4', 'color: #222;') - style('h3', 'margin: 30px 0 10px;') - style('hr', 'background-color: #ddd; height: 1px; border: 1px;') - style('a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};") - style('ul', 'margin: 0 0 0 10px; padding: 0 0 0 20px;') - style('li', 'padding-bottom: 10px') - style('div.summary-footer', 'color:#666; font-size:95%; text-align:center; padding-top:15px;') - style('span.post-count', 'margin: 0 5px; color: #777;') - style('pre', 'word-wrap: break-word; max-width: 694px;') - style('code', 'background-color: #f9f9f9; padding: 2px 5px;') - style('pre code', 'display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;') - style('pre.onebox code', 'white-space: normal;') - style('pre code li', 'white-space: pre;') - style('.featured-topic a', "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;") - style('.summary-email', "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%") + style( + ".with-accent-colors", + "background-color: #{SiteSetting.email_accent_bg_color}; color: #{SiteSetting.email_accent_fg_color};", + ) + style("h4", "color: #222;") + style("h3", "margin: 30px 0 10px;") + style("hr", "background-color: #ddd; height: 1px; border: 1px;") + style( + "a", + "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color};", + ) + style("ul", "margin: 0 0 0 10px; padding: 0 0 0 20px;") + style("li", "padding-bottom: 10px") + style("div.summary-footer", "color:#666; font-size:95%; text-align:center; padding-top:15px;") + style("span.post-count", "margin: 0 5px; color: #777;") + style("pre", "word-wrap: break-word; max-width: 694px;") + style("code", "background-color: #f9f9f9; padding: 2px 5px;") + style("pre code", "display: block; background-color: #f9f9f9; overflow: auto; padding: 5px;") + style("pre.onebox code", "white-space: normal;") + style("pre code li", "white-space: pre;") + style( + ".featured-topic a", + "text-decoration: none; font-weight: bold; color: #{SiteSetting.email_link_color}; line-height:1.5em;", + ) + style( + ".summary-email", + "-moz-box-sizing:border-box;-ms-text-size-adjust:100%;-webkit-box-sizing:border-box;-webkit-text-size-adjust:100%;box-sizing:border-box;color:#0a0a0a;font-family:Arial,sans-serif;font-size:14px;font-weight:400;line-height:1.3;margin:0;min-width:100%;padding:0;width:100%", + ) - style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;') - style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px") - style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold") - style('.username-link', "color:#{SiteSetting.email_link_color};") - style('.username-title', "color:#777;margin-left:5px;") - style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;") - style('.post-wrapper', "margin-bottom:25px;") - style('.user-avatar', 'vertical-align:top;width:55px;') - style('.user-avatar img', nil, width: '45', height: '45') - style('hr', 'background-color: #ddd; height: 1px; border: 1px;') - style('.rtl', 'direction: rtl;') - style('div.body', 'padding-top:5px;') - style('.whisper div.body', 'font-style: italic; color: #9c9c9c;') - style('.lightbox-wrapper .meta', 'display: none') - style('div.undecorated-link-footer a', "font-weight: normal;") - style('.mso-accent-link', "mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};") - style('.reply-above-line', "font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;") + style(".previous-discussion", "font-size: 17px; color: #444; margin-bottom:10px;") + style( + ".notification-date", + "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px", + ) + style( + ".username", + "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;font-weight:bold", + ) + style(".username-link", "color:#{SiteSetting.email_link_color};") + style(".username-title", "color:#777;margin-left:5px;") + style( + ".user-title", + "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:5px;color: #999;", + ) + style(".post-wrapper", "margin-bottom:25px;") + style(".user-avatar", "vertical-align:top;width:55px;") + style(".user-avatar img", nil, width: "45", height: "45") + style("hr", "background-color: #ddd; height: 1px; border: 1px;") + style(".rtl", "direction: rtl;") + style("div.body", "padding-top:5px;") + style(".whisper div.body", "font-style: italic; color: #9c9c9c;") + style(".lightbox-wrapper .meta", "display: none") + style("div.undecorated-link-footer a", "font-weight: normal;") + style( + ".mso-accent-link", + "mso-border-alt: 6px solid #{SiteSetting.email_accent_bg_color}; background-color: #{SiteSetting.email_accent_bg_color};", + ) + style( + ".reply-above-line", + "font-size: 10px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color: #b5b5b5;padding: 5px 0px 20px;border-top: 1px dotted #ddd;", + ) onebox_styles plugin_styles dark_mode_styles - style('.post-excerpt img', "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;") + style(".post-excerpt img", "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;") format_custom end def format_custom - custom_styles.each do |selector, value| - style(selector, value) - end + custom_styles.each { |selector, value| style(selector, value) } end # this method is reserved for styles specific to plugin @@ -258,33 +315,46 @@ module Email end def inline_secure_images(attachments, attachments_index) - stripped_media = @fragment.css('[data-stripped-secure-media], [data-stripped-secure-upload]') + stripped_media = @fragment.css("[data-stripped-secure-media], [data-stripped-secure-upload]") upload_shas = {} stripped_media.each do |div| - url = div['data-stripped-secure-media'] || div['data-stripped-secure-upload'] + url = div["data-stripped-secure-media"] || div["data-stripped-secure-upload"] filename = File.basename(url) filename_bare = filename.gsub(File.extname(filename), "") - sha1 = filename_bare.partition('_').first + sha1 = filename_bare.partition("_").first upload_shas[url] = sha1 end uploads = Upload.select(:original_filename, :sha1).where(sha1: upload_shas.values) stripped_media.each do |div| - upload = uploads.find do |upl| - upl.sha1 == (upload_shas[div['data-stripped-secure-media']] || upload_shas[div['data-stripped-secure-upload']]) - end + upload = + uploads.find do |upl| + upl.sha1 == + ( + upload_shas[div["data-stripped-secure-media"]] || + upload_shas[div["data-stripped-secure-upload"]] + ) + end next if !upload if attachments[attachments_index[upload.sha1]] url = attachments[attachments_index[upload.sha1]].url - onebox_type = div['data-onebox-type'] - style = if onebox_type - onebox_style = onebox_type == "avatar-inline" ? ONEBOX_INLINE_AVATAR_STYLE : ONEBOX_IMAGE_THUMBNAIL_STYLE - "#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}" - else - calculate_width_and_height_style(div) - end + onebox_type = div["data-onebox-type"] + style = + if onebox_type + onebox_style = + ( + if onebox_type == "avatar-inline" + ONEBOX_INLINE_AVATAR_STYLE + else + ONEBOX_IMAGE_THUMBNAIL_STYLE + end + ) + "#{onebox_style} #{ONEBOX_IMAGE_BASE_STYLE}" + else + calculate_width_and_height_style(div) + end div.add_next_sibling(<<~HTML) @@ -309,39 +379,45 @@ module Email end def strip_avatars_and_emojis - @fragment.search('img').each do |img| - next unless img['src'] + @fragment + .search("img") + .each do |img| + next unless img["src"] - if img['src'][/_avatar/] - img.parent['style'] = "vertical-align: top;" if img.parent&.name == 'td' - img.remove - end + if img["src"][/_avatar/] + img.parent["style"] = "vertical-align: top;" if img.parent&.name == "td" + img.remove + end - if img['title'] && img['src'][/\/_?emoji\//] - img.add_previous_sibling(img['title'] || "emoji") - img.remove + if img["title"] && img["src"][%r{/_?emoji/}] + img.add_previous_sibling(img["title"] || "emoji") + img.remove + end end - end end def decorate_hashtags - @fragment.search(".hashtag-cooked").each do |hashtag| - hashtag.children.each(&:remove) - hashtag.add_child(<<~HTML) + @fragment + .search(".hashtag-cooked") + .each do |hashtag| + hashtag.children.each(&:remove) + hashtag.add_child(<<~HTML) ##{hashtag["data-slug"]} HTML - end + end end def make_all_links_absolute site_uri = URI(Discourse.base_url) - @fragment.css("a").each do |link| - begin - link["href"] = "#{site_uri}#{link['href']}" unless URI(link["href"].to_s).host.present? - rescue URI::Error - # leave it + @fragment + .css("a") + .each do |link| + begin + link["href"] = "#{site_uri}#{link["href"]}" unless URI(link["href"].to_s).host.present? + rescue URI::Error + # leave it + end end - end end private @@ -350,8 +426,16 @@ module Email # When we ship the email template and its styles we strip all css classes so to give our # dark mode styles we are including in the template a selector we add a data-attr of 'dm=value' to # the appropriate place - style(".digest-header, .digest-topic, .digest-topic-title-wrapper, .digest-topic-stats, .popular-post-excerpt", nil, dm: "header") - style(".digest-content, .header-popular-posts, .spacer, .popular-post-spacer, .popular-post-meta, .digest-new-header, .digest-new-topic, .body", nil, dm: "body") + style( + ".digest-header, .digest-topic, .digest-topic-title-wrapper, .digest-topic-stats, .popular-post-excerpt", + nil, + dm: "header", + ) + style( + ".digest-content, .header-popular-posts, .spacer, .popular-post-spacer, .popular-post-meta, .digest-new-header, .digest-new-topic, .body", + nil, + dm: "body", + ) style(".with-accent-colors, .digest-content-header", nil, dm: "body_primary") style(".digest-topic-body", nil, dm: "topic-body") style(".summary-footer", nil, dm: "text-color") @@ -363,18 +447,19 @@ module Email host = forum_uri.host scheme = forum_uri.scheme - @fragment.css('[href]').each do |element| - href = element['href'] - if href.start_with?("\/\/#{host}") - element['href'] = "#{scheme}:#{href}" + @fragment + .css("[href]") + .each do |element| + href = element["href"] + element["href"] = "#{scheme}:#{href}" if href.start_with?("\/\/#{host}") end - end end def calculate_width_and_height_style(div) - width = div['data-width'] - height = div['data-height'] - if width.present? && height.present? && height.to_i < MAX_IMAGE_DIMENSION && width.to_i < MAX_IMAGE_DIMENSION + width = div["data-width"] + height = div["data-height"] + if width.present? && height.present? && height.to_i < MAX_IMAGE_DIMENSION && + width.to_i < MAX_IMAGE_DIMENSION "width: #{width}px; height: #{height}px;" else "max-width: 50%; max-height: #{MAX_IMAGE_DIMENSION}px;" @@ -386,59 +471,68 @@ module Email # notification template but that may not catch everything PrettyText.strip_secure_uploads(@fragment) - style('div.secure-upload-notice', 'border: 5px solid #e9e9e9; padding: 5px; display: inline-block;') - style('div.secure-upload-notice a', "color: #{SiteSetting.email_link_color}") + style( + "div.secure-upload-notice", + "border: 5px solid #e9e9e9; padding: 5px; display: inline-block;", + ) + style("div.secure-upload-notice a", "color: #{SiteSetting.email_link_color}") end def correct_first_body_margin - @fragment.css('div.body p').each do |element| - element['style'] = "margin-top:0; border: 0;" - end + @fragment.css("div.body p").each { |element| element["style"] = "margin-top:0; border: 0;" } end def correct_footer_style - @fragment.css('.footer').each do |element| - element['style'] = "color:#666;" - element.css('a').each do |inner| - inner['style'] = "color:#666;" + @fragment + .css(".footer") + .each do |element| + element["style"] = "color:#666;" + element.css("a").each { |inner| inner["style"] = "color:#666;" } end - end end def correct_footer_style_highlight_first footernum = 0 - @fragment.css('.footer.highlight').each do |element| - linknum = 0 - element.css('a').each do |inner| - # we want the first footer link to be specially highlighted as IMPORTANT - if footernum == (0) && linknum == (0) - bg_color = SiteSetting.email_accent_bg_color - inner['style'] = "background-color: #{bg_color}; color: #{SiteSetting.email_accent_fg_color}; border-top: 4px solid #{bg_color}; border-right: 6px solid #{bg_color}; border-bottom: 4px solid #{bg_color}; border-left: 6px solid #{bg_color}; display: inline-block; font-weight: bold;" - end + @fragment + .css(".footer.highlight") + .each do |element| + linknum = 0 + element + .css("a") + .each do |inner| + # we want the first footer link to be specially highlighted as IMPORTANT + if footernum == (0) && linknum == (0) + bg_color = SiteSetting.email_accent_bg_color + inner[ + "style" + ] = "background-color: #{bg_color}; color: #{SiteSetting.email_accent_fg_color}; border-top: 4px solid #{bg_color}; border-right: 6px solid #{bg_color}; border-bottom: 4px solid #{bg_color}; border-left: 6px solid #{bg_color}; display: inline-block; font-weight: bold;" + end + return + end return end - return - end end def strip_classes_and_ids - @fragment.css('*').each do |element| - element.delete('class') - element.delete('id') - end + @fragment + .css("*") + .each do |element| + element.delete("class") + element.delete("id") + end end def reset_tables - style('table', nil, cellspacing: '0', cellpadding: '0', border: '0') + style("table", nil, cellspacing: "0", cellpadding: "0", border: "0") end def style(selector, style, attribs = {}) - @fragment.css(selector).each do |element| - add_styles(element, style) if style - attribs.each do |k, v| - element[k] = v + @fragment + .css(selector) + .each do |element| + add_styles(element, style) if style + attribs.each { |k, v| element[k] = v } end - end end end end diff --git a/lib/email/validator.rb b/lib/email/validator.rb index 2795055a93..764bb7e13c 100644 --- a/lib/email/validator.rb +++ b/lib/email/validator.rb @@ -10,7 +10,7 @@ module Email end def self.ensure_valid_address_lists!(mail) - [:to, :cc, :bcc].each do |field| + %i[to cc bcc].each do |field| addresses = mail[field] if addresses&.errors.present? @@ -21,7 +21,8 @@ module Email def self.ensure_valid_date!(mail) if mail.date.nil? - raise Email::Receiver::InvalidPost, I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid") + raise Email::Receiver::InvalidPost, + I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid") end end end diff --git a/lib/email_backup_token.rb b/lib/email_backup_token.rb index 098f7c7e07..0aef08ca54 100644 --- a/lib/email_backup_token.rb +++ b/lib/email_backup_token.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class EmailBackupToken - def self.key(user_id) "email-backup-token:#{user_id}" end diff --git a/lib/email_controller_helper/base_email_unsubscriber.rb b/lib/email_controller_helper/base_email_unsubscriber.rb index b04560040c..46267cb36e 100644 --- a/lib/email_controller_helper/base_email_unsubscriber.rb +++ b/lib/email_controller_helper/base_email_unsubscriber.rb @@ -20,7 +20,7 @@ module EmailControllerHelper controller.instance_variable_set( :@unsubscribed_from_all, - key_owner.user_option.unsubscribed_from_all? + key_owner.user_option.unsubscribed_from_all?, ) end @@ -38,10 +38,12 @@ module EmailControllerHelper end if params[:unsubscribe_all] - key_owner.user_option.update_columns(email_digests: false, - email_level: UserOption.email_level_types[:never], - email_messages_level: UserOption.email_level_types[:never], - mailing_list_mode: false) + key_owner.user_option.update_columns( + email_digests: false, + email_level: UserOption.email_level_types[:never], + email_messages_level: UserOption.email_level_types[:never], + mailing_list_mode: false, + ) updated = true end diff --git a/lib/email_controller_helper/digest_email_unsubscriber.rb b/lib/email_controller_helper/digest_email_unsubscriber.rb index 7291d01258..b96e77402c 100644 --- a/lib/email_controller_helper/digest_email_unsubscriber.rb +++ b/lib/email_controller_helper/digest_email_unsubscriber.rb @@ -12,22 +12,34 @@ module EmailControllerHelper never = frequencies.delete_at(0) allowed_frequencies = %w[never weekly every_month every_six_months] - result = frequencies.reduce(frequencies: [], current: nil, selected: nil, take_next: false) do |memo, v| - memo[:current] = v[:name] if v[:value] == frequency_in_minutes && email_digests - next(memo) unless allowed_frequencies.include?(v[:name]) + result = + frequencies.reduce( + frequencies: [], + current: nil, + selected: nil, + take_next: false, + ) do |memo, v| + memo[:current] = v[:name] if v[:value] == frequency_in_minutes && email_digests + next(memo) unless allowed_frequencies.include?(v[:name]) - memo.tap do |m| - m[:selected] = v[:value] if m[:take_next] && email_digests - m[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{v[:name]}"), v[:value]] - m[:take_next] = !m[:take_next] && m[:current] + memo.tap do |m| + m[:selected] = v[:value] if m[:take_next] && email_digests + m[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{v[:name]}"), v[:value]] + m[:take_next] = !m[:take_next] && m[:current] + end end - end - digest_frequencies = result.slice(:frequencies, :current, :selected).tap do |r| - r[:frequencies] << [I18n.t("unsubscribe.digest_frequency.#{never[:name]}"), never[:value]] - r[:selected] ||= never[:value] - r[:current] ||= never[:name] - end + digest_frequencies = + result + .slice(:frequencies, :current, :selected) + .tap do |r| + r[:frequencies] << [ + I18n.t("unsubscribe.digest_frequency.#{never[:name]}"), + never[:value], + ] + r[:selected] ||= never[:value] + r[:current] ||= never[:name] + end controller.instance_variable_set(:@digest_frequencies, digest_frequencies) end @@ -40,7 +52,7 @@ module EmailControllerHelper unsubscribe_key.user.user_option.update_columns( digest_after_minutes: digest_frequency, - email_digests: digest_frequency.positive? + email_digests: digest_frequency.positive?, ) updated = true end diff --git a/lib/email_controller_helper/topic_email_unsubscriber.rb b/lib/email_controller_helper/topic_email_unsubscriber.rb index 6265853f54..eda37b7d66 100644 --- a/lib/email_controller_helper/topic_email_unsubscriber.rb +++ b/lib/email_controller_helper/topic_email_unsubscriber.rb @@ -11,16 +11,25 @@ module EmailControllerHelper controller.instance_variable_set(:@topic, topic) controller.instance_variable_set( :@watching_topic, - TopicUser.exists?(user: key_owner, notification_level: watching, topic_id: topic.id) + TopicUser.exists?(user: key_owner, notification_level: watching, topic_id: topic.id), ) return if topic.category_id.blank? - return if !CategoryUser.exists?(user: key_owner, notification_level: CategoryUser.watching_levels, category_id: topic.category_id) + if !CategoryUser.exists?( + user: key_owner, + notification_level: CategoryUser.watching_levels, + category_id: topic.category_id, + ) + return + end controller.instance_variable_set( :@watched_count, - TopicUser.joins(:topic) - .where(user: key_owner, notification_level: watching).where(topics: { category_id: topic.category_id }).count + TopicUser + .joins(:topic) + .where(user: key_owner, notification_level: watching) + .where(topics: { category_id: topic.category_id }) + .count, ) end @@ -31,27 +40,33 @@ module EmailControllerHelper return updated if topic.nil? if params[:unwatch_topic] - TopicUser.where(topic_id: topic.id, user_id: key_owner.id) - .update_all(notification_level: TopicUser.notification_levels[:tracking]) + TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all( + notification_level: TopicUser.notification_levels[:tracking], + ) updated = true end if params[:unwatch_category] && topic.category_id - TopicUser.joins(:topic) + TopicUser + .joins(:topic) .where(user: key_owner, notification_level: TopicUser.notification_levels[:watching]) .where(topics: { category_id: topic.category_id }) .update_all(notification_level: TopicUser.notification_levels[:tracking]) - CategoryUser - .where(user_id: key_owner.id, category_id: topic.category_id, notification_level: CategoryUser.watching_levels) - .destroy_all + CategoryUser.where( + user_id: key_owner.id, + category_id: topic.category_id, + notification_level: CategoryUser.watching_levels, + ).destroy_all updated = true end if params[:mute_topic] - TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all(notification_level: TopicUser.notification_levels[:muted]) + TopicUser.where(topic_id: topic.id, user_id: key_owner.id).update_all( + notification_level: TopicUser.notification_levels[:muted], + ) updated = true end diff --git a/lib/email_cook.rb b/lib/email_cook.rb index 89b59891f1..2c76e1f2ff 100644 --- a/lib/email_cook.rb +++ b/lib/email_cook.rb @@ -2,9 +2,9 @@ # A very simple formatter for imported emails class EmailCook - def self.raw_regexp - @raw_regexp ||= /^\[plaintext\]$\n(.*)\n^\[\/plaintext\]$(?:\s^\[attachments\]$\n(.*)\n^\[\/attachments\]$)?(?:\s^\[elided\]$\n(.*)\n^\[\/elided\]$)?/m + @raw_regexp ||= + %r{^\[plaintext\]$\n(.*)\n^\[/plaintext\]$(?:\s^\[attachments\]$\n(.*)\n^\[/attachments\]$)?(?:\s^\[elided\]$\n(.*)\n^\[/elided\]$)?}m end def initialize(raw) @@ -22,7 +22,7 @@ class EmailCook def link_string!(line, unescaped_line) unescaped_line = unescaped_line.strip line.gsub!(/\S+/) do |str| - if str.match?(/^(https?:\/\/)[\S]+$/i) + if str.match?(%r{^(https?://)[\S]+$}i) begin url = URI.parse(str).to_s if unescaped_line == url @@ -52,7 +52,7 @@ class EmailCook if line =~ /^\s*>/ in_quote = true - line.sub!(/^[\s>]*/, '') + line.sub!(/^[\s>]*/, "") unescaped_line = line line = CGI.escapeHTML(line) @@ -64,7 +64,6 @@ class EmailCook quote_buffer = "" in_quote = false else - sz = line.size unescaped_line = line @@ -72,9 +71,7 @@ class EmailCook link_string!(line, unescaped_line) if sz < 60 - if in_text && line == "\n" - result << "
    " - end + result << "
    " if in_text && line == "\n" result << line result << "
    " @@ -86,11 +83,9 @@ class EmailCook end end - if in_quote && quote_buffer.present? - add_quote(result, quote_buffer) - end + add_quote(result, quote_buffer) if in_quote && quote_buffer.present? - result.gsub!(/(
    \n*){3,10}/, '

    ') + result.gsub!(/(
    \n*){3,10}/, "

    ") result end @@ -98,10 +93,9 @@ class EmailCook # fallback to PrettyText if we failed to detect a body return PrettyText.cook(@raw, opts) if @body.nil? - result = htmlify(@body) + result = htmlify(@body) result << "\n
    " << @attachment_html if @attachment_html.present? result << "\n

    " << Email::Receiver.elided_html(htmlify(@elided)) if @elided.present? result end - end diff --git a/lib/email_updater.rb b/lib/email_updater.rb index 8f5fb292e2..c25c2382e8 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -26,8 +26,8 @@ class EmailUpdater if SiteSetting.hide_email_address_taken Jobs.enqueue(:critical_user_email, type: "account_exists", user_id: existing_user.id) else - error_message = +'change_email.error' - error_message << '_staged' if existing_user.staged? + error_message = +"change_email.error" + error_message << "_staged" if existing_user.staged? errors.add(:base, I18n.t(error_message)) end end @@ -57,19 +57,23 @@ class EmailUpdater @change_req.new_email = email end - if @change_req.change_state.blank? || @change_req.change_state == EmailChangeRequest.states[:complete] - @change_req.change_state = if SiteSetting.require_change_email_confirmation || @user.staff? - EmailChangeRequest.states[:authorizing_old] - else - EmailChangeRequest.states[:authorizing_new] - end + if @change_req.change_state.blank? || + @change_req.change_state == EmailChangeRequest.states[:complete] + @change_req.change_state = + if SiteSetting.require_change_email_confirmation || @user.staff? + EmailChangeRequest.states[:authorizing_old] + else + EmailChangeRequest.states[:authorizing_new] + end end if @change_req.change_state == EmailChangeRequest.states[:authorizing_old] - @change_req.old_email_token = @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update]) + @change_req.old_email_token = + @user.email_tokens.create!(email: @user.email, scope: EmailToken.scopes[:email_update]) send_email(add ? "confirm_old_email_add" : "confirm_old_email", @change_req.old_email_token) elsif @change_req.change_state == EmailChangeRequest.states[:authorizing_new] - @change_req.new_email_token = @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update]) + @change_req.new_email_token = + @user.email_tokens.create!(email: email, scope: EmailToken.scopes[:email_update]) send_email("confirm_new_email", @change_req.new_email_token) end @@ -83,7 +87,7 @@ class EmailUpdater User.transaction do email_token = EmailToken.confirmable(token, scope: EmailToken.scopes[:email_update]) if email_token.blank? - errors.add(:base, I18n.t('change_email.already_done')) + errors.add(:base, I18n.t("change_email.already_done")) confirm_result = :error next end @@ -91,15 +95,24 @@ class EmailUpdater email_token.update!(confirmed: true) @user = email_token.user - @change_req = @user.email_change_requests - .where('old_email_token_id = :token_id OR new_email_token_id = :token_id', token_id: email_token.id) - .first + @change_req = + @user + .email_change_requests + .where( + "old_email_token_id = :token_id OR new_email_token_id = :token_id", + token_id: email_token.id, + ) + .first case @change_req.try(:change_state) when EmailChangeRequest.states[:authorizing_old] @change_req.update!( change_state: EmailChangeRequest.states[:authorizing_new], - new_email_token: @user.email_tokens.create!(email: @change_req.new_email, scope: EmailToken.scopes[:email_update]) + new_email_token: + @user.email_tokens.create!( + email: @change_req.new_email, + scope: EmailToken.scopes[:email_update], + ), ) send_email("confirm_new_email", @change_req.new_email_token) confirm_result = :authorizing_new diff --git a/lib/ember_cli.rb b/lib/ember_cli.rb index fd3892918b..00c65610ba 100644 --- a/lib/ember_cli.rb +++ b/lib/ember_cli.rb @@ -2,37 +2,47 @@ module EmberCli def self.assets - @assets ||= begin - assets = %w( - discourse.js - admin.js - wizard.js - ember_jquery.js - markdown-it-bundle.js - start-discourse.js - vendor.js - ) - assets += Dir.glob("app/assets/javascripts/discourse/scripts/*.js").map { |f| File.basename(f) } + @assets ||= + begin + assets = %w[ + discourse.js + admin.js + wizard.js + ember_jquery.js + markdown-it-bundle.js + start-discourse.js + vendor.js + ] + assets += + Dir.glob("app/assets/javascripts/discourse/scripts/*.js").map { |f| File.basename(f) } - Discourse.find_plugin_js_assets(include_disabled: true).each do |file| - next if file.ends_with?("_extra") # these are still handled by sprockets - assets << "#{file}.js" + Discourse + .find_plugin_js_assets(include_disabled: true) + .each do |file| + next if file.ends_with?("_extra") # these are still handled by sprockets + assets << "#{file}.js" + end + + assets end - - assets - end end def self.script_chunks - return @@chunk_infos if defined? @@chunk_infos + return @@chunk_infos if defined?(@@chunk_infos) - raw_chunk_infos = JSON.parse(File.read("#{Rails.configuration.root}/app/assets/javascripts/discourse/dist/chunks.json")) + raw_chunk_infos = + JSON.parse( + File.read("#{Rails.configuration.root}/app/assets/javascripts/discourse/dist/chunks.json"), + ) - chunk_infos = raw_chunk_infos["scripts"].map do |info| - logical_name = info["afterFile"][/\Aassets\/(.*)\.js\z/, 1] - chunks = info["scriptChunks"].map { |filename| filename[/\Aassets\/(.*)\.js\z/, 1] } - [logical_name, chunks] - end.to_h + chunk_infos = + raw_chunk_infos["scripts"] + .map do |info| + logical_name = info["afterFile"][%r{\Aassets/(.*)\.js\z}, 1] + chunks = info["scriptChunks"].map { |filename| filename[%r{\Aassets/(.*)\.js\z}, 1] } + [logical_name, chunks] + end + .to_h @@chunk_infos = chunk_infos if Rails.env.production? chunk_infos @@ -45,9 +55,11 @@ module EmberCli end def self.ember_version - @version ||= begin - ember_source_package_raw = File.read("#{Rails.root}/app/assets/javascripts/node_modules/ember-source/package.json") - JSON.parse(ember_source_package_raw)["version"] - end + @version ||= + begin + ember_source_package_raw = + File.read("#{Rails.root}/app/assets/javascripts/node_modules/ember-source/package.json") + JSON.parse(ember_source_package_raw)["version"] + end end end diff --git a/lib/encodings.rb b/lib/encodings.rb index 8bf0c7c72b..b8e68f24e5 100644 --- a/lib/encodings.rb +++ b/lib/encodings.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'rchardet' +require "rchardet" module Encodings def self.to_utf8(string) result = CharDet.detect(string) - encoded_string = try_utf8(string, result['encoding']) if result && result['encoding'] + encoded_string = try_utf8(string, result["encoding"]) if result && result["encoding"] encoded_string = force_utf8(string) if encoded_string.nil? encoded_string end @@ -15,21 +15,18 @@ module Encodings encoded = string.encode(Encoding::UTF_8, source_encoding) encoded&.valid_encoding? ? delete_bom!(encoded) : nil rescue Encoding::InvalidByteSequenceError, - Encoding::UndefinedConversionError, - Encoding::ConverterNotFoundError + Encoding::UndefinedConversionError, + Encoding::ConverterNotFoundError nil end def self.force_utf8(string) - encoded_string = string.encode(Encoding::UTF_8, - undef: :replace, - invalid: :replace, - replace: '') + encoded_string = string.encode(Encoding::UTF_8, undef: :replace, invalid: :replace, replace: "") delete_bom!(encoded_string) end def self.delete_bom!(string) - string.sub!(/\A\xEF\xBB\xBF/, '') unless string.blank? + string.sub!(/\A\xEF\xBB\xBF/, "") unless string.blank? string end end diff --git a/lib/enum.rb b/lib/enum.rb index d440121900..9c87135c72 100644 --- a/lib/enum.rb +++ b/lib/enum.rb @@ -43,15 +43,11 @@ class Enum < Hash # Public: Create a subset of enum, only include specified keys. def only(*keys) - dup.tap do |d| - d.keep_if { |k| keys.include?(k) } - end + dup.tap { |d| d.keep_if { |k| keys.include?(k) } } end # Public: Create a subset of enum, preserve all items but specified ones. def except(*keys) - dup.tap do |d| - d.delete_if { |k| keys.include?(k) } - end + dup.tap { |d| d.delete_if { |k| keys.include?(k) } } end end diff --git a/lib/excerpt_parser.rb b/lib/excerpt_parser.rb index 2a4fbc8e3b..f01499e934 100644 --- a/lib/excerpt_parser.rb +++ b/lib/excerpt_parser.rb @@ -28,15 +28,14 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end def self.get_excerpt(html, length, options) - html ||= '' - length = html.length if html.include?('excerpt') && CUSTOM_EXCERPT_REGEX === html + html ||= "" + length = html.length if html.include?("excerpt") && CUSTOM_EXCERPT_REGEX === html me = self.new(length, options) parser = Nokogiri::HTML::SAX::Parser.new(me) - catch(:done) do - parser.parse(html) - end + catch(:done) { parser.parse(html) } excerpt = me.excerpt.strip - excerpt = excerpt.gsub(/\s*\n+\s*/, "\n\n") if options[:keep_onebox_source] || options[:keep_onebox_body] + excerpt = excerpt.gsub(/\s*\n+\s*/, "\n\n") if options[:keep_onebox_source] || + options[:keep_onebox_body] excerpt = CGI.unescapeHTML(excerpt) if options[:text_entities] == true excerpt end @@ -53,8 +52,12 @@ class ExcerptParser < Nokogiri::XML::SAX::Document end def include_tag(name, attributes) - characters("<#{name} #{attributes.map { |k, v| "#{k}=\"#{escape_attribute(v)}\"" }.join(' ')}>", - truncate: false, count_it: false, encode: false) + characters( + "<#{name} #{attributes.map { |k, v| "#{k}=\"#{escape_attribute(v)}\"" }.join(" ")}>", + truncate: false, + count_it: false, + encode: false, + ) end def start_element(name, attributes = []) @@ -62,7 +65,7 @@ class ExcerptParser < Nokogiri::XML::SAX::Document when "img" attributes = Hash[*attributes.flatten] - if attributes["class"]&.include?('emoji') + if attributes["class"]&.include?("emoji") if @remap_emoji title = (attributes["alt"] || "").gsub(":", "") title = Emoji.lookup_unicode(title) || attributes["alt"] @@ -83,68 +86,53 @@ class ExcerptParser < Nokogiri::XML::SAX::Document elsif !attributes["title"].blank? characters("[#{attributes["title"]}]") else - characters("[#{I18n.t 'excerpt_image'}]") + characters("[#{I18n.t "excerpt_image"}]") end - characters("(#{attributes['src']})") if @markdown_images + characters("(#{attributes["src"]})") if @markdown_images end - when "a" unless @strip_links include_tag(name, attributes) @in_a = true end - when "aside" attributes = Hash[*attributes.flatten] - unless (@keep_onebox_source || @keep_onebox_body) && attributes['class']&.include?('onebox') + unless (@keep_onebox_source || @keep_onebox_body) && attributes["class"]&.include?("onebox") @in_quote = true end - if attributes['class']&.include?('quote') - if @keep_quotes || (@keep_onebox_body && attributes['data-topic'].present?) + if attributes["class"]&.include?("quote") + if @keep_quotes || (@keep_onebox_body && attributes["data-topic"].present?) @in_quote = false end end - - when 'article' - if attributes.include?(['class', 'onebox-body']) - @in_quote = !@keep_onebox_body - end - - when 'header' - if attributes.include?(['class', 'source']) - @in_quote = !@keep_onebox_source - end - + when "article" + @in_quote = !@keep_onebox_body if attributes.include?(%w[class onebox-body]) + when "header" + @in_quote = !@keep_onebox_source if attributes.include?(%w[class source]) when "div", "span" - if attributes.include?(["class", "excerpt"]) + if attributes.include?(%w[class excerpt]) @excerpt = +"" @current_length = 0 @start_excerpt = true end - when "details" @detail_contents = +"" if @in_details_depth == 0 @in_details_depth += 1 - when "summary" if @in_details_depth == 1 && !@in_summary @summary_contents = +"" @in_summary = true end - when "svg" attributes = Hash[*attributes.flatten] if attributes["class"]&.include?("d-icon") && @keep_svg include_tag(name, attributes) @in_svg = true end - when "use" - if @in_svg && @keep_svg - include_tag(name, attributes) - end + include_tag(name, attributes) if @in_svg && @keep_svg end end @@ -170,20 +158,22 @@ class ExcerptParser < Nokogiri::XML::SAX::Document @detail_contents = clean(@detail_contents) if @current_length + @summary_contents.length >= @length - characters(@summary_contents, - encode: false, - before_string: "

    ", - after_string: "
    ") + characters( + @summary_contents, + encode: false, + before_string: "
    ", + after_string: "
    ", + ) else - characters(@summary_contents, - truncate: false, - encode: false, - before_string: "
    ", - after_string: "") + characters( + @summary_contents, + truncate: false, + encode: false, + before_string: "
    ", + after_string: "", + ) - characters(@detail_contents, - encode: false, - after_string: "
    ") + characters(@detail_contents, encode: false, after_string: "
    ") end end when "summary" @@ -202,7 +192,14 @@ class ExcerptParser < Nokogiri::XML::SAX::Document ERB::Util.html_escape(str.strip) end - def characters(string, truncate: true, count_it: true, encode: true, before_string: nil, after_string: nil) + def characters( + string, + truncate: true, + count_it: true, + encode: true, + before_string: nil, + after_string: nil + ) return if @in_quote # we call length on this so might as well ensure we have a string diff --git a/lib/external_upload_helpers.rb b/lib/external_upload_helpers.rb index 5b0e43f5ab..3ac4cea5bc 100644 --- a/lib/external_upload_helpers.rb +++ b/lib/external_upload_helpers.rb @@ -5,35 +5,41 @@ module ExternalUploadHelpers extend ActiveSupport::Concern - class ExternalUploadValidationError < StandardError; end + class ExternalUploadValidationError < StandardError + end PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE = 10 CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE = 10 COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE = 10 included do - before_action :external_store_check, only: [ - :generate_presigned_put, - :complete_external_upload, - :create_multipart, - :batch_presign_multipart_parts, - :abort_multipart, - :complete_multipart - ] - before_action :direct_s3_uploads_check, only: [ - :generate_presigned_put, - :complete_external_upload, - :create_multipart, - :batch_presign_multipart_parts, - :abort_multipart, - :complete_multipart - ] - before_action :can_upload_external?, only: [:create_multipart, :generate_presigned_put] + before_action :external_store_check, + only: %i[ + generate_presigned_put + complete_external_upload + create_multipart + batch_presign_multipart_parts + abort_multipart + complete_multipart + ] + before_action :direct_s3_uploads_check, + only: %i[ + generate_presigned_put + complete_external_upload + create_multipart + batch_presign_multipart_parts + abort_multipart + complete_multipart + ] + before_action :can_upload_external?, only: %i[create_multipart generate_presigned_put] end def generate_presigned_put RateLimiter.new( - current_user, "generate-presigned-put-upload-stub", ExternalUploadHelpers::PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "generate-presigned-put-upload-stub", + ExternalUploadHelpers::PRESIGNED_PUT_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! file_name = params.require(:file_name) @@ -44,28 +50,28 @@ module ExternalUploadHelpers validate_before_create_direct_upload( file_name: file_name, file_size: file_size, - upload_type: type + upload_type: type, ) rescue ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end - external_upload_data = ExternalUploadManager.create_direct_upload( - current_user: current_user, - file_name: file_name, - file_size: file_size, - upload_type: type, - metadata: parse_allowed_metadata(params[:metadata]) - ) + external_upload_data = + ExternalUploadManager.create_direct_upload( + current_user: current_user, + file_name: file_name, + file_size: file_size, + upload_type: type, + metadata: parse_allowed_metadata(params[:metadata]), + ) render json: external_upload_data end def complete_external_upload unique_identifier = params.require(:unique_identifier) - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? complete_external_upload_via_manager(external_upload_stub) @@ -73,7 +79,10 @@ module ExternalUploadHelpers def create_multipart RateLimiter.new( - current_user, "create-multipart-upload", ExternalUploadHelpers::CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "create-multipart-upload", + ExternalUploadHelpers::CREATE_MULTIPART_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! file_name = params.require(:file_name) @@ -84,22 +93,23 @@ module ExternalUploadHelpers validate_before_create_multipart( file_name: file_name, file_size: file_size, - upload_type: upload_type + upload_type: upload_type, ) rescue ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end begin - external_upload_data = create_direct_multipart_upload do - ExternalUploadManager.create_direct_multipart_upload( - current_user: current_user, - file_name: file_name, - file_size: file_size, - upload_type: upload_type, - metadata: parse_allowed_metadata(params[:metadata]) - ) - end + external_upload_data = + create_direct_multipart_upload do + ExternalUploadManager.create_direct_multipart_upload( + current_user: current_user, + file_name: file_name, + file_size: file_size, + upload_type: upload_type, + metadata: parse_allowed_metadata(params[:metadata]), + ) + end rescue ExternalUploadHelpers::ExternalUploadValidationError => err return render_json_error(err.message, status: 422) end @@ -121,21 +131,19 @@ module ExternalUploadHelpers # The other external upload endpoints are not hit as often, so they can stay as constant # values for now. RateLimiter.new( - current_user, "batch-presign", SiteSetting.max_batch_presign_multipart_per_minute, 1.minute + current_user, + "batch-presign", + SiteSetting.max_batch_presign_multipart_per_minute, + 1.minute, ).performed! - part_numbers = part_numbers.map do |part_number| - validate_part_number(part_number) - end + part_numbers = part_numbers.map { |part_number| validate_part_number(part_number) } - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? - if !multipart_upload_exists?(external_upload_stub) - return render_404 - end + return render_404 if !multipart_upload_exists?(external_upload_stub) store = multipart_store(external_upload_stub.upload_type) @@ -144,7 +152,7 @@ module ExternalUploadHelpers presigned_urls[part_number] = store.presign_multipart_part( upload_id: external_upload_stub.external_upload_identifier, key: external_upload_stub.key, - part_number: part_number + part_number: part_number, ) end @@ -157,10 +165,16 @@ module ExternalUploadHelpers store.list_multipart_parts( upload_id: external_upload_stub.external_upload_identifier, key: external_upload_stub.key, - max_parts: 1 + max_parts: 1, ) rescue Aws::S3::Errors::NoSuchUpload => err - debug_upload_error(err, I18n.t("upload.external_upload_not_found", additional_detail: "path: #{external_upload_stub.key}")) + debug_upload_error( + err, + I18n.t( + "upload.external_upload_not_found", + additional_detail: "path: #{external_upload_stub.key}", + ), + ) return false end true @@ -168,9 +182,8 @@ module ExternalUploadHelpers def abort_multipart external_upload_identifier = params.require(:external_upload_identifier) - external_upload_stub = ExternalUploadStub.find_by( - external_upload_identifier: external_upload_identifier - ) + external_upload_stub = + ExternalUploadStub.find_by(external_upload_identifier: external_upload_identifier) # The stub could have already been deleted by an earlier error via # ExternalUploadManager, so we consider this a great success if the @@ -183,12 +196,20 @@ module ExternalUploadHelpers begin store.abort_multipart( upload_id: external_upload_stub.external_upload_identifier, - key: external_upload_stub.key + key: external_upload_stub.key, ) rescue Aws::S3::Errors::ServiceError => err - return render_json_error( - debug_upload_error(err, I18n.t("upload.abort_multipart_failure", additional_detail: "external upload stub id: #{external_upload_stub.id}")), - status: 422 + return( + render_json_error( + debug_upload_error( + err, + I18n.t( + "upload.abort_multipart_failure", + additional_detail: "external upload stub id: #{external_upload_stub.id}", + ), + ), + status: 422, + ) ) end @@ -202,45 +223,57 @@ module ExternalUploadHelpers parts = params.require(:parts) RateLimiter.new( - current_user, "complete-multipart-upload", ExternalUploadHelpers::COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE, 1.minute + current_user, + "complete-multipart-upload", + ExternalUploadHelpers::COMPLETE_MULTIPART_RATE_LIMIT_PER_MINUTE, + 1.minute, ).performed! - external_upload_stub = ExternalUploadStub.find_by( - unique_identifier: unique_identifier, created_by: current_user - ) + external_upload_stub = + ExternalUploadStub.find_by(unique_identifier: unique_identifier, created_by: current_user) return render_404 if external_upload_stub.blank? - if !multipart_upload_exists?(external_upload_stub) - return render_404 - end + return render_404 if !multipart_upload_exists?(external_upload_stub) store = multipart_store(external_upload_stub.upload_type) - parts = parts.map do |part| - part_number = part[:part_number] - etag = part[:etag] - part_number = validate_part_number(part_number) + parts = + parts + .map do |part| + part_number = part[:part_number] + etag = part[:etag] + part_number = validate_part_number(part_number) - if etag.blank? - raise Discourse::InvalidParameters.new("All parts must have an etag and a valid part number") - end + if etag.blank? + raise Discourse::InvalidParameters.new( + "All parts must have an etag and a valid part number", + ) + end - # this is done so it's an array of hashes rather than an array of - # ActionController::Parameters - { part_number: part_number, etag: etag } - end.sort_by do |part| - part[:part_number] - end + # this is done so it's an array of hashes rather than an array of + # ActionController::Parameters + { part_number: part_number, etag: etag } + end + .sort_by { |part| part[:part_number] } begin - complete_response = store.complete_multipart( - upload_id: external_upload_stub.external_upload_identifier, - key: external_upload_stub.key, - parts: parts - ) + complete_response = + store.complete_multipart( + upload_id: external_upload_stub.external_upload_identifier, + key: external_upload_stub.key, + parts: parts, + ) rescue Aws::S3::Errors::ServiceError => err - return render_json_error( - debug_upload_error(err, I18n.t("upload.complete_multipart_failure", additional_detail: "external upload stub id: #{external_upload_stub.id}")), - status: 422 + return( + render_json_error( + debug_upload_error( + err, + I18n.t( + "upload.complete_multipart_failure", + additional_detail: "external upload stub id: #{external_upload_stub.id}", + ), + ), + status: 422, + ) ) end @@ -270,27 +303,40 @@ module ExternalUploadHelpers end rescue ExternalUploadManager::SizeMismatchError => err render_json_error( - debug_upload_error(err, I18n.t("upload.size_mismatch_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.size_mismatch_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::ChecksumMismatchError => err render_json_error( - debug_upload_error(err, I18n.t("upload.checksum_mismatch_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.checksum_mismatch_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::CannotPromoteError => err render_json_error( - debug_upload_error(err, I18n.t("upload.cannot_promote_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.cannot_promote_failure", additional_detail: err.message), + ), + status: 422, ) rescue ExternalUploadManager::DownloadFailedError, Aws::S3::Errors::NotFound => err render_json_error( - debug_upload_error(err, I18n.t("upload.download_failure", additional_detail: err.message)), - status: 422 + debug_upload_error( + err, + I18n.t("upload.download_failure", additional_detail: err.message), + ), + status: 422, ) rescue => err Discourse.warn_exception( - err, message: "Complete external upload failed unexpectedly for user #{current_user.id}" + err, + message: "Complete external upload failed unexpectedly for user #{current_user.id}", ) render_json_error(I18n.t("upload.failed"), status: 422) @@ -308,10 +354,8 @@ module ExternalUploadHelpers def validate_part_number(part_number) part_number = part_number.to_i - if !part_number.between?(1, 10000) - raise Discourse::InvalidParameters.new( - "Each part number should be between 1 and 10000" - ) + if !part_number.between?(1, 10_000) + raise Discourse::InvalidParameters.new("Each part number should be between 1 and 10000") end part_number end diff --git a/lib/faker/discourse.rb b/lib/faker/discourse.rb index a2c4720026..ed527841dc 100644 --- a/lib/faker/discourse.rb +++ b/lib/faker/discourse.rb @@ -1,25 +1,24 @@ # frozen_string_literal: true -require 'faker' +require "faker" module Faker class Discourse < Base class << self - def tag - fetch('discourse.tags') + fetch("discourse.tags") end def category - fetch('discourse.categories') + fetch("discourse.categories") end def group - fetch('discourse.groups') + fetch("discourse.groups") end def topic - fetch('discourse.topics') + fetch("discourse.topics") end end end diff --git a/lib/faker/discourse_markdown.rb b/lib/faker/discourse_markdown.rb index 8f5266a191..f893f05189 100644 --- a/lib/faker/discourse_markdown.rb +++ b/lib/faker/discourse_markdown.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'faker' -require 'net/http' -require 'json' +require "faker" +require "net/http" +require "json" module Faker class DiscourseMarkdown < Markdown @@ -27,11 +27,8 @@ module Faker image = next_image image_file = load_image(image) - upload = ::UploadCreator.new( - image_file, - image[:filename], - origin: image[:url] - ).create_for(user_id) + upload = + ::UploadCreator.new(image_file, image[:filename], origin: image[:url]).create_for(user_id) ::UploadMarkdown.new(upload).to_markdown if upload.present? && upload.persisted? rescue => e @@ -62,7 +59,7 @@ module Faker end image = @images.pop - { filename: "#{image['id']}.jpg", url: "#{image['download_url']}.jpg" } + { filename: "#{image["id"]}.jpg", url: "#{image["download_url"]}.jpg" } end def image_cache_dir @@ -74,12 +71,13 @@ module Faker if !::File.exist?(cache_path) FileUtils.mkdir_p(image_cache_dir) - temp_file = ::FileHelper.download( - image[:url], - max_file_size: [SiteSetting.max_image_size_kb.kilobytes, 10.megabytes].max, - tmp_file_name: "image", - follow_redirect: true - ) + temp_file = + ::FileHelper.download( + image[:url], + max_file_size: [SiteSetting.max_image_size_kb.kilobytes, 10.megabytes].max, + tmp_file_name: "image", + follow_redirect: true, + ) FileUtils.cp(temp_file, cache_path) end diff --git a/lib/feed_element_installer.rb b/lib/feed_element_installer.rb index 2e0ecd40ec..c96e89d144 100644 --- a/lib/feed_element_installer.rb +++ b/lib/feed_element_installer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rexml/document' -require 'rss' +require "rexml/document" +require "rss" class FeedElementInstaller private_class_method :new @@ -10,7 +10,7 @@ class FeedElementInstaller # RSS Specification at http://cyber.harvard.edu/rss/rss.html#extendingRss # > A RSS feed may contain [non-standard elements], only if those elements are *defined in a namespace* - new(element_name, feed).install if element_name.include?(':') + new(element_name, feed).install if element_name.include?(":") end attr_reader :feed, :original_name, :element_namespace, :element_name, :element_accessor @@ -18,12 +18,13 @@ class FeedElementInstaller def initialize(element_name, feed) @feed = feed @original_name = element_name - @element_namespace, @element_name = *element_name.split(':') + @element_namespace, @element_name = *element_name.split(":") @element_accessor = "#{@element_namespace}_#{@element_name}" end def element_uri - @element_uri ||= REXML::Document.new(feed).root&.attributes&.namespaces&.fetch(@element_namespace, '') || '' + @element_uri ||= + REXML::Document.new(feed).root&.attributes&.namespaces&.fetch(@element_namespace, "") || "" end def install @@ -34,13 +35,34 @@ class FeedElementInstaller private def install_in_rss - RSS::Rss::Channel::Item.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) + RSS::Rss::Channel::Item.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) RSS::BaseListener.install_get_text_element(element_uri, element_name, element_accessor) end def install_in_atom - RSS::Atom::Entry.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) - RSS::Atom::Feed::Entry.install_text_element(element_name, element_uri, '?', element_accessor, nil, original_name) + RSS::Atom::Entry.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) + RSS::Atom::Feed::Entry.install_text_element( + element_name, + element_uri, + "?", + element_accessor, + nil, + original_name, + ) RSS::BaseListener.install_get_text_element(element_uri, element_name, element_accessor) end @@ -49,6 +71,7 @@ class FeedElementInstaller end def installed_in_atom? - RSS::Atom::Entry.method_defined?(element_accessor) || RSS::Atom::Feed::Entry.method_defined?(element_accessor) + RSS::Atom::Entry.method_defined?(element_accessor) || + RSS::Atom::Feed::Entry.method_defined?(element_accessor) end end diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 31530a12e9..9b57251f83 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -5,11 +5,10 @@ require "mini_mime" require "open-uri" class FileHelper - def self.log(log_level, message) Rails.logger.public_send( log_level, - "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end @@ -41,29 +40,31 @@ class FileHelper attr_accessor :status end - def self.download(url, - max_file_size:, - tmp_file_name:, - follow_redirect: false, - read_timeout: 5, - skip_rate_limit: false, - verbose: false, - validate_uri: true, - retain_on_max_file_size_exceeded: false) - + def self.download( + url, + max_file_size:, + tmp_file_name:, + follow_redirect: false, + read_timeout: 5, + skip_rate_limit: false, + verbose: false, + validate_uri: true, + retain_on_max_file_size_exceeded: false + ) url = "https:" + url if url.start_with?("//") - raise Discourse::InvalidParameters.new(:url) unless url =~ /^https?:\/\// + raise Discourse::InvalidParameters.new(:url) unless url =~ %r{^https?://} tmp = nil - fd = FinalDestination.new( - url, - max_redirects: follow_redirect ? 5 : 0, - skip_rate_limit: skip_rate_limit, - verbose: verbose, - validate_uri: validate_uri, - timeout: read_timeout - ) + fd = + FinalDestination.new( + url, + max_redirects: follow_redirect ? 5 : 0, + skip_rate_limit: skip_rate_limit, + verbose: verbose, + validate_uri: validate_uri, + timeout: read_timeout, + ) fd.get do |response, chunk, uri| if tmp.nil? @@ -110,7 +111,7 @@ class FileHelper def self.optimize_image!(filename, allow_pngquant: false) image_optim( allow_pngquant: allow_pngquant, - strip_image_metadata: SiteSetting.strip_image_metadata + strip_image_metadata: SiteSetting.strip_image_metadata, ).optimize_image!(filename) end @@ -119,23 +120,26 @@ class FileHelper # sometimes up to 200ms searching for binaries and looking at versions memoize("image_optim", allow_pngquant, strip_image_metadata) do pngquant_options = false - if allow_pngquant - pngquant_options = { allow_lossy: true } - end + pngquant_options = { allow_lossy: true } if allow_pngquant ImageOptim.new( # GLOBAL timeout: 15, skip_missing_workers: true, # PNG - oxipng: { level: 3, strip: strip_image_metadata }, + oxipng: { + level: 3, + strip: strip_image_metadata, + }, optipng: false, advpng: false, pngcrush: false, pngout: false, pngquant: pngquant_options, # JPG - jpegoptim: { strip: strip_image_metadata ? "all" : "none" }, + jpegoptim: { + strip: strip_image_metadata ? "all" : "none", + }, jpegtran: false, jpegrecompress: false, # Skip looking for gifsicle, svgo binaries @@ -150,24 +154,24 @@ class FileHelper end def self.supported_gravatar_extensions - @@supported_gravatar_images ||= Set.new(%w{jpg jpeg png gif}) + @@supported_gravatar_images ||= Set.new(%w[jpg jpeg png gif]) end def self.supported_images - @@supported_images ||= Set.new %w{jpg jpeg png gif svg ico webp} + @@supported_images ||= Set.new %w[jpg jpeg png gif svg ico webp] end def self.inline_images # SVG cannot safely be shown as a document - @@inline_images ||= supported_images - %w{svg} + @@inline_images ||= supported_images - %w[svg] end def self.supported_audio - @@supported_audio ||= Set.new %w{mp3 ogg oga opus wav m4a m4b m4p m4r aac flac} + @@supported_audio ||= Set.new %w[mp3 ogg oga opus wav m4a m4b m4p m4r aac flac] end def self.supported_video - @@supported_video ||= Set.new %w{mov mp4 webm ogv m4v 3gp avi mpeg} + @@supported_video ||= Set.new %w[mov mp4 webm ogv m4v 3gp avi mpeg] end def self.supported_video_regexp diff --git a/lib/file_store/base_store.rb b/lib/file_store/base_store.rb index 8b4c41cf6f..f73114ca89 100644 --- a/lib/file_store/base_store.rb +++ b/lib/file_store/base_store.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true module FileStore - class BaseStore - UPLOAD_PATH_REGEX ||= %r|/(original/\d+X/.*)| - OPTIMIZED_IMAGE_PATH_REGEX ||= %r|/(optimized/\d+X/.*)| + UPLOAD_PATH_REGEX ||= %r{/(original/\d+X/.*)} + OPTIMIZED_IMAGE_PATH_REGEX ||= %r{/(optimized/\d+X/.*)} TEMPORARY_UPLOAD_PREFIX ||= "temp/" def store_upload(file, upload, content_type = nil) @@ -38,7 +37,7 @@ module FileStore def upload_path path = File.join("uploads", RailsMultisite::ConnectionManagement.current_db) return path if !Rails.env.test? - File.join(path, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + File.join(path, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") end def self.temporary_upload_path(file_name, folder_prefix: "") @@ -46,12 +45,7 @@ module FileStore # characters, which can interfere with external providers operations and # introduce other unexpected behaviour. file_name_random = "#{SecureRandom.hex}#{File.extname(file_name)}" - File.join( - TEMPORARY_UPLOAD_PREFIX, - folder_prefix, - SecureRandom.hex, - file_name_random - ) + File.join(TEMPORARY_UPLOAD_PREFIX, folder_prefix, SecureRandom.hex, file_name_random) end def has_been_uploaded?(url) @@ -96,25 +90,37 @@ module FileStore def download(object, max_file_size_kb: nil) DistributedMutex.synchronize("download_#{object.sha1}", validity: 3.minutes) do - extension = File.extname(object.respond_to?(:original_filename) ? object.original_filename : object.url) + extension = + File.extname( + object.respond_to?(:original_filename) ? object.original_filename : object.url, + ) filename = "#{object.sha1}#{extension}" file = get_from_cache(filename) if !file - max_file_size_kb ||= [SiteSetting.max_image_size_kb, SiteSetting.max_attachment_size_kb].max.kilobytes + max_file_size_kb ||= [ + SiteSetting.max_image_size_kb, + SiteSetting.max_attachment_size_kb, + ].max.kilobytes secure = object.respond_to?(:secure) ? object.secure? : object.upload.secure? - url = secure ? - Discourse.store.signed_url_for_path(object.url) : - Discourse.store.cdn_url(object.url) + url = + ( + if secure + Discourse.store.signed_url_for_path(object.url) + else + Discourse.store.cdn_url(object.url) + end + ) - url = SiteSetting.scheme + ":" + url if url =~ /^\/\// - file = FileHelper.download( - url, - max_file_size: max_file_size_kb, - tmp_file_name: "discourse-download", - follow_redirect: true - ) + url = SiteSetting.scheme + ":" + url if url =~ %r{^//} + file = + FileHelper.download( + url, + max_file_size: max_file_size_kb, + tmp_file_name: "discourse-download", + follow_redirect: true, + ) return nil if file.nil? @@ -162,7 +168,8 @@ module FileStore upload = optimized_image.upload version = optimized_image.version || 1 - extension = "_#{version}_#{optimized_image.width}x#{optimized_image.height}#{optimized_image.extension}" + extension = + "_#{version}_#{optimized_image.width}x#{optimized_image.height}#{optimized_image.extension}" get_path_for("optimized", upload.id, upload.sha1, extension) end @@ -214,5 +221,4 @@ module FileStore path end end - end diff --git a/lib/file_store/local_store.rb b/lib/file_store/local_store.rb index 4922eca707..c0461aa50a 100644 --- a/lib/file_store/local_store.rb +++ b/lib/file_store/local_store.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require 'file_store/base_store' +require "file_store/base_store" module FileStore - class LocalStore < BaseStore - def store_file(file, path) copy_file(file, "#{public_dir}#{path}") "#{Discourse.base_path}#{path}" @@ -64,7 +62,13 @@ module FileStore def purge_tombstone(grace_period) if Dir.exist?(Discourse.store.tombstone_dir) Discourse::Utils.execute_command( - 'find', tombstone_dir, '-mtime', "+#{grace_period}", '-type', 'f', '-delete' + "find", + tombstone_dir, + "-mtime", + "+#{grace_period}", + "-type", + "f", + "-delete", ) end end @@ -108,9 +112,13 @@ module FileStore FileUtils.mkdir_p(File.join(public_dir, upload_path)) Discourse::Utils.execute_command( - 'rsync', '-a', '--safe-links', "#{source_path}/", "#{upload_path}/", + "rsync", + "-a", + "--safe-links", + "#{source_path}/", + "#{upload_path}/", failure_message: "Failed to copy uploads.", - chdir: public_dir + chdir: public_dir, ) end @@ -119,15 +127,14 @@ module FileStore def list_missing(model) count = 0 model.find_each do |upload| - # could be a remote image - next unless upload.url =~ /^\/[^\/]/ + next unless upload.url =~ %r{^/[^/]} path = "#{public_dir}#{upload.url}" bad = true begin bad = false if File.size(path) != 0 - rescue + rescue StandardError # something is messed up end if bad diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index 074bf92446..8fdbd2d9fa 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -7,61 +7,64 @@ require "s3_helper" require "file_helper" module FileStore - class S3Store < BaseStore TOMBSTONE_PREFIX ||= "tombstone/" - delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts, - :complete_multipart, to: :s3_helper + delegate :abort_multipart, + :presign_multipart_part, + :list_multipart_parts, + :complete_multipart, + to: :s3_helper def initialize(s3_helper = nil) @s3_helper = s3_helper end def s3_helper - @s3_helper ||= S3Helper.new(s3_bucket, - Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX - ) + @s3_helper ||= + S3Helper.new( + s3_bucket, + Rails.configuration.multisite ? multisite_tombstone_prefix : TOMBSTONE_PREFIX, + ) end def store_upload(file, upload, content_type = nil) upload.url = nil path = get_path_for_upload(upload) - url, upload.etag = store_file( - file, - path, - filename: upload.original_filename, - content_type: content_type, - cache_locally: true, - private_acl: upload.secure? - ) + url, upload.etag = + store_file( + file, + path, + filename: upload.original_filename, + content_type: content_type, + cache_locally: true, + private_acl: upload.secure?, + ) url end - def move_existing_stored_upload( - existing_external_upload_key:, - upload: nil, - content_type: nil - ) + def move_existing_stored_upload(existing_external_upload_key:, upload: nil, content_type: nil) upload.url = nil path = get_path_for_upload(upload) - url, upload.etag = store_file( - nil, - path, - filename: upload.original_filename, - content_type: content_type, - cache_locally: false, - private_acl: upload.secure?, - move_existing: true, - existing_external_upload_key: existing_external_upload_key - ) + url, upload.etag = + store_file( + nil, + path, + filename: upload.original_filename, + content_type: content_type, + cache_locally: false, + private_acl: upload.secure?, + move_existing: true, + existing_external_upload_key: existing_external_upload_key, + ) url end def store_optimized_image(file, optimized_image, content_type = nil, secure: false) optimized_image.url = nil path = get_path_for_optimized_image(optimized_image) - url, optimized_image.etag = store_file(file, path, content_type: content_type, private_acl: secure) + url, optimized_image.etag = + store_file(file, path, content_type: content_type, private_acl: secure) url end @@ -85,8 +88,9 @@ module FileStore cache_file(file, File.basename(path)) if opts[:cache_locally] options = { acl: opts[:private_acl] ? "private" : "public-read", - cache_control: 'max-age=31556952, public, immutable', - content_type: opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type + cache_control: "max-age=31556952, public, immutable", + content_type: + opts[:content_type].presence || MiniMime.lookup_by_filename(filename)&.content_type, } # add a "content disposition: attachment" header with the original @@ -96,7 +100,8 @@ module FileStore # browser. if !FileHelper.is_inline_image?(filename) options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: filename + disposition: "attachment", + filename: filename, ) end @@ -106,11 +111,7 @@ module FileStore if opts[:move_existing] && opts[:existing_external_upload_key] original_path = opts[:existing_external_upload_key] options[:apply_metadata_to_destination] = true - path, etag = s3_helper.copy( - original_path, - path, - options: options - ) + path, etag = s3_helper.copy(original_path, path, options: options) delete_file(original_path) else path, etag = s3_helper.upload(file, path, options) @@ -142,7 +143,7 @@ module FileStore begin parsed_url = URI.parse(UrlHelper.encode(url)) - rescue + rescue StandardError # There are many exceptions possible here including Addressable::URI:: exceptions # and URI:: exceptions, catch all may seem wide, but it makes no sense to raise ever # on an invalid url here @@ -169,7 +170,10 @@ module FileStore s3_cdn_url = URI.parse(SiteSetting.Upload.s3_cdn_url || "") cdn_hostname = s3_cdn_url.hostname - return true if cdn_hostname.presence && url[cdn_hostname] && (s3_cdn_url.path.blank? || parsed_url.path.starts_with?(s3_cdn_url.path)) + if cdn_hostname.presence && url[cdn_hostname] && + (s3_cdn_url.path.blank? || parsed_url.path.starts_with?(s3_cdn_url.path)) + return true + end false end @@ -186,7 +190,11 @@ module FileStore end def s3_upload_host - SiteSetting.Upload.s3_cdn_url.present? ? SiteSetting.Upload.s3_cdn_url : "https:#{absolute_base_url}" + if SiteSetting.Upload.s3_cdn_url.present? + SiteSetting.Upload.s3_cdn_url + else + "https:#{absolute_base_url}" + end end def external? @@ -208,28 +216,45 @@ module FileStore def path_for(upload) url = upload&.url - FileStore::LocalStore.new.path_for(upload) if url && url[/^\/[^\/]/] + FileStore::LocalStore.new.path_for(upload) if url && url[%r{^/[^/]}] end def url_for(upload, force_download: false) - upload.secure? || force_download ? - presigned_get_url(get_upload_key(upload), force_download: force_download, filename: upload.original_filename) : + if upload.secure? || force_download + presigned_get_url( + get_upload_key(upload), + force_download: force_download, + filename: upload.original_filename, + ) + else upload.url + end end def cdn_url(url) return url if SiteSetting.Upload.s3_cdn_url.blank? - schema = url[/^(https?:)?\/\//, 1] + schema = url[%r{^(https?:)?//}, 1] folder = s3_bucket_folder_path.nil? ? "" : "#{s3_bucket_folder_path}/" - url.sub(File.join("#{schema}#{absolute_base_url}", folder), File.join(SiteSetting.Upload.s3_cdn_url, "/")) + url.sub( + File.join("#{schema}#{absolute_base_url}", folder), + File.join(SiteSetting.Upload.s3_cdn_url, "/"), + ) end - def signed_url_for_path(path, expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, force_download: false) + def signed_url_for_path( + path, + expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, + force_download: false + ) key = path.sub(absolute_base_url + "/", "") presigned_get_url(key, expires_in: expires_in, force_download: force_download) end - def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {}) + def signed_url_for_temporary_upload( + file_name, + expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, + metadata: {} + ) key = temporary_upload_path(file_name) s3_helper.presigned_url( key, @@ -237,16 +262,15 @@ module FileStore expires_in: expires_in, opts: { metadata: metadata, - acl: "private" - } + acl: "private", + }, ) end def temporary_upload_path(file_name) - folder_prefix = s3_bucket_folder_path.nil? ? upload_path : File.join(s3_bucket_folder_path, upload_path) - FileStore::BaseStore.temporary_upload_path( - file_name, folder_prefix: folder_prefix - ) + folder_prefix = + s3_bucket_folder_path.nil? ? upload_path : File.join(s3_bucket_folder_path, upload_path) + FileStore::BaseStore.temporary_upload_path(file_name, folder_prefix: folder_prefix) end def object_from_path(path) @@ -264,13 +288,15 @@ module FileStore end def s3_bucket - raise Discourse::SiteSettingMissing.new("s3_upload_bucket") if SiteSetting.Upload.s3_upload_bucket.blank? + if SiteSetting.Upload.s3_upload_bucket.blank? + raise Discourse::SiteSettingMissing.new("s3_upload_bucket") + end SiteSetting.Upload.s3_upload_bucket.downcase end def list_missing_uploads(skip_optimized: false) if SiteSetting.enable_s3_inventory - require 's3_inventory' + require "s3_inventory" S3Inventory.new(s3_helper, :upload).backfill_etags_and_list_missing S3Inventory.new(s3_helper, :optimized).backfill_etags_and_list_missing unless skip_optimized else @@ -326,7 +352,6 @@ module FileStore s3_options: FileStore::ToS3Migration.s3_options_from_site_settings, migrate_to_multisite: Rails.configuration.multisite, ).migrate - ensure FileUtils.rm(public_upload_path) if File.symlink?(public_upload_path) FileUtils.mv(old_upload_path, public_upload_path) if old_upload_path @@ -349,7 +374,8 @@ module FileStore if force_download && filename opts[:response_content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: filename + disposition: "attachment", + filename: filename, ) end @@ -375,11 +401,11 @@ module FileStore def list_missing(model, prefix) connection = ActiveRecord::Base.connection.raw_connection - connection.exec('CREATE TEMP TABLE verified_ids(val integer PRIMARY KEY)') + connection.exec("CREATE TEMP TABLE verified_ids(val integer PRIMARY KEY)") marker = nil files = s3_helper.list(prefix, marker) - while files.count > 0 do + while files.count > 0 verified_ids = [] files.each do |f| @@ -388,23 +414,25 @@ module FileStore marker = f.key end - verified_id_clause = verified_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") + verified_id_clause = + verified_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") connection.exec("INSERT INTO verified_ids VALUES #{verified_id_clause}") files = s3_helper.list(prefix, marker) end - missing_uploads = model.joins('LEFT JOIN verified_ids ON verified_ids.val = id').where("verified_ids.val IS NULL") + missing_uploads = + model.joins("LEFT JOIN verified_ids ON verified_ids.val = id").where( + "verified_ids.val IS NULL", + ) missing_count = missing_uploads.count if missing_count > 0 - missing_uploads.find_each do |upload| - puts upload.url - end + missing_uploads.find_each { |upload| puts upload.url } puts "#{missing_count} of #{model.count} #{model.name.underscore.pluralize} are missing" end ensure - connection.exec('DROP TABLE verified_ids') unless connection.nil? + connection.exec("DROP TABLE verified_ids") unless connection.nil? end end end diff --git a/lib/file_store/to_s3_migration.rb b/lib/file_store/to_s3_migration.rb index 0d6c098327..b99c911a6b 100644 --- a/lib/file_store/to_s3_migration.rb +++ b/lib/file_store/to_s3_migration.rb @@ -1,16 +1,20 @@ # frozen_string_literal: true -require 'aws-sdk-s3' +require "aws-sdk-s3" module FileStore ToS3MigrationError = Class.new(RuntimeError) class ToS3Migration - MISSING_UPLOADS_RAKE_TASK_NAME ||= 'posts:missing_uploads' + MISSING_UPLOADS_RAKE_TASK_NAME ||= "posts:missing_uploads" UPLOAD_CONCURRENCY ||= 20 - def initialize(s3_options:, dry_run: false, migrate_to_multisite: false, skip_etag_verify: false) - + def initialize( + s3_options:, + dry_run: false, + migrate_to_multisite: false, + skip_etag_verify: false + ) @s3_bucket = s3_options[:bucket] @s3_client_options = s3_options[:client_options] @dry_run = dry_run @@ -22,20 +26,18 @@ module FileStore def self.s3_options_from_site_settings { client_options: S3Helper.s3_options(SiteSetting), - bucket: SiteSetting.Upload.s3_upload_bucket + bucket: SiteSetting.Upload.s3_upload_bucket, } end def self.s3_options_from_env - unless ENV["DISCOURSE_S3_BUCKET"].present? && - ENV["DISCOURSE_S3_REGION"].present? && - ( - ( - ENV["DISCOURSE_S3_ACCESS_KEY_ID"].present? && - ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"].present? - ) || ENV["DISCOURSE_S3_USE_IAM_PROFILE"].present? - ) - + unless ENV["DISCOURSE_S3_BUCKET"].present? && ENV["DISCOURSE_S3_REGION"].present? && + ( + ( + ENV["DISCOURSE_S3_ACCESS_KEY_ID"].present? && + ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"].present? + ) || ENV["DISCOURSE_S3_USE_IAM_PROFILE"].present? + ) raise ToS3MigrationError.new(<<~TEXT) Please provide the following environment variables: - DISCOURSE_S3_BUCKET @@ -53,13 +55,10 @@ module FileStore if ENV["DISCOURSE_S3_USE_IAM_PROFILE"].blank? opts[:access_key_id] = ENV["DISCOURSE_S3_ACCESS_KEY_ID"] - opts[:secret_access_key] = ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"] + opts[:secret_access_key] = ENV["DISCOURSE_S3_SECRET_ACCESS_KEY"] end - { - client_options: opts, - bucket: ENV["DISCOURSE_S3_BUCKET"] - } + { client_options: opts, bucket: ENV["DISCOURSE_S3_BUCKET"] } end def migrate @@ -75,7 +74,8 @@ module FileStore base_url = File.join(SiteSetting.Upload.s3_base_url, prefix) count = Upload.by_users.where("url NOT LIKE '#{base_url}%'").count if count > 0 - error_message = "#{count} of #{Upload.count} uploads are not migrated to S3. #{failure_message}" + error_message = + "#{count} of #{Upload.count} uploads are not migrated to S3. #{failure_message}" raise_or_log(error_message, should_raise) success = false end @@ -88,7 +88,9 @@ module FileStore success = false end - Discourse::Application.load_tasks unless Rake::Task.task_defined?(MISSING_UPLOADS_RAKE_TASK_NAME) + unless Rake::Task.task_defined?(MISSING_UPLOADS_RAKE_TASK_NAME) + Discourse::Application.load_tasks + end Rake::Task[MISSING_UPLOADS_RAKE_TASK_NAME] count = DB.query_single(<<~SQL, Post::MISSING_UPLOADS, Post::MISSING_UPLOADS_IGNORED).first SELECT COUNT(1) @@ -109,10 +111,14 @@ module FileStore success = false end - count = Post.where('baked_version <> ? OR baked_version IS NULL', Post::BAKED_VERSION).count + count = Post.where("baked_version <> ? OR baked_version IS NULL", Post::BAKED_VERSION).count if count > 0 log("#{count} posts still require rebaking and will be rebaked during regular job") - log("To speed up migrations of posts we recommend you run 'rake posts:rebake_uncooked_posts'") if count > 100 + if count > 100 + log( + "To speed up migrations of posts we recommend you run 'rake posts:rebake_uncooked_posts'", + ) + end success = false else log("No posts require rebaking") @@ -153,8 +159,10 @@ module FileStore Upload.migrate_to_new_scheme if !uploads_migrated_to_new_scheme? - raise ToS3MigrationError.new("Some uploads could not be migrated to the new scheme. " \ - "You need to fix this manually.") + raise ToS3MigrationError.new( + "Some uploads could not be migrated to the new scheme. " \ + "You need to fix this manually.", + ) end end @@ -174,10 +182,12 @@ module FileStore log " - Listing local files" local_files = [] - IO.popen("cd #{public_directory} && find uploads/#{@current_db}/original -type f").each do |file| - local_files << file.chomp - putc "." if local_files.size % 1000 == 0 - end + IO + .popen("cd #{public_directory} && find uploads/#{@current_db}/original -type f") + .each do |file| + local_files << file.chomp + putc "." if local_files.size % 1000 == 0 + end log " => #{local_files.size} files" log " - Listing S3 files" @@ -203,19 +213,20 @@ module FileStore failed = [] lock = Mutex.new - upload_threads = UPLOAD_CONCURRENCY.times.map do - Thread.new do - while obj = queue.pop - if s3.put_object(obj[:options]).etag[obj[:etag]] - putc "." - lock.synchronize { synced += 1 } - else - putc "X" - lock.synchronize { failed << obj[:path] } + upload_threads = + UPLOAD_CONCURRENCY.times.map do + Thread.new do + while obj = queue.pop + if s3.put_object(obj[:options]).etag[obj[:etag]] + putc "." + lock.synchronize { synced += 1 } + else + putc "X" + lock.synchronize { failed << obj[:path] } + end end end end - end local_files.each do |file| path = File.join(public_directory, file) @@ -242,17 +253,17 @@ module FileStore if upload&.original_filename options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: upload.original_filename + disposition: "attachment", + filename: upload.original_filename, ) end - if upload&.secure - options[:acl] = "private" - end + options[:acl] = "private" if upload&.secure elsif !FileHelper.is_inline_image?(name) upload = Upload.find_by(url: "/#{file}") options[:content_disposition] = ActionDispatch::Http::ContentDisposition.format( - disposition: "attachment", filename: upload&.original_filename || name + disposition: "attachment", + filename: upload&.original_filename || name, ) end @@ -292,26 +303,25 @@ module FileStore [ [ "src=\"/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "src=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "src=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "src='/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "src='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "src='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "href=\"/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "href=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "href=\"#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "href='/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)", - "href='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1" + "href='#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1", ], [ "\\[img\\]/uploads/#{@current_db}/original/(\\dX/(?:[a-f0-9]/)*[a-f0-9]{40}[a-z0-9\\.]*)\\[/img\\]", - "[img]#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1[/img]" - ] + "[img]#{SiteSetting.Upload.s3_base_url}/#{prefix}\\1[/img]", + ], ].each do |from_url, to_url| - if @dry_run log "REPLACING '#{from_url}' WITH '#{to_url}'" else @@ -321,16 +331,22 @@ module FileStore unless @dry_run # Legacy inline image format - Post.where("raw LIKE '%![](/uploads/default/original/%)%'").each do |post| - regexp = /!\[\](\/uploads\/#{@current_db}\/original\/(\dX\/(?:[a-f0-9]\/)*[a-f0-9]{40}[a-z0-9\.]*))/ + Post + .where("raw LIKE '%![](/uploads/default/original/%)%'") + .each do |post| + regexp = + /!\[\](\/uploads\/#{@current_db}\/original\/(\dX\/(?:[a-f0-9]\/)*[a-f0-9]{40}[a-z0-9\.]*))/ - post.raw.scan(regexp).each do |upload_url, _| - upload = Upload.get_from_url(upload_url) - post.raw = post.raw.gsub("![](#{upload_url})", "![](#{upload.short_url})") + post + .raw + .scan(regexp) + .each do |upload_url, _| + upload = Upload.get_from_url(upload_url) + post.raw = post.raw.gsub("![](#{upload_url})", "![](#{upload.short_url})") + end + + post.save!(validate: false) end - - post.save!(validate: false) - end end if Discourse.asset_host.present? @@ -373,7 +389,6 @@ module FileStore migration_successful?(should_raise: true) log "Done!" - ensure Jobs.run_later! end diff --git a/lib/filter_best_posts.rb b/lib/filter_best_posts.rb index a243156b23..2ee63d35b5 100644 --- a/lib/filter_best_posts.rb +++ b/lib/filter_best_posts.rb @@ -1,16 +1,13 @@ # frozen_string_literal: true class FilterBestPosts - attr_accessor :filtered_posts, :posts def initialize(topic, filtered_posts, limit, options = {}) @filtered_posts = filtered_posts @topic = topic @limit = limit - options.each do |key, value| - self.instance_variable_set("@#{key}".to_sym, value) - end + options.each { |key, value| self.instance_variable_set("@#{key}".to_sym, value) } filter end @@ -31,37 +28,41 @@ class FilterBestPosts def filter_posts_liked_by_moderators return unless @only_moderator_liked - liked_by_moderators = PostAction.where(post_id: @filtered_posts.pluck(:id), post_action_type_id: PostActionType.types[:like]) - liked_by_moderators = liked_by_moderators.joins(:user).where('users.moderator').pluck(:post_id) + liked_by_moderators = + PostAction.where( + post_id: @filtered_posts.pluck(:id), + post_action_type_id: PostActionType.types[:like], + ) + liked_by_moderators = liked_by_moderators.joins(:user).where("users.moderator").pluck(:post_id) @filtered_posts = @filtered_posts.where(id: liked_by_moderators) end def setup_posts - @posts = @filtered_posts.order('percent_rank asc, sort_order asc').where("post_number > 1") + @posts = @filtered_posts.order("percent_rank asc, sort_order asc").where("post_number > 1") @posts = @posts.includes(:reply_to_user).includes(:topic).joins(:user).limit(@limit) end def filter_posts_based_on_trust_level - return unless @min_trust_level.try('>', 0) + return unless @min_trust_level.try(">", 0) @posts = - if @bypass_trust_level_score.try('>', 0) - @posts.where('COALESCE(users.trust_level,0) >= ? OR posts.score >= ?', + if @bypass_trust_level_score.try(">", 0) + @posts.where( + "COALESCE(users.trust_level,0) >= ? OR posts.score >= ?", @min_trust_level, - @bypass_trust_level_score + @bypass_trust_level_score, ) else - @posts.where('COALESCE(users.trust_level,0) >= ?', @min_trust_level) + @posts.where("COALESCE(users.trust_level,0) >= ?", @min_trust_level) end end def filter_posts_based_on_score - return unless @min_score.try('>', 0) - @posts = @posts.where('posts.score >= ?', @min_score) + return unless @min_score.try(">", 0) + @posts = @posts.where("posts.score >= ?", @min_score) end def sort_posts @posts = Post.from(@posts, :posts).order(post_number: :asc) end - end diff --git a/lib/final_destination.rb b/lib/final_destination.rb index fa5cbcc06d..893b38e9c5 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'socket' -require 'ipaddr' -require 'excon' -require 'rate_limiter' -require 'url_helper' +require "socket" +require "ipaddr" +require "excon" +require "rate_limiter" +require "url_helper" # Determine the final endpoint for a Web URI, following redirects class FinalDestination @@ -30,7 +30,8 @@ class FinalDestination "HTTPS_DOMAIN_#{domain}" end - DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15" + DEFAULT_USER_AGENT = + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15" attr_reader :status, :cookie, :status_code, :content_type, :ignored @@ -53,15 +54,11 @@ class FinalDestination if @limit > 0 ignore_redirects = [Discourse.base_url_no_prefix] - if @opts[:ignore_redirects] - ignore_redirects.concat(@opts[:ignore_redirects]) - end + ignore_redirects.concat(@opts[:ignore_redirects]) if @opts[:ignore_redirects] ignore_redirects.each do |ignore_redirect| ignore_redirect = uri(ignore_redirect) - if ignore_redirect.present? && ignore_redirect.hostname - @ignored << ignore_redirect.hostname - end + @ignored << ignore_redirect.hostname if ignore_redirect.present? && ignore_redirect.hostname end end @@ -74,7 +71,14 @@ class FinalDestination @timeout = @opts[:timeout] || nil @preserve_fragment_url = @preserve_fragment_url_hosts.any? { |host| hostname_matches?(host) } @validate_uri = @opts.fetch(:validate_uri) { true } - @user_agent = @force_custom_user_agent_hosts.any? { |host| hostname_matches?(host) } ? Onebox.options.user_agent : @default_user_agent + @user_agent = + ( + if @force_custom_user_agent_hosts.any? { |host| hostname_matches?(host) } + Onebox.options.user_agent + else + @default_user_agent + end + ) @stop_at_blocked_pages = @opts[:stop_at_blocked_pages] end @@ -107,10 +111,10 @@ class FinalDestination "User-Agent" => @user_agent, "Accept" => "*/*", "Accept-Language" => "*", - "Host" => @uri.hostname + "Host" => @uri.hostname, } - result['Cookie'] = @cookie if @cookie + result["Cookie"] = @cookie if @cookie result end @@ -119,7 +123,12 @@ class FinalDestination status_code, response_headers = nil catch(:done) do - FinalDestination::HTTP.start(@uri.host, @uri.port, use_ssl: @uri.is_a?(URI::HTTPS), open_timeout: timeout) do |http| + FinalDestination::HTTP.start( + @uri.host, + @uri.port, + use_ssl: @uri.is_a?(URI::HTTPS), + open_timeout: timeout, + ) do |http| http.read_timeout = timeout http.request_get(@uri.request_uri, request_headers) do |resp| status_code = resp.code.to_i @@ -162,7 +171,8 @@ class FinalDestination location = "#{@uri.scheme}://#{@uri.host}#{location}" if location[0] == "/" @uri = uri(location) - if @uri && redirects == @max_redirects && @https_redirect_ignore_limit && same_uri_but_https?(old_uri, @uri) + if @uri && redirects == @max_redirects && @https_redirect_ignore_limit && + same_uri_but_https?(old_uri, @uri) redirects += 1 @https_redirect_ignore_limit = false end @@ -177,7 +187,7 @@ class FinalDestination return if !@uri extra = nil - extra = { 'Cookie' => cookie } if cookie + extra = { "Cookie" => cookie } if cookie get(redirects - 1, extra_headers: extra, &blk) elsif result == :ok @@ -223,11 +233,16 @@ class FinalDestination request_start_time = Time.now response_body = +"" - request_validator = lambda do |chunk, _remaining_bytes, _total_bytes| - response_body << chunk - raise Excon::Errors::ExpectationFailed.new("response size too big: #{@uri.to_s}") if response_body.bytesize > MAX_REQUEST_SIZE_BYTES - raise Excon::Errors::ExpectationFailed.new("connect timeout reached: #{@uri.to_s}") if Time.now - request_start_time > MAX_REQUEST_TIME_SECONDS - end + request_validator = + lambda do |chunk, _remaining_bytes, _total_bytes| + response_body << chunk + if response_body.bytesize > MAX_REQUEST_SIZE_BYTES + raise Excon::Errors::ExpectationFailed.new("response size too big: #{@uri.to_s}") + end + if Time.now - request_start_time > MAX_REQUEST_TIME_SECONDS + raise Excon::Errors::ExpectationFailed.new("connect timeout reached: #{@uri.to_s}") + end + end # This technique will only use the first resolved IP # TODO: Can we standardise this by using FinalDestination::HTTP? @@ -240,18 +255,20 @@ class FinalDestination request_uri = @uri.dup request_uri.hostname = resolved_ip unless Rails.env.test? # WebMock doesn't understand the IP-based requests - response = Excon.public_send(@http_verb, - request_uri.to_s, - read_timeout: timeout, - connect_timeout: timeout, - headers: { "Host" => @uri.hostname }.merge(headers), - middlewares: middlewares, - response_block: request_validator, - ssl_verify_peer_host: @uri.hostname - ) + response = + Excon.public_send( + @http_verb, + request_uri.to_s, + read_timeout: timeout, + connect_timeout: timeout, + headers: { "Host" => @uri.hostname }.merge(headers), + middlewares: middlewares, + response_block: request_validator, + ssl_verify_peer_host: @uri.hostname, + ) if @stop_at_blocked_pages - if blocked_domain?(@uri) || response.headers['Discourse-No-Onebox'] == "1" + if blocked_domain?(@uri) || response.headers["Discourse-No-Onebox"] == "1" @status = :blocked_page return end @@ -282,7 +299,7 @@ class FinalDestination end end - @content_type = response.headers['Content-Type'] if response.headers.has_key?('Content-Type') + @content_type = response.headers["Content-Type"] if response.headers.has_key?("Content-Type") @status = :resolved return @uri when 103, 400, 405, 406, 409, 500, 501 @@ -306,11 +323,11 @@ class FinalDestination end response_headers = {} - if cookie_val = small_headers['set-cookie'] + if cookie_val = small_headers["set-cookie"] response_headers[:cookies] = cookie_val end - if location_val = small_headers['location'] + if location_val = small_headers["location"] response_headers[:location] = location_val.join end end @@ -318,21 +335,20 @@ class FinalDestination unless response_headers response_headers = { cookies: response.data[:cookies] || response.headers[:"set-cookie"], - location: response.headers[:location] + location: response.headers[:location], } end - if (300..399).include?(response_status) - location = response_headers[:location] - end + location = response_headers[:location] if (300..399).include?(response_status) if cookies = response_headers[:cookies] - @cookie = Array.wrap(cookies).map { |c| c.split(';').first.strip }.join('; ') + @cookie = Array.wrap(cookies).map { |c| c.split(";").first.strip }.join("; ") end if location redirect_uri = uri(location) - if @uri.host == redirect_uri.host && (redirect_uri.path =~ /\/login/ || redirect_uri.path =~ /\/session/) + if @uri.host == redirect_uri.host && + (redirect_uri.path =~ %r{/login} || redirect_uri.path =~ %r{/session}) @status = :resolved return @uri end @@ -342,7 +358,8 @@ class FinalDestination location = "#{@uri.scheme}://#{@uri.host}#{location}" if location[0] == "/" @uri = uri(location) - if @uri && @limit == @max_redirects && @https_redirect_ignore_limit && same_uri_but_https?(old_uri, @uri) + if @uri && @limit == @max_redirects && @https_redirect_ignore_limit && + same_uri_but_https?(old_uri, @uri) @limit += 1 @https_redirect_ignore_limit = false end @@ -376,12 +393,18 @@ class FinalDestination def validate_uri_format return false unless @uri && @uri.host - return false unless ['https', 'http'].include?(@uri.scheme) - return false if @uri.scheme == 'http' && @uri.port != 80 - return false if @uri.scheme == 'https' && @uri.port != 443 + return false unless %w[https http].include?(@uri.scheme) + return false if @uri.scheme == "http" && @uri.port != 80 + return false if @uri.scheme == "https" && @uri.port != 443 # Disallow IP based crawling - (IPAddr.new(@uri.hostname) rescue nil).nil? + ( + begin + IPAddr.new(@uri.hostname) + rescue StandardError + nil + end + ).nil? end def hostname @@ -392,11 +415,11 @@ class FinalDestination url = uri(url) if @uri&.hostname.present? && url&.hostname.present? - hostname_parts = url.hostname.split('.') - has_wildcard = hostname_parts.first == '*' + hostname_parts = url.hostname.split(".") + has_wildcard = hostname_parts.first == "*" if has_wildcard - @uri.hostname.end_with?(hostname_parts[1..-1].join('.')) + @uri.hostname.end_with?(hostname_parts[1..-1].join(".")) else @uri.hostname == url.hostname end @@ -413,7 +436,7 @@ class FinalDestination Rails.logger.public_send( log_level, - "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}", ) end @@ -425,15 +448,12 @@ class FinalDestination headers_subset = Struct.new(:location, :set_cookie).new safe_session(uri) do |http| - headers = request_headers.merge( - 'Accept-Encoding' => 'gzip', - 'Host' => uri.host - ) + headers = request_headers.merge("Accept-Encoding" => "gzip", "Host" => uri.host) req = FinalDestination::HTTP::Get.new(uri.request_uri, headers) http.request(req) do |resp| - headers_subset.set_cookie = resp['Set-Cookie'] + headers_subset.set_cookie = resp["Set-Cookie"] if @stop_at_blocked_pages dont_onebox = resp["Discourse-No-Onebox"] == "1" @@ -444,7 +464,7 @@ class FinalDestination end if Net::HTTPRedirection === resp - headers_subset.location = resp['location'] + headers_subset.location = resp["location"] result = :redirect, headers_subset end @@ -471,9 +491,7 @@ class FinalDestination end result = :ok, headers_subset else - catch(:done) do - yield resp, nil, nil - end + catch(:done) { yield resp, nil, nil } end end end @@ -490,7 +508,12 @@ class FinalDestination end def safe_session(uri) - FinalDestination::HTTP.start(uri.host, uri.port, use_ssl: (uri.scheme == "https"), open_timeout: timeout) do |http| + FinalDestination::HTTP.start( + uri.host, + uri.port, + use_ssl: (uri.scheme == "https"), + open_timeout: timeout, + ) do |http| http.read_timeout = timeout yield http end @@ -508,14 +531,14 @@ class FinalDestination def fetch_canonical_url(body) return if body.blank? - canonical_element = Nokogiri::HTML5(body).at("link[rel='canonical']") + canonical_element = Nokogiri.HTML5(body).at("link[rel='canonical']") return if canonical_element.nil? - canonical_uri = uri(canonical_element['href']) + canonical_uri = uri(canonical_element["href"]) return if canonical_uri.blank? return canonical_uri if canonical_uri.host.present? parts = [@uri.host, canonical_uri.to_s] - complete_url = canonical_uri.to_s.starts_with?('/') ? parts.join('') : parts.join('/') + complete_url = canonical_uri.to_s.starts_with?("/") ? parts.join("") : parts.join("/") complete_url = "#{@uri.scheme}://#{complete_url}" if @uri.scheme uri(complete_url) @@ -528,8 +551,7 @@ class FinalDestination def same_uri_but_https?(before, after) before = before.to_s after = after.to_s - before.start_with?("http://") && - after.start_with?("https://") && + before.start_with?("http://") && after.start_with?("https://") && before.sub("http://", "") == after.sub("https://", "") end end diff --git a/lib/final_destination/resolver.rb b/lib/final_destination/resolver.rb index f809099d4d..843a6a313b 100644 --- a/lib/final_destination/resolver.rb +++ b/lib/final_destination/resolver.rb @@ -39,18 +39,19 @@ class FinalDestination::Resolver def self.ensure_lookup_thread return if @thread&.alive? - @thread = Thread.new do - while true - @queue.deq - @error = nil - begin - @result = Addrinfo.getaddrinfo(@lookup, 80, nil, :STREAM).map(&:ip_address) - rescue => e - @error = e + @thread = + Thread.new do + while true + @queue.deq + @error = nil + begin + @result = Addrinfo.getaddrinfo(@lookup, 80, nil, :STREAM).map(&:ip_address) + rescue => e + @error = e + end + @parent.wakeup end - @parent.wakeup end - end @thread.name = "final-destination_resolver_thread" end end diff --git a/lib/final_destination/ssrf_detector.rb b/lib/final_destination/ssrf_detector.rb index aeb01d9ec9..0d06306ce8 100644 --- a/lib/final_destination/ssrf_detector.rb +++ b/lib/final_destination/ssrf_detector.rb @@ -2,8 +2,10 @@ class FinalDestination module SSRFDetector - class DisallowedIpError < SocketError; end - class LookupFailedError < SocketError; end + class DisallowedIpError < SocketError + end + class LookupFailedError < SocketError + end def self.standard_private_ranges @private_ranges ||= [ diff --git a/lib/flag_settings.rb b/lib/flag_settings.rb index da86d2c1b4..d88c01e35a 100644 --- a/lib/flag_settings.rb +++ b/lib/flag_settings.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true class FlagSettings - attr_reader( :without_custom_types, :notify_types, :topic_flag_types, :auto_action_types, - :custom_types + :custom_types, ) def initialize @@ -39,5 +38,4 @@ class FlagSettings def flag_types @all_flag_types end - end diff --git a/lib/freedom_patches/better_handlebars_errors.rb b/lib/freedom_patches/better_handlebars_errors.rb index c308f74410..a11eb27120 100644 --- a/lib/freedom_patches/better_handlebars_errors.rb +++ b/lib/freedom_patches/better_handlebars_errors.rb @@ -3,9 +3,8 @@ module Ember module Handlebars class Template - # Wrap in an IIFE in development mode to get the correct filename - def compile_ember_handlebars(string, ember_template = 'Handlebars', options = nil) + def compile_ember_handlebars(string, ember_template = "Handlebars", options = nil) if ::Rails.env.development? "(function() { try { return Ember.#{ember_template}.compile(#{indent(string).inspect}); } catch(err) { throw err; } })()" else diff --git a/lib/freedom_patches/cose_rsapkcs1.rb b/lib/freedom_patches/cose_rsapkcs1.rb index f6964e1835..55b639b5a5 100644 --- a/lib/freedom_patches/cose_rsapkcs1.rb +++ b/lib/freedom_patches/cose_rsapkcs1.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'cose' -require 'openssl/signature_algorithm/rsapkcs1' +require "cose" +require "openssl/signature_algorithm/rsapkcs1" # 'cose' gem does not implement all algorithms from the Web Authentication # (WebAuthn) standard specification. This patch implements one of the missing @@ -38,11 +38,11 @@ module COSE when OpenSSL::PKey::RSA key else - raise(COSE::Error, 'Incompatible key for algorithm') + raise(COSE::Error, "Incompatible key for algorithm") end end end - register(RSAPKCS1.new(-257, 'RS256', hash_function: 'SHA256')) + register(RSAPKCS1.new(-257, "RS256", hash_function: "SHA256")) end end diff --git a/lib/freedom_patches/fast_pluck.rb b/lib/freedom_patches/fast_pluck.rb index ef467c5666..b33e7a2723 100644 --- a/lib/freedom_patches/fast_pluck.rb +++ b/lib/freedom_patches/fast_pluck.rb @@ -5,7 +5,6 @@ # # class ActiveRecord::Relation - # Note: In discourse, the following code is included in lib/sql_builder.rb # # class RailsDateTimeDecoder < PG::SimpleDecoder @@ -43,7 +42,8 @@ class ActiveRecord::Relation end def pluck(*column_names) - if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? + if loaded? && + (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? return records.pluck(*column_names) end @@ -55,10 +55,12 @@ class ActiveRecord::Relation relation.select_values = column_names - klass.connection.select_raw(relation.arel) do |result, _| - result.type_map = DB.type_map - result.nfields == 1 ? result.column_values(0) : result.values - end + klass + .connection + .select_raw(relation.arel) do |result, _| + result.type_map = DB.type_map + result.nfields == 1 ? result.column_values(0) : result.values + end end end end diff --git a/lib/freedom_patches/inflector_backport.rb b/lib/freedom_patches/inflector_backport.rb index 67547cddad..7b1d02d404 100644 --- a/lib/freedom_patches/inflector_backport.rb +++ b/lib/freedom_patches/inflector_backport.rb @@ -6,7 +6,6 @@ module ActiveSupport module Inflector - LRU_CACHE_SIZE = 200 LRU_CACHES = [] @@ -22,26 +21,30 @@ module ActiveSupport uncached = "#{method_name}_without_cache" alias_method uncached, method_name - m = define_method(method_name) do |*arguments| - # this avoids recursive locks - found = true - data = cache.fetch(arguments) { found = false } - unless found - cache[arguments] = data = public_send(uncached, *arguments) + m = + define_method(method_name) do |*arguments| + # this avoids recursive locks + found = true + data = cache.fetch(arguments) { found = false } + cache[arguments] = data = public_send(uncached, *arguments) unless found + # so cache is never corrupted + data.dup end - # so cache is never corrupted - data.dup - end # https://bugs.ruby-lang.org/issues/16897 - if Module.respond_to?(:ruby2_keywords, true) - ruby2_keywords(m) - end + ruby2_keywords(m) if Module.respond_to?(:ruby2_keywords, true) end end - memoize :pluralize, :singularize, :camelize, :underscore, :humanize, - :titleize, :tableize, :classify, :foreign_key + memoize :pluralize, + :singularize, + :camelize, + :underscore, + :humanize, + :titleize, + :tableize, + :classify, + :foreign_key end end diff --git a/lib/freedom_patches/ip_addr.rb b/lib/freedom_patches/ip_addr.rb index e9b118b8d0..9c7053d66c 100644 --- a/lib/freedom_patches/ip_addr.rb +++ b/lib/freedom_patches/ip_addr.rb @@ -1,29 +1,28 @@ # frozen_string_literal: true class IPAddr - def self.handle_wildcards(val) return if val.blank? - num_wildcards = val.count('*') + num_wildcards = val.count("*") return val if num_wildcards == 0 # strip ranges like "/16" from the end if present - v = val.gsub(/\/.*/, '') + v = val.gsub(%r{/.*}, "") - return if v[v.index('*')..-1] =~ /[^\.\*]/ + return if v[v.index("*")..-1] =~ /[^\.\*]/ - parts = v.split('.') - (4 - parts.size).times { parts << '*' } # support strings like 192.* - v = parts.join('.') + parts = v.split(".") + (4 - parts.size).times { parts << "*" } # support strings like 192.* + v = parts.join(".") - "#{v.tr('*', '0')}/#{32 - (v.count('*') * 8)}" + "#{v.tr("*", "0")}/#{32 - (v.count("*") * 8)}" end def to_cidr_s if @addr - mask = @mask_addr.to_s(2).count('1') + mask = @mask_addr.to_s(2).count("1") if mask == 32 to_s else @@ -33,5 +32,4 @@ class IPAddr nil end end - end diff --git a/lib/freedom_patches/mail_disable_starttls.rb b/lib/freedom_patches/mail_disable_starttls.rb index 45daba893a..a8a60bda86 100644 --- a/lib/freedom_patches/mail_disable_starttls.rb +++ b/lib/freedom_patches/mail_disable_starttls.rb @@ -11,9 +11,7 @@ module FreedomPatches def build_smtp_session super.tap do |smtp| unless settings[:enable_starttls_auto] - if smtp.respond_to?(:disable_starttls) - smtp.disable_starttls - end + smtp.disable_starttls if smtp.respond_to?(:disable_starttls) end end end diff --git a/lib/freedom_patches/rails4.rb b/lib/freedom_patches/rails4.rb index 894c3dad90..806813b7c8 100644 --- a/lib/freedom_patches/rails4.rb +++ b/lib/freedom_patches/rails4.rb @@ -5,11 +5,13 @@ # Backporting a fix to rails itself may get too complex module FreedomPatches module Rails4 - - def self.distance_of_time_in_words(from_time, to_time = 0, include_seconds = false, options = {}) - options = { - scope: :'datetime.distance_in_words', - }.merge!(options) + def self.distance_of_time_in_words( + from_time, + to_time = 0, + include_seconds = false, + options = {} + ) + options = { scope: :"datetime.distance_in_words" }.merge!(options) from_time = from_time.to_time if from_time.respond_to?(:to_time) to_time = to_time.to_time if to_time.respond_to?(:to_time) @@ -20,49 +22,68 @@ module FreedomPatches I18n.with_options locale: options[:locale], scope: options[:scope] do |locale| case distance_in_minutes when 0..1 - return distance_in_minutes == 0 ? - locale.t(:less_than_x_minutes, count: 1) : - locale.t(:x_minutes, count: distance_in_minutes) unless include_seconds + unless include_seconds + return( + ( + if distance_in_minutes == 0 + locale.t(:less_than_x_minutes, count: 1) + else + locale.t(:x_minutes, count: distance_in_minutes) + end + ) + ) + end - case distance_in_seconds - when 0..4 then locale.t :less_than_x_seconds, count: 5 - when 5..9 then locale.t :less_than_x_seconds, count: 10 - when 10..19 then locale.t :less_than_x_seconds, count: 20 - when 20..39 then locale.t :half_a_minute - when 40..59 then locale.t :less_than_x_minutes, count: 1 - else locale.t :x_minutes, count: 1 - end - - when 2..44 then locale.t :x_minutes, count: distance_in_minutes - when 45..89 then locale.t :about_x_hours, count: 1 - when 90..1439 then locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round - when 1440..2519 then locale.t :x_days, count: 1 + case distance_in_seconds + when 0..4 + locale.t :less_than_x_seconds, count: 5 + when 5..9 + locale.t :less_than_x_seconds, count: 10 + when 10..19 + locale.t :less_than_x_seconds, count: 20 + when 20..39 + locale.t :half_a_minute + when 40..59 + locale.t :less_than_x_minutes, count: 1 + else + locale.t :x_minutes, count: 1 + end + when 2..44 + locale.t :x_minutes, count: distance_in_minutes + when 45..89 + locale.t :about_x_hours, count: 1 + when 90..1439 + locale.t :about_x_hours, count: (distance_in_minutes.to_f / 60.0).round + when 1440..2519 + locale.t :x_days, count: 1 # this is were we diverge from Rails - when 2520..129599 then locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round - when 129600..525599 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round - else + when 2520..129_599 + locale.t :x_days, count: (distance_in_minutes.to_f / 1440.0).round + when 129_600..525_599 + locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round + else fyear = from_time.year - fyear += 1 if from_time.month >= 3 - tyear = to_time.year - tyear -= 1 if to_time.month < 3 - leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } - minute_offset_for_leap_year = leap_years * 1440 - # Discount the leap year days when calculating year distance. - # e.g. if there are 20 leap year days between 2 dates having the same day - # and month then the based on 365 days calculation - # the distance in years will come out to over 80 years when in written - # english it would read better as about 80 years. - minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year - remainder = (minutes_with_offset % 525600) - distance_in_years = (minutes_with_offset / 525600) - if remainder < 131400 - locale.t(:about_x_years, count: distance_in_years) - elsif remainder < 394200 - locale.t(:over_x_years, count: distance_in_years) - else - locale.t(:almost_x_years, count: distance_in_years + 1) - end + fyear += 1 if from_time.month >= 3 + tyear = to_time.year + tyear -= 1 if to_time.month < 3 + leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count { |x| Date.leap?(x) } + minute_offset_for_leap_year = leap_years * 1440 + # Discount the leap year days when calculating year distance. + # e.g. if there are 20 leap year days between 2 dates having the same day + # and month then the based on 365 days calculation + # the distance in years will come out to over 80 years when in written + # english it would read better as about 80 years. + minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year + remainder = (minutes_with_offset % 525_600) + distance_in_years = (minutes_with_offset / 525_600) + if remainder < 131_400 + locale.t(:about_x_years, count: distance_in_years) + elsif remainder < 394_200 + locale.t(:over_x_years, count: distance_in_years) + else + locale.t(:almost_x_years, count: distance_in_years + 1) + end end end end diff --git a/lib/freedom_patches/rails_multisite.rb b/lib/freedom_patches/rails_multisite.rb index 99244669d7..0a78baaf32 100644 --- a/lib/freedom_patches/rails_multisite.rb +++ b/lib/freedom_patches/rails_multisite.rb @@ -19,12 +19,10 @@ module RailsMultisite handler end - ActiveRecord::Base.connected_to(role: reading_role) do - yield(db) if block_given? - end + ActiveRecord::Base.connected_to(role: reading_role) { yield(db) if block_given? } rescue => e - STDERR.puts "URGENT: Failed to initialize site #{db}: "\ - "#{e.class} #{e.message}\n#{e.backtrace.join("\n")}" + STDERR.puts "URGENT: Failed to initialize site #{db}: " \ + "#{e.class} #{e.message}\n#{e.backtrace.join("\n")}" # the show must go on, don't stop startup if multisite fails end @@ -34,11 +32,7 @@ module RailsMultisite class DiscoursePatches def self.config - { - db_lookup: lambda do |env| - env["PATH_INFO"] == "/srv/status" ? "default" : nil - end - } + { db_lookup: lambda { |env| env["PATH_INFO"] == "/srv/status" ? "default" : nil } } end end end diff --git a/lib/freedom_patches/safe_buffer.rb b/lib/freedom_patches/safe_buffer.rb index 5bc2feb01e..efc6880b6d 100644 --- a/lib/freedom_patches/safe_buffer.rb +++ b/lib/freedom_patches/safe_buffer.rb @@ -12,7 +12,8 @@ module FreedomPatches rescue Encoding::CompatibilityError raise if raise_encoding_err - encoding_diags = +"internal encoding #{Encoding.default_internal}, external encoding #{Encoding.default_external}" + encoding_diags = + +"internal encoding #{Encoding.default_internal}, external encoding #{Encoding.default_external}" if encoding != Encoding::UTF_8 encoding_diags << " my encoding is #{encoding} " force_encoding("UTF-8") @@ -20,12 +21,16 @@ module FreedomPatches encode!("utf-16", "utf-8", invalid: :replace) encode!("utf-8", "utf-16") end - Rails.logger.warn("Encountered a non UTF-8 string in SafeBuffer - #{self} - #{encoding_diags}") + Rails.logger.warn( + "Encountered a non UTF-8 string in SafeBuffer - #{self} - #{encoding_diags}", + ) end if value.encoding != Encoding::UTF_8 encoding_diags << " attempted to append encoding #{value.encoding} " value = value.dup.force_encoding("UTF-8").scrub - Rails.logger.warn("Attempted to concat a non UTF-8 string in SafeBuffer - #{value} - #{encoding_diags}") + Rails.logger.warn( + "Attempted to concat a non UTF-8 string in SafeBuffer - #{value} - #{encoding_diags}", + ) end concat(value, _raise = true) end diff --git a/lib/freedom_patches/safe_migrations.rb b/lib/freedom_patches/safe_migrations.rb index cdf989fdd5..61f9fe4754 100644 --- a/lib/freedom_patches/safe_migrations.rb +++ b/lib/freedom_patches/safe_migrations.rb @@ -5,7 +5,7 @@ # which rake:multisite_migrate uses # # The protection is only needed in Dev and Test -if ENV['RAILS_ENV'] != "production" - require_dependency 'migration/safe_migrate' +if ENV["RAILS_ENV"] != "production" + require_dependency "migration/safe_migrate" Migration::SafeMigrate.patch_active_record! end diff --git a/lib/freedom_patches/schema_migration_details.rb b/lib/freedom_patches/schema_migration_details.rb index 6b72cc8fdc..c02d9c51ac 100644 --- a/lib/freedom_patches/schema_migration_details.rb +++ b/lib/freedom_patches/schema_migration_details.rb @@ -5,9 +5,7 @@ module FreedomPatches def exec_migration(conn, direction) rval = nil - time = Benchmark.measure do - rval = super - end + time = Benchmark.measure { rval = super } sql = < err Discourse.warn_exception( - err, message: "Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name})." + err, + message: + "Unexpected error when checking SMTP credentials for group #{group.id} (#{group.name}).", ) nil end diff --git a/lib/guardian.rb b/lib/guardian.rb index e6a4ad76ac..1a9b2dd52f 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'guardian/category_guardian' -require 'guardian/ensure_magic' -require 'guardian/post_guardian' -require 'guardian/bookmark_guardian' -require 'guardian/topic_guardian' -require 'guardian/user_guardian' -require 'guardian/post_revision_guardian' -require 'guardian/group_guardian' -require 'guardian/tag_guardian' +require "guardian/category_guardian" +require "guardian/ensure_magic" +require "guardian/post_guardian" +require "guardian/bookmark_guardian" +require "guardian/topic_guardian" +require "guardian/user_guardian" +require "guardian/post_revision_guardian" +require "guardian/group_guardian" +require "guardian/tag_guardian" # The guardian is responsible for confirming access to various site resources and operations class Guardian @@ -89,7 +89,7 @@ class Guardian def user @user.presence end - alias :current_user :user + alias current_user user def anonymous? !authenticated? @@ -127,7 +127,9 @@ class Guardian if @category_group_moderator_groups.key?(reviewable_by_group_id) @category_group_moderator_groups[reviewable_by_group_id] else - @category_group_moderator_groups[reviewable_by_group_id] = category_group_moderator_scope.exists?("categories.id": category.id) + @category_group_moderator_groups[ + reviewable_by_group_id + ] = category_group_moderator_scope.exists?("categories.id": category.id) end end @@ -136,16 +138,14 @@ class Guardian end def is_developer? - @user && - is_admin? && - ( - Rails.env.development? || - Developer.user_ids.include?(@user.id) || + @user && is_admin? && ( - Rails.configuration.respond_to?(:developer_emails) && - Rails.configuration.developer_emails.include?(@user.email) + Rails.env.development? || Developer.user_ids.include?(@user.id) || + ( + Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(@user.email) + ) ) - ) end def is_staged? @@ -203,12 +203,13 @@ class Guardian end def can_moderate?(obj) - obj && authenticated? && !is_silenced? && ( - is_staff? || - (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]) && can_see_topic?(obj)) - ) + obj && authenticated? && !is_silenced? && + ( + is_staff? || + (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]) && can_see_topic?(obj)) + ) end - alias :can_see_flags? :can_moderate? + alias can_see_flags? can_moderate? def can_tag?(topic) return false if topic.blank? @@ -229,9 +230,7 @@ class Guardian end def can_delete_reviewable_queued_post?(reviewable) - reviewable.present? && - authenticated? && - reviewable.created_by_id == @user.id + reviewable.present? && authenticated? && reviewable.created_by_id == @user.id end def can_see_group?(group) @@ -243,7 +242,9 @@ class Guardian return true if is_admin? || group.members_visibility_level == Group.visibility_levels[:public] return true if is_staff? && group.members_visibility_level == Group.visibility_levels[:staff] return true if is_staff? && group.members_visibility_level == Group.visibility_levels[:members] - return true if authenticated? && group.members_visibility_level == Group.visibility_levels[:logged_on_users] + if authenticated? && group.members_visibility_level == Group.visibility_levels[:logged_on_users] + return true + end return false if user.blank? return false unless membership = GroupUser.find_by(group_id: group.id, user_id: user.id) @@ -257,10 +258,19 @@ class Guardian def can_see_groups?(groups) return false if groups.blank? - return true if is_admin? || groups.all? { |g| g.visibility_level == Group.visibility_levels[:public] } - return true if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:staff] } - return true if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:members] } - return true if authenticated? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:logged_on_users] } + if is_admin? || groups.all? { |g| g.visibility_level == Group.visibility_levels[:public] } + return true + end + if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:staff] } + return true + end + if is_staff? && groups.all? { |g| g.visibility_level == Group.visibility_levels[:members] } + return true + end + if authenticated? && + groups.all? { |g| g.visibility_level == Group.visibility_levels[:logged_on_users] } + return true + end return false if user.blank? memberships = GroupUser.where(group: groups, user_id: user.id).pluck(:owner) @@ -277,7 +287,8 @@ class Guardian return false if groups.blank? requested_group_ids = groups.map(&:id) # Can't use pluck, groups could be a regular array - matching_group_ids = Group.where(id: requested_group_ids).members_visible_groups(user).pluck(:id) + matching_group_ids = + Group.where(id: requested_group_ids).members_visible_groups(user).pluck(:id) matching_group_ids.sort == requested_group_ids.sort end @@ -285,12 +296,10 @@ class Guardian # Can we impersonate this user? def can_impersonate?(target) target && - - # You must be an admin to impersonate - is_admin? && - - # You may not impersonate other admins unless you are a dev - (!target.admin? || is_developer?) + # You must be an admin to impersonate + is_admin? && + # You may not impersonate other admins unless you are a dev + (!target.admin? || is_developer?) # Additionally, you may not impersonate yourself; # but the two tests for different admin statuses @@ -313,7 +322,7 @@ class Guardian def can_suspend?(user) user && is_staff? && user.regular? end - alias :can_deactivate? :can_suspend? + alias can_deactivate? can_suspend? def can_revoke_admin?(admin) can_administer_user?(admin) && admin.admin? @@ -337,10 +346,13 @@ class Guardian return true if title.empty? # A title set to '(none)' in the UI is an empty string return false if user != @user - return true if user.badges - .where(allow_title: true) - .pluck(:name) - .any? { |name| Badge.display_name(name) == title } + if user + .badges + .where(allow_title: true) + .pluck(:name) + .any? { |name| Badge.display_name(name) == title } + return true + end user.groups.where(title: title).exists? end @@ -349,13 +361,13 @@ class Guardian return false if !user || !group_id group = Group.find_by(id: group_id.to_i) - user.group_ids.include?(group_id.to_i) && - (group ? !group.automatic : false) + user.group_ids.include?(group_id.to_i) && (group ? !group.automatic : false) end def can_use_flair_group?(user, group_id = nil) return false if !user || !group_id || !user.group_ids.include?(group_id.to_i) - flair_icon, flair_upload_id = Group.where(id: group_id.to_i).pluck_first(:flair_icon, :flair_upload_id) + flair_icon, flair_upload_id = + Group.where(id: group_id.to_i).pluck_first(:flair_icon, :flair_upload_id) flair_icon.present? || flair_upload_id.present? end @@ -387,10 +399,9 @@ class Guardian end def can_invite_to_forum?(groups = nil) - authenticated? && - (is_staff? || SiteSetting.max_invites_per_day.to_i.positive?) && - (is_staff? || @user.has_trust_level?(SiteSetting.min_trust_level_to_allow_invite.to_i)) && - (is_admin? || groups.blank? || groups.all? { |g| can_edit_group?(g) }) + authenticated? && (is_staff? || SiteSetting.max_invites_per_day.to_i.positive?) && + (is_staff? || @user.has_trust_level?(SiteSetting.min_trust_level_to_allow_invite.to_i)) && + (is_admin? || groups.blank? || groups.all? { |g| can_edit_group?(g) }) end def can_invite_to?(object, groups = nil) @@ -402,9 +413,7 @@ class Guardian if object.private_message? return true if is_admin? - if !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) - return false - end + return false if !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) return false if object.reached_recipients_limit? && !is_staff? end @@ -441,8 +450,7 @@ class Guardian end def can_invite_group_to_private_message?(group, topic) - can_see_topic?(topic) && - can_send_private_message?(group) + can_see_topic?(topic) && can_send_private_message?(group) end ## @@ -459,8 +467,11 @@ class Guardian # User is authenticated authenticated? && # User can send PMs, this can be covered by trust levels as well via AUTO_GROUPS - (is_staff? || from_bot || from_system || \ - (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || notify_moderators) + ( + is_staff? || from_bot || from_system || + (@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map)) || + notify_moderators + ) end ## @@ -480,14 +491,14 @@ class Guardian # User is authenticated and can send PMs, this can be covered by trust levels as well via AUTO_GROUPS (can_send_private_messages?(notify_moderators: notify_moderators) || group_is_messageable) && - # User disabled private message - (is_staff? || target_is_group || target.user_option.allow_private_messages) && - # Can't send PMs to suspended users - (is_staff? || target_is_group || !target.suspended?) && - # Check group messageable level - (from_system || target_is_user || group_is_messageable || notify_moderators) && - # Silenced users can only send PM to staff - (!is_silenced? || target.staff?) + # User disabled private message + (is_staff? || target_is_group || target.user_option.allow_private_messages) && + # Can't send PMs to suspended users + (is_staff? || target_is_group || !target.suspended?) && + # Check group messageable level + (from_system || target_is_user || group_is_messageable || notify_moderators) && + # Silenced users can only send PM to staff + (!is_silenced? || target.staff?) end def can_send_private_messages_to_email? @@ -503,17 +514,18 @@ class Guardian def can_export_entity?(entity) return false if anonymous? return true if is_admin? - return entity != 'user_list' if is_moderator? + return entity != "user_list" if is_moderator? # Regular users can only export their archives return false unless entity == "user_archive" - UserExport.where(user_id: @user.id, created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day)).count == 0 + UserExport.where( + user_id: @user.id, + created_at: (Time.zone.now.beginning_of_day..Time.zone.now.end_of_day), + ).count == 0 end def can_mute_user?(target_user) - can_mute_users? && - @user.id != target_user.id && - !target_user.staff? + can_mute_users? && @user.id != target_user.id && !target_user.staff? end def can_mute_users? @@ -546,20 +558,15 @@ class Guardian return true if theme_ids.blank? if allowed_theme_ids = Theme.allowed_remote_theme_ids - if (theme_ids - allowed_theme_ids).present? - return false - end + return false if (theme_ids - allowed_theme_ids).present? end - if include_preview && is_staff? && (theme_ids - Theme.theme_ids).blank? - return true - end + return true if include_preview && is_staff? && (theme_ids - Theme.theme_ids).blank? parent = theme_ids.first components = theme_ids[1..-1] || [] - Theme.user_theme_ids.include?(parent) && - (components - Theme.components_for(parent)).empty? + Theme.user_theme_ids.include?(parent) && (components - Theme.components_for(parent)).empty? end def can_publish_page?(topic) @@ -608,7 +615,6 @@ class Guardian private def is_my_own?(obj) - unless anonymous? return obj.user_id == @user.id if obj.respond_to?(:user_id) && obj.user_id && @user.id return obj.user == @user if obj.respond_to?(:user) @@ -650,9 +656,8 @@ class Guardian end def category_group_moderator_scope - Category - .joins("INNER JOIN group_users ON group_users.group_id = categories.reviewable_by_group_id") - .where("group_users.user_id = ?", user.id) + Category.joins( + "INNER JOIN group_users ON group_users.group_id = categories.reviewable_by_group_id", + ).where("group_users.user_id = ?", user.id) end - end diff --git a/lib/guardian/category_guardian.rb b/lib/guardian/category_guardian.rb index 35bf8d30af..e437455dfc 100644 --- a/lib/guardian/category_guardian.rb +++ b/lib/guardian/category_guardian.rb @@ -2,40 +2,31 @@ #mixin for all guardian methods dealing with category permissions module CategoryGuardian - # Creating Method def can_create_category?(parent = nil) - is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? - ) + is_admin? || (SiteSetting.moderators_manage_categories_and_groups && is_moderator?) end # Editing Method def can_edit_category?(category) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see_category?(category) - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && + can_see_category?(category) + ) end def can_edit_serialized_category?(category_id:, read_restricted:) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see_serialized_category?(category_id: category_id, read_restricted: read_restricted) - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && + can_see_serialized_category?(category_id: category_id, read_restricted: read_restricted) + ) end def can_delete_category?(category) - can_edit_category?(category) && - category.topic_count <= 0 && - !category.uncategorized? && - !category.has_children? + can_edit_category?(category) && category.topic_count <= 0 && !category.uncategorized? && + !category.has_children? end def can_see_serialized_category?(category_id:, read_restricted: true) @@ -84,6 +75,7 @@ module CategoryGuardian end def topic_featured_link_allowed_category_ids - @topic_featured_link_allowed_category_ids = Category.where(topic_featured_link_allowed: true).pluck(:id) + @topic_featured_link_allowed_category_ids = + Category.where(topic_featured_link_allowed: true).pluck(:id) end end diff --git a/lib/guardian/ensure_magic.rb b/lib/guardian/ensure_magic.rb index bff9f402dc..62cece83b6 100644 --- a/lib/guardian/ensure_magic.rb +++ b/lib/guardian/ensure_magic.rb @@ -2,13 +2,14 @@ # Support for ensure_{blah}! methods. module EnsureMagic - def method_missing(method, *args, &block) if method.to_s =~ /^ensure_(.*)\!$/ can_method = :"#{Regexp.last_match[1]}?" if respond_to?(can_method) - raise Discourse::InvalidAccess.new("#{can_method} failed") unless send(can_method, *args, &block) + unless send(can_method, *args, &block) + raise Discourse::InvalidAccess.new("#{can_method} failed") + end return end end @@ -20,5 +21,4 @@ module EnsureMagic def ensure_can_see!(obj) raise Discourse::InvalidAccess.new("Can't see #{obj}") unless can_see?(obj) end - end diff --git a/lib/guardian/group_guardian.rb b/lib/guardian/group_guardian.rb index b3e571776c..7b153615a1 100644 --- a/lib/guardian/group_guardian.rb +++ b/lib/guardian/group_guardian.rb @@ -2,14 +2,9 @@ #mixin for all guardian methods dealing with group permissions module GroupGuardian - # Creating Method def can_create_group? - is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? - ) + is_admin? || (SiteSetting.moderators_manage_categories_and_groups && is_moderator?) end # Edit authority for groups means membership changes only. @@ -17,17 +12,15 @@ module GroupGuardian # table and thus do not allow membership changes. def can_edit_group?(group) !group.automatic && - (can_admin_group?(group) || group.users.where('group_users.owner').include?(user)) + (can_admin_group?(group) || group.users.where("group_users.owner").include?(user)) end def can_admin_group?(group) is_admin? || - ( - SiteSetting.moderators_manage_categories_and_groups && - is_moderator? && - can_see?(group) && - group.id != Group::AUTO_GROUPS[:admins] - ) + ( + SiteSetting.moderators_manage_categories_and_groups && is_moderator? && can_see?(group) && + group.id != Group::AUTO_GROUPS[:admins] + ) end def can_see_group_messages?(group) diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 49205fcc16..6ca1256896 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -2,26 +2,24 @@ # mixin for all guardian methods dealing with post permissions module PostGuardian - def unrestricted_link_posting? authenticated? && @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_post_links]) end def link_posting_access if unrestricted_link_posting? - 'full' + "full" elsif SiteSetting.allowed_link_domains.present? - 'limited' + "limited" else - 'none' + "none" end end def can_post_link?(host: nil) return false if host.blank? - unrestricted_link_posting? || - SiteSetting.allowed_link_domains.split('|').include?(host) + unrestricted_link_posting? || SiteSetting.allowed_link_domains.split("|").include?(host) end # Can the user act on the post in a particular way. @@ -30,47 +28,55 @@ module PostGuardian return false unless (can_see_post.nil? && can_see_post?(post)) || can_see_post # no warnings except for staff - return false if action_key == :notify_user && (post.user.blank? || (!is_staff? && opts[:is_warning].present? && opts[:is_warning] == 'true')) + if action_key == :notify_user && + ( + post.user.blank? || + (!is_staff? && opts[:is_warning].present? && opts[:is_warning] == "true") + ) + return false + end taken = opts[:taken_actions].try(:keys).to_a - is_flag = PostActionType.notify_flag_types[action_key] || PostActionType.custom_types[action_key] + is_flag = + PostActionType.notify_flag_types[action_key] || PostActionType.custom_types[action_key] already_taken_this_action = taken.any? && taken.include?(PostActionType.types[action_key]) - already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? + already_did_flagging = taken.any? && (taken & PostActionType.notify_flag_types.values).any? - result = if authenticated? && post && !@user.anonymous? + result = + if authenticated? && post && !@user.anonymous? + # Silenced users can't flag + return false if is_flag && @user.silenced? - # Silenced users can't flag - return false if is_flag && @user.silenced? + # Hidden posts can't be flagged + return false if is_flag && post.hidden? - # Hidden posts can't be flagged - return false if is_flag && post.hidden? + # post made by staff, but we don't allow staff flags + return false if is_flag && (!SiteSetting.allow_flagging_staff?) && post&.user&.staff? - # post made by staff, but we don't allow staff flags - return false if is_flag && - (!SiteSetting.allow_flagging_staff?) && - post&.user&.staff? + if action_key == :notify_user && + !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) + return false + end - if action_key == :notify_user && !@user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) - return false + # we allow flagging for trust level 1 and higher + # always allowed for private messages + ( + is_flag && not(already_did_flagging) && + ( + @user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_flag_posts]) || + post.topic.private_message? + ) + ) || + # not a flagging action, and haven't done it already + not(is_flag || already_taken_this_action) && + # nothing except flagging on archived topics + not(post.topic&.archived?) && + # nothing except flagging on deleted posts + not(post.trashed?) && + # don't like your own stuff + not(action_key == :like && (post.user.blank? || is_my_own?(post))) end - # we allow flagging for trust level 1 and higher - # always allowed for private messages - (is_flag && not(already_did_flagging) && (@user.has_trust_level?(TrustLevel[SiteSetting.min_trust_to_flag_posts]) || post.topic.private_message?)) || - - # not a flagging action, and haven't done it already - not(is_flag || already_taken_this_action) && - - # nothing except flagging on archived topics - not(post.topic&.archived?) && - - # nothing except flagging on deleted posts - not(post.trashed?) && - - # don't like your own stuff - not(action_key == :like && (post.user.blank? || is_my_own?(post))) - end - !!result end @@ -94,12 +100,16 @@ module PostGuardian end def can_delete_all_posts?(user) - is_staff? && - user && - !user.admin? && - (is_admin? || - ((user.first_post_created_at.nil? || user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago) && - user.post_count <= SiteSetting.delete_all_posts_max.to_i)) + is_staff? && user && !user.admin? && + ( + is_admin? || + ( + ( + user.first_post_created_at.nil? || + user.first_post_created_at >= SiteSetting.delete_user_max_post_age.days.ago + ) && user.post_count <= SiteSetting.delete_all_posts_max.to_i + ) + ) end def can_create_post?(topic) @@ -108,53 +118,43 @@ module PostGuardian key = topic_memoize_key(topic) @can_create_post ||= {} - @can_create_post.fetch(key) do - @can_create_post[key] = can_create_post_in_topic?(topic) - end + @can_create_post.fetch(key) { @can_create_post[key] = can_create_post_in_topic?(topic) } end def can_edit_post?(post) - if Discourse.static_doc_topic_ids.include?(post.topic_id) && !is_admin? - return false - end + return false if Discourse.static_doc_topic_ids.include?(post.topic_id) && !is_admin? return true if is_admin? # Must be staff to edit a locked post return false if post.locked? && !is_staff? - return can_create_post?(post.topic) if ( - is_staff? || - ( - SiteSetting.trusted_users_can_edit_others? && - @user.has_trust_level?(TrustLevel[4]) - ) || - is_category_group_moderator?(post.topic&.category) - ) - - if post.topic&.archived? || post.user_deleted || post.deleted_at - return false + if ( + is_staff? || + (SiteSetting.trusted_users_can_edit_others? && @user.has_trust_level?(TrustLevel[4])) || + is_category_group_moderator?(post.topic&.category) + ) + return can_create_post?(post.topic) end + return false if post.topic&.archived? || post.user_deleted || post.deleted_at + # Editing a shared draft. - return true if ( - can_see_post?(post) && - can_create_post?(post.topic) && - post.topic.category_id == SiteSetting.shared_drafts_category.to_i && - can_see_category?(post.topic.category) && - can_see_shared_draft? - ) + if ( + can_see_post?(post) && can_create_post?(post.topic) && + post.topic.category_id == SiteSetting.shared_drafts_category.to_i && + can_see_category?(post.topic.category) && can_see_shared_draft? + ) + return true + end if post.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) return can_create_post?(post.topic) end - if @user.trust_level < SiteSetting.min_trust_to_edit_post - return false - end + return false if @user.trust_level < SiteSetting.min_trust_to_edit_post if is_my_own?(post) - return false if @user.silenced? return can_edit_hidden_post?(post) if post.hidden? @@ -175,7 +175,8 @@ module PostGuardian def can_edit_hidden_post?(post) return false if post.nil? - post.hidden_at.nil? || post.hidden_at < SiteSetting.cooldown_minutes_after_hiding_posts.minutes.ago + post.hidden_at.nil? || + post.hidden_at < SiteSetting.cooldown_minutes_after_hiding_posts.minutes.ago end def can_delete_post_or_topic?(post) @@ -195,7 +196,12 @@ module PostGuardian # You can delete your own posts if is_my_own?(post) - return false if (SiteSetting.max_post_deletions_per_minute < 1 || SiteSetting.max_post_deletions_per_day < 1) + if ( + SiteSetting.max_post_deletions_per_minute < 1 || + SiteSetting.max_post_deletions_per_day < 1 + ) + return false + end return true if !post.user_deleted? end @@ -208,7 +214,9 @@ module PostGuardian return false if post.is_first_post? return false if !is_admin? || !can_edit_post?(post) return false if !post.deleted_at - return false if post.deleted_by_id == @user.id && post.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + if post.deleted_by_id == @user.id && post.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + return false + end true end @@ -220,7 +228,12 @@ module PostGuardian return true if can_moderate_topic?(topic) && !!post.deleted_at if is_my_own?(post) - return false if (SiteSetting.max_post_deletions_per_minute < 1 || SiteSetting.max_post_deletions_per_day < 1) + if ( + SiteSetting.max_post_deletions_per_minute < 1 || + SiteSetting.max_post_deletions_per_day < 1 + ) + return false + end return true if post.user_deleted && !post.deleted_at end @@ -230,19 +243,29 @@ module PostGuardian def can_delete_post_action?(post_action) return false unless is_my_own?(post_action) && !post_action.is_private_message? - post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago && !post_action.post&.topic&.archived? + post_action.created_at > SiteSetting.post_undo_action_window_mins.minutes.ago && + !post_action.post&.topic&.archived? end def can_see_post?(post) return false if post.blank? return true if is_admin? return false unless can_see_post_topic?(post) - return false unless post.user == @user || Topic.visible_post_types(@user).include?(post.post_type) + unless post.user == @user || Topic.visible_post_types(@user).include?(post.post_type) + return false + end return true if is_moderator? || is_category_group_moderator?(post.topic.category) - return true if post.deleted_at.blank? || (post.deleted_by_id == @user.id && @user.has_trust_level?(TrustLevel[4])) + return true if !post.trashed? || can_see_deleted_post?(post) false end + def can_see_deleted_post?(post) + return false if !post.trashed? + return false if @user.anonymous? + return true if is_staff? + post.deleted_by_id == @user.id && @user.has_trust_level?(TrustLevel[4]) + end + def can_view_edit_history?(post) return false unless post @@ -250,9 +273,7 @@ module PostGuardian return true if post.wiki || SiteSetting.edit_history_visible_to_public end - authenticated? && - (is_staff? || @user.id == post.user_id) && - can_see_post?(post) + authenticated? && (is_staff? || @user.id == post.user_id) && can_see_post?(post) end def can_change_post_owner? @@ -308,13 +329,18 @@ module PostGuardian private def can_create_post_in_topic?(topic) - return false if !SiteSetting.enable_system_message_replies? && topic.try(:subtype) == "system_message" + if !SiteSetting.enable_system_message_replies? && topic.try(:subtype) == "system_message" + return false + end - (!SpamRule::AutoSilence.prevent_posting?(@user) || (!!topic.try(:private_message?) && topic.allowed_users.include?(@user))) && ( - !topic || - !topic.category || - Category.post_create_allowed(self).where(id: topic.category.id).count == 1 - ) + ( + !SpamRule::AutoSilence.prevent_posting?(@user) || + (!!topic.try(:private_message?) && topic.allowed_users.include?(@user)) + ) && + ( + !topic || !topic.category || + Category.post_create_allowed(self).where(id: topic.category.id).count == 1 + ) end def topic_memoize_key(topic) @@ -329,8 +355,6 @@ module PostGuardian key = topic_memoize_key(topic) @can_see_post_topic ||= {} - @can_see_post_topic.fetch(key) do - @can_see_post_topic[key] = can_see_topic?(topic) - end + @can_see_post_topic.fetch(key) { @can_see_post_topic[key] = can_see_topic?(topic) } end end diff --git a/lib/guardian/post_revision_guardian.rb b/lib/guardian/post_revision_guardian.rb index 4372728b95..1e61b19e74 100644 --- a/lib/guardian/post_revision_guardian.rb +++ b/lib/guardian/post_revision_guardian.rb @@ -2,7 +2,6 @@ # mixin for all Guardian methods dealing with post_revisions permissions module PostRevisionGuardian - def can_see_post_revision?(post_revision) return false unless post_revision return false if post_revision.hidden && !can_view_hidden_post_revisions? @@ -21,5 +20,4 @@ module PostRevisionGuardian def can_view_hidden_post_revisions? is_staff? end - end diff --git a/lib/guardian/tag_guardian.rb b/lib/guardian/tag_guardian.rb index 5a4be92ab4..db1ec7688c 100644 --- a/lib/guardian/tag_guardian.rb +++ b/lib/guardian/tag_guardian.rb @@ -3,11 +3,13 @@ #mixin for all guardian methods dealing with tagging permissions module TagGuardian def can_create_tag? - SiteSetting.tagging_enabled && @user.has_trust_level_or_staff?(SiteSetting.min_trust_to_create_tag) + SiteSetting.tagging_enabled && + @user.has_trust_level_or_staff?(SiteSetting.min_trust_to_create_tag) end def can_tag_topics? - SiteSetting.tagging_enabled && @user.has_trust_level_or_staff?(SiteSetting.min_trust_level_to_tag_topics) + SiteSetting.tagging_enabled && + @user.has_trust_level_or_staff?(SiteSetting.min_trust_level_to_tag_topics) end def can_tag_pms? @@ -16,7 +18,8 @@ module TagGuardian return true if @user == Discourse.system_user group_ids = SiteSetting.pm_tags_allowed_for_groups_map - group_ids.include?(Group::AUTO_GROUPS[:everyone]) || @user.group_users.exists?(group_id: group_ids) + group_ids.include?(Group::AUTO_GROUPS[:everyone]) || + @user.group_users.exists?(group_id: group_ids) end def can_admin_tags? @@ -28,12 +31,13 @@ module TagGuardian end def hidden_tag_names - @hidden_tag_names ||= begin - if SiteSetting.tagging_enabled && !is_staff? - DiscourseTagging.hidden_tag_names(self) - else - [] + @hidden_tag_names ||= + begin + if SiteSetting.tagging_enabled && !is_staff? + DiscourseTagging.hidden_tag_names(self) + else + [] + end end - end end end diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index c487de6897..05fb6ad041 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -3,13 +3,11 @@ #mixin for all guardian methods dealing with topic permissions module TopicGuardian def can_remove_allowed_users?(topic, target_user = nil) - is_staff? || - (topic.user == @user && @user.has_trust_level?(TrustLevel[2])) || - ( - topic.allowed_users.count > 1 && - topic.user != target_user && - !!(target_user && user == target_user) - ) + is_staff? || (topic.user == @user && @user.has_trust_level?(TrustLevel[2])) || + ( + topic.allowed_users.count > 1 && topic.user != target_user && + !!(target_user && user == target_user) + ) end def can_review_topic?(topic) @@ -49,10 +47,10 @@ module TopicGuardian # Creating Methods def can_create_topic?(parent) is_staff? || - (user && - user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i && - can_create_post?(parent) && - Category.topic_create_allowed(self).limit(1).count == 1) + ( + user && user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i && + can_create_post?(parent) && Category.topic_create_allowed(self).limit(1).count == 1 + ) end def can_create_topic_on_category?(category) @@ -60,11 +58,18 @@ module TopicGuardian category_id = Category === category ? category.id : category can_create_topic?(nil) && - (!category || Category.topic_create_allowed(self).where(id: category_id).count == 1) + (!category || Category.topic_create_allowed(self).where(id: category_id).count == 1) end def can_move_topic_to_category?(category) - category = Category === category ? category : Category.find(category || SiteSetting.uncategorized_category_id) + category = + ( + if Category === category + category + else + Category.find(category || SiteSetting.uncategorized_category_id) + end + ) is_staff? || (can_create_topic_on_category?(category) && !category.require_topic_approval?) end @@ -75,7 +80,9 @@ module TopicGuardian return false if topic.trashed? return true if is_admin? - trusted = (authenticated? && user.has_trust_level?(TrustLevel[4])) || is_moderator? || can_perform_action_available_to_group_moderators?(topic) + trusted = + (authenticated? && user.has_trust_level?(TrustLevel[4])) || is_moderator? || + can_perform_action_available_to_group_moderators?(topic) (!(topic.closed? || topic.archived?) || trusted) && can_create_post?(topic) end @@ -97,45 +104,40 @@ module TopicGuardian # except for a tiny edge case where the topic is uncategorized and you are trying # to fix it but uncategorized is disabled if ( - SiteSetting.allow_uncategorized_topics || - topic.category_id != SiteSetting.uncategorized_category_id - ) + SiteSetting.allow_uncategorized_topics || + topic.category_id != SiteSetting.uncategorized_category_id + ) return false if !can_create_topic_on_category?(topic.category) end # Editing a shared draft. - return true if ( - !topic.archived && - !topic.private_message? && - topic.category_id == SiteSetting.shared_drafts_category.to_i && - can_see_category?(topic.category) && - can_see_shared_draft? && - can_create_post?(topic) - ) + if ( + !topic.archived && !topic.private_message? && + topic.category_id == SiteSetting.shared_drafts_category.to_i && + can_see_category?(topic.category) && can_see_shared_draft? && can_create_post?(topic) + ) + return true + end # TL4 users can edit archived topics, but can not edit private messages - return true if ( - SiteSetting.trusted_users_can_edit_others? && - topic.archived && - !topic.private_message? && - user.has_trust_level?(TrustLevel[4]) && - can_create_post?(topic) - ) + if ( + SiteSetting.trusted_users_can_edit_others? && topic.archived && !topic.private_message? && + user.has_trust_level?(TrustLevel[4]) && can_create_post?(topic) + ) + return true + end # TL3 users can not edit archived topics and private messages - return true if ( - SiteSetting.trusted_users_can_edit_others? && - !topic.archived && - !topic.private_message? && - user.has_trust_level?(TrustLevel[3]) && - can_create_post?(topic) - ) + if ( + SiteSetting.trusted_users_can_edit_others? && !topic.archived && !topic.private_message? && + user.has_trust_level?(TrustLevel[3]) && can_create_post?(topic) + ) + return true + end return false if topic.archived - is_my_own?(topic) && - !topic.edit_time_limit_expired?(user) && - !first_post&.locked? && + is_my_own?(topic) && !topic.edit_time_limit_expired?(user) && !first_post&.locked? && (!first_post&.hidden? || can_edit_hidden_post?(first_post)) end @@ -149,9 +151,13 @@ module TopicGuardian def can_delete_topic?(topic) !topic.trashed? && - (is_staff? || (is_my_own?(topic) && topic.posts_count <= 1 && topic.created_at && topic.created_at > 24.hours.ago) || is_category_group_moderator?(topic.category)) && - !topic.is_category_topic? && - !Discourse.static_doc_topic_ids.include?(topic.id) + ( + is_staff? || + ( + is_my_own?(topic) && topic.posts_count <= 1 && topic.created_at && + topic.created_at > 24.hours.ago + ) || is_category_group_moderator?(topic.category) + ) && !topic.is_category_topic? && !Discourse.static_doc_topic_ids.include?(topic.id) end def can_permanently_delete_topic?(topic) @@ -165,15 +171,21 @@ module TopicGuardian # All other posts that were deleted still must be permanently deleted # before the topic can be deleted with the exception of small action # posts that will be deleted right before the topic is. - all_posts_count = Post.with_deleted - .where(topic_id: topic.id) - .where(post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]]) - .count + all_posts_count = + Post + .with_deleted + .where(topic_id: topic.id) + .where( + post_type: [Post.types[:regular], Post.types[:moderator_action], Post.types[:whisper]], + ) + .count return false if all_posts_count > 1 return false if !is_admin? || !can_see_topic?(topic) return false if !topic.deleted_at - return false if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + if topic.deleted_by_id == @user.id && topic.deleted_at >= Post::PERMANENT_DELETE_TIMER.ago + return false + end true end @@ -181,7 +193,7 @@ module TopicGuardian can_moderate?(topic) || can_perform_action_available_to_group_moderators?(topic) end - alias :can_create_unlisted_topic? :can_toggle_topic_visibility? + alias can_create_unlisted_topic? can_toggle_topic_visibility? def can_convert_topic?(topic) return false unless @user.in_any_groups?(SiteSetting.personal_message_enabled_groups_map) @@ -228,13 +240,16 @@ module TopicGuardian # Filter out topics with shared drafts if user cannot see shared drafts if !can_see_shared_draft? - default_scope = default_scope.left_outer_joins(:shared_draft).where("shared_drafts.id IS NULL") + default_scope = + default_scope.left_outer_joins(:shared_draft).where("shared_drafts.id IS NULL") end all_topics_scope = if authenticated? Topic.unscoped.merge( - secured_regular_topic_scope(default_scope, topic_ids: topic_ids).or(private_message_topic_scope(default_scope)) + secured_regular_topic_scope(default_scope, topic_ids: topic_ids).or( + private_message_topic_scope(default_scope), + ), ) else Topic.unscoped.merge(secured_regular_topic_scope(default_scope, topic_ids: topic_ids)) @@ -256,7 +271,10 @@ module TopicGuardian category = topic.category can_see_category?(category) && - (!category.read_restricted || !is_staged? || secure_category_ids.include?(category.id) || topic.user == user) + ( + !category.read_restricted || !is_staged? || secure_category_ids.include?(category.id) || + topic.user == user + ) end def can_get_access_to_topic?(topic) @@ -266,9 +284,17 @@ module TopicGuardian def filter_allowed_categories(records) return records if is_admin? && !SiteSetting.suppress_secured_categories_from_admin - records = allowed_category_ids.size == 0 ? - records.where('topics.category_id IS NULL') : - records.where('topics.category_id IS NULL or topics.category_id IN (?)', allowed_category_ids) + records = + ( + if allowed_category_ids.size == 0 + records.where("topics.category_id IS NULL") + else + records.where( + "topics.category_id IS NULL or topics.category_id IN (?)", + allowed_category_ids, + ) + end + ) records.references(:categories) end @@ -276,7 +302,10 @@ module TopicGuardian def can_edit_featured_link?(category_id) return false unless SiteSetting.topic_featured_link_enabled return false unless @user.trust_level >= TrustLevel.levels[:basic] - Category.where(id: category_id || SiteSetting.uncategorized_category_id, topic_featured_link_allowed: true).exists? + Category.where( + id: category_id || SiteSetting.uncategorized_category_id, + topic_featured_link_allowed: true, + ).exists? end def can_update_bumped_at? @@ -292,7 +321,8 @@ module TopicGuardian return false if topic.private_message? && !can_tag_pms? return true if can_edit_topic?(topic) - if topic&.first_post&.wiki && (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) + if topic&.first_post&.wiki && + (@user.trust_level >= SiteSetting.min_trust_to_edit_wiki_post.to_i) return can_create_post?(topic) end @@ -306,12 +336,12 @@ module TopicGuardian is_category_group_moderator?(topic.category) end - alias :can_archive_topic? :can_perform_action_available_to_group_moderators? - alias :can_close_topic? :can_perform_action_available_to_group_moderators? - alias :can_open_topic? :can_perform_action_available_to_group_moderators? - alias :can_split_merge_topic? :can_perform_action_available_to_group_moderators? - alias :can_edit_staff_notes? :can_perform_action_available_to_group_moderators? - alias :can_pin_unpin_topic? :can_perform_action_available_to_group_moderators? + alias can_archive_topic? can_perform_action_available_to_group_moderators? + alias can_close_topic? can_perform_action_available_to_group_moderators? + alias can_open_topic? can_perform_action_available_to_group_moderators? + alias can_split_merge_topic? can_perform_action_available_to_group_moderators? + alias can_edit_staff_notes? can_perform_action_available_to_group_moderators? + alias can_pin_unpin_topic? can_perform_action_available_to_group_moderators? def can_move_posts?(topic) return false if is_silenced? @@ -327,12 +357,10 @@ module TopicGuardian def private_message_topic_scope(scope) pm_scope = scope.private_messages_for_user(user) - if is_moderator? - pm_scope = pm_scope.or(scope.where(<<~SQL)) + pm_scope = pm_scope.or(scope.where(<<~SQL)) if is_moderator? topics.subtype = '#{TopicSubtype.moderator_warning}' OR topics.id IN (#{Topic.has_flag_scope.select(:topic_id).to_sql}) SQL - end pm_scope end @@ -357,7 +385,8 @@ module TopicGuardian ) SQL - secured_scope = secured_scope.or(Topic.unscoped.where(sql, user_id: user.id, topic_ids: topic_ids)) + secured_scope = + secured_scope.or(Topic.unscoped.where(sql, user_id: user.id, topic_ids: topic_ids)) end scope.listable_topics.merge(secured_scope) diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 675e3431ef..2879ad036f 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -2,9 +2,8 @@ # mixin for all Guardian methods dealing with user permissions module UserGuardian - def can_claim_reviewable_topic?(topic) - SiteSetting.reviewable_claiming != 'disabled' && can_review_topic?(topic) + SiteSetting.reviewable_claiming != "disabled" && can_review_topic?(topic) end def can_pick_avatar?(user_avatar, upload) @@ -63,13 +62,14 @@ module UserGuardian if is_me?(user) !SiteSetting.enable_discourse_connect && - !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) + !user.has_more_posts_than?(SiteSetting.delete_user_self_max_post_count) else - is_staff? && ( - user.first_post_created_at.nil? || - !user.has_more_posts_than?(User::MAX_STAFF_DELETE_POST_COUNT) || - user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago - ) + is_staff? && + ( + user.first_post_created_at.nil? || + !user.has_more_posts_than?(User::MAX_STAFF_DELETE_POST_COUNT) || + user.first_post_created_at > SiteSetting.delete_user_max_post_age.to_i.days.ago + ) end end @@ -123,9 +123,7 @@ module UserGuardian return true if !SiteSetting.allow_users_to_hide_profile? # If a user has hidden their profile, restrict it to them and staff - if user.user_option.try(:hide_profile_and_presence?) - return is_me?(user) || is_staff? - end + return is_me?(user) || is_staff? if user.user_option.try(:hide_profile_and_presence?) true end @@ -141,14 +139,13 @@ module UserGuardian is_staff_or_is_me = is_staff? || is_me?(user) cache_key = is_staff_or_is_me ? :staff_or_me : :other - @allowed_user_field_ids[cache_key] ||= - begin - if is_staff_or_is_me - UserField.pluck(:id) - else - UserField.where("show_on_profile OR show_on_user_card").pluck(:id) - end + @allowed_user_field_ids[cache_key] ||= begin + if is_staff_or_is_me + UserField.pluck(:id) + else + UserField.where("show_on_profile OR show_on_user_card").pluck(:id) end + end end def can_feature_topic?(user, topic) @@ -161,13 +158,14 @@ module UserGuardian end def can_see_review_queue? - is_staff? || ( - SiteSetting.enable_category_group_moderation && - Reviewable - .where(reviewable_by_group_id: @user.group_users.pluck(:group_id)) - .where('category_id IS NULL or category_id IN (?)', allowed_category_ids) - .exists? - ) + is_staff? || + ( + SiteSetting.enable_category_group_moderation && + Reviewable + .where(reviewable_by_group_id: @user.group_users.pluck(:group_id)) + .where("category_id IS NULL or category_id IN (?)", allowed_category_ids) + .exists? + ) end def can_see_summary_stats?(target_user) @@ -175,11 +173,17 @@ module UserGuardian end def can_upload_profile_header?(user) - (is_me?(user) && user.has_trust_level?(SiteSetting.min_trust_level_to_allow_profile_background.to_i)) || is_staff? + ( + is_me?(user) && + user.has_trust_level?(SiteSetting.min_trust_level_to_allow_profile_background.to_i) + ) || is_staff? end def can_upload_user_card_background?(user) - (is_me?(user) && user.has_trust_level?(SiteSetting.min_trust_level_to_allow_user_card_background.to_i)) || is_staff? + ( + is_me?(user) && + user.has_trust_level?(SiteSetting.min_trust_level_to_allow_user_card_background.to_i) + ) || is_staff? end def can_upload_external? diff --git a/lib/has_errors.rb b/lib/has_errors.rb index 907e537d1c..daa8bb8e8d 100644 --- a/lib/has_errors.rb +++ b/lib/has_errors.rb @@ -33,11 +33,8 @@ module HasErrors def add_errors_from(obj) return if obj.blank? - if obj.is_a?(StandardError) - return add_error(obj.message) - end + return add_error(obj.message) if obj.is_a?(StandardError) obj.errors.full_messages.each { |msg| add_error(msg) } end - end diff --git a/lib/highlight_js.rb b/lib/highlight_js.rb index 8ad4f26264..48bd77baf3 100644 --- a/lib/highlight_js.rb +++ b/lib/highlight_js.rb @@ -2,12 +2,47 @@ module HighlightJs HIGHLIGHTJS_DIR ||= "#{Rails.root}/vendor/assets/javascripts/highlightjs/" - BUNDLED_LANGS = %w(bash c cpp csharp css diff go graphql ini java javascript json kotlin less lua makefile xml markdown objectivec perl php php-template plaintext python python-repl r ruby rust scss shell sql swift typescript vbnet wasm yaml) + BUNDLED_LANGS = %w[ + bash + c + cpp + csharp + css + diff + go + graphql + ini + java + javascript + json + kotlin + less + lua + makefile + xml + markdown + objectivec + perl + php + php-template + plaintext + python + python-repl + r + ruby + rust + scss + shell + sql + swift + typescript + vbnet + wasm + yaml + ] def self.languages - langs = Dir.glob(HIGHLIGHTJS_DIR + "languages/*.js").map do |path| - File.basename(path)[0..-8] - end + langs = Dir.glob(HIGHLIGHTJS_DIR + "languages/*.js").map { |path| File.basename(path)[0..-8] } langs.sort end @@ -26,8 +61,9 @@ module HighlightJs end def self.version(lang_string) - (@lang_string_cache ||= {})[lang_string] ||= - Digest::SHA1.hexdigest(bundle lang_string.split("|")) + (@lang_string_cache ||= {})[lang_string] ||= Digest::SHA1.hexdigest( + bundle lang_string.split("|") + ) end def self.path diff --git a/lib/hijack.rb b/lib/hijack.rb index 0b39abb710..eb9b5ce216 100644 --- a/lib/hijack.rb +++ b/lib/hijack.rb @@ -1,19 +1,17 @@ # frozen_string_literal: true -require 'method_profiler' +require "method_profiler" # This module allows us to hijack a request and send it to the client in the deferred job queue # For cases where we are making remote calls like onebox or proxying files and so on this helps # free up a unicorn worker while the remote IO is happening module Hijack - def hijack(info: nil, &blk) controller_class = self.class - if hijack = request.env['rack.hijack'] - - request.env['discourse.request_tracker.skip'] = true - request_tracker = request.env['discourse.request_tracker'] + if hijack = request.env["rack.hijack"] + request.env["discourse.request_tracker.skip"] = true + request_tracker = request.env["discourse.request_tracker"] # in the past unicorn would recycle env, this is not longer the case env = request.env @@ -32,7 +30,6 @@ module Hijack original_headers = response.headers.dup Scheduler::Defer.later("hijack #{params["controller"]} #{params["action"]} #{info}") do - MethodProfiler.start(transfer_timings) begin Thread.current[Logster::Logger::LOGSTER_ENV] = env @@ -47,22 +44,22 @@ module Hijack instance.response = response instance.request = request_copy - original_headers&.each do |k, v| - instance.response.headers[k] = v - end + original_headers&.each { |k, v| instance.response.headers[k] = v } view_start = Process.clock_gettime(Process::CLOCK_MONOTONIC) begin instance.instance_eval(&blk) rescue => e # TODO we need to reuse our exception handling in ApplicationController - Discourse.warn_exception(e, message: "Failed to process hijacked response correctly", env: env) + Discourse.warn_exception( + e, + message: "Failed to process hijacked response correctly", + env: env, + ) end view_runtime = Process.clock_gettime(Process::CLOCK_MONOTONIC) - view_start - unless instance.response_body || response.committed? - instance.status = 500 - end + instance.status = 500 unless instance.response_body || response.committed? response.commit! @@ -74,13 +71,11 @@ module Hijack Discourse::Cors.apply_headers(cors_origins, env, headers) end - headers['Content-Type'] ||= response.content_type || "text/plain" - headers['Content-Length'] = body.bytesize - headers['Connection'] = "close" + headers["Content-Type"] ||= response.content_type || "text/plain" + headers["Content-Length"] = body.bytesize + headers["Connection"] = "close" - if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] - headers['Discourse-Logged-Out'] = '1' - end + headers["Discourse-Logged-Out"] = "1" if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] status_string = Rack::Utils::HTTP_STATUS_CODES[response.status.to_i] || "Unknown" io.write "#{response.status} #{status_string}\r\n" @@ -90,9 +85,7 @@ module Hijack headers["X-Runtime"] = "#{"%0.6f" % duration}" end - headers.each do |name, val| - io.write "#{name}: #{val}\r\n" - end + headers.each { |name, val| io.write "#{name}: #{val}\r\n" } io.write "\r\n" io.write body @@ -100,30 +93,35 @@ module Hijack # happens if client terminated before we responded, ignore io = nil ensure - if Rails.configuration.try(:lograge).try(:enabled) if timings db_runtime = 0 - if timings[:sql] - db_runtime = timings[:sql][:duration] - end + db_runtime = timings[:sql][:duration] if timings[:sql] subscriber = Lograge::LogSubscribers::ActionController.new - payload = ActiveSupport::HashWithIndifferentAccess.new( - controller: self.class.name, - action: action_name, - params: request.filtered_parameters, - headers: request.headers, - format: request.format.ref, - method: request.request_method, - path: request.fullpath, - view_runtime: view_runtime * 1000.0, - db_runtime: db_runtime * 1000.0, - timings: timings, - status: response.status - ) + payload = + ActiveSupport::HashWithIndifferentAccess.new( + controller: self.class.name, + action: action_name, + params: request.filtered_parameters, + headers: request.headers, + format: request.format.ref, + method: request.request_method, + path: request.fullpath, + view_runtime: view_runtime * 1000.0, + db_runtime: db_runtime * 1000.0, + timings: timings, + status: response.status, + ) - event = ActiveSupport::Notifications::Event.new("hijack", Time.now, Time.now + timings[:total_duration], "", payload) + event = + ActiveSupport::Notifications::Event.new( + "hijack", + Time.now, + Time.now + timings[:total_duration], + "", + payload, + ) subscriber.process_action(event) end end @@ -131,10 +129,19 @@ module Hijack MethodProfiler.clear Thread.current[Logster::Logger::LOGSTER_ENV] = nil - io.close if io rescue nil + begin + io.close if io + rescue StandardError + nil + end if request_tracker - status = response.status rescue 500 + status = + begin + response.status + rescue StandardError + 500 + end request_tracker.log_request_info(env, [status, headers || {}, []], timings) end diff --git a/lib/homepage_constraint.rb b/lib/homepage_constraint.rb index ef6686d5fd..78492e4f01 100644 --- a/lib/homepage_constraint.rb +++ b/lib/homepage_constraint.rb @@ -6,7 +6,7 @@ class HomePageConstraint end def matches?(request) - return @filter == 'finish_installation' if SiteSetting.has_login_hint? + return @filter == "finish_installation" if SiteSetting.has_login_hint? current_user = CurrentUser.lookup_from_env(request.env) homepage = current_user&.user_option&.homepage || SiteSetting.anonymous_homepage diff --git a/lib/html_prettify.rb b/lib/html_prettify.rb index 06a00099b2..074a3c8a2a 100644 --- a/lib/html_prettify.rb +++ b/lib/html_prettify.rb @@ -82,14 +82,14 @@ class HtmlPrettify < String elsif @options.include?(-1) do_stupefy = true else - do_quotes = @options.include?(:quotes) - do_backticks = @options.include?(:backticks) - do_backticks = :both if @options.include?(:allbackticks) - do_dashes = :normal if @options.include?(:dashes) - do_dashes = :oldschool if @options.include?(:oldschool) - do_dashes = :inverted if @options.include?(:inverted) - do_ellipses = @options.include?(:ellipses) - do_stupefy = @options.include?(:stupefy) + do_quotes = @options.include?(:quotes) + do_backticks = @options.include?(:backticks) + do_backticks = :both if @options.include?(:allbackticks) + do_dashes = :normal if @options.include?(:dashes) + do_dashes = :oldschool if @options.include?(:oldschool) + do_dashes = :inverted if @options.include?(:inverted) + do_ellipses = @options.include?(:ellipses) + do_stupefy = @options.include?(:stupefy) end # Parse the HTML @@ -110,8 +110,8 @@ class HtmlPrettify < String tokens.each do |token| if token.first == :tag result << token[1] - if token[1] =~ %r!<(/?)(?:pre|code|kbd|script|math)[\s>]! - in_pre = ($1 != "/") # Opening or closing tag? + if token[1] =~ %r{<(/?)(?:pre|code|kbd|script|math)[\s>]} + in_pre = ($1 != "/") # Opening or closing tag? end else t = token[1] @@ -120,24 +120,23 @@ class HtmlPrettify < String last_char = t[-1].chr unless in_pre - t.gsub!("'", "'") t.gsub!(""", '"') if do_dashes - t = educate_dashes t if do_dashes == :normal - t = educate_dashes_oldschool t if do_dashes == :oldschool - t = educate_dashes_inverted t if do_dashes == :inverted + t = educate_dashes t if do_dashes == :normal + t = educate_dashes_oldschool t if do_dashes == :oldschool + t = educate_dashes_inverted t if do_dashes == :inverted end - t = educate_ellipses t if do_ellipses + t = educate_ellipses t if do_ellipses t = educate_fractions t # Note: backticks need to be processed before quotes. if do_backticks t = educate_backticks t - t = educate_single_backticks t if do_backticks == :both + t = educate_single_backticks t if do_backticks == :both end if do_quotes @@ -161,7 +160,7 @@ class HtmlPrettify < String end end - t = stupefy_entities t if do_stupefy + t = stupefy_entities t if do_stupefy end prev_token_last_char = last_char @@ -179,8 +178,7 @@ class HtmlPrettify < String # em-dash HTML entity. # def educate_dashes(str) - str. - gsub(/--/, entity(:em_dash)) + str.gsub(/--/, entity(:em_dash)) end # The string, with each instance of "--" translated to an @@ -188,9 +186,7 @@ class HtmlPrettify < String # em-dash HTML entity. # def educate_dashes_oldschool(str) - str. - gsub(/---/, entity(:em_dash)). - gsub(/--/, entity(:en_dash)) + str.gsub(/---/, entity(:em_dash)).gsub(/--/, entity(:en_dash)) end # Return the string, with each instance of "--" translated @@ -204,9 +200,7 @@ class HtmlPrettify < String # Aaron Swartz for the idea.) # def educate_dashes_inverted(str) - str. - gsub(/---/, entity(:en_dash)). - gsub(/--/, entity(:em_dash)) + str.gsub(/---/, entity(:en_dash)).gsub(/--/, entity(:em_dash)) end # Return the string, with each instance of "..." translated @@ -214,31 +208,25 @@ class HtmlPrettify < String # spaces between the dots. # def educate_ellipses(str) - str. - gsub('...', entity(:ellipsis)). - gsub('. . .', entity(:ellipsis)) + str.gsub("...", entity(:ellipsis)).gsub(". . .", entity(:ellipsis)) end # Return the string, with "``backticks''"-style single quotes # translated into HTML curly quote entities. # def educate_backticks(str) - str. - gsub("``", entity(:double_left_quote)). - gsub("''", entity(:double_right_quote)) + str.gsub("``", entity(:double_left_quote)).gsub("''", entity(:double_right_quote)) end # Return the string, with "`backticks'"-style single quotes # translated into HTML curly quote entities. # def educate_single_backticks(str) - str. - gsub("`", entity(:single_left_quote)). - gsub("'", entity(:single_right_quote)) + str.gsub("`", entity(:single_left_quote)).gsub("'", entity(:single_right_quote)) end def educate_fractions(str) - str.gsub(/(\s+|^)(1\/4|1\/2|3\/4)([,.;\s]|$)/) do + str.gsub(%r{(\s+|^)(1/4|1/2|3/4)([,.;\s]|$)}) do frac = if $2 == "1/2" entity(:frac12) @@ -261,52 +249,45 @@ class HtmlPrettify < String # Special case if the very first character is a quote followed by # punctuation at a non-word-break. Close the quotes by brute # force: - str.gsub!(/^'(?=#{punct_class}\B)/, - entity(:single_right_quote)) - str.gsub!(/^"(?=#{punct_class}\B)/, - entity(:double_right_quote)) + str.gsub!(/^'(?=#{punct_class}\B)/, entity(:single_right_quote)) + str.gsub!(/^"(?=#{punct_class}\B)/, entity(:double_right_quote)) # Special case for double sets of quotes, e.g.: #

    He said, "'Quoted' words in a larger quote."

    - str.gsub!(/"'(?=\w)/, - "#{entity(:double_left_quote)}#{entity(:single_left_quote)}") - str.gsub!(/'"(?=\w)/, - "#{entity(:single_left_quote)}#{entity(:double_left_quote)}") + str.gsub!(/"'(?=\w)/, "#{entity(:double_left_quote)}#{entity(:single_left_quote)}") + str.gsub!(/'"(?=\w)/, "#{entity(:single_left_quote)}#{entity(:double_left_quote)}") # Special case for decade abbreviations (the '80s): - str.gsub!(/'(?=\d\ds)/, - entity(:single_right_quote)) + str.gsub!(/'(?=\d\ds)/, entity(:single_right_quote)) close_class = %![^\ \t\r\n\\[\{\(\-]! dec_dashes = "#{entity(:en_dash)}|#{entity(:em_dash)}" # Get most opening single quotes: - str.gsub!(/(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)'(?=\w)/, - '\1' + entity(:single_left_quote)) + str.gsub!( + /(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)'(?=\w)/, + '\1' + entity(:single_left_quote), + ) # Single closing quotes: - str.gsub!(/(#{close_class})'/, - '\1' + entity(:single_right_quote)) - str.gsub!(/'(\s|s\b|$)/, - entity(:single_right_quote) + '\1') + str.gsub!(/(#{close_class})'/, '\1' + entity(:single_right_quote)) + str.gsub!(/'(\s|s\b|$)/, entity(:single_right_quote) + '\1') # Any remaining single quotes should be opening ones: - str.gsub!(/'/, - entity(:single_left_quote)) + str.gsub!(/'/, entity(:single_left_quote)) # Get most opening double quotes: - str.gsub!(/(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)"(?=\w)/, - '\1' + entity(:double_left_quote)) + str.gsub!( + /(\s| |=|--|&[mn]dash;|#{dec_dashes}|ȁ[34];)"(?=\w)/, + '\1' + entity(:double_left_quote), + ) # Double closing quotes: - str.gsub!(/(#{close_class})"/, - '\1' + entity(:double_right_quote)) - str.gsub!(/"(\s|s\b|$)/, - entity(:double_right_quote) + '\1') + str.gsub!(/(#{close_class})"/, '\1' + entity(:double_right_quote)) + str.gsub!(/"(\s|s\b|$)/, entity(:double_right_quote) + '\1') # Any remaining quotes should be opening ones: - str.gsub!(/"/, - entity(:double_left_quote)) + str.gsub!(/"/, entity(:double_left_quote)) str end @@ -320,16 +301,14 @@ class HtmlPrettify < String new_str = str.dup { - en_dash: '-', - em_dash: '--', + en_dash: "-", + em_dash: "--", single_left_quote: "'", single_right_quote: "'", double_left_quote: '"', double_right_quote: '"', - ellipsis: '...' - }.each do |k, v| - new_str.gsub!(/#{entity(k)}/, v) - end + ellipsis: "...", + }.each { |k, v| new_str.gsub!(/#{entity(k)}/, v) } new_str end @@ -354,14 +333,12 @@ class HtmlPrettify < String prev_end = 0 scan(tag_soup) do - tokens << [:text, $1] if $1 != "" + tokens << [:text, $1] if $1 != "" tokens << [:tag, $2] prev_end = $~.end(0) end - if prev_end < size - tokens << [:text, self[prev_end..-1]] - end + tokens << [:text, self[prev_end..-1]] if prev_end < size tokens end @@ -385,5 +362,4 @@ class HtmlPrettify < String def entity(key) @entities[key] end - end diff --git a/lib/html_to_markdown.rb b/lib/html_to_markdown.rb index 2d2783d467..67626fd76e 100644 --- a/lib/html_to_markdown.rb +++ b/lib/html_to_markdown.rb @@ -3,12 +3,11 @@ require "securerandom" class HtmlToMarkdown - def initialize(html, opts = {}) @opts = opts # we're only interested in - @doc = Nokogiri::HTML5(html).at("body") + @doc = Nokogiri.HTML5(html).at("body") remove_not_allowed!(@doc) remove_hidden!(@doc) @@ -17,9 +16,7 @@ class HtmlToMarkdown end def to_markdown - traverse(@doc) - .gsub(/\n{2,}/, "\n\n") - .strip + traverse(@doc).gsub(/\n{2,}/, "\n\n").strip end private @@ -50,31 +47,33 @@ class HtmlToMarkdown loop do changed = false - doc.css("br.#{klass}").each do |br| - parent = br.parent + doc + .css("br.#{klass}") + .each do |br| + parent = br.parent - if block?(parent) - br.remove_class(klass) - else - before, after = parent.children.slice_when { |n| n == br }.to_a + if block?(parent) + br.remove_class(klass) + else + before, after = parent.children.slice_when { |n| n == br }.to_a - if before.size > 1 - b = doc.document.create_element(parent.name) - before[0...-1].each { |c| b.add_child(c) } - parent.previous = b if b.inner_html.present? + if before.size > 1 + b = doc.document.create_element(parent.name) + before[0...-1].each { |c| b.add_child(c) } + parent.previous = b if b.inner_html.present? + end + + if after.present? + a = doc.document.create_element(parent.name) + after.each { |c| a.add_child(c) } + parent.next = a if a.inner_html.present? + end + + parent.replace(br) + + changed = true end - - if after.present? - a = doc.document.create_element(parent.name) - after.each { |c| a.add_child(c) } - parent.next = a if a.inner_html.present? - end - - parent.replace(br) - - changed = true end - end break if !changed end @@ -85,17 +84,21 @@ class HtmlToMarkdown def remove_whitespaces!(node) return true if "pre" == node.name - node.children.chunk { |n| is_inline?(n) }.each do |inline, nodes| - if inline - collapse_spaces!(nodes) && remove_trailing_space!(nodes) - else - nodes.each { |n| remove_whitespaces!(n) } + node + .children + .chunk { |n| is_inline?(n) } + .each do |inline, nodes| + if inline + collapse_spaces!(nodes) && remove_trailing_space!(nodes) + else + nodes.each { |n| remove_whitespaces!(n) } + end end - end end def is_inline?(node) - node.text? || ("br" != node.name && node.description&.inline? && node.children.all? { |n| is_inline?(n) }) + node.text? || + ("br" != node.name && node.description&.inline? && node.children.all? { |n| is_inline?(n) }) end def collapse_spaces!(nodes, was_space = true) @@ -141,15 +144,16 @@ class HtmlToMarkdown send(visitor, node) if respond_to?(visitor, true) end - ALLOWED_IMG_SRCS ||= %w{http:// https:// www.} + ALLOWED_IMG_SRCS ||= %w[http:// https:// www.] def allowed_hrefs - @allowed_hrefs ||= begin - hrefs = SiteSetting.allowed_href_schemes.split("|").map { |scheme| "#{scheme}:" }.to_set - ALLOWED_IMG_SRCS.each { |src| hrefs << src } - hrefs << "mailto:" - hrefs.to_a - end + @allowed_hrefs ||= + begin + hrefs = SiteSetting.allowed_href_schemes.split("|").map { |scheme| "#{scheme}:" }.to_set + ALLOWED_IMG_SRCS.each { |src| hrefs << src } + hrefs << "mailto:" + hrefs.to_a + end end def visit_a(node) @@ -176,11 +180,9 @@ class HtmlToMarkdown end end - ALLOWED ||= %w{kbd del ins small big sub sup dl dd dt mark} + ALLOWED ||= %w[kbd del ins small big sub sup dl dd dt mark] ALLOWED.each do |tag| - define_method("visit_#{tag}") do |node| - "<#{tag}>#{traverse(node)}" - end + define_method("visit_#{tag}") { |node| "<#{tag}>#{traverse(node)}" } end def visit_blockquote(node) @@ -191,7 +193,7 @@ class HtmlToMarkdown "\n\n#{text}\n\n" end - BLOCKS ||= %w{div tr} + BLOCKS ||= %w[div tr] BLOCKS.each do |tag| define_method("visit_#{tag}") do |node| prefix = block?(node.previous_element) ? "" : "\n" @@ -203,12 +205,8 @@ class HtmlToMarkdown "\n\n#{traverse(node)}\n\n" end - TRAVERSABLES ||= %w{aside font span thead tbody tfooter u} - TRAVERSABLES.each do |tag| - define_method("visit_#{tag}") do |node| - traverse(node) - end - end + TRAVERSABLES ||= %w[aside font span thead tbody tfooter u] + TRAVERSABLES.each { |tag| define_method("visit_#{tag}") { |node| traverse(node) } } def visit_tt(node) "`#{traverse(node)}`" @@ -245,18 +243,10 @@ class HtmlToMarkdown visit_abbr(node) end - (1..6).each do |n| - define_method("visit_h#{n}") do |node| - "#{"#" * n} #{traverse(node)}" - end - end + (1..6).each { |n| define_method("visit_h#{n}") { |node| "#{"#" * n} #{traverse(node)}" } } - CELLS ||= %w{th td} - CELLS.each do |tag| - define_method("visit_#{tag}") do |node| - "#{traverse(node)} " - end - end + CELLS ||= %w[th td] + CELLS.each { |tag| define_method("visit_#{tag}") { |node| "#{traverse(node)} " } } def visit_table(node) if rows = extract_rows(node) @@ -264,7 +254,8 @@ class HtmlToMarkdown text = "| " + headers.map { |td| traverse(td).gsub(/\n/, "
    ") }.join(" | ") + " |\n" text << "| " + (["-"] * headers.size).join(" | ") + " |\n" rows[1..-1].each do |row| - text << "| " + row.css("td").map { |td| traverse(td).gsub(/\n/, "
    ") }.join(" | ") + " |\n" + text << "| " + row.css("td").map { |td| traverse(td).gsub(/\n/, "
    ") }.join(" | ") + + " |\n" end "\n\n#{text}\n\n" else @@ -280,7 +271,7 @@ class HtmlToMarkdown rows end - LISTS ||= %w{ul ol} + LISTS ||= %w[ul ol] LISTS.each do |tag| define_method("visit_#{tag}") do |node| prefix = block?(node.previous_element) ? "" : "\n" @@ -304,12 +295,12 @@ class HtmlToMarkdown "#{marker}#{text}#{suffix}" end - EMPHASES ||= %w{i em} + EMPHASES ||= %w[i em] EMPHASES.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || (text["*"] && text["_"]) @@ -321,12 +312,12 @@ class HtmlToMarkdown end end - STRONGS ||= %w{b strong} + STRONGS ||= %w[b strong] STRONGS.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || (text["*"] && text["_"]) @@ -338,12 +329,12 @@ class HtmlToMarkdown end end - STRIKES ||= %w{s strike} + STRIKES ||= %w[s strike] STRIKES.each do |tag| define_method("visit_#{tag}") do |node| text = traverse(node) - return "" if text.empty? + return "" if text.empty? return " " if text.blank? return "<#{tag}>#{text}" if text["\n"] || text["~~"] @@ -358,7 +349,19 @@ class HtmlToMarkdown node.text end - HTML5_BLOCK_ELEMENTS ||= %w[article aside details dialog figcaption figure footer header main nav section] + HTML5_BLOCK_ELEMENTS ||= %w[ + article + aside + details + dialog + figcaption + figure + footer + header + main + nav + section + ] def block?(node) return false if !node node.description&.block? || HTML5_BLOCK_ELEMENTS.include?(node.name) diff --git a/lib/http_language_parser.rb b/lib/http_language_parser.rb index debfddc604..a2e24ca0aa 100644 --- a/lib/http_language_parser.rb +++ b/lib/http_language_parser.rb @@ -4,10 +4,10 @@ module HttpLanguageParser def self.parse(header) # Rails I18n uses underscores between the locale and the region; the request # headers use hyphens. - require 'http_accept_language' unless defined? HttpAcceptLanguage - available_locales = I18n.available_locales.map { |locale| locale.to_s.tr('_', '-') } + require "http_accept_language" unless defined?(HttpAcceptLanguage) + available_locales = I18n.available_locales.map { |locale| locale.to_s.tr("_", "-") } parser = HttpAcceptLanguage::Parser.new(header) - matched = parser.language_region_compatible_from(available_locales)&.tr('-', '_') + matched = parser.language_region_compatible_from(available_locales)&.tr("-", "_") matched || SiteSetting.default_locale end end diff --git a/lib/i18n/backend/discourse_i18n.rb b/lib/i18n/backend/discourse_i18n.rb index 1511e67e96..4b1e24e9ec 100644 --- a/lib/i18n/backend/discourse_i18n.rb +++ b/lib/i18n/backend/discourse_i18n.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'i18n/backend/pluralization' +require "i18n/backend/pluralization" module I18n module Backend @@ -22,9 +22,7 @@ module I18n # force explicit loading def load_translations(*filenames) unless filenames.empty? - self.class.sort_locale_files(filenames.flatten).each do |filename| - load_file(filename) - end + self.class.sort_locale_files(filenames.flatten).each { |filename| load_file(filename) } end end @@ -90,10 +88,12 @@ module I18n if overrides if options[:count] if !existing_translations - I18n.fallbacks[locale].drop(1).each do |fallback| - existing_translations = super(fallback, key, scope, options) - break if existing_translations.present? - end + I18n.fallbacks[locale] + .drop(1) + .each do |fallback| + existing_translations = super(fallback, key, scope, options) + break if existing_translations.present? + end end if existing_translations @@ -106,9 +106,11 @@ module I18n result = {} - remapped_translations.merge(overrides).each do |k, v| - result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key) - end + remapped_translations + .merge(overrides) + .each do |k, v| + result[k.split(".").last.to_sym] = v if k != key && k.start_with?(key) + end return result if result.size > 0 end end diff --git a/lib/i18n/duplicate_key_finder.rb b/lib/i18n/duplicate_key_finder.rb index 65b0f33d0e..4abd6300ff 100644 --- a/lib/i18n/duplicate_key_finder.rb +++ b/lib/i18n/duplicate_key_finder.rb @@ -3,7 +3,6 @@ require "locale_file_walker" class DuplicateKeyFinder < LocaleFileWalker - def find_duplicates(path) @keys_with_count = Hash.new { 0 } handle_document(Psych.parse_file(path)) @@ -14,6 +13,6 @@ class DuplicateKeyFinder < LocaleFileWalker def handle_scalar(node, depth, parents) super - @keys_with_count[parents.join('.')] += 1 + @keys_with_count[parents.join(".")] += 1 end end diff --git a/lib/i18n/locale_file_checker.rb b/lib/i18n/locale_file_checker.rb index 4d0088ab8c..de9eae3056 100644 --- a/lib/i18n/locale_file_checker.rb +++ b/lib/i18n/locale_file_checker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'i18n/i18n_interpolation_keys_finder' -require 'yaml' +require "i18n/i18n_interpolation_keys_finder" +require "yaml" class LocaleFileChecker TYPE_MISSING_INTERPOLATION_KEYS = 1 @@ -17,7 +17,8 @@ class LocaleFileChecker locale_files.each do |locale_path| next unless reference_path = reference_file(locale_path) - @relative_locale_path = Pathname.new(locale_path).relative_path_from(Pathname.new(Rails.root)).to_s + @relative_locale_path = + Pathname.new(locale_path).relative_path_from(Pathname.new(Rails.root)).to_s @locale_yaml = YAML.load_file(locale_path) @reference_yaml = YAML.load_file(reference_path) @@ -34,14 +35,14 @@ class LocaleFileChecker private - YML_DIRS = ["config/locales", "plugins/**/locales"] + YML_DIRS = %w[config/locales plugins/**/locales] PLURALS_FILE = "config/locales/plurals.rb" REFERENCE_LOCALE = "en" - REFERENCE_PLURAL_KEYS = ["one", "other"] + REFERENCE_PLURAL_KEYS = %w[one other] # Some languages should always use %{count} in pluralized strings. # https://meta.discourse.org/t/always-use-count-variable-when-translating-pluralized-strings/83969 - FORCE_PLURAL_COUNT_LOCALES = ["bs", "fr", "lt", "lv", "ru", "sl", "sr", "uk"] + FORCE_PLURAL_COUNT_LOCALES = %w[bs fr lt lv ru sl sr uk] def locale_files YML_DIRS.map { |dir| Dir["#{Rails.root}/#{dir}/{client,server}.#{@locale}.yml"] }.flatten @@ -92,8 +93,17 @@ class LocaleFileChecker missing_keys.delete("count") end - add_error(keys, TYPE_MISSING_INTERPOLATION_KEYS, missing_keys, pluralized: pluralized) unless missing_keys.empty? - add_error(keys, TYPE_UNSUPPORTED_INTERPOLATION_KEYS, unsupported_keys, pluralized: pluralized) unless unsupported_keys.empty? + unless missing_keys.empty? + add_error(keys, TYPE_MISSING_INTERPOLATION_KEYS, missing_keys, pluralized: pluralized) + end + unless unsupported_keys.empty? + add_error( + keys, + TYPE_UNSUPPORTED_INTERPOLATION_KEYS, + unsupported_keys, + pluralized: pluralized, + ) + end end end @@ -123,12 +133,15 @@ class LocaleFileChecker actual_plural_keys = parent.is_a?(Hash) ? parent.keys : [] missing_plural_keys = expected_plural_keys - actual_plural_keys - add_error(keys, TYPE_MISSING_PLURAL_KEYS, missing_plural_keys, pluralized: true) unless missing_plural_keys.empty? + unless missing_plural_keys.empty? + add_error(keys, TYPE_MISSING_PLURAL_KEYS, missing_plural_keys, pluralized: true) + end end end def check_message_format - mf_locale, mf_filename = JsLocaleHelper.find_message_format_locale([@locale], fallback_to_english: true) + mf_locale, mf_filename = + JsLocaleHelper.find_message_format_locale([@locale], fallback_to_english: true) traverse_hash(@locale_yaml, []) do |keys, value| next unless keys.last.ends_with?("_MF") @@ -158,17 +171,18 @@ class LocaleFileChecker end def reference_value_pluralized?(value) - value.is_a?(Hash) && - value.keys.sort == REFERENCE_PLURAL_KEYS && + value.is_a?(Hash) && value.keys.sort == REFERENCE_PLURAL_KEYS && value.keys.all? { |k| value[k].is_a?(String) } end def plural_keys - @plural_keys ||= begin - eval(File.read("#{Rails.root}/#{PLURALS_FILE}")).map do |locale, value| # rubocop:disable Security/Eval - [locale.to_s, value[:i18n][:plural][:keys].map(&:to_s)] - end.to_h - end + @plural_keys ||= + begin + # rubocop:disable Security/Eval + eval(File.read("#{Rails.root}/#{PLURALS_FILE}")) + .map { |locale, value| [locale.to_s, value[:i18n][:plural][:keys].map(&:to_s)] } + .to_h + end end def add_error(keys, type, details, pluralized:) @@ -180,10 +194,6 @@ class LocaleFileChecker joined_key = keys[1..-1].join(".") end - @errors[@relative_locale_path] << { - key: joined_key, - type: type, - details: details.to_s - } + @errors[@relative_locale_path] << { key: joined_key, type: type, details: details.to_s } end end diff --git a/lib/i18n/locale_file_walker.rb b/lib/i18n/locale_file_walker.rb index facc7235c0..94c27dc336 100644 --- a/lib/i18n/locale_file_walker.rb +++ b/lib/i18n/locale_file_walker.rb @@ -22,7 +22,11 @@ class LocaleFileWalker def handle_node(node, depth, parents, consecutive_scalars) if node_is_scalar = node.is_a?(Psych::Nodes::Scalar) - valid_scalar?(depth, consecutive_scalars) ? handle_scalar(node, depth, parents) : handle_value(node.value, parents) + if valid_scalar?(depth, consecutive_scalars) + handle_scalar(node, depth, parents) + else + handle_value(node.value, parents) + end elsif node.is_a?(Psych::Nodes::Alias) handle_alias(node, depth, parents) elsif node.is_a?(Psych::Nodes::Mapping) diff --git a/lib/image_sizer.rb b/lib/image_sizer.rb index ddb86396d5..29118b3940 100644 --- a/lib/image_sizer.rb +++ b/lib/image_sizer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ImageSizer - # Resize an image to the aspect ratio we want def self.resize(width, height, opts = {}) return if width.blank? || height.blank? @@ -12,7 +11,7 @@ module ImageSizer w = width.to_f h = height.to_f - return [w.floor, h.floor] if w <= max_width && h <= max_height + return w.floor, h.floor if w <= max_width && h <= max_height ratio = [max_width / w, max_height / h].min [(w * ratio).floor, (h * ratio).floor] @@ -27,11 +26,10 @@ module ImageSizer w = width.to_f h = height.to_f - return [w.floor, h.floor] if w <= max_width && h <= max_height + return w.floor, h.floor if w <= max_width && h <= max_height ratio = max_width / w [[max_width, w].min.floor, [max_height, (h * ratio)].min.floor] end - end diff --git a/lib/imap/providers/detector.rb b/lib/imap/providers/detector.rb index 41f356517e..7ad50c4ea2 100644 --- a/lib/imap/providers/detector.rb +++ b/lib/imap/providers/detector.rb @@ -4,7 +4,7 @@ module Imap module Providers class Detector def self.init_with_detected_provider(config) - if config[:server] == 'imap.gmail.com' + if config[:server] == "imap.gmail.com" return Imap::Providers::Gmail.new(config[:server], config) end Imap::Providers::Generic.new(config[:server], config) diff --git a/lib/imap/providers/generic.rb b/lib/imap/providers/generic.rb index 53ec57459d..59f43d35f2 100644 --- a/lib/imap/providers/generic.rb +++ b/lib/imap/providers/generic.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true -require 'net/imap' +require "net/imap" module Imap module Providers - class WriteDisabledError < StandardError; end + class WriteDisabledError < StandardError + end class TrashedMailResponse attr_accessor :trashed_emails, :trash_uid_validity @@ -50,12 +51,16 @@ module Imap end def disconnect! - imap.logout rescue nil + begin + imap.logout + rescue StandardError + nil + end imap.disconnect end def can?(capability) - @capabilities ||= imap.responses['CAPABILITY'][-1] || imap.capability + @capabilities ||= imap.responses["CAPABILITY"][-1] || imap.capability @capabilities.include?(capability) end @@ -67,22 +72,23 @@ module Imap elsif opts[:to] imap.uid_search("UID 1:#{opts[:to]}") else - imap.uid_search('ALL') + imap.uid_search("ALL") end end def labels - @labels ||= begin - labels = {} + @labels ||= + begin + labels = {} - list_mailboxes.each do |name| - if tag = to_tag(name) - labels[tag] = name + list_mailboxes.each do |name| + if tag = to_tag(name) + labels[tag] = name + end end - end - labels - end + labels + end end def open_mailbox(mailbox_name, write: false) @@ -98,9 +104,7 @@ module Imap @open_mailbox_name = mailbox_name @open_mailbox_write = write - { - uid_validity: imap.responses['UIDVALIDITY'][-1] - } + { uid_validity: imap.responses["UIDVALIDITY"][-1] } end def emails(uids, fields, opts = {}) @@ -114,9 +118,7 @@ module Imap fetched.map do |email| attributes = {} - fields.each do |field| - attributes[field] = email.attr[field] - end + fields.each { |field| attributes[field] = email.attr[field] } attributes end @@ -131,11 +133,11 @@ module Imap def to_tag(label) label = DiscourseTagging.clean_tag(label.to_s) - label if label != 'inbox' && label != 'sent' + label if label != "inbox" && label != "sent" end def tag_to_flag(tag) - :Seen if tag == 'seen' + :Seen if tag == "seen" end def tag_to_label(tag) @@ -150,24 +152,25 @@ module Imap def list_mailboxes_with_attributes(attr_filter = nil) # Basically, list all mailboxes in the root of the server. # ref: https://tools.ietf.org/html/rfc3501#section-6.3.8 - imap.list('', '*').reject do |m| - - # Noselect cannot be selected with the SELECT command. - # technically we could use this for readonly mode when - # SiteSetting.imap_write is disabled...maybe a later TODO - # ref: https://tools.ietf.org/html/rfc3501#section-7.2.2 - m.attr.include?(:Noselect) - end.select do |m| - - # There are Special-Use mailboxes denoted by an attribute. For - # example, some common ones are \Trash or \Sent. - # ref: https://tools.ietf.org/html/rfc6154 - if attr_filter - m.attr.include? attr_filter - else - true + imap + .list("", "*") + .reject do |m| + # Noselect cannot be selected with the SELECT command. + # technically we could use this for readonly mode when + # SiteSetting.imap_write is disabled...maybe a later TODO + # ref: https://tools.ietf.org/html/rfc3501#section-7.2.2 + m.attr.include?(:Noselect) + end + .select do |m| + # There are Special-Use mailboxes denoted by an attribute. For + # example, some common ones are \Trash or \Sent. + # ref: https://tools.ietf.org/html/rfc6154 + if attr_filter + m.attr.include? attr_filter + else + true + end end - end end def filter_mailboxes(mailboxes) @@ -186,16 +189,20 @@ module Imap # Look for the special Trash XLIST attribute. def trash_mailbox - Discourse.cache.fetch("imap_trash_mailbox_#{account_digest}", expires_in: 30.minutes) do - list_mailboxes(:Trash).first - end + Discourse + .cache + .fetch("imap_trash_mailbox_#{account_digest}", expires_in: 30.minutes) do + list_mailboxes(:Trash).first + end end # Look for the special Junk XLIST attribute. def spam_mailbox - Discourse.cache.fetch("imap_spam_mailbox_#{account_digest}", expires_in: 30.minutes) do - list_mailboxes(:Junk).first - end + Discourse + .cache + .fetch("imap_spam_mailbox_#{account_digest}", expires_in: 30.minutes) do + list_mailboxes(:Junk).first + end end # open the trash mailbox for inspection or writing. after the yield we @@ -232,14 +239,19 @@ module Imap def find_trashed_by_message_ids(message_ids) trashed_emails = [] - trash_uid_validity = open_trash_mailbox do - trashed_email_uids = find_uids_by_message_ids(message_ids) - if trashed_email_uids.any? - trashed_emails = emails(trashed_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + trash_uid_validity = + open_trash_mailbox do + trashed_email_uids = find_uids_by_message_ids(message_ids) + if trashed_email_uids.any? + trashed_emails = + emails(trashed_email_uids, %w[UID ENVELOPE]).map do |e| + BasicMail.new( + message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id), + uid: e["UID"], + ) + end end end - end TrashedMailResponse.new.tap do |resp| resp.trashed_emails = trashed_emails @@ -249,14 +261,19 @@ module Imap def find_spam_by_message_ids(message_ids) spam_emails = [] - spam_uid_validity = open_spam_mailbox do - spam_email_uids = find_uids_by_message_ids(message_ids) - if spam_email_uids.any? - spam_emails = emails(spam_email_uids, ["UID", "ENVELOPE"]).map do |e| - BasicMail.new(message_id: Email::MessageIdService.message_id_clean(e['ENVELOPE'].message_id), uid: e['UID']) + spam_uid_validity = + open_spam_mailbox do + spam_email_uids = find_uids_by_message_ids(message_ids) + if spam_email_uids.any? + spam_emails = + emails(spam_email_uids, %w[UID ENVELOPE]).map do |e| + BasicMail.new( + message_id: Email::MessageIdService.message_id_clean(e["ENVELOPE"].message_id), + uid: e["UID"], + ) + end end end - end SpamMailResponse.new.tap do |resp| resp.spam_emails = spam_emails @@ -265,13 +282,14 @@ module Imap end def find_uids_by_message_ids(message_ids) - header_message_id_terms = message_ids.map do |msgid| - "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'" - end + header_message_id_terms = + message_ids.map do |msgid| + "HEADER Message-ID '#{Email::MessageIdService.message_id_rfc_format(msgid)}'" + end # OR clauses are written in Polish notation...so the query looks like this: # OR OR HEADER Message-ID XXXX HEADER Message-ID XXXX HEADER Message-ID XXXX - or_clauses = 'OR ' * (header_message_id_terms.length - 1) + or_clauses = "OR " * (header_message_id_terms.length - 1) query = "#{or_clauses}#{header_message_id_terms.join(" ")}" imap.uid_search(query) @@ -280,17 +298,16 @@ module Imap def trash(uid) # MOVE is way easier than doing the COPY \Deleted EXPUNGE dance ourselves. # It is supported by Gmail and Outlook. - if can?('MOVE') + if can?("MOVE") trash_move(uid) else - # default behaviour for IMAP servers is to add the \Deleted flag # then EXPUNGE the mailbox which permanently deletes these messages # https://tools.ietf.org/html/rfc3501#section-6.4.3 # # TODO: We may want to add the option at some point to copy to some # other mailbox first before doing this (e.g. Trash) - store(uid, 'FLAGS', [], ["\\Deleted"]) + store(uid, "FLAGS", [], ["\\Deleted"]) imap.expunge end end diff --git a/lib/imap/providers/gmail.rb b/lib/imap/providers/gmail.rb index 7ac51f7304..fc888d66ed 100644 --- a/lib/imap/providers/gmail.rb +++ b/lib/imap/providers/gmail.rb @@ -8,61 +8,58 @@ module Imap # all UIDs in a thread must have the \\Inbox label removed. # class Gmail < Generic - X_GM_LABELS = 'X-GM-LABELS' - X_GM_THRID = 'X-GM-THRID' + X_GM_LABELS = "X-GM-LABELS" + X_GM_THRID = "X-GM-THRID" def imap @imap ||= super.tap { |imap| apply_gmail_patch(imap) } end def emails(uids, fields, opts = {}) - # gmail has a special header for labels - if fields.include?('LABELS') - fields[fields.index('LABELS')] = X_GM_LABELS - end + fields[fields.index("LABELS")] = X_GM_LABELS if fields.include?("LABELS") emails = super(uids, fields, opts) emails.each do |email| - email['LABELS'] = Array(email['LABELS']) + email["LABELS"] = Array(email["LABELS"]) if email[X_GM_LABELS] - email['LABELS'] << Array(email.delete(X_GM_LABELS)) - email['LABELS'].flatten! + email["LABELS"] << Array(email.delete(X_GM_LABELS)) + email["LABELS"].flatten! end - email['LABELS'] << '\\Inbox' if @open_mailbox_name == 'INBOX' + email["LABELS"] << '\\Inbox' if @open_mailbox_name == "INBOX" - email['LABELS'].uniq! + email["LABELS"].uniq! end emails end def store(uid, attribute, old_set, new_set) - attribute = X_GM_LABELS if attribute == 'LABELS' + attribute = X_GM_LABELS if attribute == "LABELS" super(uid, attribute, old_set, new_set) end def to_tag(label) # Label `\\Starred` is Gmail equivalent of :Flagged (both present) - return 'starred' if label == :Flagged - return if label == '[Gmail]/All Mail' + return "starred" if label == :Flagged + return if label == "[Gmail]/All Mail" - label = label.to_s.gsub('[Gmail]/', '') + label = label.to_s.gsub("[Gmail]/", "") super(label) end def tag_to_flag(tag) - return :Flagged if tag == 'starred' + return :Flagged if tag == "starred" super(tag) end def tag_to_label(tag) - return '\\Important' if tag == 'important' - return '\\Starred' if tag == 'starred' + return '\\Important' if tag == "important" + return '\\Starred' if tag == "starred" super(tag) end @@ -73,11 +70,14 @@ module Imap thread_id = thread_id_from_uid(uid) emails_to_archive = emails_in_thread(thread_id) emails_to_archive.each do |email| - labels = email['LABELS'] + labels = email["LABELS"] new_labels = labels.reject { |l| l == "\\Inbox" } store(email["UID"], "LABELS", labels, new_labels) end - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) archived in Gmail mailbox for #{@username}", + :debug, + ) end # Though Gmail considers the email thread unarchived if the first email @@ -87,36 +87,38 @@ module Imap thread_id = thread_id_from_uid(uid) emails_to_unarchive = emails_in_thread(thread_id) emails_to_unarchive.each do |email| - labels = email['LABELS'] + labels = email["LABELS"] new_labels = labels.dup - if !new_labels.include?("\\Inbox") - new_labels << "\\Inbox" - end + new_labels << "\\Inbox" if !new_labels.include?("\\Inbox") store(email["UID"], "LABELS", labels, new_labels) end - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) unarchived in Gmail mailbox for #{@username}", + :debug, + ) end def thread_id_from_uid(uid) fetched = imap.uid_fetch(uid, [X_GM_THRID]) - if !fetched - raise "Thread not found for UID #{uid}!" - end + raise "Thread not found for UID #{uid}!" if !fetched fetched.last.attr[X_GM_THRID] end def emails_in_thread(thread_id) uids_to_fetch = imap.uid_search("#{X_GM_THRID} #{thread_id}") - emails(uids_to_fetch, ["UID", "LABELS"]) + emails(uids_to_fetch, %w[UID LABELS]) end def trash_move(uid) thread_id = thread_id_from_uid(uid) - email_uids_to_trash = emails_in_thread(thread_id).map { |e| e['UID'] } + email_uids_to_trash = emails_in_thread(thread_id).map { |e| e["UID"] } imap.uid_move(email_uids_to_trash, trash_mailbox) - ImapSyncLog.log("Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}", :debug) + ImapSyncLog.log( + "Thread ID #{thread_id} (UID #{uid}) trashed in Gmail mailbox for #{@username}", + :debug, + ) { trash_uid_validity: open_trash_mailbox, email_uids_to_trash: email_uids_to_trash } end @@ -124,16 +126,15 @@ module Imap # used for the dropdown in the UI where we allow the user to select the # IMAP mailbox to sync with. def filter_mailboxes(mailboxes_with_attributes) - mailboxes_with_attributes.reject do |mb| - (mb.attr & [:Drafts, :Sent, :Junk, :Flagged, :Trash]).any? - end.map(&:name) + mailboxes_with_attributes + .reject { |mb| (mb.attr & %i[Drafts Sent Junk Flagged Trash]).any? } + .map(&:name) end private def apply_gmail_patch(imap) - class << imap.instance_variable_get('@parser') - + class << imap.instance_variable_get("@parser") # Modified version of the original `msg_att` from here: # https://github.com/ruby/ruby/blob/1cc8ff001da217d0e98d13fe61fbc9f5547ef722/lib/net/imap.rb#L2346 # @@ -172,15 +173,14 @@ module Imap when /\A(?:MODSEQ)\z/ni name, val = modseq_data - # Adding support for GMail extended attributes. + # Adding support for GMail extended attributes. when /\A(?:X-GM-LABELS)\z/ni name, val = label_data when /\A(?:X-GM-MSGID)\z/ni name, val = uid_data when /\A(?:X-GM-THRID)\z/ni name, val = uid_data - # End custom support for Gmail. - + # End custom support for Gmail. else parse_error("unknown attribute `%s' for {%d}", token.value, n) end diff --git a/lib/imap/sync.rb b/lib/imap/sync.rb index be5b32fb0d..0d9220131e 100644 --- a/lib/imap/sync.rb +++ b/lib/imap/sync.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'net/imap' +require "net/imap" module Imap class Sync @@ -23,13 +23,13 @@ module Imap end def can_idle? - SiteSetting.enable_imap_idle && @provider.can?('IDLE') + SiteSetting.enable_imap_idle && @provider.can?("IDLE") end def process(idle: false, import_limit: nil, old_emails_limit: nil, new_emails_limit: nil) - raise 'disconnected' if disconnected? + raise "disconnected" if disconnected? - import_limit ||= SiteSetting.imap_batch_import_email + import_limit ||= SiteSetting.imap_batch_import_email old_emails_limit ||= SiteSetting.imap_polling_old_emails new_emails_limit ||= SiteSetting.imap_polling_new_emails @@ -43,30 +43,42 @@ module Imap # If UID validity changes, the whole mailbox must be synchronized (all # emails are considered new and will be associated to existent topics # in Email::Receiver by matching Message-Ids). - ImapSyncLog.warn("UIDVALIDITY = #{@status[:uid_validity]} does not match expected #{@group.imap_uid_validity}, invalidating IMAP cache and resyncing emails for mailbox #{@group.imap_mailbox_name}", @group) + ImapSyncLog.warn( + "UIDVALIDITY = #{@status[:uid_validity]} does not match expected #{@group.imap_uid_validity}, invalidating IMAP cache and resyncing emails for mailbox #{@group.imap_mailbox_name}", + @group, + ) @group.imap_last_uid = 0 end if idle && !can_idle? - ImapSyncLog.warn("IMAP server for group cannot IDLE or imap idle site setting is disabled", @group) + ImapSyncLog.warn( + "IMAP server for group cannot IDLE or imap idle site setting is disabled", + @group, + ) idle = false end if idle - raise 'IMAP IDLE is disabled' if !SiteSetting.enable_imap_idle + raise "IMAP IDLE is disabled" if !SiteSetting.enable_imap_idle # Thread goes into sleep and it is better to return any connection # back to the pool. ActiveRecord::Base.connection_handler.clear_active_connections! idle_polling_mins = SiteSetting.imap_polling_period_mins.minutes.to_i - ImapSyncLog.debug("Going IDLE for #{idle_polling_mins} seconds to wait for more work", @group, db: false) + ImapSyncLog.debug( + "Going IDLE for #{idle_polling_mins} seconds to wait for more work", + @group, + db: false, + ) - @provider.imap.idle(idle_polling_mins) do |resp| - if resp.kind_of?(Net::IMAP::UntaggedResponse) && resp.name == 'EXISTS' - @provider.imap.idle_done + @provider + .imap + .idle(idle_polling_mins) do |resp| + if resp.kind_of?(Net::IMAP::UntaggedResponse) && resp.name == "EXISTS" + @provider.imap.idle_done + end end - end end # Fetching UIDs of old (already imported into Discourse, but might need @@ -82,7 +94,10 @@ module Imap # Sometimes, new_uids contains elements from old_uids. new_uids = new_uids - old_uids - ImapSyncLog.debug("Remote email server has #{old_uids.size} old emails and #{new_uids.size} new emails", @group) + ImapSyncLog.debug( + "Remote email server has #{old_uids.size} old emails and #{new_uids.size} new emails", + @group, + ) all_old_uids_size = old_uids.size all_new_uids_size = new_uids.size @@ -90,7 +105,7 @@ module Imap @group.update_columns( imap_last_error: nil, imap_old_emails: all_old_uids_size, - imap_new_emails: all_new_uids_size + imap_new_emails: all_new_uids_size, ) import_mode = import_limit > -1 && new_uids.size > import_limit @@ -112,10 +127,10 @@ module Imap end def update_topic(email, incoming_email, opts = {}) - return if !incoming_email || - incoming_email.imap_sync || - !incoming_email.topic || - incoming_email.post&.post_number != 1 + if !incoming_email || incoming_email.imap_sync || !incoming_email.topic || + incoming_email.post&.post_number != 1 + return + end update_topic_archived_state(email, incoming_email, opts) update_topic_tags(email, incoming_email, opts) @@ -125,33 +140,41 @@ module Imap def process_old_uids(old_uids) ImapSyncLog.debug("Syncing #{old_uids.size} randomly-selected old emails", @group) - emails = old_uids.empty? ? [] : @provider.emails(old_uids, ['UID', 'FLAGS', 'LABELS', 'ENVELOPE']) + emails = old_uids.empty? ? [] : @provider.emails(old_uids, %w[UID FLAGS LABELS ENVELOPE]) emails.each do |email| - incoming_email = IncomingEmail.find_by( - imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id - ) + incoming_email = + IncomingEmail.find_by( + imap_uid_validity: @status[:uid_validity], + imap_uid: email["UID"], + imap_group_id: @group.id, + ) if incoming_email.present? update_topic(email, incoming_email, mailbox_name: @group.imap_mailbox_name) else # try finding email by message-id instead, we may be able to set the uid etc. - incoming_email = IncomingEmail.where( - message_id: Email::MessageIdService.message_id_clean(email['ENVELOPE'].message_id), - imap_uid: nil, - imap_uid_validity: nil - ).where("to_addresses LIKE ?", "%#{@group.email_username}%").first + incoming_email = + IncomingEmail + .where( + message_id: Email::MessageIdService.message_id_clean(email["ENVELOPE"].message_id), + imap_uid: nil, + imap_uid_validity: nil, + ) + .where("to_addresses LIKE ?", "%#{@group.email_username}%") + .first if incoming_email incoming_email.update( imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id + imap_uid: email["UID"], + imap_group_id: @group.id, ) update_topic(email, incoming_email, mailbox_name: @group.imap_mailbox_name) else - ImapSyncLog.warn("Could not find old email (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email['UID']})", @group) + ImapSyncLog.warn( + "Could not find old email (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email["UID"]})", + @group, + ) end end end @@ -165,15 +188,18 @@ module Imap # if they have been deleted and if so delete the associated post/topic. then the remaining we # can just remove the imap details from the IncomingEmail table and if they end up back in the # original mailbox then they will be picked up in a future resync. - existing_incoming = IncomingEmail.includes(:post).where( - imap_group_id: @group.id, imap_uid_validity: @status[:uid_validity] - ).where.not(imap_uid: nil) + existing_incoming = + IncomingEmail + .includes(:post) + .where(imap_group_id: @group.id, imap_uid_validity: @status[:uid_validity]) + .where.not(imap_uid: nil) existing_uids = existing_incoming.map(&:imap_uid) missing_uids = existing_uids - old_uids - missing_message_ids = existing_incoming.select do |incoming| - missing_uids.include?(incoming.imap_uid) - end.map(&:message_id) + missing_message_ids = + existing_incoming + .select { |incoming| missing_uids.include?(incoming.imap_uid) } + .map(&:message_id) return if missing_message_ids.empty? @@ -183,7 +209,8 @@ module Imap potential_spam = [] response = @provider.find_trashed_by_message_ids(missing_message_ids) existing_incoming.each do |incoming| - matching_trashed = response.trashed_emails.find { |email| email.message_id == incoming.message_id } + matching_trashed = + response.trashed_emails.find { |email| email.message_id == incoming.message_id } if !matching_trashed potential_spam << incoming @@ -194,13 +221,22 @@ module Imap # not exist, and this sync is just updating the old UIDs to the new ones # in the trash, and we don't need to re-destroy the post if incoming.post - ImapSyncLog.debug("Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been deleted on the IMAP server.", @group) + ImapSyncLog.debug( + "Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been deleted on the IMAP server.", + @group, + ) PostDestroyer.new(Discourse.system_user, incoming.post).destroy end # the email has moved mailboxes, we don't want to try trashing again next time - ImapSyncLog.debug("Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_trashed.uid} | UIDVALIDITY #{response.trash_uid_validity}] (TRASHED)", @group) - incoming.update(imap_uid_validity: response.trash_uid_validity, imap_uid: matching_trashed.uid) + ImapSyncLog.debug( + "Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_trashed.uid} | UIDVALIDITY #{response.trash_uid_validity}] (TRASHED)", + @group, + ) + incoming.update( + imap_uid_validity: response.trash_uid_validity, + imap_uid: matching_trashed.uid, + ) end # This can be done because Message-ID is unique on a mail server between mailboxes, @@ -208,12 +244,16 @@ module Imap # the new UID from the spam. response = @provider.find_spam_by_message_ids(missing_message_ids) potential_spam.each do |incoming| - matching_spam = response.spam_emails.find { |email| email.message_id == incoming.message_id } + matching_spam = + response.spam_emails.find { |email| email.message_id == incoming.message_id } # if the email is not in the trash or spam then we don't know where it is... could # be in any mailbox on the server or could be permanently deleted. if !matching_spam - ImapSyncLog.debug("Email for incoming ID #{incoming.id} (#{incoming.message_id}) could not be found in the group mailbox, trash, or spam. It could be in another mailbox or permanently deleted.", @group) + ImapSyncLog.debug( + "Email for incoming ID #{incoming.id} (#{incoming.message_id}) could not be found in the group mailbox, trash, or spam. It could be in another mailbox or permanently deleted.", + @group, + ) incoming.update(imap_missing: true) next end @@ -222,12 +262,18 @@ module Imap # not exist, and this sync is just updating the old UIDs to the new ones # in the spam, and we don't need to re-destroy the post if incoming.post - ImapSyncLog.debug("Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been moved to spam on the IMAP server.", @group) + ImapSyncLog.debug( + "Deleting post ID #{incoming.post_id}, topic id #{incoming.topic_id}; email has been moved to spam on the IMAP server.", + @group, + ) PostDestroyer.new(Discourse.system_user, incoming.post).destroy end # the email has moved mailboxes, we don't want to try marking as spam again next time - ImapSyncLog.debug("Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_spam.uid} | UIDVALIDITY #{response.spam_uid_validity}] (SPAM)", @group) + ImapSyncLog.debug( + "Updating incoming ID #{incoming.id} uid data FROM [UID #{incoming.imap_uid} | UIDVALIDITY #{incoming.imap_uid_validity}] TO [UID #{matching_spam.uid} | UIDVALIDITY #{response.spam_uid_validity}] (SPAM)", + @group, + ) incoming.update(imap_uid_validity: response.spam_uid_validity, imap_uid: matching_spam.uid) end end @@ -235,7 +281,7 @@ module Imap def process_new_uids(new_uids, import_mode, all_old_uids_size, all_new_uids_size) ImapSyncLog.debug("Syncing #{new_uids.size} new emails (oldest first)", @group) - emails = @provider.emails(new_uids, ['UID', 'FLAGS', 'LABELS', 'RFC822']) + emails = @provider.emails(new_uids, %w[UID FLAGS LABELS RFC822]) processed = 0 # TODO (maybe): We might need something here to exclusively handle @@ -247,29 +293,33 @@ module Imap # (for example replies must be processed after the original email # to have a topic where the reply can be posted). begin - receiver = Email::Receiver.new( - email['RFC822'], - allow_auto_generated: true, - import_mode: import_mode, - destinations: [@group], - imap_uid_validity: @status[:uid_validity], - imap_uid: email['UID'], - imap_group_id: @group.id, - source: :imap - ) + receiver = + Email::Receiver.new( + email["RFC822"], + allow_auto_generated: true, + import_mode: import_mode, + destinations: [@group], + imap_uid_validity: @status[:uid_validity], + imap_uid: email["UID"], + imap_group_id: @group.id, + source: :imap, + ) receiver.process! update_topic(email, receiver.incoming_email, mailbox_name: @group.imap_mailbox_name) rescue Email::Receiver::ProcessingError => e - ImapSyncLog.warn("Could not process (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email['UID']}): #{e.message}", @group) + ImapSyncLog.warn( + "Could not process (UIDVALIDITY = #{@status[:uid_validity]}, UID = #{email["UID"]}): #{e.message}", + @group, + ) end processed += 1 @group.update_columns( imap_uid_validity: @status[:uid_validity], - imap_last_uid: email['UID'], + imap_last_uid: email["UID"], imap_old_emails: all_old_uids_size + processed, - imap_new_emails: all_new_uids_size - processed + imap_new_emails: all_new_uids_size - processed, ) end end @@ -281,7 +331,10 @@ module Imap if to_sync.size > 0 @provider.open_mailbox(@group.imap_mailbox_name, write: true) to_sync.each do |incoming_email| - ImapSyncLog.debug("Updating email on IMAP server for incoming email ID = #{incoming_email.id}, UID = #{incoming_email.imap_uid}", @group) + ImapSyncLog.debug( + "Updating email on IMAP server for incoming email ID = #{incoming_email.id}, UID = #{incoming_email.imap_uid}", + @group, + ) update_email(incoming_email) incoming_email.update(imap_sync: false) end @@ -292,7 +345,7 @@ module Imap topic = incoming_email.topic topic_is_archived = topic.group_archived_messages.size > 0 - email_is_archived = !email['LABELS'].include?('\\Inbox') && !email['LABELS'].include?('INBOX') + email_is_archived = !email["LABELS"].include?('\\Inbox') && !email["LABELS"].include?("INBOX") if topic_is_archived && !email_is_archived ImapSyncLog.debug("Unarchiving topic ID #{topic.id}, email was unarchived", @group) @@ -322,10 +375,10 @@ module Imap tags.add(@provider.to_tag(opts[:mailbox_name])) if opts[:mailbox_name] # Flags and labels - email['FLAGS'].each { |flag| tags.add(@provider.to_tag(flag)) } - email['LABELS'].each { |label| tags.add(@provider.to_tag(label)) } + email["FLAGS"].each { |flag| tags.add(@provider.to_tag(flag)) } + email["LABELS"].each { |label| tags.add(@provider.to_tag(label)) } - tags.subtract([nil, '']) + tags.subtract([nil, ""]) return if !tagging_enabled? @@ -354,11 +407,11 @@ module Imap # # A) the email has been deleted/moved to a different mailbox in the provider # B) the UID does not belong to the provider - email = @provider.emails(incoming_email.imap_uid, ['FLAGS', 'LABELS']).first + email = @provider.emails(incoming_email.imap_uid, %w[FLAGS LABELS]).first return if !email.present? - labels = email['LABELS'] - flags = email['FLAGS'] + labels = email["LABELS"] + flags = email["FLAGS"] new_labels = [] new_flags = [] @@ -367,7 +420,10 @@ module Imap if !topic # no need to do anything further here, we will recognize the UIDs in the # mail server email thread have been trashed on next sync - ImapSyncLog.debug("Trashing UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Trashing UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) return @provider.trash(incoming_email.imap_uid) end @@ -380,12 +436,18 @@ module Imap # at the same time. new_labels << "\\Inbox" - ImapSyncLog.debug("Unarchiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Unarchiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) # some providers need special handling for unarchiving too @provider.unarchive(incoming_email.imap_uid) else - ImapSyncLog.debug("Archiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", @group) + ImapSyncLog.debug( + "Archiving UID #{incoming_email.imap_uid} (incoming ID #{incoming_email.id})", + @group, + ) # some providers need special handling for archiving. this way we preserve # any new tag-labels, and archive, even though it may cause extra requests @@ -397,13 +459,14 @@ module Imap if tagging_enabled? tags = topic.tags.pluck(:name) new_flags = tags.map { |tag| @provider.tag_to_flag(tag) }.reject(&:blank?) - new_labels = new_labels.concat(tags.map { |tag| @provider.tag_to_label(tag) }.reject(&:blank?)) + new_labels = + new_labels.concat(tags.map { |tag| @provider.tag_to_label(tag) }.reject(&:blank?)) end # regardless of whether the topic needs to be archived we still update # the flags and the labels - @provider.store(incoming_email.imap_uid, 'FLAGS', flags, new_flags) - @provider.store(incoming_email.imap_uid, 'LABELS', labels, new_labels) + @provider.store(incoming_email.imap_uid, "FLAGS", flags, new_flags) + @provider.store(incoming_email.imap_uid, "LABELS", labels, new_labels) end def tagging_enabled? diff --git a/lib/import/normalize.rb b/lib/import/normalize.rb index c9c6dc7499..8b9b98b8e0 100644 --- a/lib/import/normalize.rb +++ b/lib/import/normalize.rb @@ -3,13 +3,14 @@ # markdown normalizer to be used by importers # # -require 'htmlentities' -module Import; end +require "htmlentities" +module Import +end module Import::Normalize def self.normalize_code_blocks(code, lang = nil) coder = HTMLEntities.new - code.gsub(/
    \s*\n?(.*?)\n?<\/code>\s*<\/pre>/m) {
    +    code.gsub(%r{
    \s*\n?(.*?)\n?\s*
    }m) do "\n```#{lang}\n#{coder.decode($1)}\n```\n" - } + end end end diff --git a/lib/import_export.rb b/lib/import_export.rb index 13d2b5eddf..feb4d93601 100644 --- a/lib/import_export.rb +++ b/lib/import_export.rb @@ -10,9 +10,11 @@ require "import_export/translation_overrides_exporter" require "json" module ImportExport - def self.import(filename) - data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }) + data = + ActiveSupport::HashWithIndifferentAccess.new( + File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }, + ) ImportExport::Importer.new(data).perform end diff --git a/lib/import_export/base_exporter.rb b/lib/import_export/base_exporter.rb index 989b7220e4..2effbd2d9f 100644 --- a/lib/import_export/base_exporter.rb +++ b/lib/import_export/base_exporter.rb @@ -4,22 +4,72 @@ module ImportExport class BaseExporter attr_reader :export_data, :categories - CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color, - :auto_close_hours, :position, :parent_category_id, :auto_close_based_on_last_post, - :topic_template, :all_topics_wiki, :permissions_params] + CATEGORY_ATTRS = %i[ + id + name + color + created_at + user_id + slug + description + text_color + auto_close_hours + position + parent_category_id + auto_close_based_on_last_post + topic_template + all_topics_wiki + permissions_params + ] - GROUP_ATTRS = [:id, :name, :created_at, :automatic_membership_email_domains, :primary_group, - :title, :grant_trust_level, :incoming_email, :bio_raw, :allow_membership_requests, - :full_name, :default_notification_level, :visibility_level, :public_exit, - :public_admission, :membership_request_template, :messageable_level, :mentionable_level, - :members_visibility_level, :publish_read_state] + GROUP_ATTRS = %i[ + id + name + created_at + automatic_membership_email_domains + primary_group + title + grant_trust_level + incoming_email + bio_raw + allow_membership_requests + full_name + default_notification_level + visibility_level + public_exit + public_admission + membership_request_template + messageable_level + mentionable_level + members_visibility_level + publish_read_state + ] - USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at, :custom_fields] + USER_ATTRS = %i[ + id + email + username + name + created_at + trust_level + active + last_emailed_at + custom_fields + ] - TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype] + TOPIC_ATTRS = %i[id title created_at views category_id closed archived archetype] - POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number, :hidden, - :hidden_reason_id, :wiki] + POST_ATTRS = %i[ + id + user_id + post_number + raw + created_at + reply_to_post_number + hidden + hidden_reason_id + wiki + ] def categories @categories ||= Category.all.to_a @@ -29,7 +79,10 @@ module ImportExport data = [] categories.each do |cat| - data << CATEGORY_ATTRS.inject({}) { |h, a| h[a] = cat.public_send(a); h } + data << CATEGORY_ATTRS.inject({}) do |h, a| + h[a] = cat.public_send(a) + h + end end data @@ -47,7 +100,11 @@ module ImportExport groups = groups.where(name: group_names) if group_names.present? groups.find_each do |group| - attrs = GROUP_ATTRS.inject({}) { |h, a| h[a] = group.public_send(a); h } + attrs = + GROUP_ATTRS.inject({}) do |h, a| + h[a] = group.public_send(a) + h + end attrs[:user_ids] = group.users.pluck(:id) data << attrs end @@ -87,9 +144,7 @@ module ImportExport def export_group_users user_ids = [] - @export_data[:groups].each do |g| - user_ids += g[:user_ids] - end + @export_data[:groups].each { |g| user_ids += g[:user_ids] } user_ids.uniq! return User.none if user_ids.empty? @@ -110,22 +165,24 @@ module ImportExport @topics.each do |topic| puts topic.title - topic_data = TOPIC_ATTRS.inject({}) do |h, a| - h[a] = topic.public_send(a) - h - end + topic_data = + TOPIC_ATTRS.inject({}) do |h, a| + h[a] = topic.public_send(a) + h + end topic_data[:posts] = [] topic.ordered_posts.find_each do |post| - attributes = POST_ATTRS.inject({}) do |h, a| - h[a] = post.public_send(a) - h - end + attributes = + POST_ATTRS.inject({}) do |h, a| + h[a] = post.public_send(a) + h + end attributes[:raw] = attributes[:raw].gsub( 'src="/uploads', - "src=\"#{Discourse.base_url_no_prefix}/uploads" + "src=\"#{Discourse.base_url_no_prefix}/uploads", ) topic_data[:posts] << attributes @@ -147,7 +204,7 @@ module ImportExport return if @export_data[:topics].blank? topic_ids = @export_data[:topics].pluck(:id) - users = User.joins(:posts).where('posts.topic_id IN (?)', topic_ids).distinct + users = User.joins(:posts).where("posts.topic_id IN (?)", topic_ids).distinct export_users(users) end @@ -164,14 +221,17 @@ module ImportExport users.find_each do |u| next if u.id == Discourse::SYSTEM_USER_ID - x = USER_ATTRS.inject({}) do |h, a| - h[a] = u.public_send(a) - h - end + x = + USER_ATTRS.inject({}) do |h, a| + h[a] = u.public_send(a) + h + end - x.merge(bio_raw: u.user_profile.bio_raw, - website: u.user_profile.website, - location: u.user_profile.location) + x.merge( + bio_raw: u.user_profile.bio_raw, + website: u.user_profile.website, + location: u.user_profile.location, + ) data << x end @@ -179,7 +239,11 @@ module ImportExport end def export_translation_overrides - @export_data[:translation_overrides] = TranslationOverride.all.select(:locale, :translation_key, :value) + @export_data[:translation_overrides] = TranslationOverride.all.select( + :locale, + :translation_key, + :value, + ) self end @@ -189,13 +253,12 @@ module ImportExport end def save_to_file(filename = nil) - output_basename = filename || File.join("#{default_filename_prefix}-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") - File.open(output_basename, "w:UTF-8") do |f| - f.write(@export_data.to_json) - end + output_basename = + filename || + File.join("#{default_filename_prefix}-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") + File.open(output_basename, "w:UTF-8") { |f| f.write(@export_data.to_json) } puts "Export saved to #{output_basename}" output_basename end - end end diff --git a/lib/import_export/category_exporter.rb b/lib/import_export/category_exporter.rb index 85f857a6aa..843c417d5e 100644 --- a/lib/import_export/category_exporter.rb +++ b/lib/import_export/category_exporter.rb @@ -5,15 +5,10 @@ require "import_export/topic_exporter" module ImportExport class CategoryExporter < BaseExporter - def initialize(category_ids) - @categories = Category.where(id: category_ids).or(Category.where(parent_category_id: category_ids)).to_a - @export_data = { - categories: [], - groups: [], - topics: [], - users: [] - } + @categories = + Category.where(id: category_ids).or(Category.where(parent_category_id: category_ids)).to_a + @export_data = { categories: [], groups: [], topics: [], users: [] } end def perform @@ -26,9 +21,12 @@ module ImportExport def export_topics_and_users all_category_ids = @categories.pluck(:id) description_topic_ids = @categories.pluck(:topic_id) - topic_exporter = ImportExport::TopicExporter.new(Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids) + topic_exporter = + ImportExport::TopicExporter.new( + Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids, + ) topic_exporter.perform - @export_data[:users] = topic_exporter.export_data[:users] + @export_data[:users] = topic_exporter.export_data[:users] @export_data[:topics] = topic_exporter.export_data[:topics] self end @@ -36,6 +34,5 @@ module ImportExport def default_filename_prefix "category-export" end - end end diff --git a/lib/import_export/category_structure_exporter.rb b/lib/import_export/category_structure_exporter.rb index ce33de39d3..edaf89b379 100644 --- a/lib/import_export/category_structure_exporter.rb +++ b/lib/import_export/category_structure_exporter.rb @@ -2,14 +2,10 @@ module ImportExport class CategoryStructureExporter < BaseExporter - def initialize(include_group_users = false) @include_group_users = include_group_users - @export_data = { - groups: [], - categories: [] - } + @export_data = { groups: [], categories: [] } @export_data[:users] = [] if @include_group_users end @@ -25,6 +21,5 @@ module ImportExport def default_filename_prefix "category-structure-export" end - end end diff --git a/lib/import_export/group_exporter.rb b/lib/import_export/group_exporter.rb index 893320cfbb..a0ed870641 100644 --- a/lib/import_export/group_exporter.rb +++ b/lib/import_export/group_exporter.rb @@ -2,13 +2,10 @@ module ImportExport class GroupExporter < BaseExporter - def initialize(include_group_users = false) @include_group_users = include_group_users - @export_data = { - groups: [] - } + @export_data = { groups: [] } @export_data[:users] = [] if @include_group_users end @@ -23,6 +20,5 @@ module ImportExport def default_filename_prefix "groups-export" end - end end diff --git a/lib/import_export/importer.rb b/lib/import_export/importer.rb index c9d03030a1..1498831e46 100644 --- a/lib/import_export/importer.rb +++ b/lib/import_export/importer.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require File.join(Rails.root, 'script', 'import_scripts', 'base.rb') +require File.join(Rails.root, "script", "import_scripts", "base.rb") module ImportExport class Importer < ImportScripts::Base - def initialize(data) @users = data[:users] @groups = data[:groups] @@ -66,7 +65,11 @@ module ImportExport external_id = g.delete(:id) new_group = Group.find_by_name(g[:name]) || Group.create!(g) user_ids.each do |external_user_id| - new_group.add(User.find(new_user_id(external_user_id))) rescue ActiveRecord::RecordNotUnique + begin + new_group.add(User.find(new_user_id(external_user_id))) + rescue StandardError + ActiveRecord::RecordNotUnique + end end end @@ -79,7 +82,11 @@ module ImportExport puts "Importing categories..." import_ids = @categories.collect { |c| "#{c[:id]}#{import_source}" } - existing_categories = CategoryCustomField.where("name = 'import_id' AND value IN (?)", import_ids).select(:category_id, :value).to_a + existing_categories = + CategoryCustomField + .where("name = 'import_id' AND value IN (?)", import_ids) + .select(:category_id, :value) + .to_a existing_category_ids = existing_categories.pluck(:value) levels = category_levels @@ -100,7 +107,10 @@ module ImportExport permissions = cat_attrs.delete(:permissions_params) category = Category.new(cat_attrs) - category.parent_category_id = new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[:parent_category_id].present? + category.parent_category_id = + new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[ + :parent_category_id + ].present? category.user_id = new_user_id(cat_attrs[:user_id]) import_id = "#{id}#{import_source}" category.custom_fields["import_id"] = import_id @@ -126,13 +136,14 @@ module ImportExport def import_topics return if @topics.blank? - puts "Importing topics...", '' + puts "Importing topics...", "" @topics.each do |t| puts "" print t[:title] - first_post_attrs = t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id]))) + first_post_attrs = + t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - %i[id category_id]))) first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id]) first_post_attrs[:category] = new_category_id(t[:category_id]) @@ -140,9 +151,7 @@ module ImportExport import_id = "#{first_post_attrs[:id]}#{import_source}" first_post = PostCustomField.where(name: "import_id", value: import_id).first&.post - unless first_post - first_post = create_post(first_post_attrs, import_id) - end + first_post = create_post(first_post_attrs, import_id) unless first_post topic_id = first_post.topic_id @@ -154,11 +163,8 @@ module ImportExport unless existing # see ImportScripts::Base create_post( - post_data.merge( - topic_id: topic_id, - user_id: new_user_id(post_data[:user_id]) - ), - post_import_id + post_data.merge(topic_id: topic_id, user_id: new_user_id(post_data[:user_id])), + post_import_id, ) end end @@ -182,51 +188,53 @@ module ImportExport end def new_user_id(external_user_id) - ucf = UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first + ucf = + UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first ucf ? ucf.user_id : Discourse::SYSTEM_USER_ID end def new_category_id(external_category_id) - CategoryCustomField.where( - name: "import_id", - value: "#{external_category_id}#{import_source}" - ).first&.category_id + CategoryCustomField + .where(name: "import_id", value: "#{external_category_id}#{import_source}") + .first + &.category_id end def import_source - @_import_source ||= "#{ENV['IMPORT_SOURCE'] || ''}" + @_import_source ||= "#{ENV["IMPORT_SOURCE"] || ""}" end def category_levels - @levels ||= begin - levels = {} + @levels ||= + begin + levels = {} - # Incomplete backups may lack definitions for some parent categories - # which would cause an infinite loop below. - parent_ids = @categories.map { |category| category[:parent_category_id] }.uniq - category_ids = @categories.map { |category| category[:id] }.uniq - (parent_ids - category_ids).each { |id| levels[id] = 0 } + # Incomplete backups may lack definitions for some parent categories + # which would cause an infinite loop below. + parent_ids = @categories.map { |category| category[:parent_category_id] }.uniq + category_ids = @categories.map { |category| category[:id] }.uniq + (parent_ids - category_ids).each { |id| levels[id] = 0 } - loop do - changed = false + loop do + changed = false - @categories.each do |category| - if !levels[category[:id]] - if !category[:parent_category_id] - levels[category[:id]] = 1 - elsif levels[category[:parent_category_id]] - levels[category[:id]] = levels[category[:parent_category_id]] + 1 + @categories.each do |category| + if !levels[category[:id]] + if !category[:parent_category_id] + levels[category[:id]] = 1 + elsif levels[category[:parent_category_id]] + levels[category[:id]] = levels[category[:parent_category_id]] + 1 + end + + changed = true end - - changed = true end + + break if !changed end - break if !changed + levels end - - levels - end end def fix_permissions @@ -242,14 +250,19 @@ module ImportExport max_level.times do @categories.each do |category| parent_category = categories_by_id[category[:parent_category_id]] - next if !parent_category || !parent_category[:permissions_params] || parent_category[:permissions_params][:everyone] + if !parent_category || !parent_category[:permissions_params] || + parent_category[:permissions_params][:everyone] + next + end parent_groups = parent_category[:permissions_params].map(&:first) child_groups = category[:permissions_params].map(&:first) only_subcategory_groups = child_groups - parent_groups if only_subcategory_groups.present? - parent_category[:permissions_params].merge!(category[:permissions_params].slice(*only_subcategory_groups)) + parent_category[:permissions_params].merge!( + category[:permissions_params].slice(*only_subcategory_groups), + ) end end end diff --git a/lib/import_export/topic_exporter.rb b/lib/import_export/topic_exporter.rb index 64ab80aaf0..e7a4efd909 100644 --- a/lib/import_export/topic_exporter.rb +++ b/lib/import_export/topic_exporter.rb @@ -4,13 +4,9 @@ require "import_export/base_exporter" module ImportExport class TopicExporter < ImportExport::BaseExporter - def initialize(topic_ids) @topics = Topic.where(id: topic_ids).to_a - @export_data = { - topics: [], - users: [] - } + @export_data = { topics: [], users: [] } end def perform @@ -24,6 +20,5 @@ module ImportExport def default_filename_prefix "topic-export" end - end end diff --git a/lib/import_export/translation_overrides_exporter.rb b/lib/import_export/translation_overrides_exporter.rb index 7094248a99..19feec1098 100644 --- a/lib/import_export/translation_overrides_exporter.rb +++ b/lib/import_export/translation_overrides_exporter.rb @@ -2,11 +2,8 @@ module ImportExport class TranslationOverridesExporter < BaseExporter - def initialize() - @export_data = { - translation_overrides: [] - } + @export_data = { translation_overrides: [] } end def perform @@ -19,6 +16,5 @@ module ImportExport def default_filename_prefix "translation-overrides" end - end end diff --git a/lib/inline_oneboxer.rb b/lib/inline_oneboxer.rb index 6383bbddff..e3ef92ee84 100644 --- a/lib/inline_oneboxer.rb +++ b/lib/inline_oneboxer.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class InlineOneboxer - MIN_TITLE_LENGTH = 2 def initialize(urls, opts = nil) @@ -60,26 +59,25 @@ class InlineOneboxer end always_allow = SiteSetting.enable_inline_onebox_on_all_domains - allowed_domains = SiteSetting.allowed_inline_onebox_domains&.split('|') unless always_allow + allowed_domains = SiteSetting.allowed_inline_onebox_domains&.split("|") unless always_allow if always_allow || allowed_domains - uri = begin - URI(url) - rescue URI::Error - end - - if uri.present? && - uri.hostname.present? && - (always_allow || allowed_domains.include?(uri.hostname)) && - !Onebox::DomainChecker.is_blocked?(uri.hostname) - if SiteSetting.block_onebox_on_redirect - max_redirects = 0 + uri = + begin + URI(url) + rescue URI::Error end - title = RetrieveTitle.crawl( - url, - max_redirects: max_redirects, - initial_https_redirect_ignore_limit: SiteSetting.block_onebox_on_redirect - ) + + if uri.present? && uri.hostname.present? && + (always_allow || allowed_domains.include?(uri.hostname)) && + !Onebox::DomainChecker.is_blocked?(uri.hostname) + max_redirects = 0 if SiteSetting.block_onebox_on_redirect + title = + RetrieveTitle.crawl( + url, + max_redirects: max_redirects, + initial_https_redirect_ignore_limit: SiteSetting.block_onebox_on_redirect, + ) title = nil if title && title.length < MIN_TITLE_LENGTH return onebox_for(url, title, opts) end @@ -95,23 +93,20 @@ class InlineOneboxer if title && opts[:post_number] title += " - " if opts[:post_author] - title += I18n.t( - "inline_oneboxer.topic_page_title_post_number_by_user", - post_number: opts[:post_number], - username: opts[:post_author] - ) + title += + I18n.t( + "inline_oneboxer.topic_page_title_post_number_by_user", + post_number: opts[:post_number], + username: opts[:post_author], + ) else - title += I18n.t( - "inline_oneboxer.topic_page_title_post_number", - post_number: opts[:post_number] - ) + title += + I18n.t("inline_oneboxer.topic_page_title_post_number", post_number: opts[:post_number]) end end title = title && Emoji.gsub_emoji_to_unicode(title) - if title.present? - title = WordWatcher.censor_text(title) - end + title = WordWatcher.censor_text(title) if title.present? onebox = { url: url, title: title } diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 009b75698f..2b8db85575 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module JsLocaleHelper - def self.plugin_client_files(locale_str) files = Dir["#{Rails.root}/plugins/*/config/locales/client*.#{locale_str}.yml"] I18n::Backend::DiscourseI18n.sort_locale_files(files) @@ -10,9 +9,7 @@ module JsLocaleHelper def self.reloadable_plugins(locale_sym, ctx) return unless Rails.env.development? I18n.fallbacks[locale_sym].each do |locale| - plugin_client_files(locale.to_s).each do |file| - ctx.depend_on(file) - end + plugin_client_files(locale.to_s).each { |file| ctx.depend_on(file) } end end @@ -44,24 +41,28 @@ module JsLocaleHelper else # If we can't find a base file in Discourse, it might only exist in a plugin # so let's start with a basic object we can merge into - translations = { - locale_str => { - 'js' => {}, - 'admin_js' => {}, - 'wizard_js' => {} - } - } + translations = { locale_str => { "js" => {}, "admin_js" => {}, "wizard_js" => {} } } end # merge translations (plugin translations overwrite default translations) if translations[locale_str] && plugin_translations(locale_str) - translations[locale_str]['js'] ||= {} - translations[locale_str]['admin_js'] ||= {} - translations[locale_str]['wizard_js'] ||= {} + translations[locale_str]["js"] ||= {} + translations[locale_str]["admin_js"] ||= {} + translations[locale_str]["wizard_js"] ||= {} - translations[locale_str]['js'].deep_merge!(plugin_translations(locale_str)['js']) if plugin_translations(locale_str)['js'] - translations[locale_str]['admin_js'].deep_merge!(plugin_translations(locale_str)['admin_js']) if plugin_translations(locale_str)['admin_js'] - translations[locale_str]['wizard_js'].deep_merge!(plugin_translations(locale_str)['wizard_js']) if plugin_translations(locale_str)['wizard_js'] + if plugin_translations(locale_str)["js"] + translations[locale_str]["js"].deep_merge!(plugin_translations(locale_str)["js"]) + end + if plugin_translations(locale_str)["admin_js"] + translations[locale_str]["admin_js"].deep_merge!( + plugin_translations(locale_str)["admin_js"], + ) + end + if plugin_translations(locale_str)["wizard_js"] + translations[locale_str]["wizard_js"].deep_merge!( + plugin_translations(locale_str)["wizard_js"], + ) + end end translations @@ -82,9 +83,7 @@ module JsLocaleHelper new_hash[key] = new_at_key end else - if checking_hashes.any? { |h| h.include?(key) } - new_hash.delete(key) - end + new_hash.delete(key) if checking_hashes.any? { |h| h.include?(key) } end end new_hash @@ -93,16 +92,21 @@ module JsLocaleHelper def self.load_translations_merged(*locales) locales = locales.uniq.compact @loaded_merges ||= {} - @loaded_merges[locales.join('-')] ||= begin + @loaded_merges[locales.join("-")] ||= begin all_translations = {} merged_translations = {} loaded_locales = [] - locales.map(&:to_s).each do |locale| - all_translations[locale] = load_translations(locale) - merged_translations[locale] = deep_delete_matches(all_translations[locale][locale], loaded_locales.map { |l| merged_translations[l] }) - loaded_locales << locale - end + locales + .map(&:to_s) + .each do |locale| + all_translations[locale] = load_translations(locale) + merged_translations[locale] = deep_delete_matches( + all_translations[locale][locale], + loaded_locales.map { |l| merged_translations[l] }, + ) + loaded_locales << locale + end merged_translations end end @@ -118,13 +122,14 @@ module JsLocaleHelper locale_sym = locale_str.to_sym - translations = I18n.with_locale(locale_sym) do - if locale_sym == :en - load_translations(locale_sym) - else - load_translations_merged(*I18n.fallbacks[locale_sym]) + translations = + I18n.with_locale(locale_sym) do + if locale_sym == :en + load_translations(locale_sym) + else + load_translations_merged(*I18n.fallbacks[locale_sym]) + end end - end Marshal.load(Marshal.dump(translations)) end @@ -139,16 +144,18 @@ module JsLocaleHelper result = generate_message_format(message_formats, mf_locale, mf_filename) translations.keys.each do |l| - translations[l].keys.each do |k| - translations[l].delete(k) unless k == "js" - end + translations[l].keys.each { |k| translations[l].delete(k) unless k == "js" } end # I18n result << "I18n.translations = #{translations.to_json};\n" result << "I18n.locale = '#{locale_str}';\n" - result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" if fallback_locale_str && fallback_locale_str != "en" - result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" if mf_locale != "en" + if fallback_locale_str && fallback_locale_str != "en" + result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" + end + if mf_locale != "en" + result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" + end # moment result << File.read("#{Rails.root}/vendor/assets/javascripts/moment.js") @@ -165,10 +172,11 @@ module JsLocaleHelper has_overrides = false I18n.fallbacks[main_locale].each do |locale| - overrides = all_overrides[locale] = TranslationOverride - .where(locale: locale) - .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") - .pluck(:translation_key, :value, :compiled_js) + overrides = + all_overrides[locale] = TranslationOverride + .where(locale: locale) + .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") + .pluck(:translation_key, :value, :compiled_js) has_overrides ||= overrides.present? end @@ -214,19 +222,15 @@ module JsLocaleHelper return "" if translations.blank? output = +"if (!I18n.extras) { I18n.extras = {}; }" - locales.each do |l| - output << <<~JS + locales.each { |l| output << <<~JS } if (!I18n.extras["#{l}"]) { I18n.extras["#{l}"] = {}; } Object.assign(I18n.extras["#{l}"], #{translations[l].to_json}); JS - end output end - MOMENT_LOCALE_MAPPING ||= { - "hy" => "hy-am" - } + MOMENT_LOCALE_MAPPING ||= { "hy" => "hy-am" } def self.find_moment_locale(locale_chain, timezone_names: false) if timezone_names @@ -240,7 +244,7 @@ module JsLocaleHelper find_locale(locale_chain, path, type, fallback_to_english: false) do |locale| locale = MOMENT_LOCALE_MAPPING[locale] if MOMENT_LOCALE_MAPPING.key?(locale) # moment.js uses a different naming scheme for locale files - locale.tr('_', '-').downcase + locale.tr("_", "-").downcase end end @@ -258,14 +262,14 @@ module JsLocaleHelper locale = yield(locale) if block_given? filename = File.join(path, "#{locale}.js") - return [locale, filename] if File.exist?(filename) + return locale, filename if File.exist?(filename) end locale_chain.map! { |locale| yield(locale) } if block_given? # try again, but this time only with the language itself - locale_chain = locale_chain.map { |l| l.split(/[-_]/)[0] } - .uniq.reject { |l| locale_chain.include?(l) } + locale_chain = + locale_chain.map { |l| l.split(/[-_]/)[0] }.uniq.reject { |l| locale_chain.include?(l) } if locale_chain.any? locale_data = find_locale(locale_chain, path, type, fallback_to_english: false) @@ -278,9 +282,9 @@ module JsLocaleHelper def self.moment_formats result = +"" - result << moment_format_function('short_date_no_year') - result << moment_format_function('short_date') - result << moment_format_function('long_date') + result << moment_format_function("short_date_no_year") + result << moment_format_function("short_date") + result << moment_format_function("long_date") result << "moment.fn.relativeAge = function(opts){ return Discourse.Formatter.relativeAge(this.toDate(), opts)};\n" end @@ -295,7 +299,10 @@ module JsLocaleHelper end def self.generate_message_format(message_formats, locale, filename) - formats = message_formats.map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) }.join(", ") + formats = + message_formats + .map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) } + .join(", ") result = +"MessageFormat = {locale: {}};\n" result << "I18n._compiledMFs = {#{formats}};\n" @@ -318,11 +325,14 @@ module JsLocaleHelper @mutex = Mutex.new def self.with_context @mutex.synchronize do - yield @ctx ||= begin - ctx = MiniRacer::Context.new(timeout: 15000, ensure_gc_after_idle: 2000) - ctx.load("#{Rails.root}/lib/javascripts/messageformat.js") - ctx - end + yield( + @ctx ||= + begin + ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000) + ctx.load("#{Rails.root}/lib/javascripts/messageformat.js") + ctx + end + ) end end @@ -339,13 +349,15 @@ module JsLocaleHelper def self.remove_message_formats!(translations, locale) message_formats = {} - I18n.fallbacks[locale].map(&:to_s).each do |l| - next unless translations.key?(l) + I18n.fallbacks[locale] + .map(&:to_s) + .each do |l| + next unless translations.key?(l) - %w{js admin_js}.each do |k| - message_formats.merge!(strip_out_message_formats!(translations[l][k])) + %w[js admin_js].each do |k| + message_formats.merge!(strip_out_message_formats!(translations[l][k])) + end end - end message_formats end @@ -353,7 +365,9 @@ module JsLocaleHelper if hash.is_a?(Hash) hash.each do |key, value| if value.is_a?(Hash) - message_formats.merge!(strip_out_message_formats!(value, join_key(prefix, key), message_formats)) + message_formats.merge!( + strip_out_message_formats!(value, join_key(prefix, key), message_formats), + ) elsif key.to_s.end_with?("_MF") message_formats[join_key(prefix, key)] = value hash.delete(key) diff --git a/lib/json_error.rb b/lib/json_error.rb index ac6ff76edc..d8867684a0 100644 --- a/lib/json_error.rb +++ b/lib/json_error.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module JsonError - def create_errors_json(obj, opts = nil) opts ||= {} @@ -32,7 +31,7 @@ module JsonError return { errors: [message] } if message.present? end - return { errors: [I18n.t('not_found')] } if obj.is_a?(HasErrors) && obj.not_found + return { errors: [I18n.t("not_found")] } if obj.is_a?(HasErrors) && obj.not_found # Log a warning (unless obj is nil) Rails.logger.warn("create_errors_json called with unrecognized type: #{obj.inspect}") if obj @@ -42,7 +41,6 @@ module JsonError end def self.generic_error - { errors: [I18n.t('js.generic_error')] } + { errors: [I18n.t("js.generic_error")] } end - end diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index 1a8524f338..88cea034a0 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true class LetterAvatar - class Identity attr_accessor :color, :letter def self.from_username(username) identity = new - identity.color = LetterAvatar::COLORS[ - Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length - ] + identity.color = + LetterAvatar::COLORS[ + Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length + ] identity.letter = username[0].upcase identity end @@ -19,11 +19,10 @@ class LetterAvatar VERSION = 5 # CHANGE these values to support more pixel ratios - FULLSIZE = 120 * 3 + FULLSIZE = 120 * 3 POINTSIZE = 280 class << self - def version "#{VERSION}_#{image_magick_version}" end @@ -72,19 +71,27 @@ class LetterAvatar filename = fullsize_path(identity) - instructions = %W{ - -size #{FULLSIZE}x#{FULLSIZE} + instructions = %W[ + -size + #{FULLSIZE}x#{FULLSIZE} xc:#{to_rgb(color)} - -pointsize #{POINTSIZE} - -fill #FFFFFFCC - -font Helvetica - -gravity Center - -annotate -0+26 #{letter} - -depth 8 + -pointsize + #{POINTSIZE} + -fill + #FFFFFFCC + -font + Helvetica + -gravity + Center + -annotate + -0+26 + #{letter} + -depth + 8 #{filename} - } + ] - Discourse::Utils.execute_command('convert', *instructions) + Discourse::Utils.execute_command("convert", *instructions) ## do not optimize image, it will end up larger than original filename @@ -110,11 +117,11 @@ class LetterAvatar begin skip = File.basename(cache_path) parent_path = File.dirname(cache_path) - Dir.entries(parent_path).each do |path| - unless ['.', '..'].include?(path) || path == skip - FileUtils.rm_rf(parent_path + "/" + path) + Dir + .entries(parent_path) + .each do |path| + FileUtils.rm_rf(parent_path + "/" + path) unless %w[. ..].include?(path) || path == skip end - end rescue Errno::ENOENT # no worries, folder doesn't exists end @@ -127,220 +134,222 @@ class LetterAvatar # - H: 0 - 360 # - C: 0 - 2 # - L: 0.75 - 1.5 - COLORS = [[198, 125, 40], - [61, 155, 243], - [74, 243, 75], - [238, 89, 166], - [52, 240, 224], - [177, 156, 155], - [240, 120, 145], - [111, 154, 78], - [237, 179, 245], - [237, 101, 95], - [89, 239, 155], - [43, 254, 70], - [163, 212, 245], - [65, 152, 142], - [165, 135, 246], - [181, 166, 38], - [187, 229, 206], - [77, 164, 25], - [179, 246, 101], - [234, 93, 37], - [225, 155, 115], - [142, 140, 188], - [223, 120, 140], - [249, 174, 27], - [244, 117, 225], - [137, 141, 102], - [75, 191, 146], - [188, 239, 142], - [164, 199, 145], - [173, 120, 149], - [59, 195, 89], - [222, 198, 220], - [68, 145, 187], - [236, 204, 179], - [159, 195, 72], - [188, 121, 189], - [166, 160, 85], - [181, 233, 37], - [236, 177, 85], - [121, 147, 160], - [234, 218, 110], - [241, 157, 191], - [62, 200, 234], - [133, 243, 34], - [88, 149, 110], - [59, 228, 248], - [183, 119, 118], - [251, 195, 45], - [113, 196, 122], - [197, 115, 70], - [80, 175, 187], - [103, 231, 238], - [240, 72, 133], - [228, 149, 241], - [180, 188, 159], - [172, 132, 85], - [180, 135, 251], - [236, 194, 58], - [217, 176, 109], - [88, 244, 199], - [186, 157, 239], - [113, 230, 96], - [206, 115, 165], - [244, 178, 163], - [230, 139, 26], - [241, 125, 89], - [83, 160, 66], - [107, 190, 166], - [197, 161, 210], - [198, 203, 245], - [238, 117, 19], - [228, 119, 116], - [131, 156, 41], - [145, 178, 168], - [139, 170, 220], - [233, 95, 125], - [87, 178, 230], - [157, 200, 119], - [237, 140, 76], - [229, 185, 186], - [144, 206, 212], - [236, 209, 158], - [185, 189, 79], - [34, 208, 66], - [84, 238, 129], - [133, 140, 134], - [67, 157, 94], - [168, 179, 25], - [140, 145, 240], - [151, 241, 125], - [67, 162, 107], - [200, 156, 21], - [169, 173, 189], - [226, 116, 189], - [133, 231, 191], - [194, 161, 63], - [241, 77, 99], - [241, 217, 53], - [123, 204, 105], - [210, 201, 119], - [229, 108, 155], - [240, 91, 72], - [187, 115, 210], - [240, 163, 100], - [178, 217, 57], - [179, 135, 116], - [204, 211, 24], - [186, 135, 57], - [223, 176, 135], - [204, 148, 151], - [116, 223, 50], - [95, 195, 46], - [123, 160, 236], - [181, 172, 131], - [142, 220, 202], - [240, 140, 112], - [172, 145, 164], - [228, 124, 45], - [135, 151, 243], - [42, 205, 125], - [192, 233, 116], - [119, 170, 114], - [158, 138, 26], - [73, 190, 183], - [185, 229, 243], - [227, 107, 55], - [196, 205, 202], - [132, 143, 60], - [233, 192, 237], - [62, 150, 220], - [205, 201, 141], - [106, 140, 190], - [161, 131, 205], - [135, 134, 158], - [198, 139, 81], - [115, 171, 32], - [101, 181, 67], - [149, 137, 119], - [37, 142, 183], - [183, 130, 175], - [168, 125, 133], - [124, 142, 87], - [236, 156, 171], - [232, 194, 91], - [219, 200, 69], - [144, 219, 34], - [219, 95, 187], - [145, 154, 217], - [165, 185, 100], - [127, 238, 163], - [224, 178, 198], - [119, 153, 120], - [124, 212, 92], - [172, 161, 105], - [231, 155, 135], - [157, 132, 101], - [122, 185, 146], - [53, 166, 51], - [70, 163, 90], - [150, 190, 213], - [210, 107, 60], - [166, 152, 185], - [159, 194, 159], - [39, 141, 222], - [202, 176, 161], - [95, 140, 229], - [168, 142, 87], - [93, 170, 203], - [159, 142, 54], - [14, 168, 39], - [94, 150, 149], - [187, 206, 136], - [157, 224, 166], - [235, 158, 208], - [109, 232, 216], - [141, 201, 87], - [208, 124, 118], - [142, 125, 214], - [19, 237, 174], - [72, 219, 41], - [234, 102, 111], - [168, 142, 79], - [188, 135, 35], - [95, 155, 143], - [148, 173, 116], - [223, 112, 95], - [228, 128, 236], - [206, 114, 54], - [195, 119, 88], - [235, 140, 94], - [235, 202, 125], - [233, 155, 153], - [214, 214, 238], - [246, 200, 35], - [151, 125, 171], - [132, 145, 172], - [131, 142, 118], - [199, 126, 150], - [61, 162, 123], - [58, 176, 151], - [215, 141, 69], - [225, 154, 220], - [220, 77, 167], - [233, 161, 64], - [130, 221, 137], - [81, 191, 129], - [169, 162, 140], - [174, 177, 222], - [236, 174, 47], - [233, 188, 180], - [69, 222, 172], - [71, 232, 93], - [118, 211, 238], - [157, 224, 83], - [218, 105, 73], - [126, 169, 36]] + COLORS = [ + [198, 125, 40], + [61, 155, 243], + [74, 243, 75], + [238, 89, 166], + [52, 240, 224], + [177, 156, 155], + [240, 120, 145], + [111, 154, 78], + [237, 179, 245], + [237, 101, 95], + [89, 239, 155], + [43, 254, 70], + [163, 212, 245], + [65, 152, 142], + [165, 135, 246], + [181, 166, 38], + [187, 229, 206], + [77, 164, 25], + [179, 246, 101], + [234, 93, 37], + [225, 155, 115], + [142, 140, 188], + [223, 120, 140], + [249, 174, 27], + [244, 117, 225], + [137, 141, 102], + [75, 191, 146], + [188, 239, 142], + [164, 199, 145], + [173, 120, 149], + [59, 195, 89], + [222, 198, 220], + [68, 145, 187], + [236, 204, 179], + [159, 195, 72], + [188, 121, 189], + [166, 160, 85], + [181, 233, 37], + [236, 177, 85], + [121, 147, 160], + [234, 218, 110], + [241, 157, 191], + [62, 200, 234], + [133, 243, 34], + [88, 149, 110], + [59, 228, 248], + [183, 119, 118], + [251, 195, 45], + [113, 196, 122], + [197, 115, 70], + [80, 175, 187], + [103, 231, 238], + [240, 72, 133], + [228, 149, 241], + [180, 188, 159], + [172, 132, 85], + [180, 135, 251], + [236, 194, 58], + [217, 176, 109], + [88, 244, 199], + [186, 157, 239], + [113, 230, 96], + [206, 115, 165], + [244, 178, 163], + [230, 139, 26], + [241, 125, 89], + [83, 160, 66], + [107, 190, 166], + [197, 161, 210], + [198, 203, 245], + [238, 117, 19], + [228, 119, 116], + [131, 156, 41], + [145, 178, 168], + [139, 170, 220], + [233, 95, 125], + [87, 178, 230], + [157, 200, 119], + [237, 140, 76], + [229, 185, 186], + [144, 206, 212], + [236, 209, 158], + [185, 189, 79], + [34, 208, 66], + [84, 238, 129], + [133, 140, 134], + [67, 157, 94], + [168, 179, 25], + [140, 145, 240], + [151, 241, 125], + [67, 162, 107], + [200, 156, 21], + [169, 173, 189], + [226, 116, 189], + [133, 231, 191], + [194, 161, 63], + [241, 77, 99], + [241, 217, 53], + [123, 204, 105], + [210, 201, 119], + [229, 108, 155], + [240, 91, 72], + [187, 115, 210], + [240, 163, 100], + [178, 217, 57], + [179, 135, 116], + [204, 211, 24], + [186, 135, 57], + [223, 176, 135], + [204, 148, 151], + [116, 223, 50], + [95, 195, 46], + [123, 160, 236], + [181, 172, 131], + [142, 220, 202], + [240, 140, 112], + [172, 145, 164], + [228, 124, 45], + [135, 151, 243], + [42, 205, 125], + [192, 233, 116], + [119, 170, 114], + [158, 138, 26], + [73, 190, 183], + [185, 229, 243], + [227, 107, 55], + [196, 205, 202], + [132, 143, 60], + [233, 192, 237], + [62, 150, 220], + [205, 201, 141], + [106, 140, 190], + [161, 131, 205], + [135, 134, 158], + [198, 139, 81], + [115, 171, 32], + [101, 181, 67], + [149, 137, 119], + [37, 142, 183], + [183, 130, 175], + [168, 125, 133], + [124, 142, 87], + [236, 156, 171], + [232, 194, 91], + [219, 200, 69], + [144, 219, 34], + [219, 95, 187], + [145, 154, 217], + [165, 185, 100], + [127, 238, 163], + [224, 178, 198], + [119, 153, 120], + [124, 212, 92], + [172, 161, 105], + [231, 155, 135], + [157, 132, 101], + [122, 185, 146], + [53, 166, 51], + [70, 163, 90], + [150, 190, 213], + [210, 107, 60], + [166, 152, 185], + [159, 194, 159], + [39, 141, 222], + [202, 176, 161], + [95, 140, 229], + [168, 142, 87], + [93, 170, 203], + [159, 142, 54], + [14, 168, 39], + [94, 150, 149], + [187, 206, 136], + [157, 224, 166], + [235, 158, 208], + [109, 232, 216], + [141, 201, 87], + [208, 124, 118], + [142, 125, 214], + [19, 237, 174], + [72, 219, 41], + [234, 102, 111], + [168, 142, 79], + [188, 135, 35], + [95, 155, 143], + [148, 173, 116], + [223, 112, 95], + [228, 128, 236], + [206, 114, 54], + [195, 119, 88], + [235, 140, 94], + [235, 202, 125], + [233, 155, 153], + [214, 214, 238], + [246, 200, 35], + [151, 125, 171], + [132, 145, 172], + [131, 142, 118], + [199, 126, 150], + [61, 162, 123], + [58, 176, 151], + [215, 141, 69], + [225, 154, 220], + [220, 77, 167], + [233, 161, 64], + [130, 221, 137], + [81, 191, 129], + [169, 162, 140], + [174, 177, 222], + [236, 174, 47], + [233, 188, 180], + [69, 222, 172], + [71, 232, 93], + [118, 211, 238], + [157, 224, 83], + [218, 105, 73], + [126, 169, 36], + ] end diff --git a/lib/markdown_linker.rb b/lib/markdown_linker.rb index fe48d4f5f2..bc5bbdd012 100644 --- a/lib/markdown_linker.rb +++ b/lib/markdown_linker.rb @@ -2,7 +2,6 @@ # Helps create links using markdown (where references are at the bottom) class MarkdownLinker - def initialize(base_url) @base_url = base_url @index = 1 @@ -19,11 +18,8 @@ class MarkdownLinker def references result = +"" - (@rendered..@index - 1).each do |i| - result << "[#{i}]: #{@markdown_links[i]}\n" - end + (@rendered..@index - 1).each { |i| result << "[#{i}]: #{@markdown_links[i]}\n" } @rendered = @index result end - end diff --git a/lib/mem_info.rb b/lib/mem_info.rb index 6404fa8733..c8ea0d0d6c 100644 --- a/lib/mem_info.rb +++ b/lib/mem_info.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MemInfo - # Total memory in kb. On Mac OS uses "sysctl", elsewhere expects the system has /proc/meminfo. # Returns nil if it cannot be determined. def mem_total @@ -15,9 +14,8 @@ class MemInfo s = `grep MemTotal /proc/meminfo` /(\d+)/.match(s)[0].try(:to_i) end - rescue + rescue StandardError nil end end - end diff --git a/lib/message_bus_diags.rb b/lib/message_bus_diags.rb index 16bde3a9a1..b2de1ad80e 100644 --- a/lib/message_bus_diags.rb +++ b/lib/message_bus_diags.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MessageBusDiags - @host_info = {} def self.my_id @@ -21,7 +20,6 @@ class MessageBusDiags end unless @subscribed - MessageBus.subscribe "/server-name-reply/#{my_id}" do |msg| MessageBusDiags.seen_host(msg.data) end diff --git a/lib/method_profiler.rb b/lib/method_profiler.rb index fd6588c6bc..aa0836278d 100644 --- a/lib/method_profiler.rb +++ b/lib/method_profiler.rb @@ -3,17 +3,16 @@ # see https://samsaffron.com/archive/2017/10/18/fastest-way-to-profile-a-method-in-ruby class MethodProfiler def self.patch(klass, methods, name, no_recurse: false) - patches = methods.map do |method_name| - - recurse_protection = "" - if no_recurse - recurse_protection = <<~RUBY + patches = + methods + .map do |method_name| + recurse_protection = "" + recurse_protection = <<~RUBY if no_recurse return #{method_name}__mp_unpatched(*args, &blk) if @mp_recurse_protect_#{method_name} @mp_recurse_protect_#{method_name} = true RUBY - end - <<~RUBY + <<~RUBY unless defined?(#{method_name}__mp_unpatched) alias_method :#{method_name}__mp_unpatched, :#{method_name} def #{method_name}(*args, &blk) @@ -33,23 +32,23 @@ class MethodProfiler end end RUBY - end.join("\n") + end + .join("\n") klass.class_eval patches end def self.patch_with_debug_sql(klass, methods, name, no_recurse: false) - patches = methods.map do |method_name| - - recurse_protection = "" - if no_recurse - recurse_protection = <<~RUBY + patches = + methods + .map do |method_name| + recurse_protection = "" + recurse_protection = <<~RUBY if no_recurse return #{method_name}__mp_unpatched_debug_sql(*args, &blk) if @mp_recurse_protect_#{method_name} @mp_recurse_protect_#{method_name} = true RUBY - end - <<~RUBY + <<~RUBY unless defined?(#{method_name}__mp_unpatched_debug_sql) alias_method :#{method_name}__mp_unpatched_debug_sql, :#{method_name} def #{method_name}(*args, &blk) @@ -77,7 +76,8 @@ class MethodProfiler end end RUBY - end.join("\n") + end + .join("\n") klass.class_eval patches end @@ -89,9 +89,8 @@ class MethodProfiler end def self.start(transfer = nil) - Thread.current[:_method_profiler] = transfer || { - __start: Process.clock_gettime(Process::CLOCK_MONOTONIC) - } + Thread.current[:_method_profiler] = transfer || + { __start: Process.clock_gettime(Process::CLOCK_MONOTONIC) } end def self.clear @@ -116,35 +115,36 @@ class MethodProfiler # filter_transactions - When true, we do not record timings of transaction # related commits (BEGIN, COMMIT, ROLLBACK) def self.output_sql_to_stderr!(filter_transactions: false) - Rails.logger.warn("Stop! This instrumentation is not intended for use in production outside of debugging scenarios. Please be sure you know what you are doing when enabling this instrumentation.") + Rails.logger.warn( + "Stop! This instrumentation is not intended for use in production outside of debugging scenarios. Please be sure you know what you are doing when enabling this instrumentation.", + ) @@instrumentation_debug_sql_filter_transactions = filter_transactions - @@instrumentation_setup_debug_sql ||= begin - MethodProfiler.patch_with_debug_sql(PG::Connection, [ - :exec, :async_exec, :exec_prepared, :send_query_prepared, :query, :exec_params - ], :sql) - true - end + @@instrumentation_setup_debug_sql ||= + begin + MethodProfiler.patch_with_debug_sql( + PG::Connection, + %i[exec async_exec exec_prepared send_query_prepared query exec_params], + :sql, + ) + true + end end def self.ensure_discourse_instrumentation! - @@instrumentation_setup ||= begin - MethodProfiler.patch(PG::Connection, [ - :exec, :async_exec, :exec_prepared, :send_query_prepared, :query, :exec_params - ], :sql) + @@instrumentation_setup ||= + begin + MethodProfiler.patch( + PG::Connection, + %i[exec async_exec exec_prepared send_query_prepared query exec_params], + :sql, + ) - MethodProfiler.patch(Redis::Client, [ - :call, :call_pipeline - ], :redis) + MethodProfiler.patch(Redis::Client, %i[call call_pipeline], :redis) - MethodProfiler.patch(Net::HTTP, [ - :request - ], :net, no_recurse: true) + MethodProfiler.patch(Net::HTTP, [:request], :net, no_recurse: true) - MethodProfiler.patch(Excon::Connection, [ - :request - ], :net) - true - end + MethodProfiler.patch(Excon::Connection, [:request], :net) + true + end end - end diff --git a/lib/middleware/anonymous_cache.rb b/lib/middleware/anonymous_cache.rb index ef6bc7e1dd..553113e789 100644 --- a/lib/middleware/anonymous_cache.rb +++ b/lib/middleware/anonymous_cache.rb @@ -7,17 +7,16 @@ require "http_language_parser" module Middleware class AnonymousCache - def self.cache_key_segments @@cache_key_segments ||= { - m: 'key_is_mobile?', - c: 'key_is_crawler?', - o: 'key_is_old_browser?', - d: 'key_is_modern_mobile_device?', - b: 'key_has_brotli?', - t: 'key_cache_theme_ids', - ca: 'key_compress_anon', - l: 'key_locale' + m: "key_is_mobile?", + c: "key_is_crawler?", + o: "key_is_old_browser?", + d: "key_is_modern_mobile_device?", + b: "key_has_brotli?", + t: "key_cache_theme_ids", + ca: "key_compress_anon", + l: "key_locale", } end @@ -46,9 +45,9 @@ module Middleware # This gives us an API to insert anonymous cache segments class Helper - RACK_SESSION = "rack.session" - USER_AGENT = "HTTP_USER_AGENT" - ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING" + RACK_SESSION = "rack.session" + USER_AGENT = "HTTP_USER_AGENT" + ACCEPT_ENCODING = "HTTP_ACCEPT_ENCODING" DISCOURSE_RENDER = "HTTP_DISCOURSE_RENDER" REDIS_STORE_SCRIPT = DiscourseRedis::EvalHelper.new <<~LUA @@ -63,13 +62,11 @@ module Middleware end def blocked_crawler? - @request.get? && - !@request.xhr? && - !@request.path.ends_with?('robots.txt') && - !@request.path.ends_with?('srv/status') && - @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && - @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? && - CrawlerDetection.is_blocked_crawler?(@env[USER_AGENT]) + @request.get? && !@request.xhr? && !@request.path.ends_with?("robots.txt") && + !@request.path.ends_with?("srv/status") && + @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && + @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? && + CrawlerDetection.is_blocked_crawler?(@env[USER_AGENT]) end def is_mobile=(val) @@ -112,10 +109,16 @@ module Middleware begin user_agent = @env[USER_AGENT] - if @env[DISCOURSE_RENDER] == "crawler" || CrawlerDetection.crawler?(user_agent, @env["HTTP_VIA"]) + if @env[DISCOURSE_RENDER] == "crawler" || + CrawlerDetection.crawler?(user_agent, @env["HTTP_VIA"]) :true else - user_agent.downcase.include?("discourse") && !user_agent.downcase.include?("mobile") ? :true : :false + if user_agent.downcase.include?("discourse") && + !user_agent.downcase.include?("mobile") + :true + else + :false + end end end @is_crawler == :true @@ -133,13 +136,14 @@ module Middleware def cache_key return @cache_key if defined?(@cache_key) - @cache_key = +"ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}" + @cache_key = + +"ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}" @cache_key << AnonymousCache.build_cache_key(self) @cache_key end def key_cache_theme_ids - theme_ids.join(',') + theme_ids.join(",") end def key_compress_anon @@ -147,7 +151,7 @@ module Middleware end def theme_ids - ids, _ = @request.cookies['theme_ids']&.split('|') + ids, _ = @request.cookies["theme_ids"]&.split("|") id = ids&.split(",")&.map(&:to_i)&.first if id && Guardian.new.allow_themes?([id]) Theme.transform_ids(id) @@ -178,31 +182,31 @@ module Middleware def no_cache_bypass request = Rack::Request.new(@env) - request.cookies['_bypass_cache'].nil? && - (request.path != '/srv/status') && + request.cookies["_bypass_cache"].nil? && (request.path != "/srv/status") && request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? end def force_anonymous! @env[Auth::DefaultCurrentUserProvider::USER_API_KEY] = nil - @env['HTTP_COOKIE'] = nil - @env['HTTP_DISCOURSE_LOGGED_IN'] = nil - @env['rack.request.cookie.hash'] = {} - @env['rack.request.cookie.string'] = '' - @env['_bypass_cache'] = nil + @env["HTTP_COOKIE"] = nil + @env["HTTP_DISCOURSE_LOGGED_IN"] = nil + @env["rack.request.cookie.hash"] = {} + @env["rack.request.cookie.string"] = "" + @env["_bypass_cache"] = nil request = Rack::Request.new(@env) - request.delete_param('api_username') - request.delete_param('api_key') + request.delete_param("api_username") + request.delete_param("api_key") end def logged_in_anon_limiter - @logged_in_anon_limiter ||= RateLimiter.new( - nil, - "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}", - GlobalSetting.force_anonymous_min_per_10_seconds, - 10 - ) + @logged_in_anon_limiter ||= + RateLimiter.new( + nil, + "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}", + GlobalSetting.force_anonymous_min_per_10_seconds, + 10, + ) end def check_logged_in_rate_limit! @@ -213,13 +217,11 @@ module Middleware ADP = "action_dispatch.request.parameters" def should_force_anonymous? - if (queue_time = @env['REQUEST_QUEUE_SECONDS']) && get? + if (queue_time = @env["REQUEST_QUEUE_SECONDS"]) && get? if queue_time > GlobalSetting.force_anonymous_min_queue_seconds return check_logged_in_rate_limit! elsif queue_time >= MIN_TIME_TO_CHECK - if !logged_in_anon_limiter.can_perform? - return check_logged_in_rate_limit! - end + return check_logged_in_rate_limit! if !logged_in_anon_limiter.can_perform? end end @@ -233,7 +235,7 @@ module Middleware def compress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) - LZ4::compress(val) + LZ4.compress(val) else val end @@ -242,7 +244,7 @@ module Middleware def decompress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) - LZ4::uncompress(val) + LZ4.uncompress(val) else val end @@ -273,7 +275,6 @@ module Middleware status, headers, response = result if status == 200 && cache_duration - if GlobalSetting.anon_cache_store_threshold > 1 count = REDIS_STORE_SCRIPT.eval(Discourse.redis, [cache_key_count], [cache_duration]) @@ -281,25 +282,24 @@ module Middleware # prudent here, hence the to_i if count.to_i < GlobalSetting.anon_cache_store_threshold headers["X-Discourse-Cached"] = "skip" - return [status, headers, response] + return status, headers, response end end - headers_stripped = headers.dup.delete_if { |k, _| ["Set-Cookie", "X-MiniProfiler-Ids"].include? k } + headers_stripped = + headers.dup.delete_if { |k, _| %w[Set-Cookie X-MiniProfiler-Ids].include? k } headers_stripped["X-Discourse-Cached"] = "true" parts = [] - response.each do |part| - parts << part - end + response.each { |part| parts << part } if req_params = env[ADP] headers_stripped[ADP] = { "action" => req_params["action"], - "controller" => req_params["controller"] + "controller" => req_params["controller"], } end - Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join)) + Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join)) Discourse.redis.setex(cache_key_other, cache_duration, [status, headers_stripped].to_json) headers["X-Discourse-Cached"] = "store" @@ -314,20 +314,18 @@ module Middleware Discourse.redis.del(cache_key_body) Discourse.redis.del(cache_key_other) end - end def initialize(app, settings = {}) @app = app end - PAYLOAD_INVALID_REQUEST_METHODS = ["GET", "HEAD"] + PAYLOAD_INVALID_REQUEST_METHODS = %w[GET HEAD] def call(env) if PAYLOAD_INVALID_REQUEST_METHODS.include?(env[Rack::REQUEST_METHOD]) && - env[Rack::RACK_INPUT].size > 0 - - return [413, { "Cache-Control" => "private, max-age=0, must-revalidate" }, []] + env[Rack::RACK_INPUT].size > 0 + return 413, { "Cache-Control" => "private, max-age=0, must-revalidate" }, [] end helper = Helper.new(env) @@ -335,7 +333,7 @@ module Middleware if helper.blocked_crawler? env["discourse.request_tracker.skip"] = true - return [403, {}, ["Crawler is not allowed!"]] + return 403, {}, ["Crawler is not allowed!"] end if helper.should_force_anonymous? @@ -348,15 +346,15 @@ module Middleware if max_time > 0 && queue_time.to_f > max_time return [ 429, - { - "content-type" => "application/json; charset=utf-8" - }, - [{ - errors: I18n.t("rate_limiter.slow_down"), - extras: { - wait_seconds: 5 + (5 * rand).round(2) - } - }.to_json] + { "content-type" => "application/json; charset=utf-8" }, + [ + { + errors: I18n.t("rate_limiter.slow_down"), + extras: { + wait_seconds: 5 + (5 * rand).round(2), + }, + }.to_json, + ] ] end end @@ -368,13 +366,9 @@ module Middleware @app.call(env) end - if force_anon - result[1]["Set-Cookie"] = "dosp=1; Path=/" - end + result[1]["Set-Cookie"] = "dosp=1; Path=/" if force_anon result end - end - end diff --git a/lib/middleware/discourse_public_exceptions.rb b/lib/middleware/discourse_public_exceptions.rb index 9a9ea11571..b507bc867a 100644 --- a/lib/middleware/discourse_public_exceptions.rb +++ b/lib/middleware/discourse_public_exceptions.rb @@ -4,11 +4,14 @@ # we need to handle certain exceptions here module Middleware class DiscoursePublicExceptions < ::ActionDispatch::PublicExceptions - INVALID_REQUEST_ERRORS = Set.new([ - Rack::QueryParser::InvalidParameterError, - ActionController::BadRequest, - ActionDispatch::Http::Parameters::ParseError, - ]) + INVALID_REQUEST_ERRORS = + Set.new( + [ + Rack::QueryParser::InvalidParameterError, + ActionController::BadRequest, + ActionDispatch::Http::Parameters::ParseError, + ], + ) def initialize(path) super @@ -35,31 +38,38 @@ module Middleware begin request.format rescue Mime::Type::InvalidMimeType - return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid MIME type"]] + return [ + 400, + { "Cache-Control" => "private, max-age=0, must-revalidate" }, + ["Invalid MIME type"] + ] end # Or badly formatted multipart requests begin request.POST rescue EOFError - return [400, { "Cache-Control" => "private, max-age=0, must-revalidate" }, ["Invalid request"]] + return [ + 400, + { "Cache-Control" => "private, max-age=0, must-revalidate" }, + ["Invalid request"] + ] end if ApplicationController.rescue_with_handler(exception, object: fake_controller) body = response.body - if String === body - body = [body] - end - return [response.status, response.headers, body] + body = [body] if String === body + return response.status, response.headers, body end rescue => e return super if INVALID_REQUEST_ERRORS.include?(e.class) - Discourse.warn_exception(e, message: "Failed to handle exception in exception app middleware") + Discourse.warn_exception( + e, + message: "Failed to handle exception in exception app middleware", + ) end - end super end - end end diff --git a/lib/middleware/enforce_hostname.rb b/lib/middleware/enforce_hostname.rb index 44ac8d7d4a..f0f604b145 100644 --- a/lib/middleware/enforce_hostname.rb +++ b/lib/middleware/enforce_hostname.rb @@ -18,7 +18,8 @@ module Middleware requested_hostname = env[Rack::HTTP_HOST] env[Discourse::REQUESTED_HOSTNAME] = requested_hostname - env[Rack::HTTP_HOST] = allowed_hostnames.find { |h| h == requested_hostname } || Discourse.current_hostname_with_port + env[Rack::HTTP_HOST] = allowed_hostnames.find { |h| h == requested_hostname } || + Discourse.current_hostname_with_port @app.call(env) end diff --git a/lib/middleware/missing_avatars.rb b/lib/middleware/missing_avatars.rb index f0f2da5d5c..958aecaa3e 100644 --- a/lib/middleware/missing_avatars.rb +++ b/lib/middleware/missing_avatars.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Middleware - # In development mode, it is common to use a database from a production site for testing # with their data. Unfortunately, you can end up with dozens of missing avatar requests # due to the files not being present locally. This middleware, only enabled in development @@ -12,11 +11,11 @@ module Middleware end def call(env) - if (env['REQUEST_PATH'] =~ /^\/uploads\/default\/avatars/) - path = "#{Rails.root}/public#{env['REQUEST_PATH']}" + if (env["REQUEST_PATH"] =~ %r{^/uploads/default/avatars}) + path = "#{Rails.root}/public#{env["REQUEST_PATH"]}" unless File.exist?(path) default_image = "#{Rails.root}/public/images/d-logo-sketch-small.png" - return [ 200, { 'Content-Type' => 'image/png' }, [ File.read(default_image)] ] + return 200, { "Content-Type" => "image/png" }, [File.read(default_image)] end end @@ -24,5 +23,4 @@ module Middleware [status, headers, response] end end - end diff --git a/lib/middleware/omniauth_bypass_middleware.rb b/lib/middleware/omniauth_bypass_middleware.rb index c794b11aab..b8c65caac8 100644 --- a/lib/middleware/omniauth_bypass_middleware.rb +++ b/lib/middleware/omniauth_bypass_middleware.rb @@ -5,7 +5,8 @@ require "csrf_token_verifier" # omniauth loves spending lots cycles in its magic middleware stack # this middleware bypasses omniauth middleware and only hits it when needed class Middleware::OmniauthBypassMiddleware - class AuthenticatorDisabled < StandardError; end + class AuthenticatorDisabled < StandardError + end def initialize(app, options = {}) @app = app @@ -15,11 +16,10 @@ class Middleware::OmniauthBypassMiddleware # if you need to test this and are having ssl issues see: # http://stackoverflow.com/questions/6756460/openssl-error-using-omniauth-specified-ssl-path-but-didnt-work # OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE if Rails.env.development? - @omniauth = OmniAuth::Builder.new(app) do - Discourse.authenticators.each do |authenticator| - authenticator.register_middleware(self) + @omniauth = + OmniAuth::Builder.new(app) do + Discourse.authenticators.each { |authenticator| authenticator.register_middleware(self) } end - end @omniauth.before_request_phase do |env| request = ActionDispatch::Request.new(env) @@ -28,7 +28,9 @@ class Middleware::OmniauthBypassMiddleware CSRFTokenVerifier.new.call(env) if request.request_method.downcase.to_sym != :get # Check whether the authenticator is enabled - if !Discourse.enabled_authenticators.any? { |a| a.name.to_sym == env['omniauth.strategy'].name.to_sym } + if !Discourse.enabled_authenticators.any? { |a| + a.name.to_sym == env["omniauth.strategy"].name.to_sym + } raise AuthenticatorDisabled end @@ -44,8 +46,9 @@ class Middleware::OmniauthBypassMiddleware if env["PATH_INFO"].start_with?("/auth") begin # When only one provider is enabled, assume it can be completely trusted, and allow GET requests - only_one_provider = !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 - OmniAuth.config.allowed_request_methods = only_one_provider ? [:get, :post] : [:post] + only_one_provider = + !SiteSetting.enable_local_logins && Discourse.enabled_authenticators.length == 1 + OmniAuth.config.allowed_request_methods = only_one_provider ? %i[get post] : [:post] @omniauth.call(env) rescue AuthenticatorDisabled => e @@ -71,5 +74,4 @@ class Middleware::OmniauthBypassMiddleware @app.call(env) end end - end diff --git a/lib/middleware/request_tracker.rb b/lib/middleware/request_tracker.rb index fda5fc46f2..23acf64d12 100644 --- a/lib/middleware/request_tracker.rb +++ b/lib/middleware/request_tracker.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'method_profiler' -require 'middleware/anonymous_cache' +require "method_profiler" +require "middleware/anonymous_cache" class Middleware::RequestTracker @@detailed_request_loggers = nil @@ -15,7 +15,8 @@ class Middleware::RequestTracker # 14.15.16.32/27 # 216.148.1.2 # - STATIC_IP_SKIPPER = ENV['DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS']&.split&.map { |ip| IPAddr.new(ip) } + STATIC_IP_SKIPPER = + ENV["DISCOURSE_MAX_REQS_PER_IP_EXCEPTIONS"]&.split&.map { |ip| IPAddr.new(ip) } # register callbacks for detailed request loggers called on every request # example: @@ -30,9 +31,7 @@ class Middleware::RequestTracker def self.unregister_detailed_request_logger(callback) @@detailed_request_loggers.delete(callback) - if @@detailed_request_loggers.length == 0 - @detailed_request_loggers = nil - end + @detailed_request_loggers = nil if @@detailed_request_loggers.length == 0 end # used for testing @@ -107,7 +106,8 @@ class Middleware::RequestTracker env_track_view = env["HTTP_DISCOURSE_TRACK_VIEW"] track_view = status == 200 track_view &&= env_track_view != "0" && env_track_view != "false" - track_view &&= env_track_view || (request.get? && !request.xhr? && headers["Content-Type"] =~ /text\/html/) + track_view &&= + env_track_view || (request.get? && !request.xhr? && headers["Content-Type"] =~ %r{text/html}) track_view = !!track_view has_auth_cookie = Auth::DefaultCurrentUserProvider.find_v0_auth_cookie(request).present? has_auth_cookie ||= Auth::DefaultCurrentUserProvider.find_v1_auth_cookie(env).present? @@ -125,15 +125,28 @@ class Middleware::RequestTracker is_api: is_api, is_user_api: is_user_api, is_background: is_message_bus || is_topic_timings, - background_type: is_message_bus ? "message-bus" : "topic-timings", is_mobile: helper.is_mobile?, track_view: track_view, timing: timing, - queue_seconds: env['REQUEST_QUEUE_SECONDS'] + queue_seconds: env["REQUEST_QUEUE_SECONDS"], } + if h[:is_background] + h[:background_type] = if is_message_bus + if request.query_string.include?("dlp=t") + "message-bus-dlp" + elsif env["HTTP_DONT_CHUNK"] + "message-bus-dontchunk" + else + "message-bus" + end + else + "topic-timings" + end + end + if h[:is_crawler] - user_agent = env['HTTP_USER_AGENT'] + user_agent = env["HTTP_USER_AGENT"] if user_agent && (user_agent.encoding != Encoding::UTF_8) user_agent = user_agent.encode("utf-8") user_agent.scrub! @@ -150,7 +163,12 @@ class Middleware::RequestTracker def log_request_info(env, result, info, request = nil) # we got to skip this on error ... its just logging - data = self.class.get_data(env, result, info, request) rescue nil + data = + begin + self.class.get_data(env, result, info, request) + rescue StandardError + nil + end if data if result && (headers = result[1]) @@ -166,15 +184,16 @@ class Middleware::RequestTracker end def self.populate_request_queue_seconds!(env) - if !env['REQUEST_QUEUE_SECONDS'] - if queue_start = env['HTTP_X_REQUEST_START'] - queue_start = if queue_start.start_with?("t=") - queue_start.split("t=")[1].to_f - else - queue_start.to_f / 1000.0 - end + if !env["REQUEST_QUEUE_SECONDS"] + if queue_start = env["HTTP_X_REQUEST_START"] + queue_start = + if queue_start.start_with?("t=") + queue_start.split("t=")[1].to_f + else + queue_start.to_f / 1000.0 + end queue_time = (Time.now.to_f - queue_start) - env['REQUEST_QUEUE_SECONDS'] = queue_time + env["REQUEST_QUEUE_SECONDS"] = queue_time end end end @@ -199,9 +218,9 @@ class Middleware::RequestTracker TEXT headers = { "Retry-After" => available_in.to_s, - "Discourse-Rate-Limit-Error-Code" => error_code + "Discourse-Rate-Limit-Error-Code" => error_code, } - return [429, headers, [message]] + return 429, headers, [message] end env["discourse.request_tracker"] = self @@ -222,21 +241,21 @@ class Middleware::RequestTracker headers["X-Sql-Calls"] = sql[:calls].to_s headers["X-Sql-Time"] = "%0.6f" % sql[:duration] end - if queue = env['REQUEST_QUEUE_SECONDS'] + if queue = env["REQUEST_QUEUE_SECONDS"] headers["X-Queue-Time"] = "%0.6f" % queue end end end if env[Auth::DefaultCurrentUserProvider::BAD_TOKEN] && (headers = result[1]) - headers['Discourse-Logged-Out'] = '1' + headers["Discourse-Logged-Out"] = "1" end result ensure - if (limiters = env['DISCOURSE_RATE_LIMITERS']) && env['DISCOURSE_IS_ASSET_PATH'] + if (limiters = env["DISCOURSE_RATE_LIMITERS"]) && env["DISCOURSE_IS_ASSET_PATH"] limiters.each(&:rollback!) - env['DISCOURSE_ASSET_RATE_LIMITERS'].each do |limiter| + env["DISCOURSE_ASSET_RATE_LIMITERS"].each do |limiter| begin limiter.performed! rescue RateLimiter::LimitExceeded @@ -244,25 +263,19 @@ class Middleware::RequestTracker end end end - if !env["discourse.request_tracker.skip"] - log_request_info(env, result, info, request) - end + log_request_info(env, result, info, request) if !env["discourse.request_tracker.skip"] end def log_later(data) Scheduler::Defer.later("Track view") do - unless Discourse.pg_readonly_mode? - self.class.log_request(data) - end + self.class.log_request(data) unless Discourse.pg_readonly_mode? end end def find_auth_cookie(env) min_allowed_timestamp = Time.now.to_i - (UserAuthToken::ROTATE_TIME_MINS + 1) * 60 cookie = Auth::DefaultCurrentUserProvider.find_v1_auth_cookie(env) - if cookie && cookie[:issued_at] >= min_allowed_timestamp - cookie - end + cookie if cookie && cookie[:issued_at] >= min_allowed_timestamp end def is_private_ip?(ip) @@ -273,10 +286,12 @@ class Middleware::RequestTracker end def rate_limit(request, cookie) - warn = GlobalSetting.max_reqs_per_ip_mode == "warn" || - GlobalSetting.max_reqs_per_ip_mode == "warn+block" - block = GlobalSetting.max_reqs_per_ip_mode == "block" || - GlobalSetting.max_reqs_per_ip_mode == "warn+block" + warn = + GlobalSetting.max_reqs_per_ip_mode == "warn" || + GlobalSetting.max_reqs_per_ip_mode == "warn+block" + block = + GlobalSetting.max_reqs_per_ip_mode == "block" || + GlobalSetting.max_reqs_per_ip_mode == "warn+block" return if !block && !warn @@ -291,54 +306,56 @@ class Middleware::RequestTracker ip_or_id = ip limit_on_id = false - if cookie && cookie[:user_id] && cookie[:trust_level] && cookie[:trust_level] >= GlobalSetting.skip_per_ip_rate_limit_trust_level + if cookie && cookie[:user_id] && cookie[:trust_level] && + cookie[:trust_level] >= GlobalSetting.skip_per_ip_rate_limit_trust_level ip_or_id = cookie[:user_id] limit_on_id = true end - limiter10 = RateLimiter.new( - nil, - "global_ip_limit_10_#{ip_or_id}", - GlobalSetting.max_reqs_per_ip_per_10_seconds, - 10, - global: !limit_on_id, - aggressive: true, - error_code: limit_on_id ? "id_10_secs_limit" : "ip_10_secs_limit" - ) + limiter10 = + RateLimiter.new( + nil, + "global_ip_limit_10_#{ip_or_id}", + GlobalSetting.max_reqs_per_ip_per_10_seconds, + 10, + global: !limit_on_id, + aggressive: true, + error_code: limit_on_id ? "id_10_secs_limit" : "ip_10_secs_limit", + ) - limiter60 = RateLimiter.new( - nil, - "global_ip_limit_60_#{ip_or_id}", - GlobalSetting.max_reqs_per_ip_per_minute, - 60, - global: !limit_on_id, - error_code: limit_on_id ? "id_60_secs_limit" : "ip_60_secs_limit", - aggressive: true - ) + limiter60 = + RateLimiter.new( + nil, + "global_ip_limit_60_#{ip_or_id}", + GlobalSetting.max_reqs_per_ip_per_minute, + 60, + global: !limit_on_id, + error_code: limit_on_id ? "id_60_secs_limit" : "ip_60_secs_limit", + aggressive: true, + ) - limiter_assets10 = RateLimiter.new( - nil, - "global_ip_limit_10_assets_#{ip_or_id}", - GlobalSetting.max_asset_reqs_per_ip_per_10_seconds, - 10, - error_code: limit_on_id ? "id_assets_10_secs_limit" : "ip_assets_10_secs_limit", - global: !limit_on_id - ) + limiter_assets10 = + RateLimiter.new( + nil, + "global_ip_limit_10_assets_#{ip_or_id}", + GlobalSetting.max_asset_reqs_per_ip_per_10_seconds, + 10, + error_code: limit_on_id ? "id_assets_10_secs_limit" : "ip_assets_10_secs_limit", + global: !limit_on_id, + ) - request.env['DISCOURSE_RATE_LIMITERS'] = [limiter10, limiter60] - request.env['DISCOURSE_ASSET_RATE_LIMITERS'] = [limiter_assets10] + request.env["DISCOURSE_RATE_LIMITERS"] = [limiter10, limiter60] + request.env["DISCOURSE_ASSET_RATE_LIMITERS"] = [limiter_assets10] if !limiter_assets10.can_perform? if warn - Discourse.warn("Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", uri: request.env["REQUEST_URI"]) + Discourse.warn( + "Global asset IP rate limit exceeded for #{ip}: 10 second rate limit", + uri: request.env["REQUEST_URI"], + ) end - if block - return [ - limiter_assets10.seconds_to_wait(Time.now.to_i), - limiter_assets10.error_code - ] - end + return limiter_assets10.seconds_to_wait(Time.now.to_i), limiter_assets10.error_code if block end begin @@ -351,7 +368,10 @@ class Middleware::RequestTracker nil rescue RateLimiter::LimitExceeded => e if warn - Discourse.warn("Global IP rate limit exceeded for #{ip}: #{type} second rate limit", uri: request.env["REQUEST_URI"]) + Discourse.warn( + "Global IP rate limit exceeded for #{ip}: #{type} second rate limit", + uri: request.env["REQUEST_URI"], + ) end if block [e.available_in, e.error_code] diff --git a/lib/middleware/turbo_dev.rb b/lib/middleware/turbo_dev.rb index 53e81cb50d..8754508caf 100644 --- a/lib/middleware/turbo_dev.rb +++ b/lib/middleware/turbo_dev.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true module Middleware - # Cheat and bypass Rails in development mode if the client attempts to download a static asset # that's already been downloaded. # @@ -19,22 +18,19 @@ module Middleware def call(env) root = "#{GlobalSetting.relative_url_root}/assets/" - is_asset = env['REQUEST_PATH'] && env['REQUEST_PATH'].starts_with?(root) + is_asset = env["REQUEST_PATH"] && env["REQUEST_PATH"].starts_with?(root) # hack to bypass all middleware if serving assets, a lot faster 4.5 seconds -> 1.5 seconds - if (etag = env['HTTP_IF_NONE_MATCH']) && is_asset - name = env['REQUEST_PATH'][(root.length)..-1] + if (etag = env["HTTP_IF_NONE_MATCH"]) && is_asset + name = env["REQUEST_PATH"][(root.length)..-1] etag = etag.gsub "\"", "" asset = Rails.application.assets.find_asset(name) - if asset && asset.digest == etag - return [304, {}, []] - end + return 304, {}, [] if asset && asset.digest == etag end status, headers, response = @app.call(env) - headers['Cache-Control'] = 'no-cache' if is_asset + headers["Cache-Control"] = "no-cache" if is_asset [status, headers, response] end end - end diff --git a/lib/migration/base_dropper.rb b/lib/migration/base_dropper.rb index 9aaf77e138..abf7a39a73 100644 --- a/lib/migration/base_dropper.rb +++ b/lib/migration/base_dropper.rb @@ -9,9 +9,14 @@ module Migration CREATE SCHEMA IF NOT EXISTS #{FUNCTION_SCHEMA_NAME}; SQL - message = column_name ? - "Discourse: #{column_name} in #{table_name} is readonly" : - "Discourse: #{table_name} is read only" + message = + ( + if column_name + "Discourse: #{column_name} in #{table_name} is readonly" + else + "Discourse: #{table_name} is read only" + end + ) DB.exec <<~SQL CREATE OR REPLACE FUNCTION #{readonly_function_name(table_name, column_name)} RETURNS trigger AS $rcr$ @@ -27,12 +32,7 @@ module Migration end def self.readonly_function_name(table_name, column_name = nil, with_schema: true) - function_name = [ - "raise", - table_name, - column_name, - "readonly()" - ].compact.join("_") + function_name = ["raise", table_name, column_name, "readonly()"].compact.join("_") if with_schema && function_schema_exists? "#{FUNCTION_SCHEMA_NAME}.#{function_name}" @@ -42,9 +42,7 @@ module Migration end def self.old_readonly_function_name(table_name, column_name = nil) - readonly_function_name(table_name, column_name).sub( - "#{FUNCTION_SCHEMA_NAME}.", '' - ) + readonly_function_name(table_name, column_name).sub("#{FUNCTION_SCHEMA_NAME}.", "") end def self.readonly_trigger_name(table_name, column_name = nil) @@ -52,7 +50,7 @@ module Migration end def self.function_schema_exists? - DB.exec(<<~SQL).to_s == '1' + DB.exec(<<~SQL).to_s == "1" SELECT schema_name FROM information_schema.schemata WHERE schema_name = '#{FUNCTION_SCHEMA_NAME}' diff --git a/lib/migration/column_dropper.rb b/lib/migration/column_dropper.rb index 71e472baa4..83d2feec64 100644 --- a/lib/migration/column_dropper.rb +++ b/lib/migration/column_dropper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/base_dropper' +require "migration/base_dropper" module Migration class ColumnDropper @@ -32,7 +32,9 @@ module Migration BaseDropper.drop_readonly_function(table_name, column_name) # Backward compatibility for old functions created in the public schema - DB.exec("DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table_name, column_name)} CASCADE") + DB.exec( + "DROP FUNCTION IF EXISTS #{BaseDropper.old_readonly_function_name(table_name, column_name)} CASCADE", + ) end end end diff --git a/lib/migration/safe_migrate.rb b/lib/migration/safe_migrate.rb index 0c8105e559..ce3013300e 100644 --- a/lib/migration/safe_migrate.rb +++ b/lib/migration/safe_migrate.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true -module Migration; end +module Migration +end -class Discourse::InvalidMigration < StandardError; end +class Discourse::InvalidMigration < StandardError +end class Migration::SafeMigrate module SafeMigration @@ -17,11 +19,9 @@ class Migration::SafeMigrate end def migrate(direction) - if direction == :up && - version && version > Migration::SafeMigrate.earliest_post_deploy_version && - @@enable_safe != false && - !is_post_deploy_migration? - + if direction == :up && version && + version > Migration::SafeMigrate.earliest_post_deploy_version && + @@enable_safe != false && !is_post_deploy_migration? Migration::SafeMigrate.enable! end @@ -44,9 +44,7 @@ class Migration::SafeMigrate return false if !method - self.method(method).source_location.first.include?( - Discourse::DB_POST_MIGRATE_PATH - ) + self.method(method).source_location.first.include?(Discourse::DB_POST_MIGRATE_PATH) end end @@ -76,7 +74,7 @@ class Migration::SafeMigrate def self.enable! return if PG::Connection.method_defined?(:exec_migrator_unpatched) - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" PG::Connection.class_eval do alias_method :exec_migrator_unpatched, :exec @@ -96,7 +94,7 @@ class Migration::SafeMigrate def self.disable! return if !PG::Connection.method_defined?(:exec_migrator_unpatched) - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" PG::Connection.class_eval do alias_method :exec, :exec_migrator_unpatched @@ -108,11 +106,9 @@ class Migration::SafeMigrate end def self.patch_active_record! - return if ENV['RAILS_ENV'] == "production" + return if ENV["RAILS_ENV"] == "production" - ActiveSupport.on_load(:active_record) do - ActiveRecord::Migration.prepend(SafeMigration) - end + ActiveSupport.on_load(:active_record) { ActiveRecord::Migration.prepend(SafeMigration) } if defined?(ActiveRecord::Tasks::DatabaseTasks) ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(NiceErrors) @@ -154,10 +150,11 @@ class Migration::SafeMigrate end def self.earliest_post_deploy_version - @@earliest_post_deploy_version ||= begin - first_file = Dir.glob("#{Discourse::DB_POST_MIGRATE_PATH}/*.rb").sort.first - file_name = File.basename(first_file, ".rb") - file_name.first(14).to_i - end + @@earliest_post_deploy_version ||= + begin + first_file = Dir.glob("#{Discourse::DB_POST_MIGRATE_PATH}/*.rb").sort.first + file_name = File.basename(first_file, ".rb") + file_name.first(14).to_i + end end end diff --git a/lib/migration/table_dropper.rb b/lib/migration/table_dropper.rb index bbd27f02e5..f2f9849cb9 100644 --- a/lib/migration/table_dropper.rb +++ b/lib/migration/table_dropper.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'migration/base_dropper' +require "migration/base_dropper" module Migration class Migration::TableDropper diff --git a/lib/mini_sql_multisite_connection.rb b/lib/mini_sql_multisite_connection.rb index 26ad1d5026..2b8ef82066 100644 --- a/lib/mini_sql_multisite_connection.rb +++ b/lib/mini_sql_multisite_connection.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection - class CustomBuilder < MiniSql::Builder - def initialize(connection, sql) super end - def secure_category(secure_category_ids, category_alias = 'c') + def secure_category(secure_category_ids, category_alias = "c") if secure_category_ids.present? - where("NOT COALESCE(#{category_alias}.read_restricted, false) OR #{category_alias}.id in (:secure_category_ids)", secure_category_ids: secure_category_ids) + where( + "NOT COALESCE(#{category_alias}.read_restricted, false) OR #{category_alias}.id in (:secure_category_ids)", + secure_category_ids: secure_category_ids, + ) else where("NOT COALESCE(#{category_alias}.read_restricted, false)") end @@ -40,8 +41,10 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection end end - def before_committed!(*); end - def rolledback!(*); end + def before_committed!(*) + end + def rolledback!(*) + end def trigger_transactional_callbacks? true end @@ -67,9 +70,7 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection def after_commit(&blk) return blk.call if !transaction_open? - ActiveRecord::Base.connection.add_transaction_record( - AfterCommitWrapper.new(&blk) - ) + ActiveRecord::Base.connection.add_transaction_record(AfterCommitWrapper.new(&blk)) end def self.instance @@ -107,5 +108,4 @@ class MiniSqlMultisiteConnection < MiniSql::ActiveRecordPostgres::Connection query end end - end diff --git a/lib/mobile_detection.rb b/lib/mobile_detection.rb index 0fb621dd37..1ff3361a15 100644 --- a/lib/mobile_detection.rb +++ b/lib/mobile_detection.rb @@ -10,10 +10,11 @@ module MobileDetection return false unless SiteSetting.enable_mobile_theme session[:mobile_view] = params[:mobile_view] if params && params.has_key?(:mobile_view) - session[:mobile_view] = nil if params && params.has_key?(:mobile_view) && params[:mobile_view] == 'auto' + session[:mobile_view] = nil if params && params.has_key?(:mobile_view) && + params[:mobile_view] == "auto" if session && session[:mobile_view] - session[:mobile_view] == '1' + session[:mobile_view] == "1" else mobile_device?(user_agent) end @@ -23,7 +24,8 @@ module MobileDetection user_agent =~ /iPad|iPhone|iPod/ end - MODERN_MOBILE_REGEX = %r{ + MODERN_MOBILE_REGEX = + %r{ \(.*iPhone\ OS\ 1[3-9].*\)| \(.*iPad.*OS\ 1[3-9].*\)| Chrome\/8[89]| @@ -37,5 +39,4 @@ module MobileDetection def self.modern_mobile_device?(user_agent) user_agent.match?(MODERN_MOBILE_REGEX) end - end diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 777d93bdb0..3903c5dd53 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -6,7 +6,6 @@ # with `NewPostManager.add_handler` to take other approaches depending # on the user or input. class NewPostManager - attr_reader :user, :args def self.sorted_handlers @@ -38,24 +37,21 @@ class NewPostManager user = manager.user args = manager.args - !!( - args[:first_post_checks] && - user.post_count == 0 && - user.topic_count == 0 - ) + !!(args[:first_post_checks] && user.post_count == 0 && user.topic_count == 0) end def self.is_fast_typer?(manager) args = manager.args is_first_post?(manager) && - args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time && - SiteSetting.auto_silence_fast_typers_on_first_post && - manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level + args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time && + SiteSetting.auto_silence_fast_typers_on_first_post && + manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level end def self.auto_silence?(manager) - is_first_post?(manager) && WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").should_silence? + is_first_post?(manager) && + WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").should_silence? end def self.matches_auto_silence_regex?(manager) @@ -74,7 +70,6 @@ class NewPostManager end "#{args[:title]} #{args[:raw]}" =~ regex - end def self.exempt_user?(user) @@ -90,19 +85,25 @@ class NewPostManager return :email_spam if manager.args[:email_spam] - return :post_count if ( - user.trust_level <= TrustLevel.levels[:basic] && - (user.post_count + user.topic_count) < SiteSetting.approve_post_count - ) + if ( + user.trust_level <= TrustLevel.levels[:basic] && + (user.post_count + user.topic_count) < SiteSetting.approve_post_count + ) + return :post_count + end return :trust_level if user.trust_level < SiteSetting.approve_unless_trust_level.to_i - return :new_topics_unless_trust_level if ( - manager.args[:title].present? && - user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i - ) + if ( + manager.args[:title].present? && + user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i + ) + return :new_topics_unless_trust_level + end - return :watched_word if WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval? + if WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval? + return :watched_word + end return :fast_typer if is_fast_typer?(manager) @@ -112,10 +113,12 @@ class NewPostManager return :category if post_needs_approval_in_its_category?(manager) - return :contains_media if ( - manager.args[:image_sizes].present? && - user.trust_level < SiteSetting.review_media_unless_trust_level.to_i - ) + if ( + manager.args[:image_sizes].present? && + user.trust_level < SiteSetting.review_media_unless_trust_level.to_i + ) + return :contains_media + end :skip end @@ -136,7 +139,6 @@ class NewPostManager end def self.default_handler(manager) - reason = post_needs_approval?(manager) return if reason == :skip @@ -171,11 +173,26 @@ class NewPostManager I18n.with_locale(SiteSetting.default_locale) do if is_fast_typer?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.new_user_typed_too_fast")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.new_user_typed_too_fast"), + ) elsif auto_silence?(manager) || matches_auto_silence_regex?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.content_matches_auto_silence_regex")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.content_matches_auto_silence_regex"), + ) elsif reason == :email_spam && is_first_post?(manager) - UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.email_in_spam_header")) + UserSilencer.silence( + manager.user, + Discourse.system_user, + keep_posts: true, + reason: I18n.t("user.email_in_spam_header"), + ) end end @@ -183,12 +200,10 @@ class NewPostManager end def self.queue_enabled? - SiteSetting.approve_post_count > 0 || - SiteSetting.approve_unless_trust_level.to_i > 0 || - SiteSetting.approve_new_topics_unless_trust_level.to_i > 0 || - SiteSetting.approve_unless_staged || - WordWatcher.words_for_action_exists?(:require_approval) || - handlers.size > 1 + SiteSetting.approve_post_count > 0 || SiteSetting.approve_unless_trust_level.to_i > 0 || + SiteSetting.approve_new_topics_unless_trust_level.to_i > 0 || + SiteSetting.approve_unless_staged || + WordWatcher.words_for_action_exists?(:require_approval) || handlers.size > 1 end def initialize(user, args) @@ -197,14 +212,15 @@ class NewPostManager end def perform - if !self.class.exempt_user?(@user) && matches = WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?.presence + if !self.class.exempt_user?(@user) && + matches = WordWatcher.new("#{@args[:title]} #{@args[:raw]}").should_block?.presence result = NewPostResult.new(:created_post, false) if matches.size == 1 - key = 'contains_blocked_word' + key = "contains_blocked_word" translation_args = { word: CGI.escapeHTML(matches[0]) } else - key = 'contains_blocked_words' - translation_args = { words: CGI.escapeHTML(matches.join(', ')) } + key = "contains_blocked_words" + translation_args = { words: CGI.escapeHTML(matches.join(", ")) } end result.errors.add(:base, I18n.t(key, translation_args)) return result @@ -217,8 +233,13 @@ class NewPostManager end # We never queue private messages - return perform_create_post if @args[:archetype] == Archetype.private_message || - (args[:topic_id] && Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists?) + if @args[:archetype] == Archetype.private_message || + ( + args[:topic_id] && + Topic.where(id: args[:topic_id], archetype: Archetype.private_message).exists? + ) + return perform_create_post + end NewPostManager.default_handler(self) || perform_create_post end @@ -226,11 +247,8 @@ class NewPostManager # Enqueue this post def enqueue(reason = nil) result = NewPostResult.new(:enqueued) - payload = { - raw: @args[:raw], - tags: @args[:tags] - } - %w(typing_duration_msecs composer_open_duration_msecs reply_to_post_number).each do |a| + payload = { raw: @args[:raw], tags: @args[:tags] } + %w[typing_duration_msecs composer_open_duration_msecs reply_to_post_number].each do |a| payload[a] = @args[a].to_i if @args[a] end @@ -239,21 +257,27 @@ class NewPostManager payload[:via_email] = true if !!@args[:via_email] payload[:raw_email] = @args[:raw_email] if @args[:raw_email].present? - reviewable = ReviewableQueuedPost.new( - created_by: @user, - payload: payload, - topic_id: @args[:topic_id], - reviewable_by_moderator: true - ) - reviewable.payload['title'] = @args[:title] if @args[:title].present? + reviewable = + ReviewableQueuedPost.new( + created_by: @user, + payload: payload, + topic_id: @args[:topic_id], + reviewable_by_moderator: true, + ) + reviewable.payload["title"] = @args[:title] if @args[:title].present? reviewable.category_id = args[:category] if args[:category].present? reviewable.created_new! create_options = reviewable.create_options - creator = @args[:topic_id] ? - PostCreator.new(@user, create_options) : - TopicCreator.new(@user, Guardian.new(@user), create_options) + creator = + ( + if @args[:topic_id] + PostCreator.new(@user, create_options) + else + TopicCreator.new(@user, Guardian.new(@user), create_options) + end + ) errors = Set.new creator.valid? @@ -265,7 +289,7 @@ class NewPostManager Discourse.system_user, ReviewableScore.types[:needs_approval], reason: reason, - force_review: true + force_review: true, ) else reviewable.errors.full_messages.each { |msg| errors << msg } @@ -293,5 +317,4 @@ class NewPostManager result end - end diff --git a/lib/new_post_result.rb b/lib/new_post_result.rb index 4fa50b2486..861fcc5ff9 100644 --- a/lib/new_post_result.rb +++ b/lib/new_post_result.rb @@ -37,7 +37,7 @@ class NewPostResult Discourse.deprecate( "NewPostManager#queued_post is deprecated. Please use #reviewable instead.", output_in_test: true, - drop_from: '2.9.0', + drop_from: "2.9.0", ) reviewable @@ -50,5 +50,4 @@ class NewPostResult def failed? !@success end - end diff --git a/lib/notification_levels.rb b/lib/notification_levels.rb index a949950664..9b43e6be70 100644 --- a/lib/notification_levels.rb +++ b/lib/notification_levels.rb @@ -2,19 +2,25 @@ module NotificationLevels def self.all - @all_levels ||= Enum.new(muted: 0, - regular: 1, - normal: 1, # alias for regular - tracking: 2, - watching: 3, - watching_first_post: 4) + @all_levels ||= + Enum.new( + muted: 0, + regular: 1, + normal: 1, # alias for regular + tracking: 2, + watching: 3, + watching_first_post: 4, + ) end def self.topic_levels - @topic_levels ||= Enum.new(muted: 0, - regular: 1, - normal: 1, # alias for regular - tracking: 2, - watching: 3) + @topic_levels ||= + Enum.new( + muted: 0, + regular: 1, + normal: 1, # alias for regular + tracking: 2, + watching: 3, + ) end end diff --git a/lib/onebox.rb b/lib/onebox.rb index e6e0eed187..feee7c8e6d 100644 --- a/lib/onebox.rb +++ b/lib/onebox.rb @@ -19,9 +19,9 @@ module Onebox max_download_kb: (10 * 1024), # 10MB load_paths: [File.join(Rails.root, "lib/onebox/templates")], allowed_ports: [80, 443], - allowed_schemes: ["http", "https"], + allowed_schemes: %w[http https], sanitize_config: SanitizeConfig::ONEBOX, - redirect_limit: 5 + redirect_limit: 5, } @@options = DEFAULTS diff --git a/lib/onebox/domain_checker.rb b/lib/onebox/domain_checker.rb index 1dd810491e..6e6c99c3e3 100644 --- a/lib/onebox/domain_checker.rb +++ b/lib/onebox/domain_checker.rb @@ -3,9 +3,10 @@ module Onebox class DomainChecker def self.is_blocked?(hostname) - SiteSetting.blocked_onebox_domains&.split('|').any? do |blocked| - hostname == blocked || hostname.end_with?(".#{blocked}") - end + SiteSetting + .blocked_onebox_domains + &.split("|") + .any? { |blocked| hostname == blocked || hostname.end_with?(".#{blocked}") } end end end diff --git a/lib/onebox/engine.rb b/lib/onebox/engine.rb index e28807c988..838986e685 100644 --- a/lib/onebox/engine.rb +++ b/lib/onebox/engine.rb @@ -7,9 +7,7 @@ module Onebox end def self.engines - constants.select do |constant| - constant.to_s =~ /Onebox$/ - end.sort.map(&method(:const_get)) + constants.select { |constant| constant.to_s =~ /Onebox$/ }.sort.map(&method(:const_get)) end def self.all_iframe_origins @@ -25,7 +23,7 @@ module Onebox escaped_origin = escaped_origin.sub("\\*", '\S*') end - Regexp.new("\\A#{escaped_origin}", 'i') + Regexp.new("\\A#{escaped_origin}", "i") end end @@ -50,7 +48,7 @@ module Onebox @url = url @uri = URI(url) if always_https? - @uri.scheme = 'https' + @uri.scheme = "https" @url = @uri.to_s end @timeout = timeout || Onebox.options.timeout diff --git a/lib/onebox/engine/allowlisted_generic_onebox.rb b/lib/onebox/engine/allowlisted_generic_onebox.rb index aa25dd54a6..3c8a2359eb 100644 --- a/lib/onebox/engine/allowlisted_generic_onebox.rb +++ b/lib/onebox/engine/allowlisted_generic_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'htmlentities' +require "htmlentities" require "ipaddr" module Onebox @@ -18,7 +18,7 @@ module Onebox # include the entire page HTML. However for some providers like Flickr it allows us # to return gifv and galleries. def self.default_html_providers - ['Flickr', 'Meetup'] + %w[Flickr Meetup] end def self.html_providers @@ -39,23 +39,33 @@ module Onebox end def self.https_hosts - %w(slideshare.net dailymotion.com livestream.com imgur.com flickr.com) + %w[slideshare.net dailymotion.com livestream.com imgur.com flickr.com] end def self.article_html_hosts - %w(imdb.com) + %w[imdb.com] end def self.host_matches(uri, list) - !!list.find { |h| %r((^|\.)#{Regexp.escape(h)}$).match(uri.host) } + !!list.find { |h| /(^|\.)#{Regexp.escape(h)}$/.match(uri.host) } end def self.allowed_twitter_labels - ['brand', 'price', 'usd', 'cad', 'reading time', 'likes'] + ["brand", "price", "usd", "cad", "reading time", "likes"] end def self.===(other) - other.is_a?(URI) ? (IPAddr.new(other.hostname) rescue nil).nil? : true + if other.is_a?(URI) + ( + begin + IPAddr.new(other.hostname) + rescue StandardError + nil + end + ).nil? + else + true + end end def to_html @@ -65,8 +75,12 @@ module Onebox def placeholder_html return article_html if (is_article? || force_article_html?) return image_html if is_image? - return Onebox::Helpers.video_placeholder_html if !SiteSetting.enable_diffhtml_preview? && (is_video? || is_card?) - return Onebox::Helpers.generic_placeholder_html if !SiteSetting.enable_diffhtml_preview? && is_embedded? + if !SiteSetting.enable_diffhtml_preview? && (is_video? || is_card?) + return Onebox::Helpers.video_placeholder_html + end + if !SiteSetting.enable_diffhtml_preview? && is_embedded? + return Onebox::Helpers.generic_placeholder_html + end to_html end @@ -75,72 +89,90 @@ module Onebox end def data - @data ||= begin - html_entities = HTMLEntities.new - d = { link: link }.merge(raw) + @data ||= + begin + html_entities = HTMLEntities.new + d = { link: link }.merge(raw) - if !Onebox::Helpers.blank?(d[:title]) - d[:title] = html_entities.decode(Onebox::Helpers.truncate(d[:title], 80)) - end - - d[:description] ||= d[:summary] - if !Onebox::Helpers.blank?(d[:description]) - d[:description] = html_entities.decode(Onebox::Helpers.truncate(d[:description], 250)) - end - - if !Onebox::Helpers.blank?(d[:site_name]) - d[:domain] = html_entities.decode(Onebox::Helpers.truncate(d[:site_name], 80)) - elsif !Onebox::Helpers.blank?(d[:domain]) - d[:domain] = "http://#{d[:domain]}" unless d[:domain] =~ /^https?:\/\// - d[:domain] = URI(d[:domain]).host.to_s.sub(/^www\./, '') rescue nil - end - - # prefer secure URLs - d[:image] = d[:image_secure_url] || d[:image_url] || d[:thumbnail_url] || d[:image] - d[:image] = Onebox::Helpers::get_absolute_image_url(d[:image], @url) - d[:image] = Onebox::Helpers::normalize_url_for_output(html_entities.decode(d[:image])) - d[:image] = nil if Onebox::Helpers.blank?(d[:image]) - - d[:video] = d[:video_secure_url] || d[:video_url] || d[:video] - d[:video] = nil if Onebox::Helpers.blank?(d[:video]) - - d[:published_time] = d[:article_published_time] unless Onebox::Helpers.blank?(d[:article_published_time]) - if !Onebox::Helpers.blank?(d[:published_time]) - d[:article_published_time] = Time.parse(d[:published_time]).strftime("%-d %b %y") - d[:article_published_time_title] = Time.parse(d[:published_time]).strftime("%I:%M%p - %d %B %Y") - end - - # Twitter labels - if !Onebox::Helpers.blank?(d[:label1]) && !Onebox::Helpers.blank?(d[:data1]) && !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| d[:label1] =~ /#{l}/i } - d[:label_1] = Onebox::Helpers.truncate(d[:label1]) - d[:data_1] = Onebox::Helpers.truncate(d[:data1]) - end - if !Onebox::Helpers.blank?(d[:label2]) && !Onebox::Helpers.blank?(d[:data2]) && !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| d[:label2] =~ /#{l}/i } - unless Onebox::Helpers.blank?(d[:label_1]) - d[:label_2] = Onebox::Helpers.truncate(d[:label2]) - d[:data_2] = Onebox::Helpers.truncate(d[:data2]) - else - d[:label_1] = Onebox::Helpers.truncate(d[:label2]) - d[:data_1] = Onebox::Helpers.truncate(d[:data2]) + if !Onebox::Helpers.blank?(d[:title]) + d[:title] = html_entities.decode(Onebox::Helpers.truncate(d[:title], 80)) end - end - if Onebox::Helpers.blank?(d[:label_1]) && !Onebox::Helpers.blank?(d[:price_amount]) && !Onebox::Helpers.blank?(d[:price_currency]) - d[:label_1] = "Price" - d[:data_1] = Onebox::Helpers.truncate("#{d[:price_currency].strip} #{d[:price_amount].strip}") - end - - skip_missing_tags = [:video] - d.each do |k, v| - next if skip_missing_tags.include?(k) - if v == nil || v == '' - errors[k] ||= [] - errors[k] << 'is blank' + d[:description] ||= d[:summary] + if !Onebox::Helpers.blank?(d[:description]) + d[:description] = html_entities.decode(Onebox::Helpers.truncate(d[:description], 250)) end - end - d - end + if !Onebox::Helpers.blank?(d[:site_name]) + d[:domain] = html_entities.decode(Onebox::Helpers.truncate(d[:site_name], 80)) + elsif !Onebox::Helpers.blank?(d[:domain]) + d[:domain] = "http://#{d[:domain]}" unless d[:domain] =~ %r{^https?://} + d[:domain] = begin + URI(d[:domain]).host.to_s.sub(/^www\./, "") + rescue StandardError + nil + end + end + + # prefer secure URLs + d[:image] = d[:image_secure_url] || d[:image_url] || d[:thumbnail_url] || d[:image] + d[:image] = Onebox::Helpers.get_absolute_image_url(d[:image], @url) + d[:image] = Onebox::Helpers.normalize_url_for_output(html_entities.decode(d[:image])) + d[:image] = nil if Onebox::Helpers.blank?(d[:image]) + + d[:video] = d[:video_secure_url] || d[:video_url] || d[:video] + d[:video] = nil if Onebox::Helpers.blank?(d[:video]) + + d[:published_time] = d[:article_published_time] unless Onebox::Helpers.blank?( + d[:article_published_time], + ) + if !Onebox::Helpers.blank?(d[:published_time]) + d[:article_published_time] = Time.parse(d[:published_time]).strftime("%-d %b %y") + d[:article_published_time_title] = Time.parse(d[:published_time]).strftime( + "%I:%M%p - %d %B %Y", + ) + end + + # Twitter labels + if !Onebox::Helpers.blank?(d[:label1]) && !Onebox::Helpers.blank?(d[:data1]) && + !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| + d[:label1] =~ /#{l}/i + } + d[:label_1] = Onebox::Helpers.truncate(d[:label1]) + d[:data_1] = Onebox::Helpers.truncate(d[:data1]) + end + if !Onebox::Helpers.blank?(d[:label2]) && !Onebox::Helpers.blank?(d[:data2]) && + !!AllowlistedGenericOnebox.allowed_twitter_labels.find { |l| + d[:label2] =~ /#{l}/i + } + unless Onebox::Helpers.blank?(d[:label_1]) + d[:label_2] = Onebox::Helpers.truncate(d[:label2]) + d[:data_2] = Onebox::Helpers.truncate(d[:data2]) + else + d[:label_1] = Onebox::Helpers.truncate(d[:label2]) + d[:data_1] = Onebox::Helpers.truncate(d[:data2]) + end + end + + if Onebox::Helpers.blank?(d[:label_1]) && !Onebox::Helpers.blank?(d[:price_amount]) && + !Onebox::Helpers.blank?(d[:price_currency]) + d[:label_1] = "Price" + d[:data_1] = Onebox::Helpers.truncate( + "#{d[:price_currency].strip} #{d[:price_amount].strip}", + ) + end + + skip_missing_tags = [:video] + d.each do |k, v| + next if skip_missing_tags.include?(k) + if v == nil || v == "" + errors[k] ||= [] + errors[k] << "is blank" + end + end + + d + end end private @@ -154,23 +186,21 @@ module Onebox end def generic_html - return article_html if (is_article? || force_article_html?) - return video_html if is_video? - return image_html if is_image? + return article_html if (is_article? || force_article_html?) + return video_html if is_video? + return image_html if is_image? return embedded_html if is_embedded? - return card_html if is_card? - return article_html if (has_text? || is_image_article?) + return card_html if is_card? + return article_html if (has_text? || is_image_article?) end def is_card? - data[:card] == 'player' && - data[:player] =~ URI::regexp && + data[:card] == "player" && data[:player] =~ URI.regexp && options[:allowed_iframe_regexes]&.any? { |r| data[:player] =~ r } end def is_article? - (data[:type] =~ /article/ || data[:asset_type] =~ /article/) && - has_text? + (data[:type] =~ /article/ || data[:asset_type] =~ /article/) && has_text? end def has_text? @@ -186,9 +216,7 @@ module Onebox end def is_image? - data[:type] =~ /photo|image/ && - data[:type] !~ /photostream/ && - has_image? + data[:type] =~ /photo|image/ && data[:type] !~ /photostream/ && has_image? end def has_image? @@ -196,8 +224,7 @@ module Onebox end def is_video? - data[:type] =~ /^video[\/\.]/ && - data[:video_type] == "video/mp4" && # Many sites include 'videos' with text/html types (i.e. iframes) + data[:type] =~ %r{^video[/\.]} && data[:video_type] == "video/mp4" && # Many sites include 'videos' with text/html types (i.e. iframes) !Onebox::Helpers.blank?(data[:video]) end @@ -206,13 +233,14 @@ module Onebox return true if AllowlistedGenericOnebox.html_providers.include?(data[:provider_name]) return false unless data[:html]["iframe"] - fragment = Nokogiri::HTML5::fragment(data[:html]) - src = fragment.at_css('iframe')&.[]("src") + fragment = Nokogiri::HTML5.fragment(data[:html]) + src = fragment.at_css("iframe")&.[]("src") options[:allowed_iframe_regexes]&.any? { |r| src =~ r } end def force_article_html? - AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.article_html_hosts) && (has_text? || is_image_article?) + AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.article_html_hosts) && + (has_text? || is_image_article?) end def card_html @@ -237,8 +265,8 @@ module Onebox escaped_src = ::Onebox::Helpers.normalize_url_for_output(data[:image]) - alt = data[:description] || data[:title] - width = data[:image_width] || data[:thumbnail_width] || data[:width] + alt = data[:description] || data[:title] + width = data[:image_width] || data[:thumbnail_width] || data[:width] height = data[:image_height] || data[:thumbnail_height] || data[:height] "#{alt}" @@ -263,7 +291,7 @@ module Onebox end def embedded_html - fragment = Nokogiri::HTML5::fragment(data[:html]) + fragment = Nokogiri::HTML5.fragment(data[:html]) fragment.css("img").each { |img| img["class"] = "thumbnail" } if iframe = fragment.at_css("iframe") iframe.remove_attribute("style") diff --git a/lib/onebox/engine/amazon_onebox.rb b/lib/onebox/engine/amazon_onebox.rb index 9f1aca9f83..ae3ae9e36f 100644 --- a/lib/onebox/engine/amazon_onebox.rb +++ b/lib/onebox/engine/amazon_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'json' +require "json" require "onebox/open_graph" module Onebox @@ -11,7 +11,9 @@ module Onebox include HTML always_https - matches_regexp(/^https?:\/\/(?:www\.)?(?:smile\.)?(amazon|amzn)\.(?com|ca|de|it|es|fr|co\.jp|co\.uk|cn|in|com\.br|com\.mx|nl|pl|sa|sg|se|com\.tr|ae)\//) + matches_regexp( + %r{^https?://(?:www\.)?(?:smile\.)?(amazon|amzn)\.(?com|ca|de|it|es|fr|co\.jp|co\.uk|cn|in|com\.br|com\.mx|nl|pl|sa|sg|se|com\.tr|ae)/}, + ) def url @raw ||= nil @@ -29,7 +31,8 @@ module Onebox end if match && match[:id] - id = Addressable::URI.encode_component(match[:id], Addressable::URI::CharacterClasses::PATH) + id = + Addressable::URI.encode_component(match[:id], Addressable::URI::CharacterClasses::PATH) return "https://www.amazon.#{tld}/dp/#{id}" end @@ -41,15 +44,13 @@ module Onebox end def http_params - if @options && @options[:user_agent] - { 'User-Agent' => @options[:user_agent] } - end + { "User-Agent" => @options[:user_agent] } if @options && @options[:user_agent] end def to_html(ignore_errors = false) unless ignore_errors verified_data # forces a check for missing fields - return '' unless errors.empty? + return "" unless errors.empty? end super() @@ -60,19 +61,20 @@ module Onebox end def verified_data - @verified_data ||= begin - result = data + @verified_data ||= + begin + result = data - required_tags = [:title, :description] - required_tags.each do |tag| - if result[tag].blank? - errors[tag] ||= [] - errors[tag] << 'is blank' + required_tags = %i[title description] + required_tags.each do |tag| + if result[tag].blank? + errors[tag] ||= [] + errors[tag] << "is blank" + end end - end - result - end + result + end @verified_data end @@ -80,13 +82,13 @@ module Onebox private def has_cached_body - body_cacher&.respond_to?('cache_response_body?') && + body_cacher&.respond_to?("cache_response_body?") && body_cacher.cache_response_body?(uri.to_s) && body_cacher.cached_response_body_exists?(uri.to_s) end def match - @match ||= @url.match(/(?:d|g)p\/(?:product\/|video\/detail\/)?(?[A-Z0-9]+)(?:\/|\?|$)/mi) + @match ||= @url.match(%r{(?:d|g)p/(?:product/|video/detail/)?(?[A-Z0-9]+)(?:/|\?|$)}mi) end def image @@ -117,14 +119,16 @@ module Onebox def price # get item price (Amazon markup is inconsistent, deal with it) - if raw.css("#priceblock_ourprice .restOfPrice")[0] && raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text + if raw.css("#priceblock_ourprice .restOfPrice")[0] && + raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text "#{raw.css("#priceblock_ourprice .restOfPrice")[0].inner_text}#{raw.css("#priceblock_ourprice .buyingPrice")[0].inner_text}.#{raw.css("#priceblock_ourprice .restOfPrice")[1].inner_text}" - elsif raw.css("#priceblock_dealprice") && (dealprice = raw.css("#priceblock_dealprice span")[0]) + elsif raw.css("#priceblock_dealprice") && + (dealprice = raw.css("#priceblock_dealprice span")[0]) dealprice.inner_text elsif !raw.css("#priceblock_ourprice").inner_text.empty? raw.css("#priceblock_ourprice").inner_text else - result = raw.css('#corePrice_feature_div .a-price .a-offscreen').first&.inner_text + result = raw.css("#corePrice_feature_div .a-price .a-offscreen").first&.inner_text if result.blank? result = raw.css(".mediaMatrixListItem.a-active .a-color-price").inner_text end @@ -134,21 +138,30 @@ module Onebox end def multiple_authors(authors_xpath) - raw - .xpath(authors_xpath) - .map { |a| a.inner_text.strip } - .join(", ") + raw.xpath(authors_xpath).map { |a| a.inner_text.strip }.join(", ") end def data og = ::Onebox::OpenGraph.new(raw) - if raw.at_css('#dp.book_mobile') # printed books + if raw.at_css("#dp.book_mobile") # printed books title = raw.at("h1#title")&.inner_text - authors = raw.at_css('#byline_secondary_view_div') ? multiple_authors("//div[@id='byline_secondary_view_div']//span[@class='a-text-bold']") : raw.at("#byline")&.inner_text - rating = raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || raw.at("#cmrsArcLink .a-icon")&.inner_text + authors = + ( + if raw.at_css("#byline_secondary_view_div") + multiple_authors( + "//div[@id='byline_secondary_view_div']//span[@class='a-text-bold']", + ) + else + raw.at("#byline")&.inner_text + end + ) + rating = + raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || + raw.at("#cmrsArcLink .a-icon")&.inner_text - table_xpath = "//div[@id='productDetails_secondary_view_div']//table[@id='productDetails_techSpec_section_1']" + table_xpath = + "//div[@id='productDetails_secondary_view_div']//table[@id='productDetails_techSpec_section_1']" isbn = raw.xpath("#{table_xpath}//tr[8]//td").inner_text.strip # if ISBN is misplaced or absent it's hard to find out which data is @@ -167,18 +180,29 @@ module Onebox by_info: authors, image: og.image || image, description: raw.at("#productDescription")&.inner_text, - rating: "#{rating}#{', ' if rating && (!isbn&.empty? || !price&.empty?)}", + rating: "#{rating}#{", " if rating && (!isbn&.empty? || !price&.empty?)}", price: price, isbn_asin_text: "ISBN", isbn_asin: isbn, publisher: publisher, - published: "#{published}#{', ' if published && !price&.empty?}" + published: "#{published}#{", " if published && !price&.empty?}", } - - elsif raw.at_css('#dp.ebooks_mobile') # ebooks + elsif raw.at_css("#dp.ebooks_mobile") # ebooks title = raw.at("#ebooksTitle")&.inner_text - authors = raw.at_css('#a-popover-mobile-udp-contributor-popover-id') ? multiple_authors("//div[@id='a-popover-mobile-udp-contributor-popover-id']//span[contains(@class,'a-text-bold')]") : (raw.at("#byline")&.inner_text&.strip || raw.at("#bylineInfo")&.inner_text&.strip) - rating = raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || raw.at("#cmrsArcLink .a-icon")&.inner_text || raw.at("#acrCustomerReviewLink .a-icon")&.inner_text + authors = + ( + if raw.at_css("#a-popover-mobile-udp-contributor-popover-id") + multiple_authors( + "//div[@id='a-popover-mobile-udp-contributor-popover-id']//span[contains(@class,'a-text-bold')]", + ) + else + (raw.at("#byline")&.inner_text&.strip || raw.at("#bylineInfo")&.inner_text&.strip) + end + ) + rating = + raw.at("#averageCustomerReviews_feature_div .a-icon")&.inner_text || + raw.at("#cmrsArcLink .a-icon")&.inner_text || + raw.at("#acrCustomerReviewLink .a-icon")&.inner_text table_xpath = "//div[@id='detailBullets_secondary_view_div']//ul" asin = raw.xpath("#{table_xpath}//li[4]/span/span[2]").inner_text @@ -198,22 +222,16 @@ module Onebox by_info: authors, image: og.image || image, description: raw.at("#productDescription")&.inner_text, - rating: "#{rating}#{', ' if rating && (!asin&.empty? || !price&.empty?)}", + rating: "#{rating}#{", " if rating && (!asin&.empty? || !price&.empty?)}", price: price, isbn_asin_text: "ASIN", isbn_asin: asin, publisher: publisher, - published: "#{published}#{', ' if published && !price&.empty?}" + published: "#{published}#{", " if published && !price&.empty?}", } - else title = og.title || CGI.unescapeHTML(raw.css("title").inner_text) - result = { - link: url, - title: title, - image: og.image || image, - price: price - } + result = { link: url, title: title, image: og.image || image, price: price } result[:by_info] = raw.at("#by-line") result[:by_info] = Onebox::Helpers.clean(result[:by_info].inner_html) if result[:by_info] @@ -221,10 +239,10 @@ module Onebox summary = raw.at("#productDescription") description = og.description || summary&.inner_text&.strip - if description.blank? - description = raw.css("meta[name=description]").first&.[]("content") - end - result[:description] = CGI.unescapeHTML(Onebox::Helpers.truncate(description, 250)) if description + description = raw.css("meta[name=description]").first&.[]("content") if description.blank? + result[:description] = CGI.unescapeHTML( + Onebox::Helpers.truncate(description, 250), + ) if description end result[:price] = nil if result[:price].start_with?("$0") || result[:price] == 0 diff --git a/lib/onebox/engine/animated_image_onebox.rb b/lib/onebox/engine/animated_image_onebox.rb index 9dc8a5f484..a960ac5e61 100644 --- a/lib/onebox/engine/animated_image_onebox.rb +++ b/lib/onebox/engine/animated_image_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*(giphy\.com|gph\.is|tenor\.com)\//) + matches_regexp(%r{^https?://.*(giphy\.com|gph\.is|tenor\.com)/}) always_https def to_html diff --git a/lib/onebox/engine/audio_onebox.rb b/lib/onebox/engine/audio_onebox.rb index 8a5f52c4b0..8a41b100dd 100644 --- a/lib/onebox/engine/audio_onebox.rb +++ b/lib/onebox/engine/audio_onebox.rb @@ -5,7 +5,7 @@ module Onebox class AudioOnebox include Engine - matches_regexp(/^(https?:)?\/\/.*\.(mp3|ogg|opus|wav|m4a)(\?.*)?$/i) + matches_regexp(%r{^(https?:)?//.*\.(mp3|ogg|opus|wav|m4a)(\?.*)?$}i) def always_https? AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.https_hosts) diff --git a/lib/onebox/engine/audioboom_onebox.rb b/lib/onebox/engine/audioboom_onebox.rb index 89986f4685..daf26f6a83 100644 --- a/lib/onebox/engine/audioboom_onebox.rb +++ b/lib/onebox/engine/audioboom_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/audioboom\.com\/posts\/\d+/) + matches_regexp(%r{^https?://audioboom\.com/posts/\d+}) always_https def placeholder_html diff --git a/lib/onebox/engine/band_camp_onebox.rb b/lib/onebox/engine/band_camp_onebox.rb index a31e589032..937826ce9e 100644 --- a/lib/onebox/engine/band_camp_onebox.rb +++ b/lib/onebox/engine/band_camp_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*\.bandcamp\.com\/(album|track)\//) + matches_regexp(%r{^https?://.*\.bandcamp\.com/(album|track)/}) always_https requires_iframe_origins "https://bandcamp.com" diff --git a/lib/onebox/engine/cloud_app_onebox.rb b/lib/onebox/engine/cloud_app_onebox.rb index f1b985edf7..2c07638141 100644 --- a/lib/onebox/engine/cloud_app_onebox.rb +++ b/lib/onebox/engine/cloud_app_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/cl\.ly/) + matches_regexp(%r{^https?://cl\.ly}) always_https def to_html diff --git a/lib/onebox/engine/coub_onebox.rb b/lib/onebox/engine/coub_onebox.rb index 7e57e45429..5cac6c5dba 100644 --- a/lib/onebox/engine/coub_onebox.rb +++ b/lib/onebox/engine/coub_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/coub\.com\/view\//) + matches_regexp(%r{^https?://coub\.com/view/}) always_https def placeholder_html diff --git a/lib/onebox/engine/facebook_media_onebox.rb b/lib/onebox/engine/facebook_media_onebox.rb index 903eccb131..cdf4d699ff 100644 --- a/lib/onebox/engine/facebook_media_onebox.rb +++ b/lib/onebox/engine/facebook_media_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/.*\.facebook\.com\/(\w+)\/(videos|\?).*/) + matches_regexp(%r{^https?://.*\.facebook\.com/(\w+)/(videos|\?).*}) always_https requires_iframe_origins "https://www.facebook.com" diff --git a/lib/onebox/engine/five_hundred_px_onebox.rb b/lib/onebox/engine/five_hundred_px_onebox.rb index 806b5f9e6a..d2aab48eaf 100644 --- a/lib/onebox/engine/five_hundred_px_onebox.rb +++ b/lib/onebox/engine/five_hundred_px_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/500px\.com\/photo\/\d+\//) + matches_regexp(%r{^https?://500px\.com/photo/\d+/}) always_https def to_html diff --git a/lib/onebox/engine/flickr_onebox.rb b/lib/onebox/engine/flickr_onebox.rb index 3ed26684a7..435a53e027 100644 --- a/lib/onebox/engine/flickr_onebox.rb +++ b/lib/onebox/engine/flickr_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative './opengraph_image' +require_relative "./opengraph_image" module Onebox module Engine @@ -8,12 +8,12 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/www\.flickr\.com\/photos\//) + matches_regexp(%r{^https?://www\.flickr\.com/photos/}) always_https def to_html og = get_opengraph - return album_html(og) if og.url =~ /\/sets\// + return album_html(og) if og.url =~ %r{/sets/} return image_html(og) if !og.image.nil? nil end diff --git a/lib/onebox/engine/flickr_shortened_onebox.rb b/lib/onebox/engine/flickr_shortened_onebox.rb index 1c1243050b..0a6baf1360 100644 --- a/lib/onebox/engine/flickr_shortened_onebox.rb +++ b/lib/onebox/engine/flickr_shortened_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative './opengraph_image' +require_relative "./opengraph_image" module Onebox module Engine @@ -9,7 +9,7 @@ module Onebox include StandardEmbed include OpengraphImage - matches_regexp(/^https?:\/\/flic\.kr\/p\//) + matches_regexp(%r{^https?://flic\.kr/p/}) always_https end end diff --git a/lib/onebox/engine/gfycat_onebox.rb b/lib/onebox/engine/gfycat_onebox.rb index 27fd4bf79e..44ca947f66 100644 --- a/lib/onebox/engine/gfycat_onebox.rb +++ b/lib/onebox/engine/gfycat_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include JSON - matches_regexp(/^https?:\/\/gfycat\.com\//) + matches_regexp(%r{^https?://gfycat\.com/}) always_https # This engine should have priority over AllowlistedGenericOnebox. @@ -60,14 +60,19 @@ module Onebox private def match - @match ||= @url.match(/^https?:\/\/gfycat\.com\/(gifs\/detail\/)?(?.+)/) + @match ||= @url.match(%r{^https?://gfycat\.com/(gifs/detail/)?(?.+)}) end def og_data return @og_data if defined?(@og_data) - response = Onebox::Helpers.fetch_response(url, redirect_limit: 10) rescue nil - page = Nokogiri::HTML(response) + response = + begin + Onebox::Helpers.fetch_response(url, redirect_limit: 10) + rescue StandardError + nil + end + page = Nokogiri.HTML(response) script = page.at_css('script[type="application/ld+json"]') if json_string = script&.text @@ -82,15 +87,15 @@ module Onebox @data = { name: match[:name], - title: og_data[:headline] || 'No Title', + title: og_data[:headline] || "No Title", author: og_data[:author], url: @url, } - if keywords = og_data[:keywords]&.split(',') + if keywords = og_data[:keywords]&.split(",") @data[:keywords] = keywords .map { |keyword| "##{keyword}" } - .join(' ') + .join(" ") end if og_data[:video] diff --git a/lib/onebox/engine/github_actions_onebox.rb b/lib/onebox/engine/github_actions_onebox.rb index 182fff49d5..6630fefa4d 100644 --- a/lib/onebox/engine/github_actions_onebox.rb +++ b/lib/onebox/engine/github_actions_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -9,7 +9,9 @@ module Onebox include LayoutSupport include JSON - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/(actions\/runs\/[[:digit:]]+|pull\/[[:digit:]]*\/checks\?check_run_id=[[:digit:]]+)/) + matches_regexp( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/(actions/runs/[[:digit:]]+|pull/[[:digit:]]*/checks\?check_run_id=[[:digit:]]+)}, + ) always_https def url @@ -29,12 +31,18 @@ module Onebox def match_url return if defined?(@match) && defined?(@type) - if match = @url.match(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/actions\/runs\/(?[[:digit:]]+)/) + if match = + @url.match( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/actions/runs/(?[[:digit:]]+)}, + ) @match = match @type = :actions_run end - if match = @url.match(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/pull\/(?[[:digit:]]*)\/checks\?check_run_id=(?[[:digit:]]+)/) + if match = + @url.match( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/pull/(?[[:digit:]]*)/checks\?check_run_id=(?[[:digit:]]+)}, + ) @match = match @type = :pr_run end @@ -67,18 +75,20 @@ module Onebox status = "pending" end - title = if type == :actions_run - raw["head_commit"]["message"].lines.first - elsif type == :pr_run - pr_url = "https://api.github.com/repos/#{match[:org]}/#{match[:repo]}/pulls/#{match[:pr_id]}" - ::MultiJson.load(URI.parse(pr_url).open(read_timeout: timeout))["title"] - end + title = + if type == :actions_run + raw["head_commit"]["message"].lines.first + elsif type == :pr_run + pr_url = + "https://api.github.com/repos/#{match[:org]}/#{match[:repo]}/pulls/#{match[:pr_id]}" + ::MultiJson.load(URI.parse(pr_url).open(read_timeout: timeout))["title"] + end { - link: @url, - title: title, - name: raw["name"], - run_number: raw["run_number"], + :link => @url, + :title => title, + :name => raw["name"], + :run_number => raw["run_number"], status => true, } end diff --git a/lib/onebox/engine/github_blob_onebox.rb b/lib/onebox/engine/github_blob_onebox.rb index fd70a4b2af..48a7e6dec0 100644 --- a/lib/onebox/engine/github_blob_onebox.rb +++ b/lib/onebox/engine/github_blob_onebox.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative '../mixins/git_blob_onebox' +require_relative "../mixins/git_blob_onebox" module Onebox module Engine class GithubBlobOnebox def self.git_regexp - /^https?:\/\/(www\.)?github\.com.*\/blob\// + %r{^https?://(www\.)?github\.com.*/blob/} end def self.onebox_name @@ -16,7 +16,7 @@ module Onebox include Onebox::Mixins::GitBlobOnebox def raw_regexp - /github\.com\/(?[^\/]+)\/(?[^\/]+)\/blob\/(?[^\/]+)\/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?/mi + %r{github\.com/(?[^/]+)/(?[^/]+)/blob/(?[^/]+)/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?}mi end def raw_template(m) @@ -24,7 +24,7 @@ module Onebox end def title - Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(/^https?\:\/\/github\.com\//, '')) + Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(%r{^https?\://github\.com/}, "")) end end end diff --git a/lib/onebox/engine/github_commit_onebox.rb b/lib/onebox/engine/github_commit_onebox.rb index d1820faa4b..e1fa68547e 100644 --- a/lib/onebox/engine/github_commit_onebox.rb +++ b/lib/onebox/engine/github_commit_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -10,7 +10,7 @@ module Onebox include JSON include Onebox::Mixins::GithubBody - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:\/)?(?:.)*\/commit\//) + matches_regexp(%r{^https?://(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:/)?(?:.)*/commit/}) always_https def url @@ -22,8 +22,12 @@ module Onebox def match return @match if defined?(@match) - @match = @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/commit/(?[^/]+)}) - @match ||= @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)/commit/(?[^/]+)}) + @match = + @url.match(%{github\.com/(?[^/]+)/(?[^/]+)/commit/(?[^/]+)}) + @match ||= + @url.match( + %{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)/commit/(?[^/]+)}, + ) @match end @@ -31,18 +35,18 @@ module Onebox def data result = raw.clone - lines = result['commit']['message'].split("\n") - result['title'] = lines.first - result['body'], result['excerpt'] = compute_body(lines[1..lines.length].join("\n")) + lines = result["commit"]["message"].split("\n") + result["title"] = lines.first + result["body"], result["excerpt"] = compute_body(lines[1..lines.length].join("\n")) - committed_at = Time.parse(result['commit']['committer']['date']) - result['committed_at'] = committed_at.strftime("%I:%M%p - %d %b %y %Z") - result['committed_at_date'] = committed_at.strftime("%F") - result['committed_at_time'] = committed_at.strftime("%T") + committed_at = Time.parse(result["commit"]["committer"]["date"]) + result["committed_at"] = committed_at.strftime("%I:%M%p - %d %b %y %Z") + result["committed_at_date"] = committed_at.strftime("%F") + result["committed_at_time"] = committed_at.strftime("%T") - result['link'] = link + result["link"] = link ulink = URI(link) - result['domain'] = "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}" + result["domain"] = "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}" result end diff --git a/lib/onebox/engine/github_folder_onebox.rb b/lib/onebox/engine/github_folder_onebox.rb index aae75888be..d77a27ca74 100644 --- a/lib/onebox/engine/github_folder_onebox.rb +++ b/lib/onebox/engine/github_folder_onebox.rb @@ -28,7 +28,7 @@ module Onebox # For links to markdown and rdoc if html_doc.css(".Box.md, .Box.rdoc").present? - node = html_doc.css('a.anchor').find { |n| n['href'] == "##{fragment}" } + node = html_doc.css("a.anchor").find { |n| n["href"] == "##{fragment}" } subtitle = node&.parent&.text end @@ -40,12 +40,12 @@ module Onebox title: Onebox::Helpers.truncate(title, 250), path: display_path, description: display_description, - favicon: get_favicon + favicon: get_favicon, } end def extract_path(root, max_length) - path = url.split('#')[0].split('?')[0] + path = url.split("#")[0].split("?")[0] path = path["#{root}/tree/".length..-1] return unless path diff --git a/lib/onebox/engine/github_gist_onebox.rb b/lib/onebox/engine/github_gist_onebox.rb index 21561d85ff..ad579428d2 100644 --- a/lib/onebox/engine/github_gist_onebox.rb +++ b/lib/onebox/engine/github_gist_onebox.rb @@ -9,7 +9,7 @@ module Onebox MAX_FILES = 3 - matches_regexp(/^http(?:s)?:\/\/gist\.(?:(?:\w)+\.)?(github)\.com(?:\/)?/) + matches_regexp(%r{^http(?:s)?://gist\.(?:(?:\w)+\.)?(github)\.com(?:/)?}) always_https def url @@ -20,10 +20,10 @@ module Onebox def data @data ||= { - title: 'gist.github.com', + title: "gist.github.com", link: link, gist_files: gist_files.take(MAX_FILES), - truncated_files?: truncated_files? + truncated_files?: truncated_files?, } end @@ -34,9 +34,7 @@ module Onebox def gist_files return [] unless gist_api - @gist_files ||= gist_api["files"].values.map do |file_json| - GistFile.new(file_json) - end + @gist_files ||= gist_api["files"].values.map { |file_json| GistFile.new(file_json) } end def gist_api diff --git a/lib/onebox/engine/github_issue_onebox.rb b/lib/onebox/engine/github_issue_onebox.rb index 4d387f4799..510fc0ca3e 100644 --- a/lib/onebox/engine/github_issue_onebox.rb +++ b/lib/onebox/engine/github_issue_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -11,7 +11,9 @@ module Onebox include JSON include Onebox::Mixins::GithubBody - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/issues\/([[:digit:]]+)/) + matches_regexp( + %r{^https?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/issues/([[:digit:]]+)}, + ) always_https def url @@ -22,35 +24,36 @@ module Onebox private def match - @match ||= @url.match(/^http(?:s)?:\/\/(?:www\.)?(?:(?:\w)+\.)?github\.com\/(?.+)\/(?.+)\/(?issues)\/(?[\d]+)/) + @match ||= + @url.match( + %r{^http(?:s)?://(?:www\.)?(?:(?:\w)+\.)?github\.com/(?.+)/(?.+)/(?issues)/(?[\d]+)}, + ) end def data - created_at = Time.parse(raw['created_at']) - closed_at = Time.parse(raw['closed_at']) if raw['closed_at'] - body, excerpt = compute_body(raw['body']) + created_at = Time.parse(raw["created_at"]) + closed_at = Time.parse(raw["closed_at"]) if raw["closed_at"] + body, excerpt = compute_body(raw["body"]) ulink = URI(link) - labels = raw['labels'].map do |l| - { name: Emoji.codes_to_img(l['name']) } - end + labels = raw["labels"].map { |l| { name: Emoji.codes_to_img(l["name"]) } } { link: @url, - title: raw['title'], + title: raw["title"], body: body, excerpt: excerpt, labels: labels, - user: raw['user'], - created_at: created_at.strftime('%I:%M%p - %d %b %y %Z'), - created_at_date: created_at.strftime('%F'), - created_at_time: created_at.strftime('%T'), - closed_at: closed_at&.strftime('%I:%M%p - %d %b %y %Z'), - closed_at_date: closed_at&.strftime('%F'), - closed_at_time: closed_at&.strftime('%T'), - closed_by: raw['closed_by'], - avatar: "https://avatars1.githubusercontent.com/u/#{raw['user']['id']}?v=2&s=96", - domain: "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}", + user: raw["user"], + created_at: created_at.strftime("%I:%M%p - %d %b %y %Z"), + created_at_date: created_at.strftime("%F"), + created_at_time: created_at.strftime("%T"), + closed_at: closed_at&.strftime("%I:%M%p - %d %b %y %Z"), + closed_at_date: closed_at&.strftime("%F"), + closed_at_time: closed_at&.strftime("%T"), + closed_by: raw["closed_by"], + avatar: "https://avatars1.githubusercontent.com/u/#{raw["user"]["id"]}?v=2&s=96", + domain: "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}", } end end diff --git a/lib/onebox/engine/github_pull_request_onebox.rb b/lib/onebox/engine/github_pull_request_onebox.rb index 1ef23815cc..77310cc46f 100644 --- a/lib/onebox/engine/github_pull_request_onebox.rb +++ b/lib/onebox/engine/github_pull_request_onebox.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../mixins/github_body' +require_relative "../mixins/github_body" module Onebox module Engine @@ -12,7 +12,7 @@ module Onebox GITHUB_COMMENT_REGEX = /(\r\n)/ - matches_regexp(/^https?:\/\/(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:\/)?(?:.)*\/pull/) + matches_regexp(%r{^https?://(?:www\.)?(?:(?:\w)+\.)?(github)\.com(?:/)?(?:.)*/pull}) always_https def url @@ -22,51 +22,59 @@ module Onebox private def match - @match ||= @url.match(%r{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)}) + @match ||= + @url.match(%r{github\.com/(?[^/]+)/(?[^/]+)/pull/(?[^/]+)}) end def data result = raw.clone - result['link'] = link + result["link"] = link - created_at = Time.parse(result['created_at']) - result['created_at'] = created_at.strftime("%I:%M%p - %d %b %y %Z") - result['created_at_date'] = created_at.strftime("%F") - result['created_at_time'] = created_at.strftime("%T") + created_at = Time.parse(result["created_at"]) + result["created_at"] = created_at.strftime("%I:%M%p - %d %b %y %Z") + result["created_at_date"] = created_at.strftime("%F") + result["created_at_time"] = created_at.strftime("%T") ulink = URI(link) - result['domain'] = "#{ulink.host}/#{ulink.path.split('/')[1]}/#{ulink.path.split('/')[2]}" + result["domain"] = "#{ulink.host}/#{ulink.path.split("/")[1]}/#{ulink.path.split("/")[2]}" - result['body'], result['excerpt'] = compute_body(result['body']) + result["body"], result["excerpt"] = compute_body(result["body"]) - if result['commit'] = load_commit(link) - result['body'], result['excerpt'] = compute_body(result['commit']['commit']['message'].lines[1..].join) - elsif result['comment'] = load_comment(link) - result['body'], result['excerpt'] = compute_body(result['comment']['body']) - elsif result['discussion'] = load_review(link) - result['body'], result['excerpt'] = compute_body(result['discussion']['body']) + if result["commit"] = load_commit(link) + result["body"], result["excerpt"] = + compute_body(result["commit"]["commit"]["message"].lines[1..].join) + elsif result["comment"] = load_comment(link) + result["body"], result["excerpt"] = compute_body(result["comment"]["body"]) + elsif result["discussion"] = load_review(link) + result["body"], result["excerpt"] = compute_body(result["discussion"]["body"]) else - result['pr'] = true + result["pr"] = true end result end def load_commit(link) - if commit_match = link.match(/commits\/(\h+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/commits/#{commit_match[1]}") + if commit_match = link.match(%r{commits/(\h+)}) + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/commits/#{commit_match[1]}", + ) end end def load_comment(link) if comment_match = link.match(/#issuecomment-(\d+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/issues/comments/#{comment_match[1]}") + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/issues/comments/#{comment_match[1]}", + ) end end def load_review(link) if review_match = link.match(/#discussion_r(\d+)/) - load_json("https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/pulls/comments/#{review_match[1]}") + load_json( + "https://api.github.com/repos/#{match[:owner]}/#{match[:repository]}/pulls/comments/#{review_match[1]}", + ) end end diff --git a/lib/onebox/engine/gitlab_blob_onebox.rb b/lib/onebox/engine/gitlab_blob_onebox.rb index d8ba197338..c948e5bf6d 100644 --- a/lib/onebox/engine/gitlab_blob_onebox.rb +++ b/lib/onebox/engine/gitlab_blob_onebox.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require_relative '../mixins/git_blob_onebox' +require_relative "../mixins/git_blob_onebox" module Onebox module Engine class GitlabBlobOnebox def self.git_regexp - /^https?:\/\/(www\.)?gitlab\.com.*\/blob\// + %r{^https?://(www\.)?gitlab\.com.*/blob/} end def self.onebox_name @@ -16,7 +16,7 @@ module Onebox include Onebox::Mixins::GitBlobOnebox def raw_regexp - /gitlab\.com\/(?[^\/]+)\/(?[^\/]+)\/blob\/(?[^\/]+)\/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?/mi + %r{gitlab\.com/(?[^/]+)/(?[^/]+)/blob/(?[^/]+)/(?[^#]+)(#(L(?[^-]*)(-L(?.*))?))?}mi end def raw_template(m) @@ -24,7 +24,7 @@ module Onebox end def title - Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(/^https?\:\/\/gitlab\.com\//, '')) + Sanitize.fragment(Onebox::Helpers.uri_unencode(link).sub(%r{^https?\://gitlab\.com/}, "")) end end end diff --git a/lib/onebox/engine/google_calendar_onebox.rb b/lib/onebox/engine/google_calendar_onebox.rb index b666df4d08..a7b57b220b 100644 --- a/lib/onebox/engine/google_calendar_onebox.rb +++ b/lib/onebox/engine/google_calendar_onebox.rb @@ -10,7 +10,7 @@ module Onebox requires_iframe_origins "https://calendar.google.com" def to_html - url = @url.split('&').first + url = @url.split("&").first src = ::Onebox::Helpers.normalize_url_for_output(url) "" end diff --git a/lib/onebox/engine/google_docs_onebox.rb b/lib/onebox/engine/google_docs_onebox.rb index cc7872aebe..ebb29a43bb 100644 --- a/lib/onebox/engine/google_docs_onebox.rb +++ b/lib/onebox/engine/google_docs_onebox.rb @@ -7,15 +7,12 @@ module Onebox include StandardEmbed include LayoutSupport - SUPPORTED_ENDPOINTS = %w(spreadsheets document forms presentation) - SHORT_TYPES = { - spreadsheets: :sheets, - document: :docs, - presentation: :slides, - forms: :forms, - } + SUPPORTED_ENDPOINTS = %w[spreadsheets document forms presentation] + SHORT_TYPES = { spreadsheets: :sheets, document: :docs, presentation: :slides, forms: :forms } - matches_regexp(/^(https?:)?\/\/(docs\.google\.com)\/(?(#{SUPPORTED_ENDPOINTS.join('|')}))\/d\/((?[\w-]*)).+$/) + matches_regexp( + %r{^(https?:)?//(docs\.google\.com)/(?(#{SUPPORTED_ENDPOINTS.join("|")}))/d/((?[\w-]*)).+$}, + ) always_https private @@ -24,17 +21,18 @@ module Onebox og_data = get_opengraph short_type = SHORT_TYPES[match[:endpoint].to_sym] - description = if Onebox::Helpers.blank?(og_data.description) - "This #{short_type.to_s.chop.capitalize} is private" - else - Onebox::Helpers.truncate(og_data.description, 250) - end + description = + if Onebox::Helpers.blank?(og_data.description) + "This #{short_type.to_s.chop.capitalize} is private" + else + Onebox::Helpers.truncate(og_data.description, 250) + end { link: link, title: og_data.title || "Google #{short_type.to_s.capitalize}", description: description, - type: short_type + type: short_type, } end diff --git a/lib/onebox/engine/google_drive_onebox.rb b/lib/onebox/engine/google_drive_onebox.rb index 82628228ea..cb8bbf797e 100644 --- a/lib/onebox/engine/google_drive_onebox.rb +++ b/lib/onebox/engine/google_drive_onebox.rb @@ -7,7 +7,7 @@ module Onebox include StandardEmbed include LayoutSupport - matches_regexp(/^(https?:)?\/\/(drive\.google\.com)\/file\/d\/(?[\w-]*)\/.+$/) + matches_regexp(%r{^(https?:)?//(drive\.google\.com)/file/d/(?[\w-]*)/.+$}) always_https protected @@ -15,14 +15,14 @@ module Onebox def data og_data = get_opengraph title = og_data.title || "Google Drive" - title = "#{og_data.title} (video)" if og_data.type =~ /^video[\/\.]/ + title = "#{og_data.title} (video)" if og_data.type =~ %r{^video[/\.]} description = og_data.description || "Google Drive file." { link: link, title: title, description: Onebox::Helpers.truncate(description, 250), - image: og_data.image + image: og_data.image, } end end diff --git a/lib/onebox/engine/google_maps_onebox.rb b/lib/onebox/engine/google_maps_onebox.rb index 0c4fbcf16d..828c7fd5dc 100644 --- a/lib/onebox/engine/google_maps_onebox.rb +++ b/lib/onebox/engine/google_maps_onebox.rb @@ -25,24 +25,30 @@ module Onebox requires_iframe_origins("https://maps.google.com", "https://google.com") # Matches shortened Google Maps URLs - matches_regexp :short, %r"^(https?:)?//goo\.gl/maps/" + matches_regexp :short, %r{^(https?:)?//goo\.gl/maps/} # Matches URLs for custom-created maps - matches_regexp :custom, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$" + matches_regexp :custom, + %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$" # Matches URLs with streetview data - matches_regexp :streetview, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?-?[\d.]+),(?-?[\d.]+),(?:\d+)a,(?[\d.]+)y,(?[\d.]+)h,(?[\d.]+)t.+?data=.*?!1s(?[^!]{22})" + matches_regexp :streetview, + %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?-?[\d.]+),(?-?[\d.]+),(?:\d+)a,(?[\d.]+)y,(?[\d.]+)h,(?[\d.]+)t.+?data=.*?!1s(?[^!]{22})" # Matches "normal" Google Maps URLs with arbitrary data - matches_regexp :standard, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps" + matches_regexp :standard, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps" # Matches URLs for the old Google Maps domain which we occasionally get redirected to - matches_regexp :canonical, %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?" + matches_regexp :canonical, %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?" def initialize(url, timeout = nil) super resolve_url! - rescue Net::HTTPServerException, Timeout::Error, Net::HTTPError, Errno::ECONNREFUSED, RuntimeError => err + rescue Net::HTTPServerException, + Timeout::Error, + Net::HTTPError, + Errno::ECONNREFUSED, + RuntimeError => err raise ArgumentError, "malformed url or unresolveable: #{err.message}" end @@ -95,17 +101,16 @@ module Onebox zoom = match[:mz] == "z" ? match[:zoom] : Math.log2(57280048.0 / match[:zoom].to_f).round location = "#{match[:lon]},#{match[:lat]}" url = "https://maps.google.com/maps?ll=#{location}&z=#{zoom}&output=embed&dg=ntvb" - url += "&q=#{$1}" if match = @url.match(/\/place\/([^\/\?]+)/) + url += "&q=#{$1}" if match = @url.match(%r{/place/([^/\?]+)}) url += "&cid=#{($1 + $2).to_i(16)}" if @url.match(/!3m1!1s0x(\h{16}):0x(\h{16})/) @url = url - @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap¢er=#{location}&zoom=#{zoom}&size=690x400&sensor=false" - + @placeholder = + "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap¢er=#{location}&zoom=#{zoom}&size=690x400&sensor=false" when :custom url = @url.dup @url = rewrite_custom_url(url, "embed") @placeholder = rewrite_custom_url(url, "thumbnail") @placeholder_height = @placeholder_width = 120 - when :streetview @streetview = true panoid = match[:pano] @@ -115,19 +120,24 @@ module Onebox pitch = (match[:pitch].to_f / 10.0).round(4).to_s fov = (match[:zoom].to_f / 100.0).round(4).to_s zoom = match[:zoom].to_f.round - @url = "https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}" - @placeholder = "https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false" - + @url = + "https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}" + @placeholder = + "https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false" when :canonical - query = URI::decode_www_form(uri.query).to_h + query = URI.decode_www_form(uri.query).to_h if !query.has_key?("ll") - raise ArgumentError, "canonical url lacks location argument" unless query.has_key?("sll") + unless query.has_key?("sll") + raise ArgumentError, "canonical url lacks location argument" + end query["ll"] = query["sll"] @url += "&ll=#{query["sll"]}" end location = query["ll"] if !query.has_key?("z") - raise ArgumentError, "canonical url has incomplete query arguments" unless query.has_key?("spn") || query.has_key?("sspn") + unless query.has_key?("spn") || query.has_key?("sspn") + raise ArgumentError, "canonical url has incomplete query arguments" + end if !query.has_key?("spn") query["spn"] = query["sspn"] @url += "&spn=#{query["sspn"]}" @@ -137,9 +147,9 @@ module Onebox else zoom = query["z"] end - @url = @url.sub('output=classic', 'output=embed') - @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false¢er=#{location}&zoom=#{zoom}" - + @url = @url.sub("output=classic", "output=embed") + @placeholder = + "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false¢er=#{location}&zoom=#{zoom}" else raise "unexpected url type #{type.inspect}" end @@ -156,27 +166,34 @@ module Onebox def rewrite_custom_url(url, target) uri = URI(url) - uri.path = uri.path.sub(/(?<=^\/maps\/d\/)\w+$/, target) + uri.path = uri.path.sub(%r{(?<=^/maps/d/)\w+$}, target) uri.to_s end def follow_redirect! begin - http = FinalDestination::HTTP.start( - uri.host, - uri.port, - use_ssl: uri.scheme == 'https', - open_timeout: timeout, - read_timeout: timeout - ) + http = + FinalDestination::HTTP.start( + uri.host, + uri.port, + use_ssl: uri.scheme == "https", + open_timeout: timeout, + read_timeout: timeout, + ) response = http.head(uri.path) - raise "unexpected response code #{response.code}" unless %w(200 301 302).include?(response.code) + unless %w[200 301 302].include?(response.code) + raise "unexpected response code #{response.code}" + end @url = response.code == "200" ? uri.to_s : response["Location"] @uri = URI(@url) ensure - http.finish rescue nil + begin + http.finish + rescue StandardError + nil + end end end end diff --git a/lib/onebox/engine/google_photos_onebox.rb b/lib/onebox/engine/google_photos_onebox.rb index 6c930ce59c..afb8aade19 100644 --- a/lib/onebox/engine/google_photos_onebox.rb +++ b/lib/onebox/engine/google_photos_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(photos)\.(app\.goo\.gl|google\.com)/) + matches_regexp(%r{^https?://(photos)\.(app\.goo\.gl|google\.com)}) always_https def to_html diff --git a/lib/onebox/engine/google_play_app_onebox.rb b/lib/onebox/engine/google_play_app_onebox.rb index 1e2557d442..f03fceee50 100644 --- a/lib/onebox/engine/google_play_app_onebox.rb +++ b/lib/onebox/engine/google_play_app_onebox.rb @@ -7,23 +7,36 @@ module Onebox include LayoutSupport include HTML - DEFAULTS = { - MAX_DESCRIPTION_CHARS: 500 - } + DEFAULTS = { MAX_DESCRIPTION_CHARS: 500 } - matches_regexp(/^https?:\/\/play\.(?:(?:\w)+\.)?(google)\.com(?:\/)?\/store\/apps\//) + matches_regexp(%r{^https?://play\.(?:(?:\w)+\.)?(google)\.com(?:/)?/store/apps/}) always_https private def data - price = raw.css("meta[itemprop=price]").first["content"] rescue "Free" + price = + begin + raw.css("meta[itemprop=price]").first["content"] + rescue StandardError + "Free" + end { link: link, - title: raw.css("meta[property='og:title']").first["content"].gsub(" - Apps on Google Play", ""), - image: ::Onebox::Helpers.normalize_url_for_output(raw.css("meta[property='og:image']").first["content"]), - description: raw.css("meta[name=description]").first["content"][0..DEFAULTS[:MAX_DESCRIPTION_CHARS]].chop + "...", - price: price == "0" ? "Free" : price + title: + raw.css("meta[property='og:title']").first["content"].gsub( + " - Apps on Google Play", + "", + ), + image: + ::Onebox::Helpers.normalize_url_for_output( + raw.css("meta[property='og:image']").first["content"], + ), + description: + raw.css("meta[name=description]").first["content"][ + 0..DEFAULTS[:MAX_DESCRIPTION_CHARS] + ].chop + "...", + price: price == "0" ? "Free" : price, } end end diff --git a/lib/onebox/engine/hackernews_onebox.rb b/lib/onebox/engine/hackernews_onebox.rb index 79d8e037a5..b4507f26e0 100644 --- a/lib/onebox/engine/hackernews_onebox.rb +++ b/lib/onebox/engine/hackernews_onebox.rb @@ -7,7 +7,7 @@ module Onebox include LayoutSupport include JSON - REGEX = /^https?:\/\/news\.ycombinator\.com\/item\?id=(?\d+)/ + REGEX = %r{^https?://news\.ycombinator\.com/item\?id=(?\d+)} matches_regexp(REGEX) @@ -23,22 +23,24 @@ module Onebox end def data - return nil unless %w{story comment}.include?(raw['type']) + return nil unless %w[story comment].include?(raw["type"]) html_entities = HTMLEntities.new data = { link: @url, - title: Onebox::Helpers.truncate(raw['title'], 80), - favicon: 'https://news.ycombinator.com/y18.gif', - timestamp: Time.at(raw['time']).strftime("%-l:%M %p - %-d %b %Y"), - author: raw['by'] + title: Onebox::Helpers.truncate(raw["title"], 80), + favicon: "https://news.ycombinator.com/y18.gif", + timestamp: Time.at(raw["time"]).strftime("%-l:%M %p - %-d %b %Y"), + author: raw["by"], } - data['description'] = html_entities.decode(Onebox::Helpers.truncate(raw['text'], 400)) if raw['text'] + data["description"] = html_entities.decode( + Onebox::Helpers.truncate(raw["text"], 400), + ) if raw["text"] - if raw['type'] == 'story' - data['data_1'] = raw['score'] - data['data_2'] = raw['descendants'] + if raw["type"] == "story" + data["data_1"] = raw["score"] + data["data_2"] = raw["descendants"] end data diff --git a/lib/onebox/engine/image_onebox.rb b/lib/onebox/engine/image_onebox.rb index d37faff841..2b6e08ac92 100644 --- a/lib/onebox/engine/image_onebox.rb +++ b/lib/onebox/engine/image_onebox.rb @@ -5,8 +5,8 @@ module Onebox class ImageOnebox include Engine - matches_content_type(/^image\/(png|jpg|jpeg|gif|bmp|tif|tiff)$/) - matches_regexp(/^(https?:)?\/\/.+\.(png|jpg|jpeg|gif|bmp|tif|tiff)(\?.*)?$/i) + matches_content_type(%r{^image/(png|jpg|jpeg|gif|bmp|tif|tiff)$}) + matches_regexp(%r{^(https?:)?//.+\.(png|jpg|jpeg|gif|bmp|tif|tiff)(\?.*)?$}i) def always_https? AllowlistedGenericOnebox.host_matches(uri, AllowlistedGenericOnebox.https_hosts) @@ -14,7 +14,7 @@ module Onebox def to_html # Fix Dropbox image links - if @url[/^https:\/\/www.dropbox.com\/s\//] + if @url[%r{^https://www.dropbox.com/s/}] @url.sub!("https://www.dropbox.com", "https://dl.dropboxusercontent.com") end diff --git a/lib/onebox/engine/imgur_onebox.rb b/lib/onebox/engine/imgur_onebox.rb index 26a90379dd..127f73baee 100644 --- a/lib/onebox/engine/imgur_onebox.rb +++ b/lib/onebox/engine/imgur_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(www\.)?imgur\.com/) + matches_regexp(%r{^https?://(www\.)?imgur\.com}) always_https def to_html @@ -23,7 +23,7 @@ module Onebox <<-HTML HTML end @@ -47,10 +47,15 @@ module Onebox end def is_album? - response = Onebox::Helpers.fetch_response("https://api.imgur.com/oembed.json?url=#{url}") rescue "{}" + response = + begin + Onebox::Helpers.fetch_response("https://api.imgur.com/oembed.json?url=#{url}") + rescue StandardError + "{}" + end oembed_data = Onebox::Helpers.symbolize_keys(::MultiJson.load(response)) - imgur_data_id = Nokogiri::HTML(oembed_data[:html]).xpath("//blockquote").attr("data-id") - imgur_data_id.to_s[/a\//] + imgur_data_id = Nokogiri.HTML(oembed_data[:html]).xpath("//blockquote").attr("data-id") + imgur_data_id.to_s[%r{a/}] end def image_html(og) diff --git a/lib/onebox/engine/instagram_onebox.rb b/lib/onebox/engine/instagram_onebox.rb index 7cc96ad3d6..86fd55947f 100644 --- a/lib/onebox/engine/instagram_onebox.rb +++ b/lib/onebox/engine/instagram_onebox.rb @@ -7,28 +7,38 @@ module Onebox include StandardEmbed include LayoutSupport - matches_regexp(/^https?:\/\/(?:www\.)?(?:instagram\.com|instagr\.am)\/?(?:.*)\/(?:p|tv)\/[a-zA-Z\d_-]+/) + matches_regexp( + %r{^https?://(?:www\.)?(?:instagram\.com|instagr\.am)/?(?:.*)/(?:p|tv)/[a-zA-Z\d_-]+}, + ) always_https requires_iframe_origins "https://www.instagram.com" def clean_url - url.scan(/^https?:\/\/(?:www\.)?(?:instagram\.com|instagr\.am)\/?(?:.*)\/(?:p|tv)\/[a-zA-Z\d_-]+/).flatten.first + url + .scan( + %r{^https?://(?:www\.)?(?:instagram\.com|instagr\.am)/?(?:.*)/(?:p|tv)/[a-zA-Z\d_-]+}, + ) + .flatten + .first end def data - @data ||= begin - oembed = get_oembed - raise "No oEmbed data found. Ensure 'facebook_app_access_token' is valid" if oembed.data.empty? + @data ||= + begin + oembed = get_oembed + if oembed.data.empty? + raise "No oEmbed data found. Ensure 'facebook_app_access_token' is valid" + end - { - link: clean_url.gsub("/#{oembed.author_name}/", "/") + '/embed', - title: "@#{oembed.author_name}", - image: oembed.thumbnail_url, - image_width: oembed.data[:thumbnail_width], - image_height: oembed.data[:thumbnail_height], - description: Onebox::Helpers.truncate(oembed.title, 250), - } - end + { + link: clean_url.gsub("/#{oembed.author_name}/", "/") + "/embed", + title: "@#{oembed.author_name}", + image: oembed.thumbnail_url, + image_width: oembed.data[:thumbnail_width], + image_height: oembed.data[:thumbnail_height], + description: Onebox::Helpers.truncate(oembed.title, 250), + } + end end def placeholder_html @@ -53,7 +63,7 @@ module Onebox end def get_oembed_url - if access_token != '' + if access_token != "" "https://graph.facebook.com/v9.0/instagram_oembed?url=#{clean_url}&access_token=#{access_token}" else # The following is officially deprecated by Instagram, but works in some limited circumstances. diff --git a/lib/onebox/engine/kaltura_onebox.rb b/lib/onebox/engine/kaltura_onebox.rb index d94091a4f0..f9ace11363 100644 --- a/lib/onebox/engine/kaltura_onebox.rb +++ b/lib/onebox/engine/kaltura_onebox.rb @@ -7,7 +7,7 @@ module Onebox include StandardEmbed always_https - matches_regexp(/^https?:\/\/[a-z0-9]+\.kaltura\.com\/id\/[a-zA-Z0-9]+/) + matches_regexp(%r{^https?://[a-z0-9]+\.kaltura\.com/id/[a-zA-Z0-9]+}) requires_iframe_origins "https://*.kaltura.com" def preview_html diff --git a/lib/onebox/engine/mixcloud_onebox.rb b/lib/onebox/engine/mixcloud_onebox.rb index 3d4e92295f..1a681d5ec0 100644 --- a/lib/onebox/engine/mixcloud_onebox.rb +++ b/lib/onebox/engine/mixcloud_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/www\.mixcloud\.com\//) + matches_regexp(%r{^https?://www\.mixcloud\.com/}) always_https requires_iframe_origins "https://www.mixcloud.com" diff --git a/lib/onebox/engine/motoko_onebox.rb b/lib/onebox/engine/motoko_onebox.rb index ec6433d2e4..1656150fbf 100644 --- a/lib/onebox/engine/motoko_onebox.rb +++ b/lib/onebox/engine/motoko_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/embed\.smartcontracts\.org\/?.*/) + matches_regexp(%r{^https?://embed\.smartcontracts\.org/?.*}) requires_iframe_origins "https://embed.smartcontracts.org" always_https diff --git a/lib/onebox/engine/opengraph_image.rb b/lib/onebox/engine/opengraph_image.rb index a104db36ff..77bf8ee210 100644 --- a/lib/onebox/engine/opengraph_image.rb +++ b/lib/onebox/engine/opengraph_image.rb @@ -3,7 +3,6 @@ module Onebox module Engine module OpengraphImage - def to_html og = get_opengraph "" diff --git a/lib/onebox/engine/pastebin_onebox.rb b/lib/onebox/engine/pastebin_onebox.rb index d9b26467f3..c542457f8d 100644 --- a/lib/onebox/engine/pastebin_onebox.rb +++ b/lib/onebox/engine/pastebin_onebox.rb @@ -8,17 +8,12 @@ module Onebox MAX_LINES = 10 - matches_regexp(/^http?:\/\/pastebin\.com/) + matches_regexp(%r{^http?://pastebin\.com}) private def data - @data ||= { - title: 'pastebin.com', - link: link, - content: content, - truncated?: truncated? - } + @data ||= { title: "pastebin.com", link: link, content: content, truncated?: truncated? } end def content @@ -31,21 +26,30 @@ module Onebox def lines return @lines if defined?(@lines) - response = Onebox::Helpers.fetch_response("http://pastebin.com/raw/#{paste_key}", redirect_limit: 1) rescue "" + response = + begin + Onebox::Helpers.fetch_response( + "http://pastebin.com/raw/#{paste_key}", + redirect_limit: 1, + ) + rescue StandardError + "" + end @lines = response.split("\n") end def paste_key - regex = case uri - when /\/raw\// - /\/raw\/([^\/]+)/ - when /\/download\// - /\/download\/([^\/]+)/ - when /\/embed\// - /\/embed\/([^\/]+)/ - else - /\/([^\/]+)/ - end + regex = + case uri + when %r{/raw/} + %r{/raw/([^/]+)} + when %r{/download/} + %r{/download/([^/]+)} + when %r{/embed/} + %r{/embed/([^/]+)} + else + %r{/([^/]+)} + end match = uri.path.match(regex) match[1] if match && match[1] diff --git a/lib/onebox/engine/pdf_onebox.rb b/lib/onebox/engine/pdf_onebox.rb index 2a8d46f0d4..30af7975b0 100644 --- a/lib/onebox/engine/pdf_onebox.rb +++ b/lib/onebox/engine/pdf_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include LayoutSupport - matches_regexp(/^(https?:)?\/\/.*\.pdf(\?.*)?$/i) + matches_regexp(%r{^(https?:)?//.*\.pdf(\?.*)?$}i) always_https private @@ -14,7 +14,7 @@ module Onebox def data begin size = Onebox::Helpers.fetch_content_length(@url) - rescue + rescue StandardError raise "Unable to read pdf file: #{@url}" end diff --git a/lib/onebox/engine/pubmed_onebox.rb b/lib/onebox/engine/pubmed_onebox.rb index 366d5d5029..fe3a39da27 100644 --- a/lib/onebox/engine/pubmed_onebox.rb +++ b/lib/onebox/engine/pubmed_onebox.rb @@ -6,22 +6,22 @@ module Onebox include Engine include LayoutSupport - matches_regexp(/^https?:\/\/(?:(?:\w)+\.)?(www.ncbi.nlm.nih)\.gov(?:\/)?\/pubmed\/\d+/) + matches_regexp(%r{^https?://(?:(?:\w)+\.)?(www.ncbi.nlm.nih)\.gov(?:/)?/pubmed/\d+}) private def xml return @xml if defined?(@xml) - doc = Nokogiri::XML(URI.join(@url, "?report=xml&format=text").open) + doc = Nokogiri.XML(URI.join(@url, "?report=xml&format=text").open) pre = doc.xpath("//pre") - @xml = Nokogiri::XML("" + pre.text + "") + @xml = Nokogiri.XML("" + pre.text + "") end def authors initials = xml.css("Initials").map { |x| x.content } last_names = xml.css("LastName").map { |x| x.content } author_list = (initials.zip(last_names)).map { |i, l| i + " " + l } - if author_list.length > 1 then + if author_list.length > 1 author_list[-2] = author_list[-2] + " and " + author_list[-1] author_list.pop end @@ -29,7 +29,8 @@ module Onebox end def date - xml.css("PubDate") + xml + .css("PubDate") .children .map { |x| x.content } .select { |s| !s.match(/^\s+$/) } @@ -48,7 +49,7 @@ module Onebox abstract: xml.css("AbstractText").text, date: date, link: @url, - pmid: match[:pmid] + pmid: match[:pmid], } end diff --git a/lib/onebox/engine/reddit_media_onebox.rb b/lib/onebox/engine/reddit_media_onebox.rb index d479a6b649..876ed41fd5 100644 --- a/lib/onebox/engine/reddit_media_onebox.rb +++ b/lib/onebox/engine/reddit_media_onebox.rb @@ -6,7 +6,7 @@ module Onebox include Engine include StandardEmbed - matches_regexp(/^https?:\/\/(www\.)?reddit\.com/) + matches_regexp(%r{^https?://(www\.)?reddit\.com}) def to_html if raw[:type] == "image" @@ -25,7 +25,7 @@ module Onebox HTML - elsif raw[:type] =~ /^video[\/\.]/ + elsif raw[:type] =~ %r{^video[/\.]} <<-HTML
    @@ -54,23 +53,22 @@ class Onebox::Engine::YoutubeOnebox yt_onebox_to_html end end - end after_initialize do - on(:reduce_cooked) do |fragment| - fragment.css(".lazyYT").each do |yt| - begin - youtube_id = yt["data-youtube-id"] - parameters = yt["data-parameters"] - uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1&#{parameters}") - yt.replace %{

    https://#{uri.host}#{uri.path}

    } - rescue URI::InvalidURIError - # remove any invalid/weird URIs - yt.remove + fragment + .css(".lazyYT") + .each do |yt| + begin + youtube_id = yt["data-youtube-id"] + parameters = yt["data-parameters"] + uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1&#{parameters}") + yt.replace %{

    https://#{uri.host}#{uri.path}

    } + rescue URI::InvalidURIError + # remove any invalid/weird URIs + yt.remove + end end - end end - end diff --git a/plugins/poll/app/controllers/polls_controller.rb b/plugins/poll/app/controllers/polls_controller.rb index 86ae5c1ba4..154d12402f 100644 --- a/plugins/poll/app/controllers/polls_controller.rb +++ b/plugins/poll/app/controllers/polls_controller.rb @@ -3,7 +3,7 @@ class DiscoursePoll::PollsController < ::ApplicationController requires_plugin DiscoursePoll::PLUGIN_NAME - before_action :ensure_logged_in, except: [:voters, :grouped_poll_results] + before_action :ensure_logged_in, except: %i[voters grouped_poll_results] def vote post_id = params.require(:post_id) @@ -63,8 +63,14 @@ class DiscoursePoll::PollsController < ::ApplicationController begin render json: { - grouped_results: DiscoursePoll::Poll.grouped_poll_results(current_user, post_id, poll_name, user_field_name) - } + grouped_results: + DiscoursePoll::Poll.grouped_poll_results( + current_user, + post_id, + poll_name, + user_field_name, + ), + } rescue DiscoursePoll::Error => e render_json_error e.message end diff --git a/plugins/poll/app/models/poll.rb b/plugins/poll/app/models/poll.rb index fe00c63509..922e398dba 100644 --- a/plugins/poll/app/models/poll.rb +++ b/plugins/poll/app/models/poll.rb @@ -9,33 +9,15 @@ class Poll < ActiveRecord::Base has_many :poll_options, -> { order(:id) }, dependent: :destroy has_many :poll_votes - enum type: { - regular: 0, - multiple: 1, - number: 2, - }, _scopes: false + enum type: { regular: 0, multiple: 1, number: 2 }, _scopes: false - enum status: { - open: 0, - closed: 1, - }, _scopes: false + enum status: { open: 0, closed: 1 }, _scopes: false - enum results: { - always: 0, - on_vote: 1, - on_close: 2, - staff_only: 3, - }, _scopes: false + enum results: { always: 0, on_vote: 1, on_close: 2, staff_only: 3 }, _scopes: false - enum visibility: { - secret: 0, - everyone: 1, - }, _scopes: false + enum visibility: { secret: 0, everyone: 1 }, _scopes: false - enum chart_type: { - bar: 0, - pie: 1 - }, _scopes: false + enum chart_type: { bar: 0, pie: 1 }, _scopes: false validates :min, numericality: { allow_nil: true, only_integer: true, greater_than_or_equal_to: 0 } validates :max, numericality: { allow_nil: true, only_integer: true, greater_than: 0 } diff --git a/plugins/poll/app/serializers/poll_serializer.rb b/plugins/poll/app/serializers/poll_serializer.rb index 0a7a1c8b89..4e75395613 100644 --- a/plugins/poll/app/serializers/poll_serializer.rb +++ b/plugins/poll/app/serializers/poll_serializer.rb @@ -48,13 +48,15 @@ class PollSerializer < ApplicationSerializer PollOptionSerializer.new( option, root: false, - scope: { can_see_results: can_see_results } + scope: { + can_see_results: can_see_results, + }, ).as_json end end def voters - object.poll_votes.count('DISTINCT user_id') + object.anonymous_voters.to_i + object.poll_votes.count("DISTINCT user_id") + object.anonymous_voters.to_i end def close @@ -72,5 +74,4 @@ class PollSerializer < ApplicationSerializer def include_preloaded_voters? object.can_see_voters?(scope.user) end - end diff --git a/plugins/poll/db/migrate/20150501152228_rename_total_votes_to_voters.rb b/plugins/poll/db/migrate/20150501152228_rename_total_votes_to_voters.rb index 12ea481d4f..4005af1364 100644 --- a/plugins/poll/db/migrate/20150501152228_rename_total_votes_to_voters.rb +++ b/plugins/poll/db/migrate/20150501152228_rename_total_votes_to_voters.rb @@ -1,31 +1,33 @@ # frozen_string_literal: true class RenameTotalVotesToVoters < ActiveRecord::Migration[4.2] - def up - PostCustomField.where(name: "polls").find_each do |pcf| - polls = ::JSON.parse(pcf.value) - polls.each_value do |poll| - next if poll.has_key?("voters") - poll["voters"] = poll["total_votes"] - poll.delete("total_votes") + PostCustomField + .where(name: "polls") + .find_each do |pcf| + polls = ::JSON.parse(pcf.value) + polls.each_value do |poll| + next if poll.has_key?("voters") + poll["voters"] = poll["total_votes"] + poll.delete("total_votes") + end + pcf.value = polls.to_json + pcf.save end - pcf.value = polls.to_json - pcf.save - end end def down - PostCustomField.where(name: "polls").find_each do |pcf| - polls = ::JSON.parse(pcf.value) - polls.each_value do |poll| - next if poll.has_key?("total_votes") - poll["total_votes"] = poll["voters"] - poll.delete("voters") + PostCustomField + .where(name: "polls") + .find_each do |pcf| + polls = ::JSON.parse(pcf.value) + polls.each_value do |poll| + next if poll.has_key?("total_votes") + poll["total_votes"] = poll["voters"] + poll.delete("voters") + end + pcf.value = polls.to_json + pcf.save end - pcf.value = polls.to_json - pcf.save - end end - end diff --git a/plugins/poll/db/migrate/20151016163051_merge_polls_votes.rb b/plugins/poll/db/migrate/20151016163051_merge_polls_votes.rb index 41c18ccb7d..afa30ea0b6 100644 --- a/plugins/poll/db/migrate/20151016163051_merge_polls_votes.rb +++ b/plugins/poll/db/migrate/20151016163051_merge_polls_votes.rb @@ -1,22 +1,27 @@ # frozen_string_literal: true class MergePollsVotes < ActiveRecord::Migration[4.2] - def up - PostCustomField.where(name: "polls").order(:post_id).pluck(:post_id).each do |post_id| - polls_votes = {} - PostCustomField.where(post_id: post_id).where("name LIKE 'polls-votes-%'").find_each do |pcf| - user_id = pcf.name["polls-votes-".size..-1] - polls_votes["#{user_id}"] = ::JSON.parse(pcf.value || "{}") - end + PostCustomField + .where(name: "polls") + .order(:post_id) + .pluck(:post_id) + .each do |post_id| + polls_votes = {} + PostCustomField + .where(post_id: post_id) + .where("name LIKE 'polls-votes-%'") + .find_each do |pcf| + user_id = pcf.name["polls-votes-".size..-1] + polls_votes["#{user_id}"] = ::JSON.parse(pcf.value || "{}") + end - pcf = PostCustomField.find_or_create_by(name: "polls-votes", post_id: post_id) - pcf.value = ::JSON.parse(pcf.value || "{}").merge(polls_votes).to_json - pcf.save - end + pcf = PostCustomField.find_or_create_by(name: "polls-votes", post_id: post_id) + pcf.value = ::JSON.parse(pcf.value || "{}").merge(polls_votes).to_json + pcf.save + end end def down end - end diff --git a/plugins/poll/db/migrate/20160321164925_close_polls_in_closed_topics.rb b/plugins/poll/db/migrate/20160321164925_close_polls_in_closed_topics.rb index 1835ae4078..3e54154baa 100644 --- a/plugins/poll/db/migrate/20160321164925_close_polls_in_closed_topics.rb +++ b/plugins/poll/db/migrate/20160321164925_close_polls_in_closed_topics.rb @@ -1,20 +1,19 @@ # frozen_string_literal: true class ClosePollsInClosedTopics < ActiveRecord::Migration[4.2] - def up - PostCustomField.joins(post: :topic) + PostCustomField + .joins(post: :topic) .where("post_custom_fields.name = 'polls'") .where("topics.closed") .find_each do |pcf| - polls = ::JSON.parse(pcf.value || "{}") - polls.values.each { |poll| poll["status"] = "closed" } - pcf.value = polls.to_json - pcf.save - end + polls = ::JSON.parse(pcf.value || "{}") + polls.values.each { |poll| poll["status"] = "closed" } + pcf.value = polls.to_json + pcf.save + end end def down end - end diff --git a/plugins/poll/db/migrate/20180820073549_create_polls_tables.rb b/plugins/poll/db/migrate/20180820073549_create_polls_tables.rb index 311f5fd92c..eadcad8a45 100644 --- a/plugins/poll/db/migrate/20180820073549_create_polls_tables.rb +++ b/plugins/poll/db/migrate/20180820073549_create_polls_tables.rb @@ -17,7 +17,7 @@ class CreatePollsTables < ActiveRecord::Migration[5.2] t.timestamps end - add_index :polls, [:post_id, :name], unique: true + add_index :polls, %i[post_id name], unique: true create_table :poll_options do |t| t.references :poll, index: true, foreign_key: true @@ -27,7 +27,7 @@ class CreatePollsTables < ActiveRecord::Migration[5.2] t.timestamps end - add_index :poll_options, [:poll_id, :digest], unique: true + add_index :poll_options, %i[poll_id digest], unique: true create_table :poll_votes, id: false do |t| t.references :poll, foreign_key: true @@ -36,6 +36,6 @@ class CreatePollsTables < ActiveRecord::Migration[5.2] t.timestamps end - add_index :poll_votes, [:poll_id, :poll_option_id, :user_id], unique: true + add_index :poll_votes, %i[poll_id poll_option_id user_id], unique: true end end diff --git a/plugins/poll/db/migrate/20180820080623_migrate_polls_data.rb b/plugins/poll/db/migrate/20180820080623_migrate_polls_data.rb index 1cd8b6abae..37b34c4e7f 100644 --- a/plugins/poll/db/migrate/20180820080623_migrate_polls_data.rb +++ b/plugins/poll/db/migrate/20180820080623_migrate_polls_data.rb @@ -5,11 +5,7 @@ class MigratePollsData < ActiveRecord::Migration[5.2] PG::Connection.escape_string(text) end - POLL_TYPES ||= { - "regular" => 0, - "multiple" => 1, - "number" => 2, - } + POLL_TYPES ||= { "regular" => 0, "multiple" => 1, "number" => 2 } PG_INTEGER_MAX ||= 2_147_483_647 @@ -61,43 +57,59 @@ class MigratePollsData < ActiveRecord::Migration[5.2] ORDER BY polls.post_id SQL - DB.query(sql).each do |r| - # for some reasons, polls or votes might be an array - r.polls = r.polls[0] if Array === r.polls && r.polls.size > 0 - r.votes = r.votes[0] if Array === r.votes && r.votes.size > 0 + DB + .query(sql) + .each do |r| + # for some reasons, polls or votes might be an array + r.polls = r.polls[0] if Array === r.polls && r.polls.size > 0 + r.votes = r.votes[0] if Array === r.votes && r.votes.size > 0 - existing_user_ids = User.where(id: r.votes.keys).pluck(:id).to_set + existing_user_ids = User.where(id: r.votes.keys).pluck(:id).to_set - # Poll votes are stored in a JSON object with the following hierarchy - # user_id -> poll_name -> options - # Since we're iterating over polls, we need to change the hierarchy to - # poll_name -> user_id -> options + # Poll votes are stored in a JSON object with the following hierarchy + # user_id -> poll_name -> options + # Since we're iterating over polls, we need to change the hierarchy to + # poll_name -> user_id -> options - votes = {} - r.votes.each do |user_id, user_votes| - # don't migrate votes from deleted/non-existing users - next unless existing_user_ids.include?(user_id.to_i) + votes = {} + r.votes.each do |user_id, user_votes| + # don't migrate votes from deleted/non-existing users + next unless existing_user_ids.include?(user_id.to_i) - user_votes.each do |poll_name, options| - votes[poll_name] ||= {} - votes[poll_name][user_id] = options + user_votes.each do |poll_name, options| + votes[poll_name] ||= {} + votes[poll_name][user_id] = options + end end - end - r.polls.values.each do |poll| - name = escape(poll["name"].presence || "poll") - type = POLL_TYPES[(poll["type"].presence || "")[/(regular|multiple|number)/, 1] || "regular"] - status = poll["status"] == "open" ? 0 : 1 - visibility = poll["public"] == "true" ? 1 : 0 - close_at = (Time.zone.parse(poll["close"]) rescue nil) - min = poll["min"].to_i.clamp(0, PG_INTEGER_MAX) - max = poll["max"].to_i.clamp(0, PG_INTEGER_MAX) - step = poll["step"].to_i.clamp(0, max) - anonymous_voters = poll["anonymous_voters"].to_i.clamp(0, PG_INTEGER_MAX) + r.polls.values.each do |poll| + name = escape(poll["name"].presence || "poll") + type = + POLL_TYPES[(poll["type"].presence || "")[/(regular|multiple|number)/, 1] || "regular"] + status = poll["status"] == "open" ? 0 : 1 + visibility = poll["public"] == "true" ? 1 : 0 + close_at = + ( + begin + Time.zone.parse(poll["close"]) + rescue StandardError + nil + end + ) + min = poll["min"].to_i.clamp(0, PG_INTEGER_MAX) + max = poll["max"].to_i.clamp(0, PG_INTEGER_MAX) + step = poll["step"].to_i.clamp(0, max) + anonymous_voters = poll["anonymous_voters"].to_i.clamp(0, PG_INTEGER_MAX) - next if DB.query_single("SELECT COUNT(*) FROM polls WHERE post_id = ? AND name = ? LIMIT 1", r.post_id, name).first > 0 + if DB.query_single( + "SELECT COUNT(*) FROM polls WHERE post_id = ? AND name = ? LIMIT 1", + r.post_id, + name, + ).first > 0 + next + end - poll_id = execute(<<~SQL + poll_id = execute(<<~SQL)[0]["id"] INSERT INTO polls ( post_id, name, @@ -126,38 +138,41 @@ class MigratePollsData < ActiveRecord::Migration[5.2] '#{r.updated_at}' ) RETURNING id SQL - )[0]["id"] - option_ids = Hash[*DB.query_single(<<~SQL + option_ids = Hash[*DB.query_single(<<~SQL)] INSERT INTO poll_options (poll_id, digest, html, anonymous_votes, created_at, updated_at) VALUES - #{poll["options"].map { |option| - "(#{poll_id}, '#{escape(option["id"])}', '#{escape(option["html"].strip)}', #{option["anonymous_votes"].to_i}, '#{r.created_at}', '#{r.updated_at}')" }.join(",") - } + #{ + poll["options"] + .map do |option| + "(#{poll_id}, '#{escape(option["id"])}', '#{escape(option["html"].strip)}', #{option["anonymous_votes"].to_i}, '#{r.created_at}', '#{r.updated_at}')" + end + .join(",") + } RETURNING digest, id SQL - )] - if votes[name].present? - poll_votes = votes[name].map do |user_id, options| - options - .select { |o| option_ids.has_key?(o) } - .map { |o| "(#{poll_id}, #{option_ids[o]}, #{user_id.to_i}, '#{r.created_at}', '#{r.updated_at}')" } - end + if votes[name].present? + poll_votes = + votes[name].map do |user_id, options| + options + .select { |o| option_ids.has_key?(o) } + .map do |o| + "(#{poll_id}, #{option_ids[o]}, #{user_id.to_i}, '#{r.created_at}', '#{r.updated_at}')" + end + end - poll_votes.flatten! - poll_votes.uniq! + poll_votes.flatten! + poll_votes.uniq! - if poll_votes.present? - execute <<~SQL + execute <<~SQL if poll_votes.present? INSERT INTO poll_votes (poll_id, poll_option_id, user_id, created_at, updated_at) VALUES #{poll_votes.join(",")} SQL end end end - end execute <<~SQL INSERT INTO post_custom_fields (name, value, post_id, created_at, updated_at) diff --git a/plugins/poll/jobs/regular/close_poll.rb b/plugins/poll/jobs/regular/close_poll.rb index c803e44376..8e2da6b986 100644 --- a/plugins/poll/jobs/regular/close_poll.rb +++ b/plugins/poll/jobs/regular/close_poll.rb @@ -1,14 +1,9 @@ # frozen_string_literal: true module Jobs - class ClosePoll < ::Jobs::Base - def execute(args) - %i{ - post_id - poll_name - }.each do |key| + %i[post_id poll_name].each do |key| raise Discourse::InvalidParameters.new(key) if args[key].blank? end @@ -17,10 +12,8 @@ module Jobs args[:post_id], args[:poll_name], "closed", - false + false, ) end - end - end diff --git a/plugins/poll/lib/poll.rb b/plugins/poll/lib/poll.rb index 1e5df985f2..fa34b0bf22 100644 --- a/plugins/poll/lib/poll.rb +++ b/plugins/poll/lib/poll.rb @@ -4,38 +4,42 @@ class DiscoursePoll::Poll def self.vote(user, post_id, poll_name, options) poll_id = nil - serialized_poll = DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll| - poll_id = poll.id - # remove options that aren't available in the poll - available_options = poll.poll_options.map { |o| o.digest }.to_set - options.select! { |o| available_options.include?(o) } + serialized_poll = + DiscoursePoll::Poll.change_vote(user, post_id, poll_name) do |poll| + poll_id = poll.id + # remove options that aren't available in the poll + available_options = poll.poll_options.map { |o| o.digest }.to_set + options.select! { |o| available_options.include?(o) } - raise DiscoursePoll::Error.new I18n.t("poll.requires_at_least_1_valid_option") if options.empty? + if options.empty? + raise DiscoursePoll::Error.new I18n.t("poll.requires_at_least_1_valid_option") + end - new_option_ids = poll.poll_options.each_with_object([]) do |option, obj| - obj << option.id if options.include?(option.digest) - end + new_option_ids = + poll + .poll_options + .each_with_object([]) do |option, obj| + obj << option.id if options.include?(option.digest) + end - self.validate_votes!(poll, new_option_ids) + self.validate_votes!(poll, new_option_ids) - old_option_ids = poll.poll_options.each_with_object([]) do |option, obj| - if option.poll_votes.where(user_id: user.id).exists? - obj << option.id + old_option_ids = + poll + .poll_options + .each_with_object([]) do |option, obj| + obj << option.id if option.poll_votes.where(user_id: user.id).exists? + end + + # remove non-selected votes + PollVote.where(poll: poll, user: user).where.not(poll_option_id: new_option_ids).delete_all + + # create missing votes + (new_option_ids - old_option_ids).each do |option_id| + PollVote.create!(poll: poll, user: user, poll_option_id: option_id) end end - # remove non-selected votes - PollVote - .where(poll: poll, user: user) - .where.not(poll_option_id: new_option_ids) - .delete_all - - # create missing votes - (new_option_ids - old_option_ids).each do |option_id| - PollVote.create!(poll: poll, user: user, poll_option_id: option_id) - end - end - # Ensure consistency here as we do not have a unique index to limit the # number of votes per the poll's configuration. is_multiple = serialized_poll[:type] == "multiple" @@ -79,20 +83,26 @@ class DiscoursePoll::Poll # topic must not be archived if post.topic&.archived - raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_toggle_status") if raise_errors + if raise_errors + raise DiscoursePoll::Error.new I18n.t("poll.topic_must_be_open_to_toggle_status") + end return end # either staff member or OP unless post.user_id == user&.id || user&.staff? - raise DiscoursePoll::Error.new I18n.t("poll.only_staff_or_op_can_toggle_status") if raise_errors + if raise_errors + raise DiscoursePoll::Error.new I18n.t("poll.only_staff_or_op_can_toggle_status") + end return end poll = Poll.find_by(post_id: post_id, name: poll_name) if !poll - raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) if raise_errors + if raise_errors + raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) + end return end @@ -110,7 +120,7 @@ class DiscoursePoll::Poll def self.serialized_voters(poll, opts = {}) limit = (opts["limit"] || 25).to_i - limit = 0 if limit < 0 + limit = 0 if limit < 0 limit = 50 if limit > 50 page = (opts["page"] || 1).to_i @@ -121,13 +131,14 @@ class DiscoursePoll::Poll option_digest = opts["option_id"].to_s if poll.number? - user_ids = PollVote - .where(poll: poll) - .group(:user_id) - .order("MIN(created_at)") - .offset(offset) - .limit(limit) - .pluck(:user_id) + user_ids = + PollVote + .where(poll: poll) + .group(:user_id) + .order("MIN(created_at)") + .offset(offset) + .limit(limit) + .pluck(:user_id) result = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash } elsif option_digest.present? @@ -135,13 +146,14 @@ class DiscoursePoll::Poll raise Discourse::InvalidParameters.new(:option_id) unless poll_option - user_ids = PollVote - .where(poll: poll, poll_option: poll_option) - .group(:user_id) - .order("MIN(created_at)") - .offset(offset) - .limit(limit) - .pluck(:user_id) + user_ids = + PollVote + .where(poll: poll, poll_option: poll_option) + .group(:user_id) + .order("MIN(created_at)") + .offset(offset) + .limit(limit) + .pluck(:user_id) user_hashes = User.where(id: user_ids).map { |u| UserNameSerializer.new(u).serializable_hash } @@ -163,10 +175,11 @@ class DiscoursePoll::Poll user_ids = votes.map(&:user_id).uniq - user_hashes = User - .where(id: user_ids) - .map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] } - .to_h + user_hashes = + User + .where(id: user_ids) + .map { |u| [u.id, UserNameSerializer.new(u).serializable_hash] } + .to_h result = {} votes.each do |v| @@ -186,10 +199,13 @@ class DiscoursePoll::Poll def self.grouped_poll_results(user, post_id, poll_name, user_field_name) raise Discourse::InvalidParameters.new(:post_id) if !Post.where(id: post_id).exists? - poll = Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name) + poll = + Poll.includes(:poll_options).includes(:poll_votes).find_by(post_id: post_id, name: poll_name) raise Discourse::InvalidParameters.new(:poll_name) unless poll - raise Discourse::InvalidParameters.new(:user_field_name) unless SiteSetting.poll_groupable_user_fields.split('|').include?(user_field_name) + unless SiteSetting.poll_groupable_user_fields.split("|").include?(user_field_name) + raise Discourse::InvalidParameters.new(:user_field_name) + end poll_votes = poll.poll_votes @@ -199,7 +215,11 @@ class DiscoursePoll::Poll end user_ids = poll_votes.map(&:user_id).uniq - user_fields = UserCustomField.where(user_id: user_ids, name: transform_for_user_field_override(user_field_name)) + user_fields = + UserCustomField.where( + user_id: user_ids, + name: transform_for_user_field_override(user_field_name), + ) user_field_map = {} user_fields.each do |f| @@ -207,78 +227,80 @@ class DiscoursePoll::Poll user_field_map[f.user_id] = f.value end - votes_with_field = poll_votes.map do |vote| - v = vote.attributes - v[:field_value] = user_field_map[vote.user_id] - v - end + votes_with_field = + poll_votes.map do |vote| + v = vote.attributes + v[:field_value] = user_field_map[vote.user_id] + v + end chart_data = [] - votes_with_field.group_by { |vote| vote[:field_value] }.each do |field_answer, votes| - grouped_selected_options = {} + votes_with_field + .group_by { |vote| vote[:field_value] } + .each do |field_answer, votes| + grouped_selected_options = {} - # Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option. - poll_options.each do |id, option| - grouped_selected_options[id] = { - digest: option[:digest], - html: option[:html], - votes: 0 - } + # Create all the options with 0 votes. This ensures all the charts will have the same order of options, and same colors per option. + poll_options.each do |id, option| + grouped_selected_options[id] = { digest: option[:digest], html: option[:html], votes: 0 } + end + + # Now go back and update the vote counts. Using hashes so we dont have n^2 + votes + .group_by { |v| v["poll_option_id"] } + .each do |option_id, votes_for_option| + grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length + end + + group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data") + chart_data << { group: group_label, options: grouped_selected_options.values } end - - # Now go back and update the vote counts. Using hashes so we dont have n^2 - votes.group_by { |v| v["poll_option_id"] }.each do |option_id, votes_for_option| - grouped_selected_options[option_id.to_s][:votes] = votes_for_option.length - end - - group_label = field_answer ? field_answer.titleize : I18n.t("poll.user_field.no_data") - chart_data << { group: group_label, options: grouped_selected_options.values } - end chart_data end def self.schedule_jobs(post) - Poll.where(post: post).find_each do |poll| - job_args = { - post_id: post.id, - poll_name: poll.name - } + Poll + .where(post: post) + .find_each do |poll| + job_args = { post_id: post.id, poll_name: poll.name } - Jobs.cancel_scheduled_job(:close_poll, job_args) + Jobs.cancel_scheduled_job(:close_poll, job_args) - if poll.open? && poll.close_at && poll.close_at > Time.zone.now - Jobs.enqueue_at(poll.close_at, :close_poll, job_args) + if poll.open? && poll.close_at && poll.close_at > Time.zone.now + Jobs.enqueue_at(poll.close_at, :close_poll, job_args) + end end - end end def self.create!(post_id, poll) - close_at = begin - Time.zone.parse(poll["close"] || '') - rescue ArgumentError - end + close_at = + begin + Time.zone.parse(poll["close"] || "") + rescue ArgumentError + end - created_poll = Poll.create!( - post_id: post_id, - name: poll["name"].presence || "poll", - close_at: close_at, - type: poll["type"].presence || "regular", - status: poll["status"].presence || "open", - visibility: poll["public"] == "true" ? "everyone" : "secret", - title: poll["title"], - results: poll["results"].presence || "always", - min: poll["min"], - max: poll["max"], - step: poll["step"], - chart_type: poll["charttype"] || "bar", - groups: poll["groups"] - ) + created_poll = + Poll.create!( + post_id: post_id, + name: poll["name"].presence || "poll", + close_at: close_at, + type: poll["type"].presence || "regular", + status: poll["status"].presence || "open", + visibility: poll["public"] == "true" ? "everyone" : "secret", + title: poll["title"], + results: poll["results"].presence || "always", + min: poll["min"], + max: poll["max"], + step: poll["step"], + chart_type: poll["charttype"] || "bar", + groups: poll["groups"], + ) poll["options"].each do |option| PollOption.create!( poll: created_poll, digest: option["id"].presence, - html: option["html"].presence&.strip + html: option["html"].presence&.strip, ) end end @@ -286,33 +308,38 @@ class DiscoursePoll::Poll def self.extract(raw, topic_id, user_id = nil) # TODO: we should fix the callback mess so that the cooked version is available # in the validators instead of cooking twice - raw = raw.sub(/\[quote.+\/quote\]/m, '') + raw = raw.sub(%r{\[quote.+/quote\]}m, "") cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id) - Nokogiri::HTML5(cooked).css("div.poll").map do |p| - poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME } + Nokogiri + .HTML5(cooked) + .css("div.poll") + .map do |p| + poll = { "options" => [], "name" => DiscoursePoll::DEFAULT_POLL_NAME } - # attributes - p.attributes.values.each do |attribute| - if attribute.name.start_with?(DiscoursePoll::DATA_PREFIX) - poll[attribute.name[DiscoursePoll::DATA_PREFIX.length..-1]] = CGI.escapeHTML(attribute.value || "") + # attributes + p.attributes.values.each do |attribute| + if attribute.name.start_with?(DiscoursePoll::DATA_PREFIX) + poll[attribute.name[DiscoursePoll::DATA_PREFIX.length..-1]] = CGI.escapeHTML( + attribute.value || "", + ) + end end - end - # options - p.css("li[#{DiscoursePoll::DATA_PREFIX}option-id]").each do |o| - option_id = o.attributes[DiscoursePoll::DATA_PREFIX + "option-id"].value.to_s - poll["options"] << { "id" => option_id, "html" => o.inner_html.strip } - end + # options + p + .css("li[#{DiscoursePoll::DATA_PREFIX}option-id]") + .each do |o| + option_id = o.attributes[DiscoursePoll::DATA_PREFIX + "option-id"].value.to_s + poll["options"] << { "id" => option_id, "html" => o.inner_html.strip } + end - # title - title_element = p.css(".poll-title").first - if title_element - poll["title"] = title_element.inner_html.strip - end + # title + title_element = p.css(".poll-title").first + poll["title"] = title_element.inner_html.strip if title_element - poll - end + poll + end end def self.validate_votes!(poll, options) @@ -320,15 +347,9 @@ class DiscoursePoll::Poll if poll.multiple? if poll.min && (num_of_options < poll.min) - raise DiscoursePoll::Error.new(I18n.t( - "poll.min_vote_per_user", - count: poll.min - )) + raise DiscoursePoll::Error.new(I18n.t("poll.min_vote_per_user", count: poll.min)) elsif poll.max && (num_of_options > poll.max) - raise DiscoursePoll::Error.new(I18n.t( - "poll.max_vote_per_user", - count: poll.max - )) + raise DiscoursePoll::Error.new(I18n.t("poll.max_vote_per_user", count: poll.max)) end elsif num_of_options > 1 raise DiscoursePoll::Error.new(I18n.t("poll.one_vote_per_user")) @@ -341,9 +362,7 @@ class DiscoursePoll::Poll post = Post.find_by(id: post_id) # post must not be deleted - if post.nil? || post.trashed? - raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted") - end + raise DiscoursePoll::Error.new I18n.t("poll.post_is_deleted") if post.nil? || post.trashed? # topic must not be archived if post.topic&.archived @@ -358,7 +377,9 @@ class DiscoursePoll::Poll poll = Poll.includes(:poll_options).find_by(post_id: post_id, name: poll_name) - raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) unless poll + unless poll + raise DiscoursePoll::Error.new I18n.t("poll.no_poll_with_this_name", name: poll_name) + end raise DiscoursePoll::Error.new I18n.t("poll.poll_must_be_open_to_vote") if poll.is_closed? if poll.groups diff --git a/plugins/poll/lib/polls_updater.rb b/plugins/poll/lib/polls_updater.rb index 39b48f45d2..22c74fd810 100644 --- a/plugins/poll/lib/polls_updater.rb +++ b/plugins/poll/lib/polls_updater.rb @@ -2,8 +2,7 @@ module DiscoursePoll class PollsUpdater - - POLL_ATTRIBUTES ||= %w{close_at max min results status step type visibility title groups} + POLL_ATTRIBUTES ||= %w[close_at max min results status step type visibility title groups] def self.update(post, polls) ::Poll.transaction do @@ -24,64 +23,81 @@ module DiscoursePoll # create polls if created_poll_names.present? has_changed = true - polls.slice(*created_poll_names).values.each do |poll| - Poll.create!(post.id, poll) - end + polls.slice(*created_poll_names).values.each { |poll| Poll.create!(post.id, poll) } end # update polls - ::Poll.includes(:poll_votes, :poll_options).where(post: post).find_each do |old_poll| - new_poll = polls[old_poll.name] - new_poll_options = new_poll["options"] + ::Poll + .includes(:poll_votes, :poll_options) + .where(post: post) + .find_each do |old_poll| + new_poll = polls[old_poll.name] + new_poll_options = new_poll["options"] - attributes = new_poll.slice(*POLL_ATTRIBUTES) - attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret" - attributes["close_at"] = Time.zone.parse(new_poll["close"]) rescue nil - attributes["status"] = old_poll["status"] - attributes["groups"] = new_poll["groups"] - poll = ::Poll.new(attributes) + attributes = new_poll.slice(*POLL_ATTRIBUTES) + attributes["visibility"] = new_poll["public"] == "true" ? "everyone" : "secret" + attributes["close_at"] = begin + Time.zone.parse(new_poll["close"]) + rescue StandardError + nil + end + attributes["status"] = old_poll["status"] + attributes["groups"] = new_poll["groups"] + poll = ::Poll.new(attributes) - if is_different?(old_poll, poll, new_poll_options) + if is_different?(old_poll, poll, new_poll_options) + # only prevent changes when there's at least 1 vote + if old_poll.poll_votes.size > 0 + # can't change after edit window (when enabled) + if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago + error = + ( + if poll.name == DiscoursePoll::DEFAULT_POLL_NAME + I18n.t( + "poll.edit_window_expired.cannot_edit_default_poll_with_votes", + minutes: edit_window, + ) + else + I18n.t( + "poll.edit_window_expired.cannot_edit_named_poll_with_votes", + minutes: edit_window, + name: poll.name, + ) + end + ) - # only prevent changes when there's at least 1 vote - if old_poll.poll_votes.size > 0 - # can't change after edit window (when enabled) - if edit_window > 0 && old_poll.created_at < edit_window.minutes.ago - error = poll.name == DiscoursePoll::DEFAULT_POLL_NAME ? - I18n.t("poll.edit_window_expired.cannot_edit_default_poll_with_votes", minutes: edit_window) : - I18n.t("poll.edit_window_expired.cannot_edit_named_poll_with_votes", minutes: edit_window, name: poll.name) - - post.errors.add(:base, error) - return + post.errors.add(:base, error) + return + end end + + # update poll + POLL_ATTRIBUTES.each do |attr| + old_poll.public_send("#{attr}=", poll.public_send(attr)) + end + + old_poll.save! + + # keep track of anonymous votes + anonymous_votes = + old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h + + # destroy existing options & votes + ::PollOption.where(poll: old_poll).destroy_all + + # create new options + new_poll_options.each do |option| + ::PollOption.create!( + poll: old_poll, + digest: option["id"], + html: option["html"].strip, + anonymous_votes: anonymous_votes[option["id"]], + ) + end + + has_changed = true end - - # update poll - POLL_ATTRIBUTES.each do |attr| - old_poll.public_send("#{attr}=", poll.public_send(attr)) - end - - old_poll.save! - - # keep track of anonymous votes - anonymous_votes = old_poll.poll_options.map { |pv| [pv.digest, pv.anonymous_votes] }.to_h - - # destroy existing options & votes - ::PollOption.where(poll: old_poll).destroy_all - - # create new options - new_poll_options.each do |option| - ::PollOption.create!( - poll: old_poll, - digest: option["id"], - html: option["html"].strip, - anonymous_votes: anonymous_votes[option["id"]], - ) - end - - has_changed = true end - end if ::Poll.exists?(post: post) post.custom_fields[HAS_POLLS] = true @@ -93,7 +109,13 @@ module DiscoursePoll if has_changed polls = ::Poll.includes(poll_options: :poll_votes).where(post: post) - polls = ActiveModel::ArraySerializer.new(polls, each_serializer: PollSerializer, root: false, scope: Guardian.new(nil)).as_json + polls = + ActiveModel::ArraySerializer.new( + polls, + each_serializer: PollSerializer, + root: false, + scope: Guardian.new(nil), + ).as_json post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls) end end @@ -108,11 +130,12 @@ module DiscoursePoll end # an option was changed? - return true if old_poll.poll_options.map { |o| o.digest }.sort != new_options.map { |o| o["id"] }.sort + if old_poll.poll_options.map { |o| o.digest }.sort != new_options.map { |o| o["id"] }.sort + return true + end # it's the same! false end - end end diff --git a/plugins/poll/lib/polls_validator.rb b/plugins/poll/lib/polls_validator.rb index ecdf3c849c..8a0846906c 100644 --- a/plugins/poll/lib/polls_validator.rb +++ b/plugins/poll/lib/polls_validator.rb @@ -2,7 +2,6 @@ module DiscoursePoll class PollsValidator - MAX_VALUE = 2_147_483_647 def initialize(post) @@ -12,17 +11,19 @@ module DiscoursePoll def validate_polls polls = {} - DiscoursePoll::Poll::extract(@post.raw, @post.topic_id, @post.user_id).each do |poll| - return false unless valid_arguments?(poll) - return false unless valid_numbers?(poll) - return false unless unique_poll_name?(polls, poll) - return false unless unique_options?(poll) - return false unless any_blank_options?(poll) - return false unless at_least_one_option?(poll) - return false unless valid_number_of_options?(poll) - return false unless valid_multiple_choice_settings?(poll) - polls[poll["name"]] = poll - end + DiscoursePoll::Poll + .extract(@post.raw, @post.topic_id, @post.user_id) + .each do |poll| + return false unless valid_arguments?(poll) + return false unless valid_numbers?(poll) + return false unless unique_poll_name?(polls, poll) + return false unless unique_options?(poll) + return false unless any_blank_options?(poll) + return false unless at_least_one_option?(poll) + return false unless valid_number_of_options?(poll) + return false unless valid_multiple_choice_settings?(poll) + polls[poll["name"]] = poll + end polls end @@ -33,17 +34,26 @@ module DiscoursePoll valid = true if poll["type"].present? && !::Poll.types.has_key?(poll["type"]) - @post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "type", value: poll["type"])) + @post.errors.add( + :base, + I18n.t("poll.invalid_argument", argument: "type", value: poll["type"]), + ) valid = false end if poll["status"].present? && !::Poll.statuses.has_key?(poll["status"]) - @post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "status", value: poll["status"])) + @post.errors.add( + :base, + I18n.t("poll.invalid_argument", argument: "status", value: poll["status"]), + ) valid = false end if poll["results"].present? && !::Poll.results.has_key?(poll["results"]) - @post.errors.add(:base, I18n.t("poll.invalid_argument", argument: "results", value: poll["results"])) + @post.errors.add( + :base, + I18n.t("poll.invalid_argument", argument: "results", value: poll["results"]), + ) valid = false end @@ -69,7 +79,10 @@ module DiscoursePoll if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME @post.errors.add(:base, I18n.t("poll.default_poll_must_have_different_options")) else - @post.errors.add(:base, I18n.t("poll.named_poll_must_have_different_options", name: poll["name"])) + @post.errors.add( + :base, + I18n.t("poll.named_poll_must_have_different_options", name: poll["name"]), + ) end return false @@ -83,7 +96,10 @@ module DiscoursePoll if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME @post.errors.add(:base, I18n.t("poll.default_poll_must_not_have_any_empty_options")) else - @post.errors.add(:base, I18n.t("poll.named_poll_must_not_have_any_empty_options", name: poll["name"])) + @post.errors.add( + :base, + I18n.t("poll.named_poll_must_not_have_any_empty_options", name: poll["name"]), + ) end return false @@ -97,7 +113,10 @@ module DiscoursePoll if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME @post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option")) else - @post.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"])) + @post.errors.add( + :base, + I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"]), + ) end return false @@ -109,9 +128,22 @@ module DiscoursePoll def valid_number_of_options?(poll) if poll["options"].size > SiteSetting.poll_maximum_options if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME - @post.errors.add(:base, I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options)) + @post.errors.add( + :base, + I18n.t( + "poll.default_poll_must_have_less_options", + count: SiteSetting.poll_maximum_options, + ), + ) else - @post.errors.add(:base, I18n.t("poll.named_poll_must_have_less_options", name: poll["name"], count: SiteSetting.poll_maximum_options)) + @post.errors.add( + :base, + I18n.t( + "poll.named_poll_must_have_less_options", + name: poll["name"], + count: SiteSetting.poll_maximum_options, + ), + ) end return false @@ -128,9 +160,18 @@ module DiscoursePoll if min > max || min <= 0 || max <= 0 || max > options || min >= options if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME - @post.errors.add(:base, I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters")) + @post.errors.add( + :base, + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), + ) else - @post.errors.add(:base, I18n.t("poll.named_poll_with_multiple_choices_has_invalid_parameters", name: poll["name"])) + @post.errors.add( + :base, + I18n.t( + "poll.named_poll_with_multiple_choices_has_invalid_parameters", + name: poll["name"], + ), + ) end return false @@ -172,7 +213,10 @@ module DiscoursePoll if poll["name"] == ::DiscoursePoll::DEFAULT_POLL_NAME @post.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_1_option")) else - @post.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"])) + @post.errors.add( + :base, + I18n.t("poll.named_poll_must_have_at_least_1_option", name: poll["name"]), + ) end valid = false end diff --git a/plugins/poll/lib/post_validator.rb b/plugins/poll/lib/post_validator.rb index 5d21f70a8c..db3b52b5f0 100644 --- a/plugins/poll/lib/post_validator.rb +++ b/plugins/poll/lib/post_validator.rb @@ -9,7 +9,13 @@ module DiscoursePoll def validate_post min_trust_level = SiteSetting.poll_minimum_trust_level_to_create - if (@post.acting_user && (@post.acting_user.staff? || @post.acting_user.trust_level >= TrustLevel[min_trust_level])) || @post.topic&.pm_with_non_human_user? + if ( + @post.acting_user && + ( + @post.acting_user.staff? || + @post.acting_user.trust_level >= TrustLevel[min_trust_level] + ) + ) || @post.topic&.pm_with_non_human_user? true else @post.errors.add(:base, I18n.t("poll.insufficient_rights_to_create")) diff --git a/plugins/poll/lib/tasks/migrate_old_polls.rake b/plugins/poll/lib/tasks/migrate_old_polls.rake index 91046fb1df..73c31498e1 100644 --- a/plugins/poll/lib/tasks/migrate_old_polls.rake +++ b/plugins/poll/lib/tasks/migrate_old_polls.rake @@ -28,57 +28,63 @@ end desc "Migrate old polls to new syntax" task "poll:migrate_old_polls" => :environment do # iterate over all polls - PluginStoreRow.where(plugin_name: "poll") + PluginStoreRow + .where(plugin_name: "poll") .where("key LIKE 'poll_options_%'") .pluck(:key) .each do |poll_options_key| - # extract the post_id - post_id = poll_options_key["poll_options_".length..-1].to_i - # load the post from the db - if post = Post.find_by(id: post_id) - putc "." - # skip if already migrated - next if post.custom_fields.include?("polls") - # go back in time - freeze_time(post.created_at + 1.minute) do - raw = post.raw.gsub(/\n\n([ ]*[-\*\+] )/, "\n\\1") + "\n\n" - # fix the RAW when needed - if raw !~ /\[poll\]/ - lists = /^[ ]*[-\*\+] .+?$\n\n/m.match(raw) - next if lists.blank? || lists.length == 0 - first_list = lists[0] - raw = raw.sub(first_list, "\n[poll]\n#{first_list}\n[/poll]\n") - end - # save the poll - post.raw = raw - post.save - # make sure we have a poll - next if post.custom_fields.blank? || !post.custom_fields.include?("polls") - # retrieve the new options - options = post.custom_fields["polls"]["poll"]["options"] - # iterate over all votes - PluginStoreRow.where(plugin_name: "poll") - .where("key LIKE ?", "poll_vote_#{post_id}_%") - .pluck(:key, :value) - .each do |poll_vote_key, vote| - # extract the user_id - user_id = poll_vote_key["poll_vote_#{post_id}_%".length..-1].to_i - # find the selected option - vote = vote.strip - selected_option = options.detect { |o| o["html"].strip === vote } - # make sure we have a match - next if selected_option.blank? - # submit vote - DiscoursePoll::Poll.vote(post_id, "poll", [selected_option["id"]], user_id) rescue nil - end - # close the poll - if post.topic.archived? || post.topic.closed? || poll_was_closed?(post.topic.title) - post.custom_fields["polls"]["poll"]["status"] = "closed" - post.save_custom_fields(true) + # extract the post_id + post_id = poll_options_key["poll_options_".length..-1].to_i + # load the post from the db + if post = Post.find_by(id: post_id) + putc "." + # skip if already migrated + next if post.custom_fields.include?("polls") + # go back in time + freeze_time(post.created_at + 1.minute) do + raw = post.raw.gsub(/\n\n([ ]*[-\*\+] )/, "\n\\1") + "\n\n" + # fix the RAW when needed + if raw !~ /\[poll\]/ + lists = /^[ ]*[-\*\+] .+?$\n\n/m.match(raw) + next if lists.blank? || lists.length == 0 + first_list = lists[0] + raw = raw.sub(first_list, "\n[poll]\n#{first_list}\n[/poll]\n") + end + # save the poll + post.raw = raw + post.save + # make sure we have a poll + next if post.custom_fields.blank? || !post.custom_fields.include?("polls") + # retrieve the new options + options = post.custom_fields["polls"]["poll"]["options"] + # iterate over all votes + PluginStoreRow + .where(plugin_name: "poll") + .where("key LIKE ?", "poll_vote_#{post_id}_%") + .pluck(:key, :value) + .each do |poll_vote_key, vote| + # extract the user_id + user_id = poll_vote_key["poll_vote_#{post_id}_%".length..-1].to_i + # find the selected option + vote = vote.strip + selected_option = options.detect { |o| o["html"].strip === vote } + # make sure we have a match + next if selected_option.blank? + # submit vote + begin + DiscoursePoll::Poll.vote(post_id, "poll", [selected_option["id"]], user_id) + rescue StandardError + nil + end + end + # close the poll + if post.topic.archived? || post.topic.closed? || poll_was_closed?(post.topic.title) + post.custom_fields["polls"]["poll"]["status"] = "closed" + post.save_custom_fields(true) + end end end end - end puts "", "Done!" end diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index 280bb5f3ec..b681593027 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -30,7 +30,8 @@ after_initialize do isolate_namespace DiscoursePoll end - class Error < StandardError; end + class Error < StandardError + end end require_relative "app/controllers/polls_controller.rb" @@ -49,13 +50,11 @@ after_initialize do put "/vote" => "polls#vote" delete "/vote" => "polls#remove_vote" put "/toggle_status" => "polls#toggle_status" - get "/voters" => 'polls#voters' - get "/grouped_poll_results" => 'polls#grouped_poll_results' + get "/voters" => "polls#voters" + get "/grouped_poll_results" => "polls#grouped_poll_results" end - Discourse::Application.routes.append do - mount ::DiscoursePoll::Engine, at: "/polls" - end + Discourse::Application.routes.append { mount ::DiscoursePoll::Engine, at: "/polls" } allow_new_queued_post_payload_attribute("is_poll") register_post_custom_field_type(DiscoursePoll::HAS_POLLS, :boolean) @@ -74,18 +73,14 @@ after_initialize do post = self Poll.transaction do - polls.values.each do |poll| - DiscoursePoll::Poll.create!(post.id, poll) - end + polls.values.each { |poll| DiscoursePoll::Poll.create!(post.id, poll) } post.custom_fields[DiscoursePoll::HAS_POLLS] = true post.save_custom_fields(true) end end end - User.class_eval do - has_many :poll_votes, dependent: :delete_all - end + User.class_eval { has_many :poll_votes, dependent: :delete_all } end validate(:post, :validate_polls) do |force = nil| @@ -115,9 +110,7 @@ after_initialize do if !DiscoursePoll::PollsValidator.new(post).validate_polls result = NewPostResult.new(:poll, false) - post.errors.full_messages.each do |message| - result.add_error(message) - end + post.errors.full_messages.each { |message| result.add_error(message) } result else @@ -127,9 +120,7 @@ after_initialize do end on(:approved_post) do |queued_post, created_post| - if queued_post.payload["is_poll"] - created_post.validate_polls(true) - end + created_post.validate_polls(true) if queued_post.payload["is_poll"] end on(:reduce_cooked) do |fragment, post| @@ -137,22 +128,27 @@ after_initialize do fragment.css(".poll, [data-poll-name]").each(&:remove) else post_url = post.full_url - fragment.css(".poll, [data-poll-name]").each do |poll| - poll.replace "

    #{I18n.t("poll.email.link_to_poll")}

    " - end + fragment + .css(".poll, [data-poll-name]") + .each do |poll| + poll.replace "

    #{I18n.t("poll.email.link_to_poll")}

    " + end end end on(:reduce_excerpt) do |doc, options| post = options[:post] - replacement = post&.url.present? ? - "#{I18n.t("poll.poll")}" : - I18n.t("poll.poll") + replacement = + ( + if post&.url.present? + "#{I18n.t("poll.poll")}" + else + I18n.t("poll.poll") + end + ) - doc.css("div.poll").each do |poll| - poll.replace(replacement) - end + doc.css("div.poll").each { |poll| poll.replace(replacement) } end on(:post_created) do |post, _opts, user| @@ -162,7 +158,13 @@ after_initialize do next if post.is_first_post? next if post.custom_fields[DiscoursePoll::HAS_POLLS].blank? - polls = ActiveModel::ArraySerializer.new(post.polls, each_serializer: PollSerializer, root: false, scope: guardian).as_json + polls = + ActiveModel::ArraySerializer.new( + post.polls, + each_serializer: PollSerializer, + root: false, + scope: guardian, + ).as_json post.publish_message!("/polls/#{post.topic_id}", post_id: post.id, polls: polls) end @@ -171,63 +173,62 @@ after_initialize do end add_to_class(:topic_view, :polls) do - @polls ||= begin - polls = {} + @polls ||= + begin + polls = {} - post_with_polls = @post_custom_fields.each_with_object([]) do |fields, obj| - obj << fields[0] if fields[1][DiscoursePoll::HAS_POLLS] - end - - if post_with_polls.present? - Poll - .where(post_id: post_with_polls) - .each do |p| - polls[p.post_id] ||= [] - polls[p.post_id] << p + post_with_polls = + @post_custom_fields.each_with_object([]) do |fields, obj| + obj << fields[0] if fields[1][DiscoursePoll::HAS_POLLS] end - end - polls - end + if post_with_polls.present? + Poll + .where(post_id: post_with_polls) + .each do |p| + polls[p.post_id] ||= [] + polls[p.post_id] << p + end + end + + polls + end end add_to_serializer(:post, :preloaded_polls, false) do - @preloaded_polls ||= if @topic_view.present? - @topic_view.polls[object.id] - else - Poll.includes(:poll_options).where(post: object) - end + @preloaded_polls ||= + if @topic_view.present? + @topic_view.polls[object.id] + else + Poll.includes(:poll_options).where(post: object) + end end - add_to_serializer(:post, :include_preloaded_polls?) do - false - end + add_to_serializer(:post, :include_preloaded_polls?) { false } add_to_serializer(:post, :polls, false) do preloaded_polls.map { |p| PollSerializer.new(p, root: false, scope: self.scope) } end - add_to_serializer(:post, :include_polls?) do - SiteSetting.poll_enabled && preloaded_polls.present? - end + add_to_serializer(:post, :include_polls?) { SiteSetting.poll_enabled && preloaded_polls.present? } add_to_serializer(:post, :polls_votes, false) do - preloaded_polls.map do |poll| - user_poll_votes = - poll - .poll_votes - .where(user_id: scope.user.id) - .joins(:poll_option) - .pluck("poll_options.digest") + preloaded_polls + .map do |poll| + user_poll_votes = + poll + .poll_votes + .where(user_id: scope.user.id) + .joins(:poll_option) + .pluck("poll_options.digest") - [poll.name, user_poll_votes] - end.to_h + [poll.name, user_poll_votes] + end + .to_h end add_to_serializer(:post, :include_polls_votes?) do - SiteSetting.poll_enabled && - scope.user&.id.present? && - preloaded_polls.present? && - preloaded_polls.any? { |p| p.has_voted?(scope.user) } + SiteSetting.poll_enabled && scope.user&.id.present? && preloaded_polls.present? && + preloaded_polls.any? { |p| p.has_voted?(scope.user) } end end diff --git a/plugins/poll/spec/controllers/polls_controller_spec.rb b/plugins/poll/spec/controllers/polls_controller_spec.rb index a37fc7413f..7e8f813ccc 100644 --- a/plugins/poll/spec/controllers/polls_controller_spec.rb +++ b/plugins/poll/spec/controllers/polls_controller_spec.rb @@ -7,20 +7,48 @@ RSpec.describe ::DiscoursePoll::PollsController do let!(:user) { log_in } let(:topic) { Fabricate(:topic) } - let(:poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") } - let(:multi_poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll min=1 max=2 type=multiple public=true]\n- A\n- B\n[/poll]") } - let(:public_poll_on_vote) { Fabricate(:post, topic: topic, user: user, raw: "[poll public=true results=on_vote]\n- A\n- B\n[/poll]") } - let(:public_poll_on_close) { Fabricate(:post, topic: topic, user: user, raw: "[poll public=true results=on_close]\n- A\n- B\n[/poll]") } + let(:poll) { Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") } + let(:multi_poll) do + Fabricate( + :post, + topic: topic, + user: user, + raw: "[poll min=1 max=2 type=multiple public=true]\n- A\n- B\n[/poll]", + ) + end + let(:public_poll_on_vote) do + Fabricate( + :post, + topic: topic, + user: user, + raw: "[poll public=true results=on_vote]\n- A\n- B\n[/poll]", + ) + end + let(:public_poll_on_close) do + Fabricate( + :post, + topic: topic, + user: user, + raw: "[poll public=true results=on_close]\n- A\n- B\n[/poll]", + ) + end describe "#vote" do it "works" do channel = "/polls/#{poll.topic_id}" - message = MessageBus.track_publish(channel) do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json - end.first + message = + MessageBus + .track_publish(channel) do + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json + end + .first expect(response.status).to eq(200) @@ -36,19 +64,30 @@ RSpec.describe ::DiscoursePoll::PollsController do it "works in PM" do user2 = Fabricate(:user) - topic = Fabricate(:private_message_topic, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: user), - Fabricate.build(:topic_allowed_user, user: user2) - ]) + topic = + Fabricate( + :private_message_topic, + topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: user), + Fabricate.build(:topic_allowed_user, user: user2), + ], + ) poll = Fabricate(:post, topic: topic, user: user, raw: "[poll]\n- A\n- B\n[/poll]") channel = "/polls/#{poll.topic_id}" - message = MessageBus.track_publish(channel) do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json - end.first + message = + MessageBus + .track_publish(channel) do + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json + end + .first expect(response.status).to eq(200) @@ -71,11 +110,18 @@ RSpec.describe ::DiscoursePoll::PollsController do channel = "/polls/#{poll.topic_id}" - message = MessageBus.track_publish(channel) do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json - end.first + message = + MessageBus + .track_publish(channel) do + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json + end + .first expect(response.status).to eq(200) @@ -90,9 +136,7 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "requires at least 1 valid option" do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["A", "B"] - }, format: :json + put :vote, params: { post_id: poll.id, poll_name: "poll", options: %w[A B] }, format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -100,15 +144,23 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "supports vote changes" do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).to eq(200) - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["e89dec30bbd9bf50fabf6a05b4324edf"], + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -118,15 +170,17 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "supports removing votes" do - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).to eq(200) - delete :remove_vote, params: { - post_id: poll.id, poll_name: "poll" - }, format: :json + delete :remove_vote, params: { post_id: poll.id, poll_name: "poll" }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -138,9 +192,13 @@ RSpec.describe ::DiscoursePoll::PollsController do it "works on closed topics" do topic.update_attribute(:closed, true) - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).to eq(200) end @@ -148,9 +206,7 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures topic is not archived" do topic.update_attribute(:archived, true) - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["A"] - }, format: :json + put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -160,9 +216,7 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures post is not trashed" do poll.trash! - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["A"] - }, format: :json + put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -172,9 +226,7 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures user can post in topic" do Guardian.any_instance.expects(:can_create_post?).returns(false) - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["A"] - }, format: :json + put :vote, params: { post_id: poll.id, poll_name: "poll", options: ["A"] }, format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -182,9 +234,7 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "checks the name of the poll" do - put :vote, params: { - post_id: poll.id, poll_name: "foobar", options: ["A"] - }, format: :json + put :vote, params: { post_id: poll.id, poll_name: "foobar", options: ["A"] }, format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -194,9 +244,13 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures poll is open" do closed_poll = create_post(raw: "[poll status=closed]\n- A\n- B\n[/poll]") - put :vote, params: { - post_id: closed_poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: closed_poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -206,13 +260,19 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures user has required trust level" do poll = create_post(raw: "[poll groups=#{Fabricate(:group).name}]\n- A\n- B\n[/poll]") - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).not_to eq(200) json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t("js.poll.results.groups.title", groups: poll.polls.first.groups)) + expect(json["errors"][0]).to eq( + I18n.t("js.poll.results.groups.title", groups: poll.polls.first.groups), + ) end it "doesn't discard anonymous votes when someone votes" do @@ -221,9 +281,13 @@ RSpec.describe ::DiscoursePoll::PollsController do the_poll.poll_options[0].update_attribute(:anonymous_votes, 11) the_poll.poll_options[1].update_attribute(:anonymous_votes, 6) - put :vote, params: { - post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] - }, format: :json + put :vote, + params: { + post_id: poll.id, + poll_name: "poll", + options: ["5c24fc1df56d764b550ceae1b9319125"], + }, + format: :json expect(response.status).to eq(200) @@ -238,13 +302,20 @@ RSpec.describe ::DiscoursePoll::PollsController do it "works for OP" do channel = "/polls/#{poll.topic_id}" - message = MessageBus.track_publish(channel) do - put :toggle_status, params: { - post_id: poll.id, poll_name: "poll", status: "closed" - }, format: :json + message = + MessageBus + .track_publish(channel) do + put :toggle_status, + params: { + post_id: poll.id, + poll_name: "poll", + status: "closed", + }, + format: :json - expect(response.status).to eq(200) - end.first + expect(response.status).to eq(200) + end + .first json = response.parsed_body expect(json["poll"]["status"]).to eq("closed") @@ -256,13 +327,20 @@ RSpec.describe ::DiscoursePoll::PollsController do channel = "/polls/#{poll.topic_id}" - message = MessageBus.track_publish(channel) do - put :toggle_status, params: { - post_id: poll.id, poll_name: "poll", status: "closed" - }, format: :json + message = + MessageBus + .track_publish(channel) do + put :toggle_status, + params: { + post_id: poll.id, + poll_name: "poll", + status: "closed", + }, + format: :json - expect(response.status).to eq(200) - end.first + expect(response.status).to eq(200) + end + .first json = response.parsed_body expect(json["poll"]["status"]).to eq("closed") @@ -272,9 +350,13 @@ RSpec.describe ::DiscoursePoll::PollsController do it "ensures post is not trashed" do poll.trash! - put :toggle_status, params: { - post_id: poll.id, poll_name: "poll", status: "closed" - }, format: :json + put :toggle_status, + params: { + post_id: poll.id, + poll_name: "poll", + status: "closed", + }, + format: :json expect(response.status).not_to eq(200) json = response.parsed_body @@ -289,31 +371,41 @@ RSpec.describe ::DiscoursePoll::PollsController do it "correctly handles offset" do user1 = log_in - put :vote, params: { - post_id: multi_poll.id, poll_name: "poll", options: [first] - }, format: :json + put :vote, + params: { + post_id: multi_poll.id, + poll_name: "poll", + options: [first], + }, + format: :json expect(response.status).to eq(200) user2 = log_in - put :vote, params: { - post_id: multi_poll.id, poll_name: "poll", options: [first] - }, format: :json + put :vote, + params: { + post_id: multi_poll.id, + poll_name: "poll", + options: [first], + }, + format: :json expect(response.status).to eq(200) user3 = log_in - put :vote, params: { - post_id: multi_poll.id, poll_name: "poll", options: [first, second] - }, format: :json + put :vote, + params: { + post_id: multi_poll.id, + poll_name: "poll", + options: [first, second], + }, + format: :json expect(response.status).to eq(200) - get :voters, params: { - poll_name: 'poll', post_id: multi_poll.id, limit: 2 - }, format: :json + get :voters, params: { poll_name: "poll", post_id: multi_poll.id, limit: 2 }, format: :json expect(response.status).to eq(200) @@ -325,15 +417,17 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "ensures voters can only be seen after casting a vote" do - put :vote, params: { - post_id: public_poll_on_vote.id, poll_name: "poll", options: [first] - }, format: :json + put :vote, + params: { + post_id: public_poll_on_vote.id, + poll_name: "poll", + options: [first], + }, + format: :json expect(response.status).to eq(200) - get :voters, params: { - poll_name: "poll", post_id: public_poll_on_vote.id - }, format: :json + get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json expect(response.status).to eq(200) @@ -343,21 +437,21 @@ RSpec.describe ::DiscoursePoll::PollsController do _user2 = log_in - get :voters, params: { - poll_name: "poll", post_id: public_poll_on_vote.id - }, format: :json + get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json expect(response.status).to eq(400) - put :vote, params: { - post_id: public_poll_on_vote.id, poll_name: "poll", options: [second] - }, format: :json + put :vote, + params: { + post_id: public_poll_on_vote.id, + poll_name: "poll", + options: [second], + }, + format: :json expect(response.status).to eq(200) - get :voters, params: { - poll_name: "poll", post_id: public_poll_on_vote.id - }, format: :json + get :voters, params: { poll_name: "poll", post_id: public_poll_on_vote.id }, format: :json expect(response.status).to eq(200) @@ -368,27 +462,31 @@ RSpec.describe ::DiscoursePoll::PollsController do end it "ensures voters can only be seen when poll is closed" do - put :vote, params: { - post_id: public_poll_on_close.id, poll_name: "poll", options: [first] - }, format: :json + put :vote, + params: { + post_id: public_poll_on_close.id, + poll_name: "poll", + options: [first], + }, + format: :json expect(response.status).to eq(200) - get :voters, params: { - poll_name: "poll", post_id: public_poll_on_close.id - }, format: :json + get :voters, params: { poll_name: "poll", post_id: public_poll_on_close.id }, format: :json expect(response.status).to eq(400) - put :toggle_status, params: { - post_id: public_poll_on_close.id, poll_name: "poll", status: "closed" - }, format: :json + put :toggle_status, + params: { + post_id: public_poll_on_close.id, + poll_name: "poll", + status: "closed", + }, + format: :json expect(response.status).to eq(200) - get :voters, params: { - poll_name: "poll", post_id: public_poll_on_close.id - }, format: :json + get :voters, params: { poll_name: "poll", post_id: public_poll_on_close.id }, format: :json expect(response.status).to eq(200) diff --git a/plugins/poll/spec/controllers/posts_controller_spec.rb b/plugins/poll/spec/controllers/posts_controller_spec.rb index dec4cf0190..fb80ff3a98 100644 --- a/plugins/poll/spec/controllers/posts_controller_spec.rb +++ b/plugins/poll/spec/controllers/posts_controller_spec.rb @@ -6,15 +6,11 @@ RSpec.describe PostsController do let!(:user) { log_in } let!(:title) { "Testing Poll Plugin" } - before do - SiteSetting.min_first_post_typing_time = 0 - end + before { SiteSetting.min_first_post_typing_time = 0 } describe "polls" do it "works" do - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -25,9 +21,12 @@ RSpec.describe PostsController do it "works on any post" do post_1 = Fabricate(:post) - post :create, params: { - topic_id: post_1.topic.id, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + topic_id: post_1.topic.id, + raw: "[poll]\n- A\n- B\n[/poll]", + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -41,12 +40,13 @@ RSpec.describe PostsController do close_date = 1.month.from_now.round expect do - post :create, params: { - title: title, - raw: "[poll name=#{name} close=#{close_date.iso8601}]\n- A\n- B\n[/poll]" - }, format: :json - end.to change { Jobs::ClosePoll.jobs.size }.by(1) & - change { Poll.count }.by(1) + post :create, + params: { + title: title, + raw: "[poll name=#{name} close=#{close_date.iso8601}]\n- A\n- B\n[/poll]", + }, + format: :json + end.to change { Jobs::ClosePoll.jobs.size }.by(1) & change { Poll.count }.by(1) expect(response.status).to eq(200) json = response.parsed_body @@ -62,9 +62,7 @@ RSpec.describe PostsController do end it "should have different options" do - post :create, params: { - title: title, raw: "[poll]\n- A\n- A\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- A\n[/poll]" }, format: :json expect(response).not_to be_successful json = response.parsed_body @@ -72,19 +70,20 @@ RSpec.describe PostsController do end it "accepts different Chinese options" do - SiteSetting.default_locale = 'zh_CN' + SiteSetting.default_locale = "zh_CN" - post :create, params: { - title: title, raw: "[poll]\n- Microsoft Edge(新)\n- Microsoft Edge(旧)\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll]\n- Microsoft Edge(新)\n- Microsoft Edge(旧)\n[/poll]", + }, + format: :json expect(response).to be_successful end it "should have at least 1 options" do - post :create, params: { - title: title, raw: "[poll]\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n[/poll]" }, format: :json expect(response).not_to be_successful json = response.parsed_body @@ -96,41 +95,54 @@ RSpec.describe PostsController do (SiteSetting.poll_maximum_options + 1).times { |n| raw << "\n- #{n}" } raw << "\n[/poll]" - post :create, params: { - title: title, raw: raw - }, format: :json + post :create, params: { title: title, raw: raw }, format: :json expect(response).not_to be_successful json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options)) + expect(json["errors"][0]).to eq( + I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options), + ) end it "should have valid parameters" do - post :create, params: { - title: title, raw: "[poll type=multiple min=5]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll type=multiple min=5]\n- A\n- B\n[/poll]", + }, + format: :json expect(response).not_to be_successful json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters")) + expect(json["errors"][0]).to eq( + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), + ) end it "prevents self-xss" do - post :create, params: { - title: title, raw: "[poll name=]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll name=]\n- A\n- B\n[/poll]", + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body expect(json["cooked"]).to match("data-poll-") expect(json["cooked"]).to include("<script>") - expect(Poll.find_by(post_id: json["id"]).name).to eq("<script>alert('xss')</script>") + expect(Poll.find_by(post_id: json["id"]).name).to eq( + "<script>alert('xss')</script>", + ) end it "also works when there is a link starting with '[poll'" do - post :create, params: { - title: title, raw: "[Polls are awesome](/foobar)\n[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[Polls are awesome](/foobar)\n[poll]\n- A\n- B\n[/poll]", + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -139,9 +151,12 @@ RSpec.describe PostsController do end it "prevents poll-inception" do - post :create, params: { - title: title, raw: "[poll name=1]\n- A\n[poll name=2]\n- B\n- C\n[/poll]\n- D\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll name=1]\n- A\n[poll name=2]\n- B\n- C\n[/poll]\n- D\n[/poll]", + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -150,9 +165,12 @@ RSpec.describe PostsController do end it "accepts polls with titles" do - post :create, params: { - title: title, raw: "[poll]\n# What's up?\n- one\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll]\n# What's up?\n- one\n[/poll]", + }, + format: :json expect(response).to be_successful poll = Poll.last @@ -161,23 +179,24 @@ RSpec.describe PostsController do end describe "edit window" do - describe "within the first 5 minutes" do - let(:post_id) do freeze_time(4.minutes.ago) do - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json response.parsed_body["id"] end end it "can be changed" do - put :update, params: { - id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } - }, format: :json + put :update, + params: { + id: post_id, + post: { + raw: "[poll]\n- A\n- B\n- C\n[/poll]", + }, + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -187,28 +206,29 @@ RSpec.describe PostsController do it "resets the votes" do DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"]) - put :update, params: { - id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } - }, format: :json + put :update, + params: { + id: post_id, + post: { + raw: "[poll]\n- A\n- B\n- C\n[/poll]", + }, + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body expect(json["post"]["polls_votes"]).to_not be end - end describe "after the poll edit window has expired" do - let(:poll) { "[poll]\n- A\n- B\n[/poll]" } let(:new_option) { "[poll]\n- A\n- C\n[/poll]" } let(:updated) { "before\n\n[poll]\n- A\n- B\n[/poll]\n\nafter" } let(:post_id) do freeze_time(6.minutes.ago) do - post :create, params: { - title: title, raw: poll - }, format: :json + post :create, params: { title: title, raw: poll }, format: :json response.parsed_body["id"] end @@ -216,16 +236,11 @@ RSpec.describe PostsController do let(:poll_edit_window_mins) { 6 } - before do - SiteSetting.poll_edit_window_mins = poll_edit_window_mins - end + before { SiteSetting.poll_edit_window_mins = poll_edit_window_mins } describe "with no vote" do - it "can change the options" do - put :update, params: { - id: post_id, post: { raw: new_option } - }, format: :json + put :update, params: { id: post_id, post: { raw: new_option } }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -238,26 +253,24 @@ RSpec.describe PostsController do json = response.parsed_body expect(json["post"]["cooked"]).to match("before") end - end describe "with at least one vote" do - before do DiscoursePoll::Poll.vote(user, post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"]) end it "cannot change the options" do - put :update, params: { - id: post_id, post: { raw: new_option } - }, format: :json + put :update, params: { id: post_id, post: { raw: new_option } }, format: :json expect(response).not_to be_successful json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t( - "poll.edit_window_expired.cannot_edit_default_poll_with_votes", - minutes: poll_edit_window_mins - )) + expect(json["errors"][0]).to eq( + I18n.t( + "poll.edit_window_expired.cannot_edit_default_poll_with_votes", + minutes: poll_edit_window_mins, + ), + ) end it "support changes on the post" do @@ -266,45 +279,49 @@ RSpec.describe PostsController do json = response.parsed_body expect(json["post"]["cooked"]).to match("before") end - end - end - end - end describe "named polls" do - it "should have different options" do - post :create, params: { - title: title, raw: "[poll name=""foo""]\n- A\n- A\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: + "[poll name=" \ + "foo" \ + "]\n- A\n- A\n[/poll]", + }, + format: :json expect(response).not_to be_successful json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_different_options", name: "foo")) + expect(json["errors"][0]).to eq( + I18n.t("poll.named_poll_must_have_different_options", name: "foo"), + ) end it "should have at least 1 option" do - post :create, params: { - title: title, raw: "[poll name='foo']\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll name='foo']\n[/poll]" }, format: :json expect(response).not_to be_successful json = response.parsed_body - expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_at_least_1_option", name: "foo")) + expect(json["errors"][0]).to eq( + I18n.t("poll.named_poll_must_have_at_least_1_option", name: "foo"), + ) end - end describe "multiple polls" do - it "works" do - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]", + }, + format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -313,9 +330,12 @@ RSpec.describe PostsController do end it "should have a name" do - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]", + }, + format: :json expect(response).not_to be_successful json = response.parsed_body @@ -323,46 +343,42 @@ RSpec.describe PostsController do end it "should have unique name" do - post :create, params: { - title: title, raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" - }, format: :json + post :create, + params: { + title: title, + raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]", + }, + format: :json expect(response).not_to be_successful json = response.parsed_body expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_with_same_name", name: "foo")) end - end describe "disabled polls" do - before do - SiteSetting.poll_enabled = false - end + before { SiteSetting.poll_enabled = false } it "doesn’t cook the poll" do log_in_user(Fabricate(:user, admin: true, trust_level: 4)) - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.status).to eq(200) json = response.parsed_body - expect(json["cooked"]).to eq("

    [poll]

    \n
      \n
    • A
    • \n
    • B
      \n[/poll]
    • \n
    ") + expect(json["cooked"]).to eq( + "

    [poll]

    \n
      \n
    • A
    • \n
    • B
      \n[/poll]
    • \n
    ", + ) end end describe "regular user with insufficient trust level" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 2 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 2 } it "invalidates the post" do log_in_user(Fabricate(:user, trust_level: 1)) - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response).not_to be_successful json = response.parsed_body @@ -371,33 +387,31 @@ RSpec.describe PostsController do it "skips the check in PMs with bots" do user = Fabricate(:user, trust_level: 1) - topic = Fabricate(:private_message_topic, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: user), - Fabricate.build(:topic_allowed_user, user: Discourse.system_user) - ]) + topic = + Fabricate( + :private_message_topic, + topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: user), + Fabricate.build(:topic_allowed_user, user: Discourse.system_user), + ], + ) Fabricate(:post, topic_id: topic.id, user_id: Discourse::SYSTEM_USER_ID) log_in_user(user) - post :create, params: { - topic_id: topic.id, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { topic_id: topic.id, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.parsed_body["errors"]).to eq(nil) end end describe "regular user with equal trust level" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 2 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 2 } it "validates the post" do log_in_user(Fabricate(:user, trust_level: 2)) - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -407,16 +421,12 @@ RSpec.describe PostsController do end describe "regular user with superior trust level" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 2 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 2 } it "validates the post" do log_in_user(Fabricate(:user, trust_level: 3)) - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -426,16 +436,12 @@ RSpec.describe PostsController do end describe "staff with insufficient trust level" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 2 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 2 } it "validates the post" do log_in_user(Fabricate(:user, moderator: true, trust_level: 1)) - post :create, params: { - title: title, raw: "[poll]\n- A\n- B\n[/poll]" - }, format: :json + post :create, params: { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }, format: :json expect(response.status).to eq(200) json = response.parsed_body @@ -445,9 +451,7 @@ RSpec.describe PostsController do end describe "staff editing posts of users with insufficient trust level" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 2 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 2 } it "validates the post" do log_in_user(Fabricate(:user, trust_level: 1)) @@ -459,9 +463,14 @@ RSpec.describe PostsController do log_in_user(Fabricate(:admin)) - put :update, params: { - id: post_id, post: { raw: "#{title}\n[poll]\n- A\n- B\n- C\n[/poll]" } - }, format: :json + put :update, + params: { + id: post_id, + post: { + raw: "#{title}\n[poll]\n- A\n- B\n- C\n[/poll]", + }, + }, + format: :json expect(response.status).to eq(200) expect(response.parsed_body["post"]["polls"][0]["options"][2]["html"]).to eq("C") diff --git a/plugins/poll/spec/integration/poll_endpoints_spec.rb b/plugins/poll/spec/integration/poll_endpoints_spec.rb index 9fff1972a8..e8b62dae9f 100644 --- a/plugins/poll/spec/integration/poll_endpoints_spec.rb +++ b/plugins/poll/spec/integration/poll_endpoints_spec.rb @@ -7,31 +7,25 @@ RSpec.describe "DiscoursePoll endpoints" do fab!(:user) { Fabricate(:user) } fab!(:post) { Fabricate(:post, raw: "[poll public=true]\n- A\n- B\n[/poll]") } - fab!(:post_with_multiple_poll) do - Fabricate(:post, raw: <<~SQL) + fab!(:post_with_multiple_poll) { Fabricate(:post, raw: <<~SQL) } [poll type=multiple public=true min=1 max=2] - A - B - C [/poll] SQL - end let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" } let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" } it "should return the right response" do - DiscoursePoll::Poll.vote( - user, - post.id, - DiscoursePoll::DEFAULT_POLL_NAME, - [option_a] - ) + DiscoursePoll::Poll.vote(user, post.id, DiscoursePoll::DEFAULT_POLL_NAME, [option_a]) - get "/polls/voters.json", params: { - post_id: post.id, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME - } + get "/polls/voters.json", + params: { + post_id: post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + } expect(response.status).to eq(200) @@ -43,19 +37,20 @@ RSpec.describe "DiscoursePoll endpoints" do expect(option.first["username"]).to eq(user.username) end - it 'should return the right response for a single option' do + it "should return the right response for a single option" do DiscoursePoll::Poll.vote( user, post_with_multiple_poll.id, DiscoursePoll::DEFAULT_POLL_NAME, - [option_a, option_b] + [option_a, option_b], ) - get "/polls/voters.json", params: { - post_id: post_with_multiple_poll.id, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME, - option_id: option_b - } + get "/polls/voters.json", + params: { + post_id: post_with_multiple_poll.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + option_id: option_b, + } expect(response.status).to eq(200) @@ -70,56 +65,60 @@ RSpec.describe "DiscoursePoll endpoints" do expect(option.first["username"]).to eq(user.username) end - describe 'when post_id is blank' do - it 'should raise the right error' do + describe "when post_id is blank" do + it "should raise the right error" do get "/polls/voters.json", params: { poll_name: DiscoursePoll::DEFAULT_POLL_NAME } expect(response.status).to eq(400) end end - describe 'when post_id is not valid' do - it 'should raise the right error' do - get "/polls/voters.json", params: { - post_id: -1, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME - } + describe "when post_id is not valid" do + it "should raise the right error" do + get "/polls/voters.json", + params: { + post_id: -1, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + } expect(response.status).to eq(400) - expect(response.body).to include('post_id') + expect(response.body).to include("post_id") end end - describe 'when poll_name is blank' do - it 'should raise the right error' do + describe "when poll_name is blank" do + it "should raise the right error" do get "/polls/voters.json", params: { post_id: post.id } expect(response.status).to eq(400) end end - describe 'when poll_name is not valid' do - it 'should raise the right error' do - get "/polls/voters.json", params: { post_id: post.id, poll_name: 'wrongpoll' } + describe "when poll_name is not valid" do + it "should raise the right error" do + get "/polls/voters.json", params: { post_id: post.id, poll_name: "wrongpoll" } expect(response.status).to eq(400) - expect(response.body).to include('poll_name') + expect(response.body).to include("poll_name") end end context "with number poll" do - let(:post) { Fabricate(:post, raw: "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]") } + let(:post) do + Fabricate(:post, raw: "[poll type=number min=1 max=20 step=1 public=true]\n[/poll]") + end - it 'should return the right response' do + it "should return the right response" do post DiscoursePoll::Poll.vote( user, post.id, DiscoursePoll::DEFAULT_POLL_NAME, - ["4d8a15e3cc35750f016ce15a43937620"] + ["4d8a15e3cc35750f016ce15a43937620"], ) - get "/polls/voters.json", params: { - post_id: post.id, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME - } + get "/polls/voters.json", + params: { + post_id: post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + } expect(response.status).to eq(200) @@ -137,31 +136,25 @@ RSpec.describe "DiscoursePoll endpoints" do fab!(:user3) { Fabricate(:user) } fab!(:user4) { Fabricate(:user) } - fab!(:post) do - Fabricate(:post, raw: <<~SQL) + fab!(:post) { Fabricate(:post, raw: <<~SQL) } [poll type=multiple public=true min=1 max=2] - A - B [/poll] SQL - end let(:option_a) { "5c24fc1df56d764b550ceae1b9319125" } let(:option_b) { "e89dec30bbd9bf50fabf6a05b4324edf" } before do - user_votes = { - user_0: option_a, - user_1: option_a, - user_2: option_b, - } + user_votes = { user_0: option_a, user_1: option_a, user_2: option_b } [user1, user2, user3].each_with_index do |user, index| DiscoursePoll::Poll.vote( user, post.id, DiscoursePoll::DEFAULT_POLL_NAME, - [user_votes["user_#{index}".to_sym]] + [user_votes["user_#{index}".to_sym]], ) UserCustomField.create(user_id: user.id, name: "something", value: "value#{index}") end @@ -171,7 +164,7 @@ RSpec.describe "DiscoursePoll endpoints" do user4, post.id, DiscoursePoll::DEFAULT_POLL_NAME, - [option_a, option_b] + [option_a, option_b], ) UserCustomField.create(user_id: user4.id, name: "something", value: "value1") end @@ -179,32 +172,52 @@ RSpec.describe "DiscoursePoll endpoints" do it "returns grouped poll results based on user field" do SiteSetting.poll_groupable_user_fields = "something" - get "/polls/grouped_poll_results.json", params: { - post_id: post.id, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME, - user_field_name: "something" - } + get "/polls/grouped_poll_results.json", + params: { + post_id: post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + user_field_name: "something", + } expect(response.status).to eq(200) expect(response.parsed_body.deep_symbolize_keys).to eq( grouped_results: [ - { group: "Value0", options: [{ digest: option_a, html: "A", votes: 1 }, { digest: option_b, html: "B", votes: 0 }] }, - { group: "Value1", options: [{ digest: option_a, html: "A", votes: 2 }, { digest: option_b, html: "B", votes: 1 }] }, - { group: "Value2", options: [{ digest: option_a, html: "A", votes: 0 }, { digest: option_b, html: "B", votes: 1 }] }, - ] + { + group: "Value0", + options: [ + { digest: option_a, html: "A", votes: 1 }, + { digest: option_b, html: "B", votes: 0 }, + ], + }, + { + group: "Value1", + options: [ + { digest: option_a, html: "A", votes: 2 }, + { digest: option_b, html: "B", votes: 1 }, + ], + }, + { + group: "Value2", + options: [ + { digest: option_a, html: "A", votes: 0 }, + { digest: option_b, html: "B", votes: 1 }, + ], + }, + ], ) end it "returns an error when poll_groupable_user_fields is empty" do SiteSetting.poll_groupable_user_fields = "" - get "/polls/grouped_poll_results.json", params: { - post_id: post.id, - poll_name: DiscoursePoll::DEFAULT_POLL_NAME, - user_field_name: "something" - } + get "/polls/grouped_poll_results.json", + params: { + post_id: post.id, + poll_name: DiscoursePoll::DEFAULT_POLL_NAME, + user_field_name: "something", + } expect(response.status).to eq(400) - expect(response.body).to include('user_field_name') + expect(response.body).to include("user_field_name") end end end diff --git a/plugins/poll/spec/jobs/regular/close_poll_spec.rb b/plugins/poll/spec/jobs/regular/close_poll_spec.rb index 891b35c9c1..08734f9691 100644 --- a/plugins/poll/spec/jobs/regular/close_poll_spec.rb +++ b/plugins/poll/spec/jobs/regular/close_poll_spec.rb @@ -5,15 +5,17 @@ require "rails_helper" RSpec.describe Jobs::ClosePoll do let(:post) { Fabricate(:post, raw: "[poll]\n- A\n- B\n[/poll]") } - describe 'missing arguments' do - it 'should raise the right error' do - expect do - Jobs::ClosePoll.new.execute(post_id: post.id) - end.to raise_error(Discourse::InvalidParameters, "poll_name") + describe "missing arguments" do + it "should raise the right error" do + expect do Jobs::ClosePoll.new.execute(post_id: post.id) end.to raise_error( + Discourse::InvalidParameters, + "poll_name", + ) - expect do - Jobs::ClosePoll.new.execute(poll_name: "poll") - end.to raise_error(Discourse::InvalidParameters, "post_id") + expect do Jobs::ClosePoll.new.execute(poll_name: "poll") end.to raise_error( + Discourse::InvalidParameters, + "post_id", + ) end end @@ -24,5 +26,4 @@ RSpec.describe Jobs::ClosePoll do expect(post.polls.first.closed?).to eq(true) end - end diff --git a/plugins/poll/spec/lib/new_post_manager_spec.rb b/plugins/poll/spec/lib/new_post_manager_spec.rb index fb10d9cf2d..5a051697cb 100644 --- a/plugins/poll/spec/lib/new_post_manager_spec.rb +++ b/plugins/poll/spec/lib/new_post_manager_spec.rb @@ -7,9 +7,7 @@ RSpec.describe NewPostManager do let(:admin) { Fabricate(:admin) } describe "when new post containing a poll is queued for approval" do - before do - SiteSetting.poll_minimum_trust_level_to_create = 0 - end + before { SiteSetting.poll_minimum_trust_level_to_create = 0 } let(:params) do { @@ -23,9 +21,10 @@ RSpec.describe NewPostManager do is_warning: false, title: "This is a test post with a poll", ip_address: "127.0.0.1", - user_agent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", + user_agent: + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36", referrer: "http://localhost:3000/", - first_post_checks: true + first_post_checks: true, } end @@ -38,7 +37,7 @@ RSpec.describe NewPostManager do expect(Poll.where(post: review_result.created_post).exists?).to eq(true) end - it 're-validates the poll when the approve_post event is triggered' do + it "re-validates the poll when the approve_post event is triggered" do invalid_raw_poll = <<~MD [poll type=multiple min=0] * 1 diff --git a/plugins/poll/spec/lib/poll_spec.rb b/plugins/poll/spec/lib/poll_spec.rb index 1b885947f7..9e42fd55f4 100644 --- a/plugins/poll/spec/lib/poll_spec.rb +++ b/plugins/poll/spec/lib/poll_spec.rb @@ -4,17 +4,14 @@ RSpec.describe DiscoursePoll::Poll do fab!(:user) { Fabricate(:user) } fab!(:user_2) { Fabricate(:user) } - fab!(:post_with_regular_poll) do - Fabricate(:post, raw: <<~RAW) + fab!(:post_with_regular_poll) { Fabricate(:post, raw: <<~RAW) } [poll] * 1 * 2 [/poll] RAW - end - fab!(:post_with_multiple_poll) do - Fabricate(:post, raw: <<~RAW) + fab!(:post_with_multiple_poll) { Fabricate(:post, raw: <<~RAW) } [poll type=multiple min=2 max=3] * 1 * 2 @@ -23,10 +20,9 @@ RSpec.describe DiscoursePoll::Poll do * 5 [/poll] RAW - end - describe '.vote' do - it 'should only allow one vote per user for a regular poll' do + describe ".vote" do + it "should only allow one vote per user for a regular poll" do poll = post_with_regular_poll.polls.first expect do @@ -34,46 +30,35 @@ RSpec.describe DiscoursePoll::Poll do user, post_with_regular_poll.id, "poll", - poll.poll_options.map(&:digest) + poll.poll_options.map(&:digest), ) end.to raise_error(DiscoursePoll::Error, I18n.t("poll.one_vote_per_user")) end - it 'should clean up bad votes for a regular poll' do + it "should clean up bad votes for a regular poll" do poll = post_with_regular_poll.polls.first - PollVote.create!( - poll: poll, - poll_option: poll.poll_options.first, - user: user - ) + PollVote.create!(poll: poll, poll_option: poll.poll_options.first, user: user) - PollVote.create!( - poll: poll, - poll_option: poll.poll_options.last, - user: user - ) + PollVote.create!(poll: poll, poll_option: poll.poll_options.last, user: user) DiscoursePoll::Poll.vote( user, post_with_regular_poll.id, "poll", - [poll.poll_options.first.digest] + [poll.poll_options.first.digest], ) - expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) - .to contain_exactly(poll.poll_options.first.id) + expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly( + poll.poll_options.first.id, + ) end - it 'allows user to vote on multiple options correctly for a multiple poll' do + it "allows user to vote on multiple options correctly for a multiple poll" do poll = post_with_multiple_poll.polls.first poll_options = poll.poll_options - [ - poll_options.first, - poll_options.second, - poll_options.third, - ].each do |poll_option| + [poll_options.first, poll_options.second, poll_options.third].each do |poll_option| PollVote.create!(poll: poll, poll_option: poll_option, user: user) end @@ -81,24 +66,28 @@ RSpec.describe DiscoursePoll::Poll do user, post_with_multiple_poll.id, "poll", - [poll_options.first.digest, poll_options.second.digest] + [poll_options.first.digest, poll_options.second.digest], ) DiscoursePoll::Poll.vote( user_2, post_with_multiple_poll.id, "poll", - [poll_options.third.digest, poll_options.fourth.digest] + [poll_options.third.digest, poll_options.fourth.digest], ) - expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) - .to contain_exactly(poll_options.first.id, poll_options.second.id) + expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly( + poll_options.first.id, + poll_options.second.id, + ) - expect(PollVote.where(poll: poll, user: user_2).pluck(:poll_option_id)) - .to contain_exactly(poll_options.third.id, poll_options.fourth.id) + expect(PollVote.where(poll: poll, user: user_2).pluck(:poll_option_id)).to contain_exactly( + poll_options.third.id, + poll_options.fourth.id, + ) end - it 'should respect the min/max votes per user for a multiple poll' do + it "should respect the min/max votes per user for a multiple poll" do poll = post_with_multiple_poll.polls.first expect do @@ -106,27 +95,21 @@ RSpec.describe DiscoursePoll::Poll do user, post_with_multiple_poll.id, "poll", - poll.poll_options.map(&:digest) + poll.poll_options.map(&:digest), ) - end.to raise_error( - DiscoursePoll::Error, - I18n.t("poll.max_vote_per_user", count: poll.max) - ) + end.to raise_error(DiscoursePoll::Error, I18n.t("poll.max_vote_per_user", count: poll.max)) expect do DiscoursePoll::Poll.vote( user, post_with_multiple_poll.id, "poll", - [poll.poll_options.first.digest] + [poll.poll_options.first.digest], ) - end.to raise_error( - DiscoursePoll::Error, - I18n.t("poll.min_vote_per_user", count: poll.min) - ) + end.to raise_error(DiscoursePoll::Error, I18n.t("poll.min_vote_per_user", count: poll.min)) end - it 'should allow user to vote on a multiple poll even if min option is not configured' do + it "should allow user to vote on a multiple poll even if min option is not configured" do post_with_multiple_poll = Fabricate(:post, raw: <<~RAW) [poll type=multiple max=3] * 1 @@ -143,14 +126,15 @@ RSpec.describe DiscoursePoll::Poll do user, post_with_multiple_poll.id, "poll", - [poll.poll_options.first.digest] + [poll.poll_options.first.digest], ) - expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) - .to contain_exactly(poll.poll_options.first.id) + expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly( + poll.poll_options.first.id, + ) end - it 'should allow user to vote on a multiple poll even if max option is not configured' do + it "should allow user to vote on a multiple poll even if max option is not configured" do post_with_multiple_poll = Fabricate(:post, raw: <<~RAW) [poll type=multiple min=1] * 1 @@ -167,11 +151,13 @@ RSpec.describe DiscoursePoll::Poll do user, post_with_multiple_poll.id, "poll", - [poll.poll_options.first.digest, poll.poll_options.second.digest] + [poll.poll_options.first.digest, poll.poll_options.second.digest], ) - expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)) - .to contain_exactly(poll.poll_options.first.id, poll.poll_options.second.id) + expect(PollVote.where(poll: poll, user: user).pluck(:poll_option_id)).to contain_exactly( + poll.poll_options.first.id, + poll.poll_options.second.id, + ) end end @@ -179,19 +165,14 @@ RSpec.describe DiscoursePoll::Poll do it "publishes on message bus if a there are polls" do first_post = Fabricate(:post) topic = first_post.topic - creator = PostCreator.new(user, - topic_id: topic.id, - raw: <<~RAW + creator = PostCreator.new(user, topic_id: topic.id, raw: <<~RAW) [poll] * 1 * 2 [/poll] RAW - ) - messages = MessageBus.track_publish("/polls/#{topic.id}") do - creator.create! - end + messages = MessageBus.track_publish("/polls/#{topic.id}") { creator.create! } expect(messages.count).to eq(1) end @@ -199,20 +180,16 @@ RSpec.describe DiscoursePoll::Poll do it "does not publish on message bus when a post with no polls is created" do first_post = Fabricate(:post) topic = first_post.topic - creator = PostCreator.new(user, - topic_id: topic.id, - raw: "Just a post with definitely no polls" - ) + creator = + PostCreator.new(user, topic_id: topic.id, raw: "Just a post with definitely no polls") - messages = MessageBus.track_publish("/polls/#{topic.id}") do - creator.create! - end + messages = MessageBus.track_publish("/polls/#{topic.id}") { creator.create! } expect(messages.count).to eq(0) end end - describe '.extract' do + describe ".extract" do it "skips the polls inside quote" do raw = <<~RAW [quote="username, post:1, topic:2"] @@ -230,18 +207,17 @@ RSpec.describe DiscoursePoll::Poll do Post with a poll and a quoted poll. RAW - expect(DiscoursePoll::Poll.extract(raw, 2)).to contain_exactly({ - "name" => "poll", - "options" => [{ - "html" => "3", - "id" => "68b434ff88aeae7054e42cd05a4d9056" - }, { - "html" => "4", - "id" => "aa2393b424f2f395abb63bf785760a3b" - }], - "status" => "open", - "type" => "regular" - }) + expect(DiscoursePoll::Poll.extract(raw, 2)).to contain_exactly( + { + "name" => "poll", + "options" => [ + { "html" => "3", "id" => "68b434ff88aeae7054e42cd05a4d9056" }, + { "html" => "4", "id" => "aa2393b424f2f395abb63bf785760a3b" }, + ], + "status" => "open", + "type" => "regular", + }, + ) end end end diff --git a/plugins/poll/spec/lib/polls_updater_spec.rb b/plugins/poll/spec/lib/polls_updater_spec.rb index b6dde6817a..f42ec70186 100644 --- a/plugins/poll/spec/lib/polls_updater_spec.rb +++ b/plugins/poll/spec/lib/polls_updater_spec.rb @@ -1,68 +1,54 @@ # frozen_string_literal: true RSpec.describe DiscoursePoll::PollsUpdater do - def update(post, polls) DiscoursePoll::PollsUpdater.update(post, polls) end let(:user) { Fabricate(:user) } - let(:post) { - Fabricate(:post, raw: <<~RAW) + let(:post) { Fabricate(:post, raw: <<~RAW) } [poll] * 1 * 2 [/poll] RAW - } - let(:post_with_3_options) { - Fabricate(:post, raw: <<~RAW) + let(:post_with_3_options) { Fabricate(:post, raw: <<~RAW) } [poll] - a - b - c [/poll] RAW - } - let(:post_with_some_attributes) { - Fabricate(:post, raw: <<~RAW) + let(:post_with_some_attributes) { Fabricate(:post, raw: <<~RAW) } [poll close=#{1.week.from_now.to_formatted_s(:iso8601)} results=on_close] - A - B - C [/poll] RAW - } - let(:polls) { - DiscoursePoll::PollsValidator.new(post).validate_polls - } + let(:polls) { DiscoursePoll::PollsValidator.new(post).validate_polls } - let(:polls_with_3_options) { + let(:polls_with_3_options) do DiscoursePoll::PollsValidator.new(post_with_3_options).validate_polls - } + end - let(:polls_with_some_attributes) { + let(:polls_with_some_attributes) do DiscoursePoll::PollsValidator.new(post_with_some_attributes).validate_polls - } + end describe "update" do - it "does nothing when there are no changes" do - message = MessageBus.track_publish("/polls/#{post.topic_id}") do - update(post, polls) - end.first + message = MessageBus.track_publish("/polls/#{post.topic_id}") { update(post, polls) }.first expect(message).to be(nil) end describe "when editing" do - - let(:raw) do - <<~RAW + let(:raw) { <<~RAW } This is a new poll with three options. [poll type=multiple results=always min=1 max=2] @@ -71,7 +57,6 @@ RSpec.describe DiscoursePoll::PollsUpdater do * third [/poll] RAW - end let(:post) { Fabricate(:post, raw: raw) } @@ -84,11 +69,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(post.errors[:base].size).to equal(0) end - end describe "deletes polls" do - it "that were removed" do update(post, {}) @@ -97,19 +80,15 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(Poll.where(post: post).exists?).to eq(false) expect(post.custom_fields[DiscoursePoll::HAS_POLLS]).to eq(nil) end - end describe "creates polls" do - it "that were added" do post = Fabricate(:post) expect(Poll.find_by(post: post)).to_not be - message = MessageBus.track_publish("/polls/#{post.topic_id}") do - update(post, polls) - end.first + message = MessageBus.track_publish("/polls/#{post.topic_id}") { update(post, polls) }.first poll = Poll.find_by(post: post) @@ -121,21 +100,19 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(message.data[:post_id]).to eq(post.id) expect(message.data[:polls][0][:name]).to eq(poll.name) end - end describe "updates polls" do - describe "when there are no votes" do - it "at any time" do post # create the post freeze_time 1.month.from_now - message = MessageBus.track_publish("/polls/#{post.topic_id}") do - update(post, polls_with_some_attributes) - end.first + message = + MessageBus + .track_publish("/polls/#{post.topic_id}") { update(post, polls_with_some_attributes) } + .first poll = Poll.find_by(post: post) @@ -150,11 +127,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(message.data[:post_id]).to eq(post.id) expect(message.data[:polls][0][:name]).to eq(poll.name) end - end describe "when there are votes" do - before do expect { DiscoursePoll::Poll.vote(user, post.id, "poll", [polls["poll"]["options"][0]["id"]]) @@ -162,11 +137,13 @@ RSpec.describe DiscoursePoll::PollsUpdater do end describe "inside the edit window" do - it "and deletes the votes" do - message = MessageBus.track_publish("/polls/#{post.topic_id}") do - update(post, polls_with_some_attributes) - end.first + message = + MessageBus + .track_publish("/polls/#{post.topic_id}") do + update(post, polls_with_some_attributes) + end + .first poll = Poll.find_by(post: post) @@ -181,11 +158,9 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(message.data[:post_id]).to eq(post.id) expect(message.data[:polls][0][:name]).to eq(poll.name) end - end describe "outside the edit window" do - it "throws an error" do edit_window = SiteSetting.poll_edit_window_mins @@ -204,17 +179,12 @@ RSpec.describe DiscoursePoll::PollsUpdater do expect(post.errors[:base]).to include( I18n.t( "poll.edit_window_expired.cannot_edit_default_poll_with_votes", - minutes: edit_window - ) + minutes: edit_window, + ), ) end - end - end - end - end - end diff --git a/plugins/poll/spec/lib/polls_validator_spec.rb b/plugins/poll/spec/lib/polls_validator_spec.rb index 2ad690c81c..583a96c80e 100644 --- a/plugins/poll/spec/lib/polls_validator_spec.rb +++ b/plugins/poll/spec/lib/polls_validator_spec.rb @@ -18,9 +18,15 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include(I18n.t("poll.invalid_argument", argument: "type", value: "not_good1")) - expect(post.errors[:base]).to include(I18n.t("poll.invalid_argument", argument: "status", value: "not_good2")) - expect(post.errors[:base]).to include(I18n.t("poll.invalid_argument", argument: "results", value: "not_good3")) + expect(post.errors[:base]).to include( + I18n.t("poll.invalid_argument", argument: "type", value: "not_good1"), + ) + expect(post.errors[:base]).to include( + I18n.t("poll.invalid_argument", argument: "status", value: "not_good2"), + ) + expect(post.errors[:base]).to include( + I18n.t("poll.invalid_argument", argument: "results", value: "not_good3"), + ) end it "ensures that all possible values are valid" do @@ -70,9 +76,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include( - I18n.t("poll.multiple_polls_without_name") - ) + expect(post.errors[:base]).to include(I18n.t("poll.multiple_polls_without_name")) raw = <<~RAW [poll name=test] @@ -90,7 +94,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.multiple_polls_with_same_name", name: "test") + I18n.t("poll.multiple_polls_with_same_name", name: "test"), ) end @@ -105,9 +109,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_must_have_different_options") - ) + expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_different_options")) raw = <<~RAW [poll name=test] @@ -120,7 +122,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.named_poll_must_have_different_options", name: "test") + I18n.t("poll.named_poll_must_have_different_options", name: "test"), ) end @@ -136,7 +138,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_must_not_have_any_empty_options") + I18n.t("poll.default_poll_must_not_have_any_empty_options"), ) raw = <<~RAW @@ -150,7 +152,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.named_poll_must_not_have_any_empty_options", name: "test") + I18n.t("poll.named_poll_must_not_have_any_empty_options", name: "test"), ) end @@ -163,9 +165,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_must_have_at_least_1_option") - ) + expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_at_least_1_option")) raw = <<~RAW [poll name=test] @@ -176,7 +176,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.named_poll_must_have_at_least_1_option", name: "test") + I18n.t("poll.named_poll_must_have_at_least_1_option", name: "test"), ) end @@ -194,10 +194,9 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include(I18n.t( - "poll.default_poll_must_have_less_options", - count: SiteSetting.poll_maximum_options - )) + expect(post.errors[:base]).to include( + I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options), + ) raw = <<~RAW [poll name=test] @@ -210,10 +209,13 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include(I18n.t( - "poll.named_poll_must_have_less_options", - name: "test", count: SiteSetting.poll_maximum_options - )) + expect(post.errors[:base]).to include( + I18n.t( + "poll.named_poll_must_have_less_options", + name: "test", + count: SiteSetting.poll_maximum_options, + ), + ) end describe "multiple type polls" do @@ -230,7 +232,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) raw = <<~RAW @@ -245,7 +247,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.named_poll_with_multiple_choices_has_invalid_parameters", name: "test") + I18n.t("poll.named_poll_with_multiple_choices_has_invalid_parameters", name: "test"), ) end @@ -261,7 +263,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) end @@ -277,7 +279,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) end @@ -293,7 +295,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) end @@ -321,7 +323,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) end @@ -337,7 +339,7 @@ RSpec.describe ::DiscoursePoll::PollsValidator do expect(post.valid?).to eq(false) expect(post.errors[:base]).to include( - I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters") + I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"), ) end end @@ -350,9 +352,15 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include("Min #{I18n.t("errors.messages.greater_than", count: 0)}") - expect(post.errors[:base]).to include("Max #{I18n.t("errors.messages.greater_than", count: "min")}") - expect(post.errors[:base]).to include("Step #{I18n.t("errors.messages.greater_than", count: 0)}") + expect(post.errors[:base]).to include( + "Min #{I18n.t("errors.messages.greater_than", count: 0)}", + ) + expect(post.errors[:base]).to include( + "Max #{I18n.t("errors.messages.greater_than", count: "min")}", + ) + expect(post.errors[:base]).to include( + "Step #{I18n.t("errors.messages.greater_than", count: 0)}", + ) raw = <<~RAW [poll type=number min=9999999999 max=9999999999 step=1] @@ -361,8 +369,12 @@ RSpec.describe ::DiscoursePoll::PollsValidator do post.raw = raw expect(post.valid?).to eq(false) - expect(post.errors[:base]).to include("Min #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}") - expect(post.errors[:base]).to include("Max #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}") + expect(post.errors[:base]).to include( + "Min #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}", + ) + expect(post.errors[:base]).to include( + "Max #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}", + ) expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_at_least_1_option")) end end diff --git a/plugins/poll/spec/lib/pretty_text_spec.rb b/plugins/poll/spec/lib/pretty_text_spec.rb index 6c4c48c188..9f6b6763dd 100644 --- a/plugins/poll/spec/lib/pretty_text_spec.rb +++ b/plugins/poll/spec/lib/pretty_text_spec.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true RSpec.describe PrettyText do - def n(html) html.strip end - it 'supports multi choice polls' do + it "supports multi choice polls" do cooked = PrettyText.cook <<~MD [poll type=multiple min=1 max=3 public=true] * option 1 @@ -24,17 +23,16 @@ RSpec.describe PrettyText do expect(cooked).to include('data-poll-public="true"') end - it 'can dynamically generate a poll' do - + it "can dynamically generate a poll" do cooked = PrettyText.cook <<~MD [poll type=number min=1 max=20 step=1] [/poll] MD - expect(cooked.scan('test @@ -83,7 +81,7 @@ RSpec.describe PrettyText do expect(tight_hashes).to eq(loose_hashes) end - it 'can correctly cook polls' do + it "can correctly cook polls" do md = <<~MD [poll type=multiple] 1. test 1 :) test @@ -115,10 +113,9 @@ RSpec.describe PrettyText do # note, hashes should remain stable even if emoji changes cause text content is hashed expect(n cooked).to eq(n expected) - end - it 'can onebox posts' do + it "can onebox posts" do post = Fabricate(:post, raw: <<~MD) A post with a poll @@ -129,13 +126,13 @@ RSpec.describe PrettyText do MD onebox = Oneboxer.onebox_raw(post.full_url, user_id: Fabricate(:user).id) - doc = Nokogiri::HTML5(onebox[:preview]) + doc = Nokogiri.HTML5(onebox[:preview]) expect(onebox[:preview]).to include("A post with a poll") expect(onebox[:preview]).to include("poll") end - it 'can reduce excerpts' do + it "can reduce excerpts" do post = Fabricate(:post, raw: <<~MD) A post with a poll @@ -187,8 +184,12 @@ RSpec.describe PrettyText do HTML - expect(cooked).to include("

    \nPre-heading

    ") - expect(cooked).to include("

    \nPost-heading

    ") + expect(cooked).to include( + "

    \nPre-heading

    ", + ) + expect(cooked).to include( + "

    \nPost-heading

    ", + ) end it "does not break when there are headings before/after a poll without a title" do @@ -209,7 +210,11 @@ RSpec.describe PrettyText do
    HTML - expect(cooked).to include("

    \nPre-heading

    ") - expect(cooked).to include("

    \nPost-heading

    ") + expect(cooked).to include( + "

    \nPre-heading

    ", + ) + expect(cooked).to include( + "

    \nPost-heading

    ", + ) end end diff --git a/plugins/poll/spec/models/poll_spec.rb b/plugins/poll/spec/models/poll_spec.rb index 9b72a4a9ec..2afc37cc59 100644 --- a/plugins/poll/spec/models/poll_spec.rb +++ b/plugins/poll/spec/models/poll_spec.rb @@ -5,13 +5,17 @@ RSpec.describe ::DiscoursePoll::Poll do it "Transforms UserField name if a matching CustomUserField is present" do user_field_name = "Something Cool" user_field = Fabricate(:user_field, name: user_field_name) - expect(::DiscoursePoll::Poll.transform_for_user_field_override(user_field_name)).to eq("user_field_#{user_field.id}") + expect(::DiscoursePoll::Poll.transform_for_user_field_override(user_field_name)).to eq( + "user_field_#{user_field.id}", + ) end it "does not transform UserField name if a matching CustomUserField is not present" do user_field_name = "Something Cool" user_field = Fabricate(:user_field, name: "Something Else!") - expect(::DiscoursePoll::Poll.transform_for_user_field_override(user_field_name)).to eq(user_field_name) + expect(::DiscoursePoll::Poll.transform_for_user_field_override(user_field_name)).to eq( + user_field_name, + ) end end @@ -61,14 +65,14 @@ RSpec.describe ::DiscoursePoll::Poll do option = poll.poll_options.first expect(poll.can_see_results?(user)).to eq(false) - poll.poll_votes.create!(poll_option_id: option.id , user_id: user.id) + poll.poll_votes.create!(poll_option_id: option.id, user_id: user.id) expect(poll.can_see_results?(user)).to eq(false) user.update!(moderator: true) expect(poll.can_see_results?(user)).to eq(true) end end - describe 'when post is trashed' do + describe "when post is trashed" do it "maintains the association" do user = Fabricate(:user) post = Fabricate(:post, raw: "[poll results=staff_only]\n- A\n- B\n[/poll]", user: user) diff --git a/plugins/poll/spec/requests/users_controller_spec.rb b/plugins/poll/spec/requests/users_controller_spec.rb index 78e387de6e..b1a0126729 100644 --- a/plugins/poll/spec/requests/users_controller_spec.rb +++ b/plugins/poll/spec/requests/users_controller_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Admin::UsersController do before { sign_in(admin) } - describe '#destroy' do + describe "#destroy" do let(:delete_me) { Fabricate(:user) } context "when user has voted" do diff --git a/plugins/poll/spec/serializers/poll_option_serializer_spec.rb b/plugins/poll/spec/serializers/poll_option_serializer_spec.rb index ad46e77961..a6eff33c33 100644 --- a/plugins/poll/spec/serializers/poll_option_serializer_spec.rb +++ b/plugins/poll/spec/serializers/poll_option_serializer_spec.rb @@ -4,7 +4,9 @@ def serialize_option(option, user) PollOptionSerializer.new( option, root: false, - scope: { can_see_results: poll.can_see_results?(user) } + scope: { + can_see_results: poll.can_see_results?(user), + }, ) end @@ -12,17 +14,15 @@ RSpec.describe PollOptionSerializer do let(:voter) { Fabricate(:user) } let(:poll) { post.polls.first } - before do - poll.poll_votes.create!(poll_option_id: poll.poll_options.first.id, user_id: voter.id) - end + before { poll.poll_votes.create!(poll_option_id: poll.poll_options.first.id, user_id: voter.id) } - context 'when poll results are public' do + context "when poll results are public" do let(:post) { Fabricate(:post, raw: "[poll]\n- A\n- B\n[/poll]") } - context 'when user is not staff' do + context "when user is not staff" do let(:user) { Fabricate(:user) } - it 'include votes' do + it "include votes" do serializer = serialize_option(poll.poll_options.first, user) expect(serializer.include_votes?).to eq(true) @@ -30,23 +30,23 @@ RSpec.describe PollOptionSerializer do end end - context 'when poll results are staff only' do + context "when poll results are staff only" do let(:post) { Fabricate(:post, raw: "[poll results=staff_only]\n- A\n- B\n[/poll]") } - context 'when user is not staff' do + context "when user is not staff" do let(:user) { Fabricate(:user) } - it 'doesn’t include votes' do + it "doesn’t include votes" do serializer = serialize_option(poll.poll_options.first, user) expect(serializer.include_votes?).to eq(false) end end - context 'when user is staff' do + context "when user is staff" do let(:admin) { Fabricate(:admin) } - it 'includes votes' do + it "includes votes" do serializer = serialize_option(poll.poll_options.first, admin) expect(serializer.include_votes?).to eq(true) diff --git a/plugins/styleguide/app/controllers/styleguide/styleguide_controller.rb b/plugins/styleguide/app/controllers/styleguide/styleguide_controller.rb index 94f853c15f..ebdab9b8f1 100644 --- a/plugins/styleguide/app/controllers/styleguide/styleguide_controller.rb +++ b/plugins/styleguide/app/controllers/styleguide/styleguide_controller.rb @@ -8,7 +8,7 @@ module Styleguide def index ensure_admin if SiteSetting.styleguide_admin_only - render 'default/empty' + render "default/empty" end end end diff --git a/plugins/styleguide/config/routes.rb b/plugins/styleguide/config/routes.rb index 57efad0014..46812dae1c 100644 --- a/plugins/styleguide/config/routes.rb +++ b/plugins/styleguide/config/routes.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true Styleguide::Engine.routes.draw do - get "/" => 'styleguide#index' - get "/:category/:section" => 'styleguide#index' + get "/" => "styleguide#index" + get "/:category/:section" => "styleguide#index" end diff --git a/plugins/styleguide/plugin.rb b/plugins/styleguide/plugin.rb index 4c3244b1f6..0611793c0c 100644 --- a/plugins/styleguide/plugin.rb +++ b/plugins/styleguide/plugin.rb @@ -8,14 +8,12 @@ register_asset "stylesheets/styleguide.scss" enabled_site_setting :styleguide_enabled -load File.expand_path('../lib/styleguide/engine.rb', __FILE__) +load File.expand_path("../lib/styleguide/engine.rb", __FILE__) -Discourse::Application.routes.append do - mount ::Styleguide::Engine, at: '/styleguide' -end +Discourse::Application.routes.append { mount ::Styleguide::Engine, at: "/styleguide" } after_initialize do register_asset_filter do |type, request, opts| - (opts[:path] || '').start_with?("#{Discourse.base_path}/styleguide") + (opts[:path] || "").start_with?("#{Discourse.base_path}/styleguide") end end diff --git a/plugins/styleguide/spec/integration/access_spec.rb b/plugins/styleguide/spec/integration/access_spec.rb index 568d35eafa..96e5a36c39 100644 --- a/plugins/styleguide/spec/integration/access_spec.rb +++ b/plugins/styleguide/spec/integration/access_spec.rb @@ -1,62 +1,48 @@ # frozen_string_literal: true -RSpec.describe 'SiteSetting.styleguide_admin_only' do - before do - SiteSetting.styleguide_enabled = true - end +RSpec.describe "SiteSetting.styleguide_admin_only" do + before { SiteSetting.styleguide_enabled = true } - context 'when styleguide is admin only' do - before do - SiteSetting.styleguide_admin_only = true - end + context "when styleguide is admin only" do + before { SiteSetting.styleguide_admin_only = true } - context 'when user is admin' do - before do - sign_in(Fabricate(:admin)) - end + context "when user is admin" do + before { sign_in(Fabricate(:admin)) } - it 'shows the styleguide' do - get '/styleguide' + it "shows the styleguide" do + get "/styleguide" expect(response.status).to eq(200) end end - context 'when user is not admin' do - before do - sign_in(Fabricate(:user)) - end + context "when user is not admin" do + before { sign_in(Fabricate(:user)) } - it 'doesn’t allow access' do - get '/styleguide' + it "doesn’t allow access" do + get "/styleguide" expect(response.status).to eq(403) end end end end -RSpec.describe 'SiteSetting.styleguide_enabled' do - before do - sign_in(Fabricate(:admin)) - end +RSpec.describe "SiteSetting.styleguide_enabled" do + before { sign_in(Fabricate(:admin)) } - context 'when style is enabled' do - before do - SiteSetting.styleguide_enabled = true - end + context "when style is enabled" do + before { SiteSetting.styleguide_enabled = true } - it 'shows the styleguide' do - get '/styleguide' + it "shows the styleguide" do + get "/styleguide" expect(response.status).to eq(200) end end - context 'when styleguide is disabled' do - before do - SiteSetting.styleguide_enabled = false - end + context "when styleguide is disabled" do + before { SiteSetting.styleguide_enabled = false } - it 'returns a page not found' do - get '/styleguide' + it "returns a page not found" do + get "/styleguide" expect(response.status).to eq(404) end end diff --git a/plugins/styleguide/spec/integration/assets_spec.rb b/plugins/styleguide/spec/integration/assets_spec.rb index e4d6f2cfa0..95fd56df96 100644 --- a/plugins/styleguide/spec/integration/assets_spec.rb +++ b/plugins/styleguide/spec/integration/assets_spec.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true -RSpec.describe 'Styleguide assets' do +RSpec.describe "Styleguide assets" do before do SiteSetting.styleguide_enabled = true sign_in(Fabricate(:admin)) end - context 'when visiting homepage' do - it 'doesn’t load styleguide assets' do - get '/' - expect(response.body).to_not include('styleguide') + context "when visiting homepage" do + it "doesn’t load styleguide assets" do + get "/" + expect(response.body).to_not include("styleguide") end end - context 'when visiting styleguide' do - it 'loads styleguide assets' do - get '/styleguide' - expect(response.body).to include('styleguide') + context "when visiting styleguide" do + it "loads styleguide assets" do + get "/styleguide" + expect(response.body).to include("styleguide") end end end diff --git a/script/analyse_message_bus.rb b/script/analyse_message_bus.rb index a65bd53f02..ef3e0eacb3 100644 --- a/script/analyse_message_bus.rb +++ b/script/analyse_message_bus.rb @@ -9,23 +9,24 @@ wait_seconds = ARGV[0]&.to_i || 10 puts "Counting messages for #{wait_seconds} seconds..." -print 'Seen 0 messages' -t = Thread.new do - MessageBus.backend_instance.global_subscribe do |m| - channel = m.channel - if channel.start_with?("/distributed_hash") - payload = JSON.parse(m.data)["data"] - info = payload["hash_key"] - # info += ".#{payload["key"]}" # Uncomment if you need more granular info - channel += " (#{info})" +print "Seen 0 messages" +t = + Thread.new do + MessageBus.backend_instance.global_subscribe do |m| + channel = m.channel + if channel.start_with?("/distributed_hash") + payload = JSON.parse(m.data)["data"] + info = payload["hash_key"] + # info += ".#{payload["key"]}" # Uncomment if you need more granular info + channel += " (#{info})" + end + + channel_counters[channel] += 1 + messages_seen += 1 + + print "\rSeen #{messages_seen} messages from #{channel_counters.size} channels" end - - channel_counters[channel] += 1 - messages_seen += 1 - - print "\rSeen #{messages_seen} messages from #{channel_counters.size} channels" end -end sleep wait_seconds @@ -53,10 +54,12 @@ puts "| #{"channel".ljust(max_channel_name_length)} | #{"message count".rjust(ma puts "|#{"-" * (max_channel_name_length + 2)}|#{"-" * (max_count_length + 2)}|" result_count = 10 -sorted_results.first(result_count).each do |name, value| - name = "`#{name}`" - puts "| #{name.ljust(max_channel_name_length)} | #{value.to_s.rjust(max_count_length)} |" -end +sorted_results + .first(result_count) + .each do |name, value| + name = "`#{name}`" + puts "| #{name.ljust(max_channel_name_length)} | #{value.to_s.rjust(max_count_length)} |" + end other_count = messages_seen - sorted_results.first(result_count).sum { |k, v| v } puts "| #{"(other)".ljust(max_channel_name_length)} | #{other_count.to_s.rjust(max_count_length)} |" puts "|#{" " * (max_channel_name_length + 2)}|#{" " * (max_count_length + 2)}|" diff --git a/script/analyze_sidekiq_queues.rb b/script/analyze_sidekiq_queues.rb index fcb736d762..d5e9c4fd34 100644 --- a/script/analyze_sidekiq_queues.rb +++ b/script/analyze_sidekiq_queues.rb @@ -2,17 +2,14 @@ require File.expand_path("../../config/environment", __FILE__) -queues = %w{default low ultra_low critical}.map { |name| Sidekiq::Queue.new(name) }.lazy.flat_map(&:lazy) +queues = + %w[default low ultra_low critical].map { |name| Sidekiq::Queue.new(name) }.lazy.flat_map(&:lazy) stats = Hash.new(0) -queues.each do |j| - stats[j.klass] += 1 -end +queues.each { |j| stats[j.klass] += 1 } -stats.sort_by { |a, b| -b }.each do |name, count| - puts "#{name}: #{count}" -end +stats.sort_by { |a, b| -b }.each { |name, count| puts "#{name}: #{count}" } dupes = Hash.new([]) queues.each do |j| diff --git a/script/bench.rb b/script/bench.rb index e114cdd2cb..6e10dcd112 100644 --- a/script/bench.rb +++ b/script/bench.rb @@ -19,46 +19,43 @@ require "uri" @skip_asset_bundle = false @unicorn_workers = 3 -opts = OptionParser.new do |o| - o.banner = "Usage: ruby bench.rb [options]" +opts = + OptionParser.new do |o| + o.banner = "Usage: ruby bench.rb [options]" - o.on("-n", "--with_default_env", "Include recommended Discourse env") do - @include_env = true - end - o.on("-o", "--output [FILE]", "Output results to this file") do |f| - @result_file = f - end - o.on("-i", "--iterations [ITERATIONS]", "Number of iterations to run the bench for") do |i| - @iterations = i.to_i - end - o.on("-b", "--best_of [NUM]", "Number of times to run the bench taking best as result") do |i| - @best_of = i.to_i - end - o.on("-d", "--heap_dump") do - @dump_heap = true - # We need an env var for config/boot.rb to enable allocation tracing prior to framework init - ENV['DISCOURSE_DUMP_HEAP'] = "1" - end - o.on("-m", "--memory_stats") do - @mem_stats = true - end - o.on("-u", "--unicorn", "Use unicorn to serve pages as opposed to puma") do - @unicorn = true - end - o.on("-c", "--concurrency [NUM]", "Run benchmark with this number of concurrent requests (default: 1)") do |i| - @concurrency = i.to_i - end - o.on("-w", "--unicorn_workers [NUM]", "Run benchmark with this number of unicorn workers (default: 3)") do |i| - @unicorn_workers = i.to_i - end - o.on("-s", "--skip-bundle-assets", "Skip bundling assets") do - @skip_asset_bundle = true - end + o.on("-n", "--with_default_env", "Include recommended Discourse env") { @include_env = true } + o.on("-o", "--output [FILE]", "Output results to this file") { |f| @result_file = f } + o.on("-i", "--iterations [ITERATIONS]", "Number of iterations to run the bench for") do |i| + @iterations = i.to_i + end + o.on("-b", "--best_of [NUM]", "Number of times to run the bench taking best as result") do |i| + @best_of = i.to_i + end + o.on("-d", "--heap_dump") do + @dump_heap = true + # We need an env var for config/boot.rb to enable allocation tracing prior to framework init + ENV["DISCOURSE_DUMP_HEAP"] = "1" + end + o.on("-m", "--memory_stats") { @mem_stats = true } + o.on("-u", "--unicorn", "Use unicorn to serve pages as opposed to puma") { @unicorn = true } + o.on( + "-c", + "--concurrency [NUM]", + "Run benchmark with this number of concurrent requests (default: 1)", + ) { |i| @concurrency = i.to_i } + o.on( + "-w", + "--unicorn_workers [NUM]", + "Run benchmark with this number of unicorn workers (default: 3)", + ) { |i| @unicorn_workers = i.to_i } + o.on("-s", "--skip-bundle-assets", "Skip bundling assets") { @skip_asset_bundle = true } - o.on("-t", "--tests [STRING]", "List of tests to run. Example: '--tests topic,categories')") do |i| - @tests = i.split(",") + o.on( + "-t", + "--tests [STRING]", + "List of tests to run. Example: '--tests topic,categories')", + ) { |i| @tests = i.split(",") } end -end opts.parse! def run(command, opt = nil) @@ -73,7 +70,7 @@ def run(command, opt = nil) end begin - require 'facter' + require "facter" raise LoadError if Gem::Version.new(Facter.version) < Gem::Version.new("4.0") rescue LoadError run "gem install facter" @@ -113,7 +110,7 @@ end puts "Ensuring config is setup" -%x{which ab > /dev/null 2>&1} +`which ab > /dev/null 2>&1` unless $? == 0 abort "Apache Bench is not installed. Try: apt-get install apache2-utils or brew install ab" end @@ -125,7 +122,7 @@ end ENV["RAILS_ENV"] = "profile" -discourse_env_vars = %w( +discourse_env_vars = %w[ DISCOURSE_DUMP_HEAP RUBY_GC_HEAP_INIT_SLOTS RUBY_GC_HEAP_FREE_SLOTS @@ -140,27 +137,22 @@ discourse_env_vars = %w( RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR RUBY_GLOBAL_METHOD_CACHE_SIZE LD_PRELOAD -) +] if @include_env puts "Running with tuned environment" - discourse_env_vars.each do |v| - ENV.delete v - end - - ENV['RUBY_GLOBAL_METHOD_CACHE_SIZE'] = '131072' - ENV['RUBY_GC_HEAP_GROWTH_MAX_SLOTS'] = '40000' - ENV['RUBY_GC_HEAP_INIT_SLOTS'] = '400000' - ENV['RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR'] = '1.5' + discourse_env_vars.each { |v| ENV.delete v } + ENV["RUBY_GLOBAL_METHOD_CACHE_SIZE"] = "131072" + ENV["RUBY_GC_HEAP_GROWTH_MAX_SLOTS"] = "40000" + ENV["RUBY_GC_HEAP_INIT_SLOTS"] = "400000" + ENV["RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR"] = "1.5" else # clean env puts "Running with the following custom environment" end -discourse_env_vars.each do |w| - puts "#{w}: #{ENV[w]}" if ENV[w].to_s.length > 0 -end +discourse_env_vars.each { |w| puts "#{w}: #{ENV[w]}" if ENV[w].to_s.length > 0 } def port_available?(port) server = TCPServer.open("0.0.0.0", port) @@ -170,20 +162,16 @@ rescue Errno::EADDRINUSE false end -@port = 60079 +@port = 60_079 -while !port_available? @port - @port += 1 -end +@port += 1 while !port_available? @port puts "Ensuring profiling DB exists and is migrated" puts `bundle exec rake db:create` `bundle exec rake db:migrate` puts "Timing loading Rails" -measure("load_rails") do - `bundle exec rake middleware` -end +measure("load_rails") { `bundle exec rake middleware` } puts "Populating Profile DB" run("bundle exec ruby script/profile_db_generator.rb") @@ -223,16 +211,21 @@ begin pid = if @unicorn - ENV['UNICORN_PORT'] = @port.to_s - ENV['UNICORN_WORKERS'] = @unicorn_workers.to_s - FileUtils.mkdir_p(File.join('tmp', 'pids')) + ENV["UNICORN_PORT"] = @port.to_s + ENV["UNICORN_WORKERS"] = @unicorn_workers.to_s + FileUtils.mkdir_p(File.join("tmp", "pids")) unicorn_pid = spawn("bundle exec unicorn -c config/unicorn.conf.rb") - while (unicorn_master_pid = `ps aux | grep "unicorn master" | grep -v "grep" | awk '{print $2}'`.strip.to_i) == 0 + while ( + unicorn_master_pid = + `ps aux | grep "unicorn master" | grep -v "grep" | awk '{print $2}'`.strip.to_i + ) == 0 sleep 1 end - while `ps -f --ppid #{unicorn_master_pid} | grep worker | awk '{ print $2 }'`.split("\n").map(&:to_i).size != @unicorn_workers.to_i + while `ps -f --ppid #{unicorn_master_pid} | grep worker | awk '{ print $2 }'`.split("\n") + .map(&:to_i) + .size != @unicorn_workers.to_i sleep 1 end @@ -241,48 +234,38 @@ begin spawn("bundle exec puma -p #{@port} -e production") end - while port_available? @port - sleep 1 - end + sleep 1 while port_available? @port puts "Starting benchmark..." - admin_headers = { - 'Api-Key' => admin_api_key, - 'Api-Username' => "admin1" - } + admin_headers = { "Api-Key" => admin_api_key, "Api-Username" => "admin1" } - user_headers = { - 'User-Api-Key' => user_api_key - } + user_headers = { "User-Api-Key" => user_api_key } # asset precompilation is a dog, wget to force it run "curl -s -o /dev/null http://127.0.0.1:#{@port}/" redirect_response = `curl -s -I "http://127.0.0.1:#{@port}/t/i-am-a-topic-used-for-perf-tests"` - if redirect_response !~ /301 Moved Permanently/ - raise "Unable to locate topic for perf tests" - end + raise "Unable to locate topic for perf tests" if redirect_response !~ /301 Moved Permanently/ - topic_url = redirect_response.match(/^location: .+(\/t\/i-am-a-topic-used-for-perf-tests\/.+)$/i)[1].strip + topic_url = + redirect_response.match(%r{^location: .+(/t/i-am-a-topic-used-for-perf-tests/.+)$}i)[1].strip all_tests = [ - ["categories", "/categories"], - ["home", "/"], + %w[categories /categories], + %w[home /], ["topic", topic_url], ["topic.json", "#{topic_url}.json"], ["user activity", "/u/admin1/activity"], ] - @tests ||= %w{categories home topic} + @tests ||= %w[categories home topic] - tests_to_run = all_tests.select do |test_name, path| - @tests.include?(test_name) - end + tests_to_run = all_tests.select { |test_name, path| @tests.include?(test_name) } tests_to_run.concat( tests_to_run.map { |k, url| ["#{k} user", "#{url}", user_headers] }, - tests_to_run.map { |k, url| ["#{k} admin", "#{url}", admin_headers] } + tests_to_run.map { |k, url| ["#{k} admin", "#{url}", admin_headers] }, ) tests_to_run.each do |test_name, path, headers_for_path| @@ -290,15 +273,11 @@ begin http = Net::HTTP.new(uri.host, uri.port) request = Net::HTTP::Get.new(uri.request_uri) - headers_for_path&.each do |key, value| - request[key] = value - end + headers_for_path&.each { |key, value| request[key] = value } response = http.request(request) - if response.code != "200" - raise "#{test_name} #{path} returned non 200 response code" - end + raise "#{test_name} #{path} returned non 200 response code" if response.code != "200" end # NOTE: we run the most expensive page first in the bench @@ -335,11 +314,17 @@ begin Facter.reset facts = Facter.to_hash - facts.delete_if { |k, v| - !["operatingsystem", "architecture", "kernelversion", - "memorysize", "physicalprocessorcount", "processor0", - "virtual"].include?(k) - } + facts.delete_if do |k, v| + !%w[ + operatingsystem + architecture + kernelversion + memorysize + physicalprocessorcount + processor0 + virtual + ].include?(k) + end run("RAILS_ENV=profile bundle exec rake assets:clean") @@ -349,10 +334,13 @@ begin mem = get_mem(pid) - results = results.merge("timings" => @timings, - "ruby-version" => "#{RUBY_DESCRIPTION}", - "rss_kb" => mem["rss_kb"], - "pss_kb" => mem["pss_kb"]).merge(facts) + results = + results.merge( + "timings" => @timings, + "ruby-version" => "#{RUBY_DESCRIPTION}", + "rss_kb" => mem["rss_kb"], + "pss_kb" => mem["pss_kb"], + ).merge(facts) if @unicorn child_pids = `ps --ppid #{pid} | awk '{ print $1; }' | grep -v PID`.split("\n") @@ -375,12 +363,7 @@ begin puts open("http://127.0.0.1:#{@port}/admin/dump_heap", headers).read end - if @result_file - File.open(@result_file, "wb") do |f| - f.write(results) - end - end - + File.open(@result_file, "wb") { |f| f.write(results) } if @result_file ensure Process.kill "KILL", pid end diff --git a/script/benchmarks/cache/bench.rb b/script/benchmarks/cache/bench.rb index fad1b73607..87d4a1e4d3 100644 --- a/script/benchmarks/cache/bench.rb +++ b/script/benchmarks/cache/bench.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'benchmark/ips' -require File.expand_path('../../../../config/environment', __FILE__) +require "benchmark/ips" +require File.expand_path("../../../../config/environment", __FILE__) Benchmark.ips do |x| - x.report("redis setex string") do |times| while times > 0 Discourse.redis.setex("test_key", 60, "test") diff --git a/script/benchmarks/markdown/bench.rb b/script/benchmarks/markdown/bench.rb index 00bd7573d8..5e54d61755 100644 --- a/script/benchmarks/markdown/bench.rb +++ b/script/benchmarks/markdown/bench.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'benchmark/ips' -require File.expand_path('../../../../config/environment', __FILE__) +require "benchmark/ips" +require File.expand_path("../../../../config/environment", __FILE__) # set any flags here # MiniRacer::Platform.set_flags! :noturbo @@ -10,7 +10,7 @@ tests = [ ["tiny post", "**hello**"], ["giant post", File.read("giant_post.md")], ["most features", File.read("most_features.md")], - ["lots of mentions", File.read("lots_of_mentions.md")] + ["lots of mentions", File.read("lots_of_mentions.md")], ] PrettyText.cook("") @@ -31,9 +31,7 @@ PrettyText.v8.eval("window.commonmark = window.markdownit('commonmark')") Benchmark.ips do |x| [true, false].each do |sanitize| tests.each do |test, text| - x.report("#{test} sanitize: #{sanitize}") do - PrettyText.markdown(text, sanitize: sanitize) - end + x.report("#{test} sanitize: #{sanitize}") { PrettyText.markdown(text, sanitize: sanitize) } end end diff --git a/script/benchmarks/middleware/test.rb b/script/benchmarks/middleware/test.rb index 1432b9227e..8b0cca5f6e 100644 --- a/script/benchmarks/middleware/test.rb +++ b/script/benchmarks/middleware/test.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'memory_profiler' -require 'benchmark/ips' +require "memory_profiler" +require "benchmark/ips" ENV["RAILS_ENV"] = "production" @@ -14,12 +14,10 @@ def req "timings[1]" => "1001", "timings[2]" => "1001", "timings[3]" => "1001", - "topic_id" => "490310" + "topic_id" => "490310", } - data = data.map do |k, v| - "#{CGI.escape(k)}=#{v}" - end.join("&") + data = data.map { |k, v| "#{CGI.escape(k)}=#{v}" }.join("&") { "REQUEST_METHOD" => "POST", @@ -33,7 +31,7 @@ def req "HTTP_COOKIE" => "_t=#{_t}", "rack.input" => StringIO.new(data), "rack.version" => [1, 2], - "rack.url_scheme" => "http" + "rack.url_scheme" => "http", } end @@ -45,11 +43,7 @@ end exit # # -StackProf.run(mode: :wall, out: 'report.dump') do - 1000.times do - Rails.application.call(req) - end -end +StackProf.run(mode: :wall, out: "report.dump") { 1000.times { Rails.application.call(req) } } # # MemoryProfiler.start # Rails.application.call(req) diff --git a/script/benchmarks/site_setting/bench.rb b/script/benchmarks/site_setting/bench.rb index 5790b41ffa..02676e8570 100644 --- a/script/benchmarks/site_setting/bench.rb +++ b/script/benchmarks/site_setting/bench.rb @@ -1,37 +1,32 @@ # frozen_string_literal: true -require 'benchmark/ips' -require File.expand_path('../../../../config/environment', __FILE__) +require "benchmark/ips" +require File.expand_path("../../../../config/environment", __FILE__) # Put pre conditions here # Used db but it's OK in the most cases # build the cache SiteSetting.title = SecureRandom.hex -SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' +SiteSetting.default_locale = SiteSetting.default_locale == "en" ? "zh_CN" : "en" SiteSetting.refresh! tests = [ - ["current cache", lambda do - SiteSetting.title - SiteSetting.enable_discourse_connect - end + [ + "current cache", + lambda do + SiteSetting.title + SiteSetting.enable_discourse_connect + end, ], - ["change default locale with current cache refreshed", lambda do - SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' - end - ], - ["change site setting", lambda do - SiteSetting.title = SecureRandom.hex - end + [ + "change default locale with current cache refreshed", + lambda { SiteSetting.default_locale = SiteSetting.default_locale == "en" ? "zh_CN" : "en" }, ], + ["change site setting", lambda { SiteSetting.title = SecureRandom.hex }], ] -Benchmark.ips do |x| - tests.each do |test, proc| - x.report(test, proc) - end -end +Benchmark.ips { |x| tests.each { |test, proc| x.report(test, proc) } } # 2017-08-02 - Erick's Site Setting change diff --git a/script/benchmarks/site_setting/profile.rb b/script/benchmarks/site_setting/profile.rb index a849a18a36..fea7977b86 100644 --- a/script/benchmarks/site_setting/profile.rb +++ b/script/benchmarks/site_setting/profile.rb @@ -1,34 +1,26 @@ # frozen_string_literal: true -require 'ruby-prof' +require "ruby-prof" def profile(&blk) result = RubyProf.profile(&blk) printer = RubyProf::GraphHtmlPrinter.new(result) printer.print(STDOUT) end -profile { '' } # loading profiler dependency +profile { "" } # loading profiler dependency -require File.expand_path('../../../../config/environment', __FILE__) +require File.expand_path("../../../../config/environment", __FILE__) # warming up SiteSetting.title SiteSetting.enable_discourse_connect -SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' +SiteSetting.default_locale = SiteSetting.default_locale == "en" ? "zh_CN" : "en" SiteSetting.title = SecureRandom.hex -profile do - SiteSetting.title -end +profile { SiteSetting.title } -profile do - SiteSetting.enable_discourse_connect -end +profile { SiteSetting.enable_discourse_connect } -profile do - SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' -end +profile { SiteSetting.default_locale = SiteSetting.default_locale == "en" ? "zh_CN" : "en" } -profile do - SiteSetting.title = SecureRandom.hex -end +profile { SiteSetting.title = SecureRandom.hex } diff --git a/script/biggest_objects.rb b/script/biggest_objects.rb index 3f99cf109b..220fff3c00 100644 --- a/script/biggest_objects.rb +++ b/script/biggest_objects.rb @@ -2,35 +2,41 @@ # simple script to measure largest objects in memory post boot -if ENV['RAILS_ENV'] != "production" - exec "RAILS_ENV=production ruby #{__FILE__}" -end +exec "RAILS_ENV=production ruby #{__FILE__}" if ENV["RAILS_ENV"] != "production" -require 'objspace' +require "objspace" ObjectSpace.trace_object_allocations do - require File.expand_path("../../config/environment", __FILE__) - Rails.application.routes.recognize_path('abc') rescue nil + begin + Rails.application.routes.recognize_path("abc") + rescue StandardError + nil + end # load up the yaml for the localization bits, in master process I18n.t(:posts) RailsMultisite::ConnectionManagement.each_connection do (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end end - end -5.times do - GC.start(full_mark: true, immediate_sweep: true) -end +5.times { GC.start(full_mark: true, immediate_sweep: true) } [String, Array, Hash].each do |klass| - ObjectSpace.each_object(klass).sort { |a, b| b.length <=> a.length }.first(50).each do |obj| - puts "#{klass} size: #{obj.length} #{ObjectSpace.allocation_sourcefile(obj)} #{ObjectSpace.allocation_sourceline(obj)}" - end + ObjectSpace + .each_object(klass) + .sort { |a, b| b.length <=> a.length } + .first(50) + .each do |obj| + puts "#{klass} size: #{obj.length} #{ObjectSpace.allocation_sourcefile(obj)} #{ObjectSpace.allocation_sourceline(obj)}" + end end diff --git a/script/boot_mem.rb b/script/boot_mem.rb index 5780ae6058..44fb2b8407 100644 --- a/script/boot_mem.rb +++ b/script/boot_mem.rb @@ -2,22 +2,30 @@ # simple script to measure memory at boot -if ENV['RAILS_ENV'] != "production" - exec "RAILS_ENV=production ruby #{__FILE__}" -end +exec "RAILS_ENV=production ruby #{__FILE__}" if ENV["RAILS_ENV"] != "production" -require 'memory_profiler' +require "memory_profiler" -MemoryProfiler.report do - require File.expand_path("../../config/environment", __FILE__) +MemoryProfiler + .report do + require File.expand_path("../../config/environment", __FILE__) - Rails.application.routes.recognize_path('abc') rescue nil + begin + Rails.application.routes.recognize_path("abc") + rescue StandardError + nil + end - # load up the yaml for the localization bits, in master process - I18n.t(:posts) + # load up the yaml for the localization bits, in master process + I18n.t(:posts) - # load up all models and schema - (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + # load up all models and schema + (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| + begin + table.classify.constantize.first + rescue StandardError + nil + end + end end -end.pretty_print + .pretty_print diff --git a/script/bulk_import/base.rb b/script/bulk_import/base.rb index 767469a83b..d92878b705 100644 --- a/script/bulk_import/base.rb +++ b/script/bulk_import/base.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if ARGV.include?('bbcode-to-md') +if ARGV.include?("bbcode-to-md") # Replace (most) bbcode with markdown before creating posts. # This will dramatically clean up the final posts in Discourse. # @@ -10,7 +10,7 @@ if ARGV.include?('bbcode-to-md') # cd ruby-bbcode-to-md # gem build ruby-bbcode-to-md.gemspec # gem install ruby-bbcode-to-md-*.gem - require 'ruby-bbcode-to-md' + require "ruby-bbcode-to-md" end require "pg" @@ -20,12 +20,12 @@ require "htmlentities" puts "Loading application..." require_relative "../../config/environment" -require_relative '../import_scripts/base/uploader' +require_relative "../import_scripts/base/uploader" -module BulkImport; end +module BulkImport +end class BulkImport::Base - NOW ||= "now()" PRIVATE_OFFSET ||= 2**30 @@ -33,41 +33,41 @@ class BulkImport::Base CHARSET_MAP = { "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, + "ascii" => Encoding::US_ASCII, + "big5" => Encoding::Big5, + "binary" => Encoding::ASCII_8BIT, + "cp1250" => Encoding::Windows_1250, + "cp1251" => Encoding::Windows_1251, + "cp1256" => Encoding::Windows_1256, + "cp1257" => Encoding::Windows_1257, + "cp850" => Encoding::CP850, + "cp852" => Encoding::CP852, + "cp866" => Encoding::IBM866, + "cp932" => Encoding::Windows_31J, + "dec8" => nil, + "eucjpms" => Encoding::EucJP_ms, + "euckr" => Encoding::EUC_KR, + "gb2312" => Encoding::EUC_CN, + "gbk" => Encoding::GBK, + "geostd8" => nil, + "greek" => Encoding::ISO_8859_7, + "hebrew" => Encoding::ISO_8859_8, + "hp8" => nil, + "keybcs2" => nil, + "koi8r" => Encoding::KOI8_R, + "koi8u" => Encoding::KOI8_U, + "latin1" => Encoding::ISO_8859_1, + "latin2" => Encoding::ISO_8859_2, + "latin5" => Encoding::ISO_8859_9, + "latin7" => Encoding::ISO_8859_13, + "macce" => Encoding::MacCentEuro, "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, + "sjis" => Encoding::SHIFT_JIS, + "swe7" => nil, + "tis620" => Encoding::TIS_620, + "ucs2" => Encoding::UTF_16BE, + "ujis" => Encoding::EucJP_ms, + "utf8" => Encoding::UTF_8, } # rubocop:enable Layout/HashAlignment @@ -82,12 +82,13 @@ class BulkImport::Base @encoding = CHARSET_MAP[charset] @bbcode_to_md = true if use_bbcode_to_md? - @markdown = Redcarpet::Markdown.new( - Redcarpet::Render::HTML.new(hard_wrap: true), - no_intra_emphasis: true, - fenced_code_blocks: true, - autolink: true - ) + @markdown = + Redcarpet::Markdown.new( + Redcarpet::Render::HTML.new(hard_wrap: true), + no_intra_emphasis: true, + fenced_code_blocks: true, + autolink: true, + ) end def run @@ -132,7 +133,9 @@ class BulkImport::Base map = [] ids = [] - @raw_connection.send_query("SELECT value, #{name}_id FROM #{name}_custom_fields WHERE name = 'import_id'") + @raw_connection.send_query( + "SELECT value, #{name}_id FROM #{name}_custom_fields WHERE name = 'import_id'", + ) @raw_connection.set_single_row_mode @raw_connection.get_result.stream_each do |row| @@ -163,12 +166,14 @@ class BulkImport::Base puts "Loading imported topic ids..." @topics, imported_topic_ids = imported_ids("topic") @last_imported_topic_id = imported_topic_ids.select { |id| id < PRIVATE_OFFSET }.max || -1 - @last_imported_private_topic_id = imported_topic_ids.select { |id| id > PRIVATE_OFFSET }.max || (PRIVATE_OFFSET - 1) + @last_imported_private_topic_id = + imported_topic_ids.select { |id| id > PRIVATE_OFFSET }.max || (PRIVATE_OFFSET - 1) puts "Loading imported post ids..." @posts, imported_post_ids = imported_ids("post") @last_imported_post_id = imported_post_ids.select { |id| id < PRIVATE_OFFSET }.max || -1 - @last_imported_private_post_id = imported_post_ids.select { |id| id > PRIVATE_OFFSET }.max || (PRIVATE_OFFSET - 1) + @last_imported_private_post_id = + imported_post_ids.select { |id| id > PRIVATE_OFFSET }.max || (PRIVATE_OFFSET - 1) end def last_id(klass) @@ -182,9 +187,7 @@ class BulkImport::Base @raw_connection.send_query("SELECT id, #{column} FROM #{name}") @raw_connection.set_single_row_mode - @raw_connection.get_result.stream_each do |row| - map[row["id"].to_i] = row[column].to_i - end + @raw_connection.get_result.stream_each { |row| map[row["id"].to_i] = row[column].to_i } @raw_connection.get_result @@ -199,13 +202,24 @@ class BulkImport::Base puts "Loading users indexes..." @last_user_id = last_id(User) @last_user_email_id = last_id(UserEmail) - @emails = User.unscoped.joins(:user_emails).pluck(:"user_emails.email", :"user_emails.user_id").to_h + @emails = + User.unscoped.joins(:user_emails).pluck(:"user_emails.email", :"user_emails.user_id").to_h @usernames_lower = User.unscoped.pluck(:username_lower).to_set - @mapped_usernames = UserCustomField.joins(:user).where(name: "import_username").pluck("user_custom_fields.value", "users.username").to_h + @mapped_usernames = + UserCustomField + .joins(:user) + .where(name: "import_username") + .pluck("user_custom_fields.value", "users.username") + .to_h puts "Loading categories indexes..." @last_category_id = last_id(Category) - @category_names = Category.unscoped.pluck(:parent_category_id, :name).map { |pci, name| "#{pci}-#{name}" }.to_set + @category_names = + Category + .unscoped + .pluck(:parent_category_id, :name) + .map { |pci, name| "#{pci}-#{name}" } + .to_set puts "Loading topics indexes..." @last_topic_id = last_id(Topic) @@ -233,13 +247,27 @@ class BulkImport::Base def fix_primary_keys puts "Updating primary key sequences..." - @raw_connection.exec("SELECT setval('#{Group.sequence_name}', #{@last_group_id})") if @last_group_id > 0 - @raw_connection.exec("SELECT setval('#{User.sequence_name}', #{@last_user_id})") if @last_user_id > 0 - @raw_connection.exec("SELECT setval('#{UserEmail.sequence_name}', #{@last_user_email_id})") if @last_user_email_id > 0 - @raw_connection.exec("SELECT setval('#{Category.sequence_name}', #{@last_category_id})") if @last_category_id > 0 - @raw_connection.exec("SELECT setval('#{Topic.sequence_name}', #{@last_topic_id})") if @last_topic_id > 0 - @raw_connection.exec("SELECT setval('#{Post.sequence_name}', #{@last_post_id})") if @last_post_id > 0 - @raw_connection.exec("SELECT setval('#{PostAction.sequence_name}', #{@last_post_action_id})") if @last_post_action_id > 0 + if @last_group_id > 0 + @raw_connection.exec("SELECT setval('#{Group.sequence_name}', #{@last_group_id})") + end + if @last_user_id > 0 + @raw_connection.exec("SELECT setval('#{User.sequence_name}', #{@last_user_id})") + end + if @last_user_email_id > 0 + @raw_connection.exec("SELECT setval('#{UserEmail.sequence_name}', #{@last_user_email_id})") + end + if @last_category_id > 0 + @raw_connection.exec("SELECT setval('#{Category.sequence_name}', #{@last_category_id})") + end + if @last_topic_id > 0 + @raw_connection.exec("SELECT setval('#{Topic.sequence_name}', #{@last_topic_id})") + end + if @last_post_id > 0 + @raw_connection.exec("SELECT setval('#{Post.sequence_name}', #{@last_post_id})") + end + if @last_post_action_id > 0 + @raw_connection.exec("SELECT setval('#{PostAction.sequence_name}', #{@last_post_action_id})") + end end def group_id_from_imported_id(id) @@ -272,63 +300,124 @@ class BulkImport::Base post_id && @topic_id_by_post_id[post_id] end - GROUP_COLUMNS ||= %i{ - id name title bio_raw bio_cooked created_at updated_at - } + GROUP_COLUMNS ||= %i[id name title bio_raw bio_cooked created_at updated_at] - USER_COLUMNS ||= %i{ - id username username_lower name active trust_level admin moderator - date_of_birth ip_address registration_ip_address primary_group_id - suspended_at suspended_till last_emailed_at created_at updated_at - } + USER_COLUMNS ||= %i[ + id + username + username_lower + name + active + trust_level + admin + moderator + date_of_birth + ip_address + registration_ip_address + primary_group_id + suspended_at + suspended_till + last_emailed_at + created_at + updated_at + ] - USER_EMAIL_COLUMNS ||= %i{ - id user_id email primary created_at updated_at - } + USER_EMAIL_COLUMNS ||= %i[id user_id email primary created_at updated_at] - USER_STAT_COLUMNS ||= %i{ - user_id topics_entered time_read days_visited posts_read_count - likes_given likes_received new_since read_faq - first_post_created_at post_count topic_count bounce_score - reset_bounce_score_after digest_attempted_at - } + USER_STAT_COLUMNS ||= %i[ + user_id + topics_entered + time_read + days_visited + posts_read_count + likes_given + likes_received + new_since + read_faq + first_post_created_at + post_count + topic_count + bounce_score + reset_bounce_score_after + digest_attempted_at + ] - USER_PROFILE_COLUMNS ||= %i{ - user_id location website bio_raw bio_cooked views - } + USER_PROFILE_COLUMNS ||= %i[user_id location website bio_raw bio_cooked views] - GROUP_USER_COLUMNS ||= %i{ - group_id user_id created_at updated_at - } + GROUP_USER_COLUMNS ||= %i[group_id user_id created_at updated_at] - CATEGORY_COLUMNS ||= %i{ - id name name_lower slug user_id description position parent_category_id - created_at updated_at - } + CATEGORY_COLUMNS ||= %i[ + id + name + name_lower + slug + user_id + description + position + parent_category_id + created_at + updated_at + ] - TOPIC_COLUMNS ||= %i{ - id archetype title fancy_title slug user_id last_post_user_id category_id - visible closed pinned_at views created_at bumped_at updated_at - } + TOPIC_COLUMNS ||= %i[ + id + archetype + title + fancy_title + slug + user_id + last_post_user_id + category_id + visible + closed + pinned_at + views + created_at + bumped_at + updated_at + ] - POST_COLUMNS ||= %i{ - id user_id last_editor_id topic_id post_number sort_order reply_to_post_number - like_count raw cooked hidden word_count created_at last_version_at updated_at - } + POST_COLUMNS ||= %i[ + id + user_id + last_editor_id + topic_id + post_number + sort_order + reply_to_post_number + like_count + raw + cooked + hidden + word_count + created_at + last_version_at + updated_at + ] - POST_ACTION_COLUMNS ||= %i{ - id post_id user_id post_action_type_id deleted_at created_at updated_at - deleted_by_id related_post_id staff_took_action deferred_by_id targets_topic - agreed_at agreed_by_id deferred_at disagreed_at disagreed_by_id - } + POST_ACTION_COLUMNS ||= %i[ + id + post_id + user_id + post_action_type_id + deleted_at + created_at + updated_at + deleted_by_id + related_post_id + staff_took_action + deferred_by_id + targets_topic + agreed_at + agreed_by_id + deferred_at + disagreed_at + disagreed_by_id + ] - TOPIC_ALLOWED_USER_COLUMNS ||= %i{ - topic_id user_id created_at updated_at - } + TOPIC_ALLOWED_USER_COLUMNS ||= %i[topic_id user_id created_at updated_at] - TOPIC_TAG_COLUMNS ||= %i{ - topic_id tag_id created_at updated_at - } + TOPIC_TAG_COLUMNS ||= %i[topic_id tag_id created_at updated_at] def create_groups(rows, &block) create_records(rows, "group", GROUP_COLUMNS, &block) @@ -340,10 +429,7 @@ class BulkImport::Base create_records(rows, "user", USER_COLUMNS, &block) create_custom_fields("user", "username", @imported_usernames.keys) do |username| - { - record_id: @imported_usernames[username], - value: username, - } + { record_id: @imported_usernames[username], value: username } end end @@ -389,8 +475,8 @@ class BulkImport::Base group[:name] = group_name end - group[:title] = group[:title].scrub.strip.presence if group[:title].present? - group[:bio_raw] = group[:bio_raw].scrub.strip.presence if group[:bio_raw].present? + group[:title] = group[:title].scrub.strip.presence if group[:title].present? + group[:bio_raw] = group[:bio_raw].scrub.strip.presence if group[:bio_raw].present? group[:bio_cooked] = pre_cook(group[:bio_raw]) if group[:bio_raw].present? group[:created_at] ||= NOW group[:updated_at] ||= group[:created_at] @@ -456,7 +542,9 @@ class BulkImport::Base user_email[:email] ||= random_email user_email[:email].downcase! # unique email - user_email[:email] = random_email until EmailAddressValidator.valid_value?(user_email[:email]) && !@emails.has_key?(user_email[:email]) + user_email[:email] = random_email until EmailAddressValidator.valid_value?( + user_email[:email], + ) && !@emails.has_key?(user_email[:email]) user_email end @@ -539,7 +627,11 @@ class BulkImport::Base post[:raw] = (post[:raw] || "").scrub.strip.presence || "" post[:raw] = process_raw post[:raw] if @bbcode_to_md - post[:raw] = post[:raw].bbcode_to_md(false, {}, :disable, :quote) rescue post[:raw] + post[:raw] = begin + post[:raw].bbcode_to_md(false, {}, :disable, :quote) + rescue StandardError + post[:raw] + end end post[:like_count] ||= 0 post[:cooked] = pre_cook post[:raw] @@ -580,22 +672,22 @@ class BulkImport::Base # [HTML]...[/HTML] raw.gsub!(/\[HTML\]/i, "\n\n```html\n") - raw.gsub!(/\[\/HTML\]/i, "\n```\n\n") + raw.gsub!(%r{\[/HTML\]}i, "\n```\n\n") # [PHP]...[/PHP] raw.gsub!(/\[PHP\]/i, "\n\n```php\n") - raw.gsub!(/\[\/PHP\]/i, "\n```\n\n") + raw.gsub!(%r{\[/PHP\]}i, "\n```\n\n") # [HIGHLIGHT="..."] raw.gsub!(/\[HIGHLIGHT="?(\w+)"?\]/i) { "\n\n```#{$1.downcase}\n" } # [CODE]...[/CODE] # [HIGHLIGHT]...[/HIGHLIGHT] - raw.gsub!(/\[\/?CODE\]/i, "\n\n```\n\n") - raw.gsub!(/\[\/?HIGHLIGHT\]/i, "\n\n```\n\n") + raw.gsub!(%r{\[/?CODE\]}i, "\n\n```\n\n") + raw.gsub!(%r{\[/?HIGHLIGHT\]}i, "\n\n```\n\n") # [SAMP]...[/SAMP] - raw.gsub!(/\[\/?SAMP\]/i, "`") + raw.gsub!(%r{\[/?SAMP\]}i, "`") # replace all chevrons with HTML entities # /!\ must be done /!\ @@ -609,61 +701,61 @@ class BulkImport::Base raw.gsub!(">", ">") raw.gsub!("\u2603", ">") - raw.gsub!(/\[\/?I\]/i, "*") - raw.gsub!(/\[\/?B\]/i, "**") - raw.gsub!(/\[\/?U\]/i, "") + raw.gsub!(%r{\[/?I\]}i, "*") + raw.gsub!(%r{\[/?B\]}i, "**") + raw.gsub!(%r{\[/?U\]}i, "") - raw.gsub!(/\[\/?RED\]/i, "") - raw.gsub!(/\[\/?BLUE\]/i, "") + raw.gsub!(%r{\[/?RED\]}i, "") + raw.gsub!(%r{\[/?BLUE\]}i, "") - raw.gsub!(/\[AUTEUR\].+?\[\/AUTEUR\]/im, "") - raw.gsub!(/\[VOIRMSG\].+?\[\/VOIRMSG\]/im, "") - raw.gsub!(/\[PSEUDOID\].+?\[\/PSEUDOID\]/im, "") + raw.gsub!(%r{\[AUTEUR\].+?\[/AUTEUR\]}im, "") + raw.gsub!(%r{\[VOIRMSG\].+?\[/VOIRMSG\]}im, "") + raw.gsub!(%r{\[PSEUDOID\].+?\[/PSEUDOID\]}im, "") # [IMG]...[/IMG] - raw.gsub!(/(?:\s*\[IMG\]\s*)+(.+?)(?:\s*\[\/IMG\]\s*)+/im) { "\n\n#{$1}\n\n" } + raw.gsub!(%r{(?:\s*\[IMG\]\s*)+(.+?)(?:\s*\[/IMG\]\s*)+}im) { "\n\n#{$1}\n\n" } # [IMG=url] raw.gsub!(/\[IMG=([^\]]*)\]/im) { "\n\n#{$1}\n\n" } # [URL=...]...[/URL] - raw.gsub!(/\[URL="?(.+?)"?\](.+?)\[\/URL\]/im) { "[#{$2.strip}](#{$1})" } + raw.gsub!(%r{\[URL="?(.+?)"?\](.+?)\[/URL\]}im) { "[#{$2.strip}](#{$1})" } # [URL]...[/URL] # [MP3]...[/MP3] # [EMAIL]...[/EMAIL] # [LEFT]...[/LEFT] - raw.gsub!(/\[\/?URL\]/i, "") - raw.gsub!(/\[\/?MP3\]/i, "") - raw.gsub!(/\[\/?EMAIL\]/i, "") - raw.gsub!(/\[\/?LEFT\]/i, "") + raw.gsub!(%r{\[/?URL\]}i, "") + raw.gsub!(%r{\[/?MP3\]}i, "") + raw.gsub!(%r{\[/?EMAIL\]}i, "") + raw.gsub!(%r{\[/?LEFT\]}i, "") # [FONT=blah] and [COLOR=blah] - raw.gsub!(/\[FONT=.*?\](.*?)\[\/FONT\]/im, "\\1") - raw.gsub!(/\[COLOR=.*?\](.*?)\[\/COLOR\]/im, "\\1") + raw.gsub!(%r{\[FONT=.*?\](.*?)\[/FONT\]}im, "\\1") + raw.gsub!(%r{\[COLOR=.*?\](.*?)\[/COLOR\]}im, "\\1") - raw.gsub!(/\[SIZE=.*?\](.*?)\[\/SIZE\]/im, "\\1") - raw.gsub!(/\[H=.*?\](.*?)\[\/H\]/im, "\\1") + raw.gsub!(%r{\[SIZE=.*?\](.*?)\[/SIZE\]}im, "\\1") + raw.gsub!(%r{\[H=.*?\](.*?)\[/H\]}im, "\\1") # [CENTER]...[/CENTER] - raw.gsub!(/\[CENTER\](.*?)\[\/CENTER\]/im, "\\1") + raw.gsub!(%r{\[CENTER\](.*?)\[/CENTER\]}im, "\\1") # [INDENT]...[/INDENT] - raw.gsub!(/\[INDENT\](.*?)\[\/INDENT\]/im, "\\1") - raw.gsub!(/\[TABLE\](.*?)\[\/TABLE\]/im, "\\1") - raw.gsub!(/\[TR\](.*?)\[\/TR\]/im, "\\1") - raw.gsub!(/\[TD\](.*?)\[\/TD\]/im, "\\1") - raw.gsub!(/\[TD="?.*?"?\](.*?)\[\/TD\]/im, "\\1") + raw.gsub!(%r{\[INDENT\](.*?)\[/INDENT\]}im, "\\1") + raw.gsub!(%r{\[TABLE\](.*?)\[/TABLE\]}im, "\\1") + raw.gsub!(%r{\[TR\](.*?)\[/TR\]}im, "\\1") + raw.gsub!(%r{\[TD\](.*?)\[/TD\]}im, "\\1") + raw.gsub!(%r{\[TD="?.*?"?\](.*?)\[/TD\]}im, "\\1") # [STRIKE] raw.gsub!(/\[STRIKE\]/i, "") - raw.gsub!(/\[\/STRIKE\]/i, "") + raw.gsub!(%r{\[/STRIKE\]}i, "") # [QUOTE]...[/QUOTE] raw.gsub!(/\[QUOTE="([^\]]+)"\]/i) { "[QUOTE=#{$1}]" } # Nested Quotes - raw.gsub!(/(\[\/?QUOTE.*?\])/mi) { |q| "\n#{q}\n" } + raw.gsub!(%r{(\[/?QUOTE.*?\])}mi) { |q| "\n#{q}\n" } # raw.gsub!(/\[QUOTE\](.+?)\[\/QUOTE\]/im) { |quote| # quote.gsub!(/\[QUOTE\](.+?)\[\/QUOTE\]/im) { "\n#{$1}\n" } @@ -686,28 +778,36 @@ class BulkImport::Base end # [YOUTUBE][/YOUTUBE] - raw.gsub!(/\[YOUTUBE\](.+?)\[\/YOUTUBE\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } - raw.gsub!(/\[DAILYMOTION\](.+?)\[\/DAILYMOTION\]/i) { "\nhttps://www.dailymotion.com/video/#{$1}\n" } + raw.gsub!(%r{\[YOUTUBE\](.+?)\[/YOUTUBE\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + raw.gsub!(%r{\[DAILYMOTION\](.+?)\[/DAILYMOTION\]}i) do + "\nhttps://www.dailymotion.com/video/#{$1}\n" + end # [VIDEO=youtube;]...[/VIDEO] - raw.gsub!(/\[VIDEO=YOUTUBE;([^\]]+)\].*?\[\/VIDEO\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } - raw.gsub!(/\[VIDEO=DAILYMOTION;([^\]]+)\].*?\[\/VIDEO\]/i) { "\nhttps://www.dailymotion.com/video/#{$1}\n" } + raw.gsub!(%r{\[VIDEO=YOUTUBE;([^\]]+)\].*?\[/VIDEO\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$1}\n" + end + raw.gsub!(%r{\[VIDEO=DAILYMOTION;([^\]]+)\].*?\[/VIDEO\]}i) do + "\nhttps://www.dailymotion.com/video/#{$1}\n" + end # [SPOILER=Some hidden stuff]SPOILER HERE!![/SPOILER] - raw.gsub!(/\[SPOILER="?(.+?)"?\](.+?)\[\/SPOILER\]/im) { "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" } + raw.gsub!(%r{\[SPOILER="?(.+?)"?\](.+?)\[/SPOILER\]}im) do + "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" + end # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) # (https://meta.discourse.org/t/phpbb-3-importer-old/17397) - raw.gsub!(/\[list\](.*?)\[\/list\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\|?[^\]]*\](.*?)\[\/list\]/im, '[ol]\1[/ol]') - raw.gsub!(/\[list\](.*?)\[\/list:u\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\|?[^\]]*\](.*?)\[\/list:o\]/im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\|?[^\]]*\](.*?)\[/list\]}im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list:u\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\|?[^\]]*\](.*?)\[/list:o\]}im, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - raw.gsub!(/\[\*\]\n/, '') - raw.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]') + raw.gsub!(/\[\*\]\n/, "") + raw.gsub!(%r{\[\*\](.*?)\[/\*:m\]}, '[li]\1[/li]') raw.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]') - raw.gsub!(/\[\*=1\]/, '') + raw.gsub!(/\[\*=1\]/, "") raw end @@ -728,7 +828,9 @@ class BulkImport::Base imported_ids |= mapped[:imported_ids] unless mapped[:imported_ids].nil? @raw_connection.put_copy_data columns.map { |c| processed[c] } unless processed[:skip] rows_created += 1 - print "\r%7d - %6d/sec" % [rows_created, rows_created.to_f / (Time.now - start)] if rows_created % 100 == 0 + if rows_created % 100 == 0 + print "\r%7d - %6d/sec" % [rows_created, rows_created.to_f / (Time.now - start)] + end rescue => e puts "\n" puts "ERROR: #{e.message}" @@ -737,15 +839,14 @@ class BulkImport::Base end end - print "\r%7d - %6d/sec\n" % [rows_created, rows_created.to_f / (Time.now - start)] if rows_created > 0 + if rows_created > 0 + print "\r%7d - %6d/sec\n" % [rows_created, rows_created.to_f / (Time.now - start)] + end id_mapping_method_name = "#{name}_id_from_imported_id".freeze return unless respond_to?(id_mapping_method_name) create_custom_fields(name, "id", imported_ids) do |imported_id| - { - record_id: send(id_mapping_method_name, imported_id), - value: imported_id, - } + { record_id: send(id_mapping_method_name, imported_id), value: imported_id } end rescue => e # FIXME: errors catched here stop the rest of the COPY @@ -755,7 +856,8 @@ class BulkImport::Base def create_custom_fields(table, name, rows) name = "import_#{name}" - sql = "COPY #{table}_custom_fields (#{table}_id, name, value, created_at, updated_at) FROM STDIN" + sql = + "COPY #{table}_custom_fields (#{table}_id, name, value, created_at, updated_at) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do rows.each do |row| next unless cf = yield(row) @@ -797,7 +899,7 @@ class BulkImport::Base cooked = raw # Convert YouTube URLs to lazyYT DOMs before being transformed into links - cooked.gsub!(/\nhttps\:\/\/www.youtube.com\/watch\?v=(\w+)\n/) do + cooked.gsub!(%r{\nhttps\://www.youtube.com/watch\?v=(\w+)\n}) do video_id = $1 result = <<-HTML
    @@ -807,7 +909,7 @@ class BulkImport::Base cooked = @markdown.render(cooked).scrub.strip - cooked.gsub!(/\[QUOTE="?([^,"]+)(?:, post:(\d+), topic:(\d+))?"?\](.+?)\[\/QUOTE\]/im) do + cooked.gsub!(%r{\[QUOTE="?([^,"]+)(?:, post:(\d+), topic:(\d+))?"?\](.+?)\[/QUOTE\]}im) do username, post_id, topic_id, quote = $1, $2, $3, $4 quote = quote.scrub.strip @@ -860,5 +962,4 @@ class BulkImport::Base return text if @encoding == Encoding::UTF_8 text && text.encode(@encoding).force_encoding(Encoding::UTF_8) end - end diff --git a/script/bulk_import/discourse_merger.rb b/script/bulk_import/discourse_merger.rb index dc555850b8..61106e6bf3 100644 --- a/script/bulk_import/discourse_merger.rb +++ b/script/bulk_import/discourse_merger.rb @@ -3,9 +3,8 @@ require_relative "base" class BulkImport::DiscourseMerger < BulkImport::Base - NOW ||= "now()" - CUSTOM_FIELDS = ['category', 'group', 'post', 'topic', 'user'] + CUSTOM_FIELDS = %w[category group post topic user] # DB_NAME: name of database being merged into the current local db # DB_HOST: hostname of database being merged @@ -17,31 +16,36 @@ class BulkImport::DiscourseMerger < BulkImport::Base # e.g. https://discourse-cdn-sjc1.com/business4 def initialize - db_password = ENV["DB_PASS"] || 'import_password' + db_password = ENV["DB_PASS"] || "import_password" local_db = ActiveRecord::Base.connection_db_config.configuration_hash - @raw_connection = PG.connect(dbname: local_db[:database], host: 'localhost', port: local_db[:port], user: 'postgres', password: db_password) + @raw_connection = + PG.connect( + dbname: local_db[:database], + host: "localhost", + port: local_db[:port], + user: "postgres", + password: db_password, + ) @source_db_config = { - dbname: ENV["DB_NAME"] || 'dd_demo', - host: ENV["DB_HOST"] || 'localhost', - user: 'postgres', - password: db_password + dbname: ENV["DB_NAME"] || "dd_demo", + host: ENV["DB_HOST"] || "localhost", + user: "postgres", + password: db_password, } - raise "SOURCE_BASE_URL missing!" unless ENV['SOURCE_BASE_URL'] + raise "SOURCE_BASE_URL missing!" unless ENV["SOURCE_BASE_URL"] @source_base_url = ENV["SOURCE_BASE_URL"] - @uploads_path = ENV['UPLOADS_PATH'] + @uploads_path = ENV["UPLOADS_PATH"] @uploader = ImportScripts::Uploader.new - if ENV['SOURCE_CDN'] - @source_cdn = ENV['SOURCE_CDN'] - end + @source_cdn = ENV["SOURCE_CDN"] if ENV["SOURCE_CDN"] local_version = @raw_connection.exec("select max(version) from schema_migrations") - local_version = local_version.first['max'] + local_version = local_version.first["max"] source_version = source_raw_connection.exec("select max(version) from schema_migrations") - source_version = source_version.first['max'] + source_version = source_version.first["max"] if local_version != source_version raise "DB schema mismatch. Databases must be at the same migration version. Local is #{local_version}, other is #{source_version}" @@ -62,7 +66,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base @auto_group_ids = Group::AUTO_GROUPS.values # add your authorized extensions here: - SiteSetting.authorized_extensions = ['jpg', 'jpeg', 'png', 'gif'].join('|') + SiteSetting.authorized_extensions = %w[jpg jpeg png gif].join("|") @sequences = {} end @@ -99,7 +103,7 @@ class BulkImport::DiscourseMerger < BulkImport::Base end def copy_users - puts '', "merging users..." + puts "", "merging users..." imported_ids = [] @@ -109,34 +113,38 @@ class BulkImport::DiscourseMerger < BulkImport::Base sql = "COPY users (#{columns.map { |c| "\"#{c}\"" }.join(",")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec("SELECT #{columns.map { |c| "u.\"#{c}\"" }.join(",")}, e.email FROM users u INNER JOIN user_emails e ON (u.id = e.user_id AND e.primary = TRUE) WHERE u.id > 0").each do |row| - old_user_id = row['id']&.to_i - if existing = UserEmail.where(email: row.delete('email')).first&.user - # Merge these users - @users[old_user_id] = existing.id - @merged_user_ids << old_user_id - next - else - # New user - unless usernames_lower.add?(row['username_lower']) - username = row['username'] + "_1" - username.next! until usernames_lower.add?(username.downcase) - row['username'] = username - row['username_lower'] = row['username'].downcase + source_raw_connection + .exec( + "SELECT #{columns.map { |c| "u.\"#{c}\"" }.join(",")}, e.email FROM users u INNER JOIN user_emails e ON (u.id = e.user_id AND e.primary = TRUE) WHERE u.id > 0", + ) + .each do |row| + old_user_id = row["id"]&.to_i + if existing = UserEmail.where(email: row.delete("email")).first&.user + # Merge these users + @users[old_user_id] = existing.id + @merged_user_ids << old_user_id + next + else + # New user + unless usernames_lower.add?(row["username_lower"]) + username = row["username"] + "_1" + username.next! until usernames_lower.add?(username.downcase) + row["username"] = username + row["username_lower"] = row["username"].downcase + end + + row["id"] = (@last_user_id += 1) + @users[old_user_id] = row["id"] + + @raw_connection.put_copy_data row.values end - - row['id'] = (@last_user_id += 1) - @users[old_user_id] = row['id'] - - @raw_connection.put_copy_data row.values + imported_ids << old_user_id end - imported_ids << old_user_id - end end @sequences[User.sequence_name] = @last_user_id + 1 if @last_user_id - create_custom_fields('user', 'id', imported_ids) do |old_user_id| + create_custom_fields("user", "id", imported_ids) do |old_user_id| { value: old_user_id, record_id: user_id_from_imported_id(old_user_id) } end end @@ -147,28 +155,32 @@ class BulkImport::DiscourseMerger < BulkImport::Base skip_if_merged: true, is_a_user_model: true, skip_processing: true, - mapping: @email_tokens + mapping: @email_tokens, ) [ - UserEmail, UserStat, UserOption, UserProfile, - UserVisit, UserSearchData, GivenDailyLike, UserSecondFactor - ].each do |c| - copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true) - end + UserEmail, + UserStat, + UserOption, + UserProfile, + UserVisit, + UserSearchData, + GivenDailyLike, + UserSecondFactor, + ].each { |c| copy_model(c, skip_if_merged: true, is_a_user_model: true, skip_processing: true) } - [UserAssociatedAccount, Oauth2UserInfo, - SingleSignOnRecord, EmailChangeRequest - ].each do |c| + [UserAssociatedAccount, Oauth2UserInfo, SingleSignOnRecord, EmailChangeRequest].each do |c| copy_model(c, skip_if_merged: true, is_a_user_model: true) end end def copy_groups - copy_model(Group, + copy_model( + Group, mapping: @groups, skip_processing: true, - select_sql: "SELECT #{Group.columns.map { |c| "\"#{c.name}\"" }.join(', ')} FROM groups WHERE automatic = false" + select_sql: + "SELECT #{Group.columns.map { |c| "\"#{c.name}\"" }.join(", ")} FROM groups WHERE automatic = false", ) copy_model(GroupUser, skip_if_merged: true) @@ -181,11 +193,12 @@ class BulkImport::DiscourseMerger < BulkImport::Base imported_ids = [] last_id = Category.unscoped.maximum(:id) || 1 - sql = "COPY categories (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + sql = "COPY categories (#{columns.map { |c| "\"#{c}\"" }.join(", ")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec( + source_raw_connection + .exec( "SELECT concat('/c/', x.parent_slug, '/', x.slug) as path, - #{columns.map { |c| "c.\"#{c}\"" }.join(', ')} + #{columns.map { |c| "c.\"#{c}\"" }.join(", ")} FROM categories c INNER JOIN ( SELECT c1.id AS id, @@ -194,61 +207,55 @@ class BulkImport::DiscourseMerger < BulkImport::Base FROM categories c1 LEFT OUTER JOIN categories c2 ON c1.parent_category_id = c2.id ) x ON c.id = x.id - ORDER BY c.id" - ).each do |row| + ORDER BY c.id", + ) + .each do |row| + # using ORDER BY id to import categories in order of creation. + # this assumes parent categories were created prior to child categories + # and have a lower category id. + # + # without this definition, categories import in different orders in subsequent imports + # and can potentially mess up parent/child structure - # using ORDER BY id to import categories in order of creation. - # this assumes parent categories were created prior to child categories - # and have a lower category id. - # - # without this definition, categories import in different orders in subsequent imports - # and can potentially mess up parent/child structure + source_category_path = row.delete("path")&.squeeze("/") - source_category_path = row.delete('path')&.squeeze('/') + existing = Category.where(slug: row["slug"]).first + parent_slug = existing&.parent_category&.slug + if existing && source_category_path == "/c/#{parent_slug}/#{existing.slug}".squeeze("/") + @categories[row["id"].to_i] = existing.id + next + elsif existing + # if not the exact path as the source, + # we still need to avoid a unique index conflict on the slug when importing + # if that's the case, we'll append the imported id + row["slug"] = "#{row["slug"]}-#{row["id"]}" + end - existing = Category.where(slug: row['slug']).first - parent_slug = existing&.parent_category&.slug - if existing && - source_category_path == "/c/#{parent_slug}/#{existing.slug}".squeeze('/') - @categories[row['id'].to_i] = existing.id - next - elsif existing - # if not the exact path as the source, - # we still need to avoid a unique index conflict on the slug when importing - # if that's the case, we'll append the imported id - row['slug'] = "#{row['slug']}-#{row['id']}" + old_user_id = row["user_id"].to_i + row["user_id"] = user_id_from_imported_id(old_user_id) || -1 if old_user_id >= 1 + + if row["parent_category_id"] + row["parent_category_id"] = category_id_from_imported_id(row["parent_category_id"]) + end + + old_id = row["id"].to_i + row["id"] = (last_id += 1) + imported_ids << old_id + @categories[old_id] = row["id"] + + @raw_connection.put_copy_data(row.values) end - - old_user_id = row['user_id'].to_i - if old_user_id >= 1 - row['user_id'] = user_id_from_imported_id(old_user_id) || -1 - end - - if row['parent_category_id'] - row['parent_category_id'] = category_id_from_imported_id(row['parent_category_id']) - end - - old_id = row['id'].to_i - row['id'] = (last_id += 1) - imported_ids << old_id - @categories[old_id] = row['id'] - - @raw_connection.put_copy_data(row.values) - end end @sequences[Category.sequence_name] = last_id + 1 - create_custom_fields('category', 'id', imported_ids) do |imported_id| - { - record_id: category_id_from_imported_id(imported_id), - value: imported_id, - } + create_custom_fields("category", "id", imported_ids) do |imported_id| + { record_id: category_id_from_imported_id(imported_id), value: imported_id } end end def fix_category_descriptions - puts 'updating category description topic ids...' + puts "updating category description topic ids..." @categories.each do |old_id, new_id| category = Category.find(new_id) if new_id.present? @@ -261,19 +268,21 @@ class BulkImport::DiscourseMerger < BulkImport::Base def copy_topics copy_model(Topic, mapping: @topics) - [TopicAllowedGroup, TopicAllowedUser, TopicEmbed, TopicSearchData, - TopicTimer, TopicUser, TopicViewItem - ].each do |k| - copy_model(k, skip_processing: true) - end + [ + TopicAllowedGroup, + TopicAllowedUser, + TopicEmbed, + TopicSearchData, + TopicTimer, + TopicUser, + TopicViewItem, + ].each { |k| copy_model(k, skip_processing: true) } end def copy_posts copy_model(Post, skip_processing: true, mapping: @posts) copy_model(PostAction, mapping: @post_actions) - [PostReply, TopicLink, UserAction, QuotedPost].each do |k| - copy_model(k) - end + [PostReply, TopicLink, UserAction, QuotedPost].each { |k| copy_model(k) } [PostStat, IncomingEmail, PostDetail, PostRevision].each do |k| copy_model(k, skip_processing: true) end @@ -286,99 +295,101 @@ class BulkImport::DiscourseMerger < BulkImport::Base imported_ids = [] last_id = Tag.unscoped.maximum(:id) || 1 - sql = "COPY tags (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + sql = "COPY tags (#{columns.map { |c| "\"#{c}\"" }.join(", ")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM tags").each do |row| + source_raw_connection + .exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(", ")} FROM tags") + .each do |row| + if existing = Tag.where_name(row["name"]).first + @tags[row["id"]] = existing.id + next + end - if existing = Tag.where_name(row['name']).first - @tags[row['id']] = existing.id - next + old_id = row["id"] + row["id"] = (last_id += 1) + @tags[old_id.to_s] = row["id"] + + @raw_connection.put_copy_data(row.values) end - - old_id = row['id'] - row['id'] = (last_id += 1) - @tags[old_id.to_s] = row['id'] - - @raw_connection.put_copy_data(row.values) - end end @sequences[Tag.sequence_name] = last_id + 1 - [TagUser, TopicTag, CategoryTag, CategoryTagStat].each do |k| - copy_model(k) - end + [TagUser, TopicTag, CategoryTag, CategoryTagStat].each { |k| copy_model(k) } copy_model(TagGroup, mapping: @tag_groups) - [TagGroupMembership, CategoryTagGroup].each do |k| - copy_model(k, skip_processing: true) - end + [TagGroupMembership, CategoryTagGroup].each { |k| copy_model(k, skip_processing: true) } - col_list = TagGroupPermission.columns.map { |c| "\"#{c.name}\"" }.join(', ') - copy_model(TagGroupPermission, + col_list = TagGroupPermission.columns.map { |c| "\"#{c.name}\"" }.join(", ") + copy_model( + TagGroupPermission, skip_processing: true, - select_sql: "SELECT #{col_list} FROM tag_group_permissions WHERE group_id NOT IN (#{@auto_group_ids.join(', ')})" + select_sql: + "SELECT #{col_list} FROM tag_group_permissions WHERE group_id NOT IN (#{@auto_group_ids.join(", ")})", ) end def copy_uploads - puts '' + puts "" print "copying uploads..." FileUtils.cp_r( - File.join(@uploads_path, '.'), - File.join(Rails.root, 'public', 'uploads', 'default') + File.join(@uploads_path, "."), + File.join(Rails.root, "public", "uploads", "default"), ) columns = Upload.columns.map(&:name) last_id = Upload.unscoped.maximum(:id) || 1 - sql = "COPY uploads (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + sql = "COPY uploads (#{columns.map { |c| "\"#{c}\"" }.join(", ")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM uploads").each do |row| + source_raw_connection + .exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(", ")} FROM uploads") + .each do |row| + next if Upload.where(sha1: row["sha1"]).exists? - next if Upload.where(sha1: row['sha1']).exists? + # make sure to get a backup with uploads then convert them to local. + # when the backup is restored to a site with s3 uploads, it will upload the items + # to the bucket + rel_filename = row["url"].gsub(%r{^/uploads/[^/]+/}, "") + # assumes if coming from amazonaws.com that we want to remove everything + # but the text after the last `/`, which should leave us the filename + rel_filename = rel_filename.gsub(%r{^//[^/]+\.amazonaws\.com/\S+/}, "") + absolute_filename = File.join(@uploads_path, rel_filename) - # make sure to get a backup with uploads then convert them to local. - # when the backup is restored to a site with s3 uploads, it will upload the items - # to the bucket - rel_filename = row['url'].gsub(/^\/uploads\/[^\/]+\//, '') - # assumes if coming from amazonaws.com that we want to remove everything - # but the text after the last `/`, which should leave us the filename - rel_filename = rel_filename.gsub(/^\/\/[^\/]+\.amazonaws\.com\/\S+\//, '') - absolute_filename = File.join(@uploads_path, rel_filename) + old_id = row["id"] + if old_id && last_id + row["id"] = (last_id += 1) + @uploads[old_id.to_s] = row["id"] + end - old_id = row['id'] - if old_id && last_id - row['id'] = (last_id += 1) - @uploads[old_id.to_s] = row['id'] + old_user_id = row["user_id"].to_i + if old_user_id >= 1 + row["user_id"] = user_id_from_imported_id(old_user_id) + next if row["user_id"].nil? + end + + row["url"] = "/uploads/default/#{rel_filename}" if File.exist?(absolute_filename) + + @raw_connection.put_copy_data(row.values) end - - old_user_id = row['user_id'].to_i - if old_user_id >= 1 - row['user_id'] = user_id_from_imported_id(old_user_id) - next if row['user_id'].nil? - end - - row['url'] = "/uploads/default/#{rel_filename}" if File.exist?(absolute_filename) - - @raw_connection.put_copy_data(row.values) - end end @sequences[Upload.sequence_name] = last_id + 1 - puts '' + puts "" copy_model(PostUpload) copy_model(UserAvatar) # Users have a column "uploaded_avatar_id" which needs to be mapped now. - User.where("id >= ?", @first_new_user_id).find_each do |u| - if u.uploaded_avatar_id - u.uploaded_avatar_id = upload_id_from_imported_id(u.uploaded_avatar_id) - u.save! unless u.uploaded_avatar_id.nil? + User + .where("id >= ?", @first_new_user_id) + .find_each do |u| + if u.uploaded_avatar_id + u.uploaded_avatar_id = upload_id_from_imported_id(u.uploaded_avatar_id) + u.save! unless u.uploaded_avatar_id.nil? + end end - end end def copy_everything_else @@ -386,16 +397,16 @@ class BulkImport::DiscourseMerger < BulkImport::Base copy_model(k, skip_processing: true) end - [UserHistory, UserWarning, GroupArchivedMessage].each do |k| - copy_model(k) - end + [UserHistory, UserWarning, GroupArchivedMessage].each { |k| copy_model(k) } copy_model(Notification, mapping: @notifications) [CategoryGroup, GroupHistory].each do |k| - col_list = k.columns.map { |c| "\"#{c.name}\"" }.join(', ') - copy_model(k, - select_sql: "SELECT #{col_list} FROM #{k.table_name} WHERE group_id NOT IN (#{@auto_group_ids.join(', ')})" + col_list = k.columns.map { |c| "\"#{c.name}\"" }.join(", ") + copy_model( + k, + select_sql: + "SELECT #{col_list} FROM #{k.table_name} WHERE group_id NOT IN (#{@auto_group_ids.join(", ")})", ) end end @@ -408,23 +419,26 @@ class BulkImport::DiscourseMerger < BulkImport::Base imported_ids = [] last_id = Badge.unscoped.maximum(:id) || 1 - sql = "COPY badges (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + sql = "COPY badges (#{columns.map { |c| "\"#{c}\"" }.join(", ")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM badges").each do |row| + source_raw_connection + .exec("SELECT #{columns.map { |c| "\"#{c}\"" }.join(", ")} FROM badges") + .each do |row| + if existing = Badge.where(name: row["name"]).first + @badges[row["id"]] = existing.id + next + end - if existing = Badge.where(name: row['name']).first - @badges[row['id']] = existing.id - next + old_id = row["id"] + row["id"] = (last_id += 1) + @badges[old_id.to_s] = row["id"] + + row["badge_grouping_id"] = @badge_groupings[row["badge_grouping_id"]] if row[ + "badge_grouping_id" + ] + + @raw_connection.put_copy_data(row.values) end - - old_id = row['id'] - row['id'] = (last_id += 1) - @badges[old_id.to_s] = row['id'] - - row['badge_grouping_id'] = @badge_groupings[row['badge_grouping_id']] if row['badge_grouping_id'] - - @raw_connection.put_copy_data(row.values) - end end @sequences[Badge.sequence_name] = last_id + 1 @@ -432,72 +446,94 @@ class BulkImport::DiscourseMerger < BulkImport::Base copy_model(UserBadge, is_a_user_model: true) end - def copy_model(klass, skip_if_merged: false, is_a_user_model: false, skip_processing: false, mapping: nil, select_sql: nil) - + def copy_model( + klass, + skip_if_merged: false, + is_a_user_model: false, + skip_processing: false, + mapping: nil, + select_sql: nil + ) puts "copying #{klass.table_name}..." columns = klass.columns.map(&:name) has_custom_fields = CUSTOM_FIELDS.include?(klass.name.downcase) imported_ids = [] - last_id = columns.include?('id') ? (klass.unscoped.maximum(:id) || 1) : nil + last_id = columns.include?("id") ? (klass.unscoped.maximum(:id) || 1) : nil - sql = "COPY #{klass.table_name} (#{columns.map { |c| "\"#{c}\"" }.join(', ')}) FROM STDIN" + sql = "COPY #{klass.table_name} (#{columns.map { |c| "\"#{c}\"" }.join(", ")}) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - source_raw_connection.exec(select_sql || "SELECT #{columns.map { |c| "\"#{c}\"" }.join(', ')} FROM #{klass.table_name}").each do |row| - if row['user_id'] - old_user_id = row['user_id'].to_i + source_raw_connection + .exec( + select_sql || + "SELECT #{columns.map { |c| "\"#{c}\"" }.join(", ")} FROM #{klass.table_name}", + ) + .each do |row| + if row["user_id"] + old_user_id = row["user_id"].to_i - next if skip_if_merged && @merged_user_ids.include?(old_user_id) + next if skip_if_merged && @merged_user_ids.include?(old_user_id) - if is_a_user_model - next if old_user_id < 1 - next if user_id_from_imported_id(old_user_id).nil? - end - - if old_user_id >= 1 - row['user_id'] = user_id_from_imported_id(old_user_id) - if is_a_user_model && row['user_id'].nil? - raise "user_id nil for user id '#{old_user_id}'" + if is_a_user_model + next if old_user_id < 1 + next if user_id_from_imported_id(old_user_id).nil? end - next if row['user_id'].nil? # associated record for a deleted user + + if old_user_id >= 1 + row["user_id"] = user_id_from_imported_id(old_user_id) + if is_a_user_model && row["user_id"].nil? + raise "user_id nil for user id '#{old_user_id}'" + end + next if row["user_id"].nil? # associated record for a deleted user + end + end + + row["group_id"] = group_id_from_imported_id(row["group_id"]) if row["group_id"] + row["category_id"] = category_id_from_imported_id(row["category_id"]) if row[ + "category_id" + ] + if row["topic_id"] && klass != Category + row["topic_id"] = topic_id_from_imported_id(row["topic_id"]) + next if row["topic_id"].nil? + end + if row["post_id"] + row["post_id"] = post_id_from_imported_id(row["post_id"]) + next if row["post_id"].nil? + end + row["tag_id"] = tag_id_from_imported_id(row["tag_id"]) if row["tag_id"] + row["tag_group_id"] = tag_group_id_from_imported_id(row["tag_group_id"]) if row[ + "tag_group_id" + ] + row["upload_id"] = upload_id_from_imported_id(row["upload_id"]) if row["upload_id"] + row["deleted_by_id"] = user_id_from_imported_id(row["deleted_by_id"]) if row[ + "deleted_by_id" + ] + row["badge_id"] = badge_id_from_imported_id(row["badge_id"]) if row["badge_id"] + + old_id = row["id"].to_i + if old_id && last_id + row["id"] = (last_id += 1) + imported_ids << old_id if has_custom_fields + mapping[old_id] = row["id"] if mapping + end + + if skip_processing + @raw_connection.put_copy_data(row.values) + else + process_method_name = "process_#{klass.name.underscore}" + + processed = + ( + if respond_to?(process_method_name) + send(process_method_name, HashWithIndifferentAccess.new(row)) + else + row + end + ) + + @raw_connection.put_copy_data columns.map { |c| processed[c] } if processed end end - - row['group_id'] = group_id_from_imported_id(row['group_id']) if row['group_id'] - row['category_id'] = category_id_from_imported_id(row['category_id']) if row['category_id'] - if row['topic_id'] && klass != Category - row['topic_id'] = topic_id_from_imported_id(row['topic_id']) - next if row['topic_id'].nil? - end - if row['post_id'] - row['post_id'] = post_id_from_imported_id(row['post_id']) - next if row['post_id'].nil? - end - row['tag_id'] = tag_id_from_imported_id(row['tag_id']) if row['tag_id'] - row['tag_group_id'] = tag_group_id_from_imported_id(row['tag_group_id']) if row['tag_group_id'] - row['upload_id'] = upload_id_from_imported_id(row['upload_id']) if row['upload_id'] - row['deleted_by_id'] = user_id_from_imported_id(row['deleted_by_id']) if row['deleted_by_id'] - row['badge_id'] = badge_id_from_imported_id(row['badge_id']) if row['badge_id'] - - old_id = row['id'].to_i - if old_id && last_id - row['id'] = (last_id += 1) - imported_ids << old_id if has_custom_fields - mapping[old_id] = row['id'] if mapping - end - - if skip_processing - @raw_connection.put_copy_data(row.values) - else - process_method_name = "process_#{klass.name.underscore}" - - processed = respond_to?(process_method_name) ? send(process_method_name, HashWithIndifferentAccess.new(row)) : row - - if processed - @raw_connection.put_copy_data columns.map { |c| processed[c] } - end - end - end end @sequences[klass.sequence_name] = last_id + 1 if last_id @@ -506,192 +542,248 @@ class BulkImport::DiscourseMerger < BulkImport::Base id_mapping_method_name = "#{klass.name.downcase}_id_from_imported_id".freeze return unless respond_to?(id_mapping_method_name) create_custom_fields(klass.name.downcase, "id", imported_ids) do |imported_id| - { - record_id: send(id_mapping_method_name, imported_id), - value: imported_id, - } + { record_id: send(id_mapping_method_name, imported_id), value: imported_id } end end end def process_topic(topic) - return nil if topic['category_id'].nil? && topic['archetype'] != Archetype.private_message - topic['last_post_user_id'] = user_id_from_imported_id(topic['last_post_user_id']) || -1 - topic['featured_user1_id'] = user_id_from_imported_id(topic['featured_user1_id']) || -1 - topic['featured_user2_id'] = user_id_from_imported_id(topic['featured_user2_id']) || -1 - topic['featured_user3_id'] = user_id_from_imported_id(topic['featured_user3_id']) || -1 - topic['featured_user4_id'] = user_id_from_imported_id(topic['featured_user4_id']) || -1 + return nil if topic["category_id"].nil? && topic["archetype"] != Archetype.private_message + topic["last_post_user_id"] = user_id_from_imported_id(topic["last_post_user_id"]) || -1 + topic["featured_user1_id"] = user_id_from_imported_id(topic["featured_user1_id"]) || -1 + topic["featured_user2_id"] = user_id_from_imported_id(topic["featured_user2_id"]) || -1 + topic["featured_user3_id"] = user_id_from_imported_id(topic["featured_user3_id"]) || -1 + topic["featured_user4_id"] = user_id_from_imported_id(topic["featured_user4_id"]) || -1 topic end def process_post(post) - post['last_editor_id'] = user_id_from_imported_id(post['last_editor_id']) || -1 - post['reply_to_user_id'] = user_id_from_imported_id(post['reply_to_user_id']) || -1 - post['locked_by_id'] = user_id_from_imported_id(post['locked_by_id']) || -1 + post["last_editor_id"] = user_id_from_imported_id(post["last_editor_id"]) || -1 + post["reply_to_user_id"] = user_id_from_imported_id(post["reply_to_user_id"]) || -1 + post["locked_by_id"] = user_id_from_imported_id(post["locked_by_id"]) || -1 @topic_id_by_post_id[post[:id]] = post[:topic_id] post end def process_post_reply(post_reply) - post_reply['reply_post_id'] = post_id_from_imported_id(post_reply['reply_post_id']) if post_reply['reply_post_id'] + post_reply["reply_post_id"] = post_id_from_imported_id( + post_reply["reply_post_id"], + ) if post_reply["reply_post_id"] post_reply end def process_quoted_post(quoted_post) - quoted_post['quoted_post_id'] = post_id_from_imported_id(quoted_post['quoted_post_id']) if quoted_post['quoted_post_id'] - return nil if quoted_post['quoted_post_id'].nil? + quoted_post["quoted_post_id"] = post_id_from_imported_id( + quoted_post["quoted_post_id"], + ) if quoted_post["quoted_post_id"] + return nil if quoted_post["quoted_post_id"].nil? quoted_post end def process_topic_link(topic_link) - old_topic_id = topic_link['link_topic_id'] - topic_link['link_topic_id'] = topic_id_from_imported_id(topic_link['link_topic_id']) if topic_link['link_topic_id'] - topic_link['link_post_id'] = post_id_from_imported_id(topic_link['link_post_id']) if topic_link['link_post_id'] - return nil if topic_link['link_topic_id'].nil? + old_topic_id = topic_link["link_topic_id"] + topic_link["link_topic_id"] = topic_id_from_imported_id( + topic_link["link_topic_id"], + ) if topic_link["link_topic_id"] + topic_link["link_post_id"] = post_id_from_imported_id(topic_link["link_post_id"]) if topic_link[ + "link_post_id" + ] + return nil if topic_link["link_topic_id"].nil? r = Regexp.new("^#{@source_base_url}/t/([^\/]+)/#{old_topic_id}(.*)") - if m = r.match(topic_link['url']) - topic_link['url'] = "#{@source_base_url}/t/#{m[1]}/#{topic_link['link_topic_id']}#{m[2]}" + if m = r.match(topic_link["url"]) + topic_link["url"] = "#{@source_base_url}/t/#{m[1]}/#{topic_link["link_topic_id"]}#{m[2]}" end topic_link end def process_post_action(post_action) - return nil unless post_action['post_id'].present? - post_action['related_post_id'] = post_id_from_imported_id(post_action['related_post_id']) - post_action['deferred_by_id'] = user_id_from_imported_id(post_action['deferred_by_id']) - post_action['agreed_by_id'] = user_id_from_imported_id(post_action['agreed_by_id']) - post_action['disagreed_by_id'] = user_id_from_imported_id(post_action['disagreed_by_id']) + return nil unless post_action["post_id"].present? + post_action["related_post_id"] = post_id_from_imported_id(post_action["related_post_id"]) + post_action["deferred_by_id"] = user_id_from_imported_id(post_action["deferred_by_id"]) + post_action["agreed_by_id"] = user_id_from_imported_id(post_action["agreed_by_id"]) + post_action["disagreed_by_id"] = user_id_from_imported_id(post_action["disagreed_by_id"]) post_action end def process_user_action(user_action) - user_action['target_topic_id'] = topic_id_from_imported_id(user_action['target_topic_id']) if user_action['target_topic_id'] - user_action['target_post_id'] = post_id_from_imported_id(user_action['target_post_id']) if user_action['target_post_id'] - user_action['target_user_id'] = user_id_from_imported_id(user_action['target_user_id']) if user_action['target_user_id'] - user_action['acting_user_id'] = user_id_from_imported_id(user_action['acting_user_id']) if user_action['acting_user_id'] - user_action['queued_post_id'] = post_id_from_imported_id(user_action['queued_post_id']) if user_action['queued_post_id'] + user_action["target_topic_id"] = topic_id_from_imported_id( + user_action["target_topic_id"], + ) if user_action["target_topic_id"] + user_action["target_post_id"] = post_id_from_imported_id( + user_action["target_post_id"], + ) if user_action["target_post_id"] + user_action["target_user_id"] = user_id_from_imported_id( + user_action["target_user_id"], + ) if user_action["target_user_id"] + user_action["acting_user_id"] = user_id_from_imported_id( + user_action["acting_user_id"], + ) if user_action["acting_user_id"] + user_action["queued_post_id"] = post_id_from_imported_id( + user_action["queued_post_id"], + ) if user_action["queued_post_id"] user_action end def process_tag_group(tag_group) - tag_group['parent_tag_id'] = tag_id_from_imported_id(tag_group['parent_tag_id']) if tag_group['parent_tag_id'] + tag_group["parent_tag_id"] = tag_id_from_imported_id(tag_group["parent_tag_id"]) if tag_group[ + "parent_tag_id" + ] tag_group end def process_category_group(category_group) - return nil if category_group['category_id'].nil? || category_group['group_id'].nil? + return nil if category_group["category_id"].nil? || category_group["group_id"].nil? category_group end def process_group_user(group_user) - if @auto_group_ids.include?(group_user['group_id'].to_i) && - @merged_user_ids.include?(group_user['user_id'].to_i) + if @auto_group_ids.include?(group_user["group_id"].to_i) && + @merged_user_ids.include?(group_user["user_id"].to_i) return nil end - return nil if group_user['user_id'].to_i < 1 + return nil if group_user["user_id"].to_i < 1 group_user end def process_group_history(group_history) - group_history['acting_user_id'] = user_id_from_imported_id(group_history['acting_user_id']) if group_history['acting_user_id'] - group_history['target_user_id'] = user_id_from_imported_id(group_history['target_user_id']) if group_history['target_user_id'] + group_history["acting_user_id"] = user_id_from_imported_id( + group_history["acting_user_id"], + ) if group_history["acting_user_id"] + group_history["target_user_id"] = user_id_from_imported_id( + group_history["target_user_id"], + ) if group_history["target_user_id"] group_history end def process_group_archived_message(gam) - return nil unless gam['topic_id'].present? && gam['group_id'].present? + return nil unless gam["topic_id"].present? && gam["group_id"].present? gam end def process_topic_link(topic_link) - topic_link['link_topic_id'] = topic_id_from_imported_id(topic_link['link_topic_id']) if topic_link['link_topic_id'] - topic_link['link_post_id'] = post_id_from_imported_id(topic_link['link_post_id']) if topic_link['link_post_id'] + topic_link["link_topic_id"] = topic_id_from_imported_id( + topic_link["link_topic_id"], + ) if topic_link["link_topic_id"] + topic_link["link_post_id"] = post_id_from_imported_id(topic_link["link_post_id"]) if topic_link[ + "link_post_id" + ] topic_link end def process_user_avatar(user_avatar) - user_avatar['custom_upload_id'] = upload_id_from_imported_id(user_avatar['custom_upload_id']) if user_avatar['custom_upload_id'] - user_avatar['gravatar_upload_id'] = upload_id_from_imported_id(user_avatar['gravatar_upload_id']) if user_avatar['gravatar_upload_id'] - return nil unless user_avatar['custom_upload_id'].present? || user_avatar['gravatar_upload_id'].present? + user_avatar["custom_upload_id"] = upload_id_from_imported_id( + user_avatar["custom_upload_id"], + ) if user_avatar["custom_upload_id"] + user_avatar["gravatar_upload_id"] = upload_id_from_imported_id( + user_avatar["gravatar_upload_id"], + ) if user_avatar["gravatar_upload_id"] + unless user_avatar["custom_upload_id"].present? || user_avatar["gravatar_upload_id"].present? + return nil + end user_avatar end def process_user_history(user_history) - user_history['acting_user_id'] = user_id_from_imported_id(user_history['acting_user_id']) if user_history['acting_user_id'] - user_history['target_user_id'] = user_id_from_imported_id(user_history['target_user_id']) if user_history['target_user_id'] + user_history["acting_user_id"] = user_id_from_imported_id( + user_history["acting_user_id"], + ) if user_history["acting_user_id"] + user_history["target_user_id"] = user_id_from_imported_id( + user_history["target_user_id"], + ) if user_history["target_user_id"] user_history end def process_user_warning(user_warning) - user_warning['created_by_id'] = user_id_from_imported_id(user_warning['created_by_id']) if user_warning['created_by_id'] + user_warning["created_by_id"] = user_id_from_imported_id( + user_warning["created_by_id"], + ) if user_warning["created_by_id"] user_warning end def process_post_upload(post_upload) - return nil unless post_upload['upload_id'].present? + return nil unless post_upload["upload_id"].present? @imported_post_uploads ||= {} - return nil if @imported_post_uploads[post_upload['post_id']]&.include?(post_upload['upload_id']) - @imported_post_uploads[post_upload['post_id']] ||= [] - @imported_post_uploads[post_upload['post_id']] << post_upload['upload_id'] + return nil if @imported_post_uploads[post_upload["post_id"]]&.include?(post_upload["upload_id"]) + @imported_post_uploads[post_upload["post_id"]] ||= [] + @imported_post_uploads[post_upload["post_id"]] << post_upload["upload_id"] - return nil if PostUpload.where(post_id: post_upload['post_id'], upload_id: post_upload['upload_id']).exists? + if PostUpload.where( + post_id: post_upload["post_id"], + upload_id: post_upload["upload_id"], + ).exists? + return nil + end post_upload end def process_notification(notification) - notification['post_action_id'] = post_action_id_from_imported_id(notification['post_action_id']) if notification['post_action_id'] + notification["post_action_id"] = post_action_id_from_imported_id( + notification["post_action_id"], + ) if notification["post_action_id"] notification end def process_oauth2_user_info(r) - return nil if Oauth2UserInfo.where(uid: r['uid'], provider: r['provider']).exists? + return nil if Oauth2UserInfo.where(uid: r["uid"], provider: r["provider"]).exists? r end def process_user_associated_account(r) - return nil if UserAssociatedAccount.where(provider_uid: r['uid'], provider_name: r['provider']).exists? + if UserAssociatedAccount.where(provider_uid: r["uid"], provider_name: r["provider"]).exists? + return nil + end r end def process_single_sign_on_record(r) - return nil if SingleSignOnRecord.where(external_id: r['external_id']).exists? + return nil if SingleSignOnRecord.where(external_id: r["external_id"]).exists? r end def process_user_badge(user_badge) - user_badge['granted_by_id'] = user_id_from_imported_id(user_badge['granted_by_id']) if user_badge['granted_by_id'] - user_badge['notification_id'] = notification_id_from_imported_id(user_badge['notification_id']) if user_badge['notification_id'] - return nil if UserBadge.where(user_id: user_badge['user_id'], badge_id: user_badge['badge_id']).exists? + user_badge["granted_by_id"] = user_id_from_imported_id( + user_badge["granted_by_id"], + ) if user_badge["granted_by_id"] + user_badge["notification_id"] = notification_id_from_imported_id( + user_badge["notification_id"], + ) if user_badge["notification_id"] + if UserBadge.where(user_id: user_badge["user_id"], badge_id: user_badge["badge_id"]).exists? + return nil + end user_badge end def process_email_change_request(ecr) - ecr['old_email_token_id'] = email_token_id_from_imported_id(ecr['old_email_token_id']) if ecr['old_email_token_id'] - ecr['new_email_token_id'] = email_token_id_from_imported_id(ecr['new_email_token_id']) if ecr['new_email_token_id'] + ecr["old_email_token_id"] = email_token_id_from_imported_id(ecr["old_email_token_id"]) if ecr[ + "old_email_token_id" + ] + ecr["new_email_token_id"] = email_token_id_from_imported_id(ecr["new_email_token_id"]) if ecr[ + "new_email_token_id" + ] ecr end def process_tag_user(x) - return nil if TagUser.where(tag_id: x['tag_id'], user_id: x['user_id']).exists? + return nil if TagUser.where(tag_id: x["tag_id"], user_id: x["user_id"]).exists? x end def process_topic_tag(x) - return nil if TopicTag.where(topic_id: x['topic_id'], tag_id: x['tag_id']).exists? + return nil if TopicTag.where(topic_id: x["topic_id"], tag_id: x["tag_id"]).exists? x end def process_category_tag(x) - return nil if CategoryTag.where(category_id: x['category_id'], tag_id: x['tag_id']).exists? + return nil if CategoryTag.where(category_id: x["category_id"], tag_id: x["tag_id"]).exists? x end def process_category_tag_stat(x) - return nil if CategoryTagStat.where(category_id: x['category_id'], tag_id: x['tag_id']).exists? + return nil if CategoryTagStat.where(category_id: x["category_id"], tag_id: x["tag_id"]).exists? x end @@ -744,27 +836,29 @@ class BulkImport::DiscourseMerger < BulkImport::Base def fix_user_columns puts "updating foreign keys in the users table..." - User.where('id >= ?', @first_new_user_id).find_each do |u| - arr = [] - sql = "UPDATE users SET".dup + User + .where("id >= ?", @first_new_user_id) + .find_each do |u| + arr = [] + sql = "UPDATE users SET".dup - if new_approved_by_id = user_id_from_imported_id(u.approved_by_id) - arr << " approved_by_id = #{new_approved_by_id}" + if new_approved_by_id = user_id_from_imported_id(u.approved_by_id) + arr << " approved_by_id = #{new_approved_by_id}" + end + if new_primary_group_id = group_id_from_imported_id(u.primary_group_id) + arr << " primary_group_id = #{new_primary_group_id}" + end + if new_notification_id = notification_id_from_imported_id(u.seen_notification_id) + arr << " seen_notification_id = #{new_notification_id}" + end + + next if arr.empty? + + sql << arr.join(", ") + sql << " WHERE id = #{u.id}" + + @raw_connection.exec(sql) end - if new_primary_group_id = group_id_from_imported_id(u.primary_group_id) - arr << " primary_group_id = #{new_primary_group_id}" - end - if new_notification_id = notification_id_from_imported_id(u.seen_notification_id) - arr << " seen_notification_id = #{new_notification_id}" - end - - next if arr.empty? - - sql << arr.join(', ') - sql << " WHERE id = #{u.id}" - - @raw_connection.exec(sql) - end end def fix_topic_links @@ -777,33 +871,37 @@ class BulkImport::DiscourseMerger < BulkImport::Base @topics.each do |old_topic_id, new_topic_id| current += 1 percent = (current * 100) / total - puts "#{current} (#{percent}\%) completed. #{update_count} rows updated." if current % 200 == 0 + if current % 200 == 0 + puts "#{current} (#{percent}\%) completed. #{update_count} rows updated." + end if topic = Topic.find_by_id(new_topic_id) replace_arg = [ "#{@source_base_url}/t/#{topic.slug}/#{old_topic_id}", - "#{@source_base_url}/t/#{topic.slug}/#{new_topic_id}" + "#{@source_base_url}/t/#{topic.slug}/#{new_topic_id}", ] - r = @raw_connection.async_exec( - "UPDATE posts + r = + @raw_connection.async_exec( + "UPDATE posts SET raw = replace(raw, $1, $2) WHERE NOT raw IS NULL AND topic_id >= #{@first_new_topic_id} AND raw <> replace(raw, $1, $2)", - replace_arg - ) + replace_arg, + ) update_count += r.cmd_tuples - r = @raw_connection.async_exec( - "UPDATE posts + r = + @raw_connection.async_exec( + "UPDATE posts SET cooked = replace(cooked, $1, $2) WHERE NOT cooked IS NULL AND topic_id >= #{@first_new_topic_id} AND cooked <> replace(cooked, $1, $2)", - replace_arg - ) + replace_arg, + ) update_count += r.cmd_tuples end @@ -811,7 +909,6 @@ class BulkImport::DiscourseMerger < BulkImport::Base puts "updated #{update_count} rows" end - end BulkImport::DiscourseMerger.new.start diff --git a/script/bulk_import/phpbb_postgresql.rb b/script/bulk_import/phpbb_postgresql.rb index cd5fa626fd..70def55fb8 100644 --- a/script/bulk_import/phpbb_postgresql.rb +++ b/script/bulk_import/phpbb_postgresql.rb @@ -3,17 +3,16 @@ require_relative "base" require "pg" require "htmlentities" -require 'ruby-bbcode-to-md' +require "ruby-bbcode-to-md" class BulkImport::PhpBB < BulkImport::Base - SUSPENDED_TILL ||= Date.new(3000, 1, 1) - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "phpbb_" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "phpbb_" def initialize super - charset = ENV["DB_CHARSET"] || "utf8" + charset = ENV["DB_CHARSET"] || "utf8" database = ENV["DB_NAME"] || "flightaware" password = ENV["DB_PASSWORD"] || "discourse" @@ -57,7 +56,7 @@ class BulkImport::PhpBB < BulkImport::Base { imported_id: row["group_id"], name: normalize_text(row["group_name"]), - bio_raw: normalize_text(row["group_desc"]) + bio_raw: normalize_text(row["group_desc"]), } end end @@ -85,15 +84,28 @@ class BulkImport::PhpBB < BulkImport::Base username: normalize_text(row["username"]), email: row["user_email"], created_at: Time.zone.at(row["user_regdate"].to_i), - last_seen_at: row["user_lastvisit"] == 0 ? Time.zone.at(row["user_regdate"].to_i) : Time.zone.at(row["user_lastvisit"].to_i), + last_seen_at: + ( + if row["user_lastvisit"] == 0 + Time.zone.at(row["user_regdate"].to_i) + else + Time.zone.at(row["user_lastvisit"].to_i) + end + ), trust_level: row["user_posts"] == 0 ? TrustLevel[0] : TrustLevel[1], date_of_birth: parse_birthday(row["user_birthday"]), - primary_group_id: group_id_from_imported_id(row["group_id"]) + primary_group_id: group_id_from_imported_id(row["group_id"]), } u[:ip_address] = row["user_ip"][/\b(?:\d{1,3}\.){3}\d{1,3}\b/] if row["user_ip"].present? if row["ban_start"] u[:suspended_at] = Time.zone.at(row["ban_start"].to_i) - u[:suspended_till] = row["ban_end"].to_i > 0 ? Time.zone.at(row["ban_end"].to_i) : SUSPENDED_TILL + u[:suspended_till] = ( + if row["ban_end"].to_i > 0 + Time.zone.at(row["ban_end"].to_i) + else + SUSPENDED_TILL + end + ) end u end @@ -114,7 +126,7 @@ class BulkImport::PhpBB < BulkImport::Base imported_id: row["user_id"], imported_user_id: row["user_id"], email: row["user_email"], - created_at: Time.zone.at(row["user_regdate"].to_i) + created_at: Time.zone.at(row["user_regdate"].to_i), } end end @@ -149,7 +161,14 @@ class BulkImport::PhpBB < BulkImport::Base create_user_profiles(user_profiles) do |row| { user_id: user_id_from_imported_id(row["user_id"]), - website: (URI.parse(row["user_website"]).to_s rescue nil), + website: + ( + begin + URI.parse(row["user_website"]).to_s + rescue StandardError + nil + end + ), location: row["user_from"], } end @@ -158,17 +177,16 @@ class BulkImport::PhpBB < BulkImport::Base def import_categories puts "Importing categories..." - categories = psql_query(<<-SQL + categories = psql_query(<<-SQL).to_a SELECT forum_id, parent_id, forum_name, forum_desc FROM #{TABLE_PREFIX}forums WHERE forum_id > #{@last_imported_category_id} ORDER BY parent_id, left_id SQL - ).to_a return if categories.empty? - parent_categories = categories.select { |c| c["parent_id"].to_i == 0 } + parent_categories = categories.select { |c| c["parent_id"].to_i == 0 } children_categories = categories.select { |c| c["parent_id"].to_i != 0 } puts "Importing parent categories..." @@ -176,7 +194,7 @@ class BulkImport::PhpBB < BulkImport::Base { imported_id: row["forum_id"], name: normalize_text(row["forum_name"]), - description: normalize_text(row["forum_desc"]) + description: normalize_text(row["forum_desc"]), } end @@ -186,7 +204,7 @@ class BulkImport::PhpBB < BulkImport::Base imported_id: row["forum_id"], name: normalize_text(row["forum_name"]), description: normalize_text(row["forum_desc"]), - parent_category_id: category_id_from_imported_id(row["parent_id"]) + parent_category_id: category_id_from_imported_id(row["parent_id"]), } end end @@ -209,7 +227,7 @@ class BulkImport::PhpBB < BulkImport::Base category_id: category_id_from_imported_id(row["forum_id"]), user_id: user_id_from_imported_id(row["topic_poster"]), created_at: Time.zone.at(row["topic_time"].to_i), - views: row["topic_views"] + views: row["topic_views"], } end end @@ -261,7 +279,7 @@ class BulkImport::PhpBB < BulkImport::Base imported_id: row["msg_id"].to_i + PRIVATE_OFFSET, title: normalize_text(title), user_id: user_id_from_imported_id(row["author_id"].to_i), - created_at: Time.zone.at(row["message_time"].to_i) + created_at: Time.zone.at(row["message_time"].to_i), } end end @@ -271,13 +289,12 @@ class BulkImport::PhpBB < BulkImport::Base allowed_users = [] - psql_query(<<-SQL + psql_query(<<-SQL).each do |row| SELECT msg_id, author_id, to_address FROM #{TABLE_PREFIX}privmsgs WHERE msg_id > (#{@last_imported_private_topic_id - PRIVATE_OFFSET}) ORDER BY msg_id SQL - ).each do |row| next unless topic_id = topic_id_from_imported_id(row["msg_id"].to_i + PRIVATE_OFFSET) user_ids = get_message_recipients(row["author_id"], row["to_address"]) @@ -287,12 +304,7 @@ class BulkImport::PhpBB < BulkImport::Base end end - create_topic_allowed_users(allowed_users) do |row| - { - topic_id: row[0], - user_id: row[1] - } - end + create_topic_allowed_users(allowed_users) { |row| { topic_id: row[0], user_id: row[1] } } end def import_private_posts @@ -316,13 +328,13 @@ class BulkImport::PhpBB < BulkImport::Base topic_id: topic_id, user_id: user_id_from_imported_id(row["author_id"].to_i), created_at: Time.zone.at(row["message_time"].to_i), - raw: process_raw_text(row["message_text"]) + raw: process_raw_text(row["message_text"]), } end end def get_message_recipients(from, to) - user_ids = to.split(':') + user_ids = to.split(":") user_ids.map! { |u| u[2..-1].to_i } user_ids.push(from.to_i) user_ids.uniq! @@ -332,15 +344,29 @@ class BulkImport::PhpBB < BulkImport::Base def extract_pm_title(title) pm_title = CGI.unescapeHTML(title) - pm_title = title.gsub(/^Re\s*:\s*/i, "") rescue nil + pm_title = + begin + title.gsub(/^Re\s*:\s*/i, "") + rescue StandardError + nil + end pm_title end def parse_birthday(birthday) return if birthday.blank? - date_of_birth = Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") rescue nil + date_of_birth = + begin + Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") + rescue StandardError + nil + end return if date_of_birth.nil? - date_of_birth.year < 1904 ? Date.new(1904, date_of_birth.month, date_of_birth.day) : date_of_birth + if date_of_birth.year < 1904 + Date.new(1904, date_of_birth.month, date_of_birth.day) + else + date_of_birth + end end def psql_query(sql) @@ -352,34 +378,36 @@ class BulkImport::PhpBB < BulkImport::Base text = raw.dup text = CGI.unescapeHTML(text) - text.gsub!(/:(?:\w{8})\]/, ']') + text.gsub!(/:(?:\w{8})\]/, "]") # Some links look like this: http://www.onegameamonth.com - text.gsub!(/(.+)<\/a>/i, '[\2](\1)') + text.gsub!(%r{(.+)}i, '[\2](\1)') # phpBB shortens link text like this, which breaks our markdown processing: # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - text.gsub!(/\[http(s)?:\/\/(www\.)?/i, '[') + text.gsub!(%r{\[http(s)?://(www\.)?}i, "[") # convert list tags to ul and list=1 tags to ol # list=a is not supported, so handle it like list=1 # list=9 and list=x have the same result as list=1 and list=a - text.gsub!(/\[list\](.*?)\[\/list:u\]/mi, '[ul]\1[/ul]') - text.gsub!(/\[list=.*?\](.*?)\[\/list:o\]/mi, '[ol]\1[/ol]') + text.gsub!(%r{\[list\](.*?)\[/list:u\]}mi, '[ul]\1[/ul]') + text.gsub!(%r{\[list=.*?\](.*?)\[/list:o\]}mi, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - text.gsub!(/\[\*\](.*?)\[\/\*:m\]/mi, '[li]\1[/li]') + text.gsub!(%r{\[\*\](.*?)\[/\*:m\]}mi, '[li]\1[/li]') # [QUOTE=""] -- add newline text.gsub!(/(\[quote="[a-zA-Z\d]+"\])/i) { "#{$1}\n" } # [/QUOTE] -- add newline - text.gsub!(/(\[\/quote\])/i) { "\n#{$1}" } + text.gsub!(%r{(\[/quote\])}i) { "\n#{$1}" } # :) is encoded as :) - text.gsub!(/(.*?)/) do + text.gsub!( + /(.*?)/, + ) do smiley = $1 @smiley_map.fetch(smiley) do # upload_smiley(smiley, $2, $3, $4) || smiley_as_text(smiley) @@ -405,33 +433,30 @@ class BulkImport::PhpBB < BulkImport::Base def add_default_smilies { - [':D', ':-D', ':grin:'] => ':smiley:', - [':)', ':-)', ':smile:'] => ':slight_smile:', - [';)', ';-)', ':wink:'] => ':wink:', - [':(', ':-(', ':sad:'] => ':frowning:', - [':o', ':-o', ':eek:'] => ':astonished:', - [':shock:'] => ':open_mouth:', - [':?', ':-?', ':???:'] => ':confused:', - ['8-)', ':cool:'] => ':sunglasses:', - [':lol:'] => ':laughing:', - [':x', ':-x', ':mad:'] => ':angry:', - [':P', ':-P', ':razz:'] => ':stuck_out_tongue:', - [':oops:'] => ':blush:', - [':cry:'] => ':cry:', - [':evil:'] => ':imp:', - [':twisted:'] => ':smiling_imp:', - [':roll:'] => ':unamused:', - [':!:'] => ':exclamation:', - [':?:'] => ':question:', - [':idea:'] => ':bulb:', - [':arrow:'] => ':arrow_right:', - [':|', ':-|'] => ':neutral_face:', - [':geek:'] => ':nerd:' - }.each do |smilies, emoji| - smilies.each { |smiley| @smiley_map[smiley] = emoji } - end + %w[:D :-D :grin:] => ":smiley:", + %w[:) :-) :smile:] => ":slight_smile:", + %w[;) ;-) :wink:] => ":wink:", + %w[:( :-( :sad:] => ":frowning:", + %w[:o :-o :eek:] => ":astonished:", + [":shock:"] => ":open_mouth:", + %w[:? :-? :???:] => ":confused:", + %w[8-) :cool:] => ":sunglasses:", + [":lol:"] => ":laughing:", + %w[:x :-x :mad:] => ":angry:", + %w[:P :-P :razz:] => ":stuck_out_tongue:", + [":oops:"] => ":blush:", + [":cry:"] => ":cry:", + [":evil:"] => ":imp:", + [":twisted:"] => ":smiling_imp:", + [":roll:"] => ":unamused:", + [":!:"] => ":exclamation:", + [":?:"] => ":question:", + [":idea:"] => ":bulb:", + [":arrow:"] => ":arrow_right:", + %w[:| :-|] => ":neutral_face:", + [":geek:"] => ":nerd:", + }.each { |smilies, emoji| smilies.each { |smiley| @smiley_map[smiley] = emoji } } end - end BulkImport::PhpBB.new.run diff --git a/script/bulk_import/vanilla.rb b/script/bulk_import/vanilla.rb index d01eed3af0..827f57c3fd 100644 --- a/script/bulk_import/vanilla.rb +++ b/script/bulk_import/vanilla.rb @@ -8,7 +8,6 @@ require "htmlentities" # NOTE: this importer expects a MySQL DB to directly connect to class BulkImport::Vanilla < BulkImport::Base - VANILLA_DB = "dbname" TABLE_PREFIX = "GDN_" ATTACHMENTS_BASE_DIR = "/my/absolute/path/to/from_vanilla/uploads" @@ -20,13 +19,14 @@ class BulkImport::Vanilla < BulkImport::Base def initialize super @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - database: VANILLA_DB, - password: "", - reconnect: true - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + database: VANILLA_DB, + password: "", + reconnect: true, + ) @import_tags = false begin @@ -88,10 +88,10 @@ class BulkImport::Vanilla < BulkImport::Base end def import_users - puts '', "Importing users..." + puts "", "Importing users..." username = nil - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}User;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}User;").first["count"] users = mysql_stream <<-SQL SELECT UserID, Name, Title, Location, Email, @@ -103,26 +103,32 @@ class BulkImport::Vanilla < BulkImport::Base SQL create_users(users) do |row| - next if row['Email'].blank? - next if row['Name'].blank? + next if row["Email"].blank? + next if row["Name"].blank? - if ip_address = row['InsertIPAddress']&.split(',').try(:[], 0) - ip_address = nil unless (IPAddr.new(ip_address) rescue false) + if ip_address = row["InsertIPAddress"]&.split(",").try(:[], 0) + ip_address = nil unless ( + begin + IPAddr.new(ip_address) + rescue StandardError + false + end + ) end u = { - imported_id: row['UserID'], - email: row['Email'], - username: row['Name'], - name: row['Name'], - created_at: row['DateInserted'] == nil ? 0 : Time.zone.at(row['DateInserted']), + imported_id: row["UserID"], + email: row["Email"], + username: row["Name"], + name: row["Name"], + created_at: row["DateInserted"] == nil ? 0 : Time.zone.at(row["DateInserted"]), registration_ip_address: ip_address, - last_seen_at: row['DateLastActive'] == nil ? 0 : Time.zone.at(row['DateLastActive']), - location: row['Location'], - admin: row['Admin'] > 0 + last_seen_at: row["DateLastActive"] == nil ? 0 : Time.zone.at(row["DateLastActive"]), + location: row["Location"], + admin: row["Admin"] > 0, } if row["Banned"] > 0 - u[:suspended_at] = Time.zone.at(row['DateInserted']) + u[:suspended_at] = Time.zone.at(row["DateInserted"]) u[:suspended_till] = SUSPENDED_TILL end u @@ -130,7 +136,7 @@ class BulkImport::Vanilla < BulkImport::Base end def import_user_emails - puts '', 'Importing user emails...' + puts "", "Importing user emails..." users = mysql_stream <<-SQL SELECT UserID, Name, Email, DateInserted @@ -141,20 +147,20 @@ class BulkImport::Vanilla < BulkImport::Base SQL create_user_emails(users) do |row| - next if row['Email'].blank? - next if row['Name'].blank? + next if row["Email"].blank? + next if row["Name"].blank? { imported_id: row["UserID"], imported_user_id: row["UserID"], email: row["Email"], - created_at: Time.zone.at(row["DateInserted"]) + created_at: Time.zone.at(row["DateInserted"]), } end end def import_user_profiles - puts '', 'Importing user profiles...' + puts "", "Importing user profiles..." user_profiles = mysql_stream <<-SQL SELECT UserID, Name, Email, Location, About @@ -165,19 +171,19 @@ class BulkImport::Vanilla < BulkImport::Base SQL create_user_profiles(user_profiles) do |row| - next if row['Email'].blank? - next if row['Name'].blank? + next if row["Email"].blank? + next if row["Name"].blank? { user_id: user_id_from_imported_id(row["UserID"]), location: row["Location"], - bio_raw: row["About"] + bio_raw: row["About"], } end end def import_user_stats - puts '', "Importing user stats..." + puts "", "Importing user stats..." users = mysql_stream <<-SQL SELECT UserID, CountDiscussions, CountComments, DateInserted @@ -190,14 +196,14 @@ class BulkImport::Vanilla < BulkImport::Base now = Time.zone.now create_user_stats(users) do |row| - next unless @users[row['UserID'].to_i] # shouldn't need this but it can be NULL :< + next unless @users[row["UserID"].to_i] # shouldn't need this but it can be NULL :< { - imported_id: row['UserID'], - imported_user_id: row['UserID'], - new_since: Time.zone.at(row['DateInserted'] || now), - post_count: row['CountComments'] || 0, - topic_count: row['CountDiscussions'] || 0 + imported_id: row["UserID"], + imported_user_id: row["UserID"], + new_since: Time.zone.at(row["DateInserted"] || now), + post_count: row["CountComments"] || 0, + topic_count: row["CountDiscussions"] || 0, } end end @@ -215,7 +221,10 @@ class BulkImport::Vanilla < BulkImport::Base next unless u.custom_fields["import_id"] - r = mysql_query("SELECT photo FROM #{TABLE_PREFIX}User WHERE UserID = #{u.custom_fields['import_id']};").first + r = + mysql_query( + "SELECT photo FROM #{TABLE_PREFIX}User WHERE UserID = #{u.custom_fields["import_id"]};", + ).first next if r.nil? photo = r["photo"] next unless photo.present? @@ -229,9 +238,9 @@ class BulkImport::Vanilla < BulkImport::Base photo_real_filename = nil parts = photo.squeeze("/").split("/") if parts[0] =~ /^[a-z0-9]{2}:/ - photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[2..-2].join('/')}".squeeze("/") + photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[2..-2].join("/")}".squeeze("/") elsif parts[0] == "~cf" - photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[1..-2].join('/')}".squeeze("/") + photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[1..-2].join("/")}".squeeze("/") else puts "UNKNOWN FORMAT: #{photo}" next @@ -272,75 +281,86 @@ class BulkImport::Vanilla < BulkImport::Base count = 0 # https://us.v-cdn.net/1234567/uploads/editor/xyz/image.jpg - cdn_regex = /https:\/\/us.v-cdn.net\/1234567\/uploads\/(\S+\/(\w|-)+.\w+)/i + cdn_regex = %r{https://us.v-cdn.net/1234567/uploads/(\S+/(\w|-)+.\w+)}i # [attachment=10109:Screen Shot 2012-04-01 at 3.47.35 AM.png] attachment_regex = /\[attachment=(\d+):(.*?)\]/i - Post.where("raw LIKE '%/us.v-cdn.net/%' OR raw LIKE '%[attachment%'").find_each do |post| - count += 1 - print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] - new_raw = post.raw.dup + Post + .where("raw LIKE '%/us.v-cdn.net/%' OR raw LIKE '%[attachment%'") + .find_each do |post| + count += 1 + print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] + new_raw = post.raw.dup - new_raw.gsub!(attachment_regex) do |s| - matches = attachment_regex.match(s) - attachment_id = matches[1] - file_name = matches[2] - next unless attachment_id + new_raw.gsub!(attachment_regex) do |s| + matches = attachment_regex.match(s) + attachment_id = matches[1] + file_name = matches[2] + next unless attachment_id - r = mysql_query("SELECT Path, Name FROM #{TABLE_PREFIX}Media WHERE MediaID = #{attachment_id};").first - next if r.nil? - path = r["Path"] - name = r["Name"] - next unless path.present? + r = + mysql_query( + "SELECT Path, Name FROM #{TABLE_PREFIX}Media WHERE MediaID = #{attachment_id};", + ).first + next if r.nil? + path = r["Path"] + name = r["Name"] + next unless path.present? - path.gsub!("s3://content/", "") - path.gsub!("s3://uploads/", "") - file_path = "#{ATTACHMENTS_BASE_DIR}/#{path}" + path.gsub!("s3://content/", "") + path.gsub!("s3://uploads/", "") + file_path = "#{ATTACHMENTS_BASE_DIR}/#{path}" - if File.exist?(file_path) - upload = create_upload(post.user.id, file_path, File.basename(file_path)) - if upload && upload.errors.empty? - # upload.url - filename = name || file_name || File.basename(file_path) - html_for_upload(upload, normalize_text(filename)) + if File.exist?(file_path) + upload = create_upload(post.user.id, file_path, File.basename(file_path)) + if upload && upload.errors.empty? + # upload.url + filename = name || file_name || File.basename(file_path) + html_for_upload(upload, normalize_text(filename)) + else + puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + end else - puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + puts "Couldn't find file for #{attachment_id}. Skipping." + next end - else - puts "Couldn't find file for #{attachment_id}. Skipping." - next end - end - new_raw.gsub!(cdn_regex) do |s| - matches = cdn_regex.match(s) - attachment_id = matches[1] + new_raw.gsub!(cdn_regex) do |s| + matches = cdn_regex.match(s) + attachment_id = matches[1] - file_path = "#{ATTACHMENTS_BASE_DIR}/#{attachment_id}" + file_path = "#{ATTACHMENTS_BASE_DIR}/#{attachment_id}" - if File.exist?(file_path) - upload = create_upload(post.user.id, file_path, File.basename(file_path)) - if upload && upload.errors.empty? - upload.url + if File.exist?(file_path) + upload = create_upload(post.user.id, file_path, File.basename(file_path)) + if upload && upload.errors.empty? + upload.url + else + puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + end else - puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + puts "Couldn't find file for #{attachment_id}. Skipping." + next end - else - puts "Couldn't find file for #{attachment_id}. Skipping." - next end - end - if new_raw != post.raw - begin - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, skip_revision: true, skip_validations: true, bypass_bump: true) - rescue - puts "PostRevisor error for #{post.id}" - post.raw = new_raw - post.save(validate: false) + if new_raw != post.raw + begin + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + skip_revision: true, + skip_validations: true, + bypass_bump: true, + ) + rescue StandardError + puts "PostRevisor error for #{post.id}" + post.raw = new_raw + post.save(validate: false) + end end end - end end end @@ -352,7 +372,7 @@ class BulkImport::Vanilla < BulkImport::Base # Otherwise, the file exists but with a prefix: # The p prefix seems to be the full file, so try to find that one first. - ['p', 't', 'n'].each do |prefix| + %w[p t n].each do |prefix| full_guess = File.join(path, "#{prefix}#{base_guess}") return full_guess if File.exist?(full_guess) end @@ -364,26 +384,30 @@ class BulkImport::Vanilla < BulkImport::Base def import_categories puts "", "Importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT CategoryID, ParentCategoryID, Name, Description, Sort FROM #{TABLE_PREFIX}Category WHERE CategoryID > 0 ORDER BY Sort, CategoryID - ").to_a + ", + ).to_a # Throw the -1 level categories away since they contain no topics. # Use the next level as root categories. - top_level_categories = categories.select { |c| c["ParentCategoryID"].blank? || c['ParentCategoryID'] == -1 } + top_level_categories = + categories.select { |c| c["ParentCategoryID"].blank? || c["ParentCategoryID"] == -1 } # Depth = 2 create_categories(top_level_categories) do |category| - next if category_id_from_imported_id(category['CategoryID']) + next if category_id_from_imported_id(category["CategoryID"]) { - imported_id: category['CategoryID'], - name: CGI.unescapeHTML(category['Name']), - description: category['Description'] ? CGI.unescapeHTML(category['Description']) : nil, - position: category['Sort'] + imported_id: category["CategoryID"], + name: CGI.unescapeHTML(category["Name"]), + description: category["Description"] ? CGI.unescapeHTML(category["Description"]) : nil, + position: category["Sort"], } end @@ -393,39 +417,39 @@ class BulkImport::Vanilla < BulkImport::Base # Depth = 3 create_categories(subcategories) do |category| - next if category_id_from_imported_id(category['CategoryID']) + next if category_id_from_imported_id(category["CategoryID"]) { - imported_id: category['CategoryID'], - parent_category_id: category_id_from_imported_id(category['ParentCategoryID']), - name: CGI.unescapeHTML(category['Name']), - description: category['Description'] ? CGI.unescapeHTML(category['Description']) : nil, - position: category['Sort'] + imported_id: category["CategoryID"], + parent_category_id: category_id_from_imported_id(category["ParentCategoryID"]), + name: CGI.unescapeHTML(category["Name"]), + description: category["Description"] ? CGI.unescapeHTML(category["Description"]) : nil, + position: category["Sort"], } end - subcategory_ids = Set.new(subcategories.map { |c| c['CategoryID'] }) + subcategory_ids = Set.new(subcategories.map { |c| c["CategoryID"] }) # Depth 4 and 5 need to be tags categories.each do |c| - next if c['ParentCategoryID'] == -1 - next if top_level_category_ids.include?(c['CategoryID']) - next if subcategory_ids.include?(c['CategoryID']) + next if c["ParentCategoryID"] == -1 + next if top_level_category_ids.include?(c["CategoryID"]) + next if subcategory_ids.include?(c["CategoryID"]) # Find a depth 3 category for topics in this category parent = c - while !parent.nil? && !subcategory_ids.include?(parent['CategoryID']) - parent = categories.find { |subcat| subcat['CategoryID'] == parent['ParentCategoryID'] } + while !parent.nil? && !subcategory_ids.include?(parent["CategoryID"]) + parent = categories.find { |subcat| subcat["CategoryID"] == parent["ParentCategoryID"] } end if parent - tag_name = DiscourseTagging.clean_tag(c['Name']) - @category_mappings[c['CategoryID']] = { - category_id: category_id_from_imported_id(parent['CategoryID']), - tag: Tag.find_by_name(tag_name) || Tag.create(name: tag_name) + tag_name = DiscourseTagging.clean_tag(c["Name"]) + @category_mappings[c["CategoryID"]] = { + category_id: category_id_from_imported_id(parent["CategoryID"]), + tag: Tag.find_by_name(tag_name) || Tag.create(name: tag_name), } else - puts '', "Couldn't find a category for #{c['CategoryID']} '#{c['Name']}'!" + puts "", "Couldn't find a category for #{c["CategoryID"]} '#{c["Name"]}'!" end end end @@ -433,7 +457,8 @@ class BulkImport::Vanilla < BulkImport::Base def import_topics puts "", "Importing topics..." - topics_sql = "SELECT DiscussionID, CategoryID, Name, Body, DateInserted, InsertUserID, Announce, Format + topics_sql = + "SELECT DiscussionID, CategoryID, Name, Body, DateInserted, InsertUserID, Announce, Format FROM #{TABLE_PREFIX}Discussion WHERE DiscussionID > #{@last_imported_topic_id} ORDER BY DiscussionID ASC" @@ -442,11 +467,12 @@ class BulkImport::Vanilla < BulkImport::Base data = { imported_id: row["DiscussionID"], title: normalize_text(row["Name"]), - category_id: category_id_from_imported_id(row["CategoryID"]) || - @category_mappings[row["CategoryID"]].try(:[], :category_id), + category_id: + category_id_from_imported_id(row["CategoryID"]) || + @category_mappings[row["CategoryID"]].try(:[], :category_id), user_id: user_id_from_imported_id(row["InsertUserID"]), - created_at: Time.zone.at(row['DateInserted']), - pinned_at: row['Announce'] == 0 ? nil : Time.zone.at(row['DateInserted']) + created_at: Time.zone.at(row["DateInserted"]), + pinned_at: row["Announce"] == 0 ? nil : Time.zone.at(row["DateInserted"]), } (data[:user_id].present? && data[:title].present?) ? data : false end @@ -455,46 +481,45 @@ class BulkImport::Vanilla < BulkImport::Base create_posts(mysql_stream(topics_sql)) do |row| data = { - imported_id: "d-" + row['DiscussionID'].to_s, - topic_id: topic_id_from_imported_id(row['DiscussionID']), + imported_id: "d-" + row["DiscussionID"].to_s, + topic_id: topic_id_from_imported_id(row["DiscussionID"]), user_id: user_id_from_imported_id(row["InsertUserID"]), - created_at: Time.zone.at(row['DateInserted']), - raw: clean_up(row['Body'], row['Format']) + created_at: Time.zone.at(row["DateInserted"]), + raw: clean_up(row["Body"], row["Format"]), } data[:topic_id].present? ? data : false end - puts '', 'converting deep categories to tags...' + puts "", "converting deep categories to tags..." create_topic_tags(mysql_stream(topics_sql)) do |row| - next unless mapping = @category_mappings[row['CategoryID']] + next unless mapping = @category_mappings[row["CategoryID"]] - { - tag_id: mapping[:tag].id, - topic_id: topic_id_from_imported_id(row["DiscussionID"]) - } + { tag_id: mapping[:tag].id, topic_id: topic_id_from_imported_id(row["DiscussionID"]) } end end def import_posts puts "", "Importing posts..." - posts = mysql_stream( - "SELECT CommentID, DiscussionID, Body, DateInserted, InsertUserID, Format + posts = + mysql_stream( + "SELECT CommentID, DiscussionID, Body, DateInserted, InsertUserID, Format FROM #{TABLE_PREFIX}Comment WHERE CommentID > #{@last_imported_post_id} - ORDER BY CommentID ASC") + ORDER BY CommentID ASC", + ) create_posts(posts) do |row| - next unless topic_id = topic_id_from_imported_id(row['DiscussionID']) - next if row['Body'].blank? + next unless topic_id = topic_id_from_imported_id(row["DiscussionID"]) + next if row["Body"].blank? { - imported_id: row['CommentID'], + imported_id: row["CommentID"], topic_id: topic_id, - user_id: user_id_from_imported_id(row['InsertUserID']), - created_at: Time.zone.at(row['DateInserted']), - raw: clean_up(row['Body'], row['Format']) + user_id: user_id_from_imported_id(row["InsertUserID"]), + created_at: Time.zone.at(row["DateInserted"]), + raw: clean_up(row["Body"], row["Format"]), } end end @@ -505,31 +530,31 @@ class BulkImport::Vanilla < BulkImport::Base tag_mapping = {} mysql_query("SELECT TagID, Name FROM #{TABLE_PREFIX}Tag").each do |row| - tag_name = DiscourseTagging.clean_tag(row['Name']) + tag_name = DiscourseTagging.clean_tag(row["Name"]) tag = Tag.find_by_name(tag_name) || Tag.create(name: tag_name) - tag_mapping[row['TagID']] = tag.id + tag_mapping[row["TagID"]] = tag.id end - tags = mysql_query( - "SELECT TagID, DiscussionID + tags = + mysql_query( + "SELECT TagID, DiscussionID FROM #{TABLE_PREFIX}TagDiscussion WHERE DiscussionID > #{@last_imported_topic_id} - ORDER BY DateInserted") + ORDER BY DateInserted", + ) create_topic_tags(tags) do |row| - next unless topic_id = topic_id_from_imported_id(row['DiscussionID']) + next unless topic_id = topic_id_from_imported_id(row["DiscussionID"]) - { - topic_id: topic_id, - tag_id: tag_mapping[row['TagID']] - } + { topic_id: topic_id, tag_id: tag_mapping[row["TagID"]] } end end def import_private_topics puts "", "Importing private topics..." - topics_sql = "SELECT c.ConversationID, c.Subject, m.MessageID, m.Body, c.DateInserted, c.InsertUserID + topics_sql = + "SELECT c.ConversationID, c.Subject, m.MessageID, m.Body, c.DateInserted, c.InsertUserID FROM #{TABLE_PREFIX}Conversation c, #{TABLE_PREFIX}ConversationMessage m WHERE c.FirstMessageID = m.MessageID AND c.ConversationID > #{@last_imported_private_topic_id - PRIVATE_OFFSET} @@ -539,9 +564,10 @@ class BulkImport::Vanilla < BulkImport::Base { archetype: Archetype.private_message, imported_id: row["ConversationID"] + PRIVATE_OFFSET, - title: row["Subject"] ? normalize_text(row["Subject"]) : "Conversation #{row["ConversationID"]}", + title: + row["Subject"] ? normalize_text(row["Subject"]) : "Conversation #{row["ConversationID"]}", user_id: user_id_from_imported_id(row["InsertUserID"]), - created_at: Time.zone.at(row['DateInserted']) + created_at: Time.zone.at(row["DateInserted"]), } end end @@ -549,7 +575,8 @@ class BulkImport::Vanilla < BulkImport::Base def import_topic_allowed_users puts "", "importing topic_allowed_users..." - topic_allowed_users_sql = " + topic_allowed_users_sql = + " SELECT ConversationID, UserID FROM #{TABLE_PREFIX}UserConversation WHERE Deleted = 0 @@ -559,45 +586,43 @@ class BulkImport::Vanilla < BulkImport::Base added = 0 create_topic_allowed_users(mysql_stream(topic_allowed_users_sql)) do |row| - next unless topic_id = topic_id_from_imported_id(row['ConversationID'] + PRIVATE_OFFSET) + next unless topic_id = topic_id_from_imported_id(row["ConversationID"] + PRIVATE_OFFSET) next unless user_id = user_id_from_imported_id(row["UserID"]) added += 1 - { - topic_id: topic_id, - user_id: user_id, - } + { topic_id: topic_id, user_id: user_id } end - puts '', "Added #{added} topic_allowed_users records." + puts "", "Added #{added} topic_allowed_users records." end def import_private_posts puts "", "importing private replies..." - private_posts_sql = " + private_posts_sql = + " SELECT ConversationID, MessageID, Body, InsertUserID, DateInserted, Format FROM GDN_ConversationMessage WHERE ConversationID > #{@last_imported_private_topic_id - PRIVATE_OFFSET} ORDER BY ConversationID ASC, MessageID ASC" create_posts(mysql_stream(private_posts_sql)) do |row| - next unless topic_id = topic_id_from_imported_id(row['ConversationID'] + PRIVATE_OFFSET) + next unless topic_id = topic_id_from_imported_id(row["ConversationID"] + PRIVATE_OFFSET) { - imported_id: row['MessageID'] + PRIVATE_OFFSET, + imported_id: row["MessageID"] + PRIVATE_OFFSET, topic_id: topic_id, - user_id: user_id_from_imported_id(row['InsertUserID']), - created_at: Time.zone.at(row['DateInserted']), - raw: clean_up(row['Body'], row['Format']) + user_id: user_id_from_imported_id(row["InsertUserID"]), + created_at: Time.zone.at(row["DateInserted"]), + raw: clean_up(row["Body"], row["Format"]), } end end # TODO: too slow def create_permalinks - puts '', 'Creating permalinks...', '' + puts "", "Creating permalinks...", "" - puts ' User pages...' + puts " User pages..." start = Time.now count = 0 @@ -606,21 +631,23 @@ class BulkImport::Vanilla < BulkImport::Base sql = "COPY permalinks (url, created_at, updated_at, external_url) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - User.includes(:_custom_fields).find_each do |u| - count += 1 - ucf = u.custom_fields - if ucf && ucf["import_id"] - vanilla_username = ucf["import_username"] || u.username - @raw_connection.put_copy_data( - ["profile/#{vanilla_username}", now, now, "/users/#{u.username}"] - ) - end + User + .includes(:_custom_fields) + .find_each do |u| + count += 1 + ucf = u.custom_fields + if ucf && ucf["import_id"] + vanilla_username = ucf["import_username"] || u.username + @raw_connection.put_copy_data( + ["profile/#{vanilla_username}", now, now, "/users/#{u.username}"], + ) + end - print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0 - end + print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0 + end end - puts '', '', ' Topics and posts...' + puts "", "", " Topics and posts..." start = Time.now count = 0 @@ -628,38 +655,36 @@ class BulkImport::Vanilla < BulkImport::Base sql = "COPY permalinks (url, topic_id, post_id, created_at, updated_at) FROM STDIN" @raw_connection.copy_data(sql, @encoder) do - Post.includes(:_custom_fields).find_each do |post| - count += 1 - pcf = post.custom_fields - if pcf && pcf["import_id"] - topic = post.topic - if topic.present? - id = pcf["import_id"].split('-').last - if post.post_number == 1 - slug = Slug.for(topic.title) # probably matches what vanilla would do... - @raw_connection.put_copy_data( - ["discussion/#{id}/#{slug}", topic.id, nil, now, now] - ) - else - @raw_connection.put_copy_data( - ["discussion/comment/#{id}", nil, post.id, now, now] - ) + Post + .includes(:_custom_fields) + .find_each do |post| + count += 1 + pcf = post.custom_fields + if pcf && pcf["import_id"] + topic = post.topic + if topic.present? + id = pcf["import_id"].split("-").last + if post.post_number == 1 + slug = Slug.for(topic.title) # probably matches what vanilla would do... + @raw_connection.put_copy_data(["discussion/#{id}/#{slug}", topic.id, nil, now, now]) + else + @raw_connection.put_copy_data(["discussion/comment/#{id}", nil, post.id, now, now]) + end end end - end - print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0 - end + print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] if count % 5000 == 0 + end end end def clean_up(raw, format) raw.encode!("utf-8", "utf-8", invalid: :replace, undef: :replace, replace: "") - raw.gsub!(/<(.+)> <\/\1>/, "\n\n") + raw.gsub!(%r{<(.+)> }, "\n\n") html = - if format == 'Html' + if format == "Html" raw else markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true) @@ -668,29 +693,23 @@ class BulkImport::Vanilla < BulkImport::Base doc = Nokogiri::HTML5.fragment(html) - doc.css("blockquote").each do |bq| - name = bq["rel"] - user = User.find_by(name: name) - bq.replace %{
    [QUOTE="#{user&.username || name}"]\n#{bq.inner_html}\n[/QUOTE]
    } - end + doc + .css("blockquote") + .each do |bq| + name = bq["rel"] + user = User.find_by(name: name) + bq.replace %{
    [QUOTE="#{user&.username || name}"]\n#{bq.inner_html}\n[/QUOTE]
    } + end - doc.css("font").reverse.each do |f| - f.replace f.inner_html - end + doc.css("font").reverse.each { |f| f.replace f.inner_html } - doc.css("span").reverse.each do |f| - f.replace f.inner_html - end + doc.css("span").reverse.each { |f| f.replace f.inner_html } - doc.css("sub").reverse.each do |f| - f.replace f.inner_html - end + doc.css("sub").reverse.each { |f| f.replace f.inner_html } - doc.css("u").reverse.each do |f| - f.replace f.inner_html - end + doc.css("u").reverse.each { |f| f.replace f.inner_html } - markdown = format == 'Html' ? ReverseMarkdown.convert(doc.to_html) : doc.to_html + markdown = format == "Html" ? ReverseMarkdown.convert(doc.to_html) : doc.to_html markdown.gsub!(/\[QUOTE="([^;]+);c-(\d+)"\]/i) { "[QUOTE=#{$1};#{$2}]" } markdown = process_raw_text(markdown) @@ -702,31 +721,31 @@ class BulkImport::Vanilla < BulkImport::Base text = raw.dup text = CGI.unescapeHTML(text) - text.gsub!(/:(?:\w{8})\]/, ']') + text.gsub!(/:(?:\w{8})\]/, "]") # Some links look like this: http://www.onegameamonth.com - text.gsub!(/(.+)<\/a>/i, '[\2](\1)') + text.gsub!(%r{(.+)}i, '[\2](\1)') # phpBB shortens link text like this, which breaks our markdown processing: # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - text.gsub!(/\[http(s)?:\/\/(www\.)?/i, '[') + text.gsub!(%r{\[http(s)?://(www\.)?}i, "[") # convert list tags to ul and list=1 tags to ol # list=a is not supported, so handle it like list=1 # list=9 and list=x have the same result as list=1 and list=a - text.gsub!(/\[list\](.*?)\[\/list:u\]/mi, '[ul]\1[/ul]') - text.gsub!(/\[list=.*?\](.*?)\[\/list:o\]/mi, '[ol]\1[/ol]') + text.gsub!(%r{\[list\](.*?)\[/list:u\]}mi, '[ul]\1[/ul]') + text.gsub!(%r{\[list=.*?\](.*?)\[/list:o\]}mi, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - text.gsub!(/\[\*\](.*?)\[\/\*:m\]/mi, '[li]\1[/li]') + text.gsub!(%r{\[\*\](.*?)\[/\*:m\]}mi, '[li]\1[/li]') # [QUOTE=""] -- add newline text.gsub!(/(\[quote="[a-zA-Z\d]+"\])/i) { "#{$1}\n" } # [/QUOTE] -- add newline - text.gsub!(/(\[\/quote\])/i) { "\n#{$1}" } + text.gsub!(%r{(\[/quote\])}i) { "\n#{$1}" } text end @@ -742,7 +761,6 @@ class BulkImport::Vanilla < BulkImport::Base def mysql_query(sql) @client.query(sql) end - end BulkImport::Vanilla.new.start diff --git a/script/bulk_import/vbulletin.rb b/script/bulk_import/vbulletin.rb index fde0fbe7d7..b836338a51 100644 --- a/script/bulk_import/vbulletin.rb +++ b/script/bulk_import/vbulletin.rb @@ -7,43 +7,42 @@ require "htmlentities" require "parallel" class BulkImport::VBulletin < BulkImport::Base - - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "vb_" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "vb_" SUSPENDED_TILL ||= Date.new(3000, 1, 1) - ATTACHMENT_DIR ||= ENV['ATTACHMENT_DIR'] || '/shared/import/data/attachments' - AVATAR_DIR ||= ENV['AVATAR_DIR'] || '/shared/import/data/customavatars' + ATTACHMENT_DIR ||= ENV["ATTACHMENT_DIR"] || "/shared/import/data/attachments" + AVATAR_DIR ||= ENV["AVATAR_DIR"] || "/shared/import/data/customavatars" def initialize super - host = ENV["DB_HOST"] || "localhost" + host = ENV["DB_HOST"] || "localhost" username = ENV["DB_USERNAME"] || "root" password = ENV["DB_PASSWORD"] database = ENV["DB_NAME"] || "vbulletin" - charset = ENV["DB_CHARSET"] || "utf8" + charset = ENV["DB_CHARSET"] || "utf8" @html_entities = HTMLEntities.new @encoding = CHARSET_MAP[charset] - @client = Mysql2::Client.new( - host: host, - username: username, - password: password, - database: database, - encoding: charset, - reconnect: true - ) + @client = + Mysql2::Client.new( + host: host, + username: username, + password: password, + database: database, + encoding: charset, + reconnect: true, + ) @client.query_options.merge!(as: :array, cache_rows: false) - @has_post_thanks = mysql_query(<<-SQL + @has_post_thanks = mysql_query(<<-SQL).to_a.count > 0 SELECT `COLUMN_NAME` FROM `INFORMATION_SCHEMA`.`COLUMNS` WHERE `TABLE_SCHEMA`='#{database}' AND `TABLE_NAME`='user' AND `COLUMN_NAME` LIKE 'post_thanks_%' SQL - ).to_a.count > 0 @user_ids_by_email = {} end @@ -95,7 +94,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_groups - puts '', "Importing groups..." + puts "", "Importing groups..." groups = mysql_stream <<-SQL SELECT usergroupid, title, description, usertitle @@ -115,7 +114,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_users - puts '', "Importing users..." + puts "", "Importing users..." users = mysql_stream <<-SQL SELECT u.userid, username, email, joindate, birthday, ipaddress, u.usergroupid, bandate, liftdate @@ -145,7 +144,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_user_emails - puts '', "Importing user emails..." + puts "", "Importing user emails..." users = mysql_stream <<-SQL SELECT u.userid, email, joindate @@ -155,7 +154,7 @@ class BulkImport::VBulletin < BulkImport::Base SQL create_user_emails(users) do |row| - user_id, email = row[0 .. 1] + user_id, email = row[0..1] @user_ids_by_email[email.downcase] ||= [] user_ids = @user_ids_by_email[email.downcase] << user_id @@ -170,7 +169,7 @@ class BulkImport::VBulletin < BulkImport::Base imported_id: user_id, imported_user_id: user_id, email: email, - created_at: Time.zone.at(row[2]) + created_at: Time.zone.at(row[2]), } end @@ -179,7 +178,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_user_stats - puts '', "Importing user stats..." + puts "", "Importing user stats..." users = mysql_stream <<-SQL SELECT u.userid, joindate, posts, COUNT(t.threadid) AS threads, p.dateline @@ -199,7 +198,7 @@ class BulkImport::VBulletin < BulkImport::Base new_since: Time.zone.at(row[1]), post_count: row[2], topic_count: row[3], - first_post_created_at: row[4] && Time.zone.at(row[4]) + first_post_created_at: row[4] && Time.zone.at(row[4]), } if @has_post_thanks @@ -212,7 +211,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_group_users - puts '', "Importing group users..." + puts "", "Importing group users..." group_users = mysql_stream <<-SQL SELECT usergroupid, userid @@ -221,15 +220,12 @@ class BulkImport::VBulletin < BulkImport::Base SQL create_group_users(group_users) do |row| - { - group_id: group_id_from_imported_id(row[0]), - user_id: user_id_from_imported_id(row[1]), - } + { group_id: group_id_from_imported_id(row[0]), user_id: user_id_from_imported_id(row[1]) } end end def import_user_passwords - puts '', "Importing user passwords..." + puts "", "Importing user passwords..." user_passwords = mysql_stream <<-SQL SELECT userid, password @@ -239,15 +235,12 @@ class BulkImport::VBulletin < BulkImport::Base SQL create_custom_fields("user", "password", user_passwords) do |row| - { - record_id: user_id_from_imported_id(row[0]), - value: row[1], - } + { record_id: user_id_from_imported_id(row[0]), value: row[1] } end end def import_user_salts - puts '', "Importing user salts..." + puts "", "Importing user salts..." user_salts = mysql_stream <<-SQL SELECT userid, salt @@ -258,15 +251,12 @@ class BulkImport::VBulletin < BulkImport::Base SQL create_custom_fields("user", "salt", user_salts) do |row| - { - record_id: user_id_from_imported_id(row[0]), - value: row[1], - } + { record_id: user_id_from_imported_id(row[0]), value: row[1] } end end def import_user_profiles - puts '', "Importing user profiles..." + puts "", "Importing user profiles..." user_profiles = mysql_stream <<-SQL SELECT userid, homepage, profilevisits @@ -278,16 +268,23 @@ class BulkImport::VBulletin < BulkImport::Base create_user_profiles(user_profiles) do |row| { user_id: user_id_from_imported_id(row[0]), - website: (URI.parse(row[1]).to_s rescue nil), + website: + ( + begin + URI.parse(row[1]).to_s + rescue StandardError + nil + end + ), views: row[2], } end end def import_categories - puts '', "Importing categories..." + puts "", "Importing categories..." - categories = mysql_query(<<-SQL + categories = mysql_query(<<-SQL).to_a select forumid, parentid, @@ -311,23 +308,20 @@ class BulkImport::VBulletin < BulkImport::Base from forum order by forumid SQL - ).to_a return if categories.empty? - parent_categories = categories.select { |c| c[1] == -1 } + parent_categories = categories.select { |c| c[1] == -1 } children_categories = categories.select { |c| c[1] != -1 } parent_category_ids = Set.new parent_categories.map { |c| c[0] } # cut down the tree to only 2 levels of categories children_categories.each do |cc| - until parent_category_ids.include?(cc[1]) - cc[1] = categories.find { |c| c[0] == cc[1] }[1] - end + cc[1] = categories.find { |c| c[0] == cc[1] }[1] until parent_category_ids.include?(cc[1]) end - puts '', "Importing parent categories..." + puts "", "Importing parent categories..." create_categories(parent_categories) do |row| { imported_id: row[0], @@ -337,7 +331,7 @@ class BulkImport::VBulletin < BulkImport::Base } end - puts '', "Importing children categories..." + puts "", "Importing children categories..." create_categories(children_categories) do |row| { imported_id: row[0], @@ -350,7 +344,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_topics - puts '', "Importing topics..." + puts "", "Importing topics..." topics = mysql_stream <<-SQL SELECT threadid, title, forumid, postuserid, open, dateline, views, visible, sticky @@ -381,7 +375,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_posts - puts '', "Importing posts..." + puts "", "Importing posts..." posts = mysql_stream <<-SQL SELECT postid, p.threadid, parentid, userid, p.dateline, p.visible, pagetext @@ -396,7 +390,8 @@ class BulkImport::VBulletin < BulkImport::Base create_posts(posts) do |row| topic_id = topic_id_from_imported_id(row[1]) replied_post_topic_id = topic_id_from_imported_post_id(row[2]) - reply_to_post_number = topic_id == replied_post_topic_id ? post_number_from_imported_id(row[2]) : nil + reply_to_post_number = + topic_id == replied_post_topic_id ? post_number_from_imported_id(row[2]) : nil post = { imported_id: row[0], @@ -415,7 +410,7 @@ class BulkImport::VBulletin < BulkImport::Base def import_likes return unless @has_post_thanks - puts '', "Importing likes..." + puts "", "Importing likes..." @imported_likes = Set.new @last_imported_post_id = 0 @@ -438,13 +433,13 @@ class BulkImport::VBulletin < BulkImport::Base post_id: post_id_from_imported_id(row[0]), user_id: user_id_from_imported_id(row[1]), post_action_type_id: 2, - created_at: Time.zone.at(row[2]) + created_at: Time.zone.at(row[2]), } end end def import_private_topics - puts '', "Importing private topics..." + puts "", "Importing private topics..." @imported_topics = {} @@ -473,34 +468,31 @@ class BulkImport::VBulletin < BulkImport::Base end def import_topic_allowed_users - puts '', "Importing topic allowed users..." + puts "", "Importing topic allowed users..." allowed_users = Set.new - mysql_stream(<<-SQL + mysql_stream(<<-SQL).each do |row| SELECT pmtextid, touserarray FROM #{TABLE_PREFIX}pmtext WHERE pmtextid > (#{@last_imported_private_topic_id - PRIVATE_OFFSET}) ORDER BY pmtextid SQL - ).each do |row| next unless topic_id = topic_id_from_imported_id(row[0] + PRIVATE_OFFSET) - row[1].scan(/i:(\d+)/).flatten.each do |id| - next unless user_id = user_id_from_imported_id(id) - allowed_users << [topic_id, user_id] - end + row[1] + .scan(/i:(\d+)/) + .flatten + .each do |id| + next unless user_id = user_id_from_imported_id(id) + allowed_users << [topic_id, user_id] + end end - create_topic_allowed_users(allowed_users) do |row| - { - topic_id: row[0], - user_id: row[1], - } - end + create_topic_allowed_users(allowed_users) { |row| { topic_id: row[0], user_id: row[1] } } end def import_private_posts - puts '', "Importing private posts..." + puts "", "Importing private posts..." posts = mysql_stream <<-SQL SELECT pmtextid, title, fromuserid, touserarray, dateline, message @@ -527,7 +519,7 @@ class BulkImport::VBulletin < BulkImport::Base end def create_permalink_file - puts '', 'Creating Permalink File...', '' + puts "", "Creating Permalink File...", "" total = Topic.listable_topics.count start = Time.now @@ -538,9 +530,9 @@ class BulkImport::VBulletin < BulkImport::Base i += 1 pcf = topic.posts.includes(:_custom_fields).where(post_number: 1).first.custom_fields if pcf && pcf["import_id"] - id = pcf["import_id"].split('-').last + id = pcf["import_id"].split("-").last - f.print [ "XXX#{id} YYY#{topic.id}" ].to_csv + f.print ["XXX#{id} YYY#{topic.id}"].to_csv print "\r%7d/%7d - %6d/sec" % [i, total, i.to_f / (Time.now - start)] if i % 5000 == 0 end end @@ -549,7 +541,8 @@ class BulkImport::VBulletin < BulkImport::Base # find the uploaded file information from the db def find_upload(post, attachment_id) - sql = "SELECT a.attachmentid attachment_id, a.userid user_id, a.filename filename + sql = + "SELECT a.attachmentid attachment_id, a.userid user_id, a.filename filename FROM #{TABLE_PREFIX}attachment a WHERE a.attachmentid = #{attachment_id}" results = mysql_query(sql) @@ -563,9 +556,10 @@ class BulkImport::VBulletin < BulkImport::Base user_id = row[1] db_filename = row[2] - filename = File.join(ATTACHMENT_DIR, user_id.to_s.split('').join('/'), "#{attachment_id}.attach") + filename = + File.join(ATTACHMENT_DIR, user_id.to_s.split("").join("/"), "#{attachment_id}.attach") real_filename = db_filename - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + real_filename.prepend SecureRandom.hex if real_filename[0] == "." unless File.exist?(filename) puts "Attachment file #{row.inspect} doesn't exist" @@ -588,7 +582,7 @@ class BulkImport::VBulletin < BulkImport::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." RateLimiter.disable current_count = 0 @@ -596,7 +590,7 @@ class BulkImport::VBulletin < BulkImport::Base success_count = 0 fail_count = 0 - attachment_regex = /\[attach[^\]]*\](\d+)\[\/attach\]/i + attachment_regex = %r{\[attach[^\]]*\](\d+)\[/attach\]}i Post.find_each do |post| current_count += 1 @@ -618,7 +612,12 @@ class BulkImport::VBulletin < BulkImport::Base end if new_raw != post.raw - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: 'Import attachments from vBulletin') + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + bypass_bump: true, + edit_reason: "Import attachments from vBulletin", + ) end success_count += 1 @@ -639,7 +638,7 @@ class BulkImport::VBulletin < BulkImport::Base Dir.foreach(AVATAR_DIR) do |item| print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] - next if item == ('.') || item == ('..') || item == ('.DS_Store') + next if item == (".") || item == ("..") || item == (".DS_Store") next unless item =~ /avatar(\d+)_(\d).gif/ scan = item.scan(/avatar(\d+)_(\d).gif/) next unless scan[0][0].present? @@ -671,11 +670,10 @@ class BulkImport::VBulletin < BulkImport::Base def import_signatures puts "Importing user signatures..." - total_count = mysql_query(<<-SQL + total_count = mysql_query(<<-SQL).first[0].to_i SELECT COUNT(userid) count FROM #{TABLE_PREFIX}sigparsed SQL - ).first[0].to_i current_count = 0 user_signatures = mysql_stream <<-SQL @@ -695,13 +693,20 @@ class BulkImport::VBulletin < BulkImport::Base next unless u.present? # can not hold dupes - UserCustomField.where(user_id: u.id, name: ["see_signatures", "signature_raw", "signature_cooked"]).destroy_all + UserCustomField.where( + user_id: u.id, + name: %w[see_signatures signature_raw signature_cooked], + ).destroy_all - user_sig.gsub!(/\[\/?sigpic\]/i, "") + user_sig.gsub!(%r{\[/?sigpic\]}i, "") UserCustomField.create!(user_id: u.id, name: "see_signatures", value: true) UserCustomField.create!(user_id: u.id, name: "signature_raw", value: user_sig) - UserCustomField.create!(user_id: u.id, name: "signature_cooked", value: PrettyText.cook(user_sig, omit_nofollow: false)) + UserCustomField.create!( + user_id: u.id, + name: "signature_cooked", + value: PrettyText.cook(user_sig, omit_nofollow: false), + ) end end @@ -710,15 +715,15 @@ class BulkImport::VBulletin < BulkImport::Base total_count = 0 duplicated = {} - @user_ids_by_email. - select { |e, ids| ids.count > 1 }. - each_with_index do |(email, ids), i| - duplicated[email] = [ ids, i ] + @user_ids_by_email + .select { |e, ids| ids.count > 1 } + .each_with_index do |(email, ids), i| + duplicated[email] = [ids, i] count += 1 total_count += ids.count end - puts '', "Merging #{total_count} duplicated users across #{count} distinct emails..." + puts "", "Merging #{total_count} duplicated users across #{count} distinct emails..." start = Time.now @@ -727,14 +732,15 @@ class BulkImport::VBulletin < BulkImport::Base next unless email.presence # queried one by one to ensure ordering - first, *rest = user_ids.map do |id| - UserCustomField.includes(:user).find_by!(name: 'import_id', value: id).user - end + first, *rest = + user_ids.map do |id| + UserCustomField.includes(:user).find_by!(name: "import_id", value: id).user + end rest.each do |dup| UserMerger.new(dup, first).merge! first.reload - printf '.' + printf "." end print "\n%6d/%6d - %6d/sec" % [i, count, i.to_f / (Time.now - start)] if i % 10 == 0 @@ -744,13 +750,11 @@ class BulkImport::VBulletin < BulkImport::Base end def save_duplicated_users - File.open('duplicated_users.json', 'w+') do |f| - f.puts @user_ids_by_email.to_json - end + File.open("duplicated_users.json", "w+") { |f| f.puts @user_ids_by_email.to_json } end def read_duplicated_users - @user_ids_by_email = JSON.parse File.read('duplicated_users.json') + @user_ids_by_email = JSON.parse File.read("duplicated_users.json") end def extract_pm_title(title) @@ -759,17 +763,26 @@ class BulkImport::VBulletin < BulkImport::Base def parse_birthday(birthday) return if birthday.blank? - date_of_birth = Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") rescue nil + date_of_birth = + begin + Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") + rescue StandardError + nil + end return if date_of_birth.nil? - date_of_birth.year < 1904 ? Date.new(1904, date_of_birth.month, date_of_birth.day) : date_of_birth + if date_of_birth.year < 1904 + Date.new(1904, date_of_birth.month, date_of_birth.day) + else + date_of_birth + end end def print_status(current, max, start_time = nil) if start_time.present? elapsed_seconds = Time.now - start_time - elements_per_minute = '[%.0f items/min] ' % [current / elapsed_seconds.to_f * 60] + elements_per_minute = "[%.0f items/min] " % [current / elapsed_seconds.to_f * 60] else - elements_per_minute = '' + elements_per_minute = "" end print "\r%9d / %d (%5.1f%%) %s" % [current, max, current / max.to_f * 100, elements_per_minute] @@ -782,7 +795,6 @@ class BulkImport::VBulletin < BulkImport::Base def mysql_query(sql) @client.query(sql) end - end BulkImport::VBulletin.new.run diff --git a/script/bulk_import/vbulletin5.rb b/script/bulk_import/vbulletin5.rb index 9be967c25c..e952ab5d76 100644 --- a/script/bulk_import/vbulletin5.rb +++ b/script/bulk_import/vbulletin5.rb @@ -5,47 +5,56 @@ require "cgi" require "set" require "mysql2" require "htmlentities" -require 'ruby-bbcode-to-md' -require 'find' +require "ruby-bbcode-to-md" +require "find" class BulkImport::VBulletin5 < BulkImport::Base - DB_PREFIX = "" SUSPENDED_TILL ||= Date.new(3000, 1, 1) - ATTACH_DIR ||= ENV['ATTACH_DIR'] || '/shared/import/data/attachments' - AVATAR_DIR ||= ENV['AVATAR_DIR'] || '/shared/import/data/customavatars' + ATTACH_DIR ||= ENV["ATTACH_DIR"] || "/shared/import/data/attachments" + AVATAR_DIR ||= ENV["AVATAR_DIR"] || "/shared/import/data/customavatars" ROOT_NODE = 2 def initialize super - host = ENV["DB_HOST"] || "localhost" + host = ENV["DB_HOST"] || "localhost" username = ENV["DB_USERNAME"] || "root" password = ENV["DB_PASSWORD"] database = ENV["DB_NAME"] || "vbulletin" - charset = ENV["DB_CHARSET"] || "utf8" + charset = ENV["DB_CHARSET"] || "utf8" @html_entities = HTMLEntities.new @encoding = CHARSET_MAP[charset] @bbcode_to_md = true - @client = Mysql2::Client.new( - host: host, - username: username, - password: password, - database: database, - encoding: charset, - reconnect: true - ) + @client = + Mysql2::Client.new( + host: host, + username: username, + password: password, + database: database, + encoding: charset, + reconnect: true, + ) @client.query_options.merge!(as: :array, cache_rows: false) # TODO: Add `LIMIT 1` to the below queries # ------ # be aware there may be other contenttypeid's in use, such as poll, link, video, etc. - @forum_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Forum'").to_a[0][0] - @channel_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Channel'").to_a[0][0] - @text_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Text'").to_a[0][0] + @forum_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Forum'").to_a[0][ + 0 + ] + @channel_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Channel'").to_a[ + 0 + ][ + 0 + ] + @text_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Text'").to_a[0][0] end def execute @@ -127,7 +136,7 @@ class BulkImport::VBulletin5 < BulkImport::Base date_of_birth: parse_birthday(row[3]), primary_group_id: group_id_from_imported_id(row[5]), admin: row[5] == 6, - moderator: row[5] == 7 + moderator: row[5] == 7, } u[:ip_address] = row[4][/\b(?:\d{1,3}\.){3}\d{1,3}\b/] if row[4].present? if row[7] @@ -153,7 +162,7 @@ class BulkImport::VBulletin5 < BulkImport::Base imported_id: row[0], imported_user_id: row[0], email: random_email, - created_at: Time.zone.at(row[2]) + created_at: Time.zone.at(row[2]), } end end @@ -203,10 +212,7 @@ class BulkImport::VBulletin5 < BulkImport::Base SQL create_group_users(group_users) do |row| - { - group_id: group_id_from_imported_id(row[0]), - user_id: user_id_from_imported_id(row[1]), - } + { group_id: group_id_from_imported_id(row[0]), user_id: user_id_from_imported_id(row[1]) } end # import secondary group memberships @@ -228,12 +234,7 @@ class BulkImport::VBulletin5 < BulkImport::Base end end - create_group_users(group_mapping) do |row| - { - group_id: row[0], - user_id: row[1] - } - end + create_group_users(group_mapping) { |row| { group_id: row[0], user_id: row[1] } } end def import_user_profiles @@ -249,7 +250,14 @@ class BulkImport::VBulletin5 < BulkImport::Base create_user_profiles(user_profiles) do |row| { user_id: user_id_from_imported_id(row[0]), - website: (URI.parse(row[1]).to_s rescue nil), + website: + ( + begin + URI.parse(row[1]).to_s + rescue StandardError + nil + end + ), views: row[2], } end @@ -258,7 +266,7 @@ class BulkImport::VBulletin5 < BulkImport::Base def import_categories puts "Importing categories..." - categories = mysql_query(<<-SQL + categories = mysql_query(<<-SQL).to_a SELECT nodeid AS forumid, title, description, displayorder, parentid, urlident FROM #{DB_PREFIX}node WHERE parentid = #{ROOT_NODE} @@ -269,11 +277,10 @@ class BulkImport::VBulletin5 < BulkImport::Base WHERE contenttypeid = #{@channel_typeid} AND nodeid > #{@last_imported_category_id} SQL - ).to_a return if categories.empty? - parent_categories = categories.select { |c| c[4] == ROOT_NODE } + parent_categories = categories.select { |c| c[4] == ROOT_NODE } children_categories = categories.select { |c| c[4] != ROOT_NODE } parent_category_ids = Set.new parent_categories.map { |c| c[0] } @@ -285,7 +292,7 @@ class BulkImport::VBulletin5 < BulkImport::Base name: normalize_text(row[1]), description: normalize_text(row[2]), position: row[3], - slug: row[5] + slug: row[5], } end @@ -297,7 +304,7 @@ class BulkImport::VBulletin5 < BulkImport::Base description: normalize_text(row[2]), position: row[3], parent_category_id: category_id_from_imported_id(row[4]), - slug: row[5] + slug: row[5], } end end @@ -428,7 +435,7 @@ class BulkImport::VBulletin5 < BulkImport::Base post_id: post_id, user_id: user_id, post_action_type_id: 2, - created_at: Time.zone.at(row[2]) + created_at: Time.zone.at(row[2]), } end end @@ -455,7 +462,6 @@ class BulkImport::VBulletin5 < BulkImport::Base user_id: user_id_from_imported_id(row[2]), created_at: Time.zone.at(row[3]), } - end end @@ -475,17 +481,18 @@ class BulkImport::VBulletin5 < BulkImport::Base users_added = Set.new create_topic_allowed_users(mysql_stream(allowed_users_sql)) do |row| - next unless topic_id = topic_id_from_imported_id(row[0] + PRIVATE_OFFSET) || topic_id_from_imported_id(row[2] + PRIVATE_OFFSET) + unless topic_id = + topic_id_from_imported_id(row[0] + PRIVATE_OFFSET) || + topic_id_from_imported_id(row[2] + PRIVATE_OFFSET) + next + end next unless user_id = user_id_from_imported_id(row[1]) next if users_added.add?([topic_id, user_id]).nil? added += 1 - { - topic_id: topic_id, - user_id: user_id, - } + { topic_id: topic_id, user_id: user_id } end - puts '', "Added #{added} topic allowed users records." + puts "", "Added #{added} topic allowed users records." end def import_private_first_posts @@ -543,7 +550,7 @@ class BulkImport::VBulletin5 < BulkImport::Base end def create_permalinks - puts '', 'creating permalinks...', '' + puts "", "creating permalinks...", "" # add permalink normalizations to site settings # EVERYTHING: /.*\/([\w-]+)$/\1 -- selects the last segment of the URL @@ -580,21 +587,23 @@ class BulkImport::VBulletin5 < BulkImport::Base return nil end - tmpfile = 'attach_' + row[6].to_s - filename = File.join('/tmp/', tmpfile) - File.open(filename, 'wb') { |f| f.write(row[5]) } + tmpfile = "attach_" + row[6].to_s + filename = File.join("/tmp/", tmpfile) + File.open(filename, "wb") { |f| f.write(row[5]) } filename end def find_upload(post, opts = {}) if opts[:node_id].present? - sql = "SELECT a.nodeid, n.parentid, a.filename, fd.userid, LENGTH(fd.filedata), filedata, fd.filedataid + sql = + "SELECT a.nodeid, n.parentid, a.filename, fd.userid, LENGTH(fd.filedata), filedata, fd.filedataid FROM #{DB_PREFIX}attach a LEFT JOIN #{DB_PREFIX}filedata fd ON fd.filedataid = a.filedataid LEFT JOIN #{DB_PREFIX}node n ON n.nodeid = a.nodeid WHERE a.nodeid = #{opts[:node_id]}" elsif opts[:attachment_id].present? - sql = "SELECT a.nodeid, n.parentid, a.filename, fd.userid, LENGTH(fd.filedata), filedata, fd.filedataid + sql = + "SELECT a.nodeid, n.parentid, a.filename, fd.userid, LENGTH(fd.filedata), filedata, fd.filedataid FROM #{DB_PREFIX}attachment a LEFT JOIN #{DB_PREFIX}filedata fd ON fd.filedataid = a.filedataid LEFT JOIN #{DB_PREFIX}node n ON n.nodeid = a.nodeid @@ -612,9 +621,9 @@ class BulkImport::VBulletin5 < BulkImport::Base user_id = row[3] db_filename = row[2] - filename = File.join(ATTACH_DIR, user_id.to_s.split('').join('/'), "#{attachment_id}.attach") + filename = File.join(ATTACH_DIR, user_id.to_s.split("").join("/"), "#{attachment_id}.attach") real_filename = db_filename - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + real_filename.prepend SecureRandom.hex if real_filename[0] == "." unless File.exist?(filename) filename = check_database_for_attachment(row) if filename.blank? @@ -637,7 +646,7 @@ class BulkImport::VBulletin5 < BulkImport::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." # add extensions to authorized setting #ext = mysql_query("SELECT GROUP_CONCAT(DISTINCT(extension)) exts FROM #{DB_PREFIX}filedata").first[0].split(',') @@ -655,8 +664,8 @@ class BulkImport::VBulletin5 < BulkImport::Base # new style matches the nodeid in the attach table # old style matches the filedataid in attach/filedata tables # if the site is very old, there may be multiple different attachment syntaxes used in posts - attachment_regex = /\[attach[^\]]*\].*\"data-attachmentid\":"?(\d+)"?,?.*\[\/attach\]/i - attachment_regex_oldstyle = /\[attach[^\]]*\](\d+)\[\/attach\]/i + attachment_regex = %r{\[attach[^\]]*\].*\"data-attachmentid\":"?(\d+)"?,?.*\[/attach\]}i + attachment_regex_oldstyle = %r{\[attach[^\]]*\](\d+)\[/attach\]}i Post.find_each do |post| current_count += 1 @@ -715,9 +724,18 @@ class BulkImport::VBulletin5 < BulkImport::Base def parse_birthday(birthday) return if birthday.blank? - date_of_birth = Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") rescue nil + date_of_birth = + begin + Date.strptime(birthday.gsub(/[^\d-]+/, ""), "%m-%d-%Y") + rescue StandardError + nil + end return if date_of_birth.nil? - date_of_birth.year < 1904 ? Date.new(1904, date_of_birth.month, date_of_birth.day) : date_of_birth + if date_of_birth.year < 1904 + Date.new(1904, date_of_birth.month, date_of_birth.day) + else + date_of_birth + end end def preprocess_raw(raw) @@ -726,33 +744,37 @@ class BulkImport::VBulletin5 < BulkImport::Base raw = raw.dup # [PLAINTEXT]...[/PLAINTEXT] - raw.gsub!(/\[\/?PLAINTEXT\]/i, "\n\n```\n\n") + raw.gsub!(%r{\[/?PLAINTEXT\]}i, "\n\n```\n\n") # [FONT=font]...[/FONT] raw.gsub!(/\[FONT=\w*\]/im, "") - raw.gsub!(/\[\/FONT\]/im, "") + raw.gsub!(%r{\[/FONT\]}im, "") # @[URL=][/URL] # [USER=id]username[/USER] # [MENTION=id]username[/MENTION] - raw.gsub!(/@\[URL=\"\S+\"\]([\w\s]+)\[\/URL\]/i) { "@#{$1.gsub(" ", "_")}" } - raw.gsub!(/\[USER=\"\d+\"\]([\S]+)\[\/USER\]/i) { "@#{$1.gsub(" ", "_")}" } - raw.gsub!(/\[MENTION=\d+\]([\S]+)\[\/MENTION\]/i) { "@#{$1.gsub(" ", "_")}" } + raw.gsub!(%r{@\[URL=\"\S+\"\]([\w\s]+)\[/URL\]}i) { "@#{$1.gsub(" ", "_")}" } + raw.gsub!(%r{\[USER=\"\d+\"\]([\S]+)\[/USER\]}i) { "@#{$1.gsub(" ", "_")}" } + raw.gsub!(%r{\[MENTION=\d+\]([\S]+)\[/MENTION\]}i) { "@#{$1.gsub(" ", "_")}" } # [IMG2=JSON]{..."src":""}[/IMG2] - raw.gsub!(/\[img2[^\]]*\].*\"src\":\"?([\w\\\/:\.\-;%]*)\"?}.*\[\/img2\]/i) { "\n#{CGI::unescape($1)}\n" } + raw.gsub!(/\[img2[^\]]*\].*\"src\":\"?([\w\\\/:\.\-;%]*)\"?}.*\[\/img2\]/i) do + "\n#{CGI.unescape($1)}\n" + end # [TABLE]...[/TABLE] raw.gsub!(/\[TABLE=\\"[\w:\-\s,]+\\"\]/i, "") - raw.gsub!(/\[\/TABLE\]/i, "") + raw.gsub!(%r{\[/TABLE\]}i, "") # [HR]...[/HR] - raw.gsub(/\[HR\]\s*\[\/HR\]/im, "---") + raw.gsub(%r{\[HR\]\s*\[/HR\]}im, "---") # [VIDEO=youtube_share;]...[/VIDEO] # [VIDEO=vimeo;]...[/VIDEO] - raw.gsub!(/\[VIDEO=YOUTUBE_SHARE;([^\]]+)\].*?\[\/VIDEO\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } - raw.gsub!(/\[VIDEO=VIMEO;([^\]]+)\].*?\[\/VIDEO\]/i) { "\nhttps://vimeo.com/#{$1}\n" } + raw.gsub!(%r{\[VIDEO=YOUTUBE_SHARE;([^\]]+)\].*?\[/VIDEO\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$1}\n" + end + raw.gsub!(%r{\[VIDEO=VIMEO;([^\]]+)\].*?\[/VIDEO\]}i) { "\nhttps://vimeo.com/#{$1}\n" } raw end @@ -760,9 +782,9 @@ class BulkImport::VBulletin5 < BulkImport::Base def print_status(current, max, start_time = nil) if start_time.present? elapsed_seconds = Time.now - start_time - elements_per_minute = '[%.0f items/min] ' % [current / elapsed_seconds.to_f * 60] + elements_per_minute = "[%.0f items/min] " % [current / elapsed_seconds.to_f * 60] else - elements_per_minute = '' + elements_per_minute = "" end print "\r%9d / %d (%5.1f%%) %s" % [current, max, current / max.to_f * 100, elements_per_minute] @@ -775,7 +797,6 @@ class BulkImport::VBulletin5 < BulkImport::Base def mysql_query(sql) @client.query(sql) end - end BulkImport::VBulletin5.new.run diff --git a/script/check_forking.rb b/script/check_forking.rb index ae8196af94..8679e6ec25 100644 --- a/script/check_forking.rb +++ b/script/check_forking.rb @@ -13,20 +13,22 @@ end Discourse.after_fork pretty -child = fork do - Discourse.after_fork - pretty - grand_child = fork do +child = + fork do Discourse.after_fork pretty - puts "try to exit" + grand_child = + fork do + Discourse.after_fork + pretty + puts "try to exit" + Process.kill "KILL", Process.pid + end + puts "before wait 2" + Process.wait grand_child + puts "after wait 2" Process.kill "KILL", Process.pid end - puts "before wait 2" - Process.wait grand_child - puts "after wait 2" - Process.kill "KILL", Process.pid -end puts "before wait 1" Process.wait child diff --git a/script/db_timestamps_mover.rb b/script/db_timestamps_mover.rb index 30701dd1f6..248f189387 100644 --- a/script/db_timestamps_mover.rb +++ b/script/db_timestamps_mover.rb @@ -12,20 +12,18 @@ class TimestampsUpdater def initialize(schema, ignore_tables) @schema = schema @ignore_tables = ignore_tables - @raw_connection = PG.connect( - host: ENV['DISCOURSE_DB_HOST'] || 'localhost', - port: ENV['DISCOURSE_DB_PORT'] || 5432, - dbname: ENV['DISCOURSE_DB_NAME'] || 'discourse_development', - user: ENV['DISCOURSE_DB_USERNAME'] || 'postgres', - password: ENV['DISCOURSE_DB_PASSWORD'] || '') + @raw_connection = + PG.connect( + host: ENV["DISCOURSE_DB_HOST"] || "localhost", + port: ENV["DISCOURSE_DB_PORT"] || 5432, + dbname: ENV["DISCOURSE_DB_NAME"] || "discourse_development", + user: ENV["DISCOURSE_DB_USERNAME"] || "postgres", + password: ENV["DISCOURSE_DB_PASSWORD"] || "", + ) end def move_by(days) - postgresql_date_types = [ - "timestamp without time zone", - "timestamp with time zone", - "date" - ] + postgresql_date_types = ["timestamp without time zone", "timestamp with time zone", "date"] postgresql_date_types.each do |data_type| columns = all_columns_of_type(data_type) @@ -118,11 +116,19 @@ class TimestampsUpdater end def is_i?(string) - true if Integer(string) rescue false + begin + true if Integer(string) + rescue StandardError + false + end end def is_date?(string) - true if Date.parse(string) rescue false + begin + true if Date.parse(string) + rescue StandardError + false + end end def create_updater diff --git a/script/diff_heaps.rb b/script/diff_heaps.rb index 5a0e91efd6..8433773d7f 100644 --- a/script/diff_heaps.rb +++ b/script/diff_heaps.rb @@ -6,8 +6,8 @@ # rbtrace -p 15193 -e 'Thread.new{require "objspace"; ObjectSpace.trace_object_allocations_start; GC.start(full_mark: true); ObjectSpace.dump_all(output: File.open("heap.json","w"))}.join' # # -require 'set' -require 'json' +require "set" +require "json" if ARGV.length != 2 puts "Usage: diff_heaps [ORIG.json] [AFTER.json]" @@ -16,26 +16,26 @@ end origs = Set.new -File.open(ARGV[0], "r").each_line do |line| - parsed = JSON.parse(line) - origs << parsed["address"] if parsed && parsed["address"] -end +File + .open(ARGV[0], "r") + .each_line do |line| + parsed = JSON.parse(line) + origs << parsed["address"] if parsed && parsed["address"] + end diff = [] -File.open(ARGV[1], "r").each_line do |line| - parsed = JSON.parse(line) - if parsed && parsed["address"] - diff << parsed unless origs.include? parsed["address"] +File + .open(ARGV[1], "r") + .each_line do |line| + parsed = JSON.parse(line) + if parsed && parsed["address"] + diff << parsed unless origs.include? parsed["address"] + end end -end -diff.group_by do |x| - [x["type"], x["file"], x["line"]] -end.map { |x, y| - [x, y.count] -}.sort { |a, b| - b[1] <=> a[1] -}.each { |x, y| - puts "Leaked #{y} #{x[0]} objects at: #{x[1]}:#{x[2]}" -} +diff + .group_by { |x| [x["type"], x["file"], x["line"]] } + .map { |x, y| [x, y.count] } + .sort { |a, b| b[1] <=> a[1] } + .each { |x, y| puts "Leaked #{y} #{x[0]} objects at: #{x[1]}:#{x[2]}" } diff --git a/script/docker_test.rb b/script/docker_test.rb index db73d531be..d51b63b5c5 100644 --- a/script/docker_test.rb +++ b/script/docker_test.rb @@ -19,11 +19,11 @@ def run_or_fail(command) exit 1 unless $?.exitstatus == 0 end -unless ENV['NO_UPDATE'] +unless ENV["NO_UPDATE"] run_or_fail("git reset --hard") run_or_fail("git fetch") - checkout = ENV['COMMIT_HASH'] || "FETCH_HEAD" + checkout = ENV["COMMIT_HASH"] || "FETCH_HEAD" run_or_fail("LEFTHOOK=0 git checkout #{checkout}") run_or_fail("bundle") @@ -31,7 +31,7 @@ end log("Running tests") -if ENV['RUN_SMOKE_TESTS'] +if ENV["RUN_SMOKE_TESTS"] run_or_fail("bundle exec rake smoke:test") else run_or_fail("bundle exec rake docker:test") diff --git a/script/i18n_lint.rb b/script/i18n_lint.rb index b00de75191..f3f96abf87 100755 --- a/script/i18n_lint.rb +++ b/script/i18n_lint.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'colored2' -require 'psych' +require "colored2" +require "psych" class I18nLinter def initialize(filenames_or_patterns) @@ -27,16 +27,22 @@ end class LocaleFileValidator ERROR_MESSAGES = { - invalid_relative_links: "The following keys have relative links, but do not start with %{base_url} or %{base_path}:", - invalid_relative_image_sources: "The following keys have relative image sources, but do not start with %{base_url} or %{base_path}:", - invalid_interpolation_key_format: "The following keys use {{key}} instead of %{key} for interpolation keys:", - wrong_pluralization_keys: "Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:", - invalid_one_keys: "The following keys contain the number 1 instead of the interpolation key %{count}:", - invalid_message_format_one_key: "The following keys use 'one {1 foo}' instead of the generic 'one {# foo}':", + invalid_relative_links: + "The following keys have relative links, but do not start with %{base_url} or %{base_path}:", + invalid_relative_image_sources: + "The following keys have relative image sources, but do not start with %{base_url} or %{base_path}:", + invalid_interpolation_key_format: + "The following keys use {{key}} instead of %{key} for interpolation keys:", + wrong_pluralization_keys: + "Pluralized strings must have only the sub-keys 'one' and 'other'.\nThe following keys have missing or additional keys:", + invalid_one_keys: + "The following keys contain the number 1 instead of the interpolation key %{count}:", + invalid_message_format_one_key: + "The following keys use 'one {1 foo}' instead of the generic 'one {# foo}':", } - PLURALIZATION_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'] - ENGLISH_KEYS = ['one', 'other'] + PLURALIZATION_KEYS = %w[zero one two few many other] + ENGLISH_KEYS = %w[one other] def initialize(filename) @filename = filename @@ -66,7 +72,7 @@ class LocaleFileValidator private - def each_translation(hash, parent_key = '', &block) + def each_translation(hash, parent_key = "", &block) hash.each do |key, value| current_key = parent_key.empty? ? key : "#{parent_key}.#{key}" @@ -85,13 +91,9 @@ class LocaleFileValidator @errors[:invalid_message_format_one_key] = [] each_translation(yaml) do |key, value| - if value.match?(/href\s*=\s*["']\/[^\/]|\]\(\/[^\/]/i) - @errors[:invalid_relative_links] << key - end + @errors[:invalid_relative_links] << key if value.match?(%r{href\s*=\s*["']/[^/]|\]\(/[^/]}i) - if value.match?(/src\s*=\s*["']\/[^\/]/i) - @errors[:invalid_relative_image_sources] << key - end + @errors[:invalid_relative_image_sources] << key if value.match?(%r{src\s*=\s*["']/[^/]}i) if value.match?(/{{.+?}}/) && !key.end_with?("_MF") @errors[:invalid_interpolation_key_format] << key @@ -103,7 +105,7 @@ class LocaleFileValidator end end - def each_pluralization(hash, parent_key = '', &block) + def each_pluralization(hash, parent_key = "", &block) hash.each do |key, value| if Hash === value current_key = parent_key.empty? ? key : "#{parent_key}.#{key}" @@ -124,8 +126,8 @@ class LocaleFileValidator @errors[:wrong_pluralization_keys] << key if hash.keys.sort != ENGLISH_KEYS - one_value = hash['one'] - if one_value && one_value.include?('1') && !one_value.match?(/%{count}|{{count}}/) + one_value = hash["one"] + if one_value && one_value.include?("1") && !one_value.match?(/%{count}|{{count}}/) @errors[:invalid_one_keys] << key end end diff --git a/script/import_scripts/answerbase.rb b/script/import_scripts/answerbase.rb index 1f436ac0ba..63de1d4a93 100644 --- a/script/import_scripts/answerbase.rb +++ b/script/import_scripts/answerbase.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require 'csv' -require 'reverse_markdown' -require_relative 'base' -require_relative 'base/generic_database' +require "csv" +require "reverse_markdown" +require_relative "base" +require_relative "base/generic_database" # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/answerbase.rb DIRNAME @@ -15,8 +15,10 @@ class ImportScripts::Answerbase < ImportScripts::Base ANSWER_IMAGE_DIRECTORY = "Answer Images" QUESTION_ATTACHMENT_DIRECTORY = "Question Attachments" QUESTION_IMAGE_DIRECTORY = "Question Images" - EMBEDDED_IMAGE_REGEX = /]*href="[^"]*relativeUrl=(?[^"\&]*)[^"]*"[^>]*>\s*]*>\s*<\/a>/i - QUESTION_LINK_REGEX = /]*?href="#{Regexp.escape(OLD_DOMAIN)}\/[^"]*?(?:q|questionid=)(?\d+)[^"]*?"[^>]*>(?.*?)<\/a>/i + EMBEDDED_IMAGE_REGEX = + %r{]*href="[^"]*relativeUrl=(?[^"\&]*)[^"]*"[^>]*>\s*]*>\s*}i + QUESTION_LINK_REGEX = + %r{]*?href="#{Regexp.escape(OLD_DOMAIN)}/[^"]*?(?:q|questionid=)(?\d+)[^"]*?"[^>]*>(?.*?)}i TOPIC_LINK_NORMALIZATION = '/.*?-(q\d+).*/\1' BATCH_SIZE = 1000 @@ -24,12 +26,13 @@ class ImportScripts::Answerbase < ImportScripts::Base super() @path = path - @db = ImportScripts::GenericDatabase.new( - @path, - batch_size: BATCH_SIZE, - recreate: true, - numeric_keys: true - ) + @db = + ImportScripts::GenericDatabase.new( + @path, + batch_size: BATCH_SIZE, + recreate: true, + numeric_keys: true, + ) end def execute @@ -47,11 +50,7 @@ class ImportScripts::Answerbase < ImportScripts::Base category_position = 0 csv_parse("categories") do |row| - @db.insert_category( - id: row[:id], - name: row[:name], - position: category_position += 1 - ) + @db.insert_category(id: row[:id], name: row[:name], position: category_position += 1) end csv_parse("users") do |row| @@ -62,7 +61,7 @@ class ImportScripts::Answerbase < ImportScripts::Base bio: row[:description], avatar_path: row[:profile_image], created_at: parse_date(row[:createtime]), - active: true + active: true, ) end @@ -74,8 +73,9 @@ class ImportScripts::Answerbase < ImportScripts::Base begin if row[:type] == "Question" - attachments = parse_filenames(row[:attachments], QUESTION_ATTACHMENT_DIRECTORY) + - parse_filenames(row[:images], QUESTION_IMAGE_DIRECTORY) + attachments = + parse_filenames(row[:attachments], QUESTION_ATTACHMENT_DIRECTORY) + + parse_filenames(row[:images], QUESTION_IMAGE_DIRECTORY) @db.insert_topic( id: row[:id], @@ -84,12 +84,13 @@ class ImportScripts::Answerbase < ImportScripts::Base category_id: row[:categorylist], user_id: user_id, created_at: created_at, - attachments: attachments + attachments: attachments, ) last_topic_id = row[:id] else - attachments = parse_filenames(row[:attachments], ANSWER_ATTACHMENT_DIRECTORY) + - parse_filenames(row[:images], ANSWER_IMAGE_DIRECTORY) + attachments = + parse_filenames(row[:attachments], ANSWER_ATTACHMENT_DIRECTORY) + + parse_filenames(row[:images], ANSWER_IMAGE_DIRECTORY) @db.insert_post( id: row[:id], @@ -97,10 +98,10 @@ class ImportScripts::Answerbase < ImportScripts::Base topic_id: last_topic_id, user_id: user_id, created_at: created_at, - attachments: attachments + attachments: attachments, ) end - rescue + rescue StandardError p row raise end @@ -110,9 +111,7 @@ class ImportScripts::Answerbase < ImportScripts::Base def parse_filenames(text, directory) return [] if text.blank? - text - .split(';') - .map { |filename| File.join(@path, directory, filename.strip) } + text.split(";").map { |filename| File.join(@path, directory, filename.strip) } end def parse_date(text) @@ -132,10 +131,10 @@ class ImportScripts::Answerbase < ImportScripts::Base create_categories(rows) do |row| { - id: row['id'], - name: row['name'], - description: row['description'], - position: row['position'] + id: row["id"], + name: row["name"], + description: row["description"], + position: row["position"], } end end @@ -153,19 +152,17 @@ class ImportScripts::Answerbase < ImportScripts::Base rows, last_id = @db.fetch_users(last_id) break if rows.empty? - next if all_records_exist?(:users, rows.map { |row| row['id'] }) + next if all_records_exist?(:users, rows.map { |row| row["id"] }) create_users(rows, total: total_count, offset: offset) do |row| { - id: row['id'], - email: row['email'], - username: row['username'], - bio_raw: row['bio'], - created_at: row['created_at'], - active: row['active'] == 1, - post_create_action: proc do |user| - create_avatar(user, row['avatar_path']) - end + id: row["id"], + email: row["email"], + username: row["username"], + bio_raw: row["bio"], + created_at: row["created_at"], + active: row["active"] == 1, + post_create_action: proc { |user| create_avatar(user, row["avatar_path"]) }, } end end @@ -191,24 +188,25 @@ class ImportScripts::Answerbase < ImportScripts::Base rows, last_id = @db.fetch_topics(last_id) break if rows.empty? - next if all_records_exist?(:posts, rows.map { |row| row['id'] }) + next if all_records_exist?(:posts, rows.map { |row| row["id"] }) create_posts(rows, total: total_count, offset: offset) do |row| - attachments = @db.fetch_topic_attachments(row['id']) if row['upload_count'] > 0 - user_id = user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id + attachments = @db.fetch_topic_attachments(row["id"]) if row["upload_count"] > 0 + user_id = user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id { - id: row['id'], - title: row['title'], - raw: raw_with_attachments(row['raw'].presence || row['title'], attachments, user_id), - category: category_id_from_imported_category_id(row['category_id']), + id: row["id"], + title: row["title"], + raw: raw_with_attachments(row["raw"].presence || row["title"], attachments, user_id), + category: category_id_from_imported_category_id(row["category_id"]), user_id: user_id, - created_at: row['created_at'], - closed: row['closed'] == 1, - post_create_action: proc do |post| - url = "q#{row['id']}" - Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) - end + created_at: row["created_at"], + closed: row["closed"] == 1, + post_create_action: + proc do |post| + url = "q#{row["id"]}" + Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) + end, } end end @@ -223,19 +221,19 @@ class ImportScripts::Answerbase < ImportScripts::Base rows, last_row_id = @db.fetch_posts(last_row_id) break if rows.empty? - next if all_records_exist?(:posts, rows.map { |row| row['id'] }) + next if all_records_exist?(:posts, rows.map { |row| row["id"] }) create_posts(rows, total: total_count, offset: offset) do |row| - topic = topic_lookup_from_imported_post_id(row['topic_id']) - attachments = @db.fetch_post_attachments(row['id']) if row['upload_count'] > 0 - user_id = user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id + topic = topic_lookup_from_imported_post_id(row["topic_id"]) + attachments = @db.fetch_post_attachments(row["id"]) if row["upload_count"] > 0 + user_id = user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id { - id: row['id'], - raw: raw_with_attachments(row['raw'], attachments, user_id), + id: row["id"], + raw: raw_with_attachments(row["raw"], attachments, user_id), user_id: user_id, topic_id: topic[:topic_id], - created_at: row['created_at'] + created_at: row["created_at"], } end end @@ -247,7 +245,7 @@ class ImportScripts::Answerbase < ImportScripts::Base raw = ReverseMarkdown.convert(raw) || "" attachments&.each do |attachment| - path = attachment['path'] + path = attachment["path"] next if embedded_paths.include?(path) if File.exist?(path) @@ -269,23 +267,24 @@ class ImportScripts::Answerbase < ImportScripts::Base paths = [] upload_ids = [] - raw = raw.gsub(EMBEDDED_IMAGE_REGEX) do - path = File.join(@path, Regexp.last_match['path']) - filename = File.basename(path) - path = find_image_path(filename) + raw = + raw.gsub(EMBEDDED_IMAGE_REGEX) do + path = File.join(@path, Regexp.last_match["path"]) + filename = File.basename(path) + path = find_image_path(filename) - if path - upload = @uploader.create_upload(user_id, path, filename) + if path + upload = @uploader.create_upload(user_id, path, filename) - if upload.present? && upload.persisted? - paths << path - upload_ids << upload.id - @uploader.html_for_upload(upload, filename) + if upload.present? && upload.persisted? + paths << path + upload_ids << upload.id + @uploader.html_for_upload(upload, filename) + end + else + STDERR.puts "Could not find file: #{path}" end - else - STDERR.puts "Could not find file: #{path}" end - end [raw, paths, upload_ids] end @@ -311,11 +310,11 @@ class ImportScripts::Answerbase < ImportScripts::Base def add_permalink_normalizations normalizations = SiteSetting.permalink_normalizations - normalizations = normalizations.blank? ? [] : normalizations.split('|') + normalizations = normalizations.blank? ? [] : normalizations.split("|") add_normalization(normalizations, TOPIC_LINK_NORMALIZATION) - SiteSetting.permalink_normalizations = normalizations.join('|') + SiteSetting.permalink_normalizations = normalizations.join("|") end def add_normalization(normalizations, normalization) @@ -327,11 +326,13 @@ class ImportScripts::Answerbase < ImportScripts::Base end def csv_parse(table_name) - CSV.foreach(File.join(@path, "#{table_name}.csv"), - headers: true, - header_converters: :symbol, - skip_blanks: true, - encoding: 'bom|utf-8') { |row| yield row } + CSV.foreach( + File.join(@path, "#{table_name}.csv"), + headers: true, + header_converters: :symbol, + skip_blanks: true, + encoding: "bom|utf-8", + ) { |row| yield row } end end diff --git a/script/import_scripts/answerhub.rb b/script/import_scripts/answerhub.rb index f2e05811ba..b4954dcccb 100644 --- a/script/import_scripts/answerhub.rb +++ b/script/import_scripts/answerhub.rb @@ -5,34 +5,29 @@ # Based on having access to a mysql dump. # Pass in the ENV variables listed below before running the script. -require_relative 'base' -require 'mysql2' -require 'open-uri' +require_relative "base" +require "mysql2" +require "open-uri" class ImportScripts::AnswerHub < ImportScripts::Base - - DB_NAME ||= ENV['DB_NAME'] || "answerhub" - DB_PASS ||= ENV['DB_PASS'] || "answerhub" - DB_USER ||= ENV['DB_USER'] || "answerhub" - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "network1" - BATCH_SIZE ||= ENV['BATCH_SIZE'].to_i || 1000 - ATTACHMENT_DIR = ENV['ATTACHMENT_DIR'] || '' - PROCESS_UPLOADS = ENV['PROCESS_UPLOADS'].to_i || 0 - ANSWERHUB_DOMAIN = ENV['ANSWERHUB_DOMAIN'] - AVATAR_DIR = ENV['AVATAR_DIR'] || "" - SITE_ID = ENV['SITE_ID'].to_i || 0 - CATEGORY_MAP_FROM = ENV['CATEGORY_MAP_FROM'].to_i || 0 - CATEGORY_MAP_TO = ENV['CATEGORY_MAP_TO'].to_i || 0 - SCRAPE_AVATARS = ENV['SCRAPE_AVATARS'].to_i || 0 + DB_NAME ||= ENV["DB_NAME"] || "answerhub" + DB_PASS ||= ENV["DB_PASS"] || "answerhub" + DB_USER ||= ENV["DB_USER"] || "answerhub" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "network1" + BATCH_SIZE ||= ENV["BATCH_SIZE"].to_i || 1000 + ATTACHMENT_DIR = ENV["ATTACHMENT_DIR"] || "" + PROCESS_UPLOADS = ENV["PROCESS_UPLOADS"].to_i || 0 + ANSWERHUB_DOMAIN = ENV["ANSWERHUB_DOMAIN"] + AVATAR_DIR = ENV["AVATAR_DIR"] || "" + SITE_ID = ENV["SITE_ID"].to_i || 0 + CATEGORY_MAP_FROM = ENV["CATEGORY_MAP_FROM"].to_i || 0 + CATEGORY_MAP_TO = ENV["CATEGORY_MAP_TO"].to_i || 0 + SCRAPE_AVATARS = ENV["SCRAPE_AVATARS"].to_i || 0 def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: DB_USER, - password: DB_PASS, - database: DB_NAME - ) + @client = + Mysql2::Client.new(host: "localhost", username: DB_USER, password: DB_PASS, database: DB_NAME) @skip_updates = true SiteSetting.tagging_enabled = true SiteSetting.max_tags_per_topic = 10 @@ -56,7 +51,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base end def import_users - puts '', "creating users" + puts "", "creating users" query = "SELECT count(*) count @@ -64,12 +59,13 @@ class ImportScripts::AnswerHub < ImportScripts::Base WHERE c_type = 'user' AND c_active = 1 AND c_system <> 1;" - total_count = @client.query(query).first['count'] + total_count = @client.query(query).first["count"] puts "Total count: #{total_count}" @last_user_id = -1 batches(BATCH_SIZE) do |offset| - query = "SELECT c_id, c_creation_date, c_name, c_primaryEmail, c_last_seen, c_description + query = + "SELECT c_id, c_creation_date, c_name, c_primaryEmail, c_last_seen, c_description FROM #{TABLE_PREFIX}_authoritables WHERE c_type = 'user' AND c_active = 1 @@ -79,17 +75,18 @@ class ImportScripts::AnswerHub < ImportScripts::Base results = @client.query(query) break if results.size < 1 - @last_user_id = results.to_a.last['c_id'] + @last_user_id = results.to_a.last["c_id"] create_users(results, total: total_count, offset: offset) do |user| # puts user['c_id'].to_s + ' ' + user['c_name'] - next if @lookup.user_id_from_imported_user_id(user['c_id']) - { id: user['c_id'], + next if @lookup.user_id_from_imported_user_id(user["c_id"]) + { + id: user["c_id"], email: "#{SecureRandom.hex}@invalid.invalid", - username: user['c_name'], - created_at: user['c_creation_date'], - bio_raw: user['c_description'], - last_seen_at: user['c_last_seen'], + username: user["c_name"], + created_at: user["c_creation_date"], + bio_raw: user["c_description"], + last_seen_at: user["c_last_seen"], } end end @@ -99,7 +96,8 @@ class ImportScripts::AnswerHub < ImportScripts::Base puts "", "importing categories..." # Import parent categories first - query = "SELECT c_id, c_name, c_plug, c_parent + query = + "SELECT c_id, c_name, c_plug, c_parent FROM containers WHERE c_type = 'space' AND c_active = 1 @@ -107,15 +105,12 @@ class ImportScripts::AnswerHub < ImportScripts::Base results = @client.query(query) create_categories(results) do |c| - { - id: c['c_id'], - name: c['c_name'], - parent_category_id: check_parent_id(c['c_parent']), - } + { id: c["c_id"], name: c["c_name"], parent_category_id: check_parent_id(c["c_parent"]) } end # Import sub-categories - query = "SELECT c_id, c_name, c_plug, c_parent + query = + "SELECT c_id, c_name, c_plug, c_parent FROM containers WHERE c_type = 'space' AND c_active = 1 @@ -125,9 +120,9 @@ class ImportScripts::AnswerHub < ImportScripts::Base create_categories(results) do |c| # puts c.inspect { - id: c['c_id'], - name: c['c_name'], - parent_category_id: category_id_from_imported_category_id(check_parent_id(c['c_parent'])), + id: c["c_id"], + name: c["c_name"], + parent_category_id: category_id_from_imported_category_id(check_parent_id(c["c_parent"])), } end end @@ -141,7 +136,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base WHERE c_visibility <> 'deleted' AND (c_type = 'question' OR c_type = 'kbentry');" - total_count = @client.query(count_query).first['count'] + total_count = @client.query(count_query).first["count"] @last_topic_id = -1 @@ -159,26 +154,25 @@ class ImportScripts::AnswerHub < ImportScripts::Base topics = @client.query(query) break if topics.size < 1 - @last_topic_id = topics.to_a.last['c_id'] + @last_topic_id = topics.to_a.last["c_id"] create_posts(topics, total: total_count, offset: offset) do |t| - user_id = user_id_from_imported_user_id(t['c_author']) || Discourse::SYSTEM_USER_ID - body = process_mentions(t['c_body']) - if PROCESS_UPLOADS == 1 - body = process_uploads(body, user_id) - end + user_id = user_id_from_imported_user_id(t["c_author"]) || Discourse::SYSTEM_USER_ID + body = process_mentions(t["c_body"]) + body = process_uploads(body, user_id) if PROCESS_UPLOADS == 1 markdown_body = HtmlToMarkdown.new(body).to_markdown { - id: t['c_id'], + id: t["c_id"], user_id: user_id, - title: t['c_title'], - category: category_id_from_imported_category_id(t['c_primaryContainer']), + title: t["c_title"], + category: category_id_from_imported_category_id(t["c_primaryContainer"]), raw: markdown_body, - created_at: t['c_creation_date'], - post_create_action: proc do |post| - tag_names = t['c_topic_names'].split(',') - DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) - end + created_at: t["c_creation_date"], + post_create_action: + proc do |post| + tag_names = t["c_topic_names"].split(",") + DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) + end, } end end @@ -194,7 +188,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base AND (c_type = 'answer' OR c_type = 'comment' OR c_type = 'kbentry');" - total_count = @client.query(count_query).first['count'] + total_count = @client.query(count_query).first["count"] @last_post_id = -1 @@ -210,49 +204,49 @@ class ImportScripts::AnswerHub < ImportScripts::Base ORDER BY c_id ASC LIMIT #{BATCH_SIZE};" posts = @client.query(query) - next if all_records_exist? :posts, posts.map { |p| p['c_id'] } + next if all_records_exist? :posts, posts.map { |p| p["c_id"] } break if posts.size < 1 - @last_post_id = posts.to_a.last['c_id'] + @last_post_id = posts.to_a.last["c_id"] create_posts(posts, total: total_count, offset: offset) do |p| - t = topic_lookup_from_imported_post_id(p['c_originalParent']) + t = topic_lookup_from_imported_post_id(p["c_originalParent"]) next unless t - reply_to_post_id = post_id_from_imported_post_id(p['c_parent']) + reply_to_post_id = post_id_from_imported_post_id(p["c_parent"]) reply_to_post = reply_to_post_id.present? ? Post.find(reply_to_post_id) : nil reply_to_post_number = reply_to_post.present? ? reply_to_post.post_number : nil - user_id = user_id_from_imported_user_id(p['c_author']) || Discourse::SYSTEM_USER_ID + user_id = user_id_from_imported_user_id(p["c_author"]) || Discourse::SYSTEM_USER_ID - body = process_mentions(p['c_body']) - if PROCESS_UPLOADS == 1 - body = process_uploads(body, user_id) - end + body = process_mentions(p["c_body"]) + body = process_uploads(body, user_id) if PROCESS_UPLOADS == 1 markdown_body = HtmlToMarkdown.new(body).to_markdown { - id: p['c_id'], + id: p["c_id"], user_id: user_id, topic_id: t[:topic_id], reply_to_post_number: reply_to_post_number, raw: markdown_body, - created_at: p['c_creation_date'], - post_create_action: proc do |post_info| - begin - if p['c_type'] == 'answer' && p['c_marked'] == 1 - post = Post.find(post_info[:id]) - if post - user_id = user_id_from_imported_user_id(p['c_author']) || Discourse::SYSTEM_USER_ID - current_user = User.find(user_id) - solved = DiscourseSolved.accept_answer!(post, current_user) - # puts "SOLVED: #{solved}" + created_at: p["c_creation_date"], + post_create_action: + proc do |post_info| + begin + if p["c_type"] == "answer" && p["c_marked"] == 1 + post = Post.find(post_info[:id]) + if post + user_id = + user_id_from_imported_user_id(p["c_author"]) || Discourse::SYSTEM_USER_ID + current_user = User.find(user_id) + solved = DiscourseSolved.accept_answer!(post, current_user) + # puts "SOLVED: #{solved}" + end end + rescue ActiveRecord::RecordInvalid + puts "SOLVED: Skipped post_id: #{post.id} because invalid" end - rescue ActiveRecord::RecordInvalid - puts "SOLVED: Skipped post_id: #{post.id} because invalid" - end - end + end, } end end @@ -269,11 +263,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base groups = @client.query(query) create_groups(groups) do |group| - { - id: group["c_id"], - name: group["c_name"], - visibility_level: 1 - } + { id: group["c_id"], name: group["c_name"], visibility_level: 1 } end end @@ -298,11 +288,16 @@ class ImportScripts::AnswerHub < ImportScripts::Base group_members.map groups.each do |group| - dgroup = find_group_by_import_id(group['c_id']) + dgroup = find_group_by_import_id(group["c_id"]) - next if dgroup.custom_fields['import_users_added'] + next if dgroup.custom_fields["import_users_added"] - group_member_ids = group_members.map { |m| user_id_from_imported_user_id(m["c_members"]) if m["c_groups"] == group['c_id'] }.compact + group_member_ids = + group_members + .map do |m| + user_id_from_imported_user_id(m["c_members"]) if m["c_groups"] == group["c_id"] + end + .compact # add members dgroup.bulk_add(group_member_ids) @@ -310,7 +305,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base # reload group dgroup.reload - dgroup.custom_fields['import_users_added'] = true + dgroup.custom_fields["import_users_added"] = true dgroup.save progress_count += 1 @@ -362,7 +357,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base avatars.each do |a| begin - user_id = user_id_from_imported_user_id(a['c_user']) + user_id = user_id_from_imported_user_id(a["c_user"]) user = User.find(user_id) if user filename = "avatar-#{user_id}.png" @@ -371,9 +366,11 @@ class ImportScripts::AnswerHub < ImportScripts::Base # Scrape Avatars - Avatars are saved in the db, but it might be easier to just scrape them if SCRAPE_AVATARS == 1 - File.open(path, 'wb') { |f| - f << open("https://#{ANSWERHUB_DOMAIN}/forums/users/#{a['c_user']}/photo/view.html?s=240").read - } + File.open(path, "wb") do |f| + f << open( + "https://#{ANSWERHUB_DOMAIN}/forums/users/#{a["c_user"]}/photo/view.html?s=240", + ).read + end end upload = @uploader.create_upload(user.id, path, filename) @@ -389,7 +386,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base end end rescue ActiveRecord::RecordNotFound - puts "Could not find User for user_id: #{a['c_user']}" + puts "Could not find User for user_id: #{a["c_user"]}" end end end @@ -438,9 +435,10 @@ class ImportScripts::AnswerHub < ImportScripts::Base raw = body.dup # https://example.forum.com/forums/users/1469/XYZ_Rob.html - raw.gsub!(/(https:\/\/example.forum.com\/forums\/users\/\d+\/[\w_%-.]*.html)/) do + raw.gsub!(%r{(https://example.forum.com/forums/users/\d+/[\w_%-.]*.html)}) do legacy_url = $1 - import_user_id = legacy_url.match(/https:\/\/example.forum.com\/forums\/users\/(\d+)\/[\w_%-.]*.html/).captures + import_user_id = + legacy_url.match(%r{https://example.forum.com/forums/users/(\d+)/[\w_%-.]*.html}).captures user = @lookup.find_user_by_import_id(import_user_id[0]) if user.present? @@ -453,9 +451,9 @@ class ImportScripts::AnswerHub < ImportScripts::Base end # /forums/users/395/petrocket.html - raw.gsub!(/(\/forums\/users\/\d+\/[\w_%-.]*.html)/) do + raw.gsub!(%r{(/forums/users/\d+/[\w_%-.]*.html)}) do legacy_url = $1 - import_user_id = legacy_url.match(/\/forums\/users\/(\d+)\/[\w_%-.]*.html/).captures + import_user_id = legacy_url.match(%r{/forums/users/(\d+)/[\w_%-.]*.html}).captures # puts raw user = @lookup.find_user_by_import_id(import_user_id[0]) @@ -472,7 +470,7 @@ class ImportScripts::AnswerHub < ImportScripts::Base end def create_permalinks - puts '', 'Creating redirects...', '' + puts "", "Creating redirects...", "" # https://example.forum.com/forums/questions/2005/missing-file.html Topic.find_each do |topic| @@ -480,8 +478,12 @@ class ImportScripts::AnswerHub < ImportScripts::Base if pcf && pcf["import_id"] id = pcf["import_id"] slug = Slug.for(topic.title) - Permalink.create(url: "questions/#{id}/#{slug}.html", topic_id: topic.id) rescue nil - print '.' + begin + Permalink.create(url: "questions/#{id}/#{slug}.html", topic_id: topic.id) + rescue StandardError + nil + end + print "." end end end @@ -496,7 +498,6 @@ class ImportScripts::AnswerHub < ImportScripts::Base return CATEGORY_MAP_TO if CATEGORY_MAP_FROM > 0 && id == CATEGORY_MAP_FROM id end - end ImportScripts::AnswerHub.new.perform diff --git a/script/import_scripts/askbot.rb b/script/import_scripts/askbot.rb index 7bc7866a0e..537c5ba71f 100644 --- a/script/import_scripts/askbot.rb +++ b/script/import_scripts/askbot.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'pg' +require "pg" class ImportScripts::MyAskBot < ImportScripts::Base # CHANGE THESE BEFORE RUNNING THE IMPORTER BATCH_SIZE = 1000 - OLD_SITE = "ask.cvxr.com" - DB_NAME = "cvxforum" - DB_USER = "cvxforum" - DB_PORT = 5432 - DB_HOST = "ask.cvxr.com" - DB_PASS = 'yeah, right' + OLD_SITE = "ask.cvxr.com" + DB_NAME = "cvxforum" + DB_USER = "cvxforum" + DB_PORT = 5432 + DB_HOST = "ask.cvxr.com" + DB_PASS = "yeah, right" # A list of categories to create. Any post with one of these tags will be # assigned to that category. Ties are broken by list order. - CATEGORIES = [ 'Nonconvex', 'TFOCS', 'MIDCP', 'FAQ' ] + CATEGORIES = %w[Nonconvex TFOCS MIDCP FAQ] def initialize super @@ -25,13 +25,8 @@ class ImportScripts::MyAskBot < ImportScripts::Base @thread_parents = {} @tagmap = [] @td = PG::TextDecoder::TimestampWithTimeZone.new - @client = PG.connect( - dbname: DB_NAME, - host: DB_HOST, - port: DB_PORT, - user: DB_USER, - password: DB_PASS - ) + @client = + PG.connect(dbname: DB_NAME, host: DB_HOST, port: DB_PORT, user: DB_USER, password: DB_PASS) end def execute @@ -55,18 +50,17 @@ class ImportScripts::MyAskBot < ImportScripts::Base def read_tags puts "", "reading thread tags..." - tag_count = @client.exec(<<-SQL + tag_count = @client.exec(<<-SQL)[0]["count"] SELECT COUNT(A.id) FROM askbot_thread_tags A JOIN tag B ON A.tag_id = B.id WHERE A.tag_id > 0 SQL - )[0]["count"] tags_done = 0 batches(BATCH_SIZE) do |offset| - tags = @client.exec(<<-SQL + tags = @client.exec(<<-SQL) SELECT A.thread_id, B.name FROM askbot_thread_tags A JOIN tag B @@ -75,7 +69,6 @@ class ImportScripts::MyAskBot < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if tags.ntuples() < 1 tags.each do |tag| tid = tag["thread_id"].to_i @@ -83,7 +76,7 @@ class ImportScripts::MyAskBot < ImportScripts::Base if @tagmap[tid] @tagmap[tid].push(tnm) else - @tagmap[tid] = [ tnm ] + @tagmap[tid] = [tnm] end tags_done += 1 print_status tags_done, tag_count @@ -94,21 +87,19 @@ class ImportScripts::MyAskBot < ImportScripts::Base def import_users puts "", "importing users" - total_count = @client.exec(<<-SQL + total_count = @client.exec(<<-SQL)[0]["count"] SELECT COUNT(id) FROM auth_user SQL - )[0]["count"] batches(BATCH_SIZE) do |offset| - users = @client.query(<<-SQL + users = @client.query(<<-SQL) SELECT id, username, email, is_staff, date_joined, last_seen, real_name, website, location, about FROM auth_user ORDER BY date_joined LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if users.ntuples() < 1 @@ -133,17 +124,16 @@ class ImportScripts::MyAskBot < ImportScripts::Base def import_posts puts "", "importing questions..." - post_count = @client.exec(<<-SQL + post_count = @client.exec(<<-SQL)[0]["count"] SELECT COUNT(A.id) FROM askbot_post A JOIN askbot_thread B ON A.thread_id = B.id WHERE NOT B.closed AND A.post_type='question' SQL - )[0]["count"] batches(BATCH_SIZE) do |offset| - posts = @client.exec(<<-SQL + posts = @client.exec(<<-SQL) SELECT A.id, A.author_id, A.added_at, A.text, A.thread_id, B.title FROM askbot_post A JOIN askbot_thread B @@ -153,7 +143,6 @@ class ImportScripts::MyAskBot < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if posts.ntuples() < 1 @@ -176,7 +165,11 @@ class ImportScripts::MyAskBot < ImportScripts::Base id: pid, title: post["title"], category: cat, - custom_fields: { import_id: pid, import_thread_id: tid, import_tags: tags }, + custom_fields: { + import_id: pid, + import_thread_id: tid, + import_tags: tags, + }, user_id: user_id_from_imported_user_id(post["author_id"]) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(post["added_at"])), raw: post["text"], @@ -188,17 +181,16 @@ class ImportScripts::MyAskBot < ImportScripts::Base def import_replies puts "", "importing answers and comments..." - post_count = @client.exec(<<-SQL + post_count = @client.exec(<<-SQL)[0]["count"] SELECT COUNT(A.id) FROM askbot_post A JOIN askbot_thread B ON A.thread_id = B.id WHERE NOT B.closed AND A.post_type<>'question' SQL - )[0]["count"] batches(BATCH_SIZE) do |offset| - posts = @client.exec(<<-SQL + posts = @client.exec(<<-SQL) SELECT A.id, A.author_id, A.added_at, A.text, A.thread_id, B.title FROM askbot_post A JOIN askbot_thread B @@ -208,7 +200,6 @@ class ImportScripts::MyAskBot < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if posts.ntuples() < 1 @@ -222,10 +213,12 @@ class ImportScripts::MyAskBot < ImportScripts::Base { id: pid, topic_id: parent[:topic_id], - custom_fields: { import_id: pid }, + custom_fields: { + import_id: pid, + }, user_id: user_id_from_imported_user_id(post["author_id"]) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(post["added_at"])), - raw: post["text"] + raw: post["text"], } end end @@ -240,32 +233,37 @@ class ImportScripts::MyAskBot < ImportScripts::Base # I am sure this is incomplete, but we didn't make heavy use of internal # links on our site. tmp = Regexp.quote("http://#{OLD_SITE}") - r1 = /"(#{tmp})?\/question\/(\d+)\/[a-zA-Z-]*\/?"/ - r2 = /\((#{tmp})?\/question\/(\d+)\/[a-zA-Z-]*\/?\)/ - r3 = /?/ + r1 = %r{"(#{tmp})?/question/(\d+)/[a-zA-Z-]*/?"} + r2 = %r{\((#{tmp})?/question/(\d+)/[a-zA-Z-]*/?\)} + r3 = %r{?} Post.find_each do |post| - raw = post.raw.gsub(r1) do - if topic = topic_lookup_from_imported_post_id($2) - "\"#{topic[:url]}\"" - else - $& + raw = + post + .raw + .gsub(r1) do + if topic = topic_lookup_from_imported_post_id($2) + "\"#{topic[:url]}\"" + else + $& + end + end + raw = + raw.gsub(r2) do + if topic = topic_lookup_from_imported_post_id($2) + "(#{topic[:url]})" + else + $& + end end - end - raw = raw.gsub(r2) do - if topic = topic_lookup_from_imported_post_id($2) - "(#{topic[:url]})" - else - $& + raw = + raw.gsub(r3) do + if topic = topic_lookup_from_imported_post_id($1) + trec = Topic.find_by(id: topic[:topic_id]) + "[#{trec.title}](#{topic[:url]})" + else + $& + end end - end - raw = raw.gsub(r3) do - if topic = topic_lookup_from_imported_post_id($1) - trec = Topic.find_by(id: topic[:topic_id]) - "[#{trec.title}](#{topic[:url]})" - else - $& - end - end if raw != post.raw post.raw = raw diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index 1345f206d7..7dbca7d353 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if ARGV.include?('bbcode-to-md') +if ARGV.include?("bbcode-to-md") # Replace (most) bbcode with markdown before creating posts. # This will dramatically clean up the final posts in Discourse. # @@ -10,17 +10,17 @@ if ARGV.include?('bbcode-to-md') # cd ruby-bbcode-to-md # gem build ruby-bbcode-to-md.gemspec # gem install ruby-bbcode-to-md-*.gem - require 'ruby-bbcode-to-md' + require "ruby-bbcode-to-md" end -require_relative '../../config/environment' -require_relative 'base/lookup_container' -require_relative 'base/uploader' +require_relative "../../config/environment" +require_relative "base/lookup_container" +require_relative "base/uploader" -module ImportScripts; end +module ImportScripts +end class ImportScripts::Base - def initialize preload_i18n @@ -62,15 +62,14 @@ class ImportScripts::Base end elapsed = Time.now - @start_times[:import] - puts '', '', 'Done (%02dh %02dmin %02dsec)' % [elapsed / 3600, elapsed / 60 % 60, elapsed % 60] - + puts "", "", "Done (%02dh %02dmin %02dsec)" % [elapsed / 3600, elapsed / 60 % 60, elapsed % 60] ensure reset_site_settings end def get_site_settings_for_import { - blocked_email_domains: '', + blocked_email_domains: "", min_topic_title_length: 1, min_post_length: 1, min_first_post_length: 1, @@ -78,21 +77,23 @@ class ImportScripts::Base min_personal_message_title_length: 1, allow_duplicate_topic_titles: true, allow_duplicate_topic_titles_category: false, - disable_emails: 'yes', - max_attachment_size_kb: 102400, - max_image_size_kb: 102400, - authorized_extensions: '*', + disable_emails: "yes", + max_attachment_size_kb: 102_400, + max_image_size_kb: 102_400, + authorized_extensions: "*", clean_up_inactive_users_after_days: 0, clean_up_unused_staged_users_after_days: 0, clean_up_uploads: false, - clean_orphan_uploads_grace_period_hours: 1800 + clean_orphan_uploads_grace_period_hours: 1800, } end def change_site_settings if SiteSetting.bootstrap_mode_enabled - SiteSetting.default_trust_level = TrustLevel[0] if SiteSetting.default_trust_level == TrustLevel[1] - SiteSetting.default_email_digest_frequency = 10080 if SiteSetting.default_email_digest_frequency == 1440 + SiteSetting.default_trust_level = TrustLevel[0] if SiteSetting.default_trust_level == + TrustLevel[1] + SiteSetting.default_email_digest_frequency = + 10_080 if SiteSetting.default_email_digest_frequency == 1440 SiteSetting.bootstrap_mode_enabled = false end @@ -131,7 +132,7 @@ class ImportScripts::Base raise NotImplementedError end - %i{ + %i[ add_category add_group add_post @@ -146,9 +147,7 @@ class ImportScripts::Base topic_lookup_from_imported_post_id user_already_imported? user_id_from_imported_user_id - }.each do |method_name| - delegate method_name, to: :@lookup - end + ].each { |method_name| delegate method_name, to: :@lookup } def create_admin(opts = {}) admin = User.new @@ -196,7 +195,11 @@ class ImportScripts::Base end end - print_status(created + skipped + failed + (opts[:offset] || 0), total, get_start_time("groups")) + print_status( + created + skipped + failed + (opts[:offset] || 0), + total, + get_start_time("groups"), + ) end [created, skipped] @@ -224,23 +227,22 @@ class ImportScripts::Base ActiveRecord::Base.transaction do begin connection = ActiveRecord::Base.connection.raw_connection - connection.exec('CREATE TEMP TABLE import_ids(val text PRIMARY KEY)') + connection.exec("CREATE TEMP TABLE import_ids(val text PRIMARY KEY)") - import_id_clause = import_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") + import_id_clause = + import_ids.map { |id| "('#{PG::Connection.escape_string(id.to_s)}')" }.join(",") connection.exec("INSERT INTO import_ids VALUES #{import_id_clause}") existing = "#{type.to_s.classify}CustomField".constantize - existing = existing.where(name: 'import_id') - .joins('JOIN import_ids ON val = value') - .count + existing = existing.where(name: "import_id").joins("JOIN import_ids ON val = value").count if existing == import_ids.length puts "Skipping #{import_ids.length} already imported #{type}" true end ensure - connection.exec('DROP TABLE import_ids') unless connection.nil? + connection.exec("DROP TABLE import_ids") unless connection.nil? end end end @@ -292,7 +294,11 @@ class ImportScripts::Base end end - print_status(created + skipped + failed + (opts[:offset] || 0), total, get_start_time("users")) + print_status( + created + skipped + failed + (opts[:offset] || 0), + total, + get_start_time("users"), + ) end [created, skipped] @@ -305,7 +311,9 @@ class ImportScripts::Base post_create_action = opts.delete(:post_create_action) existing = find_existing_user(opts[:email], opts[:username]) - return existing if existing && (merge || existing.custom_fields["import_id"].to_s == import_id.to_s) + if existing && (merge || existing.custom_fields["import_id"].to_s == import_id.to_s) + return existing + end bio_raw = opts.delete(:bio_raw) website = opts.delete(:website) @@ -316,8 +324,11 @@ class ImportScripts::Base original_name = opts[:name] original_email = opts[:email] = opts[:email].downcase - if !UsernameValidator.new(opts[:username]).valid_format? || !User.username_available?(opts[:username]) - opts[:username] = UserNameSuggester.suggest(opts[:username].presence || opts[:name].presence || opts[:email]) + if !UsernameValidator.new(opts[:username]).valid_format? || + !User.username_available?(opts[:username]) + opts[:username] = UserNameSuggester.suggest( + opts[:username].presence || opts[:name].presence || opts[:email], + ) end if !EmailAddressValidator.valid_value?(opts[:email]) @@ -339,7 +350,8 @@ class ImportScripts::Base u = User.new(opts) (opts[:custom_fields] || {}).each { |k, v| u.custom_fields[k] = v } u.custom_fields["import_id"] = import_id - u.custom_fields["import_username"] = original_username if original_username.present? && original_username != opts[:username] + u.custom_fields["import_username"] = original_username if original_username.present? && + original_username != opts[:username] u.custom_fields["import_avatar_url"] = avatar_url if avatar_url.present? u.custom_fields["import_pass"] = opts[:password] if opts[:password].present? u.custom_fields["import_email"] = original_email if original_email != opts[:email] @@ -359,9 +371,7 @@ class ImportScripts::Base end end - if opts[:active] && opts[:password].present? - u.activate - end + u.activate if opts[:active] && opts[:password].present? rescue => e # try based on email if e.try(:record).try(:errors).try(:messages).try(:[], :primary_email).present? @@ -377,7 +387,7 @@ class ImportScripts::Base end end - if u.custom_fields['import_email'] + if u.custom_fields["import_email"] u.suspended_at = Time.zone.at(Time.now) u.suspended_till = 200.years.from_now u.save! @@ -388,11 +398,15 @@ class ImportScripts::Base user_option.email_messages_level = UserOption.email_level_types[:never] user_option.save! if u.save - StaffActionLogger.new(Discourse.system_user).log_user_suspend(u, 'Invalid email address on import') + StaffActionLogger.new(Discourse.system_user).log_user_suspend( + u, + "Invalid email address on import", + ) else - Rails.logger.error("Failed to suspend user #{u.username}. #{u.errors.try(:full_messages).try(:inspect)}") + Rails.logger.error( + "Failed to suspend user #{u.username}. #{u.errors.try(:full_messages).try(:inspect)}", + ) end - end post_create_action.try(:call, u) if u.persisted? @@ -402,7 +416,8 @@ class ImportScripts::Base def find_existing_user(email, username) # Force the use of the index on the 'user_emails' table - UserEmail.where("lower(email) = ?", email.downcase).first&.user || User.where(username: username).first + UserEmail.where("lower(email) = ?", email.downcase).first&.user || + User.where(username: username).first end def created_category(category) @@ -435,7 +450,8 @@ class ImportScripts::Base # make sure categories don't go more than 2 levels deep if params[:parent_category_id] top = Category.find_by_id(params[:parent_category_id]) - top = top.parent_category while (top&.height_of_ancestors || -1) + 1 >= SiteSetting.max_category_nesting + top = top.parent_category while (top&.height_of_ancestors || -1) + 1 >= + SiteSetting.max_category_nesting params[:parent_category_id] = top.id if top end @@ -471,15 +487,16 @@ class ImportScripts::Base post_create_action = opts.delete(:post_create_action) - new_category = Category.new( - name: opts[:name], - user_id: opts[:user_id] || opts[:user].try(:id) || Discourse::SYSTEM_USER_ID, - position: opts[:position], - parent_category_id: opts[:parent_category_id], - color: opts[:color] || category_color(opts[:parent_category_id]), - text_color: opts[:text_color] || "FFF", - read_restricted: opts[:read_restricted] || false, - ) + new_category = + Category.new( + name: opts[:name], + user_id: opts[:user_id] || opts[:user].try(:id) || Discourse::SYSTEM_USER_ID, + position: opts[:position], + parent_category_id: opts[:parent_category_id], + color: opts[:color] || category_color(opts[:parent_category_id]), + text_color: opts[:text_color] || "FFF", + read_restricted: opts[:read_restricted] || false, + ) new_category.custom_fields["import_id"] = import_id if import_id new_category.save! @@ -498,10 +515,16 @@ class ImportScripts::Base end def category_color(parent_category_id) - @category_colors ||= SiteSetting.category_colors.split('|') + @category_colors ||= SiteSetting.category_colors.split("|") index = @next_category_color_index[parent_category_id].presence || 0 - @next_category_color_index[parent_category_id] = index + 1 >= @category_colors.count ? 0 : index + 1 + @next_category_color_index[parent_category_id] = ( + if index + 1 >= @category_colors.count + 0 + else + index + 1 + end + ) @category_colors[index] end @@ -571,7 +594,7 @@ class ImportScripts::Base opts = opts.merge(skip_validations: true) opts[:import_mode] = true opts[:custom_fields] ||= {} - opts[:custom_fields]['import_id'] = import_id + opts[:custom_fields]["import_id"] = import_id unless opts[:topic_id] opts[:meta_data] = meta_data = {} @@ -582,7 +605,11 @@ class ImportScripts::Base opts[:guardian] = STAFF_GUARDIAN if @bbcode_to_md - opts[:raw] = opts[:raw].bbcode_to_md(false, {}, :disable, :quote) rescue opts[:raw] + opts[:raw] = begin + opts[:raw].bbcode_to_md(false, {}, :disable, :quote) + rescue StandardError + opts[:raw] + end end post_creator = PostCreator.new(user, opts) @@ -628,7 +655,7 @@ class ImportScripts::Base created += 1 if manager.errors.none? skipped += 1 if manager.errors.any? - rescue + rescue StandardError skipped += 1 end end @@ -671,14 +698,14 @@ class ImportScripts::Base def close_inactive_topics(opts = {}) num_days = opts[:days] || 30 - puts '', "Closing topics that have been inactive for more than #{num_days} days." + puts "", "Closing topics that have been inactive for more than #{num_days} days." - query = Topic.where('last_posted_at < ?', num_days.days.ago).where(closed: false) + query = Topic.where("last_posted_at < ?", num_days.days.ago).where(closed: false) total_count = query.count closed_count = 0 query.find_each do |topic| - topic.update_status('closed', true, Discourse.system_user) + topic.update_status("closed", true, Discourse.system_user) closed_count += 1 print_status(closed_count, total_count, get_start_time("close_inactive_topics")) end @@ -790,7 +817,9 @@ class ImportScripts::Base puts "", "Updating user digest_attempted_at..." - DB.exec("UPDATE user_stats SET digest_attempted_at = now() - random() * interval '1 week' WHERE digest_attempted_at IS NULL") + DB.exec( + "UPDATE user_stats SET digest_attempted_at = now() - random() * interval '1 week' WHERE digest_attempted_at IS NULL", + ) end # scripts that are able to import last_seen_at from the source data should override this method @@ -854,13 +883,15 @@ class ImportScripts::Base count = 0 total = User.count - User.includes(:user_stat).find_each do |user| - begin - user.update_columns(trust_level: 0) if user.trust_level > 0 && user.post_count == 0 - rescue Discourse::InvalidAccess + User + .includes(:user_stat) + .find_each do |user| + begin + user.update_columns(trust_level: 0) if user.trust_level > 0 && user.post_count == 0 + rescue Discourse::InvalidAccess + end + print_status(count += 1, total, get_start_time("update_tl0")) end - print_status(count += 1, total, get_start_time("update_tl0")) - end end def update_user_signup_date_based_on_first_post @@ -870,7 +901,7 @@ class ImportScripts::Base total = User.count User.find_each do |user| - if first = user.posts.order('created_at ASC').first + if first = user.posts.order("created_at ASC").first user.created_at = first.created_at user.save! end @@ -893,16 +924,16 @@ class ImportScripts::Base def print_status(current, max, start_time = nil) if start_time.present? elapsed_seconds = Time.now - start_time - elements_per_minute = '[%.0f items/min] ' % [current / elapsed_seconds.to_f * 60] + elements_per_minute = "[%.0f items/min] " % [current / elapsed_seconds.to_f * 60] else - elements_per_minute = '' + elements_per_minute = "" end print "\r%9d / %d (%5.1f%%) %s" % [current, max, current / max.to_f * 100, elements_per_minute] end def print_spinner - @spinner_chars ||= %w{ | / - \\ } + @spinner_chars ||= %w[| / - \\] @spinner_chars.push @spinner_chars.shift print "\b#{@spinner_chars[0]}" end diff --git a/script/import_scripts/base/csv_helper.rb b/script/import_scripts/base/csv_helper.rb index 7f7becbd3d..3f211ea366 100644 --- a/script/import_scripts/base/csv_helper.rb +++ b/script/import_scripts/base/csv_helper.rb @@ -13,65 +13,69 @@ module ImportScripts def initialize(cols) cols.each_with_index do |col, idx| - self.class.public_send(:define_method, col.downcase.gsub(/[\W]/, '_').squeeze('_')) do - @row[idx] - end + self + .class + .public_send(:define_method, col.downcase.gsub(/[\W]/, "_").squeeze("_")) { @row[idx] } end end end - def csv_parse(filename, col_sep = ',') + def csv_parse(filename, col_sep = ",") first = true row = nil current_row = +"" double_quote_count = 0 - File.open(filename).each_line do |line| + File + .open(filename) + .each_line do |line| + line.strip! - line.strip! + current_row << "\n" unless current_row.empty? + current_row << line - current_row << "\n" unless current_row.empty? - current_row << line + double_quote_count += line.scan('"').count - double_quote_count += line.scan('"').count + next if double_quote_count % 2 == 1 # this row continues on a new line. don't parse until we have the whole row. - next if double_quote_count % 2 == 1 # this row continues on a new line. don't parse until we have the whole row. + raw = + begin + CSV.parse(current_row, col_sep: col_sep) + rescue CSV::MalformedCSVError => e + puts e.message + puts "*" * 100 + puts "Bad row skipped, line is: #{line}" + puts + puts current_row + puts + puts "double quote count is : #{double_quote_count}" + puts "*" * 100 - raw = begin - CSV.parse(current_row, col_sep: col_sep) - rescue CSV::MalformedCSVError => e - puts e.message - puts "*" * 100 - puts "Bad row skipped, line is: #{line}" - puts - puts current_row - puts - puts "double quote count is : #{double_quote_count}" - puts "*" * 100 + current_row = "" + double_quote_count = 0 - current_row = "" - double_quote_count = 0 + next + end[ + 0 + ] - next - end[0] + if first + row = RowResolver.create(raw) - if first - row = RowResolver.create(raw) + current_row = "" + double_quote_count = 0 + first = false + next + end + + row.load(raw) + + yield row current_row = "" double_quote_count = 0 - first = false - next end - - row.load(raw) - - yield row - - current_row = "" - double_quote_count = 0 - end end end end diff --git a/script/import_scripts/base/generic_database.rb b/script/import_scripts/base/generic_database.rb index 28bfd8ee3b..dafea94199 100644 --- a/script/import_scripts/base/generic_database.rb +++ b/script/import_scripts/base/generic_database.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'sqlite3' +require "sqlite3" module ImportScripts class GenericDatabase @@ -80,24 +80,20 @@ module ImportScripts VALUES (:id, :raw, :topic_id, :user_id, :created_at, :reply_to_post_id, :url, :upload_count) SQL - attachments&.each do |attachment| - @db.execute(<<-SQL, post_id: post[:id], path: attachment) + attachments&.each { |attachment| @db.execute(<<-SQL, post_id: post[:id], path: attachment) } INSERT OR REPLACE INTO post_upload (post_id, path) VALUES (:post_id, :path) SQL - end - like_user_ids&.each do |user_id| - @db.execute(<<-SQL, post_id: post[:id], user_id: user_id) + like_user_ids&.each { |user_id| @db.execute(<<-SQL, post_id: post[:id], user_id: user_id) } INSERT OR REPLACE INTO like (post_id, user_id) VALUES (:post_id, :user_id) SQL - end end end def sort_posts_by_created_at - @db.execute 'DELETE FROM post_order' + @db.execute "DELETE FROM post_order" @db.execute <<-SQL INSERT INTO post_order (post_id) @@ -146,7 +142,7 @@ module ImportScripts LIMIT #{@batch_size} SQL - add_last_column_value(rows, 'id') + add_last_column_value(rows, "id") end def get_user_id(username) @@ -173,7 +169,7 @@ module ImportScripts LIMIT #{@batch_size} SQL - add_last_column_value(rows, 'id') + add_last_column_value(rows, "id") end def fetch_topic_attachments(topic_id) @@ -200,7 +196,7 @@ module ImportScripts LIMIT #{@batch_size} SQL - add_last_column_value(rows, 'rowid') + add_last_column_value(rows, "rowid") end def fetch_sorted_posts(last_row_id) @@ -213,7 +209,7 @@ module ImportScripts LIMIT #{@batch_size} SQL - add_last_column_value(rows, 'rowid') + add_last_column_value(rows, "rowid") end def fetch_post_attachments(post_id) @@ -240,7 +236,7 @@ module ImportScripts LIMIT #{@batch_size} SQL - add_last_column_value(rows, 'rowid') + add_last_column_value(rows, "rowid") end def execute_sql(sql) @@ -254,12 +250,12 @@ module ImportScripts private def configure_database - @db.execute 'PRAGMA journal_mode = OFF' - @db.execute 'PRAGMA locking_mode = EXCLUSIVE' + @db.execute "PRAGMA journal_mode = OFF" + @db.execute "PRAGMA locking_mode = EXCLUSIVE" end def key_data_type - @numeric_keys ? 'INTEGER' : 'TEXT' + @numeric_keys ? "INTEGER" : "TEXT" end def create_category_table @@ -299,7 +295,7 @@ module ImportScripts ) SQL - @db.execute 'CREATE INDEX IF NOT EXISTS user_by_username ON user (username)' + @db.execute "CREATE INDEX IF NOT EXISTS user_by_username ON user (username)" end def create_topic_table @@ -317,7 +313,7 @@ module ImportScripts ) SQL - @db.execute 'CREATE INDEX IF NOT EXISTS topic_by_user_id ON topic (user_id)' + @db.execute "CREATE INDEX IF NOT EXISTS topic_by_user_id ON topic (user_id)" @db.execute <<-SQL CREATE TABLE IF NOT EXISTS topic_upload ( @@ -326,7 +322,7 @@ module ImportScripts ) SQL - @db.execute 'CREATE UNIQUE INDEX IF NOT EXISTS topic_upload_unique ON topic_upload(topic_id, path)' + @db.execute "CREATE UNIQUE INDEX IF NOT EXISTS topic_upload_unique ON topic_upload(topic_id, path)" end def create_post_table @@ -343,7 +339,7 @@ module ImportScripts ) SQL - @db.execute 'CREATE INDEX IF NOT EXISTS post_by_user_id ON post (user_id)' + @db.execute "CREATE INDEX IF NOT EXISTS post_by_user_id ON post (user_id)" @db.execute <<-SQL CREATE TABLE IF NOT EXISTS post_order ( @@ -358,7 +354,7 @@ module ImportScripts ) SQL - @db.execute 'CREATE UNIQUE INDEX IF NOT EXISTS post_upload_unique ON post_upload(post_id, path)' + @db.execute "CREATE UNIQUE INDEX IF NOT EXISTS post_upload_unique ON post_upload(post_id, path)" end def prepare(hash) diff --git a/script/import_scripts/base/lookup_container.rb b/script/import_scripts/base/lookup_container.rb index 30ac96203d..4147daf43f 100644 --- a/script/import_scripts/base/lookup_container.rb +++ b/script/import_scripts/base/lookup_container.rb @@ -3,27 +3,26 @@ module ImportScripts class LookupContainer def initialize - puts 'Loading existing groups...' - @groups = GroupCustomField.where(name: 'import_id').pluck(:value, :group_id).to_h + puts "Loading existing groups..." + @groups = GroupCustomField.where(name: "import_id").pluck(:value, :group_id).to_h - puts 'Loading existing users...' - @users = UserCustomField.where(name: 'import_id').pluck(:value, :user_id).to_h + puts "Loading existing users..." + @users = UserCustomField.where(name: "import_id").pluck(:value, :user_id).to_h - puts 'Loading existing categories...' - @categories = CategoryCustomField.where(name: 'import_id').pluck(:value, :category_id).to_h + puts "Loading existing categories..." + @categories = CategoryCustomField.where(name: "import_id").pluck(:value, :category_id).to_h - puts 'Loading existing posts...' - @posts = PostCustomField.where(name: 'import_id').pluck(:value, :post_id).to_h + puts "Loading existing posts..." + @posts = PostCustomField.where(name: "import_id").pluck(:value, :post_id).to_h - puts 'Loading existing topics...' + puts "Loading existing topics..." @topics = {} - Post.joins(:topic).pluck('posts.id, posts.topic_id, posts.post_number, topics.slug').each do |p| - @topics[p[0]] = { - topic_id: p[1], - post_number: p[2], - url: Post.url(p[3], p[1], p[2]) - } - end + Post + .joins(:topic) + .pluck("posts.id, posts.topic_id, posts.post_number, topics.slug") + .each do |p| + @topics[p[0]] = { topic_id: p[1], post_number: p[2], url: Post.url(p[3], p[1], p[2]) } + end end # Get the Discourse Post id based on the id of the source record @@ -44,7 +43,7 @@ module ImportScripts # Get the Discourse Group based on the id of the source group def find_group_by_import_id(import_id) - GroupCustomField.where(name: 'import_id', value: import_id.to_s).first.try(:group) + GroupCustomField.where(name: "import_id", value: import_id.to_s).first.try(:group) end # Get the Discourse User id based on the id of the source user @@ -54,7 +53,7 @@ module ImportScripts # Get the Discourse User based on the id of the source user def find_user_by_import_id(import_id) - UserCustomField.where(name: 'import_id', value: import_id.to_s).first.try(:user) + UserCustomField.where(name: "import_id", value: import_id.to_s).first.try(:user) end def find_username_by_import_id(import_id) @@ -84,11 +83,7 @@ module ImportScripts end def add_topic(post) - @topics[post.id] = { - post_number: post.post_number, - topic_id: post.topic_id, - url: post.url, - } + @topics[post.id] = { post_number: post.post_number, topic_id: post.topic_id, url: post.url } end def user_already_imported?(import_id) @@ -98,6 +93,5 @@ module ImportScripts def post_already_imported?(import_id) @posts.has_key?(import_id) || @posts.has_key?(import_id.to_s) end - end end diff --git a/script/import_scripts/base/uploader.rb b/script/import_scripts/base/uploader.rb index 45404bba21..4342d32887 100644 --- a/script/import_scripts/base/uploader.rb +++ b/script/import_scripts/base/uploader.rb @@ -13,8 +13,16 @@ module ImportScripts STDERR.puts "Failed to create upload: #{e}" nil ensure - tmp.close rescue nil - tmp.unlink rescue nil + begin + tmp.close + rescue StandardError + nil + end + begin + tmp.unlink + rescue StandardError + nil + end end def create_avatar(user, avatar_path) @@ -30,7 +38,7 @@ module ImportScripts STDERR.puts "Failed to upload avatar for user #{user.username}: #{avatar_path}" STDERR.puts upload.errors.inspect if upload end - rescue + rescue StandardError STDERR.puts "Failed to create avatar for user #{user.username}: #{avatar_path}" ensure tempfile.close! if tempfile @@ -52,11 +60,9 @@ module ImportScripts def copy_to_tempfile(source_path) extension = File.extname(source_path) - tmp = Tempfile.new(['discourse-upload', extension]) + tmp = Tempfile.new(["discourse-upload", extension]) - File.open(source_path) do |source_stream| - IO.copy_stream(source_stream, tmp) - end + File.open(source_path) { |source_stream| IO.copy_stream(source_stream, tmp) } tmp.rewind tmp diff --git a/script/import_scripts/bbpress.rb b/script/import_scripts/bbpress.rb index 2cd0698d12..4864ba6b94 100644 --- a/script/import_scripts/bbpress.rb +++ b/script/import_scripts/bbpress.rb @@ -1,29 +1,29 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Bbpress < ImportScripts::Base - - BB_PRESS_HOST ||= ENV['BBPRESS_HOST'] || "localhost" - BB_PRESS_DB ||= ENV['BBPRESS_DB'] || "bbpress" - BATCH_SIZE ||= 1000 - BB_PRESS_PW ||= ENV['BBPRESS_PW'] || "" - BB_PRESS_USER ||= ENV['BBPRESS_USER'] || "root" - BB_PRESS_PREFIX ||= ENV['BBPRESS_PREFIX'] || "wp_" - BB_PRESS_ATTACHMENTS_DIR ||= ENV['BBPRESS_ATTACHMENTS_DIR'] || "/path/to/attachments" + BB_PRESS_HOST ||= ENV["BBPRESS_HOST"] || "localhost" + BB_PRESS_DB ||= ENV["BBPRESS_DB"] || "bbpress" + BATCH_SIZE ||= 1000 + BB_PRESS_PW ||= ENV["BBPRESS_PW"] || "" + BB_PRESS_USER ||= ENV["BBPRESS_USER"] || "root" + BB_PRESS_PREFIX ||= ENV["BBPRESS_PREFIX"] || "wp_" + BB_PRESS_ATTACHMENTS_DIR ||= ENV["BBPRESS_ATTACHMENTS_DIR"] || "/path/to/attachments" def initialize super @he = HTMLEntities.new - @client = Mysql2::Client.new( - host: BB_PRESS_HOST, - username: BB_PRESS_USER, - database: BB_PRESS_DB, - password: BB_PRESS_PW, - ) + @client = + Mysql2::Client.new( + host: BB_PRESS_HOST, + username: BB_PRESS_USER, + database: BB_PRESS_DB, + password: BB_PRESS_PW, + ) end def execute @@ -40,17 +40,16 @@ class ImportScripts::Bbpress < ImportScripts::Base puts "", "importing users..." last_user_id = -1 - total_users = bbpress_query(<<-SQL + total_users = bbpress_query(<<-SQL).first["cnt"] SELECT COUNT(DISTINCT(u.id)) AS cnt FROM #{BB_PRESS_PREFIX}users u LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id WHERE p.post_type IN ('forum', 'reply', 'topic') AND user_email LIKE '%@%' SQL - ).first["cnt"] batches(BATCH_SIZE) do |offset| - users = bbpress_query(<<-SQL + users = bbpress_query(<<-SQL).to_a SELECT u.id, user_nicename, display_name, user_email, user_registered, user_url, user_pass FROM #{BB_PRESS_PREFIX}users u LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id @@ -61,7 +60,6 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY u.id LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -73,22 +71,20 @@ class ImportScripts::Bbpress < ImportScripts::Base user_ids_sql = user_ids.join(",") users_description = {} - bbpress_query(<<-SQL + bbpress_query(<<-SQL).each { |um| users_description[um["user_id"]] = um["description"] } SELECT user_id, meta_value description FROM #{BB_PRESS_PREFIX}usermeta WHERE user_id IN (#{user_ids_sql}) AND meta_key = 'description' SQL - ).each { |um| users_description[um["user_id"]] = um["description"] } users_last_activity = {} - bbpress_query(<<-SQL + bbpress_query(<<-SQL).each { |um| users_last_activity[um["user_id"]] = um["last_activity"] } SELECT user_id, meta_value last_activity FROM #{BB_PRESS_PREFIX}usermeta WHERE user_id IN (#{user_ids_sql}) AND meta_key = 'last_activity' SQL - ).each { |um| users_last_activity[um["user_id"]] = um["last_activity"] } create_users(users, total: total_users, offset: offset) do |u| { @@ -96,7 +92,7 @@ class ImportScripts::Bbpress < ImportScripts::Base username: u["user_nicename"], password: u["user_pass"], email: u["user_email"].downcase, - name: u["display_name"].presence || u['user_nicename'], + name: u["display_name"].presence || u["user_nicename"], created_at: u["user_registered"], website: u["user_url"], bio_raw: users_description[u["id"]], @@ -114,67 +110,60 @@ class ImportScripts::Bbpress < ImportScripts::Base emails = Array.new # gather anonymous users via postmeta table - bbpress_query(<<-SQL + bbpress_query(<<-SQL).each do |pm| SELECT post_id, meta_key, meta_value FROM #{BB_PRESS_PREFIX}postmeta WHERE meta_key LIKE '_bbp_anonymous%' SQL - ).each do |pm| - anon_posts[pm['post_id']] = Hash.new if not anon_posts[pm['post_id']] + anon_posts[pm["post_id"]] = Hash.new if not anon_posts[pm["post_id"]] - if pm['meta_key'] == '_bbp_anonymous_email' - anon_posts[pm['post_id']]['email'] = pm['meta_value'] + if pm["meta_key"] == "_bbp_anonymous_email" + anon_posts[pm["post_id"]]["email"] = pm["meta_value"] end - if pm['meta_key'] == '_bbp_anonymous_name' - anon_posts[pm['post_id']]['name'] = pm['meta_value'] + if pm["meta_key"] == "_bbp_anonymous_name" + anon_posts[pm["post_id"]]["name"] = pm["meta_value"] end - if pm['meta_key'] == '_bbp_anonymous_website' - anon_posts[pm['post_id']]['website'] = pm['meta_value'] + if pm["meta_key"] == "_bbp_anonymous_website" + anon_posts[pm["post_id"]]["website"] = pm["meta_value"] end end # gather every existent username anon_posts.each do |id, post| - anon_names[post['name']] = Hash.new if not anon_names[post['name']] + anon_names[post["name"]] = Hash.new if not anon_names[post["name"]] # overwriting email address, one user can only use one email address - anon_names[post['name']]['email'] = post['email'] - anon_names[post['name']]['website'] = post['website'] if post['website'] != '' + anon_names[post["name"]]["email"] = post["email"] + anon_names[post["name"]]["website"] = post["website"] if post["website"] != "" end # make sure every user name has a unique email address anon_names.each do |k, name| - if not emails.include? name['email'] - emails.push ( name['email']) + if not emails.include? name["email"] + emails.push (name["email"]) else - name['email'] = "anonymous_#{SecureRandom.hex}@no-email.invalid" + name["email"] = "anonymous_#{SecureRandom.hex}@no-email.invalid" end end create_users(anon_names) do |k, n| - { - id: k, - email: n["email"].downcase, - name: k, - website: n["website"] - } + { id: k, email: n["email"].downcase, name: k, website: n["website"] } end end def import_categories puts "", "importing categories..." - categories = bbpress_query(<<-SQL + categories = bbpress_query(<<-SQL) SELECT id, post_name, post_parent FROM #{BB_PRESS_PREFIX}posts WHERE post_type = 'forum' AND LENGTH(COALESCE(post_name, '')) > 0 ORDER BY post_parent, id SQL - ) create_categories(categories) do |c| - category = { id: c['id'], name: c['post_name'] } - if (parent_id = c['post_parent'].to_i) > 0 + category = { id: c["id"], name: c["post_name"] } + if (parent_id = c["post_parent"].to_i) > 0 category[:parent_category_id] = category_id_from_imported_category_id(parent_id) end category @@ -185,16 +174,15 @@ class ImportScripts::Bbpress < ImportScripts::Base puts "", "importing topics and posts..." last_post_id = -1 - total_posts = bbpress_query(<<-SQL + total_posts = bbpress_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}posts WHERE post_status <> 'spam' AND post_type IN ('topic', 'reply') SQL - ).first["count"] batches(BATCH_SIZE) do |offset| - posts = bbpress_query(<<-SQL + posts = bbpress_query(<<-SQL).to_a SELECT id, post_author, post_date, @@ -209,7 +197,6 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY id LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? @@ -221,31 +208,29 @@ class ImportScripts::Bbpress < ImportScripts::Base post_ids_sql = post_ids.join(",") posts_likes = {} - bbpress_query(<<-SQL + bbpress_query(<<-SQL).each { |pm| posts_likes[pm["post_id"]] = pm["likes"].to_i } SELECT post_id, meta_value likes FROM #{BB_PRESS_PREFIX}postmeta WHERE post_id IN (#{post_ids_sql}) AND meta_key = 'Likes' SQL - ).each { |pm| posts_likes[pm["post_id"]] = pm["likes"].to_i } anon_names = {} - bbpress_query(<<-SQL + bbpress_query(<<-SQL).each { |pm| anon_names[pm["post_id"]] = pm["meta_value"] } SELECT post_id, meta_value FROM #{BB_PRESS_PREFIX}postmeta WHERE post_id IN (#{post_ids_sql}) AND meta_key = '_bbp_anonymous_name' SQL - ).each { |pm| anon_names[pm["post_id"]] = pm["meta_value"] } create_posts(posts, total: total_posts, offset: offset) do |p| skip = false - user_id = user_id_from_imported_user_id(p["post_author"]) || - find_user_by_import_id(p["post_author"]).try(:id) || - user_id_from_imported_user_id(anon_names[p['id']]) || - find_user_by_import_id(anon_names[p['id']]).try(:id) || - -1 + user_id = + user_id_from_imported_user_id(p["post_author"]) || + find_user_by_import_id(p["post_author"]).try(:id) || + user_id_from_imported_user_id(anon_names[p["id"]]) || + find_user_by_import_id(anon_names[p["id"]]).try(:id) || -1 post = { id: p["id"], @@ -256,7 +241,9 @@ class ImportScripts::Bbpress < ImportScripts::Base } if post[:raw].present? - post[:raw].gsub!(/\\(.*?)\<\/code\>\<\/pre\>/im) { "```\n#{@he.decode($2)}\n```" } + post[:raw].gsub!(%r{\\(.*?)\\}im) do + "```\n#{@he.decode($2)}\n```" + end end if p["post_type"] == "topic" @@ -288,17 +275,16 @@ class ImportScripts::Bbpress < ImportScripts::Base count = 0 last_attachment_id = -1 - total_attachments = bbpress_query(<<-SQL + total_attachments = bbpress_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}postmeta pm JOIN #{BB_PRESS_PREFIX}posts p ON p.id = pm.post_id WHERE pm.meta_key = '_wp_attached_file' AND p.post_parent > 0 SQL - ).first["count"] batches(BATCH_SIZE) do |offset| - attachments = bbpress_query(<<-SQL + attachments = bbpress_query(<<-SQL).to_a SELECT pm.meta_id id, pm.meta_value, p.post_parent post_id FROM #{BB_PRESS_PREFIX}postmeta pm JOIN #{BB_PRESS_PREFIX}posts p ON p.id = pm.post_id @@ -308,7 +294,6 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY pm.meta_id LIMIT #{BATCH_SIZE} SQL - ).to_a break if attachments.empty? last_attachment_id = attachments[-1]["id"].to_i @@ -325,7 +310,9 @@ class ImportScripts::Bbpress < ImportScripts::Base if !post.raw[html] post.raw << "\n\n" << html post.save! - PostUpload.create!(post: post, upload: upload) unless PostUpload.where(post: post, upload: upload).exists? + unless PostUpload.where(post: post, upload: upload).exists? + PostUpload.create!(post: post, upload: upload) + end end end end @@ -340,15 +327,14 @@ class ImportScripts::Bbpress < ImportScripts::Base count = 0 last_attachment_id = -1 - total_attachments = bbpress_query(<<-SQL + total_attachments = bbpress_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}bb_attachments WHERE post_id IN (SELECT id FROM #{BB_PRESS_PREFIX}posts WHERE post_status <> 'spam' AND post_type IN ('topic', 'reply')) SQL - ).first["count"] batches(BATCH_SIZE) do |offset| - attachments = bbpress_query(<<-SQL + attachments = bbpress_query(<<-SQL).to_a SELECT id, filename, post_id FROM #{BB_PRESS_PREFIX}bb_attachments WHERE post_id IN (SELECT id FROM #{BB_PRESS_PREFIX}posts WHERE post_status <> 'spam' AND post_type IN ('topic', 'reply')) @@ -356,13 +342,16 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY id LIMIT #{BATCH_SIZE} SQL - ).to_a break if attachments.empty? last_attachment_id = attachments[-1]["id"].to_i attachments.each do |a| - print_status(count += 1, total_attachments, get_start_time("attachments_from_bb_attachments")) + print_status( + count += 1, + total_attachments, + get_start_time("attachments_from_bb_attachments"), + ) if path = find_attachment(a["filename"], a["id"]) if post = Post.find_by(id: post_id_from_imported_post_id(a["post_id"])) upload = create_upload(post.user.id, path, a["filename"]) @@ -371,7 +360,9 @@ class ImportScripts::Bbpress < ImportScripts::Base if !post.raw[html] post.raw << "\n\n" << html post.save! - PostUpload.create!(post: post, upload: upload) unless PostUpload.where(post: post, upload: upload).exists? + unless PostUpload.where(post: post, upload: upload).exists? + PostUpload.create!(post: post, upload: upload) + end end end end @@ -391,7 +382,7 @@ class ImportScripts::Bbpress < ImportScripts::Base last_topic_id = -1 batches(BATCH_SIZE) do |offset| - topics = bbpress_query(<<-SQL + topics = bbpress_query(<<-SQL).to_a SELECT id, guid FROM #{BB_PRESS_PREFIX}posts @@ -401,14 +392,17 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY id LIMIT #{BATCH_SIZE} SQL - ).to_a break if topics.empty? last_topic_id = topics[-1]["id"].to_i topics.each do |t| - topic = topic_lookup_from_imported_post_id(t['id']) - Permalink.create(url: URI.parse(t['guid']).path.chomp('/'), topic_id: topic[:topic_id]) rescue nil + topic = topic_lookup_from_imported_post_id(t["id"]) + begin + Permalink.create(url: URI.parse(t["guid"]).path.chomp("/"), topic_id: topic[:topic_id]) + rescue StandardError + nil + end end end end @@ -417,42 +411,44 @@ class ImportScripts::Bbpress < ImportScripts::Base puts "", "importing private messages..." last_post_id = -1 - total_posts = bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}bp_messages_messages").first["count"] + total_posts = + bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}bp_messages_messages").first[ + "count" + ] threads = {} - total_count = bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}bp_messages_recipients").first["count"] + total_count = + bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}bp_messages_recipients").first[ + "count" + ] current_count = 0 batches(BATCH_SIZE) do |offset| - rows = bbpress_query(<<-SQL + rows = bbpress_query(<<-SQL).to_a SELECT thread_id, user_id FROM #{BB_PRESS_PREFIX}bp_messages_recipients ORDER BY id LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ).to_a break if rows.empty? rows.each do |row| current_count += 1 - print_status(current_count, total_count, get_start_time('private_messages')) + print_status(current_count, total_count, get_start_time("private_messages")) - threads[row['thread_id']] ||= { - target_user_ids: [], - imported_topic_id: nil - } - user_id = user_id_from_imported_user_id(row['user_id']) - if user_id && !threads[row['thread_id']][:target_user_ids].include?(user_id) - threads[row['thread_id']][:target_user_ids] << user_id + threads[row["thread_id"]] ||= { target_user_ids: [], imported_topic_id: nil } + user_id = user_id_from_imported_user_id(row["user_id"]) + if user_id && !threads[row["thread_id"]][:target_user_ids].include?(user_id) + threads[row["thread_id"]][:target_user_ids] << user_id end end end batches(BATCH_SIZE) do |offset| - posts = bbpress_query(<<-SQL + posts = bbpress_query(<<-SQL).to_a SELECT id, thread_id, date_sent, @@ -464,39 +460,48 @@ class ImportScripts::Bbpress < ImportScripts::Base ORDER BY thread_id, date_sent LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? last_post_id = posts[-1]["id"].to_i create_posts(posts, total: total_posts, offset: offset) do |post| - if tcf = TopicCustomField.where(name: 'bb_thread_id', value: post['thread_id']).first + if tcf = TopicCustomField.where(name: "bb_thread_id", value: post["thread_id"]).first { - id: "pm#{post['id']}", - topic_id: threads[post['thread_id']][:imported_topic_id], - user_id: user_id_from_imported_user_id(post['sender_id']) || find_user_by_import_id(post['sender_id'])&.id || -1, - raw: post['message'], - created_at: post['date_sent'], + id: "pm#{post["id"]}", + topic_id: threads[post["thread_id"]][:imported_topic_id], + user_id: + user_id_from_imported_user_id(post["sender_id"]) || + find_user_by_import_id(post["sender_id"])&.id || -1, + raw: post["message"], + created_at: post["date_sent"], } else # First post of the thread { - id: "pm#{post['id']}", + id: "pm#{post["id"]}", archetype: Archetype.private_message, - user_id: user_id_from_imported_user_id(post['sender_id']) || find_user_by_import_id(post['sender_id'])&.id || -1, - title: post['subject'], - raw: post['message'], - created_at: post['date_sent'], - target_usernames: User.where(id: threads[post['thread_id']][:target_user_ids]).pluck(:username), - post_create_action: proc do |new_post| - if topic = new_post.topic - threads[post['thread_id']][:imported_topic_id] = topic.id - TopicCustomField.create(topic_id: topic.id, name: 'bb_thread_id', value: post['thread_id']) - else - puts "Error in post_create_action! Can't find topic!" - end - end + user_id: + user_id_from_imported_user_id(post["sender_id"]) || + find_user_by_import_id(post["sender_id"])&.id || -1, + title: post["subject"], + raw: post["message"], + created_at: post["date_sent"], + target_usernames: + User.where(id: threads[post["thread_id"]][:target_user_ids]).pluck(:username), + post_create_action: + proc do |new_post| + if topic = new_post.topic + threads[post["thread_id"]][:imported_topic_id] = topic.id + TopicCustomField.create( + topic_id: topic.id, + name: "bb_thread_id", + value: post["thread_id"], + ) + else + puts "Error in post_create_action! Can't find topic!" + end + end, } end end @@ -506,7 +511,6 @@ class ImportScripts::Bbpress < ImportScripts::Base def bbpress_query(sql) @client.query(sql, cache_rows: false) end - end ImportScripts::Bbpress.new.perform diff --git a/script/import_scripts/bespoke_1.rb b/script/import_scripts/bespoke_1.rb index 9ca420f319..833f772350 100644 --- a/script/import_scripts/bespoke_1.rb +++ b/script/import_scripts/bespoke_1.rb @@ -2,13 +2,12 @@ # bespoke importer for a customer, feel free to borrow ideas -require 'csv' +require "csv" require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/bespoke_1.rb class ImportScripts::Bespoke < ImportScripts::Base - BATCH_SIZE = 1000 def initialize(path) @@ -18,9 +17,9 @@ class ImportScripts::Bespoke < ImportScripts::Base puts "loading post mappings..." @post_number_map = {} - Post.pluck(:id, :post_number).each do |post_id, post_number| - @post_number_map[post_id] = post_number - end + Post + .pluck(:id, :post_number) + .each { |post_id, post_number| @post_number_map[post_id] = post_number } end def created_post(post) @@ -32,7 +31,6 @@ class ImportScripts::Bespoke < ImportScripts::Base import_users import_categories import_posts - end class RowResolver @@ -45,19 +43,13 @@ class ImportScripts::Bespoke < ImportScripts::Base end def initialize(cols) - cols.each_with_index do |col, idx| - self.class.public_send(:define_method, col) do - @row[idx] - end - end + cols.each_with_index { |col, idx| self.class.public_send(:define_method, col) { @row[idx] } } end end def load_user_batch!(users, offset, total) if users.length > 0 - create_users(users, offset: offset, total: total) do |user| - user - end + create_users(users, offset: offset, total: total) { |user| user } users.clear end end @@ -70,54 +62,56 @@ class ImportScripts::Bespoke < ImportScripts::Base current_row = +"" double_quote_count = 0 - File.open(filename).each_line do |line| + File + .open(filename) + .each_line do |line| + # escaping is mental here + line.gsub!(/\\(.{1})/) { |m| m[-1] == '"' ? '""' : m[-1] } + line.strip! - # escaping is mental here - line.gsub!(/\\(.{1})/) { |m| m[-1] == '"' ? '""' : m[-1] } - line.strip! + current_row << "\n" unless current_row.empty? + current_row << line - current_row << "\n" unless current_row.empty? - current_row << line + double_quote_count += line.scan('"').count - double_quote_count += line.scan('"').count + next if double_quote_count % 2 == 1 - if double_quote_count % 2 == 1 - next - end + raw = + begin + CSV.parse(current_row) + rescue CSV::MalformedCSVError => e + puts e.message + puts "*" * 100 + puts "Bad row skipped, line is: #{line}" + puts + puts current_row + puts + puts "double quote count is : #{double_quote_count}" + puts "*" * 100 - raw = begin - CSV.parse(current_row) - rescue CSV::MalformedCSVError => e - puts e.message - puts "*" * 100 - puts "Bad row skipped, line is: #{line}" - puts - puts current_row - puts - puts "double quote count is : #{double_quote_count}" - puts "*" * 100 + current_row = "" + double_quote_count = 0 + next + end[ + 0 + ] - current_row = "" - double_quote_count = 0 - next - end[0] + if first + row = RowResolver.create(raw) - if first - row = RowResolver.create(raw) + current_row = "" + double_quote_count = 0 + first = false + next + end + + row.load(raw) + + yield row current_row = "" double_quote_count = 0 - first = false - next end - - row.load(raw) - - yield row - - current_row = "" - double_quote_count = 0 - end end def total_rows(table) @@ -133,14 +127,11 @@ class ImportScripts::Bespoke < ImportScripts::Base total = total_rows("users") csv_parse("users") do |row| - id = row.id email = row.email # fake it - if row.email.blank? || row.email !~ /@/ - email = fake_email - end + email = fake_email if row.email.blank? || row.email !~ /@/ name = row.display_name username = row.key_custom @@ -150,19 +141,10 @@ class ImportScripts::Bespoke < ImportScripts::Base username = email.split("@")[0] if username.blank? name = email.split("@")[0] if name.blank? - users << { - id: id, - email: email, - name: name, - username: username, - created_at: created_at - } + users << { id: id, email: email, name: name, username: username, created_at: created_at } count += 1 - if count % BATCH_SIZE == 0 - load_user_batch! users, count - users.length, total - end - + load_user_batch! users, count - users.length, total if count % BATCH_SIZE == 0 end load_user_batch! users, count, total @@ -174,22 +156,19 @@ class ImportScripts::Bespoke < ImportScripts::Base rows << { id: row.id, name: row.name, description: row.description } end - create_categories(rows) do |row| - row - end + create_categories(rows) { |row| row } end def normalize_raw!(raw) # purple and #1223f3 raw.gsub!(/\[color=[#a-z0-9]+\]/i, "") - raw.gsub!(/\[\/color\]/i, "") - raw.gsub!(/\[signature\].+\[\/signature\]/im, "") + raw.gsub!(%r{\[/color\]}i, "") + raw.gsub!(%r{\[signature\].+\[/signature\]}im, "") raw end def import_post_batch!(posts, topics, offset, total) create_posts(posts, total: total, offset: offset) do |post| - mapped = {} mapped[:id] = post[:id] @@ -223,7 +202,7 @@ class ImportScripts::Bespoke < ImportScripts::Base mapped end - posts.clear + posts.clear end def import_posts @@ -237,7 +216,7 @@ class ImportScripts::Bespoke < ImportScripts::Base category_id: topic.forum_category_id, deleted: topic.is_deleted.to_i == 1, locked: topic.is_locked.to_i == 1, - pinned: topic.is_pinned.to_i == 1 + pinned: topic.is_pinned.to_i == 1, } end @@ -246,7 +225,6 @@ class ImportScripts::Bespoke < ImportScripts::Base posts = [] count = 0 csv_parse("posts") do |row| - unless row.dcreate puts "NO CREATION DATE FOR POST" p row @@ -261,7 +239,7 @@ class ImportScripts::Bespoke < ImportScripts::Base title: row.title, body: normalize_raw!(row.body), deleted: row.is_deleted.to_i == 1, - created_at: DateTime.parse(row.dcreate) + created_at: DateTime.parse(row.dcreate), } posts << row count += 1 @@ -275,7 +253,6 @@ class ImportScripts::Bespoke < ImportScripts::Base exit end - end unless ARGV[0] && Dir.exist?(ARGV[0]) diff --git a/script/import_scripts/csv_importer.rb b/script/import_scripts/csv_importer.rb index a414373dbb..626645f530 100644 --- a/script/import_scripts/csv_importer.rb +++ b/script/import_scripts/csv_importer.rb @@ -7,18 +7,18 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Make sure to follow the right format in your CSV files. class ImportScripts::CsvImporter < ImportScripts::Base - - CSV_FILE_PATH = ENV['CSV_USER_FILE'] || '/var/www/discourse/tmp/users.csv' - CSV_CUSTOM_FIELDS = ENV['CSV_CUSTOM_FIELDS'] || '/var/www/discourse/tmp/custom_fields.csv' - CSV_EMAILS = ENV['CSV_EMAILS'] || '/var/www/discourse/tmp/emails.csv' - CSV_CATEGORIES = ENV['CSV_CATEGORIES'] || '/var/www/discourse/tmp/categories.csv' - CSV_TOPICS = ENV['CSV_TOPICS'] || '/var/www/discourse/tmp/topics_new_users.csv' - CSV_TOPICS_EXISTING_USERS = ENV['CSV_TOPICS'] || '/var/www/discourse/tmp/topics_existing_users.csv' - IMPORT_PREFIX = ENV['IMPORT_PREFIX'] || '2022-08-11' - IMPORT_USER_ID_PREFIX = 'csv-user-import-' + IMPORT_PREFIX + '-' - IMPORT_CATEGORY_ID_PREFIX = 'csv-category-import-' + IMPORT_PREFIX + '-' - IMPORT_TOPIC_ID_PREFIX = 'csv-topic-import-' + IMPORT_PREFIX + '-' - IMPORT_TOPIC_ID_EXISITNG_PREFIX = 'csv-topic_existing-import-' + IMPORT_PREFIX + '-' + CSV_FILE_PATH = ENV["CSV_USER_FILE"] || "/var/www/discourse/tmp/users.csv" + CSV_CUSTOM_FIELDS = ENV["CSV_CUSTOM_FIELDS"] || "/var/www/discourse/tmp/custom_fields.csv" + CSV_EMAILS = ENV["CSV_EMAILS"] || "/var/www/discourse/tmp/emails.csv" + CSV_CATEGORIES = ENV["CSV_CATEGORIES"] || "/var/www/discourse/tmp/categories.csv" + CSV_TOPICS = ENV["CSV_TOPICS"] || "/var/www/discourse/tmp/topics_new_users.csv" + CSV_TOPICS_EXISTING_USERS = + ENV["CSV_TOPICS"] || "/var/www/discourse/tmp/topics_existing_users.csv" + IMPORT_PREFIX = ENV["IMPORT_PREFIX"] || "2022-08-11" + IMPORT_USER_ID_PREFIX = "csv-user-import-" + IMPORT_PREFIX + "-" + IMPORT_CATEGORY_ID_PREFIX = "csv-category-import-" + IMPORT_PREFIX + "-" + IMPORT_TOPIC_ID_PREFIX = "csv-topic-import-" + IMPORT_PREFIX + "-" + IMPORT_TOPIC_ID_EXISITNG_PREFIX = "csv-topic_existing-import-" + IMPORT_PREFIX + "-" def initialize super @@ -49,25 +49,19 @@ class ImportScripts::CsvImporter < ImportScripts::Base return nil end - CSV.parse(File.read(path, encoding: 'bom|utf-8'), headers: true) + CSV.parse(File.read(path, encoding: "bom|utf-8"), headers: true) end def username_for(name) - result = name.downcase.gsub(/[^a-z0-9\-\_]/, '') - if result.blank? - result = Digest::SHA1.hexdigest(name)[0...10] - end + result = name.downcase.gsub(/[^a-z0-9\-\_]/, "") + result = Digest::SHA1.hexdigest(name)[0...10] if result.blank? result end def get_email(id) email = nil - @imported_emails.each do |e| - if e["user_id"] == id - email = e["email"] - end - end + @imported_emails.each { |e| email = e["email"] if e["user_id"] == id } email end @@ -76,9 +70,7 @@ class ImportScripts::CsvImporter < ImportScripts::Base custom_fields = {} @imported_custom_fields.each do |cf| if cf["user_id"] == id - @imported_custom_fields_names.each do |name| - custom_fields[name] = cf[name] - end + @imported_custom_fields_names.each { |name| custom_fields[name] = cf[name] } end end @@ -86,98 +78,95 @@ class ImportScripts::CsvImporter < ImportScripts::Base end def import_users - puts '', "Importing users" + puts "", "Importing users" users = [] @imported_users.each do |u| - email = get_email(u['id']) - custom_fields = get_custom_fields(u['id']) - u['email'] = email - u['custom_fields'] = custom_fields - u['id'] = IMPORT_USER_ID_PREFIX + u['id'] + email = get_email(u["id"]) + custom_fields = get_custom_fields(u["id"]) + u["email"] = email + u["custom_fields"] = custom_fields + u["id"] = IMPORT_USER_ID_PREFIX + u["id"] users << u end users.uniq! create_users(users) do |u| { - id: u['id'], - username: u['username'], - email: u['email'], - created_at: u['created_at'], - custom_fields: u['custom_fields'], + id: u["id"], + username: u["username"], + email: u["email"], + created_at: u["created_at"], + custom_fields: u["custom_fields"], } end end def import_categories - puts '', "Importing categories" + puts "", "Importing categories" categories = [] @imported_categories.each do |c| - c['user_id'] = user_id_from_imported_user_id(IMPORT_USER_ID_PREFIX + c['user_id']) || Discourse::SYSTEM_USER_ID - c['id'] = IMPORT_CATEGORY_ID_PREFIX + c['id'] + c["user_id"] = user_id_from_imported_user_id(IMPORT_USER_ID_PREFIX + c["user_id"]) || + Discourse::SYSTEM_USER_ID + c["id"] = IMPORT_CATEGORY_ID_PREFIX + c["id"] categories << c end categories.uniq! create_categories(categories) do |c| - { - id: c['id'], - user_id: c['user_id'], - name: c['name'], - description: c['description'] - } + { id: c["id"], user_id: c["user_id"], name: c["name"], description: c["description"] } end end def import_topics - puts '', "Importing topics" + puts "", "Importing topics" topics = [] @imported_topics.each do |t| - t['user_id'] = user_id_from_imported_user_id(IMPORT_USER_ID_PREFIX + t['user_id']) || Discourse::SYSTEM_USER_ID - t['category_id'] = category_id_from_imported_category_id(IMPORT_CATEGORY_ID_PREFIX + t['category_id']) - t['id'] = IMPORT_TOPIC_ID_PREFIX + t['id'] + t["user_id"] = user_id_from_imported_user_id(IMPORT_USER_ID_PREFIX + t["user_id"]) || + Discourse::SYSTEM_USER_ID + t["category_id"] = category_id_from_imported_category_id( + IMPORT_CATEGORY_ID_PREFIX + t["category_id"], + ) + t["id"] = IMPORT_TOPIC_ID_PREFIX + t["id"] topics << t end create_posts(topics) do |t| { - id: t['id'], - user_id: t['user_id'], - title: t['title'], - category: t['category_id'], - raw: t['raw'] + id: t["id"], + user_id: t["user_id"], + title: t["title"], + category: t["category_id"], + raw: t["raw"], } end end def import_topics_existing_users # Import topics for users that already existed in the DB, not imported during this migration - puts '', "Importing topics for existing users" + puts "", "Importing topics for existing users" topics = [] @imported_topics_existing_users.each do |t| - t['id'] = IMPORT_TOPIC_ID_EXISITNG_PREFIX + t['id'] + t["id"] = IMPORT_TOPIC_ID_EXISITNG_PREFIX + t["id"] topics << t end create_posts(topics) do |t| { - id: t['id'], - user_id: t['user_id'], # This is a Discourse user ID - title: t['title'], - category: t['category_id'], # This is a Discourse category ID - raw: t['raw'] + id: t["id"], + user_id: t["user_id"], # This is a Discourse user ID + title: t["title"], + category: t["category_id"], # This is a Discourse category ID + raw: t["raw"], } end end end -if __FILE__ == $0 - ImportScripts::CsvImporter.new.perform -end +ImportScripts::CsvImporter.new.perform if __FILE__ == $0 # == CSV files format # diff --git a/script/import_scripts/csv_restore_staged_users.rb b/script/import_scripts/csv_restore_staged_users.rb index 2145bcc351..314004b880 100755 --- a/script/import_scripts/csv_restore_staged_users.rb +++ b/script/import_scripts/csv_restore_staged_users.rb @@ -6,10 +6,9 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Edit the constants and initialize method for your import data. class ImportScripts::CsvRestoreStagedUsers < ImportScripts::Base - - CSV_FILE_PATH = ENV['CSV_USER_FILE'] - CSV_CUSTOM_FIELDS = ENV['CSV_CUSTOM_FIELDS'] - CSV_EMAILS = ENV['CSV_EMAILS'] + CSV_FILE_PATH = ENV["CSV_USER_FILE"] + CSV_CUSTOM_FIELDS = ENV["CSV_CUSTOM_FIELDS"] + CSV_EMAILS = ENV["CSV_EMAILS"] BATCH_SIZE ||= 1000 @@ -35,62 +34,51 @@ class ImportScripts::CsvRestoreStagedUsers < ImportScripts::Base end def username_for(name) - result = name.downcase.gsub(/[^a-z0-9\-\_]/, '') + result = name.downcase.gsub(/[^a-z0-9\-\_]/, "") - if result.blank? - result = Digest::SHA1.hexdigest(name)[0...10] - end + result = Digest::SHA1.hexdigest(name)[0...10] if result.blank? result end def get_email(id) email = nil - @imported_emails.each do |e| - if e["user_id"] == id - email = e["email"] - end - end + @imported_emails.each { |e| email = e["email"] if e["user_id"] == id } email end def get_custom_fields(id) custom_fields = {} @imported_custom_fields.each do |cf| - if cf["user_id"] == id - custom_fields[cf["name"]] = cf["value"] - end + custom_fields[cf["name"]] = cf["value"] if cf["user_id"] == id end custom_fields end def import_users - puts '', "Importing users" + puts "", "Importing users" users = [] @imported_users.each do |u| - email = get_email(u['id']) - custom_fields = get_custom_fields(u['id']) - u['email'] = email - u['custom_fields'] = custom_fields + email = get_email(u["id"]) + custom_fields = get_custom_fields(u["id"]) + u["email"] = email + u["custom_fields"] = custom_fields users << u end users.uniq! create_users(users) do |u| { - id: u['id'], - username: u['username'], - email: u['email'], - created_at: u['created_at'], - staged: u['staged'], - custom_fields: u['custom_fields'], + id: u["id"], + username: u["username"], + email: u["email"], + created_at: u["created_at"], + staged: u["staged"], + custom_fields: u["custom_fields"], } end end - end -if __FILE__ == $0 - ImportScripts::CsvRestoreStagedUsers.new.perform -end +ImportScripts::CsvRestoreStagedUsers.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/discuz_x.rb b/script/import_scripts/discuz_x.rb index 1b3cb5b8cb..df6a28c2af 100644 --- a/script/import_scripts/discuz_x.rb +++ b/script/import_scripts/discuz_x.rb @@ -9,48 +9,47 @@ # This script is tested only on Simplified Chinese Discuz! X instances # If you want to import data other than Simplified Chinese, email me. -require 'php_serialize' -require 'miro' -require 'mysql2' +require "php_serialize" +require "miro" +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::DiscuzX < ImportScripts::Base - DISCUZX_DB = "ultrax" - DB_TABLE_PREFIX = 'pre_' + DB_TABLE_PREFIX = "pre_" BATCH_SIZE = 1000 ORIGINAL_SITE_PREFIX = "oldsite.example.com/forums" # without http(s):// - NEW_SITE_PREFIX = "http://discourse.example.com" # with http:// or https:// + NEW_SITE_PREFIX = "http://discourse.example.com" # with http:// or https:// # Set DISCUZX_BASE_DIR to the base directory of your discuz installation. - DISCUZX_BASE_DIR = '/var/www/discuz/upload' - AVATAR_DIR = '/uc_server/data/avatar' - ATTACHMENT_DIR = '/data/attachment/forum' - AUTHORIZED_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'zip', 'rar', 'pdf'] + DISCUZX_BASE_DIR = "/var/www/discuz/upload" + AVATAR_DIR = "/uc_server/data/avatar" + ATTACHMENT_DIR = "/data/attachment/forum" + AUTHORIZED_EXTENSIONS = %w[jpg jpeg png gif zip rar pdf] def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - #password: "password", - database: DISCUZX_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + #password: "password", + database: DISCUZX_DB, + ) @first_post_id_by_topic_id = {} @internal_url_regexps = [ - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=viewthread(?:&|&)tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/viewthread\.php\?tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=redirect(?:&|&)goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/redirect\.php\?goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forumdisplay\.php\?fid=(?\d+)(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/forum\.php\?mod=forumdisplay(?:&|&)fid=(?\d+)(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/(?index)\.php(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/(?stats)\.php(?:[^\[\]\s]*)/, - /http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/misc.php\?mod=(?stat|ranklist)(?:[^\[\]\s]*)/ + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/forum\.php\?mod=viewthread(?:&|&)tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/viewthread\.php\?tid=(?\d+)(?:[^\[\]\s]*)(?:pid=?(?\d+))?(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/forum\.php\?mod=redirect(?:&|&)goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/redirect\.php\?goto=findpost(?:&|&)pid=(?\d+)(?:&|&)ptid=(?\d+)(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/forumdisplay\.php\?fid=(?\d+)(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/forum\.php\?mod=forumdisplay(?:&|&)fid=(?\d+)(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/(?index)\.php(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/(?stats)\.php(?:[^\[\]\s]*)}, + %r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/misc.php\?mod=(?stat|ranklist)(?:[^\[\]\s]*)}, ] - end def execute @@ -69,75 +68,84 @@ class ImportScripts::DiscuzX < ImportScripts::Base # find which group members can be granted as admin def get_knowledge_about_group - group_table = table_name 'common_usergroup' - result = mysql_query( - "SELECT groupid group_id, radminid role_id - FROM #{group_table};") + group_table = table_name "common_usergroup" + result = + mysql_query( + "SELECT groupid group_id, radminid role_id + FROM #{group_table};", + ) @moderator_group_id = [] @admin_group_id = [] #@banned_group_id = [4,5] # 禁止的用户及其帖子均不导入,如果你想导入这些用户和帖子,请把这个数组清空。 result.each do |group| - case group['role_id'] + case group["role_id"] when 1 # 管理员 - @admin_group_id << group['group_id'] - when 2, 3 # 超级版主、版主。如果你不希望原普通版主成为Discourse版主,把3去掉。 - @moderator_group_id << group['group_id'] + @admin_group_id << group["group_id"] + when 2, + 3 # 超级版主、版主。如果你不希望原普通版主成为Discourse版主,把3去掉。 + @moderator_group_id << group["group_id"] end end end def get_knowledge_about_category_slug @category_slug = {} - results = mysql_query("SELECT svalue value - FROM #{table_name 'common_setting'} - WHERE skey = 'forumkeys'") + results = + mysql_query( + "SELECT svalue value + FROM #{table_name "common_setting"} + WHERE skey = 'forumkeys'", + ) return if results.size < 1 - value = results.first['value'] + value = results.first["value"] return if value.blank? - PHP.unserialize(value).each do |category_import_id, slug| - next if slug.blank? - @category_slug[category_import_id] = slug - end + PHP + .unserialize(value) + .each do |category_import_id, slug| + next if slug.blank? + @category_slug[category_import_id] = slug + end end def get_knowledge_about_duplicated_email @duplicated_email = {} - results = mysql_query( - "select a.uid uid, b.uid import_id from pre_common_member a + results = + mysql_query( + "select a.uid uid, b.uid import_id from pre_common_member a join (select uid, email from pre_common_member group by email having count(email) > 1 order by uid asc) b USING(email) - where a.uid != b.uid") + where a.uid != b.uid", + ) users = @lookup.instance_variable_get :@users results.each do |row| - @duplicated_email[row['uid']] = row['import_id'] - user_id = users[row['import_id']] - if user_id - users[row['uid']] = user_id - end + @duplicated_email[row["uid"]] = row["import_id"] + user_id = users[row["import_id"]] + users[row["uid"]] = user_id if user_id end end def import_users - puts '', "creating users" + puts "", "creating users" get_knowledge_about_group - sensitive_user_table = table_name 'ucenter_members' - user_table = table_name 'common_member' - profile_table = table_name 'common_member_profile' - status_table = table_name 'common_member_status' - forum_table = table_name 'common_member_field_forum' - home_table = table_name 'common_member_field_home' - total_count = mysql_query("SELECT count(*) count FROM #{user_table};").first['count'] + sensitive_user_table = table_name "ucenter_members" + user_table = table_name "common_member" + profile_table = table_name "common_member_profile" + status_table = table_name "common_member_status" + forum_table = table_name "common_member_field_forum" + home_table = table_name "common_member_field_home" + total_count = mysql_query("SELECT count(*) count FROM #{user_table};").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT u.uid id, u.username username, u.email email, u.groupid group_id, + results = + mysql_query( + "SELECT u.uid id, u.username username, u.email email, u.groupid group_id, su.regdate regdate, su.password password_hash, su.salt salt, s.regip regip, s.lastip last_visit_ip, s.lastvisit last_visit_time, s.lastpost last_posted_at, s.lastsendmail last_emailed_at, u.emailstatus email_confirmed, u.avatarstatus avatar_exists, @@ -154,7 +162,8 @@ class ImportScripts::DiscuzX < ImportScripts::Base LEFT JOIN #{home_table} h USING(uid) ORDER BY u.uid ASC LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 @@ -162,147 +171,233 @@ class ImportScripts::DiscuzX < ImportScripts::Base # next if all_records_exist? :users, users.map {|u| u["id"].to_i} create_users(results, total: total_count, offset: offset) do |user| - { id: user['id'], - email: user['email'], - username: user['username'], - name: first_exists(user['realname'], user['customstatus'], user['username']), - import_pass: user['password_hash'], + { + id: user["id"], + email: user["email"], + username: user["username"], + name: first_exists(user["realname"], user["customstatus"], user["username"]), + import_pass: user["password_hash"], active: true, - salt: user['salt'], + salt: user["salt"], # TODO: title: user['customstatus'], # move custom title to name since discourse can't let user custom title https://meta.discourse.org/t/let-users-custom-their-title/37626 - created_at: user['regdate'] ? Time.zone.at(user['regdate']) : nil, - registration_ip_address: user['regip'], - ip_address: user['last_visit_ip'], - last_seen_at: user['last_visit_time'], - last_emailed_at: user['last_emailed_at'], - last_posted_at: user['last_posted_at'], - moderator: @moderator_group_id.include?(user['group_id']), - admin: @admin_group_id.include?(user['group_id']), - website: (user['website'] && user['website'].include?('.')) ? user['website'].strip : (user['qq'] && user['qq'].strip == (user['qq'].strip.to_i) && user['qq'].strip.to_i > (10000)) ? 'http://user.qzone.qq.com/' + user['qq'].strip : nil, - bio_raw: first_exists((user['bio'] && CGI.unescapeHTML(user['bio'])), user['sightml'], user['spacenote']).strip[0, 3000], - location: first_exists(user['address'], (!user['resideprovince'].blank? ? [user['resideprovince'], user['residecity'], user['residedist'], user['residecommunity']] : [user['birthprovince'], user['birthcity'], user['birthdist'], user['birthcommunity']]).reject { |location|location.blank? }.join(' ')), - post_create_action: lambda do |newmember| - if user['avatar_exists'] == (1) && newmember.uploaded_avatar_id.blank? - path, filename = discuzx_avatar_fullpath(user['id']) - if path - begin - upload = create_upload(newmember.id, path, filename) - if !upload.nil? && upload.persisted? - newmember.import_mode = false - newmember.create_user_avatar - newmember.import_mode = true - newmember.user_avatar.update(custom_upload_id: upload.id) - newmember.update(uploaded_avatar_id: upload.id) - else - puts "Error: Upload did not persist!" + created_at: user["regdate"] ? Time.zone.at(user["regdate"]) : nil, + registration_ip_address: user["regip"], + ip_address: user["last_visit_ip"], + last_seen_at: user["last_visit_time"], + last_emailed_at: user["last_emailed_at"], + last_posted_at: user["last_posted_at"], + moderator: @moderator_group_id.include?(user["group_id"]), + admin: @admin_group_id.include?(user["group_id"]), + website: + (user["website"] && user["website"].include?(".")) ? + user["website"].strip : + if ( + user["qq"] && user["qq"].strip == (user["qq"].strip.to_i) && + user["qq"].strip.to_i > (10_000) + ) + "http://user.qzone.qq.com/" + user["qq"].strip + else + nil + end, + bio_raw: + first_exists( + (user["bio"] && CGI.unescapeHTML(user["bio"])), + user["sightml"], + user["spacenote"], + ).strip[ + 0, + 3000 + ], + location: + first_exists( + user["address"], + ( + if !user["resideprovince"].blank? + [ + user["resideprovince"], + user["residecity"], + user["residedist"], + user["residecommunity"], + ] + else + [ + user["birthprovince"], + user["birthcity"], + user["birthdist"], + user["birthcommunity"], + ] + end + ).reject { |location| location.blank? }.join(" "), + ), + post_create_action: + lambda do |newmember| + if user["avatar_exists"] == (1) && newmember.uploaded_avatar_id.blank? + path, filename = discuzx_avatar_fullpath(user["id"]) + if path + begin + upload = create_upload(newmember.id, path, filename) + if !upload.nil? && upload.persisted? + newmember.import_mode = false + newmember.create_user_avatar + newmember.import_mode = true + newmember.user_avatar.update(custom_upload_id: upload.id) + newmember.update(uploaded_avatar_id: upload.id) + else + puts "Error: Upload did not persist!" + end + rescue SystemCallError => err + puts "Could not import avatar: #{err.message}" end - rescue SystemCallError => err - puts "Could not import avatar: #{err.message}" end end - end - if !user['spacecss'].blank? && newmember.user_profile.profile_background_upload.blank? - # profile background - if matched = user['spacecss'].match(/body\s*{[^}]*url\('?(.+?)'?\)/i) - body_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last - end - if matched = user['spacecss'].match(/#hd\s*{[^}]*url\('?(.+?)'?\)/i) - header_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last - end - if matched = user['spacecss'].match(/.blocktitle\s*{[^}]*url\('?(.+?)'?\)/i) - blocktitle_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last - end - if matched = user['spacecss'].match(/#ct\s*{[^}]*url\('?(.+?)'?\)/i) - content_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + if !user["spacecss"].blank? && newmember.user_profile.profile_background_upload.blank? + # profile background + if matched = user["spacecss"].match(/body\s*{[^}]*url\('?(.+?)'?\)/i) + body_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user["spacecss"].match(/#hd\s*{[^}]*url\('?(.+?)'?\)/i) + header_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user["spacecss"].match(/.blocktitle\s*{[^}]*url\('?(.+?)'?\)/i) + blocktitle_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + if matched = user["spacecss"].match(/#ct\s*{[^}]*url\('?(.+?)'?\)/i) + content_background = matched[1].split(ORIGINAL_SITE_PREFIX, 2).last + end + + if body_background || header_background || blocktitle_background || + content_background + profile_background = + first_exists( + header_background, + body_background, + content_background, + blocktitle_background, + ) + card_background = + first_exists( + content_background, + body_background, + header_background, + blocktitle_background, + ) + upload = + create_upload( + newmember.id, + File.join(DISCUZX_BASE_DIR, profile_background), + File.basename(profile_background), + ) + if upload + newmember.user_profile.upload_profile_background upload + else + puts "WARNING: #{user["username"]} (UID: #{user["id"]}) profile_background file did not persist!" + end + upload = + create_upload( + newmember.id, + File.join(DISCUZX_BASE_DIR, card_background), + File.basename(card_background), + ) + if upload + newmember.user_profile.upload_card_background upload + else + puts "WARNING: #{user["username"]} (UID: #{user["id"]}) card_background file did not persist!" + end + end end - if body_background || header_background || blocktitle_background || content_background - profile_background = first_exists(header_background, body_background, content_background, blocktitle_background) - card_background = first_exists(content_background, body_background, header_background, blocktitle_background) - upload = create_upload(newmember.id, File.join(DISCUZX_BASE_DIR, profile_background), File.basename(profile_background)) - if upload - newmember.user_profile.upload_profile_background upload - else - puts "WARNING: #{user['username']} (UID: #{user['id']}) profile_background file did not persist!" - end - upload = create_upload(newmember.id, File.join(DISCUZX_BASE_DIR, card_background), File.basename(card_background)) - if upload - newmember.user_profile.upload_card_background upload - else - puts "WARNING: #{user['username']} (UID: #{user['id']}) card_background file did not persist!" - end + # we don't send email to the unconfirmed user + if newmember.email_digests + newmember.update(email_digests: user["email_confirmed"] == 1) end - end - - # we don't send email to the unconfirmed user - newmember.update(email_digests: user['email_confirmed'] == 1) if newmember.email_digests - newmember.update(name: '') if !newmember.name.blank? && newmember.name == (newmember.username) - end + if !newmember.name.blank? && newmember.name == (newmember.username) + newmember.update(name: "") + end + end, } end end end def import_categories - puts '', "creating categories" + puts "", "creating categories" get_knowledge_about_category_slug - forums_table = table_name 'forum_forum' - forums_data_table = table_name 'forum_forumfield' + forums_table = table_name "forum_forum" + forums_data_table = table_name "forum_forumfield" - results = mysql_query(" + results = + mysql_query( + " SELECT f.fid id, f.fup parent_id, f.name, f.type type, f.status status, f.displayorder position, d.description description, d.rules rules, d.icon, d.extra extra FROM #{forums_table} f LEFT JOIN #{forums_data_table} d USING(fid) ORDER BY parent_id ASC, id ASC - ") + ", + ) max_position = Category.all.max_by(&:position).position create_categories(results) do |row| - next if row['type'] == ('group') || row['status'] == (2) # or row['status'].to_i == 3 # 如果不想导入群组,取消注释 - extra = PHP.unserialize(row['extra']) if !row['extra'].blank? - if extra && !extra["namecolor"].blank? - color = extra["namecolor"][1, 6] - end + next if row["type"] == ("group") || row["status"] == (2) # or row['status'].to_i == 3 # 如果不想导入群组,取消注释 + extra = PHP.unserialize(row["extra"]) if !row["extra"].blank? + color = extra["namecolor"][1, 6] if extra && !extra["namecolor"].blank? Category.all.max_by(&:position).position h = { - id: row['id'], - name: row['name'], - description: row['description'], - position: row['position'].to_i + max_position, + id: row["id"], + name: row["name"], + description: row["description"], + position: row["position"].to_i + max_position, color: color, - post_create_action: lambda do |category| - if slug = @category_slug[row['id']] - category.update(slug: slug) - end - - raw = process_discuzx_post(row['rules'], nil) - if @bbcode_to_md - raw = raw.bbcode_to_md(false) rescue raw - end - category.topic.posts.first.update_attribute(:raw, raw) - if !row['icon'].empty? - upload = create_upload(Discourse::SYSTEM_USER_ID, File.join(DISCUZX_BASE_DIR, ATTACHMENT_DIR, '../common', row['icon']), File.basename(row['icon'])) - if upload - category.uploaded_logo_id = upload.id - # FIXME: I don't know how to get '/shared' by script. May change to Rails.root - category.color = Miro::DominantColors.new(File.join('/shared', upload.url)).to_hex.first[1, 6] if !color - category.save! + post_create_action: + lambda do |category| + if slug = @category_slug[row["id"]] + category.update(slug: slug) end - end - if row['status'] == (0) || row['status'] == (3) - SiteSetting.default_categories_muted = [SiteSetting.default_categories_muted, category.id].reject(&:blank?).join("|") - end - category - end + raw = process_discuzx_post(row["rules"], nil) + if @bbcode_to_md + raw = + begin + raw.bbcode_to_md(false) + rescue StandardError + raw + end + end + category.topic.posts.first.update_attribute(:raw, raw) + if !row["icon"].empty? + upload = + create_upload( + Discourse::SYSTEM_USER_ID, + File.join(DISCUZX_BASE_DIR, ATTACHMENT_DIR, "../common", row["icon"]), + File.basename(row["icon"]), + ) + if upload + category.uploaded_logo_id = upload.id + # FIXME: I don't know how to get '/shared' by script. May change to Rails.root + category.color = + Miro::DominantColors.new(File.join("/shared", upload.url)).to_hex.first[ + 1, + 6 + ] if !color + category.save! + end + end + + if row["status"] == (0) || row["status"] == (3) + SiteSetting.default_categories_muted = [ + SiteSetting.default_categories_muted, + category.id, + ].reject(&:blank?).join("|") + end + category + end, } - if row['parent_id'].to_i > 0 - h[:parent_category_id] = category_id_from_imported_category_id(row['parent_id']) + if row["parent_id"].to_i > 0 + h[:parent_category_id] = category_id_from_imported_category_id(row["parent_id"]) end h end @@ -311,14 +406,16 @@ class ImportScripts::DiscuzX < ImportScripts::Base def import_posts puts "", "creating topics and posts" - users_table = table_name 'common_member' - posts_table = table_name 'forum_post' - topics_table = table_name 'forum_thread' + users_table = table_name "common_member" + posts_table = table_name "forum_post" + topics_table = table_name "forum_thread" - total_count = mysql_query("SELECT count(*) count FROM #{posts_table}").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{posts_table}").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.pid id, p.tid topic_id, t.fid category_id, @@ -336,7 +433,8 @@ class ImportScripts::DiscuzX < ImportScripts::Base ORDER BY id ASC, topic_id ASC LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ") + ", + ) # u.status != -1 AND u.groupid != 4 AND u.groupid != 5 用户未被锁定、禁访或禁言。在现实中的 Discuz 论坛,禁止的用户通常是广告机或驱逐的用户,这些不需要导入。 break if results.size < 1 @@ -346,63 +444,70 @@ class ImportScripts::DiscuzX < ImportScripts::Base skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_discuzx_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['post_time']) - mapped[:tags] = m['tags'] + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_discuzx_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["post_time"]) + mapped[:tags] = m["tags"] - if m['id'] == m['first_id'] - mapped[:category] = category_id_from_imported_category_id(m['category_id']) - mapped[:title] = CGI.unescapeHTML(m['title']) + if m["id"] == m["first_id"] + mapped[:category] = category_id_from_imported_category_id(m["category_id"]) + mapped[:title] = CGI.unescapeHTML(m["title"]) - if m['special'] == 1 - results = mysql_query(" + if m["special"] == 1 + results = + mysql_query( + " SELECT multiple, maxchoices - FROM #{table_name 'forum_poll'} - WHERE tid = #{m['topic_id']}") + FROM #{table_name "forum_poll"} + WHERE tid = #{m["topic_id"]}", + ) poll = results.first || {} - results = mysql_query(" + results = + mysql_query( + " SELECT polloption - FROM #{table_name 'forum_polloption'} - WHERE tid = #{m['topic_id']} - ORDER BY displayorder") + FROM #{table_name "forum_polloption"} + WHERE tid = #{m["topic_id"]} + ORDER BY displayorder", + ) if results.empty? - puts "WARNING: can't find poll options for topic #{m['topic_id']}, skip poll" + puts "WARNING: can't find poll options for topic #{m["topic_id"]}, skip poll" else - mapped[:raw].prepend "[poll#{poll['multiple'] ? ' type=multiple' : ''}#{poll['maxchoices'] > 0 ? " max=#{poll['maxchoices']}" : ''}]\n#{results.map { |option|'- ' + option['polloption'] }.join("\n")}\n[/poll]\n" + mapped[ + :raw + ].prepend "[poll#{poll["multiple"] ? " type=multiple" : ""}#{poll["maxchoices"] > 0 ? " max=#{poll["maxchoices"]}" : ""}]\n#{results.map { |option| "- " + option["polloption"] }.join("\n")}\n[/poll]\n" end end else - parent = topic_lookup_from_imported_post_id(m['first_id']) + parent = topic_lookup_from_imported_post_id(m["first_id"]) if parent mapped[:topic_id] = parent[:topic_id] - reply_post_import_id = find_post_id_by_quote_number(m['raw']) + reply_post_import_id = find_post_id_by_quote_number(m["raw"]) if reply_post_import_id post_id = post_id_from_imported_post_id(reply_post_import_id.to_i) if (post = Post.find_by(id: post_id)) if post.topic_id == mapped[:topic_id] mapped[:reply_to_post_number] = post.post_number else - puts "post #{m['id']} reply to another topic, skip reply" + puts "post #{m["id"]} reply to another topic, skip reply" end else - puts "post #{m['id']} reply to not exists post #{reply_post_import_id}, skip reply" + puts "post #{m["id"]} reply to not exists post #{reply_post_import_id}, skip reply" end end else - puts "Parent topic #{m['topic_id']} doesn't exist. Skipping #{m['id']}: #{m['title'][0..40]}" + puts "Parent topic #{m["topic_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end - end - if m['status'] & 1 == 1 || mapped[:raw].blank? + if m["status"] & 1 == 1 || mapped[:raw].blank? mapped[:post_create_action] = lambda do |action_post| PostDestroyer.new(Discourse.system_user, action_post).perform_delete end - elsif (m['status'] & 2) >> 1 == 1 # waiting for approve + elsif (m["status"] & 2) >> 1 == 1 # waiting for approve mapped[:post_create_action] = lambda do |action_post| PostActionCreator.notify_user(Discourse.system_user, action_post) end @@ -413,42 +518,47 @@ class ImportScripts::DiscuzX < ImportScripts::Base end def import_bookmarks - puts '', 'creating bookmarks' - favorites_table = table_name 'home_favorite' - posts_table = table_name 'forum_post' + puts "", "creating bookmarks" + favorites_table = table_name "home_favorite" + posts_table = table_name "forum_post" - total_count = mysql_query("SELECT count(*) count FROM #{favorites_table} WHERE idtype = 'tid'").first['count'] + total_count = + mysql_query("SELECT count(*) count FROM #{favorites_table} WHERE idtype = 'tid'").first[ + "count" + ] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.pid post_id, f.uid user_id FROM #{favorites_table} f JOIN #{posts_table} p ON f.id = p.tid WHERE f.idtype = 'tid' AND p.first LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 # next if all_records_exist? create_bookmarks(results, total: total_count, offset: offset) do |row| - { - user_id: row['user_id'], - post_id: row['post_id'] - } + { user_id: row["user_id"], post_id: row["post_id"] } end end end def import_private_messages - puts '', 'creating private messages' + puts "", "creating private messages" - pm_indexes = table_name 'ucenter_pm_indexes' - pm_messages = table_name 'ucenter_pm_messages' - total_count = mysql_query("SELECT count(*) count FROM #{pm_indexes}").first['count'] + pm_indexes = table_name "ucenter_pm_indexes" + pm_messages = table_name "ucenter_pm_messages" + total_count = mysql_query("SELECT count(*) count FROM #{pm_indexes}").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT pmid id, plid thread_id, authorid user_id, message, dateline created_at FROM #{pm_messages}_1 UNION SELECT pmid id, plid thread_id, authorid user_id, message, dateline created_at @@ -469,7 +579,8 @@ class ImportScripts::DiscuzX < ImportScripts::Base FROM #{pm_messages}_9 ORDER BY thread_id ASC, id ASC LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 @@ -479,35 +590,47 @@ class ImportScripts::DiscuzX < ImportScripts::Base skip = false mapped = {} - mapped[:id] = "pm:#{m['id']}" - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_discuzx_post(m['message'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) - thread_id = "pm_#{m['thread_id']}" + mapped[:id] = "pm:#{m["id"]}" + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_discuzx_post(m["message"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) + thread_id = "pm_#{m["thread_id"]}" - if is_first_pm(m['id'], m['thread_id']) + if is_first_pm(m["id"], m["thread_id"]) # find the title from list table - pm_thread = mysql_query(" + pm_thread = + mysql_query( + " SELECT plid thread_id, subject - FROM #{table_name 'ucenter_pm_lists'} - WHERE plid = #{m['thread_id']};").first - mapped[:title] = pm_thread['subject'] + FROM #{table_name "ucenter_pm_lists"} + WHERE plid = #{m["thread_id"]};", + ).first + mapped[:title] = pm_thread["subject"] mapped[:archetype] = Archetype.private_message # Find the users who are part of this private message. - import_user_ids = mysql_query(" + import_user_ids = + mysql_query( + " SELECT plid thread_id, uid user_id - FROM #{table_name 'ucenter_pm_members'} - WHERE plid = #{m['thread_id']}; - ").map { |r| r['user_id'] }.uniq + FROM #{table_name "ucenter_pm_members"} + WHERE plid = #{m["thread_id"]}; + ", + ).map { |r| r["user_id"] }.uniq - mapped[:target_usernames] = import_user_ids.map! do |import_user_id| - import_user_id.to_s == m['user_id'].to_s ? nil : User.find_by(id: user_id_from_imported_user_id(import_user_id)).try(:username) - end.compact + mapped[:target_usernames] = import_user_ids + .map! do |import_user_id| + if import_user_id.to_s == m["user_id"].to_s + nil + else + User.find_by(id: user_id_from_imported_user_id(import_user_id)).try(:username) + end + end + .compact if mapped[:target_usernames].empty? # pm with yourself? skip = true - puts "Skipping pm:#{m['id']} due to no target" + puts "Skipping pm:#{m["id"]} due to no target" else @first_post_id_by_topic_id[thread_id] = mapped[:id] end @@ -523,22 +646,24 @@ class ImportScripts::DiscuzX < ImportScripts::Base skip ? nil : mapped end - end end # search for first pm id for the series of pm def is_first_pm(pm_id, thread_id) - result = mysql_query(" + result = + mysql_query( + " SELECT pmid id - FROM #{table_name 'ucenter_pm_indexes'} + FROM #{table_name "ucenter_pm_indexes"} WHERE plid = #{thread_id} - ORDER BY id") - result.first['id'].to_s == pm_id.to_s + ORDER BY id", + ) + result.first["id"].to_s == pm_id.to_s end def process_and_upload_inline_images(raw) - inline_image_regex = /\[img\]([\s\S]*?)\[\/img\]/ + inline_image_regex = %r{\[img\]([\s\S]*?)\[/img\]} s = raw.dup @@ -549,7 +674,6 @@ class ImportScripts::DiscuzX < ImportScripts::Base upload, filename = upload_inline_image data upload ? html_for_upload(upload, filename) : nil end - end def process_discuzx_post(raw, import_id) @@ -559,10 +683,18 @@ class ImportScripts::DiscuzX < ImportScripts::Base # Strip the quote # [quote] quotation includes the topic which is the same as reply to in Discourse # We get the pid to find the post number the post reply to. So it can be stripped - s = s.gsub(/\[b\]回复 \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].* 的帖子\[\/url\]\[\/b\]/i, '').strip - s = s.gsub(/\[b\]回复 \[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\].*?\[\/b\]/i, '').strip + s = + s.gsub( + %r{\[b\]回复 \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].* 的帖子\[/url\]\[/b\]}i, + "", + ).strip + s = + s.gsub( + %r{\[b\]回复 \[url=https?://#{ORIGINAL_SITE_PREFIX}/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[/url\].*?\[/b\]}i, + "", + ).strip - s.gsub!(/\[quote\](.*)?\[\/quote\]/im) do |matched| + s.gsub!(%r{\[quote\](.*)?\[/quote\]}im) do |matched| content = $1 post_import_id = find_post_id_by_quote_number(content) if post_import_id @@ -578,73 +710,93 @@ class ImportScripts::DiscuzX < ImportScripts::Base end end - s.gsub!(/\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[\/color\] \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\]\[\/size\]/i, '') - s.gsub!(/\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[\/color\] \[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[\/url\]\[\/size\]/i, '') + s.gsub!( + %r{\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[/color\] \[url=forum.php\?mod=redirect&goto=findpost&pid=\d+&ptid=\d+\].*?\[/url\]\[/size\]}i, + "", + ) + s.gsub!( + %r{\[size=2\]\[color=#999999\].*? 发表于 [\d\-\: ]*\[/color\] \[url=https?://#{ORIGINAL_SITE_PREFIX}/redirect.php\?goto=findpost&pid=\d+&ptid=\d+\].*?\[/url\]\[/size\]}i, + "", + ) # convert quote - s.gsub!(/\[quote\](.*?)\[\/quote\]/m) { "\n" + ($1.strip).gsub(/^/, '> ') + "\n" } + s.gsub!(%r{\[quote\](.*?)\[/quote\]}m) { "\n" + ($1.strip).gsub(/^/, "> ") + "\n" } # truncate line space, preventing line starting with many blanks to be parsed as code blocks - s.gsub!(/^ {4,}/, ' ') + s.gsub!(/^ {4,}/, " ") # TODO: Much better to use bbcode-to-md gem # Convert image bbcode with width and height - s.gsub!(/\[img[^\]]*\]https?:\/\/#{ORIGINAL_SITE_PREFIX}\/(.*)\[\/img\]/i, '[x-attach]\1[/x-attach]') # dont convert attachment - s.gsub!(/]*src="https?:\/\/#{ORIGINAL_SITE_PREFIX}\/(.*)".*?>/i, '[x-attach]\1[/x-attach]') # dont convert attachment - s.gsub!(/\[img[^\]]*\]https?:\/\/www\.touhou\.cc\/blog\/(.*)\[\/img\]/i, '[x-attach]../blog/\1[/x-attach]') # 私货 - s.gsub!(/\[img[^\]]*\]https?:\/\/www\.touhou\.cc\/ucenter\/avatar.php\?uid=(\d+)[^\]]*\[\/img\]/i) { "[x-attach]#{discuzx_avatar_fullpath($1, false)[0]}[/x-attach]" } # 私货 - s.gsub!(/\[img=(\d+),(\d+)\]([^\]]*)\[\/img\]/i, '') - s.gsub!(/\[img\]([^\]]*)\[\/img\]/i, '') + s.gsub!( + %r{\[img[^\]]*\]https?://#{ORIGINAL_SITE_PREFIX}/(.*)\[/img\]}i, + '[x-attach]\1[/x-attach]', + ) # dont convert attachment + s.gsub!( + %r{]*src="https?://#{ORIGINAL_SITE_PREFIX}/(.*)".*?>}i, + '[x-attach]\1[/x-attach]', + ) # dont convert attachment + s.gsub!( + %r{\[img[^\]]*\]https?://www\.touhou\.cc/blog/(.*)\[/img\]}i, + '[x-attach]../blog/\1[/x-attach]', + ) # 私货 + s.gsub!( + %r{\[img[^\]]*\]https?://www\.touhou\.cc/ucenter/avatar.php\?uid=(\d+)[^\]]*\[/img\]}i, + ) { "[x-attach]#{discuzx_avatar_fullpath($1, false)[0]}[/x-attach]" } # 私货 + s.gsub!(%r{\[img=(\d+),(\d+)\]([^\]]*)\[/img\]}i, '') + s.gsub!(%r{\[img\]([^\]]*)\[/img\]}i, '') - s.gsub!(/\[qq\]([^\]]*)\[\/qq\]/i, 'QQ 交谈') + s.gsub!( + %r{\[qq\]([^\]]*)\[/qq\]}i, + 'QQ 交谈', + ) - s.gsub!(/\[email\]([^\]]*)\[\/email\]/i, '[url=mailto:\1]\1[/url]') # bbcode-to-md can convert it - s.gsub!(/\[s\]([^\]]*)\[\/s\]/i, '\1') - s.gsub!(/\[sup\]([^\]]*)\[\/sup\]/i, '\1') - s.gsub!(/\[sub\]([^\]]*)\[\/sub\]/i, '\1') + s.gsub!(%r{\[email\]([^\]]*)\[/email\]}i, '[url=mailto:\1]\1[/url]') # bbcode-to-md can convert it + s.gsub!(%r{\[s\]([^\]]*)\[/s\]}i, '\1') + s.gsub!(%r{\[sup\]([^\]]*)\[/sup\]}i, '\1') + s.gsub!(%r{\[sub\]([^\]]*)\[/sub\]}i, '\1') s.gsub!(/\[hr\]/i, "\n---\n") # remove the media tag - s.gsub!(/\[\/?media[^\]]*\]/i, "\n") - s.gsub!(/\[\/?flash[^\]]*\]/i, "\n") - s.gsub!(/\[\/?audio[^\]]*\]/i, "\n") - s.gsub!(/\[\/?video[^\]]*\]/i, "\n") + s.gsub!(%r{\[/?media[^\]]*\]}i, "\n") + s.gsub!(%r{\[/?flash[^\]]*\]}i, "\n") + s.gsub!(%r{\[/?audio[^\]]*\]}i, "\n") + s.gsub!(%r{\[/?video[^\]]*\]}i, "\n") # Remove the font, p and backcolor tag # Discourse doesn't support the font tag - s.gsub!(/\[font=[^\]]*?\]/i, '') - s.gsub!(/\[\/font\]/i, '') - s.gsub!(/\[p=[^\]]*?\]/i, '') - s.gsub!(/\[\/p\]/i, '') - s.gsub!(/\[backcolor=[^\]]*?\]/i, '') - s.gsub!(/\[\/backcolor\]/i, '') + s.gsub!(/\[font=[^\]]*?\]/i, "") + s.gsub!(%r{\[/font\]}i, "") + s.gsub!(/\[p=[^\]]*?\]/i, "") + s.gsub!(%r{\[/p\]}i, "") + s.gsub!(/\[backcolor=[^\]]*?\]/i, "") + s.gsub!(%r{\[/backcolor\]}i, "") # Remove the size tag # I really have no idea what is this - s.gsub!(/\[size=[^\]]*?\]/i, '') - s.gsub!(/\[\/size\]/i, '') + s.gsub!(/\[size=[^\]]*?\]/i, "") + s.gsub!(%r{\[/size\]}i, "") # Remove the color tag - s.gsub!(/\[color=[^\]]*?\]/i, '') - s.gsub!(/\[\/color\]/i, '') + s.gsub!(/\[color=[^\]]*?\]/i, "") + s.gsub!(%r{\[/color\]}i, "") # Remove the hide tag - s.gsub!(/\[\/?hide\]/i, '') - s.gsub!(/\[\/?free[^\]]*\]/i, "\n") + s.gsub!(%r{\[/?hide\]}i, "") + s.gsub!(%r{\[/?free[^\]]*\]}i, "\n") # Remove the align tag # still don't know what it is s.gsub!(/\[align=[^\]]*?\]/i, "\n") - s.gsub!(/\[\/align\]/i, "\n") + s.gsub!(%r{\[/align\]}i, "\n") s.gsub!(/\[float=[^\]]*?\]/i, "\n") - s.gsub!(/\[\/float\]/i, "\n") + s.gsub!(%r{\[/float\]}i, "\n") # Convert code - s.gsub!(/\[\/?code\]/i, "\n```\n") + s.gsub!(%r{\[/?code\]}i, "\n```\n") # The edit notice should be removed # example: 本帖最后由 Helloworld 于 2015-1-28 22:05 编辑 - s.gsub!(/\[i=s\] 本帖最后由[\s\S]*?编辑 \[\/i\]/, '') + s.gsub!(%r{\[i=s\] 本帖最后由[\s\S]*?编辑 \[/i\]}, "") # Convert the custom smileys to emojis # `{:cry:}` to `:cry` @@ -653,35 +805,71 @@ class ImportScripts::DiscuzX < ImportScripts::Base # Replace internal forum links that aren't in the format # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) - s.gsub!(/\[list\](.*?)\[\/list:u\]/m, '[ul]\1[/ul]') - s.gsub!(/\[list=1\](.*?)\[\/list:o\]/m, '[ol]\1[/ol]') + s.gsub!(%r{\[list\](.*?)\[/list:u\]}m, '[ul]\1[/ul]') + s.gsub!(%r{\[list=1\](.*?)\[/list:o\]}m, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - s.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]') + s.gsub!(%r{\[\*\](.*?)\[/\*:m\]}, '[li]\1[/li]') # Discuz can create PM out of a post, which will generates like # [url=http://example.com/forum.php?mod=redirect&goto=findpost&pid=111&ptid=11][b]关于您在“主题名称”的帖子[/b][/url] - s.gsub!(pm_url_regexp) do |discuzx_link| - replace_internal_link(discuzx_link, $1) - end + s.gsub!(pm_url_regexp) { |discuzx_link| replace_internal_link(discuzx_link, $1) } # [url][b]text[/b][/url] to **[url]text[/url]** - s.gsub!(/(\[url=[^\[\]]*?\])\[b\](\S*)\[\/b\](\[\/url\])/, '**\1\2\3**') + s.gsub!(%r{(\[url=[^\[\]]*?\])\[b\](\S*)\[/b\](\[/url\])}, '**\1\2\3**') @internal_url_regexps.each do |internal_url_regexp| s.gsub!(internal_url_regexp) do |discuzx_link| - replace_internal_link(discuzx_link, ($~[:tid].to_i rescue nil), ($~[:pid].to_i rescue nil), ($~[:fid].to_i rescue nil), ($~[:action] rescue nil)) + replace_internal_link( + discuzx_link, + ( + begin + $~[:tid].to_i + rescue StandardError + nil + end + ), + ( + begin + $~[:pid].to_i + rescue StandardError + nil + end + ), + ( + begin + $~[:fid].to_i + rescue StandardError + nil + end + ), + ( + begin + $~[:action] + rescue StandardError + nil + end + ), + ) end end # @someone without the url - s.gsub!(/@\[url=[^\[\]]*?\](\S*)\[\/url\]/i, '@\1') + s.gsub!(%r{@\[url=[^\[\]]*?\](\S*)\[/url\]}i, '@\1') - s.scan(/http(?:s)?:\/\/#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}\/[^\[\]\s]*/) { |link|puts "WARNING: post #{import_id} can't replace internal url #{link}" } + s.scan(%r{http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/[^\[\]\s]*}) do |link| + puts "WARNING: post #{import_id} can't replace internal url #{link}" + end s.strip end - def replace_internal_link(discuzx_link, import_topic_id, import_post_id, import_category_id, action) + def replace_internal_link( + discuzx_link, + import_topic_id, + import_post_id, + import_category_id, + action + ) if import_post_id post_id = post_id_from_imported_post_id import_post_id if post_id @@ -691,15 +879,17 @@ class ImportScripts::DiscuzX < ImportScripts::Base end if import_topic_id - - results = mysql_query("SELECT pid - FROM #{table_name 'forum_post'} + results = + mysql_query( + "SELECT pid + FROM #{table_name "forum_post"} WHERE tid = #{import_topic_id} AND first - LIMIT 1") + LIMIT 1", + ) return discuzx_link unless results.size > 0 - linked_post_id = results.first['pid'] + linked_post_id = results.first["pid"] lookup = topic_lookup_from_imported_post_id(linked_post_id) if lookup @@ -707,7 +897,6 @@ class ImportScripts::DiscuzX < ImportScripts::Base else return discuzx_link end - end if import_category_id @@ -719,9 +908,9 @@ class ImportScripts::DiscuzX < ImportScripts::Base end case action - when 'index' + when "index" return "#{NEW_SITE_PREFIX}/" - when 'stat', 'stats', 'ranklist' + when "stat", "stats", "ranklist" return "#{NEW_SITE_PREFIX}/users" end @@ -729,28 +918,32 @@ class ImportScripts::DiscuzX < ImportScripts::Base end def pm_url_regexp - @pm_url_regexp ||= Regexp.new("http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub('.', '\.')}/forum\\.php\\?mod=redirect&goto=findpost&pid=\\d+&ptid=(\\d+)") + @pm_url_regexp ||= + Regexp.new( + "http(?:s)?://#{ORIGINAL_SITE_PREFIX.gsub(".", '\.')}/forum\\.php\\?mod=redirect&goto=findpost&pid=\\d+&ptid=(\\d+)", + ) end # This step is done separately because it can take multiple attempts to get right (because of # missing files, wrong paths, authorized extensions, etc.). def import_attachments - setting = AUTHORIZED_EXTENSIONS.join('|') + setting = AUTHORIZED_EXTENSIONS.join("|") SiteSetting.authorized_extensions = setting if setting != SiteSetting.authorized_extensions - attachment_regex = /\[attach\](\d+)\[\/attach\]/ - attachment_link_regex = /\[x-attach\](.+)\[\/x-attach\]/ + attachment_regex = %r{\[attach\](\d+)\[/attach\]} + attachment_link_regex = %r{\[x-attach\](.+)\[/x-attach\]} current_count = 0 - total_count = mysql_query("SELECT count(*) count FROM #{table_name 'forum_post'};").first['count'] + total_count = + mysql_query("SELECT count(*) count FROM #{table_name "forum_post"};").first["count"] success_count = 0 fail_count = 0 - puts '', "Importing attachments...", '' + puts "", "Importing attachments...", "" Post.find_each do |post| - next unless post.custom_fields['import_id'] == post.custom_fields['import_id'].to_i.to_s + next unless post.custom_fields["import_id"] == post.custom_fields["import_id"].to_i.to_s user = post.user @@ -786,17 +979,16 @@ class ImportScripts::DiscuzX < ImportScripts::Base html_for_upload(upload, filename) end - sql = "SELECT aid - FROM #{table_name 'forum_attachment'} - WHERE pid = #{post.custom_fields['import_id']}" - if !inline_attachments.empty? - sql = "#{sql} AND aid NOT IN (#{inline_attachments.join(',')})" - end + sql = + "SELECT aid + FROM #{table_name "forum_attachment"} + WHERE pid = #{post.custom_fields["import_id"]}" + sql = "#{sql} AND aid NOT IN (#{inline_attachments.join(",")})" if !inline_attachments.empty? results = mysql_query(sql) results.each do |attachment| - attachment_id = attachment['aid'] + attachment_id = attachment["aid"] upload, filename = find_upload(user, post, attachment_id) unless upload fail_count += 1 @@ -810,21 +1002,26 @@ class ImportScripts::DiscuzX < ImportScripts::Base end if new_raw != post.raw - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: '从 Discuz 中导入附件') + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + bypass_bump: true, + edit_reason: "从 Discuz 中导入附件", + ) end success_count += 1 end - puts '', '' + puts "", "" puts "succeeded: #{success_count}" puts " failed: #{fail_count}" if fail_count > 0 - puts '' + puts "" end # Create the full path to the discuz avatar specified from user id def discuzx_avatar_fullpath(user_id, absolute = true) - padded_id = user_id.to_s.rjust(9, '0') + padded_id = user_id.to_s.rjust(9, "0") part_1 = padded_id[0..2] part_2 = padded_id[3..4] @@ -844,9 +1041,9 @@ class ImportScripts::DiscuzX < ImportScripts::Base case raw when /\[url=forum.php\?mod=redirect&goto=findpost&pid=(\d+)&ptid=\d+\]/ #standard $1 - when /\[url=https?:\/\/#{ORIGINAL_SITE_PREFIX}\/redirect.php\?goto=findpost&pid=(\d+)&ptid=\d+\]/ # old discuz 7 format + when %r{\[url=https?://#{ORIGINAL_SITE_PREFIX}/redirect.php\?goto=findpost&pid=(\d+)&ptid=\d+\]} # old discuz 7 format $1 - when /\[quote\][\S\s]*pid=(\d+)[\S\s]*\[\/quote\]/ # quote + when %r{\[quote\][\S\s]*pid=(\d+)[\S\s]*\[/quote\]} # quote $1 end end @@ -856,18 +1053,18 @@ class ImportScripts::DiscuzX < ImportScripts::Base def upload_inline_image(data) return unless data - puts 'Creating inline image' + puts "Creating inline image" - encoded_photo = data['data:image/png;base64,'.length .. -1] + encoded_photo = data["data:image/png;base64,".length..-1] if encoded_photo raw_file = Base64.decode64(encoded_photo) else - puts 'Error parsed inline photo', data[0..20] + puts "Error parsed inline photo", data[0..20] return end real_filename = "#{SecureRandom.hex}.png" - filename = Tempfile.new(['inline', '.png']) + filename = Tempfile.new(%w[inline .png]) begin filename.binmode filename.write(raw_file) @@ -875,8 +1072,16 @@ class ImportScripts::DiscuzX < ImportScripts::Base upload = create_upload(Discourse::SYSTEM_USER_ID, filename, real_filename) ensure - filename.close rescue nil - filename.unlink rescue nil + begin + filename.close + rescue StandardError + nil + end + begin + filename.unlink + rescue StandardError + nil + end end if upload.nil? || !upload.valid? @@ -890,23 +1095,25 @@ class ImportScripts::DiscuzX < ImportScripts::Base # find the uploaded file and real name from the db def find_upload(user, post, upload_id) - attachment_table = table_name 'forum_attachment' + attachment_table = table_name "forum_attachment" # search for table id - sql = "SELECT a.pid post_id, + sql = + "SELECT a.pid post_id, a.aid upload_id, a.tableid table_id FROM #{attachment_table} a - WHERE a.pid = #{post.custom_fields['import_id']} + WHERE a.pid = #{post.custom_fields["import_id"]} AND a.aid = #{upload_id};" results = mysql_query(sql) unless (meta_data = results.first) - puts "Couldn't find forum_attachment record meta data for post.id = #{post.id}, import_id = #{post.custom_fields['import_id']}" + puts "Couldn't find forum_attachment record meta data for post.id = #{post.id}, import_id = #{post.custom_fields["import_id"]}" return nil end # search for uploaded file meta data - sql = "SELECT a.pid post_id, + sql = + "SELECT a.pid post_id, a.aid upload_id, a.tid topic_id, a.uid user_id, @@ -917,22 +1124,22 @@ class ImportScripts::DiscuzX < ImportScripts::Base a.description description, a.isimage is_image, a.thumb is_thumb - FROM #{attachment_table}_#{meta_data['table_id']} a + FROM #{attachment_table}_#{meta_data["table_id"]} a WHERE a.aid = #{upload_id};" results = mysql_query(sql) unless (row = results.first) - puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields['import_id']}" + puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields["import_id"]}" return nil end - filename = File.join(DISCUZX_BASE_DIR, ATTACHMENT_DIR, row['attachment_path']) + filename = File.join(DISCUZX_BASE_DIR, ATTACHMENT_DIR, row["attachment_path"]) unless File.exist?(filename) puts "Attachment file doesn't exist: #{filename}" return nil end - real_filename = row['real_filename'] - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + real_filename = row["real_filename"] + real_filename.prepend SecureRandom.hex if real_filename[0] == "." upload = create_upload(user.id, filename, real_filename) if upload.nil? || !upload.valid? @@ -950,7 +1157,7 @@ class ImportScripts::DiscuzX < ImportScripts::Base end def first_exists(*items) - items.find { |item|!item.blank? } || '' + items.find { |item| !item.blank? } || "" end def mysql_query(sql) diff --git a/script/import_scripts/disqus.rb b/script/import_scripts/disqus.rb index 5dbeb08775..1b6f4185a5 100644 --- a/script/import_scripts/disqus.rb +++ b/script/import_scripts/disqus.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'nokogiri' -require 'optparse' +require "nokogiri" +require "optparse" require File.expand_path(File.dirname(__FILE__) + "/base") class ImportScripts::Disqus < ImportScripts::Base @@ -35,7 +35,7 @@ class ImportScripts::Disqus < ImportScripts::Base by_email = {} @parser.posts.each do |id, p| - next if p[:is_spam] == 'true' || p[:is_deleted] == 'true' + next if p[:is_spam] == "true" || p[:is_deleted] == "true" by_email[p[:author_email]] = { name: p[:author_name], username: p[:author_username] } end @@ -45,13 +45,7 @@ class ImportScripts::Disqus < ImportScripts::Base create_users(by_email.keys) do |email| user = by_email[email] - { - id: email, - email: email, - username: user[:username], - name: user[:name], - merge: true - } + { id: email, email: email, username: user[:username], name: user[:name], merge: true } end end @@ -59,7 +53,6 @@ class ImportScripts::Disqus < ImportScripts::Base puts "", "importing topics..." @parser.threads.each do |id, t| - title = t[:title] title.gsub!(/“/, '"') title.gsub!(/”/, '"') @@ -79,7 +72,7 @@ class ImportScripts::Disqus < ImportScripts::Base if post.present? && post.topic.posts_count <= 1 (t[:posts] || []).each do |p| - post_user = find_existing_user(p[:author_email] || '', p[:author_username]) + post_user = find_existing_user(p[:author_email] || "", p[:author_username]) next unless post_user.present? attrs = { @@ -87,7 +80,7 @@ class ImportScripts::Disqus < ImportScripts::Base topic_id: post.topic_id, raw: p[:cooked], cooked: p[:cooked], - created_at: Date.parse(p[:created_at]) + created_at: Date.parse(p[:created_at]), } if p[:parent_id] @@ -125,23 +118,22 @@ class DisqusSAX < Nokogiri::XML::SAX::Document end def start_element(name, attrs = []) - hashed = Hash[attrs] case name - when 'post' + when "post" @post = {} - @post[:id] = hashed['dsq:id'] if @post - when 'thread' - id = hashed['dsq:id'] + @post[:id] = hashed["dsq:id"] if @post + when "thread" + id = hashed["dsq:id"] if @post thread = @threads[id] thread[:posts] << @post else @thread = { id: id, posts: [] } end - when 'parent' + when "parent" if @post - id = hashed['dsq:id'] + id = hashed["dsq:id"] @post[:parent_id] = id end end @@ -151,10 +143,10 @@ class DisqusSAX < Nokogiri::XML::SAX::Document def end_element(name) case name - when 'post' + when "post" @posts[@post[:id]] = @post @post = nil - when 'thread' + when "thread" if @post.nil? @threads[@thread[:id]] = @thread @thread = nil @@ -165,25 +157,25 @@ class DisqusSAX < Nokogiri::XML::SAX::Document end def characters(str) - record(@post, :author_email, str, 'author', 'email') - record(@post, :author_name, str, 'author', 'name') - record(@post, :author_username, str, 'author', 'username') - record(@post, :author_anonymous, str, 'author', 'isAnonymous') - record(@post, :created_at, str, 'createdAt') - record(@post, :is_deleted, str, 'isDeleted') - record(@post, :is_spam, str, 'isSpam') + record(@post, :author_email, str, "author", "email") + record(@post, :author_name, str, "author", "name") + record(@post, :author_username, str, "author", "username") + record(@post, :author_anonymous, str, "author", "isAnonymous") + record(@post, :created_at, str, "createdAt") + record(@post, :is_deleted, str, "isDeleted") + record(@post, :is_spam, str, "isSpam") - record(@thread, :link, str, 'link') - record(@thread, :title, str, 'title') - record(@thread, :created_at, str, 'createdAt') - record(@thread, :author_email, str, 'author', 'email') - record(@thread, :author_name, str, 'author', 'name') - record(@thread, :author_username, str, 'author', 'username') - record(@thread, :author_anonymous, str, 'author', 'isAnonymous') + record(@thread, :link, str, "link") + record(@thread, :title, str, "title") + record(@thread, :created_at, str, "createdAt") + record(@thread, :author_email, str, "author", "email") + record(@thread, :author_name, str, "author", "name") + record(@thread, :author_username, str, "author", "username") + record(@thread, :author_anonymous, str, "author", "isAnonymous") end def cdata_block(str) - record(@post, :cooked, str, 'message') + record(@post, :cooked, str, "message") end def record(target, sym, str, *params) @@ -205,7 +197,7 @@ class DisqusSAX < Nokogiri::XML::SAX::Document # Remove any threads that have no posts @threads.delete(id) else - t[:posts].delete_if { |p| p[:is_spam] == 'true' || p[:is_deleted] == 'true' } + t[:posts].delete_if { |p| p[:is_spam] == "true" || p[:is_deleted] == "true" } end end diff --git a/script/import_scripts/drupal-6.rb b/script/import_scripts/drupal-6.rb index 182596c63c..3e27c0becd 100644 --- a/script/import_scripts/drupal-6.rb +++ b/script/import_scripts/drupal-6.rb @@ -4,19 +4,19 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Drupal < ImportScripts::Base - - DRUPAL_DB = ENV['DRUPAL_DB'] || "newsite3" - VID = ENV['DRUPAL_VID'] || 1 + DRUPAL_DB = ENV["DRUPAL_DB"] || "newsite3" + VID = ENV["DRUPAL_VID"] || 1 def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - #password: "password", - database: DRUPAL_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + #password: "password", + database: DRUPAL_DB, + ) end def categories_query @@ -25,7 +25,12 @@ class ImportScripts::Drupal < ImportScripts::Base def execute create_users(@client.query("SELECT uid id, name, mail email, created FROM users;")) do |row| - { id: row['id'], username: row['name'], email: row['email'], created_at: Time.zone.at(row['created']) } + { + id: row["id"], + username: row["name"], + email: row["email"], + created_at: Time.zone.at(row["created"]), + } end # You'll need to edit the following query for your Drupal install: @@ -34,38 +39,36 @@ class ImportScripts::Drupal < ImportScripts::Base # * Table name may be term_data. # * May need to select a vid other than 1. create_categories(categories_query) do |c| - { id: c['tid'], name: c['name'], description: c['description'] } + { id: c["tid"], name: c["name"], description: c["description"] } end # "Nodes" in Drupal are divided into types. Here we import two types, # and will later import all the comments/replies for each node. # You will need to figure out what the type names are on your install and edit the queries to match. - if ENV['DRUPAL_IMPORT_BLOG'] - create_blog_topics - end + create_blog_topics if ENV["DRUPAL_IMPORT_BLOG"] create_forum_topics create_replies begin - create_admin(email: 'neil.lalonde@discourse.org', username: UserNameSuggester.suggest('neil')) + create_admin(email: "neil.lalonde@discourse.org", username: UserNameSuggester.suggest("neil")) rescue => e - puts '', "Failed to create admin user" + puts "", "Failed to create admin user" puts e.message end end def create_blog_topics - puts '', "creating blog topics" + puts "", "creating blog topics" - create_category({ - name: 'Blog', - user_id: -1, - description: "Articles from the blog" - }, nil) unless Category.find_by_name('Blog') + unless Category.find_by_name("Blog") + create_category({ name: "Blog", user_id: -1, description: "Articles from the blog" }, nil) + end - results = @client.query(" + results = + @client.query( + " SELECT n.nid nid, n.title title, n.uid uid, @@ -76,37 +79,48 @@ class ImportScripts::Drupal < ImportScripts::Base LEFT JOIN node_revisions nr ON nr.vid=n.vid WHERE n.type = 'blog' AND n.status = 1 - ", cache_rows: false) + ", + cache_rows: false, + ) create_posts(results) do |row| { - id: "nid:#{row['nid']}", - user_id: user_id_from_imported_user_id(row['uid']) || -1, - category: 'Blog', - raw: row['body'], - created_at: Time.zone.at(row['created']), - pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil, - title: row['title'].try(:strip), - custom_fields: { import_id: "nid:#{row['nid']}" } + id: "nid:#{row["nid"]}", + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + category: "Blog", + raw: row["body"], + created_at: Time.zone.at(row["created"]), + pinned_at: row["sticky"].to_i == 1 ? Time.zone.at(row["created"]) : nil, + title: row["title"].try(:strip), + custom_fields: { + import_id: "nid:#{row["nid"]}", + }, } end end def create_forum_topics - puts '', "creating forum topics" + puts "", "creating forum topics" - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(*) count FROM node n LEFT JOIN forum f ON f.vid=n.vid WHERE n.type = 'forum' AND n.status = 1 - ").first['count'] + ", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - results = @client.query(" + results = + @client.query( + " SELECT n.nid nid, n.title title, f.tid tid, @@ -121,48 +135,57 @@ class ImportScripts::Drupal < ImportScripts::Base AND n.status = 1 LIMIT #{batch_size} OFFSET #{offset}; - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "nid:#{p['nid']}" } + next if all_records_exist? :posts, results.map { |p| "nid:#{p["nid"]}" } create_posts(results, total: total_count, offset: offset) do |row| { - id: "nid:#{row['nid']}", - user_id: user_id_from_imported_user_id(row['uid']) || -1, - category: category_id_from_imported_category_id(row['tid']), - raw: row['body'], - created_at: Time.zone.at(row['created']), - pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil, - title: row['title'].try(:strip) + id: "nid:#{row["nid"]}", + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + category: category_id_from_imported_category_id(row["tid"]), + raw: row["body"], + created_at: Time.zone.at(row["created"]), + pinned_at: row["sticky"].to_i == 1 ? Time.zone.at(row["created"]) : nil, + title: row["title"].try(:strip), } end end end def create_replies - puts '', "creating replies in topics" + puts "", "creating replies in topics" - if ENV['DRUPAL_IMPORT_BLOG'] + if ENV["DRUPAL_IMPORT_BLOG"] node_types = "('forum','blog')" else node_types = "('forum')" end - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(*) count FROM comments c LEFT JOIN node n ON n.nid=c.nid WHERE n.type IN #{node_types} AND n.status = 1 AND c.status=0; - ").first['count'] + ", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - results = @client.query(" + results = + @client.query( + " SELECT c.cid, c.pid, c.nid, @@ -176,37 +199,36 @@ class ImportScripts::Drupal < ImportScripts::Base AND c.status=0 LIMIT #{batch_size} OFFSET #{offset}; - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "cid:#{p['cid']}" } + next if all_records_exist? :posts, results.map { |p| "cid:#{p["cid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}") + topic_mapping = topic_lookup_from_imported_post_id("nid:#{row["nid"]}") if topic_mapping && topic_id = topic_mapping[:topic_id] h = { - id: "cid:#{row['cid']}", + id: "cid:#{row["cid"]}", topic_id: topic_id, - user_id: user_id_from_imported_user_id(row['uid']) || -1, - raw: row['body'], - created_at: Time.zone.at(row['timestamp']), + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + raw: row["body"], + created_at: Time.zone.at(row["timestamp"]), } - if row['pid'] - parent = topic_lookup_from_imported_post_id("cid:#{row['pid']}") + if row["pid"] + parent = topic_lookup_from_imported_post_id("cid:#{row["pid"]}") h[:reply_to_post_number] = parent[:post_number] if parent && parent[:post_number] > (1) end h else - puts "No topic found for comment #{row['cid']}" + puts "No topic found for comment #{row["cid"]}" nil end end end end - end -if __FILE__ == $0 - ImportScripts::Drupal.new.perform -end +ImportScripts::Drupal.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/drupal.rb b/script/import_scripts/drupal.rb index 2350a4efbf..ac01a2daa4 100644 --- a/script/import_scripts/drupal.rb +++ b/script/import_scripts/drupal.rb @@ -5,9 +5,8 @@ require "htmlentities" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Drupal < ImportScripts::Base - - DRUPAL_DB = ENV['DRUPAL_DB'] || "drupal" - VID = ENV['DRUPAL_VID'] || 1 + DRUPAL_DB = ENV["DRUPAL_DB"] || "drupal" + VID = ENV["DRUPAL_VID"] || 1 BATCH_SIZE = 1000 ATTACHMENT_DIR = "/root/files/upload" @@ -16,25 +15,23 @@ class ImportScripts::Drupal < ImportScripts::Base @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - #password: "password", - database: DRUPAL_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + #password: "password", + database: DRUPAL_DB, + ) end def execute - import_users import_categories # "Nodes" in Drupal are divided into types. Here we import two types, # and will later import all the comments/replies for each node. # You will need to figure out what the type names are on your install and edit the queries to match. - if ENV['DRUPAL_IMPORT_BLOG'] - import_blog_topics - end + import_blog_topics if ENV["DRUPAL_IMPORT_BLOG"] import_forum_topics @@ -56,7 +53,7 @@ class ImportScripts::Drupal < ImportScripts::Base last_user_id = -1 batches(BATCH_SIZE) do |offset| - users = mysql_query(<<-SQL + users = mysql_query(<<-SQL).to_a SELECT uid, name username, mail email, @@ -66,7 +63,6 @@ class ImportScripts::Drupal < ImportScripts::Base ORDER BY uid LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -80,12 +76,7 @@ class ImportScripts::Drupal < ImportScripts::Base username = @htmlentities.decode(user["username"]).strip - { - id: user["uid"], - name: username, - email: email, - created_at: Time.zone.at(user["created"]) - } + { id: user["uid"], name: username, email: email, created_at: Time.zone.at(user["created"]) } end end end @@ -99,35 +90,31 @@ class ImportScripts::Drupal < ImportScripts::Base puts "", "importing categories" - categories = mysql_query(<<-SQL + categories = mysql_query(<<-SQL).to_a SELECT tid, name, description FROM taxonomy_term_data WHERE vid = #{VID} SQL - ).to_a create_categories(categories) do |category| { - id: category['tid'], - name: @htmlentities.decode(category['name']).strip, - description: @htmlentities.decode(category['description']).strip + id: category["tid"], + name: @htmlentities.decode(category["name"]).strip, + description: @htmlentities.decode(category["description"]).strip, } end end def import_blog_topics - puts '', "importing blog topics" + puts "", "importing blog topics" - create_category( - { - name: 'Blog', - description: "Articles from the blog" - }, - nil) unless Category.find_by_name('Blog') + unless Category.find_by_name("Blog") + create_category({ name: "Blog", description: "Articles from the blog" }, nil) + end - blogs = mysql_query(<<-SQL + blogs = mysql_query(<<-SQL).to_a SELECT n.nid nid, n.title title, n.uid uid, n.created created, n.sticky sticky, f.body_value body FROM node n, @@ -136,38 +123,38 @@ class ImportScripts::Drupal < ImportScripts::Base AND n.nid = f.entity_id AND n.status = 1 SQL - ).to_a - category_id = Category.find_by_name('Blog').id + category_id = Category.find_by_name("Blog").id create_posts(blogs) do |topic| { - id: "nid:#{topic['nid']}", - user_id: user_id_from_imported_user_id(topic['uid']) || -1, + id: "nid:#{topic["nid"]}", + user_id: user_id_from_imported_user_id(topic["uid"]) || -1, category: category_id, - raw: topic['body'], - created_at: Time.zone.at(topic['created']), - pinned_at: topic['sticky'].to_i == 1 ? Time.zone.at(topic['created']) : nil, - title: topic['title'].try(:strip), - custom_fields: { import_id: "nid:#{topic['nid']}" } + raw: topic["body"], + created_at: Time.zone.at(topic["created"]), + pinned_at: topic["sticky"].to_i == 1 ? Time.zone.at(topic["created"]) : nil, + title: topic["title"].try(:strip), + custom_fields: { + import_id: "nid:#{topic["nid"]}", + }, } end end def import_forum_topics - puts '', "importing forum topics" + puts "", "importing forum topics" - total_count = mysql_query(<<-SQL + total_count = mysql_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM forum_index fi, node n WHERE n.type = 'forum' AND fi.nid = n.nid AND n.status = 1 SQL - ).first['count'] batches(BATCH_SIZE) do |offset| - results = mysql_query(<<-SQL + results = mysql_query(<<-SQL).to_a SELECT fi.nid nid, fi.title title, fi.tid tid, @@ -188,34 +175,33 @@ class ImportScripts::Drupal < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset}; SQL - ).to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "nid:#{p['nid']}" } + next if all_records_exist? :posts, results.map { |p| "nid:#{p["nid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - raw = preprocess_raw(row['body']) + raw = preprocess_raw(row["body"]) topic = { - id: "nid:#{row['nid']}", - user_id: user_id_from_imported_user_id(row['uid']) || -1, - category: category_id_from_imported_category_id(row['tid']), + id: "nid:#{row["nid"]}", + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + category: category_id_from_imported_category_id(row["tid"]), raw: raw, - created_at: Time.zone.at(row['created']), - pinned_at: row['sticky'].to_i == 1 ? Time.zone.at(row['created']) : nil, - title: row['title'].try(:strip), - views: row['views'] + created_at: Time.zone.at(row["created"]), + pinned_at: row["sticky"].to_i == 1 ? Time.zone.at(row["created"]) : nil, + title: row["title"].try(:strip), + views: row["views"], } - topic[:custom_fields] = { import_solved: true } if row['solved'].present? + topic[:custom_fields] = { import_solved: true } if row["solved"].present? topic end end end def import_replies - puts '', "creating replies in topics" + puts "", "creating replies in topics" - total_count = mysql_query(<<-SQL + total_count = mysql_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM comment c, node n @@ -224,10 +210,9 @@ class ImportScripts::Drupal < ImportScripts::Base AND n.type IN ('article', 'forum') AND n.status = 1 SQL - ).first['count'] batches(BATCH_SIZE) do |offset| - results = mysql_query(<<-SQL + results = mysql_query(<<-SQL).to_a SELECT c.cid, c.pid, c.nid, c.uid, c.created, f.comment_body_value body FROM comment c, @@ -241,30 +226,29 @@ class ImportScripts::Drupal < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ).to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "cid:#{p['cid']}" } + next if all_records_exist? :posts, results.map { |p| "cid:#{p["cid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}") + topic_mapping = topic_lookup_from_imported_post_id("nid:#{row["nid"]}") if topic_mapping && topic_id = topic_mapping[:topic_id] - raw = preprocess_raw(row['body']) + raw = preprocess_raw(row["body"]) h = { - id: "cid:#{row['cid']}", + id: "cid:#{row["cid"]}", topic_id: topic_id, - user_id: user_id_from_imported_user_id(row['uid']) || -1, + user_id: user_id_from_imported_user_id(row["uid"]) || -1, raw: raw, - created_at: Time.zone.at(row['created']), + created_at: Time.zone.at(row["created"]), } - if row['pid'] - parent = topic_lookup_from_imported_post_id("cid:#{row['pid']}") + if row["pid"] + parent = topic_lookup_from_imported_post_id("cid:#{row["pid"]}") h[:reply_to_post_number] = parent[:post_number] if parent && parent[:post_number] > (1) end h else - puts "No topic found for comment #{row['cid']}" + puts "No topic found for comment #{row["cid"]}" nil end end @@ -275,7 +259,7 @@ class ImportScripts::Drupal < ImportScripts::Base puts "", "importing post likes" batches(BATCH_SIZE) do |offset| - likes = mysql_query(<<-SQL + likes = mysql_query(<<-SQL).to_a SELECT flagging_id, fid, entity_id, @@ -286,17 +270,20 @@ class ImportScripts::Drupal < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ).to_a break if likes.empty? likes.each do |l| - identifier = l['fid'] == 5 ? 'nid' : 'cid' - next unless user_id = user_id_from_imported_user_id(l['uid']) - next unless post_id = post_id_from_imported_post_id("#{identifier}:#{l['entity_id']}") + identifier = l["fid"] == 5 ? "nid" : "cid" + next unless user_id = user_id_from_imported_user_id(l["uid"]) + next unless post_id = post_id_from_imported_post_id("#{identifier}:#{l["entity_id"]}") next unless user = User.find_by(id: user_id) next unless post = Post.find_by(id: post_id) - PostActionCreator.like(user, post) rescue nil + begin + PostActionCreator.like(user, post) + rescue StandardError + nil + end end end end @@ -304,7 +291,8 @@ class ImportScripts::Drupal < ImportScripts::Base def mark_topics_as_solved puts "", "marking topics as solved" - solved_topics = TopicCustomField.where(name: "import_solved").where(value: true).pluck(:topic_id) + solved_topics = + TopicCustomField.where(name: "import_solved").where(value: true).pluck(:topic_id) solved_topics.each do |topic_id| next unless topic = Topic.find(topic_id) @@ -336,8 +324,13 @@ class ImportScripts::Drupal < ImportScripts::Base begin current_count += 1 print_status(current_count, total_count, start_time) - SingleSignOnRecord.create!(user_id: user.id, external_id: external_id, external_email: user.email, last_payload: '') - rescue + SingleSignOnRecord.create!( + user_id: user.id, + external_id: external_id, + external_email: user.email, + last_payload: "", + ) + rescue StandardError next end end @@ -350,14 +343,13 @@ class ImportScripts::Drupal < ImportScripts::Base success_count = 0 fail_count = 0 - total_count = mysql_query(<<-SQL + total_count = mysql_query(<<-SQL).first["count"] SELECT count(field_post_attachment_fid) count FROM field_data_field_post_attachment SQL - ).first["count"] batches(BATCH_SIZE) do |offset| - attachments = mysql_query(<<-SQL + attachments = mysql_query(<<-SQL).to_a SELECT * FROM field_data_field_post_attachment fp LEFT JOIN file_managed fm @@ -365,7 +357,6 @@ class ImportScripts::Drupal < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ).to_a break if attachments.size < 1 @@ -373,9 +364,11 @@ class ImportScripts::Drupal < ImportScripts::Base current_count += 1 print_status current_count, total_count - identifier = attachment['entity_type'] == "comment" ? "cid" : "nid" - next unless user_id = user_id_from_imported_user_id(attachment['uid']) - next unless post_id = post_id_from_imported_post_id("#{identifier}:#{attachment['entity_id']}") + identifier = attachment["entity_type"] == "comment" ? "cid" : "nid" + next unless user_id = user_id_from_imported_user_id(attachment["uid"]) + unless post_id = post_id_from_imported_post_id("#{identifier}:#{attachment["entity_id"]}") + next + end next unless user = User.find(user_id) next unless post = Post.find(post_id) @@ -392,9 +385,14 @@ class ImportScripts::Drupal < ImportScripts::Base new_raw = "#{new_raw}\n\n#{upload_html}" unless new_raw.include?(upload_html) if new_raw != post.raw - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: "Import attachment from Drupal") + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + bypass_bump: true, + edit_reason: "Import attachment from Drupal", + ) else - puts '', 'Skipped upload: already imported' + puts "", "Skipped upload: already imported" end success_count += 1 @@ -406,13 +404,13 @@ class ImportScripts::Drupal < ImportScripts::Base end def create_permalinks - puts '', 'creating permalinks...' + puts "", "creating permalinks..." Topic.listable_topics.find_each do |topic| begin tcf = topic.custom_fields - if tcf && tcf['import_id'] - node_id = tcf['import_id'][/nid:(\d+)/, 1] + if tcf && tcf["import_id"] + node_id = tcf["import_id"][/nid:(\d+)/, 1] slug = "/node/#{node_id}" Permalink.create(url: slug, topic_id: topic.id) end @@ -424,18 +422,16 @@ class ImportScripts::Drupal < ImportScripts::Base end def find_upload(post, attachment) - uri = attachment['uri'][/public:\/\/upload\/(.+)/, 1] + uri = attachment["uri"][%r{public://upload/(.+)}, 1] real_filename = CGI.unescapeHTML(uri) file = File.join(ATTACHMENT_DIR, real_filename) unless File.exist?(file) - puts "Attachment file #{attachment['filename']} doesn't exist" + puts "Attachment file #{attachment["filename"]} doesn't exist" tmpfile = "attachments_failed.txt" - filename = File.join('/tmp/', tmpfile) - File.open(filename, 'a') { |f| - f.puts attachment['filename'] - } + filename = File.join("/tmp/", tmpfile) + File.open(filename, "a") { |f| f.puts attachment["filename"] } end upload = create_upload(post.user.id || -1, file, real_filename) @@ -452,13 +448,13 @@ class ImportScripts::Drupal < ImportScripts::Base def preprocess_raw(raw) return if raw.blank? # quotes on new lines - raw.gsub!(/\[quote\](.+?)\[\/quote\]/im) { |quote| - quote.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[quote\](.+?)\[/quote\]}im) do |quote| + quote.gsub!(%r{\[quote\](.+?)\[/quote\]}im) { "\n#{$1}\n" } quote.gsub!(/\n(.+?)/) { "\n> #{$1}" } - } + end # [QUOTE=]...[/QUOTE] - raw.gsub!(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do + raw.gsub!(%r{\[quote=([^;\]]+)\](.+?)\[/quote\]}im) do username, quote = $1, $2 "\n[quote=\"#{username}\"]\n#{quote}\n[/quote]\n" end @@ -468,7 +464,7 @@ class ImportScripts::Drupal < ImportScripts::Base end def postprocess_posts - puts '', 'postprocessing posts' + puts "", "postprocessing posts" current = 0 max = Post.count @@ -479,7 +475,7 @@ class ImportScripts::Drupal < ImportScripts::Base new_raw = raw.dup # replace old topic to new topic links - new_raw.gsub!(/https:\/\/site.com\/forum\/topic\/(\d+)/im) do + new_raw.gsub!(%r{https://site.com/forum/topic/(\d+)}im) do post_id = post_id_from_imported_post_id("nid:#{$1}") next unless post_id topic = Post.find(post_id).topic @@ -487,7 +483,7 @@ class ImportScripts::Drupal < ImportScripts::Base end # replace old comment to reply links - new_raw.gsub!(/https:\/\/site.com\/comment\/(\d+)#comment-\d+/im) do + new_raw.gsub!(%r{https://site.com/comment/(\d+)#comment-\d+}im) do post_id = post_id_from_imported_post_id("cid:#{$1}") next unless post_id post_ref = Post.find(post_id) @@ -498,8 +494,8 @@ class ImportScripts::Drupal < ImportScripts::Base post.raw = new_raw post.save end - rescue - puts '', "Failed rewrite on post: #{post.id}" + rescue StandardError + puts "", "Failed rewrite on post: #{post.id}" ensure print_status(current += 1, max) end @@ -507,15 +503,15 @@ class ImportScripts::Drupal < ImportScripts::Base end def import_gravatars - puts '', 'importing gravatars' + puts "", "importing gravatars" current = 0 max = User.count User.find_each do |user| begin user.create_user_avatar(user_id: user.id) unless user.user_avatar user.user_avatar.update_gravatar! - rescue - puts '', 'Failed avatar update on user #{user.id}' + rescue StandardError + puts "", 'Failed avatar update on user #{user.id}' ensure print_status(current += 1, max) end @@ -523,15 +519,12 @@ class ImportScripts::Drupal < ImportScripts::Base end def parse_datetime(time) - DateTime.strptime(time, '%s') + DateTime.strptime(time, "%s") end def mysql_query(sql) @client.query(sql, cache_rows: true) end - end -if __FILE__ == $0 - ImportScripts::Drupal.new.perform -end +ImportScripts::Drupal.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/drupal_json.rb b/script/import_scripts/drupal_json.rb index d69f21e01b..f97ae683e1 100644 --- a/script/import_scripts/drupal_json.rb +++ b/script/import_scripts/drupal_json.rb @@ -5,7 +5,6 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Edit the constants and initialize method for your import data. class ImportScripts::DrupalJson < ImportScripts::Base - JSON_FILES_DIR = "/Users/techapj/Documents" def initialize @@ -28,20 +27,18 @@ class ImportScripts::DrupalJson < ImportScripts::Base end def import_users - puts '', "Importing users" + puts "", "Importing users" create_users(@users_json) do |u| { id: u["uid"], name: u["name"], email: u["mail"], - created_at: Time.zone.at(u["created"].to_i) + created_at: Time.zone.at(u["created"].to_i), } end EmailToken.delete_all end end -if __FILE__ == $0 - ImportScripts::DrupalJson.new.perform -end +ImportScripts::DrupalJson.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/drupal_qa.rb b/script/import_scripts/drupal_qa.rb index a8febbd41c..948b04590d 100644 --- a/script/import_scripts/drupal_qa.rb +++ b/script/import_scripts/drupal_qa.rb @@ -5,41 +5,51 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") require File.expand_path(File.dirname(__FILE__) + "/drupal.rb") class ImportScripts::DrupalQA < ImportScripts::Drupal - def categories_query - result = @client.query("SELECT n.nid, GROUP_CONCAT(ti.tid) AS tids + result = + @client.query( + "SELECT n.nid, GROUP_CONCAT(ti.tid) AS tids FROM node AS n INNER JOIN taxonomy_index AS ti ON ti.nid = n.nid WHERE n.type = 'question' AND n.status = 1 - GROUP BY n.nid") + GROUP BY n.nid", + ) categories = {} result.each do |r| - tids = r['tids'] + tids = r["tids"] if tids.present? - tids = tids.split(',') + tids = tids.split(",") categories[tids[0].to_i] = true end end - @client.query("SELECT tid, name, description FROM taxonomy_term_data WHERE tid IN (#{categories.keys.join(',')})") + @client.query( + "SELECT tid, name, description FROM taxonomy_term_data WHERE tid IN (#{categories.keys.join(",")})", + ) end def create_forum_topics + puts "", "creating forum topics" - puts '', "creating forum topics" - - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(*) count FROM node n WHERE n.type = 'question' - AND n.status = 1;").first['count'] + AND n.status = 1;", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - results = @client.query(" + results = + @client.query( + " SELECT n.nid, n.title, GROUP_CONCAT(t.tid) AS tid, @@ -54,40 +64,48 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal GROUP BY n.nid, n.title, n.uid, n.created, f.body_value LIMIT #{batch_size} OFFSET #{offset} - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "nid:#{p['nid']}" } + next if all_records_exist? :posts, results.map { |p| "nid:#{p["nid"]}" } create_posts(results, total: total_count, offset: offset) do |row| { - id: "nid:#{row['nid']}", - user_id: user_id_from_imported_user_id(row['uid']) || -1, - category: category_id_from_imported_category_id((row['tid'] || '').split(',')[0]), - raw: row['body'], - created_at: Time.zone.at(row['created']), + id: "nid:#{row["nid"]}", + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + category: category_id_from_imported_category_id((row["tid"] || "").split(",")[0]), + raw: row["body"], + created_at: Time.zone.at(row["created"]), pinned_at: nil, - title: row['title'].try(:strip) + title: row["title"].try(:strip), } end end end def create_direct_replies - puts '', "creating replies in topics" + puts "", "creating replies in topics" - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(*) count FROM node n WHERE n.type = 'answer' - AND n.status = 1;").first['count'] + AND n.status = 1;", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - - results = @client.query(" + results = + @client.query( + " SELECT n.nid AS cid, q.field_answer_question_nid AS nid, n.uid, @@ -100,25 +118,27 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal AND n.type = 'answer' LIMIT #{batch_size} OFFSET #{offset} - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "cid:#{p['cid']}" } + next if all_records_exist? :posts, results.map { |p| "cid:#{p["cid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}") + topic_mapping = topic_lookup_from_imported_post_id("nid:#{row["nid"]}") if topic_mapping && topic_id = topic_mapping[:topic_id] h = { - id: "cid:#{row['cid']}", + id: "cid:#{row["cid"]}", topic_id: topic_id, - user_id: user_id_from_imported_user_id(row['uid']) || -1, - raw: row['body'], - created_at: Time.zone.at(row['created']), + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + raw: row["body"], + created_at: Time.zone.at(row["created"]), } h else - puts "No topic found for answer #{row['cid']}" + puts "No topic found for answer #{row["cid"]}" nil end end @@ -126,21 +146,27 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal end def create_nested_replies - puts '', "creating nested replies to posts in topics" + puts "", "creating nested replies to posts in topics" - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(c.cid) count FROM node n INNER JOIN comment AS c ON n.nid = c.nid WHERE n.type = 'question' - AND n.status = 1;").first['count'] + AND n.status = 1;", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - # WARNING: If there are more than 1000000 this might have to be revisited - results = @client.query(" + results = + @client.query( + " SELECT (c.cid + 1000000) as cid, c.nid, c.uid, @@ -153,45 +179,53 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal AND n.type = 'question' LIMIT #{batch_size} OFFSET #{offset} - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "cid:#{p['cid']}" } + next if all_records_exist? :posts, results.map { |p| "cid:#{p["cid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}") + topic_mapping = topic_lookup_from_imported_post_id("nid:#{row["nid"]}") if topic_mapping && topic_id = topic_mapping[:topic_id] h = { - id: "cid:#{row['cid']}", + id: "cid:#{row["cid"]}", topic_id: topic_id, - user_id: user_id_from_imported_user_id(row['uid']) || -1, - raw: row['body'], - created_at: Time.zone.at(row['created']), + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + raw: row["body"], + created_at: Time.zone.at(row["created"]), } h else - puts "No topic found for comment #{row['cid']}" + puts "No topic found for comment #{row["cid"]}" nil end end end - puts '', "creating nested replies to answers in topics" + puts "", "creating nested replies to answers in topics" - total_count = @client.query(" + total_count = + @client.query( + " SELECT COUNT(c.cid) count FROM node n INNER JOIN comment AS c ON n.nid = c.nid WHERE n.type = 'answer' - AND n.status = 1;").first['count'] + AND n.status = 1;", + ).first[ + "count" + ] batch_size = 1000 batches(batch_size) do |offset| - # WARNING: If there are more than 1000000 this might have to be revisited - results = @client.query(" + results = + @client.query( + " SELECT (c.cid + 1000000) as cid, q.field_answer_question_nid AS nid, c.uid, @@ -205,25 +239,27 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal AND n.type = 'answer' LIMIT #{batch_size} OFFSET #{offset} - ", cache_rows: false) + ", + cache_rows: false, + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| "cid:#{p['cid']}" } + next if all_records_exist? :posts, results.map { |p| "cid:#{p["cid"]}" } create_posts(results, total: total_count, offset: offset) do |row| - topic_mapping = topic_lookup_from_imported_post_id("nid:#{row['nid']}") + topic_mapping = topic_lookup_from_imported_post_id("nid:#{row["nid"]}") if topic_mapping && topic_id = topic_mapping[:topic_id] h = { - id: "cid:#{row['cid']}", + id: "cid:#{row["cid"]}", topic_id: topic_id, - user_id: user_id_from_imported_user_id(row['uid']) || -1, - raw: row['body'], - created_at: Time.zone.at(row['created']), + user_id: user_id_from_imported_user_id(row["uid"]) || -1, + raw: row["body"], + created_at: Time.zone.at(row["created"]), } h else - puts "No topic found for comment #{row['cid']}" + puts "No topic found for comment #{row["cid"]}" nil end end @@ -234,9 +270,6 @@ class ImportScripts::DrupalQA < ImportScripts::Drupal create_direct_replies create_nested_replies end - end -if __FILE__ == $0 - ImportScripts::DrupalQA.new.perform -end +ImportScripts::DrupalQA.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/elgg.rb b/script/import_scripts/elgg.rb index 6eb62eb031..1293e17179 100644 --- a/script/import_scripts/elgg.rb +++ b/script/import_scripts/elgg.rb @@ -1,22 +1,16 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Elgg < ImportScripts::Base - BATCH_SIZE ||= 1000 def initialize super - @client = Mysql2::Client.new( - host: "127.0.0.1", - port: "3306", - username: "", - database: "", - password: "" - ) + @client = + Mysql2::Client.new(host: "127.0.0.1", port: "3306", username: "", database: "", password: "") SiteSetting.max_username_length = 50 end @@ -31,7 +25,7 @@ class ImportScripts::Elgg < ImportScripts::Base def create_avatar(user, guid) puts "#{@path}" # Put your avatar at the root of discourse in this folder: - path_prefix = 'import/data/www/' + path_prefix = "import/data/www/" # https://github.com/Elgg/Elgg/blob/2fc9c1910a9169bbe4010026c61d8e41a5b56239/engine/classes/ElggDiskFilestore.php#L24 # const BUCKET_SIZE = 5000; bucket_size = 5000 @@ -40,13 +34,11 @@ class ImportScripts::Elgg < ImportScripts::Base bucket_id = [guid / bucket_size * bucket_size, 1].max avatar_path = File.join(path_prefix, bucket_id.to_s, "/#{guid}/profile/#{guid}master.jpg") - if File.exist?(avatar_path) - @uploader.create_avatar(user, avatar_path) - end + @uploader.create_avatar(user, avatar_path) if File.exist?(avatar_path) end def grant_admin(user, is_admin) - if is_admin == 'yes' + if is_admin == "yes" puts "", "#{user.username} is granted admin!" user.grant_admin! end @@ -56,10 +48,11 @@ class ImportScripts::Elgg < ImportScripts::Base puts "", "importing users..." last_user_id = -1 - total_users = mysql_query("select count(*) from elgg_users_entity where banned='no'").first["count"] + total_users = + mysql_query("select count(*) from elgg_users_entity where banned='no'").first["count"] batches(BATCH_SIZE) do |offset| - users = mysql_query(<<-SQL + users = mysql_query(<<-SQL).to_a select eue.guid, eue.username, eue.name, eue.email, eue.admin, max(case when ems1.string='cae_structure' then ems2.string end)cae_structure, max(case when ems1.string='location' then ems2.string end)location, @@ -76,7 +69,6 @@ class ImportScripts::Elgg < ImportScripts::Base group by eue.guid LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -97,11 +89,12 @@ class ImportScripts::Elgg < ImportScripts::Base name: u["name"], website: u["website"], bio_raw: u["briefdescription"].to_s + " " + u["cae_structure"].to_s, - post_create_action: proc do |user| - create_avatar(user, u["guid"]) - #add_user_to_group(user, u["cae_structure"]) - grant_admin(user, u["admin"]) - end + post_create_action: + proc do |user| + create_avatar(user, u["guid"]) + #add_user_to_group(user, u["cae_structure"]) + grant_admin(user, u["admin"]) + end, } end end @@ -115,9 +108,9 @@ class ImportScripts::Elgg < ImportScripts::Base create_categories(categories) do |c| { - id: c['guid'], - name: CGI.unescapeHTML(c['name']), - description: CGI.unescapeHTML(c['description']) + id: c["guid"], + name: CGI.unescapeHTML(c["name"]), + description: CGI.unescapeHTML(c["description"]), } end end @@ -125,10 +118,13 @@ class ImportScripts::Elgg < ImportScripts::Base def import_topics puts "", "creating topics" - total_count = mysql_query("select count(*) count from elgg_entities where subtype = 32;").first["count"] + total_count = + mysql_query("select count(*) count from elgg_entities where subtype = 32;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT ee.guid id, owner_guid user_id, @@ -143,30 +139,35 @@ class ImportScripts::Elgg < ImportScripts::Base ORDER BY ee.guid LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ") + ", + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| { - id: m['id'], - user_id: user_id_from_imported_user_id(m['user_id']) || -1, - raw: CGI.unescapeHTML(m['raw']), - created_at: Time.zone.at(m['created_at']), - category: category_id_from_imported_category_id(m['category_id']), - title: CGI.unescapeHTML(m['title']), - post_create_action: proc do |post| - tag_names = mysql_query(" + id: m["id"], + user_id: user_id_from_imported_user_id(m["user_id"]) || -1, + raw: CGI.unescapeHTML(m["raw"]), + created_at: Time.zone.at(m["created_at"]), + category: category_id_from_imported_category_id(m["category_id"]), + title: CGI.unescapeHTML(m["title"]), + post_create_action: + proc do |post| + tag_names = + mysql_query( + " select ms.string from elgg_metadata md join elgg_metastrings ms on md.value_id = ms.id where name_id = 43 - and entity_guid = #{m['id']}; - ").map { |tag| tag['string'] } - DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) - end + and entity_guid = #{m["id"]}; + ", + ).map { |tag| tag["string"] } + DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) + end, } end end @@ -179,10 +180,13 @@ class ImportScripts::Elgg < ImportScripts::Base def import_posts puts "", "creating posts" - total_count = mysql_query("SELECT count(*) count FROM elgg_entities WHERE subtype = 42").first["count"] + total_count = + mysql_query("SELECT count(*) count FROM elgg_entities WHERE subtype = 42").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT ee.guid id, container_guid topic_id, @@ -195,19 +199,20 @@ class ImportScripts::Elgg < ImportScripts::Base ORDER BY ee.guid LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ") + ", + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| { - id: m['id'], - user_id: user_id_from_imported_user_id(m['user_id']) || -1, - topic_id: topic_lookup_from_imported_post_id(m['topic_id'])[:topic_id], - raw: CGI.unescapeHTML(m['raw']), - created_at: Time.zone.at(m['created_at']), + id: m["id"], + user_id: user_id_from_imported_user_id(m["user_id"]) || -1, + topic_id: topic_lookup_from_imported_post_id(m["topic_id"])[:topic_id], + raw: CGI.unescapeHTML(m["raw"]), + created_at: Time.zone.at(m["created_at"]), } end end @@ -216,7 +221,6 @@ class ImportScripts::Elgg < ImportScripts::Base def mysql_query(sql) @client.query(sql, cache_rows: false) end - end ImportScripts::Elgg.new.perform diff --git a/script/import_scripts/flarum_import.rb b/script/import_scripts/flarum_import.rb index ea4eb3dddf..737ee3a86e 100644 --- a/script/import_scripts/flarum_import.rb +++ b/script/import_scripts/flarum_import.rb @@ -1,60 +1,62 @@ # frozen_string_literal: true require "mysql2" -require 'time' -require 'date' +require "time" +require "date" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::FLARUM < ImportScripts::Base #SET THE APPROPRIATE VALUES FOR YOUR MYSQL CONNECTION - FLARUM_HOST ||= ENV['FLARUM_HOST'] || "db_host" - FLARUM_DB ||= ENV['FLARUM_DB'] || "db_name" + FLARUM_HOST ||= ENV["FLARUM_HOST"] || "db_host" + FLARUM_DB ||= ENV["FLARUM_DB"] || "db_name" BATCH_SIZE ||= 1000 - FLARUM_USER ||= ENV['FLARUM_USER'] || "db_user" - FLARUM_PW ||= ENV['FLARUM_PW'] || "db_user_pass" + FLARUM_USER ||= ENV["FLARUM_USER"] || "db_user" + FLARUM_PW ||= ENV["FLARUM_PW"] || "db_user_pass" def initialize super - @client = Mysql2::Client.new( - host: FLARUM_HOST, - username: FLARUM_USER, - password: FLARUM_PW, - database: FLARUM_DB - ) + @client = + Mysql2::Client.new( + host: FLARUM_HOST, + username: FLARUM_USER, + password: FLARUM_PW, + database: FLARUM_DB, + ) end def execute - import_users import_categories import_posts - end def import_users - puts '', "creating users" - total_count = mysql_query("SELECT count(*) count FROM users;").first['count'] + puts "", "creating users" + total_count = mysql_query("SELECT count(*) count FROM users;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT id, username, email, joined_at, last_seen_at + results = + mysql_query( + "SELECT id, username, email, joined_at, last_seen_at FROM users LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 next if all_records_exist? :users, results.map { |u| u["id"].to_i } create_users(results, total: total_count, offset: offset) do |user| - { id: user['id'], - email: user['email'], - username: user['username'], - name: user['username'], - created_at: user['joined_at'], - last_seen_at: user['last_seen_at'] + { + id: user["id"], + email: user["email"], + username: user["username"], + name: user["username"], + created_at: user["joined_at"], + last_seen_at: user["last_seen_at"], } end end @@ -63,30 +65,31 @@ class ImportScripts::FLARUM < ImportScripts::Base def import_categories puts "", "importing top level categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT id, name, description, position FROM tags ORDER BY position ASC - ").to_a + ", + ).to_a - create_categories(categories) do |category| - { - id: category["id"], - name: category["name"] - } - end + create_categories(categories) { |category| { id: category["id"], name: category["name"] } } puts "", "importing children categories..." - children_categories = mysql_query(" + children_categories = + mysql_query( + " SELECT id, name, description, position FROM tags ORDER BY position - ").to_a + ", + ).to_a create_categories(children_categories) do |category| { - id: "child##{category['id']}", + id: "child##{category["id"]}", name: category["name"], description: category["description"], } @@ -99,7 +102,9 @@ class ImportScripts::FLARUM < ImportScripts::Base total_count = mysql_query("SELECT count(*) count from posts").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.id id, d.id topic_id, d.title title, @@ -116,29 +121,30 @@ class ImportScripts::FLARUM < ImportScripts::Base ORDER BY p.created_at LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ").to_a + ", + ).to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_FLARUM_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_FLARUM_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) - if m['id'] == m['first_post_id'] - mapped[:category] = category_id_from_imported_category_id("child##{m['category_id']}") - mapped[:title] = CGI.unescapeHTML(m['title']) + if m["id"] == m["first_post_id"] + mapped[:category] = category_id_from_imported_category_id("child##{m["category_id"]}") + mapped[:title] = CGI.unescapeHTML(m["title"]) else - parent = topic_lookup_from_imported_post_id(m['first_post_id']) + parent = topic_lookup_from_imported_post_id(m["first_post_id"]) if parent mapped[:topic_id] = parent[:topic_id] else - puts "Parent post #{m['first_post_id']} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + puts "Parent post #{m["first_post_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end end diff --git a/script/import_scripts/fluxbb.rb b/script/import_scripts/fluxbb.rb index 38b84ed3c1..9af64457c8 100644 --- a/script/import_scripts/fluxbb.rb +++ b/script/import_scripts/fluxbb.rb @@ -17,23 +17,23 @@ export FLUXBB_PREFIX="" # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/fluxbb.rb class ImportScripts::FluxBB < ImportScripts::Base - - FLUXBB_HOST ||= ENV['FLUXBB_HOST'] || "localhost" - FLUXBB_DB ||= ENV['FLUXBB_DB'] || "fluxbb" + FLUXBB_HOST ||= ENV["FLUXBB_HOST"] || "localhost" + FLUXBB_DB ||= ENV["FLUXBB_DB"] || "fluxbb" BATCH_SIZE ||= 1000 - FLUXBB_USER ||= ENV['FLUXBB_USER'] || "root" - FLUXBB_PW ||= ENV['FLUXBB_PW'] || "" - FLUXBB_PREFIX ||= ENV['FLUXBB_PREFIX'] || "" + FLUXBB_USER ||= ENV["FLUXBB_USER"] || "root" + FLUXBB_PW ||= ENV["FLUXBB_PW"] || "" + FLUXBB_PREFIX ||= ENV["FLUXBB_PREFIX"] || "" def initialize super - @client = Mysql2::Client.new( - host: FLUXBB_HOST, - username: FLUXBB_USER, - password: FLUXBB_PW, - database: FLUXBB_DB - ) + @client = + Mysql2::Client.new( + host: FLUXBB_HOST, + username: FLUXBB_USER, + password: FLUXBB_PW, + database: FLUXBB_DB, + ) end def execute @@ -45,64 +45,67 @@ class ImportScripts::FluxBB < ImportScripts::Base end def import_groups - puts '', "creating groups" + puts "", "creating groups" - results = mysql_query( - "SELECT g_id id, g_title name, g_user_title title - FROM #{FLUXBB_PREFIX}groups") + results = + mysql_query( + "SELECT g_id id, g_title name, g_user_title title + FROM #{FLUXBB_PREFIX}groups", + ) - customgroups = results.select { |group| group['id'] > 2 } + customgroups = results.select { |group| group["id"] > 2 } create_groups(customgroups) do |group| - { id: group['id'], - name: group['name'], - title: group['title'] } + { id: group["id"], name: group["name"], title: group["title"] } end end def import_users - puts '', "creating users" + puts "", "creating users" - total_count = mysql_query("SELECT count(*) count FROM #{FLUXBB_PREFIX}users;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{FLUXBB_PREFIX}users;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT id, username, realname name, url website, email email, registered created_at, + results = + mysql_query( + "SELECT id, username, realname name, url website, email email, registered created_at, registration_ip registration_ip_address, last_visit last_visit_time, last_email_sent last_emailed_at, location, group_id FROM #{FLUXBB_PREFIX}users LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 next if all_records_exist? :users, results.map { |u| u["id"].to_i } create_users(results, total: total_count, offset: offset) do |user| - { id: user['id'], - email: user['email'], - username: user['username'], - name: user['name'], - created_at: Time.zone.at(user['created_at']), - website: user['website'], - registration_ip_address: user['registration_ip_address'], - last_seen_at: Time.zone.at(user['last_visit_time']), - last_emailed_at: user['last_emailed_at'] == nil ? 0 : Time.zone.at(user['last_emailed_at']), - location: user['location'], - moderator: user['group_id'] == 2, - admin: user['group_id'] == 1 } + { + id: user["id"], + email: user["email"], + username: user["username"], + name: user["name"], + created_at: Time.zone.at(user["created_at"]), + website: user["website"], + registration_ip_address: user["registration_ip_address"], + last_seen_at: Time.zone.at(user["last_visit_time"]), + last_emailed_at: + user["last_emailed_at"] == nil ? 0 : Time.zone.at(user["last_emailed_at"]), + location: user["location"], + moderator: user["group_id"] == 2, + admin: user["group_id"] == 1, + } end - groupusers = results.select { |user| user['group_id'] > 2 } + groupusers = results.select { |user| user["group_id"] > 2 } groupusers.each do |user| - if user['group_id'] - user_id = user_id_from_imported_user_id(user['id']) - group_id = group_id_from_imported_group_id(user['group_id']) + if user["group_id"] + user_id = user_id_from_imported_user_id(user["id"]) + group_id = group_id_from_imported_group_id(user["group_id"]) - if user_id && group_id - GroupUser.find_or_create_by(user_id: user_id, group_id: group_id) - end + GroupUser.find_or_create_by(user_id: user_id, group_id: group_id) if user_id && group_id end end end @@ -111,33 +114,34 @@ class ImportScripts::FluxBB < ImportScripts::Base def import_categories puts "", "importing top level categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT id, cat_name name, disp_position position FROM #{FLUXBB_PREFIX}categories ORDER BY id ASC - ").to_a + ", + ).to_a - create_categories(categories) do |category| - { - id: category["id"], - name: category["name"] - } - end + create_categories(categories) { |category| { id: category["id"], name: category["name"] } } puts "", "importing children categories..." - children_categories = mysql_query(" + children_categories = + mysql_query( + " SELECT id, forum_name name, forum_desc description, disp_position position, cat_id parent_category_id FROM #{FLUXBB_PREFIX}forums ORDER BY id - ").to_a + ", + ).to_a create_categories(children_categories) do |category| { - id: "child##{category['id']}", + id: "child##{category["id"]}", name: category["name"], description: category["description"], - parent_category_id: category_id_from_imported_category_id(category["parent_category_id"]) + parent_category_id: category_id_from_imported_category_id(category["parent_category_id"]), } end end @@ -148,7 +152,9 @@ class ImportScripts::FluxBB < ImportScripts::Base total_count = mysql_query("SELECT count(*) count from #{FLUXBB_PREFIX}posts").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.id id, t.id topic_id, t.forum_id category_id, @@ -163,29 +169,30 @@ class ImportScripts::FluxBB < ImportScripts::Base ORDER BY p.posted LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ").to_a + ", + ).to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_fluxbb_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_fluxbb_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) - if m['id'] == m['first_post_id'] - mapped[:category] = category_id_from_imported_category_id("child##{m['category_id']}") - mapped[:title] = CGI.unescapeHTML(m['title']) + if m["id"] == m["first_post_id"] + mapped[:category] = category_id_from_imported_category_id("child##{m["category_id"]}") + mapped[:title] = CGI.unescapeHTML(m["title"]) else - parent = topic_lookup_from_imported_post_id(m['first_post_id']) + parent = topic_lookup_from_imported_post_id(m["first_post_id"]) if parent mapped[:topic_id] = parent[:topic_id] else - puts "Parent post #{m['first_post_id']} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + puts "Parent post #{m["first_post_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end end @@ -196,16 +203,16 @@ class ImportScripts::FluxBB < ImportScripts::Base end def suspend_users - puts '', "updating banned users" + puts "", "updating banned users" banned = 0 failed = 0 - total = mysql_query("SELECT count(*) count FROM #{FLUXBB_PREFIX}bans").first['count'] + total = mysql_query("SELECT count(*) count FROM #{FLUXBB_PREFIX}bans").first["count"] system_user = Discourse.system_user mysql_query("SELECT username, email FROM #{FLUXBB_PREFIX}bans").each do |b| - user = User.find_by_email(b['email']) + user = User.find_by_email(b["email"]) if user user.suspended_at = Time.now user.suspended_till = 200.years.from_now @@ -218,7 +225,7 @@ class ImportScripts::FluxBB < ImportScripts::Base failed += 1 end else - puts "Not found: #{b['email']}" + puts "Not found: #{b["email"]}" failed += 1 end @@ -233,15 +240,15 @@ class ImportScripts::FluxBB < ImportScripts::Base s.gsub!(/(?:.*)/, '\1') # Some links look like this: http://www.onegameamonth.com - s.gsub!(/(.+)<\/a>/, '[\2](\1)') + s.gsub!(%r{(.+)}, '[\2](\1)') # Many bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - s.gsub!(/:(?:\w{8})\]/, ']') + s.gsub!(/:(?:\w{8})\]/, "]") # Remove video tags. - s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + s.gsub!(%r{(^\[video=.*?\])|(\[/video\]$)}, "") s = CGI.unescapeHTML(s) @@ -249,7 +256,7 @@ class ImportScripts::FluxBB < ImportScripts::Base # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + s.gsub!(%r{\[http(s)?://(www\.)?}, "[") s end diff --git a/script/import_scripts/friendsmegplus.rb b/script/import_scripts/friendsmegplus.rb index d66eed12cc..3bcab17c90 100644 --- a/script/import_scripts/friendsmegplus.rb +++ b/script/import_scripts/friendsmegplus.rb @@ -2,7 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'csv' +require "csv" # Importer for Friends+Me Google+ Exporter (F+MG+E) output. # @@ -32,18 +32,18 @@ require 'csv' # Edit values at the top of the script to fit your preferences class ImportScripts::FMGP < ImportScripts::Base - def initialize super # Set this to the base URL for the site; required for importing videos # typically just 'https:' in production - @site_base_url = 'http://localhost:3000' + @site_base_url = "http://localhost:3000" @system_user = Discourse.system_user - SiteSetting.max_image_size_kb = 40960 - SiteSetting.max_attachment_size_kb = 40960 + SiteSetting.max_image_size_kb = 40_960 + SiteSetting.max_attachment_size_kb = 40_960 # handle the same video extension as the rest of Discourse - SiteSetting.authorized_extensions = (SiteSetting.authorized_extensions.split("|") + ['mp4', 'mov', 'webm', 'ogv']).uniq.join("|") + SiteSetting.authorized_extensions = + (SiteSetting.authorized_extensions.split("|") + %w[mp4 mov webm ogv]).uniq.join("|") @invalid_bounce_score = 5.0 @min_title_words = 3 @max_title_words = 14 @@ -76,7 +76,7 @@ class ImportScripts::FMGP < ImportScripts::Base @allowlist = nil # Tags to apply to every topic; empty Array to not have any tags applied everywhere - @globaltags = [ "gplus" ] + @globaltags = ["gplus"] @imagefiles = nil @@ -101,34 +101,30 @@ class ImportScripts::FMGP < ImportScripts::Base @first_date = nil # every argument is a filename, do the right thing based on the file name ARGV.each do |arg| - if arg.end_with?('.csv') + if arg.end_with?(".csv") # CSV files produced by F+MG+E have "URL";"IsDownloaded";"FileName";"FilePath";"FileSize" - CSV.foreach(arg, headers: true, col_sep: ';') do |row| - @images[row[0]] = { - filename: row[2], - filepath: row[3], - filesize: row[4] - } + CSV.foreach(arg, headers: true, col_sep: ";") do |row| + @images[row[0]] = { filename: row[2], filepath: row[3], filesize: row[4] } end elsif arg.end_with?("upload-paths.txt") @imagefiles = File.open(arg, "w") - elsif arg.end_with?('categories.json') + elsif arg.end_with?("categories.json") @categories_filename = arg @categories = load_fmgp_json(arg) elsif arg.end_with?("usermap.json") @usermap = load_fmgp_json(arg) - elsif arg.end_with?('blocklist.json') + elsif arg.end_with?("blocklist.json") @blocklist = load_fmgp_json(arg).map { |i| i.to_s }.to_set - elsif arg.end_with?('allowlist.json') + elsif arg.end_with?("allowlist.json") @allowlist = load_fmgp_json(arg).map { |i| i.to_s }.to_set - elsif arg.end_with?('.json') + elsif arg.end_with?(".json") @feeds << load_fmgp_json(arg) - elsif arg == '--dry-run' + elsif arg == "--dry-run" @dryrun = true elsif arg.start_with?("--last-date=") - @last_date = Time.zone.parse(arg.gsub(/.*=/, '')) + @last_date = Time.zone.parse(arg.gsub(/.*=/, "")) elsif arg.start_with?("--first-date=") - @first_date = Time.zone.parse(arg.gsub(/.*=/, '')) + @first_date = Time.zone.parse(arg.gsub(/.*=/, "")) else raise RuntimeError.new("unknown argument #{arg}") end @@ -153,7 +149,6 @@ class ImportScripts::FMGP < ImportScripts::Base @blocked_posts = 0 # count uploaded file size @totalsize = 0 - end def execute @@ -222,7 +217,9 @@ class ImportScripts::FMGP < ImportScripts::Base categories_new = "#{@categories_filename}.new" File.open(categories_new, "w") do |f| f.write(@categories.to_json) - raise RuntimeError.new("Category file missing categories for #{incomplete_categories}, edit #{categories_new} and rename it to #{@category_filename} before running the same import") + raise RuntimeError.new( + "Category file missing categories for #{incomplete_categories}, edit #{categories_new} and rename it to #{@category_filename} before running the same import", + ) end end end @@ -233,28 +230,32 @@ class ImportScripts::FMGP < ImportScripts::Base @categories.each do |id, cat| if cat["parent"].present? && !cat["parent"].empty? # Two separate sub-categories can have the same name, so need to identify by parent - Category.where(name: cat["category"]).each do |category| - parent = Category.where(id: category.parent_category_id).first - @cats[id] = category if parent.name == cat["parent"] - end + Category + .where(name: cat["category"]) + .each do |category| + parent = Category.where(id: category.parent_category_id).first + @cats[id] = category if parent.name == cat["parent"] + end else if category = Category.where(name: cat["category"]).first @cats[id] = category elsif @create_categories params = {} - params[:name] = cat['category'] + params[:name] = cat["category"] params[:id] = id - puts "Creating #{cat['category']}" + puts "Creating #{cat["category"]}" category = create_category(params, id) @cats[id] = category end end - raise RuntimeError.new("Could not find category #{cat["category"]} for #{cat}") if @cats[id].nil? + if @cats[id].nil? + raise RuntimeError.new("Could not find category #{cat["category"]} for #{cat}") + end end end def import_users - puts '', "Importing Google+ post and comment author users..." + puts "", "Importing Google+ post and comment author users..." # collect authors of both posts and comments @feeds.each do |feed| @@ -263,14 +264,10 @@ class ImportScripts::FMGP < ImportScripts::Base community["categories"].each do |category| category["posts"].each do |post| import_author_user(post["author"]) - if post["message"].present? - import_message_users(post["message"]) - end + import_message_users(post["message"]) if post["message"].present? post["comments"].each do |comment| import_author_user(comment["author"]) - if comment["message"].present? - import_message_users(comment["message"]) - end + import_message_users(comment["message"]) if comment["message"].present? end end end @@ -282,12 +279,7 @@ class ImportScripts::FMGP < ImportScripts::Base # now create them all create_users(@newusers) do |id, u| - { - id: id, - email: u[:email], - name: u[:name], - post_create_action: u[:post_create_action] - } + { id: id, email: u[:email], name: u[:name], post_create_action: u[:post_create_action] } end end @@ -308,7 +300,8 @@ class ImportScripts::FMGP < ImportScripts::Base def import_google_user(id, name) if !@emails[id].present? - google_user_info = UserAssociatedAccount.find_by(provider_name: 'google_oauth2', provider_uid: id.to_i) + google_user_info = + UserAssociatedAccount.find_by(provider_name: "google_oauth2", provider_uid: id.to_i) if google_user_info.nil? # create new google user on system; expect this user to merge # when they later log in with google authentication @@ -320,36 +313,39 @@ class ImportScripts::FMGP < ImportScripts::Base @newusers[id] = { email: email, name: name, - post_create_action: proc do |newuser| - newuser.approved = true - newuser.approved_by_id = @system_user.id - newuser.approved_at = newuser.created_at - if @blocklist.include?(id.to_s) - now = DateTime.now - forever = 1000.years.from_now - # you can suspend as well if you want your blocklist to - # be hard to recover from - #newuser.suspended_at = now - #newuser.suspended_till = forever - newuser.silenced_till = forever - end - newuser.save - @users[id] = newuser - UserAssociatedAccount.create(provider_name: 'google_oauth2', user_id: newuser.id, provider_uid: id) - # Do not send email to the invalid email addresses - # this can be removed after merging with #7162 - s = UserStat.where(user_id: newuser.id).first - s.bounce_score = @invalid_bounce_score - s.reset_bounce_score_after = 1000.years.from_now - s.save - end + post_create_action: + proc do |newuser| + newuser.approved = true + newuser.approved_by_id = @system_user.id + newuser.approved_at = newuser.created_at + if @blocklist.include?(id.to_s) + now = DateTime.now + forever = 1000.years.from_now + # you can suspend as well if you want your blocklist to + # be hard to recover from + #newuser.suspended_at = now + #newuser.suspended_till = forever + newuser.silenced_till = forever + end + newuser.save + @users[id] = newuser + UserAssociatedAccount.create( + provider_name: "google_oauth2", + user_id: newuser.id, + provider_uid: id, + ) + # Do not send email to the invalid email addresses + # this can be removed after merging with #7162 + s = UserStat.where(user_id: newuser.id).first + s.bounce_score = @invalid_bounce_score + s.reset_bounce_score_after = 1000.years.from_now + s.save + end, } else # user already on system u = User.find(google_user_info.user_id) - if u.silenced? || u.suspended? - @blocklist.add(id) - end + @blocklist.add(id) if u.silenced? || u.suspended? @users[id] = u email = u.email end @@ -362,7 +358,7 @@ class ImportScripts::FMGP < ImportScripts::Base # - A google+ post is a discourse topic # - A google+ comment is a discourse post - puts '', "Importing Google+ posts and comments..." + puts "", "Importing Google+ posts and comments..." @feeds.each do |feed| feed["accounts"].each do |account| @@ -371,14 +367,16 @@ class ImportScripts::FMGP < ImportScripts::Base category["posts"].each do |post| # G+ post / Discourse topic import_topic(post, category) - print("\r#{@topics_imported}/#{@posts_imported} topics/posts (skipped: #{@topics_skipped}/#{@posts_skipped} blocklisted: #{@blocked_topics}/#{@blocked_posts}) ") + print( + "\r#{@topics_imported}/#{@posts_imported} topics/posts (skipped: #{@topics_skipped}/#{@posts_skipped} blocklisted: #{@blocked_topics}/#{@blocked_posts}) ", + ) end end end end end - puts '' + puts "" end def import_topic(post, category) @@ -431,9 +429,7 @@ class ImportScripts::FMGP < ImportScripts::Base return nil if !@frst_date.nil? && created_at < @first_date user_id = user_id_from_imported_user_id(post_author_id) - if user_id.nil? - user_id = @users[post["author"]["id"]].id - end + user_id = @users[post["author"]["id"]].id if user_id.nil? mapped = { id: post["id"], @@ -472,7 +468,8 @@ class ImportScripts::FMGP < ImportScripts::Base def title_text(post, created_at) words = message_text(post["message"]) - if words.empty? || words.join("").length < @min_title_characters || words.length < @min_title_words + if words.empty? || words.join("").length < @min_title_characters || + words.length < @min_title_words # database has minimum length # short posts appear not to work well as titles most of the time (in practice) return untitled(post["author"]["name"], created_at) @@ -483,17 +480,13 @@ class ImportScripts::FMGP < ImportScripts::Base (@min_title_words..(words.length - 1)).each do |i| # prefer full stop - if words[i].end_with?(".") - lastword = i - end + lastword = i if words[i].end_with?(".") end if lastword.nil? # fall back on other punctuation (@min_title_words..(words.length - 1)).each do |i| - if words[i].end_with?(',', ';', ':', '?') - lastword = i - end + lastword = i if words[i].end_with?(",", ";", ":", "?") end end @@ -516,9 +509,7 @@ class ImportScripts::FMGP < ImportScripts::Base text_types = [0, 3] message.each do |fragment| if text_types.include?(fragment[0]) - fragment[1].split().each do |word| - words << word - end + fragment[1].split().each { |word| words << word } elsif fragment[0] == 2 # use the display text of a link words << fragment[1] @@ -543,14 +534,10 @@ class ImportScripts::FMGP < ImportScripts::Base lines << "\n#{formatted_link(post["image"]["proxy"])}\n" end if post["images"].present? - post["images"].each do |image| - lines << "\n#{formatted_link(image["proxy"])}\n" - end + post["images"].each { |image| lines << "\n#{formatted_link(image["proxy"])}\n" } end if post["videos"].present? - post["videos"].each do |video| - lines << "\n#{formatted_link(video["proxy"])}\n" - end + post["videos"].each { |video| lines << "\n#{formatted_link(video["proxy"])}\n" } end if post["link"].present? && post["link"]["url"].present? url = post["link"]["url"] @@ -575,12 +562,8 @@ class ImportScripts::FMGP < ImportScripts::Base if fragment[2].nil? text else - if fragment[2]["italic"].present? - text = "#{text}" - end - if fragment[2]["bold"].present? - text = "#{text}" - end + text = "#{text}" if fragment[2]["italic"].present? + text = "#{text}" if fragment[2]["bold"].present? if fragment[2]["strikethrough"].present? # s more likely than del to represent user intent? text = "#{text}" @@ -594,9 +577,7 @@ class ImportScripts::FMGP < ImportScripts::Base formatted_link_text(fragment[2], fragment[1]) elsif fragment[0] == 3 # reference to a user - if @usermap.include?(fragment[2].to_s) - return "@#{@usermap[fragment[2].to_s]}" - end + return "@#{@usermap[fragment[2].to_s]}" if @usermap.include?(fragment[2].to_s) if fragment[2].nil? # deleted G+ users show up with a null ID return "+#{fragment[1]}" @@ -606,12 +587,18 @@ class ImportScripts::FMGP < ImportScripts::Base # user was in this import's authors "@#{user.username} " else - if google_user_info = UserAssociatedAccount.find_by(provider_name: 'google_oauth2', provider_uid: fragment[2]) + if google_user_info = + UserAssociatedAccount.find_by( + provider_name: "google_oauth2", + provider_uid: fragment[2], + ) # user was not in this import, but has logged in or been imported otherwise user = User.find(google_user_info.user_id) "@#{user.username} " else - raise RuntimeError.new("Google user #{fragment[1]} (id #{fragment[2]}) not imported") if !@dryrun + if !@dryrun + raise RuntimeError.new("Google user #{fragment[1]} (id #{fragment[2]}) not imported") + end # if you want to fall back to their G+ name, just erase the raise above, # but this should not happen "+#{fragment[1]}" @@ -681,6 +668,4 @@ class ImportScripts::FMGP < ImportScripts::Base end end -if __FILE__ == $0 - ImportScripts::FMGP.new.perform -end +ImportScripts::FMGP.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/getsatisfaction.rb b/script/import_scripts/getsatisfaction.rb index 50f8613e68..0458c84f94 100644 --- a/script/import_scripts/getsatisfaction.rb +++ b/script/import_scripts/getsatisfaction.rb @@ -22,15 +22,14 @@ # that correctly and will import the replies in the wrong order. # You should run `rake posts:reorder_posts` after the import. -require 'csv' -require 'set' +require "csv" +require "set" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'reverse_markdown' # gem 'reverse_markdown' +require "reverse_markdown" # gem 'reverse_markdown' # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/getsatisfaction.rb DIRNAME class ImportScripts::GetSatisfaction < ImportScripts::Base - IMPORT_ARCHIVED_TOPICS = false # The script classifies each topic as private when at least one associated category @@ -85,22 +84,24 @@ class ImportScripts::GetSatisfaction < ImportScripts::Base previous_line = nil File.open(target_filename, "w") do |file| - File.open(source_filename).each_line do |line| - line.gsub!(/(?\s*)?(.*?)<\/code>(\s*<\/pre>)?/mi) do + raw.gsub!(%r{(
    \s*)?(.*?)(\s*
    )?}mi) do code = $2 hoist = SecureRandom.hex # tidy code, wow, this is impressively crazy @@ -350,9 +347,7 @@ class ImportScripts::GetSatisfaction < ImportScripts::Base # in this case double space works best ... so odd raw.gsub!(" ", "\n\n") - hoisted.each do |hoist, code| - raw.gsub!(hoist, "\n```\n#{code}\n```\n") - end + hoisted.each { |hoist, code| raw.gsub!(hoist, "\n```\n#{code}\n```\n") } raw = CGI.unescapeHTML(raw) raw = ReverseMarkdown.convert(raw) @@ -360,7 +355,7 @@ class ImportScripts::GetSatisfaction < ImportScripts::Base end def create_permalinks - puts '', 'Creating Permalinks...', '' + puts "", "Creating Permalinks...", "" Topic.listable_topics.find_each do |topic| tcf = topic.first_post.custom_fields @@ -372,7 +367,6 @@ class ImportScripts::GetSatisfaction < ImportScripts::Base end end end - end unless ARGV[0] && Dir.exist?(ARGV[0]) diff --git a/script/import_scripts/google_groups.rb b/script/import_scripts/google_groups.rb index 494346292c..1b6fa4b420 100755 --- a/script/import_scripts/google_groups.rb +++ b/script/import_scripts/google_groups.rb @@ -20,19 +20,18 @@ DEFAULT_COOKIES_TXT = "/shared/import/cookies.txt" ABORT_AFTER_SKIPPED_TOPIC_COUNT = 10 def driver - @driver ||= begin - chrome_args = ["disable-gpu"] - chrome_args << "headless" unless ENV["NOT_HEADLESS"] == '1' - chrome_args << "no-sandbox" if inside_container? - options = Selenium::WebDriver::Chrome::Options.new(args: chrome_args) - Selenium::WebDriver.for(:chrome, options: options) - end + @driver ||= + begin + chrome_args = ["disable-gpu"] + chrome_args << "headless" unless ENV["NOT_HEADLESS"] == "1" + chrome_args << "no-sandbox" if inside_container? + options = Selenium::WebDriver::Chrome::Options.new(args: chrome_args) + Selenium::WebDriver.for(:chrome, options: options) + end end def inside_container? - File.foreach("/proc/1/cgroup") do |line| - return true if line.include?("docker") - end + File.foreach("/proc/1/cgroup") { |line| return true if line.include?("docker") } false end @@ -79,35 +78,38 @@ def base_url end def crawl_topics - 1.step(nil, 100).each do |start| - url = "#{base_url}/#{@groupname}[#{start}-#{start + 99}]" - get(url) + 1 + .step(nil, 100) + .each do |start| + url = "#{base_url}/#{@groupname}[#{start}-#{start + 99}]" + get(url) - begin - if start == 1 && find("h2").text == "Error 403" - exit_with_error(<<~TEXT.red.bold) + begin + exit_with_error(<<~TEXT.red.bold) if start == 1 && find("h2").text == "Error 403" Unable to find topics. Try running the script with the "--domain example.com" option if you are a G Suite user and your group's URL contains a path with your domain that looks like "/a/example.com". TEXT + rescue Selenium::WebDriver::Error::NoSuchElementError + # Ignore this error. It simply means there wasn't an error. end - rescue Selenium::WebDriver::Error::NoSuchElementError - # Ignore this error. It simply means there wasn't an error. - end - topic_urls = extract(".subject a[href*='#{@groupname}']") { |a| a["href"].sub("/d/topic/", "/forum/?_escaped_fragment_=topic/") } - break if topic_urls.size == 0 + topic_urls = + extract(".subject a[href*='#{@groupname}']") do |a| + a["href"].sub("/d/topic/", "/forum/?_escaped_fragment_=topic/") + end + break if topic_urls.size == 0 - topic_urls.each do |topic_url| - crawl_topic(topic_url) + topic_urls.each do |topic_url| + crawl_topic(topic_url) - # abort if this in an incremental crawl and there were too many consecutive, skipped topics - if @finished && @skipped_topic_count > ABORT_AFTER_SKIPPED_TOPIC_COUNT - puts "Skipping all other topics, because this is an incremental crawl.".green - return + # abort if this in an incremental crawl and there were too many consecutive, skipped topics + if @finished && @skipped_topic_count > ABORT_AFTER_SKIPPED_TOPIC_COUNT + puts "Skipping all other topics, because this is an incremental crawl.".green + return + end end end - end end def crawl_topic(url) @@ -126,17 +128,14 @@ def crawl_topic(url) messages_crawled = false extract(".subject a[href*='#{@groupname}']") do |a| - [ - a["href"].sub("/d/msg/", "/forum/message/raw?msg="), - a["title"].empty? - ] + [a["href"].sub("/d/msg/", "/forum/message/raw?msg="), a["title"].empty?] end.each do |msg_url, might_be_deleted| messages_crawled |= crawl_message(msg_url, might_be_deleted) end @skipped_topic_count = skippable && messages_crawled ? 0 : @skipped_topic_count + 1 @scraped_topic_urls << url -rescue +rescue StandardError puts "Failed to scrape topic at #{url}".red raise if @abort_on_error end @@ -144,18 +143,16 @@ end def crawl_message(url, might_be_deleted) get(url) - filename = File.join(@path, "#{url[/#{@groupname}\/(.+)/, 1].sub("/", "-")}.eml") + filename = File.join(@path, "#{url[%r{#{@groupname}/(.+)}, 1].sub("/", "-")}.eml") content = find("pre")["innerText"] if !@first_message_checked @first_message_checked = true - if content.match?(/From:.*\.\.\.@.*/i) && !@force_import - exit_with_error(<<~TEXT.red.bold) + exit_with_error(<<~TEXT.red.bold) if content.match?(/From:.*\.\.\.@.*/i) && !@force_import It looks like you do not have permissions to see email addresses. Aborting. Use the --force option to import anyway. TEXT - end end old_md5 = Digest::MD5.file(filename) if File.exist?(filename) @@ -169,7 +166,7 @@ rescue Selenium::WebDriver::Error::NoSuchElementError puts "Failed to scrape message at #{url}".red raise if @abort_on_error end -rescue +rescue StandardError puts "Failed to scrape message at #{url}".red raise if @abort_on_error end @@ -178,10 +175,7 @@ def login puts "Logging in..." get("https://google.com/404") - add_cookies( - "myaccount.google.com", - "google.com" - ) + add_cookies("myaccount.google.com", "google.com") get("https://myaccount.google.com/?utm_source=sign_in_no_continue") @@ -193,20 +187,24 @@ def login end def add_cookies(*domains) - File.readlines(@cookies).each do |line| - parts = line.chomp.split("\t") - next if parts.size != 7 || !domains.any? { |domain| parts[0] =~ /^\.?#{Regexp.escape(domain)}$/ } + File + .readlines(@cookies) + .each do |line| + parts = line.chomp.split("\t") + if parts.size != 7 || !domains.any? { |domain| parts[0] =~ /^\.?#{Regexp.escape(domain)}$/ } + next + end - driver.manage.add_cookie( - domain: parts[0], - httpOnly: "true".casecmp?(parts[1]), - path: parts[2], - secure: "true".casecmp?(parts[3]), - expires: parts[4] == "0" ? nil : DateTime.strptime(parts[4], "%s"), - name: parts[5], - value: parts[6] - ) - end + driver.manage.add_cookie( + domain: parts[0], + httpOnly: "true".casecmp?(parts[1]), + path: parts[2], + secure: "true".casecmp?(parts[3]), + expires: parts[4] == "0" ? nil : DateTime.strptime(parts[4], "%s"), + name: parts[5], + value: parts[6], + ) + end end def wait_for_url @@ -240,10 +238,7 @@ def crawl crawl_topics @finished = true ensure - File.write(status_filename, { - finished: @finished, - urls: @scraped_topic_urls - }.to_yaml) + File.write(status_filename, { finished: @finished, urls: @scraped_topic_urls }.to_yaml) end elapsed = Time.now - start_time @@ -258,20 +253,25 @@ def parse_arguments @abort_on_error = false @cookies = DEFAULT_COOKIES_TXT if File.exist?(DEFAULT_COOKIES_TXT) - parser = OptionParser.new do |opts| - opts.banner = "Usage: google_groups.rb [options]" + parser = + OptionParser.new do |opts| + opts.banner = "Usage: google_groups.rb [options]" - opts.on("-g", "--groupname GROUPNAME") { |v| @groupname = v } - opts.on("-d", "--domain DOMAIN") { |v| @domain = v } - opts.on("-c", "--cookies PATH", "path to cookies.txt") { |v| @cookies = v } - opts.on("--path PATH", "output path for emails") { |v| @path = v } - opts.on("-f", "--force", "force import when user isn't allowed to see email addresses") { @force_import = true } - opts.on("-a", "--abort-on-error", "abort crawl on error instead of skipping message") { @abort_on_error = true } - opts.on("-h", "--help") do - puts opts - exit + opts.on("-g", "--groupname GROUPNAME") { |v| @groupname = v } + opts.on("-d", "--domain DOMAIN") { |v| @domain = v } + opts.on("-c", "--cookies PATH", "path to cookies.txt") { |v| @cookies = v } + opts.on("--path PATH", "output path for emails") { |v| @path = v } + opts.on("-f", "--force", "force import when user isn't allowed to see email addresses") do + @force_import = true + end + opts.on("-a", "--abort-on-error", "abort crawl on error instead of skipping message") do + @abort_on_error = true + end + opts.on("-h", "--help") do + puts opts + exit + end end - end begin parser.parse! @@ -279,10 +279,12 @@ def parse_arguments exit_with_error(e.message, "", parser) end - mandatory = [:groupname, :cookies] + mandatory = %i[groupname cookies] missing = mandatory.select { |name| instance_variable_get("@#{name}").nil? } - exit_with_error("Missing arguments: #{missing.join(', ')}".red.bold, "", parser, "") if missing.any? + if missing.any? + exit_with_error("Missing arguments: #{missing.join(", ")}".red.bold, "", parser, "") + end exit_with_error("cookies.txt not found at #{@cookies}".red.bold, "") if !File.exist?(@cookies) @path = File.join(DEFAULT_OUTPUT_PATH, @groupname) if @path.nil? diff --git a/script/import_scripts/higher_logic.rb b/script/import_scripts/higher_logic.rb index fe6e19641d..0682031126 100644 --- a/script/import_scripts/higher_logic.rb +++ b/script/import_scripts/higher_logic.rb @@ -4,7 +4,6 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::HigherLogic < ImportScripts::Base - HIGHERLOGIC_DB = "higherlogic" BATCH_SIZE = 1000 ATTACHMENT_DIR = "/shared/import/data/attachments" @@ -12,11 +11,7 @@ class ImportScripts::HigherLogic < ImportScripts::Base def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - database: HIGHERLOGIC_DB - ) + @client = Mysql2::Client.new(host: "localhost", username: "root", database: HIGHERLOGIC_DB) end def execute @@ -29,7 +24,7 @@ class ImportScripts::HigherLogic < ImportScripts::Base end def import_groups - puts '', 'importing groups' + puts "", "importing groups" groups = mysql_query <<-SQL SELECT CommunityKey, CommunityName @@ -37,16 +32,11 @@ class ImportScripts::HigherLogic < ImportScripts::Base ORDER BY CommunityName SQL - create_groups(groups) do |group| - { - id: group['CommunityKey'], - name: group['CommunityName'] - } - end + create_groups(groups) { |group| { id: group["CommunityKey"], name: group["CommunityName"] } } end def import_users - puts '', 'importing users' + puts "", "importing users" total_count = mysql_query("SELECT count(*) FROM Contact").first["count"] batches(BATCH_SIZE) do |offset| @@ -59,43 +49,42 @@ class ImportScripts::HigherLogic < ImportScripts::Base break if results.size < 1 - next if all_records_exist? :users, results.map { |u| u['ContactKey'] } + next if all_records_exist? :users, results.map { |u| u["ContactKey"] } create_users(results, total: total_count, offset: offset) do |user| - next if user['EmailAddress'].blank? + next if user["EmailAddress"].blank? { - id: user['ContactKey'], - email: user['EmailAddress'], - name: "#{user['FirstName']} #{user['LastName']}", - created_at: user['CreatedOn'] == nil ? 0 : Time.zone.at(user['CreatedOn']), - bio_raw: user['Bio'], - active: user['UserStatus'] == "Active", - admin: user['HLAdminFlag'] == 1 + id: user["ContactKey"], + email: user["EmailAddress"], + name: "#{user["FirstName"]} #{user["LastName"]}", + created_at: user["CreatedOn"] == nil ? 0 : Time.zone.at(user["CreatedOn"]), + bio_raw: user["Bio"], + active: user["UserStatus"] == "Active", + admin: user["HLAdminFlag"] == 1, } end end end def import_group_users - puts '', 'importing group users' + puts "", "importing group users" - group_users = mysql_query(<<-SQL + group_users = mysql_query(<<-SQL).to_a SELECT CommunityKey, ContactKey FROM CommunityMember SQL - ).to_a group_users.each do |row| - next unless user_id = user_id_from_imported_user_id(row['ContactKey']) - next unless group_id = group_id_from_imported_group_id(row['CommunityKey']) - puts '', '.' + next unless user_id = user_id_from_imported_user_id(row["ContactKey"]) + next unless group_id = group_id_from_imported_group_id(row["CommunityKey"]) + puts "", "." GroupUser.find_or_create_by(user_id: user_id, group_id: group_id) end end def import_categories - puts '', 'importing categories' + puts "", "importing categories" categories = mysql_query <<-SQL SELECT DiscussionKey, DiscussionName @@ -103,15 +92,12 @@ class ImportScripts::HigherLogic < ImportScripts::Base SQL create_categories(categories) do |category| - { - id: category['DiscussionKey'], - name: category['DiscussionName'] - } + { id: category["DiscussionKey"], name: category["DiscussionName"] } end end def import_posts - puts '', 'importing topics and posts' + puts "", "importing topics and posts" total_count = mysql_query("SELECT count(*) FROM DiscussionPost").first["count"] batches(BATCH_SIZE) do |offset| @@ -131,28 +117,28 @@ class ImportScripts::HigherLogic < ImportScripts::Base SQL break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| p['MessageKey'] } + next if all_records_exist? :posts, results.map { |p| p["MessageKey"] } create_posts(results, total: total_count, offset: offset) do |post| - raw = preprocess_raw(post['Body']) + raw = preprocess_raw(post["Body"]) mapped = { - id: post['MessageKey'], - user_id: user_id_from_imported_user_id(post['ContactKey']), + id: post["MessageKey"], + user_id: user_id_from_imported_user_id(post["ContactKey"]), raw: raw, - created_at: Time.zone.at(post['CreatedOn']), + created_at: Time.zone.at(post["CreatedOn"]), } - if post['ParentMessageKey'].nil? - mapped[:category] = category_id_from_imported_category_id(post['DiscussionKey']).to_i - mapped[:title] = CGI.unescapeHTML(post['Subject']) - mapped[:pinned] = post['PinnedFlag'] == 1 + if post["ParentMessageKey"].nil? + mapped[:category] = category_id_from_imported_category_id(post["DiscussionKey"]).to_i + mapped[:title] = CGI.unescapeHTML(post["Subject"]) + mapped[:pinned] = post["PinnedFlag"] == 1 else - topic = topic_lookup_from_imported_post_id(post['ParentMessageKey']) + topic = topic_lookup_from_imported_post_id(post["ParentMessageKey"]) if topic.present? mapped[:topic_id] = topic[:topic_id] else - puts "Parent post #{post['ParentMessageKey']} doesn't exist. Skipping." + puts "Parent post #{post["ParentMessageKey"]} doesn't exist. Skipping." next end end @@ -163,20 +149,19 @@ class ImportScripts::HigherLogic < ImportScripts::Base end def import_attachments - puts '', 'importing attachments' + puts "", "importing attachments" count = 0 - total_attachments = mysql_query(<<-SQL + total_attachments = mysql_query(<<-SQL).first["count"] SELECT COUNT(*) count FROM LibraryEntryFile l JOIN DiscussionPost p ON p.AttachmentDocumentKey = l.DocumentKey WHERE p.CreatedOn > '2020-01-01 00:00:00' SQL - ).first['count'] batches(BATCH_SIZE) do |offset| - attachments = mysql_query(<<-SQL + attachments = mysql_query(<<-SQL).to_a SELECT l.VersionName, l.FileExtension, p.MessageKey @@ -186,17 +171,16 @@ class ImportScripts::HigherLogic < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ).to_a break if attachments.empty? attachments.each do |a| print_status(count += 1, total_attachments, get_start_time("attachments")) - original_filename = "#{a['VersionName']}.#{a['FileExtension']}" + original_filename = "#{a["VersionName"]}.#{a["FileExtension"]}" path = File.join(ATTACHMENT_DIR, original_filename) if File.exist?(path) - if post = Post.find(post_id_from_imported_post_id(a['MessageKey'])) + if post = Post.find(post_id_from_imported_post_id(a["MessageKey"])) filename = File.basename(original_filename) upload = create_upload(post.user.id, path, filename) @@ -205,7 +189,9 @@ class ImportScripts::HigherLogic < ImportScripts::Base post.raw << "\n\n" << html post.save! - PostUpload.create!(post: post, upload: upload) unless PostUpload.where(post: post, upload: upload).exists? + unless PostUpload.where(post: post, upload: upload).exists? + PostUpload.create!(post: post, upload: upload) + end end end end @@ -217,7 +203,7 @@ class ImportScripts::HigherLogic < ImportScripts::Base raw = body.dup # trim off any post text beyond ---- to remove email threading - raw = raw.slice(0..(raw.index('------'))) || raw + raw = raw.slice(0..(raw.index("------"))) || raw raw = HtmlToMarkdown.new(raw).to_markdown raw diff --git a/script/import_scripts/ipboard.rb b/script/import_scripts/ipboard.rb index dcd8cb03f5..4f5c5ed0bd 100644 --- a/script/import_scripts/ipboard.rb +++ b/script/import_scripts/ipboard.rb @@ -3,13 +3,13 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' +require "htmlentities" begin - require 'reverse_markdown' # https://github.com/jqr/php-serialize + require "reverse_markdown" # https://github.com/jqr/php-serialize rescue LoadError puts - puts 'reverse_markdown not found.' - puts 'Add to Gemfile, like this: ' + puts "reverse_markdown not found." + puts "Add to Gemfile, like this: " puts puts "echo gem \\'reverse_markdown\\' >> Gemfile" puts "bundle install" @@ -32,28 +32,27 @@ export USERDIR="user" =end class ImportScripts::IpboardSQL < ImportScripts::Base - - DB_HOST ||= ENV['DB_HOST'] || "localhost" - DB_NAME ||= ENV['DB_NAME'] || "ipboard" - DB_PW ||= ENV['DB_PW'] || "ipboard" - DB_USER ||= ENV['DB_USER'] || "ipboard" - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "ipb_" - IMPORT_AFTER ||= ENV['IMPORT_AFTER'] || "1970-01-01" - UPLOADS ||= ENV['UPLOADS'] || "http://UPLOADS+LOCATION+IS+NOT+SET/uploads" - USERDIR ||= ENV['USERDIR'] || "user" - URL ||= ENV['URL'] || "https://forum.example.com" - AVATARS_DIR ||= ENV['AVATARS_DIR'] || '/home/pfaffman/data/example.com/avatars/' + DB_HOST ||= ENV["DB_HOST"] || "localhost" + DB_NAME ||= ENV["DB_NAME"] || "ipboard" + DB_PW ||= ENV["DB_PW"] || "ipboard" + DB_USER ||= ENV["DB_USER"] || "ipboard" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "ipb_" + IMPORT_AFTER ||= ENV["IMPORT_AFTER"] || "1970-01-01" + UPLOADS ||= ENV["UPLOADS"] || "http://UPLOADS+LOCATION+IS+NOT+SET/uploads" + USERDIR ||= ENV["USERDIR"] || "user" + URL ||= ENV["URL"] || "https://forum.example.com" + AVATARS_DIR ||= ENV["AVATARS_DIR"] || "/home/pfaffman/data/example.com/avatars/" BATCH_SIZE = 1000 ID_FIRST = true QUIET = true DEBUG = false - GALLERY_CAT_ID = 1234567 - GALLERY_CAT_NAME = 'galeria' - EMO_DIR ||= ENV['EMO_DIR'] || "default" + GALLERY_CAT_ID = 1_234_567 + GALLERY_CAT_NAME = "galeria" + EMO_DIR ||= ENV["EMO_DIR"] || "default" OLD_FORMAT = false if OLD_FORMAT MEMBERS_TABLE = "#{TABLE_PREFIX}core_members" - FORUMS_TABLE = "#{TABLE_PREFIX}forums_forums" + FORUMS_TABLE = "#{TABLE_PREFIX}forums_forums" POSTS_TABLE = "#{TABLE_PREFIX}forums_posts" TOPICS_TABLE = "#{TABLE_PREFIX}forums_topics" else @@ -89,8 +88,8 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # TODO figure this out puts "WARNING: permalink_normalizations not set!!!" sleep 1 - #raw = "[ORIGINAL POST](#{URL}/topic/#{id}-#{slug})\n\n" + raw - #SiteSetting.permalink_normalizations='/topic/(.*t)\?.*/\1' + #raw = "[ORIGINAL POST](#{URL}/topic/#{id}-#{slug})\n\n" + raw + #SiteSetting.permalink_normalizations='/topic/(.*t)\?.*/\1' else # remove stuff after a "?" and work for urls that end in .html SiteSetting.permalink_normalizations = '/(.*t)[?.].*/\1' @@ -98,21 +97,15 @@ class ImportScripts::IpboardSQL < ImportScripts::Base end def initialize - if IMPORT_AFTER > "1970-01-01" - print_warning("Importing data after #{IMPORT_AFTER}") - end + print_warning("Importing data after #{IMPORT_AFTER}") if IMPORT_AFTER > "1970-01-01" super @htmlentities = HTMLEntities.new begin - @client = Mysql2::Client.new( - host: DB_HOST, - username: DB_USER, - password: DB_PW, - database: DB_NAME - ) + @client = + Mysql2::Client.new(host: DB_HOST, username: DB_USER, password: DB_PW, database: DB_NAME) rescue Exception => e - puts '=' * 50 + puts "=" * 50 puts e.message puts <<~TEXT Cannot log in to database. @@ -151,18 +144,24 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # NOT SUPPORTED import_gallery_topics update_tl0 create_permalinks - end def import_users - puts '', "creating users" + puts "", "creating users" - total_count = mysql_query("SELECT count(*) count FROM #{MEMBERS_TABLE} - WHERE last_activity > UNIX_TIMESTAMP(STR_TO_DATE('#{IMPORT_AFTER}', '%Y-%m-%d'));").first['count'] + total_count = + mysql_query( + "SELECT count(*) count FROM #{MEMBERS_TABLE} + WHERE last_activity > UNIX_TIMESTAMP(STR_TO_DATE('#{IMPORT_AFTER}', '%Y-%m-%d'));", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| #notes: no location, url, - results = mysql_query(" + results = + mysql_query( + " SELECT member_id id, name username, member_group_id usergroup, @@ -184,58 +183,64 @@ class ImportScripts::IpboardSQL < ImportScripts::Base AND member_group_id = g_id order by member_id ASC LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 - next if all_records_exist? :users, results.map { |u| u['id'].to_i } + next if all_records_exist? :users, results.map { |u| u["id"].to_i } create_users(results, total: total_count, offset: offset) do |user| - next if user['email'].blank? - next if user['username'].blank? - next if @lookup.user_id_from_imported_user_id(user['id']) + next if user["email"].blank? + next if user["username"].blank? + next if @lookup.user_id_from_imported_user_id(user["id"]) - birthday = Date.parse("#{user['bday_year']}-#{user['bday_month']}-#{user['bday_day']}") rescue nil - # TODO: what about timezones? - next if user['id'] == 0 - { id: user['id'], - email: user['email'], - username: user['username'], - avatar_url: user['avatar_url'], - title: user['member_type'], - created_at: user['created_at'] == nil ? 0 : Time.zone.at(user['created_at']), - # bio_raw: user['bio_raw'], - registration_ip_address: user['registration_ip_address'], - # birthday: birthday, - last_seen_at: user['last_seen_at'] == nil ? 0 : Time.zone.at(user['last_seen_at']), - admin: /^Admin/.match(user['member_type']) ? true : false, - moderator: /^MOD/.match(user['member_type']) ? true : false, - post_create_action: proc do |newuser| - if user['avatar_url'] && user['avatar_url'].length > 0 - photo_path = AVATARS_DIR + user['avatar_url'] - if File.exist?(photo_path) - begin - upload = create_upload(newuser.id, photo_path, File.basename(photo_path)) - if upload && upload.persisted? - newuser.import_mode = false - newuser.create_user_avatar - newuser.import_mode = true - newuser.user_avatar.update(custom_upload_id: upload.id) - newuser.update(uploaded_avatar_id: upload.id) - else - puts "Error: Upload did not persist for #{photo_path}!" - end - rescue SystemCallError => err - puts "Could not import avatar #{photo_path}: #{err.message}" - end - else - puts "avatar file not found at #{photo_path}" - end - end - if user['banned'] != 0 - suspend_user(newuser) - end + birthday = + begin + Date.parse("#{user["bday_year"]}-#{user["bday_month"]}-#{user["bday_day"]}") + rescue StandardError + nil end + # TODO: what about timezones? + next if user["id"] == 0 + { + id: user["id"], + email: user["email"], + username: user["username"], + avatar_url: user["avatar_url"], + title: user["member_type"], + created_at: user["created_at"] == nil ? 0 : Time.zone.at(user["created_at"]), + # bio_raw: user['bio_raw'], + registration_ip_address: user["registration_ip_address"], + # birthday: birthday, + last_seen_at: user["last_seen_at"] == nil ? 0 : Time.zone.at(user["last_seen_at"]), + admin: /^Admin/.match(user["member_type"]) ? true : false, + moderator: /^MOD/.match(user["member_type"]) ? true : false, + post_create_action: + proc do |newuser| + if user["avatar_url"] && user["avatar_url"].length > 0 + photo_path = AVATARS_DIR + user["avatar_url"] + if File.exist?(photo_path) + begin + upload = create_upload(newuser.id, photo_path, File.basename(photo_path)) + if upload && upload.persisted? + newuser.import_mode = false + newuser.create_user_avatar + newuser.import_mode = true + newuser.user_avatar.update(custom_upload_id: upload.id) + newuser.update(uploaded_avatar_id: upload.id) + else + puts "Error: Upload did not persist for #{photo_path}!" + end + rescue SystemCallError => err + puts "Could not import avatar #{photo_path}: #{err.message}" + end + else + puts "avatar file not found at #{photo_path}" + end + end + suspend_user(newuser) if user["banned"] != 0 + end, } end end @@ -244,7 +249,7 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def suspend_user(user) user.suspended_at = Time.now user.suspended_till = 200.years.from_now - ban_reason = 'Account deactivated by administrator' + ban_reason = "Account deactivated by administrator" user_option = user.user_option user_option.email_digests = false @@ -266,45 +271,50 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_image_categories puts "", "importing image categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT category_id id, category_name_seo name, category_parent_id as parent_id FROM #{TABLE_PREFIX}gallery_categories ORDER BY id ASC - ").to_a + ", + ).to_a - category_names = mysql_query(" + category_names = + mysql_query( + " SELECT DISTINCT word_key, word_default title FROM #{TABLE_PREFIX}core_sys_lang_words where word_app='gallery' AND word_key REGEXP 'gallery_category_[0-9]+$' ORDER BY word_key ASC - ").to_a + ", + ).to_a cat_map = {} puts "Creating gallery_cat_map" category_names.each do |name| - title = name['title'] - word_key = name['word_key'] + title = name["title"] + word_key = name["word_key"] puts "Processing #{word_key}: #{title}" - id = word_key.gsub('gallery_category_', '') + id = word_key.gsub("gallery_category_", "") next if cat_map[id] cat_map[id] = cat_map.has_value?(title) ? title + " " + id : title puts "#{id} => #{cat_map[id]}" end - params = { id: GALLERY_CAT_ID, - name: GALLERY_CAT_NAME } + params = { id: GALLERY_CAT_ID, name: GALLERY_CAT_NAME } create_category(params, params[:id]) create_categories(categories) do |category| - id = (category['id']).to_s + id = (category["id"]).to_s name = CGI.unescapeHTML(cat_map[id]) { - id: id + 'gal', + id: id + "gal", name: name, parent_category_id: @lookup.category_id_from_imported_category_id(GALLERY_CAT_ID), - color: random_category_color + color: random_category_color, } end end @@ -312,34 +322,34 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_categories puts "", "importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT id, name name, parent_id as parent_id FROM #{FORUMS_TABLE} ORDER BY parent_id ASC - ").to_a + ", + ).to_a top_level_categories = categories.select { |c| c["parent.id"] == -1 } create_categories(top_level_categories) do |category| - id = category['id'].to_s - name = category['name'] - { - id: id, - name: name, - } + id = category["id"].to_s + name = category["name"] + { id: id, name: name } end children_categories = categories.select { |c| c["parent.id"] != -1 } create_categories(children_categories) do |category| - id = category['id'].to_s - name = category['name'] + id = category["id"].to_s + name = category["name"] { id: id, name: name, - parent_category_id: @lookup.category_id_from_imported_category_id(category['parent_id']), - color: random_category_color + parent_category_id: @lookup.category_id_from_imported_category_id(category["parent_id"]), + color: random_category_color, } end end @@ -347,13 +357,17 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_topics puts "", "importing topics..." - total_count = mysql_query("SELECT count(*) count FROM #{POSTS_TABLE} + total_count = + mysql_query( + "SELECT count(*) count FROM #{POSTS_TABLE} WHERE post_date > UNIX_TIMESTAMP(STR_TO_DATE('#{IMPORT_AFTER}', '%Y-%m-%d')) - AND new_topic=1;") - .first['count'] + AND new_topic=1;", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - discussions = mysql_query(<<-SQL + discussions = mysql_query(<<-SQL) SELECT #{TOPICS_TABLE}.tid tid, #{TOPICS_TABLE}.forum_id category, #{POSTS_TABLE}.pid pid, @@ -371,29 +385,29 @@ class ImportScripts::IpboardSQL < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if discussions.size < 1 - next if all_records_exist? :posts, discussions.map { |t| "discussion#" + t['tid'].to_s } + next if all_records_exist? :posts, discussions.map { |t| "discussion#" + t["tid"].to_s } create_posts(discussions, total: total_count, offset: offset) do |discussion| - slug = discussion['slug'] - id = discussion['tid'] - raw = clean_up(discussion['raw']) + slug = discussion["slug"] + id = discussion["tid"] + raw = clean_up(discussion["raw"]) { - id: "discussion#" + discussion['tid'].to_s, - user_id: user_id_from_imported_user_id(discussion['user_id']) || Discourse::SYSTEM_USER_ID, - title: CGI.unescapeHTML(discussion['title']), - category: category_id_from_imported_category_id(discussion['category'].to_s), + id: "discussion#" + discussion["tid"].to_s, + user_id: + user_id_from_imported_user_id(discussion["user_id"]) || Discourse::SYSTEM_USER_ID, + title: CGI.unescapeHTML(discussion["title"]), + category: category_id_from_imported_category_id(discussion["category"].to_s), raw: raw, - pinned_at: discussion['pinned'].to_i == 1 ? Time.zone.at(discussion['created_at']) : nil, - created_at: Time.zone.at(discussion['created_at']), + pinned_at: discussion["pinned"].to_i == 1 ? Time.zone.at(discussion["created_at"]) : nil, + created_at: Time.zone.at(discussion["created_at"]), } end end end - def array_from_members_string(invited_members = 'a:3:{i:0;i:22629;i:1;i:21837;i:2;i:22234;}') + def array_from_members_string(invited_members = "a:3:{i:0;i:22629;i:1;i:21837;i:2;i:22234;}") out = [] count_regex = /a:(\d)+:/ count = count_regex.match(invited_members)[1] @@ -403,7 +417,7 @@ class ImportScripts::IpboardSQL < ImportScripts::Base i = m[1] rest.sub!(i_regex, "") puts "i: #{i}, #{rest}" - out += [ i.to_i ] + out += [i.to_i] end out end @@ -411,12 +425,13 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_private_messages puts "", "importing private messages..." - topic_count = mysql_query("SELECT COUNT(msg_id) count FROM #{TABLE_PREFIX}message_posts").first["count"] + topic_count = + mysql_query("SELECT COUNT(msg_id) count FROM #{TABLE_PREFIX}message_posts").first["count"] last_private_message_topic_id = -1 batches(BATCH_SIZE) do |offset| - private_messages = mysql_query(<<-SQL + private_messages = mysql_query(<<-SQL) SELECT msg_id pmtextid, msg_topic_id topic_id, msg_author_id fromuserid, @@ -433,12 +448,12 @@ class ImportScripts::IpboardSQL < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) puts "Processing #{private_messages.count} messages" break if private_messages.count < 1 puts "Processing . . . " - private_messages = private_messages.reject { |pm| @lookup.post_already_imported?("pm-#{pm['pmtextid']}") } + private_messages = + private_messages.reject { |pm| @lookup.post_already_imported?("pm-#{pm["pmtextid"]}") } title_username_of_pm_first_post = {} @@ -446,11 +461,16 @@ class ImportScripts::IpboardSQL < ImportScripts::Base skip = false mapped = {} - mapped[:id] = "pm-#{m['pmtextid']}" - mapped[:user_id] = user_id_from_imported_user_id(m['fromuserid']) || Discourse::SYSTEM_USER_ID - mapped[:raw] = clean_up(m['message']) rescue nil - mapped[:created_at] = Time.zone.at(m['dateline']) - title = @htmlentities.decode(m['title']).strip[0...255] + mapped[:id] = "pm-#{m["pmtextid"]}" + mapped[:user_id] = user_id_from_imported_user_id(m["fromuserid"]) || + Discourse::SYSTEM_USER_ID + mapped[:raw] = begin + clean_up(m["message"]) + rescue StandardError + nil + end + mapped[:created_at] = Time.zone.at(m["dateline"]) + title = @htmlentities.decode(m["title"]).strip[0...255] topic_id = nil next if mapped[:raw].blank? @@ -459,9 +479,9 @@ class ImportScripts::IpboardSQL < ImportScripts::Base target_usernames = [] target_userids = [] begin - to_user_array = [ m['to_user_id'] ] + array_from_members_string(m['touserarray']) - rescue - puts "#{m['pmtextid']} -- #{m['touserarray']}" + to_user_array = [m["to_user_id"]] + array_from_members_string(m["touserarray"]) + rescue StandardError + puts "#{m["pmtextid"]} -- #{m["touserarray"]}" skip = true end @@ -477,8 +497,8 @@ class ImportScripts::IpboardSQL < ImportScripts::Base puts "Can't find user: #{to_user}" end end - rescue - puts "skipping pm-#{m['pmtextid']} `to_user_array` is broken -- #{to_user_array.inspect}" + rescue StandardError + puts "skipping pm-#{m["pmtextid"]} `to_user_array` is broken -- #{to_user_array.inspect}" skip = true end @@ -486,30 +506,32 @@ class ImportScripts::IpboardSQL < ImportScripts::Base participants << mapped[:user_id] begin participants.sort! - rescue + rescue StandardError puts "one of the participant's id is nil -- #{participants.inspect}" end - if last_private_message_topic_id != m['topic_id'] - last_private_message_topic_id = m['topic_id'] - puts "New message: #{m['topic_id']}: #{title} from #{m['fromuserid']} (#{mapped[:user_id]})" unless QUIET + if last_private_message_topic_id != m["topic_id"] + last_private_message_topic_id = m["topic_id"] + unless QUIET + puts "New message: #{m["topic_id"]}: #{title} from #{m["fromuserid"]} (#{mapped[:user_id]})" + end # topic post message - topic_id = m['topic_id'] + topic_id = m["topic_id"] mapped[:title] = title mapped[:archetype] = Archetype.private_message - mapped[:target_usernames] = target_usernames.join(',') + mapped[:target_usernames] = target_usernames.join(",") if mapped[:target_usernames].size < 1 # pm with yourself? # skip = true mapped[:target_usernames] = "system" - puts "pm-#{m['pmtextid']} has no target (#{m['touserarray']})" + puts "pm-#{m["pmtextid"]} has no target (#{m["touserarray"]})" end else # reply topic_id = topic_lookup_from_imported_post_id("pm-#{topic_id}") - if !topic_id - skip = true - end + skip = true if !topic_id mapped[:topic_id] = topic_id - puts "Reply message #{topic_id}: #{m['topic_id']}: from #{m['fromuserid']} (#{mapped[:user_id]})" unless QUIET + unless QUIET + puts "Reply message #{topic_id}: #{m["topic_id"]}: from #{m["fromuserid"]} (#{mapped[:user_id]})" + end end # puts "#{target_usernames} -- #{mapped[:target_usernames]}" # puts "Adding #{mapped}" @@ -524,9 +546,13 @@ class ImportScripts::IpboardSQL < ImportScripts::Base puts "", "importing gallery albums..." gallery_count = 0 - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}gallery_images - ;") - .first['count'] + total_count = + mysql_query( + "SELECT count(*) count FROM #{TABLE_PREFIX}gallery_images + ;", + ).first[ + "count" + ] # NOTE: for imports with huge numbers of galleries, this needs to use limits @@ -546,7 +572,7 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # SQL # ) - images = mysql_query(<<-SQL + images = mysql_query(<<-SQL) SELECT #{TABLE_PREFIX}gallery_albums.album_id tid, #{TABLE_PREFIX}gallery_albums.album_category_id category, @@ -570,43 +596,46 @@ class ImportScripts::IpboardSQL < ImportScripts::Base SQL - ) - break if images.size < 1 - next if all_records_exist? :posts, images.map { |t| "gallery#" + t['tid'].to_s + t['image_id'].to_s } + if all_records_exist? :posts, + images.map { |t| "gallery#" + t["tid"].to_s + t["image_id"].to_s } + next + end - last_id = images.first['tid'] - raw = "Gallery ID: #{last_id}\n" + clean_up(images.first['raw']) - raw += "#{clean_up(images.first['description'])}\n" + last_id = images.first["tid"] + raw = "Gallery ID: #{last_id}\n" + clean_up(images.first["raw"]) + raw += "#{clean_up(images.first["description"])}\n" last_gallery = images.first.dup create_posts(images, total: total_count, offset: offset) do |gallery| - id = gallery['tid'].to_i + id = gallery["tid"].to_i #puts "ID: #{id}, last_id: #{last_id}, image: #{gallery['image_id']}" if id == last_id - raw += "### #{gallery['caption']}\n" - raw += "#{UPLOADS}/#{gallery['orig']}\n" + raw += "### #{gallery["caption"]}\n" + raw += "#{UPLOADS}/#{gallery["orig"]}\n" last_gallery = gallery.dup next else insert_raw = raw.dup - last_id = gallery['tid'] + last_id = gallery["tid"] if DEBUG - raw = "Gallery ID: #{last_id}\n" + clean_up(gallery['raw']) - raw += "Cat: #{last_gallery['category'].to_s} - #{category_id_from_imported_category_id(last_gallery['category'].to_s + 'gal')}" + raw = "Gallery ID: #{last_id}\n" + clean_up(gallery["raw"]) + raw += + "Cat: #{last_gallery["category"].to_s} - #{category_id_from_imported_category_id(last_gallery["category"].to_s + "gal")}" end - raw += "#{clean_up(images.first['description'])}\n" - raw += "### #{gallery['caption']}\n" - if DEBUG - raw += "User #{gallery['user_id']}, image_id: #{gallery['image_id']}\n" - end - raw += "#{UPLOADS}/#{gallery['orig']}\n" + raw += "#{clean_up(images.first["description"])}\n" + raw += "### #{gallery["caption"]}\n" + raw += "User #{gallery["user_id"]}, image_id: #{gallery["image_id"]}\n" if DEBUG + raw += "#{UPLOADS}/#{gallery["orig"]}\n" gallery_count += 1 - puts "#{gallery_count}--Cat: #{last_gallery['category'].to_s} ==> #{category_id_from_imported_category_id(last_gallery['category'].to_s + 'gal')}" unless QUIET + unless QUIET + puts "#{gallery_count}--Cat: #{last_gallery["category"].to_s} ==> #{category_id_from_imported_category_id(last_gallery["category"].to_s + "gal")}" + end { - id: "gallery#" + last_gallery['tid'].to_s + last_gallery['image_id'].to_s, - user_id: user_id_from_imported_user_id(last_gallery['user_id']) || Discourse::SYSTEM_USER_ID, - title: CGI.unescapeHTML(last_gallery['title']), - category: category_id_from_imported_category_id(last_gallery['category'].to_s + 'gal'), + id: "gallery#" + last_gallery["tid"].to_s + last_gallery["image_id"].to_s, + user_id: + user_id_from_imported_user_id(last_gallery["user_id"]) || Discourse::SYSTEM_USER_ID, + title: CGI.unescapeHTML(last_gallery["title"]), + category: category_id_from_imported_category_id(last_gallery["category"].to_s + "gal"), raw: insert_raw, } end @@ -630,11 +659,11 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_comments puts "", "importing gallery comments..." - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}gallery_comments;") - .first['count'] + total_count = + mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}gallery_comments;").first["count"] batches(BATCH_SIZE) do |offset| - comments = mysql_query(<<-SQL + comments = mysql_query(<<-SQL) SELECT #{TABLE_PREFIX}gallery_comments.tid tid, #{TABLE_PREFIX}gallery_topics.forum_id category, @@ -652,20 +681,19 @@ class ImportScripts::IpboardSQL < ImportScripts::Base OFFSET #{offset} SQL - ) break if comments.size < 1 - next if all_records_exist? :posts, comments.map { |comment| "comment#" + comment['pid'].to_s } + next if all_records_exist? :posts, comments.map { |comment| "comment#" + comment["pid"].to_s } create_posts(comments, total: total_count, offset: offset) do |comment| - next unless t = topic_lookup_from_imported_post_id("discussion#" + comment['tid'].to_s) - next if comment['raw'].blank? + next unless t = topic_lookup_from_imported_post_id("discussion#" + comment["tid"].to_s) + next if comment["raw"].blank? { - id: "comment#" + comment['pid'].to_s, - user_id: user_id_from_imported_user_id(comment['user_id']) || Discourse::SYSTEM_USER_ID, + id: "comment#" + comment["pid"].to_s, + user_id: user_id_from_imported_user_id(comment["user_id"]) || Discourse::SYSTEM_USER_ID, topic_id: t[:topic_id], - raw: clean_up(comment['raw']), - created_at: Time.zone.at(comment['created_at']) + raw: clean_up(comment["raw"]), + created_at: Time.zone.at(comment["created_at"]), } end end @@ -674,13 +702,17 @@ class ImportScripts::IpboardSQL < ImportScripts::Base def import_posts puts "", "importing posts..." - total_count = mysql_query("SELECT count(*) count FROM #{POSTS_TABLE} + total_count = + mysql_query( + "SELECT count(*) count FROM #{POSTS_TABLE} WHERE post_date > UNIX_TIMESTAMP(STR_TO_DATE('#{IMPORT_AFTER}', '%Y-%m-%d')) - AND new_topic=0;") - .first['count'] + AND new_topic=0;", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - comments = mysql_query(<<-SQL + comments = mysql_query(<<-SQL) SELECT #{TOPICS_TABLE}.tid tid, #{TOPICS_TABLE}.forum_id category, #{POSTS_TABLE}.pid pid, @@ -696,20 +728,19 @@ class ImportScripts::IpboardSQL < ImportScripts::Base LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if comments.size < 1 - next if all_records_exist? :posts, comments.map { |comment| "comment#" + comment['pid'].to_s } + next if all_records_exist? :posts, comments.map { |comment| "comment#" + comment["pid"].to_s } create_posts(comments, total: total_count, offset: offset) do |comment| - next unless t = topic_lookup_from_imported_post_id("discussion#" + comment['tid'].to_s) - next if comment['raw'].blank? + next unless t = topic_lookup_from_imported_post_id("discussion#" + comment["tid"].to_s) + next if comment["raw"].blank? { - id: "comment#" + comment['pid'].to_s, - user_id: user_id_from_imported_user_id(comment['user_id']) || Discourse::SYSTEM_USER_ID, + id: "comment#" + comment["pid"].to_s, + user_id: user_id_from_imported_user_id(comment["user_id"]) || Discourse::SYSTEM_USER_ID, topic_id: t[:topic_id], - raw: clean_up(comment['raw']), - created_at: Time.zone.at(comment['created_at']) + raw: clean_up(comment["raw"]), + created_at: Time.zone.at(comment["created_at"]), } end end @@ -719,59 +750,61 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # this makes proper quotes with user/topic/post references. # I'm not clear if it is for just some bizarre imported data, or it might ever be useful # It should be integrated into the Nokogiri section of clean_up, though. - @doc = Nokogiri::XML("" + raw + "") + @doc = Nokogiri.XML("" + raw + "") # handle
    s with links to original post - @doc.css('blockquote[class=ipsQuote]').each do |b| - # puts "\n#{'#'*50}\n#{b}\n\nCONTENT: #{b['data-ipsquote-contentid']}" - # b.options = Nokogiri::XML::ParseOptions::STRICT - imported_post_id = b['data-ipsquote-contentcommentid'].to_s - content_type = b['data-ipsquote-contenttype'].to_s - content_class = b['data-ipsquote-contentclass'].to_s - content_id = b['data-ipsquote-contentid'].to_s || b['data-cid'].to_s - topic_lookup = topic_lookup_from_imported_post_id("comment#" + imported_post_id) - post_lookup = topic_lookup_from_imported_post_id("discussion#" + content_id) - post = topic_lookup ? topic_lookup[:post_number] : nil - topic = topic_lookup ? topic_lookup[:topic_id] : nil - post ||= post_lookup ? post_lookup[:post_number] : nil - topic ||= post_lookup ? post_lookup[:topic_id] : nil + @doc + .css("blockquote[class=ipsQuote]") + .each do |b| + # puts "\n#{'#'*50}\n#{b}\n\nCONTENT: #{b['data-ipsquote-contentid']}" + # b.options = Nokogiri::XML::ParseOptions::STRICT + imported_post_id = b["data-ipsquote-contentcommentid"].to_s + content_type = b["data-ipsquote-contenttype"].to_s + content_class = b["data-ipsquote-contentclass"].to_s + content_id = b["data-ipsquote-contentid"].to_s || b["data-cid"].to_s + topic_lookup = topic_lookup_from_imported_post_id("comment#" + imported_post_id) + post_lookup = topic_lookup_from_imported_post_id("discussion#" + content_id) + post = topic_lookup ? topic_lookup[:post_number] : nil + topic = topic_lookup ? topic_lookup[:topic_id] : nil + post ||= post_lookup ? post_lookup[:post_number] : nil + topic ||= post_lookup ? post_lookup[:topic_id] : nil - # TODO: consider:
    - # consider:
    -      # TODO make sure it's the imported username
    -      # TODO: do _s still get \-escaped?
    -      ips_username = b['data-ipsquote-username'] || b['data-author']
    -      username = ips_username
    -      new_text = ""
    -      if DEBUG
    -        # new_text += "post: #{imported_post_id} --> #{post_lookup} --> |#{post}|
    \n" - # new_text += "topic: #{content_id} --> #{topic_lookup} --> |#{topic}|
    \n" - # new_text += "user: #{ips_username} --> |#{username}|
    \n" - # new_text += "class: #{content_class}
    \n" - # new_text += "type: #{content_type}
    \n" - if content_class.length > 0 && content_class != "forums_Topic" - new_text += "UNEXPECTED CONTENT CLASS! #{content_class}
    \n" + # TODO: consider:
    + # consider:
    +        # TODO make sure it's the imported username
    +        # TODO: do _s still get \-escaped?
    +        ips_username = b["data-ipsquote-username"] || b["data-author"]
    +        username = ips_username
    +        new_text = ""
    +        if DEBUG
    +          # new_text += "post: #{imported_post_id} --> #{post_lookup} --> |#{post}|
    \n" + # new_text += "topic: #{content_id} --> #{topic_lookup} --> |#{topic}|
    \n" + # new_text += "user: #{ips_username} --> |#{username}|
    \n" + # new_text += "class: #{content_class}
    \n" + # new_text += "type: #{content_type}
    \n" + if content_class.length > 0 && content_class != "forums_Topic" + new_text += "UNEXPECTED CONTENT CLASS! #{content_class}
    \n" + end + if content_type.length > 0 && content_type != "forums" + new_text += "UNEXPECTED CONTENT TYPE! #{content_type}
    \n" + end + # puts "#{'-'*20} and NOWWWWW!!!! \n #{new_text}" end - if content_type.length > 0 && content_type != "forums" - new_text += "UNEXPECTED CONTENT TYPE! #{content_type}
    \n" - end - # puts "#{'-'*20} and NOWWWWW!!!! \n #{new_text}" - end - if post && topic && username - quote = "\n[quote=\"#{username}, post:#{post}, topic: #{topic}\"]\n\n" - else - if username && username.length > 1 - quote = "\n[quote=\"#{username}\"]\n\n" + if post && topic && username + quote = "\n[quote=\"#{username}, post:#{post}, topic: #{topic}\"]\n\n" else - quote = "\n[quote]\n" + if username && username.length > 1 + quote = "\n[quote=\"#{username}\"]\n\n" + else + quote = "\n[quote]\n" + end + # new_doc = Nokogiri::XML("
    #{new_text}
    ") end - # new_doc = Nokogiri::XML("
    #{new_text}
    ") + puts "QUOTE: #{quote}" + sleep 1 + b.content = quote + b.content + "\n[/quote]\n" + b.name = "div" end - puts "QUOTE: #{quote}" - sleep 1 - b.content = quote + b.content + "\n[/quote]\n" - b.name = 'div' - end raw = @doc.to_html end @@ -783,24 +816,30 @@ class ImportScripts::IpboardSQL < ImportScripts::Base # TODO what about uploads? # raw.gsub!(//,UPLOADS) raw.gsub!(/
    /, "\n\n") - raw.gsub!(/
    /, "\n\n") - raw.gsub!(/

     <\/p>/, "\n\n") + raw.gsub!(%r{
    }, "\n\n") + raw.gsub!(%r{

     

    }, "\n\n") raw.gsub!(/\[hr\]/, "\n***\n") raw.gsub!(/'/, "'") - raw.gsub!(/\[url="(.+?)"\]http.+?\[\/url\]/, "\\1\n") - raw.gsub!(/\[media\](.+?)\[\/media\]/, "\n\\1\n\n") - raw.gsub!(/\[php\](.+?)\[\/php\]/m) { |m| "\n\n```php\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" } - raw.gsub!(/\[code\](.+?)\[\/code\]/m) { |m| "\n\n```\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" } - raw.gsub!(/\[list\](.+?)\[\/list\]/m) { |m| "\n" + $1.gsub(/\[\*\]/, "\n- ") + "\n\n" } + raw.gsub!(%r{\[url="(.+?)"\]http.+?\[/url\]}, "\\1\n") + raw.gsub!(%r{\[media\](.+?)\[/media\]}, "\n\\1\n\n") + raw.gsub!(%r{\[php\](.+?)\[/php\]}m) do |m| + "\n\n```php\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" + end + raw.gsub!(%r{\[code\](.+?)\[/code\]}m) do |m| + "\n\n```\n\n" + @htmlentities.decode($1.gsub(/\n\n/, "\n")) + "\n\n```\n\n" + end + raw.gsub!(%r{\[list\](.+?)\[/list\]}m) { |m| "\n" + $1.gsub(/\[\*\]/, "\n- ") + "\n\n" } raw.gsub!(/\[quote\]/, "\n[quote]\n") - raw.gsub!(/\[\/quote\]/, "\n[/quote]\n") - raw.gsub!(/date=\'(.+?)\'/, '') - raw.gsub!(/timestamp=\'(.+?)\' /, '') + raw.gsub!(%r{\[/quote\]}, "\n[/quote]\n") + raw.gsub!(/date=\'(.+?)\'/, "") + raw.gsub!(/timestamp=\'(.+?)\' /, "") quote_regex = /\[quote name=\'(.+?)\'\s+post=\'(\d+?)\'\s*\]/ while quote = quote_regex.match(raw) # get IPB post number and find Discourse post and topic number - puts "----------------------------------------\nName: #{quote[1]}, post: #{quote[2]}" unless QUIET + unless QUIET + puts "----------------------------------------\nName: #{quote[1]}, post: #{quote[2]}" + end imported_post_id = quote[2].to_s topic_lookup = topic_lookup_from_imported_post_id("comment#" + imported_post_id) post_lookup = topic_lookup_from_imported_post_id("discussion#" + imported_post_id) @@ -826,21 +865,24 @@ class ImportScripts::IpboardSQL < ImportScripts::Base while attach = attach_regex.match(raw) attach_id = attach[1] attachments = - mysql_query("SELECT attach_location as loc, + mysql_query( + "SELECT attach_location as loc, attach_file as filename FROM #{ATTACHMENT_TABLE} - WHERE attach_id=#{attach_id}") + WHERE attach_id=#{attach_id}", + ) if attachments.count < 1 puts "Attachment #{attach_id} not found." attach_string = "Attachment #{attach_id} not found." else - attach_url = "#{UPLOADS}/#{attachments.first['loc'].gsub(' ', '%20')}" - if attachments.first['filename'].match(/(png|jpg|jpeg|gif)$/) + attach_url = "#{UPLOADS}/#{attachments.first["loc"].gsub(" ", "%20")}" + if attachments.first["filename"].match(/(png|jpg|jpeg|gif)$/) # images are rendered as a link that contains the image - attach_string = "#{attach_id}\n\n[![#{attachments.first['filename']}](#{attach_url})](#{attach_url})\n" + attach_string = + "#{attach_id}\n\n[![#{attachments.first["filename"]}](#{attach_url})](#{attach_url})\n" else # other attachments are simple download links - attach_string = "#{attach_id}\n\n[#{attachments.first['filename']}](#{attach_url})\n" + attach_string = "#{attach_id}\n\n[#{attachments.first["filename"]}](#{attach_url})\n" end end raw.sub!(attach_regex, attach_string) @@ -850,7 +892,7 @@ class ImportScripts::IpboardSQL < ImportScripts::Base end def random_category_color - colors = SiteSetting.category_colors.split('|') + colors = SiteSetting.category_colors.split("|") colors[rand(colors.count)] end @@ -865,78 +907,78 @@ class ImportScripts::IpboardSQL < ImportScripts::Base raw.gsub!(//, UPLOADS) raw.gsub!(/
    /, "\n") - @doc = Nokogiri::XML("" + raw + "") + @doc = Nokogiri.XML("" + raw + "") # handle
    s with links to original post - @doc.css('blockquote[class=ipsQuote]').each do |b| - imported_post_id = b['data-ipsquote-contentcommentid'].to_s - content_type = b['data-ipsquote-contenttype'].to_s - content_class = b['data-ipsquote-contentclass'].to_s - content_id = b['data-ipsquote-contentid'].to_s || b['data-cid'].to_s - topic_lookup = topic_lookup_from_imported_post_id("comment#" + imported_post_id) - post_lookup = topic_lookup_from_imported_post_id("discussion#" + content_id) - post = topic_lookup ? topic_lookup[:post_number] : nil - topic = topic_lookup ? topic_lookup[:topic_id] : nil - post ||= post_lookup ? post_lookup[:post_number] : nil - topic ||= post_lookup ? post_lookup[:topic_id] : nil + @doc + .css("blockquote[class=ipsQuote]") + .each do |b| + imported_post_id = b["data-ipsquote-contentcommentid"].to_s + content_type = b["data-ipsquote-contenttype"].to_s + content_class = b["data-ipsquote-contentclass"].to_s + content_id = b["data-ipsquote-contentid"].to_s || b["data-cid"].to_s + topic_lookup = topic_lookup_from_imported_post_id("comment#" + imported_post_id) + post_lookup = topic_lookup_from_imported_post_id("discussion#" + content_id) + post = topic_lookup ? topic_lookup[:post_number] : nil + topic = topic_lookup ? topic_lookup[:topic_id] : nil + post ||= post_lookup ? post_lookup[:post_number] : nil + topic ||= post_lookup ? post_lookup[:topic_id] : nil - # TODO: consider:
    - # consider:
    -      ips_username = b['data-ipsquote-username'] || b['data-author']
    -      username = ips_username
    -      new_text = ""
    -      if DEBUG
    -        if content_class.length > 0 && content_class != "forums_Topic"
    -          new_text += "UNEXPECTED CONTENT CLASS! #{content_class}
    \n" + # TODO: consider:
    + # consider:
    +        ips_username = b["data-ipsquote-username"] || b["data-author"]
    +        username = ips_username
    +        new_text = ""
    +        if DEBUG
    +          if content_class.length > 0 && content_class != "forums_Topic"
    +            new_text += "UNEXPECTED CONTENT CLASS! #{content_class}
    \n" + end + if content_type.length > 0 && content_type != "forums" + new_text += "UNEXPECTED CONTENT TYPE! #{content_type}
    \n" + end end - if content_type.length > 0 && content_type != "forums" - new_text += "UNEXPECTED CONTENT TYPE! #{content_type}
    \n" - end - end - if post && topic && username - quote = "[quote=\"#{username}, post:#{post}, topic: #{topic}\"]\n\n" - else - if username && username.length > 1 - quote = "[quote=\"#{username}\"]\n\n" + if post && topic && username + quote = "[quote=\"#{username}, post:#{post}, topic: #{topic}\"]\n\n" else - quote = "[quote]\n" + if username && username.length > 1 + quote = "[quote=\"#{username}\"]\n\n" + else + quote = "[quote]\n" + end end + b.content = quote + b.content + "\n[/quote]\n" + b.name = "div" end - b.content = quote + b.content + "\n[/quote]\n" - b.name = 'div' - end - @doc.css('object param embed').each do |embed| - embed.replace("\n#{embed['src']}\n") - end + @doc.css("object param embed").each { |embed| embed.replace("\n#{embed["src"]}\n") } # handle }mix youtube_cooked.gsub!(re) { "\n#{$1}\n" } - re = //mix + re = %r{}mix youtube_cooked.gsub!(re) { "\n#{$1}\n" } - youtube_cooked.gsub!(/^\/\//, "https://") # make sure it has a protocol + youtube_cooked.gsub!(%r{^//}, "https://") # make sure it has a protocol unless /http/.match(youtube_cooked) # handle case of only youtube object number if youtube_cooked.length < 8 || /[<>=]/.match(youtube_cooked) # probably not a youtube id youtube_cooked = "" else - youtube_cooked = 'https://www.youtube.com/watch?v=' + youtube_cooked + youtube_cooked = "https://www.youtube.com/watch?v=" + youtube_cooked end end - print_warning("#{'-' * 40}\nBefore: #{youtube_raw}\nAfter: #{youtube_cooked}") unless QUIET + print_warning("#{"-" * 40}\nBefore: #{youtube_raw}\nAfter: #{youtube_cooked}") unless QUIET youtube_cooked end @@ -313,73 +334,79 @@ class ImportScripts::MylittleforumSQL < ImportScripts::Base raw = raw.gsub("\\'", "'") raw = raw.gsub(/\[b\]/i, "") - raw = raw.gsub(/\[\/b\]/i, "") + raw = raw.gsub(%r{\[/b\]}i, "") raw = raw.gsub(/\[i\]/i, "") - raw = raw.gsub(/\[\/i\]/i, "") + raw = raw.gsub(%r{\[/i\]}i, "") raw = raw.gsub(/\[u\]/i, "") - raw = raw.gsub(/\[\/u\]/i, "") + raw = raw.gsub(%r{\[/u\]}i, "") - raw = raw.gsub(/\[url\](\S+)\[\/url\]/im) { "#{$1}" } - raw = raw.gsub(/\[link\](\S+)\[\/link\]/im) { "#{$1}" } + raw = raw.gsub(%r{\[url\](\S+)\[/url\]}im) { "#{$1}" } + raw = raw.gsub(%r{\[link\](\S+)\[/link\]}im) { "#{$1}" } # URL & LINK with text - raw = raw.gsub(/\[url=(\S+?)\](.*?)\[\/url\]/im) { "#{$2}" } - raw = raw.gsub(/\[link=(\S+?)\](.*?)\[\/link\]/im) { "#{$2}" } + raw = raw.gsub(%r{\[url=(\S+?)\](.*?)\[/url\]}im) { "#{$2}" } + raw = raw.gsub(%r{\[link=(\S+?)\](.*?)\[/link\]}im) { "#{$2}" } # remote images - raw = raw.gsub(/\[img\](https?:.+?)\[\/img\]/im) { "" } - raw = raw.gsub(/\[img=(https?.+?)\](.+?)\[\/img\]/im) { "\"#{$2}\"" } + raw = raw.gsub(%r{\[img\](https?:.+?)\[/img\]}im) { "" } + raw = raw.gsub(%r{\[img=(https?.+?)\](.+?)\[/img\]}im) { "\"#{$2}\"" } # local images - raw = raw.gsub(/\[img\](.+?)\[\/img\]/i) { "" } - raw = raw.gsub(/\[img=(.+?)\](https?.+?)\[\/img\]/im) { "\"#{$2}\"" } + raw = raw.gsub(%r{\[img\](.+?)\[/img\]}i) { "" } + raw = + raw.gsub(%r{\[img=(.+?)\](https?.+?)\[/img\]}im) do + "\"#{$2}\"" + end # Convert image bbcode - raw.gsub!(/\[img=(\d+),(\d+)\]([^\]]*)\[\/img\]/im, '') + raw.gsub!(%r{\[img=(\d+),(\d+)\]([^\]]*)\[/img\]}im, '') # [div]s are really [quote]s raw.gsub!(/\[div\]/mix, "[quote]") - raw.gsub!(/\[\/div\]/mix, "[/quote]") + raw.gsub!(%r{\[/div\]}mix, "[/quote]") # [postedby] -> link to @user - raw.gsub(/\[postedby\](.+?)\[b\](.+?)\[\/b\]\[\/postedby\]/i) { "#{$1}@#{$2}" } + raw.gsub(%r{\[postedby\](.+?)\[b\](.+?)\[/b\]\[/postedby\]}i) { "#{$1}@#{$2}" } # CODE (not tested) - raw = raw.gsub(/\[code\](\S+)\[\/code\]/im) { "```\n#{$1}\n```" } - raw = raw.gsub(/\[pre\](\S+)\[\/pre\]/im) { "```\n#{$1}\n```" } + raw = raw.gsub(%r{\[code\](\S+)\[/code\]}im) { "```\n#{$1}\n```" } + raw = raw.gsub(%r{\[pre\](\S+)\[/pre\]}im) { "```\n#{$1}\n```" } - raw = raw.gsub(/(https:\/\/youtu\S+)/i) { "\n#{$1}\n" } #youtube links on line by themselves + raw = raw.gsub(%r{(https://youtu\S+)}i) { "\n#{$1}\n" } #youtube links on line by themselves # no center - raw = raw.gsub(/\[\/?center\]/i, "") + raw = raw.gsub(%r{\[/?center\]}i, "") # no size - raw = raw.gsub(/\[\/?size.*?\]/i, "") + raw = raw.gsub(%r{\[/?size.*?\]}i, "") ### FROM VANILLA: # fix whitespaces - raw = raw.gsub(/(\\r)?\\n/, "\n") - .gsub("\\t", "\t") + raw = raw.gsub(/(\\r)?\\n/, "\n").gsub("\\t", "\t") unless CONVERT_HTML # replace all chevrons with HTML entities # NOTE: must be done # - AFTER all the "code" processing # - BEFORE the "quote" processing - raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" } - .gsub("<", "<") - .gsub("\u2603", "<") + raw = + raw + .gsub(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" } + .gsub("<", "<") + .gsub("\u2603", "<") - raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" } - .gsub(">", ">") - .gsub("\u2603", ">") + raw = + raw + .gsub(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" } + .gsub(">", ">") + .gsub("\u2603", ">") end # Remove the color tag raw.gsub!(/\[color=[#a-z0-9]+\]/i, "") - raw.gsub!(/\[\/color\]/i, "") + raw.gsub!(%r{\[/color\]}i, "") ### END VANILLA: raw @@ -395,54 +422,72 @@ class ImportScripts::MylittleforumSQL < ImportScripts::Base end def create_permalinks - puts '', 'Creating redirects...', '' + puts "", "Creating redirects...", "" - puts '', 'Users...', '' + puts "", "Users...", "" User.find_each do |u| ucf = u.custom_fields if ucf && ucf["import_id"] && ucf["import_username"] - Permalink.create(url: "#{BASE}/user-id-#{ucf['import_id']}.html", external_url: "/u/#{u.username}") rescue nil - print '.' + begin + Permalink.create( + url: "#{BASE}/user-id-#{ucf["import_id"]}.html", + external_url: "/u/#{u.username}", + ) + rescue StandardError + nil + end + print "." end end - puts '', 'Posts...', '' + puts "", "Posts...", "" Post.find_each do |post| pcf = post.custom_fields if pcf && pcf["import_id"] topic = post.topic - id = pcf["import_id"].split('#').last + id = pcf["import_id"].split("#").last if post.post_number == 1 - Permalink.create(url: "#{BASE}/forum_entry-id-#{id}.html", topic_id: topic.id) rescue nil + begin + Permalink.create(url: "#{BASE}/forum_entry-id-#{id}.html", topic_id: topic.id) + rescue StandardError + nil + end unless QUIET print_warning("forum_entry-id-#{id}.html --> http://localhost:3000/t/#{topic.id}") end else - Permalink.create(url: "#{BASE}/forum_entry-id-#{id}.html", post_id: post.id) rescue nil + begin + Permalink.create(url: "#{BASE}/forum_entry-id-#{id}.html", post_id: post.id) + rescue StandardError + nil + end unless QUIET - print_warning("forum_entry-id-#{id}.html --> http://localhost:3000/t/#{topic.id}/#{post.id}") + print_warning( + "forum_entry-id-#{id}.html --> http://localhost:3000/t/#{topic.id}/#{post.id}", + ) end end - print '.' + print "." end end - puts '', 'Categories...', '' + puts "", "Categories...", "" Category.find_each do |cat| ccf = cat.custom_fields next unless id = ccf["import_id"] - unless QUIET - print_warning("forum-category-#{id}.html --> /t/#{cat.id}") + print_warning("forum-category-#{id}.html --> /t/#{cat.id}") unless QUIET + begin + Permalink.create(url: "#{BASE}/forum-category-#{id}.html", category_id: cat.id) + rescue StandardError + nil end - Permalink.create(url: "#{BASE}/forum-category-#{id}.html", category_id: cat.id) rescue nil - print '.' + print "." end end def print_warning(message) $stderr.puts "#{message}" end - end ImportScripts::MylittleforumSQL.new.perform diff --git a/script/import_scripts/nabble.rb b/script/import_scripts/nabble.rb index e877e9058d..c43efe8e5c 100644 --- a/script/import_scripts/nabble.rb +++ b/script/import_scripts/nabble.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'pg' -require_relative 'base/uploader' +require "pg" +require_relative "base/uploader" =begin if you want to create mock users for posts made by anonymous participants, @@ -40,7 +40,7 @@ class ImportScripts::Nabble < ImportScripts::Base BATCH_SIZE = 1000 - DB_NAME = "nabble" + DB_NAME = "nabble" CATEGORY_ID = 6 def initialize @@ -64,14 +64,13 @@ class ImportScripts::Nabble < ImportScripts::Base total_count = @client.exec("SELECT COUNT(user_id) FROM user_")[0]["count"] batches(BATCH_SIZE) do |offset| - users = @client.query(<<-SQL + users = @client.query(<<-SQL) SELECT user_id, name, email, joined FROM user_ ORDER BY joined LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL - ) break if users.ntuples() < 1 @@ -83,24 +82,23 @@ class ImportScripts::Nabble < ImportScripts::Base email: row["email"] || fake_email, created_at: Time.zone.at(@td.decode(row["joined"])), name: row["name"], - post_create_action: proc do |user| - import_avatar(user, row["user_id"]) - end + post_create_action: proc { |user| import_avatar(user, row["user_id"]) }, } end end end def import_avatar(user, org_id) - filename = 'avatar' + org_id.to_s - path = File.join('/tmp/nab', filename) - res = @client.exec("SELECT content FROM file_avatar WHERE name='avatar100.png' AND user_id = #{org_id} LIMIT 1") + filename = "avatar" + org_id.to_s + path = File.join("/tmp/nab", filename) + res = + @client.exec( + "SELECT content FROM file_avatar WHERE name='avatar100.png' AND user_id = #{org_id} LIMIT 1", + ) return if res.ntuples() < 1 - binary = res[0]['content'] - File.open(path, 'wb') { |f| - f.write(PG::Connection.unescape_bytea(binary)) - } + binary = res[0]["content"] + File.open(path, "wb") { |f| f.write(PG::Connection.unescape_bytea(binary)) } upload = @uploader.create_upload(user.id, path, filename) @@ -113,7 +111,6 @@ class ImportScripts::Nabble < ImportScripts::Base else Rails.logger.error("Could not persist avatar for user #{user.username}") end - end def parse_email(msg) @@ -128,11 +125,13 @@ class ImportScripts::Nabble < ImportScripts::Base def create_forum_topics puts "", "creating forum topics" - app_node_id = @client.exec("SELECT node_id FROM node WHERE is_app LIMIT 1")[0]['node_id'] - topic_count = @client.exec("SELECT COUNT(node_id) AS count FROM node WHERE parent_id = #{app_node_id}")[0]["count"] + app_node_id = @client.exec("SELECT node_id FROM node WHERE is_app LIMIT 1")[0]["node_id"] + topic_count = + @client.exec("SELECT COUNT(node_id) AS count FROM node WHERE parent_id = #{app_node_id}")[0][ + "count" + ] batches(BATCH_SIZE) do |offset| - topics = @client.exec <<-SQL SELECT n.node_id, n.subject, n.owner_id, n.when_created, nm.message, n.msg_fmt FROM node AS n @@ -145,43 +144,43 @@ class ImportScripts::Nabble < ImportScripts::Base break if topics.ntuples() < 1 - next if all_records_exist? :posts, topics.map { |t| t['node_id'].to_i } + next if all_records_exist? :posts, topics.map { |t| t["node_id"].to_i } create_posts(topics, total: topic_count, offset: offset) do |t| raw = body_from(t) next unless raw raw = process_content(raw) - raw = process_attachments(raw, t['node_id']) + raw = process_attachments(raw, t["node_id"]) { - id: t['node_id'], - title: t['subject'], + id: t["node_id"], + title: t["subject"], user_id: user_id_from_imported_user_id(t["owner_id"]) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(t["when_created"])), category: CATEGORY_ID, raw: raw, - cook_method: Post.cook_methods[:regular] + cook_method: Post.cook_methods[:regular], } end end end def body_from(p) - %w(m s).include?(p['msg_fmt']) ? parse_email(p['message']) : p['message'] + %w[m s].include?(p["msg_fmt"]) ? parse_email(p["message"]) : p["message"] rescue Email::Receiver::EmptyEmailError - puts "Skipped #{p['node_id']}" + puts "Skipped #{p["node_id"]}" end def process_content(txt) txt.gsub! /\/, '[quote="\1"]' - txt.gsub! /\<\/quote\>/, '[/quote]' - txt.gsub!(/\(.*?)\<\/raw\>/m) do |match| + txt.gsub! %r{\}, "[/quote]" + txt.gsub!(%r{\(.*?)\}m) do |match| c = Regexp.last_match[1].indent(4) - "\n#{c}\n" + "\n#{c}\n" end # lines starting with # are comments, not headings, insert a space to prevent markdown - txt.gsub! /\n#/m, ' #' + txt.gsub! /\n#/m, " #" # in the languagetool forum, quite a lot of XML was not marked as raw # so we treat ... and ... as raw @@ -202,12 +201,10 @@ class ImportScripts::Nabble < ImportScripts::Base def process_attachments(txt, postid) txt.gsub!(//m) do |match| basename = Regexp.last_match[1] - get_attachment_upload(basename, postid) do |upload| - @uploader.embedded_image_html(upload) - end + get_attachment_upload(basename, postid) { |upload| @uploader.embedded_image_html(upload) } end - txt.gsub!(/(.*?)<\/nabble_a>/m) do |match| + txt.gsub!(%r{(.*?)}m) do |match| basename = Regexp.last_match[1] get_attachment_upload(basename, postid) do |upload| @uploader.attachment_html(upload, basename) @@ -217,13 +214,12 @@ class ImportScripts::Nabble < ImportScripts::Base end def get_attachment_upload(basename, postid) - contents = @client.exec("SELECT content FROM file_node WHERE name='#{basename}' AND node_id = #{postid}") + contents = + @client.exec("SELECT content FROM file_node WHERE name='#{basename}' AND node_id = #{postid}") if contents.any? - binary = contents[0]['content'] - fn = File.join('/tmp/nab', basename) - File.open(fn, 'wb') { |f| - f.write(PG::Connection.unescape_bytea(binary)) - } + binary = contents[0]["content"] + fn = File.join("/tmp/nab", basename) + File.open(fn, "wb") { |f| f.write(PG::Connection.unescape_bytea(binary)) } yield @uploader.create_upload(0, fn, basename) end end @@ -231,8 +227,11 @@ class ImportScripts::Nabble < ImportScripts::Base def import_replies puts "", "creating topic replies" - app_node_id = @client.exec("SELECT node_id FROM node WHERE is_app LIMIT 1")[0]['node_id'] - post_count = @client.exec("SELECT COUNT(node_id) AS count FROM node WHERE parent_id != #{app_node_id}")[0]["count"] + app_node_id = @client.exec("SELECT node_id FROM node WHERE is_app LIMIT 1")[0]["node_id"] + post_count = + @client.exec("SELECT COUNT(node_id) AS count FROM node WHERE parent_id != #{app_node_id}")[0][ + "count" + ] topic_ids = {} @@ -249,11 +248,11 @@ class ImportScripts::Nabble < ImportScripts::Base break if posts.ntuples() < 1 - next if all_records_exist? :posts, posts.map { |p| p['node_id'].to_i } + next if all_records_exist? :posts, posts.map { |p| p["node_id"].to_i } create_posts(posts, total: post_count, offset: offset) do |p| - parent_id = p['parent_id'] - id = p['node_id'] + parent_id = p["parent_id"] + id = p["node_id"] topic_id = topic_ids[parent_id] unless topic_id @@ -268,19 +267,21 @@ class ImportScripts::Nabble < ImportScripts::Base next unless raw raw = process_content(raw) raw = process_attachments(raw, id) - { id: id, + { + id: id, topic_id: topic_id, - user_id: user_id_from_imported_user_id(p['owner_id']) || Discourse::SYSTEM_USER_ID, + user_id: user_id_from_imported_user_id(p["owner_id"]) || Discourse::SYSTEM_USER_ID, created_at: Time.zone.at(@td.decode(p["when_created"])), raw: raw, - cook_method: Post.cook_methods[:regular] } + cook_method: Post.cook_methods[:regular], + } end end end end class String - def indent(count, char = ' ') + def indent(count, char = " ") gsub(/([^\n]*)(\n|$)/) do |match| last_iteration = ($1 == "" && $2 == "") line = +"" diff --git a/script/import_scripts/ning.rb b/script/import_scripts/ning.rb index 30612ec969..3af9b080d1 100644 --- a/script/import_scripts/ning.rb +++ b/script/import_scripts/ning.rb @@ -5,28 +5,28 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Edit the constants and initialize method for your import data. class ImportScripts::Ning < ImportScripts::Base - JSON_FILES_DIR = "/Users/techapj/Downloads/ben/ADEM" - ATTACHMENT_PREFIXES = ["discussions", "pages", "blogs", "members", "photos"] - EXTRA_AUTHORIZED_EXTENSIONS = ["bmp", "ico", "txt", "pdf", "gif", "jpg", "jpeg", "html"] + ATTACHMENT_PREFIXES = %w[discussions pages blogs members photos] + EXTRA_AUTHORIZED_EXTENSIONS = %w[bmp ico txt pdf gif jpg jpeg html] def initialize super @system_user = Discourse.system_user - @users_json = load_ning_json("ning-members-local.json") + @users_json = load_ning_json("ning-members-local.json") @discussions_json = load_ning_json("ning-discussions-local.json") # An example of a custom category from Ning: @blogs_json = load_ning_json("ning-blogs-local.json") - @photos_json = load_ning_json("ning-photos-local.json") - @pages_json = load_ning_json("ning-pages-local.json") + @photos_json = load_ning_json("ning-photos-local.json") + @pages_json = load_ning_json("ning-pages-local.json") - SiteSetting.max_image_size_kb = 10240 - SiteSetting.max_attachment_size_kb = 10240 - SiteSetting.authorized_extensions = (SiteSetting.authorized_extensions.split("|") + EXTRA_AUTHORIZED_EXTENSIONS).uniq.join("|") + SiteSetting.max_image_size_kb = 10_240 + SiteSetting.max_attachment_size_kb = 10_240 + SiteSetting.authorized_extensions = + (SiteSetting.authorized_extensions.split("|") + EXTRA_AUTHORIZED_EXTENSIONS).uniq.join("|") # Example of importing a custom profile field: # @interests_field = UserField.find_by_name("My interests") @@ -60,23 +60,23 @@ class ImportScripts::Ning < ImportScripts::Base end def repair_json(arg) - arg.gsub!(/^\(/, "") # content of file is surround by ( ) + arg.gsub!(/^\(/, "") # content of file is surround by ( ) arg.gsub!(/\)$/, "") - arg.gsub!(/\]\]$/, "]") # there can be an extra ] at the end + arg.gsub!(/\]\]$/, "]") # there can be an extra ] at the end arg.gsub!(/\}\{/, "},{") # missing commas sometimes! - arg.gsub!("}]{", "},{") # surprise square brackets - arg.gsub!("}[{", "},{") # :troll: + arg.gsub!("}]{", "},{") # surprise square brackets + arg.gsub!("}[{", "},{") # :troll: arg end def import_users - puts '', "Importing users" + puts "", "Importing users" - staff_levels = ["admin", "moderator", "owner"] + staff_levels = %w[admin moderator owner] create_users(@users_json) do |u| { @@ -88,57 +88,58 @@ class ImportScripts::Ning < ImportScripts::Base location: "#{u["location"]} #{u["country"]}", avatar_url: u["profilePhoto"], bio_raw: u["profileQuestions"].is_a?(Hash) ? u["profileQuestions"]["About Me"] : nil, - post_create_action: proc do |newuser| - # if u["profileQuestions"].is_a?(Hash) - # newuser.custom_fields = {"user_field_#{@interests_field.id}" => u["profileQuestions"]["My interests"]} - # end + post_create_action: + proc do |newuser| + # if u["profileQuestions"].is_a?(Hash) + # newuser.custom_fields = {"user_field_#{@interests_field.id}" => u["profileQuestions"]["My interests"]} + # end - if staff_levels.include?(u["level"].downcase) - if u["level"].downcase == "admin" || u["level"].downcase == "owner" - newuser.admin = true - else - newuser.moderator = true - end - end - - # states: ["active", "suspended", "left", "pending"] - if u["state"] == "active" && newuser.approved_at.nil? - newuser.approved = true - newuser.approved_by_id = @system_user.id - newuser.approved_at = newuser.created_at - end - - newuser.save - - if u["profilePhoto"] && newuser.user_avatar.try(:custom_upload_id).nil? - photo_path = file_full_path(u["profilePhoto"]) - if File.exist?(photo_path) - begin - upload = create_upload(newuser.id, photo_path, File.basename(photo_path)) - if upload.persisted? - newuser.import_mode = false - newuser.create_user_avatar - newuser.import_mode = true - newuser.user_avatar.update(custom_upload_id: upload.id) - newuser.update(uploaded_avatar_id: upload.id) - else - puts "Error: Upload did not persist for #{photo_path}!" - end - rescue SystemCallError => err - puts "Could not import avatar #{photo_path}: #{err.message}" + if staff_levels.include?(u["level"].downcase) + if u["level"].downcase == "admin" || u["level"].downcase == "owner" + newuser.admin = true + else + newuser.moderator = true end - else - puts "avatar file not found at #{photo_path}" end - end - end + + # states: ["active", "suspended", "left", "pending"] + if u["state"] == "active" && newuser.approved_at.nil? + newuser.approved = true + newuser.approved_by_id = @system_user.id + newuser.approved_at = newuser.created_at + end + + newuser.save + + if u["profilePhoto"] && newuser.user_avatar.try(:custom_upload_id).nil? + photo_path = file_full_path(u["profilePhoto"]) + if File.exist?(photo_path) + begin + upload = create_upload(newuser.id, photo_path, File.basename(photo_path)) + if upload.persisted? + newuser.import_mode = false + newuser.create_user_avatar + newuser.import_mode = true + newuser.user_avatar.update(custom_upload_id: upload.id) + newuser.update(uploaded_avatar_id: upload.id) + else + puts "Error: Upload did not persist for #{photo_path}!" + end + rescue SystemCallError => err + puts "Could not import avatar #{photo_path}: #{err.message}" + end + else + puts "avatar file not found at #{photo_path}" + end + end + end, } end EmailToken.delete_all end def suspend_users - puts '', "Updating suspended users" + puts "", "Updating suspended users" count = 0 suspended = 0 @@ -151,7 +152,10 @@ class ImportScripts::Ning < ImportScripts::Base user.suspended_till = 200.years.from_now if user.save - StaffActionLogger.new(@system_user).log_user_suspend(user, "Import data indicates account is suspended.") + StaffActionLogger.new(@system_user).log_user_suspend( + user, + "Import data indicates account is suspended.", + ) suspended += 1 else puts "Failed to suspend user #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}" @@ -168,13 +172,15 @@ class ImportScripts::Ning < ImportScripts::Base def import_categories puts "", "Importing categories" - create_categories((["Blog", "Pages", "Photos"] + @discussions_json.map { |d| d["category"] }).uniq.compact) do |name| + create_categories( + (%w[Blog Pages Photos] + @discussions_json.map { |d| d["category"] }).uniq.compact, + ) do |name| if name.downcase == "uncategorized" nil else { id: name, # ning has no id for categories, so use the name - name: name + name: name, } end end @@ -220,9 +226,7 @@ class ImportScripts::Ning < ImportScripts::Base unless topic["category"].nil? || topic["category"].downcase == "uncategorized" mapped[:category] = category_id_from_imported_category_id(topic["category"]) end - if topic["category"].nil? && default_category - mapped[:category] = default_category - end + mapped[:category] = default_category if topic["category"].nil? && default_category mapped[:title] = CGI.unescapeHTML(topic["title"]) mapped[:raw] = process_ning_post_body(topic["description"]) @@ -230,13 +234,9 @@ class ImportScripts::Ning < ImportScripts::Base mapped[:raw] = add_file_attachments(mapped[:raw], topic["fileAttachments"]) end - if topic["photoUrl"] - mapped[:raw] = add_photo(mapped[:raw], topic["photoUrl"]) - end + mapped[:raw] = add_photo(mapped[:raw], topic["photoUrl"]) if topic["photoUrl"] - if topic["embedCode"] - mapped[:raw] = add_video(mapped[:raw], topic["embedCode"]) - end + mapped[:raw] = add_video(mapped[:raw], topic["embedCode"]) if topic["embedCode"] parent_post = create_post(mapped, mapped[:id]) unless parent_post.is_a?(Post) @@ -247,23 +247,24 @@ class ImportScripts::Ning < ImportScripts::Base if topic["comments"].present? topic["comments"].reverse.each do |post| - if post_id_from_imported_post_id(post["id"]) next # already imported this post end raw = process_ning_post_body(post["description"]) - if post["fileAttachments"] - raw = add_file_attachments(raw, post["fileAttachments"]) - end + raw = add_file_attachments(raw, post["fileAttachments"]) if post["fileAttachments"] - new_post = create_post({ - id: post["id"], - topic_id: parent_post.topic_id, - user_id: user_id_from_imported_user_id(post["contributorName"]) || -1, - raw: raw, - created_at: Time.zone.parse(post["createdDate"]) - }, post["id"]) + new_post = + create_post( + { + id: post["id"], + topic_id: parent_post.topic_id, + user_id: user_id_from_imported_user_id(post["contributorName"]) || -1, + raw: raw, + created_at: Time.zone.parse(post["createdDate"]), + }, + post["id"], + ) if new_post.is_a?(Post) posts += 1 @@ -288,11 +289,17 @@ class ImportScripts::Ning < ImportScripts::Base end def attachment_regex - @_attachment_regex ||= Regexp.new(%Q[]*)href="(?:#{ATTACHMENT_PREFIXES.join('|')})\/(?:[^"]+)"(?:[^>]*)>]*)src="([^"]+)"(?:[^>]*)><\/a>]) + @_attachment_regex ||= + Regexp.new( + %Q[]*)href="(?:#{ATTACHMENT_PREFIXES.join("|")})\/(?:[^"]+)"(?:[^>]*)>]*)src="([^"]+)"(?:[^>]*)><\/a>], + ) end def youtube_iframe_regex - @_youtube_iframe_regex ||= Regexp.new(%Q[

    ]*)src="\/\/www.youtube.com\/embed\/([^"]+)"(?:[^>]*)><\/iframe>(?:[^<]*)<\/p>]) + @_youtube_iframe_regex ||= + Regexp.new( + %Q[

    ]*)src="\/\/www.youtube.com\/embed\/([^"]+)"(?:[^>]*)><\/iframe>(?:[^<]*)<\/p>], + ) end def process_ning_post_body(arg) @@ -382,15 +389,16 @@ class ImportScripts::Ning < ImportScripts::Base def add_video(arg, embed_code) raw = arg - youtube_regex = Regexp.new(%Q[]*)src="http:\/\/www.youtube.com\/embed\/([^"]+)"(?:[^>]*)><\/iframe>]) + youtube_regex = + Regexp.new( + %Q[]*)src="http:\/\/www.youtube.com\/embed\/([^"]+)"(?:[^>]*)><\/iframe>], + ) raw.gsub!(youtube_regex) do |s| matches = youtube_regex.match(s) video_id = matches[1].split("?").first - if video_id - raw += "\n\nhttps://www.youtube.com/watch?v=#{video_id}\n" - end + raw += "\n\nhttps://www.youtube.com/watch?v=#{video_id}\n" if video_id end raw += "\n" + embed_code + "\n" @@ -398,6 +406,4 @@ class ImportScripts::Ning < ImportScripts::Base end end -if __FILE__ == $0 - ImportScripts::Ning.new.perform -end +ImportScripts::Ning.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/nodebb/mongo.rb b/script/import_scripts/nodebb/mongo.rb index 134704b2b2..696aec4393 100644 --- a/script/import_scripts/nodebb/mongo.rb +++ b/script/import_scripts/nodebb/mongo.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mongo' +require "mongo" module NodeBB class Mongo @@ -43,8 +43,8 @@ module NodeBB user["joindate"] = timestamp_to_date(user["joindate"]) user["lastonline"] = timestamp_to_date(user["lastonline"]) - user['banned'] = user['banned'].to_s - user['uid'] = user['uid'].to_s + user["banned"] = user["banned"].to_s + user["uid"] = user["uid"].to_s user end @@ -56,17 +56,17 @@ module NodeBB category_keys.each do |category_key| category = mongo.find(_key: "category:#{category_key}").first - category['parentCid'] = category['parentCid'].to_s - category['disabled'] = category['disabled'].to_s - category['cid'] = category['cid'].to_s + category["parentCid"] = category["parentCid"].to_s + category["disabled"] = category["disabled"].to_s + category["cid"] = category["cid"].to_s - categories[category['cid']] = category + categories[category["cid"]] = category end end end def topics(offset = 0, page_size = 2000) - topic_keys = mongo.find(_key: 'topics:tid').skip(offset).limit(page_size).pluck(:value) + topic_keys = mongo.find(_key: "topics:tid").skip(offset).limit(page_size).pluck(:value) topic_keys.map { |topic_key| topic(topic_key) } end @@ -86,11 +86,11 @@ module NodeBB end def topic_count - mongo.find(_key: 'topics:tid').count + mongo.find(_key: "topics:tid").count end def posts(offset = 0, page_size = 2000) - post_keys = mongo.find(_key: 'posts:pid').skip(offset).limit(page_size).pluck(:value) + post_keys = mongo.find(_key: "posts:pid").skip(offset).limit(page_size).pluck(:value) post_keys.map { |post_key| post(post_key) } end @@ -111,7 +111,7 @@ module NodeBB end def post_count - mongo.find(_key: 'posts:pid').count + mongo.find(_key: "posts:pid").count end private diff --git a/script/import_scripts/nodebb/nodebb.rb b/script/import_scripts/nodebb/nodebb.rb index b29f5ee1c6..df575f78d3 100644 --- a/script/import_scripts/nodebb/nodebb.rb +++ b/script/import_scripts/nodebb/nodebb.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require_relative '../base' -require_relative './redis' -require_relative './mongo' +require_relative "../base" +require_relative "./redis" +require_relative "./mongo" class ImportScripts::NodeBB < ImportScripts::Base # CHANGE THESE BEFORE RUNNING THE IMPORTER # ATTACHMENT_DIR needs to be absolute, not relative path - ATTACHMENT_DIR = '/Users/orlando/www/orlando/NodeBB/public/uploads' + ATTACHMENT_DIR = "/Users/orlando/www/orlando/NodeBB/public/uploads" BATCH_SIZE = 2000 def initialize @@ -17,17 +17,13 @@ class ImportScripts::NodeBB < ImportScripts::Base # @client = adapter.new('mongodb://127.0.0.1:27017/nodebb') adapter = NodeBB::Redis - @client = adapter.new( - host: "localhost", - port: "6379", - db: 14 - ) + @client = adapter.new(host: "localhost", port: "6379", db: 14) load_merged_posts end def load_merged_posts - puts 'loading merged posts with topics...' + puts "loading merged posts with topics..." # we keep here the posts that were merged # as topics @@ -35,13 +31,16 @@ class ImportScripts::NodeBB < ImportScripts::Base # { post_id: discourse_post_id } @merged_posts_map = {} - PostCustomField.where(name: 'import_merged_post_id').pluck(:post_id, :value).each do |post_id, import_id| - post = Post.find(post_id) - topic_id = post.topic_id - nodebb_post_id = post.custom_fields['import_merged_post_id'] + PostCustomField + .where(name: "import_merged_post_id") + .pluck(:post_id, :value) + .each do |post_id, import_id| + post = Post.find(post_id) + topic_id = post.topic_id + nodebb_post_id = post.custom_fields["import_merged_post_id"] - @merged_posts_map[nodebb_post_id] = topic_id - end + @merged_posts_map[nodebb_post_id] = topic_id + end end def execute @@ -56,19 +55,14 @@ class ImportScripts::NodeBB < ImportScripts::Base end def import_groups - puts '', 'importing groups' + puts "", "importing groups" groups = @client.groups total_count = groups.count progress_count = 0 start_time = Time.now - create_groups(groups) do |group| - { - id: group["name"], - name: group["slug"] - } - end + create_groups(groups) { |group| { id: group["name"], name: group["slug"] } } end def import_categories @@ -107,15 +101,18 @@ class ImportScripts::NodeBB < ImportScripts::Base name: category["name"], position: category["order"], description: category["description"], - parent_category_id: category_id_from_imported_category_id(category["parentCid"]) + parent_category_id: category_id_from_imported_category_id(category["parentCid"]), } end categories.each do |source_category| - cid = category_id_from_imported_category_id(source_category['cid']) - Permalink.create(url: "/category/#{source_category['slug']}", category_id: cid) rescue nil + cid = category_id_from_imported_category_id(source_category["cid"]) + begin + Permalink.create(url: "/category/#{source_category["slug"]}", category_id: cid) + rescue StandardError + nil + end end - end def import_users @@ -158,12 +155,13 @@ class ImportScripts::NodeBB < ImportScripts::Base bio_raw: user["aboutme"], active: true, custom_fields: { - import_pass: user["password"] + import_pass: user["password"], }, - post_create_action: proc do |u| - import_profile_picture(user, u) - import_profile_background(user, u) - end + post_create_action: + proc do |u| + import_profile_picture(user, u) + import_profile_background(user, u) + end, } end end @@ -204,7 +202,7 @@ class ImportScripts::NodeBB < ImportScripts::Base end # write tmp file - file = Tempfile.new(filename, encoding: 'ascii-8bit') + file = Tempfile.new(filename, encoding: "ascii-8bit") file.write string_io.read file.rewind @@ -230,9 +228,21 @@ class ImportScripts::NodeBB < ImportScripts::Base imported_user.user_avatar.update(custom_upload_id: upload.id) imported_user.update(uploaded_avatar_id: upload.id) ensure - string_io.close rescue nil - file.close rescue nil - file.unlind rescue nil + begin + string_io.close + rescue StandardError + nil + end + begin + file.close + rescue StandardError + nil + end + begin + file.unlind + rescue StandardError + nil + end end def import_profile_background(old_user, imported_user) @@ -264,7 +274,7 @@ class ImportScripts::NodeBB < ImportScripts::Base end # write tmp file - file = Tempfile.new(filename, encoding: 'ascii-8bit') + file = Tempfile.new(filename, encoding: "ascii-8bit") file.write string_io.read file.rewind @@ -288,9 +298,21 @@ class ImportScripts::NodeBB < ImportScripts::Base imported_user.user_profile.upload_profile_background(upload) ensure - string_io.close rescue nil - file.close rescue nil - file.unlink rescue nil + begin + string_io.close + rescue StandardError + nil + end + begin + file.close + rescue StandardError + nil + end + begin + file.unlink + rescue StandardError + nil + end end def add_users_to_groups @@ -305,7 +327,7 @@ class ImportScripts::NodeBB < ImportScripts::Base dgroup = find_group_by_import_id(group["name"]) # do thing if we migrated this group already - next if dgroup.custom_fields['import_users_added'] + next if dgroup.custom_fields["import_users_added"] group_member_ids = group["member_ids"].map { |uid| user_id_from_imported_user_id(uid) } group_owner_ids = group["owner_ids"].map { |uid| user_id_from_imported_user_id(uid) } @@ -320,7 +342,7 @@ class ImportScripts::NodeBB < ImportScripts::Base owners = User.find(group_owner_ids) owners.each { |owner| dgroup.add_owner(owner) } - dgroup.custom_fields['import_users_added'] = true + dgroup.custom_fields["import_users_added"] = true dgroup.save progress_count += 1 @@ -357,12 +379,13 @@ class ImportScripts::NodeBB < ImportScripts::Base created_at: topic["timestamp"], views: topic["viewcount"], closed: topic["locked"] == "1", - post_create_action: proc do |p| - # keep track of this to use in import_posts - p.custom_fields["import_merged_post_id"] = topic["mainPid"] - p.save - @merged_posts_map[topic["mainPid"]] = p.id - end + post_create_action: + proc do |p| + # keep track of this to use in import_posts + p.custom_fields["import_merged_post_id"] = topic["mainPid"] + p.save + @merged_posts_map[topic["mainPid"]] = p.id + end, } data[:pinned_at] = data[:created_at] if topic["pinned"] == "1" @@ -372,7 +395,11 @@ class ImportScripts::NodeBB < ImportScripts::Base topics.each do |import_topic| topic = topic_lookup_from_imported_post_id("t#{import_topic["tid"]}") - Permalink.create(url: "/topic/#{import_topic['slug']}", topic_id: topic[:topic_id]) rescue nil + begin + Permalink.create(url: "/topic/#{import_topic["slug"]}", topic_id: topic[:topic_id]) + rescue StandardError + nil + end end end end @@ -411,21 +438,23 @@ class ImportScripts::NodeBB < ImportScripts::Base topic_id: topic[:topic_id], raw: raw, created_at: post["timestamp"], - post_create_action: proc do |p| - post["upvoted_by"].each do |upvoter_id| - user = User.new - user.id = user_id_from_imported_user_id(upvoter_id) || Discourse::SYSTEM_USER_ID - PostActionCreator.like(user, p) - end - end + post_create_action: + proc do |p| + post["upvoted_by"].each do |upvoter_id| + user = User.new + user.id = user_id_from_imported_user_id(upvoter_id) || Discourse::SYSTEM_USER_ID + PostActionCreator.like(user, p) + end + end, } - if post['toPid'] + if post["toPid"] # Look reply to topic - parent_id = topic_lookup_from_imported_post_id("t#{post['toPid']}").try(:[], :post_number) + parent_id = topic_lookup_from_imported_post_id("t#{post["toPid"]}").try(:[], :post_number) # Look reply post if topic is missing - parent_id ||= topic_lookup_from_imported_post_id("p#{post['toPid']}").try(:[], :post_number) + parent_id ||= + topic_lookup_from_imported_post_id("p#{post["toPid"]}").try(:[], :post_number) if parent_id data[:reply_to_post_number] = parent_id @@ -448,12 +477,12 @@ class ImportScripts::NodeBB < ImportScripts::Base Post.find_each do |post| begin - next if post.custom_fields['import_post_processing'] + next if post.custom_fields["import_post_processing"] new_raw = postprocess_post(post) if new_raw != post.raw post.raw = new_raw - post.custom_fields['import_post_processing'] = true + post.custom_fields["import_post_processing"] = true post.save end ensure @@ -463,7 +492,7 @@ class ImportScripts::NodeBB < ImportScripts::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." current = 0 max = Post.count @@ -474,7 +503,7 @@ class ImportScripts::NodeBB < ImportScripts::Base print_status(current, max, start_time) new_raw = post.raw.dup - new_raw.gsub!(/\[(.*)\]\((\/assets\/uploads\/files\/.*)\)/) do + new_raw.gsub!(%r{\[(.*)\]\((/assets/uploads/files/.*)\)}) do image_md = Regexp.last_match[0] text, filepath = $1, $2 filepath = filepath.gsub("/assets/uploads", ATTACHMENT_DIR) @@ -493,7 +522,12 @@ class ImportScripts::NodeBB < ImportScripts::Base end if new_raw != post.raw - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: 'Import attachments from NodeBB') + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + bypass_bump: true, + edit_reason: "Import attachments from NodeBB", + ) end end end @@ -502,28 +536,30 @@ class ImportScripts::NodeBB < ImportScripts::Base raw = post.raw # [link to post](/post/:id) - raw = raw.gsub(/\[(.*)\]\(\/post\/(\d+).*\)/) do - text, post_id = $1, $2 + raw = + raw.gsub(%r{\[(.*)\]\(/post/(\d+).*\)}) do + text, post_id = $1, $2 - if topic_lookup = topic_lookup_from_imported_post_id("p#{post_id}") - url = topic_lookup[:url] - "[#{text}](#{url})" - else - "/404" + if topic_lookup = topic_lookup_from_imported_post_id("p#{post_id}") + url = topic_lookup[:url] + "[#{text}](#{url})" + else + "/404" + end end - end # [link to topic](/topic/:id) - raw = raw.gsub(/\[(.*)\]\(\/topic\/(\d+).*\)/) do - text, topic_id = $1, $2 + raw = + raw.gsub(%r{\[(.*)\]\(/topic/(\d+).*\)}) do + text, topic_id = $1, $2 - if topic_lookup = topic_lookup_from_imported_post_id("t#{topic_id}") - url = topic_lookup[:url] - "[#{text}](#{url})" - else - "/404" + if topic_lookup = topic_lookup_from_imported_post_id("t#{topic_id}") + url = topic_lookup[:url] + "[#{text}](#{url})" + else + "/404" + end end - end raw end diff --git a/script/import_scripts/nodebb/redis.rb b/script/import_scripts/nodebb/redis.rb index f8877c5e15..3d1f08a3f3 100644 --- a/script/import_scripts/nodebb/redis.rb +++ b/script/import_scripts/nodebb/redis.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'redis' +require "redis" module NodeBB class Redis @@ -11,7 +11,7 @@ module NodeBB end def groups - group_keys = redis.zrange('groups:visible:createtime', 0, -1) + group_keys = redis.zrange("groups:visible:createtime", 0, -1) group_keys.map { |group_key| group(group_key) } end @@ -26,7 +26,7 @@ module NodeBB end def users - user_keys = redis.zrange('users:joindate', 0, -1) + user_keys = redis.zrange("users:joindate", 0, -1) user_keys.map { |user_key| user(user_key) } end @@ -41,13 +41,13 @@ module NodeBB end def categories - category_keys = redis.zrange('categories:cid', 0, -1) + category_keys = redis.zrange("categories:cid", 0, -1) {}.tap do |categories| category_keys.each do |category_key| category = redis.hgetall("category:#{category_key}") - categories[category['cid']] = category + categories[category["cid"]] = category end end end @@ -59,7 +59,7 @@ module NodeBB from = offset to = page_size + offset - topic_keys = redis.zrange('topics:tid', from, to) + topic_keys = redis.zrange("topics:tid", from, to) topic_keys.map { |topic_key| topic(topic_key) } end @@ -75,7 +75,7 @@ module NodeBB end def topic_count - redis.zcard('topics:tid') + redis.zcard("topics:tid") end def posts(offset = 0, page_size = 2000) @@ -85,7 +85,7 @@ module NodeBB from = offset to = page_size + offset - post_keys = redis.zrange('posts:pid', from, to) + post_keys = redis.zrange("posts:pid", from, to) post_keys.map { |post_key| post(post_key) } end @@ -99,7 +99,7 @@ module NodeBB end def post_count - redis.zcard('posts:pid') + redis.zcard("posts:pid") end private diff --git a/script/import_scripts/phorum.rb b/script/import_scripts/phorum.rb index dc2639933e..f03db50ea4 100644 --- a/script/import_scripts/phorum.rb +++ b/script/import_scripts/phorum.rb @@ -5,7 +5,6 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Phorum < ImportScripts::Base - PHORUM_DB = "piwik" TABLE_PREFIX = "pw_" BATCH_SIZE = 1000 @@ -13,12 +12,13 @@ class ImportScripts::Phorum < ImportScripts::Base def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - password: "pa$$word", - database: PHORUM_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + password: "pa$$word", + database: PHORUM_DB, + ) end def execute @@ -29,30 +29,34 @@ class ImportScripts::Phorum < ImportScripts::Base end def import_users - puts '', "creating users" + puts "", "creating users" - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}users;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}users;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT user_id id, username, TRIM(email) AS email, username name, date_added created_at, + results = + mysql_query( + "SELECT user_id id, username, TRIM(email) AS email, username name, date_added created_at, date_last_active last_seen_at, admin FROM #{TABLE_PREFIX}users WHERE #{TABLE_PREFIX}users.active = 1 LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 create_users(results, total: total_count, offset: offset) do |user| - next if user['username'].blank? - { id: user['id'], - email: user['email'], - username: user['username'], - name: user['name'], - created_at: Time.zone.at(user['created_at']), - last_seen_at: Time.zone.at(user['last_seen_at']), - admin: user['admin'] == 1 } + next if user["username"].blank? + { + id: user["id"], + email: user["email"], + username: user["username"], + name: user["name"], + created_at: Time.zone.at(user["created_at"]), + last_seen_at: Time.zone.at(user["last_seen_at"]), + admin: user["admin"] == 1, + } end end end @@ -60,19 +64,18 @@ class ImportScripts::Phorum < ImportScripts::Base def import_categories puts "", "importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT forum_id id, name, description, active FROM #{TABLE_PREFIX}forums ORDER BY forum_id ASC - ").to_a + ", + ).to_a create_categories(categories) do |category| - next if category['active'] == 0 - { - id: category['id'], - name: category["name"], - description: category["description"] - } + next if category["active"] == 0 + { id: category["id"], name: category["name"], description: category["description"] } end # uncomment below lines to create permalink @@ -87,7 +90,9 @@ class ImportScripts::Phorum < ImportScripts::Base total_count = mysql_query("SELECT count(*) count from #{TABLE_PREFIX}messages").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT m.message_id id, m.parent_id, m.forum_id category_id, @@ -100,7 +105,8 @@ class ImportScripts::Phorum < ImportScripts::Base ORDER BY m.datestamp LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ").to_a + ", + ).to_a break if results.size < 1 @@ -108,20 +114,20 @@ class ImportScripts::Phorum < ImportScripts::Base skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_raw_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_raw_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) - if m['parent_id'] == 0 - mapped[:category] = category_id_from_imported_category_id(m['category_id'].to_i) - mapped[:title] = CGI.unescapeHTML(m['title']) + if m["parent_id"] == 0 + mapped[:category] = category_id_from_imported_category_id(m["category_id"].to_i) + mapped[:title] = CGI.unescapeHTML(m["title"]) else - parent = topic_lookup_from_imported_post_id(m['parent_id']) + parent = topic_lookup_from_imported_post_id(m["parent_id"]) if parent mapped[:topic_id] = parent[:topic_id] else - puts "Parent post #{m['parent_id']} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + puts "Parent post #{m["parent_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end end @@ -137,25 +143,24 @@ class ImportScripts::Phorum < ImportScripts::Base # end # end end - end def process_raw_post(raw, import_id) s = raw.dup # :) is encoded as :) - s.gsub!(/]+) \/>/, '\1') + s.gsub!(%r{]+) />}, '\1') # Some links look like this: http://www.onegameamonth.com - s.gsub!(/(.+)<\/a>/, '[\2](\1)') + s.gsub!(%r{(.+)}, '[\2](\1)') # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - s.gsub!(/:(?:\w{8})\]/, ']') + s.gsub!(/:(?:\w{8})\]/, "]") # Remove mybb video tags. - s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + s.gsub!(%r{(^\[video=.*?\])|(\[/video\]$)}, "") s = CGI.unescapeHTML(s) @@ -163,50 +168,54 @@ class ImportScripts::Phorum < ImportScripts::Base # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + s.gsub!(%r{\[http(s)?://(www\.)?}, "[") # [QUOTE]...[/QUOTE] - s.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n> #{$1}\n" } + s.gsub!(%r{\[quote\](.+?)\[/quote\]}im) { "\n> #{$1}\n" } # [URL=...]...[/URL] - s.gsub!(/\[url="?(.+?)"?\](.+)\[\/url\]/i) { "[#{$2}](#{$1})" } + s.gsub!(%r{\[url="?(.+?)"?\](.+)\[/url\]}i) { "[#{$2}](#{$1})" } # [IMG]...[/IMG] - s.gsub!(/\[\/?img\]/i, "") + s.gsub!(%r{\[/?img\]}i, "") # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) - s.gsub!(/\[list\](.*?)\[\/list\]/m, '[ul]\1[/ul]') - s.gsub!(/\[list=1\](.*?)\[\/list\]/m, '[ol]\1[/ol]') + s.gsub!(%r{\[list\](.*?)\[/list\]}m, '[ul]\1[/ul]') + s.gsub!(%r{\[list=1\](.*?)\[/list\]}m, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: s.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]') # [CODE]...[/CODE] - s.gsub!(/\[\/?code\]/i, "\n```\n") + s.gsub!(%r{\[/?code\]}i, "\n```\n") # [HIGHLIGHT]...[/HIGHLIGHT] - s.gsub!(/\[\/?highlight\]/i, "\n```\n") + s.gsub!(%r{\[/?highlight\]}i, "\n```\n") # [YOUTUBE][/YOUTUBE] - s.gsub!(/\[youtube\](.+?)\[\/youtube\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[youtube\](.+?)\[/youtube\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } # [youtube=425,350]id[/youtube] - s.gsub!(/\[youtube="?(.+?)"?\](.+)\[\/youtube\]/i) { "\nhttps://www.youtube.com/watch?v=#{$2}\n" } + s.gsub!(%r{\[youtube="?(.+?)"?\](.+)\[/youtube\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$2}\n" + end # [MEDIA=youtube]id[/MEDIA] - s.gsub!(/\[MEDIA=youtube\](.+?)\[\/MEDIA\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[MEDIA=youtube\](.+?)\[/MEDIA\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } # [ame="youtube_link"]title[/ame] - s.gsub!(/\[ame="?(.+?)"?\](.+)\[\/ame\]/i) { "\n#{$1}\n" } + s.gsub!(%r{\[ame="?(.+?)"?\](.+)\[/ame\]}i) { "\n#{$1}\n" } # [VIDEO=youtube;]...[/VIDEO] - s.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[video=youtube;([^\]]+)\].*?\[/video\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$1}\n" + end # [USER=706]@username[/USER] - s.gsub!(/\[user="?(.+?)"?\](.+)\[\/user\]/i) { $2 } + s.gsub!(%r{\[user="?(.+?)"?\](.+)\[/user\]}i) { $2 } # Remove the color tag s.gsub!(/\[color=[#a-z0-9]+\]/i, "") - s.gsub!(/\[\/color\]/i, "") + s.gsub!(%r{\[/color\]}i, "") s.gsub!(/\[hr\]/i, "


    ") @@ -221,7 +230,7 @@ class ImportScripts::Phorum < ImportScripts::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." uploads = mysql_query <<-SQL SELECT message_id, filename, FROM_BASE64(file_data) AS file_data, file_id @@ -234,26 +243,23 @@ class ImportScripts::Phorum < ImportScripts::Base total_count = uploads.count uploads.each do |upload| - # puts "*** processing file #{upload['file_id']}" - post_id = post_id_from_imported_post_id(upload['message_id']) + post_id = post_id_from_imported_post_id(upload["message_id"]) if post_id.nil? - puts "Post #{upload['message_id']} for attachment #{upload['file_id']} not found" + puts "Post #{upload["message_id"]} for attachment #{upload["file_id"]} not found" next end post = Post.find(post_id) - real_filename = upload['filename'] - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + real_filename = upload["filename"] + real_filename.prepend SecureRandom.hex if real_filename[0] == "." - tmpfile = 'attach_' + upload['file_id'].to_s - filename = File.join('/tmp/', tmpfile) - File.open(filename, 'wb') { |f| - f.write(upload['file_data']) - } + tmpfile = "attach_" + upload["file_id"].to_s + filename = File.join("/tmp/", tmpfile) + File.open(filename, "wb") { |f| f.write(upload["file_data"]) } upl_obj = create_upload(post.user.id, filename, real_filename) @@ -265,16 +271,16 @@ class ImportScripts::Phorum < ImportScripts::Base post.raw += "\n\n#{html}\n\n" post.save! if PostUpload.where(post: post, upload: upl_obj).exists? - puts "skipping creating uploaded for previously uploaded file #{upload['file_id']}" + puts "skipping creating uploaded for previously uploaded file #{upload["file_id"]}" else PostUpload.create!(post: post, upload: upl_obj) end # PostUpload.create!(post: post, upload: upl_obj) unless PostUpload.where(post: post, upload: upl_obj).exists? else - puts "Skipping attachment #{upload['file_id']}" + puts "Skipping attachment #{upload["file_id"]}" end else - puts "Failed to upload attachment #{upload['file_id']}" + puts "Failed to upload attachment #{upload["file_id"]}" exit end @@ -282,7 +288,6 @@ class ImportScripts::Phorum < ImportScripts::Base print_status(current_count, total_count) end end - end ImportScripts::Phorum.new.perform diff --git a/script/import_scripts/phpbb3.rb b/script/import_scripts/phpbb3.rb index fb1807f911..2c5ae75e44 100644 --- a/script/import_scripts/phpbb3.rb +++ b/script/import_scripts/phpbb3.rb @@ -4,32 +4,34 @@ # Documentation: https://meta.discourse.org/t/importing-from-phpbb3/30810 if ARGV.length != 1 || !File.exist?(ARGV[0]) - STDERR.puts '', 'Usage of phpBB3 importer:', 'bundle exec ruby phpbb3.rb ' - STDERR.puts '', "Use the settings file from #{File.expand_path('phpbb3/settings.yml', File.dirname(__FILE__))} as an example." - STDERR.puts '', 'Still having problems? Take a look at https://meta.discourse.org/t/importing-from-phpbb3/30810' + STDERR.puts "", "Usage of phpBB3 importer:", "bundle exec ruby phpbb3.rb " + STDERR.puts "", + "Use the settings file from #{File.expand_path("phpbb3/settings.yml", File.dirname(__FILE__))} as an example." + STDERR.puts "", + "Still having problems? Take a look at https://meta.discourse.org/t/importing-from-phpbb3/30810" exit 1 end module ImportScripts module PhpBB3 - require_relative 'phpbb3/support/settings' - require_relative 'phpbb3/database/database' + require_relative "phpbb3/support/settings" + require_relative "phpbb3/database/database" @settings = Settings.load(ARGV[0]) # We need to load the gem files for ruby-bbcode-to-md and the database adapter # (e.g. mysql2) before bundler gets initialized by the base importer. # Otherwise we get an error since those gems are not always in the Gemfile. - require 'ruby-bbcode-to-md' if @settings.use_bbcode_to_md + require "ruby-bbcode-to-md" if @settings.use_bbcode_to_md begin @database = Database.create(@settings.database) rescue UnsupportedVersionError => error - STDERR.puts '', error.message + STDERR.puts "", error.message exit 1 end - require_relative 'phpbb3/importer' + require_relative "phpbb3/importer" Importer.new(@settings, @database).perform end end diff --git a/script/import_scripts/phpbb3/database/database.rb b/script/import_scripts/phpbb3/database/database.rb index 240003edae..c31b6bb620 100644 --- a/script/import_scripts/phpbb3/database/database.rb +++ b/script/import_scripts/phpbb3/database/database.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" module ImportScripts::PhpBB3 class Database @@ -19,11 +19,11 @@ module ImportScripts::PhpBB3 def create_database version = get_phpbb_version - if version.start_with?('3.0') - require_relative 'database_3_0' + if version.start_with?("3.0") + require_relative "database_3_0" Database_3_0.new(@database_client, @database_settings) - elsif version.start_with?('3.1') || version.start_with?('3.2') || version.start_with?('3.3') - require_relative 'database_3_1' + elsif version.start_with?("3.1") || version.start_with?("3.2") || version.start_with?("3.3") + require_relative "database_3_1" Database_3_1.new(@database_client, @database_settings) else raise UnsupportedVersionError, <<~TEXT @@ -42,7 +42,7 @@ module ImportScripts::PhpBB3 username: @database_settings.username, password: @database_settings.password, database: @database_settings.schema, - reconnect: true + reconnect: true, ) end diff --git a/script/import_scripts/phpbb3/database/database_3_0.rb b/script/import_scripts/phpbb3/database/database_3_0.rb index 49f042a6e7..f45b38824a 100644 --- a/script/import_scripts/phpbb3/database/database_3_0.rb +++ b/script/import_scripts/phpbb3/database/database_3_0.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative 'database_base' -require_relative '../support/constants' +require_relative "database_base" +require_relative "../support/constants" module ImportScripts::PhpBB3 class Database_3_0 < DatabaseBase diff --git a/script/import_scripts/phpbb3/database/database_3_1.rb b/script/import_scripts/phpbb3/database/database_3_1.rb index ee666bbbc0..3255e484b1 100644 --- a/script/import_scripts/phpbb3/database/database_3_1.rb +++ b/script/import_scripts/phpbb3/database/database_3_1.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative 'database_3_0' -require_relative '../support/constants' +require_relative "database_3_0" +require_relative "../support/constants" module ImportScripts::PhpBB3 class Database_3_1 < Database_3_0 @@ -32,14 +32,15 @@ module ImportScripts::PhpBB3 private def profile_fields_query(profile_fields) - @profile_fields_query ||= begin - if profile_fields.present? - columns = profile_fields.map { |field| "pf_#{field[:phpbb_field_name]}" } - ", #{columns.join(', ')}" - else - "" + @profile_fields_query ||= + begin + if profile_fields.present? + columns = profile_fields.map { |field| "pf_#{field[:phpbb_field_name]}" } + ", #{columns.join(", ")}" + else + "" + end end - end end end end diff --git a/script/import_scripts/phpbb3/database/database_base.rb b/script/import_scripts/phpbb3/database/database_base.rb index a51bcde3a5..4419d4e78c 100644 --- a/script/import_scripts/phpbb3/database/database_base.rb +++ b/script/import_scripts/phpbb3/database/database_base.rb @@ -39,9 +39,7 @@ module ImportScripts::PhpBB3 def find_last_row(rows) last_index = rows.size - 1 - rows.each_with_index do |row, index| - return row if index == last_index - end + rows.each_with_index { |row, index| return row if index == last_index } nil end diff --git a/script/import_scripts/phpbb3/importer.rb b/script/import_scripts/phpbb3/importer.rb index b8b84e29e6..a4f64f82c4 100644 --- a/script/import_scripts/phpbb3/importer.rb +++ b/script/import_scripts/phpbb3/importer.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative '../base' -require_relative 'support/settings' -require_relative 'database/database' -require_relative 'importers/importer_factory' +require_relative "../base" +require_relative "support/settings" +require_relative "database/database" +require_relative "importers/importer_factory" module ImportScripts::PhpBB3 class Importer < ImportScripts::Base @@ -25,7 +25,7 @@ module ImportScripts::PhpBB3 protected def execute - puts '', "importing from phpBB #{@php_config[:phpbb_version]}" + puts "", "importing from phpBB #{@php_config[:phpbb_version]}" SiteSetting.tagging_enabled = true if @settings.tag_mappings.present? @@ -55,8 +55,14 @@ module ImportScripts::PhpBB3 settings[:max_attachment_size_kb] = [max_file_size_kb, SiteSetting.max_attachment_size_kb].max # temporarily disable validation since we want to import all existing images and attachments - SiteSetting.type_supervisor.load_setting(:max_image_size_kb, max: settings[:max_image_size_kb]) - SiteSetting.type_supervisor.load_setting(:max_attachment_size_kb, max: settings[:max_attachment_size_kb]) + SiteSetting.type_supervisor.load_setting( + :max_image_size_kb, + max: settings[:max_image_size_kb], + ) + SiteSetting.type_supervisor.load_setting( + :max_attachment_size_kb, + max: settings[:max_attachment_size_kb], + ) settings end @@ -66,7 +72,7 @@ module ImportScripts::PhpBB3 end def import_users - puts '', 'creating users' + puts "", "creating users" total_count = @database.count_users importer = @importers.user_importer last_user_id = 0 @@ -88,10 +94,10 @@ module ImportScripts::PhpBB3 end def import_anonymous_users - puts '', 'creating anonymous users' + puts "", "creating anonymous users" total_count = @database.count_anonymous_users importer = @importers.user_importer - last_username = '' + last_username = "" batches do |offset| rows, last_username = @database.fetch_anonymous_users(last_username) @@ -109,26 +115,34 @@ module ImportScripts::PhpBB3 end def import_groups - puts '', 'creating groups' + puts "", "creating groups" rows = @database.fetch_groups create_groups(rows) do |row| begin next if row[:group_type] == 3 - group_name = if @settings.site_name.present? - "#{@settings.site_name}_#{row[:group_name]}" - else - row[:group_name] - end[0..19].gsub(/[^a-zA-Z0-9\-_. ]/, '_') + group_name = + if @settings.site_name.present? + "#{@settings.site_name}_#{row[:group_name]}" + else + row[:group_name] + end[ + 0..19 + ].gsub(/[^a-zA-Z0-9\-_. ]/, "_") - bio_raw = @importers.text_processor.process_raw_text(row[:group_desc]) rescue row[:group_desc] + bio_raw = + begin + @importers.text_processor.process_raw_text(row[:group_desc]) + rescue StandardError + row[:group_desc] + end { id: @settings.prefix(row[:group_id]), name: group_name, full_name: row[:group_name], - bio_raw: bio_raw + bio_raw: bio_raw, } rescue => e log_error("Failed to map group with ID #{row[:group_id]}", e) @@ -137,7 +151,7 @@ module ImportScripts::PhpBB3 end def import_user_groups - puts '', 'creating user groups' + puts "", "creating user groups" rows = @database.fetch_group_users rows.each do |row| @@ -147,7 +161,11 @@ module ImportScripts::PhpBB3 user_id = @lookup.user_id_from_imported_user_id(@settings.prefix(row[:user_id])) begin - GroupUser.find_or_create_by(user_id: user_id, group_id: group_id, owner: row[:group_leader]) + GroupUser.find_or_create_by( + user_id: user_id, + group_id: group_id, + owner: row[:group_leader], + ) rescue => e log_error("Failed to add user #{row[:user_id]} to group #{row[:group_id]}", e) end @@ -155,7 +173,7 @@ module ImportScripts::PhpBB3 end def import_new_categories - puts '', 'creating new categories' + puts "", "creating new categories" create_categories(@settings.new_categories) do |row| next if row == "SKIP" @@ -163,13 +181,14 @@ module ImportScripts::PhpBB3 { id: @settings.prefix(row[:forum_id]), name: row[:name], - parent_category_id: @lookup.category_id_from_imported_category_id(@settings.prefix(row[:parent_id])) + parent_category_id: + @lookup.category_id_from_imported_category_id(@settings.prefix(row[:parent_id])), } end end def import_categories - puts '', 'creating categories' + puts "", "creating categories" rows = @database.fetch_categories importer = @importers.category_importer @@ -181,7 +200,7 @@ module ImportScripts::PhpBB3 end def import_posts - puts '', 'creating topics and posts' + puts "", "creating topics and posts" total_count = @database.count_posts importer = @importers.post_importer last_post_id = 0 @@ -202,7 +221,7 @@ module ImportScripts::PhpBB3 end def import_private_messages - puts '', 'creating private messages' + puts "", "creating private messages" total_count = @database.count_messages importer = @importers.message_importer last_msg_id = 0 @@ -223,7 +242,7 @@ module ImportScripts::PhpBB3 end def import_bookmarks - puts '', 'creating bookmarks' + puts "", "creating bookmarks" total_count = @database.count_bookmarks importer = @importers.bookmark_importer last_user_id = last_topic_id = 0 @@ -243,7 +262,7 @@ module ImportScripts::PhpBB3 end def import_likes - puts '', 'importing likes' + puts "", "importing likes" total_count = @database.count_likes last_post_id = last_user_id = 0 @@ -255,7 +274,7 @@ module ImportScripts::PhpBB3 { post_id: @settings.prefix(row[:post_id]), user_id: @settings.prefix(row[:user_id]), - created_at: Time.zone.at(row[:thanks_time]) + created_at: Time.zone.at(row[:thanks_time]), } end end diff --git a/script/import_scripts/phpbb3/importers/avatar_importer.rb b/script/import_scripts/phpbb3/importers/avatar_importer.rb index bb72572c0b..4e6e3b13bb 100644 --- a/script/import_scripts/phpbb3/importers/avatar_importer.rb +++ b/script/import_scripts/phpbb3/importers/avatar_importer.rb @@ -49,12 +49,12 @@ module ImportScripts::PhpBB3 def get_avatar_path(avatar_type, filename) case avatar_type - when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED then - filename.gsub!(/_[0-9]+\./, '.') # we need 1337.jpg, not 1337_2983745.jpg - get_uploaded_path(filename) - when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY then + when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED + filename.gsub!(/_[0-9]+\./, ".") # we need 1337.jpg, not 1337_2983745.jpg + get_uploaded_path(filename) + when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY get_gallery_path(filename) - when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE then + when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE download_avatar(filename) else puts "Invalid avatar type #{avatar_type}. Skipping..." @@ -67,12 +67,13 @@ module ImportScripts::PhpBB3 max_image_size_kb = SiteSetting.max_image_size_kb.kilobytes begin - avatar_file = FileHelper.download( - url, - max_file_size: max_image_size_kb, - tmp_file_name: 'discourse-avatar', - follow_redirect: true - ) + avatar_file = + FileHelper.download( + url, + max_file_size: max_image_size_kb, + tmp_file_name: "discourse-avatar", + follow_redirect: true, + ) rescue StandardError => err warn "Error downloading avatar: #{err.message}. Skipping..." return nil @@ -100,11 +101,11 @@ module ImportScripts::PhpBB3 def is_allowed_avatar_type?(avatar_type) case avatar_type - when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED then + when Constants::AVATAR_TYPE_UPLOADED, Constants::AVATAR_TYPE_STRING_UPLOADED @settings.import_uploaded_avatars - when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE then + when Constants::AVATAR_TYPE_REMOTE, Constants::AVATAR_TYPE_STRING_REMOTE @settings.import_remote_avatars - when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY then + when Constants::AVATAR_TYPE_GALLERY, Constants::AVATAR_TYPE_STRING_GALLERY @settings.import_gallery_avatars else false diff --git a/script/import_scripts/phpbb3/importers/bookmark_importer.rb b/script/import_scripts/phpbb3/importers/bookmark_importer.rb index 784e6c7476..6dd2a793d8 100644 --- a/script/import_scripts/phpbb3/importers/bookmark_importer.rb +++ b/script/import_scripts/phpbb3/importers/bookmark_importer.rb @@ -9,7 +9,7 @@ module ImportScripts::PhpBB3 def map_bookmark(row) { user_id: @settings.prefix(row[:user_id]), - post_id: @settings.prefix(row[:topic_first_post_id]) + post_id: @settings.prefix(row[:topic_first_post_id]), } end end diff --git a/script/import_scripts/phpbb3/importers/category_importer.rb b/script/import_scripts/phpbb3/importers/category_importer.rb index 6e5725b97e..e0b95bc79b 100644 --- a/script/import_scripts/phpbb3/importers/category_importer.rb +++ b/script/import_scripts/phpbb3/importers/category_importer.rb @@ -23,11 +23,13 @@ module ImportScripts::PhpBB3 { id: @settings.prefix(row[:forum_id]), name: CGI.unescapeHTML(row[:forum_name]), - parent_category_id: @lookup.category_id_from_imported_category_id(@settings.prefix(row[:parent_id])), - post_create_action: proc do |category| - update_category_description(category, row) - @permalink_importer.create_for_category(category, row[:forum_id]) # skip @settings.prefix because ID is used in permalink generation - end + parent_category_id: + @lookup.category_id_from_imported_category_id(@settings.prefix(row[:parent_id])), + post_create_action: + proc do |category| + update_category_description(category, row) + @permalink_importer.create_for_category(category, row[:forum_id]) # skip @settings.prefix because ID is used in permalink generation + end, } end @@ -51,7 +53,16 @@ module ImportScripts::PhpBB3 end if row[:forum_desc].present? - changes = { raw: (@text_processor.process_raw_text(row[:forum_desc]) rescue row[:forum_desc]) } + changes = { + raw: + ( + begin + @text_processor.process_raw_text(row[:forum_desc]) + rescue StandardError + row[:forum_desc] + end + ), + } opts = { revised_at: post.created_at, bypass_bump: true } post.revise(Discourse.system_user, changes, opts) end diff --git a/script/import_scripts/phpbb3/importers/importer_factory.rb b/script/import_scripts/phpbb3/importers/importer_factory.rb index b02cb92ff0..d7a3fe9c9f 100644 --- a/script/import_scripts/phpbb3/importers/importer_factory.rb +++ b/script/import_scripts/phpbb3/importers/importer_factory.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require_relative 'attachment_importer' -require_relative 'avatar_importer' -require_relative 'bookmark_importer' -require_relative 'category_importer' -require_relative 'message_importer' -require_relative 'poll_importer' -require_relative 'post_importer' -require_relative 'permalink_importer' -require_relative 'user_importer' -require_relative '../support/smiley_processor' -require_relative '../support/text_processor' +require_relative "attachment_importer" +require_relative "avatar_importer" +require_relative "bookmark_importer" +require_relative "category_importer" +require_relative "message_importer" +require_relative "poll_importer" +require_relative "post_importer" +require_relative "permalink_importer" +require_relative "user_importer" +require_relative "../support/smiley_processor" +require_relative "../support/text_processor" module ImportScripts::PhpBB3 class ImporterFactory @@ -36,7 +36,14 @@ module ImportScripts::PhpBB3 end def post_importer - PostImporter.new(@lookup, text_processor, attachment_importer, poll_importer, permalink_importer, @settings) + PostImporter.new( + @lookup, + text_processor, + attachment_importer, + poll_importer, + permalink_importer, + @settings, + ) end def message_importer @@ -64,7 +71,8 @@ module ImportScripts::PhpBB3 end def text_processor - @text_processor ||= TextProcessor.new(@lookup, @database, smiley_processor, @settings, @phpbb_config) + @text_processor ||= + TextProcessor.new(@lookup, @database, smiley_processor, @settings, @phpbb_config) end def smiley_processor diff --git a/script/import_scripts/phpbb3/importers/message_importer.rb b/script/import_scripts/phpbb3/importers/message_importer.rb index 65c5874bda..c71795b328 100644 --- a/script/import_scripts/phpbb3/importers/message_importer.rb +++ b/script/import_scripts/phpbb3/importers/message_importer.rb @@ -20,14 +20,16 @@ module ImportScripts::PhpBB3 end def map_message(row) - user_id = @lookup.user_id_from_imported_user_id(@settings.prefix(row[:author_id])) || Discourse.system_user.id + user_id = + @lookup.user_id_from_imported_user_id(@settings.prefix(row[:author_id])) || + Discourse.system_user.id attachments = import_attachments(row, user_id) mapped = { id: get_import_id(row[:msg_id]), user_id: user_id, created_at: Time.zone.at(row[:message_time]), - raw: @text_processor.process_private_msg(row[:message_text], attachments) + raw: @text_processor.process_private_msg(row[:message_text], attachments), } root_user_ids = sorted_user_ids(row[:root_author_id], row[:root_to_address]) @@ -43,7 +45,7 @@ module ImportScripts::PhpBB3 protected - RE_PREFIX = 're: ' + RE_PREFIX = "re: " def import_attachments(row, user_id) if @settings.import_attachments && row[:attachment_count] > 0 @@ -55,7 +57,7 @@ module ImportScripts::PhpBB3 mapped[:title] = get_topic_title(row) mapped[:archetype] = Archetype.private_message mapped[:target_usernames] = get_recipient_usernames(row) - mapped[:custom_fields] = { import_user_ids: current_user_ids.join(',') } + mapped[:custom_fields] = { import_user_ids: current_user_ids.join(",") } if mapped[:target_usernames].empty? puts "Private message without recipients. Skipping #{row[:msg_id]}: #{row[:message_subject][0..40]}" @@ -75,9 +77,9 @@ module ImportScripts::PhpBB3 # to_address looks like this: "u_91:u_1234:g_200" # If there is a "u_" prefix, the prefix is discarded and the rest is a user_id - user_ids = to_address.split(':') + user_ids = to_address.split(":") user_ids.uniq! - user_ids.map! { |u| u[2..-1].to_i if u[0..1] == 'u_' }.compact + user_ids.map! { |u| u[2..-1].to_i if u[0..1] == "u_" }.compact end def get_recipient_group_ids(to_address) @@ -85,16 +87,19 @@ module ImportScripts::PhpBB3 # to_address looks like this: "u_91:u_1234:g_200" # If there is a "g_" prefix, the prefix is discarded and the rest is a group_id - group_ids = to_address.split(':') + group_ids = to_address.split(":") group_ids.uniq! - group_ids.map! { |g| g[2..-1].to_i if g[0..1] == 'g_' }.compact + group_ids.map! { |g| g[2..-1].to_i if g[0..1] == "g_" }.compact end def get_recipient_usernames(row) import_user_ids = get_recipient_user_ids(row[:to_address]) - usernames = import_user_ids.map do |import_user_id| - @lookup.find_user_by_import_id(@settings.prefix(import_user_id)).try(:username) - end.compact + usernames = + import_user_ids + .map do |import_user_id| + @lookup.find_user_by_import_id(@settings.prefix(import_user_id)).try(:username) + end + .compact import_group_ids = get_recipient_group_ids(row[:to_address]) import_group_ids.each do |import_group_id| @@ -142,13 +147,19 @@ module ImportScripts::PhpBB3 topic_titles = [topic_title] topic_titles << topic_title[RE_PREFIX.length..-1] if topic_title.start_with?(RE_PREFIX) - Post.select(:topic_id) + Post + .select(:topic_id) .joins(:topic) .joins(:_custom_fields) - .where(["LOWER(topics.title) IN (:titles) AND post_custom_fields.name = 'import_user_ids' AND post_custom_fields.value = :user_ids", - { titles: topic_titles, user_ids: current_user_ids.join(',') }]) - .order('topics.created_at DESC') - .first.try(:topic_id) + .where( + [ + "LOWER(topics.title) IN (:titles) AND post_custom_fields.name = 'import_user_ids' AND post_custom_fields.value = :user_ids", + { titles: topic_titles, user_ids: current_user_ids.join(",") }, + ], + ) + .order("topics.created_at DESC") + .first + .try(:topic_id) end end end diff --git a/script/import_scripts/phpbb3/importers/permalink_importer.rb b/script/import_scripts/phpbb3/importers/permalink_importer.rb index 051604ba87..5dcd9ffe60 100644 --- a/script/import_scripts/phpbb3/importers/permalink_importer.rb +++ b/script/import_scripts/phpbb3/importers/permalink_importer.rb @@ -13,13 +13,15 @@ module ImportScripts::PhpBB3 def change_site_settings normalizations = SiteSetting.permalink_normalizations - normalizations = normalizations.blank? ? [] : normalizations.split('|') + normalizations = normalizations.blank? ? [] : normalizations.split("|") - add_normalization(normalizations, CATEGORY_LINK_NORMALIZATION) if @settings.create_category_links + if @settings.create_category_links + add_normalization(normalizations, CATEGORY_LINK_NORMALIZATION) + end add_normalization(normalizations, POST_LINK_NORMALIZATION) if @settings.create_post_links add_normalization(normalizations, TOPIC_LINK_NORMALIZATION) if @settings.create_topic_links - SiteSetting.permalink_normalizations = normalizations.join('|') + SiteSetting.permalink_normalizations = normalizations.join("|") end def create_for_category(category, import_id) @@ -50,8 +52,8 @@ module ImportScripts::PhpBB3 def add_normalization(normalizations, normalization) if @settings.normalization_prefix.present? - prefix = @settings.normalization_prefix[%r|^/?(.*?)/?$|, 1] - normalization = "/#{prefix.gsub('/', '\/')}\\#{normalization}" + prefix = @settings.normalization_prefix[%r{^/?(.*?)/?$}, 1] + normalization = "/#{prefix.gsub("/", '\/')}\\#{normalization}" end normalizations << normalization unless normalizations.include?(normalization) diff --git a/script/import_scripts/phpbb3/importers/poll_importer.rb b/script/import_scripts/phpbb3/importers/poll_importer.rb index 785fbb60b2..df4696201c 100644 --- a/script/import_scripts/phpbb3/importers/poll_importer.rb +++ b/script/import_scripts/phpbb3/importers/poll_importer.rb @@ -49,7 +49,12 @@ module ImportScripts::PhpBB3 end def get_option_text(row) - text = @text_processor.process_raw_text(row[:poll_option_text]) rescue row[:poll_option_text] + text = + begin + @text_processor.process_raw_text(row[:poll_option_text]) + rescue StandardError + row[:poll_option_text] + end text.squish! text.gsub!(/^(\d+)\./, '\1\.') text @@ -57,7 +62,12 @@ module ImportScripts::PhpBB3 # @param poll_data [ImportScripts::PhpBB3::PollData] def get_poll_text(poll_data) - title = @text_processor.process_raw_text(poll_data.title) rescue poll_data.title + title = + begin + @text_processor.process_raw_text(poll_data.title) + rescue StandardError + poll_data.title + end text = +"#{title}\n\n" arguments = ["results=always"] @@ -69,11 +79,9 @@ module ImportScripts::PhpBB3 arguments << "type=regular" end - text << "[poll #{arguments.join(' ')}]" + text << "[poll #{arguments.join(" ")}]" - poll_data.options.each do |option| - text << "\n* #{option[:text]}" - end + poll_data.options.each { |option| text << "\n* #{option[:text]}" } text << "\n[/poll]" end @@ -104,9 +112,7 @@ module ImportScripts::PhpBB3 poll.poll_options.each_with_index do |option, index| imported_option = poll_data.options[index] - imported_option[:ids].each do |imported_id| - option_ids[imported_id] = option.id - end + imported_option[:ids].each { |imported_id| option_ids[imported_id] = option.id } end option_ids diff --git a/script/import_scripts/phpbb3/importers/post_importer.rb b/script/import_scripts/phpbb3/importers/post_importer.rb index 8f41e9ed66..4f66560e34 100644 --- a/script/import_scripts/phpbb3/importers/post_importer.rb +++ b/script/import_scripts/phpbb3/importers/post_importer.rb @@ -8,7 +8,14 @@ module ImportScripts::PhpBB3 # @param poll_importer [ImportScripts::PhpBB3::PollImporter] # @param permalink_importer [ImportScripts::PhpBB3::PermalinkImporter] # @param settings [ImportScripts::PhpBB3::Settings] - def initialize(lookup, text_processor, attachment_importer, poll_importer, permalink_importer, settings) + def initialize( + lookup, + text_processor, + attachment_importer, + poll_importer, + permalink_importer, + settings + ) @lookup = lookup @text_processor = text_processor @attachment_importer = attachment_importer @@ -24,7 +31,8 @@ module ImportScripts::PhpBB3 def map_post(row) return if @settings.category_mappings.dig(row[:forum_id].to_s, :skip) - imported_user_id = @settings.prefix(row[:post_username].blank? ? row[:poster_id] : row[:post_username]) + imported_user_id = + @settings.prefix(row[:post_username].blank? ? row[:poster_id] : row[:post_username]) user_id = @lookup.user_id_from_imported_user_id(imported_user_id) || -1 is_first_post = row[:post_id] == row[:topic_first_post_id] @@ -35,7 +43,7 @@ module ImportScripts::PhpBB3 user_id: user_id, created_at: Time.zone.at(row[:post_time]), raw: @text_processor.process_post(row[:post_text], attachments), - import_topic_id: @settings.prefix(row[:topic_id]) + import_topic_id: @settings.prefix(row[:topic_id]), } if is_first_post @@ -58,7 +66,9 @@ module ImportScripts::PhpBB3 mapped[:category] = if category_mapping = @settings.category_mappings[row[:forum_id].to_s] category_mapping[:discourse_category_id] || - @lookup.category_id_from_imported_category_id(@settings.prefix(category_mapping[:target_category_id])) + @lookup.category_id_from_imported_category_id( + @settings.prefix(category_mapping[:target_category_id]), + ) else @lookup.category_id_from_imported_category_id(@settings.prefix(row[:forum_id])) end @@ -81,7 +91,8 @@ module ImportScripts::PhpBB3 end def map_other_post(row, mapped) - parent = @lookup.topic_lookup_from_imported_post_id(@settings.prefix(row[:topic_first_post_id])) + parent = + @lookup.topic_lookup_from_imported_post_id(@settings.prefix(row[:topic_first_post_id])) if parent.blank? puts "Parent post #{@settings.prefix(row[:topic_first_post_id])} doesn't exist. Skipping #{@settings.prefix(row[:post_id])}: #{row[:topic_title][0..40]}" diff --git a/script/import_scripts/phpbb3/importers/user_importer.rb b/script/import_scripts/phpbb3/importers/user_importer.rb index 3fa61d6e17..6f32223232 100644 --- a/script/import_scripts/phpbb3/importers/user_importer.rb +++ b/script/import_scripts/phpbb3/importers/user_importer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative '../support/constants' +require_relative "../support/constants" module ImportScripts::PhpBB3 class UserImporter @@ -29,8 +29,22 @@ module ImportScripts::PhpBB3 password: @settings.import_passwords ? row[:user_password] : nil, name: @settings.username_as_name ? row[:username] : row[:name].presence, created_at: Time.zone.at(row[:user_regdate]), - last_seen_at: row[:user_lastvisit] == 0 ? Time.zone.at(row[:user_regdate]) : Time.zone.at(row[:user_lastvisit]), - registration_ip_address: (IPAddr.new(row[:user_ip]) rescue nil), + last_seen_at: + ( + if row[:user_lastvisit] == 0 + Time.zone.at(row[:user_regdate]) + else + Time.zone.at(row[:user_lastvisit]) + end + ), + registration_ip_address: + ( + begin + IPAddr.new(row[:user_ip]) + rescue StandardError + nil + end + ), active: is_active_user, trust_level: trust_level, manual_locked_trust_level: manual_locked_trust_level, @@ -43,10 +57,11 @@ module ImportScripts::PhpBB3 location: row[:user_from], date_of_birth: parse_birthdate(row), custom_fields: custom_fields(row), - post_create_action: proc do |user| - suspend_user(user, row) - @avatar_importer.import_avatar(user, row) if row[:user_avatar_type].present? - end + post_create_action: + proc do |user| + suspend_user(user, row) + @avatar_importer.import_avatar(user, row) if row[:user_avatar_type].present? + end, } end @@ -61,18 +76,19 @@ module ImportScripts::PhpBB3 id: @settings.prefix(username), email: "anonymous_#{SecureRandom.hex}@no-email.invalid", username: username, - name: @settings.username_as_name ? username : '', + name: @settings.username_as_name ? username : "", created_at: Time.zone.at(row[:first_post_time]), active: true, trust_level: TrustLevel[0], approved: true, approved_by_id: Discourse.system_user.id, approved_at: Time.now, - post_create_action: proc do |user| - row[:user_inactive_reason] = Constants::INACTIVE_MANUAL - row[:ban_reason] = 'Anonymous user from phpBB3' # TODO i18n - suspend_user(user, row, true) - end + post_create_action: + proc do |user| + row[:user_inactive_reason] = Constants::INACTIVE_MANUAL + row[:ban_reason] = "Anonymous user from phpBB3" # TODO i18n + suspend_user(user, row, true) + end, } end @@ -80,25 +96,32 @@ module ImportScripts::PhpBB3 def parse_birthdate(row) return nil if row[:user_birthday].blank? - birthdate = Date.strptime(row[:user_birthday].delete(' '), '%d-%m-%Y') rescue nil + birthdate = + begin + Date.strptime(row[:user_birthday].delete(" "), "%d-%m-%Y") + rescue StandardError + nil + end birthdate && birthdate.year > 0 ? birthdate : nil end def user_fields - @user_fields ||= begin - Hash[UserField.all.map { |field| [field.name, field] }] - end + @user_fields ||= + begin + Hash[UserField.all.map { |field| [field.name, field] }] + end end def field_mappings - @field_mappings ||= begin - @settings.custom_fields.map do |field| - { - phpbb_field_name: "pf_#{field[:phpbb_field_name]}".to_sym, - discourse_user_field: user_fields[field[:discourse_field_name]] - } + @field_mappings ||= + begin + @settings.custom_fields.map do |field| + { + phpbb_field_name: "pf_#{field[:phpbb_field_name]}".to_sym, + discourse_user_field: user_fields[field[:discourse_field_name]], + } + end end - end end def custom_fields(row) @@ -114,7 +137,8 @@ module ImportScripts::PhpBB3 when "confirm" value = value == 1 ? true : nil when "dropdown" - value = user_field.user_field_options.find { |option| option.value == value } ? value : nil + value = + user_field.user_field_options.find { |option| option.value == value } ? value : nil end custom_fields["user_field_#{user_field.id}"] = value if value.present? @@ -128,7 +152,8 @@ module ImportScripts::PhpBB3 if row[:user_inactive_reason] == Constants::INACTIVE_MANUAL user.suspended_at = Time.now user.suspended_till = 200.years.from_now - ban_reason = row[:ban_reason].blank? ? 'Account deactivated by administrator' : row[:ban_reason] # TODO i18n + ban_reason = + row[:ban_reason].blank? ? "Account deactivated by administrator" : row[:ban_reason] # TODO i18n elsif row[:ban_start].present? user.suspended_at = Time.zone.at(row[:ban_start]) user.suspended_till = row[:ban_end] > 0 ? Time.zone.at(row[:ban_end]) : 200.years.from_now @@ -148,7 +173,9 @@ module ImportScripts::PhpBB3 if user.save StaffActionLogger.new(Discourse.system_user).log_user_suspend(user, ban_reason) else - Rails.logger.error("Failed to suspend user #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}") + Rails.logger.error( + "Failed to suspend user #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}", + ) end end end diff --git a/script/import_scripts/phpbb3/support/bbcode/markdown_node.rb b/script/import_scripts/phpbb3/support/bbcode/markdown_node.rb index 5a42a1bf40..c5e5048a9f 100644 --- a/script/import_scripts/phpbb3/support/bbcode/markdown_node.rb +++ b/script/import_scripts/phpbb3/support/bbcode/markdown_node.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true -module ImportScripts; end -module ImportScripts::PhpBB3; end +module ImportScripts +end +module ImportScripts::PhpBB3 +end module ImportScripts::PhpBB3::BBCode LINEBREAK_AUTO = :auto diff --git a/script/import_scripts/phpbb3/support/bbcode/xml_to_markdown.rb b/script/import_scripts/phpbb3/support/bbcode/xml_to_markdown.rb index 7041c5923f..004601d247 100644 --- a/script/import_scripts/phpbb3/support/bbcode/xml_to_markdown.rb +++ b/script/import_scripts/phpbb3/support/bbcode/xml_to_markdown.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'nokogiri' -require_relative 'markdown_node' +require "nokogiri" +require_relative "markdown_node" module ImportScripts::PhpBB3::BBCode class XmlToMarkdown @@ -14,7 +14,7 @@ module ImportScripts::PhpBB3::BBCode @allow_inline_code = opts.fetch(:allow_inline_code, false) @traditional_linebreaks = opts.fetch(:traditional_linebreaks, false) - @doc = Nokogiri::XML(xml) + @doc = Nokogiri.XML(xml) @list_stack = [] end @@ -28,9 +28,9 @@ module ImportScripts::PhpBB3::BBCode private - IGNORED_ELEMENTS = ["s", "e", "i"] - ELEMENTS_WITHOUT_LEADING_WHITESPACES = ["LIST", "LI"] - ELEMENTS_WITH_HARD_LINEBREAKS = ["B", "I", "U"] + IGNORED_ELEMENTS = %w[s e i] + ELEMENTS_WITHOUT_LEADING_WHITESPACES = %w[LIST LI] + ELEMENTS_WITH_HARD_LINEBREAKS = %w[B I U] EXPLICIT_LINEBREAK_THRESHOLD = 2 def preprocess_xml @@ -65,9 +65,7 @@ module ImportScripts::PhpBB3::BBCode xml_node.children.each { |xml_child| visit(xml_child, md_node || md_parent) } after_hook = "after_#{xml_node.name}" - if respond_to?(after_hook, include_all: true) - send(after_hook, xml_node, md_node) - end + send(after_hook, xml_node, md_node) if respond_to?(after_hook, include_all: true) end def create_node(xml_node, md_parent) @@ -84,19 +82,15 @@ module ImportScripts::PhpBB3::BBCode end def visit_B(xml_node, md_node) - if xml_node.parent&.name != 'B' - md_node.enclosed_with = "**" - end + md_node.enclosed_with = "**" if xml_node.parent&.name != "B" end def visit_I(xml_node, md_node) - if xml_node.parent&.name != 'I' - md_node.enclosed_with = "_" - end + md_node.enclosed_with = "_" if xml_node.parent&.name != "I" end def visit_U(xml_node, md_node) - if xml_node.parent&.name != 'U' + if xml_node.parent&.name != "U" md_node.prefix = "[u]" md_node.postfix = "[/u]" end @@ -122,10 +116,7 @@ module ImportScripts::PhpBB3::BBCode md_node.prefix_linebreaks = md_node.postfix_linebreaks = @list_stack.size == 0 ? 2 : 1 md_node.prefix_linebreak_type = LINEBREAK_HTML if @list_stack.size == 0 - @list_stack << { - unordered: xml_node.attribute('type').nil?, - item_count: 0 - } + @list_stack << { unordered: xml_node.attribute("type").nil?, item_count: 0 } end def after_LIST(xml_node, md_node) @@ -138,21 +129,21 @@ module ImportScripts::PhpBB3::BBCode list[:item_count] += 1 - indentation = ' ' * 2 * depth - symbol = list[:unordered] ? '*' : "#{list[:item_count]}." + indentation = " " * 2 * depth + symbol = list[:unordered] ? "*" : "#{list[:item_count]}." md_node.prefix = "#{indentation}#{symbol} " md_node.postfix_linebreaks = 1 end def visit_IMG(xml_node, md_node) - md_node.text = +"![](#{xml_node.attribute('src')})" + md_node.text = +"![](#{xml_node.attribute("src")})" md_node.prefix_linebreaks = md_node.postfix_linebreaks = 2 md_node.skip_children end def visit_URL(xml_node, md_node) - original_url = xml_node.attribute('url').to_s + original_url = xml_node.attribute("url").to_s url = CGI.unescapeHTML(original_url) url = @url_replacement.call(url) if @url_replacement @@ -173,7 +164,8 @@ module ImportScripts::PhpBB3::BBCode def visit_br(xml_node, md_node) md_node.postfix_linebreaks += 1 - if md_node.postfix_linebreaks > 1 && ELEMENTS_WITH_HARD_LINEBREAKS.include?(xml_node.parent&.name) + if md_node.postfix_linebreaks > 1 && + ELEMENTS_WITH_HARD_LINEBREAKS.include?(xml_node.parent&.name) md_node.postfix_linebreak_type = LINEBREAK_HARD end end @@ -194,7 +186,8 @@ module ImportScripts::PhpBB3::BBCode def visit_QUOTE(xml_node, md_node) if post = quoted_post(xml_node) - md_node.prefix = %Q{[quote="#{post[:username]}, post:#{post[:post_number]}, topic:#{post[:topic_id]}"]\n} + md_node.prefix = + %Q{[quote="#{post[:username]}, post:#{post[:post_number]}, topic:#{post[:topic_id]}"]\n} md_node.postfix = "\n[/quote]" elsif username = quoted_username(xml_node) md_node.prefix = %Q{[quote="#{username}"]\n} @@ -242,11 +235,11 @@ module ImportScripts::PhpBB3::BBCode return if size.nil? if size.between?(1, 99) - md_node.prefix = '' - md_node.postfix = '' + md_node.prefix = "" + md_node.postfix = "" elsif size.between?(101, 200) - md_node.prefix = '' - md_node.postfix = '' + md_node.prefix = "" + md_node.postfix = "" end end @@ -267,7 +260,8 @@ module ImportScripts::PhpBB3::BBCode parent_prefix = prefix_from_parent(md_parent) - if parent_prefix && md_node.xml_node_name != "br" && (md_parent.prefix_children || !markdown.empty?) + if parent_prefix && md_node.xml_node_name != "br" && + (md_parent.prefix_children || !markdown.empty?) prefix = "#{parent_prefix}#{prefix}" end @@ -275,11 +269,21 @@ module ImportScripts::PhpBB3::BBCode text, prefix, postfix = hoist_whitespaces!(markdown, text, prefix, postfix) end - add_linebreaks!(markdown, md_node.prefix_linebreaks, md_node.prefix_linebreak_type, parent_prefix) + add_linebreaks!( + markdown, + md_node.prefix_linebreaks, + md_node.prefix_linebreak_type, + parent_prefix, + ) markdown << prefix markdown << text markdown << postfix - add_linebreaks!(markdown, md_node.postfix_linebreaks, md_node.postfix_linebreak_type, parent_prefix) + add_linebreaks!( + markdown, + md_node.postfix_linebreaks, + md_node.postfix_linebreak_type, + parent_prefix, + ) end markdown @@ -296,9 +300,7 @@ module ImportScripts::PhpBB3::BBCode end unless postfix.empty? - if ends_with_whitespace?(text) - postfix = "#{postfix}#{text[-1]}" - end + postfix = "#{postfix}#{text[-1]}" if ends_with_whitespace?(text) text = text.rstrip end @@ -319,16 +321,24 @@ module ImportScripts::PhpBB3::BBCode if linebreak_type == LINEBREAK_HTML max_linebreak_count = [existing_linebreak_count, required_linebreak_count - 1].max + 1 - required_linebreak_count = max_linebreak_count if max_linebreak_count > EXPLICIT_LINEBREAK_THRESHOLD + required_linebreak_count = max_linebreak_count if max_linebreak_count > + EXPLICIT_LINEBREAK_THRESHOLD end return if existing_linebreak_count >= required_linebreak_count rstrip!(markdown) - alternative_linebreak_start_index = required_linebreak_count > EXPLICIT_LINEBREAK_THRESHOLD ? 1 : 2 + alternative_linebreak_start_index = + required_linebreak_count > EXPLICIT_LINEBREAK_THRESHOLD ? 1 : 2 required_linebreak_count.times do |index| - linebreak = linebreak(linebreak_type, index, alternative_linebreak_start_index, required_linebreak_count) + linebreak = + linebreak( + linebreak_type, + index, + alternative_linebreak_start_index, + required_linebreak_count, + ) markdown << (linebreak == "\n" ? prefix.rstrip : prefix) if prefix && index > 0 markdown << linebreak @@ -336,18 +346,25 @@ module ImportScripts::PhpBB3::BBCode end def rstrip!(markdown) - markdown.gsub!(/\s*(?:\\?\n|
    \n)*\z/, '') + markdown.gsub!(/\s*(?:\\?\n|
    \n)*\z/, "") end - def linebreak(linebreak_type, linebreak_index, alternative_linebreak_start_index, required_linebreak_count) + def linebreak( + linebreak_type, + linebreak_index, + alternative_linebreak_start_index, + required_linebreak_count + ) use_alternative_linebreak = linebreak_index >= alternative_linebreak_start_index is_last_linebreak = linebreak_index + 1 == required_linebreak_count - return "
    \n" if linebreak_type == LINEBREAK_HTML && - use_alternative_linebreak && is_last_linebreak + if linebreak_type == LINEBREAK_HTML && use_alternative_linebreak && is_last_linebreak + return "
    \n" + end - return "\\\n" if linebreak_type == LINEBREAK_HARD || - @traditional_linebreaks || use_alternative_linebreak + if linebreak_type == LINEBREAK_HARD || @traditional_linebreaks || use_alternative_linebreak + return "\\\n" + end "\n" end diff --git a/script/import_scripts/phpbb3/support/constants.rb b/script/import_scripts/phpbb3/support/constants.rb index af8d62dc43..c832cfee8f 100644 --- a/script/import_scripts/phpbb3/support/constants.rb +++ b/script/import_scripts/phpbb3/support/constants.rb @@ -8,8 +8,8 @@ module ImportScripts::PhpBB3 INACTIVE_MANUAL = 3 # Account deactivated by administrator INACTIVE_REMIND = 4 # Forced user account reactivation - GROUP_ADMINISTRATORS = 'ADMINISTRATORS' - GROUP_MODERATORS = 'GLOBAL_MODERATORS' + GROUP_ADMINISTRATORS = "ADMINISTRATORS" + GROUP_MODERATORS = "GLOBAL_MODERATORS" # https://wiki.phpbb.com/Table.phpbb_users USER_TYPE_NORMAL = 0 @@ -21,9 +21,9 @@ module ImportScripts::PhpBB3 AVATAR_TYPE_REMOTE = 2 AVATAR_TYPE_GALLERY = 3 - AVATAR_TYPE_STRING_UPLOADED = 'avatar.driver.upload' - AVATAR_TYPE_STRING_REMOTE = 'avatar.driver.remote' - AVATAR_TYPE_STRING_GALLERY = 'avatar.driver.local' + AVATAR_TYPE_STRING_UPLOADED = "avatar.driver.upload" + AVATAR_TYPE_STRING_REMOTE = "avatar.driver.remote" + AVATAR_TYPE_STRING_GALLERY = "avatar.driver.local" FORUM_TYPE_CATEGORY = 0 FORUM_TYPE_POST = 1 diff --git a/script/import_scripts/phpbb3/support/settings.rb b/script/import_scripts/phpbb3/support/settings.rb index b259821ab3..e308e322cf 100644 --- a/script/import_scripts/phpbb3/support/settings.rb +++ b/script/import_scripts/phpbb3/support/settings.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require 'csv' -require 'yaml' -require_relative '../../base' +require "csv" +require "yaml" +require_relative "../../base" module ImportScripts::PhpBB3 class Settings def self.load(filename) - yaml = YAML::load_file(filename) + yaml = YAML.load_file(filename) Settings.new(yaml.deep_stringify_keys.with_indifferent_access) end @@ -44,40 +44,41 @@ module ImportScripts::PhpBB3 attr_reader :database def initialize(yaml) - import_settings = yaml['import'] + import_settings = yaml["import"] - @site_name = import_settings['site_name'] + @site_name = import_settings["site_name"] - @new_categories = import_settings['new_categories'] - @category_mappings = import_settings.fetch('category_mappings', []).to_h { |m| [m[:source_category_id].to_s, m] } - @tag_mappings = import_settings['tag_mappings'] - @rank_mapping = import_settings['rank_mapping'] + @new_categories = import_settings["new_categories"] + @category_mappings = + import_settings.fetch("category_mappings", []).to_h { |m| [m[:source_category_id].to_s, m] } + @tag_mappings = import_settings["tag_mappings"] + @rank_mapping = import_settings["rank_mapping"] - @import_anonymous_users = import_settings['anonymous_users'] - @import_attachments = import_settings['attachments'] - @import_private_messages = import_settings['private_messages'] - @import_polls = import_settings['polls'] - @import_bookmarks = import_settings['bookmarks'] - @import_passwords = import_settings['passwords'] - @import_likes = import_settings['likes'] + @import_anonymous_users = import_settings["anonymous_users"] + @import_attachments = import_settings["attachments"] + @import_private_messages = import_settings["private_messages"] + @import_polls = import_settings["polls"] + @import_bookmarks = import_settings["bookmarks"] + @import_passwords = import_settings["passwords"] + @import_likes = import_settings["likes"] - avatar_settings = import_settings['avatars'] - @import_uploaded_avatars = avatar_settings['uploaded'] - @import_remote_avatars = avatar_settings['remote'] - @import_gallery_avatars = avatar_settings['gallery'] + avatar_settings = import_settings["avatars"] + @import_uploaded_avatars = avatar_settings["uploaded"] + @import_remote_avatars = avatar_settings["remote"] + @import_gallery_avatars = avatar_settings["gallery"] - @use_bbcode_to_md = import_settings['use_bbcode_to_md'] + @use_bbcode_to_md = import_settings["use_bbcode_to_md"] - @original_site_prefix = import_settings['site_prefix']['original'] - @new_site_prefix = import_settings['site_prefix']['new'] - @base_dir = import_settings['phpbb_base_dir'] - @permalinks = PermalinkSettings.new(import_settings['permalinks']) + @original_site_prefix = import_settings["site_prefix"]["original"] + @new_site_prefix = import_settings["site_prefix"]["new"] + @base_dir = import_settings["phpbb_base_dir"] + @permalinks = PermalinkSettings.new(import_settings["permalinks"]) - @username_as_name = import_settings['username_as_name'] - @emojis = import_settings.fetch('emojis', []) - @custom_fields = import_settings.fetch('custom_fields', []) + @username_as_name = import_settings["username_as_name"] + @emojis = import_settings.fetch("emojis", []) + @custom_fields = import_settings.fetch("custom_fields", []) - @database = DatabaseSettings.new(yaml['database']) + @database = DatabaseSettings.new(yaml["database"]) end def prefix(val) @@ -87,7 +88,7 @@ module ImportScripts::PhpBB3 def trust_level_for_posts(rank, trust_level: 0) if @rank_mapping.present? @rank_mapping.each do |key, value| - trust_level = [trust_level, key.gsub('trust_level_', '').to_i].max if rank >= value + trust_level = [trust_level, key.gsub("trust_level_", "").to_i].max if rank >= value end end @@ -106,14 +107,14 @@ module ImportScripts::PhpBB3 attr_reader :batch_size def initialize(yaml) - @type = yaml['type'] - @host = yaml['host'] - @port = yaml['port'] - @username = yaml['username'] - @password = yaml['password'] - @schema = yaml['schema'] - @table_prefix = yaml['table_prefix'] - @batch_size = yaml['batch_size'] + @type = yaml["type"] + @host = yaml["host"] + @port = yaml["port"] + @username = yaml["username"] + @password = yaml["password"] + @schema = yaml["schema"] + @table_prefix = yaml["table_prefix"] + @batch_size = yaml["batch_size"] end end @@ -124,10 +125,10 @@ module ImportScripts::PhpBB3 attr_reader :normalization_prefix def initialize(yaml) - @create_category_links = yaml['categories'] - @create_topic_links = yaml['topics'] - @create_post_links = yaml['posts'] - @normalization_prefix = yaml['prefix'] + @create_category_links = yaml["categories"] + @create_topic_links = yaml["topics"] + @create_post_links = yaml["posts"] + @normalization_prefix = yaml["prefix"] end end end diff --git a/script/import_scripts/phpbb3/support/smiley_processor.rb b/script/import_scripts/phpbb3/support/smiley_processor.rb index 618f99ddd2..4a861fc4c1 100644 --- a/script/import_scripts/phpbb3/support/smiley_processor.rb +++ b/script/import_scripts/phpbb3/support/smiley_processor.rb @@ -18,15 +18,16 @@ module ImportScripts::PhpBB3 def replace_smilies(text) # :) is encoded as :) - text.gsub!(/.*?/) do - emoji($1) - end + text.gsub!( + /.*?/, + ) { emoji($1) } end def emoji(smiley_code) @smiley_map.fetch(smiley_code) do smiley = @database.get_smiley(smiley_code) - emoji = upload_smiley(smiley_code, smiley[:smiley_url], smiley_code, smiley[:emotion]) if smiley + emoji = + upload_smiley(smiley_code, smiley[:smiley_url], smiley_code, smiley[:emotion]) if smiley emoji || smiley_as_text(smiley_code) end end @@ -35,37 +36,34 @@ module ImportScripts::PhpBB3 def add_default_smilies { - [':D', ':-D', ':grin:'] => ':smiley:', - [':)', ':-)', ':smile:'] => ':slight_smile:', - [';)', ';-)', ':wink:'] => ':wink:', - [':(', ':-(', ':sad:'] => ':frowning:', - [':o', ':-o', ':eek:'] => ':astonished:', - [':shock:'] => ':open_mouth:', - [':?', ':-?', ':???:'] => ':confused:', - ['8)', '8-)', ':cool:'] => ':sunglasses:', - [':lol:'] => ':laughing:', - [':x', ':-x', ':mad:'] => ':angry:', - [':P', ':-P', ':razz:'] => ':stuck_out_tongue:', - [':oops:'] => ':blush:', - [':cry:'] => ':cry:', - [':evil:'] => ':imp:', - [':twisted:'] => ':smiling_imp:', - [':roll:'] => ':unamused:', - [':!:'] => ':exclamation:', - [':?:'] => ':question:', - [':idea:'] => ':bulb:', - [':arrow:'] => ':arrow_right:', - [':|', ':-|'] => ':neutral_face:', - [':geek:'] => ':nerd:' - }.each do |smilies, emoji| - smilies.each { |smiley| @smiley_map[smiley] = emoji } - end + %w[:D :-D :grin:] => ":smiley:", + %w[:) :-) :smile:] => ":slight_smile:", + %w[;) ;-) :wink:] => ":wink:", + %w[:( :-( :sad:] => ":frowning:", + %w[:o :-o :eek:] => ":astonished:", + [":shock:"] => ":open_mouth:", + %w[:? :-? :???:] => ":confused:", + %w[8) 8-) :cool:] => ":sunglasses:", + [":lol:"] => ":laughing:", + %w[:x :-x :mad:] => ":angry:", + %w[:P :-P :razz:] => ":stuck_out_tongue:", + [":oops:"] => ":blush:", + [":cry:"] => ":cry:", + [":evil:"] => ":imp:", + [":twisted:"] => ":smiling_imp:", + [":roll:"] => ":unamused:", + [":!:"] => ":exclamation:", + [":?:"] => ":question:", + [":idea:"] => ":bulb:", + [":arrow:"] => ":arrow_right:", + %w[:| :-|] => ":neutral_face:", + [":geek:"] => ":nerd:", + }.each { |smilies, emoji| smilies.each { |smiley| @smiley_map[smiley] = emoji } } end def add_configured_smilies(emojis) emojis.each do |emoji, smilies| - Array.wrap(smilies) - .each { |smiley| @smiley_map[smiley] = ":#{emoji}:" } + Array.wrap(smilies).each { |smiley| @smiley_map[smiley] = ":#{emoji}:" } end end diff --git a/script/import_scripts/phpbb3/support/text_processor.rb b/script/import_scripts/phpbb3/support/text_processor.rb index 62547b4f15..fb788bf537 100644 --- a/script/import_scripts/phpbb3/support/text_processor.rb +++ b/script/import_scripts/phpbb3/support/text_processor.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'bbcode/xml_to_markdown' +require_relative "bbcode/xml_to_markdown" module ImportScripts::PhpBB3 class TextProcessor @@ -14,7 +14,9 @@ module ImportScripts::PhpBB3 @database = database @smiley_processor = smiley_processor @he = HTMLEntities.new - @use_xml_to_markdown = phpbb_config[:phpbb_version].start_with?('3.2') || phpbb_config[:phpbb_version].start_with?('3.3') + @use_xml_to_markdown = + phpbb_config[:phpbb_version].start_with?("3.2") || + phpbb_config[:phpbb_version].start_with?("3.3") @settings = settings @new_site_prefix = settings.new_site_prefix @@ -25,24 +27,27 @@ module ImportScripts::PhpBB3 if @use_xml_to_markdown unreferenced_attachments = attachments&.dup - converter = BBCode::XmlToMarkdown.new( - raw, - username_from_user_id: lambda { |user_id| @lookup.find_username_by_import_id(user_id) }, - smilie_to_emoji: lambda { |smilie| @smiley_processor.emoji(smilie).dup }, - quoted_post_from_post_id: lambda { |post_id| @lookup.topic_lookup_from_imported_post_id(post_id) }, - upload_md_from_file: (lambda do |filename, index| - unreferenced_attachments[index] = nil - attachments.fetch(index, filename).dup - end if attachments), - url_replacement: nil, - allow_inline_code: false - ) + converter = + BBCode::XmlToMarkdown.new( + raw, + username_from_user_id: lambda { |user_id| @lookup.find_username_by_import_id(user_id) }, + smilie_to_emoji: lambda { |smilie| @smiley_processor.emoji(smilie).dup }, + quoted_post_from_post_id: + lambda { |post_id| @lookup.topic_lookup_from_imported_post_id(post_id) }, + upload_md_from_file: + ( + lambda do |filename, index| + unreferenced_attachments[index] = nil + attachments.fetch(index, filename).dup + end if attachments + ), + url_replacement: nil, + allow_inline_code: false, + ) text = converter.convert - text.gsub!(@short_internal_link_regexp) do |link| - replace_internal_link(link, $1, $2) - end + text.gsub!(@short_internal_link_regexp) { |link| replace_internal_link(link, $1, $2) } add_unreferenced_attachments(text, unreferenced_attachments) else @@ -50,9 +55,7 @@ module ImportScripts::PhpBB3 text = CGI.unescapeHTML(text) clean_bbcodes(text) - if @settings.use_bbcode_to_md - text = bbcode_to_md(text) - end + text = bbcode_to_md(text) if @settings.use_bbcode_to_md process_smilies(text) process_links(text) process_lists(text) @@ -65,11 +68,19 @@ module ImportScripts::PhpBB3 end def process_post(raw, attachments) - process_raw_text(raw, attachments) rescue raw + begin + process_raw_text(raw, attachments) + rescue StandardError + raw + end end def process_private_msg(raw, attachments) - process_raw_text(raw, attachments) rescue raw + begin + process_raw_text(raw, attachments) + rescue StandardError + raw + end end protected @@ -78,10 +89,10 @@ module ImportScripts::PhpBB3 # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - text.gsub!(/:(?:\w{5,8})\]/, ']') + text.gsub!(/:(?:\w{5,8})\]/, "]") # remove color tags - text.gsub!(/\[\/?color(=#?[a-z0-9]*)?\]/i, "") + text.gsub!(%r{\[/?color(=#?[a-z0-9]*)?\]}i, "") end def bbcode_to_md(text) @@ -101,23 +112,19 @@ module ImportScripts::PhpBB3 # Internal forum links can have this forms: # for topics: viewtopic.php?f=26&t=3412 # for posts: viewtopic.php?p=1732#p1732 - text.gsub!(@long_internal_link_regexp) do |link| - replace_internal_link(link, $1, $2) - end + text.gsub!(@long_internal_link_regexp) { |link| replace_internal_link(link, $1, $2) } # Some links look like this: http://www.onegameamonth.com - text.gsub!(/(.+)<\/a>/i, '[\2](\1)') + text.gsub!(%r{(.+)}i, '[\2](\1)') # Replace internal forum links that aren't in the format - text.gsub!(@short_internal_link_regexp) do |link| - replace_internal_link(link, $1, $2) - end + text.gsub!(@short_internal_link_regexp) { |link| replace_internal_link(link, $1, $2) } # phpBB shortens link text like this, which breaks our markdown processing: # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - text.gsub!(/\[http(s)?:\/\/(www\.)?/i, '[') + text.gsub!(%r{\[http(s)?://(www\.)?}i, "[") end def replace_internal_link(link, import_topic_id, import_post_id) @@ -144,19 +151,20 @@ module ImportScripts::PhpBB3 # convert list tags to ul and list=1 tags to ol # list=a is not supported, so handle it like list=1 # list=9 and list=x have the same result as list=1 and list=a - text.gsub!(/\[list\](.*?)\[\/list:u\]/mi) do - $1.gsub(/\[\*\](.*?)\[\/\*:m\]\n*/mi) { "* #{$1}\n" } + text.gsub!(%r{\[list\](.*?)\[/list:u\]}mi) do + $1.gsub(%r{\[\*\](.*?)\[/\*:m\]\n*}mi) { "* #{$1}\n" } end - text.gsub!(/\[list=.*?\](.*?)\[\/list:o\]/mi) do - $1.gsub(/\[\*\](.*?)\[\/\*:m\]\n*/mi) { "1. #{$1}\n" } + text.gsub!(%r{\[list=.*?\](.*?)\[/list:o\]}mi) do + $1.gsub(%r{\[\*\](.*?)\[/\*:m\]\n*}mi) { "1. #{$1}\n" } end end # This replaces existing [attachment] BBCodes with the corresponding HTML tags for Discourse. # All attachments that haven't been referenced in the text are appended to the end of the text. def process_attachments(text, attachments) - attachment_regexp = /\[attachment=([\d])+\]([^<]+)\[\/attachment\]?/i + attachment_regexp = + %r{\[attachment=([\d])+\]([^<]+)\[/attachment\]?}i unreferenced_attachments = attachments.dup text.gsub!(attachment_regexp) do @@ -178,29 +186,34 @@ module ImportScripts::PhpBB3 end def create_internal_link_regexps(original_site_prefix) - host = original_site_prefix.gsub('.', '\.') - link_regex = "http(?:s)?://#{host}/viewtopic\\.php\\?(?:\\S*)(?:t=(\\d+)|p=(\\d+)(?:#p\\d+)?)(?:[^\\s\\)\\]]*)" + host = original_site_prefix.gsub(".", '\.') + link_regex = + "http(?:s)?://#{host}/viewtopic\\.php\\?(?:\\S*)(?:t=(\\d+)|p=(\\d+)(?:#p\\d+)?)(?:[^\\s\\)\\]]*)" - @long_internal_link_regexp = Regexp.new(%Q||, Regexp::IGNORECASE) + @long_internal_link_regexp = + Regexp.new( + %Q||, + Regexp::IGNORECASE, + ) @short_internal_link_regexp = Regexp.new(link_regex, Regexp::IGNORECASE) end def process_code(text) - text.gsub!(//, "\n") + text.gsub!(%r{}, "\n") text end def fix_markdown(text) - text.gsub!(/(\n*\[\/?quote.*?\]\n*)/mi) { |q| "\n#{q.strip}\n" } + text.gsub!(%r{(\n*\[/?quote.*?\]\n*)}mi) { |q| "\n#{q.strip}\n" } text.gsub!(/^!\[[^\]]*\]\([^\]]*\)$/i) { |img| "\n#{img.strip}\n" } # space out images single on line text end def process_videos(text) # [YOUTUBE][/YOUTUBE] - text.gsub(/\[youtube\](.+?)\[\/youtube\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + text.gsub(%r{\[youtube\](.+?)\[/youtube\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } text end end diff --git a/script/import_scripts/punbb.rb b/script/import_scripts/punbb.rb index 64cce9bbcf..b73e4f7128 100644 --- a/script/import_scripts/punbb.rb +++ b/script/import_scripts/punbb.rb @@ -7,19 +7,19 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/punbb.rb class ImportScripts::PunBB < ImportScripts::Base - PUNBB_DB = "punbb_db" BATCH_SIZE = 1000 def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - password: "pa$$word", - database: PUNBB_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + password: "pa$$word", + database: PUNBB_DB, + ) end def execute @@ -30,36 +30,41 @@ class ImportScripts::PunBB < ImportScripts::Base end def import_users - puts '', "creating users" + puts "", "creating users" - total_count = mysql_query("SELECT count(*) count FROM users;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM users;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT id, username, realname name, url website, email email, registered created_at, + results = + mysql_query( + "SELECT id, username, realname name, url website, email email, registered created_at, registration_ip registration_ip_address, last_visit last_visit_time, last_email_sent last_emailed_at, last_email_sent last_emailed_at, location, group_id FROM users LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 next if all_records_exist? :users, results.map { |u| u["id"].to_i } create_users(results, total: total_count, offset: offset) do |user| - { id: user['id'], - email: user['email'], - username: user['username'], - name: user['name'], - created_at: Time.zone.at(user['created_at']), - website: user['website'], - registration_ip_address: user['registration_ip_address'], - last_seen_at: Time.zone.at(user['last_visit_time']), - last_emailed_at: user['last_emailed_at'] == nil ? 0 : Time.zone.at(user['last_emailed_at']), - location: user['location'], - moderator: user['group_id'] == 4, - admin: user['group_id'] == 1 } + { + id: user["id"], + email: user["email"], + username: user["username"], + name: user["name"], + created_at: Time.zone.at(user["created_at"]), + website: user["website"], + registration_ip_address: user["registration_ip_address"], + last_seen_at: Time.zone.at(user["last_visit_time"]), + last_emailed_at: + user["last_emailed_at"] == nil ? 0 : Time.zone.at(user["last_emailed_at"]), + location: user["location"], + moderator: user["group_id"] == 4, + admin: user["group_id"] == 1, + } end end end @@ -67,33 +72,34 @@ class ImportScripts::PunBB < ImportScripts::Base def import_categories puts "", "importing top level categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT id, cat_name name, disp_position position FROM categories ORDER BY id ASC - ").to_a + ", + ).to_a - create_categories(categories) do |category| - { - id: category["id"], - name: category["name"] - } - end + create_categories(categories) { |category| { id: category["id"], name: category["name"] } } puts "", "importing children categories..." - children_categories = mysql_query(" + children_categories = + mysql_query( + " SELECT id, forum_name name, forum_desc description, disp_position position, cat_id parent_category_id FROM forums ORDER BY id - ").to_a + ", + ).to_a create_categories(children_categories) do |category| { - id: "child##{category['id']}", + id: "child##{category["id"]}", name: category["name"], description: category["description"], - parent_category_id: category_id_from_imported_category_id(category["parent_category_id"]) + parent_category_id: category_id_from_imported_category_id(category["parent_category_id"]), } end end @@ -104,7 +110,9 @@ class ImportScripts::PunBB < ImportScripts::Base total_count = mysql_query("SELECT count(*) count from posts").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.id id, t.id topic_id, t.forum_id category_id, @@ -119,29 +127,30 @@ class ImportScripts::PunBB < ImportScripts::Base ORDER BY p.posted LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ").to_a + ", + ).to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_punbb_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_punbb_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) - if m['id'] == m['first_post_id'] - mapped[:category] = category_id_from_imported_category_id("child##{m['category_id']}") - mapped[:title] = CGI.unescapeHTML(m['title']) + if m["id"] == m["first_post_id"] + mapped[:category] = category_id_from_imported_category_id("child##{m["category_id"]}") + mapped[:title] = CGI.unescapeHTML(m["title"]) else - parent = topic_lookup_from_imported_post_id(m['first_post_id']) + parent = topic_lookup_from_imported_post_id(m["first_post_id"]) if parent mapped[:topic_id] = parent[:topic_id] else - puts "Parent post #{m['first_post_id']} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + puts "Parent post #{m["first_post_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end end @@ -152,16 +161,16 @@ class ImportScripts::PunBB < ImportScripts::Base end def suspend_users - puts '', "updating banned users" + puts "", "updating banned users" banned = 0 failed = 0 - total = mysql_query("SELECT count(*) count FROM bans").first['count'] + total = mysql_query("SELECT count(*) count FROM bans").first["count"] system_user = Discourse.system_user mysql_query("SELECT username, email FROM bans").each do |b| - user = User.find_by_email(b['email']) + user = User.find_by_email(b["email"]) if user user.suspended_at = Time.now user.suspended_till = 200.years.from_now @@ -174,7 +183,7 @@ class ImportScripts::PunBB < ImportScripts::Base failed += 1 end else - puts "Not found: #{b['email']}" + puts "Not found: #{b["email"]}" failed += 1 end @@ -189,15 +198,15 @@ class ImportScripts::PunBB < ImportScripts::Base s.gsub!(/(?:.*)/, '\1') # Some links look like this: http://www.onegameamonth.com - s.gsub!(/(.+)<\/a>/, '[\2](\1)') + s.gsub!(%r{(.+)}, '[\2](\1)') # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - s.gsub!(/:(?:\w{8})\]/, ']') + s.gsub!(/:(?:\w{8})\]/, "]") # Remove mybb video tags. - s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + s.gsub!(%r{(^\[video=.*?\])|(\[/video\]$)}, "") s = CGI.unescapeHTML(s) @@ -205,7 +214,7 @@ class ImportScripts::PunBB < ImportScripts::Base # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + s.gsub!(%r{\[http(s)?://(www\.)?}, "[") s end diff --git a/script/import_scripts/quandora/export.rb b/script/import_scripts/quandora/export.rb index e1f87b7ec3..fbbe146ca6 100644 --- a/script/import_scripts/quandora/export.rb +++ b/script/import_scripts/quandora/export.rb @@ -1,25 +1,25 @@ # frozen_string_literal: true -require 'yaml' -require_relative 'quandora_api' +require "yaml" +require_relative "quandora_api" def load_config(file) - config = YAML::load_file(File.join(__dir__, file)) - @domain = config['domain'] - @username = config['username'] - @password = config['password'] + config = YAML.load_file(File.join(__dir__, file)) + @domain = config["domain"] + @username = config["username"] + @password = config["password"] end def export api = QuandoraApi.new @domain, @username, @password bases = api.list_bases bases.each do |base| - question_list = api.list_questions base['objectId'], 1000 + question_list = api.list_questions base["objectId"], 1000 question_list.each do |q| - question_id = q['uid'] + question_id = q["uid"] question = api.get_question question_id - File.open("output/#{question_id}.json", 'w') do |f| - puts question['title'] + File.open("output/#{question_id}.json", "w") do |f| + puts question["title"] f.write question.to_json f.close end diff --git a/script/import_scripts/quandora/import.rb b/script/import_scripts/quandora/import.rb index 7df8be302c..a3dc5dfe29 100644 --- a/script/import_scripts/quandora/import.rb +++ b/script/import_scripts/quandora/import.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require_relative './quandora_question.rb' +require_relative "./quandora_question.rb" require File.expand_path(File.dirname(__FILE__) + "/../base.rb") class ImportScripts::Quandora < ImportScripts::Base - JSON_FILES_DIR = "output" def initialize @@ -12,8 +11,8 @@ class ImportScripts::Quandora < ImportScripts::Base @system_user = Discourse.system_user @questions = [] Dir.foreach(JSON_FILES_DIR) do |filename| - next if filename == ('.') || filename == ('..') - question = File.read JSON_FILES_DIR + '/' + filename + next if filename == (".") || filename == ("..") + question = File.read JSON_FILES_DIR + "/" + filename @questions << question end end @@ -33,9 +32,7 @@ class ImportScripts::Quandora < ImportScripts::Base q = QuandoraQuestion.new question import_users q.users created_topic = import_topic q.topic - if created_topic - import_posts q.replies, created_topic.topic_id - end + import_posts q.replies, created_topic.topic_id if created_topic topics += 1 print_status topics, total end @@ -43,9 +40,7 @@ class ImportScripts::Quandora < ImportScripts::Base end def import_users(users) - users.each do |user| - create_user user, user[:id] - end + users.each { |user| create_user user, user[:id] } end def import_topic(topic) @@ -54,7 +49,7 @@ class ImportScripts::Quandora < ImportScripts::Base post = Post.find(post_id) # already imported this topic else topic[:user_id] = user_id_from_imported_user_id(topic[:author_id]) || -1 - topic[:category] = 'quandora-import' + topic[:category] = "quandora-import" post = create_post(topic, topic[:id]) @@ -68,9 +63,7 @@ class ImportScripts::Quandora < ImportScripts::Base end def import_posts(posts, topic_id) - posts.each do |post| - import_post post, topic_id - end + posts.each { |post| import_post post, topic_id } end def import_post(post, topic_id) @@ -91,6 +84,4 @@ class ImportScripts::Quandora < ImportScripts::Base end end -if __FILE__ == $0 - ImportScripts::Quandora.new.perform -end +ImportScripts::Quandora.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/quandora/quandora_api.rb b/script/import_scripts/quandora/quandora_api.rb index 747473bb79..9a74308772 100644 --- a/script/import_scripts/quandora/quandora_api.rb +++ b/script/import_scripts/quandora/quandora_api.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'base64' -require 'json' +require "base64" +require "json" class QuandoraApi - attr_accessor :domain, :username, :password def initialize(domain, username, password) @@ -38,18 +37,18 @@ class QuandoraApi def list_bases response = request list_bases_url - response['data'] + response["data"] end def list_questions(kb_id, limit = nil) url = list_questions_url(kb_id, limit) response = request url - response['data']['result'] + response["data"]["result"] end def get_question(question_id) url = "#{base_url @domain}/q/#{question_id}" response = request url - response['data'] + response["data"] end end diff --git a/script/import_scripts/quandora/quandora_question.rb b/script/import_scripts/quandora/quandora_question.rb index abbaaeeda6..767dad16fc 100644 --- a/script/import_scripts/quandora/quandora_question.rb +++ b/script/import_scripts/quandora/quandora_question.rb @@ -1,28 +1,27 @@ # frozen_string_literal: true -require 'json' -require 'cgi' -require 'time' +require "json" +require "cgi" +require "time" class QuandoraQuestion - def initialize(question_json) @question = JSON.parse question_json end def topic topic = {} - topic[:id] = @question['uid'] - topic[:author_id] = @question['author']['uid'] - topic[:title] = unescape @question['title'] - topic[:raw] = unescape @question['content'] - topic[:created_at] = Time.parse @question['created'] + topic[:id] = @question["uid"] + topic[:author_id] = @question["author"]["uid"] + topic[:title] = unescape @question["title"] + topic[:raw] = unescape @question["content"] + topic[:created_at] = Time.parse @question["created"] topic end def users users = {} - user = user_from_author @question['author'] + user = user_from_author @question["author"] users[user[:id]] = user replies.each do |reply| user = user_from_author reply[:author] @@ -32,12 +31,12 @@ class QuandoraQuestion end def user_from_author(author) - email = author['email'] - email = "#{author['uid']}@noemail.com" unless email + email = author["email"] + email = "#{author["uid"]}@noemail.com" unless email user = {} - user[:id] = author['uid'] - user[:name] = "#{author['firstName']} #{author['lastName']}" + user[:id] = author["uid"] + user[:name] = "#{author["firstName"]} #{author["lastName"]}" user[:email] = email user[:staged] = true user @@ -45,26 +44,20 @@ class QuandoraQuestion def replies posts = [] - answers = @question['answersList'] - comments = @question['comments'] - comments.each_with_index do |comment, i| - posts << post_from_comment(comment, i, @question) - end + answers = @question["answersList"] + comments = @question["comments"] + comments.each_with_index { |comment, i| posts << post_from_comment(comment, i, @question) } answers.each do |answer| posts << post_from_answer(answer) - comments = answer['comments'] - comments.each_with_index do |comment, i| - posts << post_from_comment(comment, i, answer) - end + comments = answer["comments"] + comments.each_with_index { |comment, i| posts << post_from_comment(comment, i, answer) } end order_replies posts end def order_replies(posts) posts = posts.sort_by { |p| p[:created_at] } - posts.each_with_index do |p, i| - p[:post_number] = i + 2 - end + posts.each_with_index { |p, i| p[:post_number] = i + 2 } posts.each do |p| parent = posts.select { |pp| pp[:id] == p[:parent_id] } p[:reply_to_post_number] = parent[0][:post_number] if parent.size > 0 @@ -74,35 +67,35 @@ class QuandoraQuestion def post_from_answer(answer) post = {} - post[:id] = answer['uid'] - post[:parent_id] = @question['uid'] - post[:author] = answer['author'] - post[:author_id] = answer['author']['uid'] - post[:raw] = unescape answer['content'] - post[:created_at] = Time.parse answer['created'] + post[:id] = answer["uid"] + post[:parent_id] = @question["uid"] + post[:author] = answer["author"] + post[:author_id] = answer["author"]["uid"] + post[:raw] = unescape answer["content"] + post[:created_at] = Time.parse answer["created"] post end def post_from_comment(comment, index, parent) - if comment['created'] - created_at = Time.parse comment['created'] + if comment["created"] + created_at = Time.parse comment["created"] else - created_at = Time.parse parent['created'] + created_at = Time.parse parent["created"] end - parent_id = parent['uid'] - parent_id = "#{parent['uid']}-#{index - 1}" if index > 0 + parent_id = parent["uid"] + parent_id = "#{parent["uid"]}-#{index - 1}" if index > 0 post = {} - id = "#{parent['uid']}-#{index}" + id = "#{parent["uid"]}-#{index}" post[:id] = id post[:parent_id] = parent_id - post[:author] = comment['author'] - post[:author_id] = comment['author']['uid'] - post[:raw] = unescape comment['text'] + post[:author] = comment["author"] + post[:author_id] = comment["author"]["uid"] + post[:raw] = unescape comment["text"] post[:created_at] = created_at post end - private + private def unescape(html) return nil unless html diff --git a/script/import_scripts/quandora/test/test_data.rb b/script/import_scripts/quandora/test/test_data.rb index 3166d6c44d..172e753e31 100644 --- a/script/import_scripts/quandora/test/test_data.rb +++ b/script/import_scripts/quandora/test/test_data.rb @@ -1,5 +1,6 @@ - # frozen_string_literal: true - BASES = '{ +# frozen_string_literal: true +BASES = + '{ "type" : "kbase", "data" : [ { "objectId" : "90b1ccf3-35aa-4d6f-848e-e7c122d92c58", @@ -9,7 +10,8 @@ } ] }' - QUESTIONS = '{ +QUESTIONS = + '{ "type": "question-search-result", "data": { "totalSize": 445, @@ -50,7 +52,8 @@ } }' - QUESTION = '{ +QUESTION = + '{ "type" : "question", "data" : { "uid" : "de20ed0a-5fe5-48a5-9c14-d854f9af99f1", diff --git a/script/import_scripts/quandora/test/test_quandora_api.rb b/script/import_scripts/quandora/test/test_quandora_api.rb index 784ba4fb85..a167ca9ad7 100644 --- a/script/import_scripts/quandora/test/test_quandora_api.rb +++ b/script/import_scripts/quandora/test/test_quandora_api.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true -require 'minitest/autorun' -require 'yaml' -require_relative '../quandora_api.rb' -require_relative './test_data.rb' +require "minitest/autorun" +require "yaml" +require_relative "../quandora_api.rb" +require_relative "./test_data.rb" class TestQuandoraApi < Minitest::Test - DEBUG = false def initialize(args) - config = YAML::load_file(File.join(__dir__, 'config.yml')) - @domain = config['domain'] - @username = config['username'] - @password = config['password'] - @kb_id = config['kb_id'] - @question_id = config['question_id'] + config = YAML.load_file(File.join(__dir__, "config.yml")) + @domain = config["domain"] + @username = config["username"] + @password = config["password"] + @kb_id = config["kb_id"] + @question_id = config["question_id"] super args end @@ -30,19 +29,19 @@ class TestQuandoraApi < Minitest::Test end def test_base_url - assert_equal 'https://mydomain.quandora.com/m/json', @quandora.base_url('mydomain') + assert_equal "https://mydomain.quandora.com/m/json", @quandora.base_url("mydomain") end def test_auth_header - user = 'Aladdin' - password = 'open sesame' + user = "Aladdin" + password = "open sesame" auth_header = @quandora.auth_header user, password - assert_equal 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', auth_header[:Authorization] + assert_equal "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", auth_header[:Authorization] end def test_list_bases_element_has_expected_structure element = @quandora.list_bases[0] - expected = JSON.parse(BASES)['data'][0] + expected = JSON.parse(BASES)["data"][0] debug element check_keys expected, element end @@ -50,24 +49,24 @@ class TestQuandoraApi < Minitest::Test def test_list_questions_has_expected_structure response = @quandora.list_questions @kb_id, 1 debug response - check_keys JSON.parse(QUESTIONS)['data']['result'][0], response[0] + check_keys JSON.parse(QUESTIONS)["data"]["result"][0], response[0] end def test_get_question_has_expected_structure question = @quandora.get_question @question_id - expected = JSON.parse(QUESTION)['data'] + expected = JSON.parse(QUESTION)["data"] check_keys expected, question - expected_comment = expected['comments'][0] - actual_comment = question['comments'][0] + expected_comment = expected["comments"][0] + actual_comment = question["comments"][0] check_keys expected_comment, actual_comment - expected_answer = expected['answersList'][1] - actual_answer = question['answersList'][0] + expected_answer = expected["answersList"][1] + actual_answer = question["answersList"][0] check_keys expected_answer, actual_answer - expected_answer_comment = expected_answer['comments'][0] - actual_answer_comment = actual_answer['comments'][0] + expected_answer_comment = expected_answer["comments"][0] + actual_answer_comment = actual_answer["comments"][0] check_keys expected_answer_comment, actual_answer_comment end @@ -75,18 +74,16 @@ class TestQuandoraApi < Minitest::Test def check_keys(expected, actual) msg = "### caller[0]:\nKey not found in actual keys: #{actual.keys}\n" - expected.keys.each do |k| - assert (actual.keys.include? k), "#{k}" - end + expected.keys.each { |k| assert (actual.keys.include? k), "#{k}" } end def debug(message, show = false) if show || DEBUG - puts '### ' + caller[0] - puts '' + puts "### " + caller[0] + puts "" puts message - puts '' - puts '' + puts "" + puts "" end end end diff --git a/script/import_scripts/quandora/test/test_quandora_question.rb b/script/import_scripts/quandora/test/test_quandora_question.rb index 28b5dd9885..6044951c5b 100644 --- a/script/import_scripts/quandora/test/test_quandora_question.rb +++ b/script/import_scripts/quandora/test/test_quandora_question.rb @@ -1,47 +1,46 @@ # frozen_string_literal: true -require 'minitest/autorun' -require 'cgi' -require 'time' -require_relative '../quandora_question.rb' -require_relative './test_data.rb' +require "minitest/autorun" +require "cgi" +require "time" +require_relative "../quandora_question.rb" +require_relative "./test_data.rb" class TestQuandoraQuestion < Minitest::Test - def setup - @data = JSON.parse(QUESTION)['data'] + @data = JSON.parse(QUESTION)["data"] @question = QuandoraQuestion.new @data.to_json end def test_topic topic = @question.topic - assert_equal @data['uid'], topic[:id] - assert_equal @data['author']['uid'], topic[:author_id] - assert_equal unescape(@data['title']), topic[:title] - assert_equal unescape(@data['content']), topic[:raw] - assert_equal Time.parse(@data['created']), topic[:created_at] + assert_equal @data["uid"], topic[:id] + assert_equal @data["author"]["uid"], topic[:author_id] + assert_equal unescape(@data["title"]), topic[:title] + assert_equal unescape(@data["content"]), topic[:raw] + assert_equal Time.parse(@data["created"]), topic[:created_at] end def test_user_from_author author = {} - author['uid'] = 'uid' - author['firstName'] = 'Joe' - author['lastName'] = 'Schmoe' - author['email'] = 'joe.schmoe@mydomain.com' + author["uid"] = "uid" + author["firstName"] = "Joe" + author["lastName"] = "Schmoe" + author["email"] = "joe.schmoe@mydomain.com" user = @question.user_from_author author - assert_equal 'uid', user[:id] - assert_equal 'Joe Schmoe', user[:name] - assert_equal 'joe.schmoe@mydomain.com', user[:email] + assert_equal "uid", user[:id] + assert_equal "Joe Schmoe", user[:name] + assert_equal "joe.schmoe@mydomain.com", user[:email] assert_equal true, user[:staged] end def test_user_from_author_with_no_email author = {} - author['uid'] = 'foo' + author["uid"] = "foo" user = @question.user_from_author author - assert_equal 'foo@noemail.com', user[:email] + assert_equal "foo@noemail.com", user[:email] end def test_replies @@ -57,77 +56,77 @@ class TestQuandoraQuestion < Minitest::Test assert_equal nil, replies[2][:reply_to_post_number] assert_equal 4, replies[3][:reply_to_post_number] assert_equal 3, replies[4][:reply_to_post_number] - assert_equal '2013-01-07 04:59:56 UTC', replies[0][:created_at].to_s - assert_equal '2013-01-08 16:49:32 UTC', replies[1][:created_at].to_s - assert_equal '2016-01-20 15:38:55 UTC', replies[2][:created_at].to_s - assert_equal '2016-01-21 15:38:55 UTC', replies[3][:created_at].to_s - assert_equal '2016-01-22 15:38:55 UTC', replies[4][:created_at].to_s + assert_equal "2013-01-07 04:59:56 UTC", replies[0][:created_at].to_s + assert_equal "2013-01-08 16:49:32 UTC", replies[1][:created_at].to_s + assert_equal "2016-01-20 15:38:55 UTC", replies[2][:created_at].to_s + assert_equal "2016-01-21 15:38:55 UTC", replies[3][:created_at].to_s + assert_equal "2016-01-22 15:38:55 UTC", replies[4][:created_at].to_s end def test_post_from_answer answer = {} - answer['uid'] = 'uid' - answer['content'] = 'content' - answer['created'] = '2013-01-06T18:24:54.62Z' - answer['author'] = { 'uid' => 'auid' } + answer["uid"] = "uid" + answer["content"] = "content" + answer["created"] = "2013-01-06T18:24:54.62Z" + answer["author"] = { "uid" => "auid" } post = @question.post_from_answer answer - assert_equal 'uid', post[:id] + assert_equal "uid", post[:id] assert_equal @question.topic[:id], post[:parent_id] - assert_equal answer['author'], post[:author] - assert_equal 'auid', post[:author_id] - assert_equal 'content', post[:raw] - assert_equal Time.parse('2013-01-06T18:24:54.62Z'), post[:created_at] + assert_equal answer["author"], post[:author] + assert_equal "auid", post[:author_id] + assert_equal "content", post[:raw] + assert_equal Time.parse("2013-01-06T18:24:54.62Z"), post[:created_at] end def test_post_from_comment comment = {} - comment['text'] = 'text' - comment['created'] = '2013-01-06T18:24:54.62Z' - comment['author'] = { 'uid' => 'auid' } - parent = { 'uid' => 'parent-uid' } + comment["text"] = "text" + comment["created"] = "2013-01-06T18:24:54.62Z" + comment["author"] = { "uid" => "auid" } + parent = { "uid" => "parent-uid" } post = @question.post_from_comment comment, 0, parent - assert_equal 'parent-uid-0', post[:id] - assert_equal 'parent-uid', post[:parent_id] - assert_equal comment['author'], post[:author] - assert_equal 'auid', post[:author_id] - assert_equal 'text', post[:raw] - assert_equal Time.parse('2013-01-06T18:24:54.62Z'), post[:created_at] + assert_equal "parent-uid-0", post[:id] + assert_equal "parent-uid", post[:parent_id] + assert_equal comment["author"], post[:author] + assert_equal "auid", post[:author_id] + assert_equal "text", post[:raw] + assert_equal Time.parse("2013-01-06T18:24:54.62Z"), post[:created_at] end def test_post_from_comment_uses_parent_created_if_necessary comment = {} - comment['author'] = { 'uid' => 'auid' } - parent = { 'created' => '2013-01-06T18:24:54.62Z' } + comment["author"] = { "uid" => "auid" } + parent = { "created" => "2013-01-06T18:24:54.62Z" } post = @question.post_from_comment comment, 0, parent - assert_equal Time.parse('2013-01-06T18:24:54.62Z'), post[:created_at] + assert_equal Time.parse("2013-01-06T18:24:54.62Z"), post[:created_at] end def test_post_from_comment_uses_previous_comment_as_parent comment = {} - comment['author'] = { 'uid' => 'auid' } - parent = { 'uid' => 'parent-uid', 'created' => '2013-01-06T18:24:54.62Z' } + comment["author"] = { "uid" => "auid" } + parent = { "uid" => "parent-uid", "created" => "2013-01-06T18:24:54.62Z" } post = @question.post_from_comment comment, 1, parent - assert_equal 'parent-uid-1', post[:id] - assert_equal 'parent-uid-0', post[:parent_id] - assert_equal Time.parse('2013-01-06T18:24:54.62Z'), post[:created_at] + assert_equal "parent-uid-1", post[:id] + assert_equal "parent-uid-0", post[:parent_id] + assert_equal Time.parse("2013-01-06T18:24:54.62Z"), post[:created_at] end def test_users users = @question.users assert_equal 5, users.size - assert_equal 'Ida Inquisitive', users[0][:name] - assert_equal 'Harry Helpful', users[1][:name] - assert_equal 'Sam Smarty-Pants', users[2][:name] - assert_equal 'Greta Greatful', users[3][:name] - assert_equal 'Eddy Excited', users[4][:name] + assert_equal "Ida Inquisitive", users[0][:name] + assert_equal "Harry Helpful", users[1][:name] + assert_equal "Sam Smarty-Pants", users[2][:name] + assert_equal "Greta Greatful", users[3][:name] + assert_equal "Eddy Excited", users[4][:name] end private diff --git a/script/import_scripts/question2answer.rb b/script/import_scripts/question2answer.rb index acd5b70beb..3820b8050b 100644 --- a/script/import_scripts/question2answer.rb +++ b/script/import_scripts/question2answer.rb @@ -1,21 +1,21 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' -require 'php_serialize' # https://github.com/jqr/php-serialize +require "htmlentities" +require "php_serialize" # https://github.com/jqr/php-serialize class ImportScripts::Question2Answer < ImportScripts::Base BATCH_SIZE = 1000 # CHANGE THESE BEFORE RUNNING THE IMPORTER - DB_HOST ||= ENV['DB_HOST'] || "localhost" - DB_NAME ||= ENV['DB_NAME'] || "qa_db" - DB_PW ||= ENV['DB_PW'] || "" - DB_USER ||= ENV['DB_USER'] || "root" - TIMEZONE ||= ENV['TIMEZONE'] || "America/Los_Angeles" - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "qa_" + DB_HOST ||= ENV["DB_HOST"] || "localhost" + DB_NAME ||= ENV["DB_NAME"] || "qa_db" + DB_PW ||= ENV["DB_PW"] || "" + DB_USER ||= ENV["DB_USER"] || "root" + TIMEZONE ||= ENV["TIMEZONE"] || "America/Los_Angeles" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "qa_" def initialize super @@ -26,12 +26,8 @@ class ImportScripts::Question2Answer < ImportScripts::Base @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: DB_HOST, - username: DB_USER, - password: DB_PW, - database: DB_NAME - ) + @client = + Mysql2::Client.new(host: DB_HOST, username: DB_USER, password: DB_PW, database: DB_NAME) end def execute @@ -51,11 +47,16 @@ class ImportScripts::Question2Answer < ImportScripts::Base # only import users that have posted or voted on Q2A # if you want to import all users, just leave out the WHERE and everything after it (and remove line 95 as well) - user_count = mysql_query("SELECT COUNT(userid) count FROM #{TABLE_PREFIX}users u WHERE EXISTS (SELECT 1 FROM #{TABLE_PREFIX}posts p WHERE p.userid=u.userid) or EXISTS (SELECT 1 FROM #{TABLE_PREFIX}uservotes uv WHERE u.userid=uv.userid)").first["count"] + user_count = + mysql_query( + "SELECT COUNT(userid) count FROM #{TABLE_PREFIX}users u WHERE EXISTS (SELECT 1 FROM #{TABLE_PREFIX}posts p WHERE p.userid=u.userid) or EXISTS (SELECT 1 FROM #{TABLE_PREFIX}uservotes uv WHERE u.userid=uv.userid)", + ).first[ + "count" + ] last_user_id = -1 batches(BATCH_SIZE) do |offset| - users = mysql_query(<<-SQL + users = mysql_query(<<-SQL).to_a SELECT u.userid AS id, u.email, u.handle AS username, u.created AS created_at, u.loggedin AS last_sign_in_at, u.avatarblobid FROM #{TABLE_PREFIX}users u WHERE u.userid > #{last_user_id} @@ -63,7 +64,6 @@ class ImportScripts::Question2Answer < ImportScripts::Base ORDER BY u.userid LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? last_user_id = users[-1]["id"] @@ -73,18 +73,17 @@ class ImportScripts::Question2Answer < ImportScripts::Base email = user["email"].presence username = @htmlentities.decode(user["email"]).strip.split("@").first - avatar_url = "https://your_image_bucket/#{user['cdn_slug']}" if user['cdn_slug'] + avatar_url = "https://your_image_bucket/#{user["cdn_slug"]}" if user["cdn_slug"] { id: user["id"], - name: "#{user['username']}", - username: "#{user['username']}", - password: user['password'], + name: "#{user["username"]}", + username: "#{user["username"]}", + password: user["password"], email: email, created_at: user["created_at"], last_seen_at: user["last_sign_in_at"], - post_create_action: proc do |u| - @old_username_to_new_usernames[user["username"]] = u.username - end + post_create_action: + proc { |u| @old_username_to_new_usernames[user["username"]] = u.username }, } end end @@ -93,7 +92,10 @@ class ImportScripts::Question2Answer < ImportScripts::Base def import_categories puts "", "importing top level categories..." - categories = mysql_query("SELECT categoryid, parentid, title, position FROM #{TABLE_PREFIX}categories ORDER BY categoryid").to_a + categories = + mysql_query( + "SELECT categoryid, parentid, title, position FROM #{TABLE_PREFIX}categories ORDER BY categoryid", + ).to_a top_level_categories = categories.select { |c| c["parentid"].nil? } @@ -101,7 +103,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base { id: category["categoryid"], name: @htmlentities.decode(category["title"]).strip, - position: category["position"] + position: category["position"], } end @@ -122,7 +124,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base id: category["categoryid"], name: @htmlentities.decode(category["title"]).strip, position: category["position"], - parent_category_id: category_id_from_imported_category_id(category["parentid"]) + parent_category_id: category_id_from_imported_category_id(category["parentid"]), } end end @@ -130,12 +132,15 @@ class ImportScripts::Question2Answer < ImportScripts::Base def import_topics puts "", "importing topics..." - topic_count = mysql_query("SELECT COUNT(postid) count FROM #{TABLE_PREFIX}posts WHERE type = 'Q'").first["count"] + topic_count = + mysql_query("SELECT COUNT(postid) count FROM #{TABLE_PREFIX}posts WHERE type = 'Q'").first[ + "count" + ] last_topic_id = -1 batches(BATCH_SIZE) do |offset| - topics = mysql_query(<<-SQL + topics = mysql_query(<<-SQL).to_a SELECT p.postid, p.type, p.categoryid, p.closedbyid, p.userid postuserid, p.views, p.created, p.title, p.content raw FROM #{TABLE_PREFIX}posts p WHERE type = 'Q' @@ -143,7 +148,6 @@ class ImportScripts::Question2Answer < ImportScripts::Base ORDER BY p.postid LIMIT #{BATCH_SIZE} SQL - ).to_a break if topics.empty? @@ -179,20 +183,19 @@ class ImportScripts::Question2Answer < ImportScripts::Base if topic.present? title_slugified = slugify(thread["title"], false, 50) if thread["title"].present? url_slug = "qa/#{thread["postid"]}/#{title_slugified}" if thread["title"].present? - Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) if url_slug.present? && topic[:topic_id].present? + if url_slug.present? && topic[:topic_id].present? + Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) + end end end - end end def slugify(title, ascii_only, max_length) - words = title.downcase.gsub(/[^a-zA-Z0-9\s]/, '').split(" ") + words = title.downcase.gsub(/[^a-zA-Z0-9\s]/, "").split(" ") word_lengths = {} - words.each_with_index do |word, idx| - word_lengths[idx] = word.length - end + words.each_with_index { |word, idx| word_lengths[idx] = word.length } remaining = max_length if word_lengths.inject(0) { |sum, (_, v)| sum + v } > remaining @@ -211,17 +214,16 @@ class ImportScripts::Question2Answer < ImportScripts::Base def import_posts puts "", "importing posts..." - post_count = mysql_query(<<-SQL + post_count = mysql_query(<<-SQL).first["count"] SELECT COUNT(postid) count FROM #{TABLE_PREFIX}posts p WHERE p.parentid IS NOT NULL SQL - ).first["count"] last_post_id = -1 batches(BATCH_SIZE) do |offset| - posts = mysql_query(<<-SQL + posts = mysql_query(<<-SQL).to_a SELECT p.postid, p.type, p.parentid, p.categoryid, p.closedbyid, p.userid, p.views, p.created, p.title, p.content, parent.type AS parenttype, parent.parentid AS qid FROM #{TABLE_PREFIX}posts p @@ -233,7 +235,6 @@ class ImportScripts::Question2Answer < ImportScripts::Base ORDER BY p.postid LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? last_post_id = posts[-1]["postid"] @@ -250,11 +251,11 @@ class ImportScripts::Question2Answer < ImportScripts::Base # this works as long as comments can not have a comment as parent # it's always Q-A Q-C or A-C - if post['type'] == 'A' # for answers the question/topic is always the parent + if post["type"] == "A" # for answers the question/topic is always the parent topic = topic_lookup_from_imported_post_id("thread-#{post["parentid"]}") next if topic.nil? else - if post['parenttype'] == 'Q' # for comments to questions, the question/topic is the parent as well + if post["parenttype"] == "Q" # for comments to questions, the question/topic is the parent as well topic = topic_lookup_from_imported_post_id("thread-#{post["parentid"]}") next if topic.nil? else # for comments to answers, the question/topic is the parent of the parent @@ -284,7 +285,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base ans = mysql_query("select postid, selchildid from qa_posts where selchildid is not null").to_a ans.each do |answer| begin - post = Post.find_by(id: post_id_from_imported_post_id("#{answer['selchildid']}")) + post = Post.find_by(id: post_id_from_imported_post_id("#{answer["selchildid"]}")) post.custom_fields["is_accepted_answer"] = "true" post.save topic = Topic.find(post.topic_id) @@ -293,20 +294,18 @@ class ImportScripts::Question2Answer < ImportScripts::Base rescue => e puts "error acting on post #{e}" end - end end def import_likes puts "", "importing likes..." - likes = mysql_query(<<-SQL + likes = mysql_query(<<-SQL).to_a SELECT postid, userid FROM #{TABLE_PREFIX}uservotes u WHERE u.vote=1 SQL - ).to_a likes.each do |like| - post = Post.find_by(id: post_id_from_imported_post_id("thread-#{like['postid']}")) + post = Post.find_by(id: post_id_from_imported_post_id("thread-#{like["postid"]}")) user = User.find_by(id: user_id_from_imported_user_id(like["userid"])) begin PostActionCreator.like(user, post) if user && post @@ -340,10 +339,10 @@ class ImportScripts::Question2Answer < ImportScripts::Base def preprocess_post_raw(raw) return "" if raw.blank? - raw.gsub!(/(.+)<\/a>/i, '[\2](\1)') - raw.gsub!(/

    (.+?)<\/p>/im) { "#{$1}\n\n" } - raw.gsub!('
    ', "\n") - raw.gsub!(/(.*?)<\/strong>/im, '[b]\1[/b]') + raw.gsub!(%r{(.+)}i, '[\2](\1)') + raw.gsub!(%r{

    (.+?)

    }im) { "#{$1}\n\n" } + raw.gsub!("
    ", "\n") + raw.gsub!(%r{(.*?)}im, '[b]\1[/b]') # decode HTML entities raw = @htmlentities.decode(raw) @@ -355,22 +354,22 @@ class ImportScripts::Question2Answer < ImportScripts::Base # [HTML]...[/HTML] raw.gsub!(/\[html\]/i, "\n```html\n") - raw.gsub!(/\[\/html\]/i, "\n```\n") + raw.gsub!(%r{\[/html\]}i, "\n```\n") # [PHP]...[/PHP] raw.gsub!(/\[php\]/i, "\n```php\n") - raw.gsub!(/\[\/php\]/i, "\n```\n") + raw.gsub!(%r{\[/php\]}i, "\n```\n") # [HIGHLIGHT="..."] raw.gsub!(/\[highlight="?(\w+)"?\]/i) { "\n```#{$1.downcase}\n" } # [CODE]...[/CODE] # [HIGHLIGHT]...[/HIGHLIGHT] - raw.gsub!(/\[\/?code\]/i, "\n```\n") - raw.gsub!(/\[\/?highlight\]/i, "\n```\n") + raw.gsub!(%r{\[/?code\]}i, "\n```\n") + raw.gsub!(%r{\[/?highlight\]}i, "\n```\n") # [SAMP]...[/SAMP] - raw.gsub!(/\[\/?samp\]/i, "`") + raw.gsub!(%r{\[/?samp\]}i, "`") # replace all chevrons with HTML entities # NOTE: must be done @@ -385,16 +384,16 @@ class ImportScripts::Question2Answer < ImportScripts::Base raw.gsub!("\u2603", ">") # [URL=...]...[/URL] - raw.gsub!(/\[url="?([^"]+?)"?\](.*?)\[\/url\]/im) { "[#{$2.strip}](#{$1})" } - raw.gsub!(/\[url="?(.+?)"?\](.+)\[\/url\]/im) { "[#{$2.strip}](#{$1})" } + raw.gsub!(%r{\[url="?([^"]+?)"?\](.*?)\[/url\]}im) { "[#{$2.strip}](#{$1})" } + raw.gsub!(%r{\[url="?(.+?)"?\](.+)\[/url\]}im) { "[#{$2.strip}](#{$1})" } # [URL]...[/URL] # [MP3]...[/MP3] - raw.gsub!(/\[\/?url\]/i, "") - raw.gsub!(/\[\/?mp3\]/i, "") + raw.gsub!(%r{\[/?url\]}i, "") + raw.gsub!(%r{\[/?mp3\]}i, "") # [MENTION][/MENTION] - raw.gsub!(/\[mention\](.+?)\[\/mention\]/i) do + raw.gsub!(%r{\[mention\](.+?)\[/mention\]}i) do old_username = $1 if @old_username_to_new_usernames.has_key?(old_username) old_username = @old_username_to_new_usernames[old_username] @@ -403,31 +402,31 @@ class ImportScripts::Question2Answer < ImportScripts::Base end # [FONT=blah] and [COLOR=blah] - raw.gsub!(/\[FONT=.*?\](.*?)\[\/FONT\]/im, '\1') - raw.gsub!(/\[COLOR=.*?\](.*?)\[\/COLOR\]/im, '\1') - raw.gsub!(/\[COLOR=#.*?\](.*?)\[\/COLOR\]/im, '\1') + raw.gsub!(%r{\[FONT=.*?\](.*?)\[/FONT\]}im, '\1') + raw.gsub!(%r{\[COLOR=.*?\](.*?)\[/COLOR\]}im, '\1') + raw.gsub!(%r{\[COLOR=#.*?\](.*?)\[/COLOR\]}im, '\1') - raw.gsub!(/\[SIZE=.*?\](.*?)\[\/SIZE\]/im, '\1') - raw.gsub!(/\[h=.*?\](.*?)\[\/h\]/im, '\1') + raw.gsub!(%r{\[SIZE=.*?\](.*?)\[/SIZE\]}im, '\1') + raw.gsub!(%r{\[h=.*?\](.*?)\[/h\]}im, '\1') # [CENTER]...[/CENTER] - raw.gsub!(/\[CENTER\](.*?)\[\/CENTER\]/im, '\1') + raw.gsub!(%r{\[CENTER\](.*?)\[/CENTER\]}im, '\1') # [INDENT]...[/INDENT] - raw.gsub!(/\[INDENT\](.*?)\[\/INDENT\]/im, '\1') - raw.gsub!(/\[TABLE\](.*?)\[\/TABLE\]/im, '\1') - raw.gsub!(/\[TR\](.*?)\[\/TR\]/im, '\1') - raw.gsub!(/\[TD\](.*?)\[\/TD\]/im, '\1') - raw.gsub!(/\[TD="?.*?"?\](.*?)\[\/TD\]/im, '\1') + raw.gsub!(%r{\[INDENT\](.*?)\[/INDENT\]}im, '\1') + raw.gsub!(%r{\[TABLE\](.*?)\[/TABLE\]}im, '\1') + raw.gsub!(%r{\[TR\](.*?)\[/TR\]}im, '\1') + raw.gsub!(%r{\[TD\](.*?)\[/TD\]}im, '\1') + raw.gsub!(%r{\[TD="?.*?"?\](.*?)\[/TD\]}im, '\1') # [QUOTE]...[/QUOTE] - raw.gsub!(/\[quote\](.+?)\[\/quote\]/im) { |quote| - quote.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[quote\](.+?)\[/quote\]}im) do |quote| + quote.gsub!(%r{\[quote\](.+?)\[/quote\]}im) { "\n#{$1}\n" } quote.gsub!(/\n(.+?)/) { "\n> #{$1}" } - } + end # [QUOTE=]...[/QUOTE] - raw.gsub!(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do + raw.gsub!(%r{\[quote=([^;\]]+)\](.+?)\[/quote\]}im) do old_username, quote = $1, $2 if @old_username_to_new_usernames.has_key?(old_username) old_username = @old_username_to_new_usernames[old_username] @@ -436,31 +435,33 @@ class ImportScripts::Question2Answer < ImportScripts::Base end # [YOUTUBE][/YOUTUBE] - raw.gsub!(/\[youtube\](.+?)\[\/youtube\]/i) { "\n//youtu.be/#{$1}\n" } + raw.gsub!(%r{\[youtube\](.+?)\[/youtube\]}i) { "\n//youtu.be/#{$1}\n" } # [VIDEO=youtube;]...[/VIDEO] - raw.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" } + raw.gsub!(%r{\[video=youtube;([^\]]+)\].*?\[/video\]}i) { "\n//youtu.be/#{$1}\n" } # More Additions .... # [spoiler=Some hidden stuff]SPOILER HERE!![/spoiler] - raw.gsub!(/\[spoiler="?(.+?)"?\](.+?)\[\/spoiler\]/im) { "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" } + raw.gsub!(%r{\[spoiler="?(.+?)"?\](.+?)\[/spoiler\]}im) do + "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" + end # [IMG][IMG]http://i63.tinypic.com/akga3r.jpg[/IMG][/IMG] - raw.gsub!(/\[IMG\]\[IMG\](.+?)\[\/IMG\]\[\/IMG\]/i) { "[IMG]#{$1}[/IMG]" } + raw.gsub!(%r{\[IMG\]\[IMG\](.+?)\[/IMG\]\[/IMG\]}i) { "[IMG]#{$1}[/IMG]" } # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) # (https://meta.discourse.org/t/phpbb-3-importer-old/17397) - raw.gsub!(/\[list\](.*?)\[\/list\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\](.*?)\[\/list\]/im, '[ol]\1[/ol]') - raw.gsub!(/\[list\](.*?)\[\/list:u\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\](.*?)\[\/list:o\]/im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\](.*?)\[/list\]}im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list:u\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\](.*?)\[/list:o\]}im, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - raw.gsub!(/\[\*\]\n/, '') - raw.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]') + raw.gsub!(/\[\*\]\n/, "") + raw.gsub!(%r{\[\*\](.*?)\[/\*:m\]}, '[li]\1[/li]') raw.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]') - raw.gsub!(/\[\*=1\]/, '') + raw.gsub!(/\[\*=1\]/, "") raw.strip! raw @@ -468,7 +469,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base def postprocess_post_raw(raw) # [QUOTE=;]...[/QUOTE] - raw.gsub!(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do + raw.gsub!(%r{\[quote=([^;]+);(\d+)\](.+?)\[/quote\]}im) do old_username, post_id, quote = $1, $2, $3 if @old_username_to_new_usernames.has_key?(old_username) @@ -477,7 +478,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base if topic_lookup = topic_lookup_from_imported_post_id(post_id) post_number = topic_lookup[:post_number] - topic_id = topic_lookup[:topic_id] + topic_id = topic_lookup[:topic_id] "\n[quote=\"#{old_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" else "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" @@ -485,11 +486,11 @@ class ImportScripts::Question2Answer < ImportScripts::Base end # remove attachments - raw.gsub!(/\[attach[^\]]*\]\d+\[\/attach\]/i, "") + raw.gsub!(%r{\[attach[^\]]*\]\d+\[/attach\]}i, "") # [THREAD][/THREAD] # ==> http://my.discourse.org/t/slug/ - raw.gsub!(/\[thread\](\d+)\[\/thread\]/i) do + raw.gsub!(%r{\[thread\](\d+)\[/thread\]}i) do thread_id = $1 if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") topic_lookup[:url] @@ -500,7 +501,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base # [THREAD=]...[/THREAD] # ==> [...](http://my.discourse.org/t/slug/) - raw.gsub!(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do + raw.gsub!(%r{\[thread=(\d+)\](.+?)\[/thread\]}i) do thread_id, link = $1, $2 if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") url = topic_lookup[:url] @@ -512,7 +513,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base # [POST][/POST] # ==> http://my.discourse.org/t/slug// - raw.gsub!(/\[post\](\d+)\[\/post\]/i) do + raw.gsub!(%r{\[post\](\d+)\[/post\]}i) do post_id = $1 if topic_lookup = topic_lookup_from_imported_post_id(post_id) topic_lookup[:url] @@ -523,7 +524,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base # [POST=]...[/POST] # ==> [...](http://my.discourse.org/t///) - raw.gsub!(/\[post=(\d+)\](.+?)\[\/post\]/i) do + raw.gsub!(%r{\[post=(\d+)\](.+?)\[/post\]}i) do post_id, link = $1, $2 if topic_lookup = topic_lookup_from_imported_post_id(post_id) url = topic_lookup[:url] @@ -537,7 +538,7 @@ class ImportScripts::Question2Answer < ImportScripts::Base end def create_permalinks - puts '', 'Creating permalinks...' + puts "", "Creating permalinks..." # topics Topic.find_each do |topic| @@ -546,7 +547,11 @@ class ImportScripts::Question2Answer < ImportScripts::Base if tcf && tcf["import_id"] question_id = tcf["import_id"][/thread-(\d)/, 0] url = "#{question_id}" - Permalink.create(url: url, topic_id: topic.id) rescue nil + begin + Permalink.create(url: url, topic_id: topic.id) + rescue StandardError + nil + end end end @@ -555,11 +560,21 @@ class ImportScripts::Question2Answer < ImportScripts::Base ccf = category.custom_fields if ccf && ccf["import_id"] - url = category.parent_category ? "#{category.parent_category.slug}/#{category.slug}" : category.slug - Permalink.create(url: url, category_id: category.id) rescue nil + url = + ( + if category.parent_category + "#{category.parent_category.slug}/#{category.slug}" + else + category.slug + end + ) + begin + Permalink.create(url: url, category_id: category.id) + rescue StandardError + nil + end end end - end def parse_timestamp(timestamp) @@ -569,7 +584,6 @@ class ImportScripts::Question2Answer < ImportScripts::Base def mysql_query(sql) @client.query(sql, cache_rows: true) end - end ImportScripts::Question2Answer.new.perform diff --git a/script/import_scripts/sfn.rb b/script/import_scripts/sfn.rb index e9270813d7..e10c9c1b0a 100644 --- a/script/import_scripts/sfn.rb +++ b/script/import_scripts/sfn.rb @@ -8,7 +8,6 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Sfn < ImportScripts::Base - BATCH_SIZE = 100_000 MIN_CREATED_AT = "2003-11-01" @@ -96,22 +95,27 @@ class ImportScripts::Sfn < ImportScripts::Base username: email.split("@")[0], bio_raw: bio, created_at: user["created_at"], - post_create_action: proc do |newuser| - next if user["avatar"].blank? + post_create_action: + proc do |newuser| + next if user["avatar"].blank? - avatar = Tempfile.new("sfn-avatar") - avatar.write(user["avatar"].encode("ASCII-8BIT").force_encoding("UTF-8")) - avatar.rewind + avatar = Tempfile.new("sfn-avatar") + avatar.write(user["avatar"].encode("ASCII-8BIT").force_encoding("UTF-8")) + avatar.rewind - upload = UploadCreator.new(avatar, "avatar.jpg").create_for(newuser.id) - if upload.persisted? - newuser.create_user_avatar - newuser.user_avatar.update(custom_upload_id: upload.id) - newuser.update(uploaded_avatar_id: upload.id) - end + upload = UploadCreator.new(avatar, "avatar.jpg").create_for(newuser.id) + if upload.persisted? + newuser.create_user_avatar + newuser.user_avatar.update(custom_upload_id: upload.id) + newuser.update(uploaded_avatar_id: upload.id) + end - avatar.try(:close!) rescue nil - end + begin + avatar.try(:close!) + rescue StandardError + nil + end + end, } end end @@ -198,9 +202,7 @@ class ImportScripts::Sfn < ImportScripts::Base def import_categories puts "", "importing categories..." - create_categories(NEW_CATEGORIES) do |category| - { id: category, name: category } - end + create_categories(NEW_CATEGORIES) { |category| { id: category, name: category } } end def import_topics @@ -234,7 +236,7 @@ class ImportScripts::Sfn < ImportScripts::Base SQL break if topics.size < 1 - next if all_records_exist? :posts, topics.map { |t| t['id'].to_i } + next if all_records_exist? :posts, topics.map { |t| t["id"].to_i } create_posts(topics, total: topic_count, offset: offset) do |topic| next unless category_id = CATEGORY_MAPPING[topic["category_id"]] @@ -286,7 +288,7 @@ class ImportScripts::Sfn < ImportScripts::Base break if posts.size < 1 - next if all_records_exist? :posts, posts.map { |p| p['id'].to_i } + next if all_records_exist? :posts, posts.map { |p| p["id"].to_i } create_posts(posts, total: posts_count, offset: offset) do |post| next unless parent = topic_lookup_from_imported_post_id(post["topic_id"]) @@ -307,7 +309,7 @@ class ImportScripts::Sfn < ImportScripts::Base def cleanup_raw(raw) # fix some html - raw.gsub!(//i, "\n") + raw.gsub!(%r{}i, "\n") # remove "This message has been cross posted to the following eGroups: ..." raw.gsub!(/^This message has been cross posted to the following eGroups: .+\n-{3,}/i, "") # remove signatures @@ -320,7 +322,6 @@ class ImportScripts::Sfn < ImportScripts::Base @client ||= Mysql2::Client.new(username: "root", database: "sfn") @client.query(sql) end - end ImportScripts::Sfn.new.perform diff --git a/script/import_scripts/simplepress.rb b/script/import_scripts/simplepress.rb index 3375258790..b0aa7f5f11 100644 --- a/script/import_scripts/simplepress.rb +++ b/script/import_scripts/simplepress.rb @@ -1,22 +1,17 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::SimplePress < ImportScripts::Base - - SIMPLE_PRESS_DB ||= ENV['SIMPLEPRESS_DB'] || "simplepress" + SIMPLE_PRESS_DB ||= ENV["SIMPLEPRESS_DB"] || "simplepress" TABLE_PREFIX = "wp_sf" BATCH_SIZE ||= 1000 def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - database: SIMPLE_PRESS_DB, - ) + @client = Mysql2::Client.new(host: "localhost", username: "root", database: SIMPLE_PRESS_DB) SiteSetting.max_username_length = 50 end @@ -32,10 +27,11 @@ class ImportScripts::SimplePress < ImportScripts::Base puts "", "importing users..." last_user_id = -1 - total_users = mysql_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"] + total_users = + mysql_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"] batches(BATCH_SIZE) do |offset| - users = mysql_query(<<-SQL + users = mysql_query(<<-SQL).to_a SELECT ID id, user_nicename, display_name, user_email, user_registered, user_url FROM wp_users WHERE user_email LIKE '%@%' @@ -43,7 +39,6 @@ class ImportScripts::SimplePress < ImportScripts::Base ORDER BY id LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -55,13 +50,12 @@ class ImportScripts::SimplePress < ImportScripts::Base user_ids_sql = user_ids.join(",") users_description = {} - mysql_query(<<-SQL + mysql_query(<<-SQL).each { |um| users_description[um["user_id"]] = um["description"] } SELECT user_id, meta_value description FROM wp_usermeta WHERE user_id IN (#{user_ids_sql}) AND meta_key = 'description' SQL - ).each { |um| users_description[um["user_id"]] = um["description"] } create_users(users, total: total_users, offset: offset) do |u| { @@ -71,7 +65,7 @@ class ImportScripts::SimplePress < ImportScripts::Base name: u["display_name"], created_at: u["user_registered"], website: u["user_url"], - bio_raw: users_description[u["id"]] + bio_raw: users_description[u["id"]], } end end @@ -80,16 +74,20 @@ class ImportScripts::SimplePress < ImportScripts::Base def import_categories puts "", "importing categories..." - categories = mysql_query(<<-SQL + categories = mysql_query(<<-SQL) SELECT forum_id, forum_name, forum_seq, forum_desc, parent FROM #{TABLE_PREFIX}forums ORDER BY forum_id SQL - ) create_categories(categories) do |c| - category = { id: c['forum_id'], name: CGI.unescapeHTML(c['forum_name']), description: CGI.unescapeHTML(c['forum_desc']), position: c['forum_seq'] } - if (parent_id = c['parent'].to_i) > 0 + category = { + id: c["forum_id"], + name: CGI.unescapeHTML(c["forum_name"]), + description: CGI.unescapeHTML(c["forum_desc"]), + position: c["forum_seq"], + } + if (parent_id = c["parent"].to_i) > 0 category[:parent_category_id] = category_id_from_imported_category_id(parent_id) end category @@ -99,10 +97,15 @@ class ImportScripts::SimplePress < ImportScripts::Base def import_topics puts "", "creating topics" - total_count = mysql_query("SELECT COUNT(*) count FROM #{TABLE_PREFIX}posts WHERE post_index = 1").first["count"] + total_count = + mysql_query("SELECT COUNT(*) count FROM #{TABLE_PREFIX}posts WHERE post_index = 1").first[ + "count" + ] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.post_id id, p.topic_id topic_id, t.forum_id category_id, @@ -119,23 +122,24 @@ class ImportScripts::SimplePress < ImportScripts::Base ORDER BY p.post_id LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ") + ", + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| - created_at = Time.zone.at(m['post_time']) + created_at = Time.zone.at(m["post_time"]) { - id: m['id'], - user_id: user_id_from_imported_user_id(m['user_id']) || -1, - raw: process_simplepress_post(m['raw'], m['id']), + id: m["id"], + user_id: user_id_from_imported_user_id(m["user_id"]) || -1, + raw: process_simplepress_post(m["raw"], m["id"]), created_at: created_at, - category: category_id_from_imported_category_id(m['category_id']), - title: CGI.unescapeHTML(m['title']), - views: m['views'], - pinned_at: m['pinned'] == 1 ? created_at : nil, + category: category_id_from_imported_category_id(m["category_id"]), + title: CGI.unescapeHTML(m["title"]), + views: m["views"], + pinned_at: m["pinned"] == 1 ? created_at : nil, } end end @@ -146,17 +150,24 @@ class ImportScripts::SimplePress < ImportScripts::Base topic_first_post_id = {} - mysql_query(" + mysql_query( + " SELECT t.topic_id, p.post_id FROM #{TABLE_PREFIX}topics t JOIN #{TABLE_PREFIX}posts p ON p.topic_id = t.topic_id WHERE p.post_index = 1 - ").each { |r| topic_first_post_id[r["topic_id"]] = r["post_id"] } + ", + ).each { |r| topic_first_post_id[r["topic_id"]] = r["post_id"] } - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}posts WHERE post_index <> 1").first["count"] + total_count = + mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}posts WHERE post_index <> 1").first[ + "count" + ] batches(BATCH_SIZE) do |offset| - results = mysql_query(" + results = + mysql_query( + " SELECT p.post_id id, p.topic_id topic_id, p.user_id user_id, @@ -169,23 +180,24 @@ class ImportScripts::SimplePress < ImportScripts::Base ORDER BY p.post_id LIMIT #{BATCH_SIZE} OFFSET #{offset}; - ") + ", + ) break if results.size < 1 - next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + next if all_records_exist? :posts, results.map { |m| m["id"].to_i } create_posts(results, total: total_count, offset: offset) do |m| - if parent = topic_lookup_from_imported_post_id(topic_first_post_id[m['topic_id']]) + if parent = topic_lookup_from_imported_post_id(topic_first_post_id[m["topic_id"]]) { - id: m['id'], - user_id: user_id_from_imported_user_id(m['user_id']) || -1, + id: m["id"], + user_id: user_id_from_imported_user_id(m["user_id"]) || -1, topic_id: parent[:topic_id], - raw: process_simplepress_post(m['raw'], m['id']), - created_at: Time.zone.at(m['post_time']), + raw: process_simplepress_post(m["raw"], m["id"]), + created_at: Time.zone.at(m["post_time"]), } else - puts "Parent post #{m['topic_id']} doesn't exist. Skipping #{m["id"]}" + puts "Parent post #{m["topic_id"]} doesn't exist. Skipping #{m["id"]}" nil end end @@ -196,28 +208,27 @@ class ImportScripts::SimplePress < ImportScripts::Base s = raw.dup # fix invalid byte sequence in UTF-8 (ArgumentError) - unless s.valid_encoding? - s.force_encoding("UTF-8") - end + s.force_encoding("UTF-8") unless s.valid_encoding? # convert the quote line - s.gsub!(/\[quote='([^']+)'.*?pid='(\d+).*?\]/) { - "[quote=\"#{convert_username($1, import_id)}, " + post_id_to_post_num_and_topic($2, import_id) + '"]' - } + s.gsub!(/\[quote='([^']+)'.*?pid='(\d+).*?\]/) do + "[quote=\"#{convert_username($1, import_id)}, " + + post_id_to_post_num_and_topic($2, import_id) + '"]' + end # :) is encoded as :) s.gsub!(/(?:.*)/, '\1') # Some links look like this: http://www.onegameamonth.com - s.gsub!(/(.+)<\/a>/, '[\2](\1)') + s.gsub!(%r{(.+)}, '[\2](\1)') # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - s.gsub!(/:(?:\w{8})\]/, ']') + s.gsub!(/:(?:\w{8})\]/, "]") # Remove mybb video tags. - s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + s.gsub!(%r{(^\[video=.*?\])|(\[/video\]$)}, "") s = CGI.unescapeHTML(s) @@ -225,7 +236,7 @@ class ImportScripts::SimplePress < ImportScripts::Base # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # # Work around it for now: - s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + s.gsub!(%r{\[http(s)?://(www\.)?}, "[") s end @@ -233,7 +244,6 @@ class ImportScripts::SimplePress < ImportScripts::Base def mysql_query(sql) @client.query(sql, cache_rows: false) end - end ImportScripts::SimplePress.new.perform diff --git a/script/import_scripts/smf1.rb b/script/import_scripts/smf1.rb index 90ec314ea9..5d601f2fa2 100644 --- a/script/import_scripts/smf1.rb +++ b/script/import_scripts/smf1.rb @@ -5,21 +5,21 @@ require "htmlentities" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::Smf1 < ImportScripts::Base - - BATCH_SIZE ||= 5000 + BATCH_SIZE ||= 5000 UPLOADS_DIR ||= ENV["UPLOADS_DIR"].presence - FORUM_URL ||= ENV["FORUM_URL"].presence + FORUM_URL ||= ENV["FORUM_URL"].presence def initialize - fail "UPLOADS_DIR env variable is required (example: '/path/to/attachments')" unless UPLOADS_DIR + fail "UPLOADS_DIR env variable is required (example: '/path/to/attachments')" unless UPLOADS_DIR fail "FORUM_URL env variable is required (example: 'https://domain.com/forum')" unless FORUM_URL - @client = Mysql2::Client.new( - host: ENV["DB_HOST"] || "localhost", - username: ENV["DB_USER"] || "root", - password: ENV["DB_PW"], - database: ENV["DB_NAME"], - ) + @client = + Mysql2::Client.new( + host: ENV["DB_HOST"] || "localhost", + username: ENV["DB_USER"] || "root", + password: ENV["DB_PW"], + database: ENV["DB_NAME"], + ) check_version! @@ -29,7 +29,12 @@ class ImportScripts::Smf1 < ImportScripts::Base puts "Loading existing usernames..." - @old_to_new_usernames = UserCustomField.joins(:user).where(name: "import_username").pluck("value", "users.username").to_h + @old_to_new_usernames = + UserCustomField + .joins(:user) + .where(name: "import_username") + .pluck("value", "users.username") + .to_h puts "Loading pm mapping..." @@ -41,13 +46,14 @@ class ImportScripts::Smf1 < ImportScripts::Base .where("title NOT ILIKE 'Re: %'") .group(:id) .order(:id) - .pluck("string_agg(topic_allowed_users.user_id::text, ',' ORDER BY topic_allowed_users.user_id), title, topics.id") + .pluck( + "string_agg(topic_allowed_users.user_id::text, ',' ORDER BY topic_allowed_users.user_id), title, topics.id", + ) .each do |users, title, topic_id| - @pm_mapping[users] ||= {} - @pm_mapping[users][title] ||= [] - @pm_mapping[users][title] << topic_id - end - + @pm_mapping[users] ||= {} + @pm_mapping[users][title] ||= [] + @pm_mapping[users][title] << topic_id + end end def execute @@ -71,7 +77,10 @@ class ImportScripts::Smf1 < ImportScripts::Base end def check_version! - version = mysql_query("SELECT value FROM smf_settings WHERE variable = 'smfVersion' LIMIT 1").first["value"] + version = + mysql_query("SELECT value FROM smf_settings WHERE variable = 'smfVersion' LIMIT 1").first[ + "value" + ] fail "Incompatible version (#{version})" unless version&.start_with?("1.") end @@ -84,10 +93,7 @@ class ImportScripts::Smf1 < ImportScripts::Base create_groups(groups) do |g| next if g["groupName"].blank? - { - id: g["id_group"], - full_name: g["groupName"], - } + { id: g["id_group"], full_name: g["groupName"] } end end @@ -98,7 +104,7 @@ class ImportScripts::Smf1 < ImportScripts::Base total = mysql_query("SELECT COUNT(*) count FROM smf_members").first["count"] batches(BATCH_SIZE) do |offset| - users = mysql_query(<<~SQL + users = mysql_query(<<~SQL).to_a SELECT m.id_member , memberName , dateRegistered @@ -125,7 +131,6 @@ class ImportScripts::Smf1 < ImportScripts::Base ORDER BY m.id_member LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -158,38 +163,45 @@ class ImportScripts::Smf1 < ImportScripts::Base ip_address: u["memberIP2"], active: u["is_activated"] == 1, approved: u["is_activated"] == 1, - post_create_action: proc do |user| - # usernames - @old_to_new_usernames[u["memberName"]] = user.username + post_create_action: + proc do |user| + # usernames + @old_to_new_usernames[u["memberName"]] = user.username - # groups - GroupUser.transaction do - group_ids.each do |gid| - (group_id = group_id_from_imported_group_id(gid)) && GroupUser.find_or_create_by(user: user, group_id: group_id) + # groups + GroupUser.transaction do + group_ids.each do |gid| + (group_id = group_id_from_imported_group_id(gid)) && + GroupUser.find_or_create_by(user: user, group_id: group_id) + end end - end - # avatar - avatar_url = nil + # avatar + avatar_url = nil - if u["avatar"].present? - if u["avatar"].start_with?("http") - avatar_url = u["avatar"] - elsif u["avatar"].start_with?("avatar_") - avatar_url = "#{FORUM_URL}/avatar-members/#{u["avatar"]}" + if u["avatar"].present? + if u["avatar"].start_with?("http") + avatar_url = u["avatar"] + elsif u["avatar"].start_with?("avatar_") + avatar_url = "#{FORUM_URL}/avatar-members/#{u["avatar"]}" + end end - end - avatar_url ||= if u["attachmentType"] == 0 && u["id_attach"].present? - "#{FORUM_URL}/index.php?action=dlattach;attach=#{u["id_attach"]};type=avatar" - elsif u["attachmentType"] == 1 && u["filename"].present? - "#{FORUM_URL}/avatar-members/#{u["filename"]}" - end + avatar_url ||= + if u["attachmentType"] == 0 && u["id_attach"].present? + "#{FORUM_URL}/index.php?action=dlattach;attach=#{u["id_attach"]};type=avatar" + elsif u["attachmentType"] == 1 && u["filename"].present? + "#{FORUM_URL}/avatar-members/#{u["filename"]}" + end - if avatar_url.present? - UserAvatar.import_url_for_user(avatar_url, user) rescue nil - end - end + if avatar_url.present? + begin + UserAvatar.import_url_for_user(avatar_url, user) + rescue StandardError + nil + end + end + end, } end end @@ -198,7 +210,7 @@ class ImportScripts::Smf1 < ImportScripts::Base def import_categories puts "", "Importing categories..." - categories = mysql_query(<<~SQL + categories = mysql_query(<<~SQL).to_a SELECT id_board , id_parent , boardOrder @@ -207,7 +219,6 @@ class ImportScripts::Smf1 < ImportScripts::Base FROM smf_boards ORDER BY id_parent, id_board SQL - ).to_a parent_categories = categories.select { |c| c["id_parent"] == 0 } children_categories = categories.select { |c| c["id_parent"] != 0 } @@ -218,9 +229,13 @@ class ImportScripts::Smf1 < ImportScripts::Base name: c["name"], description: pre_process_raw(c["description"].presence), position: c["boardOrder"], - post_create_action: proc do |category| - Permalink.find_or_create_by(url: "forums/index.php/board,#{c["id_board"]}.0.html", category_id: category.id) - end, + post_create_action: + proc do |category| + Permalink.find_or_create_by( + url: "forums/index.php/board,#{c["id_board"]}.0.html", + category_id: category.id, + ) + end, } end @@ -231,9 +246,13 @@ class ImportScripts::Smf1 < ImportScripts::Base name: c["name"], description: pre_process_raw(c["description"].presence), position: c["boardOrder"], - post_create_action: proc do |category| - Permalink.find_or_create_by(url: "forums/index.php/board,#{c["id_board"]}.0.html", category_id: category.id) - end, + post_create_action: + proc do |category| + Permalink.find_or_create_by( + url: "forums/index.php/board,#{c["id_board"]}.0.html", + category_id: category.id, + ) + end, } end end @@ -245,7 +264,7 @@ class ImportScripts::Smf1 < ImportScripts::Base total = mysql_query("SELECT COUNT(*) count FROM smf_messages").first["count"] batches(BATCH_SIZE) do |offset| - posts = mysql_query(<<~SQL + posts = mysql_query(<<~SQL).to_a SELECT m.id_msg , m.id_topic , m.id_board @@ -262,7 +281,6 @@ class ImportScripts::Smf1 < ImportScripts::Base ORDER BY m.id_msg LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? @@ -287,12 +305,18 @@ class ImportScripts::Smf1 < ImportScripts::Base post[:views] = p["numViews"] post[:pinned_at] = created_at if p["isSticky"] == 1 post[:post_create_action] = proc do |pp| - Permalink.find_or_create_by(url: "forums/index.php/topic,#{p["id_topic"]}.0.html", topic_id: pp.topic_id) + Permalink.find_or_create_by( + url: "forums/index.php/topic,#{p["id_topic"]}.0.html", + topic_id: pp.topic_id, + ) end elsif parent = topic_lookup_from_imported_post_id(p["id_first_msg"]) post[:topic_id] = parent[:topic_id] post[:post_create_action] = proc do |pp| - Permalink.find_or_create_by(url: "forums/index.php/topic,#{p["id_topic"]}.msg#{p["id_msg"]}.html", post_id: pp.id) + Permalink.find_or_create_by( + url: "forums/index.php/topic,#{p["id_topic"]}.msg#{p["id_msg"]}.html", + post_id: pp.id, + ) end else next @@ -307,10 +331,15 @@ class ImportScripts::Smf1 < ImportScripts::Base puts "", "Importing personal posts..." last_post_id = -1 - total = mysql_query("SELECT COUNT(*) count FROM smf_personal_messages WHERE deletedBySender = 0").first["count"] + total = + mysql_query( + "SELECT COUNT(*) count FROM smf_personal_messages WHERE deletedBySender = 0", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - posts = mysql_query(<<~SQL + posts = mysql_query(<<~SQL).to_a SELECT id_pm , id_member_from , msgtime @@ -323,7 +352,6 @@ class ImportScripts::Smf1 < ImportScripts::Base ORDER BY id_pm LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? @@ -335,7 +363,8 @@ class ImportScripts::Smf1 < ImportScripts::Base create_posts(posts, total: total, offset: offset) do |p| next unless user_id = user_id_from_imported_user_id(p["id_member_from"]) next if p["recipients"].blank? - recipients = p["recipients"].split(",").map { |id| user_id_from_imported_user_id(id) }.compact.uniq + recipients = + p["recipients"].split(",").map { |id| user_id_from_imported_user_id(id) }.compact.uniq next if recipients.empty? id = "pm-#{p["id_pm"]}" @@ -385,10 +414,13 @@ class ImportScripts::Smf1 < ImportScripts::Base count = 0 last_upload_id = -1 - total = mysql_query("SELECT COUNT(*) count FROM smf_attachments WHERE id_msg IS NOT NULL").first["count"] + total = + mysql_query("SELECT COUNT(*) count FROM smf_attachments WHERE id_msg IS NOT NULL").first[ + "count" + ] batches(BATCH_SIZE) do |offset| - uploads = mysql_query(<<~SQL + uploads = mysql_query(<<~SQL).to_a SELECT id_attach , id_msg , filename @@ -399,7 +431,6 @@ class ImportScripts::Smf1 < ImportScripts::Base ORDER BY id_attach LIMIT #{BATCH_SIZE} SQL - ).to_a break if uploads.empty? @@ -408,7 +439,13 @@ class ImportScripts::Smf1 < ImportScripts::Base uploads.each do |u| count += 1 - next unless post = PostCustomField.joins(:post).find_by(name: "import_id", value: u["id_msg"].to_s)&.post + unless post = + PostCustomField + .joins(:post) + .find_by(name: "import_id", value: u["id_msg"].to_s) + &.post + next + end path = File.join(UPLOADS_DIR, "#{u["id_attach"]}_#{u["file_hash"]}") next unless File.exist?(path) && File.size(path) > 0 @@ -433,15 +470,25 @@ class ImportScripts::Smf1 < ImportScripts::Base puts "", "Importing likes..." count = 0 - total = mysql_query("SELECT COUNT(*) count FROM smf_thank_you_post WHERE thx_time > 0").first["count"] + total = + mysql_query("SELECT COUNT(*) count FROM smf_thank_you_post WHERE thx_time > 0").first["count"] like = PostActionType.types[:like] - mysql_query("SELECT id_msg, id_member, thx_time FROM smf_thank_you_post WHERE thx_time > 0 ORDER BY id_thx_post").each do |l| + mysql_query( + "SELECT id_msg, id_member, thx_time FROM smf_thank_you_post WHERE thx_time > 0 ORDER BY id_thx_post", + ).each do |l| print_status(count += 1, total, get_start_time("likes")) next unless post_id = post_id_from_imported_post_id(l["id_msg"]) next unless user_id = user_id_from_imported_user_id(l["id_member"]) - next if PostAction.where(post_action_type_id: like, post_id: post_id, user_id: user_id).exists? - PostAction.create(post_action_type_id: like, post_id: post_id, user_id: user_id, created_at: Time.at(l["thx_time"])) + if PostAction.where(post_action_type_id: like, post_id: post_id, user_id: user_id).exists? + next + end + PostAction.create( + post_action_type_id: like, + post_id: post_id, + user_id: user_id, + created_at: Time.at(l["thx_time"]), + ) end end @@ -457,7 +504,7 @@ class ImportScripts::Smf1 < ImportScripts::Base count = 0 total = mysql_query("SELECT COUNT(*) count FROM smf_feedback WHERE approved").first["count"] - mysql_query(<<~SQL + mysql_query(<<~SQL).each do |f| SELECT feedbackid , id_member , feedbackmember_id @@ -470,7 +517,6 @@ class ImportScripts::Smf1 < ImportScripts::Base WHERE approved ORDER BY feedbackid SQL - ).each do |f| print_status(count += 1, total, get_start_time("feedbacks")) next unless user_id_from = user_id_from_imported_user_id(f["feedbackmember_id"]) next unless user_id_to = user_id_from_imported_user_id(f["id_member"]) @@ -498,7 +544,10 @@ class ImportScripts::Smf1 < ImportScripts::Base puts "", "Importing banned email domains..." blocklist = SiteSetting.blocked_email_domains.split("|") - banned_domains = mysql_query("SELECT SUBSTRING(email_address, 3) domain FROM smf_ban_items WHERE email_address RLIKE '^%@[^%]+$' GROUP BY email_address").map { |r| r["domain"] } + banned_domains = + mysql_query( + "SELECT SUBSTRING(email_address, 3) domain FROM smf_ban_items WHERE email_address RLIKE '^%@[^%]+$' GROUP BY email_address", + ).map { |r| r["domain"] } SiteSetting.blocked_email_domains = (blocklist + banned_domains).uniq.sort.join("|") end @@ -508,7 +557,10 @@ class ImportScripts::Smf1 < ImportScripts::Base count = 0 - banned_emails = mysql_query("SELECT email_address FROM smf_ban_items WHERE email_address RLIKE '^[^%]+@[^%]+$' GROUP BY email_address").map { |r| r["email_address"] } + banned_emails = + mysql_query( + "SELECT email_address FROM smf_ban_items WHERE email_address RLIKE '^[^%]+@[^%]+$' GROUP BY email_address", + ).map { |r| r["email_address"] } banned_emails.each do |email| print_status(count += 1, banned_emails.size, get_start_time("banned_emails")) ScreenedEmail.find_or_create_by(email: email) @@ -520,7 +572,7 @@ class ImportScripts::Smf1 < ImportScripts::Base count = 0 - banned_ips = mysql_query(<<~SQL + banned_ips = mysql_query(<<~SQL).to_a SELECT CONCAT_WS('.', ip_low1, ip_low2, ip_low3, ip_low4) low , CONCAT_WS('.', ip_high1, ip_high2, ip_high3, ip_high4) high , hits @@ -528,7 +580,6 @@ class ImportScripts::Smf1 < ImportScripts::Base WHERE (ip_low1 + ip_low2 + ip_low3 + ip_low4 + ip_high1 + ip_high2 + ip_high3 + ip_high4) > 0 GROUP BY low, high, hits; SQL - ).to_a banned_ips.each do |r| print_status(count += 1, banned_ips.size, get_start_time("banned_ips")) @@ -537,15 +588,15 @@ class ImportScripts::Smf1 < ImportScripts::Base ScreenedIpAddress.create(ip_address: r["low"], match_count: r["hits"]) end else - low_values = r["low"].split(".").map(&:to_i) + low_values = r["low"].split(".").map(&:to_i) high_values = r["high"].split(".").map(&:to_i) - first_diff = low_values.zip(high_values).count { |a, b| a == b } + first_diff = low_values.zip(high_values).count { |a, b| a == b } first_diff -= 1 if low_values[first_diff] == 0 && high_values[first_diff] == 255 - prefix = low_values[0...first_diff] - suffix = [0] * (3 - first_diff) - mask = 8 * (first_diff + 1) - values = (low_values[first_diff]..high_values[first_diff]) - hits = (r["hits"] / [1, values.count].max).floor + prefix = low_values[0...first_diff] + suffix = [0] * (3 - first_diff) + mask = 8 * (first_diff + 1) + values = (low_values[first_diff]..high_values[first_diff]) + hits = (r["hits"] / [1, values.count].max).floor values.each do |v| range_values = prefix + [v] + suffix ip_address = "#{range_values.join(".")}/#{mask}" @@ -562,10 +613,28 @@ class ImportScripts::Smf1 < ImportScripts::Base ScreenedIpAddress.roll_up end - IGNORED_BBCODE ||= %w{ - black blue center color email flash font glow green iurl left list move red - right shadown size table time white - } + IGNORED_BBCODE ||= %w[ + black + blue + center + color + email + flash + font + glow + green + iurl + left + list + move + red + right + shadown + size + table + time + white + ] def pre_process_raw(raw) return "" if raw.blank? @@ -573,59 +642,59 @@ class ImportScripts::Smf1 < ImportScripts::Base raw = @htmlentities.decode(raw) # [acronym] - raw.gsub!(/\[acronym=([^\]]+)\](.*?)\[\/acronym\]/im) { %{#{$2}} } + raw.gsub!(%r{\[acronym=([^\]]+)\](.*?)\[/acronym\]}im) { %{#{$2}} } # [br] raw.gsub!(/\[br\]/i, "\n") - raw.gsub!(//i, "\n") + raw.gsub!(%r{}i, "\n") # [hr] raw.gsub!(/\[hr\]/i, "
    ") # [sub] - raw.gsub!(/\[sub\](.*?)\[\/sub\]/im) { "#{$1}" } + raw.gsub!(%r{\[sub\](.*?)\[/sub\]}im) { "#{$1}" } # [sup] - raw.gsub!(/\[sup\](.*?)\[\/sup\]/im) { "#{$1}" } + raw.gsub!(%r{\[sup\](.*?)\[/sup\]}im) { "#{$1}" } # [html] raw.gsub!(/\[html\]/i, "\n```html\n") - raw.gsub!(/\[\/html\]/i, "\n```\n") + raw.gsub!(%r{\[/html\]}i, "\n```\n") # [php] raw.gsub!(/\[php\]/i, "\n```php\n") - raw.gsub!(/\[\/php\]/i, "\n```\n") + raw.gsub!(%r{\[/php\]}i, "\n```\n") # [code] - raw.gsub!(/\[\/?code\]/i, "\n```\n") + raw.gsub!(%r{\[/?code\]}i, "\n```\n") # [pre] - raw.gsub!(/\[\/?pre\]/i, "\n```\n") + raw.gsub!(%r{\[/?pre\]}i, "\n```\n") # [tt] - raw.gsub!(/\[\/?tt\]/i, "`") + raw.gsub!(%r{\[/?tt\]}i, "`") # [ftp] raw.gsub!(/\[ftp/i, "[url") - raw.gsub!(/\[\/ftp\]/i, "[/url]") + raw.gsub!(%r{\[/ftp\]}i, "[/url]") # [me] - raw.gsub!(/\[me=([^\]]*)\](.*?)\[\/me\]/im) { "_\\* #{$1} #{$2}_" } + raw.gsub!(%r{\[me=([^\]]*)\](.*?)\[/me\]}im) { "_\\* #{$1} #{$2}_" } # [li] - raw.gsub!(/\[li\](.*?)\[\/li\]/im) { "- #{$1}" } + raw.gsub!(%r{\[li\](.*?)\[/li\]}im) { "- #{$1}" } # puts [img] on their own line - raw.gsub!(/\[img[^\]]*\](.*?)\[\/img\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[img[^\]]*\](.*?)\[/img\]}im) { "\n#{$1}\n" } # puts [youtube] on their own line - raw.gsub!(/\[youtube\](.*?)\[\/youtube\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[youtube\](.*?)\[/youtube\]}im) { "\n#{$1}\n" } - IGNORED_BBCODE.each { |code| raw.gsub!(/\[#{code}[^\]]*\](.*?)\[\/#{code}\]/im, '\1') } + IGNORED_BBCODE.each { |code| raw.gsub!(%r{\[#{code}[^\]]*\](.*?)\[/#{code}\]}im, '\1') } # ensure [/quote] are on their own line - raw.gsub!(/\s*\[\/quote\]\s*/im, "\n[/quote]\n") + raw.gsub!(%r{\s*\[/quote\]\s*}im, "\n[/quote]\n") # [quote] - raw.gsub!(/\s*\[quote (.+?)\]\s/im) { + raw.gsub!(/\s*\[quote (.+?)\]\s/im) do params = $1 post_id = params[/msg(\d+)/, 1] username = params[/author=(.+) link=/, 1] @@ -636,14 +705,14 @@ class ImportScripts::Smf1 < ImportScripts::Base else %{\n[quote="#{username}"]\n} end - } + end # remove tapatalk mess - raw.gsub!(/Sent from .+? using \[url=.*?\].+?\[\/url\]/i, "") + raw.gsub!(%r{Sent from .+? using \[url=.*?\].+?\[/url\]}i, "") raw.gsub!(/Sent from .+? using .+?\z/i, "") # clean URLs - raw.gsub!(/\[url=(.+?)\]\1\[\/url\]/i, '\1') + raw.gsub!(%r{\[url=(.+?)\]\1\[/url\]}i, '\1') raw end @@ -651,7 +720,6 @@ class ImportScripts::Smf1 < ImportScripts::Base def mysql_query(sql) @client.query(sql) end - end ImportScripts::Smf1.new.perform diff --git a/script/import_scripts/smf2.rb b/script/import_scripts/smf2.rb index a70faff4cb..97eb20a92b 100644 --- a/script/import_scripts/smf2.rb +++ b/script/import_scripts/smf2.rb @@ -1,18 +1,17 @@ # coding: utf-8 # frozen_string_literal: true -require 'mysql2' -require File.expand_path(File.dirname(__FILE__) + '/base.rb') +require "mysql2" +require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' -require 'tsort' -require 'set' -require 'optparse' -require 'etc' -require 'open3' +require "htmlentities" +require "tsort" +require "set" +require "optparse" +require "etc" +require "open3" class ImportScripts::Smf2 < ImportScripts::Base - def self.run options = Options.new begin @@ -54,9 +53,9 @@ class ImportScripts::Smf2 < ImportScripts::Base exit 1 end if options.password == :ask - require 'highline' + require "highline" $stderr.print "Enter password for MySQL database `#{options.database}`: " - options.password = HighLine.new.ask('') { |q| q.echo = false } + options.password = HighLine.new.ask("") { |q| q.echo = false } end @default_db_connection = create_db_connection @@ -68,11 +67,11 @@ class ImportScripts::Smf2 < ImportScripts::Base import_categories import_posts postprocess_posts - make_prettyurl_permalinks('/forum') + make_prettyurl_permalinks("/forum") end def import_groups - puts '', 'creating groups' + puts "", "creating groups" total = query(<<-SQL, as: :single) SELECT COUNT(*) FROM {prefix}membergroups @@ -92,7 +91,7 @@ class ImportScripts::Smf2 < ImportScripts::Base MODERATORS_GROUP = 2 def import_users - puts '', 'creating users' + puts "", "creating users" total = query("SELECT COUNT(*) FROM {prefix}members", as: :single) create_users(query(<<-SQL), total: total) do |member| @@ -103,10 +102,25 @@ class ImportScripts::Smf2 < ImportScripts::Base FROM {prefix}members AS a LEFT JOIN {prefix}attachments AS b ON a.id_member = b.id_member SQL - group_ids = [ member[:id_group], *member[:additional_groups].split(',').map(&:to_i) ] - create_time = Time.zone.at(member[:date_registered]) rescue Time.now - last_seen_time = Time.zone.at(member[:last_login]) rescue nil - ip_addr = IPAddr.new(member[:member_ip]) rescue nil + group_ids = [member[:id_group], *member[:additional_groups].split(",").map(&:to_i)] + create_time = + begin + Time.zone.at(member[:date_registered]) + rescue StandardError + Time.now + end + last_seen_time = + begin + Time.zone.at(member[:last_login]) + rescue StandardError + nil + end + ip_addr = + begin + IPAddr.new(member[:member_ip]) + rescue StandardError + nil + end { id: member[:id_member], username: member[:member_name], @@ -121,27 +135,33 @@ class ImportScripts::Smf2 < ImportScripts::Base ip_address: ip_addr, admin: group_ids.include?(ADMIN_GROUP), moderator: group_ids.include?(MODERATORS_GROUP), - - post_create_action: proc do |user| - user.update(created_at: create_time) if create_time < user.created_at - user.save - GroupUser.transaction do - group_ids.each do |gid| - (group_id = group_id_from_imported_group_id(gid)) && - GroupUser.find_or_create_by(user: user, group_id: group_id) - end - end - if options.smfroot && member[:id_attach].present? && user.uploaded_avatar_id.blank? - (path = find_smf_attachment_path(member[:id_attach], member[:file_hash], member[:filename])) && begin - upload = create_upload(user.id, path, member[:filename]) - if upload.persisted? - user.update(uploaded_avatar_id: upload.id) + post_create_action: + proc do |user| + user.update(created_at: create_time) if create_time < user.created_at + user.save + GroupUser.transaction do + group_ids.each do |gid| + (group_id = group_id_from_imported_group_id(gid)) && + GroupUser.find_or_create_by(user: user, group_id: group_id) end - rescue SystemCallError => err - puts "Could not import avatar: #{err.message}" end - end - end + if options.smfroot && member[:id_attach].present? && user.uploaded_avatar_id.blank? + ( + path = + find_smf_attachment_path( + member[:id_attach], + member[:file_hash], + member[:filename], + ) + ) && + begin + upload = create_upload(user.id, path, member[:filename]) + user.update(uploaded_avatar_id: upload.id) if upload.persisted? + rescue SystemCallError => err + puts "Could not import avatar: #{err.message}" + end + end + end, } end end @@ -155,38 +175,39 @@ class ImportScripts::Smf2 < ImportScripts::Base parent_id = category_id_from_imported_category_id(board[:id_parent]) if board[:id_parent] > 0 groups = (board[:member_groups] || "").split(/,/).map(&:to_i) restricted = !groups.include?(GUEST_GROUP) && !groups.include?(MEMBER_GROUP) - if Category.find_by_name(board[:name]) - board[:name] += board[:id_board].to_s - end + board[:name] += board[:id_board].to_s if Category.find_by_name(board[:name]) { id: board[:id_board], name: board[:name], description: board[:description], parent_category_id: parent_id, - post_create_action: restricted && proc do |category| - category.update(read_restricted: true) - groups.each do |imported_group_id| - (group_id = group_id_from_imported_group_id(imported_group_id)) && - CategoryGroup.find_or_create_by(category: category, group_id: group_id) do |cg| - cg.permission_type = CategoryGroup.permission_types[:full] - end - end - end, + post_create_action: + restricted && + proc do |category| + category.update(read_restricted: true) + groups.each do |imported_group_id| + (group_id = group_id_from_imported_group_id(imported_group_id)) && + CategoryGroup.find_or_create_by(category: category, group_id: group_id) do |cg| + cg.permission_type = CategoryGroup.permission_types[:full] + end + end + end, } end end def import_posts - puts '', 'creating posts' - spinner = %w(/ - \\ |).cycle + puts "", "creating posts" + spinner = %w[/ - \\ |].cycle total = query("SELECT COUNT(*) FROM {prefix}messages", as: :single) PostCreator.class_eval do def guardian - @guardian ||= if opts[:import_mode] - @@system_guardian ||= Guardian.new(Discourse.system_user) - else - Guardian.new(@user) - end + @guardian ||= + if opts[:import_mode] + @@system_guardian ||= Guardian.new(Discourse.system_user) + else + Guardian.new(@user) + end end end @@ -208,10 +229,12 @@ class ImportScripts::Smf2 < ImportScripts::Base id: message[:id_msg], user_id: user_id_from_imported_user_id(message[:id_member]) || -1, created_at: Time.zone.at(message[:poster_time]), - post_create_action: ignore_quotes && proc do |p| - p.custom_fields['import_rebake'] = 't' - p.save - end + post_create_action: + ignore_quotes && + proc do |p| + p.custom_fields["import_rebake"] = "t" + p.save + end, } if message[:id_msg] == message[:id_first_msg] @@ -228,31 +251,48 @@ class ImportScripts::Smf2 < ImportScripts::Base end next nil if skip - attachments = message[:attachment_count] == 0 ? [] : query(<<-SQL, connection: db2, as: :array) + attachments = + message[:attachment_count] == 0 ? [] : query(<<-SQL, connection: db2, as: :array) SELECT id_attach, file_hash, filename FROM {prefix}attachments WHERE attachment_type = 0 AND id_msg = #{message[:id_msg]} ORDER BY id_attach ASC SQL - attachments.map! { |a| import_attachment(post, a) rescue (puts $! ; nil) } + attachments.map! do |a| + begin + import_attachment(post, a) + rescue StandardError + ( + puts $! + nil + ) + end + end post[:raw] = convert_message_body(message[:body], attachments, ignore_quotes: ignore_quotes) next post end end def import_attachment(post, attachment) - path = find_smf_attachment_path(attachment[:id_attach], attachment[:file_hash], attachment[:filename]) + path = + find_smf_attachment_path( + attachment[:id_attach], + attachment[:file_hash], + attachment[:filename], + ) raise "Attachment for post #{post[:id]} failed: #{attachment[:filename]}" unless path.present? upload = create_upload(post[:user_id], path, attachment[:filename]) - raise "Attachment for post #{post[:id]} failed: #{upload.errors.full_messages.join(', ')}" unless upload.persisted? + unless upload.persisted? + raise "Attachment for post #{post[:id]} failed: #{upload.errors.full_messages.join(", ")}" + end upload rescue SystemCallError => err raise "Attachment for post #{post[:id]} failed: #{err.message}" end def postprocess_posts - puts '', 'rebaking posts' + puts "", "rebaking posts" - tags = PostCustomField.where(name: 'import_rebake', value: 't') + tags = PostCustomField.where(name: "import_rebake", value: "t") tags_total = tags.count tags_done = 0 @@ -271,38 +311,47 @@ class ImportScripts::Smf2 < ImportScripts::Base private def create_db_connection - Mysql2::Client.new(host: options.host, username: options.username, - password: options.password, database: options.database) + Mysql2::Client.new( + host: options.host, + username: options.username, + password: options.password, + database: options.database, + ) end def query(sql, **opts, &block) db = opts[:connection] || @default_db_connection - return __query(db, sql).to_a if opts[:as] == :array - return __query(db, sql, as: :array).first[0] if opts[:as] == :single + return __query(db, sql).to_a if opts[:as] == :array + return __query(db, sql, as: :array).first[0] if opts[:as] == :single return __query(db, sql, stream: true).each(&block) if block_given? __query(db, sql, stream: true) end def __query(db, sql, **opts) - db.query(sql.gsub('{prefix}', options.prefix), - { symbolize_keys: true, cache_rows: false }.merge(opts)) + db.query( + sql.gsub("{prefix}", options.prefix), + { symbolize_keys: true, cache_rows: false }.merge(opts), + ) end - TRTR_TABLE = begin - from = "ŠŽšžŸÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ" - to = "SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy" - from.chars.zip(to.chars) - end + TRTR_TABLE = + begin + from = "ŠŽšžŸÀÁÂÃÄÅÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØÙÚÛÜÝàáâãäåçèéêëìíîïñòóôõöøùúûüýÿ" + to = "SZszYAAAAAACEEEEIIIINOOOOOOUUUUYaaaaaaceeeeiiiinoooooouuuuyy" + from.chars.zip(to.chars) + end def find_smf_attachment_path(attachment_id, file_hash, filename) cleaned_name = filename.dup TRTR_TABLE.each { |from, to| cleaned_name.gsub!(from, to) } - cleaned_name.gsub!(/\s/, '_') - cleaned_name.gsub!(/[^\w_\.\-]/, '') - legacy_name = "#{attachment_id}_#{cleaned_name.gsub('.', '_')}#{Digest::MD5.hexdigest(cleaned_name)}" + cleaned_name.gsub!(/\s/, "_") + cleaned_name.gsub!(/[^\w_\.\-]/, "") + legacy_name = + "#{attachment_id}_#{cleaned_name.gsub(".", "_")}#{Digest::MD5.hexdigest(cleaned_name)}" - [ filename, "#{attachment_id}_#{file_hash}", legacy_name ] - .map { |name| File.join(options.smfroot, 'attachments', name) } + [filename, "#{attachment_id}_#{file_hash}", legacy_name].map do |name| + File.join(options.smfroot, "attachments", name) + end .detect { |file| File.exist?(file) } end @@ -311,16 +360,16 @@ class ImportScripts::Smf2 < ImportScripts::Base end def convert_message_body(body, attachments = [], **opts) - body = decode_entities(body.gsub(//, "\n")) + body = decode_entities(body.gsub(%r{}, "\n")) body.gsub!(ColorPattern, '\k') body.gsub!(ListPattern) do |s| params = parse_tag_params($~[:params]) - tag = params['type'] == 'decimal' ? 'ol' : 'ul' + tag = params["type"] == "decimal" ? "ol" : "ul" "\n[#{tag}]#{$~[:inner].strip}[/#{tag}]\n" end body.gsub!(XListPattern) do |s| r = +"\n[ul]" - s.lines.each { |l| "#{r}[li]#{l.strip.sub(/^\[x\]\s*/, '')}[/li]" } + s.lines.each { |l| "#{r}[li]#{l.strip.sub(/^\[x\]\s*/, "")}[/li]" } "#{r}[/ul]\n" end @@ -338,9 +387,7 @@ class ImportScripts::Smf2 < ImportScripts::Base if use_count.keys.length < attachments.select(&:present?).length body = "#{body}\n\n---" attachments.each_with_index do |upload, num| - if upload.present? && use_count[num] == (0) - "#{body}\n\n#{get_upload_markdown(upload)}" - end + "#{body}\n\n#{get_upload_markdown(upload)}" if upload.present? && use_count[num] == (0) end end end @@ -353,26 +400,46 @@ class ImportScripts::Smf2 < ImportScripts::Base end def convert_quotes(body) - body.to_s.gsub(QuotePattern) do |s| - inner = $~[:inner].strip - params = parse_tag_params($~[:params]) - if params['author'].present? - quote = +"\n[quote=\"#{params['author']}" - if QuoteParamsPattern =~ params['link'] - tl = topic_lookup_from_imported_post_id($~[:msg].to_i) - quote = "#{quote} post:#{tl[:post_number]}, topic:#{tl[:topic_id]}" if tl + body + .to_s + .gsub(QuotePattern) do |s| + inner = $~[:inner].strip + params = parse_tag_params($~[:params]) + if params["author"].present? + quote = +"\n[quote=\"#{params["author"]}" + if QuoteParamsPattern =~ params["link"] + tl = topic_lookup_from_imported_post_id($~[:msg].to_i) + quote = "#{quote} post:#{tl[:post_number]}, topic:#{tl[:topic_id]}" if tl + end + quote = "#{quote}\"]\n#{convert_quotes(inner)}\n[/quote]" + else + "
    #{convert_quotes(inner)}
    " end - quote = "#{quote}\"]\n#{convert_quotes(inner)}\n[/quote]" - else - "
    #{convert_quotes(inner)}
    " end - end end - IGNORED_BBCODE ||= %w{ - black blue center color email flash font glow green iurl left list move red - right shadown size table time white - } + IGNORED_BBCODE ||= %w[ + black + blue + center + color + email + flash + font + glow + green + iurl + left + list + move + red + right + shadown + size + table + time + white + ] def convert_bbcode(raw) return "" if raw.blank? @@ -380,67 +447,67 @@ class ImportScripts::Smf2 < ImportScripts::Base raw = convert_quotes(raw) # [acronym] - raw.gsub!(/\[acronym=([^\]]+)\](.*?)\[\/acronym\]/im) { %{#{$2}} } + raw.gsub!(%r{\[acronym=([^\]]+)\](.*?)\[/acronym\]}im) { %{#{$2}} } # [br] raw.gsub!(/\[br\]/i, "\n") - raw.gsub!(//i, "\n") + raw.gsub!(%r{}i, "\n") # [hr] raw.gsub!(/\[hr\]/i, "
    ") # [sub] - raw.gsub!(/\[sub\](.*?)\[\/sub\]/im) { "#{$1}" } + raw.gsub!(%r{\[sub\](.*?)\[/sub\]}im) { "#{$1}" } # [sup] - raw.gsub!(/\[sup\](.*?)\[\/sup\]/im) { "#{$1}" } + raw.gsub!(%r{\[sup\](.*?)\[/sup\]}im) { "#{$1}" } # [html] raw.gsub!(/\[html\]/i, "\n```html\n") - raw.gsub!(/\[\/html\]/i, "\n```\n") + raw.gsub!(%r{\[/html\]}i, "\n```\n") # [php] raw.gsub!(/\[php\]/i, "\n```php\n") - raw.gsub!(/\[\/php\]/i, "\n```\n") + raw.gsub!(%r{\[/php\]}i, "\n```\n") # [code] - raw.gsub!(/\[\/?code\]/i, "\n```\n") + raw.gsub!(%r{\[/?code\]}i, "\n```\n") # [pre] - raw.gsub!(/\[\/?pre\]/i, "\n```\n") + raw.gsub!(%r{\[/?pre\]}i, "\n```\n") # [tt] - raw.gsub!(/\[\/?tt\]/i, "`") + raw.gsub!(%r{\[/?tt\]}i, "`") # [ftp] raw.gsub!(/\[ftp/i, "[url") - raw.gsub!(/\[\/ftp\]/i, "[/url]") + raw.gsub!(%r{\[/ftp\]}i, "[/url]") # [me] - raw.gsub!(/\[me=([^\]]*)\](.*?)\[\/me\]/im) { "_\\* #{$1} #{$2}_" } + raw.gsub!(%r{\[me=([^\]]*)\](.*?)\[/me\]}im) { "_\\* #{$1} #{$2}_" } # [ul] raw.gsub!(/\[ul\]/i, "") - raw.gsub!(/\[\/ul\]/i, "") + raw.gsub!(%r{\[/ul\]}i, "") # [li] - raw.gsub!(/\[li\](.*?)\[\/li\]/im) { "- #{$1}" } + raw.gsub!(%r{\[li\](.*?)\[/li\]}im) { "- #{$1}" } # puts [img] on their own line - raw.gsub!(/\[img[^\]]*\](.*?)\[\/img\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[img[^\]]*\](.*?)\[/img\]}im) { "\n#{$1}\n" } # puts [youtube] on their own line - raw.gsub!(/\[youtube\](.*?)\[\/youtube\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[youtube\](.*?)\[/youtube\]}im) { "\n#{$1}\n" } - IGNORED_BBCODE.each { |code| raw.gsub!(/\[#{code}[^\]]*\](.*?)\[\/#{code}\]/im, '\1') } + IGNORED_BBCODE.each { |code| raw.gsub!(%r{\[#{code}[^\]]*\](.*?)\[/#{code}\]}im, '\1') } # ensure [/quote] are on their own line - raw.gsub!(/\s*\[\/quote\]\s*/im, "\n[/quote]\n") + raw.gsub!(%r{\s*\[/quote\]\s*}im, "\n[/quote]\n") # remove tapatalk mess - raw.gsub!(/Sent from .+? using \[url=.*?\].+?\[\/url\]/i, "") + raw.gsub!(%r{Sent from .+? using \[url=.*?\].+?\[/url\]}i, "") raw.gsub!(/Sent from .+? using .+?\z/i, "") # clean URLs - raw.gsub!(/\[url=(.+?)\]\1\[\/url\]/i, '\1') + raw.gsub!(%r{\[url=(.+?)\]\1\[/url\]}i, '\1') raw end @@ -460,8 +527,14 @@ class ImportScripts::Smf2 < ImportScripts::Base # param1=value1=still1 value1 param2=value2 ... # => {'param1' => 'value1=still1 value1', 'param2' => 'value2 ...'} def parse_tag_params(params) - params.to_s.strip.scan(/(?\w+)=(?(?:(?>\S+)|\s+(?!\w+=))*)/). - inject({}) { |h, e| h[e[0]] = e[1]; h } + params + .to_s + .strip + .scan(/(?\w+)=(?(?:(?>\S+)|\s+(?!\w+=))*)/) + .inject({}) do |h, e| + h[e[0]] = e[1] + h + end end class << self @@ -474,8 +547,8 @@ class ImportScripts::Smf2 < ImportScripts::Base # => match[:params] == 'param=value param2=value2' # match[:inner] == "\n text\n [tag nested=true]text[/tag]\n" def build_nested_tag_regex(ltag, rtag = nil) - rtag ||= '/' + ltag - %r{ + rtag ||= "/" + ltag + / \[#{ltag}(?-x:[ =](?[^\]]*))?\] # consume open tag, followed by... (?(?: (?> [^\[]+ ) # non-tags, or... @@ -495,40 +568,41 @@ class ImportScripts::Smf2 < ImportScripts::Base ) )*) \[#{rtag}\] - }x + /x end end QuoteParamsPattern = /^topic=(?\d+).msg(?\d+)#msg\k$/ XListPattern = /(?(?>^\[x\]\s*(?.*)$\n?)+)/ - QuotePattern = build_nested_tag_regex('quote') - ColorPattern = build_nested_tag_regex('color') - ListPattern = build_nested_tag_regex('list') + QuotePattern = build_nested_tag_regex("quote") + ColorPattern = build_nested_tag_regex("color") + ListPattern = build_nested_tag_regex("list") AttachmentPatterns = [ [/^\[attach(?:|img|url|mini)=(?\d+)\]$/, ->(u) { "\n" + get_upload_markdown(u) + "\n" }], - [/\[attach(?:|img|url|mini)=(?\d+)\]/, ->(u) { get_upload_markdown(u) }] + [/\[attach(?:|img|url|mini)=(?\d+)\]/, ->(u) { get_upload_markdown(u) }], ] # Provides command line options and parses the SMF settings file. class Options - - class Error < StandardError ; end - class SettingsError < Error ; end + class Error < StandardError + end + class SettingsError < Error + end def parse!(args = ARGV) - raise Error, 'not enough arguments' if ARGV.empty? + raise Error, "not enough arguments" if ARGV.empty? begin parser.parse!(args) rescue OptionParser::ParseError => err raise Error, err.message end - raise Error, 'too many arguments' if args.length > 1 + raise Error, "too many arguments" if args.length > 1 self.smfroot = args.first read_smf_settings if self.smfroot - self.host ||= 'localhost' + self.host ||= "localhost" self.username ||= Etc.getlogin - self.prefix ||= 'smf_' + self.prefix ||= "smf_" self.timezone ||= get_php_timezone end @@ -547,44 +621,63 @@ class ImportScripts::Smf2 < ImportScripts::Base private def get_php_timezone - phpinfo, status = Open3.capture2('php', '-i') + phpinfo, status = Open3.capture2("php", "-i") phpinfo.lines.each do |line| - key, *vals = line.split(' => ').map(&:strip) - break vals[0] if key == 'Default timezone' + key, *vals = line.split(" => ").map(&:strip) + break vals[0] if key == "Default timezone" end rescue Errno::ENOENT $stderr.puts "Error: PHP CLI executable not found" end def read_smf_settings - settings = File.join(self.smfroot, 'Settings.php') - File.readlines(settings).each do |line| - next unless m = /\$([a-z_]+)\s*=\s*['"](.+?)['"]\s*;\s*((#|\/\/).*)?$/.match(line) - case m[1] - when 'db_server' then self.host ||= m[2] - when 'db_user' then self.username ||= m[2] - when 'db_passwd' then self.password ||= m[2] - when 'db_name' then self.database ||= m[2] - when 'db_prefix' then self.prefix ||= m[2] + settings = File.join(self.smfroot, "Settings.php") + File + .readlines(settings) + .each do |line| + next unless m = %r{\$([a-z_]+)\s*=\s*['"](.+?)['"]\s*;\s*((#|//).*)?$}.match(line) + case m[1] + when "db_server" + self.host ||= m[2] + when "db_user" + self.username ||= m[2] + when "db_passwd" + self.password ||= m[2] + when "db_name" + self.database ||= m[2] + when "db_prefix" + self.prefix ||= m[2] + end end - end rescue => err raise SettingsError, err.message unless self.database end def parser - @parser ||= OptionParser.new(nil, 12) do |o| - o.banner = "Usage:\t#{File.basename($0)} [options]\n" - o.banner = "${o.banner}\t#{File.basename($0)} -d [options]" - o.on('-h HOST', :REQUIRED, "MySQL server hostname [\"#{self.host}\"]") { |s| self.host = s } - o.on('-u USER', :REQUIRED, "MySQL username [\"#{self.username}\"]") { |s| self.username = s } - o.on('-p [PASS]', :OPTIONAL, 'MySQL password. Without argument, reads password from STDIN.') { |s| self.password = s || :ask } - o.on('-d DBNAME', :REQUIRED, 'Name of SMF database') { |s| self.database = s } - o.on('-f PREFIX', :REQUIRED, "Table names prefix [\"#{self.prefix}\"]") { |s| self.prefix = s } - o.on('-t TIMEZONE', :REQUIRED, 'Timezone used by SMF2 [auto-detected from PHP]') { |s| self.timezone = s } - end + @parser ||= + OptionParser.new(nil, 12) do |o| + o.banner = "Usage:\t#{File.basename($0)} [options]\n" + o.banner = "${o.banner}\t#{File.basename($0)} -d [options]" + o.on("-h HOST", :REQUIRED, "MySQL server hostname [\"#{self.host}\"]") do |s| + self.host = s + end + o.on("-u USER", :REQUIRED, "MySQL username [\"#{self.username}\"]") do |s| + self.username = s + end + o.on( + "-p [PASS]", + :OPTIONAL, + "MySQL password. Without argument, reads password from STDIN.", + ) { |s| self.password = s || :ask } + o.on("-d DBNAME", :REQUIRED, "Name of SMF database") { |s| self.database = s } + o.on("-f PREFIX", :REQUIRED, "Table names prefix [\"#{self.prefix}\"]") do |s| + self.prefix = s + end + o.on("-t TIMEZONE", :REQUIRED, "Timezone used by SMF2 [auto-detected from PHP]") do |s| + self.timezone = s + end + end end - end #Options # Framework around TSort, used to build a dependency graph over messages @@ -644,10 +737,14 @@ class ImportScripts::Smf2 < ImportScripts::Base end def dependencies - @dependencies ||= Set.new.tap do |deps| - deps.merge(quoted) unless ignore_quotes? - deps << prev if prev.present? - end.to_a + @dependencies ||= + Set + .new + .tap do |deps| + deps.merge(quoted) unless ignore_quotes? + deps << prev if prev.present? + end + .to_a end def hash @@ -659,7 +756,7 @@ class ImportScripts::Smf2 < ImportScripts::Base end def inspect - "#<#{self.class.name}: id=#{id.inspect}, prev=#{safe_id(@prev)}, quoted=[#{@quoted.map(&method(:safe_id)).join(', ')}]>" + "#<#{self.class.name}: id=#{id.inspect}, prev=#{safe_id(@prev)}, quoted=[#{@quoted.map(&method(:safe_id)).join(", ")}]>" end private @@ -668,11 +765,10 @@ class ImportScripts::Smf2 < ImportScripts::Base @graph[id].present? ? @graph[id].id.inspect : "(#{id})" end end #Node - end #MessageDependencyGraph def make_prettyurl_permalinks(prefix) - puts 'creating permalinks for prettyurl plugin' + puts "creating permalinks for prettyurl plugin" begin serialized = query(<<-SQL, as: :single) SELECT value FROM {prefix}settings @@ -680,9 +776,7 @@ class ImportScripts::Smf2 < ImportScripts::Base SQL board_slugs = Array.new ser = /\{(.*)\}/.match(serialized)[1] - ser.scan(/i:(\d+);s:\d+:\"(.*?)\";/).each do |nv| - board_slugs[nv[0].to_i] = nv[1] - end + ser.scan(/i:(\d+);s:\d+:\"(.*?)\";/).each { |nv| board_slugs[nv[0].to_i] = nv[1] } topic_urls = query(<<-SQL, as: :array) SELECT t.id_first_msg, t.id_board,u.pretty_url FROM smf_topics t @@ -690,12 +784,14 @@ class ImportScripts::Smf2 < ImportScripts::Base SQL topic_urls.each do |url| t = topic_lookup_from_imported_post_id(url[:id_first_msg]) - Permalink.create(url: "#{prefix}/#{board_slugs[url[:id_board]]}/#{url[:pretty_url]}", topic_id: t[:topic_id]) + Permalink.create( + url: "#{prefix}/#{board_slugs[url[:id_board]]}/#{url[:pretty_url]}", + topic_id: t[:topic_id], + ) end - rescue + rescue StandardError end end - end ImportScripts::Smf2.run diff --git a/script/import_scripts/socialcast/create_title.rb b/script/import_scripts/socialcast/create_title.rb index 8af625eddb..ea656fadb7 100644 --- a/script/import_scripts/socialcast/create_title.rb +++ b/script/import_scripts/socialcast/create_title.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'uri' +require "uri" class CreateTitle - def self.from_body(body) title = remove_mentions body title = remove_urls title @@ -24,11 +23,11 @@ class CreateTitle private def self.remove_mentions(text) - text.gsub(/@[\w]*/, '') + text.gsub(/@[\w]*/, "") end def self.remove_urls(text) - text.gsub(URI::regexp(['http', 'https', 'mailto', 'ftp', 'ldap', 'ldaps']), '') + text.gsub(URI.regexp(%w[http https mailto ftp ldap ldaps]), "") end def self.remove_stray_punctuation(text) @@ -42,7 +41,7 @@ class CreateTitle end def self.complete_sentences(text) - /(^.*[\S]{2,}[.!?:]+)\W/.match(text[0...80] + ' ') + /(^.*[\S]{2,}[.!?:]+)\W/.match(text[0...80] + " ") end def self.complete_words(text) diff --git a/script/import_scripts/socialcast/export.rb b/script/import_scripts/socialcast/export.rb index 1c44c7c5c9..ac3a4690c1 100644 --- a/script/import_scripts/socialcast/export.rb +++ b/script/import_scripts/socialcast/export.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'yaml' -require 'fileutils' -require_relative 'socialcast_api' +require "yaml" +require "fileutils" +require_relative "socialcast_api" def load_config(file) - config = YAML::load_file(File.join(__dir__, file)) - @domain = config['domain'] - @username = config['username'] - @password = config['password'] + config = YAML.load_file(File.join(__dir__, file)) + @domain = config["domain"] + @username = config["username"] + @password = config["password"] end def export @@ -23,8 +23,8 @@ def export_users(page = 1) users = @api.list_users(page: page) return if users.empty? users.each do |user| - File.open("output/users/#{user['id']}.json", 'w') do |f| - puts user['contact_info']['email'] + File.open("output/users/#{user["id"]}.json", "w") do |f| + puts user["contact_info"]["email"] f.write user.to_json f.close end @@ -36,12 +36,12 @@ def export_messages(page = 1) messages = @api.list_messages(page: page) return if messages.empty? messages.each do |message| - File.open("output/messages/#{message['id']}.json", 'w') do |f| - title = message['title'] - title = message['body'] if title.empty? + File.open("output/messages/#{message["id"]}.json", "w") do |f| + title = message["title"] + title = message["body"] if title.empty? title = title.split('\n')[0][0..50] unless title.empty? - puts "#{message['id']}: #{title}" + puts "#{message["id"]}: #{title}" f.write message.to_json f.close end @@ -51,9 +51,7 @@ end def create_dir(path) path = File.join(__dir__, path) - unless File.directory?(path) - FileUtils.mkdir_p(path) - end + FileUtils.mkdir_p(path) unless File.directory?(path) end load_config ARGV.shift diff --git a/script/import_scripts/socialcast/import.rb b/script/import_scripts/socialcast/import.rb index 413fd18ff8..c20237f66c 100644 --- a/script/import_scripts/socialcast/import.rb +++ b/script/import_scripts/socialcast/import.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require_relative './socialcast_message.rb' -require_relative './socialcast_user.rb' -require 'set' +require_relative "./socialcast_message.rb" +require_relative "./socialcast_user.rb" +require "set" require File.expand_path(File.dirname(__FILE__) + "/../base.rb") class ImportScripts::Socialcast < ImportScripts::Base - MESSAGES_DIR = "output/messages" USERS_DIR = "output/users" @@ -29,15 +28,13 @@ class ImportScripts::Socialcast < ImportScripts::Base imported = 0 total = count_files(MESSAGES_DIR) Dir.foreach(MESSAGES_DIR) do |filename| - next if filename == ('.') || filename == ('..') + next if filename == (".") || filename == ("..") topics += 1 - message_json = File.read MESSAGES_DIR + '/' + filename + message_json = File.read MESSAGES_DIR + "/" + filename message = SocialcastMessage.new(message_json) next unless message.title created_topic = import_topic message.topic - if created_topic - import_posts message.replies, created_topic.topic_id - end + import_posts message.replies, created_topic.topic_id if created_topic imported += 1 print_status topics, total end @@ -48,8 +45,8 @@ class ImportScripts::Socialcast < ImportScripts::Base users = 0 total = count_files(USERS_DIR) Dir.foreach(USERS_DIR) do |filename| - next if filename == ('.') || filename == ('..') - user_json = File.read USERS_DIR + '/' + filename + next if filename == (".") || filename == ("..") + user_json = File.read USERS_DIR + "/" + filename user = SocialcastUser.new(user_json).user create_user user, user[:id] users += 1 @@ -58,7 +55,7 @@ class ImportScripts::Socialcast < ImportScripts::Base end def count_files(path) - Dir.foreach(path).select { |f| f != '.' && f != '..' }.count + Dir.foreach(path).select { |f| f != "." && f != ".." }.count end def import_topic(topic) @@ -80,9 +77,7 @@ class ImportScripts::Socialcast < ImportScripts::Base end def import_posts(posts, topic_id) - posts.each do |post| - import_post post, topic_id - end + posts.each { |post| import_post post, topic_id } end def import_post(post, topic_id) @@ -95,9 +90,6 @@ class ImportScripts::Socialcast < ImportScripts::Base puts new_post.inspect end end - end -if __FILE__ == $0 - ImportScripts::Socialcast.new.perform -end +ImportScripts::Socialcast.new.perform if __FILE__ == $0 diff --git a/script/import_scripts/socialcast/socialcast_api.rb b/script/import_scripts/socialcast/socialcast_api.rb index 84fc639770..6b080692c3 100644 --- a/script/import_scripts/socialcast/socialcast_api.rb +++ b/script/import_scripts/socialcast/socialcast_api.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'base64' -require 'json' +require "base64" +require "json" class SocialcastApi - attr_accessor :domain, :username, :password def initialize(domain, username, password) @@ -29,12 +28,12 @@ class SocialcastApi def list_users(opts = {}) page = opts[:page] ? opts[:page] : 1 response = request "#{base_url}/users?page=#{page}" - response['users'].sort { |u| u['id'] } + response["users"].sort { |u| u["id"] } end def list_messages(opts = {}) page = opts[:page] ? opts[:page] : 1 response = request "#{base_url}/messages?page=#{page}" - response['messages'].sort { |m| m['id'] } + response["messages"].sort { |m| m["id"] } end end diff --git a/script/import_scripts/socialcast/socialcast_message.rb b/script/import_scripts/socialcast/socialcast_message.rb index 4c7cf7a445..457713983a 100644 --- a/script/import_scripts/socialcast/socialcast_message.rb +++ b/script/import_scripts/socialcast/socialcast_message.rb @@ -1,24 +1,23 @@ # frozen_string_literal: true -require 'json' -require 'cgi' -require 'time' -require_relative 'create_title.rb' +require "json" +require "cgi" +require "time" +require_relative "create_title.rb" class SocialcastMessage - DEFAULT_CATEGORY = "Socialcast Import" DEFAULT_TAG = "socialcast-import" TAGS_AND_CATEGORIES = { "somegroupname" => { category: "Apple Stems", - tags: ["waxy", "tough"] + tags: %w[waxy tough], }, "someothergroupname" => { category: "Orange Peels", - tags: ["oily"] - } - } + tags: ["oily"], + }, + } def initialize(message_json) @parsed_json = JSON.parse message_json @@ -26,18 +25,18 @@ class SocialcastMessage def topic topic = {} - topic[:id] = @parsed_json['id'] - topic[:author_id] = @parsed_json['user']['id'] + topic[:id] = @parsed_json["id"] + topic[:author_id] = @parsed_json["user"]["id"] topic[:title] = title - topic[:raw] = @parsed_json['body'] - topic[:created_at] = Time.parse @parsed_json['created_at'] + topic[:raw] = @parsed_json["body"] + topic[:created_at] = Time.parse @parsed_json["created_at"] topic[:tags] = tags topic[:category] = category topic end def title - CreateTitle.from_body @parsed_json['body'] + CreateTitle.from_body @parsed_json["body"] end def tags @@ -55,39 +54,37 @@ class SocialcastMessage def category category = DEFAULT_CATEGORY - if group && TAGS_AND_CATEGORIES[group] - category = TAGS_AND_CATEGORIES[group][:category] - end + category = TAGS_AND_CATEGORIES[group][:category] if group && TAGS_AND_CATEGORIES[group] category end def group - @parsed_json['group']['groupname'].downcase if @parsed_json['group'] && @parsed_json['group']['groupname'] + if @parsed_json["group"] && @parsed_json["group"]["groupname"] + @parsed_json["group"]["groupname"].downcase + end end def url - @parsed_json['url'] + @parsed_json["url"] end def message_type - @parsed_json['message_type'] + @parsed_json["message_type"] end def replies posts = [] - comments = @parsed_json['comments'] - comments.each do |comment| - posts << post_from_comment(comment) - end + comments = @parsed_json["comments"] + comments.each { |comment| posts << post_from_comment(comment) } posts end def post_from_comment(comment) post = {} - post[:id] = comment['id'] - post[:author_id] = comment['user']['id'] - post[:raw] = comment['text'] - post[:created_at] = Time.parse comment['created_at'] + post[:id] = comment["id"] + post[:author_id] = comment["user"]["id"] + post[:raw] = comment["text"] + post[:created_at] = Time.parse comment["created_at"] post end diff --git a/script/import_scripts/socialcast/socialcast_user.rb b/script/import_scripts/socialcast/socialcast_user.rb index 1ffc93081c..f882637f66 100644 --- a/script/import_scripts/socialcast/socialcast_user.rb +++ b/script/import_scripts/socialcast/socialcast_user.rb @@ -1,26 +1,24 @@ # frozen_string_literal: true -require 'json' -require 'cgi' -require 'time' +require "json" +require "cgi" +require "time" class SocialcastUser - def initialize(user_json) @parsed_json = JSON.parse user_json end def user - email = @parsed_json['contact_info']['email'] - email = "#{@parsed_json['id']}@noemail.com" unless email + email = @parsed_json["contact_info"]["email"] + email = "#{@parsed_json["id"]}@noemail.com" unless email user = {} - user[:id] = @parsed_json['id'] - user[:name] = @parsed_json['name'] - user[:username] = @parsed_json['username'] + user[:id] = @parsed_json["id"] + user[:name] = @parsed_json["name"] + user[:username] = @parsed_json["username"] user[:email] = email user[:staged] = true user end - end diff --git a/script/import_scripts/socialcast/test/test_create_title.rb b/script/import_scripts/socialcast/test/test_create_title.rb index ee934a4f89..0dac092550 100644 --- a/script/import_scripts/socialcast/test/test_create_title.rb +++ b/script/import_scripts/socialcast/test/test_create_title.rb @@ -1,26 +1,28 @@ # frozen_string_literal: true -require 'minitest/autorun' -require_relative '../create_title.rb' +require "minitest/autorun" +require_relative "../create_title.rb" class TestCreateTitle < Minitest::Test - def test_create_title_1 - body = "@GreatCheerThreading \nWhere can I find information on how GCTS stacks up against the competition? What are the key differentiators?" + body = + "@GreatCheerThreading \nWhere can I find information on how GCTS stacks up against the competition? What are the key differentiators?" expected = "Where can I find information on how GCTS stacks up against the competition?" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_2 - body = "GCTS in 200 stores across town. How many threads per inch would you guess? @GreatCheerThreading" + body = + "GCTS in 200 stores across town. How many threads per inch would you guess? @GreatCheerThreading" expected = "GCTS in 200 stores across town. How many threads per inch would you guess?" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_3 - body = "gFabric Sheets 1.2 now has Great Cheer Threads, letting you feel the softness running through the cotton fibers." + body = + "gFabric Sheets 1.2 now has Great Cheer Threads, letting you feel the softness running through the cotton fibers." expected = "gFabric Sheets 1.2 now has Great Cheer Threads, letting you feel the softness..." title = CreateTitle.from_body body assert_equal(expected, title) @@ -34,49 +36,56 @@ class TestCreateTitle < Minitest::Test end def test_create_title_5 - body = "One sentence. Two sentence. Three sentence. Four is going to go on and on for more words than we want." + body = + "One sentence. Two sentence. Three sentence. Four is going to go on and on for more words than we want." expected = "One sentence. Two sentence. Three sentence." title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_6 - body = "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site)?\n\n//cc @RD @GreatCheerThreading" + body = + "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site)?\n\n//cc @RD @GreatCheerThreading" expected = "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site)?" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_6b - body = "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site of yore)?\n\n//cc @RD @GreatCheerThreading" + body = + "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site of yore)?\n\n//cc @RD @GreatCheerThreading" expected = "Anyone know of any invite codes for www.greatcheer.io (the Great Cheer v2 site..." title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_6c - body = "Anyone know of any invite codes for www.greatcheer.io?! (the Great Cheer v2 site of yore)?\n\n//cc @RD @GreatCheerThreading" + body = + "Anyone know of any invite codes for www.greatcheer.io?! (the Great Cheer v2 site of yore)?\n\n//cc @RD @GreatCheerThreading" expected = "Anyone know of any invite codes for www.greatcheer.io?!" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_7 - body = "@GreatCheerThreading \n\nDoes anyone know what the plan is to move to denser 1.2 threads for GCTS?\n\nI have a customer interested in the higher thread counts offered in 1.2." + body = + "@GreatCheerThreading \n\nDoes anyone know what the plan is to move to denser 1.2 threads for GCTS?\n\nI have a customer interested in the higher thread counts offered in 1.2." expected = "Does anyone know what the plan is to move to denser 1.2 threads for GCTS?" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_8 - body = "@GreatCheerThreading @FabricWeavingWorldwide \n\nI was just chatting with a customer, after receiving this email:\n\n\"Ours is more of a ‘conceptual’ question. We have too much fiber" + body = + "@GreatCheerThreading @FabricWeavingWorldwide \n\nI was just chatting with a customer, after receiving this email:\n\n\"Ours is more of a ‘conceptual’ question. We have too much fiber" expected = "I was just chatting with a customer, after receiving this email:" title = CreateTitle.from_body body assert_equal(expected, title) end def test_create_title_9 - body = "Hi,\n\nDoes anyone have a PPT deck on whats new in cotton (around 10 or so slides) nothing to detailed as per what we have in the current 1.x version?\n\nI am not after a what's coming in cotton 2" + body = + "Hi,\n\nDoes anyone have a PPT deck on whats new in cotton (around 10 or so slides) nothing to detailed as per what we have in the current 1.x version?\n\nI am not after a what's coming in cotton 2" expected = "Does anyone have a PPT deck on whats new in cotton (around 10 or so slides)..." title = CreateTitle.from_body body assert_equal(expected, title) @@ -90,7 +99,8 @@ class TestCreateTitle < Minitest::Test end def test_create_title_11 - body = "Hi Guys,\nI'm working with #gtcs and one of the things we're playing with is TC. What better tool to demo and use than our own \nhttps://greatcheerthreading.com/themostthreads/cool-stuff\n\nThis used to work great in 2013," + body = + "Hi Guys,\nI'm working with #gtcs and one of the things we're playing with is TC. What better tool to demo and use than our own \nhttps://greatcheerthreading.com/themostthreads/cool-stuff\n\nThis used to work great in 2013," expected = "I'm working with #gtcs and one of the things we're playing with is TC." title = CreateTitle.from_body body assert_equal(expected, title) @@ -104,10 +114,10 @@ class TestCreateTitle < Minitest::Test end def test_create_title_13 - body = "Embroidered TC ... http://blogs.greatcheerthreading.com/thread/embroidering-the-threads-is-just-the-beginning\n@SoftStuff @TightWeave and team hopefully can share their thoughts on this recent post." + body = + "Embroidered TC ... http://blogs.greatcheerthreading.com/thread/embroidering-the-threads-is-just-the-beginning\n@SoftStuff @TightWeave and team hopefully can share their thoughts on this recent post." expected = "and team hopefully can share their thoughts on this recent post." title = CreateTitle.from_body body assert_equal(expected, title) end - end diff --git a/script/import_scripts/socialcast/test/test_data.rb b/script/import_scripts/socialcast/test/test_data.rb index 5bdbf52cc9..2dd018c32d 100644 --- a/script/import_scripts/socialcast/test/test_data.rb +++ b/script/import_scripts/socialcast/test/test_data.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -USERS = '{ +USERS = + '{ "users": [ { "contact_info": { @@ -1082,7 +1083,8 @@ USERS = '{ ] }' -MESSAGES = '{ +MESSAGES = + '{ "messages": [ { "id": 426, @@ -5429,7 +5431,8 @@ MESSAGES = '{ "messages_next_page": 2 }' -MESSAGES_PG_2 = '{ +MESSAGES_PG_2 = + '{ "messages": [ { "id": 386, diff --git a/script/import_scripts/socialcast/test/test_socialcast_api.rb b/script/import_scripts/socialcast/test/test_socialcast_api.rb index 70ad038c8b..f46e0fefd5 100644 --- a/script/import_scripts/socialcast/test/test_socialcast_api.rb +++ b/script/import_scripts/socialcast/test/test_socialcast_api.rb @@ -1,21 +1,20 @@ # frozen_string_literal: true -require 'minitest/autorun' -require 'yaml' -require_relative '../socialcast_api.rb' -require_relative './test_data.rb' +require "minitest/autorun" +require "yaml" +require_relative "../socialcast_api.rb" +require_relative "./test_data.rb" class TestSocialcastApi < Minitest::Test - DEBUG = false def initialize(args) - config = YAML::load_file(File.join(__dir__, 'config.ex.yml')) - @domain = config['domain'] - @username = config['username'] - @password = config['password'] - @kb_id = config['kb_id'] - @question_id = config['question_id'] + config = YAML.load_file(File.join(__dir__, "config.ex.yml")) + @domain = config["domain"] + @username = config["username"] + @password = config["password"] + @kb_id = config["kb_id"] + @question_id = config["question_id"] super args end @@ -30,18 +29,18 @@ class TestSocialcastApi < Minitest::Test end def test_base_url - assert_equal 'https://demo.socialcast.com/api', @socialcast.base_url + assert_equal "https://demo.socialcast.com/api", @socialcast.base_url end def test_headers headers = @socialcast.headers - assert_equal 'Basic ZW1pbHlAc29jaWFsY2FzdC5jb206ZGVtbw==', headers[:Authorization] - assert_equal 'application/json', headers[:Accept] + assert_equal "Basic ZW1pbHlAc29jaWFsY2FzdC5jb206ZGVtbw==", headers[:Authorization] + assert_equal "application/json", headers[:Accept] end def test_list_users users = @socialcast.list_users - expected = JSON.parse(USERS)['users'].sort { |u| u['id'] } + expected = JSON.parse(USERS)["users"].sort { |u| u["id"] } assert_equal 15, users.size assert_equal expected[0], users[0] end @@ -53,14 +52,14 @@ class TestSocialcastApi < Minitest::Test def test_list_messages messages = @socialcast.list_messages - expected = JSON.parse(MESSAGES)['messages'].sort { |m| m['id'] } + expected = JSON.parse(MESSAGES)["messages"].sort { |m| m["id"] } assert_equal 20, messages.size check_keys expected[0], messages[0] end def test_messages_next_page messages = @socialcast.list_messages(page: 2) - expected = JSON.parse(MESSAGES_PG_2)['messages'].sort { |m| m['id'] } + expected = JSON.parse(MESSAGES_PG_2)["messages"].sort { |m| m["id"] } assert_equal 20, messages.size check_keys expected[0], messages[0] end @@ -69,18 +68,16 @@ class TestSocialcastApi < Minitest::Test def check_keys(expected, actual) msg = "### caller[0]:\nKey not found in actual keys: #{actual.keys}\n" - expected.keys.each do |k| - assert (actual.keys.include? k), "#{k}" - end + expected.keys.each { |k| assert (actual.keys.include? k), "#{k}" } end def debug(message, show = false) if show || DEBUG - puts '### ' + caller[0] - puts '' + puts "### " + caller[0] + puts "" puts message - puts '' - puts '' + puts "" + puts "" end end end diff --git a/script/import_scripts/socialcast/title.rb b/script/import_scripts/socialcast/title.rb index b9f0e3c8ae..9f2c3dd82d 100644 --- a/script/import_scripts/socialcast/title.rb +++ b/script/import_scripts/socialcast/title.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require_relative './socialcast_message.rb' -require_relative './socialcast_user.rb' -require 'set' +require_relative "./socialcast_message.rb" +require_relative "./socialcast_user.rb" +require "set" require File.expand_path(File.dirname(__FILE__) + "/../base.rb") MESSAGES_DIR = "output/messages" @@ -11,8 +11,8 @@ def titles topics = 0 total = count_files(MESSAGES_DIR) Dir.foreach(MESSAGES_DIR) do |filename| - next if filename == ('.') || filename == ('..') - message_json = File.read MESSAGES_DIR + '/' + filename + next if filename == (".") || filename == ("..") + message_json = File.read MESSAGES_DIR + "/" + filename message = SocialcastMessage.new(message_json) next unless message.title #puts "#{filename}, #{message.replies.size}, #{message.topic[:raw].size}, #{message.message_type}, #{message.title}" @@ -23,7 +23,7 @@ def titles end def count_files(path) - Dir.foreach(path).select { |f| f != '.' && f != '..' }.count + Dir.foreach(path).select { |f| f != "." && f != ".." }.count end titles diff --git a/script/import_scripts/sourceforge.rb b/script/import_scripts/sourceforge.rb index 7d7de0cb8c..8d165a7fa0 100644 --- a/script/import_scripts/sourceforge.rb +++ b/script/import_scripts/sourceforge.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'base.rb' +require_relative "base.rb" # Import script for SourceForge discussions. # @@ -15,10 +15,10 @@ require_relative 'base.rb' class ImportScripts::Sourceforge < ImportScripts::Base # When the URL of your project is https://sourceforge.net/projects/foo/ # than the value of PROJECT_NAME is 'foo' - PROJECT_NAME = 'project_name' + PROJECT_NAME = "project_name" # This is the path to the discussion.json that you exported from SourceForge. - JSON_FILE = '/path/to/discussion.json' + JSON_FILE = "/path/to/discussion.json" def initialize super @@ -27,7 +27,7 @@ class ImportScripts::Sourceforge < ImportScripts::Base end def execute - puts '', 'Importing from SourceForge...' + puts "", "Importing from SourceForge..." load_json @@ -40,25 +40,26 @@ class ImportScripts::Sourceforge < ImportScripts::Base end def import_categories - puts '', 'importing categories' + puts "", "importing categories" create_categories(@json[:forums]) do |forum| { id: forum[:shortname], name: forum[:name], - post_create_action: proc do |category| - changes = { raw: forum[:description] } - opts = { revised_at: Time.now, bypass_bump: true } + post_create_action: + proc do |category| + changes = { raw: forum[:description] } + opts = { revised_at: Time.now, bypass_bump: true } - post = category.topic.first_post - post.revise(@system_user, changes, opts) - end + post = category.topic.first_post + post.revise(@system_user, changes, opts) + end, } end end def import_topics - puts '', 'importing posts' + puts "", "importing posts" imported_post_count = 0 total_post_count = count_posts @@ -78,7 +79,7 @@ class ImportScripts::Sourceforge < ImportScripts::Base id: "#{thread[:_id]}_#{post[:slug]}", user_id: @system_user, created_at: Time.zone.parse(post[:timestamp]), - raw: process_post_text(forum, thread, post) + raw: process_post_text(forum, thread, post), } if post == first_post @@ -103,9 +104,7 @@ class ImportScripts::Sourceforge < ImportScripts::Base total_count = 0 @json[:forums].each do |forum| - forum[:threads].each do |thread| - total_count += thread[:posts].size - end + forum[:threads].each { |thread| total_count += thread[:posts].size } end total_count @@ -117,20 +116,22 @@ class ImportScripts::Sourceforge < ImportScripts::Base def process_post_text(forum, thread, post) text = post[:text] - text.gsub!(/~{3,}/, '```') # Discourse doesn't recognize ~~~ as beginning/end of code blocks + text.gsub!(/~{3,}/, "```") # Discourse doesn't recognize ~~~ as beginning/end of code blocks # SourceForge doesn't allow symbols in usernames, so we are safe here. # Well, unless it's the anonymous user, which has an evil asterisk in the JSON file... username = post[:author] - username = 'anonymous' if username == '*anonymous' + username = "anonymous" if username == "*anonymous" # anonymous and nobody are nonexistent users. Make sure we don't create links for them. - user_without_profile = username == 'anonymous' || username == 'nobody' - user_link = user_without_profile ? username : "[#{username}](https://sourceforge.net/u/#{username}/)" + user_without_profile = username == "anonymous" || username == "nobody" + user_link = + user_without_profile ? username : "[#{username}](https://sourceforge.net/u/#{username}/)" # Create a nice looking header for each imported post that links to the author's user profile and the old post. - post_date = Time.zone.parse(post[:timestamp]).strftime('%A, %B %d, %Y') - post_url = "https://sourceforge.net/p/#{PROJECT_NAME}/discussion/#{forum[:shortname]}/thread/#{thread[:_id]}/##{post[:slug]}" + post_date = Time.zone.parse(post[:timestamp]).strftime("%A, %B %d, %Y") + post_url = + "https://sourceforge.net/p/#{PROJECT_NAME}/discussion/#{forum[:shortname]}/thread/#{thread[:_id]}/##{post[:slug]}" "**#{user_link}** wrote on [#{post_date}](#{post_url}):\n\n#{text}" end diff --git a/script/import_scripts/stack_overflow.rb b/script/import_scripts/stack_overflow.rb index 2eca547dbc..30ddf2e551 100644 --- a/script/import_scripts/stack_overflow.rb +++ b/script/import_scripts/stack_overflow.rb @@ -5,18 +5,18 @@ require "tiny_tds" require File.expand_path(File.dirname(__FILE__) + "/base.rb") class ImportScripts::StackOverflow < ImportScripts::Base - BATCH_SIZE ||= 1000 def initialize super - @client = TinyTds::Client.new( - host: ENV["DB_HOST"], - username: ENV["DB_USERNAME"], - password: ENV["DB_PASSWORD"], - database: ENV["DB_NAME"], - ) + @client = + TinyTds::Client.new( + host: ENV["DB_HOST"], + username: ENV["DB_USERNAME"], + password: ENV["DB_PASSWORD"], + database: ENV["DB_NAME"], + ) end def execute @@ -36,7 +36,7 @@ class ImportScripts::StackOverflow < ImportScripts::Base total = query("SELECT COUNT(*) count FROM Users WHERE Id > 0").first["count"] batches(BATCH_SIZE) do |offset| - users = query(<<~SQL + users = query(<<~SQL).to_a SELECT TOP #{BATCH_SIZE} Id , UserTypeId @@ -55,7 +55,6 @@ class ImportScripts::StackOverflow < ImportScripts::Base AND Id > #{last_user_id} ORDER BY Id SQL - ).to_a break if users.empty? @@ -77,11 +76,16 @@ class ImportScripts::StackOverflow < ImportScripts::Base name: u["RealName"], location: u["Location"], date_of_birth: u["Birthday"], - post_create_action: proc do |user| - if u["ProfileImageUrl"].present? - UserAvatar.import_url_for_user(u["ProfileImageUrl"], user) rescue nil - end - end + post_create_action: + proc do |user| + if u["ProfileImageUrl"].present? + begin + UserAvatar.import_url_for_user(u["ProfileImageUrl"], user) + rescue StandardError + nil + end + end + end, } end end @@ -91,11 +95,16 @@ class ImportScripts::StackOverflow < ImportScripts::Base puts "", "Importing posts..." last_post_id = -1 - total = query("SELECT COUNT(*) count FROM Posts WHERE PostTypeId IN (1,2,3)").first["count"] + - query("SELECT COUNT(*) count FROM PostComments WHERE PostId IN (SELECT Id FROM Posts WHERE PostTypeId IN (1,2,3))").first["count"] + total = + query("SELECT COUNT(*) count FROM Posts WHERE PostTypeId IN (1,2,3)").first["count"] + + query( + "SELECT COUNT(*) count FROM PostComments WHERE PostId IN (SELECT Id FROM Posts WHERE PostTypeId IN (1,2,3))", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - posts = query(<<~SQL + posts = query(<<~SQL).to_a SELECT TOP #{BATCH_SIZE} Id , PostTypeId @@ -113,14 +122,13 @@ class ImportScripts::StackOverflow < ImportScripts::Base AND Id > #{last_post_id} ORDER BY Id SQL - ).to_a break if posts.empty? last_post_id = posts[-1]["Id"] post_ids = posts.map { |p| p["Id"] } - comments = query(<<~SQL + comments = query(<<~SQL).to_a SELECT CONCAT('Comment-', Id) AS Id , PostId AS ParentId , Text @@ -130,7 +138,6 @@ class ImportScripts::StackOverflow < ImportScripts::Base WHERE PostId IN (#{post_ids.join(",")}) ORDER BY Id SQL - ).to_a posts_and_comments = (posts + comments).sort_by { |p| p["CreationDate"] } post_and_comment_ids = posts_and_comments.map { |p| p["Id"] } @@ -173,7 +180,7 @@ class ImportScripts::StackOverflow < ImportScripts::Base last_like_id = -1 batches(BATCH_SIZE) do |offset| - likes = query(<<~SQL + likes = query(<<~SQL).to_a SELECT TOP #{BATCH_SIZE} Id , PostId @@ -185,7 +192,6 @@ class ImportScripts::StackOverflow < ImportScripts::Base AND Id > #{last_like_id} ORDER BY Id SQL - ).to_a break if likes.empty? @@ -196,17 +202,26 @@ class ImportScripts::StackOverflow < ImportScripts::Base next unless post_id = post_id_from_imported_post_id(l["PostId"]) next unless user = User.find_by(id: user_id) next unless post = Post.find_by(id: post_id) - PostActionCreator.like(user, post) rescue nil + begin + PostActionCreator.like(user, post) + rescue StandardError + nil + end end end puts "", "Importing comment likes..." last_like_id = -1 - total = query("SELECT COUNT(*) count FROM Comments2Votes WHERE VoteTypeId = 2 AND DeletionDate IS NULL").first["count"] + total = + query( + "SELECT COUNT(*) count FROM Comments2Votes WHERE VoteTypeId = 2 AND DeletionDate IS NULL", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - likes = query(<<~SQL + likes = query(<<~SQL).to_a SELECT TOP #{BATCH_SIZE} Id , CONCAT('Comment-', PostCommentId) AS PostCommentId @@ -218,7 +233,6 @@ class ImportScripts::StackOverflow < ImportScripts::Base AND Id > #{last_like_id} ORDER BY Id SQL - ).to_a break if likes.empty? @@ -229,7 +243,11 @@ class ImportScripts::StackOverflow < ImportScripts::Base next unless post_id = post_id_from_imported_post_id(l["PostCommentId"]) next unless user = User.find_by(id: user_id) next unless post = Post.find_by(id: post_id) - PostActionCreator.like(user, post) rescue nil + begin + PostActionCreator.like(user, post) + rescue StandardError + nil + end end end end @@ -249,7 +267,6 @@ class ImportScripts::StackOverflow < ImportScripts::Base def query(sql) @client.execute(sql) end - end ImportScripts::StackOverflow.new.perform diff --git a/script/import_scripts/support/convert_mysql_xml_to_mysql.rb b/script/import_scripts/support/convert_mysql_xml_to_mysql.rb index be0e45ca2f..070bfb4123 100644 --- a/script/import_scripts/support/convert_mysql_xml_to_mysql.rb +++ b/script/import_scripts/support/convert_mysql_xml_to_mysql.rb @@ -3,11 +3,10 @@ # convert huge XML dump to mysql friendly import # -require 'ox' -require 'set' +require "ox" +require "set" class Saxy < Ox::Sax - def initialize @stack = [] end @@ -32,7 +31,6 @@ class Saxy < Ox::Sax def cdata(val) @stack[-1][:text] = val end - end class Convert < Saxy @@ -59,10 +57,13 @@ class Convert < Saxy end def output_table_definition(data) - cols = data[:cols].map do |col| - attrs = col[:attrs] - "#{attrs[:Field]} #{attrs[:Type]}" - end.join(", ") + cols = + data[:cols] + .map do |col| + attrs = col[:attrs] + "#{attrs[:Field]} #{attrs[:Type]}" + end + .join(", ") puts "CREATE TABLE #{data[:attrs][:name]} (#{cols});" end @@ -77,4 +78,4 @@ class Convert < Saxy end end -Ox.sax_parse(Convert.new(skip_data: ['metrics2', 'user_log']), File.open(ARGV[0])) +Ox.sax_parse(Convert.new(skip_data: %w[metrics2 user_log]), File.open(ARGV[0])) diff --git a/script/import_scripts/telligent.rb b/script/import_scripts/telligent.rb index 32ffda8acb..c46be23fac 100644 --- a/script/import_scripts/telligent.rb +++ b/script/import_scripts/telligent.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require_relative 'base' -require 'tiny_tds' +require_relative "base" +require "tiny_tds" # Import script for Telligent communities # @@ -40,17 +40,19 @@ require 'tiny_tds' class ImportScripts::Telligent < ImportScripts::Base BATCH_SIZE ||= 1000 - LOCAL_AVATAR_REGEX ||= /\A~\/.*(?communityserver-components-(?:selectable)?avatars)\/(?[^\/]+)\/(?.+)/i - REMOTE_AVATAR_REGEX ||= /\Ahttps?:\/\//i + LOCAL_AVATAR_REGEX ||= + %r{\A~/.*(?communityserver-components-(?:selectable)?avatars)/(?[^/]+)/(?.+)}i + REMOTE_AVATAR_REGEX ||= %r{\Ahttps?://}i ATTACHMENT_REGEXES ||= [ - /]*\shref="[^"]*?\/cfs-file(?:systemfile)?(?:\.ashx)?\/__key\/(?[^\/]+)\/(?[^\/]+)\/(?.+?)".*?>.*?<\/a>/i, - /]*\ssrc="[^"]*?\/cfs-file(?:systemfile)?(?:\.ashx)?\/__key\/(?[^\/]+)\/(?[^\/]+)\/(?.+?)".*?>/i, - /\[View:[^\]]*?\/cfs-file(?:systemfile)?(?:\.ashx)?\/__key\/(?[^\/]+)\/(?[^\/]+)\/(?.+?)(?:\:[:\d\s]*?)?\]/i, - /\[(?img|url)\][^\[]*?cfs-file(?:systemfile)?(?:\.ashx)?\/__key\/(?[^\/]+)\/(?[^\/]+)\/(?.+?)\[\/\k\]/i, - /\[(?img|url)=[^\[]*?cfs-file(?:systemfile)?(?:\.ashx)?\/__key\/(?[^\/]+)\/(?[^\/]+)\/(?.+?)\][^\[]*?\[\/\k\]/i + %r{]*\shref="[^"]*?/cfs-file(?:systemfile)?(?:\.ashx)?/__key/(?[^/]+)/(?[^/]+)/(?.+?)".*?>.*?}i, + %r{]*\ssrc="[^"]*?/cfs-file(?:systemfile)?(?:\.ashx)?/__key/(?[^/]+)/(?[^/]+)/(?.+?)".*?>}i, + %r{\[View:[^\]]*?/cfs-file(?:systemfile)?(?:\.ashx)?/__key/(?[^/]+)/(?[^/]+)/(?.+?)(?:\:[:\d\s]*?)?\]}i, + %r{\[(?img|url)\][^\[]*?cfs-file(?:systemfile)?(?:\.ashx)?/__key/(?[^/]+)/(?[^/]+)/(?.+?)\[/\k\]}i, + %r{\[(?img|url)=[^\[]*?cfs-file(?:systemfile)?(?:\.ashx)?/__key/(?[^/]+)/(?[^/]+)/(?.+?)\][^\[]*?\[/\k\]}i, ] PROPERTY_NAMES_REGEX ||= /(?\w+):S:(?\d+):(?\d+):/ - INTERNAL_LINK_REGEX ||= /\shref=".*?\/f\/\d+(?:(\/t\/(?\d+))|(?:\/p\/\d+\/(?\d+))|(?:\/p\/(?\d+)\/reply))\.aspx[^"]*?"/i + INTERNAL_LINK_REGEX ||= + %r{\shref=".*?/f/\d+(?:(/t/(?\d+))|(?:/p/\d+/(?\d+))|(?:/p/(?\d+)/reply))\.aspx[^"]*?"}i CATEGORY_LINK_NORMALIZATION = '/.*?(f\/\d+)$/\1' TOPIC_LINK_NORMALIZATION = '/.*?(f\/\d+\/t\/\d+)$/\1' @@ -82,19 +84,20 @@ class ImportScripts::Telligent < ImportScripts::Base "1D20" => "”", "B000" => "°", "0003" => ["0300".to_i(16)].pack("U"), - "0103" => ["0301".to_i(16)].pack("U") + "0103" => ["0301".to_i(16)].pack("U"), } def initialize super() - @client = TinyTds::Client.new( - host: ENV["DB_HOST"], - username: ENV["DB_USERNAME"], - password: ENV["DB_PASSWORD"], - database: ENV["DB_NAME"], - timeout: 60 # the user query is very slow - ) + @client = + TinyTds::Client.new( + host: ENV["DB_HOST"], + username: ENV["DB_USERNAME"], + password: ENV["DB_PASSWORD"], + database: ENV["DB_NAME"], + timeout: 60, # the user query is very slow + ) @filestore_root_directory = ENV["FILE_BASE_DIR"] @files = {} @@ -180,10 +183,11 @@ class ImportScripts::Telligent < ImportScripts::Base bio_raw: html_to_markdown(ap_properties["bio"]), location: ap_properties["location"], website: ap_properties["webAddress"], - post_create_action: proc do |user| - import_avatar(user, up_properties["avatarUrl"]) - suspend_user(user, up_properties["BannedUntil"], up_properties["UserBanReason"]) - end + post_create_action: + proc do |user| + import_avatar(user, up_properties["avatarUrl"]) + suspend_user(user, up_properties["BannedUntil"], up_properties["UserBanReason"]) + end, } end @@ -193,13 +197,18 @@ class ImportScripts::Telligent < ImportScripts::Base # TODO move into base importer (create_user) and use consistent error handling def import_avatar(user, avatar_url) - return if @filestore_root_directory.blank? || avatar_url.blank? || avatar_url.include?("anonymous") + if @filestore_root_directory.blank? || avatar_url.blank? || avatar_url.include?("anonymous") + return + end if match_data = avatar_url.match(LOCAL_AVATAR_REGEX) - avatar_path = File.join(@filestore_root_directory, - match_data[:directory].gsub("-", "."), - match_data[:path].split("-"), - match_data[:filename]) + avatar_path = + File.join( + @filestore_root_directory, + match_data[:directory].gsub("-", "."), + match_data[:path].split("-"), + match_data[:filename], + ) if File.file?(avatar_path) @uploader.create_avatar(user, avatar_path) @@ -207,7 +216,11 @@ class ImportScripts::Telligent < ImportScripts::Base STDERR.puts "Could not find avatar: #{avatar_path}" end elsif avatar_url.match?(REMOTE_AVATAR_REGEX) - UserAvatar.import_url_for_user(avatar_url, user) rescue nil + begin + UserAvatar.import_url_for_user(avatar_url, user) + rescue StandardError + nil + end end end @@ -224,7 +237,7 @@ class ImportScripts::Telligent < ImportScripts::Base end def import_categories - if ENV['CATEGORY_MAPPING'] + if ENV["CATEGORY_MAPPING"] import_mapped_forums_as_categories else import_groups_and_forums_as_categories @@ -234,7 +247,7 @@ class ImportScripts::Telligent < ImportScripts::Base def import_mapped_forums_as_categories puts "", "Importing categories..." - json = JSON.parse(File.read(ENV['CATEGORY_MAPPING'])) + json = JSON.parse(File.read(ENV["CATEGORY_MAPPING"])) categories = [] @forum_ids_to_tags = {} @@ -256,7 +269,7 @@ class ImportScripts::Telligent < ImportScripts::Base id: id, name: name, parent_id: parent_id, - forum_ids: index == last_index ? forum_ids : nil + forum_ids: index == last_index ? forum_ids : nil, } parent_id = id end @@ -271,9 +284,7 @@ class ImportScripts::Telligent < ImportScripts::Base id: c[:id], name: c[:name], parent_category_id: category_id_from_imported_category_id(c[:parent_id]), - post_create_action: proc do |category| - map_forum_ids(category.id, c[:forum_ids]) - end + post_create_action: proc { |category| map_forum_ids(category.id, c[:forum_ids]) }, } end end @@ -302,10 +313,10 @@ class ImportScripts::Telligent < ImportScripts::Base create_categories(parent_categories) do |row| { - id: "G#{row['GroupID']}", + id: "G#{row["GroupID"]}", name: clean_category_name(row["Name"]), description: html_to_markdown(row["HtmlDescription"]), - position: row["SortOrder"] + position: row["SortOrder"], } end @@ -320,28 +331,31 @@ class ImportScripts::Telligent < ImportScripts::Base parent_category_id = parent_category_id_for(row) if category_id = replace_with_category_id(child_categories, parent_category_id) - add_category(row['ForumId'], Category.find_by_id(category_id)) - url = "f/#{row['ForumId']}" + add_category(row["ForumId"], Category.find_by_id(category_id)) + url = "f/#{row["ForumId"]}" Permalink.create(url: url, category_id: category_id) unless Permalink.exists?(url: url) nil else { - id: row['ForumId'], + id: row["ForumId"], parent_category_id: parent_category_id, name: clean_category_name(row["Name"]), description: html_to_markdown(row["Description"]), position: row["SortOrder"], - post_create_action: proc do |category| - url = "f/#{row['ForumId']}" - Permalink.create(url: url, category_id: category.id) unless Permalink.exists?(url: url) - end + post_create_action: + proc do |category| + url = "f/#{row["ForumId"]}" + unless Permalink.exists?(url: url) + Permalink.create(url: url, category_id: category.id) + end + end, } end end end def parent_category_id_for(row) - category_id_from_imported_category_id("G#{row['GroupId']}") if row.key?("GroupId") + category_id_from_imported_category_id("G#{row["GroupId"]}") if row.key?("GroupId") end def replace_with_category_id(child_categories, parent_category_id) @@ -351,23 +365,21 @@ class ImportScripts::Telligent < ImportScripts::Base def only_child?(child_categories, parent_category_id) count = 0 - child_categories.each do |row| - count += 1 if parent_category_id_for(row) == parent_category_id - end + child_categories.each { |row| count += 1 if parent_category_id_for(row) == parent_category_id } count == 1 end def clean_category_name(name) - CGI.unescapeHTML(name) - .strip + CGI.unescapeHTML(name).strip end def import_topics puts "", "Importing topics..." last_topic_id = -1 - total_count = count("SELECT COUNT(1) AS count FROM te_Forum_Threads t WHERE #{ignored_forum_sql_condition}") + total_count = + count("SELECT COUNT(1) AS count FROM te_Forum_Threads t WHERE #{ignored_forum_sql_condition}") batches do |offset| rows = query(<<~SQL) @@ -399,13 +411,16 @@ class ImportScripts::Telligent < ImportScripts::Base created_at: row["DateCreated"], closed: row["IsLocked"], views: row["TotalViews"], - post_create_action: proc do |action_post| - topic = action_post.topic - Jobs.enqueue_at(topic.pinned_until, :unpin_topic, topic_id: topic.id) if topic.pinned_until - url = "f/#{row['ForumId']}/t/#{row['ThreadId']}" - Permalink.create(url: url, topic_id: topic.id) unless Permalink.exists?(url: url) - import_topic_views(topic, row["TopicContentId"]) - end + post_create_action: + proc do |action_post| + topic = action_post.topic + if topic.pinned_until + Jobs.enqueue_at(topic.pinned_until, :unpin_topic, topic_id: topic.id) + end + url = "f/#{row["ForumId"]}/t/#{row["ThreadId"]}" + Permalink.create(url: url, topic_id: topic.id) unless Permalink.exists?(url: url) + import_topic_views(topic, row["TopicContentId"]) + end, } if row["StickyDate"] > Time.now @@ -446,9 +461,8 @@ class ImportScripts::Telligent < ImportScripts::Base end def ignored_forum_sql_condition - @ignored_forum_sql_condition ||= @ignored_forum_ids.present? \ - ? "t.ForumId NOT IN (#{@ignored_forum_ids.join(',')})" \ - : "1 = 1" + @ignored_forum_sql_condition ||= + @ignored_forum_ids.present? ? "t.ForumId NOT IN (#{@ignored_forum_ids.join(",")})" : "1 = 1" end def import_posts @@ -492,7 +506,8 @@ class ImportScripts::Telligent < ImportScripts::Base next if all_records_exist?(:post, rows.map { |row| row["ThreadReplyId"] }) create_posts(rows, total: total_count, offset: offset) do |row| - imported_parent_id = row["ParentReplyId"]&.nonzero? ? row["ParentReplyId"] : import_topic_id(row["ThreadId"]) + imported_parent_id = + row["ParentReplyId"]&.nonzero? ? row["ParentReplyId"] : import_topic_id(row["ThreadId"]) parent_post = topic_lookup_from_imported_post_id(imported_parent_id) user_id = user_id_from_imported_user_id(row["UserId"]) || Discourse::SYSTEM_USER_ID @@ -503,13 +518,13 @@ class ImportScripts::Telligent < ImportScripts::Base user_id: user_id, topic_id: parent_post[:topic_id], created_at: row["ThreadReplyDate"], - reply_to_post_number: parent_post[:post_number] + reply_to_post_number: parent_post[:post_number], } post[:custom_fields] = { is_accepted_answer: "true" } if row["IsFirstVerifiedAnswer"] post else - puts "Failed to import post #{row['ThreadReplyId']}. Parent was not found." + puts "Failed to import post #{row["ThreadReplyId"]}. Parent was not found." end end end @@ -565,7 +580,7 @@ class ImportScripts::Telligent < ImportScripts::Base id: row["MessageId"], raw: raw_with_attachment(row, user_id, :message), user_id: user_id, - created_at: row["DateCreated"] + created_at: row["DateCreated"], } if current_conversation_id == row["ConversationId"] @@ -574,7 +589,7 @@ class ImportScripts::Telligent < ImportScripts::Base if parent_post post[:topic_id] = parent_post[:topic_id] else - puts "Failed to import message #{row['MessageId']}. Parent was not found." + puts "Failed to import message #{row["MessageId"]}. Parent was not found." post = nil end else @@ -583,7 +598,7 @@ class ImportScripts::Telligent < ImportScripts::Base post[:target_usernames] = get_recipient_usernames(row) if post[:target_usernames].empty? - puts "Private message without recipients. Skipping #{row['MessageId']}" + puts "Private message without recipients. Skipping #{row["MessageId"]}" post = nil end @@ -611,7 +626,7 @@ class ImportScripts::Telligent < ImportScripts::Base def get_recipient_user_ids(participant_ids) return [] if participant_ids.blank? - user_ids = participant_ids.split(';') + user_ids = participant_ids.split(";") user_ids.uniq! user_ids.map!(&:strip) end @@ -619,9 +634,9 @@ class ImportScripts::Telligent < ImportScripts::Base def get_recipient_usernames(row) import_user_ids = get_recipient_user_ids(row["ParticipantIds"]) - import_user_ids.map! do |import_user_id| - find_user_by_import_id(import_user_id).try(:username) - end.compact + import_user_ids + .map! { |import_user_id| find_user_by_import_id(import_user_id).try(:username) } + .compact end def index_directory(root_directory) @@ -646,17 +661,16 @@ class ImportScripts::Telligent < ImportScripts::Base filename = row["FileName"] return raw if @filestore_root_directory.blank? || filename.blank? - if row["IsRemote"] - return "#{raw}\n#{filename}" - end + return "#{raw}\n#{filename}" if row["IsRemote"] - path = File.join( - "telligent.evolution.components.attachments", - "%02d" % row["ApplicationTypeId"], - "%02d" % row["ApplicationId"], - "%02d" % row["ApplicationContentTypeId"], - ("%010d" % row["ContentId"]).scan(/.{2}/) - ) + path = + File.join( + "telligent.evolution.components.attachments", + "%02d" % row["ApplicationTypeId"], + "%02d" % row["ApplicationId"], + "%02d" % row["ApplicationContentTypeId"], + ("%010d" % row["ContentId"]).scan(/.{2}/), + ) path = fix_attachment_path(path, filename) if path && !embedded_paths.include?(path) @@ -677,11 +691,11 @@ class ImportScripts::Telligent < ImportScripts::Base def print_file_not_found_error(type, path, row) case type when :topic - id = row['ThreadId'] + id = row["ThreadId"] when :post - id = row['ThreadReplyId'] + id = row["ThreadReplyId"] when :message - id = row['MessageId'] + id = row["MessageId"] end STDERR.puts "Could not find file for #{type} #{id}: #{path}" @@ -692,30 +706,31 @@ class ImportScripts::Telligent < ImportScripts::Base paths = [] upload_ids = [] - return [raw, paths, upload_ids] if @filestore_root_directory.blank? + return raw, paths, upload_ids if @filestore_root_directory.blank? ATTACHMENT_REGEXES.each do |regex| - raw = raw.gsub(regex) do - match_data = Regexp.last_match + raw = + raw.gsub(regex) do + match_data = Regexp.last_match - path = File.join(match_data[:directory], match_data[:path]) - fixed_path = fix_attachment_path(path, match_data[:filename]) + path = File.join(match_data[:directory], match_data[:path]) + fixed_path = fix_attachment_path(path, match_data[:filename]) - if fixed_path && File.file?(fixed_path) - filename = File.basename(fixed_path) - upload = @uploader.create_upload(user_id, fixed_path, filename) + if fixed_path && File.file?(fixed_path) + filename = File.basename(fixed_path) + upload = @uploader.create_upload(user_id, fixed_path, filename) - if upload.present? && upload.persisted? - paths << fixed_path - upload_ids << upload.id - @uploader.html_for_upload(upload, filename) + if upload.present? && upload.persisted? + paths << fixed_path + upload_ids << upload.id + @uploader.html_for_upload(upload, filename) + end + else + path = File.join(path, match_data[:filename]) + print_file_not_found_error(type, path, row) + match_data[0] end - else - path = File.join(path, match_data[:filename]) - print_file_not_found_error(type, path, row) - match_data[0] end - end end [raw, paths, upload_ids] @@ -806,8 +821,8 @@ class ImportScripts::Telligent < ImportScripts::Base md = HtmlToMarkdown.new(html).to_markdown md.gsub!(/\[quote.*?\]/, "\n" + '\0' + "\n") - md.gsub!(/(?/i, "\n```\n") - .gsub(/<\/?code\s*>/i, "`") + raw + .gsub("\\n", "\n") + .gsub(%r{}i, "\n```\n") + .gsub(%r{}i, "`") .gsub("<", "<") .gsub(">", ">") end - end ImportScripts::Vanilla.new.perform diff --git a/script/import_scripts/vanilla_body_parser.rb b/script/import_scripts/vanilla_body_parser.rb index ba4608e3ff..74c39583b9 100644 --- a/script/import_scripts/vanilla_body_parser.rb +++ b/script/import_scripts/vanilla_body_parser.rb @@ -14,9 +14,9 @@ class VanillaBodyParser end def parse - return clean_up(@row['Body']) unless rich? + return clean_up(@row["Body"]) unless rich? - full_text = json.each_with_index.map(&method(:parse_fragment)).join('') + full_text = json.each_with_index.map(&method(:parse_fragment)).join("") normalize full_text end @@ -25,30 +25,46 @@ class VanillaBodyParser def clean_up(text) #
    ...
    - text = text.gsub(/\
    (.*?)\<\/pre\>/im) { "\n```\n#{$1}\n```\n" }
    +    text = text.gsub(%r{\
    (.*?)\}im) { "\n```\n#{$1}\n```\n" }
         # 
    ...
    - text = text.gsub(/\(.*?)\<\/pre\>/im) { "\n```\n#{$1}\n```\n" } + text = text.gsub(%r{\(.*?)\}im) { "\n```\n#{$1}\n```\n" } # - text = text.gsub("\\", "").gsub(/\(.*?)\<\/code\>/im) { "#{$1}" } + text = text.gsub("\\", "").gsub(%r{\(.*?)\}im) { "#{$1}" } #
    ...
    - text = text.gsub(/\
    (.*?)\<\/div\>/im) { "\n[quote]\n#{$1}\n[/quote]\n" } + text = text.gsub(%r{\
    (.*?)\}im) { "\n[quote]\n#{$1}\n[/quote]\n" } # [code], [quote] - text = text.gsub(/\[\/?code\]/i, "\n```\n").gsub(/\[quote.*?\]/i, "\n" + '\0' + "\n").gsub(/\[\/quote\]/i, "\n" + '\0' + "\n") + text = + text + .gsub(%r{\[/?code\]}i, "\n```\n") + .gsub(/\[quote.*?\]/i, "\n" + '\0' + "\n") + .gsub(%r{\[/quote\]}i, "\n" + '\0' + "\n") - text.gsub(/<\/?font[^>]*>/, '').gsub(/<\/?span[^>]*>/, '').gsub(/<\/?div[^>]*>/, '').gsub(/^ +/, '').gsub(/ +/, ' ') + text + .gsub(%r{]*>}, "") + .gsub(%r{]*>}, "") + .gsub(%r{]*>}, "") + .gsub(/^ +/, "") + .gsub(/ +/, " ") end def rich? - @row['Format'].casecmp?('Rich') + @row["Format"].casecmp?("Rich") end def json return nil unless rich? - @json ||= JSON.parse(@row['Body']).map(&:deep_symbolize_keys) + @json ||= JSON.parse(@row["Body"]).map(&:deep_symbolize_keys) end def parse_fragment(fragment, index) - text = fragment.keys.one? && fragment[:insert].is_a?(String) ? fragment[:insert] : rich_parse(fragment) + text = + ( + if fragment.keys.one? && fragment[:insert].is_a?(String) + fragment[:insert] + else + rich_parse(fragment) + end + ) text = parse_code(text, fragment, index) text = parse_list(text, fragment, index) @@ -59,16 +75,18 @@ class VanillaBodyParser def rich_parse(fragment) insert = fragment[:insert] - return parse_mention(insert[:mention]) if insert.respond_to?(:dig) && insert.dig(:mention, :userID) + if insert.respond_to?(:dig) && insert.dig(:mention, :userID) + return parse_mention(insert[:mention]) + end return parse_formatting(fragment) if fragment[:attributes] - embed_type = insert.dig(:'embed-external', :data, :embedType) + embed_type = insert.dig(:"embed-external", :data, :embedType) - quoting = embed_type == 'quote' + quoting = embed_type == "quote" return parse_quote(insert) if quoting - embed = embed_type.in? ['image', 'link', 'file'] + embed = embed_type.in? %w[image link file] parse_embed(insert, embed_type) if embed end @@ -101,10 +119,10 @@ class VanillaBodyParser def parse_code(text, fragment, index) next_fragment = next_fragment(index) - next_code = next_fragment.dig(:attributes, :'code-block') + next_code = next_fragment.dig(:attributes, :"code-block") if next_code previous_fragment = previous_fragment(index) - previous_code = previous_fragment.dig(:attributes, :'code-block') + previous_code = previous_fragment.dig(:attributes, :"code-block") if previous_code text = text.gsub(/\\n(.*?)\\n/) { "\n```\n#{$1}\n```\n" } @@ -112,7 +130,7 @@ class VanillaBodyParser last_pos = text.rindex(/\n/) if last_pos - array = [text[0..last_pos].strip, text[last_pos + 1 .. text.length].strip] + array = [text[0..last_pos].strip, text[last_pos + 1..text.length].strip] text = array.join("\n```\n") else text = "\n```\n#{text}" @@ -120,10 +138,10 @@ class VanillaBodyParser end end - current_code = fragment.dig(:attributes, :'code-block') + current_code = fragment.dig(:attributes, :"code-block") if current_code second_next_fragment = second_next_fragment(index) - second_next_code = second_next_fragment.dig(:attributes, :'code-block') + second_next_code = second_next_fragment.dig(:attributes, :"code-block") # if current is code and 2 after is not, prepend ``` text = "\n```\n#{text}" unless second_next_code @@ -138,13 +156,13 @@ class VanillaBodyParser next_list = next_fragment.dig(:attributes, :list, :type) if next_list # if next is list, prepend
  • - text = '
  • ' + text + text = "
  • " + text previous_fragment = previous_fragment(index) previous_list = previous_fragment.dig(:attributes, :list, :type) # if next is list and previous is not, prepend
      or
        - list_tag = next_list == 'ordered' ? '
          ' : '
            ' + list_tag = next_list == "ordered" ? "
              " : "
                " text = "\n#{list_tag}\n#{text}" unless previous_list end @@ -152,13 +170,13 @@ class VanillaBodyParser if current_list # if current is list prepend - tag_closings = '' + tag_closings = "" second_next_fragment = second_next_fragment(index) second_next_list = second_next_fragment.dig(:attributes, :list, :type) # if current is list and 2 after is not, prepend
            - list_tag = current_list == 'ordered' ? '
        ' : '
      ' + list_tag = current_list == "ordered" ? "
    " : "" tag_closings = "#{tag_closings}\n#{list_tag}" unless second_next_list text = tag_closings + text @@ -180,24 +198,32 @@ class VanillaBodyParser end def parse_quote(insert) - embed = insert.dig(:'embed-external', :data) + embed = insert.dig(:"embed-external", :data) import_post_id = "#{embed[:recordType]}##{embed[:recordID]}" topic = @@lookup.topic_lookup_from_imported_post_id(import_post_id) user = user_from_imported_id(embed.dig(:insertUser, :userID)) - quote_info = topic && user ? "=\"#{user.username}, post: #{topic[:post_number]}, topic: #{topic[:topic_id]}\"" : '' + quote_info = + ( + if topic && user + "=\"#{user.username}, post: #{topic[:post_number]}, topic: #{topic[:topic_id]}\"" + else + "" + end + ) - "[quote#{quote_info}]\n#{embed[:body]}\n[/quote]\n\n""" + "[quote#{quote_info}]\n#{embed[:body]}\n[/quote]\n\n" \ + "" end def parse_embed(insert, embed_type) - embed = insert.dig(:'embed-external', :data) + embed = insert.dig(:"embed-external", :data) url = embed[:url] - if /https?\:\/\/#{@@host}\/uploads\/.*/.match?(url) - remote_path = url.scan(/uploads\/(.*)/) + if %r{https?\://#{@@host}/uploads/.*}.match?(url) + remote_path = url.scan(%r{uploads/(.*)}) path = File.join(@@uploads_path, remote_path) upload = @@uploader.create_upload(@user_id, path, embed[:name]) @@ -206,7 +232,7 @@ class VanillaBodyParser return "\n" + @@uploader.html_for_upload(upload, embed[:name]) + "\n" else puts "Failed to upload #{path}" - puts upload.errors.full_messages.join(', ') if upload + puts upload.errors.full_messages.join(", ") if upload end end @@ -222,9 +248,9 @@ class VanillaBodyParser def normalize(full_text) code_matcher = /```(.*\n)+```/ code_block = full_text[code_matcher] - full_text[code_matcher] = '{{{CODE_BLOCK}}}' if code_block + full_text[code_matcher] = "{{{CODE_BLOCK}}}" if code_block full_text = double_new_lines(full_text) - full_text['{{{CODE_BLOCK}}}'] = code_block if code_block + full_text["{{{CODE_BLOCK}}}"] = code_block if code_block full_text end diff --git a/script/import_scripts/vanilla_mysql.rb b/script/import_scripts/vanilla_mysql.rb index d0a5e34893..72f294337a 100644 --- a/script/import_scripts/vanilla_mysql.rb +++ b/script/import_scripts/vanilla_mysql.rb @@ -2,12 +2,11 @@ require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' -require 'reverse_markdown' -require_relative 'vanilla_body_parser' +require "htmlentities" +require "reverse_markdown" +require_relative "vanilla_body_parser" class ImportScripts::VanillaSQL < ImportScripts::Base - VANILLA_DB = "vanilla" TABLE_PREFIX = "GDN_" ATTACHMENTS_BASE_DIR = nil # "/absolute/path/to/attachments" set the absolute path if you have attachments @@ -17,19 +16,15 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def initialize super @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - database: VANILLA_DB - ) + @client = Mysql2::Client.new(host: "localhost", username: "root", database: VANILLA_DB) # by default, don't use the body parser as it's not pertinent to all versions @vb_parser = false VanillaBodyParser.configure( lookup: @lookup, uploader: @uploader, - host: 'forum.example.com', # your Vanilla forum domain - uploads_path: 'uploads' # relative path to your vanilla uploads folder + host: "forum.example.com", # your Vanilla forum domain + uploads_path: "uploads", # relative path to your vanilla uploads folder ) @import_tags = false @@ -77,80 +72,83 @@ class ImportScripts::VanillaSQL < ImportScripts::Base SQL create_groups(groups) do |group| - { - id: group["RoleID"], - name: @htmlentities.decode(group["Name"]).strip - } + { id: group["RoleID"], name: @htmlentities.decode(group["Name"]).strip } end end def import_users - puts '', "creating users" + puts "", "creating users" @user_is_deleted = false @last_deleted_username = nil username = nil @last_user_id = -1 - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}User;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}User;").first["count"] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT UserID, Name, Title, Location, About, Email, Admin, Banned, CountComments, + results = + mysql_query( + "SELECT UserID, Name, Title, Location, About, Email, Admin, Banned, CountComments, DateInserted, DateLastActive, InsertIPAddress FROM #{TABLE_PREFIX}User WHERE UserID > #{@last_user_id} ORDER BY UserID ASC - LIMIT #{BATCH_SIZE};") + LIMIT #{BATCH_SIZE};", + ) break if results.size < 1 - @last_user_id = results.to_a.last['UserID'] - next if all_records_exist? :users, results.map { |u| u['UserID'].to_i } + @last_user_id = results.to_a.last["UserID"] + next if all_records_exist? :users, results.map { |u| u["UserID"].to_i } create_users(results, total: total_count, offset: offset) do |user| - email = user['Email'].squish + email = user["Email"].squish next if email.blank? - next if user['Name'].blank? - next if @lookup.user_id_from_imported_user_id(user['UserID']) - if user['Name'] == '[Deleted User]' + next if user["Name"].blank? + next if @lookup.user_id_from_imported_user_id(user["UserID"]) + if user["Name"] == "[Deleted User]" # EVERY deleted user record in Vanilla has the same username: [Deleted User] # Save our UserNameSuggester some pain: @user_is_deleted = true - username = @last_deleted_username || user['Name'] + username = @last_deleted_username || user["Name"] else @user_is_deleted = false - username = user['Name'] + username = user["Name"] end - banned = user['Banned'] != 0 - commented = (user['CountComments'] || 0) > 0 + banned = user["Banned"] != 0 + commented = (user["CountComments"] || 0) > 0 - { id: user['UserID'], + { + id: user["UserID"], email: email, username: username, - name: user['Name'], - created_at: user['DateInserted'] == nil ? 0 : Time.zone.at(user['DateInserted']), - bio_raw: user['About'], - registration_ip_address: user['InsertIPAddress'], - last_seen_at: user['DateLastActive'] == nil ? 0 : Time.zone.at(user['DateLastActive']), - location: user['Location'], - admin: user['Admin'] == 1, + name: user["Name"], + created_at: user["DateInserted"] == nil ? 0 : Time.zone.at(user["DateInserted"]), + bio_raw: user["About"], + registration_ip_address: user["InsertIPAddress"], + last_seen_at: user["DateLastActive"] == nil ? 0 : Time.zone.at(user["DateLastActive"]), + location: user["Location"], + admin: user["Admin"] == 1, trust_level: !banned && commented ? 2 : 0, - post_create_action: proc do |newuser| - if @user_is_deleted - @last_deleted_username = newuser.username - end - if banned - newuser.suspended_at = Time.now - # banning on Vanilla doesn't have an end, so a thousand years seems equivalent - newuser.suspended_till = 1000.years.from_now - if newuser.save - StaffActionLogger.new(Discourse.system_user).log_user_suspend(newuser, 'Imported from Vanilla Forum') - else - puts "Failed to suspend user #{newuser.username}. #{newuser.errors.full_messages.join(', ')}" + post_create_action: + proc do |newuser| + @last_deleted_username = newuser.username if @user_is_deleted + if banned + newuser.suspended_at = Time.now + # banning on Vanilla doesn't have an end, so a thousand years seems equivalent + newuser.suspended_till = 1000.years.from_now + if newuser.save + StaffActionLogger.new(Discourse.system_user).log_user_suspend( + newuser, + "Imported from Vanilla Forum", + ) + else + puts "Failed to suspend user #{newuser.username}. #{newuser.errors.full_messages.join(", ")}" + end end - end - end } + end, + } end end end @@ -162,7 +160,10 @@ class ImportScripts::VanillaSQL < ImportScripts::Base User.find_each do |u| next unless u.custom_fields["import_id"] - r = mysql_query("SELECT photo FROM #{TABLE_PREFIX}User WHERE UserID = #{u.custom_fields['import_id']};").first + r = + mysql_query( + "SELECT photo FROM #{TABLE_PREFIX}User WHERE UserID = #{u.custom_fields["import_id"]};", + ).first next if r.nil? photo = r["photo"] next unless photo.present? @@ -175,9 +176,9 @@ class ImportScripts::VanillaSQL < ImportScripts::Base photo_real_filename = nil parts = photo.squeeze("/").split("/") if parts[0] =~ /^[a-z0-9]{2}:/ - photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[2..-2].join('/')}".squeeze("/") + photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[2..-2].join("/")}".squeeze("/") elsif parts[0] == "~cf" - photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[1..-2].join('/')}".squeeze("/") + photo_path = "#{ATTACHMENTS_BASE_DIR}/#{parts[1..-2].join("/")}".squeeze("/") else puts "UNKNOWN FORMAT: #{photo}" next @@ -218,7 +219,7 @@ class ImportScripts::VanillaSQL < ImportScripts::Base # Otherwise, the file exists but with a prefix: # The p prefix seems to be the full file, so try to find that one first. - ['p', 't', 'n'].each do |prefix| + %w[p t n].each do |prefix| full_guess = File.join(path, "#{prefix}#{base_guess}") return full_guess if File.exist?(full_guess) end @@ -230,38 +231,43 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_group_users puts "", "importing group users..." - group_users = mysql_query(" + group_users = + mysql_query( + " SELECT RoleID, UserID FROM #{TABLE_PREFIX}UserRole - ").to_a + ", + ).to_a group_users.each do |row| user_id = user_id_from_imported_user_id(row["UserID"]) group_id = group_id_from_imported_group_id(row["RoleID"]) - if user_id && group_id - GroupUser.find_or_create_by(user_id: user_id, group_id: group_id) - end + GroupUser.find_or_create_by(user_id: user_id, group_id: group_id) if user_id && group_id end end def import_categories puts "", "importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT CategoryID, ParentCategoryID, Name, Description FROM #{TABLE_PREFIX}Category WHERE CategoryID > 0 ORDER BY CategoryID ASC - ").to_a + ", + ).to_a - top_level_categories = categories.select { |c| c['ParentCategoryID'].blank? || c['ParentCategoryID'] == -1 } + top_level_categories = + categories.select { |c| c["ParentCategoryID"].blank? || c["ParentCategoryID"] == -1 } create_categories(top_level_categories) do |category| { - id: category['CategoryID'], - name: CGI.unescapeHTML(category['Name']), - description: CGI.unescapeHTML(category['Description']) + id: category["CategoryID"], + name: CGI.unescapeHTML(category["Name"]), + description: CGI.unescapeHTML(category["Description"]), } end @@ -272,37 +278,37 @@ class ImportScripts::VanillaSQL < ImportScripts::Base # Depth = 3 create_categories(subcategories) do |category| { - id: category['CategoryID'], - parent_category_id: category_id_from_imported_category_id(category['ParentCategoryID']), - name: CGI.unescapeHTML(category['Name']), - description: category['Description'] ? CGI.unescapeHTML(category['Description']) : nil, + id: category["CategoryID"], + parent_category_id: category_id_from_imported_category_id(category["ParentCategoryID"]), + name: CGI.unescapeHTML(category["Name"]), + description: category["Description"] ? CGI.unescapeHTML(category["Description"]) : nil, } end - subcategory_ids = Set.new(subcategories.map { |c| c['CategoryID'] }) + subcategory_ids = Set.new(subcategories.map { |c| c["CategoryID"] }) # Depth 4 and 5 need to be tags categories.each do |c| - next if c['ParentCategoryID'] == -1 - next if top_level_category_ids.include?(c['CategoryID']) - next if subcategory_ids.include?(c['CategoryID']) + next if c["ParentCategoryID"] == -1 + next if top_level_category_ids.include?(c["CategoryID"]) + next if subcategory_ids.include?(c["CategoryID"]) # Find a depth 3 category for topics in this category parent = c - while !parent.nil? && !subcategory_ids.include?(parent['CategoryID']) - parent = categories.find { |subcat| subcat['CategoryID'] == parent['ParentCategoryID'] } + while !parent.nil? && !subcategory_ids.include?(parent["CategoryID"]) + parent = categories.find { |subcat| subcat["CategoryID"] == parent["ParentCategoryID"] } end if parent - tag_name = DiscourseTagging.clean_tag(c['Name']) + tag_name = DiscourseTagging.clean_tag(c["Name"]) tag = Tag.find_by_name(tag_name) || Tag.create(name: tag_name) - @category_mappings[c['CategoryID']] = { - category_id: category_id_from_imported_category_id(parent['CategoryID']), - tag: tag[:name] + @category_mappings[c["CategoryID"]] = { + category_id: category_id_from_imported_category_id(parent["CategoryID"]), + tag: tag[:name], } else - puts '', "Couldn't find a category for #{c['CategoryID']} '#{c['Name']}'!" + puts "", "Couldn't find a category for #{c["CategoryID"]} '#{c["Name"]}'!" end end end @@ -310,46 +316,66 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_topics puts "", "importing topics..." - tag_names_sql = "select t.name as tag_name from GDN_Tag t, GDN_TagDiscussion td where t.tagid = td.tagid and td.discussionid = {discussionid} and t.name != '';" + tag_names_sql = + "select t.name as tag_name from GDN_Tag t, GDN_TagDiscussion td where t.tagid = td.tagid and td.discussionid = {discussionid} and t.name != '';" - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}Discussion;").first['count'] + total_count = + mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}Discussion;").first["count"] @last_topic_id = -1 batches(BATCH_SIZE) do |offset| - discussions = mysql_query( - "SELECT DiscussionID, CategoryID, Name, Body, Format, CountViews, Closed, Announce, + discussions = + mysql_query( + "SELECT DiscussionID, CategoryID, Name, Body, Format, CountViews, Closed, Announce, DateInserted, InsertUserID, DateLastComment FROM #{TABLE_PREFIX}Discussion WHERE DiscussionID > #{@last_topic_id} ORDER BY DiscussionID ASC - LIMIT #{BATCH_SIZE};") + LIMIT #{BATCH_SIZE};", + ) break if discussions.size < 1 - @last_topic_id = discussions.to_a.last['DiscussionID'] - next if all_records_exist? :posts, discussions.map { |t| "discussion#" + t['DiscussionID'].to_s } + @last_topic_id = discussions.to_a.last["DiscussionID"] + if all_records_exist? :posts, discussions.map { |t| "discussion#" + t["DiscussionID"].to_s } + next + end create_posts(discussions, total: total_count, offset: offset) do |discussion| - user_id = user_id_from_imported_user_id(discussion['InsertUserID']) || Discourse::SYSTEM_USER_ID + user_id = + user_id_from_imported_user_id(discussion["InsertUserID"]) || Discourse::SYSTEM_USER_ID { - id: "discussion#" + discussion['DiscussionID'].to_s, + id: "discussion#" + discussion["DiscussionID"].to_s, user_id: user_id, - title: discussion['Name'], - category: category_id_from_imported_category_id(discussion['CategoryID']) || @category_mappings[discussion['CategoryID']].try(:[], :category_id), + title: discussion["Name"], + category: + category_id_from_imported_category_id(discussion["CategoryID"]) || + @category_mappings[discussion["CategoryID"]].try(:[], :category_id), raw: get_raw(discussion, user_id), - views: discussion['CountViews'] || 0, - closed: discussion['Closed'] == 1, - pinned_at: discussion['Announce'] == 0 ? nil : Time.zone.at(discussion['DateLastComment'] || discussion['DateInserted']), - pinned_globally: discussion['Announce'] == 1, - created_at: Time.zone.at(discussion['DateInserted']), - post_create_action: proc do |post| - if @import_tags - tag_names = @client.query(tag_names_sql.gsub('{discussionid}', discussion['DiscussionID'].to_s)).map { |row| row['tag_name'] } - category_tag = @category_mappings[discussion['CategoryID']].try(:[], :tag) - tag_names = category_tag ? tag_names.append(category_tag) : tag_names - DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) - end - end + views: discussion["CountViews"] || 0, + closed: discussion["Closed"] == 1, + pinned_at: + ( + if discussion["Announce"] == 0 + nil + else + Time.zone.at(discussion["DateLastComment"] || discussion["DateInserted"]) + end + ), + pinned_globally: discussion["Announce"] == 1, + created_at: Time.zone.at(discussion["DateInserted"]), + post_create_action: + proc do |post| + if @import_tags + tag_names = + @client + .query(tag_names_sql.gsub("{discussionid}", discussion["DiscussionID"].to_s)) + .map { |row| row["tag_name"] } + category_tag = @category_mappings[discussion["CategoryID"]].try(:[], :tag) + tag_names = category_tag ? tag_names.append(category_tag) : tag_names + DiscourseTagging.tag_topic_by_names(post.topic, staff_guardian, tag_names) + end + end, } end end @@ -358,36 +384,42 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_posts puts "", "importing posts..." - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}Comment;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}Comment;").first["count"] @last_post_id = -1 batches(BATCH_SIZE) do |offset| - comments = mysql_query( - "SELECT CommentID, DiscussionID, Body, Format, + comments = + mysql_query( + "SELECT CommentID, DiscussionID, Body, Format, DateInserted, InsertUserID, QnA FROM #{TABLE_PREFIX}Comment WHERE CommentID > #{@last_post_id} ORDER BY CommentID ASC - LIMIT #{BATCH_SIZE};") + LIMIT #{BATCH_SIZE};", + ) break if comments.size < 1 - @last_post_id = comments.to_a.last['CommentID'] - next if all_records_exist? :posts, comments.map { |comment| "comment#" + comment['CommentID'].to_s } + @last_post_id = comments.to_a.last["CommentID"] + if all_records_exist? :posts, + comments.map { |comment| "comment#" + comment["CommentID"].to_s } + next + end create_posts(comments, total: total_count, offset: offset) do |comment| - next unless t = topic_lookup_from_imported_post_id("discussion#" + comment['DiscussionID'].to_s) - next if comment['Body'].blank? - user_id = user_id_from_imported_user_id(comment['InsertUserID']) || Discourse::SYSTEM_USER_ID + unless t = topic_lookup_from_imported_post_id("discussion#" + comment["DiscussionID"].to_s) + next + end + next if comment["Body"].blank? + user_id = + user_id_from_imported_user_id(comment["InsertUserID"]) || Discourse::SYSTEM_USER_ID post = { - id: "comment#" + comment['CommentID'].to_s, + id: "comment#" + comment["CommentID"].to_s, user_id: user_id, topic_id: t[:topic_id], raw: get_raw(comment, user_id), - created_at: Time.zone.at(comment['DateInserted']) + created_at: Time.zone.at(comment["DateInserted"]), } - if comment['QnA'] == "Accepted" - post[:custom_fields] = { is_accepted_answer: true } - end + post[:custom_fields] = { is_accepted_answer: true } if comment["QnA"] == "Accepted" post end @@ -397,19 +429,22 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_likes puts "", "importing likes..." - total_count = mysql_query("SELECT count(*) count FROM GDN_ThanksLog;").first['count'] + total_count = mysql_query("SELECT count(*) count FROM GDN_ThanksLog;").first["count"] current_count = 0 start_time = Time.now - likes = mysql_query(" + likes = + mysql_query( + " SELECT CommentID, DateInserted, InsertUserID FROM #{TABLE_PREFIX}ThanksLog ORDER BY CommentID ASC; - ") + ", + ) likes.each do |like| - post_id = post_id_from_imported_post_id("comment##{like['CommentID']}") - user_id = user_id_from_imported_user_id(like['InsertUserID']) + post_id = post_id_from_imported_post_id("comment##{like["CommentID"]}") + user_id = user_id_from_imported_user_id(like["InsertUserID"]) post = Post.find(post_id) if post_id user = User.find(user_id) if user_id @@ -428,51 +463,58 @@ class ImportScripts::VanillaSQL < ImportScripts::Base def import_messages puts "", "importing messages..." - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}ConversationMessage;").first['count'] + total_count = + mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}ConversationMessage;").first["count"] @last_message_id = -1 batches(BATCH_SIZE) do |offset| - messages = mysql_query( - "SELECT m.MessageID, m.Body, m.Format, + messages = + mysql_query( + "SELECT m.MessageID, m.Body, m.Format, m.InsertUserID, m.DateInserted, m.ConversationID, c.Contributors FROM #{TABLE_PREFIX}ConversationMessage m INNER JOIN #{TABLE_PREFIX}Conversation c on c.ConversationID = m.ConversationID WHERE m.MessageID > #{@last_message_id} ORDER BY m.MessageID ASC - LIMIT #{BATCH_SIZE};") + LIMIT #{BATCH_SIZE};", + ) break if messages.size < 1 - @last_message_id = messages.to_a.last['MessageID'] - next if all_records_exist? :posts, messages.map { |t| "message#" + t['MessageID'].to_s } + @last_message_id = messages.to_a.last["MessageID"] + next if all_records_exist? :posts, messages.map { |t| "message#" + t["MessageID"].to_s } create_posts(messages, total: total_count, offset: offset) do |message| - user_id = user_id_from_imported_user_id(message['InsertUserID']) || Discourse::SYSTEM_USER_ID + user_id = + user_id_from_imported_user_id(message["InsertUserID"]) || Discourse::SYSTEM_USER_ID body = get_raw(message, user_id) common = { user_id: user_id, raw: body, - created_at: Time.zone.at(message['DateInserted']), + created_at: Time.zone.at(message["DateInserted"]), custom_fields: { - conversation_id: message['ConversationID'], - participants: message['Contributors'], - message_id: message['MessageID'] - } + conversation_id: message["ConversationID"], + participants: message["Contributors"], + message_id: message["MessageID"], + }, } - conversation_id = "conversation#" + message['ConversationID'].to_s - message_id = "message#" + message['MessageID'].to_s + conversation_id = "conversation#" + message["ConversationID"].to_s + message_id = "message#" + message["MessageID"].to_s imported_conversation = topic_lookup_from_imported_post_id(conversation_id) if imported_conversation.present? common.merge(id: message_id, topic_id: imported_conversation[:topic_id]) else - user_ids = (message['Contributors'] || '').scan(/\"(\d+)\"/).flatten.map(&:to_i) - usernames = user_ids.map { |id| @lookup.find_user_by_import_id(id).try(:username) }.compact - usernames = [@lookup.find_user_by_import_id(message['InsertUserID']).try(:username)].compact if usernames.empty? + user_ids = (message["Contributors"] || "").scan(/\"(\d+)\"/).flatten.map(&:to_i) + usernames = + user_ids.map { |id| @lookup.find_user_by_import_id(id).try(:username) }.compact + usernames = [ + @lookup.find_user_by_import_id(message["InsertUserID"]).try(:username), + ].compact if usernames.empty? title = body.truncate(40) { @@ -487,8 +529,8 @@ class ImportScripts::VanillaSQL < ImportScripts::Base end def get_raw(record, user_id) - format = (record['Format'] || "").downcase - body = record['Body'] + format = (record["Format"] || "").downcase + body = record["Body"] case format when "html" @@ -507,7 +549,7 @@ class ImportScripts::VanillaSQL < ImportScripts::Base raw = @htmlentities.decode(raw) # convert user profile links to user mentions - raw.gsub!(/(@\S+?)<\/a>/) { $1 } + raw.gsub!(%r{(@\S+?)}) { $1 } raw = ReverseMarkdown.convert(raw) unless skip_reverse_markdown @@ -526,14 +568,21 @@ class ImportScripts::VanillaSQL < ImportScripts::Base end def create_permalinks - puts '', 'Creating redirects...', '' + puts "", "Creating redirects...", "" User.find_each do |u| ucf = u.custom_fields if ucf && ucf["import_id"] && ucf["import_username"] - encoded_username = CGI.escape(ucf['import_username']).gsub('+', '%20') - Permalink.create(url: "profile/#{ucf['import_id']}/#{encoded_username}", external_url: "/users/#{u.username}") rescue nil - print '.' + encoded_username = CGI.escape(ucf["import_username"]).gsub("+", "%20") + begin + Permalink.create( + url: "profile/#{ucf["import_id"]}/#{encoded_username}", + external_url: "/users/#{u.username}", + ) + rescue StandardError + nil + end + print "." end end @@ -541,14 +590,22 @@ class ImportScripts::VanillaSQL < ImportScripts::Base pcf = post.custom_fields if pcf && pcf["import_id"] topic = post.topic - id = pcf["import_id"].split('#').last + id = pcf["import_id"].split("#").last if post.post_number == 1 slug = Slug.for(topic.title) # probably matches what vanilla would do... - Permalink.create(url: "discussion/#{id}/#{slug}", topic_id: topic.id) rescue nil + begin + Permalink.create(url: "discussion/#{id}/#{slug}", topic_id: topic.id) + rescue StandardError + nil + end else - Permalink.create(url: "discussion/comment/#{id}", post_id: post.id) rescue nil + begin + Permalink.create(url: "discussion/comment/#{id}", post_id: post.id) + rescue StandardError + nil + end end - print '.' + print "." end end end @@ -561,75 +618,86 @@ class ImportScripts::VanillaSQL < ImportScripts::Base count = 0 # https://us.v-cdn.net/1234567/uploads/editor/xyz/image.jpg - cdn_regex = /https:\/\/us.v-cdn.net\/1234567\/uploads\/(\S+\/(\w|-)+.\w+)/i + cdn_regex = %r{https://us.v-cdn.net/1234567/uploads/(\S+/(\w|-)+.\w+)}i # [attachment=10109:Screen Shot 2012-04-01 at 3.47.35 AM.png] attachment_regex = /\[attachment=(\d+):(.*?)\]/i - Post.where("raw LIKE '%/us.v-cdn.net/%' OR raw LIKE '%[attachment%'").find_each do |post| - count += 1 - print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] - new_raw = post.raw.dup + Post + .where("raw LIKE '%/us.v-cdn.net/%' OR raw LIKE '%[attachment%'") + .find_each do |post| + count += 1 + print "\r%7d - %6d/sec" % [count, count.to_f / (Time.now - start)] + new_raw = post.raw.dup - new_raw.gsub!(attachment_regex) do |s| - matches = attachment_regex.match(s) - attachment_id = matches[1] - file_name = matches[2] - next unless attachment_id + new_raw.gsub!(attachment_regex) do |s| + matches = attachment_regex.match(s) + attachment_id = matches[1] + file_name = matches[2] + next unless attachment_id - r = mysql_query("SELECT Path, Name FROM #{TABLE_PREFIX}Media WHERE MediaID = #{attachment_id};").first - next if r.nil? - path = r["Path"] - name = r["Name"] - next unless path.present? + r = + mysql_query( + "SELECT Path, Name FROM #{TABLE_PREFIX}Media WHERE MediaID = #{attachment_id};", + ).first + next if r.nil? + path = r["Path"] + name = r["Name"] + next unless path.present? - path.gsub!("s3://content/", "") - path.gsub!("s3://uploads/", "") - file_path = "#{ATTACHMENTS_BASE_DIR}/#{path}" + path.gsub!("s3://content/", "") + path.gsub!("s3://uploads/", "") + file_path = "#{ATTACHMENTS_BASE_DIR}/#{path}" - if File.exist?(file_path) - upload = create_upload(post.user.id, file_path, File.basename(file_path)) - if upload && upload.errors.empty? - # upload.url - filename = name || file_name || File.basename(file_path) - html_for_upload(upload, normalize_text(filename)) + if File.exist?(file_path) + upload = create_upload(post.user.id, file_path, File.basename(file_path)) + if upload && upload.errors.empty? + # upload.url + filename = name || file_name || File.basename(file_path) + html_for_upload(upload, normalize_text(filename)) + else + puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + end else - puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + puts "Couldn't find file for #{attachment_id}. Skipping." + next end - else - puts "Couldn't find file for #{attachment_id}. Skipping." - next end - end - new_raw.gsub!(cdn_regex) do |s| - matches = cdn_regex.match(s) - attachment_id = matches[1] + new_raw.gsub!(cdn_regex) do |s| + matches = cdn_regex.match(s) + attachment_id = matches[1] - file_path = "#{ATTACHMENTS_BASE_DIR}/#{attachment_id}" + file_path = "#{ATTACHMENTS_BASE_DIR}/#{attachment_id}" - if File.exist?(file_path) - upload = create_upload(post.user.id, file_path, File.basename(file_path)) - if upload && upload.errors.empty? - upload.url + if File.exist?(file_path) + upload = create_upload(post.user.id, file_path, File.basename(file_path)) + if upload && upload.errors.empty? + upload.url + else + puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + end else - puts "Error: Upload did not persist for #{post.id} #{attachment_id}!" + puts "Couldn't find file for #{attachment_id}. Skipping." + next end - else - puts "Couldn't find file for #{attachment_id}. Skipping." - next end - end - if new_raw != post.raw - begin - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, skip_revision: true, skip_validations: true, bypass_bump: true) - rescue - puts "PostRevisor error for #{post.id}" - post.raw = new_raw - post.save(validate: false) + if new_raw != post.raw + begin + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + skip_revision: true, + skip_validations: true, + bypass_bump: true, + ) + rescue StandardError + puts "PostRevisor error for #{post.id}" + post.raw = new_raw + post.save(validate: false) + end end end - end end end diff --git a/script/import_scripts/vbulletin.rb b/script/import_scripts/vbulletin.rb index 8b3de80bdc..f534b43848 100644 --- a/script/import_scripts/vbulletin.rb +++ b/script/import_scripts/vbulletin.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' +require "htmlentities" begin - require 'php_serialize' # https://github.com/jqr/php-serialize + require "php_serialize" # https://github.com/jqr/php-serialize rescue LoadError puts - puts 'php_serialize not found.' - puts 'Add to Gemfile, like this: ' + puts "php_serialize not found." + puts "Add to Gemfile, like this: " puts puts "echo gem \\'php-serialize\\' >> Gemfile" puts "bundle install" @@ -23,13 +23,13 @@ class ImportScripts::VBulletin < ImportScripts::Base # CHANGE THESE BEFORE RUNNING THE IMPORTER - DB_HOST ||= ENV['DB_HOST'] || "localhost" - DB_NAME ||= ENV['DB_NAME'] || "vbulletin" - DB_PW ||= ENV['DB_PW'] || "" - DB_USER ||= ENV['DB_USER'] || "root" - TIMEZONE ||= ENV['TIMEZONE'] || "America/Los_Angeles" - TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "vb_" - ATTACHMENT_DIR ||= ENV['ATTACHMENT_DIR'] || '/path/to/your/attachment/folder' + DB_HOST ||= ENV["DB_HOST"] || "localhost" + DB_NAME ||= ENV["DB_NAME"] || "vbulletin" + DB_PW ||= ENV["DB_PW"] || "" + DB_USER ||= ENV["DB_USER"] || "root" + TIMEZONE ||= ENV["TIMEZONE"] || "America/Los_Angeles" + TABLE_PREFIX ||= ENV["TABLE_PREFIX"] || "vb_" + ATTACHMENT_DIR ||= ENV["ATTACHMENT_DIR"] || "/path/to/your/attachment/folder" puts "#{DB_USER}:#{DB_PW}@#{DB_HOST} wants #{DB_NAME}" @@ -44,16 +44,12 @@ class ImportScripts::VBulletin < ImportScripts::Base @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: DB_HOST, - username: DB_USER, - password: DB_PW, - database: DB_NAME - ) - rescue Exception => e - puts '=' * 50 - puts e.message - puts <<~TEXT + @client = + Mysql2::Client.new(host: DB_HOST, username: DB_USER, password: DB_PW, database: DB_NAME) + rescue Exception => e + puts "=" * 50 + puts e.message + puts <<~TEXT Cannot connect in to database. Hostname: #{DB_HOST} @@ -72,11 +68,15 @@ class ImportScripts::VBulletin < ImportScripts::Base Exiting. TEXT - exit + exit end def execute - mysql_query("CREATE INDEX firstpostid_index ON #{TABLE_PREFIX}thread (firstpostid)") rescue nil + begin + mysql_query("CREATE INDEX firstpostid_index ON #{TABLE_PREFIX}thread (firstpostid)") + rescue StandardError + nil + end import_groups import_users @@ -104,10 +104,7 @@ class ImportScripts::VBulletin < ImportScripts::Base SQL create_groups(groups) do |group| - { - id: group["usergroupid"], - name: @htmlentities.decode(group["title"]).strip - } + { id: group["usergroupid"], name: @htmlentities.decode(group["title"]).strip } end end @@ -127,7 +124,7 @@ class ImportScripts::VBulletin < ImportScripts::Base last_user_id = -1 batches(BATCH_SIZE) do |offset| - users = mysql_query(<<-SQL + users = mysql_query(<<-SQL).to_a SELECT userid , username , homepage @@ -142,7 +139,6 @@ class ImportScripts::VBulletin < ImportScripts::Base ORDER BY userid LIMIT #{BATCH_SIZE} SQL - ).to_a break if users.empty? @@ -169,15 +165,21 @@ class ImportScripts::VBulletin < ImportScripts::Base primary_group_id: group_id_from_imported_group_id(user["usergroupid"].to_i), created_at: parse_timestamp(user["joindate"]), last_seen_at: parse_timestamp(user["lastvisit"]), - post_create_action: proc do |u| - import_profile_picture(user, u) - import_profile_background(user, u) - end + post_create_action: + proc do |u| + import_profile_picture(user, u) + import_profile_background(user, u) + end, } end end - @usernames = UserCustomField.joins(:user).where(name: 'import_username').pluck('user_custom_fields.value', 'users.username').to_h + @usernames = + UserCustomField + .joins(:user) + .where(name: "import_username") + .pluck("user_custom_fields.value", "users.username") + .to_h end def create_groups_membership @@ -190,7 +192,10 @@ class ImportScripts::VBulletin < ImportScripts::Base next if GroupUser.where(group_id: group.id).count > 0 user_ids_in_group = User.where(primary_group_id: group.id).pluck(:id).to_a next if user_ids_in_group.size == 0 - values = user_ids_in_group.map { |user_id| "(#{group.id}, #{user_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" }.join(",") + values = + user_ids_in_group + .map { |user_id| "(#{group.id}, #{user_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" } + .join(",") DB.exec <<~SQL INSERT INTO group_users (group_id, user_id, created_at, updated_at) VALUES #{values} @@ -230,8 +235,16 @@ class ImportScripts::VBulletin < ImportScripts::Base imported_user.user_avatar.update(custom_upload_id: upload.id) imported_user.update(uploaded_avatar_id: upload.id) ensure - file.close rescue nil - file.unlind rescue nil + begin + file.close + rescue StandardError + nil + end + begin + file.unlind + rescue StandardError + nil + end end def import_profile_background(old_user, imported_user) @@ -258,14 +271,25 @@ class ImportScripts::VBulletin < ImportScripts::Base imported_user.user_profile.upload_profile_background(upload) ensure - file.close rescue nil - file.unlink rescue nil + begin + file.close + rescue StandardError + nil + end + begin + file.unlink + rescue StandardError + nil + end end def import_categories puts "", "importing top level categories..." - categories = mysql_query("SELECT forumid, title, description, displayorder, parentid FROM #{TABLE_PREFIX}forum ORDER BY forumid").to_a + categories = + mysql_query( + "SELECT forumid, title, description, displayorder, parentid FROM #{TABLE_PREFIX}forum ORDER BY forumid", + ).to_a top_level_categories = categories.select { |c| c["parentid"] == -1 } @@ -274,7 +298,7 @@ class ImportScripts::VBulletin < ImportScripts::Base id: category["forumid"], name: @htmlentities.decode(category["title"]).strip, position: category["displayorder"], - description: @htmlentities.decode(category["description"]).strip + description: @htmlentities.decode(category["description"]).strip, } end @@ -296,7 +320,7 @@ class ImportScripts::VBulletin < ImportScripts::Base name: @htmlentities.decode(category["title"]).strip, position: category["displayorder"], description: @htmlentities.decode(category["description"]).strip, - parent_category_id: category_id_from_imported_category_id(category["parentid"]) + parent_category_id: category_id_from_imported_category_id(category["parentid"]), } end end @@ -304,12 +328,13 @@ class ImportScripts::VBulletin < ImportScripts::Base def import_topics puts "", "importing topics..." - topic_count = mysql_query("SELECT COUNT(threadid) count FROM #{TABLE_PREFIX}thread").first["count"] + topic_count = + mysql_query("SELECT COUNT(threadid) count FROM #{TABLE_PREFIX}thread").first["count"] last_topic_id = -1 batches(BATCH_SIZE) do |offset| - topics = mysql_query(<<-SQL + topics = mysql_query(<<-SQL).to_a SELECT t.threadid threadid, t.title title, forumid, open, postuserid, t.dateline dateline, views, t.visible visible, sticky, p.pagetext raw FROM #{TABLE_PREFIX}thread t @@ -318,7 +343,6 @@ class ImportScripts::VBulletin < ImportScripts::Base ORDER BY t.threadid LIMIT #{BATCH_SIZE} SQL - ).to_a break if topics.empty? @@ -326,7 +350,12 @@ class ImportScripts::VBulletin < ImportScripts::Base topics.reject! { |t| @lookup.post_already_imported?("thread-#{t["threadid"]}") } create_posts(topics, total: topic_count, offset: offset) do |topic| - raw = preprocess_post_raw(topic["raw"]) rescue nil + raw = + begin + preprocess_post_raw(topic["raw"]) + rescue StandardError + nil + end next if raw.blank? topic_id = "thread-#{topic["threadid"]}" t = { @@ -351,28 +380,28 @@ class ImportScripts::VBulletin < ImportScripts::Base topic = topic_lookup_from_imported_post_id(topic_id) if topic.present? url_slug = "thread/#{thread["threadid"]}" if thread["title"].present? - Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) if url_slug.present? && topic[:topic_id].present? + if url_slug.present? && topic[:topic_id].present? + Permalink.create(url: url_slug, topic_id: topic[:topic_id].to_i) + end end end - end end def import_posts puts "", "importing posts..." - post_count = mysql_query(<<-SQL + post_count = mysql_query(<<-SQL).first["count"] SELECT COUNT(postid) count FROM #{TABLE_PREFIX}post p JOIN #{TABLE_PREFIX}thread t ON t.threadid = p.threadid WHERE t.firstpostid <> p.postid SQL - ).first["count"] last_post_id = -1 batches(BATCH_SIZE) do |offset| - posts = mysql_query(<<-SQL + posts = mysql_query(<<-SQL).to_a SELECT p.postid, p.userid, p.threadid, p.pagetext raw, p.dateline, p.visible, p.parentid FROM #{TABLE_PREFIX}post p JOIN #{TABLE_PREFIX}thread t ON t.threadid = p.threadid @@ -381,7 +410,6 @@ class ImportScripts::VBulletin < ImportScripts::Base ORDER BY p.postid LIMIT #{BATCH_SIZE} SQL - ).to_a break if posts.empty? @@ -389,7 +417,12 @@ class ImportScripts::VBulletin < ImportScripts::Base posts.reject! { |p| @lookup.post_already_imported?(p["postid"].to_i) } create_posts(posts, total: post_count, offset: offset) do |post| - raw = preprocess_post_raw(post["raw"]) rescue nil + raw = + begin + preprocess_post_raw(post["raw"]) + rescue StandardError + nil + end next if raw.blank? next unless topic = topic_lookup_from_imported_post_id("thread-#{post["threadid"]}") p = { @@ -410,7 +443,8 @@ class ImportScripts::VBulletin < ImportScripts::Base # find the uploaded file information from the db def find_upload(post, attachment_id) - sql = "SELECT a.attachmentid attachment_id, a.userid user_id, a.filedataid file_id, a.filename filename, + sql = + "SELECT a.attachmentid attachment_id, a.userid user_id, a.filedataid file_id, a.filename filename, LENGTH(fd.filedata) AS dbsize, filedata, a.caption caption FROM #{TABLE_PREFIX}attachment a LEFT JOIN #{TABLE_PREFIX}filedata fd ON fd.filedataid = a.filedataid @@ -418,25 +452,24 @@ class ImportScripts::VBulletin < ImportScripts::Base results = mysql_query(sql) unless row = results.first - puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields['import_id']}" + puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields["import_id"]}" return end - filename = File.join(ATTACHMENT_DIR, row['user_id'].to_s.split('').join('/'), "#{row['file_id']}.attach") - real_filename = row['filename'] - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + filename = + File.join(ATTACHMENT_DIR, row["user_id"].to_s.split("").join("/"), "#{row["file_id"]}.attach") + real_filename = row["filename"] + real_filename.prepend SecureRandom.hex if real_filename[0] == "." unless File.exist?(filename) - if row['dbsize'].to_i == 0 - puts "Attachment file #{row['filedataid']} doesn't exist" + if row["dbsize"].to_i == 0 + puts "Attachment file #{row["filedataid"]} doesn't exist" return nil end - tmpfile = 'attach_' + row['filedataid'].to_s - filename = File.join('/tmp/', tmpfile) - File.open(filename, 'wb') { |f| - f.write(row['filedata']) - } + tmpfile = "attach_" + row["filedataid"].to_s + filename = File.join("/tmp/", tmpfile) + File.open(filename, "wb") { |f| f.write(row["filedata"]) } end upload = create_upload(post.user.id, filename, real_filename) @@ -457,24 +490,24 @@ class ImportScripts::VBulletin < ImportScripts::Base def import_private_messages puts "", "importing private messages..." - topic_count = mysql_query("SELECT COUNT(pmtextid) count FROM #{TABLE_PREFIX}pmtext").first["count"] + topic_count = + mysql_query("SELECT COUNT(pmtextid) count FROM #{TABLE_PREFIX}pmtext").first["count"] last_private_message_id = -1 batches(BATCH_SIZE) do |offset| - private_messages = mysql_query(<<-SQL + private_messages = mysql_query(<<-SQL).to_a SELECT pmtextid, fromuserid, title, message, touserarray, dateline FROM #{TABLE_PREFIX}pmtext WHERE pmtextid > #{last_private_message_id} ORDER BY pmtextid LIMIT #{BATCH_SIZE} SQL - ).to_a break if private_messages.empty? last_private_message_id = private_messages[-1]["pmtextid"] - private_messages.reject! { |pm| @lookup.post_already_imported?("pm-#{pm['pmtextid']}") } + private_messages.reject! { |pm| @lookup.post_already_imported?("pm-#{pm["pmtextid"]}") } title_username_of_pm_first_post = {} @@ -482,11 +515,16 @@ class ImportScripts::VBulletin < ImportScripts::Base skip = false mapped = {} - mapped[:id] = "pm-#{m['pmtextid']}" - mapped[:user_id] = user_id_from_imported_user_id(m['fromuserid']) || Discourse::SYSTEM_USER_ID - mapped[:raw] = preprocess_post_raw(m['message']) rescue nil - mapped[:created_at] = Time.zone.at(m['dateline']) - title = @htmlentities.decode(m['title']).strip[0...255] + mapped[:id] = "pm-#{m["pmtextid"]}" + mapped[:user_id] = user_id_from_imported_user_id(m["fromuserid"]) || + Discourse::SYSTEM_USER_ID + mapped[:raw] = begin + preprocess_post_raw(m["message"]) + rescue StandardError + nil + end + mapped[:created_at] = Time.zone.at(m["dateline"]) + title = @htmlentities.decode(m["title"]).strip[0...255] topic_id = nil next if mapped[:raw].blank? @@ -495,9 +533,9 @@ class ImportScripts::VBulletin < ImportScripts::Base target_usernames = [] target_userids = [] begin - to_user_array = PHP.unserialize(m['touserarray']) - rescue - puts "#{m['pmtextid']} -- #{m['touserarray']}" + to_user_array = PHP.unserialize(m["touserarray"]) + rescue StandardError + puts "#{m["pmtextid"]} -- #{m["touserarray"]}" skip = true end @@ -517,8 +555,8 @@ class ImportScripts::VBulletin < ImportScripts::Base target_usernames << username if username end end - rescue - puts "skipping pm-#{m['pmtextid']} `to_user_array` is not properly serialized -- #{to_user_array.inspect}" + rescue StandardError + puts "skipping pm-#{m["pmtextid"]} `to_user_array` is not properly serialized -- #{to_user_array.inspect}" skip = true end @@ -526,18 +564,18 @@ class ImportScripts::VBulletin < ImportScripts::Base participants << mapped[:user_id] begin participants.sort! - rescue + rescue StandardError puts "one of the participant's id is nil -- #{participants.inspect}" end if title =~ /^Re:/ - - parent_id = title_username_of_pm_first_post[[title[3..-1], participants]] || - title_username_of_pm_first_post[[title[4..-1], participants]] || - title_username_of_pm_first_post[[title[5..-1], participants]] || - title_username_of_pm_first_post[[title[6..-1], participants]] || - title_username_of_pm_first_post[[title[7..-1], participants]] || - title_username_of_pm_first_post[[title[8..-1], participants]] + parent_id = + title_username_of_pm_first_post[[title[3..-1], participants]] || + title_username_of_pm_first_post[[title[4..-1], participants]] || + title_username_of_pm_first_post[[title[5..-1], participants]] || + title_username_of_pm_first_post[[title[6..-1], participants]] || + title_username_of_pm_first_post[[title[7..-1], participants]] || + title_username_of_pm_first_post[[title[8..-1], participants]] if parent_id if t = topic_lookup_from_imported_post_id("pm-#{parent_id}") @@ -545,18 +583,18 @@ class ImportScripts::VBulletin < ImportScripts::Base end end else - title_username_of_pm_first_post[[title, participants]] ||= m['pmtextid'] + title_username_of_pm_first_post[[title, participants]] ||= m["pmtextid"] end unless topic_id mapped[:title] = title mapped[:archetype] = Archetype.private_message - mapped[:target_usernames] = target_usernames.join(',') + mapped[:target_usernames] = target_usernames.join(",") if mapped[:target_usernames].size < 1 # pm with yourself? # skip = true mapped[:target_usernames] = "system" - puts "pm-#{m['pmtextid']} has no target (#{m['touserarray']})" + puts "pm-#{m["pmtextid"]} has no target (#{m["touserarray"]})" end else mapped[:topic_id] = topic_id @@ -568,25 +606,24 @@ class ImportScripts::VBulletin < ImportScripts::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." mapping = {} - attachments = mysql_query(<<-SQL + attachments = mysql_query(<<-SQL) SELECT a.attachmentid, a.contentid as postid, p.threadid FROM #{TABLE_PREFIX}attachment a, #{TABLE_PREFIX}post p WHERE a.contentid = p.postid AND contenttypeid = 1 AND state = 'visible' SQL - ) attachments.each do |attachment| - post_id = post_id_from_imported_post_id(attachment['postid']) - post_id = post_id_from_imported_post_id("thread-#{attachment['threadid']}") unless post_id + post_id = post_id_from_imported_post_id(attachment["postid"]) + post_id = post_id_from_imported_post_id("thread-#{attachment["threadid"]}") unless post_id if post_id.nil? - puts "Post for attachment #{attachment['attachmentid']} not found" + puts "Post for attachment #{attachment["attachmentid"]} not found" next end mapping[post_id] ||= [] - mapping[post_id] << attachment['attachmentid'].to_i + mapping[post_id] << attachment["attachmentid"].to_i end current_count = 0 @@ -594,7 +631,7 @@ class ImportScripts::VBulletin < ImportScripts::Base success_count = 0 fail_count = 0 - attachment_regex = /\[attach[^\]]*\](\d+)\[\/attach\]/i + attachment_regex = %r{\[attach[^\]]*\](\d+)\[/attach\]}i Post.find_each do |post| current_count += 1 @@ -605,9 +642,7 @@ class ImportScripts::VBulletin < ImportScripts::Base matches = attachment_regex.match(s) attachment_id = matches[1] - unless mapping[post.id].nil? - mapping[post.id].delete(attachment_id.to_i) - end + mapping[post.id].delete(attachment_id.to_i) unless mapping[post.id].nil? upload, filename = find_upload(post, attachment_id) unless upload @@ -621,13 +656,12 @@ class ImportScripts::VBulletin < ImportScripts::Base # make resumed imports faster if new_raw == post.raw unless mapping[post.id].nil? || mapping[post.id].empty? - imported_text = mysql_query(<<-SQL + imported_text = mysql_query(<<-SQL).first["pagetext"] SELECT p.pagetext FROM #{TABLE_PREFIX}attachment a, #{TABLE_PREFIX}post p WHERE a.contentid = p.postid AND a.attachmentid = #{mapping[post.id][0]} SQL - ).first["pagetext"] imported_text.scan(attachment_regex) do |match| attachment_id = match[0] @@ -646,14 +680,17 @@ class ImportScripts::VBulletin < ImportScripts::Base # internal upload deduplication will make sure that we do not import attachments again html = html_for_upload(upload, filename) - if !new_raw[html] - new_raw += "\n\n#{html}\n\n" - end + new_raw += "\n\n#{html}\n\n" if !new_raw[html] end end if new_raw != post.raw - PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: 'Import attachments from vBulletin') + PostRevisor.new(post).revise!( + post.user, + { raw: new_raw }, + bypass_bump: true, + edit_reason: "Import attachments from vBulletin", + ) end success_count += 1 @@ -728,22 +765,22 @@ class ImportScripts::VBulletin < ImportScripts::Base # [HTML]...[/HTML] raw.gsub!(/\[html\]/i, "\n```html\n") - raw.gsub!(/\[\/html\]/i, "\n```\n") + raw.gsub!(%r{\[/html\]}i, "\n```\n") # [PHP]...[/PHP] raw.gsub!(/\[php\]/i, "\n```php\n") - raw.gsub!(/\[\/php\]/i, "\n```\n") + raw.gsub!(%r{\[/php\]}i, "\n```\n") # [HIGHLIGHT="..."] raw.gsub!(/\[highlight="?(\w+)"?\]/i) { "\n```#{$1.downcase}\n" } # [CODE]...[/CODE] # [HIGHLIGHT]...[/HIGHLIGHT] - raw.gsub!(/\[\/?code\]/i, "\n```\n") - raw.gsub!(/\[\/?highlight\]/i, "\n```\n") + raw.gsub!(%r{\[/?code\]}i, "\n```\n") + raw.gsub!(%r{\[/?highlight\]}i, "\n```\n") # [SAMP]...[/SAMP] - raw.gsub!(/\[\/?samp\]/i, "`") + raw.gsub!(%r{\[/?samp\]}i, "`") # replace all chevrons with HTML entities # NOTE: must be done @@ -758,96 +795,99 @@ class ImportScripts::VBulletin < ImportScripts::Base raw.gsub!("\u2603", ">") # [URL=...]...[/URL] - raw.gsub!(/\[url="?([^"]+?)"?\](.*?)\[\/url\]/im) { "[#{$2.strip}](#{$1})" } - raw.gsub!(/\[url="?(.+?)"?\](.+)\[\/url\]/im) { "[#{$2.strip}](#{$1})" } + raw.gsub!(%r{\[url="?([^"]+?)"?\](.*?)\[/url\]}im) { "[#{$2.strip}](#{$1})" } + raw.gsub!(%r{\[url="?(.+?)"?\](.+)\[/url\]}im) { "[#{$2.strip}](#{$1})" } # [URL]...[/URL] # [MP3]...[/MP3] - raw.gsub!(/\[\/?url\]/i, "") - raw.gsub!(/\[\/?mp3\]/i, "") + raw.gsub!(%r{\[/?url\]}i, "") + raw.gsub!(%r{\[/?mp3\]}i, "") # [MENTION][/MENTION] - raw.gsub!(/\[mention\](.+?)\[\/mention\]/i) do + raw.gsub!(%r{\[mention\](.+?)\[/mention\]}i) do new_username = get_username_for_old_username($1) "@#{new_username}" end # [FONT=blah] and [COLOR=blah] - raw.gsub! /\[FONT=.*?\](.*?)\[\/FONT\]/im, '\1' - raw.gsub! /\[COLOR=.*?\](.*?)\[\/COLOR\]/im, '\1' - raw.gsub! /\[COLOR=#.*?\](.*?)\[\/COLOR\]/im, '\1' + raw.gsub! %r{\[FONT=.*?\](.*?)\[/FONT\]}im, '\1' + raw.gsub! %r{\[COLOR=.*?\](.*?)\[/COLOR\]}im, '\1' + raw.gsub! %r{\[COLOR=#.*?\](.*?)\[/COLOR\]}im, '\1' - raw.gsub! /\[SIZE=.*?\](.*?)\[\/SIZE\]/im, '\1' - raw.gsub! /\[SUP\](.*?)\[\/SUP\]/im, '\1' - raw.gsub! /\[h=.*?\](.*?)\[\/h\]/im, '\1' + raw.gsub! %r{\[SIZE=.*?\](.*?)\[/SIZE\]}im, '\1' + raw.gsub! %r{\[SUP\](.*?)\[/SUP\]}im, '\1' + raw.gsub! %r{\[h=.*?\](.*?)\[/h\]}im, '\1' # [CENTER]...[/CENTER] - raw.gsub! /\[CENTER\](.*?)\[\/CENTER\]/im, '\1' + raw.gsub! %r{\[CENTER\](.*?)\[/CENTER\]}im, '\1' # [INDENT]...[/INDENT] - raw.gsub! /\[INDENT\](.*?)\[\/INDENT\]/im, '\1' + raw.gsub! %r{\[INDENT\](.*?)\[/INDENT\]}im, '\1' # Tables to MD - raw.gsub!(/\[TABLE.*?\](.*?)\[\/TABLE\]/im) { |t| - rows = $1.gsub!(/\s*\[TR\](.*?)\[\/TR\]\s*/im) { |r| - cols = $1.gsub! /\s*\[TD.*?\](.*?)\[\/TD\]\s*/im, '|\1' - "#{cols}|\n" - } + raw.gsub!(%r{\[TABLE.*?\](.*?)\[/TABLE\]}im) do |t| + rows = + $1.gsub!(%r{\s*\[TR\](.*?)\[/TR\]\s*}im) do |r| + cols = $1.gsub! %r{\s*\[TD.*?\](.*?)\[/TD\]\s*}im, '|\1' + "#{cols}|\n" + end header, rest = rows.split "\n", 2 c = header.count "|" sep = "|---" * (c - 1) "#{header}\n#{sep}|\n#{rest}\n" - } + end # [QUOTE]...[/QUOTE] - raw.gsub!(/\[quote\](.+?)\[\/quote\]/im) { |quote| - quote.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n#{$1}\n" } + raw.gsub!(%r{\[quote\](.+?)\[/quote\]}im) do |quote| + quote.gsub!(%r{\[quote\](.+?)\[/quote\]}im) { "\n#{$1}\n" } quote.gsub!(/\n(.+?)/) { "\n> #{$1}" } - } + end # [QUOTE=]...[/QUOTE] - raw.gsub!(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do + raw.gsub!(%r{\[quote=([^;\]]+)\](.+?)\[/quote\]}im) do old_username, quote = $1, $2 new_username = get_username_for_old_username(old_username) "\n[quote=\"#{new_username}\"]\n#{quote}\n[/quote]\n" end # [YOUTUBE][/YOUTUBE] - raw.gsub!(/\[youtube\](.+?)\[\/youtube\]/i) { "\n//youtu.be/#{$1}\n" } + raw.gsub!(%r{\[youtube\](.+?)\[/youtube\]}i) { "\n//youtu.be/#{$1}\n" } # [VIDEO=youtube;]...[/VIDEO] - raw.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" } + raw.gsub!(%r{\[video=youtube;([^\]]+)\].*?\[/video\]}i) { "\n//youtu.be/#{$1}\n" } # Fix uppercase B U and I tags - raw.gsub!(/(\[\/?[BUI]\])/i) { $1.downcase } + raw.gsub!(%r{(\[/?[BUI]\])}i) { $1.downcase } # More Additions .... # [spoiler=Some hidden stuff]SPOILER HERE!![/spoiler] - raw.gsub!(/\[spoiler="?(.+?)"?\](.+?)\[\/spoiler\]/im) { "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" } + raw.gsub!(%r{\[spoiler="?(.+?)"?\](.+?)\[/spoiler\]}im) do + "\n#{$1}\n[spoiler]#{$2}[/spoiler]\n" + end # [IMG][IMG]http://i63.tinypic.com/akga3r.jpg[/IMG][/IMG] - raw.gsub!(/\[IMG\]\[IMG\](.+?)\[\/IMG\]\[\/IMG\]/i) { "[IMG]#{$1}[/IMG]" } + raw.gsub!(%r{\[IMG\]\[IMG\](.+?)\[/IMG\]\[/IMG\]}i) { "[IMG]#{$1}[/IMG]" } # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) # (https://meta.discourse.org/t/phpbb-3-importer-old/17397) - raw.gsub!(/\[list\](.*?)\[\/list\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\](.*?)\[\/list\]/im, '[ol]\1[/ol]') - raw.gsub!(/\[list\](.*?)\[\/list:u\]/im, '[ul]\1[/ul]') - raw.gsub!(/\[list=1\](.*?)\[\/list:o\]/im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\](.*?)\[/list\]}im, '[ol]\1[/ol]') + raw.gsub!(%r{\[list\](.*?)\[/list:u\]}im, '[ul]\1[/ul]') + raw.gsub!(%r{\[list=1\](.*?)\[/list:o\]}im, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - raw.gsub!(/\[\*\]\n/, '') - raw.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]') + raw.gsub!(/\[\*\]\n/, "") + raw.gsub!(%r{\[\*\](.*?)\[/\*:m\]}, '[li]\1[/li]') raw.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]') - raw.gsub!(/\[\*=1\]/, '') + raw.gsub!(/\[\*=1\]/, "") raw end def postprocess_post_raw(raw) # [QUOTE=;]...[/QUOTE] - raw.gsub!(/\[quote=([^;]+);(\d+)\](.+?)\[\/quote\]/im) do + raw.gsub!(%r{\[quote=([^;]+);(\d+)\](.+?)\[/quote\]}im) do old_username, post_id, quote = $1, $2, $3 new_username = get_username_for_old_username(old_username) @@ -859,7 +899,7 @@ class ImportScripts::VBulletin < ImportScripts::Base if topic_lookup = topic_lookup_from_imported_post_id(post_id) post_number = topic_lookup[:post_number] - topic_id = topic_lookup[:topic_id] + topic_id = topic_lookup[:topic_id] "\n[quote=\"#{new_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" else "\n[quote=\"#{new_username}\"]\n#{quote}\n[/quote]\n" @@ -867,11 +907,11 @@ class ImportScripts::VBulletin < ImportScripts::Base end # remove attachments - raw.gsub!(/\[attach[^\]]*\]\d+\[\/attach\]/i, "") + raw.gsub!(%r{\[attach[^\]]*\]\d+\[/attach\]}i, "") # [THREAD][/THREAD] # ==> http://my.discourse.org/t/slug/ - raw.gsub!(/\[thread\](\d+)\[\/thread\]/i) do + raw.gsub!(%r{\[thread\](\d+)\[/thread\]}i) do thread_id = $1 if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") topic_lookup[:url] @@ -882,7 +922,7 @@ class ImportScripts::VBulletin < ImportScripts::Base # [THREAD=]...[/THREAD] # ==> [...](http://my.discourse.org/t/slug/) - raw.gsub!(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do + raw.gsub!(%r{\[thread=(\d+)\](.+?)\[/thread\]}i) do thread_id, link = $1, $2 if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") url = topic_lookup[:url] @@ -894,7 +934,7 @@ class ImportScripts::VBulletin < ImportScripts::Base # [POST][/POST] # ==> http://my.discourse.org/t/slug// - raw.gsub!(/\[post\](\d+)\[\/post\]/i) do + raw.gsub!(%r{\[post\](\d+)\[/post\]}i) do post_id = $1 if topic_lookup = topic_lookup_from_imported_post_id(post_id) topic_lookup[:url] @@ -905,7 +945,7 @@ class ImportScripts::VBulletin < ImportScripts::Base # [POST=]...[/POST] # ==> [...](http://my.discourse.org/t///) - raw.gsub!(/\[post=(\d+)\](.+?)\[\/post\]/i) do + raw.gsub!(%r{\[post=(\d+)\](.+?)\[/post\]}i) do post_id, link = $1, $2 if topic_lookup = topic_lookup_from_imported_post_id(post_id) url = topic_lookup[:url] @@ -919,14 +959,14 @@ class ImportScripts::VBulletin < ImportScripts::Base end def create_permalink_file - puts '', 'Creating Permalink File...', '' + puts "", "Creating Permalink File...", "" id_mapping = [] Topic.listable_topics.find_each do |topic| pcf = topic.first_post.custom_fields if pcf && pcf["import_id"] - id = pcf["import_id"].split('-').last + id = pcf["import_id"].split("-").last id_mapping.push("XXX#{id} YYY#{topic.id}") end end @@ -940,24 +980,21 @@ class ImportScripts::VBulletin < ImportScripts::Base # end CSV.open(File.expand_path("../vb_map.csv", __FILE__), "w") do |csv| - id_mapping.each do |value| - csv << [value] - end + id_mapping.each { |value| csv << [value] } end - end def suspend_users - puts '', "updating banned users" + puts "", "updating banned users" banned = 0 failed = 0 - total = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}userban").first['count'] + total = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}userban").first["count"] system_user = Discourse.system_user mysql_query("SELECT userid, bandate FROM #{TABLE_PREFIX}userban").each do |b| - user = User.find_by_id(user_id_from_imported_user_id(b['userid'])) + user = User.find_by_id(user_id_from_imported_user_id(b["userid"])) if user user.suspended_at = parse_timestamp(user["bandate"]) user.suspended_till = 200.years.from_now @@ -970,7 +1007,7 @@ class ImportScripts::VBulletin < ImportScripts::Base failed += 1 end else - puts "Not found: #{b['userid']}" + puts "Not found: #{b["userid"]}" failed += 1 end @@ -985,7 +1022,6 @@ class ImportScripts::VBulletin < ImportScripts::Base def mysql_query(sql) @client.query(sql, cache_rows: true) end - end ImportScripts::VBulletin.new.perform diff --git a/script/import_scripts/vbulletin5.rb b/script/import_scripts/vbulletin5.rb index 5e5696e4f0..af62c0a6bb 100644 --- a/script/import_scripts/vbulletin5.rb +++ b/script/import_scripts/vbulletin5.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'mysql2' +require "mysql2" require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'htmlentities' +require "htmlentities" class ImportScripts::VBulletin < ImportScripts::Base BATCH_SIZE = 1000 @@ -11,14 +11,14 @@ class ImportScripts::VBulletin < ImportScripts::Base # override these using environment vars - URL_PREFIX ||= ENV['URL_PREFIX'] || "forum/" - DB_PREFIX ||= ENV['DB_PREFIX'] || "vb_" - DB_HOST ||= ENV['DB_HOST'] || "localhost" - DB_NAME ||= ENV['DB_NAME'] || "vbulletin" - DB_PASS ||= ENV['DB_PASS'] || "password" - DB_USER ||= ENV['DB_USER'] || "username" - ATTACH_DIR ||= ENV['ATTACH_DIR'] || "/home/discourse/vbulletin/attach" - AVATAR_DIR ||= ENV['AVATAR_DIR'] || "/home/discourse/vbulletin/avatars" + URL_PREFIX ||= ENV["URL_PREFIX"] || "forum/" + DB_PREFIX ||= ENV["DB_PREFIX"] || "vb_" + DB_HOST ||= ENV["DB_HOST"] || "localhost" + DB_NAME ||= ENV["DB_NAME"] || "vbulletin" + DB_PASS ||= ENV["DB_PASS"] || "password" + DB_USER ||= ENV["DB_USER"] || "username" + ATTACH_DIR ||= ENV["ATTACH_DIR"] || "/home/discourse/vbulletin/attach" + AVATAR_DIR ||= ENV["AVATAR_DIR"] || "/home/discourse/vbulletin/avatars" def initialize super @@ -29,16 +29,21 @@ class ImportScripts::VBulletin < ImportScripts::Base @htmlentities = HTMLEntities.new - @client = Mysql2::Client.new( - host: DB_HOST, - username: DB_USER, - database: DB_NAME, - password: DB_PASS - ) + @client = + Mysql2::Client.new(host: DB_HOST, username: DB_USER, database: DB_NAME, password: DB_PASS) - @forum_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Forum'").first['contenttypeid'] - @channel_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Channel'").first['contenttypeid'] - @text_typeid = mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Text'").first['contenttypeid'] + @forum_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Forum'").first[ + "contenttypeid" + ] + @channel_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Channel'").first[ + "contenttypeid" + ] + @text_typeid = + mysql_query("SELECT contenttypeid FROM #{DB_PREFIX}contenttype WHERE class='Text'").first[ + "contenttypeid" + ] end def execute @@ -64,10 +69,7 @@ class ImportScripts::VBulletin < ImportScripts::Base SQL create_groups(groups) do |group| - { - id: group["usergroupid"], - name: @htmlentities.decode(group["title"]).strip - } + { id: group["usergroupid"], name: @htmlentities.decode(group["title"]).strip } end end @@ -102,17 +104,18 @@ class ImportScripts::VBulletin < ImportScripts::Base name: username, username: username, email: user["email"].presence || fake_email, - admin: user['admin'] == 1, + admin: user["admin"] == 1, password: user["password"], website: user["homepage"].strip, title: @htmlentities.decode(user["usertitle"]).strip, primary_group_id: group_id_from_imported_group_id(user["usergroupid"]), created_at: parse_timestamp(user["joindate"]), - post_create_action: proc do |u| - @old_username_to_new_usernames[user["username"]] = u.username - import_profile_picture(user, u) - # import_profile_background(user, u) - end + post_create_action: + proc do |u| + @old_username_to_new_usernames[user["username"]] = u.username + import_profile_picture(user, u) + # import_profile_background(user, u) + end, } end end @@ -131,18 +134,18 @@ class ImportScripts::VBulletin < ImportScripts::Base return if picture.nil? - if picture['filedata'] + if picture["filedata"] file = Tempfile.new("profile-picture") file.write(picture["filedata"].encode("ASCII-8BIT").force_encoding("UTF-8")) file.rewind upload = UploadCreator.new(file, picture["filename"]).create_for(imported_user.id) else - filename = File.join(AVATAR_DIR, picture['filename']) + filename = File.join(AVATAR_DIR, picture["filename"]) unless File.exist?(filename) puts "Avatar file doesn't exist: #{filename}" return nil end - upload = create_upload(imported_user.id, filename, picture['filename']) + upload = create_upload(imported_user.id, filename, picture["filename"]) end return if !upload.persisted? @@ -151,8 +154,16 @@ class ImportScripts::VBulletin < ImportScripts::Base imported_user.user_avatar.update(custom_upload_id: upload.id) imported_user.update(uploaded_avatar_id: upload.id) ensure - file.close rescue nil - file.unlind rescue nil + begin + file.close + rescue StandardError + nil + end + begin + file.unlind + rescue StandardError + nil + end end def import_profile_background(old_user, imported_user) @@ -178,21 +189,32 @@ class ImportScripts::VBulletin < ImportScripts::Base imported_user.user_profile.upload_profile_background(upload) ensure - file.close rescue nil - file.unlink rescue nil + begin + file.close + rescue StandardError + nil + end + begin + file.unlink + rescue StandardError + nil + end end def import_categories puts "", "importing top level categories..." - categories = mysql_query("SELECT nodeid AS forumid, title, description, displayorder, parentid + categories = + mysql_query( + "SELECT nodeid AS forumid, title, description, displayorder, parentid FROM #{DB_PREFIX}node WHERE parentid=#{ROOT_NODE} UNION SELECT nodeid, title, description, displayorder, parentid FROM #{DB_PREFIX}node WHERE contenttypeid = #{@channel_typeid} - AND parentid IN (SELECT nodeid FROM #{DB_PREFIX}node WHERE parentid=#{ROOT_NODE})").to_a + AND parentid IN (SELECT nodeid FROM #{DB_PREFIX}node WHERE parentid=#{ROOT_NODE})", + ).to_a top_level_categories = categories.select { |c| c["parentid"] == ROOT_NODE } @@ -201,7 +223,7 @@ class ImportScripts::VBulletin < ImportScripts::Base id: category["forumid"], name: @htmlentities.decode(category["title"]).strip, position: category["displayorder"], - description: @htmlentities.decode(category["description"]).strip + description: @htmlentities.decode(category["description"]).strip, } end @@ -223,7 +245,7 @@ class ImportScripts::VBulletin < ImportScripts::Base name: @htmlentities.decode(category["title"]).strip, position: category["displayorder"], description: @htmlentities.decode(category["description"]).strip, - parent_category_id: category_id_from_imported_category_id(category["parentid"]) + parent_category_id: category_id_from_imported_category_id(category["parentid"]), } end end @@ -234,13 +256,17 @@ class ImportScripts::VBulletin < ImportScripts::Base # keep track of closed topics @closed_topic_ids = [] - topic_count = mysql_query("SELECT COUNT(nodeid) cnt + topic_count = + mysql_query( + "SELECT COUNT(nodeid) cnt FROM #{DB_PREFIX}node WHERE (unpublishdate = 0 OR unpublishdate IS NULL) AND (approved = 1 AND showapproved = 1) AND parentid IN ( - SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};" - ).first["cnt"] + SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};", + ).first[ + "cnt" + ] batches(BATCH_SIZE) do |offset| topics = mysql_query <<-SQL @@ -265,7 +291,12 @@ class ImportScripts::VBulletin < ImportScripts::Base # next if all_records_exist? :posts, topics.map {|t| "thread-#{topic["threadid"]}" } create_posts(topics, total: topic_count, offset: offset) do |topic| - raw = preprocess_post_raw(topic["raw"]) rescue nil + raw = + begin + preprocess_post_raw(topic["raw"]) + rescue StandardError + nil + end next if raw.blank? topic_id = "thread-#{topic["threadid"]}" @closed_topic_ids << topic_id if topic["open"] == "0" @@ -291,11 +322,16 @@ class ImportScripts::VBulletin < ImportScripts::Base # make sure `firstpostid` is indexed begin mysql_query("CREATE INDEX firstpostid_index ON thread (firstpostid)") - rescue + rescue StandardError end - post_count = mysql_query("SELECT COUNT(nodeid) cnt FROM #{DB_PREFIX}node WHERE parentid NOT IN ( - SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};").first["cnt"] + post_count = + mysql_query( + "SELECT COUNT(nodeid) cnt FROM #{DB_PREFIX}node WHERE parentid NOT IN ( + SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};", + ).first[ + "cnt" + ] batches(BATCH_SIZE) do |offset| posts = mysql_query <<-SQL @@ -338,10 +374,14 @@ class ImportScripts::VBulletin < ImportScripts::Base end def import_attachments - puts '', 'importing attachments...' + puts "", "importing attachments..." - ext = mysql_query("SELECT GROUP_CONCAT(DISTINCT(extension)) exts FROM #{DB_PREFIX}filedata").first['exts'].split(',') - SiteSetting.authorized_extensions = (SiteSetting.authorized_extensions.split("|") + ext).uniq.join("|") + ext = + mysql_query("SELECT GROUP_CONCAT(DISTINCT(extension)) exts FROM #{DB_PREFIX}filedata").first[ + "exts" + ].split(",") + SiteSetting.authorized_extensions = + (SiteSetting.authorized_extensions.split("|") + ext).uniq.join("|") uploads = mysql_query <<-SQL SELECT n.parentid nodeid, a.filename, fd.userid, LENGTH(fd.filedata) AS dbsize, filedata, fd.filedataid @@ -354,32 +394,43 @@ class ImportScripts::VBulletin < ImportScripts::Base total_count = uploads.count uploads.each do |upload| - post_id = PostCustomField.where(name: 'import_id').where(value: upload['nodeid']).first&.post_id - post_id = PostCustomField.where(name: 'import_id').where(value: "thread-#{upload['nodeid']}").first&.post_id unless post_id + post_id = + PostCustomField.where(name: "import_id").where(value: upload["nodeid"]).first&.post_id + post_id = + PostCustomField + .where(name: "import_id") + .where(value: "thread-#{upload["nodeid"]}") + .first + &.post_id unless post_id if post_id.nil? - puts "Post for #{upload['nodeid']} not found" + puts "Post for #{upload["nodeid"]} not found" next end post = Post.find(post_id) - filename = File.join(ATTACH_DIR, upload['userid'].to_s.split('').join('/'), "#{upload['filedataid']}.attach") - real_filename = upload['filename'] - real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + filename = + File.join( + ATTACH_DIR, + upload["userid"].to_s.split("").join("/"), + "#{upload["filedataid"]}.attach", + ) + real_filename = upload["filename"] + real_filename.prepend SecureRandom.hex if real_filename[0] == "." unless File.exist?(filename) # attachments can be on filesystem or in database # try to retrieve from database if the file did not exist on filesystem - if upload['dbsize'].to_i == 0 - puts "Attachment file #{upload['filedataid']} doesn't exist" + if upload["dbsize"].to_i == 0 + puts "Attachment file #{upload["filedataid"]} doesn't exist" next end - tmpfile = 'attach_' + upload['filedataid'].to_s - filename = File.join('/tmp/', tmpfile) - File.open(filename, 'wb') { |f| + tmpfile = "attach_" + upload["filedataid"].to_s + filename = File.join("/tmp/", tmpfile) + File.open(filename, "wb") do |f| #f.write(PG::Connection.unescape_bytea(row['filedata'])) - f.write(upload['filedata']) - } + f.write(upload["filedata"]) + end end upl_obj = create_upload(post.user.id, filename, real_filename) @@ -388,7 +439,9 @@ class ImportScripts::VBulletin < ImportScripts::Base if !post.raw[html] post.raw += "\n\n#{html}\n\n" post.save! - PostUpload.create!(post: post, upload: upl_obj) unless PostUpload.where(post: post, upload: upl_obj).exists? + unless PostUpload.where(post: post, upload: upl_obj).exists? + PostUpload.create!(post: post, upload: upl_obj) + end end else puts "Fail" @@ -447,170 +500,177 @@ class ImportScripts::VBulletin < ImportScripts::Base raw = @htmlentities.decode(raw) # fix whitespaces - raw = raw.gsub(/(\\r)?\\n/, "\n") - .gsub("\\t", "\t") + raw = raw.gsub(/(\\r)?\\n/, "\n").gsub("\\t", "\t") # [HTML]...[/HTML] - raw = raw.gsub(/\[html\]/i, "\n```html\n") - .gsub(/\[\/html\]/i, "\n```\n") + raw = raw.gsub(/\[html\]/i, "\n```html\n").gsub(%r{\[/html\]}i, "\n```\n") # [PHP]...[/PHP] - raw = raw.gsub(/\[php\]/i, "\n```php\n") - .gsub(/\[\/php\]/i, "\n```\n") + raw = raw.gsub(/\[php\]/i, "\n```php\n").gsub(%r{\[/php\]}i, "\n```\n") # [HIGHLIGHT="..."] raw = raw.gsub(/\[highlight="?(\w+)"?\]/i) { "\n```#{$1.downcase}\n" } # [CODE]...[/CODE] # [HIGHLIGHT]...[/HIGHLIGHT] - raw = raw.gsub(/\[\/?code\]/i, "\n```\n") - .gsub(/\[\/?highlight\]/i, "\n```\n") + raw = raw.gsub(%r{\[/?code\]}i, "\n```\n").gsub(%r{\[/?highlight\]}i, "\n```\n") # [SAMP]...[/SAMP] - raw = raw.gsub(/\[\/?samp\]/i, "`") + raw = raw.gsub(%r{\[/?samp\]}i, "`") # replace all chevrons with HTML entities # NOTE: must be done # - AFTER all the "code" processing # - BEFORE the "quote" processing - raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" } - .gsub("<", "<") - .gsub("\u2603", "<") + raw = + raw + .gsub(/`([^`]+)`/im) { "`" + $1.gsub("<", "\u2603") + "`" } + .gsub("<", "<") + .gsub("\u2603", "<") - raw = raw.gsub(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" } - .gsub(">", ">") - .gsub("\u2603", ">") + raw = + raw + .gsub(/`([^`]+)`/im) { "`" + $1.gsub(">", "\u2603") + "`" } + .gsub(">", ">") + .gsub("\u2603", ">") # [URL=...]...[/URL] - raw.gsub!(/\[url="?(.+?)"?\](.+?)\[\/url\]/i) { "#{$2}" } + raw.gsub!(%r{\[url="?(.+?)"?\](.+?)\[/url\]}i) { "#{$2}" } # [URL]...[/URL] # [MP3]...[/MP3] - raw = raw.gsub(/\[\/?url\]/i, "") - .gsub(/\[\/?mp3\]/i, "") + raw = raw.gsub(%r{\[/?url\]}i, "").gsub(%r{\[/?mp3\]}i, "") # [MENTION][/MENTION] - raw = raw.gsub(/\[mention\](.+?)\[\/mention\]/i) do - old_username = $1 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] + raw = + raw.gsub(%r{\[mention\](.+?)\[/mention\]}i) do + old_username = $1 + if @old_username_to_new_usernames.has_key?(old_username) + old_username = @old_username_to_new_usernames[old_username] + end + "@#{old_username}" end - "@#{old_username}" - end # [USER=][/USER] - raw = raw.gsub(/\[user="?(\d+)"?\](.+?)\[\/user\]/i) do - user_id, old_username = $1, $2 - if @old_username_to_new_usernames.has_key?(old_username) - new_username = @old_username_to_new_usernames[old_username] - else - new_username = old_username + raw = + raw.gsub(%r{\[user="?(\d+)"?\](.+?)\[/user\]}i) do + user_id, old_username = $1, $2 + if @old_username_to_new_usernames.has_key?(old_username) + new_username = @old_username_to_new_usernames[old_username] + else + new_username = old_username + end + "@#{new_username}" end - "@#{new_username}" - end # [FONT=blah] and [COLOR=blah] # no idea why the /i is not matching case insensitive.. - raw.gsub! /\[color=.*?\](.*?)\[\/color\]/im, '\1' - raw.gsub! /\[COLOR=.*?\](.*?)\[\/COLOR\]/im, '\1' - raw.gsub! /\[font=.*?\](.*?)\[\/font\]/im, '\1' - raw.gsub! /\[FONT=.*?\](.*?)\[\/FONT\]/im, '\1' + raw.gsub! %r{\[color=.*?\](.*?)\[/color\]}im, '\1' + raw.gsub! %r{\[COLOR=.*?\](.*?)\[/COLOR\]}im, '\1' + raw.gsub! %r{\[font=.*?\](.*?)\[/font\]}im, '\1' + raw.gsub! %r{\[FONT=.*?\](.*?)\[/FONT\]}im, '\1' # [CENTER]...[/CENTER] - raw.gsub! /\[CENTER\](.*?)\[\/CENTER\]/im, '\1' + raw.gsub! %r{\[CENTER\](.*?)\[/CENTER\]}im, '\1' # fix LIST - raw.gsub! /\[LIST\](.*?)\[\/LIST\]/im, '
      \1
    ' - raw.gsub! /\[\*\]/im, '
  • ' + raw.gsub! %r{\[LIST\](.*?)\[/LIST\]}im, '
      \1
    ' + raw.gsub! /\[\*\]/im, "
  • " # [QUOTE]...[/QUOTE] - raw = raw.gsub(/\[quote\](.+?)\[\/quote\]/im) { "\n> #{$1}\n" } + raw = raw.gsub(%r{\[quote\](.+?)\[/quote\]}im) { "\n> #{$1}\n" } # [QUOTE=]...[/QUOTE] - raw = raw.gsub(/\[quote=([^;\]]+)\](.+?)\[\/quote\]/im) do - old_username, quote = $1, $2 + raw = + raw.gsub(%r{\[quote=([^;\]]+)\](.+?)\[/quote\]}im) do + old_username, quote = $1, $2 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] + if @old_username_to_new_usernames.has_key?(old_username) + old_username = @old_username_to_new_usernames[old_username] + end + "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" end - "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" - end # [YOUTUBE][/YOUTUBE] - raw = raw.gsub(/\[youtube\](.+?)\[\/youtube\]/i) { "\n//youtu.be/#{$1}\n" } + raw = raw.gsub(%r{\[youtube\](.+?)\[/youtube\]}i) { "\n//youtu.be/#{$1}\n" } # [VIDEO=youtube;]...[/VIDEO] - raw = raw.gsub(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\n//youtu.be/#{$1}\n" } + raw = raw.gsub(%r{\[video=youtube;([^\]]+)\].*?\[/video\]}i) { "\n//youtu.be/#{$1}\n" } raw end def postprocess_post_raw(raw) # [QUOTE=;]...[/QUOTE] - raw = raw.gsub(/\[quote=([^;]+);n(\d+)\](.+?)\[\/quote\]/im) do - old_username, post_id, quote = $1, $2, $3 + raw = + raw.gsub(%r{\[quote=([^;]+);n(\d+)\](.+?)\[/quote\]}im) do + old_username, post_id, quote = $1, $2, $3 - if @old_username_to_new_usernames.has_key?(old_username) - old_username = @old_username_to_new_usernames[old_username] - end + if @old_username_to_new_usernames.has_key?(old_username) + old_username = @old_username_to_new_usernames[old_username] + end - if topic_lookup = topic_lookup_from_imported_post_id(post_id) - post_number = topic_lookup[:post_number] - topic_id = topic_lookup[:topic_id] - "\n[quote=\"#{old_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" - else - "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" + if topic_lookup = topic_lookup_from_imported_post_id(post_id) + post_number = topic_lookup[:post_number] + topic_id = topic_lookup[:topic_id] + "\n[quote=\"#{old_username},post:#{post_number},topic:#{topic_id}\"]\n#{quote}\n[/quote]\n" + else + "\n[quote=\"#{old_username}\"]\n#{quote}\n[/quote]\n" + end end - end # remove attachments - raw = raw.gsub(/\[attach[^\]]*\]\d+\[\/attach\]/i, "") + raw = raw.gsub(%r{\[attach[^\]]*\]\d+\[/attach\]}i, "") # [THREAD][/THREAD] # ==> http://my.discourse.org/t/slug/ - raw = raw.gsub(/\[thread\](\d+)\[\/thread\]/i) do - thread_id = $1 - if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") - topic_lookup[:url] - else - $& + raw = + raw.gsub(%r{\[thread\](\d+)\[/thread\]}i) do + thread_id = $1 + if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") + topic_lookup[:url] + else + $& + end end - end # [THREAD=]...[/THREAD] # ==> [...](http://my.discourse.org/t/slug/) - raw = raw.gsub(/\[thread=(\d+)\](.+?)\[\/thread\]/i) do - thread_id, link = $1, $2 - if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") - url = topic_lookup[:url] - "[#{link}](#{url})" - else - $& + raw = + raw.gsub(%r{\[thread=(\d+)\](.+?)\[/thread\]}i) do + thread_id, link = $1, $2 + if topic_lookup = topic_lookup_from_imported_post_id("thread-#{thread_id}") + url = topic_lookup[:url] + "[#{link}](#{url})" + else + $& + end end - end # [POST][/POST] # ==> http://my.discourse.org/t/slug// - raw = raw.gsub(/\[post\](\d+)\[\/post\]/i) do - post_id = $1 - if topic_lookup = topic_lookup_from_imported_post_id(post_id) - topic_lookup[:url] - else - $& + raw = + raw.gsub(%r{\[post\](\d+)\[/post\]}i) do + post_id = $1 + if topic_lookup = topic_lookup_from_imported_post_id(post_id) + topic_lookup[:url] + else + $& + end end - end # [POST=]...[/POST] # ==> [...](http://my.discourse.org/t///) - raw = raw.gsub(/\[post=(\d+)\](.+?)\[\/post\]/i) do - post_id, link = $1, $2 - if topic_lookup = topic_lookup_from_imported_post_id(post_id) - url = topic_lookup[:url] - "[#{link}](#{url})" - else - $& + raw = + raw.gsub(%r{\[post=(\d+)\](.+?)\[/post\]}i) do + post_id, link = $1, $2 + if topic_lookup = topic_lookup_from_imported_post_id(post_id) + url = topic_lookup[:url] + "[#{link}](#{url})" + else + $& + end end - end raw end @@ -619,13 +679,17 @@ class ImportScripts::VBulletin < ImportScripts::Base puts "", "creating permalinks..." current_count = 0 - total_count = mysql_query("SELECT COUNT(nodeid) cnt + total_count = + mysql_query( + "SELECT COUNT(nodeid) cnt FROM #{DB_PREFIX}node WHERE (unpublishdate = 0 OR unpublishdate IS NULL) AND (approved = 1 AND showapproved = 1) AND parentid IN ( - SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};" - ).first["cnt"] + SELECT nodeid FROM #{DB_PREFIX}node WHERE contenttypeid=#{@channel_typeid} ) AND contenttypeid=#{@text_typeid};", + ).first[ + "cnt" + ] batches(BATCH_SIZE) do |offset| topics = mysql_query <<-SQL @@ -647,12 +711,16 @@ class ImportScripts::VBulletin < ImportScripts::Base topics.each do |topic| current_count += 1 print_status current_count, total_count - disc_topic = topic_lookup_from_imported_post_id("thread-#{topic['nodeid']}") + disc_topic = topic_lookup_from_imported_post_id("thread-#{topic["nodeid"]}") - Permalink.create( - url: "#{URL_PREFIX}#{topic['p1']}/#{topic['p2']}/#{topic['nodeid']}-#{topic['p3']}", - topic_id: disc_topic[:topic_id] - ) rescue nil + begin + Permalink.create( + url: "#{URL_PREFIX}#{topic["p1"]}/#{topic["p2"]}/#{topic["nodeid"]}-#{topic["p3"]}", + topic_id: disc_topic[:topic_id], + ) + rescue StandardError + nil + end end end @@ -664,8 +732,13 @@ class ImportScripts::VBulletin < ImportScripts::Base AND parentid=#{ROOT_NODE}; SQL cats.each do |c| - category_id = CategoryCustomField.where(name: 'import_id').where(value: c['nodeid']).first.category_id - Permalink.create(url: "#{URL_PREFIX}#{c['urlident']}", category_id: category_id) rescue nil + category_id = + CategoryCustomField.where(name: "import_id").where(value: c["nodeid"]).first.category_id + begin + Permalink.create(url: "#{URL_PREFIX}#{c["urlident"]}", category_id: category_id) + rescue StandardError + nil + end end # subcats @@ -677,8 +750,13 @@ class ImportScripts::VBulletin < ImportScripts::Base AND n1.contenttypeid=#{@channel_typeid}; SQL subcats.each do |sc| - category_id = CategoryCustomField.where(name: 'import_id').where(value: sc['nodeid']).first.category_id - Permalink.create(url: "#{URL_PREFIX}#{sc['p1']}/#{sc['p2']}", category_id: category_id) rescue nil + category_id = + CategoryCustomField.where(name: "import_id").where(value: sc["nodeid"]).first.category_id + begin + Permalink.create(url: "#{URL_PREFIX}#{sc["p1"]}/#{sc["p2"]}", category_id: category_id) + rescue StandardError + nil + end end end @@ -689,7 +767,7 @@ class ImportScripts::VBulletin < ImportScripts::Base SiteSetting.max_tags_per_topic = 100 staff_guardian = Guardian.new(Discourse.system_user) - records = mysql_query(<<~SQL + records = mysql_query(<<~SQL).to_a SELECT nodeid, GROUP_CONCAT(tagtext) tags FROM #{DB_PREFIX}tag t LEFT JOIN #{DB_PREFIX}tagnode tn ON tn.tagid = t.tagid @@ -697,7 +775,6 @@ class ImportScripts::VBulletin < ImportScripts::Base AND tn.nodeid IS NOT NULL GROUP BY nodeid SQL - ).to_a current_count = 0 total_count = records.count @@ -705,11 +782,11 @@ class ImportScripts::VBulletin < ImportScripts::Base records.each do |rec| current_count += 1 print_status current_count, total_count - tl = topic_lookup_from_imported_post_id("thread-#{rec['nodeid']}") - next if tl.nil? # topic might have been deleted + tl = topic_lookup_from_imported_post_id("thread-#{rec["nodeid"]}") + next if tl.nil? # topic might have been deleted topic = Topic.find(tl[:topic_id]) - tag_names = rec['tags'].force_encoding("UTF-8").split(',') + tag_names = rec["tags"].force_encoding("UTF-8").split(",") DiscourseTagging.tag_topic_by_names(topic, staff_guardian, tag_names) end end diff --git a/script/import_scripts/xenforo.rb b/script/import_scripts/xenforo.rb index 39ef48090d..1c9aaaeb77 100755 --- a/script/import_scripts/xenforo.rb +++ b/script/import_scripts/xenforo.rb @@ -3,11 +3,11 @@ require "mysql2" begin - require 'php_serialize' # https://github.com/jqr/php-serialize + require "php_serialize" # https://github.com/jqr/php-serialize rescue LoadError puts - puts 'php_serialize not found.' - puts 'Add to Gemfile, like this: ' + puts "php_serialize not found." + puts "Add to Gemfile, like this: " puts puts "echo gem \\'php-serialize\\' >> Gemfile" puts "bundle install" @@ -19,20 +19,20 @@ require File.expand_path(File.dirname(__FILE__) + "/base.rb") # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/xenforo.rb class ImportScripts::XenForo < ImportScripts::Base - XENFORO_DB = "xenforo_db" TABLE_PREFIX = "xf_" BATCH_SIZE = 1000 - ATTACHMENT_DIR = '/tmp/attachments' + ATTACHMENT_DIR = "/tmp/attachments" def initialize super - @client = Mysql2::Client.new( - host: "localhost", - username: "root", - password: "pa$$word", - database: XENFORO_DB - ) + @client = + Mysql2::Client.new( + host: "localhost", + username: "root", + password: "pa$$word", + database: XENFORO_DB, + ) @category_mappings = {} @prefix_as_category = false @@ -47,10 +47,8 @@ class ImportScripts::XenForo < ImportScripts::Base end def import_avatar(id, imported_user) - filename = File.join(AVATAR_DIR, 'l', (id / 1000).to_s, "#{id}.jpg") - unless File.exist?(filename) - return nil - end + filename = File.join(AVATAR_DIR, "l", (id / 1000).to_s, "#{id}.jpg") + return nil unless File.exist?(filename) upload = create_upload(imported_user.id, filename, "avatar_#{id}") return if !upload.persisted? imported_user.create_user_avatar @@ -59,36 +57,43 @@ class ImportScripts::XenForo < ImportScripts::Base end def import_users - puts '', "creating users" + puts "", "creating users" - total_count = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}user WHERE user_state = 'valid' AND is_banned = 0;").first['count'] + total_count = + mysql_query( + "SELECT count(*) count FROM #{TABLE_PREFIX}user WHERE user_state = 'valid' AND is_banned = 0;", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT user_id id, username, email, custom_title title, register_date created_at, + results = + mysql_query( + "SELECT user_id id, username, email, custom_title title, register_date created_at, last_activity last_visit_time, user_group_id, is_moderator, is_admin, is_staff FROM #{TABLE_PREFIX}user WHERE user_state = 'valid' AND is_banned = 0 LIMIT #{BATCH_SIZE} - OFFSET #{offset};") + OFFSET #{offset};", + ) break if results.size < 1 next if all_records_exist? :users, results.map { |u| u["id"].to_i } create_users(results, total: total_count, offset: offset) do |user| - next if user['username'].blank? - { id: user['id'], - email: user['email'], - username: user['username'], - title: user['title'], - created_at: Time.zone.at(user['created_at']), - last_seen_at: Time.zone.at(user['last_visit_time']), - moderator: user['is_moderator'] == 1 || user['is_staff'] == 1, - admin: user['is_admin'] == 1, - post_create_action: proc do |u| - import_avatar(user['id'], u) - end + next if user["username"].blank? + { + id: user["id"], + email: user["email"], + username: user["username"], + title: user["title"], + created_at: Time.zone.at(user["created_at"]), + last_seen_at: Time.zone.at(user["last_visit_time"]), + moderator: user["is_moderator"] == 1 || user["is_staff"] == 1, + admin: user["is_admin"] == 1, + post_create_action: proc { |u| import_avatar(user["id"], u) }, + } end end end @@ -96,7 +101,9 @@ class ImportScripts::XenForo < ImportScripts::Base def import_categories puts "", "importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT node_id id, title, description, @@ -105,20 +112,22 @@ class ImportScripts::XenForo < ImportScripts::Base display_order FROM #{TABLE_PREFIX}node ORDER BY parent_node_id, display_order - ").to_a + ", + ).to_a top_level_categories = categories.select { |c| c["parent_node_id"] == 0 } create_categories(top_level_categories) do |c| { - id: c['id'], - name: c['title'], - description: c['description'], - position: c['display_order'], - post_create_action: proc do |category| - url = "board/#{c['node_name']}" - Permalink.find_or_create_by(url: url, category_id: category.id) - end + id: c["id"], + name: c["title"], + description: c["description"], + position: c["display_order"], + post_create_action: + proc do |category| + url = "board/#{c["node_name"]}" + Permalink.find_or_create_by(url: url, category_id: category.id) + end, } end @@ -128,40 +137,41 @@ class ImportScripts::XenForo < ImportScripts::Base create_categories(subcategories) do |c| { - id: c['id'], - name: c['title'], - description: c['description'], - position: c['display_order'], - parent_category_id: category_id_from_imported_category_id(c['parent_node_id']), - post_create_action: proc do |category| - url = "board/#{c['node_name']}" - Permalink.find_or_create_by(url: url, category_id: category.id) - end + id: c["id"], + name: c["title"], + description: c["description"], + position: c["display_order"], + parent_category_id: category_id_from_imported_category_id(c["parent_node_id"]), + post_create_action: + proc do |category| + url = "board/#{c["node_name"]}" + Permalink.find_or_create_by(url: url, category_id: category.id) + end, } end - subcategory_ids = Set.new(subcategories.map { |c| c['id'] }) + subcategory_ids = Set.new(subcategories.map { |c| c["id"] }) # deeper categories need to be tags categories.each do |c| - next if c['parent_node_id'] == 0 - next if top_level_category_ids.include?(c['id']) - next if subcategory_ids.include?(c['id']) + next if c["parent_node_id"] == 0 + next if top_level_category_ids.include?(c["id"]) + next if subcategory_ids.include?(c["id"]) # Find a subcategory for topics in this category parent = c - while !parent.nil? && !subcategory_ids.include?(parent['id']) - parent = categories.find { |subcat| subcat['id'] == parent['parent_node_id'] } + while !parent.nil? && !subcategory_ids.include?(parent["id"]) + parent = categories.find { |subcat| subcat["id"] == parent["parent_node_id"] } end if parent - tag_name = DiscourseTagging.clean_tag(c['title']) - @category_mappings[c['id']] = { - category_id: category_id_from_imported_category_id(parent['id']), - tag: Tag.find_by_name(tag_name) || Tag.create(name: tag_name) + tag_name = DiscourseTagging.clean_tag(c["title"]) + @category_mappings[c["id"]] = { + category_id: category_id_from_imported_category_id(parent["id"]), + tag: Tag.find_by_name(tag_name) || Tag.create(name: tag_name), } else - puts '', "Couldn't find a category for #{c['id']} '#{c['title']}'!" + puts "", "Couldn't find a category for #{c["id"]} '#{c["title"]}'!" end end end @@ -171,40 +181,46 @@ class ImportScripts::XenForo < ImportScripts::Base def import_categories_from_thread_prefixes puts "", "importing categories..." - categories = mysql_query(" + categories = + mysql_query( + " SELECT prefix_id id FROM #{TABLE_PREFIX}thread_prefix ORDER BY prefix_id ASC - ").to_a + ", + ).to_a create_categories(categories) do |category| - { - id: category["id"], - name: "Category-#{category["id"]}" - } + { id: category["id"], name: "Category-#{category["id"]}" } end @prefix_as_category = true end def import_likes - puts '', 'importing likes' - total_count = mysql_query("SELECT COUNT(*) AS count FROM #{TABLE_PREFIX}liked_content WHERE content_type = 'post'").first["count"] + puts "", "importing likes" + total_count = + mysql_query( + "SELECT COUNT(*) AS count FROM #{TABLE_PREFIX}liked_content WHERE content_type = 'post'", + ).first[ + "count" + ] batches(BATCH_SIZE) do |offset| - results = mysql_query( - "SELECT like_id, content_id, like_user_id, like_date + results = + mysql_query( + "SELECT like_id, content_id, like_user_id, like_date FROM #{TABLE_PREFIX}liked_content WHERE content_type = 'post' ORDER BY like_id LIMIT #{BATCH_SIZE} - OFFSET #{offset};" - ) + OFFSET #{offset};", + ) break if results.size < 1 create_likes(results, total: total_count, offset: offset) do |row| { - post_id: row['content_id'], - user_id: row['like_user_id'], - created_at: Time.zone.at(row['like_date']) + post_id: row["content_id"], + user_id: row["like_user_id"], + created_at: Time.zone.at(row["like_date"]), } end end @@ -215,10 +231,11 @@ class ImportScripts::XenForo < ImportScripts::Base total_count = mysql_query("SELECT count(*) count from #{TABLE_PREFIX}post").first["count"] - posts_sql = " + posts_sql = + " SELECT p.post_id id, t.thread_id topic_id, - #{@prefix_as_category ? 't.prefix_id' : 't.node_id'} category_id, + #{@prefix_as_category ? "t.prefix_id" : "t.node_id"} category_id, t.title title, t.first_post_id first_post_id, t.view_count, @@ -237,35 +254,35 @@ class ImportScripts::XenForo < ImportScripts::Base results = mysql_query("#{posts_sql} OFFSET #{offset};").to_a break if results.size < 1 - next if all_records_exist? :posts, results.map { |p| p['id'] } + next if all_records_exist? :posts, results.map { |p| p["id"] } create_posts(results, total: total_count, offset: offset) do |m| skip = false mapped = {} - mapped[:id] = m['id'] - mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 - mapped[:raw] = process_xenforo_post(m['raw'], m['id']) - mapped[:created_at] = Time.zone.at(m['created_at']) + mapped[:id] = m["id"] + mapped[:user_id] = user_id_from_imported_user_id(m["user_id"]) || -1 + mapped[:raw] = process_xenforo_post(m["raw"], m["id"]) + mapped[:created_at] = Time.zone.at(m["created_at"]) - if m['id'] == m['first_post_id'] - if m['category_id'].to_i == 0 || m['category_id'].nil? + if m["id"] == m["first_post_id"] + if m["category_id"].to_i == 0 || m["category_id"].nil? mapped[:category] = SiteSetting.uncategorized_category_id else - mapped[:category] = category_id_from_imported_category_id(m['category_id'].to_i) || - @category_mappings[m['category_id']].try(:[], :category_id) + mapped[:category] = category_id_from_imported_category_id(m["category_id"].to_i) || + @category_mappings[m["category_id"]].try(:[], :category_id) end - mapped[:title] = CGI.unescapeHTML(m['title']) - mapped[:views] = m['view_count'] + mapped[:title] = CGI.unescapeHTML(m["title"]) + mapped[:views] = m["view_count"] mapped[:post_create_action] = proc do |pp| - Permalink.find_or_create_by(url: "threads/#{m['topic_id']}", topic_id: pp.topic_id) + Permalink.find_or_create_by(url: "threads/#{m["topic_id"]}", topic_id: pp.topic_id) end else - parent = topic_lookup_from_imported_post_id(m['first_post_id']) + parent = topic_lookup_from_imported_post_id(m["first_post_id"]) if parent mapped[:topic_id] = parent[:topic_id] else - puts "Parent post #{m['first_post_id']} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + puts "Parent post #{m["first_post_id"]} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" skip = true end end @@ -280,16 +297,15 @@ class ImportScripts::XenForo < ImportScripts::Base break if results.size < 1 results.each do |m| - next unless m['id'] == m['first_post_id'] && m['category_id'].to_i > 0 - next unless tag = @category_mappings[m['category_id']].try(:[], :tag) - next unless topic_mapping = topic_lookup_from_imported_post_id(m['id']) + next unless m["id"] == m["first_post_id"] && m["category_id"].to_i > 0 + next unless tag = @category_mappings[m["category_id"]].try(:[], :tag) + next unless topic_mapping = topic_lookup_from_imported_post_id(m["id"]) topic = Topic.find_by_id(topic_mapping[:topic_id]) topic.tags = [tag] if topic end end - end def import_private_messages @@ -297,10 +313,10 @@ class ImportScripts::XenForo < ImportScripts::Base post_count = mysql_query("SELECT COUNT(*) count FROM xf_conversation_message").first["count"] batches(BATCH_SIZE) do |offset| posts = mysql_query <<-SQL - SELECT c.conversation_id, c.recipients, c.title, m.message, m.user_id, m.message_date, m.message_id, IF(c.first_message_id != m.message_id, c.first_message_id, 0) as topic_id - FROM xf_conversation_master c - LEFT JOIN xf_conversation_message m ON m.conversation_id = c.conversation_id - ORDER BY c.conversation_id, m.message_id + SELECT c.conversation_id, c.recipients, c.title, m.message, m.user_id, m.message_date, m.message_id, IF(c.first_message_id != m.message_id, c.first_message_id, 0) as topic_id + FROM xf_conversation_master c + LEFT JOIN xf_conversation_message m ON m.conversation_id = c.conversation_id + ORDER BY c.conversation_id, m.message_id LIMIT #{BATCH_SIZE} OFFSET #{offset} SQL @@ -316,30 +332,30 @@ class ImportScripts::XenForo < ImportScripts::Base id: message_id, user_id: user_id, raw: raw, - created_at: Time.zone.at(post["message_date"].to_i), - import_mode: true + created_at: Time.zone.at(post["message_date"].to_i), + import_mode: true, } unless post["topic_id"] > 0 msg[:title] = post["title"] msg[:archetype] = Archetype.private_message - to_user_array = PHP.unserialize(post['recipients']) + to_user_array = PHP.unserialize(post["recipients"]) if to_user_array.size > 0 discourse_user_ids = to_user_array.keys.map { |id| user_id_from_imported_user_id(id) } usernames = User.where(id: [discourse_user_ids]).pluck(:username) - msg[:target_usernames] = usernames.join(',') + msg[:target_usernames] = usernames.join(",") end else topic_id = post["topic_id"] if t = topic_lookup_from_imported_post_id("pm_#{topic_id}") msg[:topic_id] = t[:topic_id] else - puts "Topic ID #{topic_id} not found, skipping post #{post['message_id']} from #{post['user_id']}" + puts "Topic ID #{topic_id} not found, skipping post #{post["message_id"]} from #{post["user_id"]}" next end end msg else - puts "Empty message, skipping post #{post['message_id']}" + puts "Empty message, skipping post #{post["message_id"]}" next end end @@ -350,18 +366,18 @@ class ImportScripts::XenForo < ImportScripts::Base s = raw.dup # :) is encoded as :) - s.gsub!(/]+) \/>/, '\1') + s.gsub!(%r{]+) />}, '\1') # Some links look like this: http://www.onegameamonth.com - s.gsub!(/(.+)<\/a>/, '[\2](\1)') + s.gsub!(%r{(.+)}, '[\2](\1)') # Many phpbb bbcode tags have a hash attached to them. Examples: # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] - s.gsub!(/:(?:\w{8})\]/, ']') + s.gsub!(/:(?:\w{8})\]/, "]") # Remove mybb video tags. - s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + s.gsub!(%r{(^\[video=.*?\])|(\[/video\]$)}, "") s = CGI.unescapeHTML(s) @@ -369,18 +385,16 @@ class ImportScripts::XenForo < ImportScripts::Base # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) # #Fix for the error: xenforo.rb: 160: in `gsub!': invalid byte sequence in UTF-8 (ArgumentError) - if ! s.valid_encoding? - s = s.encode("UTF-16be", invalid: :replace, replace: "?").encode('UTF-8') - end + s = s.encode("UTF-16be", invalid: :replace, replace: "?").encode("UTF-8") if !s.valid_encoding? # Work around it for now: - s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + s.gsub!(%r{\[http(s)?://(www\.)?}, "[") # [QUOTE]...[/QUOTE] - s.gsub!(/\[quote\](.+?)\[\/quote\]/im) { "\n> #{$1}\n" } + s.gsub!(%r{\[quote\](.+?)\[/quote\]}im) { "\n> #{$1}\n" } # Nested Quotes - s.gsub!(/(\[\/?QUOTE.*?\])/mi) { |q| "\n#{q}\n" } + s.gsub!(%r{(\[/?QUOTE.*?\])}mi) { |q| "\n#{q}\n" } # [QUOTE="username, post: 28662, member: 1283"] s.gsub!(/\[quote="(\w+), post: (\d*), member: (\d*)"\]/i) do @@ -396,48 +410,52 @@ class ImportScripts::XenForo < ImportScripts::Base end # [URL=...]...[/URL] - s.gsub!(/\[url="?(.+?)"?\](.+?)\[\/url\]/i) { "[#{$2}](#{$1})" } + s.gsub!(%r{\[url="?(.+?)"?\](.+?)\[/url\]}i) { "[#{$2}](#{$1})" } # [URL]...[/URL] - s.gsub!(/\[url\](.+?)\[\/url\]/i) { " #{$1} " } + s.gsub!(%r{\[url\](.+?)\[/url\]}i) { " #{$1} " } # [IMG]...[/IMG] - s.gsub!(/\[\/?img\]/i, "") + s.gsub!(%r{\[/?img\]}i, "") # convert list tags to ul and list=1 tags to ol # (basically, we're only missing list=a here...) - s.gsub!(/\[list\](.*?)\[\/list\]/im, '[ul]\1[/ul]') - s.gsub!(/\[list=1\](.*?)\[\/list\]/im, '[ol]\1[/ol]') - s.gsub!(/\[list\](.*?)\[\/list:u\]/im, '[ul]\1[/ul]') - s.gsub!(/\[list=1\](.*?)\[\/list:o\]/im, '[ol]\1[/ol]') + s.gsub!(%r{\[list\](.*?)\[/list\]}im, '[ul]\1[/ul]') + s.gsub!(%r{\[list=1\](.*?)\[/list\]}im, '[ol]\1[/ol]') + s.gsub!(%r{\[list\](.*?)\[/list:u\]}im, '[ul]\1[/ul]') + s.gsub!(%r{\[list=1\](.*?)\[/list:o\]}im, '[ol]\1[/ol]') # convert *-tags to li-tags so bbcode-to-md can do its magic on phpBB's lists: - s.gsub!(/\[\*\]\n/, '') - s.gsub!(/\[\*\](.*?)\[\/\*:m\]/, '[li]\1[/li]') + s.gsub!(/\[\*\]\n/, "") + s.gsub!(%r{\[\*\](.*?)\[/\*:m\]}, '[li]\1[/li]') s.gsub!(/\[\*\](.*?)\n/, '[li]\1[/li]') - s.gsub!(/\[\*=1\]/, '') + s.gsub!(/\[\*=1\]/, "") # [YOUTUBE][/YOUTUBE] - s.gsub!(/\[youtube\](.+?)\[\/youtube\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[youtube\](.+?)\[/youtube\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } # [youtube=425,350]id[/youtube] - s.gsub!(/\[youtube="?(.+?)"?\](.+?)\[\/youtube\]/i) { "\nhttps://www.youtube.com/watch?v=#{$2}\n" } + s.gsub!(%r{\[youtube="?(.+?)"?\](.+?)\[/youtube\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$2}\n" + end # [MEDIA=youtube]id[/MEDIA] - s.gsub!(/\[MEDIA=youtube\](.+?)\[\/MEDIA\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[MEDIA=youtube\](.+?)\[/MEDIA\]}i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } # [ame="youtube_link"]title[/ame] - s.gsub!(/\[ame="?(.+?)"?\](.+?)\[\/ame\]/i) { "\n#{$1}\n" } + s.gsub!(%r{\[ame="?(.+?)"?\](.+?)\[/ame\]}i) { "\n#{$1}\n" } # [VIDEO=youtube;]...[/VIDEO] - s.gsub!(/\[video=youtube;([^\]]+)\].*?\[\/video\]/i) { "\nhttps://www.youtube.com/watch?v=#{$1}\n" } + s.gsub!(%r{\[video=youtube;([^\]]+)\].*?\[/video\]}i) do + "\nhttps://www.youtube.com/watch?v=#{$1}\n" + end # [USER=706]@username[/USER] - s.gsub!(/\[user="?(.+?)"?\](.+?)\[\/user\]/i) { $2 } + s.gsub!(%r{\[user="?(.+?)"?\](.+?)\[/user\]}i) { $2 } # Remove the color tag s.gsub!(/\[color=[#a-z0-9]+\]/i, "") - s.gsub!(/\[\/color\]/i, "") + s.gsub!(%r{\[/color\]}i, "") if Dir.exist? ATTACHMENT_DIR s = process_xf_attachments(:gallery, s, import_id) @@ -450,31 +468,36 @@ class ImportScripts::XenForo < ImportScripts::Base def process_xf_attachments(xf_type, s, import_id) ids = Set.new ids.merge(s.scan(get_xf_regexp(xf_type)).map { |x| x[0].to_i }) - + # not all attachments have an [ATTACH=] tag so we need to get the other ID's from the xf_attachment table if xf_type == :attachment && import_id > 0 - sql = "SELECT attachment_id FROM #{TABLE_PREFIX}attachment WHERE content_id=#{import_id} and content_type='post';" - ids.merge(mysql_query(sql).to_a.map { |v| v["attachment_id"].to_i}) + sql = + "SELECT attachment_id FROM #{TABLE_PREFIX}attachment WHERE content_id=#{import_id} and content_type='post';" + ids.merge(mysql_query(sql).to_a.map { |v| v["attachment_id"].to_i }) end - + ids.each do |id| next unless id sql = get_xf_sql(xf_type, id).dup.squish! results = mysql_query(sql) if results.size < 1 # Strip attachment - s.gsub!(get_xf_regexp(xf_type, id), '') + s.gsub!(get_xf_regexp(xf_type, id), "") STDERR.puts "#{xf_type.capitalize} id #{id} not found in source database. Stripping." next end - original_filename = results.first['filename'] + original_filename = results.first["filename"] result = results.first - upload = import_xf_attachment(result['data_id'], result['file_hash'], result['user_id'], original_filename) + upload = + import_xf_attachment( + result["data_id"], + result["file_hash"], + result["user_id"], + original_filename, + ) if upload && upload.present? && upload.persisted? html = @uploader.html_for_upload(upload, original_filename) - unless s.gsub!(get_xf_regexp(xf_type, id), html) - s = s + "\n\n#{html}\n\n" - end + s = s + "\n\n#{html}\n\n" unless s.gsub!(get_xf_regexp(xf_type, id), html) else STDERR.puts "Could not process upload: #{original_filename}. Skipping attachment id #{id}" end @@ -502,7 +525,7 @@ class ImportScripts::XenForo < ImportScripts::Base when :gallery Regexp.new(/\[GALLERY=media,\s#{id ? id : '(\d+)'}\].+?\]/i) when :attachment - Regexp.new(/\[ATTACH(?>=\w+)?\]#{id ? id : '(\d+)'}\[\/ATTACH\]/i) + Regexp.new(%r{\[ATTACH(?>=\w+)?\]#{id ? id : '(\d+)'}\[/ATTACH\]}i) end end @@ -519,7 +542,7 @@ class ImportScripts::XenForo < ImportScripts::Base when :attachment <<-SQL SELECT a.attachment_id, a.data_id, d.filename, d.file_hash, d.user_id - FROM #{TABLE_PREFIX}attachment AS a + FROM #{TABLE_PREFIX}attachment AS a INNER JOIN #{TABLE_PREFIX}attachment_data d ON a.data_id = d.data_id WHERE attachment_id = #{id} AND content_type = 'post' diff --git a/script/import_scripts/yahoogroup.rb b/script/import_scripts/yahoogroup.rb index 93651e5d7f..4c307f506e 100644 --- a/script/import_scripts/yahoogroup.rb +++ b/script/import_scripts/yahoogroup.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require File.expand_path(File.dirname(__FILE__) + "/base.rb") -require 'mongo' +require "mongo" # Import YahooGroups data as exported into MongoDB by: # https://github.com/jonbartlett/yahoo-groups-export @@ -13,14 +13,13 @@ require 'mongo' # =end class ImportScripts::YahooGroup < ImportScripts::Base - - MONGODB_HOST = '192.168.10.1:27017' - MONGODB_DB = 'syncro' + MONGODB_HOST = "192.168.10.1:27017" + MONGODB_DB = "syncro" def initialize super - client = Mongo::Client.new([ MONGODB_HOST ], database: MONGODB_DB) + client = Mongo::Client.new([MONGODB_HOST], database: MONGODB_DB) db = client.database Mongo::Logger.logger.level = Logger::FATAL puts "connected to db...." @@ -28,7 +27,6 @@ class ImportScripts::YahooGroup < ImportScripts::Base @collection = client[:posts] @user_profile_map = {} - end def execute @@ -41,43 +39,42 @@ class ImportScripts::YahooGroup < ImportScripts::Base end def import_users - - puts '', "Importing users" + puts "", "Importing users" # fetch distinct list of Yahoo "profile" names - profiles = @collection.aggregate( - [ - { "$group": { "_id": { profile: "$ygData.profile" } } } - ] - ) + profiles = @collection.aggregate([{ "$group": { _id: { profile: "$ygData.profile" } } }]) user_id = 0 create_users(profiles.to_a) do |u| - user_id = user_id + 1 # fetch last message for profile to pickup latest user info as this may have changed - user_info = @collection.find("ygData.profile": u["_id"]["profile"]).sort("ygData.msgId": -1).limit(1).to_a[0] + user_info = + @collection + .find("ygData.profile": u["_id"]["profile"]) + .sort("ygData.msgId": -1) + .limit(1) + .to_a[ + 0 + ] # Store user_id to profile lookup @user_profile_map.store(user_info["ygData"]["profile"], user_id) puts "User created: #{user_info["ygData"]["profile"]}" - user = - { - id: user_id, # yahoo "userId" sequence appears to have changed mid forum life so generate this + user = { + id: user_id, # yahoo "userId" sequence appears to have changed mid forum life so generate this username: user_info["ygData"]["profile"], name: user_info["ygData"]["authorName"], email: user_info["ygData"]["from"], # mandatory - created_at: Time.now + created_at: Time.now, } user end puts "#{user_id} users created" - end def import_discussions @@ -86,21 +83,16 @@ class ImportScripts::YahooGroup < ImportScripts::Base topics_count = 0 posts_count = 0 - topics = @collection.aggregate( - [ - { "$group": { "_id": { topicId: "$ygData.topicId" } } } - ] - ).to_a + topics = @collection.aggregate([{ "$group": { _id: { topicId: "$ygData.topicId" } } }]).to_a # for each distinct topicId found topics.each_with_index do |t, tidx| - # create "topic" post first. # fetch topic document topic_post = @collection.find("ygData.msgId": t["_id"]["topicId"]).to_a[0] next if topic_post.nil? - puts "Topic: #{tidx + 1} / #{topics.count()} (#{sprintf('%.2f', ((tidx + 1).to_f / topics.count().to_f) * 100)}%) Subject: #{topic_post["ygData"]["subject"]}" + puts "Topic: #{tidx + 1} / #{topics.count()} (#{sprintf("%.2f", ((tidx + 1).to_f / topics.count().to_f) * 100)}%) Subject: #{topic_post["ygData"]["subject"]}" if topic_post["ygData"]["subject"].to_s.empty? topic_title = "No Subject" @@ -115,8 +107,10 @@ class ImportScripts::YahooGroup < ImportScripts::Base created_at: Time.at(topic_post["ygData"]["postDate"].to_i), cook_method: Post.cook_methods[:raw_html], title: topic_title, - category: ENV['CATEGORY_ID'], - custom_fields: { import_id: topic_post["ygData"]["msgId"] } + category: ENV["CATEGORY_ID"], + custom_fields: { + import_id: topic_post["ygData"]["msgId"], + }, } topics_count += 1 @@ -128,34 +122,31 @@ class ImportScripts::YahooGroup < ImportScripts::Base posts = @collection.find("ygData.topicId": topic_post["ygData"]["topicId"]).to_a posts.each_with_index do |p, pidx| - # skip over first post as this is created by topic above next if p["ygData"]["msgId"] == topic_post["ygData"]["topicId"] puts " Post: #{pidx + 1} / #{posts.count()}" post = { - id: pidx + 1, - topic_id: parent_post[:topic_id], - user_id: @user_profile_map[p["ygData"]["profile"]] || -1, - raw: p["ygData"]["messageBody"], - created_at: Time.at(p["ygData"]["postDate"].to_i), - cook_method: Post.cook_methods[:raw_html], - custom_fields: { import_id: p["ygData"]["msgId"] } + id: pidx + 1, + topic_id: parent_post[:topic_id], + user_id: @user_profile_map[p["ygData"]["profile"]] || -1, + raw: p["ygData"]["messageBody"], + created_at: Time.at(p["ygData"]["postDate"].to_i), + cook_method: Post.cook_methods[:raw_html], + custom_fields: { + import_id: p["ygData"]["msgId"], + }, } child_post = create_post(post, post[:id]) posts_count += 1 - end - end puts "", "Imported #{topics_count} topics with #{topics_count + posts_count} posts." - end - end ImportScripts::YahooGroup.new.perform diff --git a/script/import_scripts/zendesk.rb b/script/import_scripts/zendesk.rb index a8e44f5ffd..a8b5ef59ac 100644 --- a/script/import_scripts/zendesk.rb +++ b/script/import_scripts/zendesk.rb @@ -9,10 +9,10 @@ # - posts.csv (posts in Zendesk are topics in Discourse) # - comments.csv (comments in Zendesk are posts in Discourse) -require 'csv' -require 'reverse_markdown' -require_relative 'base' -require_relative 'base/generic_database' +require "csv" +require "reverse_markdown" +require_relative "base" +require_relative "base/generic_database" # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/zendesk.rb DIRNAME @@ -45,7 +45,7 @@ class ImportScripts::Zendesk < ImportScripts::Base name: row[:name], description: row[:description], position: row[:position], - url: row[:htmlurl] + url: row[:htmlurl], ) end @@ -56,7 +56,7 @@ class ImportScripts::Zendesk < ImportScripts::Base name: row[:name], created_at: parse_datetime(row[:createdat]), last_seen_at: parse_datetime(row[:lastloginat]), - active: true + active: true, ) end @@ -69,7 +69,7 @@ class ImportScripts::Zendesk < ImportScripts::Base closed: row[:closed] == "TRUE", user_id: row[:authorid], created_at: parse_datetime(row[:createdat]), - url: row[:htmlurl] + url: row[:htmlurl], ) end @@ -80,7 +80,7 @@ class ImportScripts::Zendesk < ImportScripts::Base topic_id: row[:postid], user_id: row[:authorid], created_at: parse_datetime(row[:createdat]), - url: row[:htmlurl] + url: row[:htmlurl], ) end @@ -99,14 +99,15 @@ class ImportScripts::Zendesk < ImportScripts::Base create_categories(rows) do |row| { - id: row['id'], - name: row['name'], - description: row['description'], - position: row['position'], - post_create_action: proc do |category| - url = remove_domain(row['url']) - Permalink.create(url: url, category_id: category.id) unless permalink_exists?(url) - end + id: row["id"], + name: row["name"], + description: row["description"], + position: row["position"], + post_create_action: + proc do |category| + url = remove_domain(row["url"]) + Permalink.create(url: url, category_id: category.id) unless permalink_exists?(url) + end, } end end @@ -118,22 +119,22 @@ class ImportScripts::Zendesk < ImportScripts::Base def import_users puts "", "creating users" total_count = @db.count_users - last_id = '' + last_id = "" batches do |offset| rows, last_id = @db.fetch_users(last_id) break if rows.empty? - next if all_records_exist?(:users, rows.map { |row| row['id'] }) + next if all_records_exist?(:users, rows.map { |row| row["id"] }) create_users(rows, total: total_count, offset: offset) do |row| { - id: row['id'], - email: row['email'], - name: row['name'], - created_at: row['created_at'], - last_seen_at: row['last_seen_at'], - active: row['active'] == 1 + id: row["id"], + email: row["email"], + name: row["name"], + created_at: row["created_at"], + last_seen_at: row["last_seen_at"], + active: row["active"] == 1, } end end @@ -142,27 +143,28 @@ class ImportScripts::Zendesk < ImportScripts::Base def import_topics puts "", "creating topics" total_count = @db.count_topics - last_id = '' + last_id = "" batches do |offset| rows, last_id = @db.fetch_topics(last_id) break if rows.empty? - next if all_records_exist?(:posts, rows.map { |row| import_topic_id(row['id']) }) + next if all_records_exist?(:posts, rows.map { |row| import_topic_id(row["id"]) }) create_posts(rows, total: total_count, offset: offset) do |row| { - id: import_topic_id(row['id']), - title: row['title'].present? ? row['title'].strip[0...255] : "Topic title missing", - raw: normalize_raw(row['raw']), - category: category_id_from_imported_category_id(row['category_id']), - user_id: user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id, - created_at: row['created_at'], - closed: row['closed'] == 1, - post_create_action: proc do |post| - url = remove_domain(row['url']) - Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) - end + id: import_topic_id(row["id"]), + title: row["title"].present? ? row["title"].strip[0...255] : "Topic title missing", + raw: normalize_raw(row["raw"]), + category: category_id_from_imported_category_id(row["category_id"]), + user_id: user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, + created_at: row["created_at"], + closed: row["closed"] == 1, + post_create_action: + proc do |post| + url = remove_domain(row["url"]) + Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) + end, } end end @@ -181,34 +183,35 @@ class ImportScripts::Zendesk < ImportScripts::Base rows, last_row_id = @db.fetch_sorted_posts(last_row_id) break if rows.empty? - next if all_records_exist?(:posts, rows.map { |row| row['id'] }) + next if all_records_exist?(:posts, rows.map { |row| row["id"] }) create_posts(rows, total: total_count, offset: offset) do |row| - topic = topic_lookup_from_imported_post_id(import_topic_id(row['topic_id'])) + topic = topic_lookup_from_imported_post_id(import_topic_id(row["topic_id"])) if topic.nil? - p "MISSING TOPIC #{row['topic_id']}" + p "MISSING TOPIC #{row["topic_id"]}" p row next end { - id: row['id'], - raw: normalize_raw(row['raw']), - user_id: user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id, + id: row["id"], + raw: normalize_raw(row["raw"]), + user_id: user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, topic_id: topic[:topic_id], - created_at: row['created_at'], - post_create_action: proc do |post| - url = remove_domain(row['url']) - Permalink.create(url: url, post_id: post.id) unless permalink_exists?(url) - end + created_at: row["created_at"], + post_create_action: + proc do |post| + url = remove_domain(row["url"]) + Permalink.create(url: url, post_id: post.id) unless permalink_exists?(url) + end, } end end end def normalize_raw(raw) - raw = raw.gsub('\n', '') + raw = raw.gsub('\n', "") raw = ReverseMarkdown.convert(raw) raw end @@ -222,11 +225,13 @@ class ImportScripts::Zendesk < ImportScripts::Base end def csv_parse(table_name) - CSV.foreach(File.join(@path, "#{table_name}.csv"), - headers: true, - header_converters: :symbol, - skip_blanks: true, - encoding: 'bom|utf-8') { |row| yield row } + CSV.foreach( + File.join(@path, "#{table_name}.csv"), + headers: true, + header_converters: :symbol, + skip_blanks: true, + encoding: "bom|utf-8", + ) { |row| yield row } end end diff --git a/script/import_scripts/zendesk_api.rb b/script/import_scripts/zendesk_api.rb index 9237a76442..d76ff1652d 100644 --- a/script/import_scripts/zendesk_api.rb +++ b/script/import_scripts/zendesk_api.rb @@ -4,10 +4,10 @@ # # This one uses their API. -require 'open-uri' -require 'reverse_markdown' -require_relative 'base' -require_relative 'base/generic_database' +require "open-uri" +require "reverse_markdown" +require_relative "base" +require_relative "base/generic_database" # Call it like this: # RAILS_ENV=production bundle exec ruby script/import_scripts/zendesk_api.rb SOURCE_URL DIRNAME AUTH_EMAIL AUTH_TOKEN @@ -23,7 +23,7 @@ class ImportScripts::ZendeskApi < ImportScripts::Base Net::ProtocolError, Timeout::Error, OpenURI::HTTPError, - OpenSSL::SSL::SSLError + OpenSSL::SSL::SSLError, ] MAX_RETRIES = 5 @@ -62,66 +62,72 @@ class ImportScripts::ZendeskApi < ImportScripts::Base end def fetch_categories - puts '', 'fetching categories...' + puts "", "fetching categories..." - get_from_api('/api/v2/community/topics.json', 'topics', show_status: true) do |row| + get_from_api("/api/v2/community/topics.json", "topics", show_status: true) do |row| @db.insert_category( - id: row['id'], - name: row['name'], - description: row['description'], - position: row['position'], - url: row['html_url'] + id: row["id"], + name: row["name"], + description: row["description"], + position: row["position"], + url: row["html_url"], ) end end def fetch_topics - puts '', 'fetching topics...' + puts "", "fetching topics..." - get_from_api('/api/v2/community/posts.json', 'posts', show_status: true) do |row| - if row['vote_count'] > 0 - like_user_ids = fetch_likes("/api/v2/community/posts/#{row['id']}/votes.json") + get_from_api("/api/v2/community/posts.json", "posts", show_status: true) do |row| + if row["vote_count"] > 0 + like_user_ids = fetch_likes("/api/v2/community/posts/#{row["id"]}/votes.json") end @db.insert_topic( - id: row['id'], - title: row['title'], - raw: row['details'], - category_id: row['topic_id'], - closed: row['closed'], - user_id: row['author_id'], - created_at: row['created_at'], - url: row['html_url'], - like_user_ids: like_user_ids + id: row["id"], + title: row["title"], + raw: row["details"], + category_id: row["topic_id"], + closed: row["closed"], + user_id: row["author_id"], + created_at: row["created_at"], + url: row["html_url"], + like_user_ids: like_user_ids, ) end end def fetch_posts - puts '', 'fetching posts...' + puts "", "fetching posts..." current_count = 0 total_count = @db.count_topics start_time = Time.now - last_id = '' + last_id = "" batches do |offset| rows, last_id = @db.fetch_topics(last_id) break if rows.empty? rows.each do |topic_row| - get_from_api("/api/v2/community/posts/#{topic_row['id']}/comments.json", 'comments') do |row| - if row['vote_count'] > 0 - like_user_ids = fetch_likes("/api/v2/community/posts/#{topic_row['id']}/comments/#{row['id']}/votes.json") + get_from_api( + "/api/v2/community/posts/#{topic_row["id"]}/comments.json", + "comments", + ) do |row| + if row["vote_count"] > 0 + like_user_ids = + fetch_likes( + "/api/v2/community/posts/#{topic_row["id"]}/comments/#{row["id"]}/votes.json", + ) end @db.insert_post( - id: row['id'], - raw: row['body'], - topic_id: topic_row['id'], - user_id: row['author_id'], - created_at: row['created_at'], - url: row['html_url'], - like_user_ids: like_user_ids + id: row["id"], + raw: row["body"], + topic_id: topic_row["id"], + user_id: row["author_id"], + created_at: row["created_at"], + url: row["html_url"], + like_user_ids: like_user_ids, ) end @@ -132,9 +138,9 @@ class ImportScripts::ZendeskApi < ImportScripts::Base end def fetch_users - puts '', 'fetching users...' + puts "", "fetching users..." - user_ids = @db.execute_sql(<<~SQL).map { |row| row['user_id'] } + user_ids = @db.execute_sql(<<~SQL).map { |row| row["user_id"] } SELECT user_id FROM topic UNION SELECT user_id FROM post @@ -147,15 +153,18 @@ class ImportScripts::ZendeskApi < ImportScripts::Base start_time = Time.now while !user_ids.empty? - get_from_api("/api/v2/users/show_many.json?ids=#{user_ids.shift(50).join(',')}", 'users') do |row| + get_from_api( + "/api/v2/users/show_many.json?ids=#{user_ids.shift(50).join(",")}", + "users", + ) do |row| @db.insert_user( - id: row['id'], - email: row['email'], - name: row['name'], - created_at: row['created_at'], - last_seen_at: row['last_login_at'], - active: row['active'], - avatar_path: row['photo'].present? ? row['photo']['content_url'] : nil + id: row["id"], + email: row["email"], + name: row["name"], + created_at: row["created_at"], + last_seen_at: row["last_login_at"], + active: row["active"], + avatar_path: row["photo"].present? ? row["photo"]["content_url"] : nil, ) current_count += 1 @@ -167,10 +176,8 @@ class ImportScripts::ZendeskApi < ImportScripts::Base def fetch_likes(url) user_ids = [] - get_from_api(url, 'votes') do |row| - if row['id'].present? && row['value'] == 1 - user_ids << row['user_id'] - end + get_from_api(url, "votes") do |row| + user_ids << row["user_id"] if row["id"].present? && row["value"] == 1 end user_ids @@ -182,14 +189,15 @@ class ImportScripts::ZendeskApi < ImportScripts::Base create_categories(rows) do |row| { - id: row['id'], - name: row['name'], - description: row['description'], - position: row['position'], - post_create_action: proc do |category| - url = remove_domain(row['url']) - Permalink.create(url: url, category_id: category.id) unless permalink_exists?(url) - end + id: row["id"], + name: row["name"], + description: row["description"], + position: row["position"], + post_create_action: + proc do |category| + url = remove_domain(row["url"]) + Permalink.create(url: url, category_id: category.id) unless permalink_exists?(url) + end, } end end @@ -197,27 +205,32 @@ class ImportScripts::ZendeskApi < ImportScripts::Base def import_users puts "", "creating users" total_count = @db.count_users - last_id = '' + last_id = "" batches do |offset| rows, last_id = @db.fetch_users(last_id) break if rows.empty? - next if all_records_exist?(:users, rows.map { |row| row['id'] }) + next if all_records_exist?(:users, rows.map { |row| row["id"] }) create_users(rows, total: total_count, offset: offset) do |row| { - id: row['id'], - email: row['email'], - name: row['name'], - created_at: row['created_at'], - last_seen_at: row['last_seen_at'], - active: row['active'] == 1, - post_create_action: proc do |user| - if row['avatar_path'].present? - UserAvatar.import_url_for_user(row['avatar_path'], user) rescue nil - end - end + id: row["id"], + email: row["email"], + name: row["name"], + created_at: row["created_at"], + last_seen_at: row["last_seen_at"], + active: row["active"] == 1, + post_create_action: + proc do |user| + if row["avatar_path"].present? + begin + UserAvatar.import_url_for_user(row["avatar_path"], user) + rescue StandardError + nil + end + end + end, } end end @@ -226,27 +239,32 @@ class ImportScripts::ZendeskApi < ImportScripts::Base def import_topics puts "", "creating topics" total_count = @db.count_topics - last_id = '' + last_id = "" batches do |offset| rows, last_id = @db.fetch_topics(last_id) break if rows.empty? - next if all_records_exist?(:posts, rows.map { |row| import_topic_id(row['id']) }) + next if all_records_exist?(:posts, rows.map { |row| import_topic_id(row["id"]) }) create_posts(rows, total: total_count, offset: offset) do |row| { - id: import_topic_id(row['id']), - title: row['title'].present? ? row['title'].strip[0...255] : "Topic title missing", - raw: normalize_raw(row['raw'], user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id), - category: category_id_from_imported_category_id(row['category_id']), - user_id: user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id, - created_at: row['created_at'], - closed: row['closed'] == 1, - post_create_action: proc do |post| - url = remove_domain(row['url']) - Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) - end + id: import_topic_id(row["id"]), + title: row["title"].present? ? row["title"].strip[0...255] : "Topic title missing", + raw: + normalize_raw( + row["raw"], + user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, + ), + category: category_id_from_imported_category_id(row["category_id"]), + user_id: user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, + created_at: row["created_at"], + closed: row["closed"] == 1, + post_create_action: + proc do |post| + url = remove_domain(row["url"]) + Permalink.create(url: url, topic_id: post.topic.id) unless permalink_exists?(url) + end, } end end @@ -266,24 +284,29 @@ class ImportScripts::ZendeskApi < ImportScripts::Base break if rows.empty? create_posts(rows, total: total_count, offset: offset) do |row| - topic = topic_lookup_from_imported_post_id(import_topic_id(row['topic_id'])) + topic = topic_lookup_from_imported_post_id(import_topic_id(row["topic_id"])) if topic.nil? - p "MISSING TOPIC #{row['topic_id']}" + p "MISSING TOPIC #{row["topic_id"]}" p row next end { - id: row['id'], - raw: normalize_raw(row['raw'], user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id), - user_id: user_id_from_imported_user_id(row['user_id']) || Discourse.system_user.id, + id: row["id"], + raw: + normalize_raw( + row["raw"], + user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, + ), + user_id: user_id_from_imported_user_id(row["user_id"]) || Discourse.system_user.id, topic_id: topic[:topic_id], - created_at: row['created_at'], - post_create_action: proc do |post| - url = remove_domain(row['url']) - Permalink.create(url: url, post_id: post.id) unless permalink_exists?(url) - end + created_at: row["created_at"], + post_create_action: + proc do |post| + url = remove_domain(row["url"]) + Permalink.create(url: url, post_id: post.id) unless permalink_exists?(url) + end, } end end @@ -301,9 +324,9 @@ class ImportScripts::ZendeskApi < ImportScripts::Base break if rows.empty? rows.each do |row| - import_id = row['topic_id'] ? import_topic_id(row['topic_id']) : row['post_id'] + import_id = row["topic_id"] ? import_topic_id(row["topic_id"]) : row["post_id"] post = Post.find_by(id: post_id_from_imported_post_id(import_id)) if import_id - user = User.find_by(id: user_id_from_imported_user_id(row['user_id'])) + user = User.find_by(id: user_id_from_imported_user_id(row["user_id"])) if post && user begin @@ -312,7 +335,7 @@ class ImportScripts::ZendeskApi < ImportScripts::Base puts "error acting on post #{e}" end else - puts "Skipping Like from #{row['user_id']} on topic #{row['topic_id']} / post #{row['post_id']}" + puts "Skipping Like from #{row["user_id"]} on topic #{row["topic_id"]} / post #{row["post_id"]}" end current_count += 1 @@ -322,23 +345,23 @@ class ImportScripts::ZendeskApi < ImportScripts::Base end def normalize_raw(raw, user_id) - raw = raw.gsub('\n', '') + raw = raw.gsub('\n', "") raw = ReverseMarkdown.convert(raw) # Process images, after the ReverseMarkdown they look like # ![](https://.zendesk.com/.) - raw.gsub!(/!\[\]\((https:\/\/#{SUBDOMAIN}\.zendesk\.com\/hc\/user_images\/([^).]+\.[^)]+))\)/i) do + raw.gsub!(%r{!\[\]\((https://#{SUBDOMAIN}\.zendesk\.com/hc/user_images/([^).]+\.[^)]+))\)}i) do image_url = $1 filename = $2 attempts = 0 begin - URI.parse(image_url).open do |image| - # IMAGE_DOWNLOAD_PATH is whatever image, it will be replaced with the downloaded image - File.open(IMAGE_DOWNLOAD_PATH, "wb") do |file| - file.write(image.read) + URI + .parse(image_url) + .open do |image| + # IMAGE_DOWNLOAD_PATH is whatever image, it will be replaced with the downloaded image + File.open(IMAGE_DOWNLOAD_PATH, "wb") { |file| file.write(image.read) } end - end rescue *HTTP_ERRORS => e if attempts < MAX_RETRIES attempts += 1 @@ -374,23 +397,25 @@ class ImportScripts::ZendeskApi < ImportScripts::Base end def connection - @_connection ||= begin - connect_uri = URI.parse(@source_url) + @_connection ||= + begin + connect_uri = URI.parse(@source_url) - http = Net::HTTP.new(connect_uri.host, connect_uri.port) - http.open_timeout = 30 - http.read_timeout = 30 - http.use_ssl = connect_uri.scheme == "https" + http = Net::HTTP.new(connect_uri.host, connect_uri.port) + http.open_timeout = 30 + http.read_timeout = 30 + http.use_ssl = connect_uri.scheme == "https" - http - end + http + end end def authorization - @_authorization ||= begin - auth_str = "#{@auth_email}/token:#{@auth_token}" - "Basic #{Base64.strict_encode64(auth_str)}" - end + @_authorization ||= + begin + auth_str = "#{@auth_email}/token:#{@auth_token}" + "Basic #{Base64.strict_encode64(auth_str)}" + end end def get_from_api(path, array_name, show_status: false) @@ -399,8 +424,8 @@ class ImportScripts::ZendeskApi < ImportScripts::Base while url get = Net::HTTP::Get.new(url) - get['User-Agent'] = 'Discourse Zendesk Importer' - get['Authorization'] = authorization + get["User-Agent"] = "Discourse Zendesk Importer" + get["Authorization"] = authorization retry_count = 0 @@ -420,26 +445,27 @@ class ImportScripts::ZendeskApi < ImportScripts::Base json = JSON.parse(response.body) - json[array_name].each do |row| - yield row - end + json[array_name].each { |row| yield row } - url = json['next_page'] + url = json["next_page"] if show_status - if json['page'] && json['page_count'] - print_status(json['page'], json['page_count'], start_time) + if json["page"] && json["page_count"] + print_status(json["page"], json["page_count"], start_time) else - print '.' + print "." end end end end - end unless ARGV.length == 4 && Dir.exist?(ARGV[1]) - puts "", "Usage:", "", "bundle exec ruby script/import_scripts/zendesk_api.rb SOURCE_URL DIRNAME AUTH_EMAIL AUTH_TOKEN", "" + puts "", + "Usage:", + "", + "bundle exec ruby script/import_scripts/zendesk_api.rb SOURCE_URL DIRNAME AUTH_EMAIL AUTH_TOKEN", + "" exit 1 end diff --git a/script/import_scripts/zoho.rb b/script/import_scripts/zoho.rb index e354b79f8d..b05debeb7e 100644 --- a/script/import_scripts/zoho.rb +++ b/script/import_scripts/zoho.rb @@ -21,14 +21,13 @@ # full names instead of usernames. This may cause duplicate users with slightly different # usernames to be created. -require 'csv' +require "csv" require File.expand_path(File.dirname(__FILE__) + "/base.rb") require File.expand_path(File.dirname(__FILE__) + "/base/csv_helper.rb") # Call it like this: # bundle exec ruby script/import_scripts/zoho.rb class ImportScripts::Zoho < ImportScripts::Base - include ImportScripts::CsvHelper BATCH_SIZE = 1000 @@ -50,19 +49,14 @@ class ImportScripts::Zoho < ImportScripts::Base end def cleanup_zoho_username(s) - s.strip.gsub(/[^A-Za-z0-9_\.\-]/, '') + s.strip.gsub(/[^A-Za-z0-9_\.\-]/, "") end def import_users puts "", "Importing users" - create_users(CSV.parse(File.read(File.join(@path, 'users.csv')))) do |u| + create_users(CSV.parse(File.read(File.join(@path, "users.csv")))) do |u| username = cleanup_zoho_username(u[0]) - { - id: username, - username: username, - email: u[1], - created_at: Time.zone.now - } + { id: username, username: username, email: u[1], created_at: Time.zone.now } end end @@ -83,9 +77,7 @@ class ImportScripts::Zoho < ImportScripts::Base csv_parse(File.join(@path, "posts.csv")) do |row| @all_posts << row.dup - if @categories[row.forum_name].nil? - @categories[row.forum_name] = [] - end + @categories[row.forum_name] = [] if @categories[row.forum_name].nil? unless @categories[row.forum_name].include?(row.category_name) @categories[row.forum_name] << row.category_name @@ -105,56 +97,61 @@ class ImportScripts::Zoho < ImportScripts::Base puts "", "Creating topics and posts" - created, skipped = create_posts(@all_posts, total: @all_posts.size) do |row| - @current_row = row + created, skipped = + create_posts(@all_posts, total: @all_posts.size) do |row| + @current_row = row - # fetch user - username = cleanup_zoho_username(row.author) + # fetch user + username = cleanup_zoho_username(row.author) - next if username.blank? # no author for this post, so skip + next if username.blank? # no author for this post, so skip - user_id = user_id_from_imported_user_id(username) + user_id = user_id_from_imported_user_id(username) - if user_id.nil? - # user CSV file didn't have a user with this username. create it now with an invalid email address. - u = create_user( - { id: username, - username: username, - email: "#{username}@example.com", - created_at: Time.zone.parse(row.posted_time) }, - username - ) - user_id = u.id - end - - if @topic_mapping[row.permalink].nil? - category_id = nil - if row.category_name != "Uncategorized" && row.category_name != "Uncategorised" - category_id = category_id_from_imported_category_id("#{row.forum_name}:#{row.category_name}") - else - category_id = category_id_from_imported_category_id(row.forum_name) + if user_id.nil? + # user CSV file didn't have a user with this username. create it now with an invalid email address. + u = + create_user( + { + id: username, + username: username, + email: "#{username}@example.com", + created_at: Time.zone.parse(row.posted_time), + }, + username, + ) + user_id = u.id end - # create topic - { - id: import_post_id(row), - user_id: user_id, - category: category_id, - title: CGI.unescapeHTML(row.topic_title), - raw: cleanup_post(row.content), - created_at: Time.zone.parse(row.posted_time) - } - # created_post callback will be called - else - { - id: import_post_id(row), - user_id: user_id, - raw: cleanup_post(row.content), - created_at: Time.zone.parse(row.posted_time), - topic_id: @topic_mapping[row.permalink] - } + if @topic_mapping[row.permalink].nil? + category_id = nil + if row.category_name != "Uncategorized" && row.category_name != "Uncategorised" + category_id = + category_id_from_imported_category_id("#{row.forum_name}:#{row.category_name}") + else + category_id = category_id_from_imported_category_id(row.forum_name) + end + + # create topic + { + id: import_post_id(row), + user_id: user_id, + category: category_id, + title: CGI.unescapeHTML(row.topic_title), + raw: cleanup_post(row.content), + created_at: Time.zone.parse(row.posted_time), + } + # created_post callback will be called + else + { + id: import_post_id(row), + user_id: user_id, + raw: cleanup_post(row.content), + created_at: Time.zone.parse(row.posted_time), + topic_id: @topic_mapping[row.permalink], + } + end end - end puts "" puts "Created: #{created}" @@ -176,31 +173,30 @@ class ImportScripts::Zoho < ImportScripts::Base STYLE_ATTR = /(\s)*style="(.)*"/ def cleanup_post(raw) - # Check if Zoho's most common form of a code block is present. # If so, don't clean up the post as much because we can't tell which markup # is inside the code block. These posts will look worse than others. has_code_block = !!(raw =~ ZOHO_CODE_BLOCK_START) - x = raw.gsub(STYLE_ATTR, '') + x = raw.gsub(STYLE_ATTR, "") if has_code_block # We have to assume all lists in this post are meant to be code blocks # to make it somewhat readable. x.gsub!(/( )*
      (\s)*/, "") - x.gsub!(/( )*<\/ol>/, "") - x.gsub!('
    1. ', '') - x.gsub!('
    2. ', '') + x.gsub!(%r{( )*
    }, "") + x.gsub!("
  • ", "") + x.gsub!("
  • ", "") else # No code block (probably...) so clean up more aggressively. x.gsub!("\n", " ") - x.gsub!('
    ', "\n\n") - x.gsub('
    ', ' ') + x.gsub!("
    ", "\n\n") + x.gsub("
    ", " ") x.gsub!("
    ", "\n") - x.gsub!('', '') - x.gsub!('', '') - x.gsub!(/]*)>/, '') - x.gsub!('', '') + x.gsub!("", "") + x.gsub!("", "") + x.gsub!(/]*)>/, "") + x.gsub!("", "") end x.gsub!(TOO_MANY_LINE_BREAKS, "\n\n") @@ -213,13 +209,10 @@ class ImportScripts::Zoho < ImportScripts::Base # The posted_time seems to be the same for all posts in a topic, so we can't use that. Digest::SHA1.hexdigest "#{row.permalink}:#{row.content}" end - end unless ARGV[0] && Dir.exist?(ARGV[0]) - if ARGV[0] && !Dir.exist?(ARGV[0]) - puts "", "ERROR! Dir #{ARGV[0]} not found.", "" - end + puts "", "ERROR! Dir #{ARGV[0]} not found.", "" if ARGV[0] && !Dir.exist?(ARGV[0]) puts "", "Usage:", "", " bundle exec ruby script/import_scripts/zoho.rb DIRNAME", "" exit 1 diff --git a/script/measure.rb b/script/measure.rb index 9711206ae6..1dc391735b 100644 --- a/script/measure.rb +++ b/script/measure.rb @@ -1,38 +1,36 @@ # frozen_string_literal: true # using this script to try figure out why Ruby 2 is slower than 1.9 -require 'flamegraph' +require "flamegraph" -Flamegraph.generate('test.html', fidelity: 2) do +Flamegraph.generate("test.html", fidelity: 2) do require File.expand_path("../../config/environment", __FILE__) end exit -require 'memory_profiler' +require "memory_profiler" -result = MemoryProfiler.report do - require File.expand_path("../../config/environment", __FILE__) -end +result = MemoryProfiler.report { require File.expand_path("../../config/environment", __FILE__) } result.pretty_print exit -require 'benchmark' +require "benchmark" def profile_allocations(name) GC.disable initial_size = ObjectSpace.count_objects yield changes = ObjectSpace.count_objects - changes.each do |k, _| - changes[k] -= initial_size[k] - end + changes.each { |k, _| changes[k] -= initial_size[k] } puts "#{name} changes" - changes.sort { |a, b| b[1] <=> a[1] }.each do |a, b| - next if b <= 0 - # 1 extra hash for tracking - puts "#{a} #{a == :T_HASH ? b - 1 : b}" - end + changes + .sort { |a, b| b[1] <=> a[1] } + .each do |a, b| + next if b <= 0 + # 1 extra hash for tracking + puts "#{a} #{a == :T_HASH ? b - 1 : b}" + end GC.enable end @@ -47,9 +45,7 @@ def profile(name, &block) ObjectSpace.trace_object_allocations do block.call - ObjectSpace.each_object do |o| - objs << o - end + ObjectSpace.each_object { |o| objs << o } objs.each do |o| g = ObjectSpace.allocation_generation(o) @@ -63,9 +59,10 @@ def profile(name, &block) end end - items.group_by { |x| x }.sort { |a, b| b[1].length <=> a[1].length }.each do |row, group| - puts "#{row} x #{group.length}" - end + items + .group_by { |x| x } + .sort { |a, b| b[1].length <=> a[1].length } + .each { |row, group| puts "#{row} x #{group.length}" } GC.enable profile_allocations(name, &block) diff --git a/script/memstats.rb b/script/memstats.rb index 998d8d525f..4ca1f531dc 100755 --- a/script/memstats.rb +++ b/script/memstats.rb @@ -28,7 +28,7 @@ #------------------------------------------------------------------------------ class Mapping - FIELDS = %w[ size rss shared_clean shared_dirty private_clean private_dirty swap pss ] + FIELDS = %w[size rss shared_clean shared_dirty private_clean private_dirty swap pss] attr_reader :address_start attr_reader :address_end attr_reader :perms @@ -48,15 +48,10 @@ class Mapping attr_accessor :pss def initialize(lines) - - FIELDS.each do |field| - self.public_send("#{field}=", 0) - end + FIELDS.each { |field| self.public_send("#{field}=", 0) } parse_first_line(lines.shift) - lines.each do |l| - parse_field_line(l) - end + lines.each { |l| parse_field_line(l) } end def parse_first_line(line) @@ -71,7 +66,7 @@ class Mapping def parse_field_line(line) parts = line.strip.split - field = parts[0].downcase.sub(':', '') + field = parts[0].downcase.sub(":", "") if respond_to? "#{field}=" value = Float(parts[1]).to_i self.public_send("#{field}=", value) @@ -82,26 +77,21 @@ end def consume_mapping(map_lines, totals) m = Mapping.new(map_lines) - Mapping::FIELDS.each do |field| - totals[field] += m.public_send(field) - end + Mapping::FIELDS.each { |field| totals[field] += m.public_send(field) } m end def create_memstats_not_available(totals) - Mapping::FIELDS.each do |field| - totals[field] += Float::NAN - end + Mapping::FIELDS.each { |field| totals[field] += Float::NAN } end -abort 'usage: memstats [pid]' unless ARGV.first +abort "usage: memstats [pid]" unless ARGV.first pid = ARGV.shift.to_i totals = Hash.new(0) mappings = [] begin File.open("/proc/#{pid}/smaps") do |smaps| - map_lines = [] loop do @@ -111,9 +101,7 @@ begin when /\w+:\s+/ map_lines << line when /[0-9a-f]+:[0-9a-f]+\s+/ - if map_lines.size > 0 then - mappings << consume_mapping(map_lines, totals) - end + mappings << consume_mapping(map_lines, totals) if map_lines.size > 0 map_lines.clear map_lines << line else @@ -121,7 +109,7 @@ begin end end end -rescue +rescue StandardError create_memstats_not_available(totals) end @@ -132,23 +120,19 @@ end def get_commandline(pid) commandline = File.read("/proc/#{pid}/cmdline").split("\0") - if commandline.first =~ /java$/ then + if commandline.first =~ /java$/ loop { break if commandline.shift == "-jar" } return "[java] #{commandline.shift}" end - commandline.join(' ') + commandline.join(" ") end -if ARGV.include? '--yaml' - require 'yaml' - puts Hash[*totals.map do |k, v| - [k + '_kb', v] - end.flatten].to_yaml +if ARGV.include? "--yaml" + require "yaml" + puts Hash[*totals.map do |k, v| [k + "_kb", v] end.flatten].to_yaml else puts "#{"Process:".ljust(20)} #{pid}" puts "#{"Command Line:".ljust(20)} #{get_commandline(pid)}" puts "Memory Summary:" - totals.keys.sort.each do |k| - puts " #{k.ljust(20)} #{format_number(totals[k]).rjust(12)} kB" - end + totals.keys.sort.each { |k| puts " #{k.ljust(20)} #{format_number(totals[k]).rjust(12)} kB" } end diff --git a/script/micro_bench.rb b/script/micro_bench.rb index 51ea6c8912..7ba5dceffe 100644 --- a/script/micro_bench.rb +++ b/script/micro_bench.rb @@ -1,32 +1,20 @@ # frozen_string_literal: true -require 'benchmark/ips' +require "benchmark/ips" require File.expand_path("../../config/environment", __FILE__) conn = ActiveRecord::Base.connection.raw_connection Benchmark.ips do |b| - b.report("simple") do - User.first.name - end + b.report("simple") { User.first.name } - b.report("simple with select") do - User.select("name").first.name - end + b.report("simple with select") { User.select("name").first.name } - b.report("pluck with first") do - User.pluck(:name).first - end + b.report("pluck with first") { User.pluck(:name).first } - b.report("pluck with limit") do - User.limit(1).pluck(:name).first - end + b.report("pluck with limit") { User.limit(1).pluck(:name).first } - b.report("pluck with pluck_first") do - User.pluck_first(:name) - end + b.report("pluck with pluck_first") { User.pluck_first(:name) } - b.report("raw") do - conn.exec("SELECT name FROM users LIMIT 1").getvalue(0, 0) - end + b.report("raw") { conn.exec("SELECT name FROM users LIMIT 1").getvalue(0, 0) } end diff --git a/script/profile_db_generator.rb b/script/profile_db_generator.rb index c49ef6f627..8222c00809 100644 --- a/script/profile_db_generator.rb +++ b/script/profile_db_generator.rb @@ -5,7 +5,7 @@ # we want our script to generate a consistent output, to do so # we monkey patch array sample so it always uses the same rng class Array - RNG = Random.new(1098109928029800) + RNG = Random.new(1_098_109_928_029_800) def sample self[RNG.rand(size)] @@ -16,9 +16,7 @@ end def unbundled_require(gem) if defined?(::Bundler) spec_path = Dir.glob("#{Gem.dir}/specifications/#{gem}-*.gemspec").last - if spec_path.nil? - raise LoadError - end + raise LoadError if spec_path.nil? spec = Gem::Specification.load spec_path spec.activate @@ -30,13 +28,14 @@ def unbundled_require(gem) end def sentence - @gabbler ||= Gabbler.new.tap do |gabbler| - story = File.read(File.dirname(__FILE__) + "/alice.txt") - gabbler.learn(story) - end + @gabbler ||= + Gabbler.new.tap do |gabbler| + story = File.read(File.dirname(__FILE__) + "/alice.txt") + gabbler.learn(story) + end sentence = +"" - until sentence.length > 800 do + until sentence.length > 800 sentence << @gabbler.sentence sentence << "\n" end @@ -74,13 +73,13 @@ if User.count > 2 exit end -require 'optparse' +require "optparse" begin - unbundled_require 'gabbler' + unbundled_require "gabbler" rescue LoadError puts "installing gabbler gem" puts `gem install gabbler` - unbundled_require 'gabbler' + unbundled_require "gabbler" end number_of_users = 100 @@ -98,41 +97,61 @@ users = User.human_users.all puts puts "Creating 10 categories" -categories = 10.times.map do |i| - putc "." - Category.create(name: "category#{i}", text_color: "ffffff", color: "000000", user: admin_user) -end +categories = + 10.times.map do |i| + putc "." + Category.create(name: "category#{i}", text_color: "ffffff", color: "000000", user: admin_user) + end puts puts "Creating 100 topics" -topic_ids = 100.times.map do - post = PostCreator.create(admin_user, raw: sentence, title: sentence[0..50].strip, category: categories.sample.id, skip_validations: true) - putc "." - post.topic_id -end +topic_ids = + 100.times.map do + post = + PostCreator.create( + admin_user, + raw: sentence, + title: sentence[0..50].strip, + category: categories.sample.id, + skip_validations: true, + ) + putc "." + post.topic_id + end puts puts "Creating 2000 replies" 2000.times do putc "." - PostCreator.create(users.sample, raw: sentence, topic_id: topic_ids.sample, skip_validations: true) + PostCreator.create( + users.sample, + raw: sentence, + topic_id: topic_ids.sample, + skip_validations: true, + ) end puts puts "creating perf test topic" -first_post = PostCreator.create( - users.sample, - raw: sentence, - title: "I am a topic used for perf tests", - category: categories.sample.id, - skip_validations: true -) +first_post = + PostCreator.create( + users.sample, + raw: sentence, + title: "I am a topic used for perf tests", + category: categories.sample.id, + skip_validations: true, + ) puts puts "Creating 100 replies for perf test topic" 100.times do putc "." - PostCreator.create(users.sample, raw: sentence, topic_id: first_post.topic_id, skip_validations: true) + PostCreator.create( + users.sample, + raw: sentence, + topic_id: first_post.topic_id, + skip_validations: true, + ) end # no sidekiq so update some stuff diff --git a/script/redis_memory.rb b/script/redis_memory.rb index 442463bc41..b191112cef 100644 --- a/script/redis_memory.rb +++ b/script/redis_memory.rb @@ -24,7 +24,10 @@ stats = {} end puts "Top 100 keys" -stats.sort { |a, b| b[1][0] <=> a[1][0] }.first(50).each do |k, (len, type, elems)| - elems = " [#{elems}]" if elems - puts "#{k} #{type} #{len}#{elems}" -end +stats + .sort { |a, b| b[1][0] <=> a[1][0] } + .first(50) + .each do |k, (len, type, elems)| + elems = " [#{elems}]" if elems + puts "#{k} #{type} #{len}#{elems}" + end diff --git a/script/require_profiler.rb b/script/require_profiler.rb index a0135d08b7..b8f3437efb 100644 --- a/script/require_profiler.rb +++ b/script/require_profiler.rb @@ -5,12 +5,11 @@ # This is a rudimentary script that allows us to # quickly determine if any gems are slowing down startup -require 'benchmark' -require 'fileutils' +require "benchmark" +require "fileutils" module RequireProfiler class << self - attr_accessor :stats def profiling_enabled? @@ -25,10 +24,19 @@ module RequireProfiler def start(tmp_options = {}) @start_time = Time.now - [ ::Kernel, (class << ::Kernel; self; end) ].each do |klass| + [ + ::Kernel, + ( + class << ::Kernel + self + end + ), + ].each do |klass| klass.class_eval do def require_with_profiling(path, *args) - RequireProfiler.measure(path, caller, :require) { require_without_profiling(path, *args) } + RequireProfiler.measure(path, caller, :require) do + require_without_profiling(path, *args) + end end alias require_without_profiling require alias require require_with_profiling @@ -47,7 +55,14 @@ module RequireProfiler def stop @stop_time = Time.now - [ ::Kernel, (class << ::Kernel; self; end) ].each do |klass| + [ + ::Kernel, + ( + class << ::Kernel + self + end + ), + ].each do |klass| klass.class_eval do alias require require_without_profiling alias load load_without_profiling @@ -63,21 +78,20 @@ module RequireProfiler @stack ||= [] self.stats ||= {} - stat = self.stats.fetch(path) { |key| self.stats[key] = { calls: 0, time: 0, parent_time: 0 } } + stat = + self.stats.fetch(path) { |key| self.stats[key] = { calls: 0, time: 0, parent_time: 0 } } @stack << stat time = Time.now begin - output = yield # do the require or load here + output = yield # do the require or load here ensure delta = Time.now - time stat[:time] += delta stat[:calls] += 1 @stack.pop - @stack.each do |frame| - frame[:parent_time] += delta - end + @stack.each { |frame| frame[:parent_time] += delta } end output @@ -102,7 +116,6 @@ module RequireProfiler puts "GC duration: #{gc_duration_finish}" puts "GC impact: #{gc_duration_finish - gc_duration_start}" end - end end @@ -122,8 +135,9 @@ RequireProfiler.profile do end end -sorted = RequireProfiler.stats.to_a.sort { |a, b| b[1][:time] - b[1][:parent_time] <=> a[1][:time] - a[1][:parent_time] } +sorted = + RequireProfiler.stats.to_a.sort do |a, b| + b[1][:time] - b[1][:parent_time] <=> a[1][:time] - a[1][:parent_time] + end -sorted[0..120].each do |k, v| - puts "#{k} : time #{v[:time] - v[:parent_time]} " -end +sorted[0..120].each { |k, v| puts "#{k} : time #{v[:time] - v[:parent_time]} " } diff --git a/script/spawn_backup_restore.rb b/script/spawn_backup_restore.rb index 4af4f667c6..7ab3721d5d 100644 --- a/script/spawn_backup_restore.rb +++ b/script/spawn_backup_restore.rb @@ -15,11 +15,8 @@ fork do BackupRestore::Restorer.new( user_id: user_id, filename: opts[:filename], - factory: BackupRestore::Factory.new( - user_id: user_id, - client_id: opts[:client_id] - ), - disable_emails: opts.fetch(:disable_emails, true) + factory: BackupRestore::Factory.new(user_id: user_id, client_id: opts[:client_id]), + disable_emails: opts.fetch(:disable_emails, true), ).run end diff --git a/script/test_email_settings.rb b/script/test_email_settings.rb index 9ef0fc8b93..c4c9272e43 100755 --- a/script/test_email_settings.rb +++ b/script/test_email_settings.rb @@ -1,16 +1,16 @@ #!/usr/bin/env ruby # frozen_string_literal: true -require 'action_mailer' +require "action_mailer" # Make this your email address. Poor example.com gets SO MUCH MAIL YOUR_EMAIL = "nobody@example.com" # Change these to be the same settings as your Discourse environment -DISCOURSE_SMTP_ADDRESS = "smtp.example.com" # (mandatory) -@DISCOURSE_SMTP_PORT = 587 # (optional) -@DISCOURSE_SMTP_USER_NAME = "username" # (optional) -@DISCOURSE_SMTP_PASSWORD = "blah" # (optional) +DISCOURSE_SMTP_ADDRESS = "smtp.example.com" # (mandatory) +@DISCOURSE_SMTP_PORT = 587 # (optional) +@DISCOURSE_SMTP_USER_NAME = "username" # (optional) +@DISCOURSE_SMTP_PASSWORD = "blah" # (optional) #@DISCOURSE_SMTP_OPENSSL_VERIFY_MODE = "none" # (optional) none|peer|client_once|fail_if_no_peer_cert # Note that DISCOURSE_SMTP_ADDRESS should NOT BE ALLOWED to relay mail to @@ -24,16 +24,18 @@ $delivery_options = { password: @DISCOURSE_SMTP_PASSWORD || nil, address: DISCOURSE_SMTP_ADDRESS, port: @DISCOURSE_SMTP_PORT || nil, - openssl_verify_mode: @DISCOURSE_SMTP_OPENSSL_VERIFY_MODE || nil + openssl_verify_mode: @DISCOURSE_SMTP_OPENSSL_VERIFY_MODE || nil, } class EmailTestMailer < ActionMailer::Base def email_test(mailfrom, mailto) - mail(from: mailfrom, - to: mailto, - body: "Testing email settings", - subject: "Discourse email settings test", - delivery_method_options: $delivery_options) + mail( + from: mailfrom, + to: mailto, + body: "Testing email settings", + subject: "Discourse email settings test", + delivery_method_options: $delivery_options, + ) end end diff --git a/script/test_mem.rb b/script/test_mem.rb index dd64ea2de3..284e99993f 100644 --- a/script/test_mem.rb +++ b/script/test_mem.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true start = Time.now -require 'objspace' +require "objspace" require File.expand_path("../../config/environment", __FILE__) # preload stuff @@ -9,11 +9,19 @@ I18n.t(:posts) # load up all models and schema (ActiveRecord::Base.connection.tables - %w[schema_migrations]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end # router warm up -Rails.application.routes.recognize_path('abc') rescue nil +begin + Rails.application.routes.recognize_path("abc") +rescue StandardError + nil +end puts "Ruby version #{RUBY_VERSION} p#{RUBY_PATCHLEVEL}" @@ -23,8 +31,11 @@ GC.start puts "RSS: #{`ps -o rss -p #{$$}`.chomp.split("\n").last.to_i} KB" -s = ObjectSpace.each_object(String).map do |o| - ObjectSpace.memsize_of(o) + 40 # rvalue size on x64 -end +s = + ObjectSpace + .each_object(String) + .map do |o| + ObjectSpace.memsize_of(o) + 40 # rvalue size on x64 + end puts "Total strings: #{s.count} space used: #{s.sum} bytes" diff --git a/script/test_memory_leak.rb b/script/test_memory_leak.rb index 3ecf01c39e..a426d36784 100644 --- a/script/test_memory_leak.rb +++ b/script/test_memory_leak.rb @@ -5,25 +5,21 @@ # this performs a trivial operation walking all multisites and grabbing first topic / localizing # the expectation is that RSS will remain static no matter how many iterations run -if ENV['RAILS_ENV'] != "production" - exec "RAILS_ENV=production ruby #{__FILE__}" -end +exec "RAILS_ENV=production ruby #{__FILE__}" if ENV["RAILS_ENV"] != "production" -if !ENV['LD_PRELOAD'] - exec "LD_PRELOAD=/usr/lib/libjemalloc.so.1 ruby #{__FILE__}" -end +exec "LD_PRELOAD=/usr/lib/libjemalloc.so.1 ruby #{__FILE__}" if !ENV["LD_PRELOAD"] -if ENV['LD_PRELOAD'].include?("jemalloc") +if ENV["LD_PRELOAD"].include?("jemalloc") # for 3.6.0 we need a patch jemal 1.1.0 gem (1.1.1 does not support 3.6.0) # however ffi is a problem so we need to patch the gem - require 'jemal' + require "jemal" $jemalloc = true end -if ENV['LD_PRELOAD'].include?("mwrap") +if ENV["LD_PRELOAD"].include?("mwrap") $mwrap = true - require 'mwrap' + require "mwrap" end def bin_diff(current) @@ -39,7 +35,11 @@ end require File.expand_path("../../config/environment", __FILE__) -Rails.application.routes.recognize_path('abc') rescue nil +begin + Rails.application.routes.recognize_path("abc") +rescue StandardError + nil +end I18n.t(:posts) def rss @@ -47,9 +47,7 @@ def rss end def loop_sites - RailsMultisite::ConnectionManagement.each_connection do - yield - end + RailsMultisite::ConnectionManagement.each_connection { yield } end def biggest_klass(klass) @@ -57,7 +55,10 @@ def biggest_klass(klass) end def iter(warmup: false) - loop_sites { Topic.first; I18n.t('too_late_to_edit') } + loop_sites do + Topic.first + I18n.t("too_late_to_edit") + end if !warmup GC.start(full_mark: true, immediate_sweep: true) @@ -75,24 +76,17 @@ def iter(warmup: false) array_delta = biggest_klass(Array).length - $biggest_array_length puts "rss: #{rss} (#{rss_delta}) #{mwrap_delta}#{jedelta} heap_delta: #{GC.stat[:heap_live_slots] - $baseline_slots} array_delta: #{array_delta}" - if $jemalloc - bin_diff(jemal_stats) - end + bin_diff(jemal_stats) if $jemalloc end - end iter(warmup: true) -4.times do - GC.start(full_mark: true, immediate_sweep: true) -end +4.times { GC.start(full_mark: true, immediate_sweep: true) } if $jemalloc $baseline = Jemal.stats $baseline_jemalloc_active = $baseline[:active] - 4.times do - GC.start(full_mark: true, immediate_sweep: true) - end + 4.times { GC.start(full_mark: true, immediate_sweep: true) } end def render_table(array) @@ -102,33 +96,33 @@ def render_table(array) cols = array[0].length array.each do |row| - row.each_with_index do |val, i| - width[i] = [width[i].to_i, val.to_s.length].max - end + row.each_with_index { |val, i| width[i] = [width[i].to_i, val.to_s.length].max } end array[0].each_with_index do |col, i| - buffer << col.to_s.ljust(width[i], ' ') + buffer << col.to_s.ljust(width[i], " ") if i == cols - 1 buffer << "\n" else - buffer << ' | ' + buffer << " | " end end buffer << ("-" * (width.sum + width.length)) buffer << "\n" - array.drop(1).each do |row| - row.each_with_index do |val, i| - buffer << val.to_s.ljust(width[i], ' ') - if i == cols - 1 - buffer << "\n" - else - buffer << ' | ' + array + .drop(1) + .each do |row| + row.each_with_index do |val, i| + buffer << val.to_s.ljust(width[i], " ") + if i == cols - 1 + buffer << "\n" + else + buffer << " | " + end end end - end buffer end @@ -141,14 +135,20 @@ def mwrap_log report << "\n" table = [] - Mwrap.each(200000) do |loc, total, allocations, frees, age_sum, max_life| - table << [total, allocations - frees, frees == 0 ? -1 : (age_sum / frees.to_f).round(2), max_life, loc] + Mwrap.each(200_000) do |loc, total, allocations, frees, age_sum, max_life| + table << [ + total, + allocations - frees, + frees == 0 ? -1 : (age_sum / frees.to_f).round(2), + max_life, + loc, + ] end table.sort! { |a, b| b[1] <=> a[1] } table = table[0..50] - table.prepend(["total", "delta", "mean_life", "max_life", "location"]) + table.prepend(%w[total delta mean_life max_life location]) report << render_table(table) end @@ -158,15 +158,13 @@ end Mwrap.clear -if $mwrap - $mwrap_baseline = Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed -end +$mwrap_baseline = Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed if $mwrap $baseline_slots = GC.stat[:heap_live_slots] $baseline_rss = rss $biggest_array_length = biggest_klass(Array).length -100000.times do +100_000.times do iter if $mwrap puts mwrap_log diff --git a/script/test_pretty_text.rb b/script/test_pretty_text.rb index 29839d9629..02e8644a9e 100644 --- a/script/test_pretty_text.rb +++ b/script/test_pretty_text.rb @@ -9,7 +9,7 @@ puts PrettyText.cook "test" # JS PrettyText.cook "test" - PrettyText.v8.eval('gc()') + PrettyText.v8.eval("gc()") # if i % 500 == 0 #p PrettyText.v8.heap_stats diff --git a/script/thread_detective.rb b/script/thread_detective.rb index c654158ede..83e886d085 100644 --- a/script/thread_detective.rb +++ b/script/thread_detective.rb @@ -9,13 +9,9 @@ class ThreadDetective Thread.new { sleep 1 } end def self.start(max_threads) - @thread ||= Thread.new do - self.new.monitor(max_threads) - end + @thread ||= Thread.new { self.new.monitor(max_threads) } - @trace = TracePoint.new(:thread_begin) do |tp| - Thread.current.origin = Thread.current.inspect - end + @trace = TracePoint.new(:thread_begin) { |tp| Thread.current.origin = Thread.current.inspect } @trace.enable end @@ -52,5 +48,4 @@ class ThreadDetective sleep 1 end end - end diff --git a/script/user_simulator.rb b/script/user_simulator.rb index fb7853ac71..562c559ed1 100644 --- a/script/user_simulator.rb +++ b/script/user_simulator.rb @@ -4,31 +4,32 @@ # # by default 1 new topic every 30 sec, 1 reply to last topic every 30 secs -require 'optparse' -require 'gabbler' +require "optparse" +require "gabbler" user_id = nil def sentence - @gabbler ||= Gabbler.new.tap do |gabbler| - story = File.read(File.dirname(__FILE__) + "/alice.txt") - gabbler.learn(story) - end + @gabbler ||= + Gabbler.new.tap do |gabbler| + story = File.read(File.dirname(__FILE__) + "/alice.txt") + gabbler.learn(story) + end sentence = +"" - until sentence.length > 800 do + until sentence.length > 800 sentence << @gabbler.sentence sentence << "\n" end sentence end -OptionParser.new do |opts| - opts.banner = "Usage: ruby user_simulator.rb [options]" - opts.on("-u", "--user NUMBER", "user id") do |u| - user_id = u.to_i +OptionParser + .new do |opts| + opts.banner = "Usage: ruby user_simulator.rb [options]" + opts.on("-u", "--user NUMBER", "user id") { |u| user_id = u.to_i } end -end.parse! + .parse! unless user_id puts "user must be specified" @@ -37,19 +38,19 @@ end require File.expand_path(File.dirname(__FILE__) + "/../config/environment") -unless ["profile", "development"].include? Rails.env +unless %w[profile development].include? Rails.env puts "Bad idea to run a script that inserts random posts in any non development environment" exit end user = User.find(user_id) -last_topics = Topic.order('id desc').limit(10).pluck(:id) +last_topics = Topic.order("id desc").limit(10).pluck(:id) puts "Simulating activity for user id #{user.id}: #{user.name}" while true puts "Creating a random topic" - category = Category.where(read_restricted: false).order('random()').first + category = Category.where(read_restricted: false).order("random()").first PostCreator.create(user, raw: sentence, title: sentence[0..50].strip, category: category.id) puts "creating random reply" diff --git a/spec/fabricators/allowed_pm_users.rb b/spec/fabricators/allowed_pm_users.rb index c160aef83d..fd7286396d 100644 --- a/spec/fabricators/allowed_pm_users.rb +++ b/spec/fabricators/allowed_pm_users.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:allowed_pm_user) do - user -end +Fabricator(:allowed_pm_user) { user } diff --git a/spec/fabricators/api_key_fabricator.rb b/spec/fabricators/api_key_fabricator.rb index 2f75d78c0b..b2c6748fb1 100644 --- a/spec/fabricators/api_key_fabricator.rb +++ b/spec/fabricators/api_key_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:api_key) do - -end +Fabricator(:api_key) {} diff --git a/spec/fabricators/associated_group_fabricator.rb b/spec/fabricators/associated_group_fabricator.rb index f7aff76893..15117bae95 100644 --- a/spec/fabricators/associated_group_fabricator.rb +++ b/spec/fabricators/associated_group_fabricator.rb @@ -2,6 +2,6 @@ Fabricator(:associated_group) do name { sequence(:name) { |n| "group_#{n}" } } - provider_name 'google' + provider_name "google" provider_id { SecureRandom.hex(20) } end diff --git a/spec/fabricators/badge_fabricator.rb b/spec/fabricators/badge_fabricator.rb index b99fe209e7..7ec3cb1b3b 100644 --- a/spec/fabricators/badge_fabricator.rb +++ b/spec/fabricators/badge_fabricator.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -Fabricator(:badge_type) do - name { sequence(:name) { |i| "Silver #{i}" } } -end +Fabricator(:badge_type) { name { sequence(:name) { |i| "Silver #{i}" } } } Fabricator(:badge) do name { sequence(:name) { |i| "Badge #{i}" } } diff --git a/spec/fabricators/bookmark_fabricator.rb b/spec/fabricators/bookmark_fabricator.rb index 30dafdf0f1..ba9271a22e 100644 --- a/spec/fabricators/bookmark_fabricator.rb +++ b/spec/fabricators/bookmark_fabricator.rb @@ -10,13 +10,14 @@ end Fabricator(:bookmark_next_business_day_reminder, from: :bookmark) do reminder_at do - date = if Time.zone.now.friday? - Time.zone.now + 3.days - elsif Time.zone.now.saturday? - Time.zone.now + 2.days - else - Time.zone.now + 1.day - end + date = + if Time.zone.now.friday? + Time.zone.now + 3.days + elsif Time.zone.now.saturday? + Time.zone.now + 2.days + else + Time.zone.now + 1.day + end date.iso8601 end reminder_set_at { Time.zone.now } diff --git a/spec/fabricators/category_fabricator.rb b/spec/fabricators/category_fabricator.rb index 0e14967af4..eb6dbcbd0c 100644 --- a/spec/fabricators/category_fabricator.rb +++ b/spec/fabricators/category_fabricator.rb @@ -6,9 +6,7 @@ Fabricator(:category) do user end -Fabricator(:category_with_definition, from: :category) do - skip_category_definition false -end +Fabricator(:category_with_definition, from: :category) { skip_category_definition false } Fabricator(:private_category, from: :category) do transient :group @@ -20,7 +18,10 @@ Fabricator(:private_category, from: :category) do after_build do |cat, transients| cat.update!(read_restricted: true) - cat.category_groups.build(group_id: transients[:group].id, permission_type: transients[:permission_type] || CategoryGroup.permission_types[:full]) + cat.category_groups.build( + group_id: transients[:group].id, + permission_type: transients[:permission_type] || CategoryGroup.permission_types[:full], + ) end end @@ -33,7 +34,7 @@ Fabricator(:link_category, from: :category) do end Fabricator(:mailinglist_mirror_category, from: :category) do - email_in 'list@example.com' + email_in "list@example.com" email_in_allow_strangers true mailinglist_mirror true end diff --git a/spec/fabricators/color_scheme_fabricator.rb b/spec/fabricators/color_scheme_fabricator.rb index 711b0f5e94..539926b71c 100644 --- a/spec/fabricators/color_scheme_fabricator.rb +++ b/spec/fabricators/color_scheme_fabricator.rb @@ -2,5 +2,7 @@ Fabricator(:color_scheme) do name { sequence(:name) { |i| "Palette #{i}" } } - color_scheme_colors(count: 2) { |attrs, i| Fabricate.build(:color_scheme_color, color_scheme: nil) } + color_scheme_colors(count: 2) do |attrs, i| + Fabricate.build(:color_scheme_color, color_scheme: nil) + end end diff --git a/spec/fabricators/external_upload_stub_fabricator.rb b/spec/fabricators/external_upload_stub_fabricator.rb index 5f8eb3e25e..e175aa0d25 100644 --- a/spec/fabricators/external_upload_stub_fabricator.rb +++ b/spec/fabricators/external_upload_stub_fabricator.rb @@ -5,7 +5,12 @@ Fabricator(:external_upload_stub) do created_by { Fabricate(:user) } original_filename "test.txt" - key { |attrs| FileStore::BaseStore.temporary_upload_path("test.txt", folder_prefix: attrs[:folder_prefix] || "") } + key do |attrs| + FileStore::BaseStore.temporary_upload_path( + "test.txt", + folder_prefix: attrs[:folder_prefix] || "", + ) + end upload_type "card_background" filesize 1024 status 1 @@ -14,16 +19,28 @@ end Fabricator(:image_external_upload_stub, from: :external_upload_stub) do original_filename "logo.png" filesize 1024 - key { |attrs| FileStore::BaseStore.temporary_upload_path("logo.png", folder_prefix: attrs[:folder_prefix] || "") } + key do |attrs| + FileStore::BaseStore.temporary_upload_path( + "logo.png", + folder_prefix: attrs[:folder_prefix] || "", + ) + end end Fabricator(:attachment_external_upload_stub, from: :external_upload_stub) do original_filename "file.pdf" filesize 1024 - key { |attrs| FileStore::BaseStore.temporary_upload_path("file.pdf", folder_prefix: attrs[:folder_prefix] || "") } + key do |attrs| + FileStore::BaseStore.temporary_upload_path( + "file.pdf", + folder_prefix: attrs[:folder_prefix] || "", + ) + end end Fabricator(:multipart_external_upload_stub, from: :external_upload_stub) do multipart true - external_upload_identifier { "#{SecureRandom.hex(6)}._#{SecureRandom.hex(6)}_#{SecureRandom.hex(6)}.d.ghQ" } + external_upload_identifier do + "#{SecureRandom.hex(6)}._#{SecureRandom.hex(6)}_#{SecureRandom.hex(6)}.d.ghQ" + end end diff --git a/spec/fabricators/group_fabricator.rb b/spec/fabricators/group_fabricator.rb index de9993d362..c0a13a3f39 100644 --- a/spec/fabricators/group_fabricator.rb +++ b/spec/fabricators/group_fabricator.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -Fabricator(:group) do - name { sequence(:name) { |n| "my_group_#{n}" } } -end +Fabricator(:group) { name { sequence(:name) { |n| "my_group_#{n}" } } } Fabricator(:public_group, from: :group) do public_admission true diff --git a/spec/fabricators/invite_fabricator.rb b/spec/fabricators/invite_fabricator.rb index aedc23c177..d8a8747796 100644 --- a/spec/fabricators/invite_fabricator.rb +++ b/spec/fabricators/invite_fabricator.rb @@ -2,5 +2,5 @@ Fabricator(:invite) do invited_by(fabricator: :user) - email 'iceking@ADVENTURETIME.ooo' + email "iceking@ADVENTURETIME.ooo" end diff --git a/spec/fabricators/muted_user.rb b/spec/fabricators/muted_user.rb index 4bee8414e0..992c205abe 100644 --- a/spec/fabricators/muted_user.rb +++ b/spec/fabricators/muted_user.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:muted_user) do - user -end +Fabricator(:muted_user) { user } diff --git a/spec/fabricators/notification_fabricator.rb b/spec/fabricators/notification_fabricator.rb index 6ee4c1bcfb..51d34032e4 100644 --- a/spec/fabricators/notification_fabricator.rb +++ b/spec/fabricators/notification_fabricator.rb @@ -27,7 +27,7 @@ Fabricator(:private_message_notification, from: :notification) do original_post_type: post.post_type, original_username: post.user.username, revision_number: nil, - display_username: post.user.username + display_username: post.user.username, }.to_json end end @@ -44,7 +44,7 @@ Fabricator(:bookmark_reminder_notification, from: :notification) do original_username: post.user.username, revision_number: nil, display_username: post.user.username, - bookmark_name: "Check out Mr Freeze's opinion here" + bookmark_name: "Check out Mr Freeze's opinion here", }.to_json end end @@ -58,7 +58,7 @@ Fabricator(:replied_notification, from: :notification) do original_post_id: post.id, original_username: post.user.username, revision_number: nil, - display_username: post.user.username + display_username: post.user.username, }.to_json end end @@ -73,7 +73,7 @@ Fabricator(:posted_notification, from: :notification) do original_post_type: post.post_type, original_username: post.user.username, revision_number: nil, - display_username: post.user.username + display_username: post.user.username, }.to_json end end @@ -87,7 +87,7 @@ Fabricator(:mentioned_notification, from: :notification) do original_post_type: attrs[:post].post_type, original_username: attrs[:post].user.username, revision_number: nil, - display_username: attrs[:post].user.username + display_username: attrs[:post].user.username, }.to_json end end @@ -101,7 +101,7 @@ Fabricator(:watching_first_post_notification, from: :notification) do original_post_type: attrs[:post].post_type, original_username: attrs[:post].user.username, revision_number: nil, - display_username: attrs[:post].user.username + display_username: attrs[:post].user.username, }.to_json end end diff --git a/spec/fabricators/permalink_fabricator.rb b/spec/fabricators/permalink_fabricator.rb index b285212606..0f21d1f2e6 100644 --- a/spec/fabricators/permalink_fabricator.rb +++ b/spec/fabricators/permalink_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:permalink) do - url { sequence(:url) { |i| "my/#{i}/url" } } -end +Fabricator(:permalink) { url { sequence(:url) { |i| "my/#{i}/url" } } } diff --git a/spec/fabricators/post_fabricator.rb b/spec/fabricators/post_fabricator.rb index e18628a141..07e8a73448 100644 --- a/spec/fabricators/post_fabricator.rb +++ b/spec/fabricators/post_fabricator.rb @@ -7,19 +7,17 @@ Fabricator(:post) do post_type Post.types[:regular] # Fabrication bypasses PostCreator, for performance reasons, where the counts are updated so we have to handle this manually here. - after_create do |post, _transients| - UserStatCountUpdater.increment!(post) - end + after_create { |post, _transients| UserStatCountUpdater.increment!(post) } end Fabricator(:post_with_long_raw_content, from: :post) do - raw 'This is a sample post with semi-long raw content. The raw content is also more than + raw "This is a sample post with semi-long raw content. The raw content is also more than two hundred characters to satisfy any test conditions that require content longer - than the typical test post raw content. It really is some long content, folks.' + than the typical test post raw content. It really is some long content, folks." end Fabricator(:post_with_youtube, from: :post) do - raw 'http://www.youtube.com/watch?v=9bZkp7q19f0' + raw "http://www.youtube.com/watch?v=9bZkp7q19f0" cooked '

    http://www.youtube.com/watch?v=9bZkp7q19f0

    ' end @@ -39,7 +37,7 @@ Fabricator(:basic_reply, from: :post) do user(fabricator: :coding_horror) reply_to_post_number 1 topic - raw 'this reply has no quotes' + raw "this reply has no quotes" end Fabricator(:reply, from: :post) do @@ -51,14 +49,12 @@ Fabricator(:reply, from: :post) do ' end -Fabricator(:post_with_plenty_of_images, from: :post) do - cooked <<~HTML +Fabricator(:post_with_plenty_of_images, from: :post) { cooked <<~HTML }

    With an emoji! smile

    HTML -end Fabricator(:post_with_uploaded_image, from: :post) do raw { "" } @@ -101,8 +97,7 @@ Fabricator(:post_with_uploads, from: :post) do " end -Fabricator(:post_with_uploads_and_links, from: :post) do - raw <<~MD +Fabricator(:post_with_uploads_and_links, from: :post) { raw <<~MD } Link Google @@ -110,7 +105,6 @@ Fabricator(:post_with_uploads_and_links, from: :post) do text.txt (20 Bytes) :smile: MD -end Fabricator(:post_with_external_links, from: :post) do user @@ -130,14 +124,15 @@ Fabricator(:private_message_post, from: :post) do transient :recipient user topic do |attrs| - Fabricate(:private_message_topic, + Fabricate( + :private_message_topic, user: attrs[:user], created_at: attrs[:created_at], subtype: TopicSubtype.user_to_user, topic_allowed_users: [ Fabricate.build(:topic_allowed_user, user: attrs[:user]), - Fabricate.build(:topic_allowed_user, user: attrs[:recipient] || Fabricate(:user)) - ] + Fabricate.build(:topic_allowed_user, user: attrs[:recipient] || Fabricate(:user)), + ], ) end raw "Ssshh! This is our secret conversation!" @@ -147,16 +142,15 @@ Fabricator(:group_private_message_post, from: :post) do transient :recipients user topic do |attrs| - Fabricate(:private_message_topic, + Fabricate( + :private_message_topic, user: attrs[:user], created_at: attrs[:created_at], subtype: TopicSubtype.user_to_user, - topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: attrs[:user]), - ], + topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: attrs[:user])], topic_allowed_groups: [ - Fabricate.build(:topic_allowed_group, group: attrs[:recipients] || Fabricate(:group)) - ] + Fabricate.build(:topic_allowed_group, group: attrs[:recipients] || Fabricate(:group)), + ], ) end raw "Ssshh! This is our group secret conversation!" @@ -165,13 +159,12 @@ end Fabricator(:private_message_post_one_user, from: :post) do user topic do |attrs| - Fabricate(:private_message_topic, + Fabricate( + :private_message_topic, user: attrs[:user], created_at: attrs[:created_at], subtype: TopicSubtype.user_to_user, - topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: attrs[:user]), - ] + topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: attrs[:user])], ) end raw "Ssshh! This is our secret conversation!" @@ -188,10 +181,6 @@ Fabricator(:post_via_email, from: :post) do end end -Fabricator(:whisper, from: :post) do - post_type Post.types[:whisper] -end +Fabricator(:whisper, from: :post) { post_type Post.types[:whisper] } -Fabricator(:small_action, from: :post) do - post_type Post.types[:small_action] -end +Fabricator(:small_action, from: :post) { post_type Post.types[:small_action] } diff --git a/spec/fabricators/post_revision_fabricator.rb b/spec/fabricators/post_revision_fabricator.rb index 059f1af5a9..848d184f59 100644 --- a/spec/fabricators/post_revision_fabricator.rb +++ b/spec/fabricators/post_revision_fabricator.rb @@ -4,7 +4,5 @@ Fabricator(:post_revision) do post user number 2 - modifications do - { "cooked" => ["

    BEFORE

    ", "

    AFTER

    "], "raw" => ["BEFORE", "AFTER"] } - end + modifications { { "cooked" => %w[

    BEFORE

    AFTER

    ], "raw" => %w[BEFORE AFTER] } } end diff --git a/spec/fabricators/reviewable_fabricator.rb b/spec/fabricators/reviewable_fabricator.rb index b2f2612040..2606a99c29 100644 --- a/spec/fabricators/reviewable_fabricator.rb +++ b/spec/fabricators/reviewable_fabricator.rb @@ -2,70 +2,75 @@ Fabricator(:reviewable) do reviewable_by_moderator true - type 'ReviewableUser' + type "ReviewableUser" created_by { Fabricate(:user) } target_id { Fabricate(:user).id } target_type "User" target_created_by { Fabricate(:user) } category score 1.23 - payload { - { list: [1, 2, 3], name: 'bandersnatch' } - } + payload { { list: [1, 2, 3], name: "bandersnatch" } } end Fabricator(:reviewable_queued_post_topic, class_name: :reviewable_queued_post) do reviewable_by_moderator true - type 'ReviewableQueuedPost' + type "ReviewableQueuedPost" created_by { Fabricate(:user) } category - payload { + payload do { raw: "hello world post contents.", title: "queued post title", - tags: ['cool', 'neat'], + tags: %w[cool neat], extra: "some extra data", - archetype: 'regular' + archetype: "regular", } - } + end end Fabricator(:reviewable_queued_post) do reviewable_by_moderator true - type 'ReviewableQueuedPost' + type "ReviewableQueuedPost" created_by { Fabricate(:user) } topic - payload { + payload do { raw: "hello world post contents.", reply_to_post_number: 1, via_email: true, - raw_email: 'store_me', + raw_email: "store_me", auto_track: true, - custom_fields: { hello: 'world' }, - cooking_options: { cat: 'hat' }, + custom_fields: { + hello: "world", + }, + cooking_options: { + cat: "hat", + }, cook_method: Post.cook_methods[:raw_html], - image_sizes: { "http://foo.bar/image.png" => { "width" => 0, "height" => 222 } } + image_sizes: { + "http://foo.bar/image.png" => { + "width" => 0, + "height" => 222, + }, + }, } - } + end end Fabricator(:reviewable_flagged_post) do reviewable_by_moderator true - type 'ReviewableFlaggedPost' + type "ReviewableFlaggedPost" created_by { Fabricate(:user) } topic - target_type 'Post' + target_type "Post" target { Fabricate(:post) } - reviewable_scores { |p| [ - Fabricate.build(:reviewable_score, reviewable_id: p[:id]), - ]} + reviewable_scores { |p| [Fabricate.build(:reviewable_score, reviewable_id: p[:id])] } end Fabricator(:reviewable_user) do reviewable_by_moderator true - type 'ReviewableUser' + type "ReviewableUser" created_by { Fabricate(:user) } - target_type 'User' + target_type "User" target { Fabricate(:user) } end diff --git a/spec/fabricators/screened_url_fabricator.rb b/spec/fabricators/screened_url_fabricator.rb index 8533946c65..7225852a5d 100644 --- a/spec/fabricators/screened_url_fabricator.rb +++ b/spec/fabricators/screened_url_fabricator.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Fabricator(:screened_url) do - url { sequence(:url) { |n| "spammers#{n}.org/buy/stuff" } } - domain { sequence(:domain) { |n| "spammers#{n}.org" } } + url { sequence(:url) { |n| "spammers#{n}.org/buy/stuff" } } + domain { sequence(:domain) { |n| "spammers#{n}.org" } } action_type ScreenedEmail.actions[:do_nothing] end diff --git a/spec/fabricators/sidebar_section_link_fabricator.rb b/spec/fabricators/sidebar_section_link_fabricator.rb index 4a5d768380..569de894fe 100644 --- a/spec/fabricators/sidebar_section_link_fabricator.rb +++ b/spec/fabricators/sidebar_section_link_fabricator.rb @@ -1,13 +1,9 @@ # frozen_string_literal: true -Fabricator(:sidebar_section_link) do - user -end +Fabricator(:sidebar_section_link) { user } Fabricator(:category_sidebar_section_link, from: :sidebar_section_link) do linkable(fabricator: :category) end -Fabricator(:tag_sidebar_section_link, from: :sidebar_section_link) do - linkable(fabricator: :tag) -end +Fabricator(:tag_sidebar_section_link, from: :sidebar_section_link) { linkable(fabricator: :tag) } diff --git a/spec/fabricators/single_sign_on_record_fabricator.rb b/spec/fabricators/single_sign_on_record_fabricator.rb index 95c6a6028f..1afbc72706 100644 --- a/spec/fabricators/single_sign_on_record_fabricator.rb +++ b/spec/fabricators/single_sign_on_record_fabricator.rb @@ -5,5 +5,7 @@ Fabricator(:single_sign_on_record) do external_id { sequence(:external_id) { |i| "ext_#{i}" } } external_username { sequence(:username) { |i| "bruce#{i}" } } external_email { sequence(:email) { |i| "bruce#{i}@wayne.com" } } - last_payload { sequence(:last_payload) { |i| "nonce=#{i}1870a940bbcbb46f06880ed338d58a07&name=" } } + last_payload do + sequence(:last_payload) { |i| "nonce=#{i}1870a940bbcbb46f06880ed338d58a07&name=" } + end end diff --git a/spec/fabricators/tag_fabricator.rb b/spec/fabricators/tag_fabricator.rb index c2192294ef..6d68de48f4 100644 --- a/spec/fabricators/tag_fabricator.rb +++ b/spec/fabricators/tag_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:tag) do - name { sequence(:name) { |i| "tag#{i + 1}" } } -end +Fabricator(:tag) { name { sequence(:name) { |i| "tag#{i + 1}" } } } diff --git a/spec/fabricators/tag_group_fabricator.rb b/spec/fabricators/tag_group_fabricator.rb index 990ec85d72..64a38f9004 100644 --- a/spec/fabricators/tag_group_fabricator.rb +++ b/spec/fabricators/tag_group_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:tag_group) do - name { sequence(:name) { |i| "tag_group_#{i}" } } -end +Fabricator(:tag_group) { name { sequence(:name) { |i| "tag_group_#{i}" } } } diff --git a/spec/fabricators/topic_allowed_user_fabricator.rb b/spec/fabricators/topic_allowed_user_fabricator.rb index eb3d75f6e2..99494eb170 100644 --- a/spec/fabricators/topic_allowed_user_fabricator.rb +++ b/spec/fabricators/topic_allowed_user_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:topic_allowed_user) do - user -end +Fabricator(:topic_allowed_user) { user } diff --git a/spec/fabricators/topic_fabricator.rb b/spec/fabricators/topic_fabricator.rb index b65cd48d7f..c693fc213c 100644 --- a/spec/fabricators/topic_fabricator.rb +++ b/spec/fabricators/topic_fabricator.rb @@ -8,25 +8,21 @@ Fabricator(:topic) do end end -Fabricator(:deleted_topic, from: :topic) do - deleted_at { 1.minute.ago } -end +Fabricator(:deleted_topic, from: :topic) { deleted_at { 1.minute.ago } } -Fabricator(:closed_topic, from: :topic) do - closed true -end +Fabricator(:closed_topic, from: :topic) { closed true } -Fabricator(:banner_topic, from: :topic) do - archetype Archetype.banner -end +Fabricator(:banner_topic, from: :topic) { archetype Archetype.banner } Fabricator(:private_message_topic, from: :topic) do transient :recipient category_id { nil } title { sequence(:title) { |i| "This is a private message #{i}" } } archetype "private_message" - topic_allowed_users { |t| [ - Fabricate.build(:topic_allowed_user, user: t[:user]), - Fabricate.build(:topic_allowed_user, user: t[:recipient] || Fabricate(:user)) - ]} + topic_allowed_users do |t| + [ + Fabricate.build(:topic_allowed_user, user: t[:user]), + Fabricate.build(:topic_allowed_user, user: t[:recipient] || Fabricate(:user)), + ] + end end diff --git a/spec/fabricators/upload_fabricator.rb b/spec/fabricators/upload_fabricator.rb index cb47f293b0..aec9a20721 100644 --- a/spec/fabricators/upload_fabricator.rb +++ b/spec/fabricators/upload_fabricator.rb @@ -12,9 +12,7 @@ Fabricator(:upload) do url do |attrs| sequence(:url) do |n| - Discourse.store.get_path_for( - "original", n + 1, attrs[:sha1], ".#{attrs[:extension]}" - ) + Discourse.store.get_path_for("original", n + 1, attrs[:sha1], ".#{attrs[:extension]}") end end @@ -35,15 +33,16 @@ Fabricator(:image_upload, from: :upload) do transient color: "white" after_create do |upload, transients| - file = Tempfile.new(['fabricated', '.png']) + file = Tempfile.new(%w[fabricated .png]) `convert -size #{upload.width}x#{upload.height} xc:#{transients[:color]} "#{file.path}"` upload.url = Discourse.store.store_upload(file, upload) upload.sha1 = Upload.generate_digest(file.path) - WebMock - .stub_request(:get, "http://#{Discourse.current_hostname}#{upload.url}") - .to_return(status: 200, body: File.new(file.path)) + WebMock.stub_request(:get, "http://#{Discourse.current_hostname}#{upload.url}").to_return( + status: 200, + body: File.new(file.path), + ) end end @@ -72,13 +71,9 @@ end Fabricator(:upload_s3, from: :upload) do url do |attrs| sequence(:url) do |n| - path = +Discourse.store.get_path_for( - "original", n + 1, attrs[:sha1], ".#{attrs[:extension]}" - ) + path = +Discourse.store.get_path_for("original", n + 1, attrs[:sha1], ".#{attrs[:extension]}") - if Rails.configuration.multisite - path.prepend(File.join(Discourse.store.upload_path, "/")) - end + path.prepend(File.join(Discourse.store.upload_path, "/")) if Rails.configuration.multisite File.join(Discourse.store.absolute_base_url, path) end @@ -87,15 +82,13 @@ end Fabricator(:s3_image_upload, from: :upload_s3) do after_create do |upload| - file = Tempfile.new(['fabricated', '.png']) + file = Tempfile.new(%w[fabricated .png]) `convert -size #{upload.width}x#{upload.height} xc:white "#{file.path}"` upload.url = Discourse.store.store_upload(file, upload) upload.sha1 = Upload.generate_digest(file.path) - WebMock - .stub_request(:get, upload.url) - .to_return(status: 200, body: File.new(file.path)) + WebMock.stub_request(:get, upload.url).to_return(status: 200, body: File.new(file.path)) end end diff --git a/spec/fabricators/user_api_key_fabricator.rb b/spec/fabricators/user_api_key_fabricator.rb index 3ca55431c8..fc2e805ecb 100644 --- a/spec/fabricators/user_api_key_fabricator.rb +++ b/spec/fabricators/user_api_key_fabricator.rb @@ -3,15 +3,15 @@ Fabricator(:user_api_key) do user client_id { SecureRandom.hex } - application_name 'some app' + application_name "some app" end Fabricator(:user_api_key_scope) Fabricator(:readonly_user_api_key, from: :user_api_key) do - scopes { [Fabricate.build(:user_api_key_scope, name: 'read')] } + scopes { [Fabricate.build(:user_api_key_scope, name: "read")] } end Fabricator(:bookmarks_calendar_user_api_key, from: :user_api_key) do - scopes { [Fabricate.build(:user_api_key_scope, name: 'bookmarks_calendar')] } + scopes { [Fabricate.build(:user_api_key_scope, name: "bookmarks_calendar")] } end diff --git a/spec/fabricators/user_avatar_fabricator.rb b/spec/fabricators/user_avatar_fabricator.rb index f7431bcbb1..773947f04b 100644 --- a/spec/fabricators/user_avatar_fabricator.rb +++ b/spec/fabricators/user_avatar_fabricator.rb @@ -1,5 +1,3 @@ # frozen_string_literal: true -Fabricator(:user_avatar) do - user -end +Fabricator(:user_avatar) { user } diff --git a/spec/fabricators/user_fabricator.rb b/spec/fabricators/user_fabricator.rb index bee8812bd8..1a0fcca19a 100644 --- a/spec/fabricators/user_fabricator.rb +++ b/spec/fabricators/user_fabricator.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -Fabricator(:user_stat) do -end +Fabricator(:user_stat) {} Fabricator(:user, class_name: :user) do - name 'Bruce Wayne' + name "Bruce Wayne" username { sequence(:username) { |i| "bruce#{i}" } } email { sequence(:email) { |i| "bruce#{i}@wayne.com" } } - password 'myawesomepassword' + password "myawesomepassword" trust_level TrustLevel[1] ip_address { sequence(:ip_address) { |i| "99.232.23.#{i % 254}" } } active true @@ -18,31 +17,31 @@ Fabricator(:user_with_secondary_email, from: :user) do end Fabricator(:coding_horror, from: :user) do - name 'Coding Horror' - username 'CodingHorror' - email 'jeff@somewhere.com' - password 'mymoreawesomepassword' + name "Coding Horror" + username "CodingHorror" + email "jeff@somewhere.com" + password "mymoreawesomepassword" end Fabricator(:evil_trout, from: :user) do - name 'Evil Trout' - username 'eviltrout' - email 'eviltrout@somewhere.com' - password 'imafish123' + name "Evil Trout" + username "eviltrout" + email "eviltrout@somewhere.com" + password "imafish123" end Fabricator(:walter_white, from: :user) do - name 'Walter White' - username 'heisenberg' - email 'wwhite@bluemeth.com' - password 'letscook123' + name "Walter White" + username "heisenberg" + email "wwhite@bluemeth.com" + password "letscook123" end Fabricator(:inactive_user, from: :user) do - name 'Inactive User' - username 'inactive_user' - email 'inactive@idontexist.com' - password 'qwerqwer123' + name "Inactive User" + username "inactive_user" + email "inactive@idontexist.com" + password "qwerqwer123" active false end @@ -59,7 +58,7 @@ Fabricator(:moderator, from: :user) do end Fabricator(:admin, from: :user) do - name 'Anne Admin' + name "Anne Admin" username { sequence(:username) { |i| "anne#{i}" } } email { sequence(:email) { |i| "anne#{i}@discourse.org" } } admin true @@ -71,17 +70,17 @@ Fabricator(:admin, from: :user) do end Fabricator(:newuser, from: :user) do - name 'Newbie Newperson' - username 'newbie' - email 'newbie@new.com' + name "Newbie Newperson" + username "newbie" + email "newbie@new.com" trust_level TrustLevel[0] end Fabricator(:active_user, from: :user) do - name 'Luke Skywalker' + name "Luke Skywalker" username { sequence(:username) { |i| "luke#{i}" } } email { sequence(:email) { |i| "luke#{i}@skywalker.com" } } - password 'myawesomepassword' + password "myawesomepassword" trust_level TrustLevel[1] after_create do |user| @@ -91,29 +90,25 @@ Fabricator(:active_user, from: :user) do end Fabricator(:leader, from: :user) do - name 'Veteran McVeteranish' + name "Veteran McVeteranish" username { sequence(:username) { |i| "leader#{i}" } } email { sequence(:email) { |i| "leader#{i}@leaderfun.com" } } trust_level TrustLevel[3] end -Fabricator(:trust_level_0, from: :user) do - trust_level TrustLevel[0] -end +Fabricator(:trust_level_0, from: :user) { trust_level TrustLevel[0] } -Fabricator(:trust_level_1, from: :user) do - trust_level TrustLevel[1] -end +Fabricator(:trust_level_1, from: :user) { trust_level TrustLevel[1] } Fabricator(:trust_level_4, from: :user) do - name 'Leader McElderson' + name "Leader McElderson" username { sequence(:username) { |i| "tl4#{i}" } } email { sequence(:email) { |i| "tl4#{i}@elderfun.com" } } trust_level TrustLevel[4] end Fabricator(:anonymous, from: :user) do - name '' + name "" username { sequence(:username) { |i| "anonymous#{i}" } } email { sequence(:email) { |i| "anonymous#{i}@anonymous.com" } } trust_level TrustLevel[1] @@ -127,13 +122,9 @@ Fabricator(:anonymous, from: :user) do end end -Fabricator(:staged, from: :user) do - staged true -end +Fabricator(:staged, from: :user) { staged true } -Fabricator(:unicode_user, from: :user) do - username { sequence(:username) { |i| "Löwe#{i}" } } -end +Fabricator(:unicode_user, from: :user) { username { sequence(:username) { |i| "Löwe#{i}" } } } Fabricator(:bot, from: :user) do id do diff --git a/spec/fabricators/user_field_fabricator.rb b/spec/fabricators/user_field_fabricator.rb index c8019b390a..8534e80657 100644 --- a/spec/fabricators/user_field_fabricator.rb +++ b/spec/fabricators/user_field_fabricator.rb @@ -3,7 +3,7 @@ Fabricator(:user_field) do name { sequence(:name) { |i| "field_#{i}" } } description "user field description" - field_type 'text' + field_type "text" editable true required true end diff --git a/spec/fabricators/user_history_fabricator.rb b/spec/fabricators/user_history_fabricator.rb index 4464b03c7c..72bdd16075 100644 --- a/spec/fabricators/user_history_fabricator.rb +++ b/spec/fabricators/user_history_fabricator.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true -Fabricator(:user_history) do -end +Fabricator(:user_history) {} diff --git a/spec/fabricators/user_option_fabricator.rb b/spec/fabricators/user_option_fabricator.rb index 17c0cbc788..77e55384a8 100644 --- a/spec/fabricators/user_option_fabricator.rb +++ b/spec/fabricators/user_option_fabricator.rb @@ -1,4 +1,3 @@ # frozen_string_literal: true -Fabricator(:user_option) do -end +Fabricator(:user_option) {} diff --git a/spec/fabricators/user_profile_fabricator.rb b/spec/fabricators/user_profile_fabricator.rb index 042474ed8b..ada2483eeb 100644 --- a/spec/fabricators/user_profile_fabricator.rb +++ b/spec/fabricators/user_profile_fabricator.rb @@ -5,6 +5,4 @@ Fabricator(:user_profile) do user end -Fabricator(:user_profile_long, from: :user_profile) do - bio_raw ("trout" * 1000) -end +Fabricator(:user_profile_long, from: :user_profile) { bio_raw ("trout" * 1000) } diff --git a/spec/fabricators/user_second_factor_fabricator.rb b/spec/fabricators/user_second_factor_fabricator.rb index cbb2d5aa4a..8d4a517631 100644 --- a/spec/fabricators/user_second_factor_fabricator.rb +++ b/spec/fabricators/user_second_factor_fabricator.rb @@ -2,7 +2,7 @@ Fabricator(:user_second_factor_totp, from: :user_second_factor) do user - data 'rcyryaqage3jexfj' + data "rcyryaqage3jexfj" enabled true method UserSecondFactor.methods[:totp] end diff --git a/spec/fabricators/user_security_key_fabricator.rb b/spec/fabricators/user_security_key_fabricator.rb index 827413a18d..edc4279f60 100644 --- a/spec/fabricators/user_security_key_fabricator.rb +++ b/spec/fabricators/user_security_key_fabricator.rb @@ -5,8 +5,12 @@ Fabricator(:user_security_key) do # Note: these values are valid and decode to a credential ID and COSE public key # HOWEVER they are largely useless unless you have the device that created # them. It is nice to have an approximation though. - credential_id { 'mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==' } - public_key { 'pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=' } + credential_id do + "mJAJ4CznTO0SuLkJbYwpgK75ao4KMNIPlU5KWM92nq39kRbXzI9mSv6GxTcsMYoiPgaouNw7b7zBiS4vsQaO6A==" + end + public_key do + "pQECAyYgASFYIMNgw4GCpwBUlR2SznJ1yY7B9yFvsuxhfo+C9kcA4IitIlggRdofrCezymy2B/YarX+gfB6gZKg648/cHIMjf6wWmmU=" + end enabled true factor_type { UserSecurityKey.factor_types[:second_factor] } name { sequence(:name) { |i| "Security Key #{i + 1}" } } diff --git a/spec/fabricators/web_hook_fabricator.rb b/spec/fabricators/web_hook_fabricator.rb index 3e490b3f4c..97f753bd13 100644 --- a/spec/fabricators/web_hook_fabricator.rb +++ b/spec/fabricators/web_hook_fabricator.rb @@ -1,62 +1,48 @@ # frozen_string_literal: true Fabricator(:web_hook) do - payload_url 'https://meta.discourse.org/webhook_listener' - content_type WebHook.content_types['application/json'] + payload_url "https://meta.discourse.org/webhook_listener" + content_type WebHook.content_types["application/json"] wildcard_web_hook false - secret 'my_lovely_secret_for_web_hook' + secret "my_lovely_secret_for_web_hook" verify_certificate true active true - transient post_hook: WebHookEventType.find_by(name: 'post') + transient post_hook: WebHookEventType.find_by(name: "post") - after_build do |web_hook, transients| - web_hook.web_hook_event_types << transients[:post_hook] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types << transients[:post_hook] } end -Fabricator(:inactive_web_hook, from: :web_hook) do - active false -end +Fabricator(:inactive_web_hook, from: :web_hook) { active false } -Fabricator(:wildcard_web_hook, from: :web_hook) do - wildcard_web_hook true -end +Fabricator(:wildcard_web_hook, from: :web_hook) { wildcard_web_hook true } Fabricator(:topic_web_hook, from: :web_hook) do - transient topic_hook: WebHookEventType.find_by(name: 'topic') + transient topic_hook: WebHookEventType.find_by(name: "topic") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:topic_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:topic_hook]] } end Fabricator(:post_web_hook, from: :web_hook) do - transient topic_hook: WebHookEventType.find_by(name: 'post') + transient topic_hook: WebHookEventType.find_by(name: "post") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:post_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:post_hook]] } end Fabricator(:user_web_hook, from: :web_hook) do - transient user_hook: WebHookEventType.find_by(name: 'user') + transient user_hook: WebHookEventType.find_by(name: "user") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:user_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:user_hook]] } end Fabricator(:group_web_hook, from: :web_hook) do - transient group_hook: WebHookEventType.find_by(name: 'group') + transient group_hook: WebHookEventType.find_by(name: "group") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:group_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:group_hook]] } end Fabricator(:category_web_hook, from: :web_hook) do - transient category_hook: WebHookEventType.find_by(name: 'category') + transient category_hook: WebHookEventType.find_by(name: "category") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:category_hook]] @@ -64,15 +50,13 @@ Fabricator(:category_web_hook, from: :web_hook) do end Fabricator(:tag_web_hook, from: :web_hook) do - transient tag_hook: WebHookEventType.find_by(name: 'tag') + transient tag_hook: WebHookEventType.find_by(name: "tag") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:tag_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:tag_hook]] } end Fabricator(:reviewable_web_hook, from: :web_hook) do - transient reviewable_hook: WebHookEventType.find_by(name: 'reviewable') + transient reviewable_hook: WebHookEventType.find_by(name: "reviewable") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:reviewable_hook]] @@ -80,7 +64,7 @@ Fabricator(:reviewable_web_hook, from: :web_hook) do end Fabricator(:notification_web_hook, from: :web_hook) do - transient notification_hook: WebHookEventType.find_by(name: 'notification') + transient notification_hook: WebHookEventType.find_by(name: "notification") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:notification_hook]] @@ -88,7 +72,7 @@ Fabricator(:notification_web_hook, from: :web_hook) do end Fabricator(:user_badge_web_hook, from: :web_hook) do - transient user_badge_hook: WebHookEventType.find_by(name: 'user_badge') + transient user_badge_hook: WebHookEventType.find_by(name: "user_badge") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:user_badge_hook]] @@ -96,7 +80,7 @@ Fabricator(:user_badge_web_hook, from: :web_hook) do end Fabricator(:group_user_web_hook, from: :web_hook) do - transient group_user_hook: WebHookEventType.find_by(name: 'group_user') + transient group_user_hook: WebHookEventType.find_by(name: "group_user") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:group_user_hook]] @@ -104,15 +88,13 @@ Fabricator(:group_user_web_hook, from: :web_hook) do end Fabricator(:like_web_hook, from: :web_hook) do - transient like_hook: WebHookEventType.find_by(name: 'like') + transient like_hook: WebHookEventType.find_by(name: "like") - after_build do |web_hook, transients| - web_hook.web_hook_event_types = [transients[:like_hook]] - end + after_build { |web_hook, transients| web_hook.web_hook_event_types = [transients[:like_hook]] } end Fabricator(:user_promoted_web_hook, from: :web_hook) do - transient user_promoted_hook: WebHookEventType.find_by(name: 'user_promoted') + transient user_promoted_hook: WebHookEventType.find_by(name: "user_promoted") after_build do |web_hook, transients| web_hook.web_hook_event_types = [transients[:user_promoted_hook]] diff --git a/spec/fixtures/db/post_migrate/change/20990309014015_drop_email_logs.rb b/spec/fixtures/db/post_migrate/change/20990309014015_drop_email_logs.rb index 7b4b1e2252..441440772a 100644 --- a/spec/fixtures/db/post_migrate/change/20990309014015_drop_email_logs.rb +++ b/spec/fixtures/db/post_migrate/change/20990309014015_drop_email_logs.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class DropEmailLogs < ActiveRecord::Migration[5.2] - DROPPED_TABLES ||= %i{email_logs} + DROPPED_TABLES ||= %i[email_logs] def change drop_table :email_logs diff --git a/spec/fixtures/db/post_migrate/drop_column/20990309014014_drop_post_columns.rb b/spec/fixtures/db/post_migrate/drop_column/20990309014014_drop_post_columns.rb index 8390f83207..556489f255 100644 --- a/spec/fixtures/db/post_migrate/drop_column/20990309014014_drop_post_columns.rb +++ b/spec/fixtures/db/post_migrate/drop_column/20990309014014_drop_post_columns.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true class DropPostColumns < ActiveRecord::Migration[5.2] - DROPPED_COLUMNS ||= { - posts: %i{via_email raw_email} - } + DROPPED_COLUMNS ||= { posts: %i[via_email raw_email] } def up remove_column :posts, :via_email diff --git a/spec/fixtures/db/post_migrate/drop_table/20990309014013_drop_email_logs_table.rb b/spec/fixtures/db/post_migrate/drop_table/20990309014013_drop_email_logs_table.rb index 5d07960ea7..ed8d31d7e6 100644 --- a/spec/fixtures/db/post_migrate/drop_table/20990309014013_drop_email_logs_table.rb +++ b/spec/fixtures/db/post_migrate/drop_table/20990309014013_drop_email_logs_table.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class DropEmailLogsTable < ActiveRecord::Migration[5.2] - DROPPED_TABLES ||= %i{email_logs} + DROPPED_TABLES ||= %i[email_logs] def up drop_table :email_logs diff --git a/spec/fixtures/plugins/csp_extension/plugin.rb b/spec/fixtures/plugins/csp_extension/plugin.rb index e694598c4a..0991754eb6 100644 --- a/spec/fixtures/plugins/csp_extension/plugin.rb +++ b/spec/fixtures/plugins/csp_extension/plugin.rb @@ -6,8 +6,8 @@ # authors: xrav3nz extend_content_security_policy( - script_src: ['https://from-plugin.com', '/local/path'], - object_src: ['https://test-stripping.com'], - frame_ancestors: ['https://frame-ancestors-plugin.ext'], - manifest_src: ['https://manifest-src.com'] + script_src: %w[https://from-plugin.com /local/path], + object_src: ["https://test-stripping.com"], + frame_ancestors: ["https://frame-ancestors-plugin.ext"], + manifest_src: ["https://manifest-src.com"], ) diff --git a/spec/fixtures/plugins/my_plugin/plugin.rb b/spec/fixtures/plugins/my_plugin/plugin.rb index 2253f3a1b0..62c7ed4dc5 100644 --- a/spec/fixtures/plugins/my_plugin/plugin.rb +++ b/spec/fixtures/plugins/my_plugin/plugin.rb @@ -5,8 +5,7 @@ # version: 0.1 # authors: Frank Zappa -auth_provider title: 'with Ubuntu', - authenticator: Auth::FacebookAuthenticator.new +auth_provider title: "with Ubuntu", authenticator: Auth::FacebookAuthenticator.new register_javascript < HTML - SiteSetting.opengraph_image = Fabricate(:upload, - url: '/images/og-image.svg' - ) + SiteSetting.opengraph_image = Fabricate(:upload, url: "/images/og-image.svg") expect(helper.crawlable_meta_data).to include(<<~HTML) HTML - SiteSetting.twitter_summary_large_image = Fabricate(:upload, - url: '/images/twitter.png' - ) + SiteSetting.twitter_summary_large_image = Fabricate(:upload, url: "/images/twitter.png") expect(helper.crawlable_meta_data).to include(<<~HTML) HTML - SiteSetting.twitter_summary_large_image = Fabricate(:upload, - url: '/images/twitter.svg' - ) + SiteSetting.twitter_summary_large_image = Fabricate(:upload, url: "/images/twitter.svg") expect(helper.crawlable_meta_data).to include(<<~HTML) HTML - SiteSetting.logo = Fabricate(:upload, url: '/images/d-logo-sketch.svg') + SiteSetting.logo = Fabricate(:upload, url: "/images/d-logo-sketch.svg") expect(helper.crawlable_meta_data).not_to include("twitter:image") end end end - describe 'discourse_color_scheme_stylesheets' do + describe "discourse_color_scheme_stylesheets" do fab!(:user) { Fabricate(:user) } - it 'returns a stylesheet link tag by default' do + it "returns a stylesheet link tag by default" do cs_stylesheets = helper.discourse_color_scheme_stylesheets expect(cs_stylesheets).to include("stylesheets/color_definitions") end - it 'returns two color scheme link tags when dark mode is enabled' do - SiteSetting.default_dark_mode_color_scheme_id = ColorScheme.where(name: "Dark").pluck_first(:id) + it "returns two color scheme link tags when dark mode is enabled" do + SiteSetting.default_dark_mode_color_scheme_id = + ColorScheme.where(name: "Dark").pluck_first(:id) cs_stylesheets = helper.discourse_color_scheme_stylesheets expect(cs_stylesheets).to include("(prefers-color-scheme: dark)") expect(cs_stylesheets.scan("stylesheets/color_definitions").size).to eq(2) end - it 'handles a missing dark color scheme gracefully' do + it "handles a missing dark color scheme gracefully" do scheme = ColorScheme.create!(name: "pyramid") SiteSetting.default_dark_mode_color_scheme_id = scheme.id scheme.destroy! @@ -638,7 +671,7 @@ RSpec.describe ApplicationHelper do context "with custom light scheme" do before do - @new_cs = Fabricate(:color_scheme, name: 'Flamboyant') + @new_cs = Fabricate(:color_scheme, name: "Flamboyant") user.user_option.color_scheme_id = @new_cs.id user.user_option.save! helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user @@ -673,9 +706,10 @@ RSpec.describe ApplicationHelper do user.user_option.dark_scheme_id = -1 user.user_option.save! helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user - @new_cs = Fabricate(:color_scheme, name: 'Custom Color Scheme') + @new_cs = Fabricate(:color_scheme, name: "Custom Color Scheme") - SiteSetting.default_dark_mode_color_scheme_id = ColorScheme.where(name: "Dark").pluck_first(:id) + SiteSetting.default_dark_mode_color_scheme_id = + ColorScheme.where(name: "Dark").pluck_first(:id) end it "returns no dark scheme stylesheet when user has disabled that option" do @@ -711,23 +745,24 @@ RSpec.describe ApplicationHelper do end describe "dark_color_scheme?" do - it 'returns false for the base color scheme' do + it "returns false for the base color scheme" do expect(helper.dark_color_scheme?).to eq(false) end - it 'works correctly for a dark scheme' do - dark_theme = Theme.create( - name: "Dark", - user_id: -1, - color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id - ) + it "works correctly for a dark scheme" do + dark_theme = + Theme.create( + name: "Dark", + user_id: -1, + color_scheme_id: ColorScheme.find_by(base_scheme_id: "Dark").id, + ) helper.request.env[:resolved_theme_id] = dark_theme.id expect(helper.dark_color_scheme?).to eq(true) end end - describe 'html_lang' do + describe "html_lang" do fab!(:user) { Fabricate(:user) } before do @@ -735,12 +770,12 @@ RSpec.describe ApplicationHelper do SiteSetting.default_locale = :fr end - it 'returns default locale if no request' do + it "returns default locale if no request" do helper.request = nil expect(helper.html_lang).to eq(SiteSetting.default_locale) end - it 'returns current user locale if request' do + it "returns current user locale if request" do helper.request.env[Auth::DefaultCurrentUserProvider::CURRENT_USER_KEY] = user expect(helper.html_lang).to eq(I18n.locale.to_s) end diff --git a/spec/helpers/redis_snapshot_helper.rb b/spec/helpers/redis_snapshot_helper.rb index 92d5be9cab..fb7e6a9b40 100644 --- a/spec/helpers/redis_snapshot_helper.rb +++ b/spec/helpers/redis_snapshot_helper.rb @@ -2,12 +2,8 @@ module RedisSnapshotHelper def use_redis_snapshotting - before(:each) do - RedisSnapshot.begin_faux_transaction - end + before(:each) { RedisSnapshot.begin_faux_transaction } - after(:each) do - RedisSnapshot.end_faux_transaction - end + after(:each) { RedisSnapshot.end_faux_transaction } end end diff --git a/spec/helpers/topics_helper_spec.rb b/spec/helpers/topics_helper_spec.rb index 5f0462fd97..075770839a 100644 --- a/spec/helpers/topics_helper_spec.rb +++ b/spec/helpers/topics_helper_spec.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true RSpec.describe TopicsHelper do - describe "#categories_breadcrumb" do let(:user) { Fabricate(:user) } let(:category) { Fabricate(:category_with_definition) } let(:subcategory) { Fabricate(:category_with_definition, parent_category_id: category.id) } - let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id) } + let(:subsubcategory) do + Fabricate(:category_with_definition, parent_category_id: subcategory.id) + end it "works with sub-sub-categories" do SiteSetting.max_category_nesting = 3 diff --git a/spec/helpers/user_notifications_helper_spec.rb b/spec/helpers/user_notifications_helper_spec.rb index 175c7bbb64..94ef066d9a 100644 --- a/spec/helpers/user_notifications_helper_spec.rb +++ b/spec/helpers/user_notifications_helper_spec.rb @@ -3,18 +3,17 @@ RSpec.describe UserNotificationsHelper do let(:upload_path) { Discourse.store.upload_path } - describe '#email_excerpt' do - let(:paragraphs) { [ - "

    This is the first paragraph, but you should read more.

    ", - "

    And here is its friend, the second paragraph.

    " - ] } - - let(:cooked) do - paragraphs.join("\n") + describe "#email_excerpt" do + let(:paragraphs) do + [ + "

    This is the first paragraph, but you should read more.

    ", + "

    And here is its friend, the second paragraph.

    ", + ] end - let(:post_quote) do - <<~HTML + let(:cooked) { paragraphs.join("\n") } + + let(:post_quote) { <<~HTML }
    HTML - end let(:image_paragraph) do '

    ' end - let(:lightbox_image) do - <<~HTML + let(:lightbox_image) { <<~HTML }

    HTML - end let(:expected_lightbox_image) do '' @@ -53,14 +49,16 @@ RSpec.describe UserNotificationsHelper do end it "doesn't count emoji images" do - with_emoji = "

    Hi \":smile:\"

    " + with_emoji = + "

    Hi \":smile:\"

    " arg = ([with_emoji] + paragraphs).join("\n") SiteSetting.digest_min_excerpt_length = 50 expect(helper.email_excerpt(arg)).to eq([with_emoji, paragraphs[0]].join) end it "only counts link text" do - with_link = "

    Hi friends!

    " + with_link = + "

    Hi friends!

    " arg = ([with_link] + paragraphs).join("\n") SiteSetting.digest_min_excerpt_length = 50 expect(helper.email_excerpt(arg)).to eq([with_link, paragraphs[0]].join) @@ -81,11 +79,12 @@ RSpec.describe UserNotificationsHelper do

    AFTER

    HTML - expect(helper.email_excerpt(cooked)).to eq "

    BEFORE

    \n

    This is a user quote

    \n

    AFTER

    " + expect( + helper.email_excerpt(cooked), + ).to eq "

    BEFORE

    \n

    This is a user quote

    \n

    AFTER

    " end it "defaults to content after post quote (image w/ no text)" do - cooked = <<~HTML #{post_quote} #{image_paragraph} @@ -94,7 +93,8 @@ RSpec.describe UserNotificationsHelper do end it "defaults to content after post quote (onebox)" do - aside_onebox = '' + aside_onebox = + '' cooked = <<~HTML #{post_quote} #{aside_onebox} @@ -120,44 +120,40 @@ RSpec.describe UserNotificationsHelper do end end - describe '#logo_url' do - describe 'local store' do + describe "#logo_url" do + describe "local store" do let(:upload) { Fabricate(:upload, sha1: "somesha1") } - before do - SiteSetting.logo = upload - end + before { SiteSetting.logo = upload } - it 'should return the right URL' do + it "should return the right URL" do expect(helper.logo_url).to eq( - "http://test.localhost/#{upload_path}/original/1X/somesha1.png" + "http://test.localhost/#{upload_path}/original/1X/somesha1.png", ) end - describe 'when cdn path is configured' do + describe "when cdn path is configured" do before do - GlobalSetting.expects(:cdn_url) - .returns('https://some.localcdn.com') - .at_least_once + GlobalSetting.expects(:cdn_url).returns("https://some.localcdn.com").at_least_once end - it 'should return the right URL' do + it "should return the right URL" do expect(helper.logo_url).to eq( - "https://some.localcdn.com/#{upload_path}/original/1X/somesha1.png" + "https://some.localcdn.com/#{upload_path}/original/1X/somesha1.png", ) end end - describe 'when logo is an SVG' do + describe "when logo is an SVG" do let(:upload) { Fabricate(:upload, extension: "svg") } - it 'should return nil' do + it "should return nil" do expect(helper.logo_url).to eq(nil) end end end - describe 's3 store' do + describe "s3 store" do let(:upload) { Fabricate(:upload_s3, sha1: "somesha1") } before do @@ -165,32 +161,27 @@ RSpec.describe UserNotificationsHelper do SiteSetting.logo = upload end - it 'should return the right URL' do + it "should return the right URL" do expect(helper.logo_url).to eq( - "http://s3-upload-bucket.s3.dualstack.#{SiteSetting.s3_region}.amazonaws.com/original/1X/somesha1.png" + "http://s3-upload-bucket.s3.dualstack.#{SiteSetting.s3_region}.amazonaws.com/original/1X/somesha1.png", ) end - describe 'when global cdn path is configured' do - it 'should return the right url' do - GlobalSetting.stubs(:cdn_url).returns('https://some.cdn.com/cluster') + describe "when global cdn path is configured" do + it "should return the right url" do + GlobalSetting.stubs(:cdn_url).returns("https://some.cdn.com/cluster") expect(helper.logo_url).to eq( - "http://s3-upload-bucket.s3.dualstack.#{SiteSetting.s3_region}.amazonaws.com/original/1X/somesha1.png" + "http://s3-upload-bucket.s3.dualstack.#{SiteSetting.s3_region}.amazonaws.com/original/1X/somesha1.png", ) end end - describe 'when cdn path is configured' do - before do - SiteSetting.s3_cdn_url = 'https://some.cdn.com' + describe "when cdn path is configured" do + before { SiteSetting.s3_cdn_url = "https://some.cdn.com" } - end - - it 'should return the right url' do - expect(helper.logo_url).to eq( - "https://some.cdn.com/original/1X/somesha1.png" - ) + it "should return the right url" do + expect(helper.logo_url).to eq("https://some.cdn.com/original/1X/somesha1.png") end end end diff --git a/spec/import_export/category_exporter_spec.rb b/spec/import_export/category_exporter_spec.rb index 5fda50252b..4a41fe9e8b 100644 --- a/spec/import_export/category_exporter_spec.rb +++ b/spec/import_export/category_exporter_spec.rb @@ -3,26 +3,23 @@ require "import_export" RSpec.describe ImportExport::CategoryExporter do - fab!(:category) { Fabricate(:category) } fab!(:group) { Fabricate(:group) } fab!(:user) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } fab!(:user3) { Fabricate(:user) } - before do - STDOUT.stubs(:write) - end + before { STDOUT.stubs(:write) } - describe '.perform' do - it 'export the category when it is found' do + describe ".perform" do + it "export the category when it is found" do data = ImportExport::CategoryExporter.new([category.id]).perform.export_data expect(data[:categories].count).to eq(1) expect(data[:groups].count).to eq(0) end - it 'export the category with permission groups' do + it "export the category with permission groups" do _category_group = Fabricate(:category_group, category: category, group: group) data = ImportExport::CategoryExporter.new([category.id]).perform.export_data @@ -30,7 +27,7 @@ RSpec.describe ImportExport::CategoryExporter do expect(data[:groups].count).to eq(1) end - it 'export multiple categories' do + it "export multiple categories" do category2 = Fabricate(:category) _category_group = Fabricate(:category_group, category: category, group: group) data = ImportExport::CategoryExporter.new([category.id, category2.id]).perform.export_data @@ -39,7 +36,7 @@ RSpec.describe ImportExport::CategoryExporter do expect(data[:groups].count).to eq(1) end - it 'export the category with topics and users' do + it "export the category with topics and users" do topic1 = Fabricate(:topic, category: category, user_id: -1) Fabricate(:post, topic: topic1, user: User.find(-1), post_number: 1) topic2 = Fabricate(:topic, category: category, user: user) @@ -54,5 +51,4 @@ RSpec.describe ImportExport::CategoryExporter do expect(data[:users].map { |u| u[:id] }).to match_array([user.id, user2.id, user3.id]) end end - end diff --git a/spec/import_export/category_structure_exporter_spec.rb b/spec/import_export/category_structure_exporter_spec.rb index b5ee22fbde..776ee20cd3 100644 --- a/spec/import_export/category_structure_exporter_spec.rb +++ b/spec/import_export/category_structure_exporter_spec.rb @@ -3,12 +3,9 @@ require "import_export/category_structure_exporter" RSpec.describe ImportExport::CategoryStructureExporter do + before { STDOUT.stubs(:write) } - before do - STDOUT.stubs(:write) - end - - it 'export all the categories' do + it "export all the categories" do category = Fabricate(:category) data = ImportExport::CategoryStructureExporter.new.perform.export_data @@ -17,7 +14,7 @@ RSpec.describe ImportExport::CategoryStructureExporter do expect(data[:users].blank?).to eq(true) end - it 'export all the categories with permission groups' do + it "export all the categories with permission groups" do category = Fabricate(:category) group = Fabricate(:group) category_group = Fabricate(:category_group, category: category, group: group) @@ -28,7 +25,7 @@ RSpec.describe ImportExport::CategoryStructureExporter do expect(data[:users].blank?).to eq(true) end - it 'export all the categories with permission groups and users' do + it "export all the categories with permission groups and users" do category = Fabricate(:category) group = Fabricate(:group) user = Fabricate(:user) @@ -40,5 +37,4 @@ RSpec.describe ImportExport::CategoryStructureExporter do expect(data[:groups].count).to eq(1) expect(data[:users].count).to eq(1) end - end diff --git a/spec/import_export/group_exporter_spec.rb b/spec/import_export/group_exporter_spec.rb index 8fd5c0f908..d6e188b29a 100644 --- a/spec/import_export/group_exporter_spec.rb +++ b/spec/import_export/group_exporter_spec.rb @@ -3,12 +3,9 @@ require "import_export/group_exporter" RSpec.describe ImportExport::GroupExporter do + before { STDOUT.stubs(:write) } - before do - STDOUT.stubs(:write) - end - - it 'exports all the groups' do + it "exports all the groups" do group = Fabricate(:group) user = Fabricate(:user) group_user = Fabricate(:group_user, group: group, user: user) @@ -18,7 +15,7 @@ RSpec.describe ImportExport::GroupExporter do expect(data[:users].blank?).to eq(true) end - it 'exports all the groups with users' do + it "exports all the groups with users" do group = Fabricate(:group) user = Fabricate(:user) group_user = Fabricate(:group_user, group: group, user: user) @@ -27,5 +24,4 @@ RSpec.describe ImportExport::GroupExporter do expect(data[:groups].map { |g| g[:id] }).to include(group.id) expect(data[:users].map { |u| u[:id] }).to include(user.id) end - end diff --git a/spec/import_export/importer_spec.rb b/spec/import_export/importer_spec.rb index 39009ddabf..00cc89203e 100644 --- a/spec/import_export/importer_spec.rb +++ b/spec/import_export/importer_spec.rb @@ -3,9 +3,7 @@ require "import_export" RSpec.describe ImportExport::Importer do - before do - STDOUT.stubs(:write) - end + before { STDOUT.stubs(:write) } let(:import_data) do import_file = Rack::Test::UploadedFile.new(file_from_fixtures("import-export.json", "json")) @@ -16,93 +14,79 @@ RSpec.describe ImportExport::Importer do ImportExport::Importer.new(data).perform end - describe '.perform' do - it 'topics and users' do + describe ".perform" do + it "topics and users" do data = import_data.dup data[:categories] = nil data[:groups] = nil - expect { - import(data) - }.to not_change { Category.count } - .and not_change { Group.count } - .and change { Topic.count }.by(2) - .and change { User.count }.by(2) + expect { import(data) }.to not_change { Category.count }.and not_change { + Group.count + }.and change { Topic.count }.by(2).and change { User.count }.by(2) end - context 'with categories and groups' do - it 'works' do + context "with categories and groups" do + it "works" do data = import_data.dup data[:topics] = nil data[:users] = nil - expect { - import(data) - }.to change { Category.count }.by(6) - .and change { Group.count }.by(2) - .and change { Topic.count }.by(6) - .and not_change { User.count } + expect { import(data) }.to change { Category.count }.by(6).and change { Group.count }.by( + 2, + ).and change { Topic.count }.by(6).and not_change { User.count } end - it 'works with sub-sub-categories' do + it "works with sub-sub-categories" do data = import_data.dup # 11 -> 10 -> 15 data[:categories].find { |c| c[:id] == 10 }[:parent_category_id] = 11 data[:categories].find { |c| c[:id] == 15 }[:parent_category_id] = 10 - expect { import(data) } - .to change { Category.count }.by(6) - .and change { SiteSetting.max_category_nesting }.from(2).to(3) + expect { import(data) }.to change { Category.count }.by(6).and change { + SiteSetting.max_category_nesting + }.from(2).to(3) end - it 'fixes permissions' do + it "fixes permissions" do data = import_data.dup data[:categories].find { |c| c[:id] == 10 }[:permissions_params] = { custom_group: 1 } data[:categories].find { |c| c[:id] == 15 }[:permissions_params] = { staff: 1 } permissions = data[:categories].find { |c| c[:id] == 10 }[:permissions_params] - expect { import(data) } - .to change { Category.count }.by(6) - .and change { permissions[:staff] }.from(nil).to(1) + expect { import(data) }.to change { Category.count }.by(6).and change { + permissions[:staff] + }.from(nil).to(1) end end - it 'categories, groups and users' do + it "categories, groups and users" do data = import_data.dup data[:topics] = nil - expect { - import(data) - }.to change { Category.count }.by(6) - .and change { Group.count }.by(2) - .and change { Topic.count }.by(6) - .and change { User.count }.by(2) + expect { import(data) }.to change { Category.count }.by(6).and change { Group.count }.by( + 2, + ).and change { Topic.count }.by(6).and change { User.count }.by(2) end - it 'groups' do + it "groups" do data = import_data.dup data[:categories] = nil data[:topics] = nil data[:users] = nil - expect { - import(data) - }.to not_change { Category.count } - .and change { Group.count }.by(2) - .and not_change { Topic.count } - .and not_change { User.count } + expect { import(data) }.to not_change { Category.count }.and change { Group.count }.by( + 2, + ).and not_change { Topic.count }.and not_change { User.count } end - it 'all' do - expect { - import(import_data) - }.to change { Category.count }.by(6) - .and change { Group.count }.by(2) - .and change { Topic.count }.by(8) - .and change { User.count }.by(2) - .and change { TranslationOverride.count }.by(1) + it "all" do + expect { import(import_data) }.to change { Category.count }.by(6).and change { + Group.count + }.by(2).and change { Topic.count }.by(8).and change { User.count }.by(2).and change { + TranslationOverride.count + }.by(1) end end end diff --git a/spec/import_export/topic_exporter_spec.rb b/spec/import_export/topic_exporter_spec.rb index 809189158c..a36318a5d3 100644 --- a/spec/import_export/topic_exporter_spec.rb +++ b/spec/import_export/topic_exporter_spec.rb @@ -3,17 +3,14 @@ require "import_export" RSpec.describe ImportExport::TopicExporter do - - before do - STDOUT.stubs(:write) - end + before { STDOUT.stubs(:write) } fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic, user: user) } fab!(:post) { Fabricate(:post, topic: topic, user: user) } - describe '.perform' do - it 'export a single topic' do + describe ".perform" do + it "export a single topic" do data = ImportExport::TopicExporter.new([topic.id]).perform.export_data expect(data[:categories].blank?).to eq(true) @@ -22,7 +19,7 @@ RSpec.describe ImportExport::TopicExporter do expect(data[:users].count).to eq(1) end - it 'export multiple topics' do + it "export multiple topics" do topic2 = Fabricate(:topic, user: user) _post2 = Fabricate(:post, user: user, topic: topic2) data = ImportExport::TopicExporter.new([topic.id, topic2.id]).perform.export_data @@ -33,5 +30,4 @@ RSpec.describe ImportExport::TopicExporter do expect(data[:users].map { |u| u[:id] }).to match_array([user.id]) end end - end diff --git a/spec/initializers/track_setting_changes_spec.rb b/spec/initializers/track_setting_changes_spec.rb index bab78a1d33..47c538b4f5 100644 --- a/spec/initializers/track_setting_changes_spec.rb +++ b/spec/initializers/track_setting_changes_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -RSpec.describe 'Setting changes' do - describe '#must_approve_users' do +RSpec.describe "Setting changes" do + describe "#must_approve_users" do before { SiteSetting.must_approve_users = false } - it 'does not approve a user with associated reviewables' do + it "does not approve a user with associated reviewables" do user_pending_approval = Fabricate(:reviewable_user).target SiteSetting.must_approve_users = true @@ -12,7 +12,7 @@ RSpec.describe 'Setting changes' do expect(user_pending_approval.reload.approved?).to eq(false) end - it 'approves a user with no associated reviewables' do + it "approves a user with no associated reviewables" do non_approved_user = Fabricate(:user, approved: false) SiteSetting.must_approve_users = true @@ -21,10 +21,10 @@ RSpec.describe 'Setting changes' do end end - describe '#reviewable_low_priority_threshold' do + describe "#reviewable_low_priority_threshold" do let(:new_threshold) { 5 } - it 'sets the low priority value' do + it "sets the low priority value" do medium_threshold = 10 Reviewable.set_priorities(medium: medium_threshold) diff --git a/spec/integration/api_keys_spec.rb b/spec/integration/api_keys_spec.rb index be9e790a68..c7ec616e20 100644 --- a/spec/integration/api_keys_spec.rb +++ b/spec/integration/api_keys_spec.rb @@ -1,25 +1,21 @@ # frozen_string_literal: true -RSpec.describe 'api keys' do +RSpec.describe "api keys" do let(:user) { Fabricate(:user) } let(:api_key) { ApiKey.create!(user_id: user.id, created_by_id: Discourse.system_user) } - it 'works in headers' do - get '/session/current.json', headers: { - HTTP_API_KEY: api_key.key - } + it "works in headers" do + get "/session/current.json", headers: { HTTP_API_KEY: api_key.key } expect(response.status).to eq(200) expect(response.parsed_body["current_user"]["username"]).to eq(user.username) end - it 'does not work in parameters' do - get '/session/current.json', params: { - api_key: api_key.key - } + it "does not work in parameters" do + get "/session/current.json", params: { api_key: api_key.key } expect(response.status).to eq(404) end - it 'allows parameters on ics routes' do + it "allows parameters on ics routes" do get "/u/#{user.username}/bookmarks.ics?api_key=#{api_key.key}&api_username=#{user.username.downcase}" expect(response.status).to eq(200) @@ -28,14 +24,14 @@ RSpec.describe 'api keys' do expect(response.status).to eq(403) end - it 'allows parameters for handle mail' do + it "allows parameters for handle mail" do admin_api_key = ApiKey.create!(user: Fabricate(:admin), created_by_id: Discourse.system_user) post "/admin/email/handle_mail.json?api_key=#{admin_api_key.key}", params: { email: "blah" } expect(response.status).to eq(200) end - it 'allows parameters for rss feeds' do + it "allows parameters for rss feeds" do SiteSetting.login_required = true get "/latest.rss?api_key=#{api_key.key}&api_username=#{user.username.downcase}" @@ -52,32 +48,28 @@ RSpec.describe 'api keys' do plugin.add_api_parameter_route methods: [:get], actions: ["session#current"] end - it 'allows parameter access to the registered route' do - get '/session/current.json', params: { - api_key: api_key.key - } + it "allows parameter access to the registered route" do + get "/session/current.json", params: { api_key: api_key.key } expect(response.status).to eq(200) end end end -RSpec.describe 'user api keys' do +RSpec.describe "user api keys" do let(:user) { Fabricate(:user) } let(:user_api_key) { Fabricate(:readonly_user_api_key, user: user) } - it 'updates last used time on use' do + it "updates last used time on use" do freeze_time user_api_key.update_columns(last_used_at: 7.days.ago) - get '/session/current.json', headers: { - HTTP_USER_API_KEY: user_api_key.key, - } + get "/session/current.json", headers: { HTTP_USER_API_KEY: user_api_key.key } expect(user_api_key.reload.last_used_at).to eq_time(Time.zone.now) end - it 'allows parameters on ics routes' do + it "allows parameters on ics routes" do get "/u/#{user.username}/bookmarks.ics?user_api_key=#{user_api_key.key}" expect(response.status).to eq(200) @@ -86,7 +78,7 @@ RSpec.describe 'user api keys' do expect(response.status).to eq(403) end - it 'allows parameters for rss feeds' do + it "allows parameters for rss feeds" do SiteSetting.login_required = true get "/latest.rss?user_api_key=#{user_api_key.key}" @@ -102,27 +94,19 @@ RSpec.describe 'user api keys' do calendar_key = Fabricate(:bookmarks_calendar_user_api_key, user: admin) - get "/u/#{user.username}/bookmarks.json", headers: { - HTTP_USER_API_KEY: calendar_key.key, - } + get "/u/#{user.username}/bookmarks.json", headers: { HTTP_USER_API_KEY: calendar_key.key } expect(response.status).to eq(403) # Does not allow json - get "/u/#{user.username}/bookmarks.ics", headers: { - HTTP_USER_API_KEY: calendar_key.key, - } + get "/u/#{user.username}/bookmarks.ics", headers: { HTTP_USER_API_KEY: calendar_key.key } expect(response.status).to eq(200) # Allows ICS # Now restrict the key calendar_key.scopes.first.update(allowed_parameters: { username: admin.username }) - get "/u/#{user.username}/bookmarks.ics", headers: { - HTTP_USER_API_KEY: calendar_key.key, - } + get "/u/#{user.username}/bookmarks.ics", headers: { HTTP_USER_API_KEY: calendar_key.key } expect(response.status).to eq(403) # Cannot access another users calendar - get "/u/#{admin.username}/bookmarks.ics", headers: { - HTTP_USER_API_KEY: calendar_key.key, - } + get "/u/#{admin.username}/bookmarks.ics", headers: { HTTP_USER_API_KEY: calendar_key.key } expect(response.status).to eq(200) # Can access own calendar end @@ -138,12 +122,9 @@ RSpec.describe 'user api keys' do user_api_key.save! end - it 'allows parameter access to the registered route' do - get '/session/current.json', headers: { - HTTP_USER_API_KEY: user_api_key.key - } + it "allows parameter access to the registered route" do + get "/session/current.json", headers: { HTTP_USER_API_KEY: user_api_key.key } expect(response.status).to eq(200) end end - end diff --git a/spec/integration/auto_reject_reviewable_users_spec.rb b/spec/integration/auto_reject_reviewable_users_spec.rb index f993f097c2..dd09810733 100644 --- a/spec/integration/auto_reject_reviewable_users_spec.rb +++ b/spec/integration/auto_reject_reviewable_users_spec.rb @@ -12,9 +12,7 @@ RSpec.describe "auto reject reviewable users" do Jobs::AutoQueueHandler.new.execute({}) expect(old_user.reload.rejected?).to eq(true) - expect(UserHistory.last.context).to eq( - I18n.t("user.destroy_reasons.reviewable_reject_auto") - ) + expect(UserHistory.last.context).to eq(I18n.t("user.destroy_reasons.reviewable_reject_auto")) end end end diff --git a/spec/integration/blocked_hotlinked_media_spec.rb b/spec/integration/blocked_hotlinked_media_spec.rb index c727210c48..c9dfdc2a6b 100644 --- a/spec/integration/blocked_hotlinked_media_spec.rb +++ b/spec/integration/blocked_hotlinked_media_spec.rb @@ -3,11 +3,20 @@ RSpec.describe "hotlinked media blocking" do let(:hotlinked_url) { "http://example.com/images/2/2e/Longcat1.png" } let(:onebox_url) { "http://example.com/onebox" } - let(:png) { Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") } + let(:png) do + Base64.decode64( + "R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==", + ) + end before do SiteSetting.download_remote_images_to_local = false - stub_request(:get, hotlinked_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) + stub_request(:get, hotlinked_url).to_return( + body: png, + headers: { + "Content-Type" => "image/png", + }, + ) stub_image_size end @@ -19,38 +28,65 @@ RSpec.describe "hotlinked media blocking" do context "with hotlinked media blocked, before post-processing" do before do SiteSetting.block_hotlinked_media = true - Oneboxer.stubs(:cached_onebox).returns("") + Oneboxer.stubs(:cached_onebox).returns( + "", + ) end it "blocks hotlinked images" do post = Fabricate(:post, raw: "") expect(post.cooked).not_to have_tag("img[src]") - expect(post.cooked).to have_tag("img", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url }) + expect(post.cooked).to have_tag( + "img", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url, + }, + ) end it "blocks hotlinked videos with src" do post = Fabricate(:post, raw: "![alt text|video](#{hotlinked_url})") expect(post.cooked).not_to have_tag("video source[src]") - expect(post.cooked).to have_tag("video source", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url }) + expect(post.cooked).to have_tag( + "video source", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url, + }, + ) end it "blocks hotlinked videos with srcset" do srcset = "#{hotlinked_url} 1x,https://example.com 2x" post = Fabricate(:post, raw: "") expect(post.cooked).not_to have_tag("video source[srcset]") - expect(post.cooked).to have_tag("video source", with: { PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR => srcset }) + expect(post.cooked).to have_tag( + "video source", + with: { + PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR => srcset, + }, + ) end it "blocks hotlinked audio" do post = Fabricate(:post, raw: "![alt text|audio](#{hotlinked_url})") expect(post.cooked).not_to have_tag("audio source[src]") - expect(post.cooked).to have_tag("audio source", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url }) + expect(post.cooked).to have_tag( + "audio source", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url, + }, + ) end it "blocks hotlinked onebox content when cached (post_analyzer)" do post = Fabricate(:post, raw: "#{onebox_url}") expect(post.cooked).not_to have_tag("img[src]") - expect(post.cooked).to have_tag("img", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url }) + expect(post.cooked).to have_tag( + "img", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => hotlinked_url, + }, + ) end it "allows relative URLs" do @@ -81,8 +117,18 @@ RSpec.describe "hotlinked media blocking" do post.reload expect(post.cooked).to have_tag("img", with: { "src" => "https://example.com" }) expect(post.cooked).to have_tag("img", with: { "src" => "https://example.com/myimage.png" }) - expect(post.cooked).to have_tag("img", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => "https://example.com.malicious.com/myimage.png" }) - expect(post.cooked).to have_tag("img", with: { PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => "https://malicious.invalid/https://example.com" }) + expect(post.cooked).to have_tag( + "img", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => "https://example.com.malicious.com/myimage.png", + }, + ) + expect(post.cooked).to have_tag( + "img", + with: { + PrettyText::BLOCKED_HOTLINKED_SRC_ATTR => "https://malicious.invalid/https://example.com", + }, + ) end it "allows multiple exceptions" do @@ -126,7 +172,7 @@ RSpec.describe "hotlinked media blocking" do expect(post.cooked).not_to have_tag("audio") expect(post.cooked).to have_tag( "a.blocked-hotlinked-placeholder[href^='http://example.com'][rel='noopener nofollow ugc']", - count: 4 + count: 4, ) end end diff --git a/spec/integration/category_tag_spec.rb b/spec/integration/category_tag_spec.rb index 59bee8e897..7a67d1928b 100644 --- a/spec/integration/category_tag_spec.rb +++ b/spec/integration/category_tag_spec.rb @@ -6,13 +6,13 @@ RSpec.describe "category tag restrictions" do DiscourseTagging.filter_allowed_tags(Guardian.new(user), opts) end - fab!(:tag1) { Fabricate(:tag, name: 'tag1') } - fab!(:tag2) { Fabricate(:tag, name: 'tag2') } - fab!(:tag3) { Fabricate(:tag, name: 'tag3') } - fab!(:tag4) { Fabricate(:tag, name: 'tag4') } - let(:tag_with_colon) { Fabricate(:tag, name: 'with:colon') } + fab!(:tag1) { Fabricate(:tag, name: "tag1") } + fab!(:tag2) { Fabricate(:tag, name: "tag2") } + fab!(:tag3) { Fabricate(:tag, name: "tag3") } + fab!(:tag4) { Fabricate(:tag, name: "tag4") } + let(:tag_with_colon) { Fabricate(:tag, name: "with:colon") } - fab!(:user) { Fabricate(:user) } + fab!(:user) { Fabricate(:user) } fab!(:admin) { Fabricate(:admin) } before do @@ -23,29 +23,29 @@ RSpec.describe "category tag restrictions" do context "with tags restricted to one category" do fab!(:category_with_tags) { Fabricate(:category) } - fab!(:other_category) { Fabricate(:category) } + fab!(:other_category) { Fabricate(:category) } - before do - category_with_tags.tags = [tag1, tag2] - end + before { category_with_tags.tags = [tag1, tag2] } it "tags belonging to that category can only be used there" do - msg = I18n.t( - "tags.forbidden.category_does_not_allow_tags", - count: 1, - tags: tag3.name, - category: category_with_tags.name - ) + msg = + I18n.t( + "tags.forbidden.category_does_not_allow_tags", + count: 1, + tags: tag3.name, + category: category_with_tags.name, + ) expect { create_post(category: category_with_tags, tags: [tag1.name, tag2.name, tag3.name]) }.to raise_error(StandardError, msg) - msg = I18n.t( - "tags.forbidden.restricted_tags_cannot_be_used_in_category", - count: 2, - tags: [tag1, tag2].map(&:name).sort.join(", "), - category: other_category.name - ) + msg = + I18n.t( + "tags.forbidden.restricted_tags_cannot_be_used_in_category", + count: 2, + tags: [tag1, tag2].map(&:name).sort.join(", "), + category: other_category.name, + ) expect { create_post(category: other_category, tags: [tag1.name, tag2.name, tag3.name]) }.to raise_error(StandardError, msg) @@ -53,27 +53,60 @@ RSpec.describe "category tag restrictions" do it "search can show only permitted tags" do expect(filter_allowed_tags.count).to eq(Tag.count) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags), [tag1, tag2]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category_with_tags), + [tag1, tag2], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name]), [tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name], term: 'tag'), [tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name]), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag'), [tag4]) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + ), + [tag2], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + term: "tag", + ), + [tag2], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name]), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: other_category, + selected_tags: [tag3.name], + term: "tag", + ), + [tag4], + ) end it "search can handle colons in tag names" do tag_with_colon - expect_same_tag_names(filter_allowed_tags(for_input: true, term: 'with:c'), [tag_with_colon]) + expect_same_tag_names(filter_allowed_tags(for_input: true, term: "with:c"), [tag_with_colon]) end it "can't create new tags in a restricted category" do - msg = I18n.t( - "tags.forbidden.category_does_not_allow_tags", - count: 1, - tags: "newtag", - category: category_with_tags.name - ) + msg = + I18n.t( + "tags.forbidden.category_does_not_allow_tags", + count: 1, + tags: "newtag", + category: category_with_tags.name, + ) expect { create_post(category: category_with_tags, tags: [tag1.name, "newtag"]) }.to raise_error(StandardError, msg) @@ -89,61 +122,161 @@ RSpec.describe "category tag restrictions" do end it "can create tags when changing category settings" do - expect { other_category.update(allowed_tags: ['newtag']) }.to change { Tag.count }.by(1) - expect { other_category.update(allowed_tags: [tag1.name, 'tag-stuff', tag2.name, 'another-tag']) }.to change { Tag.count }.by(2) + expect { other_category.update(allowed_tags: ["newtag"]) }.to change { Tag.count }.by(1) + expect { + other_category.update(allowed_tags: [tag1.name, "tag-stuff", tag2.name, "another-tag"]) + }.to change { Tag.count }.by(2) end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag3]) } - before { category_with_tags.update!(category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + before do + category_with_tags.update!( + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "search only returns the allowed tags" do - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags), [tag1]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name]), [tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag2.name]), [tag1]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category_with_tags), + [tag1], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + ), + [tag2], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag2.name], + ), + [tag1], + ) end end - context 'when category allows other tags to be used' do - before do - category_with_tags.update!(allow_global_tags: true) - end + context "when category allows other tags to be used" do + before { category_with_tags.update!(allow_global_tags: true) } it "search can show the permitted tags" do expect(filter_allowed_tags.count).to eq(Tag.count) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags), [tag1, tag2, tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category_with_tags), + [tag1, tag2, tag3, tag4], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name]), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name], term: 'tag'), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name]), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag'), [tag4]) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + ), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + term: "tag", + ), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: other_category, + selected_tags: [tag3.name], + ), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: other_category, + selected_tags: [tag3.name], + term: "tag", + ), + [tag4], + ) end it "works if no tags are restricted to the category" do other_category.update!(allow_global_tags: true) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name]), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category, selected_tags: [tag3.name], term: 'tag'), [tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: other_category, + selected_tags: [tag3.name], + ), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: other_category, + selected_tags: [tag3.name], + term: "tag", + ), + [tag4], + ) end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag3]) } - before { category_with_tags.update!(category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + before do + category_with_tags.update!( + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "search only returns the allowed tags" do - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag1.name]), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category_with_tags, selected_tags: [tag2.name]), [tag1, tag3]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category_with_tags), + [tag1, tag3], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag1.name], + ), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category_with_tags, + selected_tags: [tag2.name], + ), + [tag1, tag3], + ) end end end end context "with tag groups restricted to a category" do - fab!(:tag_group1) { Fabricate(:tag_group) } - fab!(:category) { Fabricate(:category) } - fab!(:other_category) { Fabricate(:category) } + fab!(:tag_group1) { Fabricate(:tag_group) } + fab!(:category) { Fabricate(:category) } + fab!(:other_category) { Fabricate(:category) } before do tag_group1.tags = [tag1, tag2] @@ -156,7 +289,10 @@ RSpec.describe "category tag restrictions" do expect_same_tag_names(filter_allowed_tags(for_input: true), [tag3, tag4]) tag_group1.tags = [tag2, tag3, tag4] - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag2, tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag2, tag3, tag4], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1]) expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag1]) end @@ -165,74 +301,122 @@ RSpec.describe "category tag restrictions" do category.allowed_tags = [tag4.name] category.reload - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag4], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag3]) expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3]) end it "enforces restrictions when creating a topic" do - msg = I18n.t( - "tags.forbidden.category_does_not_allow_tags", - count: 1, - tags: "newtag", - category: category.name + msg = + I18n.t( + "tags.forbidden.category_does_not_allow_tags", + count: 1, + tags: "newtag", + category: category.name, + ) + expect { create_post(category: category, tags: [tag1.name, "newtag"]) }.to raise_error( + StandardError, + msg, ) - expect { - create_post(category: category, tags: [tag1.name, "newtag"]) - }.to raise_error(StandardError, msg) end it "handles colons" do tag_with_colon - expect_same_tag_names(filter_allowed_tags(for_input: true, term: 'with:c'), [tag_with_colon]) + expect_same_tag_names(filter_allowed_tags(for_input: true, term: "with:c"), [tag_with_colon]) end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag3]) } - before { category.update!(category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + before do + category.update!( + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "search only returns the allowed tags" do expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), [tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), [tag1]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), + [tag2], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), + [tag1], + ) end end - context 'when category allows other tags to be used' do - before do - category.update!(allow_global_tags: true) - end + context "when category allows other tags to be used" do + before { category.update!(allow_global_tags: true) } - it 'filters tags correctly' do - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag3, tag4]) + it "filters tags correctly" do + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag3, tag4], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag3, tag4], + ) tag_group1.tags = [tag2, tag3, tag4] - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag3, tag4], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag1]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag1], + ) end it "works if no tags are restricted to the category" do other_category.update!(allow_global_tags: true) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag3, tag4], + ) tag_group1.tags = [tag2, tag3, tag4] - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag1]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag1], + ) end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag3]) } - before { category.update!(category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + before do + category.update!( + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "search only returns the allowed tags" do - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), [tag1, tag3]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag3], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), + [tag1, tag3], + ) end end - context 'when another category has restricted tags using groups' do + context "when another category has restricted tags using groups" do fab!(:category2) { Fabricate(:category) } fab!(:tag_group2) { Fabricate(:tag_group) } @@ -242,32 +426,59 @@ RSpec.describe "category tag restrictions" do category2.reload end - it 'filters tags correctly' do - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category2), [tag2, tag3]) + it "filters tags correctly" do + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category2), + [tag2, tag3], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag4], + ) end it "doesn't care about tags in a group that isn't used in a category" do unused_tag_group = Fabricate(:tag_group) unused_tag_group.tags = [tag4] - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category2), [tag2, tag3]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category2), + [tag2, tag3], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag4], + ) end end - context 'when another category has restricted tags' do + context "when another category has restricted tags" do fab!(:category2) { Fabricate(:category) } it "doesn't filter tags that are also restricted in another category" do category2.tags = [tag2, tag3] - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category2), [tag2, tag3]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category2), + [tag2, tag3], + ) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2, tag4], + ) end end end @@ -278,80 +489,139 @@ RSpec.describe "category tag restrictions" do tag_group = Fabricate(:tag_group, parent_tag_id: tag1.id) tag_group.tags = [tag3, tag4] expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name, tag3.name]), [tag2, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name, tag3.name]), + [tag2, tag4], + ) end it "for tagging a topic, filter_allowed_tags allows tags without parent tag" do tag_group = Fabricate(:tag_group, parent_tag_id: tag1.id) tag_group.tags = [tag3, tag4] expect_same_tag_names(filter_allowed_tags(for_topic: true), [tag1, tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_topic: true, selected_tags: [tag1.name]), [tag1, tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_topic: true, selected_tags: [tag1.name, tag3.name]), [tag1, tag2, tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_topic: true, selected_tags: [tag1.name]), + [tag1, tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags(for_topic: true, selected_tags: [tag1.name, tag3.name]), + [tag1, tag2, tag3, tag4], + ) end it "filter_allowed_tags returns tags common to more than one tag group with parent tag" do - common = Fabricate(:tag, name: 'common') + common = Fabricate(:tag, name: "common") tag_group = Fabricate(:tag_group, parent_tag_id: tag1.id) tag_group.tags = [tag2, common] tag_group = Fabricate(:tag_group, parent_tag_id: tag3.id) tag_group.tags = [tag4] expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), [tag2, tag3, common]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), [tag4, tag1]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), + [tag2, tag3, common], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), + [tag4, tag1], + ) tag_group.tags = [tag4, common] expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), [tag2, tag3, common]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), [tag4, tag1, common]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), + [tag2, tag3, common], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), + [tag4, tag1, common], + ) parent_tag_group = Fabricate(:tag_group, tags: [tag1, tag3]) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), [tag2, tag3, common]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), [tag4, tag1, common]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), + [tag2, tag3, common], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), + [tag4, tag1, common], + ) parent_tag_group.update!(one_per_topic: true) expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag3]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), [tag2, common]) - expect_same_tag_names(filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), [tag4, common]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag1.name]), + [tag2, common], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, selected_tags: [tag3.name]), + [tag4, common], + ) end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag2]) } - fab!(:category) { Fabricate(:category, category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + fab!(:category) do + Fabricate( + :category, + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "search only returns the allowed tags" do tag_group_with_parent = Fabricate(:tag_group, parent_tag_id: tag1.id, tags: [tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category), [tag1, tag2]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), [tag1]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), [tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name, tag2.name]), [tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category), + [tag1, tag2], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag2.name]), + [tag1], + ) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: category, selected_tags: [tag1.name]), + [tag2, tag3, tag4], + ) + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: category, + selected_tags: [tag1.name, tag2.name], + ), + [tag3, tag4], + ) end end context "with category restrictions" do - fab!(:car_category) { Fabricate(:category) } - fab!(:other_category) { Fabricate(:category) } - fab!(:makes) { Fabricate(:tag_group, name: "Makes") } - fab!(:honda_group) { Fabricate(:tag_group, name: "Honda Models") } - fab!(:ford_group) { Fabricate(:tag_group, name: "Ford Models") } + fab!(:car_category) { Fabricate(:category) } + fab!(:other_category) { Fabricate(:category) } + fab!(:makes) { Fabricate(:tag_group, name: "Makes") } + fab!(:honda_group) { Fabricate(:tag_group, name: "Honda Models") } + fab!(:ford_group) { Fabricate(:tag_group, name: "Ford Models") } before do @tags = {} - ['honda', 'ford', 'civic', 'accord', 'mustang', 'taurus'].each do |name| + %w[honda ford civic accord mustang taurus].each do |name| @tags[name] = Fabricate(:tag, name: name) end - makes.tags = [@tags['honda'], @tags['ford']] + makes.tags = [@tags["honda"], @tags["ford"]] - honda_group.parent_tag_id = @tags['honda'].id + honda_group.parent_tag_id = @tags["honda"].id honda_group.save - honda_group.tags = [@tags['civic'], @tags['accord']] + honda_group.tags = [@tags["civic"], @tags["accord"]] - ford_group.parent_tag_id = @tags['ford'].id + ford_group.parent_tag_id = @tags["ford"].id ford_group.save - ford_group.tags = [@tags['mustang'], @tags['taurus']] + ford_group.tags = [@tags["mustang"], @tags["taurus"]] car_category.allowed_tag_groups = [makes.name, honda_group.name, ford_group.name] end @@ -359,39 +629,75 @@ RSpec.describe "category tag restrictions" do it "handles all those rules" do # car tags can't be used outside of car category: expect_same_tag_names(filter_allowed_tags(for_input: true), [tag1, tag2, tag3, tag4]) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: other_category), [tag1, tag2, tag3, tag4]) + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: other_category), + [tag1, tag2, tag3, tag4], + ) # in car category, a make tag must be given first: - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category))).to eq(['ford', 'honda']) + expect( + sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category)), + ).to eq(%w[ford honda]) # model tags depend on which make is chosen: - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['honda']))).to eq(['accord', 'civic', 'ford']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford']))).to eq(['honda', 'mustang', 'taurus']) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["honda"]), + ), + ).to eq(%w[accord civic ford]) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["ford"]), + ), + ).to eq(%w[honda mustang taurus]) makes.update!(one_per_topic: true) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['honda']))).to eq(['accord', 'civic']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford']))).to eq(['mustang', 'taurus']) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["honda"]), + ), + ).to eq(%w[accord civic]) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["ford"]), + ), + ).to eq(%w[mustang taurus]) honda_group.update!(one_per_topic: true) ford_group.update!(one_per_topic: true) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['honda']))).to eq(['accord', 'civic']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford']))).to eq(['mustang', 'taurus']) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["honda"]), + ), + ).to eq(%w[accord civic]) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["ford"]), + ), + ).to eq(%w[mustang taurus]) car_category.update!(allow_global_tags: true) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: car_category), - ['ford', 'honda', tag1, tag2, tag3, tag4] + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: car_category), + ["ford", "honda", tag1, tag2, tag3, tag4], ) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford']), - ['mustang', 'taurus', tag1, tag2, tag3, tag4] + expect_same_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["ford"]), + ["mustang", "taurus", tag1, tag2, tag3, tag4], ) - expect_same_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford', 'mustang']), - [tag1, tag2, tag3, tag4] + expect_same_tag_names( + filter_allowed_tags( + for_input: true, + category: car_category, + selected_tags: %w[ford mustang], + ), + [tag1, tag2, tag3, tag4], ) end it "can apply the tags to a topic" do - post = create_post(category: car_category, tags: ['ford', 'mustang']) - expect(post.topic.tags.map(&:name).sort).to eq(['ford', 'mustang']) + post = create_post(category: car_category, tags: %w[ford mustang]) + expect(post.topic.tags.map(&:name).sort).to eq(%w[ford mustang]) end context "with limit one tag from each group" do @@ -402,24 +708,51 @@ RSpec.describe "category tag restrictions" do end it "can restrict one tag from each group" do - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category))).to eq(['ford', 'honda']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['honda']))).to eq(['accord', 'civic']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford']))).to eq(['mustang', 'taurus']) - expect(sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['ford', 'mustang']))).to eq([]) + expect( + sorted_tag_names(filter_allowed_tags(for_input: true, category: car_category)), + ).to eq(%w[ford honda]) + expect( + sorted_tag_names( + filter_allowed_tags( + for_input: true, + category: car_category, + selected_tags: ["honda"], + ), + ), + ).to eq(%w[accord civic]) + expect( + sorted_tag_names( + filter_allowed_tags(for_input: true, category: car_category, selected_tags: ["ford"]), + ), + ).to eq(%w[mustang taurus]) + expect( + sorted_tag_names( + filter_allowed_tags( + for_input: true, + category: car_category, + selected_tags: %w[ford mustang], + ), + ), + ).to eq([]) end it "can apply the tags to a topic" do - post = create_post(category: car_category, tags: ['ford', 'mustang']) - expect(post.topic.tags.map(&:name).sort).to eq(['ford', 'mustang']) + post = create_post(category: car_category, tags: %w[ford mustang]) + expect(post.topic.tags.map(&:name).sort).to eq(%w[ford mustang]) end it "can remove extra tags from the same group" do # A weird case that input field wouldn't allow. # Only one tag from car makers is allowed, but we're saying that two have been selected. - names = filter_allowed_tags(for_input: true, category: car_category, selected_tags: ['honda', 'ford']).map(&:name) - expect(names.include?('honda') || names.include?('ford')).to eq(false) - expect(names).to include('civic') - expect(names).to include('mustang') + names = + filter_allowed_tags( + for_input: true, + category: car_category, + selected_tags: %w[honda ford], + ).map(&:name) + expect(names.include?("honda") || names.include?("ford")).to eq(false) + expect(names).to include("civic") + expect(names).to include("mustang") end end end @@ -441,9 +774,9 @@ RSpec.describe "tag topic counts per category" do end it "counts when a topic is created with tags" do - expect { - Fabricate(:topic, category: category, tags: [tag1, tag2]) - }.to change { CategoryTagStat.count }.by(2) + expect { Fabricate(:topic, category: category, tags: [tag1, tag2]) }.to change { + CategoryTagStat.count + }.by(2) expect(CategoryTagStat.where(category: category, tag: tag1).sum(:topic_count)).to eq(1) expect(CategoryTagStat.where(category: category, tag: tag2).sum(:topic_count)).to eq(1) end @@ -461,7 +794,7 @@ RSpec.describe "tag topic counts per category" do context "with topic with 2 tags" do fab!(:topic) { Fabricate(:topic, category: category, tags: [tag1, tag2]) } - fab!(:post) { Fabricate(:post, user: topic.user, topic: topic) } + fab!(:post) { Fabricate(:post, user: topic.user, topic: topic) } it "has correct counts after tag is removed from a topic" do post @@ -473,7 +806,12 @@ RSpec.describe "tag topic counts per category" do end it "has correct counts after a topic's category changes" do - PostRevisor.new(post).revise!(topic.user, category_id: category2.id, raw: post.raw, tags: [tag1.name, tag2.name]) + PostRevisor.new(post).revise!( + topic.user, + category_id: category2.id, + raw: post.raw, + tags: [tag1.name, tag2.name], + ) expect(CategoryTagStat.where(category: category, tag: tag1).sum(:topic_count)).to eq(0) expect(CategoryTagStat.where(category: category, tag: tag2).sum(:topic_count)).to eq(0) expect(CategoryTagStat.where(category: category2, tag: tag1).sum(:topic_count)).to eq(1) @@ -481,7 +819,12 @@ RSpec.describe "tag topic counts per category" do end it "has correct counts after topic's category AND tags changed" do - PostRevisor.new(post).revise!(topic.user, raw: post.raw, tags: [tag2.name, tag3.name], category_id: category2.id) + PostRevisor.new(post).revise!( + topic.user, + raw: post.raw, + tags: [tag2.name, tag3.name], + category_id: category2.id, + ) expect(CategoryTagStat.where(category: category, tag: tag1).sum(:topic_count)).to eq(0) expect(CategoryTagStat.where(category: category, tag: tag2).sum(:topic_count)).to eq(0) expect(CategoryTagStat.where(category: category, tag: tag3).sum(:topic_count)).to eq(0) @@ -496,8 +839,18 @@ RSpec.describe "tag topic counts per category" do fab!(:post) { Fabricate(:post, user: topic.user, topic: topic) } it "counts after topic becomes uncategorized" do - PostRevisor.new(post).revise!(topic.user, raw: post.raw, tags: [tag1.name], category_id: SiteSetting.uncategorized_category_id) - expect(CategoryTagStat.where(category: Category.find(SiteSetting.uncategorized_category_id), tag: tag1).sum(:topic_count)).to eq(1) + PostRevisor.new(post).revise!( + topic.user, + raw: post.raw, + tags: [tag1.name], + category_id: SiteSetting.uncategorized_category_id, + ) + expect( + CategoryTagStat.where( + category: Category.find(SiteSetting.uncategorized_category_id), + tag: tag1, + ).sum(:topic_count), + ).to eq(1) expect(CategoryTagStat.where(category: category, tag: tag1).sum(:topic_count)).to eq(0) end diff --git a/spec/integration/content_security_policy_spec.rb b/spec/integration/content_security_policy_spec.rb index 69a56acc65..658a50bb7a 100644 --- a/spec/integration/content_security_policy_spec.rb +++ b/spec/integration/content_security_policy_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'content security policy integration' do - +RSpec.describe "content security policy integration" do it "adds the csp headers correctly" do SiteSetting.content_security_policy = false get "/" @@ -15,8 +14,10 @@ RSpec.describe 'content security policy integration' do context "with different hostnames" do before do SiteSetting.content_security_policy = true - RailsMultisite::ConnectionManagement.stubs(:current_db_hostnames).returns(['primary.example.com', 'secondary.example.com']) - RailsMultisite::ConnectionManagement.stubs(:current_hostname).returns('primary.example.com') + RailsMultisite::ConnectionManagement.stubs(:current_db_hostnames).returns( + %w[primary.example.com secondary.example.com], + ) + RailsMultisite::ConnectionManagement.stubs(:current_hostname).returns("primary.example.com") end it "works with the primary domain" do @@ -52,5 +53,4 @@ RSpec.describe 'content security policy integration' do expect(response.headers["Content-Security-Policy"]).to include("https://test.localhost") end end - end diff --git a/spec/integration/discord_omniauth_spec.rb b/spec/integration/discord_omniauth_spec.rb index 0ca80d1c96..230459a885 100644 --- a/spec/integration/discord_omniauth_spec.rb +++ b/spec/integration/discord_omniauth_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe 'Discord OAuth2' do +describe "Discord OAuth2" do let(:access_token) { "discord_access_token_448" } let(:client_id) { "abcdef11223344" } let(:client_secret) { "adddcccdddd99922" } @@ -9,15 +9,14 @@ describe 'Discord OAuth2' do fab!(:user1) { Fabricate(:user) } def setup_discord_email_stub(email, verified:) - stub_request(:get, "https://discord.com/api/users/@me") - .with( - headers: { - "Authorization" => "Bearer #{access_token}" - } - ) - .to_return( - status: 200, - body: JSON.dump( + stub_request(:get, "https://discord.com/api/users/@me").with( + headers: { + "Authorization" => "Bearer #{access_token}", + }, + ).to_return( + status: 200, + body: + JSON.dump( id: "80351110224678912", username: "Nelly", discriminator: "1337", @@ -26,14 +25,14 @@ describe 'Discord OAuth2' do email: email, flags: 64, banner: "06c16474723fe537c283b8efa61a30c8", - accent_color: 16711680, + accent_color: 16_711_680, premium_type: 1, - public_flags: 64 + public_flags: 64, ), - headers: { - "Content-Type" => "application/json" - } - ) + headers: { + "Content-Type" => "application/json", + }, + ) end before do @@ -41,50 +40,49 @@ describe 'Discord OAuth2' do SiteSetting.discord_client_id = client_id SiteSetting.discord_secret = client_secret - stub_request(:post, "https://discord.com/api/oauth2/token") - .with( - body: hash_including( + stub_request(:post, "https://discord.com/api/oauth2/token").with( + body: + hash_including( "client_id" => client_id, "client_secret" => client_secret, "code" => temp_code, "grant_type" => "authorization_code", - "redirect_uri" => "http://test.localhost/auth/discord/callback" - ) - ) - .to_return( - status: 200, - body: Rack::Utils.build_query( + "redirect_uri" => "http://test.localhost/auth/discord/callback", + ), + ).to_return( + status: 200, + body: + Rack::Utils.build_query( access_token: access_token, scope: "identify emails guilds", token_type: "Bearer", - expires_in: 604800, + expires_in: 604_800, refresh_token: "D43f5y0ahjqew82jZ4NViEr2YafMKhue", ), - headers: { - "Content-Type" => "application/x-www-form-urlencoded" - } - ) + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + ) - stub_request(:get, "https://discord.com/api/users/@me/guilds") - .with( - headers: { - "Authorization" => "Bearer #{access_token}" - } - ) - .to_return( - status: 200, - body: JSON.dump( + stub_request(:get, "https://discord.com/api/users/@me/guilds").with( + headers: { + "Authorization" => "Bearer #{access_token}", + }, + ).to_return( + status: 200, + body: + JSON.dump( id: "80351110224678912", name: "1337 Krew", icon: "8342729096ea3675442027381ff50dfe", owner: true, permissions: "36953089", - features: ["COMMUNITY", "NEWS"] + features: %w[COMMUNITY NEWS], ), - headers: { - "Content-Type" => "application/json" - } - ) + headers: { + "Content-Type" => "application/json", + }, + ) end it "doesn't sign in anyone if the email from discord is not verified" do @@ -94,10 +92,7 @@ describe 'Discord OAuth2' do setup_discord_email_stub(user1.email, verified: false) - post "/auth/discord/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/discord/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") @@ -111,10 +106,7 @@ describe 'Discord OAuth2' do setup_discord_email_stub(user1.email, verified: true) - post "/auth/discord/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/discord/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") diff --git a/spec/integration/email_style_spec.rb b/spec/integration/email_style_spec.rb index a53cff48e2..1b6aa37a7d 100644 --- a/spec/integration/email_style_spec.rb +++ b/spec/integration/email_style_spec.rb @@ -6,14 +6,14 @@ RSpec.describe EmailStyle do SiteSetting.email_custom_template = "%{email_content}<%= (111 * 333) %>" html = Email::Renderer.new(UserNotifications.signup(Fabricate(:user))).html expect(html).not_to include("36963") - expect(html).to include('') + expect(html).to include("") end end context "with a custom template" do before do SiteSetting.email_custom_template = "

    FOR YOU

    %{email_content}
    " - SiteSetting.email_custom_css = 'h1 { color: red; } div.body { color: #FAB; }' + SiteSetting.email_custom_css = "h1 { color: red; } div.body { color: #FAB; }" SiteSetting.email_custom_css_compiled = SiteSetting.email_custom_css end @@ -22,36 +22,36 @@ RSpec.describe EmailStyle do SiteSetting.remove_override!(:email_custom_css) end - context 'with invite' do + context "with invite" do fab!(:invite) { Fabricate(:invite) } let(:invite_mail) { InviteMailer.send_invite(invite) } subject(:mail_html) { Email::Renderer.new(invite_mail).html } - it 'applies customizations' do + it "applies customizations" do expect(mail_html.scan('

    FOR YOU

    ').count).to eq(1) expect(mail_html).to match("#{Discourse.base_url}/invites/#{invite.invite_key}") end - it 'applies customizations if compiled is missing' do + it "applies customizations if compiled is missing" do SiteSetting.remove_override!(:email_custom_css_compiled) expect(mail_html.scan('

    FOR YOU

    ').count).to eq(1) expect(mail_html).to match("#{Discourse.base_url}/invites/#{invite.invite_key}") end - it 'can apply RTL attrs' do - SiteSetting.default_locale = 'he' + it "can apply RTL attrs" do + SiteSetting.default_locale = "he" body_attrs = mail_html.match(/])+/) expect(body_attrs[0]&.downcase).to match(/text-align:\s*right/) expect(body_attrs[0]&.downcase).to include('dir="rtl"') end end - context 'when user_replied' do + context "when user_replied" do let(:response_by_user) { Fabricate(:user, name: "John Doe") } - let(:category) { Fabricate(:category, name: 'India') } + let(:category) { Fabricate(:category, name: "India") } let(:topic) { Fabricate(:topic, category: category, title: "Super cool topic") } - let(:post) { Fabricate(:post, topic: topic, raw: 'This is My super duper cool topic') } + let(:post) { Fabricate(:post, topic: topic, raw: "This is My super duper cool topic") } let(:response) { Fabricate(:basic_reply, topic: post.topic, user: response_by_user) } let(:user) { Fabricate(:user) } let(:notification) { Fabricate(:replied_notification, user: user, post: response) } @@ -61,7 +61,7 @@ RSpec.describe EmailStyle do user, post: response, notification_type: notification.notification_type, - notification_data_hash: notification.data_hash + notification_data_hash: notification.data_hash, ) end @@ -72,42 +72,45 @@ RSpec.describe EmailStyle do expect(mail_html.scan('

    FOR YOU

    ').count).to eq(1) matches = mail_html.match(/
    #{post.raw}/) - expect(matches[1]).to include('color: #FAB;') # custom - expect(matches[1]).to include('padding-top:5px;') # div.body + expect(matches[1]).to include("color: #FAB;") # custom + expect(matches[1]).to include("padding-top:5px;") # div.body end # TODO: translation override end - context 'with signup' do + context "with signup" do let(:signup_mail) { UserNotifications.signup(Fabricate(:user)) } subject(:mail_html) { Email::Renderer.new(signup_mail).html } it "customizations are applied to html part of emails" do expect(mail_html.scan('

    FOR YOU

    ').count).to eq(1) - expect(mail_html).to include('activate-account') + expect(mail_html).to include("activate-account") end - context 'with translation override' do + context "with translation override" do before do TranslationOverride.upsert!( SiteSetting.default_locale, - 'user_notifications.signup.text_body_template', - "CLICK THAT LINK: %{base_url}/u/activate-account/%{email_token}" + "user_notifications.signup.text_body_template", + "CLICK THAT LINK: %{base_url}/u/activate-account/%{email_token}", ) end after do - TranslationOverride.revert!(SiteSetting.default_locale, ['user_notifications.signup.text_body_template']) + TranslationOverride.revert!( + SiteSetting.default_locale, + ["user_notifications.signup.text_body_template"], + ) end it "applies customizations when translation override exists" do expect(mail_html.scan('

    FOR YOU

    ').count).to eq(1) - expect(mail_html.scan('CLICK THAT LINK').count).to eq(1) + expect(mail_html.scan("CLICK THAT LINK").count).to eq(1) end end - context 'with some bad css' do + context "with some bad css" do before do SiteSetting.email_custom_css = '@import "nope.css"; h1 {{{ size: really big; ' SiteSetting.email_custom_css_compiled = SiteSetting.email_custom_css @@ -115,13 +118,15 @@ RSpec.describe EmailStyle do it "can render the html" do expect(mail_html.scan(/FOR YOU<\/h1>/).count).to eq(1) - expect(mail_html).to include('activate-account') + expect(mail_html).to include("activate-account") end end end - context 'with digest' do - fab!(:popular_topic) { Fabricate(:topic, user: Fabricate(:coding_horror), created_at: 1.hour.ago) } + context "with digest" do + fab!(:popular_topic) do + Fabricate(:topic, user: Fabricate(:coding_horror), created_at: 1.hour.ago) + end let(:summary_email) { UserNotifications.digest(Fabricate(:user)) } subject(:mail_html) { Email::Renderer.new(summary_email).html } @@ -133,7 +138,7 @@ RSpec.describe EmailStyle do it "doesn't apply customizations if apply_custom_styles_to_digest is disabled" do SiteSetting.apply_custom_styles_to_digest = false expect(mail_html).to_not include('

    FOR YOU

    ') - expect(mail_html).to_not include('FOR YOU') + expect(mail_html).to_not include("FOR YOU") expect(mail_html).to include(popular_topic.title) end end diff --git a/spec/integration/facebook_omniauth_spec.rb b/spec/integration/facebook_omniauth_spec.rb index e1bef76668..5f596af6ee 100644 --- a/spec/integration/facebook_omniauth_spec.rb +++ b/spec/integration/facebook_omniauth_spec.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true -describe 'Facebook OAuth2' do +describe "Facebook OAuth2" do let(:access_token) { "facebook_access_token_448" } let(:app_id) { "432489234823984" } let(:app_secret) { "adddcccdddd99922" } let(:temp_code) { "facebook_temp_code_544254" } - let(:appsecret_proof) { OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, app_secret, access_token) } + let(:appsecret_proof) do + OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, app_secret, access_token) + end fab!(:user1) { Fabricate(:user) } @@ -18,19 +20,16 @@ describe 'Facebook OAuth2' do } body[:email] = email if email - stub_request(:get, "https://graph.facebook.com/v5.0/me?appsecret_proof=#{appsecret_proof}&fields=name,first_name,last_name,email") - .with( - headers: { - "Authorization" => "OAuth #{access_token}" - } - ) - .to_return( - status: 200, - body: JSON.dump(body), - headers: { - "Content-Type" => "application/json" - } - ) + stub_request( + :get, + "https://graph.facebook.com/v5.0/me?appsecret_proof=#{appsecret_proof}&fields=name,first_name,last_name,email", + ).with(headers: { "Authorization" => "OAuth #{access_token}" }).to_return( + status: 200, + body: JSON.dump(body), + headers: { + "Content-Type" => "application/json", + }, + ) end before do @@ -38,27 +37,23 @@ describe 'Facebook OAuth2' do SiteSetting.facebook_app_id = app_id SiteSetting.facebook_app_secret = app_secret - stub_request(:post, "https://graph.facebook.com/v5.0/oauth/access_token") - .with( - body: hash_including( + stub_request(:post, "https://graph.facebook.com/v5.0/oauth/access_token").with( + body: + hash_including( "client_id" => app_id, "client_secret" => app_secret, "code" => temp_code, "grant_type" => "authorization_code", - "redirect_uri" => "http://test.localhost/auth/facebook/callback" - ) - ) - .to_return( - status: 200, - body: Rack::Utils.build_query( - access_token: access_token, - scope: "email", - token_type: "Bearer", + "redirect_uri" => "http://test.localhost/auth/facebook/callback", ), - headers: { - "Content-Type" => "application/x-www-form-urlencoded" - } - ) + ).to_return( + status: 200, + body: + Rack::Utils.build_query(access_token: access_token, scope: "email", token_type: "Bearer"), + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + ) end it "signs in the user if the API response from facebook includes an email (implies it's verified) and the email matches an existing user's" do @@ -68,10 +63,7 @@ describe 'Facebook OAuth2' do setup_facebook_email_stub(email: user1.email) - post "/auth/facebook/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/facebook/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") @@ -85,10 +77,7 @@ describe 'Facebook OAuth2' do setup_facebook_email_stub(email: nil) - post "/auth/facebook/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/facebook/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") diff --git a/spec/integration/flags_spec.rb b/spec/integration/flags_spec.rb index 7459174757..61201ca49d 100644 --- a/spec/integration/flags_spec.rb +++ b/spec/integration/flags_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe PostAction do - it "triggers the 'flag_reviewed' event when there was at least one flag" do admin = Fabricate(:admin) @@ -14,5 +13,4 @@ RSpec.describe PostAction do events = DiscourseEvent.track_events { PostDestroyer.new(admin, flagged_post).destroy } expect(events.map { |e| e[:event_name] }).to include(:flag_reviewed) end - end diff --git a/spec/integration/github_omniauth_spec.rb b/spec/integration/github_omniauth_spec.rb index 9714526d1b..05cbcdddc4 100644 --- a/spec/integration/github_omniauth_spec.rb +++ b/spec/integration/github_omniauth_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe 'GitHub Oauth2' do +describe "GitHub Oauth2" do let(:access_token) { "github_access_token_448" } let(:client_id) { "abcdef11223344" } let(:client_secret) { "adddcccdddd99922" } @@ -10,19 +10,17 @@ describe 'GitHub Oauth2' do fab!(:user2) { Fabricate(:user) } def setup_github_emails_stub(emails) - stub_request(:get, "https://api.github.com/user/emails") - .with( - headers: { - "Authorization" => "Bearer #{access_token}" - } - ) - .to_return( - status: 200, - body: JSON.dump(emails), - headers: { - "Content-Type" => "application/json" - } - ) + stub_request(:get, "https://api.github.com/user/emails").with( + headers: { + "Authorization" => "Bearer #{access_token}", + }, + ).to_return( + status: 200, + body: JSON.dump(emails), + headers: { + "Content-Type" => "application/json", + }, + ) end before do @@ -30,35 +28,34 @@ describe 'GitHub Oauth2' do SiteSetting.github_client_id = client_id SiteSetting.github_client_secret = client_secret - stub_request(:post, "https://github.com/login/oauth/access_token") - .with( - body: hash_including( + stub_request(:post, "https://github.com/login/oauth/access_token").with( + body: + hash_including( "client_id" => client_id, "client_secret" => client_secret, "code" => temp_code, - ) - ) - .to_return( - status: 200, - body: Rack::Utils.build_query( + ), + ).to_return( + status: 200, + body: + Rack::Utils.build_query( access_token: access_token, scope: "user:email", - token_type: "bearer" + token_type: "bearer", ), - headers: { - "Content-Type" => "application/x-www-form-urlencoded" - } - ) + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + ) - stub_request(:get, "https://api.github.com/user") - .with( - headers: { - "Authorization" => "Bearer #{access_token}" - } - ) - .to_return( - status: 200, - body: JSON.dump( + stub_request(:get, "https://api.github.com/user").with( + headers: { + "Authorization" => "Bearer #{access_token}", + }, + ).to_return( + status: 200, + body: + JSON.dump( login: "octocat", id: 1, node_id: "MDQ6VXNlcjE=", @@ -94,20 +91,20 @@ describe 'GitHub Oauth2' do private_gists: 81, total_private_repos: 100, owned_private_repos: 100, - disk_usage: 10000, + disk_usage: 10_000, collaborators: 8, two_factor_authentication: true, plan: { name: "Medium", space: 400, private_repos: 20, - collaborators: 0 - } + collaborators: 0, + }, ), - headers: { - "Content-Type" => "application/json" - } - ) + headers: { + "Content-Type" => "application/json", + }, + ) end it "doesn't sign in anyone if none of the emails from github are verified" do @@ -117,25 +114,12 @@ describe 'GitHub Oauth2' do setup_github_emails_stub( [ - { - email: user1.email, - primary: true, - verified: false, - visibility: "private" - }, - { - email: user2.email, - primary: false, - verified: false, - visibility: "private" - } - ] + { email: user1.email, primary: true, verified: false, visibility: "private" }, + { email: user2.email, primary: false, verified: false, visibility: "private" }, + ], ) - post "/auth/github/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/github/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") expect(session[:current_user_id]).to be_blank @@ -148,25 +132,12 @@ describe 'GitHub Oauth2' do setup_github_emails_stub( [ - { - email: user1.email, - primary: true, - verified: false, - visibility: "private" - }, - { - email: user2.email, - primary: false, - verified: true, - visibility: "private" - } - ] + { email: user1.email, primary: true, verified: false, visibility: "private" }, + { email: user2.email, primary: false, verified: true, visibility: "private" }, + ], ) - post "/auth/github/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/github/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") expect(session[:current_user_id]).to eq(user2.id) @@ -183,21 +154,13 @@ describe 'GitHub Oauth2' do email: "somerandomemail@discourse.org", primary: true, verified: true, - visibility: "private" + visibility: "private", }, - { - email: user2.email, - primary: false, - verified: false, - visibility: "private" - } - ] + { email: user2.email, primary: false, verified: false, visibility: "private" }, + ], ) - post "/auth/github/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/github/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") expect(session[:current_user_id]).to be_blank @@ -210,25 +173,12 @@ describe 'GitHub Oauth2' do setup_github_emails_stub( [ - { - email: user1.email, - primary: true, - verified: true, - visibility: "private" - }, - { - email: user2.email, - primary: false, - verified: true, - visibility: "private" - } - ] + { email: user1.email, primary: true, verified: true, visibility: "private" }, + { email: user2.email, primary: false, verified: true, visibility: "private" }, + ], ) - post "/auth/github/callback", params: { - state: session["omniauth.state"], - code: temp_code - } + post "/auth/github/callback", params: { state: session["omniauth.state"], code: temp_code } expect(response.status).to eq(302) expect(response.location).to eq("http://test.localhost/") expect(session[:current_user_id]).to eq(user1.id) diff --git a/spec/integration/group_spec.rb b/spec/integration/group_spec.rb index 0573053b68..c9bf71e206 100644 --- a/spec/integration/group_spec.rb +++ b/spec/integration/group_spec.rb @@ -6,18 +6,20 @@ RSpec.describe Group do :group, visibility_level: Group.visibility_levels[:public], mentionable_level: Group::ALIAS_LEVELS[:nobody], - users: [ Fabricate(:user) ] + users: [Fabricate(:user)], ) end let(:post) { Fabricate(:post, raw: "mention @#{group.name}") } - before do - Jobs.run_immediately! - end + before { Jobs.run_immediately! } - it 'users can mention public groups, but does not create a notification' do - expect { post }.not_to change { Notification.where(notification_type: Notification.types[:group_mentioned]).count } - expect(post.cooked).to include("@#{group.name}") + it "users can mention public groups, but does not create a notification" do + expect { post }.not_to change { + Notification.where(notification_type: Notification.types[:group_mentioned]).count + } + expect(post.cooked).to include( + "@#{group.name}", + ) end end diff --git a/spec/integration/invalid_request_spec.rb b/spec/integration/invalid_request_spec.rb index e3e4e0bad7..6d19bcb9c6 100644 --- a/spec/integration/invalid_request_spec.rb +++ b/spec/integration/invalid_request_spec.rb @@ -1,27 +1,31 @@ # frozen_string_literal: true -RSpec.describe 'invalid requests', type: :request do +RSpec.describe "invalid requests", type: :request do before do @orig_logger = Rails.logger Rails.logger = @fake_logger = FakeLogger.new end - after do - Rails.logger = @orig_logger - end + after { Rails.logger = @orig_logger } it "handles NotFound with invalid json body" do - post "/latest.json", params: "{some: malformed: json", headers: { "content-type" => "application/json" } + post "/latest.json", + params: "{some: malformed: json", + headers: { + "content-type" => "application/json", + } expect(response.status).to eq(404) expect(@fake_logger.warnings.length).to eq(0) expect(@fake_logger.errors.length).to eq(0) end it "handles EOFError when multipart request is malformed" do - post "/latest.json", params: "somecontent", headers: { - "content-type" => "multipart/form-data; boundary=abcde", - "content-length" => "1" - } + post "/latest.json", + params: "somecontent", + headers: { + "content-type" => "multipart/form-data; boundary=abcde", + "content-length" => "1", + } expect(response.status).to eq(400) expect(@fake_logger.warnings.length).to eq(0) expect(@fake_logger.errors.length).to eq(0) @@ -33,5 +37,4 @@ RSpec.describe 'invalid requests', type: :request do expect(@fake_logger.warnings.length).to eq(0) expect(@fake_logger.errors.length).to eq(0) end - end diff --git a/spec/integration/invite_only_registration_spec.rb b/spec/integration/invite_only_registration_spec.rb index a30869094e..1ea4a9c1ec 100644 --- a/spec/integration/invite_only_registration_spec.rb +++ b/spec/integration/invite_only_registration_spec.rb @@ -1,45 +1,46 @@ # encoding: UTF-8 # frozen_string_literal: true -RSpec.describe 'invite only' do - - describe '#create invite only' do - it 'can create user via API' do - +RSpec.describe "invite only" do + describe "#create invite only" do + it "can create user via API" do SiteSetting.invite_only = true Jobs.run_immediately! admin = Fabricate(:admin) api_key = Fabricate(:api_key, user: admin) - post '/users.json', params: { - name: 'bob', - username: 'bob', - password: 'strongpassword', - email: 'bob@bob.com', - }, headers: { - HTTP_API_KEY: api_key.key, - HTTP_API_USERNAME: admin.username - } + post "/users.json", + params: { + name: "bob", + username: "bob", + password: "strongpassword", + email: "bob@bob.com", + }, + headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username, + } user_id = response.parsed_body["user_id"] expect(user_id).to be > 0 # activate and approve - put "/admin/users/#{user_id}/activate.json", headers: { - HTTP_API_KEY: api_key.key, - HTTP_API_USERNAME: admin.username - } + put "/admin/users/#{user_id}/activate.json", + headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username, + } - put "/admin/users/#{user_id}/approve.json", headers: { - HTTP_API_KEY: api_key.key, - HTTP_API_USERNAME: admin.username - } + put "/admin/users/#{user_id}/approve.json", + headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username, + } u = User.find(user_id) expect(u.active).to eq(true) expect(u.approved).to eq(true) - end end end diff --git a/spec/integration/message_bus_spec.rb b/spec/integration/message_bus_spec.rb index a2f961ddc5..ca814fa8ff 100644 --- a/spec/integration/message_bus_spec.rb +++ b/spec/integration/message_bus_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'message bus integration' do - +RSpec.describe "message bus integration" do it "allows anonymous requests to the messagebus" do post "/message-bus/poll" expect(response.status).to eq(200) @@ -27,5 +26,4 @@ RSpec.describe 'message bus integration' do expect(response.status).to eq(200) end end - end diff --git a/spec/integration/multisite_cookies_spec.rb b/spec/integration/multisite_cookies_spec.rb index 15bad1c74b..9a24b4d0e2 100644 --- a/spec/integration/multisite_cookies_spec.rb +++ b/spec/integration/multisite_cookies_spec.rb @@ -1,19 +1,25 @@ # frozen_string_literal: true -RSpec.describe 'multisite', type: [:multisite, :request] do +RSpec.describe "multisite", type: %i[multisite request] do it "works" do get "http://test.localhost/session/csrf.json" expect(response.status).to eq(200) cookie = CGI.escape(response.cookies["_forum_session"]) id1 = session["session_id"] - get "http://test.localhost/session/csrf.json", headers: { "Cookie" => "_forum_session=#{cookie};" } + get "http://test.localhost/session/csrf.json", + headers: { + "Cookie" => "_forum_session=#{cookie};", + } expect(response.status).to eq(200) id2 = session["session_id"] expect(id1).to eq(id2) - get "http://test2.localhost/session/csrf.json", headers: { "Cookie" => "_forum_session=#{cookie};" } + get "http://test2.localhost/session/csrf.json", + headers: { + "Cookie" => "_forum_session=#{cookie};", + } expect(response.status).to eq(200) id3 = session["session_id"] diff --git a/spec/integration/multisite_spec.rb b/spec/integration/multisite_spec.rb index d53c443560..a1f27013f6 100644 --- a/spec/integration/multisite_spec.rb +++ b/spec/integration/multisite_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'multisite', type: [:multisite, :request] do +RSpec.describe "multisite", type: %i[multisite request] do it "should always allow /srv/status through" do get "http://unknown.com/srv/status" expect(response.status).to eq(200) @@ -13,10 +13,12 @@ RSpec.describe 'multisite', type: [:multisite, :request] do end it "should hit correct site otherwise" do - site_1_url = Fabricate(:topic, title: "Site 1 Topic Title", user: Discourse.system_user).relative_url + site_1_url = + Fabricate(:topic, title: "Site 1 Topic Title", user: Discourse.system_user).relative_url - test_multisite_connection('second') do - site_2_url = Fabricate(:topic, title: "Site 2 Topic Title", user: Discourse.system_user).relative_url + test_multisite_connection("second") do + site_2_url = + Fabricate(:topic, title: "Site 2 Topic Title", user: Discourse.system_user).relative_url get "http://test.localhost/#{site_1_url}.json" expect(request.env["RAILS_MULTISITE_HOST"]).to eq("test.localhost") diff --git a/spec/integration/rate_limiting_spec.rb b/spec/integration/rate_limiting_spec.rb index ebf24e909b..cdda0a8a75 100644 --- a/spec/integration/rate_limiting_spec.rb +++ b/spec/integration/rate_limiting_spec.rb @@ -1,8 +1,7 @@ # encoding: UTF-8 # frozen_string_literal: true -RSpec.describe 'rate limiter integration' do - +RSpec.describe "rate limiter integration" do before do RateLimiter.enable RateLimiter.clear_all! @@ -13,12 +12,13 @@ RSpec.describe 'rate limiter integration' do global_setting :reject_message_bus_queue_seconds, 0.1 - post "/message-bus/#{SecureRandom.hex}/poll", headers: { - "HTTP_X_REQUEST_START" => "t=#{Time.now.to_f - 0.2}" - } + post "/message-bus/#{SecureRandom.hex}/poll", + headers: { + "HTTP_X_REQUEST_START" => "t=#{Time.now.to_f - 0.2}", + } expect(response.status).to eq(429) - expect(response.headers['Retry-After'].to_i).to be > 29 + expect(response.headers["Retry-After"].to_i).to be > 29 end it "will not rate limit when all is good" do @@ -26,9 +26,10 @@ RSpec.describe 'rate limiter integration' do global_setting :reject_message_bus_queue_seconds, 0.1 - post "/message-bus/#{SecureRandom.hex}/poll", headers: { - "HTTP_X_REQUEST_START" => "t=#{Time.now.to_f - 0.05}" - } + post "/message-bus/#{SecureRandom.hex}/poll", + headers: { + "HTTP_X_REQUEST_START" => "t=#{Time.now.to_f - 0.05}", + } expect(response.status).to eq(200) end @@ -37,15 +38,15 @@ RSpec.describe 'rate limiter integration' do name = Auth::DefaultCurrentUserProvider::TOKEN_COOKIE # we try 11 times because the rate limit is 10 - 11.times { + 11.times do cookies[name] = SecureRandom.hex - get '/categories.json' + get "/categories.json" expect(response.cookies.has_key?(name)).to eq(true) expect(response.cookies[name]).to be_nil - } + end end - it 'can cleanly limit requests and sets a Retry-After header' do + it "can cleanly limit requests and sets a Retry-After header" do freeze_time RateLimiter.clear_all! @@ -55,17 +56,19 @@ RSpec.describe 'rate limiter integration' do global_setting :max_admin_api_reqs_per_minute, 1 - get '/admin/api/keys.json', headers: { - HTTP_API_KEY: api_key.key, - HTTP_API_USERNAME: admin.username - } + get "/admin/api/keys.json", + headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username, + } expect(response.status).to eq(200) - get '/admin/api/keys.json', headers: { - HTTP_API_KEY: api_key.key, - HTTP_API_USERNAME: admin.username - } + get "/admin/api/keys.json", + headers: { + HTTP_API_KEY: api_key.key, + HTTP_API_USERNAME: admin.username, + } expect(response.status).to eq(429) diff --git a/spec/integration/request_tracker_spec.rb b/spec/integration/request_tracker_spec.rb index 239d4155d3..bec86c245b 100644 --- a/spec/integration/request_tracker_spec.rb +++ b/spec/integration/request_tracker_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -RSpec.describe 'request tracker' do +RSpec.describe "request tracker" do let(:api_key) do Fabricate( :api_key, user: Fabricate.build(:user), - api_key_scopes: [ApiKeyScope.new(resource: 'users', action: 'show')] + api_key_scopes: [ApiKeyScope.new(resource: "users", action: "show")], ) end let(:user_api_key) do - Fabricate(:user_api_key, scopes: [Fabricate.build(:user_api_key_scope, name: 'session_info')]) + Fabricate(:user_api_key, scopes: [Fabricate.build(:user_api_key_scope, name: "session_info")]) end before do @@ -25,8 +25,8 @@ RSpec.describe 'request tracker' do CachedCounting.disable end - context 'when using an api key' do - it 'is counted as an API request' do + context "when using an api key" do + it "is counted as an API request" do get "/u/#{api_key.user.username}.json", headers: { HTTP_API_KEY: api_key.key } expect(response.status).to eq(200) @@ -37,9 +37,9 @@ RSpec.describe 'request tracker' do end end - context 'when using an user api key' do - it 'is counted as a user API request' do - get '/session/current.json', headers: { HTTP_USER_API_KEY: user_api_key.key } + context "when using an user api key" do + it "is counted as a user API request" do + get "/session/current.json", headers: { HTTP_USER_API_KEY: user_api_key.key } expect(response.status).to eq(200) CachedCounting.flush diff --git a/spec/integration/same_ip_spammers_spec.rb b/spec/integration/same_ip_spammers_spec.rb index a586891cf4..2d3f928920 100644 --- a/spec/integration/same_ip_spammers_spec.rb +++ b/spec/integration/same_ip_spammers_spec.rb @@ -2,45 +2,41 @@ # frozen_string_literal: true RSpec.describe "spammers on same IP" do - let(:ip_address) { '182.189.119.174' } - let!(:spammer1) { Fabricate(:user, ip_address: ip_address) } - let!(:spammer2) { Fabricate(:user, ip_address: ip_address) } - let(:spammer3) { Fabricate(:user, ip_address: ip_address) } + let(:ip_address) { "182.189.119.174" } + let!(:spammer1) { Fabricate(:user, ip_address: ip_address) } + let!(:spammer2) { Fabricate(:user, ip_address: ip_address) } + let(:spammer3) { Fabricate(:user, ip_address: ip_address) } - context 'when flag_sockpuppets is disabled' do - let!(:first_post) { create_post(user: spammer1) } - let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } + context "when flag_sockpuppets is disabled" do + let!(:first_post) { create_post(user: spammer1) } + let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } - it 'should not increase spam count' do - expect(first_post.reload.spam_count).to eq(0) + it "should not increase spam count" do + expect(first_post.reload.spam_count).to eq(0) expect(second_post.reload.spam_count).to eq(0) end end - context 'when flag_sockpuppets is enabled' do - before do - SiteSetting.flag_sockpuppets = true - end + context "when flag_sockpuppets is enabled" do + before { SiteSetting.flag_sockpuppets = true } - after do - SiteSetting.flag_sockpuppets = false - end + after { SiteSetting.flag_sockpuppets = false } - context 'when first spammer starts a topic' do + context "when first spammer starts a topic" do let!(:first_post) { create_post(user: spammer1) } - context 'when second spammer replies' do - let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } + context "when second spammer replies" do + let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } - it 'should increase spam count' do + it "should increase spam count" do expect(first_post.reload.spam_count).to eq(1) expect(second_post.reload.spam_count).to eq(1) end - context 'with third spam post' do + context "with third spam post" do let!(:third_post) { create_post(user: spammer3, topic: first_post.topic) } - it 'should increase spam count' do + it "should increase spam count" do expect(first_post.reload.spam_count).to eq(1) expect(second_post.reload.spam_count).to eq(1) expect(third_post.reload.spam_count).to eq(1) @@ -49,16 +45,18 @@ RSpec.describe "spammers on same IP" do end end - context 'when first user is not new' do - let!(:old_user) { Fabricate(:user, ip_address: ip_address, created_at: 2.days.ago, trust_level: TrustLevel[1]) } + context "when first user is not new" do + let!(:old_user) do + Fabricate(:user, ip_address: ip_address, created_at: 2.days.ago, trust_level: TrustLevel[1]) + end - context 'when first user starts a topic' do + context "when first user starts a topic" do let!(:first_post) { create_post(user: old_user) } - context 'with a reply by a new user at the same IP address' do - let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } + context "with a reply by a new user at the same IP address" do + let!(:second_post) { create_post(user: spammer2, topic: first_post.topic) } - it 'should increase the spam count correctly' do + it "should increase the spam count correctly" do expect(first_post.reload.spam_count).to eq(0) expect(second_post.reload.spam_count).to eq(1) end diff --git a/spec/integration/spam_rules_spec.rb b/spec/integration/spam_rules_spec.rb index edefab13de..9c1f2c9f78 100644 --- a/spec/integration/spam_rules_spec.rb +++ b/spec/integration/spam_rules_spec.rb @@ -2,11 +2,11 @@ # frozen_string_literal: true RSpec.describe "spam rules for users" do - describe 'auto-silence users based on flagging' do - fab!(:admin) { Fabricate(:admin) } # needed to send a system message + describe "auto-silence users based on flagging" do + fab!(:admin) { Fabricate(:admin) } # needed to send a system message fab!(:moderator) { Fabricate(:moderator) } - fab!(:user1) { Fabricate(:user) } - fab!(:user2) { Fabricate(:user) } + fab!(:user1) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } before do SiteSetting.hide_post_sensitivity = Reviewable.sensitivities[:disabled] @@ -15,23 +15,21 @@ RSpec.describe "spam rules for users" do SiteSetting.num_users_to_silence_new_user = 2 end - context 'when spammer is a new user' do - fab!(:spammer) { Fabricate(:user, trust_level: TrustLevel[0]) } + context "when spammer is a new user" do + fab!(:spammer) { Fabricate(:user, trust_level: TrustLevel[0]) } - context 'when spammer post is not flagged enough times' do - let!(:spam_post) { create_post(user: spammer) } + context "when spammer post is not flagged enough times" do + let!(:spam_post) { create_post(user: spammer) } let!(:spam_post2) { create_post(user: spammer) } - before do - PostActionCreator.create(user1, spam_post, :spam) - end + before { PostActionCreator.create(user1, spam_post, :spam) } - it 'should not hide the post' do + it "should not hide the post" do expect(spam_post.reload).to_not be_hidden end - context 'when spam posts are flagged enough times, but not by enough users' do - it 'should not hide the post' do + context "when spam posts are flagged enough times, but not by enough users" do + it "should not hide the post" do PostActionCreator.create(user1, spam_post2, :spam) expect(spam_post.reload).to_not be_hidden @@ -40,16 +38,30 @@ RSpec.describe "spam rules for users" do end end - context 'when one spam post is flagged enough times by enough users' do + context "when one spam post is flagged enough times by enough users" do fab!(:another_topic) { Fabricate(:topic) } let!(:private_messages_count) { spammer.private_topics_count } let!(:mod_pm_count) { moderator.private_topics_count } let!(:reviewable) { PostActionCreator.spam(user2, spam_post).reviewable } - it 'should hide the posts' do + it "should hide the posts" do expect(Guardian.new(spammer).can_create_topic?(nil)).to be(false) - expect { PostCreator.create(spammer, title: 'limited time offer for you', raw: 'better buy this stuff ok', archetype_id: 1) }.to raise_error(Discourse::InvalidAccess) - expect(PostCreator.create(spammer, topic_id: another_topic.id, raw: 'my reply is spam in your topic', archetype_id: 1)).to eq(nil) + expect { + PostCreator.create( + spammer, + title: "limited time offer for you", + raw: "better buy this stuff ok", + archetype_id: 1, + ) + }.to raise_error(Discourse::InvalidAccess) + expect( + PostCreator.create( + spammer, + topic_id: another_topic.id, + raw: "my reply is spam in your topic", + archetype_id: 1, + ), + ).to eq(nil) expect(spammer.reload).to be_silenced expect(spam_post.reload).to be_hidden expect(spam_post2.reload).to be_hidden @@ -57,22 +69,24 @@ RSpec.describe "spam rules for users" do end context "when a post is deleted" do - it 'should silence the spammer' do - spam_post.trash!(moderator); spammer.reload + it "should silence the spammer" do + spam_post.trash!(moderator) + spammer.reload expect(spammer.reload).to be_silenced end end context "when spammer becomes trust level 1" do - it 'should silence the spammer' do - spammer.change_trust_level!(TrustLevel[1]); spammer.reload + it "should silence the spammer" do + spammer.change_trust_level!(TrustLevel[1]) + spammer.reload expect(spammer.reload).to be_silenced end end end - context 'with hide_post_sensitivity' do - it 'should silence the spammer' do + context "with hide_post_sensitivity" do + it "should silence the spammer" do Reviewable.set_priorities(high: 2.0) SiteSetting.hide_post_sensitivity = Reviewable.sensitivities[:low] PostActionCreator.create(user2, spam_post, :spam) @@ -84,19 +98,26 @@ RSpec.describe "spam rules for users" do end context "when spammer has trust level basic" do - let(:spammer) { Fabricate(:user, trust_level: TrustLevel[1]) } + let(:spammer) { Fabricate(:user, trust_level: TrustLevel[1]) } - context 'when one spam post is flagged enough times by enough users' do - let!(:spam_post) { Fabricate(:post, user: spammer) } + context "when one spam post is flagged enough times by enough users" do + let!(:spam_post) { Fabricate(:post, user: spammer) } let!(:private_messages_count) { spammer.private_topics_count } - it 'should not allow spammer to create new posts' do + it "should not allow spammer to create new posts" do PostActionCreator.create(user1, spam_post, :spam) PostActionCreator.create(user2, spam_post, :spam) expect(spam_post.reload).to_not be_hidden expect(Guardian.new(spammer).can_create_topic?(nil)).to be(true) - expect { PostCreator.create(spammer, title: 'limited time offer for you', raw: 'better buy this stuff ok', archetype_id: 1) }.to_not raise_error + expect { + PostCreator.create( + spammer, + title: "limited time offer for you", + raw: "better buy this stuff ok", + archetype_id: 1, + ) + }.to_not raise_error expect(spammer.reload.private_topics_count).to eq(private_messages_count) end end @@ -104,11 +125,11 @@ RSpec.describe "spam rules for users" do [[:user, trust_level: TrustLevel[2]], [:admin], [:moderator]].each do |spammer_args| context "spammer is trusted #{spammer_args[0]}" do - let!(:spammer) { Fabricate(*spammer_args) } - let!(:spam_post) { Fabricate(:post, user: spammer) } + let!(:spammer) { Fabricate(*spammer_args) } + let!(:spam_post) { Fabricate(:post, user: spammer) } let!(:private_messages_count) { spammer.private_topics_count } - it 'should not hide the post' do + it "should not hide the post" do PostActionCreator.create(user1, spam_post, :spam) PostActionCreator.create(user2, spam_post, :spam) diff --git a/spec/integration/topic_auto_close_spec.rb b/spec/integration/topic_auto_close_spec.rb index 95821bc30e..d4daaa070a 100644 --- a/spec/integration/topic_auto_close_spec.rb +++ b/spec/integration/topic_auto_close_spec.rb @@ -4,36 +4,34 @@ RSpec.describe Topic do let(:job_klass) { Jobs::CloseTopic } - context 'when creating a topic without auto-close' do + context "when creating a topic without auto-close" do let(:topic) { Fabricate(:topic, category: category) } - context 'when uncategorized' do + context "when uncategorized" do let(:category) { nil } - it 'should not schedule the topic to auto-close' do + it "should not schedule the topic to auto-close" do expect(topic.public_topic_timer).to eq(nil) expect(job_klass.jobs).to eq([]) end end - context 'with category without default auto-close' do + context "with category without default auto-close" do let(:category) { Fabricate(:category, auto_close_hours: nil) } - it 'should not schedule the topic to auto-close' do + it "should not schedule the topic to auto-close" do expect(topic.public_topic_timer).to eq(nil) expect(job_klass.jobs).to eq([]) end end - context 'when jobs may be queued' do - before do - freeze_time - end + context "when jobs may be queued" do + before { freeze_time } - context 'when category has a default auto-close' do + context "when category has a default auto-close" do let(:category) { Fabricate(:category, auto_close_hours: 2.0) } - it 'should schedule the topic to auto-close' do + it "should schedule the topic to auto-close" do topic topic_status_update = TopicTimer.last @@ -42,11 +40,11 @@ RSpec.describe Topic do expect(topic.public_topic_timer.execute_at).to be_within_one_second_of(2.hours.from_now) end - context 'when topic was created by staff user' do + context "when topic was created by staff user" do let(:admin) { Fabricate(:admin) } let(:staff_topic) { Fabricate(:topic, user: admin, category: category) } - it 'should schedule the topic to auto-close' do + it "should schedule the topic to auto-close" do staff_topic topic_status_update = TopicTimer.last @@ -56,23 +54,24 @@ RSpec.describe Topic do expect(topic_status_update.user).to eq(Discourse.system_user) end - context 'when topic is closed manually' do - it 'should remove the schedule to auto-close the topic' do + context "when topic is closed manually" do + it "should remove the schedule to auto-close the topic" do topic_timer_id = staff_topic.public_topic_timer.id - staff_topic.update_status('closed', true, admin) + staff_topic.update_status("closed", true, admin) - expect(TopicTimer.with_deleted.find(topic_timer_id).deleted_at) - .to be_within_one_second_of(Time.zone.now) + expect( + TopicTimer.with_deleted.find(topic_timer_id).deleted_at, + ).to be_within_one_second_of(Time.zone.now) end end end - context 'when topic was created by a non-staff user' do + context "when topic was created by a non-staff user" do let(:regular_user) { Fabricate(:user) } let(:regular_user_topic) { Fabricate(:topic, user: regular_user, category: category) } - it 'should schedule the topic to auto-close' do + it "should schedule the topic to auto-close" do regular_user_topic topic_status_update = TopicTimer.last diff --git a/spec/integration/topic_thumbnail_spec.rb b/spec/integration/topic_thumbnail_spec.rb index 9a51836c5a..2674394097 100644 --- a/spec/integration/topic_thumbnail_spec.rb +++ b/spec/integration/topic_thumbnail_spec.rb @@ -9,11 +9,11 @@ RSpec.describe "Topic Thumbnails" do fab!(:topic) { Fabricate(:topic, image_upload_id: image.id) } fab!(:user) { Fabricate(:user) } - describe 'latest' do + describe "latest" do def get_topic Discourse.redis.del(topic.thumbnail_job_redis_key(Topic.thumbnail_sizes)) Discourse.redis.del(topic.thumbnail_job_redis_key([])) - get '/latest.json' + get "/latest.json" expect(response.status).to eq(200) response.parsed_body["topic_list"]["topics"][0] end @@ -27,11 +27,7 @@ RSpec.describe "Topic Thumbnails" do context "with a theme" do before do theme = Fabricate(:theme) - theme.theme_modifier_set.topic_thumbnail_sizes = [ - [10, 10], - [20, 20], - [30, 30] - ] + theme.theme_modifier_set.topic_thumbnail_sizes = [[10, 10], [20, 20], [30, 30]] theme.theme_modifier_set.save! theme.set_default! end @@ -39,15 +35,15 @@ RSpec.describe "Topic Thumbnails" do it "includes the theme specified resolutions" do topic_json = nil - expect do - topic_json = get_topic - end.to change { Jobs::GenerateTopicThumbnails.jobs.size }.by(2) + expect do topic_json = get_topic end.to change { + Jobs::GenerateTopicThumbnails.jobs.size + }.by(2) - expect( - Jobs::GenerateTopicThumbnails.jobs.map { |j| j["args"][0]["extra_sizes"] } - ).to eq([ - nil, # Job for core/plugin sizes - [[10, 10], [20, 20], [30, 30]]] # Job for theme sizes + expect(Jobs::GenerateTopicThumbnails.jobs.map { |j| j["args"][0]["extra_sizes"] }).to eq( + [ + nil, # Job for core/plugin sizes + [[10, 10], [20, 20], [30, 30]], + ], # Job for theme sizes ) thumbnails = topic_json["thumbnails"] @@ -67,9 +63,9 @@ RSpec.describe "Topic Thumbnails" do Jobs::GenerateTopicThumbnails.new.execute(args.with_indifferent_access) # Request again - expect do - topic_json = get_topic - end.not_to change { Jobs::GenerateTopicThumbnails.jobs.size } + expect do topic_json = get_topic end.not_to change { + Jobs::GenerateTopicThumbnails.jobs.size + } thumbnails = topic_json["thumbnails"] @@ -82,7 +78,6 @@ RSpec.describe "Topic Thumbnails" do expect(thumbnails[1]["width"]).to eq(9) expect(thumbnails[1]["height"]).to eq(9) expect(thumbnails[1]["url"]).to include("/optimized/") - end end @@ -92,25 +87,23 @@ RSpec.describe "Topic Thumbnails" do plugin.register_topic_thumbnail_size [512, 512] end - after do - DiscoursePluginRegistry.reset! - end + after { DiscoursePluginRegistry.reset! } it "includes the theme specified resolutions" do topic_json = nil - expect do - topic_json = get_topic - end.to change { Jobs::GenerateTopicThumbnails.jobs.size }.by(1) + expect do topic_json = get_topic end.to change { + Jobs::GenerateTopicThumbnails.jobs.size + }.by(1) # Run the job args = Jobs::GenerateTopicThumbnails.jobs.last["args"].first Jobs::GenerateTopicThumbnails.new.execute(args.with_indifferent_access) # Request again - expect do - topic_json = get_topic - end.not_to change { Jobs::GenerateTopicThumbnails.jobs.size } + expect do topic_json = get_topic end.not_to change { + Jobs::GenerateTopicThumbnails.jobs.size + } thumbnails = topic_json["thumbnails"] diff --git a/spec/integration/twitter_omniauth_spec.rb b/spec/integration/twitter_omniauth_spec.rb index 4e4e54793e..f64960c6e9 100644 --- a/spec/integration/twitter_omniauth_spec.rb +++ b/spec/integration/twitter_omniauth_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe 'Twitter OAuth 1.0a' do +describe "Twitter OAuth 1.0a" do let(:access_token) { "twitter_access_token_448" } let(:consumer_key) { "abcdef11223344" } let(:consumer_secret) { "adddcccdddd99922" } @@ -14,14 +14,15 @@ describe 'Twitter OAuth 1.0a' do created_at: "Sat May 09 17:58:22 +0000 2009", default_profile: false, default_profile_image: false, - description: "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", + description: + "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter. ", favourites_count: 588, follow_request_sent: nil, - followers_count: 10625, + followers_count: 10_625, following: nil, friends_count: 1181, geo_enabled: true, - id: 38895958, + id: 38_895_958, id_str: "38895958", is_translator: false, lang: "en", @@ -30,11 +31,14 @@ describe 'Twitter OAuth 1.0a' do name: "Sean Cook", notifications: nil, profile_background_color: "1A1B1F", - profile_background_image_url: "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", - profile_background_image_url_https: "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", + profile_background_image_url: + "http://a0.twimg.com/profile_background_images/495742332/purty_wood.png", + profile_background_image_url_https: + "https://si0.twimg.com/profile_background_images/495742332/purty_wood.png", profile_background_tile: true, profile_image_url: "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", - profile_image_url_https: "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", + profile_image_url_https: + "https://si0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG", profile_link_color: "2FC2EF", profile_sidebar_border_color: "181A1E", profile_sidebar_fill_color: "252429", @@ -46,19 +50,17 @@ describe 'Twitter OAuth 1.0a' do statuses_count: 2609, time_zone: "Pacific Time (US & Canada)", url: nil, - utc_offset: -28800, + utc_offset: -28_800, verified: true, - email: email + email: email, } - stub_request(:get, "https://api.twitter.com/1.1/account/verify_credentials.json") - .with( - query: { - include_email: true, - include_entities: false, - skip_status: true - } - ) - .to_return(status: 200, body: JSON.dump(body)) + stub_request(:get, "https://api.twitter.com/1.1/account/verify_credentials.json").with( + query: { + include_email: true, + include_entities: false, + skip_status: true, + }, + ).to_return(status: 200, body: JSON.dump(body)) end before do @@ -66,28 +68,28 @@ describe 'Twitter OAuth 1.0a' do SiteSetting.twitter_consumer_key = consumer_key SiteSetting.twitter_consumer_secret = consumer_secret - stub_request(:post, "https://api.twitter.com/oauth/request_token") - .to_return( - status: 200, - body: Rack::Utils.build_query( + stub_request(:post, "https://api.twitter.com/oauth/request_token").to_return( + status: 200, + body: + Rack::Utils.build_query( oauth_token: access_token, oauth_token_secret: oauth_token_secret, - oauth_callback_confirmed: true + oauth_callback_confirmed: true, ), - headers: { - "Content-Type" => "application/x-www-form-urlencoded" - } - ) - stub_request(:post, "https://api.twitter.com/oauth/access_token") - .to_return( - status: 200, - body: Rack::Utils.build_query( + headers: { + "Content-Type" => "application/x-www-form-urlencoded", + }, + ) + stub_request(:post, "https://api.twitter.com/oauth/access_token").to_return( + status: 200, + body: + Rack::Utils.build_query( oauth_token: access_token, oauth_token_secret: oauth_token_secret, user_id: "43423432422", - screen_name: "twitterapi" - ) - ) + screen_name: "twitterapi", + ), + ) end it "signs in the user if the API response from twitter includes an email (implies it's verified) and the email matches an existing user's" do diff --git a/spec/integration/watched_words_spec.rb b/spec/integration/watched_words_spec.rb index 2eef6fdc32..2304fb6bce 100644 --- a/spec/integration/watched_words_spec.rb +++ b/spec/integration/watched_words_spec.rb @@ -8,80 +8,121 @@ RSpec.describe WatchedWord do fab!(:topic) { Fabricate(:topic) } fab!(:first_post) { Fabricate(:post, topic: topic) } - let(:require_approval_word) { Fabricate(:watched_word, action: WatchedWord.actions[:require_approval]) } + let(:require_approval_word) do + Fabricate(:watched_word, action: WatchedWord.actions[:require_approval]) + end let(:flag_word) { Fabricate(:watched_word, action: WatchedWord.actions[:flag]) } let(:block_word) { Fabricate(:watched_word, action: WatchedWord.actions[:block]) } let(:another_block_word) { Fabricate(:watched_word, action: WatchedWord.actions[:block]) } - before_all do - WordWatcher.clear_cache! - end + before_all { WordWatcher.clear_cache! } - after do - WordWatcher.clear_cache! - end + after { WordWatcher.clear_cache! } context "with block" do def should_block_post(manager) expect { result = manager.perform expect(result).to_not be_success - expect(result.errors[:base]&.first).to eq(I18n.t('contains_blocked_word', word: block_word.word)) + expect(result.errors[:base]&.first).to eq( + I18n.t("contains_blocked_word", word: block_word.word), + ) }.to_not change { Post.count } end it "escapes the blocked word in error message" do block_word = Fabricate(:watched_word, action: WatchedWord.actions[:block], word: "") - manager = NewPostManager.new(tl2_user, raw: "Want some #{block_word.word} for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + tl2_user, + raw: "Want some #{block_word.word} for cheap?", + topic_id: topic.id, + ) result = manager.perform expect(result).to_not be_success - expect(result.errors[:base]&.first).to eq(I18n.t('contains_blocked_word', word: "<a>")) + expect(result.errors[:base]&.first).to eq(I18n.t("contains_blocked_word", word: "<a>")) end it "should prevent the post from being created" do - manager = NewPostManager.new(tl2_user, raw: "Want some #{block_word.word} for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + tl2_user, + raw: "Want some #{block_word.word} for cheap?", + topic_id: topic.id, + ) should_block_post(manager) end it "look at title too" do - manager = NewPostManager.new(tl2_user, title: "We sell #{block_word.word} online", raw: "Want some poutine for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + tl2_user, + title: "We sell #{block_word.word} online", + raw: "Want some poutine for cheap?", + topic_id: topic.id, + ) should_block_post(manager) end it "should block the post from admin" do - manager = NewPostManager.new(admin, raw: "Want some #{block_word.word} for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + admin, + raw: "Want some #{block_word.word} for cheap?", + topic_id: topic.id, + ) should_block_post(manager) end it "should block the post from moderator" do - manager = NewPostManager.new(moderator, raw: "Want some #{block_word.word} for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + moderator, + raw: "Want some #{block_word.word} for cheap?", + topic_id: topic.id, + ) should_block_post(manager) end it "should block the post if it contains multiple blocked words" do - manager = NewPostManager.new(moderator, raw: "Want some #{block_word.word} #{another_block_word.word} for cheap?", topic_id: topic.id) + manager = + NewPostManager.new( + moderator, + raw: "Want some #{block_word.word} #{another_block_word.word} for cheap?", + topic_id: topic.id, + ) expect { result = manager.perform expect(result).to_not be_success - expect(result.errors[:base]&.first).to eq(I18n.t('contains_blocked_words', words: [block_word.word, another_block_word.word].sort.join(', '))) + expect(result.errors[:base]&.first).to eq( + I18n.t( + "contains_blocked_words", + words: [block_word.word, another_block_word.word].sort.join(", "), + ), + ) }.to_not change { Post.count } end it "should block in a private message too" do - manager = NewPostManager.new( - tl2_user, - raw: "Want some #{block_word.word} for cheap?", - title: 'this is a new title', - archetype: Archetype.private_message, - target_usernames: Fabricate(:user, trust_level: TrustLevel[2]).username - ) + manager = + NewPostManager.new( + tl2_user, + raw: "Want some #{block_word.word} for cheap?", + title: "this is a new title", + archetype: Archetype.private_message, + target_usernames: Fabricate(:user, trust_level: TrustLevel[2]).username, + ) should_block_post(manager) end it "blocks on revisions" do post = Fabricate(:post, topic: Fabricate(:topic, user: tl2_user), user: tl2_user) expect { - PostRevisor.new(post).revise!(post.user, { raw: "Want some #{block_word.word} for cheap?" }, revised_at: post.updated_at + 10.seconds) + PostRevisor.new(post).revise!( + post.user, + { raw: "Want some #{block_word.word} for cheap?" }, + revised_at: post.updated_at + 10.seconds, + ) expect(post.errors).to be_present post.reload }.to_not change { post.raw } @@ -90,27 +131,48 @@ RSpec.describe WatchedWord do context "with require_approval" do it "should queue the post for approval" do - manager = NewPostManager.new(tl2_user, raw: "My dog's name is #{require_approval_word.word}.", topic_id: topic.id) + manager = + NewPostManager.new( + tl2_user, + raw: "My dog's name is #{require_approval_word.word}.", + topic_id: topic.id, + ) result = manager.perform expect(result.action).to eq(:enqueued) expect(result.reason).to eq(:watched_word) end it "looks at title too" do - manager = NewPostManager.new(tl2_user, title: "You won't believe these #{require_approval_word.word} dog names!", raw: "My dog's name is Porkins.", topic_id: topic.id) + manager = + NewPostManager.new( + tl2_user, + title: "You won't believe these #{require_approval_word.word} dog names!", + raw: "My dog's name is Porkins.", + topic_id: topic.id, + ) result = manager.perform expect(result.action).to eq(:enqueued) end it "should not queue posts from admin" do - manager = NewPostManager.new(admin, raw: "My dog's name is #{require_approval_word.word}.", topic_id: topic.id) + manager = + NewPostManager.new( + admin, + raw: "My dog's name is #{require_approval_word.word}.", + topic_id: topic.id, + ) result = manager.perform expect(result).to be_success expect(result.action).to eq(:create_post) end it "should not queue posts from moderator" do - manager = NewPostManager.new(moderator, raw: "My dog's name is #{require_approval_word.word}.", topic_id: topic.id) + manager = + NewPostManager.new( + moderator, + raw: "My dog's name is #{require_approval_word.word}.", + topic_id: topic.id, + ) result = manager.perform expect(result).to be_success expect(result.action).to eq(:create_post) @@ -118,13 +180,14 @@ RSpec.describe WatchedWord do it "doesn't need approval in a private message" do Group.refresh_automatic_groups! - manager = NewPostManager.new( - tl2_user, - raw: "Want some #{require_approval_word.word} for cheap?", - title: 'this is a new title', - archetype: Archetype.private_message, - target_usernames: Fabricate(:user, trust_level: TrustLevel[2]).username - ) + manager = + NewPostManager.new( + tl2_user, + raw: "Want some #{require_approval_word.word} for cheap?", + title: "this is a new title", + archetype: Archetype.private_message, + target_usernames: Fabricate(:user, trust_level: TrustLevel[2]).username, + ) result = manager.perform expect(result).to be_success expect(result.action).to eq(:create_post) @@ -134,74 +197,122 @@ RSpec.describe WatchedWord do context "with flag" do def should_flag_post(author, raw, topic) post = Fabricate(:post, raw: raw, topic: topic, user: author) - expect { - Jobs::ProcessPost.new.execute(post_id: post.id) - }.to change { PostAction.count }.by(1) - expect(PostAction.where(post_id: post.id, post_action_type_id: PostActionType.types[:inappropriate]).exists?).to eq(true) + expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to change { PostAction.count }.by( + 1, + ) + expect( + PostAction.where( + post_id: post.id, + post_action_type_id: PostActionType.types[:inappropriate], + ).exists?, + ).to eq(true) end def should_not_flag_post(author, raw, topic) post = Fabricate(:post, raw: raw, topic: topic, user: author) - expect { - Jobs::ProcessPost.new.execute(post_id: post.id) - }.to_not change { PostAction.count } + expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to_not change { PostAction.count } end it "should flag the post as inappropriate" do topic = Fabricate(:topic, user: tl2_user) post = Fabricate(:post, raw: "I said.... #{flag_word.word}", topic: topic, user: tl2_user) Jobs::ProcessPost.new.execute(post_id: post.id) - expect(PostAction.where(post_id: post.id, post_action_type_id: PostActionType.types[:inappropriate]).exists?).to eq(true) + expect( + PostAction.where( + post_id: post.id, + post_action_type_id: PostActionType.types[:inappropriate], + ).exists?, + ).to eq(true) reviewable = ReviewableFlaggedPost.where(target: post) expect(reviewable).to be_present - expect(ReviewableScore.where(reviewable: reviewable, reason: 'watched_word')).to be_present + expect(ReviewableScore.where(reviewable: reviewable, reason: "watched_word")).to be_present end it "should look at the title too" do - should_flag_post(tl2_user, "I thought the movie was not bad actually.", Fabricate(:topic, user: tl2_user, title: "Read my #{flag_word.word} review!")) + should_flag_post( + tl2_user, + "I thought the movie was not bad actually.", + Fabricate(:topic, user: tl2_user, title: "Read my #{flag_word.word} review!"), + ) end it "shouldn't flag posts by admin" do - should_not_flag_post(admin, "I thought the #{flag_word.word} was bad.", Fabricate(:topic, user: admin)) + should_not_flag_post( + admin, + "I thought the #{flag_word.word} was bad.", + Fabricate(:topic, user: admin), + ) end it "shouldn't flag posts by moderator" do - should_not_flag_post(moderator, "I thought the #{flag_word.word} was bad.", Fabricate(:topic, user: moderator)) + should_not_flag_post( + moderator, + "I thought the #{flag_word.word} was bad.", + Fabricate(:topic, user: moderator), + ) end it "is compatible with flag_sockpuppets" do SiteSetting.flag_sockpuppets = true - ip_address = '182.189.119.174' + ip_address = "182.189.119.174" user1 = Fabricate(:user, ip_address: ip_address, created_at: 2.days.ago) user2 = Fabricate(:user, ip_address: ip_address) first = create_post(user: user1, created_at: 2.days.ago) - sockpuppet_post = create_post(user: user2, topic: first.topic, raw: "I thought the #{flag_word.word} was bad.") + sockpuppet_post = + create_post( + user: user2, + topic: first.topic, + raw: "I thought the #{flag_word.word} was bad.", + ) expect(PostAction.where(post_id: sockpuppet_post.id).count).to eq(1) end it "flags in private message too" do - post = Fabricate(:private_message_post, raw: "Want some #{flag_word.word} for cheap?", user: tl2_user) - expect { - Jobs::ProcessPost.new.execute(post_id: post.id) - }.to change { PostAction.count }.by(1) - expect(PostAction.where(post_id: post.id, post_action_type_id: PostActionType.types[:inappropriate]).exists?).to eq(true) + post = + Fabricate( + :private_message_post, + raw: "Want some #{flag_word.word} for cheap?", + user: tl2_user, + ) + expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to change { PostAction.count }.by( + 1, + ) + expect( + PostAction.where( + post_id: post.id, + post_action_type_id: PostActionType.types[:inappropriate], + ).exists?, + ).to eq(true) end it "flags on revisions" do Jobs.run_immediately! post = Fabricate(:post, topic: Fabricate(:topic, user: tl2_user), user: tl2_user) expect { - PostRevisor.new(post).revise!(post.user, { raw: "Want some #{flag_word.word} for cheap?" }, revised_at: post.updated_at + 10.seconds) + PostRevisor.new(post).revise!( + post.user, + { raw: "Want some #{flag_word.word} for cheap?" }, + revised_at: post.updated_at + 10.seconds, + ) }.to change { PostAction.count }.by(1) - expect(PostAction.where(post_id: post.id, post_action_type_id: PostActionType.types[:inappropriate]).exists?).to eq(true) + expect( + PostAction.where( + post_id: post.id, + post_action_type_id: PostActionType.types[:inappropriate], + ).exists?, + ).to eq(true) end it "should not flag on rebake" do - post = Fabricate(:post, topic: Fabricate(:topic, user: tl2_user), user: tl2_user, raw: "I have coupon codes. Message me.") + post = + Fabricate( + :post, + topic: Fabricate(:topic, user: tl2_user), + user: tl2_user, + raw: "I have coupon codes. Message me.", + ) Fabricate(:watched_word, action: WatchedWord.actions[:flag], word: "coupon") - expect { - post.rebake! - }.to_not change { PostAction.count } + expect { post.rebake! }.to_not change { PostAction.count } end end end diff --git a/spec/integrity/coding_style_spec.rb b/spec/integrity/coding_style_spec.rb index b51e6da61f..ebd3ade729 100644 --- a/spec/integrity/coding_style_spec.rb +++ b/spec/integrity/coding_style_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -def list_files(base_dir, pattern = '*') +def list_files(base_dir, pattern = "*") Dir[File.join("#{base_dir}", pattern)] end def list_js_files(base_dir) - list_files(base_dir, '**/*.es6') + list_files(base_dir, "**/*.es6") end def grep_files(files, regex) @@ -17,10 +17,10 @@ def grep_file(file, regex) lines.count > 0 ? file : nil end -RSpec.describe 'Coding style' do - describe 'Javascript' do +RSpec.describe "Coding style" do + describe "Javascript" do it 'prevents this.get("foo") pattern' do - js_files = list_js_files('app/assets/javascripts') + js_files = list_js_files("app/assets/javascripts") offenses = grep_files(js_files, /this\.get\("\w+"\)/) expect(offenses).to be_empty, <<~TEXT @@ -33,7 +33,7 @@ RSpec.describe 'Coding style' do end end - describe 'Post Migrations' do + describe "Post Migrations" do def check_offenses(files, method_name, constant_name) method_name_regex = /#{Regexp.escape(method_name)}/ constant_name_regex = /#{Regexp.escape(constant_name)}/ @@ -57,8 +57,8 @@ RSpec.describe 'Coding style' do contains_method_name ? contains_constant_name : true end - it 'ensures dropped tables and columns are stored in constants' do - migration_files = list_files('db/post_migrate', '**/*.rb') + it "ensures dropped tables and columns are stored in constants" do + migration_files = list_files("db/post_migrate", "**/*.rb") check_offenses(migration_files, "ColumnDropper.execute_drop", "DROPPED_COLUMNS") check_offenses(migration_files, "TableDropper.execute_drop", "DROPPED_TABLES") diff --git a/spec/integrity/common_mark_spec.rb b/spec/integrity/common_mark_spec.rb index 01cc77683f..c89f9ff0e8 100644 --- a/spec/integrity/common_mark_spec.rb +++ b/spec/integrity/common_mark_spec.rb @@ -1,76 +1,72 @@ # frozen_string_literal: true RSpec.describe "CommonMark" do - it 'passes spec' do - + it "passes spec" do SiteSetting.traditional_markdown_linebreaks = true SiteSetting.enable_markdown_typographer = false - SiteSetting.highlighted_languages = 'ruby|aa' + SiteSetting.highlighted_languages = "ruby|aa" html, state, md = nil failed = 0 - File.readlines(Rails.root + 'spec/fixtures/md/spec.txt').each do |line| - if line == "```````````````````````````````` example\n" - state = :example - next - end - - if line == "````````````````````````````````\n" - md.gsub!('→', "\t") - html ||= String.new - html.gsub!('→', "\t") - html.strip! - - # normalize brs - html.gsub!('
    ', '
    ') - html.gsub!('
    ', '
    ') - html.gsub!(/]+) \/>/, "") - - SiteSetting.enable_markdown_linkify = false - cooked = PrettyText.markdown(md, sanitize: false) - cooked.strip! - cooked.gsub!(" class=\"lang-auto\"", '') - cooked.gsub!(/(.*)<\/span>/, "\\1") - cooked.gsub!(/
    <\/a>/, "") - # we support data-attributes which is not in the spec - cooked.gsub!("
    ", '
    ')
    -        # we don't care about this
    -        cooked.gsub!("
    \n
    ", "
    ") - html.gsub!("
    \n
    ", "
    ") - html.gsub!("language-ruby", "lang-ruby") - html.gsub!("language-aa", "lang-aa") - # strip out unsupported languages - html.gsub!(/ class="language-[;f].*"/, "") - - unless cooked == html - failed += 1 - puts "FAILED SPEC" - puts "Expected: " - puts html - puts "Got: " - puts cooked - puts "Markdown: " - puts md - puts + File + .readlines(Rails.root + "spec/fixtures/md/spec.txt") + .each do |line| + if line == "```````````````````````````````` example\n" + state = :example + next end - html, state, md = nil - next - end - if state == :example && line == ".\n" - state = :html - next - end + if line == "````````````````````````````````\n" + md.gsub!("→", "\t") + html ||= String.new + html.gsub!("→", "\t") + html.strip! - if state == :example - md = (md || String.new) << line - end + # normalize brs + html.gsub!("
    ", "
    ") + html.gsub!("
    ", "
    ") + html.gsub!(%r{]+) />}, "") - if state == :html - html = (html || String.new) << line - end + SiteSetting.enable_markdown_linkify = false + cooked = PrettyText.markdown(md, sanitize: false) + cooked.strip! + cooked.gsub!(" class=\"lang-auto\"", "") + cooked.gsub!(%r{(.*)}, "\\1") + cooked.gsub!(%r{
    }, "") + # we support data-attributes which is not in the spec + cooked.gsub!("
    ", "
    ")
    +          # we don't care about this
    +          cooked.gsub!("
    \n
    ", "
    ") + html.gsub!("
    \n
    ", "
    ") + html.gsub!("language-ruby", "lang-ruby") + html.gsub!("language-aa", "lang-aa") + # strip out unsupported languages + html.gsub!(%r{ class="language-[;f].*"}, "") - end + unless cooked == html + failed += 1 + puts "FAILED SPEC" + puts "Expected: " + puts html + puts "Got: " + puts cooked + puts "Markdown: " + puts md + puts + end + html, state, md = nil + next + end + + if state == :example && line == ".\n" + state = :html + next + end + + md = (md || String.new) << line if state == :example + + html = (html || String.new) << line if state == :html + end expect(failed).to eq(0) end diff --git a/spec/integrity/i18n_spec.rb b/spec/integrity/i18n_spec.rb index a78811a395..edf7bea714 100644 --- a/spec/integrity/i18n_spec.rb +++ b/spec/integrity/i18n_spec.rb @@ -37,10 +37,12 @@ RSpec.describe "i18n integrity checks" do end it "has an i18n key for each Badge description" do - Badge.where(system: true).each do |b| - expect(b.long_description).to be_present - expect(b.description).to be_present - end + Badge + .where(system: true) + .each do |b| + expect(b.long_description).to be_present + expect(b.description).to be_present + end end Dir["#{Rails.root}/config/locales/{client,server}.*.yml"].each do |path| @@ -116,20 +118,14 @@ RSpec.describe "fallbacks" do it "finds the fallback translation" do I18n.backend.store_translations(:en, test: "en test") - I18n.with_locale("pl_PL") do - expect(I18n.t("test")).to eq("en test") - end + I18n.with_locale("pl_PL") { expect(I18n.t("test")).to eq("en test") } end context "when in a multi-threaded environment" do it "finds the fallback translation" do I18n.backend.store_translations(:en, test: "en test") - thread = Thread.new do - I18n.with_locale("pl_PL") do - expect(I18n.t("test")).to eq("en test") - end - end + thread = Thread.new { I18n.with_locale("pl_PL") { expect(I18n.t("test")).to eq("en test") } } begin thread.join diff --git a/spec/integrity/js_constants_spec.rb b/spec/integrity/js_constants_spec.rb index fe13669d54..559f2c4533 100644 --- a/spec/integrity/js_constants_spec.rb +++ b/spec/integrity/js_constants_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true RSpec.describe "constants match ruby" do - let(:ctx) { MiniRacer::Context.new } def parse(file) # mini racer doesn't handle JS modules so we'll do this hack source = File.read("#{Rails.root}/app/assets/javascripts/#{file}") - source.gsub!(/^export */, '') + source.gsub!(/^export */, "") ctx.eval(source) end @@ -16,12 +15,9 @@ RSpec.describe "constants match ruby" do parse("pretty-text/addon/emoji/version.js") priorities = ctx.eval("SEARCH_PRIORITIES") - Searchable::PRIORITIES.each do |key, value| - expect(priorities[key.to_s]).to eq(value) - end + Searchable::PRIORITIES.each { |key, value| expect(priorities[key.to_s]).to eq(value) } expect(ctx.eval("SEARCH_PHRASE_REGEXP")).to eq(Search::PHRASE_MATCH_REGEXP_PATTERN) expect(ctx.eval("IMAGE_VERSION")).to eq(Emoji::EMOJI_VERSION) end - end diff --git a/spec/integrity/oj_spec.rb b/spec/integrity/oj_spec.rb index 799f7578f2..ede78a648a 100644 --- a/spec/integrity/oj_spec.rb +++ b/spec/integrity/oj_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'Oj' do +RSpec.describe "Oj" do it "is enabled" do classes = Set.new tracer = TracePoint.new(:c_call) { |tp| classes << tp.defined_class } @@ -11,7 +11,7 @@ RSpec.describe 'Oj' do it "escapes HTML entities the same as ActiveSupport" do expect("hello".to_json).to eq("\"\\u003cb\\u003ehello\\u003c/b\\u003e\"") - expect('"hello world"'.to_json). to eq('"\"hello world\""') + expect('"hello world"'.to_json).to eq('"\"hello world\""') expect("\u2028\u2029><&".to_json).to eq('"\u2028\u2029\u003e\u003c\u0026"') end end diff --git a/spec/integrity/onceoff_integrity_spec.rb b/spec/integrity/onceoff_integrity_spec.rb index e913d2c816..2952cfa1be 100644 --- a/spec/integrity/onceoff_integrity_spec.rb +++ b/spec/integrity/onceoff_integrity_spec.rb @@ -3,11 +3,12 @@ RSpec.describe ::Jobs::Onceoff do it "can run all once off jobs without errors" do # Load all once offs - Dir[Rails.root + 'app/jobs/onceoff/*.rb'].each do |f| - require_relative '../../app/jobs/onceoff/' + File.basename(f) + Dir[Rails.root + "app/jobs/onceoff/*.rb"].each do |f| + require_relative "../../app/jobs/onceoff/" + File.basename(f) end - ObjectSpace.each_object(Class) + ObjectSpace + .each_object(Class) .select { |klass| klass.superclass == ::Jobs::Onceoff } .each { |job| job.new.execute_onceoff(nil) } end diff --git a/spec/integrity/site_setting_spec.rb b/spec/integrity/site_setting_spec.rb index 8779af4576..412902d874 100644 --- a/spec/integrity/site_setting_spec.rb +++ b/spec/integrity/site_setting_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true RSpec.describe "site setting integrity checks" do - let(:site_setting_file) { File.join(Rails.root, 'config', 'site_settings.yml') } + let(:site_setting_file) { File.join(Rails.root, "config", "site_settings.yml") } let(:yaml) { YAML.load_file(site_setting_file) } - %w(hidden client).each do |property| + %w[hidden client].each do |property| it "set #{property} value as true or not set" do yaml.each_value do |category| category.each_value do |setting| @@ -24,16 +24,16 @@ RSpec.describe "site setting integrity checks" do yaml.each_value do |category| category.each do |setting_name, setting| next unless setting.is_a?(Hash) - if setting['locale_default'] - setting['locale_default'].each_pair do |k, v| + if setting["locale_default"] + setting["locale_default"].each_pair do |k, v| expect(LocaleSiteSetting.valid_value?(k.to_s)).to be_truthy, - "'#{k}' is not a valid locale_default key for '#{setting_name}' site setting" + "'#{k}' is not a valid locale_default key for '#{setting_name}' site setting" - case setting['default'] + case setting["default"] when TrueClass, FalseClass expect(v.class == TrueClass || v.class == FalseClass).to be_truthy else - expect(v).to be_a_kind_of(setting['default'].class) + expect(v).to be_a_kind_of(setting["default"].class) end end end diff --git a/spec/jobs/about_stats_spec.rb b/spec/jobs/about_stats_spec.rb index 3a2e5d6aa9..fa6ecaa016 100644 --- a/spec/jobs/about_stats_spec.rb +++ b/spec/jobs/about_stats_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::AboutStats do - it 'caches the stats' do + it "caches the stats" do begin stats = About.fetch_stats.to_json cache_key = About.stats_cache_key diff --git a/spec/jobs/activation_reminder_emails_spec.rb b/spec/jobs/activation_reminder_emails_spec.rb index d7956d3dcd..e70cc9abaa 100644 --- a/spec/jobs/activation_reminder_emails_spec.rb +++ b/spec/jobs/activation_reminder_emails_spec.rb @@ -6,33 +6,33 @@ RSpec.describe Jobs::ActivationReminderEmails do # should be between 2 and 3 days let(:created_at) { 50.hours.ago } - it 'should email inactive users' do + it "should email inactive users" do user = Fabricate(:user, active: false, created_at: created_at) - expect { described_class.new.execute({}) } - .to change { ActionMailer::Base.deliveries.size }.by(1) - .and change { user.email_tokens.count }.by(1) + expect { described_class.new.execute({}) }.to change { ActionMailer::Base.deliveries.size }.by( + 1, + ).and change { user.email_tokens.count }.by(1) - expect(user.custom_fields['activation_reminder']).to eq("t") + expect(user.custom_fields["activation_reminder"]).to eq("t") expect { described_class.new.execute({}) }.not_to change { ActionMailer::Base.deliveries.size } user.activate - expect(user.reload.custom_fields['activation_reminder']).to eq(nil) + expect(user.reload.custom_fields["activation_reminder"]).to eq(nil) end - it 'should not email active users' do + it "should not email active users" do user = Fabricate(:user, active: true, created_at: created_at) - expect { described_class.new.execute({}) } - .to not_change { ActionMailer::Base.deliveries.size } - .and not_change { user.email_tokens.count } + expect { described_class.new.execute({}) }.to not_change { + ActionMailer::Base.deliveries.size + }.and not_change { user.email_tokens.count } end - it 'should not email staged users' do + it "should not email staged users" do user = Fabricate(:user, active: false, staged: true, created_at: created_at) - expect { described_class.new.execute({}) } - .to not_change { ActionMailer::Base.deliveries.size } - .and not_change { user.email_tokens.count } + expect { described_class.new.execute({}) }.to not_change { + ActionMailer::Base.deliveries.size + }.and not_change { user.email_tokens.count } end end diff --git a/spec/jobs/auto_expire_user_api_keys_spec.rb b/spec/jobs/auto_expire_user_api_keys_spec.rb index abed8ceba6..5c6ce1c8a6 100644 --- a/spec/jobs/auto_expire_user_api_keys_spec.rb +++ b/spec/jobs/auto_expire_user_api_keys_spec.rb @@ -4,12 +4,10 @@ RSpec.describe Jobs::AutoExpireUserApiKeys do fab!(:key1) { Fabricate(:readonly_user_api_key) } fab!(:key2) { Fabricate(:readonly_user_api_key) } - context 'when user api key is unused in last 1 days' do - before do - SiteSetting.expire_user_api_keys_days = 1 - end + context "when user api key is unused in last 1 days" do + before { SiteSetting.expire_user_api_keys_days = 1 } - it 'should revoke the key' do + it "should revoke the key" do freeze_time key1.update!(last_used_at: 2.days.ago) diff --git a/spec/jobs/auto_queue_handler_spec.rb b/spec/jobs/auto_queue_handler_spec.rb index 6fb6c2a900..f4d1ada3af 100644 --- a/spec/jobs/auto_queue_handler_spec.rb +++ b/spec/jobs/auto_queue_handler_spec.rb @@ -9,15 +9,15 @@ RSpec.describe Jobs::AutoQueueHandler do Fabricate(:user), Fabricate(:post), PostActionType.types[:spam], - message: 'this is the initial message' + message: "this is the initial message", ).perform end fab!(:post_action) { spam_result.post_action } - fab!(:old) { + fab!(:old) do spam_result.reviewable.update_column(:created_at, 61.days.ago) spam_result.reviewable - } + end fab!(:not_old) { Fabricate(:reviewable_flagged_post, created_at: 59.days.ago) } diff --git a/spec/jobs/automatic_group_membership_spec.rb b/spec/jobs/automatic_group_membership_spec.rb index 76efc6cd8a..ce130987c0 100644 --- a/spec/jobs/automatic_group_membership_spec.rb +++ b/spec/jobs/automatic_group_membership_spec.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true RSpec.describe Jobs::AutomaticGroupMembership do - it "raises an error when the group id is missing" do - expect { Jobs::AutomaticGroupMembership.new.execute({}) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::AutomaticGroupMembership.new.execute({}) }.to raise_error( + Discourse::InvalidParameters, + ) end it "updates the membership" do @@ -14,19 +15,28 @@ RSpec.describe Jobs::AutomaticGroupMembership do user4 = Fabricate(:user, email: "yes@wat.com") EmailToken.confirm(Fabricate(:email_token, user: user4).token) user5 = Fabricate(:user, email: "sso@wat.com") - user5.create_single_sign_on_record(external_id: 123, external_email: "hacker@wat.com", last_payload: "") + user5.create_single_sign_on_record( + external_id: 123, + external_email: "hacker@wat.com", + last_payload: "", + ) user6 = Fabricate(:user, email: "sso2@wat.com") - user6.create_single_sign_on_record(external_id: 456, external_email: "sso2@wat.com", last_payload: "") + user6.create_single_sign_on_record( + external_id: 456, + external_email: "sso2@wat.com", + last_payload: "", + ) group = Fabricate(:group, automatic_membership_email_domains: "wat.com") automatic = nil called = false - blk = Proc.new do |_u, _g, options| - automatic = options[:automatic] - called = true - end + blk = + Proc.new do |_u, _g, options| + automatic = options[:automatic] + called = true + end begin DiscourseEvent.on(:user_added_to_group, &blk) @@ -47,5 +57,4 @@ RSpec.describe Jobs::AutomaticGroupMembership do expect(group.users.include?(user6)).to eq(true) expect(group.user_count).to eq(2) end - end diff --git a/spec/jobs/bookmark_reminder_notifications_spec.rb b/spec/jobs/bookmark_reminder_notifications_spec.rb index 6a68eb439e..22a9a17f83 100644 --- a/spec/jobs/bookmark_reminder_notifications_spec.rb +++ b/spec/jobs/bookmark_reminder_notifications_spec.rb @@ -8,13 +8,7 @@ RSpec.describe Jobs::BookmarkReminderNotifications do let(:bookmark1) { Fabricate(:bookmark, user: user) } let(:bookmark2) { Fabricate(:bookmark, user: user) } let(:bookmark3) { Fabricate(:bookmark, user: user) } - let!(:bookmarks) do - [ - bookmark1, - bookmark2, - bookmark3 - ] - end + let!(:bookmarks) { [bookmark1, bookmark2, bookmark3] } before do # this is done to avoid model validations on Bookmark @@ -66,10 +60,12 @@ RSpec.describe Jobs::BookmarkReminderNotifications do end end - it 'will not send notification when topic is not available' do + it "will not send notification when topic is not available" do bookmark1.bookmarkable.topic.destroy bookmark2.bookmarkable.topic.destroy bookmark3.bookmarkable.topic.destroy - expect { subject.execute }.not_to change { Notification.where(notification_type: Notification.types[:bookmark_reminder]).count } + expect { subject.execute }.not_to change { + Notification.where(notification_type: Notification.types[:bookmark_reminder]).count + } end end diff --git a/spec/jobs/bulk_grant_trust_level_spec.rb b/spec/jobs/bulk_grant_trust_level_spec.rb index bfda953eba..b2963069da 100644 --- a/spec/jobs/bulk_grant_trust_level_spec.rb +++ b/spec/jobs/bulk_grant_trust_level_spec.rb @@ -1,13 +1,16 @@ # frozen_string_literal: true RSpec.describe Jobs::BulkGrantTrustLevel do - it "raises an error when trust_level is missing" do - expect { Jobs::BulkGrantTrustLevel.new.execute(user_ids: [1, 2]) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::BulkGrantTrustLevel.new.execute(user_ids: [1, 2]) }.to raise_error( + Discourse::InvalidParameters, + ) end it "raises an error when user_ids are missing" do - expect { Jobs::BulkGrantTrustLevel.new.execute(trust_level: 0) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::BulkGrantTrustLevel.new.execute(trust_level: 0) }.to raise_error( + Discourse::InvalidParameters, + ) end it "updates the trust_level" do diff --git a/spec/jobs/bulk_invite_spec.rb b/spec/jobs/bulk_invite_spec.rb index 53d38d7888..3a8d370ab7 100644 --- a/spec/jobs/bulk_invite_spec.rb +++ b/spec/jobs/bulk_invite_spec.rb @@ -1,34 +1,43 @@ # frozen_string_literal: true RSpec.describe Jobs::BulkInvite do - describe '#execute' do + describe "#execute" do fab!(:user) { Fabricate(:user) } fab!(:admin) { Fabricate(:admin) } - fab!(:group1) { Fabricate(:group, name: 'group1') } - fab!(:group2) { Fabricate(:group, name: 'group2') } + fab!(:group1) { Fabricate(:group, name: "group1") } + fab!(:group2) { Fabricate(:group, name: "group2") } fab!(:topic) { Fabricate(:topic) } let(:staged_user) { Fabricate(:user, staged: true, active: false) } - let(:email) { 'test@discourse.org' } - let(:invites) { [{ email: user.email }, { email: staged_user.email }, { email: 'test2@discourse.org' }, { email: 'test@discourse.org', groups: 'GROUP1;group2', topic_id: topic.id }, { email: 'invalid' }] } - - it 'raises an error when the invites array is missing' do - expect { Jobs::BulkInvite.new.execute(current_user_id: user.id) } - .to raise_error(Discourse::InvalidParameters, /invites/) + let(:email) { "test@discourse.org" } + let(:invites) do + [ + { email: user.email }, + { email: staged_user.email }, + { email: "test2@discourse.org" }, + { email: "test@discourse.org", groups: "GROUP1;group2", topic_id: topic.id }, + { email: "invalid" }, + ] end - it 'raises an error when current_user_id is not valid' do - expect { Jobs::BulkInvite.new.execute(invites: invites) } - .to raise_error(Discourse::InvalidParameters, /current_user_id/) - end - - it 'creates the right invites' do - described_class.new.execute( - current_user_id: admin.id, - invites: invites + it "raises an error when the invites array is missing" do + expect { Jobs::BulkInvite.new.execute(current_user_id: user.id) }.to raise_error( + Discourse::InvalidParameters, + /invites/, ) + end + + it "raises an error when current_user_id is not valid" do + expect { Jobs::BulkInvite.new.execute(invites: invites) }.to raise_error( + Discourse::InvalidParameters, + /current_user_id/, + ) + end + + it "creates the right invites" do + described_class.new.execute(current_user_id: admin.id, invites: invites) expect(Invite.exists?(email: staged_user.email)).to eq(true) - expect(Invite.exists?(email: 'test2@discourse.org')).to eq(true) + expect(Invite.exists?(email: "test2@discourse.org")).to eq(true) invite = Invite.last expect(invite.email).to eq(email) @@ -42,13 +51,10 @@ RSpec.describe Jobs::BulkInvite do expect(post.raw).to include("1 error") end - it 'does not create invited groups for automatic groups' do + it "does not create invited groups for automatic groups" do group2.update!(automatic: true) - described_class.new.execute( - current_user_id: admin.id, - invites: invites - ) + described_class.new.execute(current_user_id: admin.id, invites: invites) invite = Invite.last expect(invite.email).to eq(email) @@ -58,37 +64,31 @@ RSpec.describe Jobs::BulkInvite do expect(post.raw).to include("1 warning") end - it 'does not create invited groups record if the user can not manage the group' do + it "does not create invited groups record if the user can not manage the group" do group1.add_owner(user) - described_class.new.execute( - current_user_id: user.id, - invites: invites - ) + described_class.new.execute(current_user_id: user.id, invites: invites) invite = Invite.last expect(invite.email).to eq(email) expect(invite.invited_groups.pluck(:group_id)).to contain_exactly(group1.id) end - it 'adds existing users to valid groups' do - existing_user = Fabricate(:user, email: 'test@discourse.org') + it "adds existing users to valid groups" do + existing_user = Fabricate(:user, email: "test@discourse.org") group2.update!(automatic: true) expect do - described_class.new.execute( - current_user_id: admin.id, - invites: invites - ) + described_class.new.execute(current_user_id: admin.id, invites: invites) end.to change { Invite.count }.by(2) expect(Invite.exists?(email: staged_user.email)).to eq(true) - expect(Invite.exists?(email: 'test2@discourse.org')).to eq(true) + expect(Invite.exists?(email: "test2@discourse.org")).to eq(true) expect(existing_user.reload.groups).to eq([group1]) end - it 'can create staged users and prepopulate user fields' do + it "can create staged users and prepopulate user fields" do user_field = Fabricate(:user_field, name: "Location") user_field_color = Fabricate(:user_field, field_type: "dropdown", name: "Color") user_field_color.user_field_options.create!(value: "Red") @@ -98,40 +98,33 @@ RSpec.describe Jobs::BulkInvite do described_class.new.execute( current_user_id: admin.id, invites: [ - { email: 'test@discourse.org' }, # new user without user fields - { email: user.email, location: 'value 1', color: 'blue' }, # existing user with user fields - { email: staged_user.email, location: 'value 2', color: 'redd' }, # existing staged user with user fields - { email: 'test2@discourse.org', location: 'value 3' } # new staged user with user fields - ] + { email: "test@discourse.org" }, # new user without user fields + { email: user.email, location: "value 1", color: "blue" }, # existing user with user fields + { email: staged_user.email, location: "value 2", color: "redd" }, # existing staged user with user fields + { email: "test2@discourse.org", location: "value 3" }, # new staged user with user fields + ], ) expect(Invite.count).to eq(3) - expect(User.where(staged: true).find_by_email('test@discourse.org')).to eq(nil) - expect(user.user_fields[user_field.id.to_s]).to eq('value 1') - expect(user.user_fields[user_field_color.id.to_s]).to eq('Blue') - expect(staged_user.user_fields[user_field.id.to_s]).to eq('value 2') + expect(User.where(staged: true).find_by_email("test@discourse.org")).to eq(nil) + expect(user.user_fields[user_field.id.to_s]).to eq("value 1") + expect(user.user_fields[user_field_color.id.to_s]).to eq("Blue") + expect(staged_user.user_fields[user_field.id.to_s]).to eq("value 2") expect(staged_user.user_fields[user_field_color.id.to_s]).to eq(nil) - new_staged_user = User.where(staged: true).find_by_email('test2@discourse.org') - expect(new_staged_user.user_fields[user_field.id.to_s]).to eq('value 3') + new_staged_user = User.where(staged: true).find_by_email("test2@discourse.org") + expect(new_staged_user.user_fields[user_field.id.to_s]).to eq("value 3") end - context 'when there are more than 200 invites' do + context "when there are more than 200 invites" do let(:bulk_invites) { [] } - before do - 202.times do |i| - bulk_invites << { "email": "test_#{i}@discourse.org" } - end - end + before { 202.times { |i| bulk_invites << { email: "test_#{i}@discourse.org" } } } - it 'rate limits email sending' do - described_class.new.execute( - current_user_id: admin.id, - invites: bulk_invites - ) + it "rate limits email sending" do + described_class.new.execute(current_user_id: admin.id, invites: bulk_invites) invite = Invite.last - expect(invite.email).to eq('test_201@discourse.org') + expect(invite.email).to eq("test_201@discourse.org") expect(invite.emailed_status).to eq(Invite.emailed_status_types[:bulk_pending]) expect(Jobs::ProcessBulkInviteEmails.jobs.size).to eq(1) end diff --git a/spec/jobs/bump_topic_spec.rb b/spec/jobs/bump_topic_spec.rb index ed7668d9c5..ce4d2aecbe 100644 --- a/spec/jobs/bump_topic_spec.rb +++ b/spec/jobs/bump_topic_spec.rb @@ -31,5 +31,4 @@ RSpec.describe Jobs::BumpTopic do expect(topic.reload.public_topic_timer).to eq(nil) end - end diff --git a/spec/jobs/check_new_features_spec.rb b/spec/jobs/check_new_features_spec.rb index ab20272145..43fb9be37f 100644 --- a/spec/jobs/check_new_features_spec.rb +++ b/spec/jobs/check_new_features_spec.rb @@ -4,53 +4,40 @@ RSpec.describe Jobs::CheckNewFeatures do def build_feature_hash(id:, created_at:, discourse_version: "2.9.0.beta10") { id: id, - user_id: 89432, + user_id: 89_432, emoji: "👤", title: "New fancy feature!", description: "", link: "https://meta.discourse.org/t/-/238821", created_at: created_at.iso8601, updated_at: (created_at + 1.minutes).iso8601, - discourse_version: discourse_version + discourse_version: discourse_version, } end def stub_meta_new_features_endpoint(*features) - stub_request(:get, "https://meta.discourse.org/new-features.json") - .to_return( - status: 200, - body: JSON.dump(features), - headers: { - "Content-Type" => "application/json" - } - ) + stub_request(:get, "https://meta.discourse.org/new-features.json").to_return( + status: 200, + body: JSON.dump(features), + headers: { + "Content-Type" => "application/json", + }, + ) end fab!(:admin1) { Fabricate(:admin) } fab!(:admin2) { Fabricate(:admin) } let(:feature1) do - build_feature_hash( - id: 35, - created_at: 3.days.ago, - discourse_version: "2.8.1.beta12" - ) + build_feature_hash(id: 35, created_at: 3.days.ago, discourse_version: "2.8.1.beta12") end let(:feature2) do - build_feature_hash( - id: 34, - created_at: 2.days.ago, - discourse_version: "2.8.1.beta13" - ) + build_feature_hash(id: 34, created_at: 2.days.ago, discourse_version: "2.8.1.beta13") end let(:pending_feature) do - build_feature_hash( - id: 37, - created_at: 1.day.ago, - discourse_version: "2.8.1.beta14" - ) + build_feature_hash(id: 37, created_at: 1.day.ago, discourse_version: "2.8.1.beta14") end before do @@ -59,9 +46,7 @@ RSpec.describe Jobs::CheckNewFeatures do stub_meta_new_features_endpoint(feature1, feature2, pending_feature) end - after do - DiscourseUpdates.clean_state - end + after { DiscourseUpdates.clean_state } it "backfills last viewed feature for admins who don't have last viewed feature" do DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta12") @@ -70,8 +55,12 @@ RSpec.describe Jobs::CheckNewFeatures do described_class.new.execute({}) - expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id).iso8601).to eq(feature1[:created_at]) - expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id).iso8601).to eq(Time.zone.now.iso8601) + expect(DiscourseUpdates.get_last_viewed_feature_date(admin2.id).iso8601).to eq( + feature1[:created_at], + ) + expect(DiscourseUpdates.get_last_viewed_feature_date(admin1.id).iso8601).to eq( + Time.zone.now.iso8601, + ) end it "notifies admins about new features that are available in the site's version" do @@ -79,14 +68,18 @@ RSpec.describe Jobs::CheckNewFeatures do described_class.new.execute({}) - expect(admin1.notifications.where( - notification_type: Notification.types[:new_features], - read: false - ).count).to eq(1) - expect(admin2.notifications.where( - notification_type: Notification.types[:new_features], - read: false - ).count).to eq(1) + expect( + admin1 + .notifications + .where(notification_type: Notification.types[:new_features], read: false) + .count, + ).to eq(1) + expect( + admin2 + .notifications + .where(notification_type: Notification.types[:new_features], read: false) + .count, + ).to eq(1) end it "consolidates new features notifications" do @@ -94,10 +87,11 @@ RSpec.describe Jobs::CheckNewFeatures do described_class.new.execute({}) - notification = admin1.notifications.where( - notification_type: Notification.types[:new_features], - read: false - ).first + notification = + admin1 + .notifications + .where(notification_type: Notification.types[:new_features], read: false) + .first expect(notification).to be_present DiscourseUpdates.stubs(:current_version).returns("2.8.1.beta14") @@ -106,10 +100,11 @@ RSpec.describe Jobs::CheckNewFeatures do # old notification is destroyed expect(Notification.find_by(id: notification.id)).to eq(nil) - notification = admin1.notifications.where( - notification_type: Notification.types[:new_features], - read: false - ).first + notification = + admin1 + .notifications + .where(notification_type: Notification.types[:new_features], read: false) + .first # new notification is created expect(notification).to be_present end @@ -121,6 +116,8 @@ RSpec.describe Jobs::CheckNewFeatures do described_class.new.execute({}) expect(admin1.notifications.count).to eq(0) - expect(admin2.notifications.where(notification_type: Notification.types[:new_features]).count).to eq(1) + expect( + admin2.notifications.where(notification_type: Notification.types[:new_features]).count, + ).to eq(1) end end diff --git a/spec/jobs/clean_dismissed_topic_users_spec.rb b/spec/jobs/clean_dismissed_topic_users_spec.rb index 187ce7c79d..b6e411814b 100644 --- a/spec/jobs/clean_dismissed_topic_users_spec.rb +++ b/spec/jobs/clean_dismissed_topic_users_spec.rb @@ -5,13 +5,13 @@ RSpec.describe Jobs::CleanDismissedTopicUsers do fab!(:topic) { Fabricate(:topic, created_at: 5.hours.ago) } fab!(:dismissed_topic_user) { Fabricate(:dismissed_topic_user, user: user, topic: topic) } - describe '#delete_overdue_dismissals!' do - it 'does not delete when new_topic_duration_minutes is set to always' do + describe "#delete_overdue_dismissals!" do + it "does not delete when new_topic_duration_minutes is set to always" do user.user_option.update(new_topic_duration_minutes: User::NewTopicDuration::ALWAYS) expect { described_class.new.execute({}) }.not_to change { DismissedTopicUser.count } end - it 'deletes when new_topic_duration_minutes is set to since last visit' do + it "deletes when new_topic_duration_minutes is set to since last visit" do user.user_option.update(new_topic_duration_minutes: User::NewTopicDuration::LAST_VISIT) expect { described_class.new.execute({}) }.not_to change { DismissedTopicUser.count } @@ -19,7 +19,7 @@ RSpec.describe Jobs::CleanDismissedTopicUsers do expect { described_class.new.execute({}) }.to change { DismissedTopicUser.count }.by(-1) end - it 'deletes when new_topic_duration_minutes is set to created in the last day' do + it "deletes when new_topic_duration_minutes is set to created in the last day" do user.user_option.update(new_topic_duration_minutes: 1440) expect { described_class.new.execute({}) }.not_to change { DismissedTopicUser.count } @@ -28,7 +28,7 @@ RSpec.describe Jobs::CleanDismissedTopicUsers do end end - describe '#delete_over_the_limit_dismissals!' do + describe "#delete_over_the_limit_dismissals!" do fab!(:user2) { Fabricate(:user, created_at: 1.days.ago, previous_visit_at: 1.days.ago) } fab!(:topic2) { Fabricate(:topic, created_at: 6.hours.ago) } fab!(:topic3) { Fabricate(:topic, created_at: 2.hours.ago) } @@ -41,7 +41,7 @@ RSpec.describe Jobs::CleanDismissedTopicUsers do user2.user_option.update(new_topic_duration_minutes: User::NewTopicDuration::ALWAYS) end - it 'deletes over the limit dismissals' do + it "deletes over the limit dismissals" do described_class.new.execute({}) expect(dismissed_topic_user.reload).to be_present expect(dismissed_topic_user2.reload).to be_present diff --git a/spec/jobs/clean_up_associated_accounts_spec.rb b/spec/jobs/clean_up_associated_accounts_spec.rb index 433c5fcaac..cefcac82bd 100644 --- a/spec/jobs/clean_up_associated_accounts_spec.rb +++ b/spec/jobs/clean_up_associated_accounts_spec.rb @@ -6,12 +6,27 @@ RSpec.describe Jobs::CleanUpAssociatedAccounts do it "deletes the correct records" do freeze_time - last_week = UserAssociatedAccount.create!(provider_name: "twitter", provider_uid: "1", updated_at: 7.days.ago) - today = UserAssociatedAccount.create!(provider_name: "twitter", provider_uid: "12", updated_at: 12.hours.ago) - connected = UserAssociatedAccount.create!(provider_name: "twitter", provider_uid: "123", user: Fabricate(:user), updated_at: 12.hours.ago) + last_week = + UserAssociatedAccount.create!( + provider_name: "twitter", + provider_uid: "1", + updated_at: 7.days.ago, + ) + today = + UserAssociatedAccount.create!( + provider_name: "twitter", + provider_uid: "12", + updated_at: 12.hours.ago, + ) + connected = + UserAssociatedAccount.create!( + provider_name: "twitter", + provider_uid: "123", + user: Fabricate(:user), + updated_at: 12.hours.ago, + ) expect { subject }.to change { UserAssociatedAccount.count }.by(-1) expect(UserAssociatedAccount.all).to contain_exactly(today, connected) end - end diff --git a/spec/jobs/clean_up_email_logs_spec.rb b/spec/jobs/clean_up_email_logs_spec.rb index 3d0ec29b09..b4e7669675 100644 --- a/spec/jobs/clean_up_email_logs_spec.rb +++ b/spec/jobs/clean_up_email_logs_spec.rb @@ -5,9 +5,7 @@ RSpec.describe Jobs::CleanUpEmailLogs do fab!(:email_log2) { Fabricate(:email_log, created_at: 2.weeks.ago) } fab!(:email_log3) { Fabricate(:email_log, created_at: 2.days.ago) } - let!(:skipped_email_log) do - Fabricate(:skipped_email_log, created_at: 2.years.ago) - end + let!(:skipped_email_log) { Fabricate(:skipped_email_log, created_at: 2.years.ago) } fab!(:skipped_email_log2) { Fabricate(:skipped_email_log) } @@ -23,10 +21,6 @@ RSpec.describe Jobs::CleanUpEmailLogs do expect(EmailLog.all).to contain_exactly(email_log, email_log2, email_log3) - expect(SkippedEmailLog.all).to contain_exactly( - skipped_email_log, - skipped_email_log2 - ) + expect(SkippedEmailLog.all).to contain_exactly(skipped_email_log, skipped_email_log2) end - end diff --git a/spec/jobs/clean_up_inactive_users_spec.rb b/spec/jobs/clean_up_inactive_users_spec.rb index 55bc28625f..985659519d 100644 --- a/spec/jobs/clean_up_inactive_users_spec.rb +++ b/spec/jobs/clean_up_inactive_users_spec.rb @@ -4,22 +4,16 @@ RSpec.describe Jobs::CleanUpInactiveUsers do it "should clean up new users that have been inactive" do SiteSetting.clean_up_inactive_users_after_days = 0 - user = Fabricate(:user, - last_seen_at: 5.days.ago, - trust_level: TrustLevel.levels[:newuser] - ) + user = Fabricate(:user, last_seen_at: 5.days.ago, trust_level: TrustLevel.levels[:newuser]) Fabricate(:active_user) - Fabricate(:post, user: Fabricate(:user, - trust_level: TrustLevel.levels[:newuser], - last_seen_at: 5.days.ago - )).user + Fabricate( + :post, + user: Fabricate(:user, trust_level: TrustLevel.levels[:newuser], last_seen_at: 5.days.ago), + ).user - Fabricate(:user, - trust_level: TrustLevel.levels[:newuser], - last_seen_at: 2.days.ago - ) + Fabricate(:user, trust_level: TrustLevel.levels[:newuser], last_seen_at: 2.days.ago) Fabricate(:user, trust_level: TrustLevel.levels[:basic]) @@ -27,8 +21,7 @@ RSpec.describe Jobs::CleanUpInactiveUsers do SiteSetting.clean_up_inactive_users_after_days = 4 - expect { described_class.new.execute({}) } - .to change { User.count }.by(-1) + expect { described_class.new.execute({}) }.to change { User.count }.by(-1) expect(User.exists?(id: user.id)).to eq(false) end @@ -43,7 +36,8 @@ RSpec.describe Jobs::CleanUpInactiveUsers do it "doesn't delete inactive mods" do SiteSetting.clean_up_inactive_users_after_days = 4 - moderator = Fabricate(:moderator, last_seen_at: 5.days.ago, trust_level: TrustLevel.levels[:newuser]) + moderator = + Fabricate(:moderator, last_seen_at: 5.days.ago, trust_level: TrustLevel.levels[:newuser]) expect { described_class.new.execute({}) }.to_not change { User.count } expect(User.exists?(moderator.id)).to eq(true) diff --git a/spec/jobs/clean_up_post_reply_keys_spec.rb b/spec/jobs/clean_up_post_reply_keys_spec.rb index 10594c1ac5..bd20b4f1df 100644 --- a/spec/jobs/clean_up_post_reply_keys_spec.rb +++ b/spec/jobs/clean_up_post_reply_keys_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::CleanUpPostReplyKeys do - it 'removes old post_reply_keys' do + it "removes old post_reply_keys" do freeze_time reply_key1 = Fabricate(:post_reply_key, created_at: 1.day.ago) @@ -10,16 +10,12 @@ RSpec.describe Jobs::CleanUpPostReplyKeys do SiteSetting.disallow_reply_by_email_after_days = 0 - expect { Jobs::CleanUpPostReplyKeys.new.execute({}) } - .not_to change { PostReplyKey.count } + expect { Jobs::CleanUpPostReplyKeys.new.execute({}) }.not_to change { PostReplyKey.count } SiteSetting.disallow_reply_by_email_after_days = 2 - expect { Jobs::CleanUpPostReplyKeys.new.execute({}) } - .to change { PostReplyKey.count }.by(-1) + expect { Jobs::CleanUpPostReplyKeys.new.execute({}) }.to change { PostReplyKey.count }.by(-1) - expect(PostReplyKey.all).to contain_exactly( - reply_key1, reply_key2 - ) + expect(PostReplyKey.all).to contain_exactly(reply_key1, reply_key2) end end diff --git a/spec/jobs/clean_up_unused_staged_users_spec.rb b/spec/jobs/clean_up_unused_staged_users_spec.rb index daaad64912..f66bc60460 100644 --- a/spec/jobs/clean_up_unused_staged_users_spec.rb +++ b/spec/jobs/clean_up_unused_staged_users_spec.rb @@ -37,7 +37,7 @@ RSpec.describe Jobs::CleanUpUnusedStagedUsers do end end - context 'when staged user is not old enough' do + context "when staged user is not old enough" do before { staged_user.update!(created_at: 5.months.ago) } include_examples "does not delete" end diff --git a/spec/jobs/clean_up_uploads_spec.rb b/spec/jobs/clean_up_uploads_spec.rb index 461123394b..39ba932329 100644 --- a/spec/jobs/clean_up_uploads_spec.rb +++ b/spec/jobs/clean_up_uploads_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe Jobs::CleanUpUploads do - def fabricate_upload(attributes = {}) Fabricate(:upload, { created_at: 2.hours.ago }.merge(attributes)) end @@ -22,7 +21,6 @@ RSpec.describe Jobs::CleanUpUploads do end it "only runs upload cleanup every grace period / 2 time" do - SiteSetting.clean_orphan_uploads_grace_period_hours = 48 expired = fabricate_upload(created_at: 49.hours.ago) Jobs::CleanUpUploads.new.execute(nil) @@ -38,44 +36,31 @@ RSpec.describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) expect(Upload.exists?(id: upload.id)).to eq(false) - end it "deletes orphan uploads" do - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.to change { Upload.count }.by(-1) + expect do Jobs::CleanUpUploads.new.execute(nil) end.to change { Upload.count }.by(-1) expect(Upload.exists?(id: expired_upload.id)).to eq(false) end - describe 'unused callbacks' do - before do - Upload.add_unused_callback do |uploads| - uploads.where.not(id: expired_upload.id) - end - end + describe "unused callbacks" do + before { Upload.add_unused_callback { |uploads| uploads.where.not(id: expired_upload.id) } } - after do - Upload.reset_unused_callbacks - end + after { Upload.reset_unused_callbacks } - it 'does not delete uploads skipped by an unused callback' do - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.not_to change { Upload.count } + it "does not delete uploads skipped by an unused callback" do + expect do Jobs::CleanUpUploads.new.execute(nil) end.not_to change { Upload.count } expect(Upload.exists?(id: expired_upload.id)).to eq(true) end - it 'deletes other uploads not skipped by an unused callback' do + it "deletes other uploads not skipped by an unused callback" do expired_upload2 = fabricate_upload upload = fabricate_upload UploadReference.create(target: Fabricate(:post), upload: upload) - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.to change { Upload.count }.by(-1) + expect do Jobs::CleanUpUploads.new.execute(nil) end.to change { Upload.count }.by(-1) expect(Upload.exists?(id: expired_upload.id)).to eq(true) expect(Upload.exists?(id: expired_upload2.id)).to eq(false) @@ -83,33 +68,23 @@ RSpec.describe Jobs::CleanUpUploads do end end - describe 'in use callbacks' do - before do - Upload.add_in_use_callback do |upload| - expired_upload.id == upload.id - end - end + describe "in use callbacks" do + before { Upload.add_in_use_callback { |upload| expired_upload.id == upload.id } } - after do - Upload.reset_in_use_callbacks - end + after { Upload.reset_in_use_callbacks } - it 'does not delete uploads that are in use by callback' do - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.not_to change { Upload.count } + it "does not delete uploads that are in use by callback" do + expect do Jobs::CleanUpUploads.new.execute(nil) end.not_to change { Upload.count } expect(Upload.exists?(id: expired_upload.id)).to eq(true) end - it 'deletes other uploads that are not in use by callback' do + it "deletes other uploads that are not in use by callback" do expired_upload2 = fabricate_upload upload = fabricate_upload UploadReference.create(target: Fabricate(:post), upload: upload) - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.to change { Upload.count }.by(-1) + expect do Jobs::CleanUpUploads.new.execute(nil) end.to change { Upload.count }.by(-1) expect(Upload.exists?(id: expired_upload.id)).to eq(true) expect(Upload.exists?(id: expired_upload2.id)).to eq(false) @@ -117,27 +92,20 @@ RSpec.describe Jobs::CleanUpUploads do end end - describe 'when clean_up_uploads is disabled' do - before do - SiteSetting.clean_up_uploads = false - end + describe "when clean_up_uploads is disabled" do + before { SiteSetting.clean_up_uploads = false } - it 'should still delete invalid upload records' do - upload2 = fabricate_upload( - url: "", - retain_hours: nil - ) + it "should still delete invalid upload records" do + upload2 = fabricate_upload(url: "", retain_hours: nil) - expect do - Jobs::CleanUpUploads.new.execute(nil) - end.to change { Upload.count }.by(-1) + expect do Jobs::CleanUpUploads.new.execute(nil) end.to change { Upload.count }.by(-1) expect(Upload.exists?(id: expired_upload.id)).to eq(true) expect(Upload.exists?(id: upload2.id)).to eq(false) end end - it 'does not clean up upload site settings' do + it "does not clean up upload site settings" do begin original_provider = SiteSetting.provider SiteSetting.provider = SiteSettings::DbProvider.new(SiteSetting) @@ -161,8 +129,7 @@ RSpec.describe Jobs::CleanUpUploads do SiteSetting.large_icon = large_icon_upload SiteSetting.opengraph_image = opengraph_image_upload - SiteSetting.twitter_summary_large_image = - twitter_summary_large_image_upload + SiteSetting.twitter_summary_large_image = twitter_summary_large_image_upload SiteSetting.favicon = favicon_upload SiteSetting.apple_touch_icon = apple_touch_icon_upload @@ -170,20 +137,13 @@ RSpec.describe Jobs::CleanUpUploads do Jobs::CleanUpUploads.new.execute(nil) [ - logo_upload, - logo_small_upload, - digest_logo_upload, - mobile_logo_upload, - large_icon_upload, - opengraph_image_upload, - twitter_summary_large_image_upload, - favicon_upload, - apple_touch_icon_upload, - system_upload + logo_upload, logo_small_upload, digest_logo_upload, mobile_logo_upload, large_icon_upload, + opengraph_image_upload, twitter_summary_large_image_upload, favicon_upload, + apple_touch_icon_upload, system_upload, ].each { |record| expect(Upload.exists?(id: record.id)).to eq(true) } fabricate_upload - SiteSetting.opengraph_image = '' + SiteSetting.opengraph_image = "" Jobs::CleanUpUploads.new.execute(nil) ensure @@ -307,15 +267,19 @@ RSpec.describe Jobs::CleanUpUploads do upload2 = fabricate_upload upload3 = fabricate_upload - Fabricate(:reviewable_queued_post_topic, payload: { - raw: "#{upload.short_url}\n#{upload2.short_url}" - }) - - Fabricate(:reviewable_queued_post_topic, + Fabricate( + :reviewable_queued_post_topic, payload: { - raw: "#{upload3.short_url}" + raw: "#{upload.short_url}\n#{upload2.short_url}", }, - status: Reviewable.statuses[:rejected] + ) + + Fabricate( + :reviewable_queued_post_topic, + payload: { + raw: "#{upload3.short_url}", + }, + status: Reviewable.statuses[:rejected], ) Jobs::CleanUpUploads.new.execute(nil) @@ -350,7 +314,7 @@ RSpec.describe Jobs::CleanUpUploads do it "does not delete custom emojis" do upload = fabricate_upload - CustomEmoji.create!(name: 'test', upload: upload) + CustomEmoji.create!(name: "test", upload: upload) Jobs::CleanUpUploads.new.execute(nil) @@ -371,7 +335,12 @@ RSpec.describe Jobs::CleanUpUploads do it "does not delete theme setting uploads" do theme = Fabricate(:theme) theme_upload = fabricate_upload - ThemeSetting.create!(theme: theme, data_type: ThemeSetting.types[:upload], value: theme_upload.id.to_s, name: "my_setting_name") + ThemeSetting.create!( + theme: theme, + data_type: ThemeSetting.types[:upload], + value: theme_upload.id.to_s, + name: "my_setting_name", + ) Jobs::CleanUpUploads.new.execute(nil) @@ -390,10 +359,30 @@ RSpec.describe Jobs::CleanUpUploads do end it "deletes external upload stubs that have expired" do - external_stub1 = Fabricate(:external_upload_stub, status: ExternalUploadStub.statuses[:created], created_at: 10.minutes.ago) - external_stub2 = Fabricate(:external_upload_stub, status: ExternalUploadStub.statuses[:created], created_at: (ExternalUploadStub::CREATED_EXPIRY_HOURS.hours + 10.minutes).ago) - external_stub3 = Fabricate(:external_upload_stub, status: ExternalUploadStub.statuses[:uploaded], created_at: 10.minutes.ago) - external_stub4 = Fabricate(:external_upload_stub, status: ExternalUploadStub.statuses[:uploaded], created_at: (ExternalUploadStub::UPLOADED_EXPIRY_HOURS.hours + 10.minutes).ago) + external_stub1 = + Fabricate( + :external_upload_stub, + status: ExternalUploadStub.statuses[:created], + created_at: 10.minutes.ago, + ) + external_stub2 = + Fabricate( + :external_upload_stub, + status: ExternalUploadStub.statuses[:created], + created_at: (ExternalUploadStub::CREATED_EXPIRY_HOURS.hours + 10.minutes).ago, + ) + external_stub3 = + Fabricate( + :external_upload_stub, + status: ExternalUploadStub.statuses[:uploaded], + created_at: 10.minutes.ago, + ) + external_stub4 = + Fabricate( + :external_upload_stub, + status: ExternalUploadStub.statuses[:uploaded], + created_at: (ExternalUploadStub::UPLOADED_EXPIRY_HOURS.hours + 10.minutes).ago, + ) Jobs::CleanUpUploads.new.execute(nil) expect(ExternalUploadStub.pluck(:id)).to contain_exactly(external_stub1.id, external_stub3.id) end diff --git a/spec/jobs/clean_up_user_export_topics_spec.rb b/spec/jobs/clean_up_user_export_topics_spec.rb index 3bf13967d8..e26d612ad5 100644 --- a/spec/jobs/clean_up_user_export_topics_spec.rb +++ b/spec/jobs/clean_up_user_export_topics_spec.rb @@ -3,27 +3,29 @@ RSpec.describe Jobs::CleanUpUserExportTopics do fab!(:user) { Fabricate(:user) } - it 'should delete ancient user export system messages' do - post_en = SystemMessage.create_from_system_user( - user, - :csv_export_succeeded, - download_link: "http://example.com/download", - file_name: "xyz_en.gz", - file_size: "55", - export_title: "user_archive" - ) + it "should delete ancient user export system messages" do + post_en = + SystemMessage.create_from_system_user( + user, + :csv_export_succeeded, + download_link: "http://example.com/download", + file_name: "xyz_en.gz", + file_size: "55", + export_title: "user_archive", + ) topic_en = post_en.topic topic_en.update!(created_at: 5.days.ago) I18n.locale = :fr - post_fr = SystemMessage.create_from_system_user( - user, - :csv_export_succeeded, - download_link: "http://example.com/download", - file_name: "xyz_fr.gz", - file_size: "56", - export_title: "user_archive" - ) + post_fr = + SystemMessage.create_from_system_user( + user, + :csv_export_succeeded, + download_link: "http://example.com/download", + file_name: "xyz_fr.gz", + file_size: "56", + export_title: "user_archive", + ) topic_fr = post_fr.topic topic_fr.update!(created_at: 5.days.ago) diff --git a/spec/jobs/close_topic_spec.rb b/spec/jobs/close_topic_spec.rb index a3b24a82fd..fd7476897a 100644 --- a/spec/jobs/close_topic_spec.rb +++ b/spec/jobs/close_topic_spec.rb @@ -3,22 +3,15 @@ RSpec.describe Jobs::CloseTopic do fab!(:admin) { Fabricate(:admin) } - fab!(:topic) do - Fabricate(:topic_timer, user: admin).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: admin).topic } - it 'should be able to close a topic' do + it "should be able to close a topic" do freeze_time(61.minutes.from_now) do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) expect(topic.reload.closed).to eq(true) - expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_enabled_minutes', count: 61 - )) + expect(Post.last.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 61)) end end @@ -30,61 +23,47 @@ RSpec.describe Jobs::CloseTopic do end end - describe 'when trying to close a topic that has already been closed' do - it 'should delete the topic timer' do + describe "when trying to close a topic that has already been closed" do + it "should delete the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) topic.update!(closed: true) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) end end - describe 'when trying to close a topic that has been deleted' do - it 'should delete the topic timer' do + describe "when trying to close a topic that has been deleted" do + it "should delete the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) topic.trash! expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) end end - describe 'when user is no longer authorized to close topics' do + describe "when user is no longer authorized to close topics" do fab!(:user) { Fabricate(:user) } - fab!(:topic) do - Fabricate(:topic_timer, user: user).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: user).topic } - it 'should destroy the topic timer' do + it "should destroy the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) expect(topic.reload.closed).to eq(false) end it "should reconfigure topic timer if category's topics are set to autoclose" do - category = Fabricate(:category, - auto_close_based_on_last_post: true, - auto_close_hours: 5 - ) + category = Fabricate(:category, auto_close_based_on_last_post: true, auto_close_hours: 5) topic = Fabricate(:topic, category: category) topic.public_topic_timer.update!(user: user) @@ -92,12 +71,10 @@ RSpec.describe Jobs::CloseTopic do freeze_time(topic.public_topic_timer.execute_at + 1.minute) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) - end.to change { topic.reload.public_topic_timer.user }.from(user).to(Discourse.system_user) - .and change { topic.public_topic_timer.id } + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) + end.to change { topic.reload.public_topic_timer.user }.from(user).to( + Discourse.system_user, + ).and change { topic.public_topic_timer.id } expect(topic.reload.closed).to eq(false) end diff --git a/spec/jobs/correct_missing_dualstack_urls_spec.rb b/spec/jobs/correct_missing_dualstack_urls_spec.rb index 09c35a3abd..7af8e9ece7 100644 --- a/spec/jobs/correct_missing_dualstack_urls_spec.rb +++ b/spec/jobs/correct_missing_dualstack_urls_spec.rb @@ -1,60 +1,70 @@ # frozen_string_literal: true RSpec.describe Jobs::CorrectMissingDualstackUrls do - it 'corrects the urls' do + it "corrects the urls" do setup_s3 SiteSetting.s3_region = "us-east-1" SiteSetting.s3_upload_bucket = "custom-bucket" # we will only correct for our base_url, random urls will be left alone - expect(Discourse.store.absolute_base_url).to eq('//custom-bucket.s3.dualstack.us-east-1.amazonaws.com') - - current_upload = Upload.create!( - url: '//custom-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png', - original_filename: 'a.png', - filesize: 100, - user_id: -1, + expect(Discourse.store.absolute_base_url).to eq( + "//custom-bucket.s3.dualstack.us-east-1.amazonaws.com", ) - bad_upload = Upload.create!( - url: '//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png', - original_filename: 'a.png', - filesize: 100, - user_id: -1, - ) + current_upload = + Upload.create!( + url: "//custom-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png", + original_filename: "a.png", + filesize: 100, + user_id: -1, + ) - current_optimized = OptimizedImage.create!( - url: '//custom-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png', - filesize: 100, - upload_id: current_upload.id, - width: 100, - height: 100, - sha1: 'xxx', - extension: '.png' - ) + bad_upload = + Upload.create!( + url: "//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png", + original_filename: "a.png", + filesize: 100, + user_id: -1, + ) - bad_optimized = OptimizedImage.create!( - url: '//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png', - filesize: 100, - upload_id: current_upload.id, - width: 110, - height: 100, - sha1: 'xxx', - extension: '.png' - ) + current_optimized = + OptimizedImage.create!( + url: "//custom-bucket.s3-us-east-1.amazonaws.com/somewhere/a.png", + filesize: 100, + upload_id: current_upload.id, + width: 100, + height: 100, + sha1: "xxx", + extension: ".png", + ) + + bad_optimized = + OptimizedImage.create!( + url: "//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png", + filesize: 100, + upload_id: current_upload.id, + width: 110, + height: 100, + sha1: "xxx", + extension: ".png", + ) Jobs::CorrectMissingDualstackUrls.new.execute_onceoff(nil) bad_upload.reload - expect(bad_upload.url).to eq('//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png') + expect(bad_upload.url).to eq("//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png") current_upload.reload - expect(current_upload.url).to eq('//custom-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') + expect(current_upload.url).to eq( + "//custom-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png", + ) bad_optimized.reload - expect(bad_optimized.url).to eq('//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png') + expect(bad_optimized.url).to eq("//custom-bucket.s3-us-west-1.amazonaws.com/somewhere/a.png") current_optimized.reload - expect(current_optimized.url).to eq('//custom-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png') + expect(current_optimized.url).to eq( + "//custom-bucket.s3.dualstack.us-east-1.amazonaws.com/somewhere/a.png", + ) end end diff --git a/spec/jobs/crawl_topic_link_spec.rb b/spec/jobs/crawl_topic_link_spec.rb index 2ced470cff..95818fb234 100644 --- a/spec/jobs/crawl_topic_link_spec.rb +++ b/spec/jobs/crawl_topic_link_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe Jobs::CrawlTopicLink do - let(:job) { Jobs::CrawlTopicLink.new } it "needs a topic_link_id" do diff --git a/spec/jobs/create_linked_topic_spec.rb b/spec/jobs/create_linked_topic_spec.rb index 9583116469..d2c6244de5 100644 --- a/spec/jobs/create_linked_topic_spec.rb +++ b/spec/jobs/create_linked_topic_spec.rb @@ -2,15 +2,13 @@ RSpec.describe Jobs::CreateLinkedTopic do it "returns when the post cannot be found" do - expect { Jobs::CreateLinkedTopic.new.perform(post_id: 1, sync_exec: true) }.not_to raise_error + expect { Jobs::CreateLinkedTopic.new.execute(post_id: 1) }.not_to raise_error end - context 'with a post' do + context "with a post" do fab!(:category) { Fabricate(:category) } fab!(:topic) { Fabricate(:topic, category: category) } - fab!(:post) do - Fabricate(:post, topic: topic) - end + fab!(:post) { Fabricate(:post, topic: topic) } fab!(:user_1) { Fabricate(:user) } fab!(:user_2) { Fabricate(:user) } @@ -32,21 +30,40 @@ RSpec.describe Jobs::CreateLinkedTopic do Fabricate(:topic_user, notification_level: muted, topic: topic, user: user_2) end - it 'creates a linked topic' do - small_action_post = Fabricate(:post, topic: topic, post_type: Post.types[:small_action], action_code: "closed.enabled") + it "creates a linked topic" do + small_action_post = + Fabricate( + :post, + topic: topic, + post_type: Post.types[:small_action], + action_code: "closed.enabled", + ) Jobs::CreateLinkedTopic.new.execute(post_id: post.id) raw_title = topic.title topic.reload new_topic = Topic.last linked_topic = new_topic.linked_topic - expect(topic.title).to include(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 1)) - expect(topic.posts.last.raw).to include(I18n.t('create_linked_topic.small_action_post_raw', new_title: "[#{new_topic.title}](#{new_topic.url})")) - expect(new_topic.title).to include(I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 2)) + expect(topic.title).to include( + I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 1), + ) + expect(topic.posts.last.raw).to include( + I18n.t( + "create_linked_topic.small_action_post_raw", + new_title: "[#{new_topic.title}](#{new_topic.url})", + ), + ) + expect(new_topic.title).to include( + I18n.t("create_linked_topic.topic_title_with_sequence", topic_title: raw_title, count: 2), + ) expect(new_topic.first_post.raw).to include(topic.url) expect(new_topic.category.id).to eq(category.id) expect(new_topic.topic_users.count).to eq(3) - expect(new_topic.topic_users.pluck(:notification_level)).to contain_exactly(muted, tracking, watching) + expect(new_topic.topic_users.pluck(:notification_level)).to contain_exactly( + muted, + tracking, + watching, + ) expect(linked_topic.topic_id).to eq(new_topic.id) expect(linked_topic.original_topic_id).to eq(topic.id) expect(linked_topic.sequence).to eq(2) diff --git a/spec/jobs/create_recent_post_search_indexes_spec.rb b/spec/jobs/create_recent_post_search_indexes_spec.rb index fa7e9a931e..1d350d254b 100644 --- a/spec/jobs/create_recent_post_search_indexes_spec.rb +++ b/spec/jobs/create_recent_post_search_indexes_spec.rb @@ -13,21 +13,19 @@ RSpec.describe Jobs::CreateRecentPostSearchIndexes do Fabricate(:post) end - before do - SearchIndexer.enable - end + before { SearchIndexer.enable } - describe '#execute' do - it 'should not create the index if requried posts size has not been reached' do + describe "#execute" do + it "should not create the index if requried posts size has not been reached" do SiteSetting.search_recent_posts_size = 1 SiteSetting.search_enable_recent_regular_posts_offset_size = 3 - expect do - subject.execute({}) - end.to_not change { SiteSetting.search_recent_regular_posts_offset_post_id } + expect do subject.execute({}) end.to_not change { + SiteSetting.search_recent_regular_posts_offset_post_id + } end - it 'should create the right index' do + it "should create the right index" do SiteSetting.search_recent_posts_size = 1 SiteSetting.search_enable_recent_regular_posts_offset_size = 1 diff --git a/spec/jobs/create_user_reviewable_spec.rb b/spec/jobs/create_user_reviewable_spec.rb index 1a9634af94..3c9a2c2b37 100644 --- a/spec/jobs/create_user_reviewable_spec.rb +++ b/spec/jobs/create_user_reviewable_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe Jobs::CreateUserReviewable do - let(:user) { Fabricate(:user) } it "creates the reviewable" do @@ -11,9 +10,9 @@ RSpec.describe Jobs::CreateUserReviewable do reviewable = Reviewable.find_by(target: user) expect(reviewable).to be_present expect(reviewable.pending?).to eq(true) - expect(reviewable.payload['username']).to eq(user.username) - expect(reviewable.payload['name']).to eq(user.name) - expect(reviewable.payload['email']).to eq(user.email) + expect(reviewable.payload["username"]).to eq(user.username) + expect(reviewable.payload["name"]).to eq(user.name) + expect(reviewable.payload["email"]).to eq(user.email) end it "should not raise an error if there is a reviewable already" do @@ -37,7 +36,7 @@ RSpec.describe Jobs::CreateUserReviewable do reviewable = Reviewable.find_by(target: user) score = reviewable.reviewable_scores.first expect(score).to be_present - expect(score.reason).to eq('must_approve_users') + expect(score.reason).to eq("must_approve_users") end it "adds invite_only if enabled" do @@ -46,8 +45,7 @@ RSpec.describe Jobs::CreateUserReviewable do reviewable = Reviewable.find_by(target: user) score = reviewable.reviewable_scores.first expect(score).to be_present - expect(score.reason).to eq('invite_only') + expect(score.reason).to eq("invite_only") end end - end diff --git a/spec/jobs/dashboard_stats_spec.rb b/spec/jobs/dashboard_stats_spec.rb index 3f2304db9c..82aeaab122 100644 --- a/spec/jobs/dashboard_stats_spec.rb +++ b/spec/jobs/dashboard_stats_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true RSpec.describe ::Jobs::DashboardStats do - let(:group_message) { GroupMessage.new(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) } + let(:group_message) do + GroupMessage.new(Group[:admins].name, :dashboard_problems, limit_once_per: 7.days.to_i) + end def clear_recently_sent! # We won't immediately create new PMs due to the limit_once_per option, reset the value for testing purposes. Discourse.redis.del(group_message.sent_recently_key) end - after do - clear_recently_sent! - end + after { clear_recently_sent! } - it 'creates group message when problems are persistent for 2 days' do + it "creates group message when problems are persistent for 2 days" do Discourse.redis.setex(AdminDashboardData.problems_started_key, 14.days.to_i, Time.zone.now.to_s) expect { described_class.new.execute({}) }.not_to change { Topic.count } @@ -20,7 +20,7 @@ RSpec.describe ::Jobs::DashboardStats do expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) end - it 'replaces old message' do + it "replaces old message" do Discourse.redis.setex(AdminDashboardData.problems_started_key, 14.days.to_i, 3.days.ago) expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) old_topic = Topic.last @@ -32,14 +32,14 @@ RSpec.describe ::Jobs::DashboardStats do expect(new_topic.title).to eq(old_topic.title) end - it 'respects the sent_recently? check when deleting previous message' do + it "respects the sent_recently? check when deleting previous message" do Discourse.redis.setex(AdminDashboardData.problems_started_key, 14.days.to_i, 3.days.ago) expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) expect { described_class.new.execute({}) }.not_to change { Topic.count } end - it 'duplicates message if previous one has replies' do + it "duplicates message if previous one has replies" do Discourse.redis.setex(AdminDashboardData.problems_started_key, 14.days.to_i, 3.days.ago) expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) clear_recently_sent! @@ -48,7 +48,7 @@ RSpec.describe ::Jobs::DashboardStats do expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) end - it 'duplicates message if previous was 3 months ago' do + it "duplicates message if previous was 3 months ago" do freeze_time 3.months.ago do Discourse.redis.setex(AdminDashboardData.problems_started_key, 14.days.to_i, 3.days.ago) expect { described_class.new.execute({}) }.to change { Topic.count }.by(1) diff --git a/spec/jobs/delete_replies_spec.rb b/spec/jobs/delete_replies_spec.rb index 7bd9e219db..cab17ca543 100644 --- a/spec/jobs/delete_replies_spec.rb +++ b/spec/jobs/delete_replies_spec.rb @@ -5,21 +5,26 @@ RSpec.describe Jobs::DeleteReplies do fab!(:topic) { Fabricate(:topic) } fab!(:topic_timer) do - Fabricate(:topic_timer, status_type: TopicTimer.types[:delete_replies], duration_minutes: 2880, user: admin, topic: topic, execute_at: 2.days.from_now) + Fabricate( + :topic_timer, + status_type: TopicTimer.types[:delete_replies], + duration_minutes: 2880, + user: admin, + topic: topic, + execute_at: 2.days.from_now, + ) end - before do - 3.times { create_post(topic: topic) } - end + before { 3.times { create_post(topic: topic) } } it "can delete replies of a topic" do SiteSetting.skip_auto_delete_reply_likes = 0 freeze_time (2.days.from_now) - expect { - described_class.new.execute(topic_timer_id: topic_timer.id) - }.to change { topic.posts.count }.by(-2) + expect { described_class.new.execute(topic_timer_id: topic_timer.id) }.to change { + topic.posts.count + }.by(-2) topic_timer.reload expect(topic_timer.execute_at).to eq_time(2.day.from_now) @@ -32,8 +37,8 @@ RSpec.describe Jobs::DeleteReplies do topic.posts.last.update!(like_count: SiteSetting.skip_auto_delete_reply_likes + 1) - expect { - described_class.new.execute(topic_timer_id: topic_timer.id) - }.to change { topic.posts.count }.by(-1) + expect { described_class.new.execute(topic_timer_id: topic_timer.id) }.to change { + topic.posts.count + }.by(-1) end end diff --git a/spec/jobs/delete_topic_spec.rb b/spec/jobs/delete_topic_spec.rb index a65fb3052b..18ce649fdb 100644 --- a/spec/jobs/delete_topic_spec.rb +++ b/spec/jobs/delete_topic_spec.rb @@ -3,9 +3,7 @@ RSpec.describe Jobs::DeleteTopic do fab!(:admin) { Fabricate(:admin) } - fab!(:topic) do - Fabricate(:topic_timer, user: admin).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: admin).topic } let(:first_post) { create_post(topic: topic) } @@ -18,7 +16,6 @@ RSpec.describe Jobs::DeleteTopic do expect(topic.reload).to be_trashed expect(first_post.reload).to be_trashed expect(topic.reload.public_topic_timer).to eq(nil) - end it "should do nothing if topic is already deleted" do @@ -42,9 +39,7 @@ RSpec.describe Jobs::DeleteTopic do end describe "user isn't authorized to delete topics" do - let(:topic) { - Fabricate(:topic_timer, user: Fabricate(:user)).topic - } + let(:topic) { Fabricate(:topic_timer, user: Fabricate(:user)).topic } it "shouldn't delete the topic" do create_post(topic: topic) @@ -55,5 +50,4 @@ RSpec.describe Jobs::DeleteTopic do expect(topic.reload).to_not be_trashed end end - end diff --git a/spec/jobs/disable_bootstrap_mode_spec.rb b/spec/jobs/disable_bootstrap_mode_spec.rb index 3d5b8d4e13..9b53e49b28 100644 --- a/spec/jobs/disable_bootstrap_mode_spec.rb +++ b/spec/jobs/disable_bootstrap_mode_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::DisableBootstrapMode do - - describe '.execute' do + describe ".execute" do fab!(:admin) { Fabricate(:admin) } before do @@ -11,30 +10,28 @@ RSpec.describe Jobs::DisableBootstrapMode do SiteSetting.default_email_digest_frequency = 1440 end - it 'does not execute if bootstrap mode is already disabled' do + it "does not execute if bootstrap mode is already disabled" do SiteSetting.bootstrap_mode_enabled = false StaffActionLogger.any_instance.expects(:log_site_setting_change).never Jobs::DisableBootstrapMode.new.execute(user_id: admin.id) end - it 'turns off bootstrap mode if bootstrap_mode_min_users is set to 0' do + it "turns off bootstrap mode if bootstrap_mode_min_users is set to 0" do SiteSetting.bootstrap_mode_min_users = 0 StaffActionLogger.any_instance.expects(:log_site_setting_change).times(3) Jobs::DisableBootstrapMode.new.execute(user_id: admin.id) end - it 'does not amend setting that is not in bootstrap state' do + it "does not amend setting that is not in bootstrap state" do SiteSetting.bootstrap_mode_min_users = 0 SiteSetting.default_trust_level = TrustLevel[3] StaffActionLogger.any_instance.expects(:log_site_setting_change).twice Jobs::DisableBootstrapMode.new.execute(user_id: admin.id) end - it 'successfully turns off bootstrap mode' do + it "successfully turns off bootstrap mode" do SiteSetting.bootstrap_mode_min_users = 5 - 6.times do - Fabricate(:user) - end + 6.times { Fabricate(:user) } StaffActionLogger.any_instance.expects(:log_site_setting_change).times(3) Jobs::DisableBootstrapMode.new.execute(user_id: admin.id) end diff --git a/spec/jobs/download_avatar_from_url_spec.rb b/spec/jobs/download_avatar_from_url_spec.rb index 36ab8d446d..a3c36d257f 100644 --- a/spec/jobs/download_avatar_from_url_spec.rb +++ b/spec/jobs/download_avatar_from_url_spec.rb @@ -3,13 +3,10 @@ RSpec.describe Jobs::DownloadAvatarFromUrl do fab!(:user) { Fabricate(:user) } - describe 'when url is invalid' do - it 'should not raise any error' do + describe "when url is invalid" do + it "should not raise any error" do expect do - described_class.new.execute( - url: '/assets/something/nice.jpg', - user_id: user.id - ) + described_class.new.execute(url: "/assets/something/nice.jpg", user_id: user.id) end.to_not raise_error end end diff --git a/spec/jobs/download_backup_email_spec.rb b/spec/jobs/download_backup_email_spec.rb index 9b3325af2f..efafdfba60 100644 --- a/spec/jobs/download_backup_email_spec.rb +++ b/spec/jobs/download_backup_email_spec.rb @@ -4,17 +4,16 @@ RSpec.describe Jobs::DownloadBackupEmail do fab!(:user) { Fabricate(:admin) } it "should work" do - described_class.new.execute( - user_id: user.id, - backup_file_path: "http://some.example.test/" - ) + described_class.new.execute(user_id: user.id, backup_file_path: "http://some.example.test/") email = ActionMailer::Base.deliveries.last - expect(email.subject).to eq(I18n.t('download_backup_mailer.subject_template', - email_prefix: SiteSetting.title - )) + expect(email.subject).to eq( + I18n.t("download_backup_mailer.subject_template", email_prefix: SiteSetting.title), + ) - expect(email.body.raw_source).to include("http://some.example.test/?token=#{EmailBackupToken.get(user.id)}") + expect(email.body.raw_source).to include( + "http://some.example.test/?token=#{EmailBackupToken.get(user.id)}", + ) end end diff --git a/spec/jobs/download_profile_background_from_url_spec.rb b/spec/jobs/download_profile_background_from_url_spec.rb index 1870aa1062..0fbf682a84 100644 --- a/spec/jobs/download_profile_background_from_url_spec.rb +++ b/spec/jobs/download_profile_background_from_url_spec.rb @@ -3,13 +3,10 @@ RSpec.describe Jobs::DownloadProfileBackgroundFromUrl do fab!(:user) { Fabricate(:user) } - describe 'when url is invalid' do - it 'should not raise any error' do + describe "when url is invalid" do + it "should not raise any error" do expect do - described_class.new.execute( - url: '/assets/something/nice.jpg', - user_id: user.id - ) + described_class.new.execute(url: "/assets/something/nice.jpg", user_id: user.id) end.to_not raise_error end end diff --git a/spec/jobs/emit_web_hook_event_spec.rb b/spec/jobs/emit_web_hook_event_spec.rb index 5abe40e996..efddb33a04 100644 --- a/spec/jobs/emit_web_hook_event_spec.rb +++ b/spec/jobs/emit_web_hook_event_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'excon' +require "excon" RSpec.describe Jobs::EmitWebHookEvent do fab!(:post_hook) { Fabricate(:web_hook) } @@ -8,22 +8,22 @@ RSpec.describe Jobs::EmitWebHookEvent do fab!(:post) { Fabricate(:post) } fab!(:user) { Fabricate(:user) } - it 'raises an error when there is no web hook record' do - expect do - subject.execute(event_type: 'post', payload: {}) - end.to raise_error(Discourse::InvalidParameters) + it "raises an error when there is no web hook record" do + expect do subject.execute(event_type: "post", payload: {}) end.to raise_error( + Discourse::InvalidParameters, + ) end - it 'raises an error when there is no event type' do - expect do - subject.execute(web_hook_id: post_hook.id, payload: {}) - end.to raise_error(Discourse::InvalidParameters) + it "raises an error when there is no event type" do + expect do subject.execute(web_hook_id: post_hook.id, payload: {}) end.to raise_error( + Discourse::InvalidParameters, + ) end - it 'raises an error when there is no payload' do - expect do - subject.execute(web_hook_id: post_hook.id, event_type: 'post') - end.to raise_error(Discourse::InvalidParameters) + it "raises an error when there is no payload" do + expect do subject.execute(web_hook_id: post_hook.id, event_type: "post") end.to raise_error( + Discourse::InvalidParameters, + ) end it "should not destroy webhook event in case of error" do @@ -32,139 +32,127 @@ RSpec.describe Jobs::EmitWebHookEvent do subject.execute( web_hook_id: post_hook.id, payload: { id: post.id }.to_json, - event_type: WebHookEventType::POST + event_type: WebHookEventType::POST, ) expect(WebHookEvent.last.web_hook_id).to eq(post_hook.id) end - context 'when the web hook is failed' do - before do - SiteSetting.retry_web_hook_events = true - end + context "when the web hook is failed" do + before { SiteSetting.retry_web_hook_events = true } - context 'when the webhook has failed for 404 or 410' do + context "when the webhook has failed for 404 or 410" do before do - stub_request(:post, post_hook.payload_url).to_return(body: 'Invalid Access', status: response_status) + stub_request(:post, post_hook.payload_url).to_return( + body: "Invalid Access", + status: response_status, + ) end let(:response_status) { 410 } - it 'disables the webhook' do + it "disables the webhook" do expect do subject.execute( web_hook_id: post_hook.id, event_type: described_class::PING_EVENT, - retry_count: described_class::MAX_RETRY_COUNT + retry_count: described_class::MAX_RETRY_COUNT, ) end.to change { post_hook.reload.active }.to(false) end - it 'logs webhook deactivation reason' do + it "logs webhook deactivation reason" do subject.execute( web_hook_id: post_hook.id, event_type: described_class::PING_EVENT, - retry_count: described_class::MAX_RETRY_COUNT + retry_count: described_class::MAX_RETRY_COUNT, ) - user_history = UserHistory.find_by(action: UserHistory.actions[:web_hook_deactivate], acting_user: Discourse.system_user) + user_history = + UserHistory.find_by( + action: UserHistory.actions[:web_hook_deactivate], + acting_user: Discourse.system_user, + ) expect(user_history).to be_present - expect(user_history.context).to eq([ - "webhook_id: #{post_hook.id}", - "webhook_response_status: #{response_status}" - ].to_s) + expect(user_history.context).to eq( + ["webhook_id: #{post_hook.id}", "webhook_response_status: #{response_status}"].to_s, + ) end end - context 'when the webhook has failed' do + context "when the webhook has failed" do before do - stub_request(:post, post_hook.payload_url).to_return(body: 'Invalid Access', status: 403) + stub_request(:post, post_hook.payload_url).to_return(body: "Invalid Access", status: 403) end - it 'retry if site setting is enabled' do + it "retry if site setting is enabled" do expect do - subject.execute( - web_hook_id: post_hook.id, - event_type: described_class::PING_EVENT - ) + subject.execute(web_hook_id: post_hook.id, event_type: described_class::PING_EVENT) end.to change { Jobs::EmitWebHookEvent.jobs.size }.by(1) end - it 'retries at most 5 times' do + it "retries at most 5 times" do Jobs.run_immediately! expect(Jobs::EmitWebHookEvent::MAX_RETRY_COUNT + 1).to eq(5) expect do - subject.execute( - web_hook_id: post_hook.id, - event_type: described_class::PING_EVENT - ) + subject.execute(web_hook_id: post_hook.id, event_type: described_class::PING_EVENT) end.to change { WebHookEvent.count }.by(Jobs::EmitWebHookEvent::MAX_RETRY_COUNT + 1) end - it 'does not retry for more than maximum allowed times' do + it "does not retry for more than maximum allowed times" do expect do subject.execute( web_hook_id: post_hook.id, event_type: described_class::PING_EVENT, - retry_count: described_class::MAX_RETRY_COUNT + retry_count: described_class::MAX_RETRY_COUNT, ) end.to_not change { Jobs::EmitWebHookEvent.jobs.size } end - it 'does not retry if site setting is disabled' do + it "does not retry if site setting is disabled" do SiteSetting.retry_web_hook_events = false expect do - subject.execute( - web_hook_id: post_hook.id, - event_type: described_class::PING_EVENT - ) + subject.execute(web_hook_id: post_hook.id, event_type: described_class::PING_EVENT) end.not_to change { Jobs::EmitWebHookEvent.jobs.size } end - it 'properly logs error on rescue' do + it "properly logs error on rescue" do stub_request(:post, post_hook.payload_url).to_raise("connection error") - subject.execute( - web_hook_id: post_hook.id, - event_type: described_class::PING_EVENT - ) + subject.execute(web_hook_id: post_hook.id, event_type: described_class::PING_EVENT) event = WebHookEvent.last - expect(event.payload).to eq(MultiJson.dump(ping: 'OK')) + expect(event.payload).to eq(MultiJson.dump(ping: "OK")) expect(event.status).to eq(-1) - expect(MultiJson.load(event.response_headers)['error']).to eq('connection error') + expect(MultiJson.load(event.response_headers)["error"]).to eq("connection error") end end end - it 'does not raise an error for a ping event without payload' do - stub_request(:post, post_hook.payload_url) - .to_return(body: 'OK', status: 200) + it "does not raise an error for a ping event without payload" do + stub_request(:post, post_hook.payload_url).to_return(body: "OK", status: 200) - subject.execute( - web_hook_id: post_hook.id, - event_type: described_class::PING_EVENT - ) + subject.execute(web_hook_id: post_hook.id, event_type: described_class::PING_EVENT) end it "doesn't emit when the hook is inactive" do subject.execute( web_hook_id: inactive_hook.id, - event_type: 'post', - payload: { test: "some payload" }.to_json + event_type: "post", + payload: { test: "some payload" }.to_json, ) end - it 'emits normally with sufficient arguments' do - stub_request(:post, post_hook.payload_url) - .with(body: "{\"post\":{\"test\":\"some payload\"}}") - .to_return(body: 'OK', status: 200) + it "emits normally with sufficient arguments" do + stub_request(:post, post_hook.payload_url).with( + body: "{\"post\":{\"test\":\"some payload\"}}", + ).to_return(body: "OK", status: 200) subject.execute( web_hook_id: post_hook.id, - event_type: 'post', - payload: { test: "some payload" }.to_json + event_type: "post", + payload: { test: "some payload" }.to_json, ) end @@ -172,17 +160,19 @@ RSpec.describe Jobs::EmitWebHookEvent do FinalDestination::TestHelper.stub_to_fail do subject.execute( web_hook_id: post_hook.id, - event_type: 'post', - payload: { test: "some payload" }.to_json + event_type: "post", + payload: { test: "some payload" }.to_json, ) end event = post_hook.web_hook_events.last - expect(event.response_headers).to eq({ error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json) + expect(event.response_headers).to eq( + { error: I18n.t("webhooks.payload_url.blocked_or_internal") }.to_json, + ) expect(event.response_body).to eq(nil) expect(event.status).to eq(-1) end - context 'with category filters' do + context "with category filters" do fab!(:category) { Fabricate(:category) } fab!(:topic) { Fabricate(:topic) } fab!(:topic_with_category) { Fabricate(:topic, category_id: category.id) } @@ -191,27 +181,27 @@ RSpec.describe Jobs::EmitWebHookEvent do it "doesn't emit when event is not related with defined categories" do subject.execute( web_hook_id: topic_hook.id, - event_type: 'topic', + event_type: "topic", category_id: topic.category.id, - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end - it 'emit when event is related with defined categories' do - stub_request(:post, post_hook.payload_url) - .with(body: "{\"topic\":{\"test\":\"some payload\"}}") - .to_return(body: 'OK', status: 200) + it "emit when event is related with defined categories" do + stub_request(:post, post_hook.payload_url).with( + body: "{\"topic\":{\"test\":\"some payload\"}}", + ).to_return(body: "OK", status: 200) subject.execute( web_hook_id: topic_hook.id, - event_type: 'topic', + event_type: "topic", category_id: topic_with_category.category.id, - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end end - context 'with tag filters' do + context "with tag filters" do fab!(:tag) { Fabricate(:tag) } fab!(:topic) { Fabricate(:topic, tags: [tag]) } fab!(:topic_hook) { Fabricate(:topic_web_hook, tags: [tag]) } @@ -219,35 +209,35 @@ RSpec.describe Jobs::EmitWebHookEvent do it "doesn't emit when event is not included any tags" do subject.execute( web_hook_id: topic_hook.id, - event_type: 'topic', - payload: { test: "some payload" }.to_json + event_type: "topic", + payload: { test: "some payload" }.to_json, ) end it "doesn't emit when event is not related with defined tags" do subject.execute( web_hook_id: topic_hook.id, - event_type: 'topic', + event_type: "topic", tag_ids: [Fabricate(:tag).id], - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end - it 'emit when event is related with defined tags' do - stub_request(:post, post_hook.payload_url) - .with(body: "{\"topic\":{\"test\":\"some payload\"}}") - .to_return(body: 'OK', status: 200) + it "emit when event is related with defined tags" do + stub_request(:post, post_hook.payload_url).with( + body: "{\"topic\":{\"test\":\"some payload\"}}", + ).to_return(body: "OK", status: 200) subject.execute( web_hook_id: topic_hook.id, - event_type: 'topic', + event_type: "topic", tag_ids: topic.tags.pluck(:id), - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end end - context 'with group filters' do + context "with group filters" do fab!(:group) { Fabricate(:group) } fab!(:user) { Fabricate(:user, groups: [group]) } fab!(:like_hook) { Fabricate(:like_web_hook, groups: [group]) } @@ -255,38 +245,37 @@ RSpec.describe Jobs::EmitWebHookEvent do it "doesn't emit when event is not included any groups" do subject.execute( web_hook_id: like_hook.id, - event_type: 'like', - payload: { test: "some payload" }.to_json + event_type: "like", + payload: { test: "some payload" }.to_json, ) end it "doesn't emit when event is not related with defined groups" do subject.execute( web_hook_id: like_hook.id, - event_type: 'like', + event_type: "like", group_ids: [Fabricate(:group).id], - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end - it 'emit when event is related with defined groups' do - stub_request(:post, like_hook.payload_url) - .with(body: "{\"like\":{\"test\":\"some payload\"}}") - .to_return(body: 'OK', status: 200) + it "emit when event is related with defined groups" do + stub_request(:post, like_hook.payload_url).with( + body: "{\"like\":{\"test\":\"some payload\"}}", + ).to_return(body: "OK", status: 200) subject.execute( web_hook_id: like_hook.id, - event_type: 'like', + event_type: "like", group_ids: user.groups.pluck(:id), - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end end - describe '#send_webhook!' do - it 'creates delivery event record' do - stub_request(:post, post_hook.payload_url) - .to_return(body: 'OK', status: 200) + describe "#send_webhook!" do + it "creates delivery event record" do + stub_request(:post, post_hook.payload_url).to_return(body: "OK", status: 200) topic_event_type = WebHookEventType.all.first web_hook_id = Fabricate("#{topic_event_type.name}_web_hook").id @@ -295,55 +284,64 @@ RSpec.describe Jobs::EmitWebHookEvent do subject.execute( web_hook_id: web_hook_id, event_type: topic_event_type.name, - payload: { test: "some payload" }.to_json + payload: { test: "some payload" }.to_json, ) end.to change(WebHookEvent, :count).by(1) end - it 'sets up proper request headers' do - stub_request(:post, post_hook.payload_url) - .to_return(headers: { test: 'string' }, body: 'OK', status: 200) + it "sets up proper request headers" do + stub_request(:post, post_hook.payload_url).to_return( + headers: { + test: "string", + }, + body: "OK", + status: 200, + ) subject.execute( web_hook_id: post_hook.id, event_type: described_class::PING_EVENT, event_name: described_class::PING_EVENT, - payload: { test: "this payload shouldn't appear" }.to_json + payload: { test: "this payload shouldn't appear" }.to_json, ) event = WebHookEvent.last headers = MultiJson.load(event.headers) - expect(headers['Content-Length']).to eq("13") - expect(headers['Host']).to eq("meta.discourse.org") - expect(headers['X-Discourse-Event-Id']).to eq(event.id.to_s) - expect(headers['X-Discourse-Event-Type']).to eq(described_class::PING_EVENT) - expect(headers['X-Discourse-Event']).to eq(described_class::PING_EVENT) - expect(headers['X-Discourse-Event-Signature']).to eq('sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6') - expect(event.payload).to eq(MultiJson.dump(ping: 'OK')) + expect(headers["Content-Length"]).to eq("13") + expect(headers["Host"]).to eq("meta.discourse.org") + expect(headers["X-Discourse-Event-Id"]).to eq(event.id.to_s) + expect(headers["X-Discourse-Event-Type"]).to eq(described_class::PING_EVENT) + expect(headers["X-Discourse-Event"]).to eq(described_class::PING_EVENT) + expect(headers["X-Discourse-Event-Signature"]).to eq( + "sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6", + ) + expect(event.payload).to eq(MultiJson.dump(ping: "OK")) expect(event.status).to eq(200) - expect(MultiJson.load(event.response_headers)['test']).to eq('string') - expect(event.response_body).to eq('OK') + expect(MultiJson.load(event.response_headers)["test"]).to eq("string") + expect(event.response_body).to eq("OK") end - it 'sets up proper request headers when an error raised' do + it "sets up proper request headers when an error raised" do stub_request(:post, post_hook.payload_url).to_raise("error") subject.execute( web_hook_id: post_hook.id, event_type: described_class::PING_EVENT, event_name: described_class::PING_EVENT, - payload: { test: "this payload shouldn't appear" }.to_json + payload: { test: "this payload shouldn't appear" }.to_json, ) event = WebHookEvent.last headers = MultiJson.load(event.headers) - expect(headers['Content-Length']).to eq("13") - expect(headers['Host']).to eq("meta.discourse.org") - expect(headers['X-Discourse-Event-Id']).to eq(event.id.to_s) - expect(headers['X-Discourse-Event-Type']).to eq(described_class::PING_EVENT) - expect(headers['X-Discourse-Event']).to eq(described_class::PING_EVENT) - expect(headers['X-Discourse-Event-Signature']).to eq('sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6') - expect(event.payload).to eq(MultiJson.dump(ping: 'OK')) + expect(headers["Content-Length"]).to eq("13") + expect(headers["Host"]).to eq("meta.discourse.org") + expect(headers["X-Discourse-Event-Id"]).to eq(event.id.to_s) + expect(headers["X-Discourse-Event-Type"]).to eq(described_class::PING_EVENT) + expect(headers["X-Discourse-Event"]).to eq(described_class::PING_EVENT) + expect(headers["X-Discourse-Event-Signature"]).to eq( + "sha256=162f107f6b5022353274eb1a7197885cfd35744d8d08e5bcea025d309386b7d6", + ) + expect(event.payload).to eq(MultiJson.dump(ping: "OK")) end end end diff --git a/spec/jobs/enable_bootstrap_mode_spec.rb b/spec/jobs/enable_bootstrap_mode_spec.rb index 583caa041e..faaa5f8215 100644 --- a/spec/jobs/enable_bootstrap_mode_spec.rb +++ b/spec/jobs/enable_bootstrap_mode_spec.rb @@ -1,37 +1,36 @@ # frozen_string_literal: true RSpec.describe Jobs::EnableBootstrapMode do - - describe '.execute' do + describe ".execute" do fab!(:admin) { Fabricate(:admin) } - before do - SiteSetting.bootstrap_mode_enabled = false + before { SiteSetting.bootstrap_mode_enabled = false } + + it "raises an error when user_id is missing" do + expect { Jobs::EnableBootstrapMode.new.execute({}) }.to raise_error( + Discourse::InvalidParameters, + ) end - it 'raises an error when user_id is missing' do - expect { Jobs::EnableBootstrapMode.new.execute({}) }.to raise_error(Discourse::InvalidParameters) - end - - it 'does not execute if bootstrap mode is already enabled' do + it "does not execute if bootstrap mode is already enabled" do SiteSetting.bootstrap_mode_enabled = true StaffActionLogger.any_instance.expects(:log_site_setting_change).never Jobs::EnableBootstrapMode.new.execute(user_id: admin.id) end - it 'does not turn on bootstrap mode if first admin already exists' do + it "does not turn on bootstrap mode if first admin already exists" do first_admin = Fabricate(:admin) StaffActionLogger.any_instance.expects(:log_site_setting_change).never Jobs::EnableBootstrapMode.new.execute(user_id: admin.id) end - it 'does not amend setting that is not in default state' do + it "does not amend setting that is not in default state" do SiteSetting.default_trust_level = TrustLevel[3] StaffActionLogger.any_instance.expects(:log_site_setting_change).twice Jobs::EnableBootstrapMode.new.execute(user_id: admin.id) end - it 'successfully turns on bootstrap mode' do + it "successfully turns on bootstrap mode" do StaffActionLogger.any_instance.expects(:log_site_setting_change).times(3) Jobs::EnableBootstrapMode.new.execute(user_id: admin.id) end diff --git a/spec/jobs/enqueue_digest_emails_spec.rb b/spec/jobs/enqueue_digest_emails_spec.rb index 82b18c8c64..681060b9a6 100644 --- a/spec/jobs/enqueue_digest_emails_spec.rb +++ b/spec/jobs/enqueue_digest_emails_spec.rb @@ -1,54 +1,74 @@ # frozen_string_literal: true RSpec.describe Jobs::EnqueueDigestEmails do - - describe '#target_users' do - context 'with disabled digests' do + describe "#target_users" do + context "with disabled digests" do before { SiteSetting.default_email_digest_frequency = 0 } - let!(:user_no_digests) { Fabricate(:active_user, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) } + let!(:user_no_digests) do + Fabricate(:active_user, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) + end it "doesn't return users with email disabled" do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_no_digests.id)).to eq(false) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_no_digests.id)).to eq( + false, + ) end end - context 'with unapproved users' do - before do - SiteSetting.must_approve_users = true + context "with unapproved users" do + before { SiteSetting.must_approve_users = true } + + let!(:unapproved_user) do + Fabricate( + :active_user, + approved: false, + last_emailed_at: 8.days.ago, + last_seen_at: 10.days.ago, + ) end - let!(:unapproved_user) { Fabricate(:active_user, approved: false, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) } - - it 'should enqueue the right digest emails' do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(false) + it "should enqueue the right digest emails" do + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq( + false, + ) # As a moderator unapproved_user.update_column(:moderator, true) - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq( + true, + ) # As an admin unapproved_user.update(admin: true, moderator: false) - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq( + true, + ) # As an approved user unapproved_user.update(admin: false, moderator: false, approved: true) - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq( + true, + ) end end - context 'with staged users' do - let!(:staged_user) { Fabricate(:active_user, staged: true, last_emailed_at: 1.year.ago, last_seen_at: 1.year.ago) } + context "with staged users" do + let!(:staged_user) do + Fabricate(:active_user, staged: true, last_emailed_at: 1.year.ago, last_seen_at: 1.year.ago) + end it "doesn't return staged users" do expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(staged_user.id)).to eq(false) end end - context 'when recently emailed' do + context "when recently emailed" do let!(:user_emailed_recently) { Fabricate(:active_user, last_emailed_at: 6.days.ago) } it "doesn't return users who have been emailed recently" do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_emailed_recently.id)).to eq(false) + expect( + Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_emailed_recently.id), + ).to eq(false) end end @@ -56,19 +76,25 @@ RSpec.describe Jobs::EnqueueDigestEmails do let!(:inactive_user) { Fabricate(:user, active: false) } it "doesn't return users who have been emailed recently" do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(inactive_user.id)).to eq(false) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(inactive_user.id)).to eq( + false, + ) end end context "with suspended user" do - let!(:suspended_user) { Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) } + let!(:suspended_user) do + Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) + end it "doesn't return users who are suspended" do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(suspended_user.id)).to eq(false) + expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(suspended_user.id)).to eq( + false, + ) end end - context 'when visited the site this week' do + context "when visited the site this week" do let(:user_visited_this_week) { Fabricate(:active_user, last_seen_at: 6.days.ago) } it "doesn't return users who have been emailed recently" do @@ -77,23 +103,30 @@ RSpec.describe Jobs::EnqueueDigestEmails do end end - context 'when visited the site a year ago' do + context "when visited the site a year ago" do let!(:user_visited_a_year_ago) { Fabricate(:active_user, last_seen_at: 370.days.ago) } it "doesn't return the user who have not visited the site for more than 365 days" do - expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_visited_a_year_ago.id)).to eq(false) + expect( + Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_visited_a_year_ago.id), + ).to eq(false) end end - context 'with regular users' do - let!(:user) { Fabricate(:active_user, last_seen_at: (SiteSetting.suppress_digest_email_after_days - 1).days.ago) } + context "with regular users" do + let!(:user) do + Fabricate( + :active_user, + last_seen_at: (SiteSetting.suppress_digest_email_after_days - 1).days.ago, + ) + end it "returns the user" do expect(Jobs::EnqueueDigestEmails.new.target_user_ids).to eq([user.id]) end end - context 'with too many bounces' do + context "with too many bounces" do let!(:bounce_user) { Fabricate(:active_user, last_seen_at: 6.month.ago) } it "doesn't return users with too many bounces" do @@ -112,7 +145,7 @@ RSpec.describe Jobs::EnqueueDigestEmails do end end - describe '#execute' do + describe "#execute" do let(:user) { Fabricate(:user) } it "limits jobs enqueued per max_digests_enqueued_per_30_mins_per_site" do @@ -125,27 +158,29 @@ RSpec.describe Jobs::EnqueueDigestEmails do global_setting :max_digests_enqueued_per_30_mins_per_site, 1 expect_enqueued_with(job: :user_email, args: { type: :digest, user_id: user2.id }) do - expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change(Jobs::UserEmail.jobs, :size).by (1) + expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change( + Jobs::UserEmail.jobs, + :size, + ).by (1) end # The job didn't actually run, so fake the user_stat update user2.user_stat.update(digest_attempted_at: Time.zone.now) expect_enqueued_with(job: :user_email, args: { type: :digest, user_id: user1.id }) do - expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change(Jobs::UserEmail.jobs, :size).by (1) + expect { Jobs::EnqueueDigestEmails.new.execute(nil) }.to change( + Jobs::UserEmail.jobs, + :size, + ).by (1) end user1.user_stat.update(digest_attempted_at: Time.zone.now) - expect_not_enqueued_with(job: :user_email) do - Jobs::EnqueueDigestEmails.new.execute(nil) - end + expect_not_enqueued_with(job: :user_email) { Jobs::EnqueueDigestEmails.new.execute(nil) } end context "when digest emails are enabled" do - before do - Jobs::EnqueueDigestEmails.any_instance.expects(:target_user_ids).returns([user.id]) - end + before { Jobs::EnqueueDigestEmails.any_instance.expects(:target_user_ids).returns([user.id]) } it "enqueues the digest email job" do SiteSetting.disable_digest_emails = false diff --git a/spec/jobs/enqueue_suspect_users_spec.rb b/spec/jobs/enqueue_suspect_users_spec.rb index d1c52285ba..125e2e0de4 100644 --- a/spec/jobs/enqueue_suspect_users_spec.rb +++ b/spec/jobs/enqueue_suspect_users_spec.rb @@ -3,27 +3,28 @@ RSpec.describe Jobs::EnqueueSuspectUsers do before { SiteSetting.approve_suspect_users = true } - it 'does nothing when there are no suspect users' do + it "does nothing when there are no suspect users" do subject.execute({}) expect(ReviewableUser.count).to be_zero end - context 'with suspect users' do + context "with suspect users" do let!(:suspect_user) { Fabricate(:active_user, created_at: 1.day.ago) } - it 'creates a reviewable when there is a suspect user' do + it "creates a reviewable when there is a suspect user" do subject.execute({}) expect(ReviewableUser.count).to eq(1) end - it 'only creates one reviewable per user' do - review_user = ReviewableUser.needs_review!( - target: suspect_user, - created_by: Discourse.system_user, - reviewable_by_moderator: true - ) + it "only creates one reviewable per user" do + review_user = + ReviewableUser.needs_review!( + target: suspect_user, + created_by: Discourse.system_user, + reviewable_by_moderator: true, + ) subject.execute({}) @@ -31,14 +32,14 @@ RSpec.describe Jobs::EnqueueSuspectUsers do expect(ReviewableUser.last).to eq(review_user) end - it 'adds a score' do + it "adds a score" do subject.execute({}) score = ReviewableScore.last - expect(score.reason).to eq('suspect_user') + expect(score.reason).to eq("suspect_user") end - it 'only enqueues non-approved users' do + it "only enqueues non-approved users" do suspect_user.update!(approved: true) subject.execute({}) @@ -46,7 +47,7 @@ RSpec.describe Jobs::EnqueueSuspectUsers do expect(ReviewableUser.where(target: suspect_user).exists?).to eq(false) end - it 'does nothing if must_approve_users is set to true' do + it "does nothing if must_approve_users is set to true" do SiteSetting.must_approve_users = true suspect_user.update!(approved: false) @@ -55,7 +56,7 @@ RSpec.describe Jobs::EnqueueSuspectUsers do expect(ReviewableUser.where(target: suspect_user).exists?).to eq(false) end - it 'ignores users created more than six months ago' do + it "ignores users created more than six months ago" do suspect_user.update!(created_at: 1.year.ago) subject.execute({}) @@ -63,40 +64,50 @@ RSpec.describe Jobs::EnqueueSuspectUsers do expect(ReviewableUser.where(target: suspect_user).exists?).to eq(false) end - it 'ignores users that were imported from another site' do - suspect_user.upsert_custom_fields({ import_id: 'fake_id' }) + it "ignores users that were imported from another site" do + suspect_user.upsert_custom_fields({ import_id: "fake_id" }) subject.execute({}) expect(ReviewableUser.where(target: suspect_user).exists?).to eq(false) end - it 'enqueues a suspect users with custom fields' do - suspect_user.upsert_custom_fields({ field_a: 'value', field_b: 'value' }) + it "enqueues a suspect users with custom fields" do + suspect_user.upsert_custom_fields({ field_a: "value", field_b: "value" }) subject.execute({}) expect(ReviewableUser.where(target: suspect_user).exists?).to eq(true) end - it 'ignores imported users even if they have multiple custom fields' do - suspect_user.upsert_custom_fields({ field_a: 'value', field_b: 'value', import_id: 'fake_id' }) + it "ignores imported users even if they have multiple custom fields" do + suspect_user.upsert_custom_fields( + { field_a: "value", field_b: "value", import_id: "fake_id" }, + ) subject.execute({}) expect(ReviewableUser.where(target: suspect_user).exists?).to eq(false) end - it 'enqueues a suspect user with not enough time read' do - suspect_user.user_stat.update!(posts_read_count: 2, topics_entered: 2, time_read: 30.seconds.to_i) + it "enqueues a suspect user with not enough time read" do + suspect_user.user_stat.update!( + posts_read_count: 2, + topics_entered: 2, + time_read: 30.seconds.to_i, + ) subject.execute({}) expect(ReviewableUser.count).to eq(1) end - it 'ignores users if their time read is higher than one minute' do - suspect_user.user_stat.update!(posts_read_count: 2, topics_entered: 2, time_read: 2.minutes.to_i) + it "ignores users if their time read is higher than one minute" do + suspect_user.user_stat.update!( + posts_read_count: 2, + topics_entered: 2, + time_read: 2.minutes.to_i, + ) subject.execute({}) diff --git a/spec/jobs/export_csv_file_spec.rb b/spec/jobs/export_csv_file_spec.rb index c126cbc9a8..e395bddec5 100644 --- a/spec/jobs/export_csv_file_spec.rb +++ b/spec/jobs/export_csv_file_spec.rb @@ -1,40 +1,43 @@ # frozen_string_literal: true RSpec.describe Jobs::ExportCsvFile do - - describe '#execute' do + describe "#execute" do let(:other_user) { Fabricate(:user) } let(:admin) { Fabricate(:admin) } let(:action_log) { StaffActionLogger.new(admin).log_revoke_moderation(other_user) } - it 'raises an error when the entity is missing' do - expect { Jobs::ExportCsvFile.new.execute(user_id: admin.id) }.to raise_error(Discourse::InvalidParameters) + it "raises an error when the entity is missing" do + expect { Jobs::ExportCsvFile.new.execute(user_id: admin.id) }.to raise_error( + Discourse::InvalidParameters, + ) end - it 'works' do + it "works" do action_log begin expect do - Jobs::ExportCsvFile.new.execute( - user_id: admin.id, - entity: "staff_action" - ) + Jobs::ExportCsvFile.new.execute(user_id: admin.id, entity: "staff_action") end.to change { Upload.count }.by(1) system_message = admin.topics_allowed.last - expect(system_message.title).to eq(I18n.t( - "system_messages.csv_export_succeeded.subject_template", - export_title: "Staff Action" - )) + expect(system_message.title).to eq( + I18n.t( + "system_messages.csv_export_succeeded.subject_template", + export_title: "Staff Action", + ), + ) upload = system_message.first_post.uploads.first - expect(system_message.first_post.raw).to eq(I18n.t( - "system_messages.csv_export_succeeded.text_body_template", - download_link: "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)" - ).chomp) + expect(system_message.first_post.raw).to eq( + I18n.t( + "system_messages.csv_export_succeeded.text_body_template", + download_link: + "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)", + ).chomp, + ) expect(system_message.id).to eq(UserExport.last.topic_id) expect(system_message.closed).to eq(true) @@ -51,31 +54,35 @@ RSpec.describe Jobs::ExportCsvFile do end end - describe '.report_export' do - + describe ".report_export" do let(:user) { Fabricate(:admin) } let(:exporter) do exporter = Jobs::ExportCsvFile.new - exporter.entity = 'report' - exporter.extra = HashWithIndifferentAccess.new(start_date: '2010-01-01', end_date: '2011-01-01') + exporter.entity = "report" + exporter.extra = + HashWithIndifferentAccess.new(start_date: "2010-01-01", end_date: "2011-01-01") exporter.current_user = User.find_by(id: user.id) exporter end it "does not throw an error when the dates are invalid" do Jobs::ExportCsvFile.new.execute( - entity: 'report', + entity: "report", user_id: user.id, - args: { start_date: 'asdfasdf', end_date: 'not-a-date', name: 'dau_by_mau' } + args: { + start_date: "asdfasdf", + end_date: "not-a-date", + name: "dau_by_mau", + }, ) end - it 'works with single-column reports' do - user.user_visits.create!(visited_at: '2010-01-01', posts_read: 42) - Fabricate(:user).user_visits.create!(visited_at: '2010-01-03', posts_read: 420) + it "works with single-column reports" do + user.user_visits.create!(visited_at: "2010-01-01", posts_read: 42) + Fabricate(:user).user_visits.create!(visited_at: "2010-01-03", posts_read: 420) - exporter.extra['name'] = 'dau_by_mau' + exporter.extra["name"] = "dau_by_mau" report = exporter.report_export.to_a expect(report.first).to contain_exactly("Day", "Percent") @@ -83,16 +90,16 @@ RSpec.describe Jobs::ExportCsvFile do expect(report.third).to contain_exactly("2010-01-03", "50.0") end - it 'works with filters' do - user.user_visits.create!(visited_at: '2010-01-01', posts_read: 42) + it "works with filters" do + user.user_visits.create!(visited_at: "2010-01-01", posts_read: 42) group = Fabricate(:group) user1 = Fabricate(:user) group_user = Fabricate(:group_user, group: group, user: user1) - user1.user_visits.create!(visited_at: '2010-01-03', posts_read: 420) + user1.user_visits.create!(visited_at: "2010-01-03", posts_read: 420) - exporter.extra['name'] = 'visits' - exporter.extra['group'] = group.id + exporter.extra["name"] = "visits" + exporter.extra["group"] = group.id report = exporter.report_export.to_a expect(report.length).to eq(2) @@ -100,11 +107,11 @@ RSpec.describe Jobs::ExportCsvFile do expect(report.second).to contain_exactly("2010-01-03", "1") end - it 'works with single-column reports with default label' do - user.user_visits.create!(visited_at: '2010-01-01') - Fabricate(:user).user_visits.create!(visited_at: '2010-01-03') + it "works with single-column reports with default label" do + user.user_visits.create!(visited_at: "2010-01-01") + Fabricate(:user).user_visits.create!(visited_at: "2010-01-03") - exporter.extra['name'] = 'visits' + exporter.extra["name"] = "visits" report = exporter.report_export.to_a expect(report.first).to contain_exactly("Day", "Count") @@ -112,24 +119,33 @@ RSpec.describe Jobs::ExportCsvFile do expect(report.third).to contain_exactly("2010-01-03", "1") end - it 'works with multi-columns reports' do + it "works with multi-columns reports" do DiscourseIpInfo.stubs(:get).with("1.1.1.1").returns(location: "Earth") - user.user_auth_token_logs.create!(action: "login", client_ip: "1.1.1.1", created_at: '2010-01-01') + user.user_auth_token_logs.create!( + action: "login", + client_ip: "1.1.1.1", + created_at: "2010-01-01", + ) - exporter.extra['name'] = 'staff_logins' + exporter.extra["name"] = "staff_logins" report = exporter.report_export.to_a expect(report.first).to contain_exactly("User", "Location", "Login at") expect(report.second).to contain_exactly(user.username, "Earth", "2010-01-01 00:00:00 UTC") end - it 'works with topic reports' do - freeze_time DateTime.parse('2010-01-01 6:00') + it "works with topic reports" do + freeze_time DateTime.parse("2010-01-01 6:00") - exporter.extra['name'] = 'top_referred_topics' + exporter.extra["name"] = "top_referred_topics" post1 = Fabricate(:post) post2 = Fabricate(:post) - IncomingLink.add(host: "a.com", referer: "http://twitter.com", post_id: post1.id, ip_address: '1.1.1.1') + IncomingLink.add( + host: "a.com", + referer: "http://twitter.com", + post_id: post1.id, + ip_address: "1.1.1.1", + ) report = exporter.report_export.to_a @@ -137,20 +153,20 @@ RSpec.describe Jobs::ExportCsvFile do expect(report.second).to contain_exactly(post1.topic.id.to_s, "1") end - it 'works with stacked_chart reports' do - ApplicationRequest.create!(date: '2010-01-01', req_type: 'page_view_logged_in', count: 1) - ApplicationRequest.create!(date: '2010-01-02', req_type: 'page_view_logged_in', count: 2) - ApplicationRequest.create!(date: '2010-01-03', req_type: 'page_view_logged_in', count: 3) + it "works with stacked_chart reports" do + ApplicationRequest.create!(date: "2010-01-01", req_type: "page_view_logged_in", count: 1) + ApplicationRequest.create!(date: "2010-01-02", req_type: "page_view_logged_in", count: 2) + ApplicationRequest.create!(date: "2010-01-03", req_type: "page_view_logged_in", count: 3) - ApplicationRequest.create!(date: '2010-01-01', req_type: 'page_view_anon', count: 4) - ApplicationRequest.create!(date: '2010-01-02', req_type: 'page_view_anon', count: 5) - ApplicationRequest.create!(date: '2010-01-03', req_type: 'page_view_anon', count: 6) + ApplicationRequest.create!(date: "2010-01-01", req_type: "page_view_anon", count: 4) + ApplicationRequest.create!(date: "2010-01-02", req_type: "page_view_anon", count: 5) + ApplicationRequest.create!(date: "2010-01-03", req_type: "page_view_anon", count: 6) - ApplicationRequest.create!(date: '2010-01-01', req_type: 'page_view_crawler', count: 7) - ApplicationRequest.create!(date: '2010-01-02', req_type: 'page_view_crawler', count: 8) - ApplicationRequest.create!(date: '2010-01-03', req_type: 'page_view_crawler', count: 9) + ApplicationRequest.create!(date: "2010-01-01", req_type: "page_view_crawler", count: 7) + ApplicationRequest.create!(date: "2010-01-02", req_type: "page_view_crawler", count: 8) + ApplicationRequest.create!(date: "2010-01-03", req_type: "page_view_crawler", count: 9) - exporter.extra['name'] = 'consolidated_page_views' + exporter.extra["name"] = "consolidated_page_views" report = exporter.report_export.to_a expect(report[0]).to contain_exactly("Day", "Logged in users", "Anonymous users", "Crawlers") @@ -159,37 +175,74 @@ RSpec.describe Jobs::ExportCsvFile do expect(report[3]).to contain_exactly("2010-01-03", "3", "6", "9") end - it 'works with posts reports and filters' do + it "works with posts reports and filters" do category = Fabricate(:category) subcategory = Fabricate(:category, parent_category: category) - Fabricate(:post, topic: Fabricate(:topic, category: category), created_at: '2010-01-01 12:00:00 UTC') - Fabricate(:post, topic: Fabricate(:topic, category: subcategory), created_at: '2010-01-01 12:00:00 UTC') + Fabricate( + :post, + topic: Fabricate(:topic, category: category), + created_at: "2010-01-01 12:00:00 UTC", + ) + Fabricate( + :post, + topic: Fabricate(:topic, category: subcategory), + created_at: "2010-01-01 12:00:00 UTC", + ) - exporter.extra['name'] = 'posts' + exporter.extra["name"] = "posts" - exporter.extra['category'] = category.id + exporter.extra["category"] = category.id report = exporter.report_export.to_a expect(report[0]).to contain_exactly("Count", "Day") expect(report[1]).to contain_exactly("1", "2010-01-01") - exporter.extra['include_subcategories'] = true + exporter.extra["include_subcategories"] = true report = exporter.report_export.to_a expect(report[0]).to contain_exactly("Count", "Day") expect(report[1]).to contain_exactly("2", "2010-01-01") end end - let(:user_list_header) { - %w{ - id name username email title created_at last_seen_at last_posted_at - last_emailed_at trust_level approved suspended_at suspended_till blocked - active admin moderator ip_address staged secondary_emails topics_entered - posts_read_count time_read topic_count post_count likes_given - likes_received location website views external_id external_email - external_username external_name external_avatar_url - } - } + let(:user_list_header) do + %w[ + id + name + username + email + title + created_at + last_seen_at + last_posted_at + last_emailed_at + trust_level + approved + suspended_at + suspended_till + blocked + active + admin + moderator + ip_address + staged + secondary_emails + topics_entered + posts_read_count + time_read + topic_count + post_count + likes_given + likes_received + location + website + views + external_id + external_email + external_username + external_name + external_avatar_url + ] + end let(:user_list_export) { Jobs::ExportCsvFile.new.user_list_export } @@ -207,12 +260,16 @@ RSpec.describe Jobs::ExportCsvFile do expect(user["secondary_emails"].split(";")).to match_array(secondary_emails) end - it 'exports sso data' do + it "exports sso data" do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true user = Fabricate(:user) user.user_profile.update_column(:location, "La,La Land") - user.create_single_sign_on_record(external_id: "123", last_payload: "xxx", external_email: 'test@test.com') + user.create_single_sign_on_record( + external_id: "123", + last_payload: "xxx", + external_email: "test@test.com", + ) user = to_hash(user_list_export.find { |u| u[0].to_i == user.id }) diff --git a/spec/jobs/export_user_archive_spec.rb b/spec/jobs/export_user_archive_spec.rb index 2c047791b2..ea6f91cf1f 100644 --- a/spec/jobs/export_user_archive_spec.rb +++ b/spec/jobs/export_user_archive_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -require 'csv' +require "csv" RSpec.describe Jobs::ExportUserArchive do fab!(:user) { Fabricate(:user, username: "john_doe") } fab!(:user2) { Fabricate(:user) } let(:extra) { {} } - let(:job) { + let(:job) do j = Jobs::ExportUserArchive.new j.current_user = user j.extra = extra j - } - let(:component) { raise 'component not set' } + end + let(:component) { raise "component not set" } fab!(:admin) { Fabricate(:admin) } fab!(:category) { Fabricate(:category_with_definition, name: "User Archive Category") } @@ -22,13 +22,17 @@ RSpec.describe Jobs::ExportUserArchive do def make_component_csv data_rows = [] - csv_out = CSV.generate do |csv| - csv << job.get_header(component) - job.public_send(:"#{component}_export") do |row| - csv << row - data_rows << Jobs::ExportUserArchive::HEADER_ATTRS_FOR[component].zip(row.map(&:to_s)).to_h.with_indifferent_access + csv_out = + CSV.generate do |csv| + csv << job.get_header(component) + job.public_send(:"#{component}_export") do |row| + csv << row + data_rows << Jobs::ExportUserArchive::HEADER_ATTRS_FOR[component] + .zip(row.map(&:to_s)) + .to_h + .with_indifferent_access + end end - end [data_rows, csv_out] end @@ -36,16 +40,17 @@ RSpec.describe Jobs::ExportUserArchive do JSON.parse(MultiJson.dump(job.public_send(:"#{component}_export"))) end - describe '#execute' do + describe "#execute" do before do _ = post - user.user_profile.website = 'https://doe.example.com/john' + user.user_profile.website = "https://doe.example.com/john" user.user_profile.save # force a UserAuthTokenLog entry - env = create_request_env.merge( - 'HTTP_USER_AGENT' => 'MyWebBrowser', - 'REQUEST_PATH' => '/some_path/456852', - ) + env = + create_request_env.merge( + "HTTP_USER_AGENT" => "MyWebBrowser", + "REQUEST_PATH" => "/some_path/456852", + ) cookie_jar = ActionDispatch::Request.new(env).cookie_jar Discourse.current_user_provider.new(env).log_on_user(user, {}, cookie_jar) @@ -53,34 +58,37 @@ RSpec.describe Jobs::ExportUserArchive do PostAction.new(user: user, post: post, post_action_type_id: 5).save end - after do - user.uploads.each(&:destroy!) + after { user.uploads.each(&:destroy!) } + + it "raises an error when the user is missing" do + expect { Jobs::ExportCsvFile.new.execute(user_id: user.id + (1 << 20)) }.to raise_error( + Discourse::InvalidParameters, + ) end - it 'raises an error when the user is missing' do - expect { Jobs::ExportCsvFile.new.execute(user_id: user.id + (1 << 20)) }.to raise_error(Discourse::InvalidParameters) - end - - it 'works' do - expect do - Jobs::ExportUserArchive.new.execute( - user_id: user.id, - ) - end.to change { Upload.count }.by(1) + it "works" do + expect do Jobs::ExportUserArchive.new.execute(user_id: user.id) end.to change { + Upload.count + }.by(1) system_message = user.topics_allowed.last - expect(system_message.title).to eq(I18n.t( - "system_messages.csv_export_succeeded.subject_template", - export_title: "User Archive" - )) + expect(system_message.title).to eq( + I18n.t( + "system_messages.csv_export_succeeded.subject_template", + export_title: "User Archive", + ), + ) upload = system_message.first_post.uploads.first - expect(system_message.first_post.raw).to eq(I18n.t( - "system_messages.csv_export_succeeded.text_body_template", - download_link: "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.human_filesize})" - ).chomp) + expect(system_message.first_post.raw).to eq( + I18n.t( + "system_messages.csv_export_succeeded.text_body_template", + download_link: + "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.human_filesize})", + ).chomp, + ) expect(system_message.id).to eq(UserExport.last.topic_id) expect(system_message.closed).to eq(true) @@ -91,37 +99,46 @@ RSpec.describe Jobs::ExportUserArchive do end expect(files.size).to eq(Jobs::ExportUserArchive::COMPONENTS.length) - expect(files.find { |f| f == 'user_archive.csv' }).to_not be_nil - expect(files.find { |f| f == 'category_preferences.csv' }).to_not be_nil + expect(files.find { |f| f == "user_archive.csv" }).to_not be_nil + expect(files.find { |f| f == "category_preferences.csv" }).to_not be_nil end - it 'sends a message if it fails' do + it "sends a message if it fails" do SiteSetting.max_export_file_size_kb = 1 - expect do - Jobs::ExportUserArchive.new.execute( - user_id: user.id, - ) - end.not_to change { Upload.count } + expect do Jobs::ExportUserArchive.new.execute(user_id: user.id) end.not_to change { + Upload.count + } system_message = user.topics_allowed.last - expect(system_message.title).to eq(I18n.t("system_messages.csv_export_failed.subject_template")) + expect(system_message.title).to eq( + I18n.t("system_messages.csv_export_failed.subject_template"), + ) end end - describe 'user_archive posts' do - let(:component) { 'user_archive' } - let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id) } + describe "user_archive posts" do + let(:component) { "user_archive" } + let(:subsubcategory) do + Fabricate(:category_with_definition, parent_category_id: subcategory.id) + end let(:subsubtopic) { Fabricate(:topic, category: subsubcategory) } let(:subsubpost) { Fabricate(:post, user: user, topic: subsubtopic) } let(:normal_post) { Fabricate(:post, user: user, topic: topic) } - let(:reply) { PostCreator.new(user2, raw: 'asdf1234qwert7896', topic_id: topic.id, reply_to_post_number: normal_post.post_number).create } + let(:reply) do + PostCreator.new( + user2, + raw: "asdf1234qwert7896", + topic_id: topic.id, + reply_to_post_number: normal_post.post_number, + ).create + end let(:message) { Fabricate(:private_message_topic) } let(:message_post) { Fabricate(:post, user: user, topic: message) } - it 'properly exports posts' do + it "properly exports posts" do SiteSetting.max_category_nesting = 3 [reply, subsubpost, message_post] @@ -129,17 +146,19 @@ RSpec.describe Jobs::ExportUserArchive do rows = [] job.user_archive_export do |row| - rows << Jobs::ExportUserArchive::HEADER_ATTRS_FOR['user_archive'].zip(row).to_h + rows << Jobs::ExportUserArchive::HEADER_ATTRS_FOR["user_archive"].zip(row).to_h end expect(rows.length).to eq(3) - post1 = rows.find { |r| r['topic_title'] == topic.title } - post2 = rows.find { |r| r['topic_title'] == subsubtopic.title } - post3 = rows.find { |r| r['topic_title'] == message.title } + post1 = rows.find { |r| r["topic_title"] == topic.title } + post2 = rows.find { |r| r["topic_title"] == subsubtopic.title } + post3 = rows.find { |r| r["topic_title"] == message.title } expect(post1["categories"]).to eq("#{category.name}") - expect(post2["categories"]).to eq("#{category.name}|#{subcategory.name}|#{subsubcategory.name}") + expect(post2["categories"]).to eq( + "#{category.name}|#{subcategory.name}|#{subsubcategory.name}", + ) expect(post3["categories"]).to eq("-") expect(post1["is_pm"]).to eq(I18n.t("csv_export.boolean_no")) @@ -154,14 +173,14 @@ RSpec.describe Jobs::ExportUserArchive do expect(post2["post_cooked"]).to eq(subsubpost.cooked) expect(post3["post_cooked"]).to eq(message_post.cooked) - expect(post1['like_count']).to eq(1) - expect(post2['like_count']).to eq(0) + expect(post1["like_count"]).to eq(1) + expect(post2["like_count"]).to eq(0) - expect(post1['reply_count']).to eq(1) - expect(post2['reply_count']).to eq(0) + expect(post1["reply_count"]).to eq(1) + expect(post2["reply_count"]).to eq(0) end - it 'can export a post from a deleted category' do + it "can export a post from a deleted category" do cat2 = Fabricate(:category) topic2 = Fabricate(:topic, category: cat2, user: user) _post2 = Fabricate(:post, topic: topic2, user: user) @@ -185,11 +204,11 @@ RSpec.describe Jobs::ExportUserArchive do end end - describe 'preferences' do - let(:component) { 'preferences' } + describe "preferences" do + let(:component) { "preferences" } before do - user.user_profile.website = 'https://doe.example.com/john' + user.user_profile.website = "https://doe.example.com/john" user.user_profile.bio_raw = "I am John Doe\n\nHere I am" user.user_profile.save user.user_option.text_size = :smaller @@ -197,59 +216,60 @@ RSpec.describe Jobs::ExportUserArchive do user.user_option.save end - it 'properly includes the profile fields' do + it "properly includes the profile fields" do _serializer = job.preferences_export # puts MultiJson.dump(serializer, indent: 4) output = make_component_json - payload = output['user'] + payload = output["user"] - expect(payload['website']).to match('doe.example.com') - expect(payload['bio_raw']).to match("Doe\n\nHere") - expect(payload['user_option']['automatically_unpin_topics']).to eq(false) - expect(payload['user_option']['text_size']).to eq('smaller') + expect(payload["website"]).to match("doe.example.com") + expect(payload["bio_raw"]).to match("Doe\n\nHere") + expect(payload["user_option"]["automatically_unpin_topics"]).to eq(false) + expect(payload["user_option"]["text_size"]).to eq("smaller") end end - describe 'auth tokens' do - let(:component) { 'auth_tokens' } + describe "auth tokens" do + let(:component) { "auth_tokens" } before do - env = create_request_env.merge( - 'HTTP_USER_AGENT' => 'MyWebBrowser', - 'REQUEST_PATH' => '/some_path/456852', - ) + env = + create_request_env.merge( + "HTTP_USER_AGENT" => "MyWebBrowser", + "REQUEST_PATH" => "/some_path/456852", + ) cookie_jar = ActionDispatch::Request.new(env).cookie_jar Discourse.current_user_provider.new(env).log_on_user(user, {}, cookie_jar) end - it 'properly includes session records' do + it "properly includes session records" do data, _csv_out = make_component_csv expect(data.length).to eq(1) - expect(data[0]['user_agent']).to eq('MyWebBrowser') + expect(data[0]["user_agent"]).to eq("MyWebBrowser") end - context 'with auth token logs' do - let(:component) { 'auth_token_logs' } - it 'includes details such as the path' do + context "with auth token logs" do + let(:component) { "auth_token_logs" } + it "includes details such as the path" do data, _csv_out = make_component_csv expect(data.length).to eq(1) - expect(data[0]['action']).to eq('generate') - expect(data[0]['path']).to eq('/some_path/456852') + expect(data[0]["action"]).to eq("generate") + expect(data[0]["path"]).to eq("/some_path/456852") end end end - describe 'badges' do - let(:component) { 'badges' } + describe "badges" do + let(:component) { "badges" } let(:badge1) { Fabricate(:badge) } let(:badge2) { Fabricate(:badge, multiple_grant: true) } let(:badge3) { Fabricate(:badge, multiple_grant: true) } let(:day_ago) { 1.day.ago } - it 'properly includes badge records' do + it "properly includes badge records" do grant_start = Time.now.utc BadgeGranter.grant(badge1, user) BadgeGranter.grant(badge2, user) @@ -261,19 +281,19 @@ RSpec.describe Jobs::ExportUserArchive do data, _csv_out = make_component_csv expect(data.length).to eq(6) - expect(data[0]['badge_id']).to eq(badge1.id.to_s) - expect(data[0]['badge_name']).to eq(badge1.display_name) - expect(data[0]['featured_rank']).to_not eq('') - expect(DateTime.parse(data[0]['granted_at'])).to be >= DateTime.parse(grant_start.to_s) - expect(data[2]['granted_manually']).to eq('true') - expect(Post.find(data[3]['post_id'])).to_not be_nil + expect(data[0]["badge_id"]).to eq(badge1.id.to_s) + expect(data[0]["badge_name"]).to eq(badge1.display_name) + expect(data[0]["featured_rank"]).to_not eq("") + expect(DateTime.parse(data[0]["granted_at"])).to be >= DateTime.parse(grant_start.to_s) + expect(data[2]["granted_manually"]).to eq("true") + expect(Post.find(data[3]["post_id"])).to_not be_nil end end - describe 'bookmarks' do - let(:component) { 'bookmarks' } + describe "bookmarks" do + let(:component) { "bookmarks" } - let(:name) { 'Collect my thoughts on this' } + let(:name) { "Collect my thoughts on this" } let(:manager) { BookmarkManager.new(user) } let(:topic1) { Fabricate(:topic) } let(:post1) { Fabricate(:post, topic: topic1, post_number: 5) } @@ -284,15 +304,30 @@ RSpec.describe Jobs::ExportUserArchive do let(:reminder_at) { 1.day.from_now } it "properly includes bookmark records" do - now = freeze_time '2017-03-01 12:00' + now = freeze_time "2017-03-01 12:00" - bookmark1 = manager.create_for(bookmarkable_id: post1.id, bookmarkable_type: "Post", name: name) + bookmark1 = + manager.create_for(bookmarkable_id: post1.id, bookmarkable_type: "Post", name: name) update1_at = now + 1.hours - bookmark1.update(name: 'great food recipe', updated_at: update1_at) + bookmark1.update(name: "great food recipe", updated_at: update1_at) - manager.create_for(bookmarkable_id: post2.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at, options: { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] }) + manager.create_for( + bookmarkable_id: post2.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + options: { + auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent], + }, + ) twelve_hr_ago = freeze_time now - 12.hours - pending_reminder = manager.create_for(bookmarkable_id: post3.id, bookmarkable_type: "Post", name: name, reminder_at: now - 8.hours) + pending_reminder = + manager.create_for( + bookmarkable_id: post3.id, + bookmarkable_type: "Post", + name: name, + reminder_at: now - 8.hours, + ) freeze_time now tau_record = private_message_topic.topic_allowed_users.create!(user_id: user.id) @@ -305,35 +340,43 @@ RSpec.describe Jobs::ExportUserArchive do expect(data.length).to eq(4) - expect(data[0]['bookmarkable_id']).to eq(post1.id.to_s) - expect(data[0]['bookmarkable_type']).to eq("Post") - expect(data[0]['link']).to eq(post1.full_url) - expect(DateTime.parse(data[0]['updated_at'])).to eq(DateTime.parse(update1_at.to_s)) + expect(data[0]["bookmarkable_id"]).to eq(post1.id.to_s) + expect(data[0]["bookmarkable_type"]).to eq("Post") + expect(data[0]["link"]).to eq(post1.full_url) + expect(DateTime.parse(data[0]["updated_at"])).to eq(DateTime.parse(update1_at.to_s)) - expect(data[1]['name']).to eq(name) - expect(DateTime.parse(data[1]['reminder_at'])).to eq(DateTime.parse(reminder_at.to_s)) - expect(data[1]['auto_delete_preference']).to eq('when_reminder_sent') + expect(data[1]["name"]).to eq(name) + expect(DateTime.parse(data[1]["reminder_at"])).to eq(DateTime.parse(reminder_at.to_s)) + expect(data[1]["auto_delete_preference"]).to eq("when_reminder_sent") - expect(DateTime.parse(data[2]['created_at'])).to eq(DateTime.parse(twelve_hr_ago.to_s)) - expect(DateTime.parse(data[2]['reminder_last_sent_at'])).to eq(DateTime.parse(now.to_s)) - expect(data[2]['reminder_set_at']).to eq('') + expect(DateTime.parse(data[2]["created_at"])).to eq(DateTime.parse(twelve_hr_ago.to_s)) + expect(DateTime.parse(data[2]["reminder_last_sent_at"])).to eq(DateTime.parse(now.to_s)) + expect(data[2]["reminder_set_at"]).to eq("") - expect(data[3]['bookmarkable_id']).to eq(post4.id.to_s) - expect(data[3]['bookmarkable_type']).to eq("Post") - expect(data[3]['link']).to eq('') + expect(data[3]["bookmarkable_id"]).to eq(post4.id.to_s) + expect(data[3]["bookmarkable_type"]).to eq("Post") + expect(data[3]["link"]).to eq("") end end - describe 'category_preferences' do - let(:component) { 'category_preferences' } + describe "category_preferences" do + let(:component) { "category_preferences" } - let(:subsubcategory) { Fabricate(:category_with_definition, parent_category_id: subcategory.id, name: "User Archive Subcategory") } + let(:subsubcategory) do + Fabricate( + :category_with_definition, + parent_category_id: subcategory.id, + name: "User Archive Subcategory", + ) + end let(:announcements) { Fabricate(:category_with_definition, name: "Announcements") } let(:deleted_category) { Fabricate(:category, name: "Deleted Category") } let(:secure_category_group) { Fabricate(:group) } - let(:secure_category) { Fabricate(:private_category, group: secure_category_group, name: "Super Secret Category") } + let(:secure_category) do + Fabricate(:private_category, group: secure_category_group, name: "Super Secret Category") + end - let(:reset_at) { DateTime.parse('2017-03-01 12:00') } + let(:reset_at) { DateTime.parse("2017-03-01 12:00") } before do SiteSetting.max_category_nesting = 3 @@ -349,61 +392,83 @@ RSpec.describe Jobs::ExportUserArchive do end # Set Watching First Post on announcements, Tracking on subcategory, Muted on deleted, nothing on subsubcategory - CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:watching_first_post], announcements.id) - CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:tracking], subcategory.id) - CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], deleted_category.id) + CategoryUser.set_notification_level_for_category( + user, + NotificationLevels.all[:watching_first_post], + announcements.id, + ) + CategoryUser.set_notification_level_for_category( + user, + NotificationLevels.all[:tracking], + subcategory.id, + ) + CategoryUser.set_notification_level_for_category( + user, + NotificationLevels.all[:muted], + deleted_category.id, + ) deleted_category.destroy! end - it 'correctly exports the CategoryUser table, excluding deleted categories' do + it "correctly exports the CategoryUser table, excluding deleted categories" do data, _csv_out = make_component_csv - expect(data.find { |r| r['category_id'] == category.id.to_s }).to be_nil - expect(data.find { |r| r['category_id'] == deleted_category.id.to_s }).to be_nil + expect(data.find { |r| r["category_id"] == category.id.to_s }).to be_nil + expect(data.find { |r| r["category_id"] == deleted_category.id.to_s }).to be_nil expect(data.length).to eq(3) - data.sort! { |a, b| a['category_id'].to_i <=> b['category_id'].to_i } + data.sort! { |a, b| a["category_id"].to_i <=> b["category_id"].to_i } expect(data[0][:category_id]).to eq(subcategory.id.to_s) - expect(data[0][:notification_level].to_s).to eq('tracking') + expect(data[0][:notification_level].to_s).to eq("tracking") expect(DateTime.parse(data[0][:dismiss_new_timestamp])).to eq(reset_at) expect(data[1][:category_id]).to eq(subsubcategory.id.to_s) - expect(data[1][:category_names]).to eq("#{category.name}|#{subcategory.name}|#{subsubcategory.name}") - expect(data[1][:notification_level]).to eq('regular') + expect(data[1][:category_names]).to eq( + "#{category.name}|#{subcategory.name}|#{subsubcategory.name}", + ) + expect(data[1][:notification_level]).to eq("regular") expect(DateTime.parse(data[1][:dismiss_new_timestamp])).to eq(reset_at) expect(data[2][:category_id]).to eq(announcements.id.to_s) expect(data[2][:category_names]).to eq(announcements.name) - expect(data[2][:notification_level]).to eq('watching_first_post') - expect(data[2][:dismiss_new_timestamp]).to eq('') + expect(data[2][:notification_level]).to eq("watching_first_post") + expect(data[2][:dismiss_new_timestamp]).to eq("") end it "does not include any secure categories the user does not have access to, even if the user has a CategoryUser record" do - CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id) + CategoryUser.set_notification_level_for_category( + user, + NotificationLevels.all[:muted], + secure_category.id, + ) data, _csv_out = make_component_csv - expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(false) + expect(data.any? { |r| r["category_id"] == secure_category.id.to_s }).to eq(false) expect(data.length).to eq(3) end it "does include secure categories that the user has access to" do - CategoryUser.set_notification_level_for_category(user, NotificationLevels.all[:muted], secure_category.id) + CategoryUser.set_notification_level_for_category( + user, + NotificationLevels.all[:muted], + secure_category.id, + ) GroupUser.create!(user: user, group: secure_category_group) data, _csv_out = make_component_csv - expect(data.any? { |r| r['category_id'] == secure_category.id.to_s }).to eq(true) + expect(data.any? { |r| r["category_id"] == secure_category.id.to_s }).to eq(true) expect(data.length).to eq(4) end end - describe 'flags' do - let(:component) { 'flags' } + describe "flags" do + let(:component) { "flags" } let(:other_post) { Fabricate(:post, user: admin) } let(:post3) { Fabricate(:post) } let(:post4) { Fabricate(:post) } - it 'correctly exports flags' do + it "correctly exports flags" do result0 = PostActionCreator.notify_moderators(user, other_post, "helping out the admins") PostActionCreator.spam(user, post3) PostActionDestroyer.destroy(user, post3, :spam) @@ -414,30 +479,30 @@ RSpec.describe Jobs::ExportUserArchive do data, _csv_out = make_component_csv expect(data.length).to eq(4) - data.sort_by! { |row| row['post_id'].to_i } + data.sort_by! { |row| row["post_id"].to_i } - expect(data[0]['post_id']).to eq(other_post.id.to_s) - expect(data[0]['flag_type']).to eq('notify_moderators') - expect(data[0]['related_post_id']).to eq(result0.post_action.related_post_id.to_s) + expect(data[0]["post_id"]).to eq(other_post.id.to_s) + expect(data[0]["flag_type"]).to eq("notify_moderators") + expect(data[0]["related_post_id"]).to eq(result0.post_action.related_post_id.to_s) - expect(data[1]['flag_type']).to eq('spam') - expect(data[2]['flag_type']).to eq('inappropriate') - expect(data[1]['deleted_at']).to_not be_empty - expect(data[1]['deleted_by']).to eq('self') - expect(data[2]['deleted_at']).to be_empty + expect(data[1]["flag_type"]).to eq("spam") + expect(data[2]["flag_type"]).to eq("inappropriate") + expect(data[1]["deleted_at"]).to_not be_empty + expect(data[1]["deleted_by"]).to eq("self") + expect(data[2]["deleted_at"]).to be_empty - expect(data[3]['post_id']).to eq(post4.id.to_s) - expect(data[3]['flag_type']).to eq('off_topic') - expect(data[3]['deleted_at']).to be_empty + expect(data[3]["post_id"]).to eq(post4.id.to_s) + expect(data[3]["flag_type"]).to eq("off_topic") + expect(data[3]["deleted_at"]).to be_empty end end - describe 'likes' do - let(:component) { 'likes' } + describe "likes" do + let(:component) { "likes" } let(:other_post) { Fabricate(:post, user: admin) } let(:post3) { Fabricate(:post) } - it 'correctly exports likes' do + it "correctly exports likes" do PostActionCreator.like(user, other_post) PostActionCreator.like(user, post3) PostActionCreator.like(admin, post3) @@ -446,25 +511,27 @@ RSpec.describe Jobs::ExportUserArchive do data, _csv_out = make_component_csv expect(data.length).to eq(2) - data.sort_by! { |row| row['post_id'].to_i } + data.sort_by! { |row| row["post_id"].to_i } - expect(data[0]['post_id']).to eq(other_post.id.to_s) - expect(data[1]['post_id']).to eq(post3.id.to_s) - expect(data[1]['deleted_at']).to_not be_empty - expect(data[1]['deleted_by']).to eq('self') + expect(data[0]["post_id"]).to eq(other_post.id.to_s) + expect(data[1]["post_id"]).to eq(post3.id.to_s) + expect(data[1]["deleted_at"]).to_not be_empty + expect(data[1]["deleted_by"]).to eq("self") end end - describe 'queued posts' do - let(:component) { 'queued_posts' } + describe "queued posts" do + let(:component) { "queued_posts" } let(:reviewable_post) { Fabricate(:reviewable_queued_post, topic: topic, created_by: user) } - let(:reviewable_topic) { Fabricate(:reviewable_queued_post_topic, category: category, created_by: user) } + let(:reviewable_topic) do + Fabricate(:reviewable_queued_post_topic, category: category, created_by: user) + end - it 'correctly exports queued posts' do + it "correctly exports queued posts" do SiteSetting.tagging_enabled = true reviewable_post.perform(admin, :reject_post) - reviewable_topic.payload['tags'] = ['example_tag'] + reviewable_topic.payload["tags"] = ["example_tag"] result = reviewable_topic.perform(admin, :approve_post) expect(result.success?).to eq(true) @@ -475,35 +542,65 @@ RSpec.describe Jobs::ExportUserArchive do approved = data.find { |el| el["verdict"] === "approved" } rejected = data.find { |el| el["verdict"] === "rejected" } - expect(approved['other_json']).to match('example_tag') - expect(rejected['post_raw']).to eq('hello world post contents.') - expect(rejected['other_json']).to match('reply_to_post_number') + expect(approved["other_json"]).to match("example_tag") + expect(rejected["post_raw"]).to eq("hello world post contents.") + expect(rejected["other_json"]).to match("reply_to_post_number") end end - describe 'visits' do - let(:component) { 'visits' } + describe "visits" do + let(:component) { "visits" } - it 'correctly exports the UserVisit table' do - freeze_time '2017-03-01 12:00' + it "correctly exports the UserVisit table" do + freeze_time "2017-03-01 12:00" - UserVisit.create(user_id: user.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 10) - UserVisit.create(user_id: user.id, visited_at: 2.days.ago, posts_read: 2, mobile: false, time_read: 20) - UserVisit.create(user_id: user.id, visited_at: 1.week.ago, posts_read: 3, mobile: true, time_read: 30) - UserVisit.create(user_id: user.id, visited_at: 1.year.ago, posts_read: 4, mobile: false, time_read: 40) - UserVisit.create(user_id: user2.id, visited_at: 1.minute.ago, posts_read: 1, mobile: false, time_read: 50) + UserVisit.create( + user_id: user.id, + visited_at: 1.minute.ago, + posts_read: 1, + mobile: false, + time_read: 10, + ) + UserVisit.create( + user_id: user.id, + visited_at: 2.days.ago, + posts_read: 2, + mobile: false, + time_read: 20, + ) + UserVisit.create( + user_id: user.id, + visited_at: 1.week.ago, + posts_read: 3, + mobile: true, + time_read: 30, + ) + UserVisit.create( + user_id: user.id, + visited_at: 1.year.ago, + posts_read: 4, + mobile: false, + time_read: 40, + ) + UserVisit.create( + user_id: user2.id, + visited_at: 1.minute.ago, + posts_read: 1, + mobile: false, + time_read: 50, + ) data, _csv_out = make_component_csv # user2's data is not mixed in expect(data.length).to eq(4) - expect(data.find { |r| r['time_read'] == 50 }).to be_nil + expect(data.find { |r| r["time_read"] == 50 }).to be_nil - expect(data[0]['visited_at']).to eq('2016-03-01') - expect(data[0]['posts_read']).to eq('4') - expect(data[0]['time_read']).to eq('40') - expect(data[1]['mobile']).to eq('true') - expect(data[3]['visited_at']).to eq('2017-03-01') + expect(data[0]["visited_at"]).to eq("2016-03-01") + expect(data[0]["posts_read"]).to eq("4") + expect(data[0]["time_read"]).to eq("40") + expect(data[1]["mobile"]).to eq("true") + expect(data[3]["visited_at"]).to eq("2017-03-01") end end end diff --git a/spec/jobs/feature_topic_users_spec.rb b/spec/jobs/feature_topic_users_spec.rb index 3fa7eba092..4eecff5e54 100644 --- a/spec/jobs/feature_topic_users_spec.rb +++ b/spec/jobs/feature_topic_users_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Jobs::FeatureTopicUsers do Jobs::FeatureTopicUsers.new.execute(topic_id: 123) end - context 'with a topic' do + context "with a topic" do let!(:post) { create_post } let(:topic) { post.topic } fab!(:coding_horror) { Fabricate(:coding_horror) } diff --git a/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb b/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb index b6da24ac57..bd5a32a745 100644 --- a/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb +++ b/spec/jobs/fix_out_of_sync_user_uploaded_avatar_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::FixOutOfSyncUserUploadedAvatar do - it 'should fix out of sync user uploaded avatars' do + it "should fix out of sync user uploaded avatars" do user_with_custom_upload = Fabricate(:user) custom_upload1 = Fabricate(:upload, user: user_with_custom_upload) gravatar_upload1 = Fabricate(:upload, user: user_with_custom_upload) @@ -9,7 +9,7 @@ RSpec.describe Jobs::FixOutOfSyncUserUploadedAvatar do user_with_custom_upload.user_avatar.update!( custom_upload: custom_upload1, - gravatar_upload: gravatar_upload1 + gravatar_upload: gravatar_upload1, ) user_out_of_sync = Fabricate(:user) @@ -22,23 +22,19 @@ RSpec.describe Jobs::FixOutOfSyncUserUploadedAvatar do user_out_of_sync.user_avatar.update!( custom_upload: custom_upload2, - gravatar_upload: gravatar_upload2 + gravatar_upload: gravatar_upload2, ) user_without_uploaded_avatar = Fabricate(:user) gravatar_upload3 = Fabricate(:upload, user: user_without_uploaded_avatar) - user_without_uploaded_avatar.user_avatar.update!( - gravatar_upload: gravatar_upload3 - ) + user_without_uploaded_avatar.user_avatar.update!(gravatar_upload: gravatar_upload3) described_class.new.execute_onceoff({}) expect(user_with_custom_upload.reload.uploaded_avatar).to eq(custom_upload1) expect(user_out_of_sync.reload.uploaded_avatar).to eq(gravatar_upload2) - expect(user_without_uploaded_avatar.reload.uploaded_avatar) - .to eq(nil) - + expect(user_without_uploaded_avatar.reload.uploaded_avatar).to eq(nil) end end diff --git a/spec/jobs/fix_primary_emails_for_staged_users_spec.rb b/spec/jobs/fix_primary_emails_for_staged_users_spec.rb index 54a997c477..fbf4af9624 100644 --- a/spec/jobs/fix_primary_emails_for_staged_users_spec.rb +++ b/spec/jobs/fix_primary_emails_for_staged_users_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.describe Jobs::FixPrimaryEmailsForStagedUsers do - it 'should clean up duplicated staged users' do - common_email = 'test@reply' + it "should clean up duplicated staged users" do + common_email = "test@reply" staged_user = Fabricate(:user, staged: true, active: false) staged_user2 = Fabricate(:user, staged: true, active: false) @@ -24,11 +24,15 @@ RSpec.describe Jobs::FixPrimaryEmailsForStagedUsers do # it will raise error in https://github.com/discourse/discourse/blob/d0b027d88deeabf8bc105419f7d3fae0087091cd/app/models/user.rb#L942 WebHook.stubs(:generate_payload).returns(nil) - expect { described_class.new.execute_onceoff({}) } - .to change { User.count }.by(-2) - .and change { staged_user.posts.count }.by(3) + expect { described_class.new.execute_onceoff({}) }.to change { User.count }.by(-2).and change { + staged_user.posts.count + }.by(3) - expect(User.where('id > -2')).to contain_exactly(Discourse.system_user, staged_user, active_user) + expect(User.where("id > -2")).to contain_exactly( + Discourse.system_user, + staged_user, + active_user, + ) expect(staged_user.posts.all).to contain_exactly(post1, post2, post3) expect(staged_user.reload.email).to eq(common_email) end diff --git a/spec/jobs/fix_s3_etags_spec.rb b/spec/jobs/fix_s3_etags_spec.rb index 225837fed1..2f72829f08 100644 --- a/spec/jobs/fix_s3_etags_spec.rb +++ b/spec/jobs/fix_s3_etags_spec.rb @@ -2,9 +2,9 @@ RSpec.describe Jobs::FixS3Etags do let(:etag_with_quotes) { '"ETag"' } - let(:etag_without_quotes) { 'ETag' } + let(:etag_without_quotes) { "ETag" } - it 'should remove double quotes from etags' do + it "should remove double quotes from etags" do upload1 = Fabricate(:upload, etag: etag_with_quotes) upload2 = Fabricate(:upload, etag: etag_without_quotes) optimized = Fabricate(:optimized_image, etag: etag_with_quotes) diff --git a/spec/jobs/fix_user_usernames_and_groups_names_clash_spec.rb b/spec/jobs/fix_user_usernames_and_groups_names_clash_spec.rb index e237f9c6be..05ba09781d 100644 --- a/spec/jobs/fix_user_usernames_and_groups_names_clash_spec.rb +++ b/spec/jobs/fix_user_usernames_and_groups_names_clash_spec.rb @@ -1,15 +1,15 @@ # frozen_string_literal: true RSpec.describe Jobs::FixUserUsernamesAndGroupsNamesClash do - it 'update usernames of users that clashes with a group name' do + it "update usernames of users that clashes with a group name" do user = Fabricate(:user) - Fabricate(:user, username: 'test1') - group = Fabricate(:group, name: 'test') - user.update_columns(username: 'test', username_lower: 'test') + Fabricate(:user, username: "test1") + group = Fabricate(:group, name: "test") + user.update_columns(username: "test", username_lower: "test") Jobs::FixUserUsernamesAndGroupsNamesClash.new.execute({}) - expect(user.reload.username).to eq('test2') - expect(group.reload.name).to eq('test') + expect(user.reload.username).to eq("test2") + expect(group.reload.name).to eq("test") end end diff --git a/spec/jobs/grant_anniversary_badges_spec.rb b/spec/jobs/grant_anniversary_badges_spec.rb index ae5cad056c..5cee313087 100644 --- a/spec/jobs/grant_anniversary_badges_spec.rb +++ b/spec/jobs/grant_anniversary_badges_spec.rb @@ -100,9 +100,7 @@ RSpec.describe Jobs::GrantAnniversaryBadges do user = Fabricate(:user, created_at: 800.days.ago) Fabricate(:post, user: user, created_at: 450.days.ago) - freeze_time(400.days.ago) do - granter.execute({}) - end + freeze_time(400.days.ago) { granter.execute({}) } badge = user.user_badges.where(badge_id: Badge::Anniversary) expect(badge.count).to eq(1) diff --git a/spec/jobs/grant_new_user_of_the_month_badges_spec.rb b/spec/jobs/grant_new_user_of_the_month_badges_spec.rb index 419f709cad..9ac859a514 100644 --- a/spec/jobs/grant_new_user_of_the_month_badges_spec.rb +++ b/spec/jobs/grant_new_user_of_the_month_badges_spec.rb @@ -1,16 +1,15 @@ # frozen_string_literal: true RSpec.describe Jobs::GrantNewUserOfTheMonthBadges do - let(:granter) { described_class.new } it "runs correctly" do - freeze_time(DateTime.parse('2019-11-30 23:59 UTC')) + freeze_time(DateTime.parse("2019-11-30 23:59 UTC")) u0 = Fabricate(:user, created_at: 2.weeks.ago) BadgeGranter.grant(Badge.find(Badge::NewUserOfTheMonth), u0, created_at: Time.now) - freeze_time(DateTime.parse('2020-01-01 00:00 UTC')) + freeze_time(DateTime.parse("2020-01-01 00:00 UTC")) user = Fabricate(:user, created_at: 1.week.ago) p = Fabricate(:post, user: user) @@ -25,11 +24,11 @@ RSpec.describe Jobs::GrantNewUserOfTheMonthBadges do badges = user.user_badges.where(badge_id: Badge::NewUserOfTheMonth) expect(badges).to be_present - expect(badges.first.granted_at.to_s).to eq('2019-12-31 23:59:59 UTC') + expect(badges.first.granted_at.to_s).to eq("2019-12-31 23:59:59 UTC") end it "does not include people created after the previous month" do - freeze_time(DateTime.parse('2020-01-15 00:00 UTC')) + freeze_time(DateTime.parse("2020-01-15 00:00 UTC")) user = Fabricate(:user, created_at: 1.week.ago) p = Fabricate(:post, user: user) @@ -85,12 +84,12 @@ RSpec.describe Jobs::GrantNewUserOfTheMonthBadges do end it "does nothing if it's already been awarded in previous month" do - freeze_time(DateTime.parse('2019-11-30 23:59 UTC')) + freeze_time(DateTime.parse("2019-11-30 23:59 UTC")) u0 = Fabricate(:user, created_at: 2.weeks.ago) BadgeGranter.grant(Badge.find(Badge::NewUserOfTheMonth), u0, created_at: Time.now) - freeze_time(DateTime.parse('2019-12-01 00:00 UTC')) + freeze_time(DateTime.parse("2019-12-01 00:00 UTC")) user = Fabricate(:user, created_at: 1.week.ago) p = Fabricate(:post, user: user) @@ -107,7 +106,7 @@ RSpec.describe Jobs::GrantNewUserOfTheMonthBadges do expect(badge).to be_blank end - describe '.scores' do + describe ".scores" do def scores granter.scores(1.month.ago, Time.now) end @@ -223,7 +222,5 @@ RSpec.describe Jobs::GrantNewUserOfTheMonthBadges do expect(scores.keys.size).to eq(2) end - end - end diff --git a/spec/jobs/heartbeat_spec.rb b/spec/jobs/heartbeat_spec.rb index f62043e287..983b8832c5 100644 --- a/spec/jobs/heartbeat_spec.rb +++ b/spec/jobs/heartbeat_spec.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true RSpec.describe ::Jobs::Heartbeat do - after do - Discourse.disable_readonly_mode - end + after { Discourse.disable_readonly_mode } it "still enqueues heartbeats in readonly mode" do freeze_time 1.week.from_now diff --git a/spec/jobs/ignored_users_summary_spec.rb b/spec/jobs/ignored_users_summary_spec.rb index 1768161a7c..461d94081a 100644 --- a/spec/jobs/ignored_users_summary_spec.rb +++ b/spec/jobs/ignored_users_summary_spec.rb @@ -27,9 +27,7 @@ RSpec.describe Jobs::IgnoredUsersSummary do context "when no system message exists for the ignored users" do context "when threshold is not hit" do - before do - SiteSetting.ignored_users_count_message_threshold = 5 - end + before { SiteSetting.ignored_users_count_message_threshold = 5 } it "does nothing" do subject @@ -40,10 +38,13 @@ RSpec.describe Jobs::IgnoredUsersSummary do context "when threshold is hit" do it "creates a system message" do subject - posts = Post.joins(:topic).where(topics: { - archetype: Archetype.private_message, - subtype: TopicSubtype.system_message - }) + posts = + Post.joins(:topic).where( + topics: { + archetype: Archetype.private_message, + subtype: TopicSubtype.system_message, + }, + ) expect(posts.count).to eq(2) expect(posts.find { |post| post.raw.include?(matt.username) }).to be_present expect(posts.find { |post| post.raw.include?(john.username) }).to be_present @@ -53,9 +54,7 @@ RSpec.describe Jobs::IgnoredUsersSummary do context "when a system message already exists for the ignored users" do context "when threshold is not hit" do - before do - SiteSetting.ignored_users_count_message_threshold = 5 - end + before { SiteSetting.ignored_users_count_message_threshold = 5 } it "does nothing" do subject diff --git a/spec/jobs/invalidate_inactive_admins_spec.rb b/spec/jobs/invalidate_inactive_admins_spec.rb index 8994e9f730..7c070aa5c1 100644 --- a/spec/jobs/invalidate_inactive_admins_spec.rb +++ b/spec/jobs/invalidate_inactive_admins_spec.rb @@ -17,17 +17,15 @@ RSpec.describe Jobs::InvalidateInactiveAdmins do fab!(:not_seen_admin) { Fabricate(:admin, last_seen_at: 370.days.ago) } before { not_seen_admin.email_tokens.update_all(confirmed: true) } - context 'when invalidate_inactive_admin_email_after_days = 365' do - before do - SiteSetting.invalidate_inactive_admin_email_after_days = 365 - end + context "when invalidate_inactive_admin_email_after_days = 365" do + before { SiteSetting.invalidate_inactive_admin_email_after_days = 365 } - it 'marks email tokens as unconfirmed' do + it "marks email tokens as unconfirmed" do subject expect(not_seen_admin.reload.email_tokens.where(confirmed: true).exists?).to eq(false) end - it 'makes the user as not active and logs the action' do + it "makes the user as not active and logs the action" do subject expect(not_seen_admin.reload.active).to eq(false) @@ -36,17 +34,24 @@ RSpec.describe Jobs::InvalidateInactiveAdmins do expect(log.action).to eq(UserHistory.actions[:deactivate_user]) end - it 'adds a staff log' do + it "adds a staff log" do subject expect(not_seen_admin.reload.active).to eq(false) end - context 'with social logins' do + context "with social logins" do before do - UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: not_seen_admin.id, provider_uid: 100, info: { email: "bob@google.account.com" }) + UserAssociatedAccount.create!( + provider_name: "google_oauth2", + user_id: not_seen_admin.id, + provider_uid: 100, + info: { + email: "bob@google.account.com", + }, + ) end - it 'removes the social logins' do + it "removes the social logins" do subject expect(UserAssociatedAccount.where(user_id: not_seen_admin.id).exists?).to eq(false) end @@ -65,12 +70,10 @@ RSpec.describe Jobs::InvalidateInactiveAdmins do end end - context 'when invalidate_inactive_admin_email_after_days = 0 to disable this feature' do - before do - SiteSetting.invalidate_inactive_admin_email_after_days = 0 - end + context "when invalidate_inactive_admin_email_after_days = 0 to disable this feature" do + before { SiteSetting.invalidate_inactive_admin_email_after_days = 0 } - it 'does nothing' do + it "does nothing" do subject expect(active_admin.reload.active).to eq(true) expect(active_admin.email_tokens.where(confirmed: true).exists?).to eq(true) diff --git a/spec/jobs/invite_email_spec.rb b/spec/jobs/invite_email_spec.rb index ec4bae6c51..0d6b52d483 100644 --- a/spec/jobs/invite_email_spec.rb +++ b/spec/jobs/invite_email_spec.rb @@ -1,19 +1,18 @@ # frozen_string_literal: true RSpec.describe Jobs::InviteEmail do - - describe '.execute' do - - it 'raises an error when the invite_id is missing' do + describe ".execute" do + it "raises an error when the invite_id is missing" do expect { Jobs::InviteEmail.new.execute({}) }.to raise_error(Discourse::InvalidParameters) end - context 'with an invite id' do - - let (:mailer) { Mail::Message.new(to: 'eviltrout@test.domain') } + context "with an invite id" do + let (:mailer) { + Mail::Message.new(to: "eviltrout@test.domain") + } fab!(:invite) { Fabricate(:invite) } - it 'delegates to the test mailer' do + it "delegates to the test mailer" do Email::Sender.any_instance.expects(:send) InviteMailer.expects(:send_invite).with(invite, anything).returns(mailer) Jobs::InviteEmail.new.execute(invite_id: invite.id) diff --git a/spec/jobs/jobs_base_spec.rb b/spec/jobs/jobs_base_spec.rb index 6b267af9c3..a76fed09e3 100644 --- a/spec/jobs/jobs_base_spec.rb +++ b/spec/jobs/jobs_base_spec.rb @@ -22,14 +22,14 @@ RSpec.describe ::Jobs::Base do end end - it 'handles correct jobs' do + it "handles correct jobs" do job = GoodJob.new job.perform({}) expect(job.count).to eq(1) end - it 'handles errors in multisite' do - RailsMultisite::ConnectionManagement.expects(:all_dbs).returns(['default', 'default', 'default']) + it "handles errors in multisite" do + RailsMultisite::ConnectionManagement.expects(:all_dbs).returns(%w[default default default]) # one exception per database Discourse.expects(:handle_job_exception).times(3) @@ -38,17 +38,13 @@ RSpec.describe ::Jobs::Base do expect(bad.fail_count).to eq(3) end - describe '#perform' do - context 'when a job raises an error' do - before do - Discourse.reset_job_exception_stats! - end + describe "#perform" do + context "when a job raises an error" do + before { Discourse.reset_job_exception_stats! } - after do - Discourse.reset_job_exception_stats! - end + after { Discourse.reset_job_exception_stats! } - it 'collects stats for failing jobs in Discourse.job_exception_stats' do + it "collects stats for failing jobs in Discourse.job_exception_stats" do bad = BadJob.new 3.times do # During test env handle_job_exception errors out @@ -61,51 +57,47 @@ RSpec.describe ::Jobs::Base do end end - it 'delegates the process call to execute' do - ::Jobs::Base.any_instance.expects(:execute).with({ 'hello' => 'world' }) - ::Jobs::Base.new.perform('hello' => 'world', 'sync_exec' => true) + it "delegates the process call to execute" do + ::Jobs::Base.any_instance.expects(:execute).with({ "hello" => "world" }) + ::Jobs::Base.new.perform("hello" => "world") end - it 'converts to an indifferent access hash' do + it "converts to an indifferent access hash" do ::Jobs::Base.any_instance.expects(:execute).with(instance_of(HashWithIndifferentAccess)) - ::Jobs::Base.new.perform('hello' => 'world', 'sync_exec' => true) + ::Jobs::Base.new.perform("hello" => "world") end context "with fake jobs" do let(:common_state) { [] } - let(:test_job_1) { - Class.new(Jobs::Base).tap do |klass| - state = common_state - klass.define_method(:execute) do |args| - state << "job_1_executed" + let(:test_job_1) do + Class + .new(Jobs::Base) + .tap do |klass| + state = common_state + klass.define_method(:execute) { |args| state << "job_1_executed" } end - end - } + end - let(:test_job_2) { - Class.new(Jobs::Base).tap do |klass| - state = common_state - job_1 = test_job_1 - klass.define_method(:execute) do |args| - state << "job_2_started" - Jobs.enqueue(job_1) - state << "job_2_finished" + let(:test_job_2) do + Class + .new(Jobs::Base) + .tap do |klass| + state = common_state + job_1 = test_job_1 + klass.define_method(:execute) do |args| + state << "job_2_started" + Jobs.enqueue(job_1) + state << "job_2_finished" + end end - end - } + end it "runs jobs synchronously sequentially in tests" do Jobs.run_immediately! Jobs.enqueue(test_job_2) - expect(common_state).to eq([ - "job_2_started", - "job_2_finished", - "job_1_executed" - ]) + expect(common_state).to eq(%w[job_2_started job_2_finished job_1_executed]) end - end - end diff --git a/spec/jobs/jobs_spec.rb b/spec/jobs/jobs_spec.rb index 09406eb66b..c26af358cf 100644 --- a/spec/jobs/jobs_spec.rb +++ b/spec/jobs/jobs_spec.rb @@ -1,15 +1,11 @@ # frozen_string_literal: true RSpec.describe Jobs do + describe "enqueue" do + describe "run_later!" do + before { Jobs.run_later! } - describe 'enqueue' do - - describe 'run_later!' do - before do - Jobs.run_later! - end - - it 'enqueues a job in sidekiq' do + it "enqueues a job in sidekiq" do Sidekiq::Testing.fake! do jobs = Jobs::ProcessPost.jobs @@ -21,7 +17,7 @@ RSpec.describe Jobs do expected = { "class" => "Jobs::ProcessPost", "args" => [{ "post_id" => 1, "current_site_id" => "default" }], - "queue" => "default" + "queue" => "default", } expect(job.slice("class", "args", "queue")).to eq(expected) end @@ -62,7 +58,7 @@ RSpec.describe Jobs do expected = { "class" => "Jobs::ProcessPost", "args" => [{ "post_id" => 1 }], - "queue" => "default" + "queue" => "default", } expect(job.slice("class", "args", "queue")).to eq(expected) end @@ -75,12 +71,11 @@ RSpec.describe Jobs do end it "should enqueue with the correct database id when the current_site_id option is given" do - Sidekiq::Testing.fake! do jobs = Jobs::ProcessPost.jobs jobs.clear - Jobs.enqueue(:process_post, post_id: 1, current_site_id: 'test_db') + Jobs.enqueue(:process_post, post_id: 1, current_site_id: "test_db") expect(jobs.length).to eq(1) job = jobs.first @@ -88,17 +83,15 @@ RSpec.describe Jobs do expected = { "class" => "Jobs::ProcessPost", "args" => [{ "post_id" => 1, "current_site_id" => "test_db" }], - "queue" => "default" + "queue" => "default", } expect(job.slice("class", "args", "queue")).to eq(expected) end end end - describe 'run_immediately!' do - before do - Jobs.run_immediately! - end + describe "run_immediately!" do + before { Jobs.run_immediately! } it "doesn't enqueue in sidekiq" do Sidekiq::Client.expects(:enqueue).with(Jobs::ProcessPost, {}).never @@ -106,45 +99,46 @@ RSpec.describe Jobs do end it "executes the job right away" do - Jobs::ProcessPost.any_instance.expects(:perform).with({ "post_id" => 1, "sync_exec" => true, "current_site_id" => "default" }) + Jobs::ProcessPost + .any_instance + .expects(:perform_immediately) + .with({ "post_id" => 1, "current_site_id" => "default" }) + Jobs.enqueue(:process_post, post_id: 1) end - context 'when current_site_id option is given and does not match the current connection' do + context "when current_site_id option is given and does not match the current connection" do before do Sidekiq::Client.stubs(:enqueue) Jobs::ProcessPost.any_instance.stubs(:execute).returns(true) end - it 'should raise an exception' do + it "should raise an exception" do Jobs::ProcessPost.any_instance.expects(:execute).never RailsMultisite::ConnectionManagement.expects(:establish_connection).never expect { - Jobs.enqueue(:process_post, post_id: 1, current_site_id: 'test_db') + Jobs.enqueue(:process_post, post_id: 1, current_site_id: "test_db") }.to raise_error(ArgumentError) end end end - end - describe 'cancel_scheduled_job' do + describe "cancel_scheduled_job" do let(:scheduled_jobs) { Sidekiq::ScheduledSet.new } - after do - scheduled_jobs.clear - end + after { scheduled_jobs.clear } - it 'deletes the matching job' do + it "deletes the matching job" do Sidekiq::Testing.disable! do scheduled_jobs.clear expect(scheduled_jobs.size).to eq(0) Jobs.enqueue_in(1.year, :run_heartbeat, topic_id: 123) Jobs.enqueue_in(2.years, :run_heartbeat, topic_id: 456) - Jobs.enqueue_in(3.years, :run_heartbeat, topic_id: 123, current_site_id: 'foo') - Jobs.enqueue_in(4.years, :run_heartbeat, topic_id: 123, current_site_id: 'bar') + Jobs.enqueue_in(3.years, :run_heartbeat, topic_id: 123, current_site_id: "foo") + Jobs.enqueue_in(4.years, :run_heartbeat, topic_id: 123, current_site_id: "bar") expect(scheduled_jobs.size).to eq(4) @@ -157,11 +151,10 @@ RSpec.describe Jobs do expect(scheduled_jobs.size).to eq(1) end end - end - describe 'enqueue_at' do - it 'calls enqueue_in for you' do + describe "enqueue_at" do + it "calls enqueue_in for you" do freeze_time expect_enqueued_with(job: :process_post, at: 3.hours.from_now) do @@ -169,7 +162,7 @@ RSpec.describe Jobs do end end - it 'handles datetimes that are in the past' do + it "handles datetimes that are in the past" do freeze_time expect_enqueued_with(job: :process_post, at: Time.zone.now) do @@ -177,5 +170,4 @@ RSpec.describe Jobs do end end end - end diff --git a/spec/jobs/mass_award_badge_spec.rb b/spec/jobs/mass_award_badge_spec.rb index 024014a84a..361dfda270 100644 --- a/spec/jobs/mass_award_badge_spec.rb +++ b/spec/jobs/mass_award_badge_spec.rb @@ -1,28 +1,35 @@ # frozen_string_literal: true RSpec.describe Jobs::MassAwardBadge do - describe '#execute' do + describe "#execute" do fab!(:badge) { Fabricate(:badge) } fab!(:user) { Fabricate(:user) } - let(:email_mode) { 'email' } + let(:email_mode) { "email" } - it 'creates the badge for an existing user' do + it "creates the badge for an existing user" do execute_job(user) expect(UserBadge.where(user: user, badge: badge).exists?).to eq(true) end - it 'also creates a notification for the user' do + it "also creates a notification for the user" do execute_job(user) expect(Notification.exists?(user: user)).to eq(true) - expect(UserBadge.where.not(notification_id: nil).exists?(user: user, badge: badge)).to eq(true) + expect(UserBadge.where.not(notification_id: nil).exists?(user: user, badge: badge)).to eq( + true, + ) end - it 'updates badge ranks correctly' do + it "updates badge ranks correctly" do user_2 = Fabricate(:user) - UserBadge.create!(badge_id: Badge::Member, user: user, granted_by: Discourse.system_user, granted_at: Time.now) + UserBadge.create!( + badge_id: Badge::Member, + user: user, + granted_by: Discourse.system_user, + granted_at: Time.now, + ) execute_job(user) execute_job(user_2) @@ -31,7 +38,7 @@ RSpec.describe Jobs::MassAwardBadge do expect(UserBadge.find_by(user: user_2, badge: badge).featured_rank).to eq(1) end - it 'grants a badge multiple times to a user' do + it "grants a badge multiple times to a user" do badge.update!(multiple_grant: true) Notification.destroy_all execute_job(user, count: 4, grant_existing_holders: true) @@ -44,7 +51,12 @@ RSpec.describe Jobs::MassAwardBadge do end def execute_job(user, count: 1, grant_existing_holders: false) - subject.execute(user: user.id, badge: badge.id, count: count, grant_existing_holders: grant_existing_holders) + subject.execute( + user: user.id, + badge: badge.id, + count: count, + grant_existing_holders: grant_existing_holders, + ) end end end diff --git a/spec/jobs/migrate_badge_image_to_uploads_spec.rb b/spec/jobs/migrate_badge_image_to_uploads_spec.rb index 21dfabe780..21c39338ea 100644 --- a/spec/jobs/migrate_badge_image_to_uploads_spec.rb +++ b/spec/jobs/migrate_badge_image_to_uploads_spec.rb @@ -9,21 +9,18 @@ RSpec.describe Jobs::MigrateBadgeImageToUploads do Rails.logger = @fake_logger = FakeLogger.new end - after do - Rails.logger = @orig_logger - end + after { Rails.logger = @orig_logger } - it 'should migrate to the new badge `image_upload_id` column correctly' do + it "should migrate to the new badge `image_upload_id` column correctly" do stub_request(:get, image_url).to_return( - status: 200, body: file_from_fixtures("smallest.png").read + status: 200, + body: file_from_fixtures("smallest.png").read, ) DB.exec(<<~SQL, flair_url: image_url, id: badge.id) UPDATE badges SET image = :flair_url WHERE id = :id SQL - expect do - described_class.new.execute_onceoff({}) - end.to change { Upload.count }.by(1) + expect do described_class.new.execute_onceoff({}) end.to change { Upload.count }.by(1) badge.reload upload = Upload.last @@ -32,7 +29,7 @@ RSpec.describe Jobs::MigrateBadgeImageToUploads do expect(badge[:image]).to eq(nil) end - it 'should skip badges with invalid flair URLs' do + it "should skip badges with invalid flair URLs" do DB.exec("UPDATE badges SET image = 'abc' WHERE id = ?", badge.id) described_class.new.execute_onceoff({}) expect(@fake_logger.warnings.count).to eq(0) @@ -41,7 +38,7 @@ RSpec.describe Jobs::MigrateBadgeImageToUploads do # this case has a couple of hacks that are needed to test this behavior, so if it # starts failing randomly in the future, I'd just delete it and not bother with it - it 'should not keep retrying forever if download fails' do + it "should not keep retrying forever if download fails" do stub_request(:get, image_url).to_return(status: 403) instance = described_class.new instance.expects(:sleep).times(2) @@ -50,9 +47,7 @@ RSpec.describe Jobs::MigrateBadgeImageToUploads do UPDATE badges SET image = :flair_url WHERE id = :id SQL - expect do - instance.execute_onceoff({}) - end.not_to change { Upload.count } + expect do instance.execute_onceoff({}) end.not_to change { Upload.count } badge.reload expect(badge.image_upload).to eq(nil) diff --git a/spec/jobs/notify_category_change_spec.rb b/spec/jobs/notify_category_change_spec.rb index d218760aed..567aab68f6 100644 --- a/spec/jobs/notify_category_change_spec.rb +++ b/spec/jobs/notify_category_change_spec.rb @@ -4,13 +4,19 @@ RSpec.describe ::Jobs::NotifyCategoryChange do fab!(:user) { Fabricate(:user) } fab!(:regular_user) { Fabricate(:trust_level_4) } fab!(:post) { Fabricate(:post, user: regular_user) } - fab!(:category) { Fabricate(:category, name: 'test') } + fab!(:category) { Fabricate(:category, name: "test") } it "doesn't create notification for the editor who watches new tag" do - CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:watching_first_post], category.id) + CategoryUser.set_notification_level_for_category( + user, + CategoryUser.notification_levels[:watching_first_post], + category.id, + ) post.topic.update!(category: category) post.update!(last_editor_id: user.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: []) }.not_to change { Notification.count } + expect { described_class.new.execute(post_id: post.id, notified_user_ids: []) }.not_to change { + Notification.count + } end end diff --git a/spec/jobs/notify_mailing_list_subscribers_spec.rb b/spec/jobs/notify_mailing_list_subscribers_spec.rb index ace5161f91..71cf7347a4 100644 --- a/spec/jobs/notify_mailing_list_subscribers_spec.rb +++ b/spec/jobs/notify_mailing_list_subscribers_spec.rb @@ -3,10 +3,10 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do fab!(:mailing_list_user) { Fabricate(:user) } - before { mailing_list_user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 1) } before do - SiteSetting.tagging_enabled = true + mailing_list_user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 1) end + before { SiteSetting.tagging_enabled = true } fab!(:tag) { Fabricate(:tag) } fab!(:topic) { Fabricate(:topic, tags: [tag]) } @@ -27,10 +27,14 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do end it "triggers :notify_mailing_list_subscribers" do - events = DiscourseEvent.track_events do - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end - expect(events).to include(event_name: :notify_mailing_list_subscribers, params: [[mailing_list_user], post]) + events = + DiscourseEvent.track_events do + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) + end + expect(events).to include( + event_name: :notify_mailing_list_subscribers, + params: [[mailing_list_user], post], + ) end end @@ -133,12 +137,24 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do end context "when from a muted topic" do - before { TopicUser.create(user: mailing_list_user, topic: post.topic, notification_level: TopicUser.notification_levels[:muted]) } + before do + TopicUser.create( + user: mailing_list_user, + topic: post.topic, + notification_level: TopicUser.notification_levels[:muted], + ) + end include_examples "no emails" end context "when from a muted category" do - before { CategoryUser.create(user: mailing_list_user, category: post.topic.category, notification_level: CategoryUser.notification_levels[:muted]) } + before do + CategoryUser.create( + user: mailing_list_user, + category: post.topic.category, + notification_level: CategoryUser.notification_levels[:muted], + ) + end include_examples "no emails" end @@ -150,7 +166,11 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do context "with mute all categories by default setting but user is watching category" do before do SiteSetting.mute_all_categories_by_default = true - CategoryUser.create(user: mailing_list_user, category: post.topic.category, notification_level: CategoryUser.notification_levels[:watching]) + CategoryUser.create( + user: mailing_list_user, + category: post.topic.category, + notification_level: CategoryUser.notification_levels[:watching], + ) end include_examples "one email" end @@ -158,7 +178,11 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do context "with mute all categories by default setting but user is watching tag" do before do SiteSetting.mute_all_categories_by_default = true - TagUser.create(user: mailing_list_user, tag: tag, notification_level: TagUser.notification_levels[:watching]) + TagUser.create( + user: mailing_list_user, + tag: tag, + notification_level: TagUser.notification_levels[:watching], + ) end include_examples "one email" end @@ -166,13 +190,23 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do context "with mute all categories by default setting but user is watching topic" do before do SiteSetting.mute_all_categories_by_default = true - TopicUser.create(user: mailing_list_user, topic: post.topic, notification_level: TopicUser.notification_levels[:watching]) + TopicUser.create( + user: mailing_list_user, + topic: post.topic, + notification_level: TopicUser.notification_levels[:watching], + ) end include_examples "one email" end context "when from a muted tag" do - before { TagUser.create(user: mailing_list_user, tag: tag, notification_level: TagUser.notification_levels[:muted]) } + before do + TagUser.create( + user: mailing_list_user, + tag: tag, + notification_level: TagUser.notification_levels[:muted], + ) + end include_examples "no emails" end @@ -180,44 +214,39 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do before { SiteSetting.max_emails_per_day_per_user = 2 } it "doesn't send any emails" do - (SiteSetting.max_emails_per_day_per_user + 1).times { - mailing_list_user.email_logs.create(email_type: 'foobar', to_address: mailing_list_user.email) - } + (SiteSetting.max_emails_per_day_per_user + 1).times do + mailing_list_user.email_logs.create( + email_type: "foobar", + to_address: mailing_list_user.email, + ) + end expect do - UserNotifications.expects(:mailing_list_notify) - .with(mailing_list_user, post) - .never + UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never - 2.times do - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end + 2.times { Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) } - Jobs::NotifyMailingListSubscribers.new.execute( - post_id: Fabricate(:post, user: user).id - ) + Jobs::NotifyMailingListSubscribers.new.execute(post_id: Fabricate(:post, user: user).id) end.to change { SkippedEmailLog.count }.by(1) - expect(SkippedEmailLog.exists?( - email_type: "mailing_list", - user: mailing_list_user, - post: post, - to_address: mailing_list_user.email, - reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "mailing_list", + user: mailing_list_user, + post: post, + to_address: mailing_list_user.email, + reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit], + ), + ).to eq(true) freeze_time(Time.zone.now.tomorrow + 1.second) expect do post = Fabricate(:post, user: user) - UserNotifications.expects(:mailing_list_notify) - .with(mailing_list_user, post) - .once + UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).once - Jobs::NotifyMailingListSubscribers.new.execute( - post_id: post.id - ) + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) end.not_to change { SkippedEmailLog.count } end end @@ -229,13 +258,15 @@ RSpec.describe Jobs::NotifyMailingListSubscribers do Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) UserNotifications.expects(:mailing_list_notify).with(mailing_list_user, post).never - expect(SkippedEmailLog.exists?( - email_type: "mailing_list", - user: mailing_list_user, - post: post, - to_address: mailing_list_user.email, - reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "mailing_list", + user: mailing_list_user, + post: post, + to_address: mailing_list_user.email, + reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit], + ), + ).to eq(true) end end end diff --git a/spec/jobs/notify_moved_posts_spec.rb b/spec/jobs/notify_moved_posts_spec.rb index dda5fdbbe6..35e40b24e8 100644 --- a/spec/jobs/notify_moved_posts_spec.rb +++ b/spec/jobs/notify_moved_posts_spec.rb @@ -1,33 +1,43 @@ # frozen_string_literal: true RSpec.describe Jobs::NotifyMovedPosts do - it "raises an error without post_ids" do - expect { Jobs::NotifyMovedPosts.new.execute(moved_by_id: 1234) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::NotifyMovedPosts.new.execute(moved_by_id: 1234) }.to raise_error( + Discourse::InvalidParameters, + ) end it "raises an error without moved_by_id" do - expect { Jobs::NotifyMovedPosts.new.execute(post_ids: [1, 2, 3]) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::NotifyMovedPosts.new.execute(post_ids: [1, 2, 3]) }.to raise_error( + Discourse::InvalidParameters, + ) end - context 'with post ids' do + context "with post ids" do fab!(:p1) { Fabricate(:post) } fab!(:p2) { Fabricate(:post, user: Fabricate(:evil_trout), topic: p1.topic) } fab!(:p3) { Fabricate(:post, user: p1.user, topic: p1.topic) } fab!(:admin) { Fabricate(:admin) } - let(:moved_post_notifications) { Notification.where(notification_type: Notification.types[:moved_post]) } + let(:moved_post_notifications) do + Notification.where(notification_type: Notification.types[:moved_post]) + end it "should create two notifications" do - expect { Jobs::NotifyMovedPosts.new.execute(post_ids: [p1.id, p2.id, p3.id], moved_by_id: admin.id) }.to change(moved_post_notifications, :count).by(2) + expect { + Jobs::NotifyMovedPosts.new.execute(post_ids: [p1.id, p2.id, p3.id], moved_by_id: admin.id) + }.to change(moved_post_notifications, :count).by(2) end - context 'when moved by one of the posters' do + context "when moved by one of the posters" do it "create one notifications, because the poster is the mover" do - expect { Jobs::NotifyMovedPosts.new.execute(post_ids: [p1.id, p2.id, p3.id], moved_by_id: p1.user_id) }.to change(moved_post_notifications, :count).by(1) + expect { + Jobs::NotifyMovedPosts.new.execute( + post_ids: [p1.id, p2.id, p3.id], + moved_by_id: p1.user_id, + ) + }.to change(moved_post_notifications, :count).by(1) end end - end - end diff --git a/spec/jobs/notify_reviewable_spec.rb b/spec/jobs/notify_reviewable_spec.rb index fe7c46bd98..4eecaf75e1 100644 --- a/spec/jobs/notify_reviewable_spec.rb +++ b/spec/jobs/notify_reviewable_spec.rb @@ -3,7 +3,7 @@ RSpec.describe Jobs::NotifyReviewable do # remove all the legacy stuff here when redesigned_user_menu_enabled is # removed - describe '#execute' do + describe "#execute" do fab!(:admin) { Fabricate(:admin, moderator: true) } fab!(:moderator) { Fabricate(:moderator) } fab!(:group_user) { Fabricate(:group_user) } @@ -20,9 +20,8 @@ RSpec.describe Jobs::NotifyReviewable do admin_reviewable = Fabricate(:reviewable, reviewable_by_moderator: false) admin.update!(last_seen_reviewable_id: admin_reviewable.id) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: admin_reviewable.id) - end + messages = + MessageBus.track_publish { described_class.new.execute(reviewable_id: admin_reviewable.id) } expect(messages.size).to eq(1) @@ -36,9 +35,10 @@ RSpec.describe Jobs::NotifyReviewable do # Content for moderators moderator_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: moderator_reviewable.id) - end + messages = + MessageBus.track_publish do + described_class.new.execute(reviewable_id: moderator_reviewable.id) + end expect(messages.size).to eq(2) admin_message = messages.find { |m| m.user_ids == [admin.id] } @@ -56,11 +56,11 @@ RSpec.describe Jobs::NotifyReviewable do moderator.update!(last_seen_reviewable_id: moderator_reviewable.id) # Content for a group - group_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) + group_reviewable = + Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: group_reviewable.id) - end + messages = + MessageBus.track_publish { described_class.new.execute(reviewable_id: group_reviewable.id) } expect(messages.size).to eq(3) @@ -93,9 +93,8 @@ RSpec.describe Jobs::NotifyReviewable do admin_reviewable = Fabricate(:reviewable, reviewable_by_moderator: false) admin.update!(last_seen_reviewable_id: admin_reviewable.id) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: admin_reviewable.id) - end + messages = + MessageBus.track_publish { described_class.new.execute(reviewable_id: admin_reviewable.id) } expect(messages.size).to eq(1) @@ -109,9 +108,10 @@ RSpec.describe Jobs::NotifyReviewable do # Content for moderators moderator_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: moderator_reviewable.id) - end + messages = + MessageBus.track_publish do + described_class.new.execute(reviewable_id: moderator_reviewable.id) + end expect(messages.size).to eq(2) @@ -128,11 +128,11 @@ RSpec.describe Jobs::NotifyReviewable do moderator.update!(last_seen_reviewable_id: moderator_reviewable.id) # Content for a group - group_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) + group_reviewable = + Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) - messages = MessageBus.track_publish do - described_class.new.execute(reviewable_id: group_reviewable.id) - end + messages = + MessageBus.track_publish { described_class.new.execute(reviewable_id: group_reviewable.id) } expect(messages.size).to eq(3) @@ -158,9 +158,10 @@ RSpec.describe Jobs::NotifyReviewable do GroupUser.create!(group_id: group.id, user_id: moderator.id) reviewable = Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) - messages = MessageBus.track_publish("/reviewable_counts") do - described_class.new.execute(reviewable_id: reviewable.id) - end + messages = + MessageBus.track_publish("/reviewable_counts") do + described_class.new.execute(reviewable_id: reviewable.id) + end group_user_message = messages.find { |m| m.user_ids.include?(user.id) } @@ -171,16 +172,17 @@ RSpec.describe Jobs::NotifyReviewable do SiteSetting.navigation_menu = "legacy" SiteSetting.enable_category_group_moderation = true Reviewable.set_priorities(medium: 2.0) - SiteSetting.reviewable_default_visibility = 'medium' + SiteSetting.reviewable_default_visibility = "medium" GroupUser.create!(group_id: group.id, user_id: moderator.id) # Content for admins only admin_reviewable = Fabricate(:reviewable, reviewable_by_moderator: false) - messages = MessageBus.track_publish("/reviewable_counts") do - described_class.new.execute(reviewable_id: admin_reviewable.id) - end + messages = + MessageBus.track_publish("/reviewable_counts") do + described_class.new.execute(reviewable_id: admin_reviewable.id) + end admin_message = messages.find { |m| m.user_ids.include?(admin.id) } expect(admin_message.data[:reviewable_count]).to eq(0) @@ -188,9 +190,10 @@ RSpec.describe Jobs::NotifyReviewable do # Content for moderators moderator_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true) - messages = MessageBus.track_publish("/reviewable_counts") do - described_class.new.execute(reviewable_id: moderator_reviewable.id) - end + messages = + MessageBus.track_publish("/reviewable_counts") do + described_class.new.execute(reviewable_id: moderator_reviewable.id) + end admin_message = messages.find { |m| m.user_ids.include?(admin.id) } @@ -200,11 +203,13 @@ RSpec.describe Jobs::NotifyReviewable do expect(moderator_message.data[:reviewable_count]).to eq(0) # Content for a group - group_reviewable = Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) + group_reviewable = + Fabricate(:reviewable, reviewable_by_moderator: true, reviewable_by_group: group) - messages = MessageBus.track_publish("/reviewable_counts") do - described_class.new.execute(reviewable_id: group_reviewable.id) - end + messages = + MessageBus.track_publish("/reviewable_counts") do + described_class.new.execute(reviewable_id: group_reviewable.id) + end admin_message = messages.find { |m| m.user_ids.include?(admin.id) } expect(admin_message.data[:reviewable_count]).to eq(0) @@ -218,13 +223,14 @@ RSpec.describe Jobs::NotifyReviewable do end end - it 'skips sending notifications if user_ids is empty' do + it "skips sending notifications if user_ids is empty" do reviewable = Fabricate(:reviewable, reviewable_by_moderator: true) regular_user = Fabricate(:user) - messages = MessageBus.track_publish("/reviewable_counts") do - described_class.new.execute(reviewable_id: reviewable.id) - end + messages = + MessageBus.track_publish("/reviewable_counts") do + described_class.new.execute(reviewable_id: reviewable.id) + end expect(messages.size).to eq(0) end diff --git a/spec/jobs/notify_tag_change_spec.rb b/spec/jobs/notify_tag_change_spec.rb index 581fd0e251..964024f44a 100644 --- a/spec/jobs/notify_tag_change_spec.rb +++ b/spec/jobs/notify_tag_change_spec.rb @@ -4,20 +4,19 @@ RSpec.describe ::Jobs::NotifyTagChange do fab!(:user) { Fabricate(:user) } fab!(:regular_user) { Fabricate(:trust_level_4) } fab!(:post) { Fabricate(:post, user: regular_user) } - fab!(:tag) { Fabricate(:tag, name: 'test') } + fab!(:tag) { Fabricate(:tag, name: "test") } it "creates notification for watched tag" do TagUser.create!( user_id: user.id, tag_id: tag.id, - notification_level: NotificationLevels.topic_levels[:watching] - ) - TopicTag.create!( - topic_id: post.topic.id, - tag_id: tag.id + notification_level: NotificationLevels.topic_levels[:watching], ) + TopicTag.create!(topic_id: post.topic.id, tag_id: tag.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) }.to change { Notification.count } + expect { + described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) + }.to change { Notification.count } notification = Notification.last expect(notification.user_id).to eq(user.id) expect(notification.topic_id).to eq(post.topic_id) @@ -30,14 +29,13 @@ RSpec.describe ::Jobs::NotifyTagChange do TagUser.create!( user_id: user.id, tag_id: tag.id, - notification_level: NotificationLevels.topic_levels[:watching] - ) - TopicTag.create!( - topic_id: post.topic.id, - tag_id: tag.id + notification_level: NotificationLevels.topic_levels[:watching], ) + TopicTag.create!(topic_id: post.topic.id, tag_id: tag.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) }.not_to change { Notification.count } + expect { + described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) + }.not_to change { Notification.count } end it "doesn't create notification for the editor who watches new tag" do @@ -45,41 +43,78 @@ RSpec.describe ::Jobs::NotifyTagChange do TopicTag.create!(topic: post.topic, tag: tag) post.update!(last_editor_id: user.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: []) }.not_to change { Notification.count } + expect { described_class.new.execute(post_id: post.id, notified_user_ids: []) }.not_to change { + Notification.count + } end - it 'doesnt create notification for user watching category' do + it "doesnt create notification for user watching category" do CategoryUser.create!( user_id: user.id, category_id: post.topic.category_id, - notification_level: TopicUser.notification_levels[:watching] + notification_level: TopicUser.notification_levels[:watching], ) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) }.not_to change { Notification.count } + expect { + described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id]) + }.not_to change { Notification.count } end - describe 'hidden tag' do - let!(:hidden_group) { Fabricate(:group, name: 'hidden_group') } - let!(:hidden_tag_group) { Fabricate(:tag_group, name: 'hidden', permissions: [[hidden_group.id, :full]]) } - let!(:topic_user) { Fabricate(:topic_user, user: user, topic: post.topic, notification_level: TopicUser.notification_levels[:watching]) } + describe "hidden tag" do + let!(:hidden_group) { Fabricate(:group, name: "hidden_group") } + let!(:hidden_tag_group) do + Fabricate(:tag_group, name: "hidden", permissions: [[hidden_group.id, :full]]) + end + let!(:topic_user) do + Fabricate( + :topic_user, + user: user, + topic: post.topic, + notification_level: TopicUser.notification_levels[:watching], + ) + end - it 'does not create notification for watching user who does not belong to group' do + it "does not create notification for watching user who does not belong to group" do TagGroupMembership.create!(tag_group_id: hidden_tag_group.id, tag_id: tag.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id], diff_tags: [tag.name]) }.not_to change { Notification.count } + expect { + described_class.new.execute( + post_id: post.id, + notified_user_ids: [regular_user.id], + diff_tags: [tag.name], + ) + }.not_to change { Notification.count } Fabricate(:group_user, group: hidden_group, user: user) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id], diff_tags: [tag.name]) }.to change { Notification.count } + expect { + described_class.new.execute( + post_id: post.id, + notified_user_ids: [regular_user.id], + diff_tags: [tag.name], + ) + }.to change { Notification.count } end - it 'creates notification when at least added or removed tag is visible to everyone' do - visible_tag = Fabricate(:tag, name: 'visible tag') - visible_group = Fabricate(:tag_group, name: 'visible group') + it "creates notification when at least added or removed tag is visible to everyone" do + visible_tag = Fabricate(:tag, name: "visible tag") + visible_group = Fabricate(:tag_group, name: "visible group") TagGroupMembership.create!(tag_group_id: visible_group.id, tag_id: visible_tag.id) TagGroupMembership.create!(tag_group_id: hidden_tag_group.id, tag_id: tag.id) - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id], diff_tags: [tag.name]) }.not_to change { Notification.count } - expect { described_class.new.execute(post_id: post.id, notified_user_ids: [regular_user.id], diff_tags: [tag.name, visible_tag.name]) }.to change { Notification.count } + expect { + described_class.new.execute( + post_id: post.id, + notified_user_ids: [regular_user.id], + diff_tags: [tag.name], + ) + }.not_to change { Notification.count } + expect { + described_class.new.execute( + post_id: post.id, + notified_user_ids: [regular_user.id], + diff_tags: [tag.name, visible_tag.name], + ) + }.to change { Notification.count } end end end diff --git a/spec/jobs/old_keys_reminder_spec.rb b/spec/jobs/old_keys_reminder_spec.rb index a6d4fb89db..f2ccae695d 100644 --- a/spec/jobs/old_keys_reminder_spec.rb +++ b/spec/jobs/old_keys_reminder_spec.rb @@ -1,28 +1,48 @@ # frozen_string_literal: true RSpec.describe Jobs::OldKeysReminder do - let!(:google_secret) { SiteSetting.create!(name: 'google_oauth2_client_secret', value: '123', data_type: 1) } - let!(:github_secret) { SiteSetting.create!(name: 'github_client_secret', value: '123', data_type: 1) } - let!(:api_key) { Fabricate(:api_key, description: 'api key description') } + let!(:google_secret) do + SiteSetting.create!(name: "google_oauth2_client_secret", value: "123", data_type: 1) + end + let!(:github_secret) do + SiteSetting.create!(name: "github_client_secret", value: "123", data_type: 1) + end + let!(:api_key) { Fabricate(:api_key, description: "api key description") } let!(:admin) { Fabricate(:admin) } let!(:another_admin) { Fabricate(:admin) } - let!(:recent_twitter_secret) { SiteSetting.create!(name: 'twitter_consumer_secret', value: '123', data_type: 1, updated_at: 2.years.from_now) } - let!(:recent_api_key) { Fabricate(:api_key, description: 'recent api key description', created_at: 2.years.from_now, user_id: admin.id) } + let!(:recent_twitter_secret) do + SiteSetting.create!( + name: "twitter_consumer_secret", + value: "123", + data_type: 1, + updated_at: 2.years.from_now, + ) + end + let!(:recent_api_key) do + Fabricate( + :api_key, + description: "recent api key description", + created_at: 2.years.from_now, + user_id: admin.id, + ) + end - it 'is disabled be default' do + it "is disabled be default" do freeze_time 2.years.from_now expect { described_class.new.execute({}) }.not_to change { Post.count } end - it 'sends message to admin with old credentials' do - SiteSetting.send_old_credential_reminder_days = '365' + it "sends message to admin with old credentials" do + SiteSetting.send_old_credential_reminder_days = "365" freeze_time 2.years.from_now expect { described_class.new.execute({}) }.to change { Post.count }.by(1) post = Post.last expect(post.archetype).to eq(Archetype.private_message) - expect(post.topic.topic_allowed_users.map(&:user_id).sort).to eq([Discourse.system_user.id, admin.id, another_admin.id].sort) - expect(post.topic.title).to eq('Reminder about old credentials') + expect(post.topic.topic_allowed_users.map(&:user_id).sort).to eq( + [Discourse.system_user.id, admin.id, another_admin.id].sort, + ) + expect(post.topic.title).to eq("Reminder about old credentials") expect(post.raw).to eq(<<~TEXT.rstrip) Hello! This is a routine yearly security reminder from your Discourse instance. @@ -39,7 +59,7 @@ RSpec.describe Jobs::OldKeysReminder do freeze_time 4.years.from_now described_class.new.execute({}) post = Post.last - expect(post.topic.title).to eq('Reminder about old credentials') + expect(post.topic.title).to eq("Reminder about old credentials") expect(post.raw).to eq(<<~TEXT.rstrip) Hello! This is a routine yearly security reminder from your Discourse instance. @@ -55,15 +75,15 @@ RSpec.describe Jobs::OldKeysReminder do TEXT end - it 'does not send message when send_old_credential_reminder_days is set to 0 or no old keys' do + it "does not send message when send_old_credential_reminder_days is set to 0 or no old keys" do expect { described_class.new.execute({}) }.not_to change { Post.count } - SiteSetting.send_old_credential_reminder_days = '0' + SiteSetting.send_old_credential_reminder_days = "0" freeze_time 2.years.from_now expect { described_class.new.execute({}) }.not_to change { Post.count } end - it 'does not send a message if already exists' do - SiteSetting.send_old_credential_reminder_days = '367' + it "does not send a message if already exists" do + SiteSetting.send_old_credential_reminder_days = "367" freeze_time 2.years.from_now expect { described_class.new.execute({}) }.to change { Post.count }.by(1) Topic.last.trash! diff --git a/spec/jobs/open_topic_spec.rb b/spec/jobs/open_topic_spec.rb index 9a401db727..fc6bc26bab 100644 --- a/spec/jobs/open_topic_spec.rb +++ b/spec/jobs/open_topic_spec.rb @@ -3,23 +3,17 @@ RSpec.describe Jobs::OpenTopic do fab!(:admin) { Fabricate(:admin) } - fab!(:topic) do - Fabricate(:topic_timer, user: admin).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: admin).topic } - before do - topic.update!(closed: true) - end + before { topic.update!(closed: true) } - it 'should work' do + it "should work" do freeze_time(61.minutes.from_now) do described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) expect(topic.reload.open?).to eq(true) - expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_disabled_minutes', count: 61 - )) + expect(Post.last.raw).to eq(I18n.t("topic_statuses.autoclosed_disabled_minutes", count: 61)) end end @@ -31,22 +25,15 @@ RSpec.describe Jobs::OpenTopic do end end - describe 'when category has auto close configured' do + describe "when category has auto close configured" do fab!(:category) do - Fabricate(:category, - auto_close_based_on_last_post: true, - auto_close_hours: 5 - ) + Fabricate(:category, auto_close_based_on_last_post: true, auto_close_hours: 5) end fab!(:topic) { Fabricate(:topic, category: category, closed: true) } it "should restore the category's auto close timer" do - Fabricate(:topic_timer, - status_type: TopicTimer.types[:open], - topic: topic, - user: admin - ) + Fabricate(:topic_timer, status_type: TopicTimer.types[:open], topic: topic, user: admin) freeze_time(61.minutes.from_now) do described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) @@ -61,14 +48,12 @@ RSpec.describe Jobs::OpenTopic do end end - describe 'when user is no longer authorized to open topics' do + describe "when user is no longer authorized to open topics" do fab!(:user) { Fabricate(:user) } - fab!(:topic) do - Fabricate(:topic_timer, user: user).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: user).topic } - it 'should destroy the topic timer' do + it "should destroy the topic timer" do topic.update!(closed: true) freeze_time(topic.public_topic_timer.execute_at + 1.minute) @@ -80,10 +65,7 @@ RSpec.describe Jobs::OpenTopic do end it "should reconfigure topic timer if category's topics are set to autoclose" do - category = Fabricate(:category, - auto_close_based_on_last_post: true, - auto_close_hours: 5 - ) + category = Fabricate(:category, auto_close_based_on_last_post: true, auto_close_hours: 5) topic = Fabricate(:topic, category: category) topic.public_topic_timer.update!(user: user) @@ -92,12 +74,10 @@ RSpec.describe Jobs::OpenTopic do freeze_time(topic.public_topic_timer.execute_at + 1.minute) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) - end.to change { topic.reload.public_topic_timer.user }.from(user).to(Discourse.system_user) - .and change { topic.public_topic_timer.id } + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) + end.to change { topic.reload.public_topic_timer.user }.from(user).to( + Discourse.system_user, + ).and change { topic.public_topic_timer.id } expect(topic.reload.closed).to eq(false) end diff --git a/spec/jobs/pending_queued_posts_reminder_spec.rb b/spec/jobs/pending_queued_posts_reminder_spec.rb index 7611556dd5..0d33627c04 100644 --- a/spec/jobs/pending_queued_posts_reminder_spec.rb +++ b/spec/jobs/pending_queued_posts_reminder_spec.rb @@ -8,9 +8,7 @@ RSpec.describe Jobs::PendingQueuedPostsReminder do it "never emails" do described_class.any_instance.expects(:should_notify_ids).never - expect { - job.execute({}) - }.to_not change { Post.count } + expect { job.execute({}) }.to_not change { Post.count } end end @@ -25,37 +23,35 @@ RSpec.describe Jobs::PendingQueuedPostsReminder do Fabricate(:reviewable_queued_post, created_at: 14.minutes.ago) # expect 16 minute post to be picked up but not 14 min post expect { job.execute({}) }.to change { Post.count }.by(1) - expect(Topic.where( - subtype: TopicSubtype.system_message, - title: I18n.t('system_messages.queued_posts_reminder.subject_template', count: 1) - ).exists?).to eq(true) + expect( + Topic.where( + subtype: TopicSubtype.system_message, + title: I18n.t("system_messages.queued_posts_reminder.subject_template", count: 1), + ).exists?, + ).to eq(true) end end context "when notify_about_queued_posts_after is 24" do - before do - SiteSetting.notify_about_queued_posts_after = 24 - end + before { SiteSetting.notify_about_queued_posts_after = 24 } context "when we haven't been notified in a while" do - before do - job.last_notified_id = nil - end + before { job.last_notified_id = nil } it "doesn't create system message if there are no queued posts" do - expect { - job.execute({}) - }.to_not change { Post.count } + expect { job.execute({}) }.to_not change { Post.count } end it "creates system message if there are new queued posts" do Fabricate(:reviewable_queued_post, created_at: 48.hours.ago) Fabricate(:reviewable_queued_post, created_at: 45.hours.ago) expect { job.execute({}) }.to change { Post.count }.by(1) - expect(Topic.where( - subtype: TopicSubtype.system_message, - title: I18n.t('system_messages.queued_posts_reminder.subject_template', count: 2) - ).exists?).to eq(true) + expect( + Topic.where( + subtype: TopicSubtype.system_message, + title: I18n.t("system_messages.queued_posts_reminder.subject_template", count: 2), + ).exists?, + ).to eq(true) end end diff --git a/spec/jobs/pending_reviewables_reminder_spec.rb b/spec/jobs/pending_reviewables_reminder_spec.rb index 2b6d4deafd..bb3fa0f4c7 100644 --- a/spec/jobs/pending_reviewables_reminder_spec.rb +++ b/spec/jobs/pending_reviewables_reminder_spec.rb @@ -4,7 +4,12 @@ RSpec.describe Jobs::PendingReviewablesReminder do let(:job) { described_class.new } def create_flag(created_at) - PostActionCreator.create(Fabricate(:user), Fabricate(:post), :spam, created_at: created_at).reviewable + PostActionCreator.create( + Fabricate(:user), + Fabricate(:post), + :spam, + created_at: created_at, + ).reviewable end def execute @@ -44,9 +49,7 @@ RSpec.describe Jobs::PendingReviewablesReminder do described_class.clear_key end - after do - described_class.clear_key - end + after { described_class.clear_key } it "doesn't send message when flags are less than 48 hours old" do create_flag(47.hours.ago) @@ -76,27 +79,30 @@ RSpec.describe Jobs::PendingReviewablesReminder do it "doesn't send a message when `reviewable_default_visibility` is not met" do Reviewable.set_priorities(medium: 3.0) - SiteSetting.reviewable_default_visibility = 'medium' + SiteSetting.reviewable_default_visibility = "medium" expect(execute.sent_reminder).to eq(false) end it "sends a message when `reviewable_default_visibility` is met" do Reviewable.set_priorities(medium: 2.0) - SiteSetting.reviewable_default_visibility = 'medium' + SiteSetting.reviewable_default_visibility = "medium" expect(execute.sent_reminder).to eq(true) end end - it 'deletes previous messages' do + it "deletes previous messages" do GroupMessage.create( - Group[:moderators].name, 'reviewables_reminder', - { limit_once_per: false, message_params: { mentions: '', count: 1 } } + Group[:moderators].name, + "reviewables_reminder", + { limit_once_per: false, message_params: { mentions: "", count: 1 } }, ) create_flag(49.hours.ago) execute - expect(Topic.where(title: I18n.t("system_messages.reviewables_reminder.subject_template")).count).to eq(1) + expect( + Topic.where(title: I18n.t("system_messages.reviewables_reminder.subject_template")).count, + ).to eq(1) end end end diff --git a/spec/jobs/pending_users_reminder_spec.rb b/spec/jobs/pending_users_reminder_spec.rb index c482231d6e..bb2e32c6b0 100644 --- a/spec/jobs/pending_users_reminder_spec.rb +++ b/spec/jobs/pending_users_reminder_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::PendingUsersReminder do - context 'when must_approve_users is true' do + context "when must_approve_users is true" do before do SiteSetting.must_approve_users = true Jobs::PendingUsersReminder.any_instance.stubs(:previous_newest_username).returns(nil) @@ -42,16 +42,17 @@ RSpec.describe Jobs::PendingUsersReminder do it "sets the correct pending user count in the notification" do SiteSetting.pending_users_reminder_delay_minutes = 8 Fabricate(:user, created_at: 9.minutes.ago) - PostCreator.expects(:create).with(Discourse.system_user, has_entries(title: '1 user waiting for approval')) + PostCreator.expects(:create).with( + Discourse.system_user, + has_entries(title: "1 user waiting for approval"), + ) Jobs::PendingUsersReminder.new.execute({}) end end end - context 'when must_approve_users is false' do - before do - SiteSetting.must_approve_users = false - end + context "when must_approve_users is false" do + before { SiteSetting.must_approve_users = false } it "doesn't send a message to anyone when there are pending users" do AdminUserIndexQuery.any_instance.stubs(:find_users_query).returns(stub_everything(count: 1)) diff --git a/spec/jobs/periodical_updates_spec.rb b/spec/jobs/periodical_updates_spec.rb index f6282fb956..3b0749eb4e 100644 --- a/spec/jobs/periodical_updates_spec.rb +++ b/spec/jobs/periodical_updates_spec.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true RSpec.describe Jobs::PeriodicalUpdates do - it "works" do - # does not blow up, no mocks, everything is called Jobs::PeriodicalUpdates.new.execute(nil) end diff --git a/spec/jobs/poll_mailbox_spec.rb b/spec/jobs/poll_mailbox_spec.rb index 3ba8fe72fe..ab6162e60b 100644 --- a/spec/jobs/poll_mailbox_spec.rb +++ b/spec/jobs/poll_mailbox_spec.rb @@ -20,8 +20,7 @@ RSpec.describe Jobs::PollMailbox do describe ".poll_pop3" do # the date is dynamic here because there is a 1 week cutoff for # the pop3 polling - let(:example_email) do - email = <<~EMAIL + let(:example_email) { email = <<~EMAIL } Return-Path: From: One To: team@bar.com @@ -34,27 +33,22 @@ RSpec.describe Jobs::PollMailbox do This is an email example. EMAIL - end context "with pop errors" do - before do - Discourse.expects(:handle_job_exception).at_least_once - end + before { Discourse.expects(:handle_job_exception).at_least_once } - after do - Discourse.redis.del(Jobs::PollMailbox::POLL_MAILBOX_TIMEOUT_ERROR_KEY) - end + after { Discourse.redis.del(Jobs::PollMailbox::POLL_MAILBOX_TIMEOUT_ERROR_KEY) } it "add an admin dashboard message on pop authentication error" do - Net::POP3.any_instance.expects(:start) - .raises(Net::POPAuthenticationError.new).at_least_once + Net::POP3.any_instance.expects(:start).raises(Net::POPAuthenticationError.new).at_least_once poller.poll_pop3 - i18n_key = 'dashboard.poll_pop3_auth_error' + i18n_key = "dashboard.poll_pop3_auth_error" - expect(AdminDashboardData.problem_message_check(i18n_key)) - .to eq(I18n.t(i18n_key, base_path: Discourse.base_path)) + expect(AdminDashboardData.problem_message_check(i18n_key)).to eq( + I18n.t(i18n_key, base_path: Discourse.base_path), + ) end it "logs an error on pop connection timeout error" do @@ -62,10 +56,11 @@ RSpec.describe Jobs::PollMailbox do 4.times { poller.poll_pop3 } - i18n_key = 'dashboard.poll_pop3_timeout' + i18n_key = "dashboard.poll_pop3_timeout" - expect(AdminDashboardData.problem_message_check(i18n_key)) - .to eq(I18n.t(i18n_key, base_path: Discourse.base_path)) + expect(AdminDashboardData.problem_message_check(i18n_key)).to eq( + I18n.t(i18n_key, base_path: Discourse.base_path), + ) end it "logs an error when pop fails and continues with next message" do @@ -91,7 +86,14 @@ RSpec.describe Jobs::PollMailbox do SiteSetting.pop3_polling_delete_from_server = true - poller.expects(:mail_too_old?).returns(false).then.raises(RuntimeError).then.returns(false).times(3) + poller + .expects(:mail_too_old?) + .returns(false) + .then + .raises(RuntimeError) + .then + .returns(false) + .times(3) poller.expects(:process_popmail).times(2) poller.poll_pop3 end @@ -162,12 +164,14 @@ RSpec.describe Jobs::PollMailbox do end it "does not reply to a bounced email" do - expect { process_popmail(:bounced_email) }.to_not change { ActionMailer::Base.deliveries.count } + expect { process_popmail(:bounced_email) }.to_not change { + ActionMailer::Base.deliveries.count + } incoming_email = IncomingEmail.last expect(incoming_email.rejection_message).to eq( - I18n.t("emails.incoming.errors.bounced_email_error") + I18n.t("emails.incoming.errors.bounced_email_error"), ) end end diff --git a/spec/jobs/post_update_topic_tracking_state_spec.rb b/spec/jobs/post_update_topic_tracking_state_spec.rb index 3bbed8a1ef..442f557401 100644 --- a/spec/jobs/post_update_topic_tracking_state_spec.rb +++ b/spec/jobs/post_update_topic_tracking_state_spec.rb @@ -3,12 +3,12 @@ RSpec.describe Jobs::PostUpdateTopicTrackingState do fab!(:post) { Fabricate(:post) } - it 'should publish messages' do + it "should publish messages" do messages = MessageBus.track_publish { subject.execute({ post_id: post.id }) } expect(messages.size).not_to eq(0) end - it 'should not publish messages for deleted topics' do + it "should not publish messages for deleted topics" do post.topic.trash! messages = MessageBus.track_publish { subject.execute({ post_id: post.id }) } expect(messages.size).to eq(0) diff --git a/spec/jobs/post_uploads_recovery_spec.rb b/spec/jobs/post_uploads_recovery_spec.rb index 58ab91a506..57c7633406 100644 --- a/spec/jobs/post_uploads_recovery_spec.rb +++ b/spec/jobs/post_uploads_recovery_spec.rb @@ -1,15 +1,13 @@ # frozen_string_literal: true RSpec.describe Jobs::PostUploadsRecovery do - describe '#grace_period' do - it 'should restrict the grace period to the right range' do - SiteSetting.purge_deleted_uploads_grace_period_days = - described_class::MIN_PERIOD - 1 + describe "#grace_period" do + it "should restrict the grace period to the right range" do + SiteSetting.purge_deleted_uploads_grace_period_days = described_class::MIN_PERIOD - 1 expect(described_class.new.grace_period).to eq(30) - SiteSetting.purge_deleted_uploads_grace_period_days = - described_class::MAX_PERIOD + 1 + SiteSetting.purge_deleted_uploads_grace_period_days = described_class::MAX_PERIOD + 1 expect(described_class.new.grace_period).to eq(120) end diff --git a/spec/jobs/problem_checks_spec.rb b/spec/jobs/problem_checks_spec.rb index 7f254f001d..39144f64fc 100644 --- a/spec/jobs/problem_checks_spec.rb +++ b/spec/jobs/problem_checks_spec.rb @@ -22,7 +22,11 @@ RSpec.describe Jobs::ProblemChecks do AdminDashboardData.add_scheduled_problem_check(:test_identifier) do [ AdminDashboardData::Problem.new("big problem"), - AdminDashboardData::Problem.new("yuge problem", priority: "high", identifier: "config_is_a_mess") + AdminDashboardData::Problem.new( + "yuge problem", + priority: "high", + identifier: "config_is_a_mess", + ), ] end @@ -34,8 +38,16 @@ RSpec.describe Jobs::ProblemChecks do it "does not add the same problem twice if the identifier already exists" do AdminDashboardData.add_scheduled_problem_check(:test_identifier) do [ - AdminDashboardData::Problem.new("yuge problem", priority: "high", identifier: "config_is_a_mess"), - AdminDashboardData::Problem.new("nasty problem", priority: "high", identifier: "config_is_a_mess") + AdminDashboardData::Problem.new( + "yuge problem", + priority: "high", + identifier: "config_is_a_mess", + ), + AdminDashboardData::Problem.new( + "nasty problem", + priority: "high", + identifier: "config_is_a_mess", + ), ] end diff --git a/spec/jobs/process_bulk_invite_emails_spec.rb b/spec/jobs/process_bulk_invite_emails_spec.rb index a177e244b8..021dcb0b34 100644 --- a/spec/jobs/process_bulk_invite_emails_spec.rb +++ b/spec/jobs/process_bulk_invite_emails_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.describe Jobs::ProcessBulkInviteEmails do - describe '#execute' do - it 'processes pending invites' do + describe "#execute" do + it "processes pending invites" do invite = Fabricate(:invite, emailed_status: Invite.emailed_status_types[:bulk_pending]) described_class.new.execute({}) diff --git a/spec/jobs/process_email_spec.rb b/spec/jobs/process_email_spec.rb index eeedaf8ace..a6240d4464 100644 --- a/spec/jobs/process_email_spec.rb +++ b/spec/jobs/process_email_spec.rb @@ -1,12 +1,10 @@ # frozen_string_literal: true RSpec.describe Jobs::ProcessEmail do - let(:mail) { "From: foo@bar.com\nTo: bar@foo.com\nSubject: FOO BAR\n\nFoo foo bar bar?" } it "process an email without retry" do Email::Processor.expects(:process!).with(mail, retry_on_rate_limit: false, source: nil) Jobs::ProcessEmail.new.execute(mail: mail) end - end diff --git a/spec/jobs/process_post_spec.rb b/spec/jobs/process_post_spec.rb index 98d843fcdc..20c1b1c46f 100644 --- a/spec/jobs/process_post_spec.rb +++ b/spec/jobs/process_post_spec.rb @@ -2,15 +2,15 @@ RSpec.describe Jobs::ProcessPost do it "returns when the post cannot be found" do - expect { Jobs::ProcessPost.new.perform(post_id: 1, sync_exec: true) }.not_to raise_error + expect { Jobs::ProcessPost.new.execute(post_id: 1) }.not_to raise_error end - context 'with a post' do + context "with a post" do fab!(:post) { Fabricate(:post) } - it 'does not erase posts when CookedPostProcessor malfunctions' do + it "does not erase posts when CookedPostProcessor malfunctions" do # Look kids, an actual reason why you want to use mocks - CookedPostProcessor.any_instance.expects(:html).returns(' ') + CookedPostProcessor.any_instance.expects(:html).returns(" ") cooked = post.cooked post.reload @@ -19,7 +19,7 @@ RSpec.describe Jobs::ProcessPost do Jobs::ProcessPost.new.execute(post_id: post.id, cook: true) end - it 'recooks if needed' do + it "recooks if needed" do cooked = post.cooked post.update_columns(cooked: "frogs") @@ -29,8 +29,9 @@ RSpec.describe Jobs::ProcessPost do expect(post.cooked).to eq(cooked) end - it 'processes posts' do - post = Fabricate(:post, raw: "") + it "processes posts" do + post = + Fabricate(:post, raw: "") expect(post.cooked).to match(/http/) stub_image_size @@ -47,8 +48,16 @@ RSpec.describe Jobs::ProcessPost do end it "extracts links to quoted posts" do - quoted_post = Fabricate(:post, raw: "This is a post with a link to https://www.discourse.org", post_number: 42) - post.update_columns(raw: "This quote is the best\n\n[quote=\"#{quoted_post.user.username}, topic:#{quoted_post.topic_id}, post:#{quoted_post.post_number}\"]\n#{quoted_post.excerpt}\n[/quote]") + quoted_post = + Fabricate( + :post, + raw: "This is a post with a link to https://www.discourse.org", + post_number: 42, + ) + post.update_columns( + raw: + "This quote is the best\n\n[quote=\"#{quoted_post.user.username}, topic:#{quoted_post.topic_id}, post:#{quoted_post.post_number}\"]\n#{quoted_post.excerpt}\n[/quote]", + ) stub_image_size # when creating a quote, we also create the reflexion link expect { Jobs::ProcessPost.new.execute(post_id: post.id) }.to change { TopicLink.count }.by(2) @@ -102,9 +111,7 @@ RSpec.describe Jobs::ProcessPost do end context "when download_remote_images_to_local? is enabled" do - before do - SiteSetting.download_remote_images_to_local = true - end + before { SiteSetting.download_remote_images_to_local = true } it "enqueues" do expect_enqueued_with(job: :pull_hotlinked_images, args: { post_id: post.id }) do @@ -117,6 +124,5 @@ RSpec.describe Jobs::ProcessPost do expect(Jobs::PullHotlinkedImages.jobs.size).to eq(0) end end - end end diff --git a/spec/jobs/process_shelved_notifications_spec.rb b/spec/jobs/process_shelved_notifications_spec.rb index fb100d36da..f0e419f733 100644 --- a/spec/jobs/process_shelved_notifications_spec.rb +++ b/spec/jobs/process_shelved_notifications_spec.rb @@ -8,16 +8,22 @@ RSpec.describe Jobs::ProcessShelvedNotifications do future = Fabricate(:do_not_disturb_timing, ends_at: 1.day.from_now) past = Fabricate(:do_not_disturb_timing, starts_at: 2.day.ago, ends_at: 1.minute.ago) - expect { - subject.execute({}) - }.to change { DoNotDisturbTiming.count }.by (-1) + expect { subject.execute({}) }.to change { DoNotDisturbTiming.count }.by (-1) expect(DoNotDisturbTiming.find_by(id: future.id)).to eq(future) expect(DoNotDisturbTiming.find_by(id: past.id)).to eq(nil) end it "does not process shelved_notifications when the user is in DND" do user.do_not_disturb_timings.create(starts_at: 2.days.ago, ends_at: 2.days.from_now) - notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) + notification = + Notification.create( + read: false, + user_id: user.id, + topic_id: 2, + post_number: 1, + data: "{}", + notification_type: 1, + ) expect(notification.shelved_notification).to be_present subject.execute({}) expect(notification.shelved_notification).to be_present @@ -25,7 +31,15 @@ RSpec.describe Jobs::ProcessShelvedNotifications do it "processes and destroys shelved_notifications when the user leaves DND" do user.do_not_disturb_timings.create(starts_at: 2.days.ago, ends_at: 2.days.from_now) - notification = Notification.create(read: false, user_id: user.id, topic_id: 2, post_number: 1, data: '{}', notification_type: 1) + notification = + Notification.create( + read: false, + user_id: user.id, + topic_id: 2, + post_number: 1, + data: "{}", + notification_type: 1, + ) user.do_not_disturb_timings.last.update(ends_at: 1.days.ago) expect(notification.shelved_notification).to be_present diff --git a/spec/jobs/publish_topic_to_category_spec.rb b/spec/jobs/publish_topic_to_category_spec.rb index 0f5c213f19..7632005561 100644 --- a/spec/jobs/publish_topic_to_category_spec.rb +++ b/spec/jobs/publish_topic_to_category_spec.rb @@ -7,12 +7,13 @@ RSpec.describe Jobs::PublishTopicToCategory do let(:topic) do topic = Fabricate(:topic, category: category) - Fabricate(:topic_timer, + Fabricate( + :topic_timer, status_type: TopicTimer.types[:publish_to_category], category_id: another_category.id, topic: topic, execute_at: 1.minute.ago, - created_at: 5.minutes.ago + created_at: 5.minutes.ago, ) Fabricate(:post, topic: topic, user: topic.user) @@ -20,8 +21,8 @@ RSpec.describe Jobs::PublishTopicToCategory do topic end - describe 'when topic has been deleted' do - it 'should not publish the topic to the new category' do + describe "when topic has been deleted" do + it "should not publish the topic to the new category" do created_at = freeze_time 1.hour.ago topic @@ -36,18 +37,17 @@ RSpec.describe Jobs::PublishTopicToCategory do end end - it 'should publish the topic to the new category' do + it "should publish the topic to the new category" do freeze_time 1.hour.ago do topic.update!(visible: false) end now = freeze_time - message = MessageBus.track_publish do - described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) - end.find do |m| - Hash === m.data && m.data.key?(:reload_topic) && m.data.key?(:refresh_stream) - end + message = + MessageBus + .track_publish { described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) } + .find { |m| Hash === m.data && m.data.key?(:reload_topic) && m.data.key?(:refresh_stream) } topic.reload expect(topic.category).to eq(another_category) @@ -55,32 +55,36 @@ RSpec.describe Jobs::PublishTopicToCategory do expect(topic.public_topic_timer).to eq(nil) expect(message.channel).to eq("/topic/#{topic.id}") - %w{created_at bumped_at updated_at last_posted_at}.each do |attribute| + %w[created_at bumped_at updated_at last_posted_at].each do |attribute| expect(topic.public_send(attribute)).to eq_time(now) end end - describe 'when topic is a private message' do - it 'should publish the topic to the new category' do + describe "when topic is a private message" do + it "should publish the topic to the new category" do freeze_time 1.hour.ago do - expect { topic.convert_to_private_message(Discourse.system_user) } - .to change { topic.private_message? }.to(true) + expect { topic.convert_to_private_message(Discourse.system_user) }.to change { + topic.private_message? + }.to(true) end topic.allowed_users << topic.public_topic_timer.user now = freeze_time - message = MessageBus.track_publish do - described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) - end.last + message = + MessageBus + .track_publish do + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id) + end + .last topic.reload expect(topic.category).to eq(another_category) expect(topic.visible).to eq(true) expect(topic.private_message?).to eq(false) - %w{created_at bumped_at updated_at last_posted_at}.each do |attribute| + %w[created_at bumped_at updated_at last_posted_at].each do |attribute| expect(topic.public_send(attribute)).to eq_time(now) end @@ -118,8 +122,8 @@ RSpec.describe Jobs::PublishTopicToCategory do end end - describe 'when new category has a default auto-close' do - it 'should apply the auto-close timer upon publishing' do + describe "when new category has a default auto-close" do + it "should apply the auto-close timer upon publishing" do freeze_time another_category.update!(auto_close_hours: 5) diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index b16915ae42..3f842c275c 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -5,26 +5,44 @@ RSpec.describe Jobs::PullHotlinkedImages do let(:broken_image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat2.png" } let(:large_image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat3.png" } let(:encoded_image_url) { "https://example.com/אלחוט-.jpg" } - let(:png) { Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") } - let(:large_png) { Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAK10lEQVR42r3aeVRTVx4H8Oc2atWO7Sw9OnM6HWvrOON0aFlcAZ3RopZWOyqgoCACKqPWBUVQi4gIqAVllciiKPu+JOyGnQQSNgkIIQgoKljAYVARCZnf4yXhkeXlJmDP+f4hOUF+n3fvffe++y5W0i4qJqWoDU8hKQUPxWFKcq9VnHxJ8gTi5EqS0yJOtiRZfHEyJWE0i0MnJaMJTzopaQ/wpJKS0ogneTQYABANTDlDvpxBCsiu72eUP0zPq8Fzr45e8TircRDFQAAy5ABpcgDCgJV2iCbRQM+rinU/E26ie9NgfrDO1GBtTBy96SH/WhBhaxwfGEjndmfKGeiaGsYAJXIANQyCkfR05u3dhuOKVhLamnmRzocyKp9mNo9QG9IRDDiAiMaG3Nqfo45aoJROzk3DDxNCbjGahBM0yAKoDfIDOpNZE/bNYrVKJyfylB2D91pdA3lAjwE0MDAyS+BCalw9kdu2xvT6AY0NWBkJoNaAzsrj4CN1YtUTidi/hdH4BvGmJGPAAYgGMuMery/U6ONJqZ5I1PlTjNExre7kgJU/EqEbJC0gjDpiiv9hnSkJ2z+t9dzxwNcSUudlUuuxnXP+W/bZTWWO64uO6hccWQ0pPm4IP1a6GFe5bYXvNF7f0xxg3XrzgCDYjn1m4+218/D/SndaYnSqBpMDDlDXkHYnMlh7Srj+HLanxfOsyyOVN0ScYI0zkOeVZvYZGEI2/DFDMkWgTw7jAGWUA5owMOt7QtcvDF09qybA/mGC6zA7aCLVExkq9U3895/wm9LpgyonBxmDGKDQoHBySPQ8B5e/zM2kJdalN/fqxKsn8oLhFr5mdvDyX6UVNqqcpMmDAWNJACjtUMDrDVn7m6SdS/kxPwrizg+zAycLAKm5tA0a4a7DPpSFhmIAxWAgDKm0IJrutBr/g3D5n9E9J7F6oiNFGf2WtnI2vboH3YADEA0AuG2ml2i2BC4/AAYKr00uAHL/ihk0QnxQMPqKFWM/FiEamFWPYMHD8tgF1UMmZfjKZLDIJ1z/vQibzTKrbop2wAGIhoxbt8IN5zZHnoHqO5LdJr16IkXHDG4afJDJG0B8chADUAxxTnbp1trE5Z/0ASDN09hTcJdLy+EoawQZgyyAwhCxcznr0k4C0JNz5R0BYFqM3PBhQugtxKdQrEICUGFoE4ZtWPAg4jQBeJHv/Y4AkBKHdTHuZ8lP0hSDAQdQGwhAUUNv4s6/EvcfSD/T590B2u8cj3SwltkNUGaQBSgbDAXc9pxTW4jqIf8ruAa37efJLg/DfuBd21ftYU7OA387+QXSk2gHWMmRw/M2F9D2d8WffsW8Sv5+X/mtyBN7s+V2NBQasMpOEYqhuLG3MimMqL4h/GTu4fW01b/z05qrMKEGC96W+8sA8g/qKX281JuWafX350lniG++rIpOTcknb8lQGHAAoqG+pgqqr7hqE2K4kCg0bO3CJDMthvVKInTrlUmm/4j+9vO7mxYNlfrJAJiHVsYaL0g1XZy194scmy+JMCyXxWz+CAD4anTFjLrLpiMVQW+4t1G2lQiDGIBiuF/NLbmwM1B3PpQe892SFtqh4fIAhZ14mBUo34WE7ECFC29hRdDz5LO5dtrwdAGM0pP/HKoMzWsZRtwakwVQGPJjo/2/ej9Q74N8xy19o+tQYcWNzjT3mJNmR/W/uPi9fobr3ifpl6hXeG9Zge1JF5LPWvz4zYoTa7VSzu0mniggMEigNcBQ7GjE5A9Kt/eoOxLGkQBUGkoyGeEbPqnys2+OPlcbdir80PdOX+usmDFdG8OIwCc3bI0vm657WeSrsPouhuelbQZh/9nqY7FB+lsGc2ad27w86oTJo5SLrwu9s/dpVXuYFPEHELcocQC1QXpjhS4EpcMwiPhh2/U9XzfedYYFhe7UKdJSqkNOIt4oMy/uIwP68n6C3/WzMmIFHIUeJawMLm7ul9lmVdYOYgCKob6aK72NEo8yQ+UBtl99BkXoTMFcv1sF3UNaIpd24vCqvykDvCr2PbJ6GQFwNtKFrjhuCHFCCvmvcuW2ihUaMO4TWYCyAU0GSJcSsCblRTjDSJAZoFnuNiafLqReMrQlukKTylQvBZC3iikMOIDCQGaQAT9nq1gLqQRQBABFLa9U7tcTBjEApR3IALh1/DIAlQZZAIWBDOjO9HrXAMT3JliVBKCyHciALsYvAUAx4IAqOYDCmxKPBFD5QDNBQHHLS2XvfmQMYgCKgQx4muGhFmCw1B8dIOTQyvj9FO+vyDclrPqpLECZgVczBoAlA3URMCubLv6D9I657ZOP0lws1QJQv4OTGnAAogEdAF+A+TXHw3b0R5qoszLLyx4+gc8RAeUt/SrfIxIGMYDCoBDwONVdaQ9mB+3XWeK87kvJ1EYTDfYLn9XDgsdO+3NYKSACUN6FQsYAKg2IgIqgY6tnzmi6bP8y2X2EmGUbkkWCPJitV82cURfuqPq5nhPM4vchvpDGauQAygxkAMW+ULCdsfWSj/tCTr8IdeqPdBnK94FnFCEr8DXd68CyRXeObkfpRWx+D+JLdRxANlC0QwMaINHZfP37c4oczQkDnjDnvlCnMuc9RvPnxp/ehQKokAAoOlIeGUDdDvKAtsQLyv72mzJ/P6uN+rNnHtf5S7GjRVeQQ6nTbge9pdB/vEzWDso9aqoEUBuw2mciZY0gY0AEEBHEuZzZqAdFG743c/n0aQ7rtBruOKO/y+HwnyMebsABiIbG2jFAa7wryh4bPDaUXD+swWuoKv5TxMMNYgCFgQSoIgHOv7uNLbgLcfldiAc0xgAqDbVtLwTJXgQAeojmLzLKAzjBxyl257vqcgsfChUeDJA3YHUkgEpDQz2vJU7cCDJTEnQSWOHBDK0wMACgL0U7mLptXWO/fGmCk7myGW2gOra09Q36aSUcoIahc4Rfmi59JBi3H5j3k5fJOs8dhgoTYL0Jqi/1PfyMTrUKHOKGcwS9Kg9okA1iALqh+tGggBFIGJRtn2gWWEHwmlsRD5lIDdj9LpG8gXpyuN/yRJBwEQCwRYWytkEcuB28iuK2EXVPXOEAqaEW2dBUzZI+HE/wTT2RnjpGSZtQg1NjYoDa7dA50sKMIgywyTPB6l9VRbPaXmt28m0MQNEOCgdDbXu/IM17tCO5TaQjveWG1Qi6NT75htWTAOoaeA/4gnhXlF0Wiq7f3NSk1okrGQMO0NzQOdLMziU60usSPw2q7+SVlnWMlE3g1BjG6xZNxFDe1s2OO0Z0JHhxBuMBJlroUSgju682ldUxTH24QaVhDFAvB1Bp4HS+PRO/5ZDP7xtjnaXLJGKlBMtVeGqDuRk2If97z/tl0XVYZg+T3nF0F3tcjN1W2vFWrdNK8gYcgGiQvykFFl7a7oFBvG5o5UfvVRQrRuQu+mjgH5lRu7JjLPISLAtTrJ1pf94dj4U0+mhw4opsEAPU6kiEIZ1XYnZlFgFQKzu8MYtYzKYUs63E7Lnz0ls5iKeVFBrGAGq1A6uj1zZw0XZPzPwuZhqE7biiqm4vzNQP/7JVFmZbgdlxxnKienFBe4/G7YA1kADI7TDilmQJZVlE41cRirBlYdZMzIqB7UnGdseRkohZZmDW+ZhNmfibEHvuzAOcaWTD5XpLuBepdfKtiAxQ1xDPTdnhOdXUH7Nlj7uWKDnAme7bvPlI1a/Hfz4ljp+BfnqPPKD/DzQWIVWNoUiJAAAAAElFTkSuQmCC") } + let(:png) do + Base64.decode64( + "R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==", + ) + end + let(:large_png) do + Base64.decode64( + "iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAK10lEQVR42r3aeVRTVx4H8Oc2atWO7Sw9OnM6HWvrOON0aFlcAZ3RopZWOyqgoCACKqPWBUVQi4gIqAVllciiKPu+JOyGnQQSNgkIIQgoKljAYVARCZnf4yXhkeXlJmDP+f4hOUF+n3fvffe++y5W0i4qJqWoDU8hKQUPxWFKcq9VnHxJ8gTi5EqS0yJOtiRZfHEyJWE0i0MnJaMJTzopaQ/wpJKS0ogneTQYABANTDlDvpxBCsiu72eUP0zPq8Fzr45e8TircRDFQAAy5ABpcgDCgJV2iCbRQM+rinU/E26ie9NgfrDO1GBtTBy96SH/WhBhaxwfGEjndmfKGeiaGsYAJXIANQyCkfR05u3dhuOKVhLamnmRzocyKp9mNo9QG9IRDDiAiMaG3Nqfo45aoJROzk3DDxNCbjGahBM0yAKoDfIDOpNZE/bNYrVKJyfylB2D91pdA3lAjwE0MDAyS+BCalw9kdu2xvT6AY0NWBkJoNaAzsrj4CN1YtUTidi/hdH4BvGmJGPAAYgGMuMery/U6ONJqZ5I1PlTjNExre7kgJU/EqEbJC0gjDpiiv9hnSkJ2z+t9dzxwNcSUudlUuuxnXP+W/bZTWWO64uO6hccWQ0pPm4IP1a6GFe5bYXvNF7f0xxg3XrzgCDYjn1m4+218/D/SndaYnSqBpMDDlDXkHYnMlh7Srj+HLanxfOsyyOVN0ScYI0zkOeVZvYZGEI2/DFDMkWgTw7jAGWUA5owMOt7QtcvDF09qybA/mGC6zA7aCLVExkq9U3895/wm9LpgyonBxmDGKDQoHBySPQ8B5e/zM2kJdalN/fqxKsn8oLhFr5mdvDyX6UVNqqcpMmDAWNJACjtUMDrDVn7m6SdS/kxPwrizg+zAycLAKm5tA0a4a7DPpSFhmIAxWAgDKm0IJrutBr/g3D5n9E9J7F6oiNFGf2WtnI2vboH3YADEA0AuG2ml2i2BC4/AAYKr00uAHL/ihk0QnxQMPqKFWM/FiEamFWPYMHD8tgF1UMmZfjKZLDIJ1z/vQibzTKrbop2wAGIhoxbt8IN5zZHnoHqO5LdJr16IkXHDG4afJDJG0B8chADUAxxTnbp1trE5Z/0ASDN09hTcJdLy+EoawQZgyyAwhCxcznr0k4C0JNz5R0BYFqM3PBhQugtxKdQrEICUGFoE4ZtWPAg4jQBeJHv/Y4AkBKHdTHuZ8lP0hSDAQdQGwhAUUNv4s6/EvcfSD/T590B2u8cj3SwltkNUGaQBSgbDAXc9pxTW4jqIf8ruAa37efJLg/DfuBd21ftYU7OA387+QXSk2gHWMmRw/M2F9D2d8WffsW8Sv5+X/mtyBN7s+V2NBQasMpOEYqhuLG3MimMqL4h/GTu4fW01b/z05qrMKEGC96W+8sA8g/qKX281JuWafX350lniG++rIpOTcknb8lQGHAAoqG+pgqqr7hqE2K4kCg0bO3CJDMthvVKInTrlUmm/4j+9vO7mxYNlfrJAJiHVsYaL0g1XZy194scmy+JMCyXxWz+CAD4anTFjLrLpiMVQW+4t1G2lQiDGIBiuF/NLbmwM1B3PpQe892SFtqh4fIAhZ14mBUo34WE7ECFC29hRdDz5LO5dtrwdAGM0pP/HKoMzWsZRtwakwVQGPJjo/2/ej9Q74N8xy19o+tQYcWNzjT3mJNmR/W/uPi9fobr3ifpl6hXeG9Zge1JF5LPWvz4zYoTa7VSzu0mniggMEigNcBQ7GjE5A9Kt/eoOxLGkQBUGkoyGeEbPqnys2+OPlcbdir80PdOX+usmDFdG8OIwCc3bI0vm657WeSrsPouhuelbQZh/9nqY7FB+lsGc2ad27w86oTJo5SLrwu9s/dpVXuYFPEHELcocQC1QXpjhS4EpcMwiPhh2/U9XzfedYYFhe7UKdJSqkNOIt4oMy/uIwP68n6C3/WzMmIFHIUeJawMLm7ul9lmVdYOYgCKob6aK72NEo8yQ+UBtl99BkXoTMFcv1sF3UNaIpd24vCqvykDvCr2PbJ6GQFwNtKFrjhuCHFCCvmvcuW2ihUaMO4TWYCyAU0GSJcSsCblRTjDSJAZoFnuNiafLqReMrQlukKTylQvBZC3iikMOIDCQGaQAT9nq1gLqQRQBABFLa9U7tcTBjEApR3IALh1/DIAlQZZAIWBDOjO9HrXAMT3JliVBKCyHciALsYvAUAx4IAqOYDCmxKPBFD5QDNBQHHLS2XvfmQMYgCKgQx4muGhFmCw1B8dIOTQyvj9FO+vyDclrPqpLECZgVczBoAlA3URMCubLv6D9I657ZOP0lws1QJQv4OTGnAAogEdAF+A+TXHw3b0R5qoszLLyx4+gc8RAeUt/SrfIxIGMYDCoBDwONVdaQ9mB+3XWeK87kvJ1EYTDfYLn9XDgsdO+3NYKSACUN6FQsYAKg2IgIqgY6tnzmi6bP8y2X2EmGUbkkWCPJitV82cURfuqPq5nhPM4vchvpDGauQAygxkAMW+ULCdsfWSj/tCTr8IdeqPdBnK94FnFCEr8DXd68CyRXeObkfpRWx+D+JLdRxANlC0QwMaINHZfP37c4oczQkDnjDnvlCnMuc9RvPnxp/ehQKokAAoOlIeGUDdDvKAtsQLyv72mzJ/P6uN+rNnHtf5S7GjRVeQQ6nTbge9pdB/vEzWDso9aqoEUBuw2mciZY0gY0AEEBHEuZzZqAdFG743c/n0aQ7rtBruOKO/y+HwnyMebsABiIbG2jFAa7wryh4bPDaUXD+swWuoKv5TxMMNYgCFgQSoIgHOv7uNLbgLcfldiAc0xgAqDbVtLwTJXgQAeojmLzLKAzjBxyl257vqcgsfChUeDJA3YHUkgEpDQz2vJU7cCDJTEnQSWOHBDK0wMACgL0U7mLptXWO/fGmCk7myGW2gOra09Q36aSUcoIahc4Rfmi59JBi3H5j3k5fJOs8dhgoTYL0Jqi/1PfyMTrUKHOKGcwS9Kg9okA1iALqh+tGggBFIGJRtn2gWWEHwmlsRD5lIDdj9LpG8gXpyuN/yRJBwEQCwRYWytkEcuB28iuK2EXVPXOEAqaEW2dBUzZI+HE/wTT2RnjpGSZtQg1NjYoDa7dA50sKMIgywyTPB6l9VRbPaXmt28m0MQNEOCgdDbXu/IM17tCO5TaQjveWG1Qi6NT75htWTAOoaeA/4gnhXlF0Wiq7f3NSk1okrGQMO0NzQOdLMziU60usSPw2q7+SVlnWMlE3g1BjG6xZNxFDe1s2OO0Z0JHhxBuMBJlroUSgju682ldUxTH24QaVhDFAvB1Bp4HS+PRO/5ZDP7xtjnaXLJGKlBMtVeGqDuRk2If97z/tl0XVYZg+T3nF0F3tcjN1W2vFWrdNK8gYcgGiQvykFFl7a7oFBvG5o5UfvVRQrRuQu+mjgH5lRu7JjLPISLAtTrJ1pf94dj4U0+mhw4opsEAPU6kiEIZ1XYnZlFgFQKzu8MYtYzKYUs63E7Lnz0ls5iKeVFBrGAGq1A6uj1zZw0XZPzPwuZhqE7biiqm4vzNQP/7JVFmZbgdlxxnKienFBe4/G7YA1kADI7TDilmQJZVlE41cRirBlYdZMzIqB7UnGdseRkohZZmDW+ZhNmfibEHvuzAOcaWTD5XpLuBepdfKtiAxQ1xDPTdnhOdXUH7Nlj7uWKDnAme7bvPlI1a/Hfz4ljp+BfnqPPKD/DzQWIVWNoUiJAAAAAElFTkSuQmCC", + ) + end let(:upload_path) { Discourse.store.upload_path } before do Jobs.run_immediately! stub_request(:get, image_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) - stub_request(:get, encoded_image_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) + stub_request(:get, encoded_image_url).to_return( + body: png, + headers: { + "Content-Type" => "image/png", + }, + ) stub_request(:get, broken_image_url).to_return(status: 404) - stub_request(:get, large_image_url).to_return(body: large_png, headers: { "Content-Type" => "image/png" }) - - stub_request( - :get, - "#{Discourse.base_url}/#{upload_path}/original/1X/f59ea56fe8ebe42048491d43a19d9f34c5d0f8dc.gif" + stub_request(:get, large_image_url).to_return( + body: large_png, + headers: { + "Content-Type" => "image/png", + }, ) stub_request( :get, - "#{Discourse.base_url}/#{upload_path}/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png" + "#{Discourse.base_url}/#{upload_path}/original/1X/f59ea56fe8ebe42048491d43a19d9f34c5d0f8dc.gif", + ) + + stub_request( + :get, + "#{Discourse.base_url}/#{upload_path}/original/1X/c530c06cf89c410c0355d7852644a73fc3ec8c04.png", ) SiteSetting.download_remote_images_to_local = true @@ -32,22 +50,20 @@ RSpec.describe Jobs::PullHotlinkedImages do SiteSetting.download_remote_images_threshold = 0 end - describe '#execute' do - before do - Jobs.run_immediately! - end + describe "#execute" do + before { Jobs.run_immediately! } - it 'does nothing if topic has been deleted' do + it "does nothing if topic has been deleted" do post = Fabricate(:post, raw: "") post.topic.destroy! - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.not_to change { Upload.count } + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.not_to change { + Upload.count + } end - it 'does nothing if there are no large images to pull' do - post = Fabricate(:post, raw: 'bob bob') + it "does nothing if there are no large images to pull" do + post = Fabricate(:post, raw: "bob bob") orig = post.updated_at freeze_time 1.week.from_now @@ -55,18 +71,18 @@ RSpec.describe Jobs::PullHotlinkedImages do expect(orig).to eq_time(post.reload.updated_at) end - it 'replaces images' do + it "replaces images" do post = Fabricate(:post, raw: "") stub_image_size - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) - .and not_change { UserHistory.count } # Should not add to the staff log + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1).and not_change { UserHistory.count } # Should not add to the staff log expect(post.reload.raw).to eq("") end - it 'enqueues raw replacement job with a delay' do + it "enqueues raw replacement job with a delay" do Jobs.run_later! post = Fabricate(:post, raw: "") @@ -76,12 +92,16 @@ RSpec.describe Jobs::PullHotlinkedImages do Jobs.expects(:cancel_scheduled_job).with(:update_hotlinked_raw, post_id: post.id).once delay = SiteSetting.editing_grace_period + 1 - expect_enqueued_with(job: :update_hotlinked_raw, args: { post_id: post.id }, at: Time.zone.now + delay.seconds) do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end + expect_enqueued_with( + job: :update_hotlinked_raw, + args: { + post_id: post.id, + }, + at: Time.zone.now + delay.seconds, + ) { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } end - it 'removes downloaded images when they are no longer needed' do + it "removes downloaded images when they are no longer needed" do post = Fabricate(:post, raw: "") stub_image_size post.rebake! @@ -94,43 +114,41 @@ RSpec.describe Jobs::PullHotlinkedImages do expect(post.upload_references.count).to eq(0) end - it 'replaces images again after edit' do + it "replaces images again after edit" do post = Fabricate(:post, raw: "") stub_image_size - expect do - post.rebake! - end.to change { Upload.count }.by(1) + expect do post.rebake! end.to change { Upload.count }.by(1) expect(post.reload.raw).to eq("") # Post raw is updated back to the old value (e.g. by wordpress integration) post.update(raw: "") - expect do - post.rebake! - end.not_to change { Upload.count } # We alread have the upload + expect do post.rebake! end.not_to change { Upload.count } # We alread have the upload expect(post.reload.raw).to eq("") end - it 'replaces encoded image urls' do + it "replaces encoded image urls" do post = Fabricate(:post, raw: "") stub_image_size - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.to change { Upload.count }.by(1) + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.to change { + Upload.count + }.by(1) expect(post.reload.raw).to eq("") end - it 'replaces images in an anchor tag with weird indentation' do + it "replaces images in an anchor tag with weird indentation" do # Skipped pending https://meta.discourse.org/t/152801 # This spec was previously passing, even though the resulting markdown was invalid # Now the spec has been improved, and shows the issue - stub_request(:get, "http://test.localhost/uploads/short-url/z2QSs1KJWoj51uYhDjb6ifCzxH6.gif") - .to_return(status: 200, body: "") + stub_request( + :get, + "http://test.localhost/uploads/short-url/z2QSs1KJWoj51uYhDjb6ifCzxH6.gif", + ).to_return(status: 200, body: "") post = Fabricate(:post, raw: <<~MD)

    @@ -139,9 +157,9 @@ RSpec.describe Jobs::PullHotlinkedImages do MD - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.to change { Upload.count }.by(1) + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.to change { + Upload.count + }.by(1) upload = post.uploads.last @@ -153,55 +171,55 @@ RSpec.describe Jobs::PullHotlinkedImages do MD end - it 'replaces correct image URL' do - url = image_url.sub("/2e/Longcat1.png", '') + it "replaces correct image URL" do + url = image_url.sub("/2e/Longcat1.png", "") post = Fabricate(:post, raw: "[Images](#{url})\n![](#{image_url})") stub_image_size - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.to change { Upload.count }.by(1) + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.to change { + Upload.count + }.by(1) expect(post.reload.raw).to eq("[Images](#{url})\n![](#{Upload.last.short_url})") end - it 'replaces images without protocol' do - url = image_url.sub(/^https?\:/, '') + it "replaces images without protocol" do + url = image_url.sub(/^https?\:/, "") post = Fabricate(:post, raw: "test") stub_image_size - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.to change { Upload.count }.by(1) + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.to change { + Upload.count + }.by(1) expect(post.reload.raw).to eq("\"test\"") end - it 'replaces images without extension' do - url = image_url.sub(/\.[a-zA-Z0-9]+$/, '') + it "replaces images without extension" do + url = image_url.sub(/\.[a-zA-Z0-9]+$/, "") stub_request(:get, url).to_return(body: png, headers: { "Content-Type" => "image/png" }) post = Fabricate(:post, raw: "") stub_image_size - expect do - Jobs::PullHotlinkedImages.new.execute(post_id: post.id) - end.to change { Upload.count }.by(1) + expect do Jobs::PullHotlinkedImages.new.execute(post_id: post.id) end.to change { + Upload.count + }.by(1) expect(post.reload.raw).to eq("") end - it 'replaces optimized images' do + it "replaces optimized images" do optimized_image = Fabricate(:optimized_image) url = "#{Discourse.base_url}#{optimized_image.url}" - stub_request(:get, url) - .to_return(status: 200, body: file_from_fixtures("smallest.png")) + stub_request(:get, url).to_return(status: 200, body: file_from_fixtures("smallest.png")) post = Fabricate(:post, raw: "") stub_image_size - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) upload = Upload.last post.reload @@ -233,8 +251,9 @@ RSpec.describe Jobs::PullHotlinkedImages do url = Discourse.base_url + url post = Fabricate(:post, raw: "") upload.update(access_control_post: post) - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .not_to change { Upload.count } + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.not_to change { + Upload.count + } end context "when the upload original_sha1 is missing" do @@ -253,8 +272,9 @@ RSpec.describe Jobs::PullHotlinkedImages do # without this we get an infinite hang... Post.any_instance.stubs(:trigger_post_process) - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) end end @@ -272,16 +292,18 @@ RSpec.describe Jobs::PullHotlinkedImages do upload.update(access_control_post: Fabricate(:post)) FileStore::S3Store.any_instance.stubs(:store_upload).returns(upload.url) - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .not_to change { Upload.count } + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.not_to change { + Upload.count + } end end end - it 'replaces markdown image' do + it "replaces markdown image" do post = Fabricate(:post, raw: <<~MD) [![some test](#{image_url})](https://somelink.com) ![some test](#{image_url}) @@ -291,8 +313,9 @@ RSpec.describe Jobs::PullHotlinkedImages do MD stub_image_size - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) post.reload @@ -305,18 +328,19 @@ RSpec.describe Jobs::PullHotlinkedImages do MD end - it 'works when invalid url in post' do + it "works when invalid url in post" do post = Fabricate(:post, raw: <<~MD) ![some test](#{image_url}) ![some test 2]("#{image_url}) MD stub_image_size - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) end - it 'replaces bbcode images' do + it "replaces bbcode images" do post = Fabricate(:post, raw: <<~MD) [img] #{image_url} @@ -328,8 +352,9 @@ RSpec.describe Jobs::PullHotlinkedImages do MD stub_image_size - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) post.reload @@ -340,17 +365,21 @@ RSpec.describe Jobs::PullHotlinkedImages do MD end - describe 'onebox' do + describe "onebox" do let(:media) { "File:Brisbane_May_2013201.jpg" } let(:url) { "https://commons.wikimedia.org/wiki/#{media}" } - let(:api_url) { "https://en.wikipedia.org/w/api.php?action=query&titles=#{media}&prop=imageinfo&iilimit=50&iiprop=timestamp|user|url&iiurlwidth=500&format=json" } + let(:api_url) do + "https://en.wikipedia.org/w/api.php?action=query&titles=#{media}&prop=imageinfo&iilimit=50&iiprop=timestamp|user|url&iiurlwidth=500&format=json" + end before do stub_request(:head, url) - stub_request(:get, url).to_return(body: '') + stub_request(:get, url).to_return(body: "") stub_request(:head, image_url) - stub_request(:get, api_url).to_return(body: "{ + stub_request(:get, api_url).to_return( + body: + "{ \"query\": { \"pages\": { \"-1\": { @@ -363,21 +392,22 @@ RSpec.describe Jobs::PullHotlinkedImages do } } } - }") + }", + ) end - it 'replaces image src' do + it "replaces image src" do post = Fabricate(:post, raw: "#{url}") stub_image_size post.rebake! post.reload - expect(post.cooked).to match(/ #{url} @@ -402,9 +432,7 @@ RSpec.describe Jobs::PullHotlinkedImages do MD stub_image_size - 2.times do - post.rebake! - end + 2.times { post.rebake! } post.reload @@ -416,13 +444,13 @@ RSpec.describe Jobs::PullHotlinkedImages do ![](upload://z2QSs1KJWoj51uYhDjb6ifCzxH6.gif) MD - expect(post.cooked).to match(/

    /) end - it 'rewrites a lone onebox' do + it "rewrites a lone onebox" do post = Fabricate(:post, raw: <<~MD) Onebox here: #{image_url} @@ -438,35 +466,35 @@ RSpec.describe Jobs::PullHotlinkedImages do ![](upload://z2QSs1KJWoj51uYhDjb6ifCzxH6.gif) MD - expect(post.cooked).to match(/", - title: "Some title that is long enough" - ) + it "replaces missing local uploads in lightbox link" do + post = + PostCreator.create!( + user, + raw: "", + title: "Some title that is long enough", + ) expect(post.reload.cooked).to have_tag(:a, with: { class: "lightbox" }) - stub_request(:get, "#{Discourse.base_url}#{upload.url}") - .to_return(status: 200, body: file_from_fixtures("smallest.png")) + stub_request(:get, "#{Discourse.base_url}#{upload.url}").to_return( + status: 200, + body: file_from_fixtures("smallest.png"), + ) upload.delete - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .to change { Upload.count }.by(1) + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.to change { + Upload.count + }.by(1) post.reload @@ -572,16 +600,18 @@ RSpec.describe Jobs::PullHotlinkedImages do end it "doesn't remove optimized images from lightboxes" do - post = PostCreator.create!( - user, - raw: "![alt](#{upload.short_url})", - title: "Some title that is long enough" - ) + post = + PostCreator.create!( + user, + raw: "![alt](#{upload.short_url})", + title: "Some title that is long enough", + ) expect(post.reload.cooked).to have_tag(:a, with: { class: "lightbox" }) - expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) } - .not_to change { Upload.count } + expect { Jobs::PullHotlinkedImages.new.execute(post_id: post.id) }.not_to change { + Upload.count + } post.reload @@ -605,12 +635,14 @@ RSpec.describe Jobs::PullHotlinkedImages do end context "when there's not enough disk space" do - before { SiteSetting.download_remote_images_threshold = 75 } it "disables download_remote_images_threshold and send a notification to the admin" do StaffActionLogger.any_instance.expects(:log_site_setting_change).once - SystemMessage.expects(:create_from_system_user).with(Discourse.site_contact_user, :download_remote_images_disabled).once + SystemMessage + .expects(:create_from_system_user) + .with(Discourse.site_contact_user, :download_remote_images_disabled) + .once job.execute({ post_id: post.id }) expect(SiteSetting.download_remote_images_to_local).to eq(false) @@ -622,12 +654,14 @@ RSpec.describe Jobs::PullHotlinkedImages do expect(SiteSetting.download_remote_images_to_local).to eq(true) end - end end def stub_s3(upload) stub_upload(upload) - stub_request(:get, "https:" + upload.url).to_return(status: 200, body: file_from_fixtures("smallest.png")) + stub_request(:get, "https:" + upload.url).to_return( + status: 200, + body: file_from_fixtures("smallest.png"), + ) end end diff --git a/spec/jobs/pull_user_profile_hotlinked_images_spec.rb b/spec/jobs/pull_user_profile_hotlinked_images_spec.rb index 2dbce8437f..de96bf09af 100644 --- a/spec/jobs/pull_user_profile_hotlinked_images_spec.rb +++ b/spec/jobs/pull_user_profile_hotlinked_images_spec.rb @@ -4,26 +4,32 @@ RSpec.describe Jobs::PullUserProfileHotlinkedImages do fab!(:user) { Fabricate(:user) } let(:image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat1.png" } - let(:png) { Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") } + let(:png) do + Base64.decode64( + "R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==", + ) + end before do stub_request(:get, image_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) SiteSetting.download_remote_images_to_local = true end - describe '#execute' do - before do - stub_image_size - end + describe "#execute" do + before { stub_image_size } - it 'replaces images' do + it "replaces images" do user.user_profile.update!(bio_raw: "![](#{image_url})") - expect { Jobs::PullUserProfileHotlinkedImages.new.execute(user_id: user.id) }.to change { Upload.count }.by(1) + expect { Jobs::PullUserProfileHotlinkedImages.new.execute(user_id: user.id) }.to change { + Upload.count + }.by(1) expect(user.user_profile.reload.bio_cooked).to include(Upload.last.url) end - it 'handles nil bio' do - expect { Jobs::PullUserProfileHotlinkedImages.new.execute(user_id: user.id) }.not_to change { Upload.count } + it "handles nil bio" do + expect { Jobs::PullUserProfileHotlinkedImages.new.execute(user_id: user.id) }.not_to change { + Upload.count + } expect(user.user_profile.reload.bio_cooked).to eq(nil) end end diff --git a/spec/jobs/rebake_custom_emoji_posts_spec.rb b/spec/jobs/rebake_custom_emoji_posts_spec.rb index 5f64ba824d..49951a4660 100644 --- a/spec/jobs/rebake_custom_emoji_posts_spec.rb +++ b/spec/jobs/rebake_custom_emoji_posts_spec.rb @@ -1,20 +1,20 @@ # frozen_string_literal: true RSpec.describe Jobs::RebakeCustomEmojiPosts do - it 'should rebake posts that are using a given custom emoji' do + it "should rebake posts that are using a given custom emoji" do upload = Fabricate(:upload) - custom_emoji = CustomEmoji.create!(name: 'test', upload: upload) + custom_emoji = CustomEmoji.create!(name: "test", upload: upload) Emoji.clear_cache - post = Fabricate(:post, raw: 'some post with :test: yay') + post = Fabricate(:post, raw: "some post with :test: yay") expect(post.reload.cooked).to eq( - "

    some post with \":test:\" yay

    " + "

    some post with \":test:\" yay

    ", ) custom_emoji.destroy! Emoji.clear_cache - described_class.new.execute(name: 'test') + described_class.new.execute(name: "test") - expect(post.reload.cooked).to eq('

    some post with :test: yay

    ') + expect(post.reload.cooked).to eq("

    some post with :test: yay

    ") end end diff --git a/spec/jobs/refresh_users_reviewable_counts_spec.rb b/spec/jobs/refresh_users_reviewable_counts_spec.rb index 7437996391..667ca6c038 100644 --- a/spec/jobs/refresh_users_reviewable_counts_spec.rb +++ b/spec/jobs/refresh_users_reviewable_counts_spec.rb @@ -7,7 +7,9 @@ RSpec.describe Jobs::RefreshUsersReviewableCounts do fab!(:group) { Fabricate(:group) } fab!(:topic) { Fabricate(:topic) } - fab!(:reviewable1) { Fabricate(:reviewable, reviewable_by_group: group, reviewable_by_moderator: true, topic: topic) } + fab!(:reviewable1) do + Fabricate(:reviewable, reviewable_by_group: group, reviewable_by_moderator: true, topic: topic) + end fab!(:reviewable2) { Fabricate(:reviewable, reviewable_by_moderator: false) } fab!(:reviewable3) { Fabricate(:reviewable, reviewable_by_moderator: true) } @@ -19,11 +21,12 @@ RSpec.describe Jobs::RefreshUsersReviewableCounts do Group.refresh_automatic_groups! end - describe '#execute' do + describe "#execute" do it "publishes reviewable counts for the members of the specified groups" do - messages = MessageBus.track_publish do - described_class.new.execute(group_ids: [Group::AUTO_GROUPS[:staff]]) - end + messages = + MessageBus.track_publish do + described_class.new.execute(group_ids: [Group::AUTO_GROUPS[:staff]]) + end expect(messages.size).to eq(2) moderator_message = messages.find { |m| m.user_ids == [moderator.id] } @@ -32,9 +35,7 @@ RSpec.describe Jobs::RefreshUsersReviewableCounts do admin_message = messages.find { |m| m.user_ids == [admin.id] } expect(moderator_message.channel).to eq("/reviewable_counts/#{moderator.id}") - messages = MessageBus.track_publish do - described_class.new.execute(group_ids: [group.id]) - end + messages = MessageBus.track_publish { described_class.new.execute(group_ids: [group.id]) } expect(messages.size).to eq(1) user_message = messages.find { |m| m.user_ids == [user.id] } @@ -42,9 +43,10 @@ RSpec.describe Jobs::RefreshUsersReviewableCounts do end it "published counts respect reviewables visibility" do - messages = MessageBus.track_publish do - described_class.new.execute(group_ids: [Group::AUTO_GROUPS[:staff], group.id]) - end + messages = + MessageBus.track_publish do + described_class.new.execute(group_ids: [Group::AUTO_GROUPS[:staff], group.id]) + end expect(messages.size).to eq(3) admin_message = messages.find { |m| m.user_ids == [admin.id] } @@ -52,22 +54,13 @@ RSpec.describe Jobs::RefreshUsersReviewableCounts do user_message = messages.find { |m| m.user_ids == [user.id] } expect(admin_message.channel).to eq("/reviewable_counts/#{admin.id}") - expect(admin_message.data).to eq( - reviewable_count: 3, - unseen_reviewable_count: 3 - ) + expect(admin_message.data).to eq(reviewable_count: 3, unseen_reviewable_count: 3) expect(moderator_message.channel).to eq("/reviewable_counts/#{moderator.id}") - expect(moderator_message.data).to eq( - reviewable_count: 2, - unseen_reviewable_count: 2 - ) + expect(moderator_message.data).to eq(reviewable_count: 2, unseen_reviewable_count: 2) expect(user_message.channel).to eq("/reviewable_counts/#{user.id}") - expect(user_message.data).to eq( - reviewable_count: 1, - unseen_reviewable_count: 1 - ) + expect(user_message.data).to eq(reviewable_count: 1, unseen_reviewable_count: 1) end end end diff --git a/spec/jobs/regular/bulk_user_title_update_spec.rb b/spec/jobs/regular/bulk_user_title_update_spec.rb index fa6c9b8da1..ebe66c0e4d 100644 --- a/spec/jobs/regular/bulk_user_title_update_spec.rb +++ b/spec/jobs/regular/bulk_user_title_update_spec.rb @@ -1,33 +1,37 @@ # frozen_string_literal: true RSpec.describe Jobs::BulkUserTitleUpdate do - fab!(:badge) { Fabricate(:badge, name: 'Protector of the Realm', allow_title: true) } + fab!(:badge) { Fabricate(:badge, name: "Protector of the Realm", allow_title: true) } fab!(:user) { Fabricate(:user) } fab!(:other_user) { Fabricate(:user) } - describe 'update action' do + describe "update action" do before do BadgeGranter.grant(badge, user) user.update(title: badge.name) end - it 'updates the title of all users with the attached granted title badge id on their profile' do + it "updates the title of all users with the attached granted title badge id on their profile" do execute_update - expect(user.reload.title).to eq('King of the Forum') + expect(user.reload.title).to eq("King of the Forum") end - it 'does not set the title for any other users' do + it "does not set the title for any other users" do execute_update - expect(other_user.reload.title).not_to eq('King of the Forum') + expect(other_user.reload.title).not_to eq("King of the Forum") end def execute_update - described_class.new.execute(new_title: 'King of the Forum', granted_badge_id: badge.id, action: described_class::UPDATE_ACTION) + described_class.new.execute( + new_title: "King of the Forum", + granted_badge_id: badge.id, + action: described_class::UPDATE_ACTION, + ) end end - describe 'reset action' do - let(:customized_badge_name) { 'Merit Badge' } + describe "reset action" do + let(:customized_badge_name) { "Merit Badge" } before do TranslationOverride.upsert!(I18n.locale, Badge.i18n_key(badge.name), customized_badge_name) @@ -35,14 +39,12 @@ RSpec.describe Jobs::BulkUserTitleUpdate do user.update(title: customized_badge_name) end - it 'updates the title of all users back to the original badge name' do + it "updates the title of all users back to the original badge name" do expect(user.reload.title).to eq(customized_badge_name) described_class.new.execute(granted_badge_id: badge.id, action: described_class::RESET_ACTION) - expect(user.reload.title).to eq('Protector of the Realm') + expect(user.reload.title).to eq("Protector of the Realm") end - after do - TranslationOverride.revert!(I18n.locale, Badge.i18n_key(badge.name)) - end + after { TranslationOverride.revert!(I18n.locale, Badge.i18n_key(badge.name)) } end end diff --git a/spec/jobs/regular/group_smtp_email_spec.rb b/spec/jobs/regular/group_smtp_email_spec.rb index d01ce2c02d..475e939d9b 100644 --- a/spec/jobs/regular/group_smtp_email_spec.rb +++ b/spec/jobs/regular/group_smtp_email_spec.rb @@ -15,7 +15,7 @@ RSpec.describe Jobs::GroupSmtpEmail do group_id: group.id, post_id: post_id, email: "test@test.com", - cc_emails: ["otherguy@test.com", "cormac@lit.com"] + cc_emails: %w[otherguy@test.com cormac@lit.com], } end let(:staged1) { Fabricate(:staged, email: "otherguy@test.com") } @@ -51,14 +51,20 @@ RSpec.describe Jobs::GroupSmtpEmail do it "includes a 'reply above this line' message" do subject.execute(args) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) - expect(email_log.as_mail_message.html_part.to_s).to include(I18n.t("user_notifications.reply_above_line")) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + expect(email_log.as_mail_message.html_part.to_s).to include( + I18n.t("user_notifications.reply_above_line"), + ) end it "does not include context posts" do subject.execute(args) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) - expect(email_log.as_mail_message.text_part.to_s).not_to include(I18n.t("user_notifications.previous_discussion")) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + expect(email_log.as_mail_message.text_part.to_s).not_to include( + I18n.t("user_notifications.previous_discussion"), + ) expect(email_log.as_mail_message.text_part.to_s).not_to include("some first post content") end @@ -67,14 +73,20 @@ RSpec.describe Jobs::GroupSmtpEmail do post.update!(reply_to_post_number: 1, reply_to_user: second_post.user) PostReply.create(post: second_post, reply: post) subject.execute(args) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) - expect(email_log.raw_headers).to include("In-Reply-To: ") - expect(email_log.as_mail_message.html_part.to_s).not_to include(I18n.t("user_notifications.in_reply_to")) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + expect(email_log.raw_headers).to include( + "In-Reply-To: ", + ) + expect(email_log.as_mail_message.html_part.to_s).not_to include( + I18n.t("user_notifications.in_reply_to"), + ) end it "includes the participants in the correct format (but not the recipient user), and does not have links for the staged users" do subject.execute(args) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) email_text = email_log.as_mail_message.text_part.to_s expect(email_text).to include("Support Group") expect(email_text).to include("otherguy@test.com") @@ -87,7 +99,8 @@ RSpec.describe Jobs::GroupSmtpEmail do it "creates an EmailLog record with the correct details" do subject.execute(args) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log).not_to eq(nil) expect(email_log.message_id).to eq("discourse/post/#{post.id}@test.localhost") end @@ -96,7 +109,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - incoming_email = IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) + incoming_email = + IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) expect(incoming_email).not_to eq(nil) expect(incoming_email.message_id).to eq("discourse/post/#{post.id}@test.localhost") expect(incoming_email.created_via).to eq(IncomingEmail.created_via_types[:group_smtp]) @@ -109,7 +123,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) post_reply_key = PostReplyKey.where(user_id: recipient_user, post_id: post.id).first expect(post_reply_key).to eq(nil) expect(email_log.raw_headers).not_to include("Reply-To: Support Group <#{group.email_username}") @@ -120,7 +135,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log).not_to eq(nil) expect(email_log.message_id).to eq("discourse/post/#{post.id}@test.localhost") end @@ -129,7 +145,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - incoming_email = IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) + incoming_email = + IncomingEmail.find_by(post_id: post.id, topic_id: post.topic_id, user_id: post.user.id) expect(incoming_email).not_to eq(nil) expect(incoming_email.message_id).to eq("discourse/post/#{post.id}@test.localhost") expect(incoming_email.created_via).to eq(IncomingEmail.created_via_types[:group_smtp]) @@ -142,7 +159,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) post_reply_key = PostReplyKey.where(user_id: recipient_user, post_id: post.id).first expect(post_reply_key).to eq(nil) expect(email_log.raw).not_to include("Reply-To: Support Group <#{group.email_username}") @@ -154,7 +172,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log.raw_headers).to include("From: support-group <#{group.email_username}") end @@ -162,7 +181,8 @@ RSpec.describe Jobs::GroupSmtpEmail do subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(1) expect(ActionMailer::Base.deliveries.last.subject).to eq("Re: Help I need support") - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log.to_address).to eq("test@test.com") expect(email_log.smtp_group_id).to eq(group.id) end @@ -174,7 +194,7 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(ActionMailer::Base.deliveries.count).to eq(1) last_email = ActionMailer::Base.deliveries.last expect(last_email.subject).to eq("Re: Help I need support") - expect(last_email.cc).to match_array(["otherguy@test.com", "cormac@lit.com"]) + expect(last_email.cc).to match_array(%w[otherguy@test.com cormac@lit.com]) end context "when there are cc_addresses" do @@ -183,8 +203,9 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(ActionMailer::Base.deliveries.count).to eq(1) sent_mail = ActionMailer::Base.deliveries.last expect(sent_mail.subject).to eq("Re: Help I need support") - expect(sent_mail.cc).to eq(["otherguy@test.com", "cormac@lit.com"]) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + expect(sent_mail.cc).to eq(%w[otherguy@test.com cormac@lit.com]) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log.cc_addresses).to eq("otherguy@test.com;cormac@lit.com") expect(email_log.cc_user_ids).to match_array([staged1.id, staged2.id]) end @@ -197,7 +218,8 @@ RSpec.describe Jobs::GroupSmtpEmail do expect(sent_mail.subject).to eq("Re: Help I need support") expect(sent_mail.cc).to eq(["otherguy@test.com"]) expect(sent_mail.bcc).to eq(["cormac@lit.com"]) - email_log = EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) + email_log = + EmailLog.find_by(post_id: post.id, topic_id: post.topic_id, user_id: recipient_user.id) expect(email_log.cc_addresses).to eq("otherguy@test.com") expect(email_log.bcc_addresses).to eq("cormac@lit.com") expect(email_log.cc_user_ids).to match_array([staged1.id]) @@ -208,9 +230,7 @@ RSpec.describe Jobs::GroupSmtpEmail do let(:post_id) { post.topic.posts.first.id } context "when the group has imap enabled" do - before do - group.update!(imap_enabled: true) - end + before { group.update!(imap_enabled: true) } it "aborts and does not send a group SMTP email; the OP is the one that sent the email in the first place" do expect { subject.execute(args) }.not_to(change { EmailLog.count }) @@ -219,9 +239,7 @@ RSpec.describe Jobs::GroupSmtpEmail do end context "when the group does not have imap enabled" do - before do - group.update!(imap_enabled: false) - end + before { group.update!(imap_enabled: false) } it "sends the email as expected" do subject.execute(args) @@ -235,13 +253,15 @@ RSpec.describe Jobs::GroupSmtpEmail do post.trash! subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(0) - expect(SkippedEmailLog.exists?( - email_type: "group_smtp", - user: recipient_user, - post: nil, - to_address: recipient_user.email, - reason_type: SkippedEmailLog.reason_types[:group_smtp_post_deleted] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "group_smtp", + user: recipient_user, + post: nil, + to_address: recipient_user.email, + reason_type: SkippedEmailLog.reason_types[:group_smtp_post_deleted], + ), + ).to eq(true) end end @@ -250,13 +270,15 @@ RSpec.describe Jobs::GroupSmtpEmail do post.topic.trash! subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(0) - expect(SkippedEmailLog.exists?( - email_type: "group_smtp", - user: recipient_user, - post: post, - to_address: recipient_user.email, - reason_type: SkippedEmailLog.reason_types[:group_smtp_topic_deleted] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "group_smtp", + user: recipient_user, + post: post, + to_address: recipient_user.email, + reason_type: SkippedEmailLog.reason_types[:group_smtp_topic_deleted], + ), + ).to eq(true) end end @@ -289,13 +311,15 @@ RSpec.describe Jobs::GroupSmtpEmail do group.update!(smtp_enabled: false) subject.execute(args) expect(ActionMailer::Base.deliveries.count).to eq(0) - expect(SkippedEmailLog.exists?( - email_type: "group_smtp", - user: recipient_user, - post: post, - to_address: recipient_user.email, - reason_type: SkippedEmailLog.reason_types[:group_smtp_disabled_for_group] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "group_smtp", + user: recipient_user, + post: post, + to_address: recipient_user.email, + reason_type: SkippedEmailLog.reason_types[:group_smtp_disabled_for_group], + ), + ).to eq(true) end end end diff --git a/spec/jobs/regular/publish_group_membership_updates_spec.rb b/spec/jobs/regular/publish_group_membership_updates_spec.rb index 822c39e711..6f5479b090 100644 --- a/spec/jobs/regular/publish_group_membership_updates_spec.rb +++ b/spec/jobs/regular/publish_group_membership_updates_spec.rb @@ -4,63 +4,54 @@ describe Jobs::PublishGroupMembershipUpdates do fab!(:user) { Fabricate(:user) } fab!(:group) { Fabricate(:group) } - it 'publishes events for added users' do - events = DiscourseEvent.track_events do - subject.execute( - user_ids: [user.id], group_id: group.id, type: 'add' - ) - end + it "publishes events for added users" do + events = + DiscourseEvent.track_events do + subject.execute(user_ids: [user.id], group_id: group.id, type: "add") + end expect(events).to include( event_name: :user_added_to_group, - params: [user, group, { automatic: group.automatic }] + params: [user, group, { automatic: group.automatic }], ) end - it 'publishes events for removed users' do - events = DiscourseEvent.track_events do - subject.execute( - user_ids: [user.id], group_id: group.id, type: 'remove' - ) - end + it "publishes events for removed users" do + events = + DiscourseEvent.track_events do + subject.execute(user_ids: [user.id], group_id: group.id, type: "remove") + end - expect(events).to include( - event_name: :user_removed_from_group, - params: [user, group] - ) + expect(events).to include(event_name: :user_removed_from_group, params: [user, group]) end it "does nothing if the group doesn't exist" do - events = DiscourseEvent.track_events do - subject.execute( - user_ids: [user.id], group_id: nil, type: 'add' - ) - end + events = + DiscourseEvent.track_events do + subject.execute(user_ids: [user.id], group_id: nil, type: "add") + end expect(events).not_to include( event_name: :user_added_to_group, - params: [user, group, { automatic: group.automatic }] + params: [user, group, { automatic: group.automatic }], ) end - it 'fails when the update type is invalid' do - expect { - subject.execute( - user_ids: [user.id], group_id: nil, type: nil - ) - }.to raise_error(Discourse::InvalidParameters) + it "fails when the update type is invalid" do + expect { subject.execute(user_ids: [user.id], group_id: nil, type: nil) }.to raise_error( + Discourse::InvalidParameters, + ) end - it 'does nothing when the user is not human' do - events = DiscourseEvent.track_events do - subject.execute( - user_ids: [Discourse.system_user.id], group_id: nil, type: 'add' - ) - end + it "does nothing when the user is not human" do + events = + DiscourseEvent.track_events do + subject.execute(user_ids: [Discourse.system_user.id], group_id: nil, type: "add") + end expect(events).not_to include( event_name: :user_added_to_group, - params: [user, group, { automatic: group.automatic }] + params: [user, group, { automatic: group.automatic }], ) end end diff --git a/spec/jobs/regular/update_post_uploads_secure_status_spec.rb b/spec/jobs/regular/update_post_uploads_secure_status_spec.rb index 8d728f770d..ea9fbd2c41 100644 --- a/spec/jobs/regular/update_post_uploads_secure_status_spec.rb +++ b/spec/jobs/regular/update_post_uploads_secure_status_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Jobs::UpdatePostUploadsSecureStatus do fab!(:post) { Fabricate(:post) } diff --git a/spec/jobs/reindex_search_spec.rb b/spec/jobs/reindex_search_spec.rb index e61f9e71a8..ae63f64d05 100644 --- a/spec/jobs/reindex_search_spec.rb +++ b/spec/jobs/reindex_search_spec.rb @@ -6,13 +6,13 @@ RSpec.describe Jobs::ReindexSearch do Jobs.run_immediately! end - let(:locale) { 'fr' } + let(:locale) { "fr" } # This works since test db has a small record less than limit. # Didn't check `topic` because topic doesn't have posts in fabrication # thus no search data - %w(post category user).each do |m| + %w[post category user].each do |m| it "should rebuild `#{m}` when default_locale changed" do - SiteSetting.default_locale = 'en' + SiteSetting.default_locale = "en" model = Fabricate(m.to_sym) SiteSetting.default_locale = locale subject.execute({}) @@ -27,12 +27,13 @@ RSpec.describe Jobs::ReindexSearch do model.reload subject.execute({}) - expect(model.public_send("#{m}_search_data").version) - .to eq("SearchIndexer::#{m.upcase}_INDEX_VERSION".constantize) + expect(model.public_send("#{m}_search_data").version).to eq( + "SearchIndexer::#{m.upcase}_INDEX_VERSION".constantize, + ) end end - describe 'rebuild_posts' do + describe "rebuild_posts" do class FakeIndexer def self.index(post, force:) get_posts.push(post) @@ -53,9 +54,7 @@ RSpec.describe Jobs::ReindexSearch do end end - after do - FakeIndexer.reset - end + after { FakeIndexer.reset } it "should not reindex posts that belong to a deleted topic or have been trashed" do post = Fabricate(:post) @@ -70,7 +69,7 @@ RSpec.describe Jobs::ReindexSearch do expect(FakeIndexer.posts).to contain_exactly(post) end - it 'should not reindex posts with a developmental version' do + it "should not reindex posts with a developmental version" do post = Fabricate(:post, version: SearchIndexer::MIN_POST_REINDEX_VERSION + 1) subject.rebuild_posts(indexer: FakeIndexer) @@ -78,14 +77,11 @@ RSpec.describe Jobs::ReindexSearch do expect(FakeIndexer.posts).to eq([]) end - it 'should not reindex posts with empty raw' do + it "should not reindex posts with empty raw" do post = Fabricate(:post) post.post_search_data.destroy! - post2 = Fabricate.build(:post, - raw: "", - post_type: Post.types[:small_action] - ) + post2 = Fabricate.build(:post, raw: "", post_type: Post.types[:small_action]) post2.save!(validate: false) @@ -95,7 +91,7 @@ RSpec.describe Jobs::ReindexSearch do end end - describe '#execute' do + describe "#execute" do it "should clean up topic_search_data of trashed topics" do topic = Fabricate(:post).topic topic2 = Fabricate(:post).topic @@ -107,9 +103,7 @@ RSpec.describe Jobs::ReindexSearch do expect { subject.execute({}) }.to change { TopicSearchData.count }.by(-1) expect(Topic.pluck(:id)).to contain_exactly(topic2.id) - expect(TopicSearchData.pluck(:topic_id)).to contain_exactly( - topic2.topic_search_data.topic_id - ) + expect(TopicSearchData.pluck(:topic_id)).to contain_exactly(topic2.topic_search_data.topic_id) end it "should clean up post_search_data of posts with empty raw or posts from trashed topics" do @@ -132,13 +126,9 @@ RSpec.describe Jobs::ReindexSearch do expect { subject.execute({}) }.to change { PostSearchData.count }.by(-3) - expect(Post.pluck(:id)).to contain_exactly( - post.id, post2.id, post3.id, post4.id, post5.id - ) + expect(Post.pluck(:id)).to contain_exactly(post.id, post2.id, post3.id, post4.id, post5.id) - expect(PostSearchData.pluck(:post_id)).to contain_exactly( - post.id, post3.id, post5.id - ) + expect(PostSearchData.pluck(:post_id)).to contain_exactly(post.id, post3.id, post5.id) end end end diff --git a/spec/jobs/remove_banner_spec.rb b/spec/jobs/remove_banner_spec.rb index 6456568eb1..fecdcfbd09 100644 --- a/spec/jobs/remove_banner_spec.rb +++ b/spec/jobs/remove_banner_spec.rb @@ -4,44 +4,40 @@ RSpec.describe Jobs::RemoveBanner do fab!(:topic) { Fabricate(:topic) } fab!(:user) { topic.user } - context 'when topic is not bannered until' do - it 'doesn’t enqueue a future job to remove it' do - expect do - topic.make_banner!(user) - end.not_to change { Jobs::RemoveBanner.jobs.size } + context "when topic is not bannered until" do + it "doesn’t enqueue a future job to remove it" do + expect do topic.make_banner!(user) end.not_to change { Jobs::RemoveBanner.jobs.size } end end - context 'when topic is bannered until' do - context 'when bannered_until is a valid date' do - it 'enqueues a future job to remove it' do + context "when topic is bannered until" do + context "when bannered_until is a valid date" do + it "enqueues a future job to remove it" do bannered_until = 5.days.from_now expect(topic.archetype).to eq(Archetype.default) - expect do - topic.make_banner!(user, bannered_until.to_s) - end.to change { Jobs::RemoveBanner.jobs.size }.by(1) + expect do topic.make_banner!(user, bannered_until.to_s) end.to change { + Jobs::RemoveBanner.jobs.size + }.by(1) topic.reload expect(topic.archetype).to eq(Archetype.banner) job = Jobs::RemoveBanner.jobs[0] - expect(Time.at(job['at'])).to be_within_one_minute_of(bannered_until) - expect(job['args'][0]['topic_id']).to eq(topic.id) + expect(Time.at(job["at"])).to be_within_one_minute_of(bannered_until) + expect(job["args"][0]["topic_id"]).to eq(topic.id) - job['class'].constantize.new.perform(*job['args']) + job["class"].constantize.new.perform(*job["args"]) topic.reload expect(topic.archetype).to eq(Archetype.default) end end - context 'when bannered_until is an invalid date' do - it 'doesn’t enqueue a future job to remove it' do + context "when bannered_until is an invalid date" do + it "doesn’t enqueue a future job to remove it" do expect do - expect do - topic.make_banner!(user, 'xxx') - end.to raise_error(Discourse::InvalidParameters) + expect do topic.make_banner!(user, "xxx") end.to raise_error(Discourse::InvalidParameters) end.not_to change { Jobs::RemoveBanner.jobs.size } end end diff --git a/spec/jobs/reviewable_priorities_spec.rb b/spec/jobs/reviewable_priorities_spec.rb index 9b3ea9f030..adad762a6c 100644 --- a/spec/jobs/reviewable_priorities_spec.rb +++ b/spec/jobs/reviewable_priorities_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe Jobs::ReviewablePriorities do - it "needs returns 0s with no existing reviewables" do Jobs::ReviewablePriorities.new.execute({}) @@ -38,7 +37,7 @@ RSpec.describe Jobs::ReviewablePriorities do expect(Reviewable.score_required_to_hide_post).to eq(8.33) end - context 'when there are enough reviewables' do + context "when there are enough reviewables" do let(:medium_threshold) { 8.0 } let(:high_threshold) { 13.0 } let(:score_to_hide_post) { 8.66 } @@ -54,7 +53,7 @@ RSpec.describe Jobs::ReviewablePriorities do expect(Reviewable.score_required_to_hide_post).to eq(score_to_hide_post) end - it 'ignore negative scores when calculating priorities' do + it "ignore negative scores when calculating priorities" do create_reviewables(Jobs::ReviewablePriorities.min_reviewables) negative_score = -9 10.times { create_with_score(negative_score) } @@ -67,7 +66,7 @@ RSpec.describe Jobs::ReviewablePriorities do expect(Reviewable.score_required_to_hide_post).to eq(score_to_hide_post) end - it 'ignores non-approved reviewables' do + it "ignores non-approved reviewables" do create_reviewables(Jobs::ReviewablePriorities.min_reviewables) low_score = 2 10.times { create_with_score(low_score, status: :pending) } diff --git a/spec/jobs/send_system_message_spec.rb b/spec/jobs/send_system_message_spec.rb index a21942d01f..7bacdd5940 100644 --- a/spec/jobs/send_system_message_spec.rb +++ b/spec/jobs/send_system_message_spec.rb @@ -2,25 +2,37 @@ RSpec.describe Jobs::SendSystemMessage do it "raises an error without a user_id" do - expect { Jobs::SendSystemMessage.new.execute(message_type: 'welcome_invite') }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::SendSystemMessage.new.execute(message_type: "welcome_invite") }.to raise_error( + Discourse::InvalidParameters, + ) end it "raises an error without a message_type" do - expect { Jobs::SendSystemMessage.new.execute(user_id: 1234) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::SendSystemMessage.new.execute(user_id: 1234) }.to raise_error( + Discourse::InvalidParameters, + ) end - context 'with valid parameters' do + context "with valid parameters" do fab!(:user) { Fabricate(:user) } it "should call SystemMessage.create" do - SystemMessage.any_instance.expects(:create).with('welcome_invite', {}) - Jobs::SendSystemMessage.new.execute(user_id: user.id, message_type: 'welcome_invite') + SystemMessage.any_instance.expects(:create).with("welcome_invite", {}) + Jobs::SendSystemMessage.new.execute(user_id: user.id, message_type: "welcome_invite") end it "can send message parameters" do - options = { url: "/t/no-spammers-please/123", edit_delay: 5, flag_reason: "Flagged by community" } - SystemMessage.any_instance.expects(:create).with('post_hidden', options) - Jobs::SendSystemMessage.new.execute(user_id: user.id, message_type: 'post_hidden', message_options: options) + options = { + url: "/t/no-spammers-please/123", + edit_delay: 5, + flag_reason: "Flagged by community", + } + SystemMessage.any_instance.expects(:create).with("post_hidden", options) + Jobs::SendSystemMessage.new.execute( + user_id: user.id, + message_type: "post_hidden", + message_options: options, + ) end end end diff --git a/spec/jobs/suspicious_login_spec.rb b/spec/jobs/suspicious_login_spec.rb index 5d0ac1aa0a..973aeef20b 100644 --- a/spec/jobs/suspicious_login_spec.rb +++ b/spec/jobs/suspicious_login_spec.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true RSpec.describe Jobs::SuspiciousLogin do - fab!(:user) { Fabricate(:moderator) } let(:zurich) { { latitude: 47.3686498, longitude: 8.5391825 } } # Zurich, Switzerland - let(:bern) { { latitude: 46.947922, longitude: 7.444608 } } # Bern, Switzerland + let(:bern) { { latitude: 46.947922, longitude: 7.444608 } } # Bern, Switzerland let(:london) { { latitude: 51.5073509, longitude: -0.1277583 } } # London, United Kingdom before do @@ -51,8 +50,6 @@ RSpec.describe Jobs::SuspiciousLogin do expect(UserAuthTokenLog.where(action: "suspicious").count).to eq(1) - expect(Jobs::CriticalUserEmail.jobs.first["args"].first["type"]) - .to eq('suspicious_login') + expect(Jobs::CriticalUserEmail.jobs.first["args"].first["type"]).to eq("suspicious_login") end - end diff --git a/spec/jobs/sync_acls_for_uploads_spec.rb b/spec/jobs/sync_acls_for_uploads_spec.rb index a7b43162ca..8f28e27b26 100644 --- a/spec/jobs/sync_acls_for_uploads_spec.rb +++ b/spec/jobs/sync_acls_for_uploads_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" RSpec.describe Jobs::SyncAclsForUploads do let(:upload1) { Fabricate(:upload) } @@ -29,7 +29,13 @@ RSpec.describe Jobs::SyncAclsForUploads do end it "handles updates throwing an exception" do - Discourse.store.expects(:update_upload_ACL).raises(StandardError).then.returns(true, true).times(3) + Discourse + .store + .expects(:update_upload_ACL) + .raises(StandardError) + .then + .returns(true, true) + .times(3) Discourse.expects(:warn_exception).once run_job end diff --git a/spec/jobs/tl3_promotions_spec.rb b/spec/jobs/tl3_promotions_spec.rb index 770bb39c16..d1f4455665 100644 --- a/spec/jobs/tl3_promotions_spec.rb +++ b/spec/jobs/tl3_promotions_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Jobs::Tl3Promotions do topics_entered: 1000, posts_read_count: 1000, likes_given: 1000, - likes_received: 1000 + likes_received: 1000, ) end @@ -51,9 +51,7 @@ RSpec.describe Jobs::Tl3Promotions do user end - before do - SiteSetting.tl3_promotion_min_duration = 3 - end + before { SiteSetting.tl3_promotion_min_duration = 3 } it "demotes if was promoted more than X days ago" do user = nil @@ -82,9 +80,7 @@ RSpec.describe Jobs::Tl3Promotions do it "doesn't demote if user hasn't lost requirements (low water mark)" do user = nil - freeze_time(4.days.ago) do - user = create_leader_user - end + freeze_time(4.days.ago) { user = create_leader_user } TrustLevel3Requirements.any_instance.stubs(:requirements_met?).returns(false) TrustLevel3Requirements.any_instance.stubs(:requirements_lost?).returns(false) @@ -103,7 +99,6 @@ RSpec.describe Jobs::Tl3Promotions do TrustLevel3Requirements.any_instance.stubs(:requirements_lost?).returns(true) run_job expect(user.reload.trust_level).to eq(TrustLevel[2]) - end it "doesn't demote user if their group_granted_trust_level is 3" do @@ -120,11 +115,9 @@ RSpec.describe Jobs::Tl3Promotions do end it "doesn't demote with very high tl3_promotion_min_duration value" do - SiteSetting.stubs(:tl3_promotion_min_duration).returns(2000000000) + SiteSetting.stubs(:tl3_promotion_min_duration).returns(2_000_000_000) user = nil - freeze_time(500.days.ago) do - user = create_leader_user - end + freeze_time(500.days.ago) { user = create_leader_user } expect(user).to be_on_tl3_grace_period TrustLevel3Requirements.any_instance.stubs(:requirements_met?).returns(false) TrustLevel3Requirements.any_instance.stubs(:requirements_lost?).returns(true) diff --git a/spec/jobs/toggle_topic_closed_spec.rb b/spec/jobs/toggle_topic_closed_spec.rb index b7c8eacbc4..406749d897 100644 --- a/spec/jobs/toggle_topic_closed_spec.rb +++ b/spec/jobs/toggle_topic_closed_spec.rb @@ -3,67 +3,45 @@ RSpec.describe Jobs::ToggleTopicClosed do fab!(:admin) { Fabricate(:admin) } - fab!(:topic) do - Fabricate(:topic_timer, user: admin).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: admin).topic } - it 'should be able to close a topic' do + it "should be able to close a topic" do topic freeze_time(61.minutes.from_now) do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) expect(topic.reload.closed).to eq(true) - expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_enabled_minutes', count: 61 - )) + expect(Post.last.raw).to eq(I18n.t("topic_statuses.autoclosed_enabled_minutes", count: 61)) end end - describe 'opening a topic' do - it 'should be work' do + describe "opening a topic" do + it "should be work" do topic.update!(closed: true) freeze_time(61.minutes.from_now) do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: false - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: false) expect(topic.reload.closed).to eq(false) - expect(Post.last.raw).to eq(I18n.t( - 'topic_statuses.autoclosed_disabled_minutes', count: 61 - )) + expect(Post.last.raw).to eq(I18n.t("topic_statuses.autoclosed_disabled_minutes", count: 61)) end end - describe 'when category has auto close configured' do + describe "when category has auto close configured" do fab!(:category) do - Fabricate(:category, - auto_close_based_on_last_post: true, - auto_close_hours: 5 - ) + Fabricate(:category, auto_close_based_on_last_post: true, auto_close_hours: 5) end fab!(:topic) { Fabricate(:topic, category: category, closed: true) } it "should restore the category's auto close timer" do - Fabricate(:topic_timer, - status_type: TopicTimer.types[:open], - topic: topic, - user: admin - ) + Fabricate(:topic_timer, status_type: TopicTimer.types[:open], topic: topic, user: admin) freeze_time(61.minutes.from_now) do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: false - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: false) expect(topic.reload.closed).to eq(false) @@ -76,61 +54,47 @@ RSpec.describe Jobs::ToggleTopicClosed do end end - describe 'when trying to close a topic that has already been closed' do - it 'should delete the topic timer' do + describe "when trying to close a topic that has already been closed" do + it "should delete the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) topic.update!(closed: true) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) end end - describe 'when trying to close a topic that has been deleted' do - it 'should delete the topic timer' do + describe "when trying to close a topic that has been deleted" do + it "should delete the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) topic.trash! expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) end end - describe 'when user is no longer authorized to close topics' do + describe "when user is no longer authorized to close topics" do fab!(:user) { Fabricate(:user) } - fab!(:topic) do - Fabricate(:topic_timer, user: user).topic - end + fab!(:topic) { Fabricate(:topic_timer, user: user).topic } - it 'should destroy the topic timer' do + it "should destroy the topic timer" do freeze_time(topic.public_topic_timer.execute_at + 1.minute) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) end.to change { TopicTimer.exists?(topic_id: topic.id) }.from(true).to(false) expect(topic.reload.closed).to eq(false) end it "should reconfigure topic timer if category's topics are set to autoclose" do - category = Fabricate(:category, - auto_close_based_on_last_post: true, - auto_close_hours: 5 - ) + category = Fabricate(:category, auto_close_based_on_last_post: true, auto_close_hours: 5) topic = Fabricate(:topic, category: category) topic.public_topic_timer.update!(user: user) @@ -138,12 +102,10 @@ RSpec.describe Jobs::ToggleTopicClosed do freeze_time(topic.public_topic_timer.execute_at + 1.minute) expect do - described_class.new.execute( - topic_timer_id: topic.public_topic_timer.id, - state: true - ) - end.to change { topic.reload.public_topic_timer.user }.from(user).to(Discourse.system_user) - .and change { topic.public_topic_timer.id } + described_class.new.execute(topic_timer_id: topic.public_topic_timer.id, state: true) + end.to change { topic.reload.public_topic_timer.user }.from(user).to( + Discourse.system_user, + ).and change { topic.public_topic_timer.id } expect(topic.reload.closed).to eq(false) end diff --git a/spec/jobs/topic_timer_enqueuer_spec.rb b/spec/jobs/topic_timer_enqueuer_spec.rb index 1500cb4f4f..95f73fd93e 100644 --- a/spec/jobs/topic_timer_enqueuer_spec.rb +++ b/spec/jobs/topic_timer_enqueuer_spec.rb @@ -4,21 +4,39 @@ RSpec.describe Jobs::TopicTimerEnqueuer do subject { described_class.new } fab!(:timer1) do - Fabricate(:topic_timer, execute_at: 1.minute.ago, created_at: 1.hour.ago, status_type: TopicTimer.types[:close]) + Fabricate( + :topic_timer, + execute_at: 1.minute.ago, + created_at: 1.hour.ago, + status_type: TopicTimer.types[:close], + ) end fab!(:timer2) do - Fabricate(:topic_timer, execute_at: 1.minute.ago, created_at: 1.hour.ago, status_type: TopicTimer.types[:open]) + Fabricate( + :topic_timer, + execute_at: 1.minute.ago, + created_at: 1.hour.ago, + status_type: TopicTimer.types[:open], + ) end fab!(:future_timer) do - Fabricate(:topic_timer, execute_at: 1.hours.from_now, created_at: 1.hour.ago, status_type: TopicTimer.types[:close]) + Fabricate( + :topic_timer, + execute_at: 1.hours.from_now, + created_at: 1.hour.ago, + status_type: TopicTimer.types[:close], + ) end fab!(:deleted_timer) do - Fabricate(:topic_timer, execute_at: 1.minute.ago, created_at: 1.hour.ago, status_type: TopicTimer.types[:close]) + Fabricate( + :topic_timer, + execute_at: 1.minute.ago, + created_at: 1.hour.ago, + status_type: TopicTimer.types[:close], + ) end - before do - deleted_timer.trash! - end + before { deleted_timer.trash! } it "does not enqueue deleted timers" do expect_not_enqueued_with(job: :close_topic, args: { topic_timer_id: deleted_timer.id }) diff --git a/spec/jobs/truncate_user_flag_stats_spec.rb b/spec/jobs/truncate_user_flag_stats_spec.rb index 3c64369ff2..ffd1c0981e 100644 --- a/spec/jobs/truncate_user_flag_stats_spec.rb +++ b/spec/jobs/truncate_user_flag_stats_spec.rb @@ -15,9 +15,7 @@ RSpec.describe Jobs::TruncateUserFlagStats do end it "raises an error without user ids" do - expect { - described_class.new.execute({}) - }.to raise_error(Discourse::InvalidParameters) + expect { described_class.new.execute({}) }.to raise_error(Discourse::InvalidParameters) end it "does nothing if the user doesn't have enough flags" do @@ -79,5 +77,4 @@ RSpec.describe Jobs::TruncateUserFlagStats do expect(other_user.user_stat.flags_disagreed).to eq(1) expect(other_user.user_stat.flags_ignored).to eq(1) end - end diff --git a/spec/jobs/update_gravatar_spec.rb b/spec/jobs/update_gravatar_spec.rb index 4b72fe9527..24637edb01 100644 --- a/spec/jobs/update_gravatar_spec.rb +++ b/spec/jobs/update_gravatar_spec.rb @@ -2,14 +2,18 @@ RSpec.describe Jobs::UpdateGravatar do fab!(:user) { Fabricate(:user) } - let(:temp) { Tempfile.new('test') } + let(:temp) { Tempfile.new("test") } fab!(:upload) { Fabricate(:upload, user: user) } let(:avatar) { user.create_user_avatar! } it "picks gravatar if system avatar is picked and gravatar was just downloaded" do temp.binmode # tiny valid png - temp.write(Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==")) + temp.write( + Base64.decode64( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", + ), + ) temp.rewind FileHelper.expects(:download).returns(temp) @@ -38,8 +42,7 @@ RSpec.describe Jobs::UpdateGravatar do SiteSetting.automatically_download_gravatars = true - expect { user.refresh_avatar } - .not_to change { Jobs::UpdateGravatar.jobs.count } + expect { user.refresh_avatar }.not_to change { Jobs::UpdateGravatar.jobs.count } user.reload expect(user.uploaded_avatar_id).to eq(nil) diff --git a/spec/jobs/update_s3_inventory_spec.rb b/spec/jobs/update_s3_inventory_spec.rb index ff8e032f04..ab43aa4060 100644 --- a/spec/jobs/update_s3_inventory_spec.rb +++ b/spec/jobs/update_s3_inventory_spec.rb @@ -20,7 +20,8 @@ RSpec.describe Jobs::UpdateS3Inventory do @client.expects(:put_bucket_policy).with( bucket: "special-bucket", - policy: %Q|{"Version":"2012-10-17","Statement":[{"Sid":"InventoryAndAnalyticsPolicy","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::special-bucket/#{path}/*"],"Condition":{"ArnLike":{"aws:SourceArn":"arn:aws:s3:::special-bucket"},"StringEquals":{"s3:x-amz-acl":"bucket-owner-full-control"}}}]}| + policy: + %Q|{"Version":"2012-10-17","Statement":[{"Sid":"InventoryAndAnalyticsPolicy","Effect":"Allow","Principal":{"Service":"s3.amazonaws.com"},"Action":["s3:PutObject"],"Resource":["arn:aws:s3:::special-bucket/#{path}/*"],"Condition":{"ArnLike":{"aws:SourceArn":"arn:aws:s3:::special-bucket"},"StringEquals":{"s3:x-amz-acl":"bucket-owner-full-control"}}}]}|, ) @client.expects(:put_bucket_inventory_configuration) @client.expects(:put_bucket_inventory_configuration).with( @@ -31,19 +32,21 @@ RSpec.describe Jobs::UpdateS3Inventory do s3_bucket_destination: { bucket: "arn:aws:s3:::special-bucket", prefix: path, - format: "CSV" - } + format: "CSV", + }, }, filter: { - prefix: id + prefix: id, }, is_enabled: true, id: id, included_object_versions: "Current", optional_fields: ["ETag"], - schedule: { frequency: "Daily" } + schedule: { + frequency: "Daily", + }, }, - use_accelerate_endpoint: false + use_accelerate_endpoint: false, ) described_class.new.execute(nil) diff --git a/spec/jobs/update_username_spec.rb b/spec/jobs/update_username_spec.rb index fd4a57a92b..f95699a077 100644 --- a/spec/jobs/update_username_spec.rb +++ b/spec/jobs/update_username_spec.rb @@ -3,15 +3,16 @@ RSpec.describe Jobs::UpdateUsername do fab!(:user) { Fabricate(:user) } - it 'does not do anything if user_id is invalid' do - events = DiscourseEvent.track_events do - described_class.new.execute( - user_id: -999, - old_username: user.username, - new_username: 'somenewusername', - avatar_template: user.avatar_template - ) - end + it "does not do anything if user_id is invalid" do + events = + DiscourseEvent.track_events do + described_class.new.execute( + user_id: -999, + old_username: user.username, + new_username: "somenewusername", + avatar_template: user.avatar_template, + ) + end expect(events).to eq([]) end diff --git a/spec/jobs/user_email_spec.rb b/spec/jobs/user_email_spec.rb index efda1132b6..3a52f547be 100644 --- a/spec/jobs/user_email_spec.rb +++ b/spec/jobs/user_email_spec.rb @@ -1,33 +1,44 @@ # frozen_string_literal: true RSpec.describe Jobs::UserEmail do - before do - SiteSetting.email_time_window_mins = 10 - end + before { SiteSetting.email_time_window_mins = 10 } fab!(:user) { Fabricate(:user, last_seen_at: 11.minutes.ago) } fab!(:staged) { Fabricate(:user, staged: true, last_seen_at: 11.minutes.ago) } - fab!(:suspended) { Fabricate(:user, last_seen_at: 10.minutes.ago, suspended_at: 5.minutes.ago, suspended_till: 7.days.from_now) } + fab!(:suspended) do + Fabricate( + :user, + last_seen_at: 10.minutes.ago, + suspended_at: 5.minutes.ago, + suspended_till: 7.days.from_now, + ) + end fab!(:anonymous) { Fabricate(:anonymous, last_seen_at: 11.minutes.ago) } it "raises an error when there is no user" do - expect { Jobs::UserEmail.new.execute(type: :digest) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::UserEmail.new.execute(type: :digest) }.to raise_error( + Discourse::InvalidParameters, + ) end it "raises an error when there is no type" do - expect { Jobs::UserEmail.new.execute(user_id: user.id) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::UserEmail.new.execute(user_id: user.id) }.to raise_error( + Discourse::InvalidParameters, + ) end it "raises an error when the type doesn't exist" do - expect { Jobs::UserEmail.new.execute(type: :no_method, user_id: user.id) }.to raise_error(Discourse::InvalidParameters) + expect { Jobs::UserEmail.new.execute(type: :no_method, user_id: user.id) }.to raise_error( + Discourse::InvalidParameters, + ) end - context 'when digest can be generated' do + context "when digest can be generated" do fab!(:user) { Fabricate(:user, last_seen_at: 8.days.ago, last_emailed_at: 8.days.ago) } fab!(:popular_topic) { Fabricate(:topic, user: Fabricate(:admin), created_at: 1.hour.ago) } it "doesn't call the mailer when the user is missing" do - Jobs::UserEmail.new.execute(type: :digest, user_id: User.last.id + 10000) + Jobs::UserEmail.new.execute(type: :digest, user_id: User.last.id + 10_000) expect(ActionMailer::Base.deliveries).to eq([]) end @@ -37,7 +48,7 @@ RSpec.describe Jobs::UserEmail do expect(ActionMailer::Base.deliveries).to eq([]) end - context 'when not emailed recently' do + context "when not emailed recently" do before do freeze_time user.update!(last_emailed_at: 8.days.ago) @@ -50,14 +61,14 @@ RSpec.describe Jobs::UserEmail do end end - context 'when recently emailed' do + context "when recently emailed" do before do freeze_time user.update!(last_emailed_at: 2.hours.ago) user.user_option.update!(digest_after_minutes: 1.day.to_i / 60) end - it 'skips sending digest email' do + it "skips sending digest email" do Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) expect(ActionMailer::Base.deliveries).to eq([]) expect(user.user_stat.reload.digest_attempted_at).to eq_time(Time.zone.now) @@ -70,39 +81,41 @@ RSpec.describe Jobs::UserEmail do email_token = Fabricate(:email_token) user.user_stat.update(bounce_score: SiteSetting.bounce_score_threshold + 1) - Jobs::CriticalUserEmail.new.execute(type: "signup", user_id: user.id, email_token: email_token.token) + Jobs::CriticalUserEmail.new.execute( + type: "signup", + user_id: user.id, + email_token: email_token.token, + ) email_log = EmailLog.where(user_id: user.id).last expect(email_log.email_type).to eq("signup") - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end end - context 'with to_address' do - it 'overwrites a to_address when present' do - Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, to_address: 'jake@adventuretime.ooo') - - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - 'jake@adventuretime.ooo' + context "with to_address" do + it "overwrites a to_address when present" do + Jobs::UserEmail.new.execute( + type: :confirm_new_email, + user_id: user.id, + to_address: "jake@adventuretime.ooo", ) + + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly("jake@adventuretime.ooo") end end context "with disable_emails setting" do it "sends when no" do - SiteSetting.disable_emails = 'no' + SiteSetting.disable_emails = "no" Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end it "does not send an email when yes" do - SiteSetting.disable_emails = 'yes' + SiteSetting.disable_emails = "yes" Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id) expect(ActionMailer::Base.deliveries).to eq([]) @@ -111,17 +124,16 @@ RSpec.describe Jobs::UserEmail do context "when recently seen" do fab!(:post) { Fabricate(:post, user: user) } - fab!(:notification) { Fabricate( + fab!(:notification) do + Fabricate( :notification, user: user, topic: post.topic, post_number: post.post_number, - data: { original_post_id: post.id }.to_json + data: { original_post_id: post.id }.to_json, ) - } - before do - user.update_column(:last_seen_at, 9.minutes.ago) end + before { user.update_column(:last_seen_at, 9.minutes.ago) } it "doesn't send an email to a user that's been recently seen" do Jobs::UserEmail.new.execute(type: :user_replied, user_id: user.id, post_id: post.id) @@ -130,30 +142,38 @@ RSpec.describe Jobs::UserEmail do it "does send an email to a user that's been recently seen but has email_level set to always" do user.user_option.update(email_level: UserOption.email_level_types[:always]) - PostTiming.create!(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id, msecs: 100) - - Jobs::UserEmail.new.execute( - type: :user_replied, + PostTiming.create!( + topic_id: post.topic_id, + post_number: post.post_number, user_id: user.id, - post_id: post.id, - notification_id: notification.id + msecs: 100, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) - end - - it "doesn't send an email even if email_level is set to always if `force_respect_seen_recently` arg is true" do - user.user_option.update(email_level: UserOption.email_level_types[:always]) - PostTiming.create!(topic_id: post.topic_id, post_number: post.post_number, user_id: user.id, msecs: 100) - Jobs::UserEmail.new.execute( type: :user_replied, user_id: user.id, post_id: post.id, notification_id: notification.id, - force_respect_seen_recently: true + ) + + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) + end + + it "doesn't send an email even if email_level is set to always if `force_respect_seen_recently` arg is true" do + user.user_option.update(email_level: UserOption.email_level_types[:always]) + PostTiming.create!( + topic_id: post.topic_id, + post_number: post.post_number, + user_id: user.id, + msecs: 100, + ) + + Jobs::UserEmail.new.execute( + type: :user_replied, + user_id: user.id, + post_id: post.id, + notification_id: notification.id, + force_respect_seen_recently: true, ) expect(ActionMailer::Base.deliveries).to eq([]) end @@ -170,7 +190,7 @@ RSpec.describe Jobs::UserEmail do type: :user_private_message, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) email = ActionMailer::Base.deliveries.first @@ -178,7 +198,7 @@ RSpec.describe Jobs::UserEmail do expect(email.to).to contain_exactly(user.email) html_part = email.parts.find { |x| x.content_type.include? "html" } - expect(html_part.body.to_s).to_not include('%{email_content}') + expect(html_part.body.to_s).to_not include("%{email_content}") expect(html_part.body.to_s).to include('\0') end @@ -196,7 +216,7 @@ RSpec.describe Jobs::UserEmail do type: :user_private_message, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) email = ActionMailer::Base.deliveries.first @@ -218,12 +238,10 @@ RSpec.describe Jobs::UserEmail do type: :user_private_message, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end it "doesn't send a PM email to a user that's been recently seen and has email_messages_level set to never" do @@ -246,9 +264,7 @@ RSpec.describe Jobs::UserEmail do context "with email_log" do fab!(:post) { Fabricate(:post, created_at: 30.seconds.ago) } - before do - SiteSetting.editing_grace_period = 0 - end + before { SiteSetting.editing_grace_period = 0 } it "creates an email log when the mail is sent (via Email::Sender)" do freeze_time @@ -257,9 +273,9 @@ RSpec.describe Jobs::UserEmail do user.update!(last_emailed_at: last_emailed_at) Topic.last.update(created_at: 1.minute.ago) - expect do - Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) - end.to change { EmailLog.count }.by(1) + expect do Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) end.to change { + EmailLog.count + }.by(1) email_log = EmailLog.last @@ -273,22 +289,21 @@ RSpec.describe Jobs::UserEmail do freeze_time last_emailed_at = 7.days.ago - user.update!( - last_emailed_at: last_emailed_at, - suspended_till: 1.year.from_now - ) + user.update!(last_emailed_at: last_emailed_at, suspended_till: 1.year.from_now) - expect do - Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) - end.to change { SkippedEmailLog.count }.by(1) + expect do Jobs::UserEmail.new.execute(type: :digest, user_id: user.id) end.to change { + SkippedEmailLog.count + }.by(1) - expect(SkippedEmailLog.exists?( - email_type: "digest", - user: user, - post: nil, - to_address: user.email, - reason_type: SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "digest", + user: user, + post: nil, + to_address: user.email, + reason_type: SkippedEmailLog.reason_types[:user_email_user_suspended_not_pm], + ), + ).to eq(true) # last_emailed_at doesn't change expect(user.last_emailed_at).to eq_time(last_emailed_at) @@ -302,21 +317,23 @@ RSpec.describe Jobs::UserEmail do Jobs::UserEmail.new.execute(type: :user_posted, user_id: user.id, post_id: post.id) end.to change { SkippedEmailLog.count }.by(1) - expect(SkippedEmailLog.exists?( - email_type: "user_posted", - user: user, - post: post, - to_address: user.email, - reason_type: SkippedEmailLog.reason_types[:user_email_access_denied] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "user_posted", + user: user, + post: post, + to_address: user.email, + reason_type: SkippedEmailLog.reason_types[:user_email_access_denied], + ), + ).to eq(true) expect(ActionMailer::Base.deliveries).to eq([]) end end - context 'with args' do - it 'passes a token as an argument when a token is present' do - Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, email_token: 'asdfasdf') + context "with args" do + it "passes a token as an argument when a token is present" do + Jobs::UserEmail.new.execute(type: :forgot_password, user_id: user.id, email_token: "asdfasdf") mail = ActionMailer::Base.deliveries.first @@ -332,14 +349,18 @@ RSpec.describe Jobs::UserEmail do requested_by: requested_by, new_email_token: email_token, new_email: "testnew@test.com", - change_state: EmailChangeRequest.states[:authorizing_new] + change_state: EmailChangeRequest.states[:authorizing_new], ) end context "when the change was requested by admin" do let(:requested_by) { Fabricate(:admin) } it "passes along true for the requested_by_admin param which changes the wording in the email" do - Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, email_token: email_token.token) + Jobs::UserEmail.new.execute( + type: :confirm_new_email, + user_id: user.id, + email_token: email_token.token, + ) mail = ActionMailer::Base.deliveries.first expect(mail.body).to include("This email change was requested by a site admin.") end @@ -348,7 +369,11 @@ RSpec.describe Jobs::UserEmail do context "when the change was requested by the user" do let(:requested_by) { user } it "passes along false for the requested_by_admin param which changes the wording in the email" do - Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, email_token: email_token.token) + Jobs::UserEmail.new.execute( + type: :confirm_new_email, + user_id: user.id, + email_token: email_token.token, + ) mail = ActionMailer::Base.deliveries.first expect(mail.body).not_to include("This email change was requested by a site admin.") end @@ -357,7 +382,11 @@ RSpec.describe Jobs::UserEmail do context "when requested_by record is not present" do let(:requested_by) { nil } it "passes along false for the requested_by_admin param which changes the wording in the email" do - Jobs::UserEmail.new.execute(type: :confirm_new_email, user_id: user.id, email_token: email_token.token) + Jobs::UserEmail.new.execute( + type: :confirm_new_email, + user_id: user.id, + email_token: email_token.token, + ) mail = ActionMailer::Base.deliveries.first expect(mail.body).not_to include("This email change was requested by a site admin.") end @@ -368,7 +397,12 @@ RSpec.describe Jobs::UserEmail do fab!(:post) { Fabricate(:post, user: user) } it "doesn't send the email if you've seen the post" do - PostTiming.record_timing(topic_id: post.topic_id, user_id: user.id, post_number: post.post_number, msecs: 6666) + PostTiming.record_timing( + topic_id: post.topic_id, + user_id: user.id, + post_number: post.post_number, + msecs: 6666, + ) Jobs::UserEmail.new.execute(type: :user_private_message, user_id: user.id, post_id: post.id) expect(ActionMailer::Base.deliveries).to eq([]) @@ -388,9 +422,13 @@ RSpec.describe Jobs::UserEmail do expect(ActionMailer::Base.deliveries).to eq([]) end - context 'when user is suspended' do + context "when user is suspended" do it "doesn't send email for a pm from a regular user" do - Jobs::UserEmail.new.execute(type: :user_private_message, user_id: suspended.id, post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_private_message, + user_id: suspended.id, + post_id: post.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end @@ -399,51 +437,57 @@ RSpec.describe Jobs::UserEmail do pm_from_staff = Fabricate(:post, user: Fabricate(:moderator)) pm_from_staff.topic.topic_allowed_users.create!(user_id: suspended.id) - pm_notification = Fabricate(:notification, - user: suspended, - topic: pm_from_staff.topic, - post_number: pm_from_staff.post_number, - data: { original_post_id: pm_from_staff.id }.to_json - ) + pm_notification = + Fabricate( + :notification, + user: suspended, + topic: pm_from_staff.topic, + post_number: pm_from_staff.post_number, + data: { original_post_id: pm_from_staff.id }.to_json, + ) Jobs::UserEmail.new.execute( type: :user_private_message, user_id: suspended.id, post_id: pm_from_staff.id, - notification_id: pm_notification.id + notification_id: pm_notification.id, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - suspended.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(suspended.email) end it "doesn't send PM from system user" do pm_from_system = SystemMessage.create(suspended, :unsilenced) - system_pm_notification = Fabricate(:notification, - user: suspended, - topic: pm_from_system.topic, - post_number: pm_from_system.post_number, - data: { original_post_id: pm_from_system.id }.to_json - ) + system_pm_notification = + Fabricate( + :notification, + user: suspended, + topic: pm_from_system.topic, + post_number: pm_from_system.post_number, + data: { original_post_id: pm_from_system.id }.to_json, + ) Jobs::UserEmail.new.execute( type: :user_private_message, user_id: suspended.id, post_id: pm_from_system.id, - notification_id: system_pm_notification.id + notification_id: system_pm_notification.id, ) expect(ActionMailer::Base.deliveries).to eq([]) end end - context 'when user is anonymous' do + context "when user is anonymous" do before { SiteSetting.allow_anonymous_posting = true } it "doesn't send email for a pm from a regular user" do - Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_private_message, + user_id: anonymous.id, + post_id: post.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end @@ -451,46 +495,52 @@ RSpec.describe Jobs::UserEmail do it "doesn't send email for a pm from a staff user" do pm_from_staff = Fabricate(:post, user: Fabricate(:moderator)) pm_from_staff.topic.topic_allowed_users.create!(user_id: anonymous.id) - Jobs::UserEmail.new.execute(type: :user_private_message, user_id: anonymous.id, post_id: pm_from_staff.id) + Jobs::UserEmail.new.execute( + type: :user_private_message, + user_id: anonymous.id, + post_id: pm_from_staff.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end end end - context 'with notification' do + context "with notification" do fab!(:post) { Fabricate(:post, user: user) } - fab!(:notification) { - Fabricate(:notification, - user: user, - topic: post.topic, - post_number: post.post_number, - data: { - original_post_id: post.id - }.to_json - ) - } + fab!(:notification) do + Fabricate( + :notification, + user: user, + topic: post.topic, + post_number: post.post_number, + data: { original_post_id: post.id }.to_json, + ) + end it "doesn't send the email if the notification has been seen" do notification.update_column(:read, true) - message, err = Jobs::UserEmail.new.message_for_email( - user, - post, - :user_mentioned, - notification, - notification_type: notification.notification_type, - notification_data_hash: notification.data_hash - ) + message, err = + Jobs::UserEmail.new.message_for_email( + user, + post, + :user_mentioned, + notification, + notification_type: notification.notification_type, + notification_data_hash: notification.data_hash, + ) expect(message).to eq(nil) - expect(SkippedEmailLog.exists?( - email_type: "user_mentioned", - user: user, - post: post, - to_address: user.email, - reason_type: SkippedEmailLog.reason_types[:user_email_notification_already_read] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "user_mentioned", + user: user, + post: post, + to_address: user.email, + reason_type: SkippedEmailLog.reason_types[:user_email_notification_already_read], + ), + ).to eq(true) end it "does send the email if the notification has been seen but user has email_level set to always" do @@ -501,12 +551,10 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end it "does send the email if the user is using daily mailing list mode" do @@ -516,12 +564,10 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end it "sends the mail if the user enabled mailing list mode, but mailing list mode is disabled globally" do @@ -531,7 +577,7 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) @@ -545,7 +591,7 @@ RSpec.describe Jobs::UserEmail do type: :user_replied, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) expect(ActionMailer::Base.deliveries).to eq([]) @@ -559,19 +605,17 @@ RSpec.describe Jobs::UserEmail do type: :user_replied, user_id: user.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) - expect(ActionMailer::Base.deliveries.first.to).to contain_exactly( - user.email - ) + expect(ActionMailer::Base.deliveries.first.to).to contain_exactly(user.email) end end - context 'when max_emails_per_day_per_user limit is reached' do + context "when max_emails_per_day_per_user limit is reached" do before do SiteSetting.max_emails_per_day_per_user = 2 - 2.times { Fabricate(:email_log, user: user, email_type: 'blah', to_address: user.email) } + 2.times { Fabricate(:email_log, user: user, email_type: "blah", to_address: user.email) } end it "does not send notification if limit is reached" do @@ -581,18 +625,20 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, notification_id: notification.id, - post_id: post.id + post_id: post.id, ) end end.to change { SkippedEmailLog.count }.by(1) - expect(SkippedEmailLog.exists?( - email_type: "user_mentioned", - user: user, - post: post, - to_address: user.email, - reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "user_mentioned", + user: user, + post: post, + to_address: user.email, + reason_type: SkippedEmailLog.reason_types[:exceeded_emails_limit], + ), + ).to eq(true) freeze_time(Time.zone.now.tomorrow + 1.second) @@ -601,7 +647,7 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, notification_id: notification.id, - post_id: post.id + post_id: post.id, ) end.not_to change { SkippedEmailLog.count } end @@ -615,10 +661,7 @@ RSpec.describe Jobs::UserEmail do ) end.to change { EmailLog.count }.by(1) - expect(EmailLog.exists?( - email_type: "forgot_password", - user: user, - )).to eq(true) + expect(EmailLog.exists?(email_type: "forgot_password", user: user)).to eq(true) end end @@ -631,7 +674,7 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, notification_id: notification.id, - post_id: post.id + post_id: post.id, ) user.user_stat.reload @@ -643,7 +686,7 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, notification_id: notification.id, - post_id: post.id + post_id: post.id, ) user.user_stat.reload @@ -658,17 +701,19 @@ RSpec.describe Jobs::UserEmail do type: :user_mentioned, user_id: user.id, notification_id: notification.id, - post_id: post.id + post_id: post.id, ) end.to change { SkippedEmailLog.count }.by(1) - expect(SkippedEmailLog.exists?( - email_type: "user_mentioned", - user: user, - post: post, - to_address: user.email, - reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit] - )).to eq(true) + expect( + SkippedEmailLog.exists?( + email_type: "user_mentioned", + user: user, + post: post, + to_address: user.email, + reason_type: SkippedEmailLog.reason_types[:exceeded_bounces_limit], + ), + ).to eq(true) end it "doesn't send the mail if the user is using individual mailing list mode" do @@ -676,15 +721,34 @@ RSpec.describe Jobs::UserEmail do user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 1) # sometimes, we pass the notification_id - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_id: notification.id, + post_id: post.id, + ) # other times, we only pass the type of notification - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + post_id: post.id, + ) # When post is nil - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted") + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + ) # When post does not have a topic post = Fabricate(:post) post.topic.destroy - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + post_id: post.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end @@ -694,57 +758,84 @@ RSpec.describe Jobs::UserEmail do user.user_option.update(mailing_list_mode: true, mailing_list_mode_frequency: 2) # sometimes, we pass the notification_id - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_id: notification.id, + post_id: post.id, + ) # other times, we only pass the type of notification - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + post_id: post.id, + ) # When post is nil - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted") + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + ) # When post does not have a topic post = Fabricate(:post) post.topic.destroy - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_type: "posted", post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_type: "posted", + post_id: post.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end it "doesn't send the email if the post has been user deleted" do post.update_column(:user_deleted, true) - Jobs::UserEmail.new.execute(type: :user_mentioned, user_id: user.id, notification_id: notification.id, post_id: post.id) + Jobs::UserEmail.new.execute( + type: :user_mentioned, + user_id: user.id, + notification_id: notification.id, + post_id: post.id, + ) expect(ActionMailer::Base.deliveries).to eq([]) end - context 'when user is suspended' do + context "when user is suspended" do it "doesn't send email for a pm from a regular user" do - msg, err = Jobs::UserEmail.new.message_for_email( + msg, err = + Jobs::UserEmail.new.message_for_email( suspended, Fabricate.build(:post), :user_private_message, - notification - ) + notification, + ) expect(msg).to eq(nil) expect(err).not_to eq(nil) end - context 'with pm from staff' do + context "with pm from staff" do before do @pm_from_staff = Fabricate(:post, user: Fabricate(:moderator)) @pm_from_staff.topic.topic_allowed_users.create!(user_id: suspended.id) - @pm_notification = Fabricate(:notification, - user: suspended, - topic: @pm_from_staff.topic, - post_number: @pm_from_staff.post_number, - data: { original_post_id: @pm_from_staff.id }.to_json - ) + @pm_notification = + Fabricate( + :notification, + user: suspended, + topic: @pm_from_staff.topic, + post_number: @pm_from_staff.post_number, + data: { original_post_id: @pm_from_staff.id }.to_json, + ) end let :sent_message do Jobs::UserEmail.new.message_for_email( - suspended, - @pm_from_staff, - :user_private_message, - @pm_notification + suspended, + @pm_from_staff, + :user_private_message, + @pm_notification, ) end @@ -764,7 +855,7 @@ RSpec.describe Jobs::UserEmail do end end - context 'when user is anonymous' do + context "when user is anonymous" do before { SiteSetting.allow_anonymous_posting = true } it "doesn't send email for a pm from a regular user" do @@ -772,7 +863,7 @@ RSpec.describe Jobs::UserEmail do type: :user_private_message, user_id: anonymous.id, post_id: post.id, - notification_id: notification.id + notification_id: notification.id, ) expect(ActionMailer::Base.deliveries).to eq([]) @@ -781,17 +872,19 @@ RSpec.describe Jobs::UserEmail do it "doesn't send email for a pm from staff" do pm_from_staff = Fabricate(:post, user: Fabricate(:moderator)) pm_from_staff.topic.topic_allowed_users.create!(user_id: anonymous.id) - pm_notification = Fabricate(:notification, - user: anonymous, - topic: pm_from_staff.topic, - post_number: pm_from_staff.post_number, - data: { original_post_id: pm_from_staff.id }.to_json - ) + pm_notification = + Fabricate( + :notification, + user: anonymous, + topic: pm_from_staff.topic, + post_number: pm_from_staff.post_number, + data: { original_post_id: pm_from_staff.id }.to_json, + ) Jobs::UserEmail.new.execute( type: :user_private_message, user_id: anonymous.id, post_id: pm_from_staff.id, - notification_id: pm_notification.id + notification_id: pm_notification.id, ) expect(ActionMailer::Base.deliveries).to eq([]) diff --git a/spec/lib/admin_confirmation_spec.rb b/spec/lib/admin_confirmation_spec.rb index 10e065c603..e9d345939c 100644 --- a/spec/lib/admin_confirmation_spec.rb +++ b/spec/lib/admin_confirmation_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true -require 'admin_confirmation' +require "admin_confirmation" RSpec.describe AdminConfirmation do - fab!(:admin) { Fabricate(:admin) } fab!(:user) { Fabricate(:user) } @@ -32,25 +31,27 @@ RSpec.describe AdminConfirmation do expect(ac.target_user).to eq(user) expect(ac.token).to eq(@token) - expect_enqueued_with(job: :send_system_message, args: { user_id: user.id, message_type: 'welcome_staff', message_options: { role: :admin } }) do - ac.email_confirmed! - end + expect_enqueued_with( + job: :send_system_message, + args: { + user_id: user.id, + message_type: "welcome_staff", + message_options: { + role: :admin, + }, + }, + ) { ac.email_confirmed! } user.reload expect(user.admin?).to eq(true) # It creates a staff log - logs = UserHistory.where( - action: UserHistory.actions[:grant_admin], - target_user_id: user.id - ) + logs = UserHistory.where(action: UserHistory.actions[:grant_admin], target_user_id: user.id) expect(logs).to be_present # It removes the redis keys for another user expect(AdminConfirmation.find_by_code(ac.token)).to eq(nil) expect(AdminConfirmation.exists_for?(user.id)).to eq(false) end - end - end diff --git a/spec/lib/admin_user_index_query_spec.rb b/spec/lib/admin_user_index_query_spec.rb index 71fe1856d8..d603d3eaeb 100644 --- a/spec/lib/admin_user_index_query_spec.rb +++ b/spec/lib/admin_user_index_query_spec.rb @@ -2,7 +2,7 @@ RSpec.describe AdminUserIndexQuery do def real_users(query) - query.find_users_query.where('users.id > 0') + query.find_users_query.where("users.id > 0") end describe "sql order" do @@ -65,18 +65,15 @@ RSpec.describe AdminUserIndexQuery do end describe "no users with trust level" do - TrustLevel.levels.each do |key, value| it "#{key} returns no records" do query = ::AdminUserIndexQuery.new(query: key.to_s) expect(real_users(query)).to eq([]) end end - end describe "users with trust level" do - TrustLevel.levels.each do |key, value| it "finds user with trust #{key}" do user = Fabricate(:user, trust_level: value) @@ -88,107 +85,99 @@ RSpec.describe AdminUserIndexQuery do expect(real_users(query).to_a).to eq([user]) end end - end describe "with a pending user" do - fab!(:user) { Fabricate(:user, active: true, approved: false) } fab!(:inactive_user) { Fabricate(:user, approved: false, active: false) } it "finds the unapproved user" do - query = ::AdminUserIndexQuery.new(query: 'pending') + query = ::AdminUserIndexQuery.new(query: "pending") expect(query.find_users).to include(user) expect(query.find_users).not_to include(inactive_user) end - context 'with a suspended pending user' do - fab!(:suspended_user) { Fabricate(:user, approved: false, suspended_at: 1.hour.ago, suspended_till: 20.years.from_now) } + context "with a suspended pending user" do + fab!(:suspended_user) do + Fabricate( + :user, + approved: false, + suspended_at: 1.hour.ago, + suspended_till: 20.years.from_now, + ) + end it "doesn't return the suspended user" do - query = ::AdminUserIndexQuery.new(query: 'pending') + query = ::AdminUserIndexQuery.new(query: "pending") expect(query.find_users).not_to include(suspended_user) end end - end describe "correct order with nil values" do - before(:each) do - Fabricate(:user, email: "test2@example.com", last_emailed_at: 1.hour.ago) - end + before(:each) { Fabricate(:user, email: "test2@example.com", last_emailed_at: 1.hour.ago) } it "shows nil values first with asc" do users = ::AdminUserIndexQuery.new(order: "last_emailed", asc: true).find_users - expect(users.where('users.id > -2').count).to eq(2) - expect(users.where('users.id > -2').order('users.id asc').first.username).to eq("system") + expect(users.where("users.id > -2").count).to eq(2) + expect(users.where("users.id > -2").order("users.id asc").first.username).to eq("system") expect(users.first.last_emailed_at).to eq(nil) end it "shows nil values last with desc" do users = ::AdminUserIndexQuery.new(order: "last_emailed").find_users - expect(users.where('users.id > -2').count).to eq(2) + expect(users.where("users.id > -2").count).to eq(2) expect(users.first.last_emailed_at).to_not eq(nil) end - end describe "with an admin user" do - fab!(:user) { Fabricate(:user, admin: true) } fab!(:user2) { Fabricate(:user, admin: false) } it "finds the admin" do - query = ::AdminUserIndexQuery.new(query: 'admins') + query = ::AdminUserIndexQuery.new(query: "admins") expect(real_users(query)).to eq([user]) end - end describe "with a moderator" do - fab!(:user) { Fabricate(:user, moderator: true) } fab!(:user2) { Fabricate(:user, moderator: false) } it "finds the moderator" do - query = ::AdminUserIndexQuery.new(query: 'moderators') + query = ::AdminUserIndexQuery.new(query: "moderators") expect(real_users(query)).to eq([user]) end - end describe "with a silenced user" do - fab!(:user) { Fabricate(:user, silenced_till: 1.year.from_now) } fab!(:user2) { Fabricate(:user) } it "finds the silenced user" do - query = ::AdminUserIndexQuery.new(query: 'silenced') + query = ::AdminUserIndexQuery.new(query: "silenced") expect(real_users(query)).to eq([user]) end - end describe "with a staged user" do - fab!(:user) { Fabricate(:user, staged: true) } fab!(:user2) { Fabricate(:user, staged: false) } it "finds the staged user" do - query = ::AdminUserIndexQuery.new(query: 'staged') + query = ::AdminUserIndexQuery.new(query: "staged") expect(real_users(query)).to eq([user]) end - end describe "filtering" do - context "with exact email bypass" do it "can correctly bypass expensive ilike query" do - user = Fabricate(:user, email: 'sam@Sam.com') + user = Fabricate(:user, email: "sam@Sam.com") - query = AdminUserIndexQuery.new(filter: 'Sam@sam.com').find_users_query + query = AdminUserIndexQuery.new(filter: "Sam@sam.com").find_users_query expect(query.count).to eq(1) expect(query.first.id).to eq(user.id) @@ -196,22 +185,20 @@ RSpec.describe AdminUserIndexQuery do end it "can correctly bypass expensive ilike query" do - user = Fabricate(:user, email: 'sam2@Sam.com') + user = Fabricate(:user, email: "sam2@Sam.com") - query = AdminUserIndexQuery.new(email: 'Sam@sam.com').find_users_query + query = AdminUserIndexQuery.new(email: "Sam@sam.com").find_users_query expect(query.count).to eq(0) expect(query.to_sql.downcase).not_to include("ilike") - query = AdminUserIndexQuery.new(email: 'Sam2@sam.com').find_users_query + query = AdminUserIndexQuery.new(email: "Sam2@sam.com").find_users_query expect(query.first.id).to eq(user.id) expect(query.count).to eq(1) expect(query.to_sql.downcase).not_to include("ilike") - end end context "with email fragment" do - before(:each) { Fabricate(:user, email: "test1@example.com") } it "matches the email" do @@ -223,11 +210,9 @@ RSpec.describe AdminUserIndexQuery do query = ::AdminUserIndexQuery.new(filter: "Test1\t") expect(query.find_users.count()).to eq(1) end - end context "with username fragment" do - before(:each) { Fabricate(:user, username: "test_user_1") } it "matches the username" do @@ -242,15 +227,12 @@ RSpec.describe AdminUserIndexQuery do end context "with ip address fragment" do - fab!(:user) { Fabricate(:user, ip_address: "117.207.94.9") } it "matches the ip address" do query = ::AdminUserIndexQuery.new(filter: " 117.207.94.9 ") expect(query.find_users.count()).to eq(1) end - end - end end diff --git a/spec/lib/archetype_spec.rb b/spec/lib/archetype_spec.rb index 9b5a6b6945..9843012072 100644 --- a/spec/lib/archetype_spec.rb +++ b/spec/lib/archetype_spec.rb @@ -1,36 +1,36 @@ # encoding: utf-8 # frozen_string_literal: true -require 'archetype' +require "archetype" RSpec.describe Archetype do - describe 'default archetype' do - it 'has an Archetype by default' do + describe "default archetype" do + it "has an Archetype by default" do expect(Archetype.list).to be_present end - it 'has an id of default' do + it "has an id of default" do expect(Archetype.list.first.id).to eq(Archetype.default) end - context 'with duplicate' do + context "with duplicate" do before do @old_size = Archetype.list.size Archetype.register(Archetype.default) end - it 'does not add the same archetype twice' do + it "does not add the same archetype twice" do expect(Archetype.list.size).to eq(@old_size) end end end - describe 'register an archetype' do - it 'has one more element' do + describe "register an archetype" do + it "has one more element" do @list = Archetype.list.dup - Archetype.register('glados') + Archetype.register("glados") expect(Archetype.list.size).to eq(@list.size + 1) - expect(Archetype.list.find { |a| a.id == 'glados' }).to be_present + expect(Archetype.list.find { |a| a.id == "glados" }).to be_present end end end diff --git a/spec/lib/auth/default_current_user_provider_spec.rb b/spec/lib/auth/default_current_user_provider_spec.rb index 9c542e7965..39b84153f3 100644 --- a/spec/lib/auth/default_current_user_provider_spec.rb +++ b/spec/lib/auth/default_current_user_provider_spec.rb @@ -54,17 +54,13 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "raises for a revoked key" do api_key = ApiKey.create! params = { "HTTP_API_USERNAME" => user.username.downcase, "HTTP_API_KEY" => api_key.key } - expect( - provider("/", params).current_user.id - ).to eq(user.id) + expect(provider("/", params).current_user.id).to eq(user.id) api_key.reload.update(revoked_at: Time.zone.now, last_used_at: nil) expect(api_key.reload.last_used_at).to eq(nil) params = { "HTTP_API_USERNAME" => user.username.downcase, "HTTP_API_KEY" => api_key.key } - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) api_key.reload expect(api_key.last_used_at).to eq(nil) @@ -72,9 +68,10 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "raises errors for incorrect api_key" do params = { "HTTP_API_KEY" => "INCORRECT" } - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess, /API username or key is invalid/) + expect { provider("/", params).current_user }.to raise_error( + Discourse::InvalidAccess, + /API username or key is invalid/, + ) end it "finds a user for a correct per-user api key" do @@ -83,9 +80,9 @@ RSpec.describe Auth::DefaultCurrentUserProvider do good_provider = provider("/", params) - expect do - expect(good_provider.current_user.id).to eq(user.id) - end.to change { api_key.reload.last_used_at } + expect do expect(good_provider.current_user.id).to eq(user.id) end.to change { + api_key.reload.last_used_at + } expect(good_provider.is_api?).to eq(true) expect(good_provider.is_user_api?).to eq(false) @@ -93,15 +90,11 @@ RSpec.describe Auth::DefaultCurrentUserProvider do user.update_columns(active: false) - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) user.update_columns(active: true, suspended_till: 1.day.from_now) - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) end it "raises for a user pretending" do @@ -109,26 +102,22 @@ RSpec.describe Auth::DefaultCurrentUserProvider do api_key = ApiKey.create!(user_id: user.id, created_by_id: -1) params = { "HTTP_API_KEY" => api_key.key, "HTTP_API_USERNAME" => user2.username.downcase } - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) end it "raises for a user with a mismatching ip" do - api_key = ApiKey.create!(user_id: user.id, created_by_id: -1, allowed_ips: ['10.0.0.0/24']) + api_key = ApiKey.create!(user_id: user.id, created_by_id: -1, allowed_ips: ["10.0.0.0/24"]) params = { "HTTP_API_KEY" => api_key.key, "HTTP_API_USERNAME" => user.username.downcase, - "REMOTE_ADDR" => "10.1.0.1" + "REMOTE_ADDR" => "10.1.0.1", } - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) end it "allows a user with a matching ip" do - api_key = ApiKey.create!(user_id: user.id, created_by_id: -1, allowed_ips: ['100.0.0.0/24']) + api_key = ApiKey.create!(user_id: user.id, created_by_id: -1, allowed_ips: ["100.0.0.0/24"]) params = { "HTTP_API_KEY" => api_key.key, "HTTP_API_USERNAME" => user.username.downcase, @@ -142,7 +131,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do params = { "HTTP_API_KEY" => api_key.key, "HTTP_API_USERNAME" => user.username.downcase, - "HTTP_X_FORWARDED_FOR" => "10.1.1.1, 100.0.0.22" + "HTTP_X_FORWARDED_FOR" => "10.1.1.1, 100.0.0.22", } found_user = provider("/", params).current_user @@ -165,18 +154,18 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "finds a user for a correct system api key with external id" do api_key = ApiKey.create!(created_by_id: -1) - SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: '') + SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: "") params = { "HTTP_API_KEY" => api_key.key, "HTTP_API_USER_EXTERNAL_ID" => "abc" } expect(provider("/", params).current_user.id).to eq(user.id) end it "raises for a mismatched api_key header and param external id" do api_key = ApiKey.create!(created_by_id: -1) - SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: '') + SingleSignOnRecord.create(user_id: user.id, external_id: "abc", last_payload: "") params = { "HTTP_API_KEY" => api_key.key } - expect { - provider("/?api_user_external_id=abc", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/?api_user_external_id=abc", params).current_user }.to raise_error( + Discourse::InvalidAccess, + ) end it "finds a user for a correct system api key with id" do @@ -188,19 +177,15 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "raises for a mismatched api_key header and param user id" do api_key = ApiKey.create!(created_by_id: -1) params = { "HTTP_API_KEY" => api_key.key } - expect { - provider("/?api_user_id=#{user.id}", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/?api_user_id=#{user.id}", params).current_user }.to raise_error( + Discourse::InvalidAccess, + ) end describe "when readonly mode is enabled due to postgres" do - before do - Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + before { Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } - after do - Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + after { Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } it "should not update ApiKey#last_used_at" do api_key = ApiKey.create!(user_id: user.id, created_by_id: -1) @@ -208,16 +193,14 @@ RSpec.describe Auth::DefaultCurrentUserProvider do good_provider = provider("/", params) - expect do - expect(good_provider.current_user.id).to eq(user.id) - end.to_not change { api_key.reload.last_used_at } + expect do expect(good_provider.current_user.id).to eq(user.id) end.to_not change { + api_key.reload.last_used_at + } end end context "with rate limiting" do - before do - RateLimiter.enable - end + before { RateLimiter.enable } it "rate limits admin api requests" do global_setting :max_admin_api_reqs_per_minute, 3 @@ -233,15 +216,15 @@ RSpec.describe Auth::DefaultCurrentUserProvider do provider("/", system_params).current_user provider("/", params).current_user - expect do - provider("/", system_params).current_user - end.to raise_error(RateLimiter::LimitExceeded) + expect do provider("/", system_params).current_user end.to raise_error( + RateLimiter::LimitExceeded, + ) freeze_time 59.seconds.from_now - expect do - provider("/", system_params).current_user - end.to raise_error(RateLimiter::LimitExceeded) + expect do provider("/", system_params).current_user end.to raise_error( + RateLimiter::LimitExceeded, + ) freeze_time 2.seconds.from_now @@ -259,7 +242,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do describe "#current_user" do let(:cookie) do - new_provider = provider('/') + new_provider = provider("/") new_provider.log_on_user(user, {}, new_provider.cookie_jar) CGI.escape(new_provider.cookie_jar["_t"]) end @@ -269,9 +252,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do user.clear_last_seen_cache!(@orig) end - after do - user.clear_last_seen_cache!(@orig) - end + after { user.clear_last_seen_cache!(@orig) } it "should not update last seen for suspended users" do provider2 = provider("/", "HTTP_COOKIE" => "_t=#{cookie}") @@ -295,13 +276,9 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end describe "when readonly mode is enabled due to postgres" do - before do - Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + before { Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } - after do - Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + after { Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } it "should not update User#last_seen_at" do provider2 = provider("/", "HTTP_COOKIE" => "_t=#{cookie}") @@ -338,35 +315,47 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "should update ajax reqs with discourse visible" do - expect(provider("/topic/anything/goes", - :method => "POST", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_DISCOURSE_PRESENT" => "true" - ).should_update_last_seen?).to eq(true) + expect( + provider( + "/topic/anything/goes", + :method => "POST", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "HTTP_DISCOURSE_PRESENT" => "true", + ).should_update_last_seen?, + ).to eq(true) end it "should not update last seen for ajax calls without Discourse-Present header" do - expect(provider("/topic/anything/goes", - :method => "POST", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" - ).should_update_last_seen?).to eq(false) + expect( + provider( + "/topic/anything/goes", + :method => "POST", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + ).should_update_last_seen?, + ).to eq(false) end it "should update last seen for API calls with Discourse-Present header" do api_key = ApiKey.create!(user_id: user.id, created_by_id: -1) - params = { :method => "POST", - "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", - "HTTP_API_KEY" => api_key.key - } + params = { + :method => "POST", + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "HTTP_API_KEY" => api_key.key, + } expect(provider("/topic/anything/goes", params).should_update_last_seen?).to eq(false) - expect(provider("/topic/anything/goes", params.merge("HTTP_DISCOURSE_PRESENT" => "true")).should_update_last_seen?).to eq(true) + expect( + provider( + "/topic/anything/goes", + params.merge("HTTP_DISCOURSE_PRESENT" => "true"), + ).should_update_last_seen?, + ).to eq(true) end it "supports non persistent sessions" do SiteSetting.persistent_sessions = false - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie_info = get_cookie_info(@provider.cookie_jar, "_t") @@ -377,12 +366,12 @@ RSpec.describe Auth::DefaultCurrentUserProvider do token = UserAuthToken.generate!(user_id: user.id).unhashed_auth_token ip = "10.0.0.1" env = { "HTTP_COOKIE" => "_t=#{token}", "REMOTE_ADDR" => ip } - expect(provider('/', env).current_user.id).to eq(user.id) + expect(provider("/", env).current_user.id).to eq(user.id) end it "correctly rotates tokens" do SiteSetting.maximum_session_age = 3 - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie = @provider.cookie_jar["_t"] @@ -405,12 +394,8 @@ RSpec.describe Auth::DefaultCurrentUserProvider do expect(token.auth_token_seen).to eq(true) provider2.refresh_session(user, {}, provider2.cookie_jar) - expect( - decrypt_auth_cookie(provider2.cookie_jar["_t"])[:token] - ).not_to eq(unhashed_token) - expect( - decrypt_auth_cookie(provider2.cookie_jar["_t"])[:token].size - ).to eq(32) + expect(decrypt_auth_cookie(provider2.cookie_jar["_t"])[:token]).not_to eq(unhashed_token) + expect(decrypt_auth_cookie(provider2.cookie_jar["_t"])[:token].size).to eq(32) token.reload expect(token.auth_token_seen).to eq(false) @@ -432,23 +417,20 @@ RSpec.describe Auth::DefaultCurrentUserProvider do # assume it never reached the client expect(token.prev_auth_token).to eq(old_token) expect(token.auth_token).not_to eq(unverified_token) - end describe "events" do before do @refreshes = 0 - @increase_refreshes = -> (user) { @refreshes += 1 } + @increase_refreshes = ->(user) { @refreshes += 1 } DiscourseEvent.on(:user_session_refreshed, &@increase_refreshes) end - after do - DiscourseEvent.off(:user_session_refreshed, &@increase_refreshes) - end + after { DiscourseEvent.off(:user_session_refreshed, &@increase_refreshes) } it "fires event when updating last seen" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie = @provider.cookie_jar["_t"] unhashed_token = decrypt_auth_cookie(cookie)[:token] @@ -460,7 +442,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "does not fire an event when last seen does not update" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie = @provider.cookie_jar["_t"] unhashed_token = decrypt_auth_cookie(cookie)[:token] @@ -473,42 +455,38 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end describe "rate limiting" do - before do - RateLimiter.enable - end + before { RateLimiter.enable } it "can only try 10 bad cookies a minute" do token = UserAuthToken.generate!(user_id: user.id) - cookie = create_auth_cookie( - token: token.unhashed_auth_token, - user_id: user.id, - trust_level: user.trust_level, - issued_at: 5.minutes.ago - ) + cookie = + create_auth_cookie( + token: token.unhashed_auth_token, + user_id: user.id, + trust_level: user.trust_level, + issued_at: 5.minutes.ago, + ) - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) RateLimiter.new(nil, "cookie_auth_10.0.0.1", 10, 60).clear! RateLimiter.new(nil, "cookie_auth_10.0.0.2", 10, 60).clear! ip = "10.0.0.1" - bad_cookie = create_auth_cookie( - token: SecureRandom.hex, - user_id: user.id, - trust_level: user.trust_level, - issued_at: 5.minutes.ago, - ) + bad_cookie = + create_auth_cookie( + token: SecureRandom.hex, + user_id: user.id, + trust_level: user.trust_level, + issued_at: 5.minutes.ago, + ) env = { "HTTP_COOKIE" => "_t=#{bad_cookie}", "REMOTE_ADDR" => ip } - 10.times do - provider('/', env).current_user - end + 10.times { provider("/", env).current_user } - expect { - provider('/', env).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", env).current_user }.to raise_error(Discourse::InvalidAccess) expect { env["HTTP_COOKIE"] = "_t=#{cookie}" @@ -517,29 +495,28 @@ RSpec.describe Auth::DefaultCurrentUserProvider do env["REMOTE_ADDR"] = "10.0.0.2" - expect { - provider('/', env).current_user - }.not_to raise_error + expect { provider("/", env).current_user }.not_to raise_error end end it "correctly removes invalid cookies" do - bad_cookie = create_auth_cookie( - token: SecureRandom.hex, - user_id: 1, - trust_level: 4, - issued_at: 5.minutes.ago, - ) - @provider = provider('/') + bad_cookie = + create_auth_cookie( + token: SecureRandom.hex, + user_id: 1, + trust_level: 4, + issued_at: 5.minutes.ago, + ) + @provider = provider("/") @provider.cookie_jar["_t"] = bad_cookie @provider.refresh_session(nil, {}, @provider.cookie_jar) expect(@provider.cookie_jar.key?("_t")).to eq(false) end it "logging on user always creates a new token" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) - @provider2 = provider('/') + @provider2 = provider("/") @provider2.log_on_user(user, {}, @provider2.cookie_jar) expect(UserAuthToken.where(user_id: user.id).count).to eq(2) @@ -548,22 +525,24 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "cleans up old sessions when a user logs in" do yesterday = 1.day.ago - UserAuthToken.insert_all((1..(UserAuthToken::MAX_SESSION_COUNT + 2)).to_a.map do |i| - { - user_id: user.id, - created_at: yesterday + i.seconds, - updated_at: yesterday + i.seconds, - rotated_at: yesterday + i.seconds, - prev_auth_token: "abc#{i}", - auth_token: "abc#{i}" - } - end) + UserAuthToken.insert_all( + (1..(UserAuthToken::MAX_SESSION_COUNT + 2)).to_a.map do |i| + { + user_id: user.id, + created_at: yesterday + i.seconds, + updated_at: yesterday + i.seconds, + rotated_at: yesterday + i.seconds, + prev_auth_token: "abc#{i}", + auth_token: "abc#{i}", + } + end, + ) # Check the oldest 3 still exist expect(UserAuthToken.where(auth_token: (1..3).map { |i| "abc#{i}" }).count).to eq(3) # On next login, gets fixed - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) expect(UserAuthToken.where(user_id: user.id).count).to eq(UserAuthToken::MAX_SESSION_COUNT) @@ -575,7 +554,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do SiteSetting.force_https = false SiteSetting.same_site_cookies = "Lax" - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie_info = get_cookie_info(@provider.cookie_jar, "_t") @@ -586,7 +565,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do SiteSetting.force_https = true SiteSetting.same_site_cookies = "Disabled" - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie_info = get_cookie_info(@provider.cookie_jar, "_t") @@ -597,14 +576,15 @@ RSpec.describe Auth::DefaultCurrentUserProvider do it "correctly expires session" do SiteSetting.maximum_session_age = 2 token = UserAuthToken.generate!(user_id: user.id) - cookie = create_auth_cookie( - token: token.unhashed_auth_token, - user_id: user.id, - trust_level: user.trust_level, - issued_at: 5.minutes.ago - ) + cookie = + create_auth_cookie( + token: token.unhashed_auth_token, + user_id: user.id, + trust_level: user.trust_level, + issued_at: 5.minutes.ago, + ) - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) expect(provider("/", "HTTP_COOKIE" => "_t=#{cookie}").current_user.id).to eq(user.id) @@ -628,20 +608,21 @@ RSpec.describe Auth::DefaultCurrentUserProvider do let :api_key do UserApiKey.create!( - application_name: 'my app', - client_id: '1234', - scopes: ['read'].map { |name| UserApiKeyScope.new(name: name) }, - user_id: user.id + application_name: "my app", + client_id: "1234", + scopes: ["read"].map { |name| UserApiKeyScope.new(name: name) }, + user_id: user.id, ) end it "can clear old duplicate keys correctly" do - dupe = UserApiKey.create!( - application_name: 'my app', - client_id: '12345', - scopes: ['read'].map { |name| UserApiKeyScope.new(name: name) }, - user_id: user.id - ) + dupe = + UserApiKey.create!( + application_name: "my app", + client_id: "12345", + scopes: ["read"].map { |name| UserApiKeyScope.new(name: name) }, + user_id: user.id, + ) params = { "REQUEST_METHOD" => "GET", @@ -655,16 +636,13 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "allows user API access correctly" do - params = { - "REQUEST_METHOD" => "GET", - "HTTP_USER_API_KEY" => api_key.key, - } + params = { "REQUEST_METHOD" => "GET", "HTTP_USER_API_KEY" => api_key.key } good_provider = provider("/", params) - expect do - expect(good_provider.current_user.id).to eq(user.id) - end.to change { api_key.reload.last_used_at } + expect do expect(good_provider.current_user.id).to eq(user.id) end.to change { + api_key.reload.last_used_at + } expect(good_provider.is_api?).to eq(false) expect(good_provider.is_user_api?).to eq(true) @@ -676,38 +654,27 @@ RSpec.describe Auth::DefaultCurrentUserProvider do user.update_columns(suspended_till: 1.year.from_now) - expect { - provider("/", params).current_user - }.to raise_error(Discourse::InvalidAccess) + expect { provider("/", params).current_user }.to raise_error(Discourse::InvalidAccess) end describe "when readonly mode is enabled due to postgres" do - before do - Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + before { Discourse.enable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } - after do - Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) - end + after { Discourse.disable_readonly_mode(Discourse::PG_READONLY_MODE_KEY) } - it 'should not update ApiKey#last_used_at' do - params = { - "REQUEST_METHOD" => "GET", - "HTTP_USER_API_KEY" => api_key.key, - } + it "should not update ApiKey#last_used_at" do + params = { "REQUEST_METHOD" => "GET", "HTTP_USER_API_KEY" => api_key.key } good_provider = provider("/", params) - expect do - expect(good_provider.current_user.id).to eq(user.id) - end.to_not change { api_key.reload.last_used_at } + expect do expect(good_provider.current_user.id).to eq(user.id) end.to_not change { + api_key.reload.last_used_at + } end end context "with rate limiting" do - before do - RateLimiter.enable - end + before { RateLimiter.enable } it "rate limits api usage" do limiter1 = RateLimiter.new(nil, "user_api_day_#{ApiKey.hash_key(api_key.key)}", 10, 60) @@ -718,18 +685,11 @@ RSpec.describe Auth::DefaultCurrentUserProvider do global_setting :max_user_api_reqs_per_day, 3 global_setting :max_user_api_reqs_per_minute, 4 - params = { - "REQUEST_METHOD" => "GET", - "HTTP_USER_API_KEY" => api_key.key, - } + params = { "REQUEST_METHOD" => "GET", "HTTP_USER_API_KEY" => api_key.key } - 3.times do - provider("/", params).current_user - end + 3.times { provider("/", params).current_user } - expect { - provider("/", params).current_user - }.to raise_error(RateLimiter::LimitExceeded) + expect { provider("/", params).current_user }.to raise_error(RateLimiter::LimitExceeded) global_setting :max_user_api_reqs_per_day, 4 global_setting :max_user_api_reqs_per_minute, 3 @@ -737,19 +697,15 @@ RSpec.describe Auth::DefaultCurrentUserProvider do limiter1.clear! limiter2.clear! - 3.times do - provider("/", params).current_user - end + 3.times { provider("/", params).current_user } - expect { - provider("/", params).current_user - }.to raise_error(RateLimiter::LimitExceeded) + expect { provider("/", params).current_user }.to raise_error(RateLimiter::LimitExceeded) end end end it "ignores a valid auth cookie that has been tampered with" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) cookie = @provider.cookie_jar["_t"] @@ -757,33 +713,38 @@ RSpec.describe Auth::DefaultCurrentUserProvider do ip = "10.0.0.1" env = { "HTTP_COOKIE" => "_t=#{cookie}", "REMOTE_ADDR" => ip } - expect(provider('/', env).current_user).to eq(nil) + expect(provider("/", env).current_user).to eq(nil) end it "copes with json-serialized auth cookies" do # We're switching to :json during the Rails 7 upgrade, but we want a clean revert path # back to Rails 6 if needed - @provider = provider('/', { # The upcoming default - ActionDispatch::Cookies::COOKIES_SERIALIZER => :json, - method: "GET", - }) + @provider = + provider( + "/", + { # The upcoming default + ActionDispatch::Cookies::COOKIES_SERIALIZER => :json, + :method => "GET", + }, + ) @provider.log_on_user(user, {}, @provider.cookie_jar) cookie = CGI.escape(@provider.cookie_jar["_t"]) ip = "10.0.0.1" env = { "HTTP_COOKIE" => "_t=#{cookie}", "REMOTE_ADDR" => ip } - provider2 = provider('/', env) + provider2 = provider("/", env) expect(provider2.current_user).to eq(user) expect(provider2.cookie_jar.encrypted["_t"].keys).to include("user_id", "token") # (strings) end describe "#log_off_user" do it "should work when the current user was cached by a different provider instance" do - user_provider = provider('/') + user_provider = provider("/") user_provider.log_on_user(user, {}, user_provider.cookie_jar) cookie = CGI.escape(user_provider.cookie_jar["_t"]) - env = create_request_env(path: "/").merge({ method: "GET", "HTTP_COOKIE" => "_t=#{cookie}" }) + env = + create_request_env(path: "/").merge({ :method => "GET", "HTTP_COOKIE" => "_t=#{cookie}" }) user_provider = TestProvider.new(env) expect(user_provider.current_user).to eq(user) @@ -802,7 +763,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "makes the user into an admin if their email is in DISCOURSE_DEVELOPER_EMAILS" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) expect(user.reload.admin).to eq(true) user2 = Fabricate(:user) @@ -811,7 +772,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "adds the user to the correct staff/admin auto groups" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) user.reload expect(user.in_any_groups?([Group::AUTO_GROUPS[:staff]])).to eq(true) @@ -819,7 +780,7 @@ RSpec.describe Auth::DefaultCurrentUserProvider do end it "runs the job to enable bootstrap mode" do - @provider = provider('/') + @provider = provider("/") @provider.log_on_user(user, {}, @provider.cookie_jar) expect_job_enqueued(job: :enable_bootstrap_mode, args: { user_id: user.id }) end diff --git a/spec/lib/auth/discord_authenticator_spec.rb b/spec/lib/auth/discord_authenticator_spec.rb index cff7c662f4..7aea3e5d09 100644 --- a/spec/lib/auth/discord_authenticator_spec.rb +++ b/spec/lib/auth/discord_authenticator_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Auth::DiscordAuthenticator do - let(:hash) { + let(:hash) do OmniAuth::AuthHash.new( provider: "facebook", extra: { @@ -10,27 +10,27 @@ RSpec.describe Auth::DiscordAuthenticator do username: "bobbob", guilds: [ { - "id": "80351110224678912", - "name": "1337 Krew", - "icon": "8342729096ea3675442027381ff50dfe", - "owner": true, - "permissions": 36953089 - } - ] - } + id: "80351110224678912", + name: "1337 Krew", + icon: "8342729096ea3675442027381ff50dfe", + owner: true, + permissions: 36_953_089, + }, + ], + }, }, info: { email: "bob@bob.com", - name: "bobbob" + name: "bobbob", }, - uid: "100" + uid: "100", ) - } + end let(:authenticator) { described_class.new } - describe 'after_authenticate' do - it 'works normally' do + describe "after_authenticate" do + it "works normally" do result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) expect(result.failed).to eq(false) @@ -38,16 +38,16 @@ RSpec.describe Auth::DiscordAuthenticator do expect(result.email).to eq("bob@bob.com") end - it 'denies access when guilds are restricted' do - SiteSetting.discord_trusted_guilds = ["someguildid", "someotherguildid"].join("|") + it "denies access when guilds are restricted" do + SiteSetting.discord_trusted_guilds = %w[someguildid someotherguildid].join("|") result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) expect(result.failed).to eq(true) expect(result.failed_reason).to eq(I18n.t("discord.not_in_allowed_guild")) end - it 'allows access when in an allowed guild' do - SiteSetting.discord_trusted_guilds = ["80351110224678912", "anothertrustedguild"].join("|") + it "allows access when in an allowed guild" do + SiteSetting.discord_trusted_guilds = %w[80351110224678912 anothertrustedguild].join("|") result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) expect(result.failed).to eq(false) diff --git a/spec/lib/auth/facebook_authenticator_spec.rb b/spec/lib/auth/facebook_authenticator_spec.rb index 9033506af3..d19319d3a1 100644 --- a/spec/lib/auth/facebook_authenticator_spec.rb +++ b/spec/lib/auth/facebook_authenticator_spec.rb @@ -1,31 +1,32 @@ # frozen_string_literal: true RSpec.describe Auth::FacebookAuthenticator do - let(:hash) { + let(:hash) do { provider: "facebook", extra: { - raw_info: {} + raw_info: { + }, }, info: { email: "bob@bob.com", first_name: "Bob", - last_name: "Smith" + last_name: "Smith", }, - uid: "100" + uid: "100", } - } + end let(:authenticator) { Auth::FacebookAuthenticator.new } - describe 'after_authenticate' do - it 'can authenticate and create a user record for already existing users' do + describe "after_authenticate" do + it "can authenticate and create a user record for already existing users" do user = Fabricate(:user) result = authenticator.after_authenticate(hash.deep_merge(info: { email: user.email })) expect(result.user.id).to eq(user.id) end - it 'can connect to a different existing user account' do + it "can connect to a different existing user account" do user1 = Fabricate(:user) user2 = Fabricate(:user) @@ -34,46 +35,64 @@ RSpec.describe Auth::FacebookAuthenticator do result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user2.id) - expect(UserAssociatedAccount.exists?(provider_name: "facebook", user_id: user1.id)).to eq(false) - expect(UserAssociatedAccount.exists?(provider_name: "facebook", user_id: user2.id)).to eq(true) + expect(UserAssociatedAccount.exists?(provider_name: "facebook", user_id: user1.id)).to eq( + false, + ) + expect(UserAssociatedAccount.exists?(provider_name: "facebook", user_id: user2.id)).to eq( + true, + ) end - it 'can create a proper result for non existing users' do + it "can create a proper result for non existing users" do result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) expect(result.name).to eq("Bob Smith") end end - describe 'description_for_user' do + describe "description_for_user" do fab!(:user) { Fabricate(:user) } - it 'returns empty string if no entry for user' do + it "returns empty string if no entry for user" do expect(authenticator.description_for_user(user)).to eq("") end - it 'returns correct information' do - UserAssociatedAccount.create!(provider_name: "facebook", user_id: user.id, provider_uid: 100, info: { email: "someuser@somedomain.tld" }) - expect(authenticator.description_for_user(user)).to eq('someuser@somedomain.tld') + it "returns correct information" do + UserAssociatedAccount.create!( + provider_name: "facebook", + user_id: user.id, + provider_uid: 100, + info: { + email: "someuser@somedomain.tld", + }, + ) + expect(authenticator.description_for_user(user)).to eq("someuser@somedomain.tld") end end - describe 'revoke' do + describe "revoke" do fab!(:user) { Fabricate(:user) } let(:authenticator) { Auth::FacebookAuthenticator.new } - it 'raises exception if no entry for user' do + it "raises exception if no entry for user" do expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) end context "with valid record" do before do - SiteSetting.facebook_app_id = '123' - SiteSetting.facebook_app_secret = 'abcde' - UserAssociatedAccount.create!(provider_name: "facebook", user_id: user.id, provider_uid: 100, info: { email: "someuser@somedomain.tld" }) + SiteSetting.facebook_app_id = "123" + SiteSetting.facebook_app_secret = "abcde" + UserAssociatedAccount.create!( + provider_name: "facebook", + user_id: user.id, + provider_uid: 100, + info: { + email: "someuser@somedomain.tld", + }, + ) end - it 'revokes correctly' do + it "revokes correctly" do expect(authenticator.description_for_user(user)).to eq("someuser@somedomain.tld") expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) diff --git a/spec/lib/auth/github_authenticator_spec.rb b/spec/lib/auth/github_authenticator_spec.rb index 15020a9636..b5ec3162fa 100644 --- a/spec/lib/auth/github_authenticator_spec.rb +++ b/spec/lib/auth/github_authenticator_spec.rb @@ -4,11 +4,7 @@ def auth_token_for(user) { provider: "github", extra: { - all_emails: [{ - email: user.email, - primary: true, - verified: true, - }] + all_emails: [{ email: user.email, primary: true, verified: true }], }, info: { email: user.email, @@ -16,7 +12,7 @@ def auth_token_for(user) name: user.name, image: "https://avatars3.githubusercontent.com/u/#{user.username}", }, - uid: '100' + uid: "100", } end @@ -24,10 +20,10 @@ RSpec.describe Auth::GithubAuthenticator do let(:authenticator) { described_class.new } fab!(:user) { Fabricate(:user) } - describe 'after_authenticate' do + describe "after_authenticate" do let(:data) { auth_token_for(user) } - it 'can authenticate and create a user record for already existing users' do + it "can authenticate and create a user record for already existing users" do result = authenticator.after_authenticate(data) expect(result.user.id).to eq(user.id) @@ -43,37 +39,41 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(true) end - it 'can authenticate and update GitHub screen_name for existing user' do - UserAssociatedAccount.create!(user_id: user.id, provider_name: "github", provider_uid: 100, info: { nickname: "boris" }) + it "can authenticate and update GitHub screen_name for existing user" do + UserAssociatedAccount.create!( + user_id: user.id, + provider_name: "github", + provider_uid: 100, + info: { + nickname: "boris", + }, + ) result = authenticator.after_authenticate(data) expect(result.user.id).to eq(user.id) expect(result.email).to eq(user.email) expect(result.email_valid).to eq(true) - expect(UserAssociatedAccount.find_by(provider_name: "github", user_id: user.id).info["nickname"]).to eq(user.username) + expect( + UserAssociatedAccount.find_by(provider_name: "github", user_id: user.id).info["nickname"], + ).to eq(user.username) end - it 'should use primary email for new user creation over other available emails' do + it "should use primary email for new user creation over other available emails" do hash = { provider: "github", extra: { - all_emails: [{ - email: "bob@example.com", - primary: false, - verified: true, - }, { - email: "john@example.com", - primary: true, - verified: true, - }] + all_emails: [ + { email: "bob@example.com", primary: false, verified: true }, + { email: "john@example.com", primary: true, verified: true }, + ], }, info: { email: "john@example.com", nickname: "john", name: "John Bob", }, - uid: "100" + uid: "100", } result = authenticator.after_authenticate(hash) @@ -81,28 +81,30 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email).to eq("john@example.com") end - it 'should not error out if user already has a different old github account attached' do - + it "should not error out if user already has a different old github account attached" do # There is a rare case where an end user had # 2 different github accounts and moved emails between the 2 - UserAssociatedAccount.create!(user_id: user.id, info: { nickname: 'bob' }, provider_uid: 100, provider_name: "github") + UserAssociatedAccount.create!( + user_id: user.id, + info: { + nickname: "bob", + }, + provider_uid: 100, + provider_name: "github", + ) hash = { provider: "github", extra: { - all_emails: [{ - email: user.email, - primary: false, - verified: true, - }] + all_emails: [{ email: user.email, primary: false, verified: true }], }, info: { email: "john@example.com", nickname: "john", name: "John Bob", }, - uid: "1001" + uid: "1001", } result = authenticator.after_authenticate(hash) @@ -111,22 +113,18 @@ RSpec.describe Auth::GithubAuthenticator do expect(UserAssociatedAccount.where(user_id: user.id).pluck(:provider_uid)).to eq(["1001"]) end - it 'will not authenticate for already existing users with an unverified email' do + it "will not authenticate for already existing users with an unverified email" do hash = { provider: "github", extra: { - all_emails: [{ - email: user.email, - primary: true, - verified: false, - }] + all_emails: [{ email: user.email, primary: true, verified: false }], }, info: { email: user.email, nickname: user.username, name: user.name, }, - uid: "100" + uid: "100", } result = authenticator.after_authenticate(hash) @@ -138,22 +136,18 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(false) end - it 'can create a proper result for non existing users' do + it "can create a proper result for non existing users" do hash = { provider: "github", extra: { - all_emails: [{ - email: "person@example.com", - primary: true, - verified: true, - }] + all_emails: [{ email: "person@example.com", primary: true, verified: true }], }, info: { email: "person@example.com", nickname: "person", name: "Person Lastname", }, - uid: "100" + uid: "100", } result = authenticator.after_authenticate(hash) @@ -165,26 +159,21 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(hash[:info][:email].present?) end - it 'will skip blocklisted domains for non existing users' do + it "will skip blocklisted domains for non existing users" do hash = { provider: "github", extra: { - all_emails: [{ - email: "not_allowed@blocklist.com", - primary: true, - verified: true, - }, { - email: "allowed@allowlist.com", - primary: false, - verified: true, - }] + all_emails: [ + { email: "not_allowed@blocklist.com", primary: true, verified: true }, + { email: "allowed@allowlist.com", primary: false, verified: true }, + ], }, info: { email: "not_allowed@blocklist.com", nickname: "person", name: "Person Lastname", }, - uid: "100" + uid: "100", } SiteSetting.blocked_email_domains = "blocklist.com" @@ -197,30 +186,22 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(true) end - it 'will find allowlisted domains for non existing users' do + it "will find allowlisted domains for non existing users" do hash = { provider: "github", extra: { - all_emails: [{ - email: "person@example.com", - primary: true, - verified: true, - }, { - email: "not_allowed@blocklist.com", - primary: false, - verified: true, - }, { - email: "allowed@allowlist.com", - primary: false, - verified: true, - }] + all_emails: [ + { email: "person@example.com", primary: true, verified: true }, + { email: "not_allowed@blocklist.com", primary: false, verified: true }, + { email: "allowed@allowlist.com", primary: false, verified: true }, + ], }, info: { email: "person@example.com", nickname: "person", name: "Person Lastname", }, - uid: "100" + uid: "100", } SiteSetting.allowed_email_domains = "allowlist.com" @@ -233,13 +214,20 @@ RSpec.describe Auth::GithubAuthenticator do expect(result.email_valid).to eq(true) end - it 'can connect to a different existing user account' do + it "can connect to a different existing user account" do user1 = Fabricate(:user) user2 = Fabricate(:user) expect(authenticator.can_connect_existing_user?).to eq(true) - UserAssociatedAccount.create!(provider_name: "github", user_id: user1.id, provider_uid: 100, info: { nickname: "boris" }) + UserAssociatedAccount.create!( + provider_name: "github", + user_id: user1.id, + provider_uid: 100, + info: { + nickname: "boris", + }, + ) result = authenticator.after_authenticate(data, existing_account: user2) @@ -249,47 +237,55 @@ RSpec.describe Auth::GithubAuthenticator do end end - describe 'revoke' do + describe "revoke" do fab!(:user) { Fabricate(:user) } let(:authenticator) { Auth::GithubAuthenticator.new } - it 'raises exception if no entry for user' do + it "raises exception if no entry for user" do expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) end - it 'revokes correctly' do - UserAssociatedAccount.create!(provider_name: "github", user_id: user.id, provider_uid: 100, info: { nickname: "boris" }) + it "revokes correctly" do + UserAssociatedAccount.create!( + provider_name: "github", + user_id: user.id, + provider_uid: 100, + info: { + nickname: "boris", + }, + ) expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) expect(authenticator.description_for_user(user)).to eq("") end end - describe 'avatar retrieval' do + describe "avatar retrieval" do let(:job_klass) { Jobs::DownloadAvatarFromUrl } - context 'when user has a custom avatar' do + context "when user has a custom avatar" do fab!(:user_avatar) { Fabricate(:user_avatar, custom_upload: Fabricate(:upload)) } fab!(:user_with_custom_avatar) { Fabricate(:user, user_avatar: user_avatar) } - it 'does not enqueue a download_avatar_from_url job' do + it "does not enqueue a download_avatar_from_url job" do expect { authenticator.after_authenticate(auth_token_for(user_with_custom_avatar)) }.to_not change(job_klass.jobs, :size) end end - context 'when user does not have a custom avatar' do - it 'enqueues a download_avatar_from_url job' do - expect { - authenticator.after_authenticate(auth_token_for(user)) - }.to change(job_klass.jobs, :size).by(1) + context "when user does not have a custom avatar" do + it "enqueues a download_avatar_from_url job" do + expect { authenticator.after_authenticate(auth_token_for(user)) }.to change( + job_klass.jobs, + :size, + ).by(1) - job_args = job_klass.jobs.last['args'].first + job_args = job_klass.jobs.last["args"].first - expect(job_args['url']).to eq("https://avatars3.githubusercontent.com/u/#{user.username}") - expect(job_args['user_id']).to eq(user.id) - expect(job_args['override_gravatar']).to eq(false) + expect(job_args["url"]).to eq("https://avatars3.githubusercontent.com/u/#{user.username}") + expect(job_args["user_id"]).to eq(user.id) + expect(job_args["override_gravatar"]).to eq(false) end end end diff --git a/spec/lib/auth/google_oauth2_authenticator_spec.rb b/spec/lib/auth/google_oauth2_authenticator_spec.rb index f610763ed3..d42a08d1d1 100644 --- a/spec/lib/auth/google_oauth2_authenticator_spec.rb +++ b/spec/lib/auth/google_oauth2_authenticator_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.describe Auth::GoogleOAuth2Authenticator do - it 'does not look up user unless email is verified' do + it "does not look up user unless email is verified" do # note, emails that come back from google via omniauth are always valid # this protects against future regressions @@ -12,16 +12,16 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do provider: "google_oauth2", uid: "123456789", info: { - name: "John Doe", - email: user.email + name: "John Doe", + email: user.email, }, extra: { raw_info: { email: user.email, email_verified: false, - name: "John Doe" - } - } + name: "John Doe", + }, + }, } result = authenticator.after_authenticate(hash) @@ -29,8 +29,8 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do expect(result.user).to eq(nil) end - describe 'after_authenticate' do - it 'can authenticate and create a user record for already existing users' do + describe "after_authenticate" do + it "can authenticate and create a user record for already existing users" do authenticator = Auth::GoogleOAuth2Authenticator.new user = Fabricate(:user) @@ -38,16 +38,16 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do provider: "google_oauth2", uid: "123456789", info: { - name: "John Doe", - email: user.email + name: "John Doe", + email: user.email, }, extra: { raw_info: { email: user.email, email_verified: true, - name: "John Doe" - } - } + name: "John Doe", + }, + }, } result = authenticator.after_authenticate(hash) @@ -55,27 +55,31 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do expect(result.user.id).to eq(user.id) end - it 'can connect to a different existing user account' do + it "can connect to a different existing user account" do authenticator = Auth::GoogleOAuth2Authenticator.new user1 = Fabricate(:user) user2 = Fabricate(:user) - UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user1.id, provider_uid: 100) + UserAssociatedAccount.create!( + provider_name: "google_oauth2", + user_id: user1.id, + provider_uid: 100, + ) hash = { provider: "google_oauth2", uid: "100", info: { - name: "John Doe", - email: user1.email + name: "John Doe", + email: user1.email, }, extra: { raw_info: { email: user1.email, email_verified: true, - name: "John Doe" - } - } + name: "John Doe", + }, + }, } result = authenticator.after_authenticate(hash, existing_account: user2) @@ -85,23 +89,23 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true) end - it 'can create a proper result for non existing users' do + it "can create a proper result for non existing users" do hash = { provider: "google_oauth2", uid: "123456789", info: { - first_name: "Jane", - last_name: "Doe", - name: "Jane Doe", - email: "jane.doe@the.google.com" + first_name: "Jane", + last_name: "Doe", + name: "Jane Doe", + email: "jane.doe@the.google.com", }, extra: { raw_info: { email: "jane.doe@the.google.com", email_verified: true, - name: "Jane Doe" - } - } + name: "Jane Doe", + }, + }, } authenticator = described_class.new @@ -117,48 +121,38 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do group1 = OmniAuth::AuthHash.new(id: "12345", name: "group1") group2 = OmniAuth::AuthHash.new(id: "67890", name: "group2") @groups = [group1, group2] - @auth_hash = OmniAuth::AuthHash.new( - provider: "google_oauth2", - uid: "123456789", - info: { - first_name: "Jane", - last_name: "Doe", - name: "Jane Doe", - email: "jane.doe@the.google.com" - }, - extra: { - raw_info: { + @auth_hash = + OmniAuth::AuthHash.new( + provider: "google_oauth2", + uid: "123456789", + info: { + first_name: "Jane", + last_name: "Doe", + name: "Jane Doe", email: "jane.doe@the.google.com", - email_verified: true, - name: "Jane Doe" }, - } - ) + extra: { + raw_info: { + email: "jane.doe@the.google.com", + email_verified: true, + name: "Jane Doe", + }, + }, + ) end context "when enabled" do let(:private_key) { OpenSSL::PKey::RSA.generate(2048) } - let(:group_response) { - { - groups: [ - { - id: "12345", - name: "group1" - }, - { - id: "67890", - name: "group2" - } - ] - } - } + let(:group_response) do + { groups: [{ id: "12345", name: "group1" }, { id: "67890", name: "group2" }] } + end before do SiteSetting.google_oauth2_hd_groups_service_account_admin_email = "admin@example.com" SiteSetting.google_oauth2_hd_groups_service_account_json = { "private_key" => private_key.to_s, - "client_email": "discourse-group-sync@example.iam.gserviceaccount.com", + :"client_email" => "discourse-group-sync@example.iam.gserviceaccount.com", }.to_json SiteSetting.google_oauth2_hd_groups = true @@ -166,28 +160,30 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do stub_request(:post, "https://oauth2.googleapis.com/token").to_return do |request| jwt = Rack::Utils.parse_query(request.body)["assertion"] - decoded_token = JWT.decode(jwt, private_key.public_key, true, { algorithm: 'RS256' }) + decoded_token = JWT.decode(jwt, private_key.public_key, true, { algorithm: "RS256" }) { status: 200, body: { "access_token" => token, "type" => "bearer" }.to_json, - headers: { "Content-Type" => "application/json" } + headers: { + "Content-Type" => "application/json", + }, } rescue JWT::VerificationError - { - status: 403, - body: "Invalid JWT" - } + { status: 403, body: "Invalid JWT" } end - stub_request(:get, "https://admin.googleapis.com/admin/directory/v1/groups?userKey=#{@auth_hash.uid}"). - with(headers: { "Authorization" => "Bearer #{token}" }). - to_return do + stub_request( + :get, + "https://admin.googleapis.com/admin/directory/v1/groups?userKey=#{@auth_hash.uid}", + ) + .with(headers: { "Authorization" => "Bearer #{token}" }) + .to_return do { status: 200, body: group_response.to_json, headers: { - "Content-Type" => "application/json" - } + "Content-Type" => "application/json", + }, } end end @@ -206,7 +202,7 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do it "doesn't explode with invalid credentials" do SiteSetting.google_oauth2_hd_groups_service_account_json = { "private_key" => OpenSSL::PKey::RSA.generate(2048).to_s, - "client_email": "discourse-group-sync@example.iam.gserviceaccount.com", + :"client_email" => "discourse-group-sync@example.iam.gserviceaccount.com", }.to_json result = described_class.new.after_authenticate(@auth_hash) @@ -215,9 +211,7 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do end context "when disabled" do - before do - SiteSetting.google_oauth2_hd_groups = false - end + before { SiteSetting.google_oauth2_hd_groups = false } it "doesnt add associated groups" do result = described_class.new.after_authenticate(@auth_hash) @@ -227,16 +221,20 @@ RSpec.describe Auth::GoogleOAuth2Authenticator do end end - describe 'revoke' do + describe "revoke" do fab!(:user) { Fabricate(:user) } let(:authenticator) { Auth::GoogleOAuth2Authenticator.new } - it 'raises exception if no entry for user' do + it "raises exception if no entry for user" do expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) end - it 'revokes correctly' do - UserAssociatedAccount.create!(provider_name: "google_oauth2", user_id: user.id, provider_uid: 12345) + it "revokes correctly" do + UserAssociatedAccount.create!( + provider_name: "google_oauth2", + user_id: user.id, + provider_uid: 12_345, + ) expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) expect(authenticator.description_for_user(user)).to eq("") diff --git a/spec/lib/auth/managed_authenticator_spec.rb b/spec/lib/auth/managed_authenticator_spec.rb index a91cae9933..940be5776b 100644 --- a/spec/lib/auth/managed_authenticator_spec.rb +++ b/spec/lib/auth/managed_authenticator_spec.rb @@ -1,19 +1,21 @@ # frozen_string_literal: true RSpec.describe Auth::ManagedAuthenticator do - let(:authenticator) { - Class.new(described_class) do - def name - "myauth" - end + let(:authenticator) do + Class + .new(described_class) do + def name + "myauth" + end - def primary_email_verified?(auth_token) - auth_token[:info][:email_verified] + def primary_email_verified?(auth_token) + auth_token[:info][:email_verified] + end end - end.new - } + .new + end - let(:hash) { + let(:hash) do OmniAuth::AuthHash.new( provider: "myauth", uid: "1234", @@ -21,25 +23,20 @@ RSpec.describe Auth::ManagedAuthenticator do name: "Best Display Name", email: "awesome@example.com", nickname: "IAmGroot", - email_verified: true + email_verified: true, }, credentials: { - token: "supersecrettoken" + token: "supersecrettoken", }, extra: { raw_info: { - randominfo: "some info" - } - } + randominfo: "some info", + }, + }, ) - } + end - let(:create_hash) { - OmniAuth::AuthHash.new( - provider: "myauth", - uid: "1234" - ) - } + let(:create_hash) { OmniAuth::AuthHash.new(provider: "myauth", uid: "1234") } def create_auth_result(attrs) auth_result = Auth::Result.new @@ -47,10 +44,16 @@ RSpec.describe Auth::ManagedAuthenticator do auth_result end - describe 'after_authenticate' do - it 'can match account from an existing association' do + describe "after_authenticate" do + it "can match account from an existing association" do user = Fabricate(:user) - associated = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", last_used: 1.year.ago) + associated = + UserAssociatedAccount.create!( + user: user, + provider_name: "myauth", + provider_uid: "1234", + last_used: 1.year.ago, + ) result = authenticator.after_authenticate(hash) expect(result.user.id).to eq(user.id) @@ -62,36 +65,50 @@ RSpec.describe Auth::ManagedAuthenticator do expect(associated.extra["raw_info"]["randominfo"]).to eq("some info") end - it 'only sets email valid for present strings' do + it "only sets email valid for present strings" do # (Twitter sometimes sends empty email strings) - result = authenticator.after_authenticate(create_hash.merge(info: { email: "email@example.com", email_verified: true })) + result = + authenticator.after_authenticate( + create_hash.merge(info: { email: "email@example.com", email_verified: true }), + ) expect(result.email_valid).to eq(true) - result = authenticator.after_authenticate(create_hash.merge(info: { email: "", email_verified: true })) + result = + authenticator.after_authenticate( + create_hash.merge(info: { email: "", email_verified: true }), + ) expect(result.email_valid).to be_falsey - result = authenticator.after_authenticate(create_hash.merge(info: { email: nil, email_verified: true })) + result = + authenticator.after_authenticate( + create_hash.merge(info: { email: nil, email_verified: true }), + ) expect(result.email_valid).to be_falsey end - it 'does not set email valid if email_verified is false' do - result = authenticator.after_authenticate(create_hash.merge(info: { email: "email@example.com", email_verified: false })) + it "does not set email valid if email_verified is false" do + result = + authenticator.after_authenticate( + create_hash.merge(info: { email: "email@example.com", email_verified: false }), + ) expect(result.email_valid).to eq(false) end - describe 'connecting to another user account' do + describe "connecting to another user account" do fab!(:user1) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } - before { UserAssociatedAccount.create!(user: user1, provider_name: 'myauth', provider_uid: "1234") } + before do + UserAssociatedAccount.create!(user: user1, provider_name: "myauth", provider_uid: "1234") + end - it 'works by default' do + it "works by default" do result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user2.id) expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(false) expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true) end - it 'still works if another user has a matching email' do + it "still works if another user has a matching email" do Fabricate(:user, email: hash.dig(:info, :email)) result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user2.id) @@ -99,15 +116,18 @@ RSpec.describe Auth::ManagedAuthenticator do expect(UserAssociatedAccount.exists?(user_id: user2.id)).to eq(true) end - it 'does not work when disabled' do - authenticator = Class.new(described_class) do - def name - "myauth" - end - def can_connect_existing_user? - false - end - end.new + it "does not work when disabled" do + authenticator = + Class + .new(described_class) do + def name + "myauth" + end + def can_connect_existing_user? + false + end + end + .new result = authenticator.after_authenticate(hash, existing_account: user2) expect(result.user.id).to eq(user1.id) expect(UserAssociatedAccount.exists?(user_id: user1.id)).to eq(true) @@ -115,42 +135,48 @@ RSpec.describe Auth::ManagedAuthenticator do end end - describe 'match by email' do - it 'downcases the email address from the authprovider' do - result = authenticator.after_authenticate(hash.deep_merge(info: { email: "HELLO@example.com" })) - expect(result.email).to eq('hello@example.com') + describe "match by email" do + it "downcases the email address from the authprovider" do + result = + authenticator.after_authenticate(hash.deep_merge(info: { email: "HELLO@example.com" })) + expect(result.email).to eq("hello@example.com") end - it 'works normally' do + it "works normally" do user = Fabricate(:user) result = authenticator.after_authenticate(hash.deep_merge(info: { email: user.email })) expect(result.user.id).to eq(user.id) - expect(UserAssociatedAccount.find_by(provider_name: 'myauth', provider_uid: "1234").user_id).to eq(user.id) + expect( + UserAssociatedAccount.find_by(provider_name: "myauth", provider_uid: "1234").user_id, + ).to eq(user.id) end - it 'works if there is already an association with the target account' do + it "works if there is already an association with the target account" do user = Fabricate(:user, email: "awesome@example.com") result = authenticator.after_authenticate(hash) expect(result.user.id).to eq(user.id) end - it 'does not match if match_by_email is false' do - authenticator = Class.new(described_class) do - def name - "myauth" - end - def match_by_email - false - end - end.new + it "does not match if match_by_email is false" do + authenticator = + Class + .new(described_class) do + def name + "myauth" + end + def match_by_email + false + end + end + .new user = Fabricate(:user, email: "awesome@example.com") result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) end end - context 'when no matching user' do - it 'returns the correct information' do + context "when no matching user" do + it "returns the correct information" do expect { result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) @@ -161,13 +187,13 @@ RSpec.describe Auth::ManagedAuthenticator do expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot") end - it 'works if there is already an association with the target account' do + it "works if there is already an association with the target account" do user = Fabricate(:user, email: "awesome@example.com") result = authenticator.after_authenticate(hash) expect(result.user.id).to eq(user.id) end - it 'works if there is no email' do + it "works if there is no email" do expect { result = authenticator.after_authenticate(hash.deep_merge(info: { email: nil })) expect(result.user).to eq(nil) @@ -178,7 +204,7 @@ RSpec.describe Auth::ManagedAuthenticator do expect(UserAssociatedAccount.last.info["nickname"]).to eq("IAmGroot") end - it 'will ignore name when equal to email' do + it "will ignore name when equal to email" do result = authenticator.after_authenticate(hash.deep_merge(info: { name: hash.info.email })) expect(result.email).to eq(hash.info.email) expect(result.name).to eq(nil) @@ -187,39 +213,61 @@ RSpec.describe Auth::ManagedAuthenticator do describe "avatar on update" do fab!(:user) { Fabricate(:user) } - let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") } + let!(:associated) do + UserAssociatedAccount.create!(user: user, provider_name: "myauth", provider_uid: "1234") + end it "schedules the job upon update correctly" do # No image supplied, do not schedule - expect { result = authenticator.after_authenticate(hash) } - .not_to change { Jobs::DownloadAvatarFromUrl.jobs.count } + expect { result = authenticator.after_authenticate(hash) }.not_to change { + Jobs::DownloadAvatarFromUrl.jobs.count + } # Image supplied, schedule - expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) } - .to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1) + expect { + result = + authenticator.after_authenticate( + hash.deep_merge(info: { image: "https://some.domain/image.jpg" }), + ) + }.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1) # User already has profile picture, don't schedule user.user_avatar = Fabricate(:user_avatar, custom_upload: Fabricate(:upload)) user.save! - expect { result = authenticator.after_authenticate(hash.deep_merge(info: { image: "https://some.domain/image.jpg" })) } - .not_to change { Jobs::DownloadAvatarFromUrl.jobs.count } + expect { + result = + authenticator.after_authenticate( + hash.deep_merge(info: { image: "https://some.domain/image.jpg" }), + ) + }.not_to change { Jobs::DownloadAvatarFromUrl.jobs.count } end end describe "profile on update" do fab!(:user) { Fabricate(:user) } - let!(:associated) { UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234") } + let!(:associated) do + UserAssociatedAccount.create!(user: user, provider_name: "myauth", provider_uid: "1234") + end it "updates the user's location and bio, unless already set" do { description: :bio_raw, location: :location }.each do |auth_hash_key, profile_key| user.user_profile.update(profile_key => "Initial Value") # No value supplied, do not overwrite - expect { result = authenticator.after_authenticate(hash) } - .not_to change { user.user_profile.reload; user.user_profile[profile_key] } + expect { result = authenticator.after_authenticate(hash) }.not_to change { + user.user_profile.reload + user.user_profile[profile_key] + } # Value supplied, still do not overwrite - expect { result = authenticator.after_authenticate(hash.deep_merge(info: { auth_hash_key => "New Value" })) } - .not_to change { user.user_profile.reload; user.user_profile[profile_key] } + expect { + result = + authenticator.after_authenticate( + hash.deep_merge(info: { auth_hash_key => "New Value" }), + ) + }.not_to change { + user.user_profile.reload + user.user_profile[profile_key] + } # User has not set a value, so overwrite user.user_profile.update(profile_key => "") @@ -232,24 +280,32 @@ RSpec.describe Auth::ManagedAuthenticator do describe "avatar on create" do fab!(:user) { Fabricate(:user) } - let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") } + let!(:association) do + UserAssociatedAccount.create!(provider_name: "myauth", provider_uid: "1234") + end it "doesn't schedule with no image" do - expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) } - .not_to change { Jobs::DownloadAvatarFromUrl.jobs.count } + expect { + result = + authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) + }.not_to change { Jobs::DownloadAvatarFromUrl.jobs.count } end it "schedules with image" do association.info["image"] = "https://some.domain/image.jpg" association.save! - expect { result = authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) } - .to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1) + expect { + result = + authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) + }.to change { Jobs::DownloadAvatarFromUrl.jobs.count }.by(1) end end describe "profile on create" do fab!(:user) { Fabricate(:user) } - let!(:association) { UserAssociatedAccount.create!(provider_name: 'myauth', provider_uid: "1234") } + let!(:association) do + UserAssociatedAccount.create!(provider_name: "myauth", provider_uid: "1234") + end it "doesn't explode without profile" do authenticator.after_create_account(user, create_auth_result(extra_data: create_hash)) @@ -265,37 +321,44 @@ RSpec.describe Auth::ManagedAuthenticator do end end - describe 'match by username' do - let(:user_match_authenticator) { - Class.new(described_class) do - def name - "myauth" + describe "match by username" do + let(:user_match_authenticator) do + Class + .new(described_class) do + def name + "myauth" + end + def match_by_email + false + end + def match_by_username + true + end end - def match_by_email - false - end - def match_by_username - true - end - end.new - } - - it 'works normally' do - SiteSetting.username_change_period = 0 - user = Fabricate(:user) - result = user_match_authenticator.after_authenticate(hash.deep_merge(info: { nickname: user.username })) - expect(result.user.id).to eq(user.id) - expect(UserAssociatedAccount.find_by(provider_name: 'myauth', provider_uid: "1234").user_id).to eq(user.id) + .new end - it 'works if there is already an association with the target account' do + it "works normally" do + SiteSetting.username_change_period = 0 + user = Fabricate(:user) + result = + user_match_authenticator.after_authenticate( + hash.deep_merge(info: { nickname: user.username }), + ) + expect(result.user.id).to eq(user.id) + expect( + UserAssociatedAccount.find_by(provider_name: "myauth", provider_uid: "1234").user_id, + ).to eq(user.id) + end + + it "works if there is already an association with the target account" do SiteSetting.username_change_period = 0 user = Fabricate(:user, username: "IAmGroot") result = user_match_authenticator.after_authenticate(hash) expect(result.user.id).to eq(user.id) end - it 'works if the username is different case' do + it "works if the username is different case" do SiteSetting.username_change_period = 0 user = Fabricate(:user, username: "IAMGROOT") result = user_match_authenticator.after_authenticate(hash) @@ -309,28 +372,34 @@ RSpec.describe Auth::ManagedAuthenticator do expect(result.user).to eq(nil) end - it 'does not match if default match_by_username not overriden' do + it "does not match if default match_by_username not overriden" do SiteSetting.username_change_period = 0 - authenticator = Class.new(described_class) do - def name - "myauth" - end - end.new + authenticator = + Class + .new(described_class) do + def name + "myauth" + end + end + .new user = Fabricate(:user, username: "IAmGroot") result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) end - it 'does not match if match_by_username is false' do + it "does not match if match_by_username is false" do SiteSetting.username_change_period = 0 - authenticator = Class.new(described_class) do - def name - "myauth" - end - def match_by_username - false - end - end.new + authenticator = + Class + .new(described_class) do + def name + "myauth" + end + def match_by_username + false + end + end + .new user = Fabricate(:user, username: "IAmGroot") result = authenticator.after_authenticate(hash) expect(result.user).to eq(nil) @@ -338,38 +407,57 @@ RSpec.describe Auth::ManagedAuthenticator do end end - describe 'description_for_user' do + describe "description_for_user" do fab!(:user) { Fabricate(:user) } - it 'returns empty string if no entry for user' do + it "returns empty string if no entry for user" do expect(authenticator.description_for_user(user)).to eq("") end - it 'returns correct information' do - association = UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { nickname: "somenickname", email: "test@domain.tld", name: "bestname" }) - expect(authenticator.description_for_user(user)).to eq('test@domain.tld') + it "returns correct information" do + association = + UserAssociatedAccount.create!( + user: user, + provider_name: "myauth", + provider_uid: "1234", + info: { + nickname: "somenickname", + email: "test@domain.tld", + name: "bestname", + }, + ) + expect(authenticator.description_for_user(user)).to eq("test@domain.tld") association.update(info: { nickname: "somenickname", name: "bestname" }) - expect(authenticator.description_for_user(user)).to eq('somenickname') + expect(authenticator.description_for_user(user)).to eq("somenickname") association.update(info: { nickname: "bestname" }) - expect(authenticator.description_for_user(user)).to eq('bestname') + expect(authenticator.description_for_user(user)).to eq("bestname") association.update(info: {}) - expect(authenticator.description_for_user(user)).to eq(I18n.t("associated_accounts.connected")) + expect(authenticator.description_for_user(user)).to eq( + I18n.t("associated_accounts.connected"), + ) end end - describe 'revoke' do + describe "revoke" do fab!(:user) { Fabricate(:user) } - it 'raises exception if no entry for user' do + it "raises exception if no entry for user" do expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) end context "with valid record" do before do - UserAssociatedAccount.create!(user: user, provider_name: 'myauth', provider_uid: "1234", info: { name: "somename" }) + UserAssociatedAccount.create!( + user: user, + provider_name: "myauth", + provider_uid: "1234", + info: { + name: "somename", + }, + ) end - it 'revokes correctly' do + it "revokes correctly" do expect(authenticator.description_for_user(user)).to eq("somename") expect(authenticator.can_revoke?).to eq(true) expect(authenticator.revoke(user)).to eq(true) @@ -378,5 +466,4 @@ RSpec.describe Auth::ManagedAuthenticator do end end end - end diff --git a/spec/lib/auth/result_spec.rb b/spec/lib/auth/result_spec.rb index 2c9a83e057..ebc5830ce7 100644 --- a/spec/lib/auth/result_spec.rb +++ b/spec/lib/auth/result_spec.rb @@ -3,7 +3,9 @@ RSpec.describe Auth::Result do fab!(:initial_email) { "initialemail@example.org" } fab!(:initial_username) { "initialusername" } fab!(:initial_name) { "Initial Name" } - fab!(:user) { Fabricate(:user, email: initial_email, username: initial_username, name: initial_name) } + fab!(:user) do + Fabricate(:user, email: initial_email, username: initial_username, name: initial_name) + end let(:new_email) { "newemail@example.org" } let(:new_username) { "newusername" } diff --git a/spec/lib/auth/twitter_authenticator_spec.rb b/spec/lib/auth/twitter_authenticator_spec.rb index 14f5c834a6..c6e1bdfd69 100644 --- a/spec/lib/auth/twitter_authenticator_spec.rb +++ b/spec/lib/auth/twitter_authenticator_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Auth::TwitterAuthenticator do nickname: "minion", }, uid: "123", - provider: "twitter" + provider: "twitter", } result = auth.after_authenticate(auth_token) @@ -25,7 +25,7 @@ RSpec.describe Auth::TwitterAuthenticator do expect(info.info["email"]).to eq(user.email) end - it 'can connect to a different existing user account' do + it "can connect to a different existing user account" do authenticator = Auth::TwitterAuthenticator.new user1 = Fabricate(:user) user2 = Fabricate(:user) @@ -40,7 +40,7 @@ RSpec.describe Auth::TwitterAuthenticator do nickname: "minion", }, uid: "100", - provider: "twitter" + provider: "twitter", } result = authenticator.after_authenticate(hash, existing_account: user2) @@ -50,19 +50,19 @@ RSpec.describe Auth::TwitterAuthenticator do expect(UserAssociatedAccount.exists?(provider_name: "twitter", user_id: user2.id)).to eq(true) end - describe 'revoke' do + describe "revoke" do fab!(:user) { Fabricate(:user) } let(:authenticator) { Auth::TwitterAuthenticator.new } - it 'raises exception if no entry for user' do + it "raises exception if no entry for user" do expect { authenticator.revoke(user) }.to raise_error(Discourse::NotFound) end - it 'revokes correctly' do - UserAssociatedAccount.create!(provider_name: "twitter", user_id: user.id, provider_uid: 100) - expect(authenticator.can_revoke?).to eq(true) - expect(authenticator.revoke(user)).to eq(true) - expect(authenticator.description_for_user(user)).to eq("") - end + it "revokes correctly" do + UserAssociatedAccount.create!(provider_name: "twitter", user_id: user.id, provider_uid: 100) + expect(authenticator.can_revoke?).to eq(true) + expect(authenticator.revoke(user)).to eq(true) + expect(authenticator.description_for_user(user)).to eq("") + end end end diff --git a/spec/lib/backup_restore/backup_file_handler_multisite_spec.rb b/spec/lib/backup_restore/backup_file_handler_multisite_spec.rb index 5a0b1f6de1..40a01e26bc 100644 --- a/spec/lib/backup_restore/backup_file_handler_multisite_spec.rb +++ b/spec/lib/backup_restore/backup_file_handler_multisite_spec.rb @@ -10,7 +10,7 @@ RSpec.describe BackupRestore::BackupFileHandler, type: :multisite do expect_decompress_and_clean_up_to_work( backup_filename: "backup_till_v1.5.tar.gz", require_metadata_file: true, - require_uploads: true + require_uploads: true, ) end end diff --git a/spec/lib/backup_restore/backup_file_handler_spec.rb b/spec/lib/backup_restore/backup_file_handler_spec.rb index 8803de7890..c7f017692e 100644 --- a/spec/lib/backup_restore/backup_file_handler_spec.rb +++ b/spec/lib/backup_restore/backup_file_handler_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'shared_context_for_backup_restore' +require_relative "shared_context_for_backup_restore" RSpec.describe BackupRestore::BackupFileHandler do include_context "with shared stuff" @@ -9,7 +9,7 @@ RSpec.describe BackupRestore::BackupFileHandler do expect_decompress_and_clean_up_to_work( backup_filename: "backup_since_v1.6.tar.gz", require_metadata_file: false, - require_uploads: true + require_uploads: true, ) end @@ -18,7 +18,7 @@ RSpec.describe BackupRestore::BackupFileHandler do backup_filename: "sql_only_backup.sql.gz", expected_dump_filename: "sql_only_backup.sql", require_metadata_file: false, - require_uploads: false + require_uploads: false, ) end @@ -27,11 +27,11 @@ RSpec.describe BackupRestore::BackupFileHandler do backup_filename: "backup_with_wrong_upload_path.tar.gz", require_metadata_file: false, require_uploads: true, - expected_upload_paths: [ - "uploads/default/original/1X/both.txt", - "uploads/default/original/1X/only-uploads.txt", - "uploads/default/original/1X/only-var.txt" - ] + expected_upload_paths: %w[ + uploads/default/original/1X/both.txt + uploads/default/original/1X/only-uploads.txt + uploads/default/original/1X/only-var.txt + ], ) do |upload_path| content = File.read(upload_path).chomp @@ -54,7 +54,7 @@ RSpec.describe BackupRestore::BackupFileHandler do backup_filename: "backup_since_v1.6.tar.gz", require_metadata_file: false, require_uploads: true, - location: BackupLocationSiteSetting::LOCAL + location: BackupLocationSiteSetting::LOCAL, ) end end diff --git a/spec/lib/backup_restore/backuper_spec.rb b/spec/lib/backup_restore/backuper_spec.rb index c66006d631..1578a44f44 100644 --- a/spec/lib/backup_restore/backuper_spec.rb +++ b/spec/lib/backup_restore/backuper_spec.rb @@ -1,55 +1,55 @@ # frozen_string_literal: true RSpec.describe BackupRestore::Backuper do - it 'returns a non-empty parameterized title when site title contains unicode' do - SiteSetting.title = 'Ɣ' + it "returns a non-empty parameterized title when site title contains unicode" do + SiteSetting.title = "Ɣ" backuper = BackupRestore::Backuper.new(Discourse.system_user.id) expect(backuper.send(:get_parameterized_title)).to eq("discourse") end - it 'returns a valid parameterized site title' do + it "returns a valid parameterized site title" do SiteSetting.title = "Coding Horror" backuper = BackupRestore::Backuper.new(Discourse.system_user.id) expect(backuper.send(:get_parameterized_title)).to eq("coding-horror") end - describe '#notify_user' do - before do - freeze_time Time.zone.parse('2010-01-01 12:00') - end + describe "#notify_user" do + before { freeze_time Time.zone.parse("2010-01-01 12:00") } - it 'includes logs if short' do + it "includes logs if short" do SiteSetting.max_export_file_size_kb = 1 SiteSetting.export_authorized_extensions = "tar.gz" silence_stdout do backuper = BackupRestore::Backuper.new(Discourse.system_user.id) - expect { backuper.send(:notify_user) } - .to change { Topic.private_messages.count }.by(1) - .and not_change { Upload.count } + expect { backuper.send(:notify_user) }.to change { Topic.private_messages.count }.by( + 1, + ).and not_change { Upload.count } end - expect(Topic.last.first_post.raw).to include("```text\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n```") + expect(Topic.last.first_post.raw).to include( + "```text\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n```", + ) end - it 'include upload if log is long' do + it "include upload if log is long" do SiteSetting.max_post_length = 250 silence_stdout do backuper = BackupRestore::Backuper.new(Discourse.system_user.id) - expect { backuper.send(:notify_user) } - .to change { Topic.private_messages.count }.by(1) - .and change { Upload.where(original_filename: "log.txt.zip").count }.by(1) + expect { backuper.send(:notify_user) }.to change { Topic.private_messages.count }.by( + 1, + ).and change { Upload.where(original_filename: "log.txt.zip").count }.by(1) end expect(Topic.last.first_post.raw).to include("[log.txt.zip|attachment]") end - it 'includes trimmed logs if log is long and upload cannot be saved' do + it "includes trimmed logs if log is long and upload cannot be saved" do SiteSetting.max_post_length = 348 SiteSetting.max_export_file_size_kb = 1 SiteSetting.export_authorized_extensions = "tar.gz" @@ -57,16 +57,16 @@ RSpec.describe BackupRestore::Backuper do silence_stdout do backuper = BackupRestore::Backuper.new(Discourse.system_user.id) - 1.upto(10).each do |i| - backuper.send(:log, "Line #{i}") - end + 1.upto(10).each { |i| backuper.send(:log, "Line #{i}") } - expect { backuper.send(:notify_user) } - .to change { Topic.private_messages.count }.by(1) - .and not_change { Upload.count } + expect { backuper.send(:notify_user) }.to change { Topic.private_messages.count }.by( + 1, + ).and not_change { Upload.count } end - expect(Topic.last.first_post.raw).to include("```text\n...\n[2010-01-01 12:00:00] Line 10\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n```") + expect(Topic.last.first_post.raw).to include( + "```text\n...\n[2010-01-01 12:00:00] Line 10\n[2010-01-01 12:00:00] Notifying 'system' of the end of the backup...\n```", + ) end end end diff --git a/spec/lib/backup_restore/database_restorer_spec.rb b/spec/lib/backup_restore/database_restorer_spec.rb index e8d69c0723..afa4bea4f3 100644 --- a/spec/lib/backup_restore/database_restorer_spec.rb +++ b/spec/lib/backup_restore/database_restorer_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'shared_context_for_backup_restore' +require_relative "shared_context_for_backup_restore" RSpec.describe BackupRestore::DatabaseRestorer do include_context "with shared stuff" @@ -32,7 +32,7 @@ RSpec.describe BackupRestore::DatabaseRestorer do context "with real psql" do after do psql = BackupRestore::DatabaseRestorer.psql_command - system("#{psql} -c 'DROP TABLE IF EXISTS foo'", [:out, :err] => File::NULL) + system("#{psql} -c 'DROP TABLE IF EXISTS foo'", %i[out err] => File::NULL) end def restore(filename, stub_migrate: true) @@ -74,24 +74,27 @@ RSpec.describe BackupRestore::DatabaseRestorer do end it "detects error during restore" do - expect { restore("error.sql", stub_migrate: false) } - .to raise_error(BackupRestore::DatabaseRestoreError) + expect { restore("error.sql", stub_migrate: false) }.to raise_error( + BackupRestore::DatabaseRestoreError, + ) end end describe "rewrites database dump" do let(:logger) do - Class.new do - attr_reader :log_messages + Class + .new do + attr_reader :log_messages - def initialize - @log_messages = [] - end + def initialize + @log_messages = [] + end - def log(message, ex = nil) - @log_messages << message if message + def log(message, ex = nil) + @log_messages << message if message + end end - end.new + .new end def restore_and_log_output(filename) @@ -108,8 +111,12 @@ RSpec.describe BackupRestore::DatabaseRestorer do expect(log).not_to be_blank expect(log).not_to match(/CREATE SCHEMA public/) expect(log).not_to match(/EXECUTE FUNCTION/) - expect(log).to match(/^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_topic_id_readonly/) - expect(log).to match(/^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_user_id_readonly/) + expect(log).to match( + /^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_topic_id_readonly/, + ) + expect(log).to match( + /^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE PROCEDURE discourse_functions.raise_foo_user_id_readonly/, + ) end it "does not replace `EXECUTE FUNCTION` when restoring on PostgreSQL >= 11" do @@ -119,13 +126,17 @@ RSpec.describe BackupRestore::DatabaseRestorer do expect(log).not_to be_blank expect(log).not_to match(/CREATE SCHEMA public/) expect(log).not_to match(/EXECUTE PROCEDURE/) - expect(log).to match(/^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_topic_id_readonly/) - expect(log).to match(/^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_user_id_readonly/) + expect(log).to match( + /^CREATE TRIGGER foo_topic_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_topic_id_readonly/, + ) + expect(log).to match( + /^CREATE TRIGGER foo_user_id_readonly .+? EXECUTE FUNCTION discourse_functions.raise_foo_user_id_readonly/, + ) end end describe "database connection" do - it 'it is not erroring for non-multisite' do + it "it is not erroring for non-multisite" do expect { execute_stubbed_restore }.not_to raise_error end end @@ -147,7 +158,7 @@ RSpec.describe BackupRestore::DatabaseRestorer do describe "readonly functions" do before do BackupRestore::DatabaseRestorer.stubs(:core_migration_files).returns( - Dir[Rails.root.join("spec/fixtures/db/post_migrate/drop_column/**/*.rb")] + Dir[Rails.root.join("spec/fixtures/db/post_migrate/drop_column/**/*.rb")], ) end @@ -167,8 +178,9 @@ RSpec.describe BackupRestore::DatabaseRestorer do end it "creates and drops only missing functions during restore" do - Migration::BaseDropper.stubs(:existing_discourse_function_names) - .returns(%w(raise_email_logs_readonly raise_posts_raw_email_readonly)) + Migration::BaseDropper.stubs(:existing_discourse_function_names).returns( + %w[raise_email_logs_readonly raise_posts_raw_email_readonly], + ) Migration::BaseDropper.expects(:create_readonly_function).with(:posts, :via_email) execute_stubbed_restore(stub_readonly_functions: false) @@ -191,9 +203,7 @@ RSpec.describe BackupRestore::DatabaseRestorer do end context "when a backup schema exists" do - before do - ActiveRecord::Base.connection.expects(:schema_exists?).with("backup").returns(true) - end + before { ActiveRecord::Base.connection.expects(:schema_exists?).with("backup").returns(true) } it "drops the schema when the last restore was long ago" do ActiveRecord::Base.connection.expects(:drop_schema).with("backup") diff --git a/spec/lib/backup_restore/local_backup_store_spec.rb b/spec/lib/backup_restore/local_backup_store_spec.rb index faa01c4f16..345fb4e4d1 100644 --- a/spec/lib/backup_restore/local_backup_store_spec.rb +++ b/spec/lib/backup_restore/local_backup_store_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'backup_restore/local_backup_store' -require_relative 'shared_examples_for_backup_store' +require "backup_restore/local_backup_store" +require_relative "shared_examples_for_backup_store" RSpec.describe BackupRestore::LocalBackupStore do before do @@ -10,9 +10,7 @@ RSpec.describe BackupRestore::LocalBackupStore do SiteSetting.backup_location = BackupLocationSiteSetting::LOCAL end - after do - FileUtils.remove_dir(@root_directory, true) - end + after { FileUtils.remove_dir(@root_directory, true) } subject(:store) { BackupRestore::BackupStore.create(root_directory: @root_directory) } let(:expected_type) { BackupRestore::LocalBackupStore } @@ -24,14 +22,49 @@ RSpec.describe BackupRestore::LocalBackupStore do end def create_backups - create_file(db_name: "default", filename: "b.tar.gz", last_modified: "2018-09-13T15:10:00Z", size_in_bytes: 17) - create_file(db_name: "default", filename: "a.tgz", last_modified: "2018-02-11T09:27:00Z", size_in_bytes: 29) - create_file(db_name: "default", filename: "r.sql.gz", last_modified: "2017-12-20T03:48:00Z", size_in_bytes: 11) - create_file(db_name: "default", filename: "no-backup.txt", last_modified: "2018-09-05T14:27:00Z", size_in_bytes: 12) - create_file(db_name: "default/subfolder", filename: "c.tar.gz", last_modified: "2019-01-24T18:44:00Z", size_in_bytes: 23) + create_file( + db_name: "default", + filename: "b.tar.gz", + last_modified: "2018-09-13T15:10:00Z", + size_in_bytes: 17, + ) + create_file( + db_name: "default", + filename: "a.tgz", + last_modified: "2018-02-11T09:27:00Z", + size_in_bytes: 29, + ) + create_file( + db_name: "default", + filename: "r.sql.gz", + last_modified: "2017-12-20T03:48:00Z", + size_in_bytes: 11, + ) + create_file( + db_name: "default", + filename: "no-backup.txt", + last_modified: "2018-09-05T14:27:00Z", + size_in_bytes: 12, + ) + create_file( + db_name: "default/subfolder", + filename: "c.tar.gz", + last_modified: "2019-01-24T18:44:00Z", + size_in_bytes: 23, + ) - create_file(db_name: "second", filename: "multi-2.tar.gz", last_modified: "2018-11-27T03:16:54Z", size_in_bytes: 19) - create_file(db_name: "second", filename: "multi-1.tar.gz", last_modified: "2018-11-26T03:17:09Z", size_in_bytes: 22) + create_file( + db_name: "second", + filename: "multi-2.tar.gz", + last_modified: "2018-11-27T03:16:54Z", + size_in_bytes: 19, + ) + create_file( + db_name: "second", + filename: "multi-1.tar.gz", + last_modified: "2018-11-26T03:17:09Z", + size_in_bytes: 22, + ) end def remove_backups diff --git a/spec/lib/backup_restore/meta_data_handler_spec.rb b/spec/lib/backup_restore/meta_data_handler_spec.rb index 9dd08f0e82..70f062e89d 100644 --- a/spec/lib/backup_restore/meta_data_handler_spec.rb +++ b/spec/lib/backup_restore/meta_data_handler_spec.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require_relative 'shared_context_for_backup_restore' +require_relative "shared_context_for_backup_restore" RSpec.describe BackupRestore::MetaDataHandler do include_context "with shared stuff" - let!(:backup_filename) { 'discourse-2019-11-18-143242-v20191108000414.tar.gz' } + let!(:backup_filename) { "discourse-2019-11-18-143242-v20191108000414.tar.gz" } def with_metadata_file(content) Dir.mktmpdir do |directory| @@ -27,8 +27,7 @@ RSpec.describe BackupRestore::MetaDataHandler do metadata = '{"source":"discourse","version":20160329101122}' with_metadata_file(metadata) do |dir| - expect(validate_metadata(backup_filename, dir)) - .to include(version: 20160329101122) + expect(validate_metadata(backup_filename, dir)).to include(version: 20_160_329_101_122) end end @@ -36,15 +35,17 @@ RSpec.describe BackupRestore::MetaDataHandler do corrupt_metadata = '{"version":20160329101122' with_metadata_file(corrupt_metadata) do |dir| - expect { validate_metadata(backup_filename, dir) } - .to raise_error(BackupRestore::MetaDataError) + expect { validate_metadata(backup_filename, dir) }.to raise_error( + BackupRestore::MetaDataError, + ) end end it "raises an exception when the metadata file is empty" do - with_metadata_file('') do |dir| - expect { validate_metadata(backup_filename, dir) } - .to raise_error(BackupRestore::MetaDataError) + with_metadata_file("") do |dir| + expect { validate_metadata(backup_filename, dir) }.to raise_error( + BackupRestore::MetaDataError, + ) end end @@ -52,8 +53,9 @@ RSpec.describe BackupRestore::MetaDataHandler do metadata = '{"source":"discourse","version":"1abcdefghijklm"}' with_metadata_file(metadata) do |dir| - expect { validate_metadata(backup_filename, dir) } - .to raise_error(BackupRestore::MetaDataError) + expect { validate_metadata(backup_filename, dir) }.to raise_error( + BackupRestore::MetaDataError, + ) end end @@ -61,8 +63,9 @@ RSpec.describe BackupRestore::MetaDataHandler do metadata = '{"source":"discourse","version":""}' with_metadata_file(metadata) do |dir| - expect { validate_metadata(backup_filename, dir) } - .to raise_error(BackupRestore::MetaDataError) + expect { validate_metadata(backup_filename, dir) }.to raise_error( + BackupRestore::MetaDataError, + ) end end end @@ -70,36 +73,32 @@ RSpec.describe BackupRestore::MetaDataHandler do describe "filename" do it "extracts metadata from filename when metadata file does not exist" do with_metadata_file(nil) do |dir| - expect(validate_metadata(backup_filename, dir)) - .to include(version: 20191108000414) + expect(validate_metadata(backup_filename, dir)).to include(version: 20_191_108_000_414) end end it "raises an exception when the filename contains no version number" do - filename = 'discourse-2019-11-18-143242.tar.gz' + filename = "discourse-2019-11-18-143242.tar.gz" - expect { validate_metadata(filename, nil) } - .to raise_error(BackupRestore::MetaDataError) + expect { validate_metadata(filename, nil) }.to raise_error(BackupRestore::MetaDataError) end it "raises an exception when the filename contains an invalid version number" do - filename = 'discourse-2019-11-18-143242-v123456789.tar.gz' - expect { validate_metadata(filename, nil) } - .to raise_error(BackupRestore::MetaDataError) + filename = "discourse-2019-11-18-143242-v123456789.tar.gz" + expect { validate_metadata(filename, nil) }.to raise_error(BackupRestore::MetaDataError) - filename = 'discourse-2019-11-18-143242-v1abcdefghijklm.tar.gz' - expect { validate_metadata(filename, nil) } - .to raise_error(BackupRestore::MetaDataError) + filename = "discourse-2019-11-18-143242-v1abcdefghijklm.tar.gz" + expect { validate_metadata(filename, nil) }.to raise_error(BackupRestore::MetaDataError) end end it "raises an exception when the backup's version is newer than the current version" do - new_backup_filename = 'discourse-2019-11-18-143242-v20191113193141.sql.gz' + new_backup_filename = "discourse-2019-11-18-143242-v20191113193141.sql.gz" - BackupRestore.expects(:current_version) - .returns(20191025005204).once + BackupRestore.expects(:current_version).returns(20_191_025_005_204).once - expect { validate_metadata(new_backup_filename, nil) } - .to raise_error(BackupRestore::MigrationRequiredError) + expect { validate_metadata(new_backup_filename, nil) }.to raise_error( + BackupRestore::MigrationRequiredError, + ) end end diff --git a/spec/lib/backup_restore/s3_backup_store_spec.rb b/spec/lib/backup_restore/s3_backup_store_spec.rb index 4aad803cfd..7989fbaa4d 100644 --- a/spec/lib/backup_restore/s3_backup_store_spec.rb +++ b/spec/lib/backup_restore/s3_backup_store_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 's3_helper' -require 'backup_restore/s3_backup_store' -require_relative 'shared_examples_for_backup_store' +require "s3_helper" +require "backup_restore/s3_backup_store" +require_relative "shared_examples_for_backup_store" RSpec.describe BackupRestore::S3BackupStore do before do @@ -21,49 +21,64 @@ RSpec.describe BackupRestore::S3BackupStore do expect(context.params[:prefix]).to eq(expected_prefix) if context.params.key?(:prefix) end - @s3_client.stub_responses(:list_objects_v2, -> (context) do - check_context(context) + @s3_client.stub_responses( + :list_objects_v2, + ->(context) { + check_context(context) - { contents: objects_with_prefix(context) } - end) + { contents: objects_with_prefix(context) } + }, + ) - @s3_client.stub_responses(:delete_object, -> (context) do - check_context(context) + @s3_client.stub_responses( + :delete_object, + ->(context) { + check_context(context) - expect do - @objects.delete_if { |obj| obj[:key] == context.params[:key] } - end.to change { @objects } - end) + expect do @objects.delete_if { |obj| obj[:key] == context.params[:key] } end.to change { + @objects + } + }, + ) - @s3_client.stub_responses(:head_object, -> (context) do - check_context(context) + @s3_client.stub_responses( + :head_object, + ->(context) { + check_context(context) - if object = @objects.find { |obj| obj[:key] == context.params[:key] } - { content_length: object[:size], last_modified: object[:last_modified] } - else - { status_code: 404, headers: {}, body: "", } - end - end) + if object = @objects.find { |obj| obj[:key] == context.params[:key] } + { content_length: object[:size], last_modified: object[:last_modified] } + else + { status_code: 404, headers: {}, body: "" } + end + }, + ) - @s3_client.stub_responses(:get_object, -> (context) do - check_context(context) + @s3_client.stub_responses( + :get_object, + ->(context) { + check_context(context) - if object = @objects.find { |obj| obj[:key] == context.params[:key] } - { content_length: object[:size], body: "A" * object[:size] } - else - { status_code: 404, headers: {}, body: "", } - end - end) + if object = @objects.find { |obj| obj[:key] == context.params[:key] } + { content_length: object[:size], body: "A" * object[:size] } + else + { status_code: 404, headers: {}, body: "" } + end + }, + ) - @s3_client.stub_responses(:put_object, -> (context) do - check_context(context) + @s3_client.stub_responses( + :put_object, + ->(context) { + check_context(context) - @objects << { - key: context.params[:key], - size: context.params[:body].size, - last_modified: Time.zone.now - } - end) + @objects << { + key: context.params[:key], + size: context.params[:body].size, + last_modified: Time.zone.now, + } + }, + ) SiteSetting.s3_backup_bucket = "s3-backup-bucket" SiteSetting.s3_access_key_id = "s3-access-key-id" @@ -105,15 +120,47 @@ RSpec.describe BackupRestore::S3BackupStore do def create_backups @objects.clear - @objects << { key: "default/b.tar.gz", size: 17, last_modified: Time.parse("2018-09-13T15:10:00Z") } - @objects << { key: "default/a.tgz", size: 29, last_modified: Time.parse("2018-02-11T09:27:00Z") } - @objects << { key: "default/r.sql.gz", size: 11, last_modified: Time.parse("2017-12-20T03:48:00Z") } - @objects << { key: "default/no-backup.txt", size: 12, last_modified: Time.parse("2018-09-05T14:27:00Z") } - @objects << { key: "default/subfolder/c.tar.gz", size: 23, last_modified: Time.parse("2019-01-24T18:44:00Z") } + @objects << { + key: "default/b.tar.gz", + size: 17, + last_modified: Time.parse("2018-09-13T15:10:00Z"), + } + @objects << { + key: "default/a.tgz", + size: 29, + last_modified: Time.parse("2018-02-11T09:27:00Z"), + } + @objects << { + key: "default/r.sql.gz", + size: 11, + last_modified: Time.parse("2017-12-20T03:48:00Z"), + } + @objects << { + key: "default/no-backup.txt", + size: 12, + last_modified: Time.parse("2018-09-05T14:27:00Z"), + } + @objects << { + key: "default/subfolder/c.tar.gz", + size: 23, + last_modified: Time.parse("2019-01-24T18:44:00Z"), + } - @objects << { key: "second/multi-2.tar.gz", size: 19, last_modified: Time.parse("2018-11-27T03:16:54Z") } - @objects << { key: "second/multi-1.tar.gz", size: 22, last_modified: Time.parse("2018-11-26T03:17:09Z") } - @objects << { key: "second/subfolder/multi-3.tar.gz", size: 23, last_modified: Time.parse("2019-01-24T18:44:00Z") } + @objects << { + key: "second/multi-2.tar.gz", + size: 19, + last_modified: Time.parse("2018-11-27T03:16:54Z"), + } + @objects << { + key: "second/multi-1.tar.gz", + size: 22, + last_modified: Time.parse("2018-11-26T03:17:09Z"), + } + @objects << { + key: "second/subfolder/multi-3.tar.gz", + size: 23, + last_modified: Time.parse("2019-01-24T18:44:00Z"), + } end def remove_backups @@ -126,7 +173,7 @@ RSpec.describe BackupRestore::S3BackupStore do filename = Regexp.escape(filename) expires = SiteSetting.s3_presigned_get_url_expires_after_seconds - /\Ahttps:\/\/#{bucket}.*#{prefix}\/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z/ + %r{\Ahttps://#{bucket}.*#{prefix}/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z} end def upload_url_regex(db_name, filename, multisite:) @@ -135,7 +182,7 @@ RSpec.describe BackupRestore::S3BackupStore do filename = Regexp.escape(filename) expires = BackupRestore::S3BackupStore::UPLOAD_URL_EXPIRES_AFTER_SECONDS - /\Ahttps:\/\/#{bucket}.*#{prefix}\/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z/ + %r{\Ahttps://#{bucket}.*#{prefix}/#{filename}\?.*X-Amz-Expires=#{expires}.*X-Amz-Signature=.*\z} end def file_prefix(db_name, multisite) diff --git a/spec/lib/backup_restore/shared_context_for_backup_restore.rb b/spec/lib/backup_restore/shared_context_for_backup_restore.rb index 5ad59e604f..402a774993 100644 --- a/spec/lib/backup_restore/shared_context_for_backup_restore.rb +++ b/spec/lib/backup_restore/shared_context_for_backup_restore.rb @@ -2,9 +2,12 @@ RSpec.shared_context "with shared stuff" do let!(:logger) do - Class.new do - def log(message, ex = nil); end - end.new + Class + .new do + def log(message, ex = nil) + end + end + .new end def expect_create_readonly_functions @@ -33,13 +36,14 @@ RSpec.shared_context "with shared stuff" do end def expect_db_migrate - Discourse::Utils.expects(:execute_command).with do |env, *command, options| - env["SKIP_POST_DEPLOYMENT_MIGRATIONS"] == "0" && - env["SKIP_OPTIMIZE_ICONS"] == "1" && - env["DISABLE_TRANSLATION_OVERRIDES"] == "1" && - command == ["rake", "db:migrate"] && - options[:chdir] == Rails.root - end.once + Discourse::Utils + .expects(:execute_command) + .with do |env, *command, options| + env["SKIP_POST_DEPLOYMENT_MIGRATIONS"] == "0" && env["SKIP_OPTIMIZE_ICONS"] == "1" && + env["DISABLE_TRANSLATION_OVERRIDES"] == "1" && command == %w[rake db:migrate] && + options[:chdir] == Rails.root + end + .once end def expect_db_reconnect @@ -76,11 +80,14 @@ RSpec.shared_context "with shared stuff" do Dir.mktmpdir do |root_directory| current_db = RailsMultisite::ConnectionManagement.current_db - file_handler = BackupRestore::BackupFileHandler.new( - logger, backup_filename, current_db, - root_tmp_directory: root_directory, - location: location - ) + file_handler = + BackupRestore::BackupFileHandler.new( + logger, + backup_filename, + current_db, + root_tmp_directory: root_directory, + location: location, + ) tmp_directory, db_dump_path = file_handler.decompress expected_tmp_path = File.join(root_directory, "tmp/restores", current_db, "2019-12-24-143148") @@ -93,11 +100,14 @@ RSpec.shared_context "with shared stuff" do expect(File.exist?(File.join(tmp_directory, "meta.json"))).to eq(require_metadata_file) if require_uploads - expected_upload_paths ||= ["uploads/default/original/3X/b/d/bd269860bb508aebcb6f08fe7289d5f117830383.png"] + expected_upload_paths ||= [ + "uploads/default/original/3X/b/d/bd269860bb508aebcb6f08fe7289d5f117830383.png", + ] expected_upload_paths.each do |upload_path| absolute_upload_path = File.join(tmp_directory, upload_path) - expect(File.exist?(absolute_upload_path)).to eq(true), "expected file #{upload_path} does not exist" + expect(File.exist?(absolute_upload_path)).to eq(true), + "expected file #{upload_path} does not exist" yield(absolute_upload_path) if block_given? end else @@ -112,6 +122,10 @@ RSpec.shared_context "with shared stuff" do # We don't want to delete the directory unless it is empty, otherwise this could be annoying # when tests run for the "default" database in a development environment. - FileUtils.rmdir(target_directory) rescue nil + begin + FileUtils.rmdir(target_directory) + rescue StandardError + nil + end end end diff --git a/spec/lib/backup_restore/shared_examples_for_backup_store.rb b/spec/lib/backup_restore/shared_examples_for_backup_store.rb index eeb8ee2e05..a35456a83c 100644 --- a/spec/lib/backup_restore/shared_examples_for_backup_store.rb +++ b/spec/lib/backup_restore/shared_examples_for_backup_store.rb @@ -6,13 +6,39 @@ RSpec.shared_context "with backups" do after { remove_backups } # default backup files - let(:backup1) { BackupFile.new(filename: "b.tar.gz", size: 17, last_modified: Time.parse("2018-09-13T15:10:00Z")) } - let(:backup2) { BackupFile.new(filename: "a.tgz", size: 29, last_modified: Time.parse("2018-02-11T09:27:00Z")) } - let(:backup3) { BackupFile.new(filename: "r.sql.gz", size: 11, last_modified: Time.parse("2017-12-20T03:48:00Z")) } + let(:backup1) do + BackupFile.new( + filename: "b.tar.gz", + size: 17, + last_modified: Time.parse("2018-09-13T15:10:00Z"), + ) + end + let(:backup2) do + BackupFile.new(filename: "a.tgz", size: 29, last_modified: Time.parse("2018-02-11T09:27:00Z")) + end + let(:backup3) do + BackupFile.new( + filename: "r.sql.gz", + size: 11, + last_modified: Time.parse("2017-12-20T03:48:00Z"), + ) + end # backup files on another multisite - let(:backup4) { BackupFile.new(filename: "multi-1.tar.gz", size: 22, last_modified: Time.parse("2018-11-26T03:17:09Z")) } - let(:backup5) { BackupFile.new(filename: "multi-2.tar.gz", size: 19, last_modified: Time.parse("2018-11-27T03:16:54Z")) } + let(:backup4) do + BackupFile.new( + filename: "multi-1.tar.gz", + size: 22, + last_modified: Time.parse("2018-11-26T03:17:09Z"), + ) + end + let(:backup5) do + BackupFile.new( + filename: "multi-2.tar.gz", + size: 19, + last_modified: Time.parse("2018-11-27T03:16:54Z"), + ) + end end RSpec.shared_examples "backup store" do @@ -56,13 +82,15 @@ RSpec.shared_examples "backup store" do it "returns only *.gz and *.tgz files" do files = store.files expect(files).to_not be_empty - expect(files.map(&:filename)).to contain_exactly(backup1.filename, backup2.filename, backup3.filename) + expect(files.map(&:filename)).to contain_exactly( + backup1.filename, + backup2.filename, + backup3.filename, + ) end it "works with multisite", type: :multisite do - test_multisite_connection("second") do - expect(store.files).to eq([backup5, backup4]) - end + test_multisite_connection("second") { expect(store.files).to eq([backup5, backup4]) } end end @@ -77,9 +105,7 @@ RSpec.shared_examples "backup store" do end it "works with multisite", type: :multisite do - test_multisite_connection("second") do - expect(store.latest_file).to eq(backup5) - end + test_multisite_connection("second") { expect(store.latest_file).to eq(backup5) } end end @@ -222,11 +248,7 @@ RSpec.shared_examples "remote backup store" do # to that freeze_time(Time.now.to_s) - backup = BackupFile.new( - filename: "foo.tar.gz", - size: 33, - last_modified: Time.zone.now - ) + backup = BackupFile.new(filename: "foo.tar.gz", size: 33, last_modified: Time.zone.now) expect(store.files).to_not include(backup) @@ -246,15 +268,14 @@ RSpec.shared_examples "remote backup store" do end it "works with multisite", type: :multisite do - test_multisite_connection("second") do - upload_file - end + test_multisite_connection("second") { upload_file } end it "raises an exception when a file with same filename exists" do Tempfile.create(backup1.filename) do |file| - expect { store.upload_file(backup1.filename, file.path, "application/gzip") } - .to raise_exception(BackupRestore::BackupStore::BackupFileExists) + expect { + store.upload_file(backup1.filename, file.path, "application/gzip") + }.to raise_exception(BackupRestore::BackupStore::BackupFileExists) end end end @@ -268,8 +289,9 @@ RSpec.shared_examples "remote backup store" do end it "raises an exception when a file with same filename exists" do - expect { store.generate_upload_url(backup1.filename) } - .to raise_exception(BackupRestore::BackupStore::BackupFileExists) + expect { store.generate_upload_url(backup1.filename) }.to raise_exception( + BackupRestore::BackupStore::BackupFileExists, + ) end it "works with multisite", type: :multisite do diff --git a/spec/lib/backup_restore/system_interface_spec.rb b/spec/lib/backup_restore/system_interface_spec.rb index 20a701d8d3..8f1461cc17 100644 --- a/spec/lib/backup_restore/system_interface_spec.rb +++ b/spec/lib/backup_restore/system_interface_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require_relative 'shared_context_for_backup_restore' +require_relative "shared_context_for_backup_restore" RSpec.describe BackupRestore::SystemInterface do include_context "with shared stuff" @@ -8,9 +8,7 @@ RSpec.describe BackupRestore::SystemInterface do subject { BackupRestore::SystemInterface.new(logger) } describe "readonly mode" do - after do - Discourse::READONLY_KEYS.each { |key| Discourse.redis.del(key) } - end + after { Discourse::READONLY_KEYS.each { |key| Discourse.redis.del(key) } } describe "#enable_readonly_mode" do it "enables readonly mode" do @@ -107,45 +105,43 @@ RSpec.describe BackupRestore::SystemInterface do end context "with Sidekiq workers" do - after do - flush_sidekiq_redis_namespace - end + after { flush_sidekiq_redis_namespace } def flush_sidekiq_redis_namespace - Sidekiq.redis do |redis| - redis.scan_each { |key| redis.del(key) } - end + Sidekiq.redis { |redis| redis.scan_each { |key| redis.del(key) } } end def create_workers(site_id: nil, all_sites: false) - payload = Sidekiq::Testing.fake! do - data = { post_id: 1 } + payload = + Sidekiq::Testing.fake! do + data = { post_id: 1 } - if all_sites - data[:all_sites] = true - else - data[:current_site_id] = site_id || RailsMultisite::ConnectionManagement.current_db + if all_sites + data[:all_sites] = true + else + data[:current_site_id] = site_id || RailsMultisite::ConnectionManagement.current_db + end + + Jobs.enqueue(:process_post, data) + Jobs::ProcessPost.jobs.last end - Jobs.enqueue(:process_post, data) - Jobs::ProcessPost.jobs.last - end - Sidekiq.redis do |conn| hostname = "localhost" pid = 7890 key = "#{hostname}:#{pid}" process = { pid: pid, hostname: hostname } - conn.sadd('processes', key) - conn.hmset(key, 'info', Sidekiq.dump_json(process)) + conn.sadd("processes", key) + conn.hmset(key, "info", Sidekiq.dump_json(process)) - data = Sidekiq.dump_json( - queue: 'default', - run_at: Time.now.to_i, - payload: Sidekiq.dump_json(payload) - ) - conn.hmset("#{key}:work", '444', data) + data = + Sidekiq.dump_json( + queue: "default", + run_at: Time.now.to_i, + payload: Sidekiq.dump_json(payload), + ) + conn.hmset("#{key}:work", "444", data) end end diff --git a/spec/lib/backup_restore/uploads_restorer_spec.rb b/spec/lib/backup_restore/uploads_restorer_spec.rb index 3314358e9e..ff00811c9a 100644 --- a/spec/lib/backup_restore/uploads_restorer_spec.rb +++ b/spec/lib/backup_restore/uploads_restorer_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # rubocop:disable Discourse/OnlyTopLevelMultisiteSpecs -require_relative 'shared_context_for_backup_restore' +require_relative "shared_context_for_backup_restore" RSpec.describe BackupRestore::UploadsRestorer do include_context "with shared stuff" @@ -21,11 +21,19 @@ RSpec.describe BackupRestore::UploadsRestorer do expect_remaps( source_site_name: source_site_name, target_site_name: target_site_name, - metadata: metadata + metadata: metadata, ) end - def expect_remap(source_site_name: nil, target_site_name:, metadata: [], from:, to:, regex: false, &block) + def expect_remap( + source_site_name: nil, + target_site_name:, + metadata: [], + from:, + to:, + regex: false, + &block + ) expect_remaps( source_site_name: source_site_name, target_site_name: target_site_name, @@ -54,19 +62,25 @@ RSpec.describe BackupRestore::UploadsRestorer do if remaps.blank? DbHelper.expects(:remap).never else - DbHelper.expects(:remap).with do |from, to, args| - args[:excluded_tables]&.include?("backup_metadata") - remaps.shift == { from: from, to: to } - end.times(remaps.size) + DbHelper + .expects(:remap) + .with do |from, to, args| + args[:excluded_tables]&.include?("backup_metadata") + remaps.shift == { from: from, to: to } + end + .times(remaps.size) end if regex_remaps.blank? DbHelper.expects(:regexp_replace).never else - DbHelper.expects(:regexp_replace).with do |from, to, args| - args[:excluded_tables]&.include?("backup_metadata") - regex_remaps.shift == { from: from, to: to } - end.times(regex_remaps.size) + DbHelper + .expects(:regexp_replace) + .with do |from, to, args| + args[:excluded_tables]&.include?("backup_metadata") + regex_remaps.shift == { from: from, to: to } + end + .times(regex_remaps.size) end if target_site_name == "default" @@ -85,13 +99,14 @@ RSpec.describe BackupRestore::UploadsRestorer do def uploads_path(database) path = File.join("uploads", database) - path = File.join(path, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + path = File.join(path, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") "/#{path}/" end def s3_url_regex(bucket, path) - Regexp.escape("//#{bucket}") + %q*\.s3(?:\.dualstack\.[a-z0-9\-]+?|[.\-][a-z0-9\-]+?)?\.amazonaws\.com* + Regexp.escape(path) + Regexp.escape("//#{bucket}") + + %q*\.s3(?:\.dualstack\.[a-z0-9\-]+?|[.\-][a-z0-9\-]+?)?\.amazonaws\.com* + Regexp.escape(path) end describe "uploads" do @@ -99,8 +114,8 @@ RSpec.describe BackupRestore::UploadsRestorer do let!(:no_multisite) { { name: "multisite", value: false } } let!(:source_db_name) { { name: "db_name", value: "foo" } } let!(:base_url) { { name: "base_url", value: "https://test.localhost/forum" } } - let!(:no_cdn_url) { { name: "cdn_url", value: nil } } - let!(:cdn_url) { { name: "cdn_url", value: "https://some-cdn.example.com" } } + let!(:no_cdn_url) { { name: "cdn_url", value: nil } } + let!(:cdn_url) { { name: "cdn_url", value: "https://some-cdn.example.com" } } let(:target_site_name) { target_site_type == multisite ? "second" : "default" } let(:target_hostname) { target_site_type == multisite ? "test2.localhost" : "test.localhost" } @@ -127,7 +142,7 @@ RSpec.describe BackupRestore::UploadsRestorer do source_site_name: "foo", target_site_name: "default", from: "/uploads/foo/", - to: uploads_path("default") + to: uploads_path("default"), ) end end @@ -142,13 +157,16 @@ RSpec.describe BackupRestore::UploadsRestorer do post.link_post_uploads FileHelper.stubs(:download).returns(file_from_fixtures("logo.png")) - FileStore::S3Store.any_instance.stubs(:store_upload).returns do - File.join( - "//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com", - target_site_type == multisite ? "/uploads/#{target_site_name}" : "", - "original/1X/bc975735dfc6409c1c2aa5ebf2239949bcbdbd65.png" - ) - end + FileStore::S3Store + .any_instance + .stubs(:store_upload) + .returns do + File.join( + "//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com", + target_site_type == multisite ? "/uploads/#{target_site_name}" : "", + "original/1X/bc975735dfc6409c1c2aa5ebf2239949bcbdbd65.png", + ) + end UserAvatar.import_url_for_user("logo.png", Fabricate(:user)) end @@ -158,10 +176,11 @@ RSpec.describe BackupRestore::UploadsRestorer do with_temp_uploads_directory do |directory, path| store_class.any_instance.expects(:copy_from).with(path).once - expect { subject.restore(directory) } - .to change { OptimizedImage.count }.by_at_most(-1) - .and change { Jobs::CreateAvatarThumbnails.jobs.size }.by(1) - .and change { Post.where(baked_version: nil).count }.by(1) + expect { subject.restore(directory) }.to change { OptimizedImage.count }.by_at_most( + -1, + ).and change { Jobs::CreateAvatarThumbnails.jobs.size }.by(1).and change { + Post.where(baked_version: nil).count + }.by(1) end end @@ -171,10 +190,11 @@ RSpec.describe BackupRestore::UploadsRestorer do with_temp_uploads_directory(with_optimized: true) do |directory, path| store_class.any_instance.expects(:copy_from).with(path).once - expect { subject.restore(directory) } - .to not_change { OptimizedImage.count } - .and not_change { Jobs::CreateAvatarThumbnails.jobs.size } - .and change { Post.where(baked_version: nil).count }.by(1) + expect { subject.restore(directory) }.to not_change { + OptimizedImage.count + }.and not_change { Jobs::CreateAvatarThumbnails.jobs.size }.and change { + Post.where(baked_version: nil).count + }.by(1) end end end @@ -187,14 +207,14 @@ RSpec.describe BackupRestore::UploadsRestorer do target_site_name: target_site_name, metadata: [source_site_type, base_url], from: "https://test.localhost/forum", - to: "http://localhost" + to: "http://localhost", ) end it "doesn't remap when `cdn_url` in `backup_metadata` is empty" do expect_no_remap( target_site_name: target_site_name, - metadata: [source_site_type, no_cdn_url] + metadata: [source_site_type, no_cdn_url], ) end @@ -206,8 +226,8 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_site_type, cdn_url], remaps: [ { from: "https://some-cdn.example.com/", to: "https://new-cdn.example.com/" }, - { from: "some-cdn.example.com", to: "new-cdn.example.com" } - ] + { from: "some-cdn.example.com", to: "new-cdn.example.com" }, + ], ) end @@ -220,8 +240,8 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_site_type, cdn_url], remaps: [ { from: "https://some-cdn.example.com/", to: "//example.com/discourse/" }, - { from: "some-cdn.example.com", to: "example.com" } - ] + { from: "some-cdn.example.com", to: "example.com" }, + ], ) end end @@ -230,22 +250,20 @@ RSpec.describe BackupRestore::UploadsRestorer do it "doesn't remap when `s3_base_url` in `backup_metadata` is empty" do expect_no_remap( target_site_name: target_site_name, - metadata: [source_site_type, s3_base_url] + metadata: [source_site_type, s3_base_url], ) end it "doesn't remap when `s3_cdn_url` in `backup_metadata` is empty" do expect_no_remap( target_site_name: target_site_name, - metadata: [source_site_type, s3_cdn_url] + metadata: [source_site_type, s3_cdn_url], ) end end context "when currently stored locally" do - before do - SiteSetting.enable_s3_uploads = false - end + before { SiteSetting.enable_s3_uploads = false } let!(:store_class) { FileStore::LocalStore } @@ -297,7 +315,9 @@ RSpec.describe BackupRestore::UploadsRestorer do end context "with uploads previously stored on S3" do - let!(:s3_base_url) { { name: "s3_base_url", value: "//old-bucket.s3-us-east-1.amazonaws.com" } } + let!(:s3_base_url) do + { name: "s3_base_url", value: "//old-bucket.s3-us-east-1.amazonaws.com" } + end let!(:s3_cdn_url) { { name: "s3_cdn_url", value: "https://s3-cdn.example.com" } } shared_examples "regular site remaps from S3" do @@ -307,7 +327,7 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [no_multisite, s3_base_url], from: s3_url_regex("old-bucket", "/"), to: uploads_path(target_site_name), - regex: true + regex: true, ) end @@ -316,9 +336,12 @@ RSpec.describe BackupRestore::UploadsRestorer do target_site_name: target_site_name, metadata: [no_multisite, s3_cdn_url], remaps: [ - { from: "https://s3-cdn.example.com/", to: "//#{target_hostname}#{uploads_path(target_site_name)}" }, - { from: "s3-cdn.example.com", to: target_hostname } - ] + { + from: "https://s3-cdn.example.com/", + to: "//#{target_hostname}#{uploads_path(target_site_name)}", + }, + { from: "s3-cdn.example.com", to: target_hostname }, + ], ) end end @@ -330,7 +353,7 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_db_name, multisite, s3_base_url], from: s3_url_regex("old-bucket", "/"), to: "/", - regex: true + regex: true, ) end @@ -340,8 +363,8 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_db_name, multisite, s3_cdn_url], remaps: [ { from: "https://s3-cdn.example.com/", to: "//#{target_hostname}/" }, - { from: "s3-cdn.example.com", to: target_hostname } - ] + { from: "s3-cdn.example.com", to: target_hostname }, + ], ) end end @@ -386,9 +409,7 @@ RSpec.describe BackupRestore::UploadsRestorer do end context "when currently stored on S3" do - before do - setup_s3 - end + before { setup_s3 } let!(:store_class) { FileStore::S3Store } @@ -440,7 +461,9 @@ RSpec.describe BackupRestore::UploadsRestorer do end context "with uploads previously stored on S3" do - let!(:s3_base_url) { { name: "s3_base_url", value: "//old-bucket.s3-us-east-1.amazonaws.com" } } + let!(:s3_base_url) do + { name: "s3_base_url", value: "//old-bucket.s3-us-east-1.amazonaws.com" } + end let!(:s3_cdn_url) { { name: "s3_cdn_url", value: "https://s3-cdn.example.com" } } shared_examples "regular site remaps from S3" do @@ -450,20 +473,26 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [no_multisite, s3_base_url], from: s3_url_regex("old-bucket", "/"), to: uploads_path(target_site_name), - regex: true + regex: true, ) end it "remaps when `s3_cdn_url` changes" do - SiteSetting::Upload.expects(:s3_cdn_url).returns("https://new-s3-cdn.example.com").at_least_once + SiteSetting::Upload + .expects(:s3_cdn_url) + .returns("https://new-s3-cdn.example.com") + .at_least_once expect_remaps( target_site_name: target_site_name, metadata: [no_multisite, s3_cdn_url], remaps: [ - { from: "https://s3-cdn.example.com/", to: "https://new-s3-cdn.example.com#{uploads_path(target_site_name)}" }, - { from: "s3-cdn.example.com", to: "new-s3-cdn.example.com" } - ] + { + from: "https://s3-cdn.example.com/", + to: "https://new-s3-cdn.example.com#{uploads_path(target_site_name)}", + }, + { from: "s3-cdn.example.com", to: "new-s3-cdn.example.com" }, + ], ) end end @@ -475,21 +504,24 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_db_name, multisite, s3_base_url], from: s3_url_regex("old-bucket", "/"), to: "/", - regex: true + regex: true, ) end context "when `s3_cdn_url` is configured" do it "remaps when `s3_cdn_url` changes" do - SiteSetting::Upload.expects(:s3_cdn_url).returns("http://new-s3-cdn.example.com").at_least_once + SiteSetting::Upload + .expects(:s3_cdn_url) + .returns("http://new-s3-cdn.example.com") + .at_least_once expect_remaps( target_site_name: target_site_name, metadata: [source_db_name, multisite, s3_cdn_url], remaps: [ { from: "https://s3-cdn.example.com/", to: "//new-s3-cdn.example.com/" }, - { from: "s3-cdn.example.com", to: "new-s3-cdn.example.com" } - ] + { from: "s3-cdn.example.com", to: "new-s3-cdn.example.com" }, + ], ) end end @@ -503,8 +535,8 @@ RSpec.describe BackupRestore::UploadsRestorer do metadata: [source_db_name, multisite, s3_cdn_url], remaps: [ { from: "https://s3-cdn.example.com/", to: "//#{target_hostname}/" }, - { from: "s3-cdn.example.com", to: target_hostname } - ] + { from: "s3-cdn.example.com", to: target_hostname }, + ], ) end end @@ -593,7 +625,7 @@ RSpec.describe BackupRestore::UploadsRestorer do source_site_name: "xylan", target_site_name: "default", from: "/uploads/xylan/", - to: uploads_path("default") + to: uploads_path("default"), ) do |directory| FileUtils.mkdir_p(File.join(directory, "uploads", "PaxHeaders.27134")) FileUtils.mkdir_p(File.join(directory, "uploads", ".hidden")) diff --git a/spec/lib/bookmark_manager_spec.rb b/spec/lib/bookmark_manager_spec.rb index e362a06199..164cee1872 100644 --- a/spec/lib/bookmark_manager_spec.rb +++ b/spec/lib/bookmark_manager_spec.rb @@ -5,7 +5,7 @@ RSpec.describe BookmarkManager do let(:reminder_at) { 1.day.from_now } fab!(:post) { Fabricate(:post) } - let(:name) { 'Check this out!' } + let(:name) { "Check this out!" } subject { described_class.new(user) } @@ -40,7 +40,14 @@ RSpec.describe BookmarkManager do end describe ".update" do - let!(:bookmark) { Fabricate(:bookmark_next_business_day_reminder, user: user, bookmarkable: post, name: "Old name") } + let!(:bookmark) do + Fabricate( + :bookmark_next_business_day_reminder, + user: user, + bookmarkable: post, + name: "Old name", + ) + end let(:new_name) { "Some new name" } let(:new_reminder_at) { 10.days.from_now } let(:options) { {} } @@ -50,7 +57,7 @@ RSpec.describe BookmarkManager do bookmark_id: bookmark.id, name: new_name, reminder_at: new_reminder_at, - options: options + options: options, ) end @@ -69,7 +76,9 @@ RSpec.describe BookmarkManager do end context "when options are provided" do - let(:options) { { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } } + let(:options) do + { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } + end it "saves any additional options successfully" do update_bookmark @@ -86,9 +95,7 @@ RSpec.describe BookmarkManager do end context "if the bookmark no longer exists" do - before do - bookmark.destroy! - end + before { bookmark.destroy! } it "raises a not found error" do expect { update_bookmark }.to raise_error(Discourse::NotFound) end @@ -97,8 +104,12 @@ RSpec.describe BookmarkManager do describe ".destroy_for_topic" do let!(:topic) { Fabricate(:topic) } - let!(:bookmark1) { Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) } - let!(:bookmark2) { Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) } + let!(:bookmark1) do + Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) + end + let!(:bookmark2) do + Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: topic), user: user) + end it "destroys all bookmarks for the topic for the specified user" do subject.destroy_for_topic(topic) @@ -136,9 +147,7 @@ RSpec.describe BookmarkManager do end context "when the bookmark does no longer exist" do - before do - bookmark.destroy - end + before { bookmark.destroy } it "does not error, and does not create a notification" do described_class.send_reminder_notification(bookmark.id) expect(notifications_for_user.any?).to eq(false) @@ -146,9 +155,7 @@ RSpec.describe BookmarkManager do end context "if the post has been deleted" do - before do - bookmark.bookmarkable.trash! - end + before { bookmark.bookmarkable.trash! } it "does not error and does not create a notification" do described_class.send_reminder_notification(bookmark.id) bookmark.reload @@ -157,7 +164,10 @@ RSpec.describe BookmarkManager do end def notifications_for_user - Notification.where(notification_type: Notification.types[:bookmark_reminder], user_id: bookmark.user.id) + Notification.where( + notification_type: Notification.types[:bookmark_reminder], + user_id: bookmark.user.id, + ) end end @@ -179,14 +189,14 @@ RSpec.describe BookmarkManager do context "if the bookmark is belonging to some other user" do let!(:bookmark) { Fabricate(:bookmark, user: Fabricate(:admin)) } it "raises an invalid access error" do - expect { subject.toggle_pin(bookmark_id: bookmark.id) }.to raise_error(Discourse::InvalidAccess) + expect { subject.toggle_pin(bookmark_id: bookmark.id) }.to raise_error( + Discourse::InvalidAccess, + ) end end context "if the bookmark no longer exists" do - before do - bookmark.destroy! - end + before { bookmark.destroy! } it "raises a not found error" do expect { subject.toggle_pin(bookmark_id: bookmark.id) }.to raise_error(Discourse::NotFound) end @@ -220,11 +230,18 @@ RSpec.describe BookmarkManager do it "adds a validation error when the bookmarkable_type is not registered" do subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "BlahFactory", name: name) - expect(subject.errors.full_messages).to include(I18n.t("bookmarks.errors.invalid_bookmarkable", type: "BlahFactory")) + expect(subject.errors.full_messages).to include( + I18n.t("bookmarks.errors.invalid_bookmarkable", type: "BlahFactory"), + ) end it "updates the topic user bookmarked column to true if any post is bookmarked" do - subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + ) tu = TopicUser.find_by(user: user) expect(tu.bookmarked).to eq(true) tu.update(bookmarked: false) @@ -235,35 +252,63 @@ RSpec.describe BookmarkManager do end it "sets auto_delete_preference to clear_reminder by default" do - bookmark = subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) - expect(bookmark.auto_delete_preference).to eq(Bookmark.auto_delete_preferences[:clear_reminder]) - end - - context "when the user has set their bookmark_auto_delete_preference" do - before do - user.user_option.update!(bookmark_auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply]) - end - - it "sets auto_delete_preferences to the user's user_option.bookmark_auto_delete_preference" do - bookmark = subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) - expect(bookmark.auto_delete_preference).to eq(Bookmark.auto_delete_preferences[:on_owner_reply]) - end - - it "uses the passed in auto_delete_preference option instead of the user's one" do - bookmark = subject.create_for( + bookmark = + subject.create_for( bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at, - options: { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } ) - expect(bookmark.auto_delete_preference).to eq(Bookmark.auto_delete_preferences[:when_reminder_sent]) + expect(bookmark.auto_delete_preference).to eq( + Bookmark.auto_delete_preferences[:clear_reminder], + ) + end + + context "when the user has set their bookmark_auto_delete_preference" do + before do + user.user_option.update!( + bookmark_auto_delete_preference: Bookmark.auto_delete_preferences[:on_owner_reply], + ) + end + + it "sets auto_delete_preferences to the user's user_option.bookmark_auto_delete_preference" do + bookmark = + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + ) + expect(bookmark.auto_delete_preference).to eq( + Bookmark.auto_delete_preferences[:on_owner_reply], + ) + end + + it "uses the passed in auto_delete_preference option instead of the user's one" do + bookmark = + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + options: { + auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent], + }, + ) + expect(bookmark.auto_delete_preference).to eq( + Bookmark.auto_delete_preferences[:when_reminder_sent], + ) end end context "when a reminder time is provided" do it "saves the values correctly" do - subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + ) bookmark = Bookmark.find_by(user: user, bookmarkable: post) expect(bookmark.reminder_at).to eq_time(reminder_at) @@ -272,10 +317,18 @@ RSpec.describe BookmarkManager do end context "when options are provided" do - let(:options) { { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } } + let(:options) do + { auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent] } + end it "saves any additional options successfully" do - subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at, options: options) + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + options: options, + ) bookmark = Bookmark.find_by(user: user, bookmarkable: post) expect(bookmark.auto_delete_preference).to eq(1) @@ -283,20 +336,22 @@ RSpec.describe BookmarkManager do end context "when the bookmark already exists for the user & post" do - before do - Bookmark.create(bookmarkable: post, user: user) - end + before { Bookmark.create(bookmarkable: post, user: user) } it "adds an error to the manager" do subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post") - expect(subject.errors.full_messages).to include(I18n.t("bookmarks.errors.already_bookmarked", type: "Post")) + expect(subject.errors.full_messages).to include( + I18n.t("bookmarks.errors.already_bookmarked", type: "Post"), + ) end end context "when the bookmark name is too long" do it "adds an error to the manager" do subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: "test" * 100) - expect(subject.errors.full_messages).to include("Name is too long (maximum is 100 characters)") + expect(subject.errors.full_messages).to include( + "Name is too long (maximum is 100 characters)", + ) end end @@ -304,8 +359,15 @@ RSpec.describe BookmarkManager do let(:reminder_at) { 10.days.ago } it "adds an error to the manager" do - subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) - expect(subject.errors.full_messages).to include(I18n.t("bookmarks.errors.cannot_set_past_reminder")) + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + ) + expect(subject.errors.full_messages).to include( + I18n.t("bookmarks.errors.cannot_set_past_reminder"), + ) end end @@ -313,26 +375,33 @@ RSpec.describe BookmarkManager do let(:reminder_at) { 11.years.from_now } it "adds an error to the manager" do - subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name, reminder_at: reminder_at) - expect(subject.errors.full_messages).to include(I18n.t("bookmarks.errors.cannot_set_reminder_in_distant_future")) + subject.create_for( + bookmarkable_id: post.id, + bookmarkable_type: "Post", + name: name, + reminder_at: reminder_at, + ) + expect(subject.errors.full_messages).to include( + I18n.t("bookmarks.errors.cannot_set_reminder_in_distant_future"), + ) end end context "when the post is inaccessible for the user" do - before do - post.trash! - end + before { post.trash! } it "raises an invalid access error" do - expect { subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name) }.to raise_error(Discourse::InvalidAccess) + expect { + subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name) + }.to raise_error(Discourse::InvalidAccess) end end context "when the topic is inaccessible for the user" do - before do - post.topic.update(category: Fabricate(:private_category, group: Fabricate(:group))) - end + before { post.topic.update(category: Fabricate(:private_category, group: Fabricate(:group))) } it "raises an invalid access error" do - expect { subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name) }.to raise_error(Discourse::InvalidAccess) + expect { + subject.create_for(bookmarkable_id: post.id, bookmarkable_type: "Post", name: name) + }.to raise_error(Discourse::InvalidAccess) end end end diff --git a/spec/lib/bookmark_query_spec.rb b/spec/lib/bookmark_query_spec.rb index 69d218aa57..0017dfbf59 100644 --- a/spec/lib/bookmark_query_spec.rb +++ b/spec/lib/bookmark_query_spec.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true RSpec.describe BookmarkQuery do - before do - SearchIndexer.enable - end + before { SearchIndexer.enable } fab!(:user) { Fabricate(:user) } let(:params) { {} } @@ -24,12 +22,12 @@ RSpec.describe BookmarkQuery do let(:post_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post)) } let(:topic_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:topic)) } - let(:user_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user, username: "bookmarkqueen")) } - - after do - Bookmark.reset_bookmarkables + let(:user_bookmark) do + Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user, username: "bookmarkqueen")) end + after { Bookmark.reset_bookmarkables } + it "returns all the bookmarks for a user" do expect(bookmark_query.list_all.count).to eq(3) end @@ -42,16 +40,16 @@ RSpec.describe BookmarkQuery do it "runs the on_preload block provided passing in bookmarks" do preloaded_bookmarks = [] - BookmarkQuery.on_preload do |bookmarks, bq| - (preloaded_bookmarks << bookmarks).flatten - end + BookmarkQuery.on_preload { |bookmarks, bq| (preloaded_bookmarks << bookmarks).flatten } bookmark_query.list_all expect(preloaded_bookmarks.any?).to eq(true) end it "returns a mixture of post, topic, and custom bookmarkable type bookmarks" do bookmarks = bookmark_query.list_all - expect(bookmarks.map(&:id)).to match_array([post_bookmark.id, topic_bookmark.id, user_bookmark.id]) + expect(bookmarks.map(&:id)).to match_array( + [post_bookmark.id, topic_bookmark.id, user_bookmark.id], + ) end it "handles the user not having permission for all of the bookmarks of a certain bookmarkable" do @@ -61,7 +59,9 @@ RSpec.describe BookmarkQuery do end it "handles the user not having permission to see any of their bookmarks" do - topic_bookmark.bookmarkable.update(category: Fabricate(:private_category, group: Fabricate(:group))) + topic_bookmark.bookmarkable.update( + category: Fabricate(:private_category, group: Fabricate(:group)), + ) post_bookmark.bookmarkable.topic.update(category: topic_bookmark.bookmarkable.category) UserTestBookmarkable.expects(:list_query).returns(nil) bookmarks = bookmark_query.list_all @@ -69,17 +69,21 @@ RSpec.describe BookmarkQuery do end context "when q param is provided" do - let!(:post) { Fabricate(:post, raw: "Some post content here", topic: Fabricate(:topic, title: "Bugfix game for devs")) } - - before do - Bookmark.reset_bookmarkables + let!(:post) do + Fabricate( + :post, + raw: "Some post content here", + topic: Fabricate(:topic, title: "Bugfix game for devs"), + ) end - after do - Bookmark.reset_bookmarkables - end + before { Bookmark.reset_bookmarkables } - let(:bookmark3) { Fabricate(:bookmark, user: user, name: "Check up later", bookmarkable: Fabricate(:post)) } + after { Bookmark.reset_bookmarkables } + + let(:bookmark3) do + Fabricate(:bookmark, user: user, name: "Check up later", bookmarkable: Fabricate(:post)) + end let(:bookmark4) { Fabricate(:bookmark, user: user, bookmarkable: post) } before do @@ -88,29 +92,29 @@ RSpec.describe BookmarkQuery do end it "can search by bookmark name" do - bookmarks = bookmark_query(params: { q: 'check' }).list_all + bookmarks = bookmark_query(params: { q: "check" }).list_all expect(bookmarks.map(&:id)).to eq([bookmark3.id]) end it "can search by post content" do - bookmarks = bookmark_query(params: { q: 'content' }).list_all + bookmarks = bookmark_query(params: { q: "content" }).list_all expect(bookmarks.map(&:id)).to eq([bookmark4.id]) end it "can search by topic title" do - bookmarks = bookmark_query(params: { q: 'bugfix' }).list_all + bookmarks = bookmark_query(params: { q: "bugfix" }).list_all expect(bookmarks.map(&:id)).to eq([bookmark4.id]) end context "with custom bookmarkable fitering" do - before do - register_test_bookmarkable + before { register_test_bookmarkable } + + let!(:bookmark5) do + Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user, username: "bookmarkking")) end - let!(:bookmark5) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user, username: "bookmarkking")) } - it "allows searching bookmarkables by fields in other tables" do - bookmarks = bookmark_query(params: { q: 'bookmarkk' }).list_all + bookmarks = bookmark_query(params: { q: "bookmarkk" }).list_all expect(bookmarks.map(&:id)).to eq([bookmark5.id]) end end @@ -157,9 +161,7 @@ RSpec.describe BookmarkQuery do end context "when the user is a topic_allowed_user" do - before do - TopicAllowedUser.create(topic: pm_topic, user: user) - end + before { TopicAllowedUser.create(topic: pm_topic, user: user) } it "shows the user the bookmark in the PM" do expect(bookmark_query.list_all.map(&:id).count).to eq(3) end @@ -192,7 +194,9 @@ RSpec.describe BookmarkQuery do context "when the topic category is private" do let(:group) { Fabricate(:group) } before do - post_bookmark.bookmarkable.topic.update(category: Fabricate(:private_category, group: group)) + post_bookmark.bookmarkable.topic.update( + category: Fabricate(:private_category, group: group), + ) post_bookmark.reload end it "does not show the user a post/topic in a private category they cannot see" do @@ -227,26 +231,18 @@ RSpec.describe BookmarkQuery do end it "order defaults to updated_at DESC" do - expect(bookmark_query.list_all.map(&:id)).to eq([ - bookmark1.id, - bookmark2.id, - bookmark5.id, - bookmark4.id, - bookmark3.id - ]) + expect(bookmark_query.list_all.map(&:id)).to eq( + [bookmark1.id, bookmark2.id, bookmark5.id, bookmark4.id, bookmark3.id], + ) end it "orders by reminder_at, then updated_at" do bookmark4.update_column(:reminder_at, 1.day.from_now) bookmark5.update_column(:reminder_at, 26.hours.from_now) - expect(bookmark_query.list_all.map(&:id)).to eq([ - bookmark4.id, - bookmark5.id, - bookmark1.id, - bookmark2.id, - bookmark3.id - ]) + expect(bookmark_query.list_all.map(&:id)).to eq( + [bookmark4.id, bookmark5.id, bookmark1.id, bookmark2.id, bookmark3.id], + ) end it "shows pinned bookmarks first ordered by reminder_at ASC then updated_at DESC" do @@ -261,13 +257,9 @@ RSpec.describe BookmarkQuery do bookmark5.update_column(:reminder_at, 1.day.from_now) - expect(bookmark_query.list_all.map(&:id)).to eq([ - bookmark3.id, - bookmark4.id, - bookmark1.id, - bookmark2.id, - bookmark5.id - ]) + expect(bookmark_query.list_all.map(&:id)).to eq( + [bookmark3.id, bookmark4.id, bookmark1.id, bookmark2.id, bookmark5.id], + ) end end end diff --git a/spec/lib/bookmark_reminder_notification_handler_spec.rb b/spec/lib/bookmark_reminder_notification_handler_spec.rb index cf660394ce..9a77ad9bb1 100644 --- a/spec/lib/bookmark_reminder_notification_handler_spec.rb +++ b/spec/lib/bookmark_reminder_notification_handler_spec.rb @@ -5,9 +5,7 @@ RSpec.describe BookmarkReminderNotificationHandler do fab!(:user) { Fabricate(:user) } - before do - Discourse.redis.flushdb - end + before { Discourse.redis.flushdb } describe "#send_notification" do let!(:bookmark) do @@ -42,7 +40,9 @@ RSpec.describe BookmarkReminderNotificationHandler do context "when the auto_delete_preference is when_reminder_sent" do before do TopicUser.create!(topic: bookmark.bookmarkable.topic, user: user, bookmarked: true) - bookmark.update(auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent]) + bookmark.update( + auto_delete_preference: Bookmark.auto_delete_preferences[:when_reminder_sent], + ) end it "deletes the bookmark after the reminder gets sent" do @@ -52,17 +52,25 @@ RSpec.describe BookmarkReminderNotificationHandler do it "changes the TopicUser bookmarked column to false" do subject.new(bookmark).send_notification - expect(TopicUser.find_by(topic: bookmark.bookmarkable.topic, user: user).bookmarked).to eq(false) + expect(TopicUser.find_by(topic: bookmark.bookmarkable.topic, user: user).bookmarked).to eq( + false, + ) end context "if there are still other bookmarks in the topic" do before do - Fabricate(:bookmark, bookmarkable: Fabricate(:post, topic: bookmark.bookmarkable.topic), user: user) + Fabricate( + :bookmark, + bookmarkable: Fabricate(:post, topic: bookmark.bookmarkable.topic), + user: user, + ) end it "does not change the TopicUser bookmarked column to false" do subject.new(bookmark).send_notification - expect(TopicUser.find_by(topic: bookmark.bookmarkable.topic, user: user).bookmarked).to eq(true) + expect( + TopicUser.find_by(topic: bookmark.bookmarkable.topic, user: user).bookmarked, + ).to eq(true) end end end diff --git a/spec/lib/browser_detection_spec.rb b/spec/lib/browser_detection_spec.rb index d0820bc511..0232020c7c 100644 --- a/spec/lib/browser_detection_spec.rb +++ b/spec/lib/browser_detection_spec.rb @@ -1,42 +1,150 @@ # frozen_string_literal: true -require 'browser_detection' +require "browser_detection" RSpec.describe BrowserDetection do - it "detects browser, device and operating system" do [ ["Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1)", :ie, :windows, :windows], - ["Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", :ie, :windows, :windows], - ["Mozilla/5.0 (iPad; CPU OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1", :safari, :ipad, :ios], - ["Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", :safari, :iphone, :ios], - ["Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/51.0.2704.104 Mobile/13F69 Safari/601.1.46", :chrome, :iphone, :ios], - ["Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19", :chrome, :android, :android], - ["Mozilla/5.0 (Linux; Android 4.4.2; XMP-6250 Build/HAWK) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Safari/537.36 ADAPI/2.0 (UUID:9e7df0ed-2a5c-4a19-bec7-2cc54800f99d) RK3188-ADAPI/1.2.84.533 (MODEL:XMP-6250)", :chrome, :android, :android], - ["Mozilla/5.0 (Linux; Android 5.1; Nexus 7 Build/LMY47O) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.105 Safari/537.36", :chrome, :android, :android], - ["Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", :chrome, :android, :android], - ["Mozilla/5.0 (Linux; Android; 4.1.2; GT-I9100 Build/000000) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1234.12 Mobile Safari/537.22 OPR/14.0.123.123", :opera, :android, :android], - ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", :firefox, :mac, :macos], - ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", :firefox, :mac, :macos], - ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", :chrome, :mac, :macos], - ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", :chrome, :windows, :windows], - ["Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1", :firefox, :windows, :windows], + [ + "Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)", + :ie, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (iPad; CPU OS 9_3_2 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13F69 Safari/601.1", + :safari, + :ipad, + :ios, + ], + [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1", + :safari, + :iphone, + :ios, + ], + [ + "Mozilla/5.0 (iPhone; CPU iPhone OS 9_3_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/51.0.2704.104 Mobile/13F69 Safari/601.1.46", + :chrome, + :iphone, + :ios, + ], + [ + "Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19", + :chrome, + :android, + :android, + ], + [ + "Mozilla/5.0 (Linux; Android 4.4.2; XMP-6250 Build/HAWK) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/30.0.0.0 Safari/537.36 ADAPI/2.0 (UUID:9e7df0ed-2a5c-4a19-bec7-2cc54800f99d) RK3188-ADAPI/1.2.84.533 (MODEL:XMP-6250)", + :chrome, + :android, + :android, + ], + [ + "Mozilla/5.0 (Linux; Android 5.1; Nexus 7 Build/LMY47O) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.105 Safari/537.36", + :chrome, + :android, + :android, + ], + [ + "Mozilla/5.0 (Linux; Android 6.0.1; vivo 1603 Build/MMB29M) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.83 Mobile Safari/537.36", + :chrome, + :android, + :android, + ], + [ + "Mozilla/5.0 (Linux; Android; 4.1.2; GT-I9100 Build/000000) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1234.12 Mobile Safari/537.22 OPR/14.0.123.123", + :opera, + :android, + :android, + ], + [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", + :firefox, + :mac, + :macos, + ], + [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.12; rv:54.0) Gecko/20100101 Firefox/54.0", + :firefox, + :mac, + :macos, + ], + [ + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36", + :chrome, + :mac, + :macos, + ], + [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36", + :chrome, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (Windows NT 5.1; rv:7.0.1) Gecko/20100101 Firefox/7.0.1", + :firefox, + :windows, + :windows, + ], ["Mozilla/5.0 (Windows NT 6.1; Trident/7.0; rv:11.0) like Gecko", :ie, :windows, :windows], - ["Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", :chrome, :windows, :windows], - ["Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", :firefox, :windows, :windows], - ["Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0", :firefox, :windows, :windows], - ["Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", :chrome, :linux, :linux], - ["Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", :firefox, :linux, :linux], + [ + "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36", + :chrome, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1", + :firefox, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0", + :firefox, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36", + :chrome, + :linux, + :linux, + ], + [ + "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20150101 Firefox/47.0 (Chrome)", + :firefox, + :linux, + :linux, + ], ["Opera/9.80 (X11; Linux zvav; U; en) Presto/2.12.423 Version/12.16", :opera, :linux, :linux], - ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", :edge, :windows, :windows], - ["Mozilla/5.0 (X11; CrOS x86_64 11895.95.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.125 Safari/537.36 ", :chrome, :chromebook, :chromeos], - ["Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edg/75.10240", :edge, :windows, :windows], - ["Discourse/163 CFNetwork/978.0.7 Darwin/18.6.0", :discoursehub, :unknown, :ios] + [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246", + :edge, + :windows, + :windows, + ], + [ + "Mozilla/5.0 (X11; CrOS x86_64 11895.95.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.125 Safari/537.36 ", + :chrome, + :chromebook, + :chromeos, + ], + [ + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edg/75.10240", + :edge, + :windows, + :windows, + ], + ["Discourse/163 CFNetwork/978.0.7 Darwin/18.6.0", :discoursehub, :unknown, :ios], ].each do |user_agent, browser, device, os| expect(BrowserDetection.browser(user_agent)).to eq(browser) expect(BrowserDetection.device(user_agent)).to eq(device) expect(BrowserDetection.os(user_agent)).to eq(os) end end - end diff --git a/spec/lib/cache_spec.rb b/spec/lib/cache_spec.rb index 19d79d81ba..ae09bcc39b 100644 --- a/spec/lib/cache_spec.rb +++ b/spec/lib/cache_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'cache' +require "cache" RSpec.describe Cache do - let :cache do Cache.new end @@ -43,9 +42,7 @@ RSpec.describe Cache do it "can delete correctly" do cache.delete("key") - cache.fetch("key", expires_in: 1.minute) do - "test" - end + cache.fetch("key", expires_in: 1.minute) { "test" } expect(cache.fetch("key")).to eq("test") @@ -59,9 +56,7 @@ RSpec.describe Cache do key = cache.normalize_key("key") - cache.fetch("key", expires_in: 1.minute) do - "bob" - end + cache.fetch("key", expires_in: 1.minute) { "bob" } expect(Discourse.redis.ttl(key)).to be_within(2.seconds).of(1.minute) @@ -75,9 +70,10 @@ RSpec.describe Cache do it "can store and fetch correctly" do cache.delete "key" - r = cache.fetch "key" do - "bob" - end + r = + cache.fetch "key" do + "bob" + end expect(r).to eq("bob") end @@ -85,9 +81,10 @@ RSpec.describe Cache do it "can fetch existing correctly" do cache.write "key", "bill" - r = cache.fetch "key" do - "bob" - end + r = + cache.fetch "key" do + "bob" + end expect(r).to eq("bill") end diff --git a/spec/lib/category_badge_spec.rb b/spec/lib/category_badge_spec.rb index c6d04cead2..3d36d1af03 100644 --- a/spec/lib/category_badge_spec.rb +++ b/spec/lib/category_badge_spec.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true -require 'category_badge' +require "category_badge" RSpec.describe CategoryBadge do it "escapes HTML in category names / descriptions" do - c = Fabricate(:category, name: 'name', description: 'title') + c = Fabricate(:category, name: "name", description: "title") html = CategoryBadge.html_for(c) diff --git a/spec/lib/category_guardian_spec.rb b/spec/lib/category_guardian_spec.rb index a2679c81d3..0cbd94cea1 100644 --- a/spec/lib/category_guardian_spec.rb +++ b/spec/lib/category_guardian_spec.rb @@ -22,7 +22,13 @@ RSpec.describe CategoryGuardian do context "when restricted category" do fab!(:group) { Fabricate(:group) } - fab!(:category) { Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:readonly]) } + fab!(:category) do + Fabricate( + :private_category, + group: group, + permission_type: CategoryGroup.permission_types[:readonly], + ) + end fab!(:group_user) { Fabricate(:group_user, group: group, user: user) } it "returns false for anonymous user" do @@ -38,12 +44,22 @@ RSpec.describe CategoryGuardian do end it "returns true for member of group with create_post access" do - category = Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:create_post]) + category = + Fabricate( + :private_category, + group: group, + permission_type: CategoryGroup.permission_types[:create_post], + ) expect(Guardian.new(user).can_post_in_category?(category)).to eq(true) end it "returns true for member of group with full access" do - category = Fabricate(:private_category, group: group, permission_type: CategoryGroup.permission_types[:full]) + category = + Fabricate( + :private_category, + group: group, + permission_type: CategoryGroup.permission_types[:full], + ) expect(Guardian.new(user).can_post_in_category?(category)).to eq(true) end end diff --git a/spec/lib/common_passwords/common_passwords_spec.rb b/spec/lib/common_passwords/common_passwords_spec.rb index fba5e6804f..7373882a20 100644 --- a/spec/lib/common_passwords/common_passwords_spec.rb +++ b/spec/lib/common_passwords/common_passwords_spec.rb @@ -12,7 +12,7 @@ RSpec.describe CommonPasswords do it "returns false if password isn't in the common passwords list" do described_class.stubs(:password_list).returns(stub_everything(include?: false)) - @password = 'uncommonPassword' + @password = "uncommonPassword" expect(subject).to eq(false) end @@ -35,19 +35,19 @@ RSpec.describe CommonPasswords do end end - describe '#password_list' do + describe "#password_list" do before { Discourse.redis.flushdb } after { Discourse.redis.flushdb } it "loads the passwords file if redis doesn't have it" do Discourse.redis.without_namespace.stubs(:scard).returns(0) - described_class.expects(:load_passwords).returns(['password']) + described_class.expects(:load_passwords).returns(["password"]) list = described_class.password_list expect(list).to respond_to(:include?) end it "doesn't load the passwords file if redis has it" do - Discourse.redis.without_namespace.stubs(:scard).returns(10000) + Discourse.redis.without_namespace.stubs(:scard).returns(10_000) described_class.expects(:load_passwords).never list = described_class.password_list expect(list).to respond_to(:include?) @@ -55,7 +55,7 @@ RSpec.describe CommonPasswords do it "loads the passwords file if redis has an empty list" do Discourse.redis.without_namespace.stubs(:scard).returns(0) - described_class.expects(:load_passwords).returns(['password']) + described_class.expects(:load_passwords).returns(["password"]) list = described_class.password_list expect(list).to respond_to(:include?) end diff --git a/spec/lib/composer_messages_finder_spec.rb b/spec/lib/composer_messages_finder_spec.rb index 7ff75f30f3..1d7ad381a8 100644 --- a/spec/lib/composer_messages_finder_spec.rb +++ b/spec/lib/composer_messages_finder_spec.rb @@ -1,12 +1,12 @@ # encoding: utf-8 # frozen_string_literal: true -require 'composer_messages_finder' +require "composer_messages_finder" RSpec.describe ComposerMessagesFinder do describe "delegates work" do let(:user) { Fabricate.build(:user) } - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic') } + let(:finder) { ComposerMessagesFinder.new(user, composer_action: "createTopic") } it "calls all the message finders" do finder.expects(:check_education_message).once @@ -20,15 +20,13 @@ RSpec.describe ComposerMessagesFinder do end end - describe '.check_education_message' do + describe ".check_education_message" do let(:user) { Fabricate.build(:user) } - context 'when creating topic' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic') } + context "when creating topic" do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: "createTopic") } - before do - SiteSetting.educate_until_posts = 10 - end + before { SiteSetting.educate_until_posts = 10 } it "returns a message for a user who has not posted any topics" do user.expects(:created_topic_count).returns(9) @@ -41,32 +39,34 @@ RSpec.describe ComposerMessagesFinder do end end - context 'with private message' do + context "with private message" do fab!(:topic) { Fabricate(:private_message_topic) } - context 'when starting a new private message' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic', topic_id: topic.id) } + context "when starting a new private message" do + let(:finder) do + ComposerMessagesFinder.new(user, composer_action: "createTopic", topic_id: topic.id) + end - it 'should return an empty string' do + it "should return an empty string" do expect(finder.check_education_message).to eq(nil) end end - context 'when replying to a private message' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id) } + context "when replying to a private message" do + let(:finder) do + ComposerMessagesFinder.new(user, composer_action: "reply", topic_id: topic.id) + end - it 'should return an empty string' do + it "should return an empty string" do expect(finder.check_education_message).to eq(nil) end end end - context 'when creating reply' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply') } + context "when creating reply" do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: "reply") } - before do - SiteSetting.educate_until_posts = 10 - end + before { SiteSetting.educate_until_posts = 10 } it "returns a message for a user who has not posted any topics" do user.expects(:post_count).returns(9) @@ -80,11 +80,11 @@ RSpec.describe ComposerMessagesFinder do end end - describe '.check_new_user_many_replies' do + describe ".check_new_user_many_replies" do let(:user) { Fabricate.build(:user) } - context 'when replying' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply') } + context "when replying" do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: "reply") } it "has no message when `posted_too_much_in_topic?` is false" do user.expects(:posted_too_much_in_topic?).returns(false) @@ -96,11 +96,10 @@ RSpec.describe ComposerMessagesFinder do expect(finder.check_new_user_many_replies).to be_present end end - end - describe '.check_avatar_notification' do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic') } + describe ".check_avatar_notification" do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: "createTopic") } fab!(:user) { Fabricate(:user) } context "with success" do @@ -126,7 +125,10 @@ RSpec.describe ComposerMessagesFinder do end it "doesn't notify users who have been notified already" do - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_avatar], + target_user_id: user.id, + ) expect(finder.check_avatar_notification).to be_blank end @@ -141,12 +143,12 @@ RSpec.describe ComposerMessagesFinder do end it "doesn't notify users if 'allow_uploaded_avatars' setting is disabled" do - SiteSetting.allow_uploaded_avatars = 'disabled' + SiteSetting.allow_uploaded_avatars = "disabled" expect(finder.check_avatar_notification).to be_blank end end - describe '.check_sequential_replies' do + describe ".check_sequential_replies" do fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic) } @@ -164,16 +166,20 @@ RSpec.describe ComposerMessagesFinder do end it "does not give a message for new topics" do - finder = ComposerMessagesFinder.new(user, composer_action: 'createTopic') + finder = ComposerMessagesFinder.new(user, composer_action: "createTopic") expect(finder.check_sequential_replies).to be_blank end it "does not give a message without a topic id" do - expect(ComposerMessagesFinder.new(user, composer_action: 'reply').check_sequential_replies).to be_blank + expect( + ComposerMessagesFinder.new(user, composer_action: "reply").check_sequential_replies, + ).to be_blank end context "with reply" do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id) } + let(:finder) do + ComposerMessagesFinder.new(user, composer_action: "reply", topic_id: topic.id) + end it "does not give a message to users who are still in the 'education' phase" do user.stubs(:post_count).returns(9) @@ -181,12 +187,20 @@ RSpec.describe ComposerMessagesFinder do end it "doesn't notify a user it has already notified about sequential replies" do - UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], target_user_id: user.id, topic_id: topic.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_sequential_replies], + target_user_id: user.id, + topic_id: topic.id, + ) expect(finder.check_sequential_replies).to be_blank end it "will notify you if it hasn't in the current topic" do - UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], target_user_id: user.id, topic_id: topic.id + 1) + UserHistory.create!( + action: UserHistory.actions[:notified_about_sequential_replies], + target_user_id: user.id, + topic_id: topic.id + 1, + ) expect(finder.check_sequential_replies).to be_present end @@ -215,13 +229,11 @@ RSpec.describe ComposerMessagesFinder do it "creates a notified_about_sequential_replies log" do expect(UserHistory.exists_for_user?(user, :notified_about_sequential_replies)).to eq(true) end - end end - end - describe '.check_dominating_topic' do + describe ".check_dominating_topic" do fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic) } @@ -239,16 +251,20 @@ RSpec.describe ComposerMessagesFinder do end it "does not give a message for new topics" do - finder = ComposerMessagesFinder.new(user, composer_action: 'createTopic') + finder = ComposerMessagesFinder.new(user, composer_action: "createTopic") expect(finder.check_dominating_topic).to be_blank end it "does not give a message without a topic id" do - expect(ComposerMessagesFinder.new(user, composer_action: 'reply').check_dominating_topic).to be_blank + expect( + ComposerMessagesFinder.new(user, composer_action: "reply").check_dominating_topic, + ).to be_blank end context "with reply" do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id) } + let(:finder) do + ComposerMessagesFinder.new(user, composer_action: "reply", topic_id: topic.id) + end it "does not give a message to users who are still in the 'education' phase" do user.stubs(:post_count).returns(9) @@ -261,12 +277,20 @@ RSpec.describe ComposerMessagesFinder do end it "doesn't notify a user it has already notified in this topic" do - UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic], topic_id: topic.id, target_user_id: user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_dominating_topic], + topic_id: topic.id, + target_user_id: user.id, + ) expect(finder.check_dominating_topic).to be_blank end it "notifies a user if the topic is different" do - UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic], topic_id: topic.id + 1, target_user_id: user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_dominating_topic], + topic_id: topic.id + 1, + target_user_id: user.id, + ) expect(finder.check_dominating_topic).to be_present end @@ -300,13 +324,11 @@ RSpec.describe ComposerMessagesFinder do it "creates a notified_about_dominating_topic log" do expect(UserHistory.exists_for_user?(user, :notified_about_dominating_topic)).to eq(true) end - end end - end - describe '.check_get_a_room' do + describe ".check_get_a_room" do fab!(:user) { Fabricate(:user) } fab!(:other_user) { Fabricate(:user) } fab!(:third_user) { Fabricate(:user) } @@ -317,13 +339,9 @@ RSpec.describe ComposerMessagesFinder do Fabricate(:post, topic: topic, user: third_user, reply_to_user_id: op.user_id) end - fab!(:first_reply) do - Fabricate(:post, topic: topic, user: user, reply_to_user_id: op.user_id) - end + fab!(:first_reply) { Fabricate(:post, topic: topic, user: user, reply_to_user_id: op.user_id) } - fab!(:second_reply) do - Fabricate(:post, topic: topic, user: user, reply_to_user_id: op.user_id) - end + fab!(:second_reply) { Fabricate(:post, topic: topic, user: user, reply_to_user_id: op.user_id) } before do SiteSetting.educate_until_posts = 10 @@ -332,23 +350,40 @@ RSpec.describe ComposerMessagesFinder do end it "does not show the message for new topics" do - finder = ComposerMessagesFinder.new(user, composer_action: 'createTopic') + finder = ComposerMessagesFinder.new(user, composer_action: "createTopic") expect(finder.check_get_a_room(min_users_posted: 2)).to be_blank end it "does not give a message without a topic id" do - expect(ComposerMessagesFinder.new(user, composer_action: 'reply').check_get_a_room(min_users_posted: 2)).to be_blank + expect( + ComposerMessagesFinder.new(user, composer_action: "reply").check_get_a_room( + min_users_posted: 2, + ), + ).to be_blank end it "does not give a message if the topic's category is read_restricted" do topic.category.update(read_restricted: true) - finder = ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id, post_id: op.id) + finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: op.id, + ) finder.check_get_a_room(min_users_posted: 2) expect(UserHistory.exists_for_user?(user, :notified_about_get_a_room)).to eq(false) end context "with reply" do - let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id, post_id: op.id) } + let(:finder) do + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: op.id, + ) + end it "does not give a message to users who are still in the 'education' phase" do user.stubs(:post_count).returns(9) @@ -359,7 +394,7 @@ RSpec.describe ComposerMessagesFinder do UserHistory.create!( action: UserHistory.actions[:notified_about_get_a_room], target_user_id: user.id, - topic_id: topic.id + topic_id: topic.id, ) expect(finder.check_get_a_room(min_users_posted: 2)).to be_blank end @@ -368,7 +403,7 @@ RSpec.describe ComposerMessagesFinder do UserHistory.create!( action: UserHistory.actions[:notified_about_get_a_room], target_user_id: user.id, - topic_id: topic.id + 1 + topic_id: topic.id + 1, ) expect(finder.check_get_a_room(min_users_posted: 2)).to be_present end @@ -390,17 +425,18 @@ RSpec.describe ComposerMessagesFinder do end it "doesn't notify in a message" do - topic.update_columns(category_id: nil, archetype: 'private_message') + topic.update_columns(category_id: nil, archetype: "private_message") expect(finder.check_get_a_room(min_users_posted: 2)).to be_blank end it "doesn't notify when replying to a different user" do - other_finder = ComposerMessagesFinder.new( - user, - composer_action: 'reply', - topic_id: topic.id, - post_id: other_user_reply.id - ) + other_finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: other_user_reply.id, + ) expect(other_finder.check_get_a_room(min_users_posted: 2)).to be_blank end @@ -418,79 +454,94 @@ RSpec.describe ComposerMessagesFinder do it "works as expected" do expect(message).to be_present - expect(message[:id]).to eq('get_a_room') + expect(message[:id]).to eq("get_a_room") expect(message[:wait_for_typing]).to eq(true) - expect(message[:templateName]).to eq('get-a-room') + expect(message[:templateName]).to eq("get-a-room") expect(UserHistory.exists_for_user?(user, :notified_about_get_a_room)).to eq(true) end end end - end - describe '.check_reviving_old_topic' do - fab!(:user) { Fabricate(:user) } + describe ".check_reviving_old_topic" do + fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:topic) } it "does not give a message without a topic id" do - expect(described_class.new(user, composer_action: 'createTopic').check_reviving_old_topic).to be_blank - expect(described_class.new(user, composer_action: 'reply').check_reviving_old_topic).to be_blank + expect( + described_class.new(user, composer_action: "createTopic").check_reviving_old_topic, + ).to be_blank + expect( + described_class.new(user, composer_action: "reply").check_reviving_old_topic, + ).to be_blank end context "with a reply" do context "when warn_reviving_old_topic_age is 180 days" do - before do - SiteSetting.warn_reviving_old_topic_age = 180 - end + before { SiteSetting.warn_reviving_old_topic_age = 180 } it "does not notify if last post is recent" do topic = Fabricate(:topic, last_posted_at: 1.hour.ago) - expect(described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_reviving_old_topic).to be_blank + expect( + described_class.new( + user, + composer_action: "reply", + topic_id: topic.id, + ).check_reviving_old_topic, + ).to be_blank end it "notifies if last post is old" do topic = Fabricate(:topic, last_posted_at: 181.days.ago) - message = described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_reviving_old_topic + message = + described_class.new( + user, + composer_action: "reply", + topic_id: topic.id, + ).check_reviving_old_topic expect(message).not_to be_blank expect(message[:body]).to match(/6 months ago/) end end context "when warn_reviving_old_topic_age is 0" do - before do - SiteSetting.warn_reviving_old_topic_age = 0 - end + before { SiteSetting.warn_reviving_old_topic_age = 0 } it "does not notify if last post is new" do topic = Fabricate(:topic, last_posted_at: 1.hour.ago) - expect(described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_reviving_old_topic).to be_blank + expect( + described_class.new( + user, + composer_action: "reply", + topic_id: topic.id, + ).check_reviving_old_topic, + ).to be_blank end it "does not notify if last post is old" do topic = Fabricate(:topic, last_posted_at: 365.days.ago) - expect(described_class.new(user, composer_action: 'reply', topic_id: topic.id).check_reviving_old_topic).to be_blank + expect( + described_class.new( + user, + composer_action: "reply", + topic_id: topic.id, + ).check_reviving_old_topic, + ).to be_blank end end end end - context 'when editing a post' do + context "when editing a post" do fab!(:user) { Fabricate(:user) } fab!(:topic) { Fabricate(:post).topic } let!(:post) do - PostCreator.create!( - user, - topic_id: topic.id, - post_number: 1, - raw: 'omg my first post' - ) + PostCreator.create!(user, topic_id: topic.id, post_number: 1, raw: "omg my first post") end - let(:edit_post_finder) do - ComposerMessagesFinder.new(user, composer_action: 'edit') - end + let(:edit_post_finder) { ComposerMessagesFinder.new(user, composer_action: "edit") } before do SiteSetting.disable_avatar_education_message = true @@ -502,25 +553,28 @@ RSpec.describe ComposerMessagesFinder do end end - describe '#user_not_seen_in_a_while' do + describe "#user_not_seen_in_a_while" do fab!(:user_1) { Fabricate(:user, last_seen_at: 3.years.ago) } fab!(:user_2) { Fabricate(:user, last_seen_at: 2.years.ago) } fab!(:user_3) { Fabricate(:user, last_seen_at: 6.months.ago) } - before do - SiteSetting.pm_warn_user_last_seen_months_ago = 24 - end + before { SiteSetting.pm_warn_user_last_seen_months_ago = 24 } - it 'returns users that have not been seen recently' do - users = ComposerMessagesFinder.user_not_seen_in_a_while([user_1.username, user_2.username, user_3.username]) + it "returns users that have not been seen recently" do + users = + ComposerMessagesFinder.user_not_seen_in_a_while( + [user_1.username, user_2.username, user_3.username], + ) expect(users).to contain_exactly(user_1.username, user_2.username) end - it 'accounts for pm_warn_user_last_seen_months_ago site setting' do + it "accounts for pm_warn_user_last_seen_months_ago site setting" do SiteSetting.pm_warn_user_last_seen_months_ago = 30 - users = ComposerMessagesFinder.user_not_seen_in_a_while([user_1.username, user_2.username, user_3.username]) + users = + ComposerMessagesFinder.user_not_seen_in_a_while( + [user_1.username, user_2.username, user_3.username], + ) expect(users).to contain_exactly(user_1.username) end end - end diff --git a/spec/lib/compression/engine_spec.rb b/spec/lib/compression/engine_spec.rb index 6ca9a969ed..87cbed2488 100644 --- a/spec/lib/compression/engine_spec.rb +++ b/spec/lib/compression/engine_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Compression::Engine do let(:available_size) { SiteSetting.decompressed_theme_max_file_size_mb } - let(:folder_name) { 'test' } + let(:folder_name) { "test" } let(:temp_folder) do path = "#{Pathname.new(Dir.tmpdir).realpath}/#{SecureRandom.hex}" FileUtils.mkdir(path) @@ -12,30 +12,36 @@ RSpec.describe Compression::Engine do before do Dir.chdir(temp_folder) do FileUtils.mkdir_p("#{folder_name}/a") - File.write("#{folder_name}/hello.txt", 'hello world') - File.write("#{folder_name}/a/inner", 'hello world inner') + File.write("#{folder_name}/hello.txt", "hello world") + File.write("#{folder_name}/a/inner", "hello world inner") end end after { FileUtils.rm_rf(temp_folder) } - it 'raises an exception when the file is not supported' do - unknown_extension = 'a_file.crazyext' - expect { described_class.engine_for(unknown_extension) }.to raise_error Compression::Engine::UnsupportedFileExtension + it "raises an exception when the file is not supported" do + unknown_extension = "a_file.crazyext" + expect { + described_class.engine_for(unknown_extension) + }.to raise_error Compression::Engine::UnsupportedFileExtension end - describe 'compressing and decompressing files' do + describe "compressing and decompressing files" do before do Dir.chdir(temp_folder) do - @compressed_path = Compression::Engine.engine_for("#{folder_name}#{extension}").compress(temp_folder, folder_name) + @compressed_path = + Compression::Engine.engine_for("#{folder_name}#{extension}").compress( + temp_folder, + folder_name, + ) FileUtils.rm_rf("#{folder_name}/") end end - context 'when working with zip files' do - let(:extension) { '.zip' } + context "when working with zip files" do + let(:extension) { ".zip" } - it 'decompresses the folder and inspects files correctly' do + it "decompresses the folder and inspects files correctly" do engine = described_class.engine_for(@compressed_path) extract_location = "#{temp_folder}/extract_location" @@ -52,16 +58,12 @@ RSpec.describe Compression::Engine do zip_file = "#{temp_folder}/theme.zip" Zip::File.open(zip_file, create: true) do |zipfile| - zipfile.get_output_stream("child-file") do |f| - f.puts("child file") - end + zipfile.get_output_stream("child-file") { |f| f.puts("child file") } zipfile.get_output_stream("../escape-decompression-folder.txt") do |f| f.puts("file that attempts to escape the decompression destination directory") end zipfile.mkdir("child-dir") - zipfile.get_output_stream("child-dir/grandchild-file") do |f| - f.puts("grandchild file") - end + zipfile.get_output_stream("child-dir/grandchild-file") { |f| f.puts("grandchild file") } end extract_location = "#{temp_folder}/extract_location" @@ -74,7 +76,7 @@ RSpec.describe Compression::Engine do "extract_location/child-file", "extract_location/child-dir", "extract_location/child-dir/grandchild-file", - "theme.zip" + "theme.zip", ) end end @@ -97,10 +99,10 @@ RSpec.describe Compression::Engine do end end - context 'when working with .tar.gz files' do - let(:extension) { '.tar.gz' } + context "when working with .tar.gz files" do + let(:extension) { ".tar.gz" } - it 'decompresses the folder and inspects files correctly' do + it "decompresses the folder and inspects files correctly" do engine = described_class.engine_for(@compressed_path) engine.decompress(temp_folder, "#{temp_folder}/#{folder_name}.tar.gz", available_size) @@ -116,16 +118,12 @@ RSpec.describe Compression::Engine do tar_file = "#{temp_folder}/theme.tar" File.open(tar_file, "wb") do |file| Gem::Package::TarWriter.new(file) do |tar| - tar.add_file("child-file", 644) do |tf| - tf.write("child file") - end + tar.add_file("child-file", 644) { |tf| tf.write("child file") } tar.add_file("../escape-extraction-folder", 644) do |tf| tf.write("file that attempts to escape the decompression destination directory") end tar.mkdir("child-dir", 755) - tar.add_file("child-dir/grandchild-file", 644) do |tf| - tf.write("grandchild file") - end + tar.add_file("child-dir/grandchild-file", 644) { |tf| tf.write("grandchild file") } end end tar_gz_file = "#{temp_folder}/theme.tar.gz" @@ -167,10 +165,10 @@ RSpec.describe Compression::Engine do end end - context 'when working with .tar files' do - let(:extension) { '.tar' } + context "when working with .tar files" do + let(:extension) { ".tar" } - it 'decompress the folder and inspect files correctly' do + it "decompress the folder and inspect files correctly" do engine = described_class.engine_for(@compressed_path) engine.decompress(temp_folder, "#{temp_folder}/#{folder_name}.tar", available_size) diff --git a/spec/lib/concern/cached_counting_spec.rb b/spec/lib/concern/cached_counting_spec.rb index 260744902d..041a4c3c24 100644 --- a/spec/lib/concern/cached_counting_spec.rb +++ b/spec/lib/concern/cached_counting_spec.rb @@ -39,7 +39,6 @@ RSpec.describe CachedCounting do end it "can dispatch counts to backing class" do - CachedCounting.queue("a,a", TestCachedCounting) CachedCounting.queue("a,a", TestCachedCounting) CachedCounting.queue("b", TestCachedCounting) @@ -48,7 +47,6 @@ RSpec.describe CachedCounting do CachedCounting.flush_to_db expect(TestCachedCounting.data).to eq({ "a,a" => 2, "b" => 1 }) - end end end @@ -76,20 +74,15 @@ RSpec.describe CachedCounting do CachedCounting.enable end - after do - CachedCounting.disable - end + after { CachedCounting.disable } it "can dispatch data via background thread" do - freeze_time d1 = Time.now.utc.to_date RailsCacheCounter.perform_increment!("a,a") RailsCacheCounter.perform_increment!("b") - 20.times do - RailsCacheCounter.perform_increment!("a,a") - end + 20.times { RailsCacheCounter.perform_increment!("a,a") } freeze_time 2.days.from_now d2 = Time.now.utc.to_date @@ -99,12 +92,7 @@ RSpec.describe CachedCounting do CachedCounting.flush - expected = { - ["a,a", d1] => 21, - ["b", d1] => 1, - ["a,a", d2] => 1, - ["d", d2] => 1, - } + expected = { ["a,a", d1] => 21, ["b", d1] => 1, ["a,a", d2] => 1, ["d", d2] => 1 } expect(RailsCacheCounter.cache_data).to eq(expected) end diff --git a/spec/lib/concern/category_hashtag_spec.rb b/spec/lib/concern/category_hashtag_spec.rb index 57d687beb2..9a815653d5 100644 --- a/spec/lib/concern/category_hashtag_spec.rb +++ b/spec/lib/concern/category_hashtag_spec.rb @@ -1,18 +1,20 @@ # frozen_string_literal: true RSpec.describe CategoryHashtag do - describe '#query_from_hashtag_slug' do + describe "#query_from_hashtag_slug" do fab!(:parent_category) { Fabricate(:category) } fab!(:child_category) { Fabricate(:category, parent_category: parent_category) } it "should return the right result for a parent category slug" do - expect(Category.query_from_hashtag_slug(parent_category.slug)) - .to eq(parent_category) + expect(Category.query_from_hashtag_slug(parent_category.slug)).to eq(parent_category) end it "should return the right result for a parent and child category slug" do - expect(Category.query_from_hashtag_slug("#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{child_category.slug}")) - .to eq(child_category) + expect( + Category.query_from_hashtag_slug( + "#{parent_category.slug}#{CategoryHashtag::SEPARATOR}#{child_category.slug}", + ), + ).to eq(child_category) end it "should return nil for incorrect parent category slug" do @@ -20,22 +22,29 @@ RSpec.describe CategoryHashtag do end it "should return nil for incorrect parent and child category slug" do - expect(Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug")).to eq(nil) + expect( + Category.query_from_hashtag_slug("random-slug#{CategoryHashtag::SEPARATOR}random-slug"), + ).to eq(nil) end it "should return nil for a non-existent root and a parent subcategory" do - expect(Category.query_from_hashtag_slug("non-existent#{CategoryHashtag::SEPARATOR}#{parent_category.slug}")).to eq(nil) + expect( + Category.query_from_hashtag_slug( + "non-existent#{CategoryHashtag::SEPARATOR}#{parent_category.slug}", + ), + ).to eq(nil) end context "with multi-level categories" do - before do - SiteSetting.max_category_nesting = 3 - end + before { SiteSetting.max_category_nesting = 3 } it "should return the right result for a grand child category slug" do category = Fabricate(:category, parent_category: child_category) - expect(Category.query_from_hashtag_slug("#{child_category.slug}#{CategoryHashtag::SEPARATOR}#{category.slug}")) - .to eq(category) + expect( + Category.query_from_hashtag_slug( + "#{child_category.slug}#{CategoryHashtag::SEPARATOR}#{category.slug}", + ), + ).to eq(category) end end end diff --git a/spec/lib/concern/has_custom_fields_spec.rb b/spec/lib/concern/has_custom_fields_spec.rb index af0bbaebd8..0557193c61 100644 --- a/spec/lib/concern/has_custom_fields_spec.rb +++ b/spec/lib/concern/has_custom_fields_spec.rb @@ -4,7 +4,9 @@ RSpec.describe HasCustomFields do describe "custom_fields" do before do DB.exec("create temporary table custom_fields_test_items(id SERIAL primary key)") - DB.exec("create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text, created_at TIMESTAMP, updated_at TIMESTAMP)") + DB.exec( + "create temporary table custom_fields_test_item_custom_fields(id SERIAL primary key, custom_fields_test_item_id int, name varchar(256) not null, value text, created_at TIMESTAMP, updated_at TIMESTAMP)", + ) DB.exec(<<~SQL) CREATE UNIQUE INDEX ON custom_fields_test_item_custom_fields (custom_fields_test_item_id) WHERE NAME = 'rare' @@ -38,7 +40,9 @@ RSpec.describe HasCustomFields do it "errors if a custom field is not preloaded" do test_item = CustomFieldsTestItem.new CustomFieldsTestItem.preload_custom_fields([test_item], ["test_field"]) - expect { test_item.custom_fields["other_field"] }.to raise_error(HasCustomFields::NotPreloadedError) + expect { test_item.custom_fields["other_field"] }.to raise_error( + HasCustomFields::NotPreloadedError, + ) end it "resets the preloaded_custom_fields if preload_custom_fields is called twice" do @@ -105,7 +109,10 @@ RSpec.describe HasCustomFields do # should be casted right after saving expect(test_item.custom_fields["a"]).to eq("0") - DB.exec("UPDATE custom_fields_test_item_custom_fields SET value='1' WHERE custom_fields_test_item_id=? AND name='a'", test_item.id) + DB.exec( + "UPDATE custom_fields_test_item_custom_fields SET value='1' WHERE custom_fields_test_item_id=? AND name='a'", + test_item.id, + ) # still the same, did not load expect(test_item.custom_fields["a"]).to eq("0") @@ -137,25 +144,25 @@ RSpec.describe HasCustomFields do expect(db_item.custom_fields).to eq("array" => [1]) test_item = CustomFieldsTestItem.new - test_item.custom_fields = { "a" => ["b", "c", "d"] } + test_item.custom_fields = { "a" => %w[b c d] } test_item.save db_item = CustomFieldsTestItem.find(test_item.id) - expect(db_item.custom_fields).to eq("a" => ["b", "c", "d"]) + expect(db_item.custom_fields).to eq("a" => %w[b c d]) - db_item.custom_fields.update('a' => ['c', 'd']) + db_item.custom_fields.update("a" => %w[c d]) db_item.save - expect(db_item.custom_fields).to eq("a" => ["c", "d"]) + expect(db_item.custom_fields).to eq("a" => %w[c d]) # It can be updated to the exact same value - db_item.custom_fields.update('a' => ['c']) + db_item.custom_fields.update("a" => ["c"]) db_item.save expect(db_item.custom_fields).to eq("a" => "c") - db_item.custom_fields.update('a' => ['c']) + db_item.custom_fields.update("a" => ["c"]) db_item.save expect(db_item.custom_fields).to eq("a" => "c") - db_item.custom_fields.delete('a') + db_item.custom_fields.delete("a") expect(db_item.custom_fields).to eq({}) end @@ -176,10 +183,10 @@ RSpec.describe HasCustomFields do test_item = CustomFieldsTestItem.new test_item.custom_fields = { "a" => ["b", 10, "d"] } test_item.save - expect(test_item.custom_fields).to eq("a" => ["b", "10", "d"]) + expect(test_item.custom_fields).to eq("a" => %w[b 10 d]) db_item = CustomFieldsTestItem.find(test_item.id) - expect(db_item.custom_fields).to eq("a" => ["b", "10", "d"]) + expect(db_item.custom_fields).to eq("a" => %w[b 10 d]) end it "supports type coercion" do @@ -192,14 +199,22 @@ RSpec.describe HasCustomFields do test_item.save test_item.reload - expect(test_item.custom_fields).to eq("bool" => true, "int" => 1, "json" => { "foo" => "bar" }) + expect(test_item.custom_fields).to eq( + "bool" => true, + "int" => 1, + "json" => { + "foo" => "bar", + }, + ) - before_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) + before_ids = + CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) test_item.custom_fields["bool"] = false test_item.save - after_ids = CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) + after_ids = + CustomFieldsTestItemCustomField.where(custom_fields_test_item_id: test_item.id).pluck(:id) # we updated only 1 custom field, so there should be only 1 different id expect((before_ids - after_ids).size).to eq(1) @@ -234,23 +249,19 @@ RSpec.describe HasCustomFields do CustomFieldsTestItem.register_custom_field_type(field_type, :json) item = CustomFieldsTestItem.new - item.custom_fields = { - "json_array" => [{ a: "test" }, { b: "another" }] - } + item.custom_fields = { "json_array" => [{ a: "test" }, { b: "another" }] } item.save item.reload - expect(item.custom_fields[field_type]).to eq( - [{ "a" => "test" }, { "b" => "another" }] - ) + expect(item.custom_fields[field_type]).to eq([{ "a" => "test" }, { "b" => "another" }]) - item.custom_fields["json_array"] = ['a', 'b'] + item.custom_fields["json_array"] = %w[a b] item.save item.reload - expect(item.custom_fields[field_type]).to eq(["a", "b"]) + expect(item.custom_fields[field_type]).to eq(%w[a b]) end it "will not fail to load custom fields if json is corrupt" do @@ -262,7 +273,7 @@ RSpec.describe HasCustomFields do CustomFieldsTestItemCustomField.create!( custom_fields_test_item_id: item.id, name: field_type, - value: "{test" + value: "{test", ) item = item.reload @@ -271,34 +282,34 @@ RSpec.describe HasCustomFields do it "supports bulk retrieval with a list of ids" do item1 = CustomFieldsTestItem.new - item1.custom_fields = { "a" => ["b", "c", "d"], 'not_allowlisted' => 'secret' } + item1.custom_fields = { "a" => %w[b c d], "not_allowlisted" => "secret" } item1.save item2 = CustomFieldsTestItem.new - item2.custom_fields = { "e" => 'hallo' } + item2.custom_fields = { "e" => "hallo" } item2.save - fields = CustomFieldsTestItem.custom_fields_for_ids([item1.id, item2.id], ['a', 'e']) + fields = CustomFieldsTestItem.custom_fields_for_ids([item1.id, item2.id], %w[a e]) expect(fields).to be_present - expect(fields[item1.id]['a']).to match_array(['b', 'c', 'd']) - expect(fields[item1.id]['not_allowlisted']).to be_blank - expect(fields[item2.id]['e']).to eq('hallo') + expect(fields[item1.id]["a"]).to match_array(%w[b c d]) + expect(fields[item1.id]["not_allowlisted"]).to be_blank + expect(fields[item2.id]["e"]).to eq("hallo") end it "handles interleaving saving properly" do - field_type = 'deep-nest-test' + field_type = "deep-nest-test" CustomFieldsTestItem.register_custom_field_type(field_type, :json) test_item = CustomFieldsTestItem.create! test_item.custom_fields[field_type] ||= {} - test_item.custom_fields[field_type]['b'] ||= {} - test_item.custom_fields[field_type]['b']['c'] = 'd' + test_item.custom_fields[field_type]["b"] ||= {} + test_item.custom_fields[field_type]["b"]["c"] = "d" test_item.save_custom_fields(true) db_item = CustomFieldsTestItem.find(test_item.id) - db_item.custom_fields[field_type]['b']['e'] = 'f' - test_item.custom_fields[field_type]['b']['e'] = 'f' - expected = { field_type => { 'b' => { 'c' => 'd', 'e' => 'f' } } } + db_item.custom_fields[field_type]["b"]["e"] = "f" + test_item.custom_fields[field_type]["b"]["e"] = "f" + expected = { field_type => { "b" => { "c" => "d", "e" => "f" } } } db_item.save_custom_fields(true) expect(db_item.reload.custom_fields).to eq(expected) @@ -307,7 +318,7 @@ RSpec.describe HasCustomFields do expect(test_item.reload.custom_fields).to eq(expected) end - it 'determines clean state correctly for mutable fields' do + it "determines clean state correctly for mutable fields" do json_field = "json_field" array_field = "array_field" CustomFieldsTestItem.register_custom_field_type(json_field, :json) @@ -339,94 +350,94 @@ RSpec.describe HasCustomFields do describe "create_singular" do it "creates new records" do item = CustomFieldsTestItem.create! - item.create_singular('hello', 'world') - expect(item.reload.custom_fields['hello']).to eq('world') + item.create_singular("hello", "world") + expect(item.reload.custom_fields["hello"]).to eq("world") end it "upserts on a database constraint error" do item0 = CustomFieldsTestItem.new item0.custom_fields = { "rare" => "gem" } item0.save - expect(item0.reload.custom_fields['rare']).to eq("gem") + expect(item0.reload.custom_fields["rare"]).to eq("gem") - item0.create_singular('rare', "diamond") - expect(item0.reload.custom_fields['rare']).to eq("diamond") + item0.create_singular("rare", "diamond") + expect(item0.reload.custom_fields["rare"]).to eq("diamond") end end describe "upsert_custom_fields" do - it 'upserts records' do + it "upserts records" do test_item = CustomFieldsTestItem.create - test_item.upsert_custom_fields('hello' => 'world', 'abc' => 'def') + test_item.upsert_custom_fields("hello" => "world", "abc" => "def") # In memory - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('def') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("def") # Persisted test_item.reload - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('def') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("def") # In memory - test_item.upsert_custom_fields('abc' => 'ghi') - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('ghi') + test_item.upsert_custom_fields("abc" => "ghi") + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("ghi") # Persisted test_item.reload - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('ghi') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("ghi") end - it 'allows upsert to use keywords' do + it "allows upsert to use keywords" do test_item = CustomFieldsTestItem.create - test_item.upsert_custom_fields(hello: 'world', abc: 'def') + test_item.upsert_custom_fields(hello: "world", abc: "def") # In memory - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('def') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("def") # Persisted test_item.reload - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('def') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("def") # In memory - test_item.upsert_custom_fields('abc' => 'ghi') - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('ghi') + test_item.upsert_custom_fields("abc" => "ghi") + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("ghi") # Persisted test_item.reload - expect(test_item.custom_fields['hello']).to eq('world') - expect(test_item.custom_fields['abc']).to eq('ghi') + expect(test_item.custom_fields["hello"]).to eq("world") + expect(test_item.custom_fields["abc"]).to eq("ghi") end - it 'allows using string and symbol indices interchangeably' do + it "allows using string and symbol indices interchangeably" do test_item = CustomFieldsTestItem.new test_item.custom_fields["bob"] = "marley" test_item.custom_fields["jack"] = "black" # In memory - expect(test_item.custom_fields[:bob]).to eq('marley') - expect(test_item.custom_fields[:jack]).to eq('black') + expect(test_item.custom_fields[:bob]).to eq("marley") + expect(test_item.custom_fields[:jack]).to eq("black") # Persisted test_item.save test_item.reload - expect(test_item.custom_fields[:bob]).to eq('marley') - expect(test_item.custom_fields[:jack]).to eq('black') + expect(test_item.custom_fields[:bob]).to eq("marley") + expect(test_item.custom_fields[:jack]).to eq("black") # Update via string index again - test_item.custom_fields['bob'] = 'the builder' + test_item.custom_fields["bob"] = "the builder" - expect(test_item.custom_fields[:bob]).to eq('the builder') + expect(test_item.custom_fields[:bob]).to eq("the builder") test_item.save test_item.reload - expect(test_item.custom_fields[:bob]).to eq('the builder') + expect(test_item.custom_fields[:bob]).to eq("the builder") end end end diff --git a/spec/lib/concern/has_search_data_spec.rb b/spec/lib/concern/has_search_data_spec.rb index 74f4fbb66d..b7e1f57942 100644 --- a/spec/lib/concern/has_search_data_spec.rb +++ b/spec/lib/concern/has_search_data_spec.rb @@ -4,7 +4,9 @@ RSpec.describe HasSearchData do describe "belongs to its model" do before do DB.exec("create temporary table model_items(id SERIAL primary key)") - DB.exec("create temporary table model_item_search_data(model_item_id int primary key, search_data tsvector, raw_data text, locale text)") + DB.exec( + "create temporary table model_item_search_data(model_item_id int primary key, search_data tsvector, raw_data text, locale text)", + ) class ModelItem < ActiveRecord::Base has_one :model_item_search_data, dependent: :destroy @@ -29,17 +31,18 @@ RSpec.describe HasSearchData do item = ModelItem.create! item.create_model_item_search_data!( model_item_id: item.id, - search_data: 'a', - raw_data: 'a', - locale: 'en') + search_data: "a", + raw_data: "a", + locale: "en", + ) item end - it 'sets its primary key into associated model' do - expect(ModelItemSearchData.primary_key).to eq 'model_item_id' + it "sets its primary key into associated model" do + expect(ModelItemSearchData.primary_key).to eq "model_item_id" end - it 'can access the model' do + it "can access the model" do record_id = item.id expect(ModelItemSearchData.find_by(model_item_id: record_id).model_item_id).to eq record_id end diff --git a/spec/lib/concern/positionable_spec.rb b/spec/lib/concern/positionable_spec.rb index f4a5746386..7beb129820 100644 --- a/spec/lib/concern/positionable_spec.rb +++ b/spec/lib/concern/positionable_spec.rb @@ -2,7 +2,7 @@ RSpec.describe Positionable do def positions - TestItem.order('position asc, id asc').pluck(:id) + TestItem.order("position asc, id asc").pluck(:id) end describe "move_to" do @@ -23,9 +23,7 @@ RSpec.describe Positionable do end it "can position stuff correctly" do - 5.times do |i| - DB.exec("insert into test_items(id,position) values(#{i}, #{i})") - end + 5.times { |i| DB.exec("insert into test_items(id,position) values(#{i}, #{i})") } expect(positions).to eq([0, 1, 2, 3, 4]) TestItem.find(3).move_to(0) diff --git a/spec/lib/concern/searchable_spec.rb b/spec/lib/concern/searchable_spec.rb index b92b0eb4af..22bb1b5fc2 100644 --- a/spec/lib/concern/searchable_spec.rb +++ b/spec/lib/concern/searchable_spec.rb @@ -4,14 +4,16 @@ RSpec.describe Searchable do describe "has search data" do before do DB.exec("create temporary table searchable_records(id SERIAL primary key)") - DB.exec("create temporary table searchable_record_search_data(searchable_record_id int primary key, search_data tsvector, raw_data text, locale text)") + DB.exec( + "create temporary table searchable_record_search_data(searchable_record_id int primary key, search_data tsvector, raw_data text, locale text)", + ) class SearchableRecord < ActiveRecord::Base include Searchable end class SearchableRecordSearchData < ActiveRecord::Base - self.primary_key = 'searchable_record_id' + self.primary_key = "searchable_record_id" belongs_to :test_item end end @@ -28,26 +30,20 @@ RSpec.describe Searchable do let(:item) { SearchableRecord.create! } - it 'can build the data' do + it "can build the data" do expect(item.build_searchable_record_search_data).to be_truthy end - it 'can save the data' do - item.build_searchable_record_search_data( - search_data: '', - raw_data: 'a', - locale: 'en') + it "can save the data" do + item.build_searchable_record_search_data(search_data: "", raw_data: "a", locale: "en") item.save loaded = SearchableRecord.find(item.id) - expect(loaded.searchable_record_search_data.raw_data).to eq 'a' + expect(loaded.searchable_record_search_data.raw_data).to eq "a" end - it 'destroy the search data when the item is deprived' do - item.build_searchable_record_search_data( - search_data: '', - raw_data: 'a', - locale: 'en') + it "destroy the search data when the item is deprived" do + item.build_searchable_record_search_data(search_data: "", raw_data: "a", locale: "en") item.save item_id = item.id item.destroy diff --git a/spec/lib/concern/second_factor_manager_spec.rb b/spec/lib/concern/second_factor_manager_spec.rb index a30b726e73..80dfeb67a4 100644 --- a/spec/lib/concern/second_factor_manager_spec.rb +++ b/spec/lib/concern/second_factor_manager_spec.rb @@ -8,29 +8,31 @@ RSpec.describe SecondFactorManager do :user_security_key, user: user, public_key: valid_security_key_data[:public_key], - credential_id: valid_security_key_data[:credential_id] + credential_id: valid_security_key_data[:credential_id], ) end fab!(:another_user) { Fabricate(:user) } fab!(:user_second_factor_backup) { Fabricate(:user_second_factor_backup) } - let(:user_backup) { user_second_factor_backup.user } + let(:user_backup) { user_second_factor_backup.user } - describe '#totp' do - it 'should return the right data' do + describe "#totp" do + it "should return the right data" do totp = nil - expect do - totp = another_user.create_totp(enabled: true) - end.to change { UserSecondFactor.count }.by(1) + expect do totp = another_user.create_totp(enabled: true) end.to change { + UserSecondFactor.count + }.by(1) expect(totp.totp_object.issuer).to eq(SiteSetting.title) - expect(totp.totp_object.secret).to eq(another_user.reload.user_second_factors.totps.first.data) + expect(totp.totp_object.secret).to eq( + another_user.reload.user_second_factors.totps.first.data, + ) end end - describe '#create_totp' do - it 'should create the right record' do + describe "#create_totp" do + it "should create the right record" do second_factor = another_user.create_totp(enabled: true) expect(second_factor.method).to eq(UserSecondFactor.methods[:totp]) @@ -39,28 +41,28 @@ RSpec.describe SecondFactorManager do end end - describe '#totp_provisioning_uri' do - it 'should return the right uri' do + describe "#totp_provisioning_uri" do + it "should return the right uri" do expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq( - "otpauth://totp/#{SiteSetting.title}:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=#{SiteSetting.title}" + "otpauth://totp/#{SiteSetting.title}:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=#{SiteSetting.title}", ) end - it 'should handle a colon in the site title' do - SiteSetting.title = 'Spaceballs: The Discourse' + it "should handle a colon in the site title" do + SiteSetting.title = "Spaceballs: The Discourse" expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq( - "otpauth://totp/Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Spaceballs%20The%20Discourse" + "otpauth://totp/Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Spaceballs%20The%20Discourse", ) end - it 'should handle a two words before a colon in the title' do - SiteSetting.title = 'Our Spaceballs: The Discourse' + it "should handle a two words before a colon in the title" do + SiteSetting.title = "Our Spaceballs: The Discourse" expect(user.user_second_factors.totps.first.totp_provisioning_uri).to eq( - "otpauth://totp/Our%20Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Our%20Spaceballs%20The%20Discourse" + "otpauth://totp/Our%20Spaceballs%20The%20Discourse:#{ERB::Util.url_encode(user.email)}?secret=#{user_second_factor_totp.data}&issuer=Our%20Spaceballs%20The%20Discourse", ) end end - describe '#authenticate_totp' do - it 'should be able to authenticate a token' do + describe "#authenticate_totp" do + it "should be able to authenticate a token" do freeze_time do expect(user.user_second_factors.totps.first.last_used).to eq(nil) @@ -72,52 +74,52 @@ RSpec.describe SecondFactorManager do end end - describe 'when token is blank' do - it 'should be false' do + describe "when token is blank" do + it "should be false" do expect(user.authenticate_totp(nil)).to eq(false) expect(user.user_second_factors.totps.first.last_used).to eq(nil) end end - describe 'when token is invalid' do - it 'should be false' do - expect(user.authenticate_totp('111111')).to eq(false) + describe "when token is invalid" do + it "should be false" do + expect(user.authenticate_totp("111111")).to eq(false) expect(user.user_second_factors.totps.first.last_used).to eq(nil) end end end - describe '#totp_enabled?' do - describe 'when user does not have a second factor record' do - it 'should return false' do + describe "#totp_enabled?" do + describe "when user does not have a second factor record" do + it "should return false" do expect(another_user.totp_enabled?).to eq(false) end end describe "when user's second factor record is disabled" do - it 'should return false' do + it "should return false" do disable_totp expect(user.totp_enabled?).to eq(false) end end describe "when user's second factor record is enabled" do - it 'should return true' do + it "should return true" do expect(user.totp_enabled?).to eq(true) end end - describe 'when SSO is enabled' do - it 'should return false' do - SiteSetting.discourse_connect_url = 'http://someurl.com' + describe "when SSO is enabled" do + it "should return false" do + SiteSetting.discourse_connect_url = "http://someurl.com" SiteSetting.enable_discourse_connect = true expect(user.totp_enabled?).to eq(false) end end - describe 'when local login is disabled' do - it 'should return false' do + describe "when local login is disabled" do + it "should return false" do SiteSetting.enable_local_logins = false expect(user.totp_enabled?).to eq(false) @@ -166,9 +168,7 @@ RSpec.describe SecondFactorManager do let(:secure_session) { {} } context "when neither security keys nor totp/backup codes are enabled" do - before do - disable_security_key && disable_totp - end + before { disable_security_key && disable_totp } it "returns OK, because it doesn't need to authenticate" do expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true) end @@ -186,13 +186,20 @@ RSpec.describe SecondFactorManager do end context "when security key params are valid" do - let(:params) { { second_factor_token: valid_security_key_auth_post_data, second_factor_method: UserSecondFactor.methods[:security_key] } } + let(:params) do + { + second_factor_token: valid_security_key_auth_post_data, + second_factor_method: UserSecondFactor.methods[:security_key], + } + end it "returns OK" do expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true) end it "sets used_2fa_method to security keys" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:security_key]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:security_key], + ) end end @@ -200,12 +207,12 @@ RSpec.describe SecondFactorManager do let(:params) do { second_factor_token: { - signature: 'bad', - clientData: 'bad', - authenticatorData: 'bad', - credentialId: 'bad' + signature: "bad", + clientData: "bad", + authenticatorData: "bad", + credentialId: "bad", }, - second_factor_method: UserSecondFactor.methods[:security_key] + second_factor_method: UserSecondFactor.methods[:security_key], } end it "returns not OK" do @@ -218,15 +225,13 @@ RSpec.describe SecondFactorManager do end context "when only totp is enabled" do - before do - disable_security_key - end + before { disable_security_key } context "when totp is valid" do let(:params) do { second_factor_token: user.user_second_factors.totps.first.totp_object.now, - second_factor_method: UserSecondFactor.methods[:totp] + second_factor_method: UserSecondFactor.methods[:totp], } end it "returns OK" do @@ -234,16 +239,15 @@ RSpec.describe SecondFactorManager do end it "sets used_2fa_method to totp" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:totp]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:totp], + ) end end context "when totp is invalid" do let(:params) do - { - second_factor_token: "blah", - second_factor_method: UserSecondFactor.methods[:totp] - } + { second_factor_token: "blah", second_factor_method: UserSecondFactor.methods[:totp] } end it "returns not OK" do result = user.authenticate_second_factor(params, secure_session) @@ -277,26 +281,21 @@ RSpec.describe SecondFactorManager do let(:token) { user.user_second_factors.totps.first.totp_object.now } context "when totp params are provided" do - let(:params) do - { - second_factor_token: token, - second_factor_method: method - } - end + let(:params) { { second_factor_token: token, second_factor_method: method } } it "validates totp OK" do expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true) end it "sets used_2fa_method to totp" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:totp]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:totp], + ) end context "when the user does not have TOTP enabled" do - let(:token) { 'test' } - before do - user.totps.destroy_all - end + let(:token) { "test" } + before { user.totps.destroy_all } it "returns an error" do result = user.authenticate_second_factor(params, secure_session) @@ -317,19 +316,21 @@ RSpec.describe SecondFactorManager do end context "when security key params are valid" do - let(:params) { { second_factor_token: valid_security_key_auth_post_data, second_factor_method: method } } + let(:params) do + { second_factor_token: valid_security_key_auth_post_data, second_factor_method: method } + end it "returns OK" do expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true) end it "sets used_2fa_method to security keys" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:security_key]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:security_key], + ) end context "when the user does not have security keys enabled" do - before do - user.security_keys.destroy_all - end + before { user.security_keys.destroy_all } it "returns an error" do result = user.authenticate_second_factor(params, secure_session) @@ -347,10 +348,7 @@ RSpec.describe SecondFactorManager do context "when backup code params are provided" do let(:params) do - { - second_factor_token: 'iAmValidBackupCode', - second_factor_method: method - } + { second_factor_token: "iAmValidBackupCode", second_factor_method: method } end context "when backup codes enabled" do @@ -359,14 +357,14 @@ RSpec.describe SecondFactorManager do end it "sets used_2fa_method to backup codes" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:backup_codes]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:backup_codes], + ) end end context "when backup codes disabled" do - before do - user.user_second_factors.backup_codes.destroy_all - end + before { user.user_second_factors.backup_codes.destroy_all } it "returns an error" do result = user.authenticate_second_factor(params, secure_session) @@ -379,14 +377,21 @@ RSpec.describe SecondFactorManager do end context "when no totp params are provided" do - let(:params) { { second_factor_token: valid_security_key_auth_post_data, second_factor_method: UserSecondFactor.methods[:security_key] } } + let(:params) do + { + second_factor_token: valid_security_key_auth_post_data, + second_factor_method: UserSecondFactor.methods[:security_key], + } + end it "validates the security key OK" do expect(user.authenticate_second_factor(params, secure_session).ok).to eq(true) end it "sets used_2fa_method to security keys" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:security_key]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:security_key], + ) end end @@ -394,7 +399,7 @@ RSpec.describe SecondFactorManager do let(:params) do { second_factor_token: user.user_second_factors.totps.first.totp_object.now, - second_factor_method: UserSecondFactor.methods[:totp] + second_factor_method: UserSecondFactor.methods[:totp], } end @@ -403,26 +408,30 @@ RSpec.describe SecondFactorManager do end it "sets used_2fa_method to totp" do - expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq(UserSecondFactor.methods[:totp]) + expect(user.authenticate_second_factor(params, secure_session).used_2fa_method).to eq( + UserSecondFactor.methods[:totp], + ) end end end end - describe 'backup codes' do - describe '#generate_backup_codes' do - it 'should generate and store 10 backup codes' do + describe "backup codes" do + describe "#generate_backup_codes" do + it "should generate and store 10 backup codes" do backup_codes = user.generate_backup_codes expect(backup_codes.length).to be 10 expect(user_backup.user_second_factors.backup_codes).to be_present - expect(user_backup.user_second_factors.backup_codes.pluck(:method).uniq[0]).to eq(UserSecondFactor.methods[:backup_codes]) + expect(user_backup.user_second_factors.backup_codes.pluck(:method).uniq[0]).to eq( + UserSecondFactor.methods[:backup_codes], + ) expect(user_backup.user_second_factors.backup_codes.pluck(:enabled).uniq[0]).to eq(true) end end - describe '#create_backup_codes' do - it 'should create 10 backup code records' do + describe "#create_backup_codes" do + it "should create 10 backup code records" do raw_codes = Array.new(10) { SecureRandom.hex(8) } backup_codes = another_user.create_backup_codes(raw_codes) @@ -430,58 +439,58 @@ RSpec.describe SecondFactorManager do end end - describe '#authenticate_backup_code' do - it 'should be able to authenticate a backup code' do + describe "#authenticate_backup_code" do + it "should be able to authenticate a backup code" do backup_code = "iAmValidBackupCode" expect(user_backup.authenticate_backup_code(backup_code)).to eq(true) expect(user_backup.authenticate_backup_code(backup_code)).to eq(false) end - describe 'when code is blank' do - it 'should be false' do + describe "when code is blank" do + it "should be false" do expect(user_backup.authenticate_backup_code(nil)).to eq(false) end end - describe 'when code is invalid' do - it 'should be false' do + describe "when code is invalid" do + it "should be false" do expect(user_backup.authenticate_backup_code("notValidBackupCode")).to eq(false) end end end - describe '#backup_codes_enabled?' do - describe 'when user does not have a second factor backup enabled' do - it 'should return false' do + describe "#backup_codes_enabled?" do + describe "when user does not have a second factor backup enabled" do + it "should return false" do expect(another_user.backup_codes_enabled?).to eq(false) end end describe "when user's second factor backup codes have been used" do - it 'should return false' do + it "should return false" do user_backup.user_second_factors.backup_codes.update_all(enabled: false) expect(user_backup.backup_codes_enabled?).to eq(false) end end describe "when user's second factor code is available" do - it 'should return true' do + it "should return true" do expect(user_backup.backup_codes_enabled?).to eq(true) end end - describe 'when SSO is enabled' do - it 'should return false' do - SiteSetting.discourse_connect_url = 'http://someurl.com' + describe "when SSO is enabled" do + it "should return false" do + SiteSetting.discourse_connect_url = "http://someurl.com" SiteSetting.enable_discourse_connect = true expect(user_backup.backup_codes_enabled?).to eq(false) end end - describe 'when local login is disabled' do - it 'should return false' do + describe "when local login is disabled" do + it "should return false" do SiteSetting.enable_local_logins = false expect(user_backup.backup_codes_enabled?).to eq(false) diff --git a/spec/lib/content_buffer_spec.rb b/spec/lib/content_buffer_spec.rb index efc8ffe005..567f808b6e 100644 --- a/spec/lib/content_buffer_spec.rb +++ b/spec/lib/content_buffer_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'content_buffer' +require "content_buffer" RSpec.describe ContentBuffer do - it "handles deletion across lines properly" do c = ContentBuffer.new("a\nbc\nc") c.apply_transform!(start: { row: 0, col: 0 }, finish: { col: 1, row: 1 }, operation: :delete) @@ -26,5 +25,4 @@ RSpec.describe ContentBuffer do c.apply_transform!(start: { row: 0, col: 5 }, operation: :insert, text: "\nworld") expect(c.to_s).to eq("hello\nworld!") end - end diff --git a/spec/lib/content_security_policy/builder_spec.rb b/spec/lib/content_security_policy/builder_spec.rb index fd0ad5ba10..ec58256d2a 100644 --- a/spec/lib/content_security_policy/builder_spec.rb +++ b/spec/lib/content_security_policy/builder_spec.rb @@ -2,29 +2,29 @@ RSpec.describe ContentSecurityPolicy::Builder do let(:builder) { described_class.new(base_url: Discourse.base_url) } - describe '#<<' do - it 'normalizes directive name' do + describe "#<<" do + it "normalizes directive name" do builder << { - script_src: ['symbol_underscore'], - 'script-src': ['symbol_dash'], - 'script_src' => ['string_underscore'], - 'script-src' => ['string_dash'], + :script_src => ["symbol_underscore"], + :"script-src" => ["symbol_dash"], + "script_src" => ["string_underscore"], + "script-src" => ["string_dash"], } - script_srcs = parse(builder.build)['script-src'] + script_srcs = parse(builder.build)["script-src"] - expect(script_srcs).to include(*%w[symbol_underscore symbol_dash string_underscore symbol_underscore]) + expect(script_srcs).to include( + *%w[symbol_underscore symbol_dash string_underscore symbol_underscore], + ) end - it 'rejects invalid directives and ones that are not allowed to be extended' do - builder << { - invalid_src: ['invalid'], - } + it "rejects invalid directives and ones that are not allowed to be extended" do + builder << { invalid_src: ["invalid"] } - expect(builder.build).to_not include('invalid') + expect(builder.build).to_not include("invalid") end - it 'no-ops on invalid values' do + it "no-ops on invalid values" do previous = builder.build builder << nil @@ -38,9 +38,12 @@ RSpec.describe ContentSecurityPolicy::Builder do end def parse(csp_string) - csp_string.split(';').map do |policy| - directive, *sources = policy.split - [directive, sources] - end.to_h + csp_string + .split(";") + .map do |policy| + directive, *sources = policy.split + [directive, sources] + end + .to_h end end diff --git a/spec/lib/content_security_policy_spec.rb b/spec/lib/content_security_policy_spec.rb index 46f11f63be..2d36edfbdc 100644 --- a/spec/lib/content_security_policy_spec.rb +++ b/spec/lib/content_security_policy_spec.rb @@ -1,223 +1,231 @@ # frozen_string_literal: true RSpec.describe ContentSecurityPolicy do - after do - DiscoursePluginRegistry.reset! - end + after { DiscoursePluginRegistry.reset! } - describe 'report-uri' do - it 'is enabled by SiteSetting' do + describe "report-uri" do + it "is enabled by SiteSetting" do SiteSetting.content_security_policy_collect_reports = true - report_uri = parse(policy)['report-uri'].first - expect(report_uri).to eq('http://test.localhost/csp_reports') + report_uri = parse(policy)["report-uri"].first + expect(report_uri).to eq("http://test.localhost/csp_reports") SiteSetting.content_security_policy_collect_reports = false - report_uri = parse(policy)['report-uri'] + report_uri = parse(policy)["report-uri"] expect(report_uri).to eq(nil) end end - describe 'base-uri' do - it 'is set to self' do - base_uri = parse(policy)['base-uri'] + describe "base-uri" do + it "is set to self" do + base_uri = parse(policy)["base-uri"] expect(base_uri).to eq(["'self'"]) end end - describe 'object-src' do - it 'is set to none' do - object_srcs = parse(policy)['object-src'] + describe "object-src" do + it "is set to none" do + object_srcs = parse(policy)["object-src"] expect(object_srcs).to eq(["'none'"]) end end - describe 'upgrade-insecure-requests' do - it 'is not included when force_https is off' do + describe "upgrade-insecure-requests" do + it "is not included when force_https is off" do SiteSetting.force_https = false - expect(parse(policy)['upgrade-insecure-requests']).to eq(nil) + expect(parse(policy)["upgrade-insecure-requests"]).to eq(nil) end - it 'is included when force_https is on' do + it "is included when force_https is on" do SiteSetting.force_https = true - expect(parse(policy)['upgrade-insecure-requests']).to eq([]) + expect(parse(policy)["upgrade-insecure-requests"]).to eq([]) end end - describe 'worker-src' do - it 'has expected values' do - worker_srcs = parse(policy)['worker-src'] - expect(worker_srcs).to eq(%w[ - 'self' - http://test.localhost/assets/ - http://test.localhost/brotli_asset/ - http://test.localhost/javascripts/ - http://test.localhost/plugins/ - ]) + describe "worker-src" do + it "has expected values" do + worker_srcs = parse(policy)["worker-src"] + expect(worker_srcs).to eq( + %w[ + 'self' + http://test.localhost/assets/ + http://test.localhost/brotli_asset/ + http://test.localhost/javascripts/ + http://test.localhost/plugins/ + ], + ) end end - describe 'script-src' do - it 'always has self, logster, sidekiq, and assets' do - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - http://test.localhost/logs/ - http://test.localhost/sidekiq/ - http://test.localhost/mini-profiler-resources/ - http://test.localhost/assets/ - http://test.localhost/brotli_asset/ - http://test.localhost/extra-locales/ - http://test.localhost/highlight-js/ - http://test.localhost/javascripts/ - http://test.localhost/plugins/ - http://test.localhost/theme-javascripts/ - http://test.localhost/svg-sprite/ - ]) + describe "script-src" do + it "always has self, logster, sidekiq, and assets" do + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + http://test.localhost/logs/ + http://test.localhost/sidekiq/ + http://test.localhost/mini-profiler-resources/ + http://test.localhost/assets/ + http://test.localhost/brotli_asset/ + http://test.localhost/extra-locales/ + http://test.localhost/highlight-js/ + http://test.localhost/javascripts/ + http://test.localhost/plugins/ + http://test.localhost/theme-javascripts/ + http://test.localhost/svg-sprite/ + ], + ) end it 'includes "report-sample" when report collection is enabled' do SiteSetting.content_security_policy_collect_reports = true - script_srcs = parse(policy)['script-src'] + script_srcs = parse(policy)["script-src"] expect(script_srcs).to include("'report-sample'") end - context 'for Google Analytics' do - before do - SiteSetting.ga_universal_tracking_code = 'UA-12345678-9' + context "for Google Analytics" do + before { SiteSetting.ga_universal_tracking_code = "UA-12345678-9" } + + it "allowlists Google Analytics v3 when integrated" do + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include("https://www.google-analytics.com/analytics.js") + expect(script_srcs).not_to include("https://www.googletagmanager.com/gtag/js") end - it 'allowlists Google Analytics v3 when integrated' do - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include('https://www.google-analytics.com/analytics.js') - expect(script_srcs).not_to include('https://www.googletagmanager.com/gtag/js') - end + it "allowlists Google Analytics v4 when integrated" do + SiteSetting.ga_version = "v4_gtag" - it 'allowlists Google Analytics v4 when integrated' do - SiteSetting.ga_version = 'v4_gtag' - - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include('https://www.google-analytics.com/analytics.js') - expect(script_srcs).to include('https://www.googletagmanager.com/gtag/js') + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include("https://www.google-analytics.com/analytics.js") + expect(script_srcs).to include("https://www.googletagmanager.com/gtag/js") end end - it 'allowlists Google Tag Manager when integrated' do - SiteSetting.gtm_container_id = 'GTM-ABCDEF' + it "allowlists Google Tag Manager when integrated" do + SiteSetting.gtm_container_id = "GTM-ABCDEF" - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include('https://www.googletagmanager.com/gtm.js') - expect(script_srcs.to_s).to include('nonce-') + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include("https://www.googletagmanager.com/gtm.js") + expect(script_srcs.to_s).to include("nonce-") end - it 'allowlists CDN assets when integrated' do - set_cdn_url('https://cdn.com') + it "allowlists CDN assets when integrated" do + set_cdn_url("https://cdn.com") - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - https://cdn.com/assets/ - https://cdn.com/brotli_asset/ - https://cdn.com/highlight-js/ - https://cdn.com/javascripts/ - https://cdn.com/plugins/ - https://cdn.com/theme-javascripts/ - http://test.localhost/extra-locales/ - ]) + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + https://cdn.com/assets/ + https://cdn.com/brotli_asset/ + https://cdn.com/highlight-js/ + https://cdn.com/javascripts/ + https://cdn.com/plugins/ + https://cdn.com/theme-javascripts/ + http://test.localhost/extra-locales/ + ], + ) - global_setting(:s3_cdn_url, 'https://s3-cdn.com') + global_setting(:s3_cdn_url, "https://s3-cdn.com") - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - https://s3-cdn.com/assets/ - https://s3-cdn.com/brotli_asset/ - https://cdn.com/highlight-js/ - https://cdn.com/javascripts/ - https://cdn.com/plugins/ - https://cdn.com/theme-javascripts/ - http://test.localhost/extra-locales/ - ]) + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + https://s3-cdn.com/assets/ + https://s3-cdn.com/brotli_asset/ + https://cdn.com/highlight-js/ + https://cdn.com/javascripts/ + https://cdn.com/plugins/ + https://cdn.com/theme-javascripts/ + http://test.localhost/extra-locales/ + ], + ) - global_setting(:s3_asset_cdn_url, 'https://s3-asset-cdn.com') + global_setting(:s3_asset_cdn_url, "https://s3-asset-cdn.com") - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - https://s3-asset-cdn.com/assets/ - https://s3-asset-cdn.com/brotli_asset/ - https://cdn.com/highlight-js/ - https://cdn.com/javascripts/ - https://cdn.com/plugins/ - https://cdn.com/theme-javascripts/ - http://test.localhost/extra-locales/ - ]) + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + https://s3-asset-cdn.com/assets/ + https://s3-asset-cdn.com/brotli_asset/ + https://cdn.com/highlight-js/ + https://cdn.com/javascripts/ + https://cdn.com/plugins/ + https://cdn.com/theme-javascripts/ + http://test.localhost/extra-locales/ + ], + ) end - it 'adds subfolder to CDN assets' do - set_cdn_url('https://cdn.com') - set_subfolder('/forum') + it "adds subfolder to CDN assets" do + set_cdn_url("https://cdn.com") + set_subfolder("/forum") - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - https://cdn.com/forum/assets/ - https://cdn.com/forum/brotli_asset/ - https://cdn.com/forum/highlight-js/ - https://cdn.com/forum/javascripts/ - https://cdn.com/forum/plugins/ - https://cdn.com/forum/theme-javascripts/ - http://test.localhost/forum/extra-locales/ - ]) + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + https://cdn.com/forum/assets/ + https://cdn.com/forum/brotli_asset/ + https://cdn.com/forum/highlight-js/ + https://cdn.com/forum/javascripts/ + https://cdn.com/forum/plugins/ + https://cdn.com/forum/theme-javascripts/ + http://test.localhost/forum/extra-locales/ + ], + ) - global_setting(:s3_cdn_url, 'https://s3-cdn.com') + global_setting(:s3_cdn_url, "https://s3-cdn.com") - script_srcs = parse(policy)['script-src'] - expect(script_srcs).to include(*%w[ - https://s3-cdn.com/assets/ - https://s3-cdn.com/brotli_asset/ - https://cdn.com/forum/highlight-js/ - https://cdn.com/forum/javascripts/ - https://cdn.com/forum/plugins/ - https://cdn.com/forum/theme-javascripts/ - http://test.localhost/forum/extra-locales/ - ]) + script_srcs = parse(policy)["script-src"] + expect(script_srcs).to include( + *%w[ + https://s3-cdn.com/assets/ + https://s3-cdn.com/brotli_asset/ + https://cdn.com/forum/highlight-js/ + https://cdn.com/forum/javascripts/ + https://cdn.com/forum/plugins/ + https://cdn.com/forum/theme-javascripts/ + http://test.localhost/forum/extra-locales/ + ], + ) end end - describe 'manifest-src' do - it 'is set to self' do - expect(parse(policy)['manifest-src']).to eq(["'self'"]) + describe "manifest-src" do + it "is set to self" do + expect(parse(policy)["manifest-src"]).to eq(["'self'"]) end end - describe 'frame-ancestors' do - context 'with content_security_policy_frame_ancestors enabled' do + describe "frame-ancestors" do + context "with content_security_policy_frame_ancestors enabled" do before do SiteSetting.content_security_policy_frame_ancestors = true - Fabricate(:embeddable_host, host: 'https://a.org') - Fabricate(:embeddable_host, host: 'https://b.org') + Fabricate(:embeddable_host, host: "https://a.org") + Fabricate(:embeddable_host, host: "https://b.org") end - it 'always has self' do - frame_ancestors = parse(policy)['frame-ancestors'] + it "always has self" do + frame_ancestors = parse(policy)["frame-ancestors"] expect(frame_ancestors).to include("'self'") end - it 'includes all EmbeddableHost' do + it "includes all EmbeddableHost" do EmbeddableHost - frame_ancestors = parse(policy)['frame-ancestors'] + frame_ancestors = parse(policy)["frame-ancestors"] expect(frame_ancestors).to include("https://a.org") expect(frame_ancestors).to include("https://b.org") end end - context 'with content_security_policy_frame_ancestors disabled' do - before do - SiteSetting.content_security_policy_frame_ancestors = false - end + context "with content_security_policy_frame_ancestors disabled" do + before { SiteSetting.content_security_policy_frame_ancestors = false } - it 'does not set frame-ancestors' do - frame_ancestors = parse(policy)['frame-ancestors'] + it "does not set frame-ancestors" do + frame_ancestors = parse(policy)["frame-ancestors"] expect(frame_ancestors).to be_nil end end end - context 'with a plugin' do + context "with a plugin" do let(:plugin_class) do Class.new(Plugin::Instance) do attr_accessor :enabled @@ -227,29 +235,29 @@ RSpec.describe ContentSecurityPolicy do end end - it 'can extend script-src, object-src, manifest-src' do + it "can extend script-src, object-src, manifest-src" do plugin = plugin_class.new(nil, "#{Rails.root}/spec/fixtures/plugins/csp_extension/plugin.rb") plugin.activate! Discourse.plugins << plugin plugin.enabled = true - expect(parse(policy)['script-src']).to include('https://from-plugin.com') - expect(parse(policy)['script-src']).to include('http://test.localhost/local/path') - expect(parse(policy)['object-src']).to include('https://test-stripping.com') - expect(parse(policy)['object-src']).to_not include("'none'") - expect(parse(policy)['manifest-src']).to include("'self'") - expect(parse(policy)['manifest-src']).to include('https://manifest-src.com') + expect(parse(policy)["script-src"]).to include("https://from-plugin.com") + expect(parse(policy)["script-src"]).to include("http://test.localhost/local/path") + expect(parse(policy)["object-src"]).to include("https://test-stripping.com") + expect(parse(policy)["object-src"]).to_not include("'none'") + expect(parse(policy)["manifest-src"]).to include("'self'") + expect(parse(policy)["manifest-src"]).to include("https://manifest-src.com") plugin.enabled = false - expect(parse(policy)['script-src']).to_not include('https://from-plugin.com') - expect(parse(policy)['manifest-src']).to_not include('https://manifest-src.com') + expect(parse(policy)["script-src"]).to_not include("https://from-plugin.com") + expect(parse(policy)["manifest-src"]).to_not include("https://manifest-src.com") Discourse.plugins.delete plugin DiscoursePluginRegistry.reset! end - it 'can extend frame_ancestors' do + it "can extend frame_ancestors" do SiteSetting.content_security_policy_frame_ancestors = true plugin = plugin_class.new(nil, "#{Rails.root}/spec/fixtures/plugins/csp_extension/plugin.rb") @@ -257,25 +265,25 @@ RSpec.describe ContentSecurityPolicy do Discourse.plugins << plugin plugin.enabled = true - expect(parse(policy)['frame-ancestors']).to include("'self'") - expect(parse(policy)['frame-ancestors']).to include('https://frame-ancestors-plugin.ext') + expect(parse(policy)["frame-ancestors"]).to include("'self'") + expect(parse(policy)["frame-ancestors"]).to include("https://frame-ancestors-plugin.ext") plugin.enabled = false - expect(parse(policy)['frame-ancestors']).to_not include('https://frame-ancestors-plugin.ext') + expect(parse(policy)["frame-ancestors"]).to_not include("https://frame-ancestors-plugin.ext") Discourse.plugins.delete plugin DiscoursePluginRegistry.reset! end end - it 'only includes unsafe-inline for qunit paths' do - expect(parse(policy(path_info: "/qunit"))['script-src']).to include("'unsafe-eval'") - expect(parse(policy(path_info: "/wizard/qunit"))['script-src']).to include("'unsafe-eval'") - expect(parse(policy(path_info: "/"))['script-src']).to_not include("'unsafe-eval'") + it "only includes unsafe-inline for qunit paths" do + expect(parse(policy(path_info: "/qunit"))["script-src"]).to include("'unsafe-eval'") + expect(parse(policy(path_info: "/wizard/qunit"))["script-src"]).to include("'unsafe-eval'") + expect(parse(policy(path_info: "/"))["script-src"]).to_not include("'unsafe-eval'") end context "with a theme" do - let!(:theme) { + let!(:theme) do Fabricate(:theme).tap do |t| settings = <<~YML extend_content_security_policy: @@ -285,58 +293,67 @@ RSpec.describe ContentSecurityPolicy do t.set_field(target: :settings, name: :yaml, value: settings) t.save! end - } + end def theme_policy policy(theme.id) end - it 'can be extended by themes' do + it "can be extended by themes" do policy # call this first to make sure further actions clear the cache - expect(parse(policy)['script-src']).not_to include('from-theme.com') + expect(parse(policy)["script-src"]).not_to include("from-theme.com") - expect(parse(theme_policy)['script-src']).to include('from-theme.com') + expect(parse(theme_policy)["script-src"]).to include("from-theme.com") - theme.update_setting(:extend_content_security_policy, "script-src: https://from-theme.net|worker-src: from-theme.com") + theme.update_setting( + :extend_content_security_policy, + "script-src: https://from-theme.net|worker-src: from-theme.com", + ) theme.save! - expect(parse(theme_policy)['script-src']).to_not include('from-theme.com') - expect(parse(theme_policy)['script-src']).to include('https://from-theme.net') - expect(parse(theme_policy)['worker-src']).to include('from-theme.com') + expect(parse(theme_policy)["script-src"]).to_not include("from-theme.com") + expect(parse(theme_policy)["script-src"]).to include("https://from-theme.net") + expect(parse(theme_policy)["worker-src"]).to include("from-theme.com") theme.destroy! - expect(parse(theme_policy)['script-src']).to_not include('https://from-theme.net') - expect(parse(theme_policy)['worker-src']).to_not include('from-theme.com') + expect(parse(theme_policy)["script-src"]).to_not include("https://from-theme.net") + expect(parse(theme_policy)["worker-src"]).to_not include("from-theme.com") end - it 'can be extended by theme modifiers' do + it "can be extended by theme modifiers" do policy # call this first to make sure further actions clear the cache - theme.theme_modifier_set.csp_extensions = ["script-src: https://from-theme-flag.script", "worker-src: from-theme-flag.worker"] + theme.theme_modifier_set.csp_extensions = [ + "script-src: https://from-theme-flag.script", + "worker-src: from-theme-flag.worker", + ] theme.save! child_theme = Fabricate(:theme, component: true) theme.add_relative_theme!(:child, child_theme) - child_theme.theme_modifier_set.csp_extensions = ["script-src: https://child-theme-flag.script", "worker-src: child-theme-flag.worker"] + child_theme.theme_modifier_set.csp_extensions = [ + "script-src: https://child-theme-flag.script", + "worker-src: child-theme-flag.worker", + ] child_theme.save! - expect(parse(theme_policy)['script-src']).to include('https://from-theme-flag.script') - expect(parse(theme_policy)['script-src']).to include('https://child-theme-flag.script') - expect(parse(theme_policy)['worker-src']).to include('from-theme-flag.worker') - expect(parse(theme_policy)['worker-src']).to include('child-theme-flag.worker') + expect(parse(theme_policy)["script-src"]).to include("https://from-theme-flag.script") + expect(parse(theme_policy)["script-src"]).to include("https://child-theme-flag.script") + expect(parse(theme_policy)["worker-src"]).to include("from-theme-flag.worker") + expect(parse(theme_policy)["worker-src"]).to include("child-theme-flag.worker") theme.destroy! child_theme.destroy! - expect(parse(theme_policy)['script-src']).to_not include('https://from-theme-flag.script') - expect(parse(theme_policy)['worker-src']).to_not include('from-theme-flag.worker') - expect(parse(theme_policy)['worker-src']).to_not include('from-theme-flag.worker') - expect(parse(theme_policy)['worker-src']).to_not include('child-theme-flag.worker') + expect(parse(theme_policy)["script-src"]).to_not include("https://from-theme-flag.script") + expect(parse(theme_policy)["worker-src"]).to_not include("from-theme-flag.worker") + expect(parse(theme_policy)["worker-src"]).to_not include("from-theme-flag.worker") + expect(parse(theme_policy)["worker-src"]).to_not include("child-theme-flag.worker") end - it 'is extended automatically when themes reference external scripts' do + it "is extended automatically when themes reference external scripts" do policy # call this first to make sure further actions clear the cache theme.set_field(target: :common, name: "header", value: <<~HTML) @@ -350,30 +367,35 @@ RSpec.describe ContentSecurityPolicy do theme.set_field(target: :desktop, name: "header", value: "") theme.save! - expect(parse(theme_policy)['script-src']).to include('https://example.com/myscript.js') - expect(parse(theme_policy)['script-src']).to include('https://example.com/myscript2.js') - expect(parse(theme_policy)['script-src']).not_to include('?') - expect(parse(theme_policy)['script-src']).to include('example2.com/protocol-less-script.js') - expect(parse(theme_policy)['script-src']).not_to include('domain-only.com') - expect(parse(theme_policy)['script-src']).not_to include(a_string_matching /^\/theme-javascripts/) + expect(parse(theme_policy)["script-src"]).to include("https://example.com/myscript.js") + expect(parse(theme_policy)["script-src"]).to include("https://example.com/myscript2.js") + expect(parse(theme_policy)["script-src"]).not_to include("?") + expect(parse(theme_policy)["script-src"]).to include("example2.com/protocol-less-script.js") + expect(parse(theme_policy)["script-src"]).not_to include("domain-only.com") + expect(parse(theme_policy)["script-src"]).not_to include( + a_string_matching %r{^/theme-javascripts} + ) theme.destroy! - expect(parse(theme_policy)['script-src']).to_not include('https://example.com/myscript.js') + expect(parse(theme_policy)["script-src"]).to_not include("https://example.com/myscript.js") end end - it 'can be extended by site setting' do - SiteSetting.content_security_policy_script_src = 'from-site-setting.com|from-site-setting.net' + it "can be extended by site setting" do + SiteSetting.content_security_policy_script_src = "from-site-setting.com|from-site-setting.net" - expect(parse(policy)['script-src']).to include('from-site-setting.com', 'from-site-setting.net') + expect(parse(policy)["script-src"]).to include("from-site-setting.com", "from-site-setting.net") end def parse(csp_string) - csp_string.split(';').map do |policy| - directive, *sources = policy.split - [directive, sources] - end.to_h + csp_string + .split(";") + .map do |policy| + directive, *sources = policy.split + [directive, sources] + end + .to_h end def policy(theme_id = nil, path_info: "/") diff --git a/spec/lib/cooked_post_processor_spec.rb b/spec/lib/cooked_post_processor_spec.rb index c703ea1909..f55ef0e689 100644 --- a/spec/lib/cooked_post_processor_spec.rb +++ b/spec/lib/cooked_post_processor_spec.rb @@ -5,15 +5,13 @@ require "file_store/s3_store" RSpec.describe CookedPostProcessor do fab!(:upload) { Fabricate(:upload) } - fab!(:large_image_upload) { Fabricate(:large_image_upload) } + fab!(:large_image_upload) { Fabricate(:large_image_upload) } let(:upload_path) { Discourse.store.upload_path } describe "#post_process" do - fab!(:post) do - Fabricate(:post, raw: <<~RAW) + fab!(:post) { Fabricate(:post, raw: <<~RAW) } RAW - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } let(:post_process) { sequence("post_process") } @@ -27,21 +25,16 @@ RSpec.describe CookedPostProcessor do expect(UploadReference.exists?(target: post, upload: upload)).to eq(true) end - describe 'when post contains oneboxes and inline oneboxes' do - let(:url_hostname) { 'meta.discourse.org' } + describe "when post contains oneboxes and inline oneboxes" do + let(:url_hostname) { "meta.discourse.org" } - let(:url) do - "https://#{url_hostname}/t/mini-inline-onebox-support-rfc/66400" - end + let(:url) { "https://#{url_hostname}/t/mini-inline-onebox-support-rfc/66400" } - let(:not_oneboxed_url) do - "https://#{url_hostname}/t/random-url" - end + let(:not_oneboxed_url) { "https://#{url_hostname}/t/random-url" } - let(:title) { 'some title' } + let(:title) { "some title" } - let(:post) do - Fabricate(:post, raw: <<~RAW) + let(:post) { Fabricate(:post, raw: <<~RAW) } #{url} This is a #{url} with path @@ -52,7 +45,6 @@ RSpec.describe CookedPostProcessor do #{url} RAW - end before do SiteSetting.enable_inline_onebox_on_all_domains = true @@ -71,10 +63,8 @@ RSpec.describe CookedPostProcessor do HTML Oneboxer.stubs(:cached_onebox).with(not_oneboxed_url).returns(nil) - %i{head get}.each do |method| - stub_request(method, url).to_return( - status: 200, - body: <<~RAW + %i[head get].each do |method| + stub_request(method, url).to_return(status: 200, body: <<~RAW) #{title} @@ -83,7 +73,6 @@ RSpec.describe CookedPostProcessor do RAW - ) end end @@ -92,47 +81,50 @@ RSpec.describe CookedPostProcessor do Oneboxer.invalidate(url) end - it 'should respect SiteSetting.max_oneboxes_per_post' do + it "should respect SiteSetting.max_oneboxes_per_post" do SiteSetting.max_oneboxes_per_post = 2 SiteSetting.add_rel_nofollow_to_user_content = false cpp.post_process - expect(cpp.html).to have_tag('a', - with: { href: url, class: "inline-onebox" }, + expect(cpp.html).to have_tag( + "a", + with: { + href: url, + class: "inline-onebox", + }, text: title, - count: 2 + count: 2, ) - expect(cpp.html).to have_tag('aside.onebox a', text: title, count: 1) + expect(cpp.html).to have_tag("aside.onebox a", text: title, count: 1) - expect(cpp.html).to have_tag('aside.onebox a', - text: url_hostname, - count: 1 - ) + expect(cpp.html).to have_tag("aside.onebox a", text: url_hostname, count: 1) - expect(cpp.html).to have_tag('a', - without: { class: "inline-onebox-loading" }, - text: not_oneboxed_url, - count: 1 - ) - - expect(cpp.html).to have_tag('a', + expect(cpp.html).to have_tag( + "a", without: { - class: 'onebox' + class: "inline-onebox-loading", }, text: not_oneboxed_url, - count: 1 + count: 1, + ) + + expect(cpp.html).to have_tag( + "a", + without: { + class: "onebox", + }, + text: not_oneboxed_url, + count: 1, ) end end - describe 'when post contains inline oneboxes' do - before do - SiteSetting.enable_inline_onebox_on_all_domains = true - end + describe "when post contains inline oneboxes" do + before { SiteSetting.enable_inline_onebox_on_all_domains = true } - describe 'internal links' do + describe "internal links" do fab!(:topic) { Fabricate(:topic) } fab!(:post) { Fabricate(:post, raw: "Hello #{topic.url}") } let(:url) { topic.url } @@ -140,52 +132,49 @@ RSpec.describe CookedPostProcessor do it "includes the topic title" do cpp.post_process - expect(cpp.html).to have_tag('a', - with: { href: UrlHelper.cook_url(url) }, - without: { class: "inline-onebox-loading" }, + expect(cpp.html).to have_tag( + "a", + with: { + href: UrlHelper.cook_url(url), + }, + without: { + class: "inline-onebox-loading", + }, text: topic.title, - count: 1 + count: 1, ) topic.update!(title: "Updated to something else") cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) cpp.post_process - expect(cpp.html).to have_tag('a', - with: { href: UrlHelper.cook_url(url) }, - without: { class: "inline-onebox-loading" }, + expect(cpp.html).to have_tag( + "a", + with: { + href: UrlHelper.cook_url(url), + }, + without: { + class: "inline-onebox-loading", + }, text: topic.title, - count: 1 + count: 1, ) end end - describe 'external links' do - let(:url_with_path) do - 'https://meta.discourse.org/t/mini-inline-onebox-support-rfc/66400' - end + describe "external links" do + let(:url_with_path) { "https://meta.discourse.org/t/mini-inline-onebox-support-rfc/66400" } - let(:url_with_query_param) do - 'https://meta.discourse.org?a' - end + let(:url_with_query_param) { "https://meta.discourse.org?a" } - let(:url_no_path) do - 'https://meta.discourse.org/' - end + let(:url_no_path) { "https://meta.discourse.org/" } - let(:urls) do - [ - url_with_path, - url_with_query_param, - url_no_path - ] - end + let(:urls) { [url_with_path, url_with_query_param, url_no_path] } - let(:title) { 'some title' } + let(:title) { "some title" } let(:escaped_title) { CGI.escapeHTML(title) } - let(:post) do - Fabricate(:post, raw: <<~RAW) + let(:post) { Fabricate(:post, raw: <<~RAW) } This is a #{url_with_path} topic This should not be inline #{url_no_path} oneboxed @@ -194,55 +183,65 @@ RSpec.describe CookedPostProcessor do - #{url_with_query_param} RAW - end - let(:staff_post) do - Fabricate(:post, user: Fabricate(:admin), raw: <<~RAW) + let(:staff_post) { Fabricate(:post, user: Fabricate(:admin), raw: <<~RAW) } This is a #{url_with_path} topic RAW - end before do urls.each do |url| stub_request(:get, url).to_return( status: 200, - body: "#{escaped_title}" + body: "#{escaped_title}", ) end end - after do - urls.each { |url| InlineOneboxer.invalidate(url) } - end + after { urls.each { |url| InlineOneboxer.invalidate(url) } } - it 'should convert the right links to inline oneboxes' do + it "should convert the right links to inline oneboxes" do cpp.post_process html = cpp.html - expect(html).to_not have_tag('a', - with: { href: url_no_path }, - without: { class: "inline-onebox-loading" }, - text: title + expect(html).to_not have_tag( + "a", + with: { + href: url_no_path, + }, + without: { + class: "inline-onebox-loading", + }, + text: title, ) - expect(html).to have_tag('a', - with: { href: url_with_path }, - without: { class: "inline-onebox-loading" }, + expect(html).to have_tag( + "a", + with: { + href: url_with_path, + }, + without: { + class: "inline-onebox-loading", + }, text: title, - count: 2 + count: 2, ) - expect(html).to have_tag('a', - with: { href: url_with_query_param }, - without: { class: "inline-onebox-loading" }, + expect(html).to have_tag( + "a", + with: { + href: url_with_query_param, + }, + without: { + class: "inline-onebox-loading", + }, text: title, - count: 1 + count: 1, ) expect(html).to have_tag("a[rel='noopener nofollow ugc']") end - it 'removes nofollow if user is staff/tl3' do + it "removes nofollow if user is staff/tl3" do cpp = CookedPostProcessor.new(staff_post, invalidate_oneboxes: true) cpp.post_process expect(cpp.html).to_not have_tag("a[rel='noopener nofollow ugc']") @@ -251,15 +250,13 @@ RSpec.describe CookedPostProcessor do end context "when processing images" do - before do - SiteSetting.responsive_post_image_sizes = "" - end + before { SiteSetting.responsive_post_image_sizes = "" } context "with responsive images" do before { SiteSetting.responsive_post_image_sizes = "1|1.5|3" } it "includes responsive images on demand" do - upload.update!(width: 2000, height: 1500, filesize: 10000, dominant_color: "FFFFFF") + upload.update!(width: 2000, height: 1500, filesize: 10_000, dominant_color: "FFFFFF") post = Fabricate(:post, raw: "hello ") # fake some optimized images @@ -269,9 +266,9 @@ RSpec.describe CookedPostProcessor do height: 500, upload_id: upload.id, sha1: SecureRandom.hex, - extension: '.jpg', + extension: ".jpg", filesize: 500, - version: OptimizedImage::VERSION + version: OptimizedImage::VERSION, ) # fake 3x optimized image, we lose 2 pixels here over original due to rounding on downsize @@ -281,8 +278,8 @@ RSpec.describe CookedPostProcessor do height: 1500, upload_id: upload.id, sha1: SecureRandom.hex, - extension: '.jpg', - filesize: 800 + extension: ".jpg", + filesize: 800, ) cpp = CookedPostProcessor.new(post) @@ -294,7 +291,9 @@ RSpec.describe CookedPostProcessor do expect(html).to include(%Q|data-dominant-color="FFFFFF"|) # 1.5x is skipped cause we have a missing thumb - expect(html).to include("srcset=\"//test.localhost/#{upload_path}/666x500.jpg, //test.localhost/#{upload_path}/1998x1500.jpg 3x\"") + expect(html).to include( + "srcset=\"//test.localhost/#{upload_path}/666x500.jpg, //test.localhost/#{upload_path}/1998x1500.jpg 3x\"", + ) expect(html).to include("src=\"//test.localhost/#{upload_path}/666x500.jpg\"") # works with CDN @@ -307,23 +306,25 @@ RSpec.describe CookedPostProcessor do html = cpp.html expect(html).to include(%Q|data-dominant-color="FFFFFF"|) - expect(html).to include("srcset=\"//cdn.localhost/#{upload_path}/666x500.jpg, //cdn.localhost/#{upload_path}/1998x1500.jpg 3x\"") + expect(html).to include( + "srcset=\"//cdn.localhost/#{upload_path}/666x500.jpg, //cdn.localhost/#{upload_path}/1998x1500.jpg 3x\"", + ) expect(html).to include("src=\"//cdn.localhost/#{upload_path}/666x500.jpg\"") end it "doesn't include response images for cropped images" do - upload.update!(width: 200, height: 4000, filesize: 12345) + upload.update!(width: 200, height: 4000, filesize: 12_345) post = Fabricate(:post, raw: "hello ") # fake some optimized images OptimizedImage.create!( - url: 'http://a.b.c/200x500.jpg', + url: "http://a.b.c/200x500.jpg", width: 200, height: 500, upload_id: upload.id, sha1: SecureRandom.hex, - extension: '.jpg', - filesize: 500 + extension: ".jpg", + filesize: 500, ) cpp = CookedPostProcessor.new(post) @@ -336,8 +337,8 @@ RSpec.describe CookedPostProcessor do shared_examples "leave dimensions alone" do it "doesn't use them" do - expect(cpp.html).to match(/src="http:\/\/foo.bar\/image.png" width="" height=""/) - expect(cpp.html).to match(/src="http:\/\/domain.com\/picture.jpg" width="50" height="42"/) + expect(cpp.html).to match(%r{src="http://foo.bar/image.png" width="" height=""}) + expect(cpp.html).to match(%r{src="http://domain.com/picture.jpg" width="50" height="42"}) expect(cpp).to be_dirty end end @@ -352,11 +353,15 @@ RSpec.describe CookedPostProcessor do end context "when valid" do - let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 111, "height" => 222 } } } + let(:image_sizes) do + { "http://foo.bar/image.png" => { "width" => 111, "height" => 222 } } + end it "uses them" do - expect(cpp.html).to match(/src="http:\/\/foo.bar\/image.png" width="111" height="222"/) - expect(cpp.html).to match(/src="http:\/\/domain.com\/picture.jpg" width="50" height="42"/) + expect(cpp.html).to match(%r{src="http://foo.bar/image.png" width="111" height="222"}) + expect(cpp.html).to match( + %r{src="http://domain.com/picture.jpg" width="50" height="42"}, + ) expect(cpp).to be_dirty end end @@ -375,17 +380,14 @@ RSpec.describe CookedPostProcessor do let(:image_sizes) { { "http://foo.bar/image.png" => { "width" => 0, "height" => 0 } } } include_examples "leave dimensions alone" end - end context "with unsized images" do fab!(:upload) { Fabricate(:image_upload, width: 123, height: 456) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post) } @@ -394,17 +396,14 @@ RSpec.describe CookedPostProcessor do expect(cpp.html).to match(/width="123" height="456"/) expect(cpp).to be_dirty end - end context "with large images" do fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } @@ -423,15 +422,20 @@ RSpec.describe CookedPostProcessor do expect(cpp).to be_dirty end - context 'when image is inside onebox' do - let(:url) { 'https://image.com/my-avatar' } + context "when image is inside onebox" do + let(:url) { "https://image.com/my-avatar" } let(:post) { Fabricate(:post, raw: url) } before do - Oneboxer.stubs(:onebox).with(url, anything).returns("") + Oneboxer + .stubs(:onebox) + .with(url, anything) + .returns( + "", + ) end - it 'should not add lightbox' do + it "should not add lightbox" do FastImage.expects(:size).returns([1750, 2000]) cpp.post_process @@ -442,12 +446,15 @@ RSpec.describe CookedPostProcessor do end end - context 'when image is an svg' do + context "when image is an svg" do fab!(:post) do - Fabricate(:post, raw: "") + Fabricate( + :post, + raw: "", + ) end - it 'should not add lightbox' do + it "should not add lightbox" do FastImage.expects(:size).returns([1750, 2000]) cpp.post_process @@ -457,17 +464,23 @@ RSpec.describe CookedPostProcessor do HTML end - context 'when image src is an URL' do + context "when image src is an URL" do let(:post) do - Fabricate(:post, raw: "") + Fabricate( + :post, + raw: + "", + ) end - it 'should not add lightbox' do + it "should not add lightbox" do FastImage.expects(:size).returns([1750, 2000]) cpp.post_process - expect(cpp.html).to match_html("

    ") + expect(cpp.html).to match_html( + "

    ", + ) end end end @@ -495,19 +508,23 @@ RSpec.describe CookedPostProcessor do Fabricate(:post, raw: "![large.png|#{optimized_size}](#{upload.short_url})") end - let(:cooked_html) do - <<~HTML + let(:cooked_html) { <<~HTML }

    HTML - end context "when the upload is attached to the correct post" do before do FastImage.expects(:size).returns([1750, 2000]) OptimizedImage.expects(:resize).returns(true) - Discourse.store.class.any_instance.expects(:has_been_uploaded?).at_least_once.returns(true) + Discourse + .store + .class + .any_instance + .expects(:has_been_uploaded?) + .at_least_once + .returns(true) upload.update(secure: true, access_control_post: post) end @@ -540,17 +557,13 @@ RSpec.describe CookedPostProcessor do context "with tall images > default aspect ratio" do fab!(:upload) { Fabricate(:image_upload, width: 500, height: 2200) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } - before do - SiteSetting.create_thumbnails = true - end + before { SiteSetting.create_thumbnails = true } it "resizes the image instead of crop" do cpp.post_process @@ -558,23 +571,18 @@ RSpec.describe CookedPostProcessor do expect(cpp.html).to match(/width="113" height="500">/) expect(cpp).to be_dirty end - end context "with taller images < default aspect ratio" do fab!(:upload) { Fabricate(:image_upload, width: 500, height: 2300) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } - before do - SiteSetting.create_thumbnails = true - end + before { SiteSetting.create_thumbnails = true } it "crops the image" do cpp.post_process @@ -582,23 +590,18 @@ RSpec.describe CookedPostProcessor do expect(cpp.html).to match(/width="500" height="500">/) expect(cpp).to be_dirty end - end context "with iPhone X screenshots" do fab!(:upload) { Fabricate(:image_upload, width: 1125, height: 2436) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } - before do - SiteSetting.create_thumbnails = true - end + before { SiteSetting.create_thumbnails = true } it "crops the image" do cpp.post_process @@ -609,23 +612,23 @@ RSpec.describe CookedPostProcessor do expect(cpp).to be_dirty end - end context "with large images when using subfolders" do fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } before do set_subfolder "/subfolder" - stub_request(:get, "http://#{Discourse.current_hostname}/subfolder#{upload.url}").to_return(status: 200, body: File.new(Discourse.store.path_for(upload))) + stub_request( + :get, + "http://#{Discourse.current_hostname}/subfolder#{upload.url}", + ).to_return(status: 200, body: File.new(Discourse.store.path_for(upload))) SiteSetting.max_image_height = 2000 SiteSetting.create_thumbnails = true @@ -634,7 +637,7 @@ RSpec.describe CookedPostProcessor do it "generates overlay information" do cpp.post_process - expect(cpp.html). to match_html <<~HTML + expect(cpp.html).to match_html <<~HTML

    HTML @@ -649,17 +652,14 @@ RSpec.describe CookedPostProcessor do

    HTML end - end context "with title and alt" do fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } RED HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } @@ -677,17 +677,14 @@ RSpec.describe CookedPostProcessor do expect(cpp).to be_dirty end - end context "with title only" do fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } @@ -705,17 +702,14 @@ RSpec.describe CookedPostProcessor do expect(cpp).to be_dirty end - end context "with alt only" do fab!(:upload) { Fabricate(:image_upload, width: 1750, height: 2000) } - fab!(:post) do - Fabricate(:post, raw: <<~HTML) + fab!(:post) { Fabricate(:post, raw: <<~HTML) } RED HTML - end let(:cpp) { CookedPostProcessor.new(post, disable_dominant_color: true) } @@ -733,7 +727,6 @@ RSpec.describe CookedPostProcessor do expect(cpp).to be_dirty end - end context "with topic image" do @@ -839,29 +832,29 @@ RSpec.describe CookedPostProcessor do let(:cpp) { CookedPostProcessor.new(post) } it "returns the size when width and height are specified" do - img = { 'src' => 'http://foo.bar/image3.png', 'width' => 50, 'height' => 70 } + img = { "src" => "http://foo.bar/image3.png", "width" => 50, "height" => 70 } expect(cpp.get_size_from_attributes(img)).to eq([50, 70]) end it "returns the size when width and height are floats" do - img = { 'src' => 'http://foo.bar/image3.png', 'width' => 50.2, 'height' => 70.1 } + img = { "src" => "http://foo.bar/image3.png", "width" => 50.2, "height" => 70.1 } expect(cpp.get_size_from_attributes(img)).to eq([50, 70]) end it "resizes when only width is specified" do - img = { 'src' => 'http://foo.bar/image3.png', 'width' => 100 } + img = { "src" => "http://foo.bar/image3.png", "width" => 100 } FastImage.expects(:size).returns([200, 400]) expect(cpp.get_size_from_attributes(img)).to eq([100, 200]) end it "resizes when only height is specified" do - img = { 'src' => 'http://foo.bar/image3.png', 'height' => 100 } + img = { "src" => "http://foo.bar/image3.png", "height" => 100 } FastImage.expects(:size).returns([100, 300]) expect(cpp.get_size_from_attributes(img)).to eq([33, 100]) end it "doesn't raise an error with a weird url" do - img = { 'src' => nil, 'height' => 100 } + img = { "src" => nil, "height" => 100 } expect(cpp.get_size_from_attributes(img)).to be_nil end end @@ -940,7 +933,9 @@ RSpec.describe CookedPostProcessor do it "returns a generic name for pasted images" do upload = build(:upload, original_filename: "blob.png") - expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq(I18n.t('upload.pasted_image_filename')) + expect(cpp.get_filename(upload, "http://domain.com/image.png")).to eq( + I18n.t("upload.pasted_image_filename"), + ) end end @@ -952,10 +947,10 @@ RSpec.describe CookedPostProcessor do cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) + doc = Nokogiri::HTML5.fragment(cpp.html) - expect(doc.css('.lightbox-wrapper').size).to eq(1) - expect(doc.css('img').first['srcset']).to_not eq(nil) + expect(doc.css(".lightbox-wrapper").size).to eq(1) + expect(doc.css("img").first["srcset"]).to_not eq(nil) end it "processes animated images correctly" do @@ -968,35 +963,52 @@ RSpec.describe CookedPostProcessor do cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) - expect(doc.css('.lightbox-wrapper').size).to eq(0) - expect(doc.css('img').first['src']).to include(upload.url) - expect(doc.css('img').first['srcset']).to eq(nil) - expect(doc.css('img.animated').size).to eq(1) + doc = Nokogiri::HTML5.fragment(cpp.html) + expect(doc.css(".lightbox-wrapper").size).to eq(0) + expect(doc.css("img").first["src"]).to include(upload.url) + expect(doc.css("img").first["srcset"]).to eq(nil) + expect(doc.css("img.animated").size).to eq(1) end context "with giphy/tenor images" do before do - CookedPostProcessor.any_instance.stubs(:get_size).with("https://media2.giphy.com/media/7Oifk90VrCdNe/giphy.webp").returns([311, 280]) - CookedPostProcessor.any_instance.stubs(:get_size).with("https://media1.tenor.com/images/20c7ddd5e84c7427954f430439c5209d/tenor.gif").returns([833, 104]) + CookedPostProcessor + .any_instance + .stubs(:get_size) + .with("https://media2.giphy.com/media/7Oifk90VrCdNe/giphy.webp") + .returns([311, 280]) + CookedPostProcessor + .any_instance + .stubs(:get_size) + .with("https://media1.tenor.com/images/20c7ddd5e84c7427954f430439c5209d/tenor.gif") + .returns([833, 104]) end it "marks giphy images as animated" do - post = Fabricate(:post, raw: "![tennis-gif|311x280](https://media2.giphy.com/media/7Oifk90VrCdNe/giphy.webp)") + post = + Fabricate( + :post, + raw: "![tennis-gif|311x280](https://media2.giphy.com/media/7Oifk90VrCdNe/giphy.webp)", + ) cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) - expect(doc.css('img.animated').size).to eq(1) + doc = Nokogiri::HTML5.fragment(cpp.html) + expect(doc.css("img.animated").size).to eq(1) end it "marks giphy images as animated" do - post = Fabricate(:post, raw: "![cat](https://media1.tenor.com/images/20c7ddd5e84c7427954f430439c5209d/tenor.gif)") + post = + Fabricate( + :post, + raw: + "![cat](https://media1.tenor.com/images/20c7ddd5e84c7427954f430439c5209d/tenor.gif)", + ) cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) - expect(doc.css('img.animated').size).to eq(1) + doc = Nokogiri::HTML5.fragment(cpp.html) + expect(doc.css("img.animated").size).to eq(1) end end @@ -1010,24 +1022,27 @@ RSpec.describe CookedPostProcessor do cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) - expect(doc.css('.lightbox-wrapper').size).to eq(0) - expect(doc.css('img').first['srcset']).to_not eq(nil) + doc = Nokogiri::HTML5.fragment(cpp.html) + expect(doc.css(".lightbox-wrapper").size).to eq(0) + expect(doc.css("img").first["srcset"]).to_not eq(nil) end it "optimizes images in Onebox" do - Oneboxer.expects(:onebox) + Oneboxer + .expects(:onebox) .with("https://discourse.org", anything) - .returns("") + .returns( + "", + ) post = Fabricate(:post, raw: "https://discourse.org") cpp = CookedPostProcessor.new(post, disable_dominant_color: true) cpp.post_process - doc = Nokogiri::HTML5::fragment(cpp.html) - expect(doc.css('.lightbox-wrapper').size).to eq(0) - expect(doc.css('img').first['srcset']).to_not eq(nil) + doc = Nokogiri::HTML5.fragment(cpp.html) + expect(doc.css(".lightbox-wrapper").size).to eq(0) + expect(doc.css("img").first["srcset"]).to_not eq(nil) end end @@ -1038,7 +1053,12 @@ RSpec.describe CookedPostProcessor do before do Oneboxer .expects(:onebox) - .with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id) + .with( + "http://www.youtube.com/watch?v=9bZkp7q19f0", + invalidate_oneboxes: true, + user_id: nil, + category_id: post.topic.category_id, + ) .returns("
    GANGNAM STYLE
    ") cpp.post_process_oneboxes @@ -1050,29 +1070,41 @@ RSpec.describe CookedPostProcessor do end describe "replacing downloaded onebox image" do - let(:url) { 'https://image.com/my-avatar' } - let(:image_url) { 'https://image.com/avatar.png' } + let(:url) { "https://image.com/my-avatar" } + let(:image_url) { "https://image.com/avatar.png" } it "successfully replaces the image" do - Oneboxer.stubs(:onebox).with(url, anything).returns("") + Oneboxer + .stubs(:onebox) + .with(url, anything) + .returns("") post = Fabricate(:post, raw: url) upload.update!(url: "https://test.s3.amazonaws.com/something.png") - PostHotlinkedMedia.create!(url: "//image.com/avatar.png", post: post, status: 'downloaded', upload: upload) + PostHotlinkedMedia.create!( + url: "//image.com/avatar.png", + post: post, + status: "downloaded", + upload: upload, + ) cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) stub_image_size(width: 100, height: 200) cpp.post_process_oneboxes - expect(cpp.doc.to_s).to eq("

    ") + expect(cpp.doc.to_s).to eq( + "

    ", + ) upload.destroy! cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) stub_image_size(width: 100, height: 200) cpp.post_process_oneboxes - expect(cpp.doc.to_s).to eq("

    ") + expect(cpp.doc.to_s).to eq( + "

    ", + ) Oneboxer.unstub(:onebox) end @@ -1086,12 +1118,20 @@ RSpec.describe CookedPostProcessor do end it "does not use the direct URL, uses the cooked URL instead (because of the private ACL preventing w/h fetch)" do - Oneboxer.stubs(:onebox).with(url, anything).returns("") + Oneboxer + .stubs(:onebox) + .with(url, anything) + .returns("") post = Fabricate(:post, raw: url) upload.update!(url: "https://test.s3.amazonaws.com/something.png") - PostHotlinkedMedia.create!(url: "//image.com/avatar.png", post: post, status: 'downloaded', upload: upload) + PostHotlinkedMedia.create!( + url: "//image.com/avatar.png", + post: post, + status: "downloaded", + upload: upload, + ) cooked_url = "https://localhost/secure-uploads/test.png" UrlHelper.expects(:cook_url).with(upload.url, secure: true).returns(cooked_url) @@ -1100,31 +1140,38 @@ RSpec.describe CookedPostProcessor do stub_image_size(width: 100, height: 200) cpp.post_process_oneboxes - expect(cpp.doc.to_s).to eq("

    ") + expect(cpp.doc.to_s).to eq( + "

    ", + ) end end end it "replaces large image placeholder" do SiteSetting.max_image_size_kb = 4096 - url = 'https://image.com/my-avatar' - image_url = 'https://image.com/avatar.png' + url = "https://image.com/my-avatar" + image_url = "https://image.com/avatar.png" - Oneboxer.stubs(:onebox).with(url, anything).returns("") + Oneboxer + .stubs(:onebox) + .with(url, anything) + .returns("") post = Fabricate(:post, raw: url) - PostHotlinkedMedia.create!(url: "//image.com/avatar.png", post: post, status: 'too_large') + PostHotlinkedMedia.create!(url: "//image.com/avatar.png", post: post, status: "too_large") cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) cpp.post_process expect(cpp.doc.to_s).to match(/
    /) - expect(cpp.doc.to_s).to include(I18n.t("upload.placeholders.too_large_humanized", max_size: "4 MB")) + expect(cpp.doc.to_s).to include( + I18n.t("upload.placeholders.too_large_humanized", max_size: "4 MB"), + ) end it "removes large images from onebox" do - url = 'https://example.com/article' + url = "https://example.com/article" Oneboxer.stubs(:onebox).with(url, anything).returns <<~HTML
    ") + Oneboxer + .expects(:onebox) + .with( + "http://www.youtube.com/watch?v=9bZkp7q19f0", + invalidate_oneboxes: true, + user_id: nil, + category_id: post.topic.category_id, + ) + .returns( + "
    ", + ) cpp.post_process_oneboxes - expect(cpp.html).to match_html('') + expect(cpp.html).to match_html( + '', + ) end it "applies aspect ratio when wrapped in link" do - Oneboxer.expects(:onebox) - .with("http://www.youtube.com/watch?v=9bZkp7q19f0", invalidate_oneboxes: true, user_id: nil, category_id: post.topic.category_id) - .returns("
    ") + Oneboxer + .expects(:onebox) + .with( + "http://www.youtube.com/watch?v=9bZkp7q19f0", + invalidate_oneboxes: true, + user_id: nil, + category_id: post.topic.category_id, + ) + .returns( + "
    ") + expect(DiscourseDiff.new("", "").side_by_side_markdown).to eq( + "
    ", + ) end it "properly escape html tags" do before = "" after = "\"" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    <img src="//domain.com/image.png>"
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    <img src="//domain.com/image.png>"
    ", + ) end it "returns the diffed content on both columns when there is no difference" do before = after = "this is a paragraph" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    this is a paragraphthis is a paragraph
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    this is a paragraphthis is a paragraph
    ", + ) end it "adds tags around added text on the second column" do before = "this is a paragraph" after = "this is a great paragraph" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    this is a paragraphthis is a great paragraph
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    this is a paragraphthis is a great paragraph
    ", + ) end it "adds tags around removed text on the first column" do before = "this is a great paragraph" after = "this is a paragraph" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    this is a great paragraphthis is a paragraph
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    this is a great paragraphthis is a paragraph
    ", + ) end it "adds .diff-ins class when a paragraph is added" do before = "this is the first paragraph" after = "this is the first paragraph\nthis is the second paragraph" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    this is the first paragraphthis is the first paragraph\nthis is the second paragraph
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    this is the first paragraphthis is the first paragraph\nthis is the second paragraph
    ", + ) end it "adds .diff-del class when a paragraph is removed" do before = "this is the first paragraph\nthis is the second paragraph" after = "this is the second paragraph" - expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq("
    this is the first paragraph\n
    this is the second paragraphthis is the second paragraph
    ") + expect(DiscourseDiff.new(before, after).side_by_side_markdown).to eq( + "
    this is the first paragraph\n
    this is the second paragraphthis is the second paragraph
    ", + ) end - end - end diff --git a/spec/lib/discourse_event_spec.rb b/spec/lib/discourse_event_spec.rb index b3e49e5f96..33a9059fce 100644 --- a/spec/lib/discourse_event_spec.rb +++ b/spec/lib/discourse_event_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe DiscourseEvent do - describe "#events" do it "defaults to {}" do begin @@ -20,86 +19,74 @@ RSpec.describe DiscourseEvent do end end - context 'when calling events' do + context "when calling events" do + let(:harvey) { OpenStruct.new(name: "Harvey Dent", job: "District Attorney") } - let(:harvey) { - OpenStruct.new( - name: 'Harvey Dent', - job: 'District Attorney' - ) - } + let(:event_handler) { Proc.new { |user| user.name = "Two Face" } } - let(:event_handler) do - Proc.new { |user| user.name = 'Two Face' } - end + before { DiscourseEvent.on(:acid_face, &event_handler) } - before do - DiscourseEvent.on(:acid_face, &event_handler) - end - - after do - DiscourseEvent.off(:acid_face, &event_handler) - end - - context 'when event does not exist' do + after { DiscourseEvent.off(:acid_face, &event_handler) } + context "when event does not exist" do it "does not raise an error" do DiscourseEvent.trigger(:missing_event) end - end - context 'when single event exists' do - + context "when single event exists" do it "doesn't raise an error" do DiscourseEvent.trigger(:acid_face, harvey) end it "changes the name" do DiscourseEvent.trigger(:acid_face, harvey) - expect(harvey.name).to eq('Two Face') + expect(harvey.name).to eq("Two Face") end - end - context 'when multiple events exist' do - - let(:event_handler_2) do - Proc.new { |user| user.job = 'Supervillain' } - end + context "when multiple events exist" do + let(:event_handler_2) { Proc.new { |user| user.job = "Supervillain" } } before do DiscourseEvent.on(:acid_face, &event_handler_2) DiscourseEvent.trigger(:acid_face, harvey) end - after do - DiscourseEvent.off(:acid_face, &event_handler_2) - end + after { DiscourseEvent.off(:acid_face, &event_handler_2) } - it 'triggers both events' do - expect(harvey.job).to eq('Supervillain') - expect(harvey.name).to eq('Two Face') + it "triggers both events" do + expect(harvey.job).to eq("Supervillain") + expect(harvey.name).to eq("Two Face") end end - describe '#all_off' do + describe "#all_off" do + let(:event_handler_2) { Proc.new { |user| user.job = "Supervillain" } } - let(:event_handler_2) do - Proc.new { |user| user.job = 'Supervillain' } - end + before { DiscourseEvent.on(:acid_face, &event_handler_2) } - before do - DiscourseEvent.on(:acid_face, &event_handler_2) - end - - it 'removes all handlers with a key' do - harvey.job = 'gardening' + it "removes all handlers with a key" do + harvey.job = "gardening" DiscourseEvent.all_off(:acid_face) DiscourseEvent.trigger(:acid_face, harvey) # Doesn't change anything - expect(harvey.job).to eq('gardening') + expect(harvey.job).to eq("gardening") end + end + end + it "allows using kwargs" do + begin + handler = + Proc.new do |name:, message:| + expect(name).to eq("Supervillain") + expect(message).to eq("Two Face") + end + + DiscourseEvent.on(:acid_face, &handler) + DiscourseEvent.trigger(:acid_face, name: "Supervillain", message: "Two Face") + ensure + DiscourseEvent.off(:acid_face, &handler) end end end diff --git a/spec/lib/discourse_hub_spec.rb b/spec/lib/discourse_hub_spec.rb index b80edb397b..591e7ccf23 100644 --- a/spec/lib/discourse_hub_spec.rb +++ b/spec/lib/discourse_hub_spec.rb @@ -1,22 +1,25 @@ # frozen_string_literal: true RSpec.describe DiscourseHub do - describe '.discourse_version_check' do - it 'should return just return the json that the hub returns' do - hub_response = { 'success' => 'OK', 'latest_version' => '0.8.1', 'critical_updates' => false } + describe ".discourse_version_check" do + it "should return just return the json that the hub returns" do + hub_response = { "success" => "OK", "latest_version" => "0.8.1", "critical_updates" => false } - stub_request(:get, (ENV['HUB_BASE_URL'] || "http://local.hub:3000/api") + "/version_check"). - with(query: DiscourseHub.version_check_payload). - to_return(status: 200, body: hub_response.to_json) + stub_request( + :get, + (ENV["HUB_BASE_URL"] || "http://local.hub:3000/api") + "/version_check", + ).with(query: DiscourseHub.version_check_payload).to_return( + status: 200, + body: hub_response.to_json, + ) expect(DiscourseHub.discourse_version_check).to eq(hub_response) end end - describe '.version_check_payload' do - - describe 'when Discourse Hub has not fetched stats since past 7 days' do - it 'should include stats' do + describe ".version_check_payload" do + describe "when Discourse Hub has not fetched stats since past 7 days" do + it "should include stats" do DiscourseHub.stats_fetched_at = 8.days.ago json = JSON.parse(DiscourseHub.version_check_payload.to_json) @@ -39,8 +42,8 @@ RSpec.describe DiscourseHub do end end - describe 'when Discourse Hub has fetched stats in past 7 days' do - it 'should not include stats' do + describe "when Discourse Hub has fetched stats in past 7 days" do + it "should not include stats" do DiscourseHub.stats_fetched_at = 2.days.ago json = JSON.parse(DiscourseHub.version_check_payload.to_json) @@ -55,9 +58,9 @@ RSpec.describe DiscourseHub do end end - describe 'when send_anonymize_stats is disabled' do - describe 'when Discourse Hub has not fetched stats for the past year' do - it 'should not include stats' do + describe "when send_anonymize_stats is disabled" do + describe "when Discourse Hub has not fetched stats for the past year" do + it "should not include stats" do DiscourseHub.stats_fetched_at = 1.year.ago SiteSetting.share_anonymized_statistics = false json = JSON.parse(DiscourseHub.version_check_payload.to_json) @@ -75,29 +78,27 @@ RSpec.describe DiscourseHub do end end - describe '.collection_action' do + describe ".collection_action" do before do @orig_logger = Rails.logger Rails.logger = @fake_logger = FakeLogger.new end - after do - Rails.logger = @orig_logger - end + after { Rails.logger = @orig_logger } - it 'should log correctly on error' do - stub_request(:get, (ENV['HUB_BASE_URL'] || "http://local.hub:3000/api") + '/test'). - to_return(status: 500, body: "", headers: {}) + it "should log correctly on error" do + stub_request(:get, (ENV["HUB_BASE_URL"] || "http://local.hub:3000/api") + "/test").to_return( + status: 500, + body: "", + headers: { + }, + ) - DiscourseHub.collection_action(:get, '/test') + DiscourseHub.collection_action(:get, "/test") - expect(@fake_logger.warnings).to eq([ - DiscourseHub.response_status_log_message('/test', 500), - ]) + expect(@fake_logger.warnings).to eq([DiscourseHub.response_status_log_message("/test", 500)]) - expect(@fake_logger.errors).to eq([ - DiscourseHub.response_body_log_message("") - ]) + expect(@fake_logger.errors).to eq([DiscourseHub.response_body_log_message("")]) end end end diff --git a/spec/lib/discourse_js_processor_spec.rb b/spec/lib/discourse_js_processor_spec.rb index df4c288a53..3c2eeac803 100644 --- a/spec/lib/discourse_js_processor_spec.rb +++ b/spec/lib/discourse_js_processor_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -require 'discourse_js_processor' +require "discourse_js_processor" RSpec.describe DiscourseJsProcessor do - - describe 'should_transpile?' do + describe "should_transpile?" do it "returns false for empty strings" do expect(DiscourseJsProcessor.should_transpile?(nil)).to eq(false) - expect(DiscourseJsProcessor.should_transpile?('')).to eq(false) + expect(DiscourseJsProcessor.should_transpile?("")).to eq(false) end it "returns false for a regular js file" do @@ -24,11 +23,13 @@ RSpec.describe DiscourseJsProcessor do describe "skip_module?" do it "returns false for empty strings" do expect(DiscourseJsProcessor.skip_module?(nil)).to eq(false) - expect(DiscourseJsProcessor.skip_module?('')).to eq(false) + expect(DiscourseJsProcessor.skip_module?("")).to eq(false) end it "returns true if the header is present" do - expect(DiscourseJsProcessor.skip_module?("// cool comment\n// discourse-skip-module")).to eq(true) + expect(DiscourseJsProcessor.skip_module?("// cool comment\n// discourse-skip-module")).to eq( + true, + ) end it "returns false if the header is not present" do @@ -83,8 +84,7 @@ RSpec.describe DiscourseJsProcessor do let(:compiler) { DiscourseJsProcessor::Transpiler.new } let(:theme_id) { 22 } - let(:helpers) { - <<~JS + let(:helpers) { <<~JS } Handlebars.registerHelper('theme-prefix', function(themeId, string) { return `theme_translations.${themeId}.${string}` }) @@ -98,37 +98,48 @@ RSpec.describe DiscourseJsProcessor do return `dummy(${string})` }) JS - } - let(:mini_racer) { + let(:mini_racer) do ctx = MiniRacer::Context.new - ctx.eval(File.open("#{Rails.root}/app/assets/javascripts/node_modules/handlebars/dist/handlebars.js").read) + ctx.eval( + File.open( + "#{Rails.root}/app/assets/javascripts/node_modules/handlebars/dist/handlebars.js", + ).read, + ) ctx.eval(helpers) ctx - } + end def render(template) compiled = compiler.compile_raw_template(template, theme_id: theme_id) mini_racer.eval "Handlebars.template(#{compiled.squish})({})" end - it 'adds the theme id to the helpers' do + it "adds the theme id to the helpers" do # Works normally - expect(render("{{theme-prefix 'translation_key'}}")). - to eq('theme_translations.22.translation_key') - expect(render("{{theme-i18n 'translation_key'}}")). - to eq('translated(theme_translations.22.translation_key)') - expect(render("{{theme-setting 'setting_key'}}")). - to eq('setting(22:setting_key)') + expect(render("{{theme-prefix 'translation_key'}}")).to eq( + "theme_translations.22.translation_key", + ) + expect(render("{{theme-i18n 'translation_key'}}")).to eq( + "translated(theme_translations.22.translation_key)", + ) + expect(render("{{theme-setting 'setting_key'}}")).to eq("setting(22:setting_key)") # Works when used inside other statements - expect(render("{{dummy-helper (theme-prefix 'translation_key')}}")). - to eq('dummy(theme_translations.22.translation_key)') + expect(render("{{dummy-helper (theme-prefix 'translation_key')}}")).to eq( + "dummy(theme_translations.22.translation_key)", + ) end it "doesn't duplicate number parameter inside {{each}}" do - expect(compiler.compile_raw_template("{{#each item as |test test2|}}{{theme-setting 'setting_key'}}{{/each}}", theme_id: theme_id)). - to include('{"name":"theme-setting","hash":{},"hashTypes":{},"hashContexts":{},"types":["NumberLiteral","StringLiteral"]') + expect( + compiler.compile_raw_template( + "{{#each item as |test test2|}}{{theme-setting 'setting_key'}}{{/each}}", + theme_id: theme_id, + ), + ).to include( + '{"name":"theme-setting","hash":{},"hashTypes":{},"hashContexts":{},"types":["NumberLiteral","StringLiteral"]', + ) # Fail would be if theme-setting is defined with types:["NumberLiteral","NumberLiteral","StringLiteral"] end end @@ -144,7 +155,7 @@ RSpec.describe DiscourseJsProcessor do export default hbs(#{template.to_json}); JS result = DiscourseJsProcessor.transpile(script, "", "theme/blah", theme_id: theme_id) - result.gsub(/\/\*(.*)\*\//m, "/* (js comment stripped) */") + result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */") end def standard_compile(template) @@ -153,32 +164,24 @@ RSpec.describe DiscourseJsProcessor do export default hbs(#{template.to_json}); JS result = DiscourseJsProcessor.transpile(script, "", "theme/blah") - result.gsub(/\/\*(.*)\*\//m, "/* (js comment stripped) */") + result.gsub(%r{/\*(.*)\*/}m, "/* (js comment stripped) */") end - it 'adds the theme id to the helpers' do - expect( - theme_compile "{{theme-prefix 'translation_key'}}" - ).to eq( + it "adds the theme id to the helpers" do + expect(theme_compile "{{theme-prefix 'translation_key'}}").to eq( standard_compile "{{theme-prefix #{theme_id} 'translation_key'}}" ) - expect( - theme_compile "{{theme-i18n 'translation_key'}}" - ).to eq( + expect(theme_compile "{{theme-i18n 'translation_key'}}").to eq( standard_compile "{{theme-i18n #{theme_id} 'translation_key'}}" ) - expect( - theme_compile "{{theme-setting 'setting_key'}}" - ).to eq( + expect(theme_compile "{{theme-setting 'setting_key'}}").to eq( standard_compile "{{theme-setting #{theme_id} 'setting_key'}}" ) # Works when used inside other statements - expect( - theme_compile "{{dummy-helper (theme-prefix 'translation_key')}}" - ).to eq( + expect(theme_compile "{{dummy-helper (theme-prefix 'translation_key')}}").to eq( standard_compile "{{dummy-helper (theme-prefix #{theme_id} 'translation_key')}}" ) end @@ -188,10 +191,14 @@ RSpec.describe DiscourseJsProcessor do it "can minify code and provide sourcemaps" do sources = { "multiply.js" => "let multiply = (firstValue, secondValue) => firstValue * secondValue;", - "add.js" => "let add = (firstValue, secondValue) => firstValue + secondValue;" + "add.js" => "let add = (firstValue, secondValue) => firstValue + secondValue;", } - result = DiscourseJsProcessor::Transpiler.new.terser(sources, { sourceMap: { includeSources: true } }) + result = + DiscourseJsProcessor::Transpiler.new.terser( + sources, + { sourceMap: { includeSources: true } }, + ) expect(result.keys).to contain_exactly("code", "decoded_map", "map") begin diff --git a/spec/lib/discourse_plugin_registry_spec.rb b/spec/lib/discourse_plugin_registry_spec.rb index 96f84df3df..c74db5e14f 100644 --- a/spec/lib/discourse_plugin_registry_spec.rb +++ b/spec/lib/discourse_plugin_registry_spec.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true -require 'discourse_plugin_registry' +require "discourse_plugin_registry" RSpec.describe DiscoursePluginRegistry do - class TestRegistry < DiscoursePluginRegistry; end + class TestRegistry < DiscoursePluginRegistry + end let(:registry) { TestRegistry } let(:registry_instance) { registry.new } - describe '.define_register' do + describe ".define_register" do let(:fresh_registry) { Class.new(TestRegistry) } let(:plugin_class) do @@ -22,7 +23,7 @@ RSpec.describe DiscoursePluginRegistry do let(:plugin) { plugin_class.new } - it 'works for a set' do + it "works for a set" do fresh_registry.define_register(:test_things, Set) fresh_registry.test_things << "My Thing" expect(fresh_registry.test_things).to contain_exactly("My Thing") @@ -30,7 +31,7 @@ RSpec.describe DiscoursePluginRegistry do expect(fresh_registry.test_things.length).to eq(0) end - it 'works for a hash' do + it "works for a hash" do fresh_registry.define_register(:test_things, Hash) fresh_registry.test_things[:test] = "hello world" expect(fresh_registry.test_things[:test]).to eq("hello world") @@ -38,8 +39,8 @@ RSpec.describe DiscoursePluginRegistry do expect(fresh_registry.test_things[:test]).to eq(nil) end - describe '.define_filtered_register' do - it 'works' do + describe ".define_filtered_register" do + it "works" do fresh_registry.define_filtered_register(:test_things) expect(fresh_registry.test_things.length).to eq(0) @@ -54,53 +55,53 @@ RSpec.describe DiscoursePluginRegistry do end end - describe '#stylesheets' do - it 'defaults to an empty Set' do + describe "#stylesheets" do + it "defaults to an empty Set" do registry.reset! expect(registry.stylesheets).to eq(Hash.new) end end - describe '#mobile_stylesheets' do - it 'defaults to an empty Set' do + describe "#mobile_stylesheets" do + it "defaults to an empty Set" do registry.reset! expect(registry.mobile_stylesheets).to eq(Hash.new) end end - describe '#javascripts' do - it 'defaults to an empty Set' do + describe "#javascripts" do + it "defaults to an empty Set" do registry.reset! expect(registry.javascripts).to eq(Set.new) end end - describe '#auth_providers' do - it 'defaults to an empty Set' do + describe "#auth_providers" do + it "defaults to an empty Set" do registry.reset! expect(registry.auth_providers).to eq(Set.new) end end - describe '#admin_javascripts' do - it 'defaults to an empty Set' do + describe "#admin_javascripts" do + it "defaults to an empty Set" do registry.reset! expect(registry.admin_javascripts).to eq(Set.new) end end - describe '#seed_data' do - it 'defaults to an empty Set' do + describe "#seed_data" do + it "defaults to an empty Set" do registry.reset! expect(registry.seed_data).to be_a(Hash) expect(registry.seed_data.size).to eq(0) end end - describe '.register_html_builder' do + describe ".register_html_builder" do it "can register and build html" do DiscoursePluginRegistry.register_html_builder(:my_html) { "my html" } - expect(DiscoursePluginRegistry.build_html(:my_html)).to eq('my html') + expect(DiscoursePluginRegistry.build_html(:my_html)).to eq("my html") DiscoursePluginRegistry.reset! expect(DiscoursePluginRegistry.build_html(:my_html)).to be_blank end @@ -113,41 +114,43 @@ RSpec.describe DiscoursePluginRegistry do end end - describe '.register_css' do + describe ".register_css" do let(:plugin_directory_name) { "hello" } - before do - registry_instance.register_css('hello.css', plugin_directory_name) - end + before { registry_instance.register_css("hello.css", plugin_directory_name) } - it 'is not leaking' do + it "is not leaking" do expect(DiscoursePluginRegistry.new.stylesheets[plugin_directory_name]).to be_nil end - it 'is returned by DiscoursePluginRegistry.stylesheets' do - expect(registry_instance.stylesheets[plugin_directory_name].include?('hello.css')).to eq(true) + it "is returned by DiscoursePluginRegistry.stylesheets" do + expect(registry_instance.stylesheets[plugin_directory_name].include?("hello.css")).to eq(true) end it "won't add the same file twice" do - expect { registry_instance.register_css('hello.css', plugin_directory_name) }.not_to change(registry.stylesheets[plugin_directory_name], :size) + expect { registry_instance.register_css("hello.css", plugin_directory_name) }.not_to change( + registry.stylesheets[plugin_directory_name], + :size, + ) end end - describe '.register_js' do - before do - registry_instance.register_js('hello.js') - end + describe ".register_js" do + before { registry_instance.register_js("hello.js") } - it 'is returned by DiscoursePluginRegistry.javascripts' do - expect(registry_instance.javascripts.include?('hello.js')).to eq(true) + it "is returned by DiscoursePluginRegistry.javascripts" do + expect(registry_instance.javascripts.include?("hello.js")).to eq(true) end it "won't add the same file twice" do - expect { registry_instance.register_js('hello.js') }.not_to change(registry.javascripts, :size) + expect { registry_instance.register_js("hello.js") }.not_to change( + registry.javascripts, + :size, + ) end end - describe '.register_auth_provider' do + describe ".register_auth_provider" do let(:registry) { DiscoursePluginRegistry } let(:auth_provider) do provider = Auth::AuthProvider.new @@ -155,53 +158,42 @@ RSpec.describe DiscoursePluginRegistry do provider end - before do - registry.register_auth_provider(auth_provider) - end + before { registry.register_auth_provider(auth_provider) } - after do - registry.reset! - end + after { registry.reset! } - it 'is returned by DiscoursePluginRegistry.auth_providers' do + it "is returned by DiscoursePluginRegistry.auth_providers" do expect(registry.auth_providers.include?(auth_provider)).to eq(true) end - end - describe '.register_service_worker' do + describe ".register_service_worker" do let(:registry) { DiscoursePluginRegistry } - before do - registry.register_service_worker('hello.js') - end + before { registry.register_service_worker("hello.js") } - after do - registry.reset! - end + after { registry.reset! } it "should register the file once" do - 2.times { registry.register_service_worker('hello.js') } + 2.times { registry.register_service_worker("hello.js") } expect(registry.service_workers.size).to eq(1) - expect(registry.service_workers).to include('hello.js') + expect(registry.service_workers).to include("hello.js") end end - describe '.register_archetype' do + describe ".register_archetype" do it "delegates archetypes to the Archetype component" do - Archetype.expects(:register).with('threaded', { hello: 123 }) - registry_instance.register_archetype('threaded', hello: 123) + Archetype.expects(:register).with("threaded", { hello: 123 }) + registry_instance.register_archetype("threaded", hello: 123) end end - describe '#register_asset' do + describe "#register_asset" do let(:registry) { DiscoursePluginRegistry } let(:plugin_directory_name) { "my_plugin" } - after do - registry.reset! - end + after { registry.reset! } it "does register general css properly" do registry.register_asset("test.css", nil, plugin_directory_name) @@ -227,7 +219,7 @@ RSpec.describe DiscoursePluginRegistry do it "registers color definitions properly" do registry.register_asset("test.css", :color_definitions, plugin_directory_name) - expect(registry.color_definition_stylesheets[plugin_directory_name]).to eq('test.css') + expect(registry.color_definition_stylesheets[plugin_directory_name]).to eq("test.css") expect(registry.stylesheets[plugin_directory_name]).to eq(nil) end @@ -246,19 +238,24 @@ RSpec.describe DiscoursePluginRegistry do end end - describe '#register_seed_data' do + describe "#register_seed_data" do let(:registry) { DiscoursePluginRegistry } - after do - registry.reset! - end + after { registry.reset! } it "registers seed data properly" do registry.register_seed_data("admin_quick_start_title", "Banana Hosting: Quick Start Guide") - registry.register_seed_data("admin_quick_start_filename", File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__)) + registry.register_seed_data( + "admin_quick_start_filename", + File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__), + ) - expect(registry.seed_data["admin_quick_start_title"]).to eq("Banana Hosting: Quick Start Guide") - expect(registry.seed_data["admin_quick_start_filename"]).to eq(File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__)) + expect(registry.seed_data["admin_quick_start_title"]).to eq( + "Banana Hosting: Quick Start Guide", + ) + expect(registry.seed_data["admin_quick_start_filename"]).to eq( + File.expand_path("../docs/BANANA-QUICK-START.md", __FILE__), + ) end end end diff --git a/spec/lib/discourse_redis_spec.rb b/spec/lib/discourse_redis_spec.rb index acf3cc96bf..183e8a41c9 100644 --- a/spec/lib/discourse_redis_spec.rb +++ b/spec/lib/discourse_redis_spec.rb @@ -6,116 +6,112 @@ RSpec.describe DiscourseRedis do expect(result).to eq(nil) end - describe 'redis commands' do + describe "redis commands" do let(:raw_redis) { Redis.new(DiscourseRedis.config) } - before do - raw_redis.flushdb - end + before { raw_redis.flushdb } - after do - raw_redis.flushdb - end + after { raw_redis.flushdb } - describe 'pipelined / multi' do + describe "pipelined / multi" do let(:redis) { DiscourseRedis.new } - it 'should support multi commands' do - val = redis.multi do |transaction| - transaction.set 'foo', 'bar' - transaction.set 'bar', 'foo' - transaction.get 'bar' - end + it "should support multi commands" do + val = + redis.multi do |transaction| + transaction.set "foo", "bar" + transaction.set "bar", "foo" + transaction.get "bar" + end - expect(raw_redis.get('foo')).to eq(nil) - expect(raw_redis.get('bar')).to eq(nil) - expect(redis.get('foo')).to eq('bar') - expect(redis.get('bar')).to eq('foo') + expect(raw_redis.get("foo")).to eq(nil) + expect(raw_redis.get("bar")).to eq(nil) + expect(redis.get("foo")).to eq("bar") + expect(redis.get("bar")).to eq("foo") - expect(val).to eq(["OK", "OK", "foo"]) + expect(val).to eq(%w[OK OK foo]) end - it 'should support pipelined commands' do + it "should support pipelined commands" do set, incr = nil - val = redis.pipelined do |pipeline| - set = pipeline.set "foo", "baz" - incr = pipeline.incr "baz" - end + val = + redis.pipelined do |pipeline| + set = pipeline.set "foo", "baz" + incr = pipeline.incr "baz" + end expect(val).to eq(["OK", 1]) expect(set.value).to eq("OK") expect(incr.value).to eq(1) - expect(raw_redis.get('foo')).to eq(nil) - expect(raw_redis.get('baz')).to eq(nil) + expect(raw_redis.get("foo")).to eq(nil) + expect(raw_redis.get("baz")).to eq(nil) - expect(redis.get('foo')).to eq("baz") - expect(redis.get('baz')).to eq("1") + expect(redis.get("foo")).to eq("baz") + expect(redis.get("baz")).to eq("1") end - it 'should noop pipelined commands against a readonly redis' do - redis.without_namespace - .expects(:pipelined) - .raises(Redis::CommandError.new("READONLY")) + it "should noop pipelined commands against a readonly redis" do + redis.without_namespace.expects(:pipelined).raises(Redis::CommandError.new("READONLY")) set, incr = nil - val = redis.pipelined do |pipeline| - set = pipeline.set "foo", "baz" - incr = pipeline.incr "baz" - end + val = + redis.pipelined do |pipeline| + set = pipeline.set "foo", "baz" + incr = pipeline.incr "baz" + end expect(val).to eq(nil) - expect(redis.get('foo')).to eq(nil) - expect(redis.get('baz')).to eq(nil) + expect(redis.get("foo")).to eq(nil) + expect(redis.get("baz")).to eq(nil) end - it 'should noop multi commands against a readonly redis' do - redis.without_namespace - .expects(:multi) - .raises(Redis::CommandError.new("READONLY")) + it "should noop multi commands against a readonly redis" do + redis.without_namespace.expects(:multi).raises(Redis::CommandError.new("READONLY")) - val = redis.multi do |transaction| - transaction.set 'foo', 'bar' - transaction.set 'bar', 'foo' - transaction.get 'bar' - end + val = + redis.multi do |transaction| + transaction.set "foo", "bar" + transaction.set "bar", "foo" + transaction.get "bar" + end expect(val).to eq(nil) - expect(redis.get('foo')).to eq(nil) - expect(redis.get('bar')).to eq(nil) + expect(redis.get("foo")).to eq(nil) + expect(redis.get("bar")).to eq(nil) end end - describe 'when namespace is enabled' do + describe "when namespace is enabled" do let(:redis) { DiscourseRedis.new } - it 'should append namespace to the keys' do - raw_redis.set('default:key', 1) - raw_redis.set('test:key2', 1) - raw_redis.set('default:key3', 1) + it "should append namespace to the keys" do + raw_redis.set("default:key", 1) + raw_redis.set("test:key2", 1) + raw_redis.set("default:key3", 1) - expect(redis.keys).to include('key') - expect(redis.keys).to_not include('key2') - expect(redis.scan_each.to_a).to contain_exactly('key', 'key3') + expect(redis.keys).to include("key") + expect(redis.keys).to_not include("key2") + expect(redis.scan_each.to_a).to contain_exactly("key", "key3") - redis.del('key', 'key3') + redis.del("key", "key3") - expect(raw_redis.get('default:key')).to eq(nil) - expect(raw_redis.get('default:key3')).to eq(nil) + expect(raw_redis.get("default:key")).to eq(nil) + expect(raw_redis.get("default:key3")).to eq(nil) expect(redis.scan_each.to_a).to eq([]) - raw_redis.set('default:key1', '1') - raw_redis.set('default:key2', '2') + raw_redis.set("default:key1", "1") + raw_redis.set("default:key2", "2") - expect(redis.mget('key1', 'key2')).to eq(['1', '2']) - expect(redis.scan_each.to_a).to contain_exactly('key1', 'key2') + expect(redis.mget("key1", "key2")).to eq(%w[1 2]) + expect(redis.scan_each.to_a).to contain_exactly("key1", "key2") end end - describe '#sadd?' do + describe "#sadd?" do it "should send the right command with the right key prefix to redis" do redis = DiscourseRedis.new @@ -125,7 +121,7 @@ RSpec.describe DiscourseRedis do end end - describe '#srem?' do + describe "#srem?" do it "should send the right command with the right key prefix to redis" do redis = DiscourseRedis.new @@ -135,33 +131,31 @@ RSpec.describe DiscourseRedis do end end - describe 'when namespace is disabled' do + describe "when namespace is disabled" do let(:redis) { DiscourseRedis.new(nil, namespace: false) } - it 'should not append any namespace to the keys' do - raw_redis.set('default:key', 1) - raw_redis.set('test:key2', 1) + it "should not append any namespace to the keys" do + raw_redis.set("default:key", 1) + raw_redis.set("test:key2", 1) - expect(redis.keys).to include('default:key', 'test:key2') + expect(redis.keys).to include("default:key", "test:key2") - raw_redis.set('key1', '1') - raw_redis.set('key2', '2') + raw_redis.set("key1", "1") + raw_redis.set("key2", "2") - expect(redis.mget('key1', 'key2')).to eq(['1', '2']) + expect(redis.mget("key1", "key2")).to eq(%w[1 2]) - redis.del('key1', 'key2') + redis.del("key1", "key2") - expect(redis.mget('key1', 'key2')).to eq([nil, nil]) + expect(redis.mget("key1", "key2")).to eq([nil, nil]) end - it 'should noop a readonly redis' do + it "should noop a readonly redis" do expect(Discourse.recently_readonly?).to eq(false) - redis.without_namespace - .expects(:set) - .raises(Redis::CommandError.new("READONLY")) + redis.without_namespace.expects(:set).raises(Redis::CommandError.new("READONLY")) - redis.set('key', 1) + redis.set("key", 1) expect(Discourse.recently_readonly?).to eq(true) end @@ -169,54 +163,32 @@ RSpec.describe DiscourseRedis do describe "#eval" do it "keys and arvg are passed correcty" do - keys = ["key1", "key2"] - argv = ["arg1", "arg2"] + keys = %w[key1 key2] + argv = %w[arg1 arg2] - expect(Discourse.redis.eval( - "return { KEYS, ARGV };", - keys: keys, - argv: argv, - )).to eq([keys, argv]) + expect(Discourse.redis.eval("return { KEYS, ARGV };", keys: keys, argv: argv)).to eq( + [keys, argv], + ) - expect(Discourse.redis.eval( - "return { KEYS, ARGV };", - keys, - argv: argv, - )).to eq([keys, argv]) + expect(Discourse.redis.eval("return { KEYS, ARGV };", keys, argv: argv)).to eq([keys, argv]) - expect(Discourse.redis.eval( - "return { KEYS, ARGV };", - keys, - argv, - )).to eq([keys, argv]) + expect(Discourse.redis.eval("return { KEYS, ARGV };", keys, argv)).to eq([keys, argv]) end end describe "#evalsha" do it "keys and arvg are passed correcty" do - keys = ["key1", "key2"] - argv = ["arg1", "arg2"] + keys = %w[key1 key2] + argv = %w[arg1 arg2] script = "return { KEYS, ARGV };" Discourse.redis.script(:load, script) sha = Digest::SHA1.hexdigest(script) - expect(Discourse.redis.evalsha( - sha, - keys: keys, - argv: argv, - )).to eq([keys, argv]) + expect(Discourse.redis.evalsha(sha, keys: keys, argv: argv)).to eq([keys, argv]) - expect(Discourse.redis.evalsha( - sha, - keys, - argv: argv, - )).to eq([keys, argv]) + expect(Discourse.redis.evalsha(sha, keys, argv: argv)).to eq([keys, argv]) - expect(Discourse.redis.evalsha( - sha, - keys, - argv, - )).to eq([keys, argv]) + expect(Discourse.redis.evalsha(sha, keys, argv)).to eq([keys, argv]) end end end @@ -226,32 +198,35 @@ RSpec.describe DiscourseRedis do helper = DiscourseRedis::EvalHelper.new <<~LUA return 'hello world' LUA - expect(helper.eval(Discourse.redis)).to eq('hello world') + expect(helper.eval(Discourse.redis)).to eq("hello world") end it "works with arguments" do helper = DiscourseRedis::EvalHelper.new <<~LUA return ARGV[1]..ARGV[2]..KEYS[1]..KEYS[2] LUA - expect(helper.eval(Discourse.redis, ['key1', 'key2'], ['arg1', 'arg2'])).to eq("arg1arg2key1key2") + expect(helper.eval(Discourse.redis, %w[key1 key2], %w[arg1 arg2])).to eq("arg1arg2key1key2") end it "works with arguments" do helper = DiscourseRedis::EvalHelper.new <<~LUA return ARGV[1]..ARGV[2]..KEYS[1]..KEYS[2] LUA - expect(helper.eval(Discourse.redis, ['key1', 'key2'], ['arg1', 'arg2'])).to eq("arg1arg2key1key2") + expect(helper.eval(Discourse.redis, %w[key1 key2], %w[arg1 arg2])).to eq("arg1arg2key1key2") end it "uses evalsha correctly" do - redis_proxy = Class.new do - attr_reader :calls - def method_missing(meth, *args, **kwargs, &block) - @calls ||= [] - @calls.push(meth) - Discourse.redis.public_send(meth, *args, **kwargs, &block) - end - end.new + redis_proxy = + Class + .new do + attr_reader :calls + def method_missing(meth, *args, **kwargs, &block) + @calls ||= [] + @calls.push(meth) + Discourse.redis.public_send(meth, *args, **kwargs, &block) + end + end + .new Discourse.redis.call("SCRIPT", "FLUSH", "SYNC") @@ -262,12 +237,12 @@ RSpec.describe DiscourseRedis do expect(helper.eval(redis_proxy)).to eq("hello world") expect(helper.eval(redis_proxy)).to eq("hello world") - expect(redis_proxy.calls).to eq([:evalsha, :eval, :evalsha, :evalsha]) + expect(redis_proxy.calls).to eq(%i[evalsha eval evalsha evalsha]) end end describe ".new_redis_store" do - let(:cache) { Cache.new(namespace: 'foo') } + let(:cache) { Cache.new(namespace: "foo") } let(:store) { DiscourseRedis.new_redis_store } before do @@ -276,9 +251,7 @@ RSpec.describe DiscourseRedis do end it "can store stuff" do - store.fetch("key") do - "key in store" - end + store.fetch("key") { "key in store" } r = store.read("key") @@ -286,13 +259,9 @@ RSpec.describe DiscourseRedis do end it "doesn't collide with our Cache" do - store.fetch("key") do - "key in store" - end + store.fetch("key") { "key in store" } - cache.fetch("key") do - "key in cache" - end + cache.fetch("key") { "key in cache" } r = store.read("key") @@ -303,13 +272,9 @@ RSpec.describe DiscourseRedis do cache.clear store.clear - store.fetch("key") do - "key in store" - end + store.fetch("key") { "key in store" } - cache.fetch("key") do - "key in cache" - end + cache.fetch("key") { "key in cache" } store.clear diff --git a/spec/lib/discourse_sourcemapping_url_processor_spec.rb b/spec/lib/discourse_sourcemapping_url_processor_spec.rb index 6aad3809a8..1948598d1f 100644 --- a/spec/lib/discourse_sourcemapping_url_processor_spec.rb +++ b/spec/lib/discourse_sourcemapping_url_processor_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'discourse_sourcemapping_url_processor' +require "discourse_sourcemapping_url_processor" RSpec.describe DiscourseSourcemappingUrlProcessor do def process(input) @@ -15,17 +15,19 @@ RSpec.describe DiscourseSourcemappingUrlProcessor do end end - input = { environment: env, data: input, name: 'mapped', filename: 'mapped.js', metadata: {} } + input = { environment: env, data: input, name: "mapped", filename: "mapped.js", metadata: {} } DiscourseSourcemappingUrlProcessor.call(input)[:data] end - it 'maintains relative paths' do + it "maintains relative paths" do output = process "var mapped;\n//# sourceMappingURL=mapped.js.map" expect(output).to eq("var mapped;\n//# sourceMappingURL=mapped-HEXGOESHERE.js.map\n//!\n") end - it 'uses default behaviour for non-adjacent relative paths' do + it "uses default behaviour for non-adjacent relative paths" do output = process "var mapped;\n//# sourceMappingURL=/assets/mapped.js.map" - expect(output).to eq("var mapped;\n//# sourceMappingURL=/assets/mapped-HEXGOESHERE.js.map\n//!\n") + expect(output).to eq( + "var mapped;\n//# sourceMappingURL=/assets/mapped-HEXGOESHERE.js.map\n//!\n", + ) end end diff --git a/spec/lib/discourse_spec.rb b/spec/lib/discourse_spec.rb index a26a54bfb2..1a4227fe11 100644 --- a/spec/lib/discourse_spec.rb +++ b/spec/lib/discourse_spec.rb @@ -1,61 +1,72 @@ # frozen_string_literal: true -require 'discourse' +require "discourse" RSpec.describe Discourse do - before do - RailsMultisite::ConnectionManagement.stubs(:current_hostname).returns('foo.com') - end + before { RailsMultisite::ConnectionManagement.stubs(:current_hostname).returns("foo.com") } - describe 'current_hostname' do - it 'returns the hostname from the current db connection' do - expect(Discourse.current_hostname).to eq('foo.com') + describe "current_hostname" do + it "returns the hostname from the current db connection" do + expect(Discourse.current_hostname).to eq("foo.com") end end - describe 'avatar_sizes' do - it 'returns a list of integers' do - expect(Discourse.avatar_sizes).to contain_exactly(20, 25, 30, 32, 37, 40, 45, 48, 50, 60, 64, 67, 75, 90, 96, 120, 135, 180, 240, 360) + describe "avatar_sizes" do + it "returns a list of integers" do + expect(Discourse.avatar_sizes).to contain_exactly( + 20, + 25, + 30, + 32, + 37, + 40, + 45, + 48, + 50, + 60, + 64, + 67, + 75, + 90, + 96, + 120, + 135, + 180, + 240, + 360, + ) end end - describe 'running_in_rack' do - after do - ENV.delete("DISCOURSE_RUNNING_IN_RACK") - end + describe "running_in_rack" do + after { ENV.delete("DISCOURSE_RUNNING_IN_RACK") } - it 'should not be running in rack' do + it "should not be running in rack" do expect(Discourse.running_in_rack?).to eq(false) ENV["DISCOURSE_RUNNING_IN_RACK"] = "1" expect(Discourse.running_in_rack?).to eq(true) end end - describe 'base_url' do - context 'when https is off' do - before do - SiteSetting.force_https = false - end + describe "base_url" do + context "when https is off" do + before { SiteSetting.force_https = false } - it 'has a non https base url' do + it "has a non https base url" do expect(Discourse.base_url).to eq("http://foo.com") end end - context 'when https is on' do - before do - SiteSetting.force_https = true - end + context "when https is on" do + before { SiteSetting.force_https = true } - it 'has a non-ssl base url' do + it "has a non-ssl base url" do expect(Discourse.base_url).to eq("https://foo.com") end end - context 'with a non standard port specified' do - before do - SiteSetting.port = 3000 - end + context "with a non standard port specified" do + before { SiteSetting.port = 3000 } it "returns the non standart port in the base url" do expect(Discourse.base_url).to eq("http://foo.com:3000") @@ -76,7 +87,7 @@ RSpec.describe Discourse do end end - describe 'plugins' do + describe "plugins" do let(:plugin_class) do Class.new(Plugin::Instance) do attr_accessor :enabled @@ -86,12 +97,20 @@ RSpec.describe Discourse do end end - let(:plugin1) { plugin_class.new.tap { |p| p.enabled = true; p.path = "my-plugin-1" } } - let(:plugin2) { plugin_class.new.tap { |p| p.enabled = false; p.path = "my-plugin-1" } } - - before do - Discourse.plugins.append(plugin1, plugin2) + let(:plugin1) do + plugin_class.new.tap do |p| + p.enabled = true + p.path = "my-plugin-1" + end end + let(:plugin2) do + plugin_class.new.tap do |p| + p.enabled = false + p.path = "my-plugin-1" + end + end + + before { Discourse.plugins.append(plugin1, plugin2) } after do Discourse.plugins.delete plugin1 @@ -104,7 +123,7 @@ RSpec.describe Discourse do plugin_class.any_instance.stubs(:js_asset_exists?).returns(true) end - it 'can find plugins correctly' do + it "can find plugins correctly" do expect(Discourse.plugins).to include(plugin1, plugin2) # Exclude disabled plugins by default @@ -114,86 +133,80 @@ RSpec.describe Discourse do expect(Discourse.find_plugins(include_disabled: true)).to include(plugin1, plugin2) end - it 'can find plugin assets' do + it "can find plugin assets" do plugin2.enabled = true expect(Discourse.find_plugin_css_assets({}).length).to eq(2) expect(Discourse.find_plugin_js_assets({}).length).to eq(2) - plugin1.register_asset_filter do |type, request, opts| - false - end + plugin1.register_asset_filter { |type, request, opts| false } expect(Discourse.find_plugin_css_assets({}).length).to eq(1) expect(Discourse.find_plugin_js_assets({}).length).to eq(1) end - end - describe 'authenticators' do - it 'returns inbuilt authenticators' do + describe "authenticators" do + it "returns inbuilt authenticators" do expect(Discourse.authenticators).to match_array(Discourse::BUILTIN_AUTH.map(&:authenticator)) end - context 'with authentication plugin installed' do + context "with authentication plugin installed" do let(:plugin_auth_provider) do - authenticator_class = Class.new(Auth::Authenticator) do - def name - 'pluginauth' - end + authenticator_class = + Class.new(Auth::Authenticator) do + def name + "pluginauth" + end - def enabled? - true + def enabled? + true + end end - end provider = Auth::AuthProvider.new provider.authenticator = authenticator_class.new provider end - before do - DiscoursePluginRegistry.register_auth_provider(plugin_auth_provider) - end + before { DiscoursePluginRegistry.register_auth_provider(plugin_auth_provider) } - after do - DiscoursePluginRegistry.reset! - end + after { DiscoursePluginRegistry.reset! } - it 'returns inbuilt and plugin authenticators' do + it "returns inbuilt and plugin authenticators" do expect(Discourse.authenticators).to match_array( - Discourse::BUILTIN_AUTH.map(&:authenticator) + [plugin_auth_provider.authenticator]) + Discourse::BUILTIN_AUTH.map(&:authenticator) + [plugin_auth_provider.authenticator], + ) end - end end - describe 'enabled_authenticators' do - it 'only returns enabled authenticators' do + describe "enabled_authenticators" do + it "only returns enabled authenticators" do expect(Discourse.enabled_authenticators.length).to be(0) - expect { SiteSetting.enable_twitter_logins = true } - .to change { Discourse.enabled_authenticators.length }.by(1) + expect { SiteSetting.enable_twitter_logins = true }.to change { + Discourse.enabled_authenticators.length + }.by(1) expect(Discourse.enabled_authenticators.length).to be(1) expect(Discourse.enabled_authenticators.first).to be_instance_of(Auth::TwitterAuthenticator) end end - describe '#site_contact_user' do + describe "#site_contact_user" do fab!(:admin) { Fabricate(:admin) } fab!(:another_admin) { Fabricate(:admin) } - it 'returns the user specified by the site setting site_contact_username' do + it "returns the user specified by the site setting site_contact_username" do SiteSetting.site_contact_username = another_admin.username expect(Discourse.site_contact_user).to eq(another_admin) end - it 'returns the system user otherwise' do + it "returns the system user otherwise" do SiteSetting.site_contact_username = nil expect(Discourse.site_contact_user.username).to eq("system") end - end - describe '#system_user' do - it 'returns the system user' do + describe "#system_user" do + it "returns the system user" do expect(Discourse.system_user.id).to eq(-1) end end @@ -212,7 +225,7 @@ RSpec.describe Discourse do end end - describe 'readonly mode' do + describe "readonly mode" do let(:readonly_mode_key) { Discourse::READONLY_MODE_KEY } let(:readonly_mode_ttl) { Discourse::READONLY_MODE_KEY_TTL } let(:user_readonly_mode_key) { Discourse::USER_READONLY_MODE_KEY } @@ -240,7 +253,7 @@ RSpec.describe Discourse do expect(Discourse.redis.get(readonly_mode_key)).to eq(nil) end - context 'when user enabled readonly mode' do + context "when user enabled readonly mode" do it "adds a key in redis and publish a message through the message bus" do expect(Discourse.redis.get(user_readonly_mode_key)).to eq(nil) end @@ -248,9 +261,12 @@ RSpec.describe Discourse do end describe ".disable_readonly_mode" do - context 'when user disabled readonly mode' do + context "when user disabled readonly mode" do it "removes readonly key in redis and publish a message through the message bus" do - message = MessageBus.track_publish { Discourse.disable_readonly_mode(user_readonly_mode_key) }.first + message = + MessageBus + .track_publish { Discourse.disable_readonly_mode(user_readonly_mode_key) } + .first assert_readonly_mode_disabled(message, user_readonly_mode_key) end end @@ -289,14 +305,14 @@ RSpec.describe Discourse do describe ".received_postgres_readonly!" do it "sets the right time" do time = Discourse.received_postgres_readonly! - expect(Discourse.postgres_last_read_only['default']).to eq(time) + expect(Discourse.postgres_last_read_only["default"]).to eq(time) end end describe ".received_redis_readonly!" do it "sets the right time" do time = Discourse.received_redis_readonly! - expect(Discourse.redis_last_read_only['default']).to eq(time) + expect(Discourse.redis_last_read_only["default"]).to eq(time) end end @@ -305,12 +321,11 @@ RSpec.describe Discourse do Discourse.received_postgres_readonly! messages = [] - expect do - messages = MessageBus.track_publish { Discourse.clear_readonly! } - end.to change { Discourse.postgres_last_read_only['default'] }.to(nil) + expect do messages = MessageBus.track_publish { Discourse.clear_readonly! } end.to change { + Discourse.postgres_last_read_only["default"] + }.to(nil) - expect(messages.any? { |m| m.channel == Site::SITE_JSON_CHANNEL }) - .to eq(true) + expect(messages.any? { |m| m.channel == Site::SITE_JSON_CHANNEL }).to eq(true) end end end @@ -327,25 +342,17 @@ RSpec.describe Discourse do let!(:logger) { TempSidekiqLogger.new } - before do - Sidekiq.error_handlers << logger - end + before { Sidekiq.error_handlers << logger } - after do - Sidekiq.error_handlers.delete(logger) - end + after { Sidekiq.error_handlers.delete(logger) } describe "#job_exception_stats" do class FakeTestError < StandardError end - before do - Discourse.reset_job_exception_stats! - end + before { Discourse.reset_job_exception_stats! } - after do - Discourse.reset_job_exception_stats! - end + after { Discourse.reset_job_exception_stats! } it "should not fail on incorrectly shaped hash" do expect do @@ -354,42 +361,48 @@ RSpec.describe Discourse do end it "should collect job exception stats" do - # see MiniScheduler Manager which reports it like this # https://github.com/discourse/mini_scheduler/blob/2b2c1c56b6e76f51108c2a305775469e24cf2b65/lib/mini_scheduler/manager.rb#L95 exception_context = { message: "Running a scheduled job", - job: { "class" => Jobs::ReindexSearch } + job: { + "class" => Jobs::ReindexSearch, + }, } # re-raised unconditionally in test env 2.times do - expect { Discourse.handle_job_exception(FakeTestError.new, exception_context) }.to raise_error(FakeTestError) + expect { + Discourse.handle_job_exception(FakeTestError.new, exception_context) + }.to raise_error(FakeTestError) end exception_context = { message: "Running a scheduled job", - job: { "class" => Jobs::PollMailbox } + job: { + "class" => Jobs::PollMailbox, + }, } - expect { Discourse.handle_job_exception(FakeTestError.new, exception_context) }.to raise_error(FakeTestError) + expect { + Discourse.handle_job_exception(FakeTestError.new, exception_context) + }.to raise_error(FakeTestError) - expect(Discourse.job_exception_stats).to eq({ - Jobs::PollMailbox => 1, - Jobs::ReindexSearch => 2, - }) + expect(Discourse.job_exception_stats).to eq( + { Jobs::PollMailbox => 1, Jobs::ReindexSearch => 2 }, + ) end end it "should not fail when called" do exception = StandardError.new - expect do - Discourse.handle_job_exception(exception, nil, nil) - end.to raise_error(StandardError) # Raises in test mode, catch it + expect do Discourse.handle_job_exception(exception, nil, nil) end.to raise_error( + StandardError, + ) # Raises in test mode, catch it expect(logger.exception).to eq(exception) - expect(logger.context.keys).to eq([:current_db, :current_hostname]) + expect(logger.context.keys).to eq(%i[current_db current_hostname]) end it "correctly passes extra context" do @@ -400,11 +413,11 @@ RSpec.describe Discourse do end.to raise_error(StandardError) # Raises in test mode, catch it expect(logger.exception).to eq(exception) - expect(logger.context.keys.sort).to eq([:current_db, :current_hostname, :message, :post_id].sort) + expect(logger.context.keys.sort).to eq(%i[current_db current_hostname message post_id].sort) end end - describe '#deprecate' do + describe "#deprecate" do def old_method(m) Discourse.deprecate(m) end @@ -418,11 +431,9 @@ RSpec.describe Discourse do Rails.logger = @fake_logger = FakeLogger.new end - after do - Rails.logger = @orig_logger - end + after { Rails.logger = @orig_logger } - it 'can deprecate usage' do + it "can deprecate usage" do k = SecureRandom.hex expect(old_method_caller(k)).to include("old_method_caller") expect(old_method_caller(k)).to include("discourse_spec") @@ -431,29 +442,31 @@ RSpec.describe Discourse do expect(@fake_logger.warnings).to eq([old_method_caller(k)]) end - it 'can report the deprecated version' do + it "can report the deprecated version" do Discourse.deprecate(SecureRandom.hex, since: "2.1.0.beta1") expect(@fake_logger.warnings[0]).to include("(deprecated since Discourse 2.1.0.beta1)") end - it 'can report the drop version' do + it "can report the drop version" do Discourse.deprecate(SecureRandom.hex, drop_from: "2.3.0") expect(@fake_logger.warnings[0]).to include("(removal in Discourse 2.3.0)") end - it 'can raise deprecation error' do - expect { - Discourse.deprecate(SecureRandom.hex, raise_error: true) - }.to raise_error(Discourse::Deprecation) + it "can raise deprecation error" do + expect { Discourse.deprecate(SecureRandom.hex, raise_error: true) }.to raise_error( + Discourse::Deprecation, + ) end end describe "Utils.execute_command" do it "works for individual commands" do expect(Discourse::Utils.execute_command("pwd").strip).to eq(Rails.root.to_s) - expect(Discourse::Utils.execute_command("pwd", chdir: "plugins").strip).to eq("#{Rails.root.to_s}/plugins") + expect(Discourse::Utils.execute_command("pwd", chdir: "plugins").strip).to eq( + "#{Rails.root.to_s}/plugins", + ) end it "supports timeouts" do @@ -462,7 +475,12 @@ RSpec.describe Discourse do end.to raise_error(RuntimeError) expect do - Discourse::Utils.execute_command({ "MYENV" => "MYVAL" }, "sleep", "999999999999", timeout: 0.001) + Discourse::Utils.execute_command( + { "MYENV" => "MYVAL" }, + "sleep", + "999999999999", + timeout: 0.001, + ) end.to raise_error(RuntimeError) end @@ -471,10 +489,11 @@ RSpec.describe Discourse do expect(runner.exec("pwd").strip).to eq(Rails.root.to_s) end - result = Discourse::Utils.execute_command(chdir: "plugins") do |runner| - expect(runner.exec("pwd").strip).to eq("#{Rails.root.to_s}/plugins") - runner.exec("pwd") - end + result = + Discourse::Utils.execute_command(chdir: "plugins") do |runner| + expect(runner.exec("pwd").strip).to eq("#{Rails.root.to_s}/plugins") + runner.exec("pwd") + end # Should return output of block expect(result.strip).to eq("#{Rails.root.to_s}/plugins") @@ -484,12 +503,13 @@ RSpec.describe Discourse do has_done_chdir = false has_checked_chdir = false - thread = Thread.new do - Discourse::Utils.execute_command(chdir: "plugins") do - has_done_chdir = true - sleep(0.01) until has_checked_chdir + thread = + Thread.new do + Discourse::Utils.execute_command(chdir: "plugins") do + has_done_chdir = true + sleep(0.01) until has_checked_chdir + end end - end sleep(0.01) until has_done_chdir expect(Discourse::Utils.execute_command("pwd").strip).to eq(Rails.root.to_s) @@ -500,16 +520,16 @@ RSpec.describe Discourse do it "raises error for unsafe shell" do expect(Discourse::Utils.execute_command("pwd").strip).to eq(Rails.root.to_s) - expect do - Discourse::Utils.execute_command("echo a b c") - end.to raise_error(RuntimeError) + expect do Discourse::Utils.execute_command("echo a b c") end.to raise_error(RuntimeError) expect do Discourse::Utils.execute_command({ "ENV1" => "VAL" }, "echo a b c") end.to raise_error(RuntimeError) expect(Discourse::Utils.execute_command("echo", "a", "b", "c").strip).to eq("a b c") - expect(Discourse::Utils.execute_command("echo a b c", unsafe_shell: true).strip).to eq("a b c") + expect(Discourse::Utils.execute_command("echo a b c", unsafe_shell: true).strip).to eq( + "a b c", + ) end end @@ -540,7 +560,7 @@ RSpec.describe Discourse do type_id: ThemeField.types[:html], target_id: Theme.targets[:common], name: "head_tag", - value: <<~HTML + value: <<~HTML, @@ -554,7 +574,7 @@ RSpec.describe Discourse do type_id: ThemeField.types[:js], target_id: Theme.targets[:extra_js], name: "somefile.js", - value: <<~JS + value: <<~JS, console.log(settings.uploads.imajee); JS ) @@ -566,7 +586,7 @@ RSpec.describe Discourse do type_id: ThemeField.types[:scss], target_id: Theme.targets[:common], name: "scss", - value: <<~SCSS + value: <<~SCSS, .something { background: url($imajee); } SCSS ) @@ -577,62 +597,77 @@ RSpec.describe Discourse do old_upload_url = Discourse.store.cdn_url(upload.url) - head_tag_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :desktop, "head_tag") - ).css('script').first + head_tag_script = + Nokogiri::HTML5 + .fragment(Theme.lookup_field(theme.id, :desktop, "head_tag")) + .css("script") + .first head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content expect(head_tag_js).to include(old_upload_url) - js_file_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :extra_js, nil) - ).css('script').first + js_file_script = + Nokogiri::HTML5.fragment(Theme.lookup_field(theme.id, :extra_js, nil)).css("script").first file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content expect(file_js).to include(old_upload_url) - css_link_tag = Nokogiri::HTML5.fragment( - Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') - ).css('link').first + css_link_tag = + Nokogiri::HTML5 + .fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, "all"), + ) + .css("link") + .first css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content expect(css).to include("url(#{old_upload_url})") SiteSetting.s3_cdn_url = "https://new.s3.cdn.com/gg" new_upload_url = Discourse.store.cdn_url(upload.url) - head_tag_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :desktop, "head_tag") - ).css('script').first + head_tag_script = + Nokogiri::HTML5 + .fragment(Theme.lookup_field(theme.id, :desktop, "head_tag")) + .css("script") + .first head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content expect(head_tag_js).to include(old_upload_url) - js_file_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :extra_js, nil) - ).css('script').first + js_file_script = + Nokogiri::HTML5.fragment(Theme.lookup_field(theme.id, :extra_js, nil)).css("script").first file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content expect(file_js).to include(old_upload_url) - css_link_tag = Nokogiri::HTML5.fragment( - Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') - ).css('link').first + css_link_tag = + Nokogiri::HTML5 + .fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, "all"), + ) + .css("link") + .first css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content expect(css).to include("url(#{old_upload_url})") Discourse.clear_all_theme_cache! - head_tag_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :desktop, "head_tag") - ).css('script').first + head_tag_script = + Nokogiri::HTML5 + .fragment(Theme.lookup_field(theme.id, :desktop, "head_tag")) + .css("script") + .first head_tag_js = JavascriptCache.find_by(digest: head_tag_script[:src][/\h{40}/]).content expect(head_tag_js).to include(new_upload_url) - js_file_script = Nokogiri::HTML5.fragment( - Theme.lookup_field(theme.id, :extra_js, nil) - ).css('script').first + js_file_script = + Nokogiri::HTML5.fragment(Theme.lookup_field(theme.id, :extra_js, nil)).css("script").first file_js = JavascriptCache.find_by(digest: js_file_script[:src][/\h{40}/]).content expect(file_js).to include(new_upload_url) - css_link_tag = Nokogiri::HTML5.fragment( - Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, 'all') - ).css('link').first + css_link_tag = + Nokogiri::HTML5 + .fragment( + Stylesheet::Manager.new(theme_id: theme.id).stylesheet_link_tag(:desktop_theme, "all"), + ) + .css("link") + .first css = StylesheetCache.find_by(digest: css_link_tag[:href][/\h{40}/]).content expect(css).to include("url(#{new_upload_url})") end diff --git a/spec/lib/discourse_tagging_spec.rb b/spec/lib/discourse_tagging_spec.rb index c952a899d3..1188243c57 100644 --- a/spec/lib/discourse_tagging_spec.rb +++ b/spec/lib/discourse_tagging_spec.rb @@ -1,13 +1,13 @@ # encoding: UTF-8 # frozen_string_literal: true -require 'discourse_tagging' +require "discourse_tagging" # More tests are found in the category_tag_spec integration specs RSpec.describe DiscourseTagging do fab!(:admin) { Fabricate(:admin) } - fab!(:user) { Fabricate(:user) } + fab!(:user) { Fabricate(:user) } let(:admin_guardian) { Guardian.new(admin) } let(:guardian) { Guardian.new(user) } @@ -21,10 +21,10 @@ RSpec.describe DiscourseTagging do SiteSetting.min_trust_level_to_tag_topics = 0 end - describe 'visible_tags' do + describe "visible_tags" do fab!(:tag4) { Fabricate(:tag, name: "fun4") } - fab!(:user2) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } let(:guardian2) { Guardian.new(user2) } fab!(:group) { Fabricate(:group, name: "my-group") } @@ -42,7 +42,7 @@ RSpec.describe DiscourseTagging do Fabricate(:tag_group, permissions: { "my-group" => 1 }, tag_names: [tag4.name]) end - context 'for admin' do + context "for admin" do it "includes tags with no tag_groups" do expect(DiscourseTagging.visible_tags(admin_guardian)).to include(tag1) end @@ -60,7 +60,7 @@ RSpec.describe DiscourseTagging do end end - context 'for users in a group' do + context "for users in a group" do it "includes tags with no tag_groups" do expect(DiscourseTagging.visible_tags(guardian)).to include(tag1) end @@ -78,7 +78,7 @@ RSpec.describe DiscourseTagging do end end - context 'for other users' do + context "for other users" do it "includes tags with no tag_groups" do expect(DiscourseTagging.visible_tags(guardian2)).to include(tag1) end @@ -97,84 +97,96 @@ RSpec.describe DiscourseTagging do end end - describe 'filter_allowed_tags' do - context 'for input fields' do + describe "filter_allowed_tags" do + context "for input fields" do it "doesn't return selected tags if there's a search term" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - selected_tags: [tag2.name], - for_input: true, - term: 'fun' - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + selected_tags: [tag2.name], + for_input: true, + term: "fun", + ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end it "doesn't return selected tags if there's no search term" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - selected_tags: [tag2.name], - for_input: true - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + selected_tags: [tag2.name], + for_input: true, + ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag3.name) end - context 'with tag with colon' do - fab!(:tag_with_colon) { Fabricate(:tag, name: 'with:colon') } + context "with tag with colon" do + fab!(:tag_with_colon) { Fabricate(:tag, name: "with:colon") } it "can use it as selected tag" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - selected_tags: [tag_with_colon.name], - for_input: true - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + selected_tags: [tag_with_colon.name], + for_input: true, + ).map(&:name) expect(tags).to contain_exactly(tag1.name, tag2.name, tag3.name) end it "can search for tags with colons" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - term: 'with:c', - order_search_results: true - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + term: "with:c", + order_search_results: true, + ).map(&:name) expect(tags).to contain_exactly(tag_with_colon.name) end it "can limit results to the tag" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_topic: true, - only_tag_names: [tag_with_colon.name] - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_topic: true, + only_tag_names: [tag_with_colon.name], + ).map(&:name) expect(tags).to contain_exactly(tag_with_colon.name) end end - context 'with tags visible only to staff' do + context "with tags visible only to staff" do fab!(:hidden_tag) { Fabricate(:tag) } - let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let!(:staff_tag_group) do + Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) + end - it 'should return all tags to staff' do + it "should return all tags to staff" do tags = DiscourseTagging.filter_allowed_tags(Guardian.new(admin)).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3, hidden_tag])) end - it 'should not return hidden tag to non-staff' do + it "should not return hidden tag to non-staff" do tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user)).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3])) end end - context 'with tags visible only to non-admin group' do + context "with tags visible only to non-admin group" do fab!(:hidden_tag) { Fabricate(:tag) } fab!(:group) { Fabricate(:group, name: "my-group") } - let!(:user_tag_group) { Fabricate(:tag_group, permissions: { "my-group" => 1 }, tag_names: [hidden_tag.name]) } - - before do - group.add(user) + let!(:user_tag_group) do + Fabricate(:tag_group, permissions: { "my-group" => 1 }, tag_names: [hidden_tag.name]) end - it 'should return all tags to member of group' do + before { group.add(user) } + + it "should return all tags to member of group" do tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user)).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3, hidden_tag])) end - it 'should allow a tag group to have multiple group permissions' do + it "should allow a tag group to have multiple group permissions" do group2 = Fabricate(:group, name: "another-group") user2 = Fabricate(:user) user3 = Fabricate(:user) @@ -191,58 +203,73 @@ RSpec.describe DiscourseTagging do expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3])) end - it 'should not hide group tags to member of group' do + it "should not hide group tags to member of group" do tags = DiscourseTagging.hidden_tag_names(Guardian.new(user)).to_a expect(sorted_tag_names(tags)).to eq([]) end - it 'should hide group tags to non-member of group' do + it "should hide group tags to non-member of group" do other_user = Fabricate(:user) tags = DiscourseTagging.hidden_tag_names(Guardian.new(other_user)).to_a expect(sorted_tag_names(tags)).to eq([hidden_tag.name]) end end - context 'with required tags from tag group' do + context "with required tags from tag group" do fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag2]) } - fab!(:category) { Fabricate(:category, category_required_tag_groups: [ CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1) ]) } + fab!(:category) do + Fabricate( + :category, + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "returns the required tags if none have been selected" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - term: 'fun' - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + term: "fun", + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2])) end it "returns all allowed tags if a required tag is selected" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - selected_tags: [tag1.name], - term: 'fun' - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + selected_tags: [tag1.name], + term: "fun", + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag2, tag3])) end it "returns required tags if not enough are selected" do category.category_required_tag_groups.first.update!(min_count: 2) - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - selected_tags: [tag1.name], - term: 'fun' - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + selected_tags: [tag1.name], + term: "fun", + ).to_a expect(sorted_tag_names(tags)).to contain_exactly(tag2.name) end it "lets staff ignore the requirement" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(admin), - for_input: true, - category: category, - limit: 5 - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(admin), + for_input: true, + category: category, + limit: 5, + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3])) end @@ -250,144 +277,184 @@ RSpec.describe DiscourseTagging do it "handles multiple required tag groups in sequence" do tag4 = Fabricate(:tag) tag_group_2 = Fabricate(:tag_group, tags: [tag4]) - CategoryRequiredTagGroup.create!(category: category, tag_group: tag_group_2, min_count: 1, order: 2) + CategoryRequiredTagGroup.create!( + category: category, + tag_group: tag_group_2, + min_count: 1, + order: 2, + ) category.reload # In the beginning, show tags for tag_group - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2])) # Once a tag_group tag has been selected, move on to tag_group_2 tags - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - selected_tags: [tag1.name], - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + selected_tags: [tag1.name], + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag4])) # Once all requirements are satisfied, show all tags - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - category: category, - selected_tags: [tag1.name, tag4.name], - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + category: category, + selected_tags: [tag1.name, tag4.name], + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag2, tag3])) end end - context 'with many required tags in a tag group' do + context "with many required tags in a tag group" do fab!(:tag4) { Fabricate(:tag, name: "T4") } fab!(:tag5) { Fabricate(:tag, name: "T5") } fab!(:tag6) { Fabricate(:tag, name: "T6") } fab!(:tag7) { Fabricate(:tag, name: "T7") } fab!(:tag_group) { Fabricate(:tag_group, tags: [tag1, tag2, tag4, tag5, tag6, tag7]) } - fab!(:category) { Fabricate(:category, category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)]) } + fab!(:category) do + Fabricate( + :category, + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], + ) + end it "returns required tags for staff by default" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(admin), - for_input: true, - category: category - ).to_a - expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag4, tag5, tag6, tag7])) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(admin), + for_input: true, + category: category, + ).to_a + expect(sorted_tag_names(tags)).to eq( + sorted_tag_names([tag1, tag2, tag4, tag5, tag6, tag7]), + ) end it "ignores required tags for staff when searching using a term" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(admin), - for_input: true, - category: category, - term: 'fun' - ).to_a + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(admin), + for_input: true, + category: category, + term: "fun", + ).to_a expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3])) end it "returns required tags for nonstaff and overrides limit" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - limit: 5, - category: category - ).to_a - expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag4, tag5, tag6, tag7])) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + limit: 5, + category: category, + ).to_a + expect(sorted_tag_names(tags)).to eq( + sorted_tag_names([tag1, tag2, tag4, tag5, tag6, tag7]), + ) end - end - context 'with empty term' do + context "with empty term" do it "works with an empty term" do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - term: '', - order_search_results: true - ).map(&:name) + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + term: "", + order_search_results: true, + ).map(&:name) expect(sorted_tag_names(tags)).to eq(sorted_tag_names([tag1, tag2, tag3])) end end - context 'with tag synonyms' do - fab!(:base_tag) { Fabricate(:tag, name: 'discourse') } - fab!(:synonym) { Fabricate(:tag, name: 'discource', target_tag: base_tag) } + context "with tag synonyms" do + fab!(:base_tag) { Fabricate(:tag, name: "discourse") } + fab!(:synonym) { Fabricate(:tag, name: "discource", target_tag: base_tag) } - it 'returns synonyms by default' do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - term: 'disc' - ).map(&:name) + it "returns synonyms by default" do + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + term: "disc", + ).map(&:name) expect(tags).to contain_exactly(base_tag.name, synonym.name) end - it 'excludes synonyms with exclude_synonyms param' do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - exclude_synonyms: true, - term: 'disc' - ).map(&:name) + it "excludes synonyms with exclude_synonyms param" do + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + exclude_synonyms: true, + term: "disc", + ).map(&:name) expect(tags).to contain_exactly(base_tag.name) end - it 'excludes tags with synonyms with exclude_has_synonyms params' do - tags = DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - exclude_has_synonyms: true, - term: 'disc' - ).map(&:name) + it "excludes tags with synonyms with exclude_has_synonyms params" do + tags = + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + exclude_has_synonyms: true, + term: "disc", + ).map(&:name) expect(tags).to contain_exactly(synonym.name) end - it 'can exclude synonyms and tags with synonyms' do - expect(DiscourseTagging.filter_allowed_tags(Guardian.new(user), - for_input: true, - exclude_has_synonyms: true, - exclude_synonyms: true, - term: 'disc' - )).to be_empty + it "can exclude synonyms and tags with synonyms" do + expect( + DiscourseTagging.filter_allowed_tags( + Guardian.new(user), + for_input: true, + exclude_has_synonyms: true, + exclude_synonyms: true, + term: "disc", + ), + ).to be_empty end end end end - describe 'filter_visible' do + describe "filter_visible" do fab!(:hidden_tag) { Fabricate(:tag) } - let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let!(:staff_tag_group) do + Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) + end fab!(:topic) { Fabricate(:topic, tags: [tag1, tag2, tag3, hidden_tag]) } - it 'returns all tags to staff' do + it "returns all tags to staff" do tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(admin)) expect(tags.size).to eq(4) expect(tags).to contain_exactly(tag1, tag2, tag3, hidden_tag) end - it 'does not return hidden tags to non-staff' do + it "does not return hidden tags to non-staff" do tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(user)) expect(tags.size).to eq(3) expect(tags).to contain_exactly(tag1, tag2, tag3) end - it 'returns staff only tags to everyone' do - create_staff_only_tags(['important']) - staff_tag = Tag.where(name: 'important').first + it "returns staff only tags to everyone" do + create_staff_only_tags(["important"]) + staff_tag = Tag.where(name: "important").first topic.tags << staff_tag tags = DiscourseTagging.filter_visible(topic.tags, Guardian.new(user)) expect(tags.size).to eq(4) @@ -395,69 +462,71 @@ RSpec.describe DiscourseTagging do end end - describe 'tag_topic_by_names' do - context 'with visible but restricted tags' do + describe "tag_topic_by_names" do + context "with visible but restricted tags" do fab!(:topic) { Fabricate(:topic) } - before do - create_staff_only_tags(['alpha']) - end + before { create_staff_only_tags(["alpha"]) } it "regular users can't add staff-only tags" do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ['alpha']) + valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ["alpha"]) expect(valid).to eq(false) - expect(topic.errors[:base]&.first).to eq(I18n.t("tags.restricted_tag_disallowed", tag: 'alpha')) + expect(topic.errors[:base]&.first).to eq( + I18n.t("tags.restricted_tag_disallowed", tag: "alpha"), + ) end it "does not send a discourse event for regular users who can't add staff-only tags" do - events = DiscourseEvent.track_events do - DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ['alpha']) - end + events = + DiscourseEvent.track_events do + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ["alpha"]) + end expect(events.count).to eq(0) end - it 'staff can add staff-only tags' do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), ['alpha']) + it "staff can add staff-only tags" do + valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), ["alpha"]) expect(valid).to eq(true) expect(topic.errors[:base]).to be_empty end - it 'sends a discourse event when the staff adds a staff-only tag' do + it "sends a discourse event when the staff adds a staff-only tag" do old_tag_names = topic.tags.pluck(:name) - tag_changed_event = DiscourseEvent.track_events do - DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), ['alpha']) - end.last + tag_changed_event = + DiscourseEvent + .track_events do + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), ["alpha"]) + end + .last expect(tag_changed_event[:event_name]).to eq(:topic_tags_changed) expect(tag_changed_event[:params].first).to eq(topic) expect(tag_changed_event[:params].second[:old_tag_names]).to eq(old_tag_names) - expect(tag_changed_event[:params].second[:new_tag_names]).to eq(['alpha']) + expect(tag_changed_event[:params].second[:new_tag_names]).to eq(["alpha"]) end - context 'with non-staff users in tag group groups' do - fab!(:non_staff_group) { Fabricate(:group, name: 'non_staff_group') } + context "with non-staff users in tag group groups" do + fab!(:non_staff_group) { Fabricate(:group, name: "non_staff_group") } - before do - create_limited_tags('Group for Non-Staff', non_staff_group.id, ['alpha']) - end + before { create_limited_tags("Group for Non-Staff", non_staff_group.id, ["alpha"]) } - it 'can use hidden tag if in correct group' do + it "can use hidden tag if in correct group" do non_staff_group.add(user) - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ['alpha']) + valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), ["alpha"]) expect(valid).to eq(true) expect(topic.errors[:base]).to be_empty end - it 'will return error if user is not in correct group' do + it "will return error if user is not in correct group" do user2 = Fabricate(:user) - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user2), ['alpha']) + valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user2), ["alpha"]) expect(valid).to eq(false) end end end - it 'respects category allow_global_tags setting' do + it "respects category allow_global_tags setting" do tag = Fabricate(:tag) other_tag = Fabricate(:tag) tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag.name]) @@ -465,17 +534,27 @@ RSpec.describe DiscourseTagging do other_category = Fabricate(:category, allowed_tags: [other_tag.name]) topic = Fabricate(:topic, category: category) - result = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), [tag.name, other_tag.name, 'hello']) + result = + DiscourseTagging.tag_topic_by_names( + topic, + Guardian.new(admin), + [tag.name, other_tag.name, "hello"], + ) expect(result).to eq(true) expect(topic.tags.pluck(:name)).to contain_exactly(tag.name) category.update!(allow_global_tags: true) - result = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), [tag.name, other_tag.name, 'hello']) + result = + DiscourseTagging.tag_topic_by_names( + topic, + Guardian.new(admin), + [tag.name, other_tag.name, "hello"], + ) expect(result).to eq(true) - expect(topic.tags.pluck(:name)).to contain_exactly(tag.name, 'hello') + expect(topic.tags.pluck(:name)).to contain_exactly(tag.name, "hello") end - it 'raises an error if no tags could be updated' do + it "raises an error if no tags could be updated" do tag = Fabricate(:tag) other_tag = Fabricate(:tag) tag_group = Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [tag.name]) @@ -488,7 +567,7 @@ RSpec.describe DiscourseTagging do expect(topic.tags.pluck(:name)).to be_blank end - it 'can remove tags and keep existent ones' do + it "can remove tags and keep existent ones" do tag1 = Fabricate(:tag) tag2 = Fabricate(:tag) topic = Fabricate(:topic, tags: [tag1, tag2]) @@ -500,66 +579,81 @@ RSpec.describe DiscourseTagging do expect(topic.reload.tags.pluck(:name)).to eq([tag1.name]) end - context 'when respecting category minimum_required_tags setting' do + context "when respecting category minimum_required_tags setting" do fab!(:category) { Fabricate(:category, minimum_required_tags: 2) } fab!(:topic) { Fabricate(:topic, category: category) } - it 'when tags are not present' do + it "when tags are not present" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), []) expect(valid).to eq(false) - expect(topic.errors[:base]&.first).to eq(I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags)) + expect(topic.errors[:base]&.first).to eq( + I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags), + ) end - it 'when tags are less than minimum_required_tags' do + it "when tags are less than minimum_required_tags" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name]) expect(valid).to eq(false) - expect(topic.errors[:base]&.first).to eq(I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags)) + expect(topic.errors[:base]&.first).to eq( + I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags), + ) end - it 'when tags are equal to minimum_required_tags' do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag2.name]) + it "when tags are equal to minimum_required_tags" do + valid = + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag2.name]) expect(valid).to eq(true) expect(topic.errors[:base]).to be_empty end - it 'lets admin tag a topic regardless of minimum_required_tags' do + it "lets admin tag a topic regardless of minimum_required_tags" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(admin), [tag1.name]) expect(valid).to eq(true) expect(topic.errors[:base]).to be_empty end end - context 'with hidden tags' do + context "with hidden tags" do fab!(:hidden_tag) { Fabricate(:tag) } - let!(:staff_tag_group) { Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) } + let!(:staff_tag_group) do + Fabricate(:tag_group, permissions: { "staff" => 1 }, tag_names: [hidden_tag.name]) + end fab!(:topic) { Fabricate(:topic, user: user) } fab!(:post) { Fabricate(:post, user: user, topic: topic, post_number: 1) } - it 'user cannot add hidden tag by knowing its name' do - expect(PostRevisor.new(post).revise!(topic.user, raw: post.raw + " edit", tags: [hidden_tag.name])).to be_falsey + it "user cannot add hidden tag by knowing its name" do + expect( + PostRevisor.new(post).revise!( + topic.user, + raw: post.raw + " edit", + tags: [hidden_tag.name], + ), + ).to be_falsey expect(topic.reload.tags).to be_empty end - it 'admin can add hidden tag' do - expect(PostRevisor.new(post).revise!(admin, raw: post.raw, tags: [hidden_tag.name])).to be_truthy + it "admin can add hidden tag" do + expect( + PostRevisor.new(post).revise!(admin, raw: post.raw, tags: [hidden_tag.name]), + ).to be_truthy expect(topic.reload.tags).to eq([hidden_tag]) end - it 'user does not get an error when editing their topic with a hidden tag' do + it "user does not get an error when editing their topic with a hidden tag" do PostRevisor.new(post).revise!(admin, raw: post.raw, tags: [hidden_tag.name]) - expect(PostRevisor.new(post).revise!(topic.user, raw: post.raw + " edit", tags: [])).to be_truthy + expect( + PostRevisor.new(post).revise!(topic.user, raw: post.raw + " edit", tags: []), + ).to be_truthy expect(topic.reload.tags).to eq([hidden_tag]) end end - context 'with tag group with parent tag' do + context "with tag group with parent tag" do let(:topic) { Fabricate(:topic, user: user) } let(:post) { Fabricate(:post, user: user, topic: topic, post_number: 1) } let(:tag_group) { Fabricate(:tag_group, parent_tag_id: tag1.id) } - before do - tag_group.tags = [tag3] - end + before { tag_group.tags = [tag3] } it "can tag with parent" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name]) @@ -568,31 +662,38 @@ RSpec.describe DiscourseTagging do end it "can tag with parent and a tag" do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag3.name]) + valid = + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag3.name]) expect(valid).to eq(true) expect(topic.reload.tags.map(&:name)).to contain_exactly(*[tag1, tag3].map(&:name)) end it "adds all parent tags that are missing" do - parent_tag = Fabricate(:tag, name: 'parent') + parent_tag = Fabricate(:tag, name: "parent") tag_group2 = Fabricate(:tag_group, parent_tag_id: parent_tag.id) tag_group2.tags = [tag2] - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag3.name, tag2.name]) + valid = + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag3.name, tag2.name]) expect(valid).to eq(true) expect(topic.reload.tags.map(&:name)).to contain_exactly( - *[tag1, tag2, tag3, parent_tag].map(&:name) + *[tag1, tag2, tag3, parent_tag].map(&:name), ) end it "adds only the necessary parent tags" do - common = Fabricate(:tag, name: 'common') + common = Fabricate(:tag, name: "common") tag_group.tags = [tag3, common] - parent_tag = Fabricate(:tag, name: 'parent') + parent_tag = Fabricate(:tag, name: "parent") tag_group2 = Fabricate(:tag_group, parent_tag_id: parent_tag.id) tag_group2.tags = [tag2, common] - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [parent_tag.name, common.name]) + valid = + DiscourseTagging.tag_topic_by_names( + topic, + Guardian.new(user), + [parent_tag.name, common.name], + ) expect(valid).to eq(true) expect(topic.reload.tags.map(&:name)).to contain_exactly(*[parent_tag, common].map(&:name)) end @@ -606,7 +707,9 @@ RSpec.describe DiscourseTagging do before do tag_group.tags = [tag1, tag2] category.update( - category_required_tag_groups: [CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1)], + category_required_tag_groups: [ + CategoryRequiredTagGroup.new(tag_group: tag_group, min_count: 1), + ], ) end @@ -614,7 +717,12 @@ RSpec.describe DiscourseTagging do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), []) expect(valid).to eq(false) expect(topic.errors[:base]&.first).to eq( - I18n.t("tags.required_tags_from_group", count: 1, tag_group_name: tag_group.name, tags: tag_group.tags.pluck(:name).join(", ")) + I18n.t( + "tags.required_tags_from_group", + count: 1, + tag_group_name: tag_group.name, + tags: tag_group.tags.pluck(:name).join(", "), + ), ) end @@ -622,16 +730,23 @@ RSpec.describe DiscourseTagging do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag3.name]) expect(valid).to eq(false) expect(topic.errors[:base]&.first).to eq( - I18n.t("tags.required_tags_from_group", count: 1, tag_group_name: tag_group.name, tags: tag_group.tags.pluck(:name).join(", ")) + I18n.t( + "tags.required_tags_from_group", + count: 1, + tag_group_name: tag_group.name, + tags: tag_group.tags.pluck(:name).join(", "), + ), ) end it "when requirement is met" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name]) expect(valid).to eq(true) - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag2.name]) + valid = + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, tag2.name]) expect(valid).to eq(true) - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag2.name, tag3.name]) + valid = + DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag2.name, tag3.name]) expect(valid).to eq(true) end @@ -643,11 +758,11 @@ RSpec.describe DiscourseTagging do end end - context 'with tag synonyms' do + context "with tag synonyms" do fab!(:topic) { Fabricate(:topic) } - fab!(:syn1) { Fabricate(:tag, name: 'synonym1', target_tag: tag1) } - fab!(:syn2) { Fabricate(:tag, name: 'synonym2', target_tag: tag1) } + fab!(:syn1) { Fabricate(:tag, name: "synonym1", target_tag: tag1) } + fab!(:syn2) { Fabricate(:tag, name: "synonym2", target_tag: tag1) } it "uses the base tag when a synonym is given" do valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [syn1.name]) @@ -657,7 +772,12 @@ RSpec.describe DiscourseTagging do end it "handles multiple synonyms for the same tag" do - valid = DiscourseTagging.tag_topic_by_names(topic, Guardian.new(user), [tag1.name, syn1.name, syn2.name]) + valid = + DiscourseTagging.tag_topic_by_names( + topic, + Guardian.new(user), + [tag1.name, syn1.name, syn2.name], + ) expect(valid).to eq(true) expect(topic.errors[:base]).to be_empty expect_same_tag_names(topic.reload.tags, [tag1]) @@ -665,7 +785,7 @@ RSpec.describe DiscourseTagging do end end - describe '#tags_for_saving' do + describe "#tags_for_saving" do it "returns empty array if input is nil" do expect(described_class.tags_for_saving(nil, guardian)).to eq([]) end @@ -676,7 +796,7 @@ RSpec.describe DiscourseTagging do it "returns empty array if can't tag topics" do guardian.stubs(:can_tag_topics?).returns(false) - expect(described_class.tags_for_saving(['newtag'], guardian)).to eq([]) + expect(described_class.tags_for_saving(["newtag"], guardian)).to eq([]) end describe "can tag topics but not create tags" do @@ -686,13 +806,15 @@ RSpec.describe DiscourseTagging do end it "returns empty array if all tags are new" do - expect(described_class.tags_for_saving(['newtag', 'newtagplz'], guardian)).to eq([]) + expect(described_class.tags_for_saving(%w[newtag newtagplz], guardian)).to eq([]) end it "returns only existing tag names" do - Fabricate(:tag, name: 'oldtag') - Fabricate(:tag, name: 'oldTag2') - expect(described_class.tags_for_saving(['newtag', 'oldtag', 'oldtag2'], guardian)).to contain_exactly('oldtag', 'oldTag2') + Fabricate(:tag, name: "oldtag") + Fabricate(:tag, name: "oldTag2") + expect( + described_class.tags_for_saving(%w[newtag oldtag oldtag2], guardian), + ).to contain_exactly("oldtag", "oldTag2") end end @@ -703,12 +825,16 @@ RSpec.describe DiscourseTagging do end it "returns given tag names if can create new tags and tag topics" do - expect(described_class.tags_for_saving(['newtag1', 'newtag2'], guardian).try(:sort)).to eq(['newtag1', 'newtag2']) + expect(described_class.tags_for_saving(%w[newtag1 newtag2], guardian).try(:sort)).to eq( + %w[newtag1 newtag2], + ) end it "only sanitizes new tags" do # for backwards compat - Tag.new(name: 'math=fun').save(validate: false) - expect(described_class.tags_for_saving(['math=fun', 'fun*2@gmail.com'], guardian).try(:sort)).to eq(['math=fun', 'fun2gmailcom'].sort) + Tag.new(name: "math=fun").save(validate: false) + expect( + described_class.tags_for_saving(%w[math=fun fun*2@gmail.com], guardian).try(:sort), + ).to eq(%w[math=fun fun2gmailcom].sort) end end @@ -732,26 +858,32 @@ RSpec.describe DiscourseTagging do fab!(:staff_tag) { Fabricate(:tag) } fab!(:other_staff_tag) { Fabricate(:tag) } - let!(:staff_tag_group) { + let!(:staff_tag_group) do Fabricate( :tag_group, - permissions: { "staff" => 1, "everyone" => 3 }, - tag_names: [staff_tag.name] + permissions: { + "staff" => 1, + "everyone" => 3, + }, + tag_names: [staff_tag.name], ) - } + end it "returns all staff tags" do expect(DiscourseTagging.staff_tag_names).to contain_exactly(staff_tag.name) staff_tag_group.update(tag_names: [staff_tag.name, other_staff_tag.name]) - expect(DiscourseTagging.staff_tag_names).to contain_exactly(staff_tag.name, other_staff_tag.name) + expect(DiscourseTagging.staff_tag_names).to contain_exactly( + staff_tag.name, + other_staff_tag.name, + ) staff_tag_group.update(tag_names: [other_staff_tag.name]) expect(DiscourseTagging.staff_tag_names).to contain_exactly(other_staff_tag.name) end end - describe '#add_or_create_synonyms_by_name' do + describe "#add_or_create_synonyms_by_name" do it "can add an existing tag" do expect { expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name])).to eq(true) @@ -772,7 +904,9 @@ RSpec.describe DiscourseTagging do it "can add existing tag with wrong case" do expect { - expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name.upcase])).to eq(true) + expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name.upcase])).to eq( + true, + ) }.to_not change { Tag.count } expect_same_tag_names(tag1.reload.synonyms, [tag2]) expect(tag2.reload.target_tag).to eq(tag1) @@ -780,7 +914,9 @@ RSpec.describe DiscourseTagging do it "removes target tag name from synonyms if present " do expect { - expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag1.name, tag2.name])).to eq(true) + expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag1.name, tag2.name])).to eq( + true, + ) }.to_not change { Tag.count } expect_same_tag_names(tag1.reload.synonyms, [tag2]) expect(tag2.reload.target_tag).to eq(tag1) @@ -788,25 +924,27 @@ RSpec.describe DiscourseTagging do it "can create new tags" do expect { - expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, ['synonym1'])).to eq(true) + expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, ["synonym1"])).to eq(true) }.to change { Tag.count }.by(1) - s = Tag.where_name('synonym1').first + s = Tag.where_name("synonym1").first expect_same_tag_names(tag1.reload.synonyms, [s]) expect(s.target_tag).to eq(tag1) end it "can add existing and new tags" do expect { - expect(DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name, 'synonym1'])).to eq(true) + expect( + DiscourseTagging.add_or_create_synonyms_by_name(tag1, [tag2.name, "synonym1"]), + ).to eq(true) }.to change { Tag.count }.by(1) - s = Tag.where_name('synonym1').first + s = Tag.where_name("synonym1").first expect_same_tag_names(tag1.reload.synonyms, [tag2, s]) expect(s.target_tag).to eq(tag1) expect(tag2.reload.target_tag).to eq(tag1) end it "can change a synonym's target tag" do - synonym = Fabricate(:tag, name: 'synonym1', target_tag: tag1) + synonym = Fabricate(:tag, name: "synonym1", target_tag: tag1) expect { expect(DiscourseTagging.add_or_create_synonyms_by_name(tag2, [synonym.name])).to eq(true) }.to_not change { Tag.count } diff --git a/spec/lib/discourse_updates_spec.rb b/spec/lib/discourse_updates_spec.rb index 158f850ebc..efbb2467af 100644 --- a/spec/lib/discourse_updates_spec.rb +++ b/spec/lib/discourse_updates_spec.rb @@ -10,17 +10,15 @@ RSpec.describe DiscourseUpdates do subject { DiscourseUpdates.check_version } - context 'when version check was done at the current installed version' do - before do - DiscourseUpdates.last_installed_version = Discourse::VERSION::STRING - end + context "when version check was done at the current installed version" do + before { DiscourseUpdates.last_installed_version = Discourse::VERSION::STRING } - context 'when a good version check request happened recently' do - context 'when server is up-to-date' do + context "when a good version check request happened recently" do + context "when server is up-to-date" do let(:time) { 12.hours.ago } before { stub_data(Discourse::VERSION::STRING, 0, false, time) } - it 'returns all the version fields' do + it "returns all the version fields" do expect(subject.latest_version).to eq(Discourse::VERSION::STRING) expect(subject.missing_versions_count).to eq(0) expect(subject.critical_updates).to eq(false) @@ -28,144 +26,151 @@ RSpec.describe DiscourseUpdates do expect(subject.stale_data).to eq(false) end - it 'returns the timestamp of the last version check' do + it "returns the timestamp of the last version check" do expect(subject.updated_at).to be_within_one_second_of(time) end end - context 'when server is not up-to-date' do + context "when server is not up-to-date" do let(:time) { 12.hours.ago } - before { stub_data('0.9.0', 2, false, time) } + before { stub_data("0.9.0", 2, false, time) } - it 'returns all the version fields' do - expect(subject.latest_version).to eq('0.9.0') + it "returns all the version fields" do + expect(subject.latest_version).to eq("0.9.0") expect(subject.missing_versions_count).to eq(2) expect(subject.critical_updates).to eq(false) expect(subject.installed_version).to eq(Discourse::VERSION::STRING) end - it 'returns the timestamp of the last version check' do + it "returns the timestamp of the last version check" do expect(subject.updated_at).to be_within_one_second_of(time) end end end - context 'when a version check has never been performed' do + context "when a version check has never been performed" do before { stub_data(nil, nil, false, nil) } - it 'returns the installed version' do + it "returns the installed version" do expect(subject.installed_version).to eq(Discourse::VERSION::STRING) end - it 'indicates that version check has not been performed' do + it "indicates that version check has not been performed" do expect(subject.updated_at).to eq(nil) expect(subject.stale_data).to eq(true) end - it 'does not return latest version info' do + it "does not return latest version info" do expect(subject.latest_version).to eq(nil) expect(subject.missing_versions_count).to eq(nil) expect(subject.critical_updates).to eq(nil) end - it 'queues a version check' do - expect_enqueued_with(job: :version_check) do - subject - end + it "queues a version check" do + expect_enqueued_with(job: :version_check) { subject } end end # These cases should never happen anymore, but keep the specs to be sure # they're handled in a sane way. - context 'with old version check data' do + context "with old version check data" do shared_examples "queue version check and report that version is ok" do - it 'queues a version check' do - expect_enqueued_with(job: :version_check) do - subject - end + it "queues a version check" do + expect_enqueued_with(job: :version_check) { subject } end - it 'reports 0 missing versions' do + it "reports 0 missing versions" do expect(subject.missing_versions_count).to eq(0) end - it 'reports that a version check will be run soon' do + it "reports that a version check will be run soon" do expect(subject.version_check_pending).to eq(true) end end - context 'when installed is latest' do + context "when installed is latest" do before { stub_data(Discourse::VERSION::STRING, 1, false, 8.hours.ago) } include_examples "queue version check and report that version is ok" end - context 'when installed does not match latest version, but missing_versions_count is 0' do - before { stub_data('0.10.10.123', 0, false, 8.hours.ago) } + context "when installed does not match latest version, but missing_versions_count is 0" do + before { stub_data("0.10.10.123", 0, false, 8.hours.ago) } include_examples "queue version check and report that version is ok" end end end - context 'when version check was done at a different installed version' do - before do - DiscourseUpdates.last_installed_version = '0.9.1' - end + context "when version check was done at a different installed version" do + before { DiscourseUpdates.last_installed_version = "0.9.1" } shared_examples "when last_installed_version is old" do - it 'queues a version check' do - expect_enqueued_with(job: :version_check) do - subject - end + it "queues a version check" do + expect_enqueued_with(job: :version_check) { subject } end - it 'reports 0 missing versions' do + it "reports 0 missing versions" do expect(subject.missing_versions_count).to eq(0) end - it 'reports that a version check will be run soon' do + it "reports that a version check will be run soon" do expect(subject.version_check_pending).to eq(true) end end - context 'when missing_versions_count is 0' do - before { stub_data('0.9.7', 0, false, 8.hours.ago) } + context "when missing_versions_count is 0" do + before { stub_data("0.9.7", 0, false, 8.hours.ago) } include_examples "when last_installed_version is old" end - context 'when missing_versions_count is not 0' do - before { stub_data('0.9.7', 1, false, 8.hours.ago) } + context "when missing_versions_count is not 0" do + before { stub_data("0.9.7", 1, false, 8.hours.ago) } include_examples "when last_installed_version is old" end end - describe 'new features' do + describe "new features" do fab!(:admin) { Fabricate(:admin) } fab!(:admin2) { Fabricate(:admin) } let!(:last_item_date) { 5.minutes.ago } - let!(:sample_features) { [ - { "emoji" => "🤾", "title" => "Super Fruits", "description" => "Taste explosion!", "created_at" => 40.minutes.ago }, - { "emoji" => "🙈", "title" => "Fancy Legumes", "description" => "Magic legumes!", "created_at" => 15.minutes.ago }, - { "emoji" => "🤾", "title" => "Quality Veggies", "description" => "Green goodness!", "created_at" => last_item_date }, - ] } + let!(:sample_features) do + [ + { + "emoji" => "🤾", + "title" => "Super Fruits", + "description" => "Taste explosion!", + "created_at" => 40.minutes.ago, + }, + { + "emoji" => "🙈", + "title" => "Fancy Legumes", + "description" => "Magic legumes!", + "created_at" => 15.minutes.ago, + }, + { + "emoji" => "🤾", + "title" => "Quality Veggies", + "description" => "Green goodness!", + "created_at" => last_item_date, + }, + ] + end before(:each) do Discourse.redis.del "new_features_last_seen_user_#{admin.id}" Discourse.redis.del "new_features_last_seen_user_#{admin2.id}" - Discourse.redis.set('new_features', MultiJson.dump(sample_features)) + Discourse.redis.set("new_features", MultiJson.dump(sample_features)) end - after do - DiscourseUpdates.clean_state - end + after { DiscourseUpdates.clean_state } - it 'returns all items on the first run' do + it "returns all items on the first run" do result = DiscourseUpdates.new_features expect(result.length).to eq(3) expect(result[2]["title"]).to eq("Super Fruits") end - it 'correctly marks unseen items by user' do + it "correctly marks unseen items by user" do DiscourseUpdates.stubs(:new_features_last_seen).with(admin.id).returns(10.minutes.ago) DiscourseUpdates.stubs(:new_features_last_seen).with(admin2.id).returns(30.minutes.ago) @@ -173,7 +178,7 @@ RSpec.describe DiscourseUpdates do expect(DiscourseUpdates.has_unseen_features?(admin2.id)).to eq(true) end - it 'can mark features as seen for a given user' do + it "can mark features as seen for a given user" do expect(DiscourseUpdates.has_unseen_features?(admin.id)).to be_truthy DiscourseUpdates.mark_new_features_as_seen(admin.id) @@ -183,31 +188,58 @@ RSpec.describe DiscourseUpdates do expect(DiscourseUpdates.has_unseen_features?(admin2.id)).to eq(true) end - it 'correctly sees newly added features as unseen' do + it "correctly sees newly added features as unseen" do DiscourseUpdates.mark_new_features_as_seen(admin.id) expect(DiscourseUpdates.has_unseen_features?(admin.id)).to eq(false) - expect(DiscourseUpdates.new_features_last_seen(admin.id)).to be_within(1.second).of (last_item_date) + expect(DiscourseUpdates.new_features_last_seen(admin.id)).to be_within(1.second).of ( + last_item_date + ) updated_features = [ - { "emoji" => "🤾", "title" => "Brand New Item", "created_at" => 2.minutes.ago } + { "emoji" => "🤾", "title" => "Brand New Item", "created_at" => 2.minutes.ago }, ] updated_features += sample_features - Discourse.redis.set('new_features', MultiJson.dump(updated_features)) + Discourse.redis.set("new_features", MultiJson.dump(updated_features)) expect(DiscourseUpdates.has_unseen_features?(admin.id)).to eq(true) end - it 'correctly shows features by Discourse version' do + it "correctly shows features by Discourse version" do features_with_versions = [ { "emoji" => "🤾", "title" => "Bells", "created_at" => 2.days.ago }, - { "emoji" => "🙈", "title" => "Whistles", "created_at" => 120.minutes.ago, discourse_version: "2.6.0.beta1" }, - { "emoji" => "🙈", "title" => "Confetti", "created_at" => 15.minutes.ago, discourse_version: "2.7.0.beta2" }, - { "emoji" => "🤾", "title" => "Not shown yet", "created_at" => 10.minutes.ago, discourse_version: "2.7.0.beta5" }, - { "emoji" => "🤾", "title" => "Not shown yet (beta < stable)", "created_at" => 10.minutes.ago, discourse_version: "2.7.0" }, - { "emoji" => "🤾", "title" => "Ignore invalid version", "created_at" => 10.minutes.ago, discourse_version: "invalid-version" }, + { + "emoji" => "🙈", + "title" => "Whistles", + "created_at" => 120.minutes.ago, + :discourse_version => "2.6.0.beta1", + }, + { + "emoji" => "🙈", + "title" => "Confetti", + "created_at" => 15.minutes.ago, + :discourse_version => "2.7.0.beta2", + }, + { + "emoji" => "🤾", + "title" => "Not shown yet", + "created_at" => 10.minutes.ago, + :discourse_version => "2.7.0.beta5", + }, + { + "emoji" => "🤾", + "title" => "Not shown yet (beta < stable)", + "created_at" => 10.minutes.ago, + :discourse_version => "2.7.0", + }, + { + "emoji" => "🤾", + "title" => "Ignore invalid version", + "created_at" => 10.minutes.ago, + :discourse_version => "invalid-version", + }, ] - Discourse.redis.set('new_features', MultiJson.dump(features_with_versions)) + Discourse.redis.set("new_features", MultiJson.dump(features_with_versions)) DiscourseUpdates.last_installed_version = "2.7.0.beta2" result = DiscourseUpdates.new_features diff --git a/spec/lib/distributed_cache_spec.rb b/spec/lib/distributed_cache_spec.rb index 7099365d86..0d6225360b 100644 --- a/spec/lib/distributed_cache_spec.rb +++ b/spec/lib/distributed_cache_spec.rb @@ -1,26 +1,26 @@ # frozen_string_literal: true RSpec.describe DistributedCache do - let(:cache) { described_class.new('mytest') } + let(:cache) { described_class.new("mytest") } it "can defer_get_set" do - messages = MessageBus.track_publish("/distributed_hash") do - cache.defer_get_set("key") { "value" } - end + messages = + MessageBus.track_publish("/distributed_hash") { cache.defer_get_set("key") { "value" } } expect(messages.size).to eq(1) expect(cache["key"]).to eq("value") end it "works correctly for nil values" do block_called_counter = 0 - messages = MessageBus.track_publish("/distributed_hash") do - 2.times do - cache.defer_get_set("key") do - block_called_counter += 1 - nil + messages = + MessageBus.track_publish("/distributed_hash") do + 2.times do + cache.defer_get_set("key") do + block_called_counter += 1 + nil + end end end - end expect(block_called_counter).to eq(1) expect(messages.size).to eq(1) diff --git a/spec/lib/distributed_memoizer_spec.rb b/spec/lib/distributed_memoizer_spec.rb index 980a3a6678..630a9f5dd5 100644 --- a/spec/lib/distributed_memoizer_spec.rb +++ b/spec/lib/distributed_memoizer_spec.rb @@ -15,9 +15,7 @@ RSpec.describe DistributedMemoizer do end it "return the old value once memoized" do - memoize do - "abc" - end + memoize { "abc" } expect(memoize { "world" }).to eq("abc") end diff --git a/spec/lib/distributed_mutex_spec.rb b/spec/lib/distributed_mutex_spec.rb index b1cf14c178..d786737a58 100644 --- a/spec/lib/distributed_mutex_spec.rb +++ b/spec/lib/distributed_mutex_spec.rb @@ -1,31 +1,27 @@ # frozen_string_literal: true RSpec.describe DistributedMutex do - before do - DistributedMutex.any_instance.stubs(:sleep) - end + before { DistributedMutex.any_instance.stubs(:sleep) } let(:key) { "test_mutex_key" } - after do - Discourse.redis.del(key) - end + after { Discourse.redis.del(key) } it "allows only one mutex object to have the lock at a time" do - mutexes = (1..10).map do - DistributedMutex.new(key, redis: DiscourseRedis.new) - end + mutexes = (1..10).map { DistributedMutex.new(key, redis: DiscourseRedis.new) } x = 0 - mutexes.map do |m| - Thread.new do - m.synchronize do - y = x - sleep 0.001 - x = y + 1 + mutexes + .map do |m| + Thread.new do + m.synchronize do + y = x + sleep 0.001 + x = y + 1 + end end end - end.map(&:join) + .map(&:join) expect(x).to eq(10) end @@ -36,9 +32,7 @@ RSpec.describe DistributedMutex do Discourse.redis.setnx key, Time.now.to_i - 1 start = Time.now - m.synchronize do - "nop" - end + m.synchronize { "nop" } # no longer than a second expect(Time.now).to be <= start + 1 @@ -56,39 +50,29 @@ RSpec.describe DistributedMutex do mutex.synchronize do expect(Discourse.redis.ttl(key)).to be <= DistributedMutex::DEFAULT_VALIDITY + 1 - expect(Discourse.redis.get(key).to_i).to be_within(1.second).of(Time.now.to_i + DistributedMutex::DEFAULT_VALIDITY) + expect(Discourse.redis.get(key).to_i).to be_within(1.second).of( + Time.now.to_i + DistributedMutex::DEFAULT_VALIDITY, + ) end end it "maintains mutex semantics" do m = DistributedMutex.new(key) - expect { - m.synchronize do - m.synchronize {} - end - }.to raise_error(ThreadError) + expect { m.synchronize { m.synchronize {} } }.to raise_error(ThreadError) end describe "readonly redis" do - before do - Discourse.redis.slaveof "127.0.0.1", "65534" - end + before { Discourse.redis.slaveof "127.0.0.1", "65534" } - after do - Discourse.redis.slaveof "no", "one" - end + after { Discourse.redis.slaveof "no", "one" } it "works even if redis is in readonly" do m = DistributedMutex.new(key) start = Time.now done = false - expect { - m.synchronize do - done = true - end - }.to raise_error(Discourse::ReadOnly) + expect { m.synchronize { done = true } }.to raise_error(Discourse::ReadOnly) expect(done).to eq(false) expect(Time.now).to be <= start + 1 @@ -103,23 +87,17 @@ RSpec.describe DistributedMutex do Concurrency::Scenario.new do |execution| locked = false - Discourse.redis.del('mutex_key') + Discourse.redis.del("mutex_key") - connections.each do |connection| - connection.unwatch - end + connections.each { |connection| connection.unwatch } 3.times do |i| execution.spawn do begin - redis = - Concurrency::RedisWrapper.new( - connections[i], - execution - ) + redis = Concurrency::RedisWrapper.new(connections[i], execution) 2.times do - DistributedMutex.synchronize('mutex_key', redis: redis) do + DistributedMutex.synchronize("mutex_key", redis: redis) do raise "already locked #{execution.path}" if locked locked = true diff --git a/spec/lib/email/authentication_results_spec.rb b/spec/lib/email/authentication_results_spec.rb index 3c32eb4dc2..d8cdcc5451 100644 --- a/spec/lib/email/authentication_results_spec.rb +++ b/spec/lib/email/authentication_results_spec.rb @@ -13,11 +13,10 @@ RSpec.describe Email::AuthenticationResults do it "parses 'Service Provided, Authentication Done' correctly" do # https://tools.ietf.org/html/rfc8601#appendix-B.3 - results = described_class.new(<<~RAW + results = described_class.new(<<~RAW).results example.com; spf=pass smtp.mailfrom=example.net RAW - ).results expect(results[0][:authserv_id]).to eq "example.com" expect(results[0][:resinfo][0][:method]).to eq "spf" expect(results[0][:resinfo][0][:result]).to eq "pass" @@ -29,16 +28,15 @@ RSpec.describe Email::AuthenticationResults do it "parses 'Service Provided, Several Authentications Done, Single MTA' correctly" do # https://tools.ietf.org/html/rfc8601#appendix-B.4 - results = described_class.new([<<~RAW , + results = described_class.new([<<~RAW, <<~RAW]).results example.com; auth=pass (cram-md5) smtp.auth=sender@example.net; spf=pass smtp.mailfrom=example.net RAW - <<~RAW , example.com; iprev=pass policy.iprev=192.0.2.200 RAW - ]).results + expect(results[0][:authserv_id]).to eq "example.com" expect(results[0][:resinfo][0][:method]).to eq "auth" expect(results[0][:resinfo][0][:result]).to eq "pass" @@ -63,16 +61,14 @@ RSpec.describe Email::AuthenticationResults do it "parses 'Service Provided, Several Authentications Done, Different MTAs' correctly" do # https://tools.ietf.org/html/rfc8601#appendix-B.5 - results = described_class.new([<<~RAW , + results = described_class.new([<<~RAW, <<~RAW]).results example.com; dkim=pass (good signature) header.d=example.com RAW - <<~RAW , example.com; auth=pass (cram-md5) smtp.auth=sender@example.com; spf=fail smtp.mailfrom=example.com RAW - ]).results expect(results[0][:authserv_id]).to eq "example.com" expect(results[0][:resinfo][0][:method]).to eq "dkim" @@ -98,18 +94,16 @@ RSpec.describe Email::AuthenticationResults do it "parses 'Service Provided, Multi-tiered Authentication Done' correctly" do # https://tools.ietf.org/html/rfc8601#appendix-B.6 - results = described_class.new([<<~RAW , + results = described_class.new([<<~RAW, <<~RAW]).results example.com; dkim=pass reason="good signature" header.i=@mail-router.example.net; dkim=fail reason="bad signature" header.i=@newyork.example.com RAW - <<~RAW , example.net; dkim=pass (good signature) header.i=@newyork.example.com RAW - ]).results expect(results[0][:authserv_id]).to eq "example.com" expect(results[0][:resinfo][0][:method]).to eq "dkim" @@ -135,13 +129,12 @@ RSpec.describe Email::AuthenticationResults do it "parses 'Comment-Heavy Example' correctly" do # https://tools.ietf.org/html/rfc8601#appendix-B.7 - results = described_class.new(<<~RAW + results = described_class.new(<<~RAW).results foo.example.net (foobar) 1 (baz); dkim (Because I like it) / 1 (One yay) = (wait for it) fail policy (A dot can go here) . (like that) expired (this surprised me) = (as I wasn't expecting it) 1362471462 RAW - ).results expect(results[0][:authserv_id]).to eq "foo.example.net" expect(results[0][:resinfo][0][:method]).to eq "dkim" @@ -162,13 +155,12 @@ RSpec.describe Email::AuthenticationResults do end it "parses header with multiple props correctly" do - results = described_class.new(<<~RAW + results = described_class.new(<<~RAW).results mx.google.com; dkim=pass header.i=@email.example.com header.s=20111006 header.b=URn9MW+F; spf=pass (google.com: domain of foo@b.email.example.com designates 1.2.3.4 as permitted sender) smtp.mailfrom=foo@b.email.example.com; dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=email.example.com RAW - ).results expect(results[0][:authserv_id]).to eq "mx.google.com" expect(results[0][:resinfo][0][:method]).to eq "dkim" @@ -199,9 +191,7 @@ RSpec.describe Email::AuthenticationResults do end describe "#verdict" do - before do - SiteSetting.email_in_authserv_id = "valid.com" - end + before { SiteSetting.email_in_authserv_id = "valid.com" } shared_examples "is verdict" do |verdict| it "is #{verdict}" do @@ -294,5 +284,4 @@ RSpec.describe Email::AuthenticationResults do expect(results.action).to eq (:accept) end end - end diff --git a/spec/lib/email/cleaner_spec.rb b/spec/lib/email/cleaner_spec.rb index b4ae09403d..906d86a1af 100644 --- a/spec/lib/email/cleaner_spec.rb +++ b/spec/lib/email/cleaner_spec.rb @@ -3,26 +3,29 @@ require "email/receiver" RSpec.describe Email::Cleaner do - it 'removes attachments from raw message' do + it "removes attachments from raw message" do email = email(:attached_txt_file) - expected_message = "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease find some text file attached.\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" + expected_message = + "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease find some text file attached.\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" expect(described_class.new(email).execute).to eq(expected_message) end - it 'truncates message' do + it "truncates message" do email = email(:attached_txt_file) SiteSetting.raw_email_max_length = 10 - expected_message = "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease fin\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" + expected_message = + "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease fin\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" expect(described_class.new(email).execute).to eq(expected_message) end - it 'truncates rejected message' do + it "truncates rejected message" do email = email(:attached_txt_file) SiteSetting.raw_rejected_email_max_length = 10 - expected_message = "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease fin\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" + expected_message = + "Return-Path: \r\nDate: Sat, 30 Jan 2016 01:10:11 +0100\r\nFrom: Foo Bar \r\nTo: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com\r\nMessage-ID: <38@foo.bar.mail>\r\nMime-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\";\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\n\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad\r\nContent-Type: text/plain;\r\n charset=UTF-8\r\nContent-Transfer-Encoding: 7bit\r\n\r\nPlease fin\r\n----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad--\r\n" expect(described_class.new(email, rejected: true).execute).to eq(expected_message) end end diff --git a/spec/lib/email/email_spec.rb b/spec/lib/email/email_spec.rb index 46f8cd77f9..dabfc153d3 100644 --- a/spec/lib/email/email_spec.rb +++ b/spec/lib/email/email_spec.rb @@ -1,66 +1,71 @@ # frozen_string_literal: true -require 'email' +require "email" RSpec.describe Email do - describe "is_valid?" do - - it 'treats a nil as invalid' do + it "treats a nil as invalid" do expect(Email.is_valid?(nil)).to eq(false) end - it 'treats a good email as valid' do - expect(Email.is_valid?('sam@sam.com')).to eq(true) + it "treats a good email as valid" do + expect(Email.is_valid?("sam@sam.com")).to eq(true) end - it 'treats a bad email as invalid' do - expect(Email.is_valid?('sam@sam')).to eq(false) + it "treats a bad email as invalid" do + expect(Email.is_valid?("sam@sam")).to eq(false) end - it 'allows museum tld' do - expect(Email.is_valid?('sam@nic.museum')).to eq(true) + it "allows museum tld" do + expect(Email.is_valid?("sam@nic.museum")).to eq(true) end - it 'does not think a word is an email' do - expect(Email.is_valid?('sam')).to eq(false) + it "does not think a word is an email" do + expect(Email.is_valid?("sam")).to eq(false) end - end describe "downcase" do - - it 'downcases local and host part' do - expect(Email.downcase('SAM@GMAIL.COM')).to eq('sam@gmail.com') - expect(Email.downcase('sam@GMAIL.COM')).to eq('sam@gmail.com') + it "downcases local and host part" do + expect(Email.downcase("SAM@GMAIL.COM")).to eq("sam@gmail.com") + expect(Email.downcase("sam@GMAIL.COM")).to eq("sam@gmail.com") end - it 'leaves invalid emails untouched' do - expect(Email.downcase('SAM@GMAILCOM')).to eq('SAM@GMAILCOM') - expect(Email.downcase('samGMAIL.COM')).to eq('samGMAIL.COM') - expect(Email.downcase('sam@GM@AIL.COM')).to eq('sam@GM@AIL.COM') + it "leaves invalid emails untouched" do + expect(Email.downcase("SAM@GMAILCOM")).to eq("SAM@GMAILCOM") + expect(Email.downcase("samGMAIL.COM")).to eq("samGMAIL.COM") + expect(Email.downcase("sam@GM@AIL.COM")).to eq("sam@GM@AIL.COM") end - end describe "obfuscate" do - - it 'correctly obfuscates emails' do - expect(Email.obfuscate('a@b.com')).to eq('*@*.com') - expect(Email.obfuscate('test@test.co.uk')).to eq('t***@t***.**.uk') - expect(Email.obfuscate('simple@example.com')).to eq('s****e@e*****e.com') - expect(Email.obfuscate('very.common@example.com')).to eq('v*********n@e*****e.com') - expect(Email.obfuscate('disposable.style.email.with+symbol@example.com')).to eq('d********************************l@e*****e.com') - expect(Email.obfuscate('other.email-with-hyphen@example.com')).to eq('o*********************n@e*****e.com') - expect(Email.obfuscate('fully-qualified-domain@example.com')).to eq('f********************n@e*****e.com') - expect(Email.obfuscate('user.name+tag+sorting@example.com')).to eq('u*******************g@e*****e.com') - expect(Email.obfuscate('x@example.com')).to eq('*@e*****e.com') - expect(Email.obfuscate('example-indeed@strange-example.com')).to eq('e************d@s*************e.com') - expect(Email.obfuscate('example@s.example')).to eq('e*****e@*.example') - expect(Email.obfuscate('mailhost!username@example.org')).to eq('m***************e@e*****e.org') - expect(Email.obfuscate('user%example.com@example.org')).to eq('u**************m@e*****e.org') - expect(Email.obfuscate('user-@example.org')).to eq('u***-@e*****e.org') + it "correctly obfuscates emails" do + expect(Email.obfuscate("a@b.com")).to eq("*@*.com") + expect(Email.obfuscate("test@test.co.uk")).to eq("t***@t***.**.uk") + expect(Email.obfuscate("simple@example.com")).to eq("s****e@e*****e.com") + expect(Email.obfuscate("very.common@example.com")).to eq("v*********n@e*****e.com") + expect(Email.obfuscate("disposable.style.email.with+symbol@example.com")).to eq( + "d********************************l@e*****e.com", + ) + expect(Email.obfuscate("other.email-with-hyphen@example.com")).to eq( + "o*********************n@e*****e.com", + ) + expect(Email.obfuscate("fully-qualified-domain@example.com")).to eq( + "f********************n@e*****e.com", + ) + expect(Email.obfuscate("user.name+tag+sorting@example.com")).to eq( + "u*******************g@e*****e.com", + ) + expect(Email.obfuscate("x@example.com")).to eq("*@e*****e.com") + expect(Email.obfuscate("example-indeed@strange-example.com")).to eq( + "e************d@s*************e.com", + ) + expect(Email.obfuscate("example@s.example")).to eq("e*****e@*.example") + expect(Email.obfuscate("mailhost!username@example.org")).to eq( + "m***************e@e*****e.org", + ) + expect(Email.obfuscate("user%example.com@example.org")).to eq("u**************m@e*****e.org") + expect(Email.obfuscate("user-@example.org")).to eq("u***-@e*****e.org") end - end end diff --git a/spec/lib/email/message_builder_spec.rb b/spec/lib/email/message_builder_spec.rb index 00487992c0..567f4b5a92 100644 --- a/spec/lib/email/message_builder_spec.rb +++ b/spec/lib/email/message_builder_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'email/message_builder' +require "email/message_builder" RSpec.describe Email::MessageBuilder do let(:to_address) { "jake@adventuretime.ooo" } @@ -28,22 +28,24 @@ RSpec.describe Email::MessageBuilder do end it "ask politely not to receive automated responses" do - expect(header_args['X-Auto-Response-Suppress']).to eq("All") + expect(header_args["X-Auto-Response-Suppress"]).to eq("All") end describe "reply by email" do context "without allow_reply_by_email" do it "does not have a X-Discourse-Reply-Key" do - expect(header_args['X-Discourse-Reply-Key']).to be_blank + expect(header_args["X-Discourse-Reply-Key"]).to be_blank end it "returns a Reply-To header that's the same as From" do - expect(header_args['Reply-To']).to eq(build_args[:from]) + expect(header_args["Reply-To"]).to eq(build_args[:from]) end end context "with allow_reply_by_email" do - let(:reply_by_email_builder) { Email::MessageBuilder.new(to_address, allow_reply_by_email: true) } + let(:reply_by_email_builder) do + Email::MessageBuilder.new(to_address, allow_reply_by_email: true) + end context "with the SiteSetting enabled" do before do @@ -52,45 +54,44 @@ RSpec.describe Email::MessageBuilder do end it "returns a Reply-To header with the reply key" do - expect(reply_by_email_builder.header_args['Reply-To']) - .to eq("\"#{SiteSetting.title}\" ") + expect(reply_by_email_builder.header_args["Reply-To"]).to eq( + "\"#{SiteSetting.title}\" ", + ) - expect(reply_by_email_builder.header_args[allow_reply_header]) - .to eq(true) + expect(reply_by_email_builder.header_args[allow_reply_header]).to eq(true) end it "cleans up the site title" do SiteSetting.stubs(:title).returns(">>>Obnoxious Title: Deal, \"With\" It<<<") - expect(reply_by_email_builder.header_args['Reply-To']) - .to eq("\"Obnoxious Title Deal With It\" ") + expect(reply_by_email_builder.header_args["Reply-To"]).to eq( + "\"Obnoxious Title Deal With It\" ", + ) - expect(reply_by_email_builder.header_args[allow_reply_header]) - .to eq(true) + expect(reply_by_email_builder.header_args[allow_reply_header]).to eq(true) end end context "with the SiteSetting disabled" do - before do - SiteSetting.stubs(:reply_by_email_enabled?).returns(false) - end + before { SiteSetting.stubs(:reply_by_email_enabled?).returns(false) } it "returns a Reply-To header that's the same as From" do - expect(reply_by_email_builder.header_args['Reply-To']) - .to eq(reply_by_email_builder.build_args[:from]) + expect(reply_by_email_builder.header_args["Reply-To"]).to eq( + reply_by_email_builder.build_args[:from], + ) - expect(reply_by_email_builder.header_args[allow_reply_header]) - .to eq(nil) + expect(reply_by_email_builder.header_args[allow_reply_header]).to eq(nil) end end end context "with allow_reply_by_email" do let(:reply_by_email_builder) do - Email::MessageBuilder.new(to_address, + Email::MessageBuilder.new( + to_address, allow_reply_by_email: true, private_reply: true, - from_alias: "Username" + from_alias: "Username", ) end @@ -98,42 +99,48 @@ RSpec.describe Email::MessageBuilder do before do SiteSetting.stubs(:reply_by_email_enabled?).returns(true) - SiteSetting.stubs(:reply_by_email_address) - .returns("r+%{reply_key}@reply.myforum.com") + SiteSetting.stubs(:reply_by_email_address).returns("r+%{reply_key}@reply.myforum.com") end it "returns a Reply-To header with the reply key" do - expect(reply_by_email_builder.header_args['Reply-To']) - .to eq("\"Username\" ") + expect(reply_by_email_builder.header_args["Reply-To"]).to eq( + "\"Username\" ", + ) - expect(reply_by_email_builder.header_args[allow_reply_header]) - .to eq(true) + expect(reply_by_email_builder.header_args[allow_reply_header]).to eq(true) end end context "with the SiteSetting disabled" do - before do - SiteSetting.stubs(:reply_by_email_enabled?).returns(false) - end + before { SiteSetting.stubs(:reply_by_email_enabled?).returns(false) } it "returns a Reply-To header that's the same as From" do - expect(reply_by_email_builder.header_args['Reply-To']) - .to eq(reply_by_email_builder.build_args[:from]) + expect(reply_by_email_builder.header_args["Reply-To"]).to eq( + reply_by_email_builder.build_args[:from], + ) - expect(reply_by_email_builder.header_args[allow_reply_header]) - .to eq(nil) + expect(reply_by_email_builder.header_args[allow_reply_header]).to eq(nil) end end end - end describe "custom headers" do - let(:custom_headers_string) { " Precedence : bulk | :: | No-colon | No-Value: | Multi-colon : : value : : | Auto-Submitted : auto-generated " } - let(:custom_headers_result) { { "Precedence" => "bulk", "Multi-colon" => ": value : :", "Auto-Submitted" => "auto-generated" } } + let(:custom_headers_string) do + " Precedence : bulk | :: | No-colon | No-Value: | Multi-colon : : value : : | Auto-Submitted : auto-generated " + end + let(:custom_headers_result) do + { + "Precedence" => "bulk", + "Multi-colon" => ": value : :", + "Auto-Submitted" => "auto-generated", + } + end it "custom headers builder" do - expect(Email::MessageBuilder.custom_headers(custom_headers_string)).to eq(custom_headers_result) + expect(Email::MessageBuilder.custom_headers(custom_headers_string)).to eq( + custom_headers_result, + ) end it "empty headers builder" do @@ -143,7 +150,6 @@ RSpec.describe Email::MessageBuilder do it "null headers builder" do expect(Email::MessageBuilder.custom_headers(nil)).to eq({}) end - end describe "header args" do @@ -152,23 +158,35 @@ RSpec.describe Email::MessageBuilder do Email::MessageBuilder.new( to_address, { - body: 'hello world', - topic_id: 1234, - post_id: 4567, - }.merge(additional_opts) + body: "hello world", + topic_id: 1234, + post_id: 4567, + show_tags_in_subject: "foo bar baz", + show_category_in_subject: "random", + }.merge(additional_opts), ) end it "passes through a post_id" do - expect(message_with_header_args.header_args['X-Discourse-Post-Id']).to eq('4567') + expect(message_with_header_args.header_args["X-Discourse-Post-Id"]).to eq("4567") end it "passes through a topic_id" do - expect(message_with_header_args.header_args['X-Discourse-Topic-Id']).to eq('1234') + expect(message_with_header_args.header_args["X-Discourse-Topic-Id"]).to eq("1234") end it "uses the default reply-to header" do - expect(message_with_header_args.header_args['Reply-To']).to eq("\"Discourse\" <#{SiteSetting.notification_email}>") + expect(message_with_header_args.header_args["Reply-To"]).to eq( + "\"Discourse\" <#{SiteSetting.notification_email}>", + ) + end + + it "passes through the topic tags" do + expect(message_with_header_args.header_args["X-Discourse-Tags"]).to eq("foo bar baz") + end + + it "passes through the topic category" do + expect(message_with_header_args.header_args["X-Discourse-Category"]).to eq("random") end context "when allow_reply_by_email is enabled " do @@ -178,7 +196,9 @@ RSpec.describe Email::MessageBuilder do SiteSetting.manual_polling_enabled = true SiteSetting.reply_by_email_address = "test+%{reply_key}@test.com" SiteSetting.reply_by_email_enabled = true - expect(message_with_header_args.header_args['Reply-To']).to eq("\"Discourse\" ") + expect(message_with_header_args.header_args["Reply-To"]).to eq( + "\"Discourse\" ", + ) end end @@ -189,18 +209,22 @@ RSpec.describe Email::MessageBuilder do SiteSetting.manual_polling_enabled = true SiteSetting.reply_by_email_address = "test+%{reply_key}@test.com" SiteSetting.reply_by_email_enabled = true - expect(message_with_header_args.header_args['Reply-To']).to eq("\"Discourse\" <#{SiteSetting.notification_email}>") + expect(message_with_header_args.header_args["Reply-To"]).to eq( + "\"Discourse\" <#{SiteSetting.notification_email}>", + ) end end context "when allow_reply_by_email is enabled and use_from_address_for_reply_to is enabled and from is specified" do - let(:additional_opts) { { allow_reply_by_email: true, use_from_address_for_reply_to: true, from: "team@test.com" } } + let(:additional_opts) do + { allow_reply_by_email: true, use_from_address_for_reply_to: true, from: "team@test.com" } + end it "removes the reply-to header because it is identical to the from header" do SiteSetting.manual_polling_enabled = true SiteSetting.reply_by_email_address = "test+%{reply_key}@test.com" SiteSetting.reply_by_email_enabled = true - expect(message_with_header_args.header_args['Reply-To']).to eq(nil) + expect(message_with_header_args.header_args["Reply-To"]).to eq(nil) end end end @@ -208,37 +232,39 @@ RSpec.describe Email::MessageBuilder do describe "unsubscribe link" do context "with add_unsubscribe_link false" do it "has no unsubscribe header by default" do - expect(builder.header_args['List-Unsubscribe']).to be_blank + expect(builder.header_args["List-Unsubscribe"]).to be_blank end it "doesn't have the user preferences url in the body" do expect(builder.body).not_to match(builder.template_args[:user_preferences_url]) end - end context "with add_unsubscribe_link true" do - - let(:message_with_unsubscribe) { Email::MessageBuilder.new(to_address, - body: 'hello world', - add_unsubscribe_link: true, - url: "/t/1234", - unsubscribe_url: "/t/1234/unsubscribe") } + let(:message_with_unsubscribe) do + Email::MessageBuilder.new( + to_address, + body: "hello world", + add_unsubscribe_link: true, + url: "/t/1234", + unsubscribe_url: "/t/1234/unsubscribe", + ) + end it "has an List-Unsubscribe header" do - expect(message_with_unsubscribe.header_args['List-Unsubscribe']).to be_present + expect(message_with_unsubscribe.header_args["List-Unsubscribe"]).to be_present end it "has the unsubscribe url in the body" do - expect(message_with_unsubscribe.body).to match('/t/1234/unsubscribe') + expect(message_with_unsubscribe.body).to match("/t/1234/unsubscribe") end it "does not add unsubscribe via email link without site setting set" do - expect(message_with_unsubscribe.body).to_not match(/mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/) + expect(message_with_unsubscribe.body).to_not match( + /mailto:reply@#{Discourse.current_hostname}\?subject=unsubscribe/, + ) end - end - end describe "template_args" do @@ -249,7 +275,7 @@ RSpec.describe Email::MessageBuilder do end it "has email prefix as email_prefix when `SiteSetting.email_prefix` is present" do - SiteSetting.email_prefix = 'some email prefix' + SiteSetting.email_prefix = "some email prefix" expect(template_args[:email_prefix]).to eq(SiteSetting.email_prefix) end @@ -264,51 +290,66 @@ RSpec.describe Email::MessageBuilder do describe "email prefix in subject" do context "when use_site_subject is true" do - let(:message_with_email_prefix) { Email::MessageBuilder.new(to_address, - body: 'hello world', - use_site_subject: true) } + let(:message_with_email_prefix) do + Email::MessageBuilder.new(to_address, body: "hello world", use_site_subject: true) + end it "when email_prefix is set it should be present in subject" do - SiteSetting.email_prefix = 'some email prefix' + SiteSetting.email_prefix = "some email prefix" expect(message_with_email_prefix.subject).to match(SiteSetting.email_prefix) end end end describe "subject_template" do - let(:templated_builder) { Email::MessageBuilder.new(to_address, template: 'mystery') } + let(:templated_builder) { Email::MessageBuilder.new(to_address, template: "mystery") } let(:rendered_template) { "rendered template" } it "has the body rendered from a template" do - I18n.expects(:t).with("mystery.text_body_template", templated_builder.template_args).returns(rendered_template) + I18n + .expects(:t) + .with("mystery.text_body_template", templated_builder.template_args) + .returns(rendered_template) expect(templated_builder.body).to eq(rendered_template) end it "has the subject rendered from a template" do - I18n.expects(:t).with("mystery.subject_template", templated_builder.template_args).returns(rendered_template) + I18n + .expects(:t) + .with("mystery.subject_template", templated_builder.template_args) + .returns(rendered_template) expect(templated_builder.subject).to eq(rendered_template) end context "when use_site_subject is true" do - let(:templated_builder) { Email::MessageBuilder.new(to_address, template: 'user_notifications.user_replied', use_site_subject: true, topic_title: "Topic Title") } + let(:templated_builder) do + Email::MessageBuilder.new( + to_address, + template: "user_notifications.user_replied", + use_site_subject: true, + topic_title: "Topic Title", + ) + end it "can use subject override" do - override = TranslationOverride.upsert!( - I18n.locale, - "user_notifications.user_replied.subject_template", - "my customized subject" - ) + override = + TranslationOverride.upsert!( + I18n.locale, + "user_notifications.user_replied.subject_template", + "my customized subject", + ) override.save! expect(templated_builder.subject).to eq(override.value) end it "can use interpolation arguments in the override" do - SiteSetting.email_prefix = 'some email prefix' - override = TranslationOverride.upsert!( - I18n.locale, - "user_notifications.user_replied.subject_template", - "[%{site_name}] %{topic_title} my customized subject" - ).save! + SiteSetting.email_prefix = "some email prefix" + override = + TranslationOverride.upsert!( + I18n.locale, + "user_notifications.user_replied.subject_template", + "[%{site_name}] %{topic_title} my customized subject", + ).save! expect(templated_builder.subject).to match("some email prefix") expect(templated_builder.subject).to match("customized subject") end @@ -326,7 +367,7 @@ RSpec.describe Email::MessageBuilder do expect(build_args[:from]).to eq("\"Dog Talk\" <#{SiteSetting.notification_email}>") end - let(:finn_email) { 'finn@adventuretime.ooo' } + let(:finn_email) { "finn@adventuretime.ooo" } let(:custom_from) { Email::MessageBuilder.new(to_address, from: finn_email).build_args } it "allows us to override from" do @@ -336,12 +377,14 @@ RSpec.describe Email::MessageBuilder do let(:aliased_from) { Email::MessageBuilder.new(to_address, from_alias: "Finn the Dog") } it "allows us to alias the from address" do - expect(aliased_from.build_args[:from]).to eq("\"Finn the Dog\" <#{SiteSetting.notification_email}>") + expect(aliased_from.build_args[:from]).to eq( + "\"Finn the Dog\" <#{SiteSetting.notification_email}>", + ) end - let(:custom_aliased_from) { Email::MessageBuilder.new(to_address, - from_alias: "Finn the Dog", - from: finn_email) } + let(:custom_aliased_from) do + Email::MessageBuilder.new(to_address, from_alias: "Finn the Dog", from: finn_email) + end it "allows us to alias a custom from address" do expect(custom_aliased_from.build_args[:from]).to eq("\"Finn the Dog\" <#{finn_email}>") @@ -359,13 +402,16 @@ RSpec.describe Email::MessageBuilder do end it "cleans up aliases in the from_alias arg" do - builder = Email::MessageBuilder.new(to_address, from_alias: "Finn: the Dog, <3", from: finn_email) + builder = + Email::MessageBuilder.new(to_address, from_alias: "Finn: the Dog, <3", from: finn_email) expect(builder.build_args[:from]).to eq("\"Finn the Dog 3\" <#{finn_email}>") end it "cleans up the email_site_title" do SiteSetting.stubs(:email_site_title).returns("::>>>Best \"Forum\", EU: Award Winning<<<") - expect(build_args[:from]).to eq("\"Best Forum EU Award Winning\" <#{SiteSetting.notification_email}>") + expect(build_args[:from]).to eq( + "\"Best Forum EU Award Winning\" <#{SiteSetting.notification_email}>", + ) end end end diff --git a/spec/lib/email/processor_spec.rb b/spec/lib/email/processor_spec.rb index 42b6efe37e..4f57124145 100644 --- a/spec/lib/email/processor_spec.rb +++ b/spec/lib/email/processor_spec.rb @@ -3,22 +3,21 @@ require "email/processor" RSpec.describe Email::Processor do - after do - Discourse.redis.flushdb - end + after { Discourse.redis.flushdb } let(:from) { "foo@bar.com" } context "when reply via email is too short" do let(:mail) { file_from_fixtures("chinese_reply.eml", "emails").read } fab!(:post) { Fabricate(:post) } - fab!(:user) { Fabricate(:user, email: 'discourse@bar.com') } + fab!(:user) { Fabricate(:user, email: "discourse@bar.com") } fab!(:post_reply_key) do - Fabricate(:post_reply_key, + Fabricate( + :post_reply_key, user: user, post: post, - reply_key: '4f97315cc828096c9cb34c6f1a0d6fe8' + reply_key: "4f97315cc828096c9cb34c6f1a0d6fe8", ) end @@ -39,11 +38,12 @@ RSpec.describe Email::Processor do former_title = processor.receiver.mail.subject expect(rejection_raw.gsub(/\r/, "")).to eq( - I18n.t("system_messages.email_reject_post_too_short.text_body_template", + I18n.t( + "system_messages.email_reject_post_too_short.text_body_template", count: count, destination: destination, - former_title: former_title - ).gsub(/\r/, "") + former_title: former_title, + ).gsub(/\r/, ""), ) end end @@ -52,9 +52,7 @@ RSpec.describe Email::Processor do let(:mail) { "From: #{from}\nTo: bar@foo.com\nSubject: FOO BAR\n\nFoo foo bar bar?" } let(:limit_exceeded) { RateLimiter::LimitExceeded.new(10) } - before do - Email::Receiver.any_instance.expects(:process!).raises(limit_exceeded) - end + before { Email::Receiver.any_instance.expects(:process!).raises(limit_exceeded) } it "enqueues a background job by default" do expect_enqueued_with(job: :process_email, args: { mail: mail }) do @@ -64,7 +62,9 @@ RSpec.describe Email::Processor do it "doesn't enqueue a background job when retry is disabled" do expect_not_enqueued_with(job: :process_email, args: { mail: mail }) do - expect { Email::Processor.process!(mail, retry_on_rate_limit: false) }.to raise_error(limit_exceeded) + expect { Email::Processor.process!(mail, retry_on_rate_limit: false) }.to raise_error( + limit_exceeded, + ) end end end @@ -78,28 +78,26 @@ RSpec.describe Email::Processor do key = "rejection_email:#{[from]}:email_reject_empty:#{Date.today}" Discourse.redis.expire(key, 0) - expect { - Email::Processor.process!(mail) - }.to change { EmailLog.count }.by(1) + expect { Email::Processor.process!(mail) }.to change { EmailLog.count }.by(1) - expect { - Email::Processor.process!(mail2) - }.not_to change { EmailLog.count } + expect { Email::Processor.process!(mail2) }.not_to change { EmailLog.count } freeze_time(Date.today + 1) key = "rejection_email:#{[from]}:email_reject_empty:#{Date.today}" Discourse.redis.expire(key, 0) - expect { - Email::Processor.process!(mail3) - }.to change { EmailLog.count }.by(1) + expect { Email::Processor.process!(mail3) }.to change { EmailLog.count }.by(1) end end describe "unrecognized error" do - let(:mail) { "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: #{from}\nTo: bar@foo.com\nSubject: FOO BAR\n\nFoo foo bar bar?" } - let(:mail2) { "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: #{from}\nTo: foo@foo.com\nSubject: BAR BAR\n\nBar bar bar bar?" } + let(:mail) do + "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: #{from}\nTo: bar@foo.com\nSubject: FOO BAR\n\nFoo foo bar bar?" + end + let(:mail2) do + "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: #{from}\nTo: foo@foo.com\nSubject: BAR BAR\n\nBar bar bar bar?" + end it "sends a rejection email on an unrecognized error" do begin @@ -130,25 +128,24 @@ RSpec.describe Email::Processor do key = "rejection_email:#{[from]}:email_reject_unrecognized_error:#{Date.today}" Discourse.redis.expire(key, 0) - expect { - Email::Processor.process!(mail) - }.to change { EmailLog.count }.by(1) + expect { Email::Processor.process!(mail) }.to change { EmailLog.count }.by(1) - expect { - Email::Processor.process!(mail2) - }.to change { EmailLog.count }.by(1) + expect { Email::Processor.process!(mail2) }.to change { EmailLog.count }.by(1) end end describe "from reply to email address" do - let(:mail) { "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: reply@bar.com\nTo: reply@bar.com\nSubject: FOO BAR\n\nFoo foo bar bar?" } + let(:mail) do + "Date: Fri, 15 Jan 2016 00:12:43 +0100\nFrom: reply@bar.com\nTo: reply@bar.com\nSubject: FOO BAR\n\nFoo foo bar bar?" + end it "ignores the email" do - Email::Receiver.any_instance.stubs(:process_internal).raises(Email::Receiver::FromReplyByAddressError.new) + Email::Receiver + .any_instance + .stubs(:process_internal) + .raises(Email::Receiver::FromReplyByAddressError.new) - expect { - Email::Processor.process!(mail) - }.not_to change { EmailLog.count } + expect { Email::Processor.process!(mail) }.not_to change { EmailLog.count } end end @@ -171,32 +168,40 @@ RSpec.describe Email::Processor do end end - describe 'when replying to a post that is too old' do + describe "when replying to a post that is too old" do fab!(:user) { Fabricate(:user, email: "discourse@bar.com") } fab!(:topic) { Fabricate(:topic) } fab!(:post) { Fabricate(:post, topic: topic, created_at: 3.days.ago) } - let(:mail) { file_from_fixtures("old_destination.eml", "emails").read.gsub("424242", topic.id.to_s).gsub("123456", post.id.to_s) } + let(:mail) do + file_from_fixtures("old_destination.eml", "emails") + .read + .gsub("424242", topic.id.to_s) + .gsub("123456", post.id.to_s) + end - it 'rejects the email with the right response' do + it "rejects the email with the right response" do SiteSetting.disallow_reply_by_email_after_days = 2 processor = Email::Processor.new(mail) processor.process! rejection_raw = ActionMailer::Base.deliveries.first.body.to_s - expect(rejection_raw).to eq(I18n.t("system_messages.email_reject_old_destination.text_body_template", - destination: '["reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com"]', - former_title: 'Some Old Post', - short_url: "#{Discourse.base_url}/p/#{post.id}", - number_of_days: 2 - )) + expect(rejection_raw).to eq( + I18n.t( + "system_messages.email_reject_old_destination.text_body_template", + destination: '["reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com"]', + former_title: "Some Old Post", + short_url: "#{Discourse.base_url}/p/#{post.id}", + number_of_days: 2, + ), + ) end end - describe 'when group email recipients exceeds maximum_recipients_per_new_group_email site setting' do + describe "when group email recipients exceeds maximum_recipients_per_new_group_email site setting" do let(:mail) { file_from_fixtures("cc.eml", "emails").read } - it 'rejects the email with the right response' do + it "rejects the email with the right response" do SiteSetting.maximum_recipients_per_new_group_email = 3 processor = Email::Processor.new(mail) @@ -205,12 +210,13 @@ RSpec.describe Email::Processor do rejection_raw = ActionMailer::Base.deliveries.first.body.to_s expect(rejection_raw).to eq( - I18n.t("system_messages.email_reject_too_many_recipients.text_body_template", + I18n.t( + "system_messages.email_reject_too_many_recipients.text_body_template", destination: '["someone@else.com"]', - former_title: 'The more, the merrier', + former_title: "The more, the merrier", max_recipients_count: 3, base_url: Discourse.base_url, - ) + ), ) end end diff --git a/spec/lib/email/receiver_spec.rb b/spec/lib/email/receiver_spec.rb index 01565f420e..99035646be 100644 --- a/spec/lib/email/receiver_spec.rb +++ b/spec/lib/email/receiver_spec.rb @@ -41,8 +41,12 @@ RSpec.describe Email::Receiver do end it "raises an AutoGeneratedEmailError when the mail is auto generated" do - expect { process(:auto_generated_precedence) }.to raise_error(Email::Receiver::AutoGeneratedEmailError) - expect { process(:auto_generated_header) }.to raise_error(Email::Receiver::AutoGeneratedEmailError) + expect { process(:auto_generated_precedence) }.to raise_error( + Email::Receiver::AutoGeneratedEmailError, + ) + expect { process(:auto_generated_header) }.to raise_error( + Email::Receiver::AutoGeneratedEmailError, + ) end it "raises a NoBodyDetectedError when the body is blank" do @@ -67,11 +71,13 @@ RSpec.describe Email::Receiver do user = Fabricate(:user, email: "staged@bar.com", active: false, staged: true) post = Fabricate(:post) - post_reply_key = Fabricate(:post_reply_key, - user: user, - post: post, - reply_key: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' - ) + post_reply_key = + Fabricate( + :post_reply_key, + user: user, + post: post, + reply_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ) expect { process(:staged_sender) }.not_to raise_error end @@ -84,19 +90,19 @@ RSpec.describe Email::Receiver do SiteSetting.disallow_reply_by_email_after_days = 2 topic = Fabricate(:topic) - post = Fabricate(:post, topic: topic) - user = Fabricate(:user, email: "discourse@bar.com") + post = Fabricate(:post, topic: topic) + user = Fabricate(:user, email: "discourse@bar.com") mail = email(:old_destination).gsub("424242", topic.id.to_s) expect { Email::Receiver.new(mail).process! }.to raise_error( - Email::Receiver::BadDestinationAddress + Email::Receiver::BadDestinationAddress, ) IncomingEmail.destroy_all post.update!(created_at: 3.days.ago) expect { Email::Receiver.new(mail).process! }.to raise_error( - Email::Receiver::OldDestinationError + Email::Receiver::OldDestinationError, ) expect(IncomingEmail.last.error).to eq("Email::Receiver::OldDestinationError") @@ -104,7 +110,7 @@ RSpec.describe Email::Receiver do IncomingEmail.destroy_all expect { Email::Receiver.new(mail).process! }.to raise_error( - Email::Receiver::BadDestinationAddress + Email::Receiver::BadDestinationAddress, ) end @@ -113,7 +119,9 @@ RSpec.describe Email::Receiver do expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError) expect(IncomingEmail.last.is_bounce).to eq(true) - expect { process(:bounced_email_multiple_status_codes) }.to raise_error(Email::Receiver::BouncedEmailError) + expect { process(:bounced_email_multiple_status_codes) }.to raise_error( + Email::Receiver::BouncedEmailError, + ) expect(IncomingEmail.last.is_bounce).to eq(true) end @@ -121,7 +129,15 @@ RSpec.describe Email::Receiver do let(:email_address) { "linux-admin@b-s-c.co.jp" } fab!(:user1) { Fabricate(:user) } let(:user2) { Fabricate(:staged, email: email_address) } - let(:topic) { Fabricate(:topic, archetype: 'private_message', category_id: nil, user: user1, allowed_users: [user1, user2]) } + let(:topic) do + Fabricate( + :topic, + archetype: "private_message", + category_id: nil, + user: user1, + allowed_users: [user1, user2], + ) + end let(:post) { create_post(topic: topic, user: user1) } before do @@ -130,11 +146,7 @@ RSpec.describe Email::Receiver do end def create_post_reply_key(value) - Fabricate(:post_reply_key, - reply_key: value, - user: user2, - post: post - ) + Fabricate(:post_reply_key, reply_key: value, user: user2, post: post) end it "when bounce without verp" do @@ -143,7 +155,13 @@ RSpec.describe Email::Receiver do expect { process(:bounced_email) }.to raise_error(Email::Receiver::BouncedEmailError) post = Post.last expect(post.whisper?).to eq(true) - expect(post.raw).to eq(I18n.t("system_messages.email_bounced", email: email_address, raw: "Your email bounced").strip) + expect(post.raw).to eq( + I18n.t( + "system_messages.email_bounced", + email: email_address, + raw: "Your email bounced", + ).strip, + ) expect(IncomingEmail.last.is_bounce).to eq(true) end @@ -153,19 +171,35 @@ RSpec.describe Email::Receiver do before do SiteSetting.reply_by_email_address = "foo+%{reply_key}@discourse.org" create_post_reply_key(bounce_key) - Fabricate(:email_log, to_address: email_address, user: user2, bounce_key: bounce_key, post: post) + Fabricate( + :email_log, + to_address: email_address, + user: user2, + bounce_key: bounce_key, + post: post, + ) end it "creates a post with the bounce error" do - expect { process(:hard_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError) + expect { process(:hard_bounce_via_verp) }.to raise_error( + Email::Receiver::BouncedEmailError, + ) post = Post.last expect(post.whisper?).to eq(true) - expect(post.raw).to eq(I18n.t("system_messages.email_bounced", email: email_address, raw: "Your email bounced").strip) + expect(post.raw).to eq( + I18n.t( + "system_messages.email_bounced", + email: email_address, + raw: "Your email bounced", + ).strip, + ) expect(IncomingEmail.last.is_bounce).to eq(true) end it "updates the email log with the bounce error message" do - expect { process(:hard_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError) + expect { process(:hard_bounce_via_verp) }.to raise_error( + Email::Receiver::BouncedEmailError, + ) email_log = EmailLog.find_by(bounce_key: bounce_key) expect(email_log.bounced).to eq(true) expect(email_log.bounce_error_code).to eq("5.1.1") @@ -176,7 +210,11 @@ RSpec.describe Email::Receiver do it "logs a blank error" do Email::Receiver.any_instance.stubs(:process_internal).raises(RuntimeError, "") - process(:existing_user) rescue RuntimeError + begin + process(:existing_user) + rescue StandardError + RuntimeError + end expect(IncomingEmail.last.error).to eq("RuntimeError") end @@ -189,17 +227,21 @@ RSpec.describe Email::Receiver do end it "strips null bytes from the subject" do - expect do - process(:null_byte_in_subject) - end.to raise_error(Email::Receiver::BadDestinationAddress) + expect do process(:null_byte_in_subject) end.to raise_error( + Email::Receiver::BadDestinationAddress, + ) end describe "bounces to VERP" do let(:bounce_key) { "14b08c855160d67f2e0c2f8ef36e251e" } let(:bounce_key_2) { "b542fb5a9bacda6d28cc061d18e4eb83" } fab!(:user) { Fabricate(:user, email: "linux-admin@b-s-c.co.jp") } - let!(:email_log) { Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key) } - let!(:email_log_2) { Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key_2) } + let!(:email_log) do + Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key) + end + let!(:email_log_2) do + Fabricate(:email_log, to_address: user.email, user: user, bounce_key: bounce_key_2) + end it "deals with soft bounces" do expect { process(:soft_bounce_via_verp) }.to raise_error(Email::Receiver::BouncedEmailError) @@ -224,7 +266,9 @@ RSpec.describe Email::Receiver do end it "works when the final recipient is different" do - expect { process(:verp_bounce_different_final_recipient) }.to raise_error(Email::Receiver::BouncedEmailError) + expect { process(:verp_bounce_different_final_recipient) }.to raise_error( + Email::Receiver::BouncedEmailError, + ) email_log.reload expect(email_log.bounced).to eq(true) @@ -251,11 +295,7 @@ RSpec.describe Email::Receiver do fab!(:post) { create_post(topic: topic) } let!(:post_reply_key) do - Fabricate(:post_reply_key, - reply_key: reply_key, - user: user, - post: post - ) + Fabricate(:post_reply_key, reply_key: reply_key, user: user, post: post) end let :topic_user do @@ -270,24 +310,25 @@ RSpec.describe Email::Receiver do it "raises a ReplyUserNotMatchingError when the email address isn't matching the one we sent the notification to" do Fabricate(:user, email: "someone_else@bar.com") - expect { process(:reply_user_not_matching) }.to raise_error(Email::Receiver::ReplyUserNotMatchingError) + expect { process(:reply_user_not_matching) }.to raise_error( + Email::Receiver::ReplyUserNotMatchingError, + ) end it "raises a FromReplyByAddressError when the email is from the reply by email address" do - expect { process(:from_reply_by_email_address) }.to raise_error(Email::Receiver::FromReplyByAddressError) + expect { process(:from_reply_by_email_address) }.to raise_error( + Email::Receiver::FromReplyByAddressError, + ) end it "accepts reply from secondary email address" do Fabricate(:secondary_email, email: "someone_else@bar.com", user: user) - expect { process(:reply_user_not_matching) } - .to change { topic.posts.count } + expect { process(:reply_user_not_matching) }.to change { topic.posts.count } post = Post.last - expect(post.raw).to eq( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - ) + expect(post.raw).to eq("Lorem ipsum dolor sit amet, consectetur adipiscing elit.") expect(post.user).to eq(user) end @@ -296,7 +337,9 @@ RSpec.describe Email::Receiver do Fabricate(:user, email: "bob@bar.com") category.set_permissions(admins: :full) category.save - expect { process(:reply_user_not_matching_but_known) }.to raise_error(Email::Receiver::ReplyNotAllowedError) + expect { process(:reply_user_not_matching_but_known) }.to raise_error( + Email::Receiver::ReplyNotAllowedError, + ) end it "raises a TopicNotFoundError when the topic was deleted" do @@ -305,9 +348,7 @@ RSpec.describe Email::Receiver do end context "with a closed topic" do - before do - topic.update_columns(closed: true) - end + before { topic.update_columns(closed: true) } it "raises a TopicClosedError when the topic was closed" do expect { process(:reply_user_matching) }.to raise_error(Email::Receiver::TopicClosedError) @@ -356,7 +397,9 @@ RSpec.describe Email::Receiver do DiscourseEvent.on(:topic_created, &handler) expect { process(:text_reply) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ’") + expect(topic.posts.last.raw).to eq( + "This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ’", + ) expect(topic.posts.last.via_email).to eq(true) expect(topic.posts.last.cooked).not_to match(/
    \n

    ···\n\nThis is the *elided* part!\n\n") + expect(topic.posts.last.raw).to eq( + "This is a **GMAIL** reply ;)\n\n
    \n···\n\nThis is the *elided* part!\n\n
    ", + ) end it "doesn't process email with same message-id more than once" do @@ -416,13 +461,15 @@ RSpec.describe Email::Receiver do it "prefers html over text when site setting is enabled" do SiteSetting.incoming_email_prefer_html = true expect { process(:text_and_html_reply) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq('This is the **html** part.') + expect(topic.posts.last.raw).to eq("This is the **html** part.") end it "uses text when prefer_html site setting is enabled but no html is available" do SiteSetting.incoming_email_prefer_html = true expect { process(:text_reply) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ’") + expect(topic.posts.last.raw).to eq( + "This is a text reply :)\n\nEmail parsing should not break because of a UTF-8 character: ’", + ) end it "removes the 'on , wrote' quoting line" do @@ -432,27 +479,37 @@ RSpec.describe Email::Receiver do it "removes the 'Previous Replies' marker" do expect { process(:previous_replies) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This will not include the previous discussion that is present in this email.") + expect(topic.posts.last.raw).to eq( + "This will not include the previous discussion that is present in this email.", + ) end it "removes the translated 'Previous Replies' marker" do expect { process(:previous_replies_de) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This will not include the previous discussion that is present in this email.") + expect(topic.posts.last.raw).to eq( + "This will not include the previous discussion that is present in this email.", + ) end it "removes the 'type reply above' marker" do expect { process(:reply_above) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This will not include the previous discussion that is present in this email.") + expect(topic.posts.last.raw).to eq( + "This will not include the previous discussion that is present in this email.", + ) end it "removes the translated 'Previous Replies' marker" do expect { process(:reply_above_de) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This will not include the previous discussion that is present in this email.") + expect(topic.posts.last.raw).to eq( + "This will not include the previous discussion that is present in this email.", + ) end it "handles multiple paragraphs" do expect { process(:paragraphs) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)") + expect(topic.posts.last.raw).to eq( + "Do you like liquorice?\n\nI really like them. One could even say that I am *addicted* to liquorice. And if\nyou can mix it up with some anise, then I'm in heaven ;)", + ) end it "handles invalid from header" do @@ -491,38 +548,50 @@ RSpec.describe Email::Receiver do expect(topic.ordered_posts.last.reply_to_post_number).to be_nil end - describe 'Unsubscribing via email' do + describe "Unsubscribing via email" do let(:last_email) { ActionMailer::Base.deliveries.last } - describe 'unsubscribe_subject.eml' do - it 'sends an email asking the user to confirm the unsubscription' do - expect { process("unsubscribe_subject") }.to change { ActionMailer::Base.deliveries.count }.by(1) + describe "unsubscribe_subject.eml" do + it "sends an email asking the user to confirm the unsubscription" do + expect { process("unsubscribe_subject") }.to change { + ActionMailer::Base.deliveries.count + }.by(1) expect(last_email.to.length).to eq 1 expect(last_email.from.length).to eq 1 expect(last_email.from).to include "noreply@#{Discourse.current_hostname}" expect(last_email.to).to include "discourse@bar.com" - expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub("%{site_title}", SiteSetting.title) + expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub( + "%{site_title}", + SiteSetting.title, + ) end - it 'does nothing unless unsubscribe_via_email is turned on' do + it "does nothing unless unsubscribe_via_email is turned on" do SiteSetting.unsubscribe_via_email = false before_deliveries = ActionMailer::Base.deliveries.count - expect { process("unsubscribe_subject") }.to raise_error { Email::Receiver::BadDestinationAddress } + expect { process("unsubscribe_subject") }.to raise_error { + Email::Receiver::BadDestinationAddress + } expect(before_deliveries).to eq ActionMailer::Base.deliveries.count end end - describe 'unsubscribe_body.eml' do - it 'sends an email asking the user to confirm the unsubscription' do - expect { process("unsubscribe_body") }.to change { ActionMailer::Base.deliveries.count }.by(1) + describe "unsubscribe_body.eml" do + it "sends an email asking the user to confirm the unsubscription" do + expect { process("unsubscribe_body") }.to change { + ActionMailer::Base.deliveries.count + }.by(1) expect(last_email.to.length).to eq 1 expect(last_email.from.length).to eq 1 expect(last_email.from).to include "noreply@#{Discourse.current_hostname}" expect(last_email.to).to include "discourse@bar.com" - expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub("%{site_title}", SiteSetting.title) + expect(last_email.subject).to eq I18n.t(:"unsubscribe_mailer.subject_template").gsub( + "%{site_title}", + SiteSetting.title, + ) end - it 'does nothing unless unsubscribe_via_email is turned on' do + it "does nothing unless unsubscribe_via_email is turned on" do SiteSetting.unsubscribe_via_email = false before_deliveries = ActionMailer::Base.deliveries.count expect { process("unsubscribe_body") }.to raise_error { Email::Receiver::InvalidPost } @@ -532,7 +601,9 @@ RSpec.describe Email::Receiver do it "raises an UnsubscribeNotAllowed and does not send an unsubscribe email" do before_deliveries = ActionMailer::Base.deliveries.count - expect { process(:unsubscribe_new_user) }.to raise_error { Email::Receiver::UnsubscribeNotAllowed } + expect { process(:unsubscribe_new_user) }.to raise_error { + Email::Receiver::UnsubscribeNotAllowed + } expect(before_deliveries).to eq ActionMailer::Base.deliveries.count end end @@ -544,7 +615,9 @@ RSpec.describe Email::Receiver do it "retrieves the first part of multiple replies" do expect { process(:inline_mixed_replies) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("> WAT November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:\n\n> This is another post.\n\nAnd this is **another** reply.") + expect(topic.posts.last.raw).to eq( + "> WAT November 28\n>\n> This is the previous post.\n\nAnd this is *my* reply :+1:\n\n> This is another post.\n\nAnd this is **another** reply.", + ) end it "strips mobile/webmail signatures" do @@ -563,7 +636,9 @@ RSpec.describe Email::Receiver do topic.save expect { process(:original_message) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a reply :)\n\n
    \n···\n\n---Original Message---\nThis part should not be included\n\n
    ") + expect(topic.posts.last.raw).to eq( + "This is a reply :)\n\n
    \n···\n\n---Original Message---\nThis part should not be included\n\n
    ", + ) end it "doesn't include the 'elided' part of the original message when always_show_trimmed_content is disabled" do @@ -575,13 +650,17 @@ RSpec.describe Email::Receiver do it "adds the 'elided' part of the original message for public replies when always_show_trimmed_content is enabled" do SiteSetting.always_show_trimmed_content = true expect { process(:original_message) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a reply :)\n\n
    \n···\n\n---Original Message---\nThis part should not be included\n\n
    ") + expect(topic.posts.last.raw).to eq( + "This is a reply :)\n\n
    \n···\n\n---Original Message---\nThis part should not be included\n\n
    ", + ) end it "doesn't trim the message when trim_incoming_emails is disabled" do SiteSetting.trim_incoming_emails = false expect { process(:original_message) }.to change { topic.posts.count } - expect(topic.posts.last.raw).to eq("This is a reply :)\n\n---Original Message---\nThis part should not be included") + expect(topic.posts.last.raw).to eq( + "This is a reply :)\n\n---Original Message---\nThis part should not be included", + ) end it "supports attached images in TEXT part" do @@ -593,7 +672,7 @@ RSpec.describe Email::Receiver do upload = post.uploads.first expect(post.raw).to include( - "![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})" + "![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})", ) expect { process(:inline_image) }.to change { topic.posts.count } @@ -602,7 +681,7 @@ RSpec.describe Email::Receiver do upload = post.uploads.first expect(post.raw).to include( - "![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})" + "![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url})", ) end @@ -630,7 +709,7 @@ RSpec.describe Email::Receiver do upload = post.uploads.last expect(post.raw).to eq(<<~MD.chomp) - [image:#{'0' * 5000} + [image:#{"0" * 5000} ![#{upload.original_filename}|#{upload.width}x#{upload.height}](#{upload.short_url}) MD @@ -725,7 +804,8 @@ RSpec.describe Email::Receiver do end it "creates the post with attachment missing message" do - missing_attachment_regex = Regexp.escape(I18n.t('emails.incoming.missing_attachment', filename: "text.txt")) + missing_attachment_regex = + Regexp.escape(I18n.t("emails.incoming.missing_attachment", filename: "text.txt")) expect { process(:attached_txt_file) }.to change { topic.posts.count } post = topic.posts.last expect(post.raw).to match(/#{missing_attachment_regex}/) @@ -740,7 +820,7 @@ RSpec.describe Email::Receiver do upload = post.uploads.last expect(post.raw).to include( - "[#{upload.original_filename}|attachment](#{upload.short_url}) (64 KB)" + "[#{upload.original_filename}|attachment](#{upload.short_url}) (64 KB)", ) end @@ -758,8 +838,9 @@ RSpec.describe Email::Receiver do it "accepts emails with wrong reply key if the system knows about the forwarded email" do Fabricate(:user, email: "bob@bar.com") - Fabricate(:incoming_email, - raw: <<~RAW, + Fabricate( + :incoming_email, + raw: <<~RAW, Return-Path: From: Alice To: dave@bar.com, reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com @@ -773,12 +854,13 @@ RSpec.describe Email::Receiver do This post was created by email. RAW - from_address: "discourse@bar.com", - to_addresses: "dave@bar.com;reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com", - cc_addresses: "carol@bar.com;bob@bar.com", - topic: topic, - post: post, - user: user) + from_address: "discourse@bar.com", + to_addresses: "dave@bar.com;reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com", + cc_addresses: "carol@bar.com;bob@bar.com", + topic: topic, + post: post, + user: user, + ) expect { process(:reply_user_not_matching_but_known) }.to change { topic.posts.count } end @@ -793,7 +875,6 @@ RSpec.describe Email::Receiver do user.reload expect(user.user_option.email_messages_level).to eq(UserOption.email_level_types[:always]) end - end shared_examples "creates topic with forwarded message as quote" do |destination, address| @@ -864,21 +945,25 @@ RSpec.describe Email::Receiver do end it "invites users with a secondary email in the chain" do - user1 = Fabricate(:user, - trust_level: SiteSetting.email_in_min_trust, - user_emails: [ - Fabricate.build(:secondary_email, email: "discourse@bar.com"), - Fabricate.build(:secondary_email, email: "someone@else.com"), - ] - ) + user1 = + Fabricate( + :user, + trust_level: SiteSetting.email_in_min_trust, + user_emails: [ + Fabricate.build(:secondary_email, email: "discourse@bar.com"), + Fabricate.build(:secondary_email, email: "someone@else.com"), + ], + ) - user2 = Fabricate(:user, - trust_level: SiteSetting.email_in_min_trust, - user_emails: [ - Fabricate.build(:secondary_email, email: "team@bar.com"), - Fabricate.build(:secondary_email, email: "wat@bar.com"), - ] - ) + user2 = + Fabricate( + :user, + trust_level: SiteSetting.email_in_min_trust, + user_emails: [ + Fabricate.build(:secondary_email, email: "team@bar.com"), + Fabricate.build(:secondary_email, email: "wat@bar.com"), + ], + ) expect { process(:cc) }.to change(Topic, :count) expect(Topic.last.allowed_users).to contain_exactly(user1, user2) @@ -886,8 +971,7 @@ RSpec.describe Email::Receiver do it "cap the number of staged users created per email" do SiteSetting.maximum_staged_users_per_email = 1 - expect { process(:cc) }.to change(Topic, :count).by(1) - .and change(User, :count).by(1) + expect { process(:cc) }.to change(Topic, :count).by(1).and change(User, :count).by(1) expect(Topic.last.ordered_posts[-1].post_type).to eq(Post.types[:moderator_action]) end @@ -912,9 +996,7 @@ RSpec.describe Email::Receiver do end describe "reply-to header" do - before do - SiteSetting.block_auto_generated_emails = false - end + before { SiteSetting.block_auto_generated_emails = false } it "extracts address and uses it for comparison" do expect { process(:reply_to_whitespaces) }.to change(Topic, :count).by(1) @@ -935,7 +1017,10 @@ RSpec.describe Email::Receiver do end it "allows for quotes around the display name of the Reply-To address" do - expect { process(:reply_to_different_to_from_quoted_display_name) }.to change(Topic, :count).by(1) + expect { process(:reply_to_different_to_from_quoted_display_name) }.to change( + Topic, + :count, + ).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic @@ -953,7 +1038,10 @@ RSpec.describe Email::Receiver do end it "does not use the reply-to address if the X-Original-From header is different from the reply-to address" do - expect { process(:reply_to_different_to_from_x_original_different) }.to change(Topic, :count).by(1) + expect { process(:reply_to_different_to_from_x_original_different) }.to change( + Topic, + :count, + ).by(1) user = User.last incoming = IncomingEmail.find_by(message_id: "3848c3m98r439c348mc349@test.mailinglist.com") topic = incoming.topic @@ -963,25 +1051,22 @@ RSpec.describe Email::Receiver do end describe "when 'find_related_post_with_key' is disabled" do - before do - SiteSetting.find_related_post_with_key = false - end + before { SiteSetting.find_related_post_with_key = false } it "associates email replies using both 'In-Reply-To' and 'References' headers" do - expect { process(:email_reply_1) } - .to change(Topic, :count).by(1) & change(Post, :count).by(3) + expect { process(:email_reply_1) }.to change(Topic, :count).by(1) & + change(Post, :count).by(3) topic = Topic.last ordered_posts = topic.ordered_posts - expect(ordered_posts.first.raw).to eq('This is email reply **1**.') + expect(ordered_posts.first.raw).to eq("This is email reply **1**.") ordered_posts[1..-1].each do |post| - expect(post.action_code).to eq('invited_user') - expect(post.user.email).to eq('one@foo.com') + expect(post.action_code).to eq("invited_user") + expect(post.user.email).to eq("one@foo.com") - expect(%w{two three}.include?(post.custom_fields["action_code_who"])) - .to eq(true) + expect(%w[two three].include?(post.custom_fields["action_code_who"])).to eq(true) end expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1) @@ -1016,28 +1101,50 @@ RSpec.describe Email::Receiver do end it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID@HOST" do - expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}@test.localhost") }.to change { Post.count }.by(1) - expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + expect { + process_mail_with_message_id("topic/#{topic.id}/#{post.id}@test.localhost") + }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include( + "This is email reply testing with Message-ID formats", + ) end it "posts a reply using a message-id in the format topic/TOPIC_ID@HOST" do - expect { process_mail_with_message_id("topic/#{topic.id}@test.localhost") }.to change { Post.count }.by(1) - expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + expect { process_mail_with_message_id("topic/#{topic.id}@test.localhost") }.to change { + Post.count + }.by(1) + expect(topic.reload.posts.last.raw).to include( + "This is email reply testing with Message-ID formats", + ) end it "posts a reply using a message-id in the format topic/TOPIC_ID/POST_ID.RANDOM_SUFFIX@HOST" do - expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.rjc3yr79834y@test.localhost") }.to change { Post.count }.by(1) - expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + expect { + process_mail_with_message_id("topic/#{topic.id}/#{post.id}.rjc3yr79834y@test.localhost") + }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include( + "This is email reply testing with Message-ID formats", + ) end it "posts a reply using a message-id in the format topic/TOPIC_ID.RANDOM_SUFFIX@HOST" do - expect { process_mail_with_message_id("topic/#{topic.id}/#{post.id}.x3487nxy877843x@test.localhost") }.to change { Post.count }.by(1) - expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + expect { + process_mail_with_message_id( + "topic/#{topic.id}/#{post.id}.x3487nxy877843x@test.localhost", + ) + }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include( + "This is email reply testing with Message-ID formats", + ) end it "posts a reply using a message-id in the format discourse/post/POST_ID@HOST" do - expect { process_mail_with_message_id("discourse/post/#{post.id}@test.localhost") }.to change { Post.count }.by(1) - expect(topic.reload.posts.last.raw).to include("This is email reply testing with Message-ID formats") + expect { + process_mail_with_message_id("discourse/post/#{post.id}@test.localhost") + }.to change { Post.count }.by(1) + expect(topic.reload.posts.last.raw).to include( + "This is email reply testing with Message-ID formats", + ) end end end @@ -1050,7 +1157,7 @@ RSpec.describe Email::Receiver do upload = post.uploads.first expect(post.raw).to include( - "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)" + "[#{upload.original_filename}|attachment](#{upload.short_url}) (#{upload.filesize} Bytes)", ) end @@ -1120,11 +1227,11 @@ RSpec.describe Email::Receiver do end context "with forwarded emails behaviour set to quote" do - before do - SiteSetting.forwarded_emails_behaviour = "quote" - end + before { SiteSetting.forwarded_emails_behaviour = "quote" } - include_examples "creates topic with forwarded message as quote", :group, "team@bar.com|meat@bar.com" + include_examples "creates topic with forwarded message as quote", + :group, + "team@bar.com|meat@bar.com" end context "when a reply is sent to a group's email_username" do @@ -1140,7 +1247,9 @@ RSpec.describe Email::Receiver do end it "creates the reply when the sender and referenced messsage id are known" do - expect { process(:email_reply_to_group_email_username) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } + expect { process(:email_reply_to_group_email_username) }.to change { topic.posts.count }.by( + 1, + ).and not_change { Topic.count } end end @@ -1152,7 +1261,7 @@ RSpec.describe Email::Receiver do smtp_server: "smtp.test.com", smtp_port: 587, smtp_ssl: true, - smtp_enabled: true + smtp_enabled: true, ) end @@ -1164,9 +1273,7 @@ RSpec.describe Email::Receiver do end context "with forwarded emails behaviour set to create replies" do - before do - SiteSetting.forwarded_emails_behaviour = "create_replies" - end + before { SiteSetting.forwarded_emails_behaviour = "create_replies" } it "does not use the team's address as the from_address; it uses the original sender address" do process(:forwarded_by_group_to_inbox) @@ -1176,7 +1283,9 @@ RSpec.describe Email::Receiver do end it "does not say the email was forwarded by the original sender, it says the email is forwarded by the group" do - expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(4) + expect { process(:forwarded_by_group_to_inbox) }.to change { + User.where(staged: true).count + }.by(4) topic = Topic.last forwarded_small_post = topic.ordered_posts.last expect(forwarded_small_post.action_code).to eq("forwarded") @@ -1184,30 +1293,36 @@ RSpec.describe Email::Receiver do end it "keeps track of the cc addresses of the forwarded email and creates staged users for them" do - expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(4) + expect { process(:forwarded_by_group_to_inbox) }.to change { + User.where(staged: true).count + }.by(4) topic = Topic.last cc_user1 = User.find_by_email("terry@ccland.com") cc_user2 = User.find_by_email("don@ccland.com") fred_user = User.find_by_email("fred@bedrock.com") team_user = User.find_by_email("team@somesmtpaddress.com") expect(topic.incoming_email.first.cc_addresses).to eq("terry@ccland.com;don@ccland.com") - expect(topic.topic_allowed_users.pluck(:user_id)).to match_array([ - fred_user.id, team_user.id, cc_user1.id, cc_user2.id - ]) + expect(topic.topic_allowed_users.pluck(:user_id)).to match_array( + [fred_user.id, team_user.id, cc_user1.id, cc_user2.id], + ) end it "keeps track of the cc addresses of the final forwarded email as well" do - expect { process(:forwarded_by_group_to_inbox_double_cc) }.to change { User.where(staged: true).count }.by(5) + expect { process(:forwarded_by_group_to_inbox_double_cc) }.to change { + User.where(staged: true).count + }.by(5) topic = Topic.last cc_user1 = User.find_by_email("terry@ccland.com") cc_user2 = User.find_by_email("don@ccland.com") fred_user = User.find_by_email("fred@bedrock.com") team_user = User.find_by_email("team@somesmtpaddress.com") someother_user = User.find_by_email("someotherparty@test.com") - expect(topic.incoming_email.first.cc_addresses).to eq("someotherparty@test.com;terry@ccland.com;don@ccland.com") - expect(topic.topic_allowed_users.pluck(:user_id)).to match_array([ - fred_user.id, team_user.id, someother_user.id, cc_user1.id, cc_user2.id - ]) + expect(topic.incoming_email.first.cc_addresses).to eq( + "someotherparty@test.com;terry@ccland.com;don@ccland.com", + ) + expect(topic.topic_allowed_users.pluck(:user_id)).to match_array( + [fred_user.id, team_user.id, someother_user.id, cc_user1.id, cc_user2.id], + ) end context "when staged user for the team email already exists" do @@ -1216,12 +1331,14 @@ RSpec.describe Email::Receiver do email: "team@somesmtpaddress.com", username: UserNameSuggester.suggest("team@somesmtpaddress.com"), name: "team teamson", - staged: true + staged: true, ) end it "uses that and does not create another staged user" do - expect { process(:forwarded_by_group_to_inbox) }.to change { User.where(staged: true).count }.by(3) + expect { process(:forwarded_by_group_to_inbox) }.to change { + User.where(staged: true).count + }.by(3) topic = Topic.last forwarded_small_post = topic.ordered_posts.last expect(forwarded_small_post.action_code).to eq("forwarded") @@ -1239,7 +1356,7 @@ RSpec.describe Email::Receiver do smtp_server: "smtp.test.com", smtp_port: 587, smtp_ssl: true, - smtp_enabled: true + smtp_enabled: true, ) process(:email_to_group_email_username_1) Topic.last @@ -1252,16 +1369,17 @@ RSpec.describe Email::Receiver do before do NotificationEmailer.enable - SiteSetting.disallow_reply_by_email_after_days = 10000 + SiteSetting.disallow_reply_by_email_after_days = 10_000 Jobs.run_immediately! end def reply_as_group_user - group_post = PostCreator.create( - user_in_group, - raw: "Thanks for your request. Please try to restart.", - topic_id: original_inbound_email_topic.id - ) + group_post = + PostCreator.create( + user_in_group, + raw: "Thanks for your request. Please try to restart.", + topic_id: original_inbound_email_topic.id, + ) email_log = EmailLog.last [email_log, group_post] end @@ -1291,9 +1409,9 @@ RSpec.describe Email::Receiver do reply_email = email(:email_to_group_email_username_2) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) - expect do - Email::Receiver.new(reply_email).process! - end.to not_change { Topic.count }.and change { Post.count }.by(1) + expect do Email::Receiver.new(reply_email).process! end.to not_change { + Topic.count + }.and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.reply_to_user).to eq(user_in_group) @@ -1305,9 +1423,9 @@ RSpec.describe Email::Receiver do reply_email = email(:email_to_group_email_username_2_as_unknown_sender) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) - expect do - Email::Receiver.new(reply_email).process! - end.to change { Topic.count }.by(1).and change { Post.count }.by(1) + expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by( + 1, + ).and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.topic_id).not_to eq(original_inbound_email_topic.id) @@ -1319,9 +1437,9 @@ RSpec.describe Email::Receiver do reply_email = email(:email_to_group_email_username_2_as_unknown_sender) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) - expect do - Email::Receiver.new(reply_email).process! - end.to not_change { Topic.count }.and change { Post.count }.by(1) + expect do Email::Receiver.new(reply_email).process! end.to not_change { + Topic.count + }.and change { Post.count }.by(1) reply_post = Post.last expect(reply_post.topic_id).to eq(original_inbound_email_topic.id) @@ -1336,9 +1454,9 @@ RSpec.describe Email::Receiver do reply_email = email(:email_to_group_email_username_2) reply_email.gsub!("MESSAGE_ID_REPLY_TO", email_log.message_id) - expect do - Email::Receiver.new(reply_email).process! - end.to change { Topic.count }.by(1).and change { Post.count }.by(1) + expect do Email::Receiver.new(reply_email).process! end.to change { Topic.count }.by( + 1, + ).and change { Post.count }.by(1) reply_post = Post.last new_topic = Topic.last @@ -1347,8 +1465,10 @@ RSpec.describe Email::Receiver do expect(reply_post.raw).to include( I18n.t( "emails.incoming.continuing_old_discussion", - url: group_post.topic.url, title: group_post.topic.title, count: SiteSetting.disallow_reply_by_email_after_days - ) + url: group_post.topic.url, + title: group_post.topic.title, + count: SiteSetting.disallow_reply_by_email_after_days, + ), ) end end @@ -1360,24 +1480,32 @@ RSpec.describe Email::Receiver do end it "creates a reply when the sender and referenced message id are known" do - expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } + expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { + Topic.count + } end it "creates a new topic when the sender is not known and the group does not allow unknown senders to reply to topics" do - IncomingEmail.where(message_id: '34@foo.bar.mail').update(cc_addresses: 'three@foo.com') + IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: false) - expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { Topic.count }.by(1) + expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { + Topic.count + }.by(1) end it "creates a new topic when the referenced message id is not known" do - IncomingEmail.where(message_id: '34@foo.bar.mail').update(message_id: '99@foo.bar.mail') - expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { Topic.count }.by(1) + IncomingEmail.where(message_id: "34@foo.bar.mail").update(message_id: "99@foo.bar.mail") + expect { process(:email_reply_2) }.to not_change { topic.posts.count }.and change { + Topic.count + }.by(1) end it "includes the sender on the topic when the message id is known, the sender is not known, and the group allows unknown senders to reply to topics" do - IncomingEmail.where(message_id: '34@foo.bar.mail').update(cc_addresses: 'three@foo.com') + IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: true) - expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } + expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { + Topic.count + } end context "when the sender is not in the topic allowed users" do @@ -1387,16 +1515,24 @@ RSpec.describe Email::Receiver do end it "adds them to the topic at the same time" do - IncomingEmail.where(message_id: '34@foo.bar.mail').update(cc_addresses: 'three@foo.com') + IncomingEmail.where(message_id: "34@foo.bar.mail").update(cc_addresses: "three@foo.com") group.update(allow_unknown_sender_topic_replies: true) - expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { Topic.count } + expect { process(:email_reply_2) }.to change { topic.posts.count }.by(1).and not_change { + Topic.count + } end end end end describe "new topic in a category" do - fab!(:category) { Fabricate(:category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: false) } + fab!(:category) do + Fabricate( + :category, + email_in: "category@bar.com|category@foo.com", + email_in_allow_strangers: false, + ) + end it "raises a StrangersNotAllowedError when 'email_in_allow_strangers' is disabled" do expect { process(:new_user) }.to raise_error(Email::Receiver::StrangersNotAllowedError) @@ -1405,19 +1541,26 @@ RSpec.describe Email::Receiver do it "raises an InsufficientTrustLevelError when user's trust level isn't enough" do Fabricate(:user, email: "existing@bar.com", trust_level: 3) SiteSetting.email_in_min_trust = 4 - expect { process(:existing_user) }.to raise_error(Email::Receiver::InsufficientTrustLevelError) + expect { process(:existing_user) }.to raise_error( + Email::Receiver::InsufficientTrustLevelError, + ) end it "works" do handler_calls = 0 - handler = proc { |topic| - expect(topic.incoming_email_addresses).to contain_exactly("discourse@bar.com", "category@foo.com") - handler_calls += 1 - } + handler = + proc do |topic| + expect(topic.incoming_email_addresses).to contain_exactly( + "discourse@bar.com", + "category@foo.com", + ) + handler_calls += 1 + end DiscourseEvent.on(:topic_created, &handler) - user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) + user = + Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) group = Fabricate(:group) group.add(user) @@ -1439,7 +1582,7 @@ RSpec.describe Email::Receiver do end it "creates visible topic for ham" do - SiteSetting.email_in_spam_header = 'none' + SiteSetting.email_in_spam_header = "none" Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) expect { process(:existing_user) }.to change { Topic.count }.by(1) # Topic created @@ -1454,33 +1597,37 @@ RSpec.describe Email::Receiver do end it "creates hidden topic for X-Spam-Flag" do - SiteSetting.email_in_spam_header = 'X-Spam-Flag' + SiteSetting.email_in_spam_header = "X-Spam-Flag" - user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) + user = + Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) expect { process(:spam_x_spam_flag) }.to change { ReviewableQueuedPost.count }.by(1) expect(user.reload.silenced?).to be(true) end it "creates hidden topic for X-Spam-Status" do - SiteSetting.email_in_spam_header = 'X-Spam-Status' + SiteSetting.email_in_spam_header = "X-Spam-Status" - user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) + user = + Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) expect { process(:spam_x_spam_status) }.to change { ReviewableQueuedPost.count }.by(1) expect(user.reload.silenced?).to be(true) end it "creates hidden topic for X-SES-Spam-Verdict" do - SiteSetting.email_in_spam_header = 'X-SES-Spam-Verdict' + SiteSetting.email_in_spam_header = "X-SES-Spam-Verdict" - user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) + user = + Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) expect { process(:spam_x_ses_spam_verdict) }.to change { ReviewableQueuedPost.count }.by(1) expect(user.reload.silenced?).to be(true) end it "creates hidden topic for failed Authentication-Results header" do - SiteSetting.email_in_authserv_id = 'example.com' + SiteSetting.email_in_authserv_id = "example.com" - user = Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) + user = + Fabricate(:user, email: "existing@bar.com", trust_level: SiteSetting.email_in_min_trust) expect { process(:dmarc_fail) }.to change { ReviewableQueuedPost.count }.by(1) expect(user.reload.silenced?).to be(false) end @@ -1492,7 +1639,11 @@ RSpec.describe Email::Receiver do expect { process(:forwarded_email_to_category) }.to change { Topic.count }.by(1) # Topic created new_post, = Post.last - expect(new_post.raw).to include("Hi everyone, can you have a look at the email below?", "···", "Discoursing much today?") + expect(new_post.raw).to include( + "Hi everyone, can you have a look at the email below?", + "···", + "Discoursing much today?", + ) end it "works when approving is enabled" do @@ -1516,26 +1667,31 @@ RSpec.describe Email::Receiver do end it "associates email from a secondary address with user" do - user = Fabricate(:user, - trust_level: SiteSetting.email_in_min_trust, - user_emails: [ - Fabricate.build(:secondary_email, email: "existing@bar.com") - ] - ) + user = + Fabricate( + :user, + trust_level: SiteSetting.email_in_min_trust, + user_emails: [Fabricate.build(:secondary_email, email: "existing@bar.com")], + ) expect { process(:existing_user) }.to change(Topic, :count).by(1) topic = Topic.last - expect(topic.posts.last.raw) - .to eq("Hey, this is a topic from an existing user ;)") + expect(topic.posts.last.raw).to eq("Hey, this is a topic from an existing user ;)") expect(topic.user).to eq(user) end end describe "new topic in a category that allows strangers" do - fab!(:category) { Fabricate(:category, email_in: "category@bar.com|category@foo.com", email_in_allow_strangers: true) } + fab!(:category) do + Fabricate( + :category, + email_in: "category@bar.com|category@foo.com", + email_in_allow_strangers: true, + ) + end it "lets an email in from a stranger" do expect { process(:new_user) }.to change(Topic, :count) @@ -1551,7 +1707,6 @@ RSpec.describe Email::Receiver do Fabricate(:user, email: "tl3@bar.com", trust_level: TrustLevel[3]) expect { process(:tl3_user) }.to raise_error(Email::Receiver::InsufficientTrustLevelError) end - end describe "#reply_by_email_address_regex" do @@ -1577,28 +1732,27 @@ RSpec.describe Email::Receiver do it "combines both 'reply_by_email' settings" do SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" SiteSetting.alternative_reply_by_email_addresses = "alt.foo+%{reply_key}@bar.com" - expect(Email::Receiver.reply_by_email_address_regex).to eq(/foo\+?(\h{32})?@bar\.com|alt\.foo\+?(\h{32})?@bar\.com/) + expect(Email::Receiver.reply_by_email_address_regex).to eq( + /foo\+?(\h{32})?@bar\.com|alt\.foo\+?(\h{32})?@bar\.com/, + ) end - end describe "check_address" do - before do - SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" - end + before { SiteSetting.reply_by_email_address = "foo+%{reply_key}@bar.com" } it "returns nil when the key is invalid" do - expect(Email::Receiver.check_address('fake@fake.com')).to be_nil - expect(Email::Receiver.check_address('foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com')).to be_nil + expect(Email::Receiver.check_address("fake@fake.com")).to be_nil + expect( + Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com"), + ).to be_nil end context "with a valid reply" do it "returns the destination when the key is valid" do - post_reply_key = Fabricate(:post_reply_key, - reply_key: '4f97315cc828096c9cb34c6f1a0d6fe8' - ) + post_reply_key = Fabricate(:post_reply_key, reply_key: "4f97315cc828096c9cb34c6f1a0d6fe8") - dest = Email::Receiver.check_address('foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com') + dest = Email::Receiver.check_address("foo+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com") expect(dest).to eq(post_reply_key) end @@ -1606,9 +1760,7 @@ RSpec.describe Email::Receiver do end describe "staged users" do - before do - SiteSetting.enable_staged_users = true - end + before { SiteSetting.enable_staged_users = true } shared_examples "does not create staged users" do |email_name, expected_exception| it "does not create staged users" do @@ -1635,27 +1787,35 @@ RSpec.describe Email::Receiver do end context "when email address is screened" do - before do - ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) - end + before { ScreenedEmail.expects(:should_block?).with("screened@mail.com").returns(true) } - include_examples "does not create staged users", :screened_email, Email::Receiver::ScreenedEmailError + include_examples "does not create staged users", + :screened_email, + Email::Receiver::ScreenedEmailError end context "when the mail is auto generated" do - include_examples "does not create staged users", :auto_generated_header, Email::Receiver::AutoGeneratedEmailError + include_examples "does not create staged users", + :auto_generated_header, + Email::Receiver::AutoGeneratedEmailError end context "when email is a bounced email" do - include_examples "does not create staged users", :bounced_email, Email::Receiver::BouncedEmailError + include_examples "does not create staged users", + :bounced_email, + Email::Receiver::BouncedEmailError end context "when the body is blank" do - include_examples "does not create staged users", :no_body, Email::Receiver::NoBodyDetectedError + include_examples "does not create staged users", + :no_body, + Email::Receiver::NoBodyDetectedError end context "when unsubscribe via email is not allowed" do - include_examples "does not create staged users", :unsubscribe_new_user, Email::Receiver::UnsubscribeNotAllowed + include_examples "does not create staged users", + :unsubscribe_new_user, + Email::Receiver::UnsubscribeNotAllowed end context "when From email address is not on allowlist" do @@ -1664,7 +1824,9 @@ RSpec.describe Email::Receiver do Fabricate(:group, incoming_email: "some_group@bar.com") end - include_examples "does not create staged users", :blocklist_allowlist_email, Email::Receiver::EmailNotAllowed + include_examples "does not create staged users", + :blocklist_allowlist_email, + Email::Receiver::EmailNotAllowed end context "when From email address is on blocklist" do @@ -1673,13 +1835,13 @@ RSpec.describe Email::Receiver do Fabricate(:group, incoming_email: "some_group@bar.com") end - include_examples "does not create staged users", :blocklist_allowlist_email, Email::Receiver::EmailNotAllowed + include_examples "does not create staged users", + :blocklist_allowlist_email, + Email::Receiver::EmailNotAllowed end context "with blocklist and allowlist for To and Cc" do - before do - Fabricate(:group, incoming_email: "some_group@bar.com") - end + before { Fabricate(:group, incoming_email: "some_group@bar.com") } it "does not create staged users for email addresses not on allowlist" do SiteSetting.allowed_email_domains = "mail.com|example.com" @@ -1701,39 +1863,55 @@ RSpec.describe Email::Receiver do end context "when destinations aren't matching any of the incoming emails" do - include_examples "does not create staged users", :bad_destinations, Email::Receiver::BadDestinationAddress + include_examples "does not create staged users", + :bad_destinations, + Email::Receiver::BadDestinationAddress end context "when email is sent to category" do context "when email is sent by a new user and category does not allow strangers" do - fab!(:category) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false) } + fab!(:category) do + Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: false) + end - include_examples "does not create staged users", :new_user, Email::Receiver::StrangersNotAllowedError + include_examples "does not create staged users", + :new_user, + Email::Receiver::StrangersNotAllowedError end context "when email has no date" do - fab!(:category) { Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true) } + fab!(:category) do + Fabricate(:category, email_in: "category@foo.com", email_in_allow_strangers: true) + end it "includes the translated string in the error" do - expect { process(:no_date) }.to raise_error(Email::Receiver::InvalidPost).with_message(I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid")) + expect { process(:no_date) }.to raise_error(Email::Receiver::InvalidPost).with_message( + I18n.t("system_messages.email_reject_invalid_post_specified.date_invalid"), + ) end include_examples "does not create staged users", :no_date, Email::Receiver::InvalidPost end context "with forwarded emails behaviour set to quote" do - before do - SiteSetting.forwarded_emails_behaviour = "quote" - end + before { SiteSetting.forwarded_emails_behaviour = "quote" } context "with a category which allows strangers" do - fab!(:category) { Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: true) } - include_examples "creates topic with forwarded message as quote", :category, "team@bar.com" + fab!(:category) do + Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: true) + end + include_examples "creates topic with forwarded message as quote", + :category, + "team@bar.com" end context "with a category which doesn't allow strangers" do - fab!(:category) { Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: false) } - include_examples "cleans up staged users", :forwarded_email_1, Email::Receiver::StrangersNotAllowedError + fab!(:category) do + Fabricate(:category, email_in: "team@bar.com", email_in_allow_strangers: false) + end + include_examples "cleans up staged users", + :forwarded_email_1, + Email::Receiver::StrangersNotAllowedError end end end @@ -1751,24 +1929,24 @@ RSpec.describe Email::Receiver do end context "when the email address isn't matching the one we sent the notification to" do - include_examples "does not create staged users", :reply_user_not_matching, Email::Receiver::ReplyUserNotMatchingError + include_examples "does not create staged users", + :reply_user_not_matching, + Email::Receiver::ReplyUserNotMatchingError end context "with forwarded emails behaviour set to create replies" do - before do - SiteSetting.forwarded_emails_behaviour = "create_replies" - end + before { SiteSetting.forwarded_emails_behaviour = "create_replies" } context "when a reply contains a forwarded email" do include_examples "does not create staged users", :reply_and_forwarded end context "with forwarded email to category that doesn't allow strangers" do - before do - category.update!(email_in: "team@bar.com", email_in_allow_strangers: false) - end + before { category.update!(email_in: "team@bar.com", email_in_allow_strangers: false) } - include_examples "cleans up staged users", :forwarded_email_1, Email::Receiver::StrangersNotAllowedError + include_examples "cleans up staged users", + :forwarded_email_1, + Email::Receiver::StrangersNotAllowedError end end end @@ -1782,33 +1960,34 @@ RSpec.describe Email::Receiver do end context "when the topic was deleted" do - before do - topic.update_columns(deleted_at: 1.day.ago) - end + before { topic.update_columns(deleted_at: 1.day.ago) } - include_examples "cleans up staged users", :email_reply_staged, Email::Receiver::TopicNotFoundError + include_examples "cleans up staged users", + :email_reply_staged, + Email::Receiver::TopicNotFoundError end context "when the topic was closed" do - before do - topic.update_columns(closed: true) - end + before { topic.update_columns(closed: true) } - include_examples "cleans up staged users", :email_reply_staged, Email::Receiver::TopicClosedError + include_examples "cleans up staged users", + :email_reply_staged, + Email::Receiver::TopicClosedError end context "when they aren't allowed to like a post" do - before do - topic.update_columns(archived: true) - end + before { topic.update_columns(archived: true) } - include_examples "cleans up staged users", :email_reply_like, Email::Receiver::InvalidPostAction + include_examples "cleans up staged users", + :email_reply_like, + Email::Receiver::InvalidPostAction end end it "does not remove the incoming email record when staged users are deleted" do - expect { process(:bad_destinations) }.to change { IncomingEmail.count } - .and raise_error(Email::Receiver::BadDestinationAddress) + expect { process(:bad_destinations) }.to change { IncomingEmail.count }.and raise_error( + Email::Receiver::BadDestinationAddress, + ) expect(IncomingEmail.last.message_id).to eq("9@foo.bar.mail") end end @@ -1816,9 +1995,7 @@ RSpec.describe Email::Receiver do describe "mailing list mirror" do fab!(:category) { Fabricate(:mailinglist_mirror_category) } - before do - SiteSetting.block_auto_generated_emails = true - end + before { SiteSetting.block_auto_generated_emails = true } it "should allow creating topic even when email is autogenerated" do expect { process(:mailinglist) }.to change { Topic.count } @@ -1867,7 +2044,9 @@ RSpec.describe Email::Receiver do SiteSetting.unsubscribe_via_email = true Fabricate(:user, email: "alice@foo.com") - expect { process("mailinglist_unsubscribe") }.to_not change { ActionMailer::Base.deliveries.count } + expect { process("mailinglist_unsubscribe") }.to_not change { + ActionMailer::Base.deliveries.count + } end it "ignores dmarc fails" do @@ -1880,7 +2059,9 @@ RSpec.describe Email::Receiver do end it "tries to fix unparsable email addresses in To and CC headers" do - expect { process(:unparsable_email_addresses) }.to raise_error(Email::Receiver::BadDestinationAddress) + expect { process(:unparsable_email_addresses) }.to raise_error( + Email::Receiver::BadDestinationAddress, + ) email = IncomingEmail.last expect(email.to_addresses).to eq("foo@bar.com") @@ -1888,8 +2069,7 @@ RSpec.describe Email::Receiver do end describe "#select_body" do - let(:email) { - <<~EMAIL + let(:email) { <<~EMAIL } MIME-Version: 1.0 Date: Tue, 01 Jan 2019 00:00:00 +0300 Subject: An email with whitespaces @@ -1939,10 +2119,8 @@ RSpec.describe Email::Receiver do Bye! EMAIL - } - let(:stripped_text) { - <<~MD + let(:stripped_text) { <<~MD } This is a line that will be stripped This is another line that will be stripped @@ -1985,7 +2163,6 @@ RSpec.describe Email::Receiver do Bye! MD - } it "strips lines if strip_incoming_email_lines is enabled" do SiteSetting.strip_incoming_email_lines = true @@ -2019,14 +2196,16 @@ RSpec.describe Email::Receiver do describe "replying to digest" do fab!(:user) { Fabricate(:user) } fab!(:digest_message_id) { "7402d8ae-1c6e-44bc-9948-48e007839bcc@localhost" } - fab!(:email_log) { Fabricate(:email_log, - user: user, - email_type: 'digest', - to_address: user.email, - message_id: digest_message_id - )} - let(:email) { - <<~EMAIL + fab!(:email_log) do + Fabricate( + :email_log, + user: user, + email_type: "digest", + to_address: user.email, + message_id: digest_message_id, + ) + end + let(:email) { <<~EMAIL } MIME-Version: 1.0 Date: Tue, 01 Jan 2019 00:00:00 +0300 From: someone <#{user.email}> @@ -2040,14 +2219,13 @@ RSpec.describe Email::Receiver do hello there! I like the digest! EMAIL - } - before do - Jobs.run_immediately! - end + before { Jobs.run_immediately! } - it 'returns a ReplyToDigestError' do - expect { Email::Receiver.new(email).process! }.to raise_error(Email::Receiver::ReplyToDigestError) + it "returns a ReplyToDigestError" do + expect { Email::Receiver.new(email).process! }.to raise_error( + Email::Receiver::ReplyToDigestError, + ) end end @@ -2055,7 +2233,8 @@ RSpec.describe Email::Receiver do let(:user) { Fabricate(:user) } let(:group) { Fabricate(:group, users: [user]) } - let (:email_1) { <<~EMAIL + let (:email_1) { + <<~EMAIL MIME-Version: 1.0 Date: Wed, 01 Jan 2019 12:00:00 +0200 Message-ID: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com> @@ -2077,15 +2256,19 @@ RSpec.describe Email::Receiver do } let (:post_2) { - incoming_email = IncomingEmail.find_by(message_id: "7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com") + incoming_email = + IncomingEmail.find_by(message_id: "7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com") - PostCreator.create(user, - raw: "Vestibulum rutrum tortor vitae arcu varius, non vestibulum ipsum tempor. Integer nibh libero, dignissim eu velit vel, interdum posuere mi. Aliquam erat volutpat. Pellentesque id nulla ultricies, eleifend ipsum non, fringilla purus. Aliquam pretium dolor lobortis urna volutpat, vel consectetur arcu porta. In non erat quis nibh gravida pharetra consequat vel risus. Aliquam rutrum consectetur est ac posuere. Praesent mattis nunc risus, a molestie lectus accumsan porta.", - topic_id: incoming_email.topic_id + PostCreator.create( + user, + raw: + "Vestibulum rutrum tortor vitae arcu varius, non vestibulum ipsum tempor. Integer nibh libero, dignissim eu velit vel, interdum posuere mi. Aliquam erat volutpat. Pellentesque id nulla ultricies, eleifend ipsum non, fringilla purus. Aliquam pretium dolor lobortis urna volutpat, vel consectetur arcu porta. In non erat quis nibh gravida pharetra consequat vel risus. Aliquam rutrum consectetur est ac posuere. Praesent mattis nunc risus, a molestie lectus accumsan porta.", + topic_id: incoming_email.topic_id, ) } - let (:email_3) { <<~EMAIL + let (:email_3) { + <<~EMAIL MIME-Version: 1.0 Date: Wed, 01 Jan 2019 12:00:00 +0200 References: <7aN1uwcokt2xkfG3iYrpKmiuVhy4w9b5@mail.gmail.com> @@ -2112,19 +2295,23 @@ RSpec.describe Email::Receiver do } def receive(email_string) - Email::Receiver.new(email_string, - destinations: [group] - ).process! + Email::Receiver.new(email_string, destinations: [group]).process! end it "makes all posts in same topic" do - expect { receive(email_1) }.to change { Topic.count }.by(1).and change { Post.where(post_type: Post.types[:regular]).count }.by(1) - expect { post_2 }.to not_change { Topic.count }.and change { Post.where(post_type: Post.types[:regular]).count }.by(1) - expect { receive(email_3) }.to not_change { Topic.count }.and change { Post.where(post_type: Post.types[:regular]).count }.by(1) + expect { receive(email_1) }.to change { Topic.count }.by(1).and change { + Post.where(post_type: Post.types[:regular]).count + }.by(1) + expect { post_2 }.to not_change { Topic.count }.and change { + Post.where(post_type: Post.types[:regular]).count + }.by(1) + expect { receive(email_3) }.to not_change { Topic.count }.and change { + Post.where(post_type: Post.types[:regular]).count + }.by(1) end end - it 'fixes valid addresses in embedded emails' do + it "fixes valid addresses in embedded emails" do Fabricate(:group, incoming_email: "group-fwd@example.com") process(:long_embedded_email_headers) incoming_email = IncomingEmail.find_by(message_id: "example1@mail.gmail.com") diff --git a/spec/lib/email/renderer_spec.rb b/spec/lib/email/renderer_spec.rb index 8e74115486..53796146ea 100644 --- a/spec/lib/email/renderer_spec.rb +++ b/spec/lib/email/renderer_spec.rb @@ -1,20 +1,18 @@ # frozen_string_literal: true -require 'email/renderer' +require "email/renderer" RSpec.describe Email::Renderer do - let(:message) do mail = Mail.new - mail.text_part = Mail::Part.new do - body 'Key & Peele' - end + mail.text_part = Mail::Part.new { body "Key & Peele" } - mail.html_part = Mail::Part.new do - content_type 'text/html; charset=UTF-8' - body '

    Key & Peele

    ' - end + mail.html_part = + Mail::Part.new do + content_type "text/html; charset=UTF-8" + body "

    Key & Peele

    " + end mail end @@ -23,5 +21,4 @@ RSpec.describe Email::Renderer do renderer = Email::Renderer.new(message) expect(renderer.text).to eq("Key & Peele") end - end diff --git a/spec/lib/email/sender_spec.rb b/spec/lib/email/sender_spec.rb index 5dd1b5f3ef..a3619e28c2 100644 --- a/spec/lib/email/sender_spec.rb +++ b/spec/lib/email/sender_spec.rb @@ -1,18 +1,16 @@ # frozen_string_literal: true -require 'email/sender' +require "email/sender" RSpec.describe Email::Sender do - before do - SiteSetting.secure_uploads_allow_embed_images_in_emails = false - end + before { SiteSetting.secure_uploads_allow_embed_images_in_emails = false } fab!(:post) { Fabricate(:post) } - let(:mock_smtp_transaction_response) { "250 Ok: queued as 2l3Md07BObzB8kRyHZeoN0baSUAhzc7A-NviRioOr80=@mailhog.example" } + let(:mock_smtp_transaction_response) do + "250 Ok: queued as 2l3Md07BObzB8kRyHZeoN0baSUAhzc7A-NviRioOr80=@mailhog.example" + end def stub_deliver_response(message) - message.stubs(:deliver!).returns( - Net::SMTP::Response.new("250", mock_smtp_transaction_response) - ) + message.stubs(:deliver!).returns(Net::SMTP::Response.new("250", mock_smtp_transaction_response)) end context "when disable_emails is enabled" do @@ -62,15 +60,16 @@ RSpec.describe Email::Sender do it "delivers mail to staff user when confirming new email if user is provided" do Mail::Message.any_instance.expects(:deliver!).once - Fabricate(:email_change_request, { - user: moderator, - new_email: "newemail@testmoderator.com", - old_email: moderator.email, - change_state: EmailChangeRequest.states[:authorizing_new] - }) - message = Mail::Message.new( - to: "newemail@testmoderator.com", body: "hello" + Fabricate( + :email_change_request, + { + user: moderator, + new_email: "newemail@testmoderator.com", + old_email: moderator.email, + change_state: EmailChangeRequest.states[:authorizing_new], + }, ) + message = Mail::Message.new(to: "newemail@testmoderator.com", body: "hello") Email::Sender.new(message, :confirm_new_email, moderator).send end end @@ -88,20 +87,23 @@ RSpec.describe Email::Sender do end it "doesn't deliver when the to address is nil" do - message = Mail::Message.new(body: 'hello') + message = Mail::Message.new(body: "hello") message.expects(:deliver!).never Email::Sender.new(message, :hello).send end it "doesn't deliver when the to address uses the .invalid tld" do - message = Mail::Message.new(body: 'hello', to: 'myemail@example.invalid') + message = Mail::Message.new(body: "hello", to: "myemail@example.invalid") message.expects(:deliver!).never - expect { Email::Sender.new(message, :hello).send }. - to change { SkippedEmailLog.where(reason_type: SkippedEmailLog.reason_types[:sender_message_to_invalid]).count }.by(1) + expect { Email::Sender.new(message, :hello).send }.to change { + SkippedEmailLog.where( + reason_type: SkippedEmailLog.reason_types[:sender_message_to_invalid], + ).count + }.by(1) end it "doesn't deliver when the body is nil" do - message = Mail::Message.new(to: 'eviltrout@test.domain') + message = Mail::Message.new(to: "eviltrout@test.domain") message.expects(:deliver!).never Email::Sender.new(message, :hello).send end @@ -122,69 +124,67 @@ RSpec.describe Email::Sender do it "downcases hosts" do expect(Email::Sender.host_for("http://ForumSite.com")).to eq("forumsite.com") end - end - context 'with a valid message' do + context "with a valid message" do let(:reply_key) { "abcd" * 8 } let(:message) do - message = Mail::Message.new( - to: 'eviltrout@test.domain', - body: '**hello**' - ) + message = Mail::Message.new(to: "eviltrout@test.domain", body: "**hello**") stub_deliver_response(message) message end let(:email_sender) { Email::Sender.new(message, :valid_type) } - it 'calls deliver' do + it "calls deliver" do message.expects(:deliver!).once email_sender.send end context "when no plus addressing" do - before { SiteSetting.reply_by_email_address = '%{reply_key}@test.com' } + before { SiteSetting.reply_by_email_address = "%{reply_key}@test.com" } - it 'should not set the return_path' do + it "should not set the return_path" do email_sender.send expect(message.header[:return_path].to_s).to eq("") end end context "with plus addressing" do - before { SiteSetting.reply_by_email_address = 'replies+%{reply_key}@test.com' } + before { SiteSetting.reply_by_email_address = "replies+%{reply_key}@test.com" } - it 'should set the return_path' do + it "should set the return_path" do email_sender.send - expect(message.header[:return_path].to_s).to eq("replies+verp-#{EmailLog.last.bounce_key}@test.com") + expect(message.header[:return_path].to_s).to eq( + "replies+verp-#{EmailLog.last.bounce_key}@test.com", + ) end end context "when topic id is present" do - fab!(:category) { Fabricate(:category, name: 'Name With Space') } + fab!(:category) { Fabricate(:category, name: "Name With Space") } fab!(:topic) { Fabricate(:topic, category: category) } fab!(:post) { Fabricate(:post, topic: topic) } before do - message.header['X-Discourse-Post-Id'] = post.id - message.header['X-Discourse-Topic-Id'] = topic.id + message.header["X-Discourse-Post-Id"] = post.id + message.header["X-Discourse-Topic-Id"] = topic.id end - it 'should add the right header' do + it "should add the right header" do email_sender.send - expect(message.header['List-ID']).to be_present - expect(message.header['List-ID'].to_s).to match('name-with-space') + expect(message.header["List-ID"]).to be_present + expect(message.header["List-ID"].to_s).to match("name-with-space") end end context "when topic id is not present" do - it 'should add the right header' do + it "should add the right header" do email_sender.send - expect(message.header['Message-ID']).to be_present + expect(message.header["Message-ID"]).to be_present end end @@ -194,34 +194,37 @@ RSpec.describe Email::Sender do let(:reply_key) { PostReplyKey.find_by!(post_id: post.id, user_id: user.id).reply_key } before do - SiteSetting.reply_by_email_address = 'replies+%{reply_key}@test.com' - SiteSetting.email_custom_headers = 'Auto-Submitted: auto-generated|Mail-Reply-To: sender-name+%{reply_key}@domain.net' + SiteSetting.reply_by_email_address = "replies+%{reply_key}@test.com" + SiteSetting.email_custom_headers = + "Auto-Submitted: auto-generated|Mail-Reply-To: sender-name+%{reply_key}@domain.net" - message.header['X-Discourse-Post-Id'] = post.id + message.header["X-Discourse-Post-Id"] = post.id end - it 'replaces headers with reply_key if present' do - message.header[Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER] = 'test-%{reply_key}@test.com' - message.header['Reply-To'] = 'Test ' - message.header['Auto-Submitted'] = 'auto-generated' - message.header['Mail-Reply-To'] = 'sender-name+%{reply_key}@domain.net' + it "replaces headers with reply_key if present" do + message.header[ + Email::MessageBuilder::ALLOW_REPLY_BY_EMAIL_HEADER + ] = "test-%{reply_key}@test.com" + message.header["Reply-To"] = "Test " + message.header["Auto-Submitted"] = "auto-generated" + message.header["Mail-Reply-To"] = "sender-name+%{reply_key}@domain.net" email_sender.send - expect(message.header['Reply-To'].to_s).to eq("Test ") - expect(message.header['Auto-Submitted'].to_s).to eq('auto-generated') - expect(message.header['Mail-Reply-To'].to_s).to eq("sender-name+#{reply_key}@domain.net") + expect(message.header["Reply-To"].to_s).to eq("Test ") + expect(message.header["Auto-Submitted"].to_s).to eq("auto-generated") + expect(message.header["Mail-Reply-To"].to_s).to eq("sender-name+#{reply_key}@domain.net") end - it 'removes headers with reply_key if absent' do - message.header['Auto-Submitted'] = 'auto-generated' - message.header['Mail-Reply-To'] = 'sender-name+%{reply_key}@domain.net' + it "removes headers with reply_key if absent" do + message.header["Auto-Submitted"] = "auto-generated" + message.header["Mail-Reply-To"] = "sender-name+%{reply_key}@domain.net" email_sender.send - expect(message.header['Reply-To'].to_s).to eq('') - expect(message.header['Auto-Submitted'].to_s). to eq('auto-generated') - expect(message.header['Mail-Reply-To'].to_s).to eq('') + expect(message.header["Reply-To"].to_s).to eq("") + expect(message.header["Auto-Submitted"].to_s).to eq("auto-generated") + expect(message.header["Mail-Reply-To"].to_s).to eq("") end end @@ -230,22 +233,22 @@ RSpec.describe Email::Sender do fab!(:post) { Fabricate(:post, topic: topic) } before do - message.header['X-Discourse-Post-Id'] = post.id - message.header['X-Discourse-Topic-Id'] = topic.id + message.header["X-Discourse-Post-Id"] = post.id + message.header["X-Discourse-Topic-Id"] = topic.id end - it 'should add the right header' do + it "should add the right header" do email_sender.send - expect(message.header['Precedence']).to be_present + expect(message.header["Precedence"]).to be_present end end describe "removes custom Discourse headers from digest/registration/other mails" do - it 'should remove the right headers' do + it "should remove the right headers" do email_sender.send - expect(message.header['X-Discourse-Topic-Id']).not_to be_present - expect(message.header['X-Discourse-Post-Id']).not_to be_present - expect(message.header['X-Discourse-Reply-Key']).not_to be_present + expect(message.header["X-Discourse-Topic-Id"]).not_to be_present + expect(message.header["X-Discourse-Post-Id"]).not_to be_present + expect(message.header["X-Discourse-Reply-Key"]).not_to be_present end end @@ -266,153 +269,175 @@ RSpec.describe Email::Sender do let!(:post_reply_4_6) { PostReply.create(post: post_4, reply: post_6) } let!(:post_reply_5_6) { PostReply.create(post: post_5, reply: post_6) } - before do - message.header['X-Discourse-Topic-Id'] = topic.id - end + before { message.header["X-Discourse-Topic-Id"] = topic.id } it "doesn't set References or In-Reply-To headers on the first post, only generates a Message-ID and saves it against the post" do - message.header['X-Discourse-Post-Id'] = post_1.id + message.header["X-Discourse-Post-Id"] = post_1.id email_sender.send post_1.reload - expect(message.header['Message-Id'].to_s).to eq("") + expect(message.header["Message-Id"].to_s).to eq( + "", + ) expect(post_1.outbound_message_id).to eq("discourse/post/#{post_1.id}@test.localhost") - expect(message.header['In-Reply-To'].to_s).to be_blank - expect(message.header['References'].to_s).to be_blank + expect(message.header["In-Reply-To"].to_s).to be_blank + expect(message.header["References"].to_s).to be_blank end it "uses the existing Message-ID header from the incoming email when sending the first post email" do - incoming = Fabricate( - :incoming_email, - topic: topic, - post: post_1, - message_id: "blah1234@someemailprovider.com", - created_via: IncomingEmail.created_via_types[:handle_mail] - ) + incoming = + Fabricate( + :incoming_email, + topic: topic, + post: post_1, + message_id: "blah1234@someemailprovider.com", + created_via: IncomingEmail.created_via_types[:handle_mail], + ) post_1.update!(outbound_message_id: incoming.message_id) - message.header['X-Discourse-Post-Id'] = post_1.id + message.header["X-Discourse-Post-Id"] = post_1.id email_sender.send - expect(message.header['Message-Id'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to be_blank - expect(message.header['References'].to_s).to be_blank + expect(message.header["Message-Id"].to_s).to eq("") + expect(message.header["In-Reply-To"].to_s).to be_blank + expect(message.header["References"].to_s).to be_blank end it "if no post is directly replied to then the Message-ID of post 1 via outbound_message_id should be used" do - message.header['X-Discourse-Post-Id'] = post_2.id + message.header["X-Discourse-Post-Id"] = post_2.id email_sender.send - expect(message.header['Message-Id'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq("") - expect(message.header['References'].to_s).to eq("") + expect(message.header["Message-Id"].to_s).to eq( + "", + ) + expect(message.header["In-Reply-To"].to_s).to eq( + "", + ) + expect(message.header["References"].to_s).to eq( + "", + ) end it "sets the References header to the most recently created replied post, as well as the OP, if there are no other replies in the chain" do - message.header['X-Discourse-Post-Id'] = post_4.id + message.header["X-Discourse-Post-Id"] = post_4.id email_sender.send - expect(message.header['Message-ID'].to_s).to eq("") - expect(message.header['References'].to_s).to eq(" ") + expect(message.header["Message-ID"].to_s).to eq( + "", + ) + expect(message.header["References"].to_s).to eq( + " ", + ) end it "sets the In-Reply-To header to all the posts that the post is connected to via PostReply" do - message.header['X-Discourse-Post-Id'] = post_6.id + message.header["X-Discourse-Post-Id"] = post_6.id email_sender.send - expect(message.header['Message-ID'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq(" ") + expect(message.header["Message-ID"].to_s).to eq( + "", + ) + expect(message.header["In-Reply-To"].to_s).to eq( + " ", + ) end it "sets the In-Reply-To and References header to the most recently created replied post and includes the parents of that post in References, as well as the OP" do - message.header['X-Discourse-Post-Id'] = post_4.id + message.header["X-Discourse-Post-Id"] = post_4.id PostReply.create(post: post_2, reply: post_3) email_sender.send - expect(message.header['Message-ID'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq(" ") + expect(message.header["Message-ID"].to_s).to eq( + "", + ) + expect(message.header["In-Reply-To"].to_s).to eq( + " ", + ) references = [ "", "", - "" + "", ] - expect(message.header['References'].to_s).to eq(references.join(" ")) + expect(message.header["References"].to_s).to eq(references.join(" ")) end it "handles a complex reply tree to the OP for References, only using one Message-ID if there are multiple parents for a post" do - message.header['X-Discourse-Post-Id'] = post_6.id + message.header["X-Discourse-Post-Id"] = post_6.id PostReply.create(post: post_2, reply: post_6) email_sender.send - expect(message.header['Message-ID'].to_s).to eq("") - expect(message.header['In-Reply-To'].to_s).to eq(" ") + expect(message.header["Message-ID"].to_s).to eq( + "", + ) + expect(message.header["In-Reply-To"].to_s).to eq( + " ", + ) references = [ "", "", "", - "" + "", ] - expect(message.header['References'].to_s).to eq(references.join(" ")) + expect(message.header["References"].to_s).to eq(references.join(" ")) end end describe "merges custom mandrill header" do before do ActionMailer::Base.smtp_settings[:address] = "smtp.mandrillapp.com" - message.header['X-MC-Metadata'] = { foo: "bar" }.to_json + message.header["X-MC-Metadata"] = { foo: "bar" }.to_json end - it 'should set the right header' do + it "should set the right header" do email_sender.send - expect(message.header['X-MC-Metadata'].to_s).to match(message.message_id) + expect(message.header["X-MC-Metadata"].to_s).to match(message.message_id) end end describe "merges custom sparkpost header" do before do ActionMailer::Base.smtp_settings[:address] = "smtp.sparkpostmail.com" - message.header['X-MSYS-API'] = { foo: "bar" }.to_json + message.header["X-MSYS-API"] = { foo: "bar" }.to_json end - it 'should set the right header' do + it "should set the right header" do email_sender.send - expect(message.header['X-MSYS-API'].to_s).to match(message.message_id) + expect(message.header["X-MSYS-API"].to_s).to match(message.message_id) end end - context 'with email logs' do + context "with email logs" do let(:email_log) { EmailLog.last } - it 'should create the right log' do - expect do - email_sender.send - end.to_not change { PostReplyKey.count } + it "should create the right log" do + expect do email_sender.send end.to_not change { PostReplyKey.count } expect(email_log).to be_present - expect(email_log.email_type).to eq('valid_type') - expect(email_log.to_address).to eq('eviltrout@test.domain') + expect(email_log.email_type).to eq("valid_type") + expect(email_log.to_address).to eq("eviltrout@test.domain") expect(email_log.user_id).to be_blank expect(email_log.raw).to eq(nil) end - context 'when the email is sent using group SMTP credentials' do - let(:reply) { Fabricate(:post, topic: post.topic, reply_to_user: post.user, reply_to_post_number: post.post_number) } - let(:notification) { Fabricate(:posted_notification, user: post.user, post: reply) } - let(:message) do - GroupSmtpMailer.send_mail( - group, - post.user.email, - post + context "when the email is sent using group SMTP credentials" do + let(:reply) do + Fabricate( + :post, + topic: post.topic, + reply_to_user: post.user, + reply_to_post_number: post.post_number, ) end + let(:notification) { Fabricate(:posted_notification, user: post.user, post: reply) } + let(:message) { GroupSmtpMailer.send_mail(group, post.user.email, post) } let(:group) { Fabricate(:smtp_group) } before do @@ -420,13 +445,13 @@ RSpec.describe Email::Sender do stub_deliver_response(message) end - it 'adds the group id and raw content to the email log' do + it "adds the group id and raw content to the email log" do TopicAllowedGroup.create(topic: post.topic, group: group) email_sender.send expect(email_log).to be_present - expect(email_log.email_type).to eq('valid_type') + expect(email_log.email_type).to eq("valid_type") expect(email_log.to_address).to eq(post.user.email) expect(email_log.user_id).to be_blank expect(email_log.smtp_group_id).to eq(group.id) @@ -437,17 +462,17 @@ RSpec.describe Email::Sender do TopicAllowedGroup.create(topic: post.topic, group: group) email_sender.send - expect(message.header['List-ID']).to eq(nil) - expect(message.header['List-Archive']).to eq(nil) - expect(message.header['Precedence']).to eq(nil) - expect(message.header['List-Unsubscribe']).to eq(nil) + expect(message.header["List-ID"]).to eq(nil) + expect(message.header["List-Archive"]).to eq(nil) + expect(message.header["Precedence"]).to eq(nil) + expect(message.header["List-Unsubscribe"]).to eq(nil) end it "removes the Auto-Submitted header" do TopicAllowedGroup.create!(topic: post.topic, group: group) email_sender.send - expect(message.header['Auto-Submitted']).to eq(nil) + expect(message.header["Auto-Submitted"]).to eq(nil) end end end @@ -456,13 +481,13 @@ RSpec.describe Email::Sender do let(:topic) { post.topic } before do - message.header['X-Discourse-Post-Id'] = post.id - message.header['X-Discourse-Topic-Id'] = topic.id + message.header["X-Discourse-Post-Id"] = post.id + message.header["X-Discourse-Topic-Id"] = topic.id end let(:email_log) { EmailLog.last } - it 'should create the right log' do + it "should create the right log" do email_sender.send expect(email_log.post_id).to eq(post.id) expect(email_log.topic_id).to eq(topic.id) @@ -470,13 +495,13 @@ RSpec.describe Email::Sender do end end - context 'with email parts' do - it 'should contain the right message' do + context "with email parts" do + it "should contain the right message" do email_sender.send expect(message).to be_multipart - expect(message.text_part.content_type).to eq('text/plain; charset=UTF-8') - expect(message.html_part.content_type).to eq('text/html; charset=UTF-8') + expect(message.text_part.content_type).to eq("text/plain; charset=UTF-8") + expect(message.html_part.content_type).to eq("text/html; charset=UTF-8") expect(message.html_part.body.to_s).to match("

    hello

    ") end end @@ -484,24 +509,28 @@ RSpec.describe Email::Sender do context "with attachments" do fab!(:small_pdf) do - SiteSetting.authorized_extensions = 'pdf' - UploadCreator.new(file_from_fixtures("small.pdf", "pdf"), "small.pdf") - .create_for(Discourse.system_user.id) + SiteSetting.authorized_extensions = "pdf" + UploadCreator.new(file_from_fixtures("small.pdf", "pdf"), "small.pdf").create_for( + Discourse.system_user.id, + ) end fab!(:large_pdf) do - SiteSetting.authorized_extensions = 'pdf' - UploadCreator.new(file_from_fixtures("large.pdf", "pdf"), "large.pdf") - .create_for(Discourse.system_user.id) + SiteSetting.authorized_extensions = "pdf" + UploadCreator.new(file_from_fixtures("large.pdf", "pdf"), "large.pdf").create_for( + Discourse.system_user.id, + ) end fab!(:csv_file) do - SiteSetting.authorized_extensions = 'csv' - UploadCreator.new(file_from_fixtures("words.csv", "csv"), "words.csv") - .create_for(Discourse.system_user.id) + SiteSetting.authorized_extensions = "csv" + UploadCreator.new(file_from_fixtures("words.csv", "csv"), "words.csv").create_for( + Discourse.system_user.id, + ) end fab!(:image) do - SiteSetting.authorized_extensions = 'png' - UploadCreator.new(file_from_fixtures("logo.png", "images"), "logo.png") - .create_for(Discourse.system_user.id) + SiteSetting.authorized_extensions = "png" + UploadCreator.new(file_from_fixtures("logo.png", "images"), "logo.png").create_for( + Discourse.system_user.id, + ) end fab!(:post) { Fabricate(:post) } fab!(:reply) do @@ -522,7 +551,7 @@ RSpec.describe Email::Sender do post.user, post: reply, notification_type: notification.notification_type, - notification_data_hash: notification.data_hash + notification_data_hash: notification.data_hash, ) end @@ -531,8 +560,9 @@ RSpec.describe Email::Sender do Email::Sender.new(message, :valid_type).send expect(message.attachments.length).to eq(3) - expect(message.attachments.map(&:filename)) - .to contain_exactly(*[small_pdf, large_pdf, csv_file].map(&:original_filename)) + expect(message.attachments.map(&:filename)).to contain_exactly( + *[small_pdf, large_pdf, csv_file].map(&:original_filename), + ) end it "changes the hashtags to the slug with a # symbol beforehand rather than the full name of the resource" do @@ -563,8 +593,8 @@ RSpec.describe Email::Sender do CookedPostProcessor.any_instance.stubs(:get_size).returns([244, 66]) @secure_image_file = file_from_fixtures("logo.png", "images") - @secure_image = UploadCreator.new(@secure_image_file, "logo.png") - .create_for(Discourse.system_user.id) + @secure_image = + UploadCreator.new(@secure_image_file, "logo.png").create_for(Discourse.system_user.id) @secure_image.update_secure_status(override: true) @secure_image.update(access_control_post_id: reply.id) reply.update(raw: reply.raw + "\n" + "#{UploadMarkdown.new(@secure_image).image_markdown}") @@ -577,17 +607,21 @@ RSpec.describe Email::Sender do end context "when embedding secure images in email is allowed" do - before do - SiteSetting.secure_uploads_allow_embed_images_in_emails = true - end + before { SiteSetting.secure_uploads_allow_embed_images_in_emails = true } it "can inline images with duplicate names" do - @secure_image_2 = UploadCreator.new(file_from_fixtures("logo-dev.png", "images"), "logo.png").create_for(Discourse.system_user.id) + @secure_image_2 = + UploadCreator.new(file_from_fixtures("logo-dev.png", "images"), "logo.png").create_for( + Discourse.system_user.id, + ) @secure_image_2.update_secure_status(override: true) @secure_image_2.update(access_control_post_id: reply.id) Jobs::PullHotlinkedImages.any_instance.expects(:execute) - reply.update(raw: "#{UploadMarkdown.new(@secure_image).image_markdown}\n#{UploadMarkdown.new(@secure_image_2).image_markdown}") + reply.update( + raw: + "#{UploadMarkdown.new(@secure_image).image_markdown}\n#{UploadMarkdown.new(@secure_image_2).image_markdown}", + ) reply.rebake! Email::Sender.new(message, :valid_type).send @@ -610,9 +644,12 @@ RSpec.describe Email::Sender do it "uses the email styles to inline secure images and attaches the secure image upload to the email" do Email::Sender.new(message, :valid_type).send expect(message.attachments.length).to eq(4) - expect(message.attachments.map(&:filename)) - .to contain_exactly(*[small_pdf, large_pdf, csv_file, @secure_image].map(&:original_filename)) - expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq(File.read(@secure_image_file)) + expect(message.attachments.map(&:filename)).to contain_exactly( + *[small_pdf, large_pdf, csv_file, @secure_image].map(&:original_filename), + ) + expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq( + File.read(@secure_image_file), + ) expect(message.html_part.body).to include("cid:") expect(message.html_part.body).to include("embedded-secure-image") expect(message.attachments.length).to eq(4) @@ -631,16 +668,19 @@ RSpec.describe Email::Sender do before do url = Discourse.store.store_optimized_image(optimized_image_file, optimized) - optimized.update(url: Discourse.store.absolute_base_url + '/' + url) + optimized.update(url: Discourse.store.absolute_base_url + "/" + url) Discourse.store.cache_file(optimized_image_file, File.basename("#{optimized.sha1}.png")) end it "uses the email styles and the optimized image to inline secure images and attaches the secure image upload to the email" do Email::Sender.new(message, :valid_type).send expect(message.attachments.length).to eq(4) - expect(message.attachments.map(&:filename)) - .to contain_exactly(*[small_pdf, large_pdf, csv_file, @secure_image].map(&:original_filename)) - expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq(File.read(optimized_image_file)) + expect(message.attachments.map(&:filename)).to contain_exactly( + *[small_pdf, large_pdf, csv_file, @secure_image].map(&:original_filename), + ) + expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq( + File.read(optimized_image_file), + ) expect(message.html_part.body).to include("cid:") expect(message.html_part.body).to include("embedded-secure-image") end @@ -649,7 +689,9 @@ RSpec.describe Email::Sender do SiteSetting.email_total_attachment_size_limit_kb = 45 Email::Sender.new(message, :valid_type).send expect(message.attachments.length).to eq(4) - expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq(File.read(optimized_image_file)) + expect(message.attachments["logo.png"].body.raw_source.force_encoding("UTF-8")).to eq( + File.read(optimized_image_file), + ) end end end @@ -660,8 +702,9 @@ RSpec.describe Email::Sender do Email::Sender.new(message, :valid_type).send expect(message.attachments.length).to eq(3) - expect(message.attachments.map(&:filename)) - .to contain_exactly(*[small_pdf, large_pdf, csv_file].map(&:original_filename)) + expect(message.attachments.map(&:filename)).to contain_exactly( + *[small_pdf, large_pdf, csv_file].map(&:original_filename), + ) expect(message.html_part.body).to include("") + frag = + basic_fragment("") expect(frag.at("img")["width"]).to eq("20") expect(frag.at("img")["height"]).to eq("20") end @@ -45,71 +46,75 @@ RSpec.describe Email::Styles do frag = basic_fragment("
    ") expect(frag.to_html).to eq("
    ") end - end describe "html template formatter" do it "attaches a style to h3 tags" do frag = html_fragment("

    hello

    ") - expect(frag.at('h3')['style']).to be_present + expect(frag.at("h3")["style"]).to be_present end it "attaches a style to hr tags" do frag = html_fragment("hello
    ") - expect(frag.at('hr')['style']).to be_present + expect(frag.at("hr")["style"]).to be_present end it "attaches a style to a tags" do frag = html_fragment("wat") - expect(frag.at('a')['style']).to be_present + expect(frag.at("a")["style"]).to be_present end it "attaches a style to a tags" do frag = html_fragment("wat") - expect(frag.at('a')['style']).to be_present + expect(frag.at("a")["style"]).to be_present end it "attaches a style to ul and li tags" do frag = html_fragment("
    • hello
    ") - expect(frag.at('ul')['style']).to be_present - expect(frag.at('li')['style']).to be_present + expect(frag.at("ul")["style"]).to be_present + expect(frag.at("li")["style"]).to be_present end it "converts iframes to links" do iframe_url = "http://www.youtube.com/embed/7twifrxOTQY?feature=oembed&wmode=opaque" frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_present - expect(frag.at('a')['href']).to eq(iframe_url) + expect(frag.at("iframe")).to be_blank + expect(frag.at("a")).to be_present + expect(frag.at("a")["href"]).to eq(iframe_url) end it "won't allow non URLs in iframe src, strips them with no link" do iframe_url = "alert('xss hole')" frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_blank + expect(frag.at("iframe")).to be_blank + expect(frag.at("a")).to be_blank end it "won't allow empty iframe src, strips them with no link" do frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_blank + expect(frag.at("iframe")).to be_blank + expect(frag.at("a")).to be_blank end it "prefers data-original-href attribute to get iframe link" do original_url = "https://vimeo.com/329875646/85f1546a42" iframe_url = "https://player.vimeo.com/video/329875646" - frag = html_fragment("") - expect(frag.at('iframe')).to be_blank - expect(frag.at('a')).to be_present - expect(frag.at('a')['href']).to eq(original_url) + frag = + html_fragment( + "", + ) + expect(frag.at("iframe")).to be_blank + expect(frag.at("a")).to be_present + expect(frag.at("a")["href"]).to eq(original_url) end it "replaces hashtag-cooked text with raw #hashtag" do - hashtag_html = "Dev Zone" + hashtag_html = + "Dev Zone" frag = html_fragment(hashtag_html) expect(frag.at("a").text.chomp).to eq("#dev") - hashtag_html = "Dev Zone" + hashtag_html = + "Dev Zone" frag = html_fragment(hashtag_html) expect(frag.at("a").text.chomp).to eq("#dev") end @@ -118,51 +123,56 @@ RSpec.describe Email::Styles do describe "rewriting protocol relative URLs to the forum" do it "doesn't rewrite a url to another site" do frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("//youtube.com/discourse") + expect(frag.at("a")["href"]).to eq("//youtube.com/discourse") end context "without https" do - before do - SiteSetting.force_https = false - end + before { SiteSetting.force_https = false } it "rewrites the href to have http" do frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("http://test.localhost/discourse") + expect(frag.at("a")["href"]).to eq("http://test.localhost/discourse") end it "rewrites the href for attachment files to have http" do - frag = html_fragment('attachment_file.txt') - expect(frag.at('a')['href']).to eq("http://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") + frag = + html_fragment( + 'attachment_file.txt', + ) + expect(frag.at("a")["href"]).to eq( + "http://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt", + ) end it "rewrites the src to have http" do frag = html_fragment('') - expect(frag.at('img')['src']).to eq("http://test.localhost/blah.jpg") + expect(frag.at("img")["src"]).to eq("http://test.localhost/blah.jpg") end end context "with https" do - before do - SiteSetting.force_https = true - end + before { SiteSetting.force_https = true } it "rewrites the forum URL to have https" do frag = html_fragment('hello') - expect(frag.at('a')['href']).to eq("https://test.localhost/discourse") + expect(frag.at("a")["href"]).to eq("https://test.localhost/discourse") end it "rewrites the href for attachment files to have https" do - frag = html_fragment('attachment_file.txt') - expect(frag.at('a')['href']).to eq("https://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt") + frag = + html_fragment( + 'attachment_file.txt', + ) + expect(frag.at("a")["href"]).to eq( + "https://try-discourse.global.ssl.fastly.net/uploads/default/368/40b610b0aa90cfcf.txt", + ) end it "rewrites the src to have https" do frag = html_fragment('') - expect(frag.at('img')['src']).to eq("https://test.localhost/blah.jpg") + expect(frag.at("img")["src"]).to eq("https://test.localhost/blah.jpg") end end - end describe "dark mode emails" do @@ -194,7 +204,8 @@ RSpec.describe Email::Styles do end it "works if img tag has no attrs" do - cooked = "Create a method for click on image and use ng-click in in your slide box...it is simple" + cooked = + "Create a method for click on image and use ng-click in in your slide box...it is simple" style = Email::Styles.new(cooked) style.strip_avatars_and_emojis expect(style.to_html).to include(cooked) @@ -203,13 +214,24 @@ RSpec.describe Email::Styles do describe "onebox_styles" do it "renders quote as
    " do - fragment = html_fragment('') - expect(fragment.to_s.squish).to match(/^$/) + fragment = + html_fragment( + '', + ) + expect(fragment.to_s.squish).to match(%r{^$}) end it "removes GitHub excerpts" do - stub_request(:head, "https://github.com/discourse/discourse/pull/1253").to_return(status: 200, body: "", headers: {}) - stub_request(:get, "https://api.github.com/repos/discourse/discourse/pulls/1253").to_return(status: 200, body: onebox_response("githubpullrequest")) + stub_request(:head, "https://github.com/discourse/discourse/pull/1253").to_return( + status: 200, + body: "", + headers: { + }, + ) + stub_request(:get, "https://api.github.com/repos/discourse/discourse/pulls/1253").to_return( + status: 200, + body: onebox_response("githubpullrequest"), + ) onebox = Oneboxer.onebox("https://github.com/discourse/discourse/pull/1253") fragment = html_fragment(onebox) @@ -223,48 +245,55 @@ RSpec.describe Email::Styles do SiteSetting.secure_uploads = true end - let(:attachments) { { 'testimage.png' => stub(url: 'email/test.png') } } + let(:attachments) { { "testimage.png" => stub(url: "email/test.png") } } it "replaces secure uploads within a link with a placeholder" do - frag = html_fragment("") - expect(frag.at('img')).not_to be_present + frag = + html_fragment( + "", + ) + expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "replaces secure images with a placeholder" do frag = html_fragment("") - expect(frag.at('img')).not_to be_present + expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "does not replace topic links with secure-uploads in the name" do - frag = html_fragment("Visit Topic") + frag = + html_fragment("Visit Topic") expect(frag.to_s).not_to include("Redacted") end it "works in lightboxes with missing srcset attribute" do - frag = html_fragment("") - expect(frag.at('img')).not_to be_present + frag = + html_fragment( + "", + ) + expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "works in lightboxes with srcset attribute set" do - frag = html_fragment( - <<~HTML + frag = html_fragment(<<~HTML) HTML - ) - expect(frag.at('img')).not_to be_present + expect(frag.at("img")).not_to be_present expect(frag.to_s).to include("Redacted") end it "skips links with no images as children" do - frag = html_fragment("Clearly not an image") + frag = + html_fragment( + "Clearly not an image", + ) expect(frag.to_s).to include("not an image") end - end describe "inline_secure_images" do @@ -273,10 +302,14 @@ RSpec.describe Email::Styles do SiteSetting.secure_uploads = true end - fab!(:upload) { Fabricate(:upload, original_filename: 'testimage.png', secure: true, sha1: '123456') } - let(:attachments) { [stub(url: 'cid:email/test.png')] } + fab!(:upload) do + Fabricate(:upload, original_filename: "testimage.png", secure: true, sha1: "123456") + end + let(:attachments) { [stub(url: "cid:email/test.png")] } let(:attachments_index) { { upload.sha1 => 0 } } - let(:html) { "" } + let(:html) do + "" + end def strip_and_inline # strip out the secure uploads @@ -294,40 +327,39 @@ RSpec.describe Email::Styles do it "inlines attachments where stripped-secure-media data attr is present" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") - expect(@frag.css('[data-stripped-secure-upload]')).not_to be_present - expect(@frag.children.attr('style').value).to eq("width: 20px; height: 30px;") + expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present + expect(@frag.children.attr("style").value).to eq("width: 20px; height: 30px;") end it "does not inline anything if the upload cannot be found" do - upload.update(sha1: 'blah12') + upload.update(sha1: "blah12") strip_and_inline expect(@frag.to_s).not_to include("cid:email/test.png") - expect(@frag.css('[data-stripped-secure-upload]')).to be_present + expect(@frag.css("[data-stripped-secure-upload]")).to be_present end context "when an optimized image is used instead of the original" do - let(:html) { "" } + let(:html) do + "" + end it "inlines attachments where the stripped-secure-media data attr is present" do optimized = Fabricate(:optimized_image, upload: upload, width: 20, height: 30) strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") - expect(@frag.css('[data-stripped-secure-upload]')).not_to be_present - expect(@frag.children.attr('style').value).to eq("width: 20px; height: 30px;") + expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present + expect(@frag.children.attr("style").value).to eq("width: 20px; height: 30px;") end end context "when inlining an originally oneboxed image" do - before do - SiteSetting.authorized_extensions = "*" - end + before { SiteSetting.authorized_extensions = "*" } let(:siteicon) { Fabricate(:upload, original_filename: "siteicon.ico") } - let(:attachments) { [stub(url: 'cid:email/test.png'), stub(url: 'cid:email/test2.ico')] } + let(:attachments) { [stub(url: "cid:email/test.png"), stub(url: "cid:email/test2.ico")] } let(:attachments_index) { { upload.sha1 => 0, siteicon.sha1 => 1 } } - let(:html) do - <<~HTML + let(:html) { <<~HTML } HTML - end it "keeps the special site icon width and height and onebox styles" do optimized = Fabricate(:optimized_image, upload: upload, width: 20, height: 30) strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.to_s).to include("cid:email/test2.ico") - expect(@frag.css('[data-stripped-secure-upload]')).not_to be_present - expect(@frag.css('[data-embedded-secure-image]')[0].attr('style')).to eq('width: 16px; height: 16px;') - expect(@frag.css('[data-embedded-secure-image]')[1].attr('style')).to eq('width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;') + expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present + expect(@frag.css("[data-embedded-secure-image]")[0].attr("style")).to eq( + "width: 16px; height: 16px;", + ) + expect(@frag.css("[data-embedded-secure-image]")[1].attr("style")).to eq( + "width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", + ) end context "when inlining a oneboxed image with a direct parent of onebox-body" do - let(:html) do - <<~HTML + let(:html) { <<~HTML } HTML - end it "keeps the special onebox styles" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") expect(@frag.to_s).to include("cid:email/test2.ico") - expect(@frag.css('[data-stripped-secure-upload]')).not_to be_present - expect(@frag.css('[data-embedded-secure-image]')[1].attr('style')).to eq('width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;') + expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present + expect(@frag.css("[data-embedded-secure-image]")[1].attr("style")).to eq( + "width: 60px; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", + ) end end context "when there is an inline-avatar in the onebox" do - let(:html) do - <<~HTML + let(:html) { <<~HTML }

    @martin check this out:

    HTML - end it "keeps the special onebox styles" do strip_and_inline expect(@frag.to_s).to include("cid:email/test.png") - expect(@frag.css('[data-stripped-secure-upload]')).not_to be_present - expect(@frag.css('[data-embedded-secure-image]')[0].attr('style')).to eq('width: 20px; height: 20px; float: none; vertical-align: middle; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;') + expect(@frag.css("[data-stripped-secure-upload]")).not_to be_present + expect(@frag.css("[data-embedded-secure-image]")[0].attr("style")).to eq( + "width: 20px; height: 20px; float: none; vertical-align: middle; max-height: 80%; max-width: 20%; height: auto; float: left; margin-right: 10px;", + ) end end end diff --git a/spec/lib/email_cook_spec.rb b/spec/lib/email_cook_spec.rb index 19ac9c4f38..3d1f025d87 100644 --- a/spec/lib/email_cook_spec.rb +++ b/spec/lib/email_cook_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'email_cook' -require 'pretty_text' +require "email_cook" +require "pretty_text" RSpec.describe EmailCook do it "uses to PrettyText when there is no [plaintext] in raw" do @@ -109,22 +109,30 @@ RSpec.describe EmailCook do it "creates oneboxed link when the line contains only a link" do raw = plaintext("https://www.eviltrout.com") - expect(cook(raw)).to eq('https://www.eviltrout.com
    ') + expect(cook(raw)).to eq( + 'https://www.eviltrout.com
    ', + ) end it "autolinks without the beginning of a line" do raw = plaintext("my site: https://www.eviltrout.com") - expect(cook(raw)).to eq('my site: https://www.eviltrout.com
    ') + expect(cook(raw)).to eq( + 'my site: https://www.eviltrout.com
    ', + ) end it "autolinks without the end of a line" do raw = plaintext("https://www.eviltrout.com is my site") - expect(cook(raw)).to eq('https://www.eviltrout.com is my site
    ') + expect(cook(raw)).to eq( + 'https://www.eviltrout.com is my site
    ', + ) end it "links even within a quote" do raw = plaintext("> https://www.eviltrout.com is my site") - expect(cook(raw)).to eq('
    https://www.eviltrout.com is my site
    ') + expect(cook(raw)).to eq( + '
    https://www.eviltrout.com is my site
    ', + ) end it "it works and does not interpret Markdown in plaintext and elided" do diff --git a/spec/lib/email_updater_spec.rb b/spec/lib/email_updater_spec.rb index a7afd76979..f96c57e85d 100644 --- a/spec/lib/email_updater_spec.rb +++ b/spec/lib/email_updater_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true RSpec.describe EmailUpdater do - let(:old_email) { 'old.email@example.com' } - let(:new_email) { 'new.email@example.com' } + let(:old_email) { "old.email@example.com" } + let(:new_email) { "new.email@example.com" } it "provides better error message when a staged user has the same email" do Fabricate(:user, staged: true, email: new_email) @@ -29,26 +29,39 @@ RSpec.describe EmailUpdater do let(:updater) { EmailUpdater.new(guardian: admin.guardian, user: user) } def expect_old_email_job - expect_enqueued_with(job: :critical_user_email, args: { to_address: old_email, type: :notify_old_email, user_id: user.id }) do - yield - end + expect_enqueued_with( + job: :critical_user_email, + args: { + to_address: old_email, + type: :notify_old_email, + user_id: user.id, + }, + ) { yield } end context "for a regular user" do let(:user) { Fabricate(:user, email: old_email) } it "sends an email to the user for them to confirm the email change" do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_new_email, + to_address: new_email, + }, + ) { updater.change_to(new_email) } end it "sends an email to confirm old email first if require_change_email_confirmation is enabled" do SiteSetting.require_change_email_confirmation = true - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_old_email, + to_address: old_email, + }, + ) { updater.change_to(new_email) } expect(updater.change_req).to be_present expect(updater.change_req.old_email).to eq(old_email) @@ -81,9 +94,13 @@ RSpec.describe EmailUpdater do let(:user) { Fabricate(:moderator, email: old_email) } before do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_old_email, + to_address: old_email, + }, + ) { updater.change_to(new_email) } end it "starts the old confirmation process" do @@ -109,9 +126,13 @@ RSpec.describe EmailUpdater do before do admin.update(email: old_email) - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_old_email, + to_address: old_email, + }, + ) { updater.change_to(new_email) } end it "logs the user as the requester" do @@ -137,15 +158,19 @@ RSpec.describe EmailUpdater do end end - context 'as a regular user' do + context "as a regular user" do let(:user) { Fabricate(:user, email: old_email) } let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) } context "when changing primary email" do before do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_new_email, + to_address: new_email, + }, + ) { updater.change_to(new_email) } end it "starts the new confirmation process" do @@ -160,21 +185,28 @@ RSpec.describe EmailUpdater do expect(updater.change_req.new_email_token.email).to eq(new_email) end - context 'when confirming an invalid token' do + context "when confirming an invalid token" do it "produces an error" do - updater.confirm('random') + updater.confirm("random") expect(updater.errors).to be_present expect(user.reload.email).not_to eq(new_email) end end - context 'when confirming a valid token' do + context "when confirming a valid token" do it "updates the user's email" do - event = DiscourseEvent.track_events { - expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do - updater.confirm(updater.change_req.new_email_token.token) - end - }.last + event = + DiscourseEvent + .track_events do + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :notify_old_email, + to_address: old_email, + }, + ) { updater.confirm(updater.change_req.new_email_token.token) } + end + .last expect(updater.errors).to be_blank expect(user.reload.email).to eq(new_email) @@ -190,23 +222,42 @@ RSpec.describe EmailUpdater do context "when adding an email" do before do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do - updater.change_to(new_email, add: true) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_new_email, + to_address: new_email, + }, + ) { updater.change_to(new_email, add: true) } end - context 'when confirming a valid token' do + context "when confirming a valid token" do it "adds a user email" do - expect(UserHistory.where(action: UserHistory.actions[:add_email], acting_user_id: user.id).last).to be_present + expect( + UserHistory.where( + action: UserHistory.actions[:add_email], + acting_user_id: user.id, + ).last, + ).to be_present - event = DiscourseEvent.track_events { - expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do - updater.confirm(updater.change_req.new_email_token.token) - end - }.last + event = + DiscourseEvent + .track_events do + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :notify_old_email_add, + to_address: old_email, + }, + ) { updater.confirm(updater.change_req.new_email_token.token) } + end + .last expect(updater.errors).to be_blank - expect(UserEmail.where(user_id: user.id).pluck(:email)).to contain_exactly(user.email, new_email) + expect(UserEmail.where(user_id: user.id).pluck(:email)).to contain_exactly( + user.email, + new_email, + ) expect(event[:event_name]).to eq(:user_updated) expect(event[:params].first).to eq(user) @@ -216,24 +267,36 @@ RSpec.describe EmailUpdater do end end - context 'when it was deleted before' do - it 'works' do - expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do - updater.confirm(updater.change_req.new_email_token.token) - end + context "when it was deleted before" do + it "works" do + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :notify_old_email_add, + to_address: old_email, + }, + ) { updater.confirm(updater.change_req.new_email_token.token) } expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email) user.user_emails.where(email: new_email).delete_all expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email) - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do - updater.change_to(new_email, add: true) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_new_email, + to_address: new_email, + }, + ) { updater.change_to(new_email, add: true) } - expect_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email_add, to_address: old_email }) do - updater.confirm(updater.change_req.new_email_token.token) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :notify_old_email_add, + to_address: old_email, + }, + ) { updater.confirm(updater.change_req.new_email_token.token) } expect(user.reload.user_emails.pluck(:email)).to contain_exactly(old_email, new_email) end @@ -253,19 +316,25 @@ RSpec.describe EmailUpdater do it "max secondary_emails limit reached" do updater.change_to(new_email, add: true) expect(updater.errors).to be_present - expect(updater.errors.messages[:base].first).to be I18n.t("change_email.max_secondary_emails_error") + expect(updater.errors.messages[:base].first).to be I18n.t( + "change_email.max_secondary_emails_error", + ) end end end - context 'as a staff user' do + context "as a staff user" do let(:user) { Fabricate(:moderator, email: old_email) } let(:updater) { EmailUpdater.new(guardian: user.guardian, user: user) } before do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_old_email, to_address: old_email }) do - updater.change_to(new_email) - end + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_old_email, + to_address: old_email, + }, + ) { updater.change_to(new_email) } end it "starts the old confirmation process" do @@ -280,17 +349,23 @@ RSpec.describe EmailUpdater do expect(updater.change_req.new_email_token).to be_blank end - context 'when confirming an invalid token' do + context "when confirming an invalid token" do it "produces an error" do - updater.confirm('random') + updater.confirm("random") expect(updater.errors).to be_present expect(user.reload.email).not_to eq(new_email) end end - context 'when confirming a valid token' do + context "when confirming a valid token" do before do - expect_enqueued_with(job: :critical_user_email, args: { type: :confirm_new_email, to_address: new_email }) do + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :confirm_new_email, + to_address: new_email, + }, + ) do @old_token = updater.change_req.old_email_token.token updater.confirm(@old_token) end @@ -316,9 +391,13 @@ RSpec.describe EmailUpdater do context "when completing the new update process" do before do - expect_not_enqueued_with(job: :critical_user_email, args: { type: :notify_old_email, to_address: old_email }) do - updater.confirm(updater.change_req.new_email_token.token) - end + expect_not_enqueued_with( + job: :critical_user_email, + args: { + type: :notify_old_email, + to_address: old_email, + }, + ) { updater.confirm(updater.change_req.new_email_token.token) } end it "updates the user's email" do @@ -332,10 +411,8 @@ RSpec.describe EmailUpdater do end end - context 'when hide_email_address_taken is enabled' do - before do - SiteSetting.hide_email_address_taken = true - end + context "when hide_email_address_taken is enabled" do + before { SiteSetting.hide_email_address_taken = true } let(:user) { Fabricate(:user, email: old_email) } let(:existing) { Fabricate(:user, email: new_email) } @@ -347,10 +424,14 @@ RSpec.describe EmailUpdater do expect(user.email_change_requests).to be_empty end - it 'sends an email to the owner of the account with the new email' do - expect_enqueued_with(job: :critical_user_email, args: { type: :account_exists, user_id: existing.id }) do - updater.change_to(existing.email) - end + it "sends an email to the owner of the account with the new email" do + expect_enqueued_with( + job: :critical_user_email, + args: { + type: :account_exists, + user_id: existing.id, + }, + ) { updater.change_to(existing.email) } end end end diff --git a/spec/lib/encodings_spec.rb b/spec/lib/encodings_spec.rb index bf3a5e0f02..70aac08e7e 100644 --- a/spec/lib/encodings_spec.rb +++ b/spec/lib/encodings_spec.rb @@ -7,26 +7,30 @@ RSpec.describe Encodings do end describe "unicode" do - let(:expected) { 'Το σύστημα γραφής είναι ένα συμβολικό, οπτικό σύστημα καταγραφής της γλώσσας.' } + let(:expected) do + "Το σύστημα γραφής είναι ένα συμβολικό, οπτικό σύστημα καταγραφής της γλώσσας." + end it "correctly encodes UTF-8 as UTF-8" do - expect(to_utf8('utf-8.txt')).to eq(expected) + expect(to_utf8("utf-8.txt")).to eq(expected) end it "correctly encodes UTF-8 with BOM as UTF-8" do - expect(to_utf8('utf-8-bom.txt')).to eq(expected) + expect(to_utf8("utf-8-bom.txt")).to eq(expected) end it "correctly encodes UTF-16LE with BOM as UTF-8" do - expect(to_utf8('utf-16le.txt')).to eq(expected) + expect(to_utf8("utf-16le.txt")).to eq(expected) end it "correctly encodes UTF-16BE with BOM as UTF-8" do - expect(to_utf8('utf-16be.txt')).to eq(expected) + expect(to_utf8("utf-16be.txt")).to eq(expected) end end it "correctly encodes ISO-8859-5 as UTF-8" do - expect(to_utf8('iso-8859-5.txt')).to eq('Письменность отличается от других существующих или возможных систем символической коммуникации тем, что всегда ассоциируется с некоторым языком и устной речью на этом языке') + expect(to_utf8("iso-8859-5.txt")).to eq( + "Письменность отличается от других существующих или возможных систем символической коммуникации тем, что всегда ассоциируется с некоторым языком и устной речью на этом языке", + ) end end diff --git a/spec/lib/enum_spec.rb b/spec/lib/enum_spec.rb index e0c1f51b35..7de14a51ba 100644 --- a/spec/lib/enum_spec.rb +++ b/spec/lib/enum_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'email' +require "email" RSpec.describe Enum do let(:array_enum) { Enum.new(:jake, :finn, :princess_bubblegum, :peppermint_butler) } diff --git a/spec/lib/excerpt_parser_spec.rb b/spec/lib/excerpt_parser_spec.rb index cdc7adc0c8..5a6e494df0 100644 --- a/spec/lib/excerpt_parser_spec.rb +++ b/spec/lib/excerpt_parser_spec.rb @@ -3,7 +3,6 @@ require "excerpt_parser" RSpec.describe ExcerptParser do - it "handles nested
    blocks" do html = <<~HTML.strip
    @@ -22,31 +21,48 @@ RSpec.describe ExcerptParser do Lorem ipsum dolor sit amet, consectetur adi…
    HTML - expect(ExcerptParser.get_excerpt(html, 6, {})).to match_html('
    FOOBAR…
    ') - expect(ExcerptParser.get_excerpt(html, 3, {})).to match_html('
    FOO
    ') + expect(ExcerptParser.get_excerpt(html, 6, {})).to match_html( + "
    FOOBAR…
    ", + ) + expect(ExcerptParser.get_excerpt(html, 3, {})).to match_html( + '
    FOO
    ', + ) end it "respects length parameter for
    block" do - html = '
    foo

    bar

    ' - expect(ExcerptParser.get_excerpt(html, 100, {})).to match_html('
    foobar
    ') - expect(ExcerptParser.get_excerpt(html, 5, {})).to match_html('
    fooba…
    ') - expect(ExcerptParser.get_excerpt(html, 3, {})).to match_html('
    foo
    ') - expect(ExcerptParser.get_excerpt(html, 2, {})).to match_html('
    fo…
    ') + html = "
    foo

    bar

    " + expect(ExcerptParser.get_excerpt(html, 100, {})).to match_html( + "
    foobar
    ", + ) + expect(ExcerptParser.get_excerpt(html, 5, {})).to match_html( + "
    fooba…
    ", + ) + expect(ExcerptParser.get_excerpt(html, 3, {})).to match_html( + '
    foo
    ', + ) + expect(ExcerptParser.get_excerpt(html, 2, {})).to match_html( + '
    fo…
    ', + ) end it "allows with inside for icons when keep_svg is true" do html = '' - expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html('') - expect(ExcerptParser.get_excerpt(html, 100, {})).to match_html('') + expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html( + '', + ) + expect(ExcerptParser.get_excerpt(html, 100, {})).to match_html("") html = '' - expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html('') + expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html("") html = '' - expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html('') + expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html("") - html = '' - expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html('') + html = + '' + expect(ExcerptParser.get_excerpt(html, 100, { keep_svg: true })).to match_html( + '', + ) end describe "keep_onebox_body parameter" do @@ -105,7 +121,9 @@ RSpec.describe ExcerptParser do
    HTML - expect(ExcerptParser.get_excerpt(html, 100, keep_quotes: true)).to eq("This is a quoted text.") + expect(ExcerptParser.get_excerpt(html, 100, keep_quotes: true)).to eq( + "This is a quoted text.", + ) end end end diff --git a/spec/lib/feed_element_installer_spec.rb b/spec/lib/feed_element_installer_spec.rb index 4e327d41d0..3239b5befd 100644 --- a/spec/lib/feed_element_installer_spec.rb +++ b/spec/lib/feed_element_installer_spec.rb @@ -1,37 +1,37 @@ # frozen_string_literal: true -require 'feed_element_installer' +require "feed_element_installer" RSpec.describe FeedElementInstaller do - describe '#install_rss_element' do - let(:raw_feed) { file_from_fixtures('feed.rss', 'feed').read } + describe "#install_rss_element" do + let(:raw_feed) { file_from_fixtures("feed.rss", "feed").read } - it 'creates parsing for a non-standard, namespaced element' do - FeedElementInstaller.install('discourse:username', raw_feed) + it "creates parsing for a non-standard, namespaced element" do + FeedElementInstaller.install("discourse:username", raw_feed) feed = RSS::Parser.parse(raw_feed) - expect(feed.items.first.discourse_username).to eq('xrav3nz') + expect(feed.items.first.discourse_username).to eq("xrav3nz") end - it 'does not create parsing for a non-standard, non-namespaced element' do - FeedElementInstaller.install('username', raw_feed) + it "does not create parsing for a non-standard, non-namespaced element" do + FeedElementInstaller.install("username", raw_feed) feed = RSS::Parser.parse(raw_feed) expect { feed.items.first.username }.to raise_error(NoMethodError) end end - describe '#install_atom_element' do - let(:raw_feed) { file_from_fixtures('feed.atom', 'feed').read } + describe "#install_atom_element" do + let(:raw_feed) { file_from_fixtures("feed.atom", "feed").read } - it 'creates parsing for a non-standard, namespaced element' do - FeedElementInstaller.install('discourse:username', raw_feed) + it "creates parsing for a non-standard, namespaced element" do + FeedElementInstaller.install("discourse:username", raw_feed) feed = RSS::Parser.parse(raw_feed) - expect(feed.items.first.discourse_username).to eq('xrav3nz') + expect(feed.items.first.discourse_username).to eq("xrav3nz") end - it 'does not create parsing for a non-standard, non-namespaced element' do - FeedElementInstaller.install('username', raw_feed) + it "does not create parsing for a non-standard, non-namespaced element" do + FeedElementInstaller.install("username", raw_feed) feed = RSS::Parser.parse(raw_feed) expect { feed.items.first.username }.to raise_error(NoMethodError) diff --git a/spec/lib/feed_item_accessor_spec.rb b/spec/lib/feed_item_accessor_spec.rb index c86baacecc..bbaaeb7d26 100644 --- a/spec/lib/feed_item_accessor_spec.rb +++ b/spec/lib/feed_item_accessor_spec.rb @@ -1,44 +1,46 @@ # frozen_string_literal: true -require 'rss' -require 'feed_item_accessor' +require "rss" +require "feed_item_accessor" RSpec.describe FeedItemAccessor do - context 'for ATOM feed' do - let(:atom_feed) { RSS::Parser.parse(file_from_fixtures('feed.atom', 'feed'), false) } + context "for ATOM feed" do + let(:atom_feed) { RSS::Parser.parse(file_from_fixtures("feed.atom", "feed"), false) } let(:atom_feed_item) { atom_feed.items.first } let(:item_accessor) { FeedItemAccessor.new(atom_feed_item) } - describe '#element_content' do - it { expect(item_accessor.element_content('title')).to eq(atom_feed_item.title.content) } + describe "#element_content" do + it { expect(item_accessor.element_content("title")).to eq(atom_feed_item.title.content) } end - describe '#link' do + describe "#link" do it { expect(item_accessor.link).to eq(atom_feed_item.link.href) } end end - context 'for RSS feed' do - let(:rss_feed) { RSS::Parser.parse(file_from_fixtures('feed.rss', 'feed'), false) } + context "for RSS feed" do + let(:rss_feed) { RSS::Parser.parse(file_from_fixtures("feed.rss", "feed"), false) } let(:rss_feed_item) { rss_feed.items.first } let(:item_accessor) { FeedItemAccessor.new(rss_feed_item) } - describe '#element_content' do - it { expect(item_accessor.element_content('title')).to eq(rss_feed_item.title) } + describe "#element_content" do + it { expect(item_accessor.element_content("title")).to eq(rss_feed_item.title) } end - describe '#link' do + describe "#link" do it { expect(item_accessor.link).to eq(rss_feed_item.link) } end end - context 'with multiple links' do - let(:rss_feed) { RSS::Parser.parse(file_from_fixtures('multiple-links.atom', 'feed'), false) } + context "with multiple links" do + let(:rss_feed) { RSS::Parser.parse(file_from_fixtures("multiple-links.atom", "feed"), false) } let(:rss_feed_item) { rss_feed.items.first } let(:item_accessor) { FeedItemAccessor.new(rss_feed_item) } - describe '#link' do - it 'gets the web page link' do - expect(item_accessor.link).to eq('http://workspaceupdates.googleblog.com/2022/01/improved-editing-experience-in-google.html') + describe "#link" do + it "gets the web page link" do + expect(item_accessor.link).to eq( + "http://workspaceupdates.googleblog.com/2022/01/improved-editing-experience-in-google.html", + ) end end end diff --git a/spec/lib/file_helper_spec.rb b/spec/lib/file_helper_spec.rb index e8bc0c3ef5..4337ae31bc 100644 --- a/spec/lib/file_helper_spec.rb +++ b/spec/lib/file_helper_spec.rb @@ -1,14 +1,13 @@ # frozen_string_literal: true -require 'file_helper' +require "file_helper" RSpec.describe FileHelper do - let(:url) { "https://eviltrout.com/trout.png" } let(:png) { File.read("#{Rails.root}/spec/fixtures/images/cropped.png") } before do - stub_request(:any, /https:\/\/eviltrout.com/) + stub_request(:any, %r{https://eviltrout.com}) stub_request(:get, url).to_return(body: png) end @@ -21,9 +20,9 @@ RSpec.describe FileHelper do begin FileHelper.download( url, - max_file_size: 10000, - tmp_file_name: 'trouttmp', - follow_redirect: true + max_file_size: 10_000, + tmp_file_name: "trouttmp", + follow_redirect: true, ) rescue => e expect(e.io.status[0]).to eq("404") @@ -36,12 +35,13 @@ RSpec.describe FileHelper do url2 = "https://test.com/image.png" stub_request(:get, url).to_return(status: 302, body: "", headers: { location: url2 }) - missing = FileHelper.download( - url, - max_file_size: 10000, - tmp_file_name: 'trouttmp', - follow_redirect: false - ) + missing = + FileHelper.download( + url, + max_file_size: 10_000, + tmp_file_name: "trouttmp", + follow_redirect: false, + ) expect(missing).to eq(nil) end @@ -52,12 +52,13 @@ RSpec.describe FileHelper do stub_request(:get, url2).to_return(status: 200, body: "i am the body") begin - found = FileHelper.download( - url, - max_file_size: 10000, - tmp_file_name: 'trouttmp', - follow_redirect: true - ) + found = + FileHelper.download( + url, + max_file_size: 10_000, + tmp_file_name: "trouttmp", + follow_redirect: true, + ) expect(found.read).to eq("i am the body") ensure @@ -75,9 +76,9 @@ RSpec.describe FileHelper do begin FileHelper.download( url, - max_file_size: 10000, - tmp_file_name: 'trouttmp', - follow_redirect: false + max_file_size: 10_000, + tmp_file_name: "trouttmp", + follow_redirect: false, ) rescue => e expect(e.io.status[0]).to eq("404") @@ -88,11 +89,7 @@ RSpec.describe FileHelper do it "returns a file with the image" do begin - tmpfile = FileHelper.download( - url, - max_file_size: 10000, - tmp_file_name: 'trouttmp' - ) + tmpfile = FileHelper.download(url, max_file_size: 10_000, tmp_file_name: "trouttmp") expect(Base64.encode64(tmpfile.read)).to eq(Base64.encode64(png)) ensure @@ -103,11 +100,12 @@ RSpec.describe FileHelper do it "works with a protocol relative url" do begin - tmpfile = FileHelper.download( - "//eviltrout.com/trout.png", - max_file_size: 10000, - tmp_file_name: 'trouttmp' - ) + tmpfile = + FileHelper.download( + "//eviltrout.com/trout.png", + max_file_size: 10_000, + tmp_file_name: "trouttmp", + ) expect(Base64.encode64(tmpfile.read)).to eq(Base64.encode64(png)) ensure @@ -116,25 +114,27 @@ RSpec.describe FileHelper do end end - describe 'when max_file_size is exceeded' do - it 'should return nil' do - tmpfile = FileHelper.download( - "//eviltrout.com/trout.png", - max_file_size: 1, - tmp_file_name: 'trouttmp' - ) + describe "when max_file_size is exceeded" do + it "should return nil" do + tmpfile = + FileHelper.download( + "//eviltrout.com/trout.png", + max_file_size: 1, + tmp_file_name: "trouttmp", + ) expect(tmpfile).to eq(nil) end - it 'is able to retain the tmpfile' do + it "is able to retain the tmpfile" do begin - tmpfile = FileHelper.download( - "//eviltrout.com/trout.png", - max_file_size: 1, - tmp_file_name: 'trouttmp', - retain_on_max_file_size_exceeded: true - ) + tmpfile = + FileHelper.download( + "//eviltrout.com/trout.png", + max_file_size: 1, + tmp_file_name: "trouttmp", + retain_on_max_file_size_exceeded: true, + ) expect(tmpfile.closed?).to eq(false) ensure @@ -144,22 +144,16 @@ RSpec.describe FileHelper do end end - describe 'when url is a jpeg' do + describe "when url is a jpeg" do let(:url) { "https://eviltrout.com/trout.jpg" } it "should prioritize the content type returned by the response" do begin - stub_request(:get, url).to_return(body: png, headers: { - "content-type": "image/png" - }) + stub_request(:get, url).to_return(body: png, headers: { "content-type": "image/png" }) - tmpfile = FileHelper.download( - url, - max_file_size: 10000, - tmp_file_name: 'trouttmp' - ) + tmpfile = FileHelper.download(url, max_file_size: 10_000, tmp_file_name: "trouttmp") - expect(File.extname(tmpfile)).to eq('.png') + expect(File.extname(tmpfile)).to eq(".png") ensure tmpfile&.close tmpfile&.unlink @@ -167,5 +161,4 @@ RSpec.describe FileHelper do end end end - end diff --git a/spec/lib/file_store/base_store_spec.rb b/spec/lib/file_store/base_store_spec.rb index 18bf38d08c..0214ebdbc6 100644 --- a/spec/lib/file_store/base_store_spec.rb +++ b/spec/lib/file_store/base_store_spec.rb @@ -3,34 +3,32 @@ RSpec.describe FileStore::BaseStore do fab!(:upload) do Upload.delete(9999) # In case of any collisions - Fabricate(:upload, id: 9999, sha1: Digest::SHA1.hexdigest('9999')) + Fabricate(:upload, id: 9999, sha1: Digest::SHA1.hexdigest("9999")) end - describe '#get_path_for_upload' do + describe "#get_path_for_upload" do def expect_correct_path(expected_path) expect(described_class.new.get_path_for_upload(upload)).to eq(expected_path) end context "with empty URL" do - before do - upload.update!(url: "") + before { upload.update!(url: "") } + + it "should return the right path" do + expect_correct_path("original/2X/4/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png") end - it 'should return the right path' do - expect_correct_path('original/2X/4/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png') - end - - describe 'when Upload#extension has not been set' do - it 'should return the right path' do + describe "when Upload#extension has not been set" do + it "should return the right path" do upload.update!(extension: nil) - expect_correct_path('original/2X/4/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png') + expect_correct_path("original/2X/4/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png") end end - describe 'when id is negative' do - it 'should return the right depth' do + describe "when id is negative" do + it "should return the right depth" do upload.update!(id: -999) - expect_correct_path('original/1X/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png') + expect_correct_path("original/1X/4170ac2a2782a1516fe9e13d7322ae482c1bd594.png") end end end @@ -38,63 +36,92 @@ RSpec.describe FileStore::BaseStore do context "with existing URL" do context "with regular site" do it "returns the correct path for files stored on local storage" do - upload.update!(url: "/uploads/default/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: "/uploads/default/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") - upload.update!(url: "/uploads/default/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: "/uploads/default/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") end it "returns the correct path for files stored on S3" do - upload.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") - upload.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") end end context "with multisite" do it "returns the correct path for files stored on local storage" do - upload.update!(url: "/uploads/foo/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: "/uploads/foo/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") - upload.update!(url: "/uploads/foo/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: "/uploads/foo/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") end it "returns the correct path for files stored on S3" do - upload.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") - upload.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/3X/63/63b76551662ccea1a594e161c37dd35188d77657.jpeg") end it "returns the correct path when the site name is 'original'" do - upload.update!(url: "/uploads/original/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: "/uploads/original/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") - upload.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/original/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") + upload.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/original/original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg", + ) expect_correct_path("original/1X/63b76551662ccea1a594e161c37dd35188d77657.jpeg") end end end end - describe '#get_path_for_optimized_image' do + describe "#get_path_for_optimized_image" do let!(:upload) { Fabricate.build(:upload, id: 100) } let!(:optimized_path) { "optimized/1X/#{upload.sha1}_1_100x200.png" } context "with empty URL" do - it 'should return the right path' do + it "should return the right path" do optimized = Fabricate.build(:optimized_image, upload: upload, version: 1) - expect(FileStore::BaseStore.new.get_path_for_optimized_image(optimized)).to eq(optimized_path) + expect(FileStore::BaseStore.new.get_path_for_optimized_image(optimized)).to eq( + optimized_path, + ) end - it 'should return the right path for `nil` version' do + it "should return the right path for `nil` version" do optimized = Fabricate.build(:optimized_image, upload: upload, version: nil) - expect(FileStore::BaseStore.new.get_path_for_optimized_image(optimized)).to eq(optimized_path) + expect(FileStore::BaseStore.new.get_path_for_optimized_image(optimized)).to eq( + optimized_path, + ) end end @@ -113,7 +140,10 @@ RSpec.describe FileStore::BaseStore do end it "returns the correct path for files stored on S3" do - optimized.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/optimized/1X/#{upload.sha1}_1_100x200.jpg") + optimized.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/optimized/1X/#{upload.sha1}_1_100x200.jpg", + ) expect_correct_optimized_path end end @@ -125,7 +155,10 @@ RSpec.describe FileStore::BaseStore do end it "returns the correct path for files stored on S3" do - optimized.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/optimized/1X/#{upload.sha1}_1_100x200.jpg") + optimized.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/foo/optimized/1X/#{upload.sha1}_1_100x200.jpg", + ) expect_correct_optimized_path end @@ -133,14 +166,17 @@ RSpec.describe FileStore::BaseStore do optimized.update!(url: "/uploads/optimized/optimized/1X/#{upload.sha1}_1_100x200.jpg") expect_correct_optimized_path - optimized.update!(url: "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/optimized/optimized/1X/#{upload.sha1}_1_100x200.jpg") + optimized.update!( + url: + "//bucket-name.s3.dualstack.us-west-2.amazonaws.com/uploads/optimized/optimized/1X/#{upload.sha1}_1_100x200.jpg", + ) expect_correct_optimized_path end end end end - describe '#download' do + describe "#download" do before do setup_s3 stub_request(:get, upload_s3.url).to_return(status: 200, body: "Hello world") @@ -169,7 +205,10 @@ RSpec.describe FileStore::BaseStore do it "should return the file when s3 cdn enabled" do SiteSetting.s3_cdn_url = "https://cdn.s3.#{SiteSetting.s3_region}.amazonaws.com" - stub_request(:get, Discourse.store.cdn_url(upload_s3.url)).to_return(status: 200, body: "Hello world") + stub_request(:get, Discourse.store.cdn_url(upload_s3.url)).to_return( + status: 200, + body: "Hello world", + ) file = store.download(upload_s3) diff --git a/spec/lib/file_store/local_store_spec.rb b/spec/lib/file_store/local_store_spec.rb index 0ed98ef01e..d020e13022 100644 --- a/spec/lib/file_store/local_store_spec.rb +++ b/spec/lib/file_store/local_store_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'file_store/local_store' +require "file_store/local_store" RSpec.describe FileStore::LocalStore do - let(:store) { FileStore::LocalStore.new } fab!(:upload) { Fabricate(:upload) } @@ -13,25 +12,24 @@ RSpec.describe FileStore::LocalStore do fab!(:optimized_image) { Fabricate(:optimized_image) } describe "#store_upload" do - it "returns a relative url" do store.expects(:copy_file) - expect(store.store_upload(uploaded_file, upload)).to match(/\/#{upload_path}\/original\/.+#{upload.sha1}\.png/) + expect(store.store_upload(uploaded_file, upload)).to match( + %r{/#{upload_path}/original/.+#{upload.sha1}\.png}, + ) end - end describe "#store_optimized_image" do - it "returns a relative url" do store.expects(:copy_file) - expect(store.store_optimized_image({}, optimized_image)).to match(/\/#{upload_path}\/optimized\/.+#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png/) + expect(store.store_optimized_image({}, optimized_image)).to match( + %r{/#{upload_path}/optimized/.+#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png}, + ) end - end describe "#remove_upload" do - it "does not delete non uploaded" do FileUtils.expects(:mkdir_p).never store.remove_upload(upload) @@ -39,10 +37,10 @@ RSpec.describe FileStore::LocalStore do it "moves the file to the tombstone" do begin - upload = UploadCreator.new( - file_from_fixtures("smallest.png"), - "smallest.png" - ).create_for(Fabricate(:user).id) + upload = + UploadCreator.new(file_from_fixtures("smallest.png"), "smallest.png").create_for( + Fabricate(:user).id, + ) path = store.path_for(upload) mtime = File.mtime(path) @@ -54,21 +52,18 @@ RSpec.describe FileStore::LocalStore do expect(File.exist?(tombstone_path)).to eq(true) expect(File.mtime(tombstone_path)).to_not eq(mtime) ensure - [path, tombstone_path].each do |file_path| - File.delete(file_path) if File.exist?(file_path) - end + [path, tombstone_path].each { |file_path| File.delete(file_path) if File.exist?(file_path) } end end - end describe "#remove_optimized_image" do it "moves the file to the tombstone" do begin - upload = UploadCreator.new( - file_from_fixtures("smallest.png"), - "smallest.png" - ).create_for(Fabricate(:user).id) + upload = + UploadCreator.new(file_from_fixtures("smallest.png"), "smallest.png").create_for( + Fabricate(:user).id, + ) upload.create_thumbnail!(1, 1) upload.reload @@ -81,41 +76,47 @@ RSpec.describe FileStore::LocalStore do expect(File.exist?(tombstone_path)).to eq(true) ensure - [path, tombstone_path].each do |file_path| - File.delete(file_path) if File.exist?(file_path) - end + [path, tombstone_path].each { |file_path| File.delete(file_path) if File.exist?(file_path) } end end - end describe "#has_been_uploaded?" do - it "identifies relatives urls" do expect(store.has_been_uploaded?("/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(true) end it "identifies local urls" do Discourse.stubs(:base_url_no_prefix).returns("http://discuss.site.com") - expect(store.has_been_uploaded?("http://discuss.site.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(true) - expect(store.has_been_uploaded?("//discuss.site.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(true) + expect( + store.has_been_uploaded?("http://discuss.site.com/#{upload_path}/42/0123456789ABCDEF.jpg"), + ).to eq(true) + expect( + store.has_been_uploaded?("//discuss.site.com/#{upload_path}/42/0123456789ABCDEF.jpg"), + ).to eq(true) end it "identifies local urls when using a CDN" do Rails.configuration.action_controller.stubs(:asset_host).returns("http://my.cdn.com") - expect(store.has_been_uploaded?("http://my.cdn.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(true) - expect(store.has_been_uploaded?("//my.cdn.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(true) + expect( + store.has_been_uploaded?("http://my.cdn.com/#{upload_path}/42/0123456789ABCDEF.jpg"), + ).to eq(true) + expect(store.has_been_uploaded?("//my.cdn.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq( + true, + ) end it "does not match dummy urls" do - expect(store.has_been_uploaded?("http://domain.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(false) - expect(store.has_been_uploaded?("//domain.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq(false) + expect( + store.has_been_uploaded?("http://domain.com/#{upload_path}/42/0123456789ABCDEF.jpg"), + ).to eq(false) + expect(store.has_been_uploaded?("//domain.com/#{upload_path}/42/0123456789ABCDEF.jpg")).to eq( + false, + ) end - end describe "#absolute_base_url" do - it "is present" do expect(store.absolute_base_url).to eq("http://test.localhost/#{upload_path}") end @@ -124,11 +125,9 @@ RSpec.describe FileStore::LocalStore do set_subfolder "/forum" expect(store.absolute_base_url).to eq("http://test.localhost/forum/#{upload_path}") end - end describe "#relative_base_url" do - it "is present" do expect(store.relative_base_url).to eq("/#{upload_path}") end @@ -137,7 +136,6 @@ RSpec.describe FileStore::LocalStore do set_subfolder "/forum" expect(store.relative_base_url).to eq("/forum/#{upload_path}") end - end it "is internal" do @@ -147,22 +145,25 @@ RSpec.describe FileStore::LocalStore do describe "#get_path_for" do it "returns the correct path" do - expect(store.get_path_for("original", upload.id, upload.sha1, ".#{upload.extension}")) - .to match(%r|/#{upload_path}/original/.+#{upload.sha1}\.png|) + expect( + store.get_path_for("original", upload.id, upload.sha1, ".#{upload.extension}"), + ).to match(%r{/#{upload_path}/original/.+#{upload.sha1}\.png}) end end describe "#get_path_for_upload" do it "returns the correct path" do - expect(store.get_path_for_upload(upload)) - .to match(%r|/#{upload_path}/original/.+#{upload.sha1}\.png|) + expect(store.get_path_for_upload(upload)).to match( + %r{/#{upload_path}/original/.+#{upload.sha1}\.png}, + ) end end describe "#get_path_for_optimized_image" do it "returns the correct path" do - expect(store.get_path_for_optimized_image(optimized_image)) - .to match(%r|/#{upload_path}/optimized/.+#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png|) + expect(store.get_path_for_optimized_image(optimized_image)).to match( + %r{/#{upload_path}/optimized/.+#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png}, + ) end end end diff --git a/spec/lib/file_store/s3_store_spec.rb b/spec/lib/file_store/s3_store_spec.rb index 0473c5ab19..856ff53e68 100644 --- a/spec/lib/file_store/s3_store_spec.rb +++ b/spec/lib/file_store/s3_store_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'file_store/s3_store' -require 'file_store/local_store' +require "file_store/s3_store" +require "file_store/local_store" RSpec.describe FileStore::S3Store do let(:store) { FileStore::S3Store.new } @@ -15,31 +15,37 @@ RSpec.describe FileStore::S3Store do fab!(:optimized_image) { Fabricate(:optimized_image) } let(:optimized_image_file) { file_from_fixtures("logo.png") } let(:uploaded_file) { file_from_fixtures("logo.png") } - fab!(:upload) do - Fabricate(:upload, sha1: Digest::SHA1.hexdigest('secret image string')) - end + fab!(:upload) { Fabricate(:upload, sha1: Digest::SHA1.hexdigest("secret image string")) } before do setup_s3 - SiteSetting.s3_region = 'us-west-1' + SiteSetting.s3_region = "us-west-1" end - describe 'uploading to s3' do + describe "uploading to s3" do let(:etag) { "etag" } describe "#store_upload" do it "returns an absolute schemaless url" do s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})).returns(s3_object) - s3_object.expects(:put).with({ - acl: "public-read", - cache_control: "max-age=31556952, public, immutable", - content_type: "image/png", - body: uploaded_file - }).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})) + .returns(s3_object) + s3_object + .expects(:put) + .with( + { + acl: "public-read", + cache_control: "max-age=31556952, public, immutable", + content_type: "image/png", + body: uploaded_file, + }, + ) + .returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) expect(store.store_upload(uploaded_file, upload)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png}, ) expect(upload.etag).to eq(etag) end @@ -53,10 +59,13 @@ RSpec.describe FileStore::S3Store do it "returns an absolute schemaless url" do s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_bucket.expects(:object).with(regexp_matches(%r{discourse-uploads/original/\d+X.*/#{upload.sha1}\.png})).returns(s3_object) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{discourse-uploads/original/\d+X.*/#{upload.sha1}\.png})) + .returns(s3_object) expect(store.store_upload(uploaded_file, upload)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/discourse-uploads/original/\d+X.*/#{upload.sha1}\.png} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/discourse-uploads/original/\d+X.*/#{upload.sha1}\.png}, ) expect(upload.etag).to eq(etag) end @@ -66,20 +75,30 @@ RSpec.describe FileStore::S3Store do it "saves secure attachment using private ACL" do SiteSetting.prevent_anons_from_downloading_files = true SiteSetting.authorized_extensions = "pdf|png|jpg|gif" - upload = Fabricate(:upload, original_filename: "small.pdf", extension: "pdf", secure: true) + upload = + Fabricate(:upload, original_filename: "small.pdf", extension: "pdf", secure: true) s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})).returns(s3_object) - s3_object.expects(:put).with({ - acl: "private", - cache_control: "max-age=31556952, public, immutable", - content_type: "application/pdf", - content_disposition: "attachment; filename=\"#{upload.original_filename}\"; filename*=UTF-8''#{upload.original_filename}", - body: uploaded_file - }).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})) + .returns(s3_object) + s3_object + .expects(:put) + .with( + { + acl: "private", + cache_control: "max-age=31556952, public, immutable", + content_type: "application/pdf", + content_disposition: + "attachment; filename=\"#{upload.original_filename}\"; filename*=UTF-8''#{upload.original_filename}", + body: uploaded_file, + }, + ) + .returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) expect(store.store_upload(uploaded_file, upload)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.pdf} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.pdf}, ) end @@ -87,16 +106,25 @@ RSpec.describe FileStore::S3Store do SiteSetting.prevent_anons_from_downloading_files = true s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})).returns(s3_object).at_least_once - s3_object.expects(:put).with({ - acl: "public-read", - cache_control: "max-age=31556952, public, immutable", - content_type: "image/png", - body: uploaded_file - }).returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})) + .returns(s3_object) + .at_least_once + s3_object + .expects(:put) + .with( + { + acl: "public-read", + cache_control: "max-age=31556952, public, immutable", + content_type: "image/png", + body: uploaded_file, + }, + ) + .returns(Aws::S3::Types::PutObjectOutput.new(etag: "\"#{etag}\"")) expect(store.store_upload(uploaded_file, upload)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/original/\d+X.*/#{upload.sha1}\.png}, ) expect(store.url_for(upload)).to eq(upload.url) @@ -111,29 +139,29 @@ RSpec.describe FileStore::S3Store do it "returns an absolute schemaless url" do s3_helper.expects(:s3_bucket).returns(s3_bucket) - path = %r{optimized/\d+X.*/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png} + path = + %r{optimized/\d+X.*/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png} s3_bucket.expects(:object).with(regexp_matches(path)).returns(s3_object) expect(store.store_optimized_image(optimized_image_file, optimized_image)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/#{path}} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/#{path}}, ) expect(optimized_image.etag).to eq(etag) end describe "when s3_upload_bucket includes folders path" do - before do - SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" - end + before { SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" } it "returns an absolute schemaless url" do s3_helper.expects(:s3_bucket).returns(s3_bucket) - path = %r{discourse-uploads/optimized/\d+X.*/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png} + path = + %r{discourse-uploads/optimized/\d+X.*/#{optimized_image.upload.sha1}_#{OptimizedImage::VERSION}_100x200\.png} s3_bucket.expects(:object).with(regexp_matches(path)).returns(s3_object) expect(store.store_optimized_image(optimized_image_file, optimized_image)).to match( - %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/#{path}} + %r{//s3-upload-bucket\.s3\.dualstack\.us-west-1\.amazonaws\.com/#{path}}, ) expect(optimized_image.etag).to eq(etag) end @@ -145,62 +173,85 @@ RSpec.describe FileStore::S3Store do let(:upload_sha1) { Digest::SHA1.hexdigest(File.read(uploaded_file)) } let(:original_filename) { "smallest.png" } let(:s3_client) { Aws::S3::Client.new(stub_responses: true) } - let(:s3_helper) { S3Helper.new(SiteSetting.s3_upload_bucket, '', client: s3_client) } + let(:s3_helper) { S3Helper.new(SiteSetting.s3_upload_bucket, "", client: s3_client) } let(:store) { FileStore::S3Store.new(s3_helper) } let(:upload_opts) do { acl: "public-read", cache_control: "max-age=31556952, public, immutable", content_type: "image/png", - apply_metadata_to_destination: true + apply_metadata_to_destination: true, } end let(:external_upload_stub) { Fabricate(:image_external_upload_stub) } let(:existing_external_upload_key) { external_upload_stub.key } - before do - SiteSetting.authorized_extensions = "pdf|png" - end + before { SiteSetting.authorized_extensions = "pdf|png" } it "does not provide a content_disposition for images" do - s3_helper.expects(:copy).with(external_upload_stub.key, kind_of(String), options: upload_opts).returns(["path", "etag"]) + s3_helper + .expects(:copy) + .with(external_upload_stub.key, kind_of(String), options: upload_opts) + .returns(%w[path etag]) s3_helper.expects(:delete_object).with(external_upload_stub.key) - upload = Fabricate(:upload, extension: "png", sha1: upload_sha1, original_filename: original_filename) + upload = + Fabricate( + :upload, + extension: "png", + sha1: upload_sha1, + original_filename: original_filename, + ) store.move_existing_stored_upload( existing_external_upload_key: external_upload_stub.key, upload: upload, - content_type: "image/png" + content_type: "image/png", ) end context "when the file is a PDF" do - let(:external_upload_stub) { Fabricate(:attachment_external_upload_stub, original_filename: original_filename) } + let(:external_upload_stub) do + Fabricate(:attachment_external_upload_stub, original_filename: original_filename) + end let(:original_filename) { "small.pdf" } let(:uploaded_file) { file_from_fixtures("small.pdf", "pdf") } it "adds an attachment content-disposition with the original filename" do - disp_opts = { content_disposition: "attachment; filename=\"#{original_filename}\"; filename*=UTF-8''#{original_filename}", content_type: "application/pdf" } - s3_helper.expects(:copy).with(external_upload_stub.key, kind_of(String), options: upload_opts.merge(disp_opts)).returns(["path", "etag"]) - upload = Fabricate(:upload, extension: "png", sha1: upload_sha1, original_filename: original_filename) + disp_opts = { + content_disposition: + "attachment; filename=\"#{original_filename}\"; filename*=UTF-8''#{original_filename}", + content_type: "application/pdf", + } + s3_helper + .expects(:copy) + .with(external_upload_stub.key, kind_of(String), options: upload_opts.merge(disp_opts)) + .returns(%w[path etag]) + upload = + Fabricate( + :upload, + extension: "png", + sha1: upload_sha1, + original_filename: original_filename, + ) store.move_existing_stored_upload( existing_external_upload_key: external_upload_stub.key, upload: upload, - content_type: "application/pdf" + content_type: "application/pdf", ) end end end end - describe 'copying files in S3' do - describe '#copy_file' do + describe "copying files in S3" do + describe "#copy_file" do it "copies the from in S3 with the right paths" do upload.update!( - url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png" + url: + "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/original/1X/#{upload.sha1}.png", ) source = "#{upload_path}/#{Discourse.store.get_path_for_upload(upload)}" - destination = source.sub('.png', '.jpg') + destination = source.sub(".png", ".jpg") bucket = prepare_fake_s3(source, upload) expect(bucket.find_object(source)).to be_present @@ -214,7 +265,7 @@ RSpec.describe FileStore::S3Store do end end - describe 'removal from s3' do + describe "removal from s3" do describe "#remove_upload" do it "removes the file from s3 with the right paths" do upload_key = Discourse.store.get_path_for_upload(upload) @@ -233,16 +284,17 @@ RSpec.describe FileStore::S3Store do end describe "when s3_upload_bucket includes folders path" do - before do - SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" - end + before { SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" } it "removes the file from s3 with the right paths" do upload_key = "discourse-uploads/#{Discourse.store.get_path_for_upload(upload)}" - tombstone_key = "discourse-uploads/tombstone/#{Discourse.store.get_path_for_upload(upload)}" + tombstone_key = + "discourse-uploads/tombstone/#{Discourse.store.get_path_for_upload(upload)}" bucket = prepare_fake_s3(upload_key, upload) - upload.update!(url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{upload_key}") + upload.update!( + url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{upload_key}", + ) expect(bucket.find_object(upload_key)).to be_present expect(bucket.find_object(tombstone_key)).to be_nil @@ -256,13 +308,15 @@ RSpec.describe FileStore::S3Store do end describe "#remove_optimized_image" do - let(:optimized_key) { FileStore::BaseStore.new.get_path_for_optimized_image(optimized_image) } + let(:optimized_key) { FileStore::BaseStore.new.get_path_for_optimized_image(optimized_image) } let(:tombstone_key) { "tombstone/#{optimized_key}" } let(:upload) { optimized_image.upload } let(:upload_key) { Discourse.store.get_path_for_upload(upload) } before do - optimized_image.update!(url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{optimized_key}") + optimized_image.update!( + url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{optimized_key}", + ) upload.update!(url: "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/#{upload_key}") end @@ -282,12 +336,10 @@ RSpec.describe FileStore::S3Store do end describe "when s3_upload_bucket includes folders path" do - before do - SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" - end + before { SiteSetting.s3_upload_bucket = "s3-upload-bucket/discourse-uploads" } let(:image_path) { FileStore::BaseStore.new.get_path_for_optimized_image(optimized_image) } - let(:optimized_key) { "discourse-uploads/#{image_path}" } + let(:optimized_key) { "discourse-uploads/#{image_path}" } let(:tombstone_key) { "discourse-uploads/tombstone/#{image_path}" } let(:upload_key) { "discourse-uploads/#{Discourse.store.get_path_for_upload(upload)}" } @@ -315,42 +367,60 @@ RSpec.describe FileStore::S3Store do end it "doesn't crash if URL contains non-ascii characters" do - expect(store.has_been_uploaded?("//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/漢1337.png")).to eq(true) + expect( + store.has_been_uploaded?( + "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/漢1337.png", + ), + ).to eq(true) expect(store.has_been_uploaded?("//s3-upload-bucket.s3.amazonaws.com/漢1337.png")).to eq(false) end it "identifies S3 uploads" do - expect(store.has_been_uploaded?("//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/1337.png")).to eq(true) + expect( + store.has_been_uploaded?( + "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com/1337.png", + ), + ).to eq(true) end it "does not match other s3 urls" do expect(store.has_been_uploaded?("//s3-upload-bucket.s3.amazonaws.com/1337.png")).to eq(false) - expect(store.has_been_uploaded?("//s3-upload-bucket.s3-us-west-1.amazonaws.com/1337.png")).to eq(false) + expect( + store.has_been_uploaded?("//s3-upload-bucket.s3-us-west-1.amazonaws.com/1337.png"), + ).to eq(false) expect(store.has_been_uploaded?("//s3.amazonaws.com/s3-upload-bucket/1337.png")).to eq(false) expect(store.has_been_uploaded?("//s4_upload_bucket.s3.amazonaws.com/1337.png")).to eq(false) end - end describe ".absolute_base_url" do it "returns a lowercase schemaless absolute url" do - expect(store.absolute_base_url).to eq("//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com") + expect(store.absolute_base_url).to eq( + "//s3-upload-bucket.s3.dualstack.us-west-1.amazonaws.com", + ) end it "uses the proper endpoint" do SiteSetting.s3_region = "us-east-1" - expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com") + expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq( + "//s3-upload-bucket.s3.dualstack.us-east-1.amazonaws.com", + ) SiteSetting.s3_region = "us-west-2" - expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3-upload-bucket.s3.dualstack.us-west-2.amazonaws.com") + expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq( + "//s3-upload-bucket.s3.dualstack.us-west-2.amazonaws.com", + ) SiteSetting.s3_region = "cn-north-1" - expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3-upload-bucket.s3.cn-north-1.amazonaws.com.cn") + expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq( + "//s3-upload-bucket.s3.cn-north-1.amazonaws.com.cn", + ) SiteSetting.s3_region = "cn-northwest-1" - expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3-upload-bucket.s3.cn-northwest-1.amazonaws.com.cn") + expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq( + "//s3-upload-bucket.s3.cn-northwest-1.amazonaws.com.cn", + ) end - end it "is external" do @@ -383,17 +453,18 @@ RSpec.describe FileStore::S3Store do end end - describe 'update ACL' do - before do - SiteSetting.authorized_extensions = "pdf|png" - end + describe "update ACL" do + before { SiteSetting.authorized_extensions = "pdf|png" } describe ".update_upload_ACL" do let(:upload) { Fabricate(:upload, original_filename: "small.pdf", extension: "pdf") } it "sets acl to public by default" do s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})).returns(s3_object) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})) + .returns(s3_object) s3_object.expects(:acl).returns(s3_object) s3_object.expects(:put).with(acl: "public-read").returns(s3_object) @@ -403,7 +474,10 @@ RSpec.describe FileStore::S3Store do it "sets acl to private when upload is marked secure" do upload.update!(secure: true) s3_helper.expects(:s3_bucket).returns(s3_bucket) - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})).returns(s3_object) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.pdf})) + .returns(s3_object) s3_object.expects(:acl).returns(s3_object) s3_object.expects(:put).with(acl: "private").returns(s3_object) @@ -412,10 +486,10 @@ RSpec.describe FileStore::S3Store do end end - describe '.cdn_url' do - it 'supports subfolder' do - SiteSetting.s3_upload_bucket = 's3-upload-bucket/livechat' - SiteSetting.s3_cdn_url = 'https://rainbow.com' + describe ".cdn_url" do + it "supports subfolder" do + SiteSetting.s3_upload_bucket = "s3-upload-bucket/livechat" + SiteSetting.s3_cdn_url = "https://rainbow.com" # none of this should matter at all # subfolder should not leak into uploads @@ -436,10 +510,14 @@ RSpec.describe FileStore::S3Store do describe ".url_for" do it "returns signed URL with content disposition when requesting to download image" do s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once - s3_bucket.expects(:object).with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})).returns(s3_object) + s3_bucket + .expects(:object) + .with(regexp_matches(%r{original/\d+X.*/#{upload.sha1}\.png})) + .returns(s3_object) opts = { expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds, - response_content_disposition: %Q|attachment; filename="#{upload.original_filename}"; filename*=UTF-8''#{upload.original_filename}| + response_content_disposition: + %Q|attachment; filename="#{upload.original_filename}"; filename*=UTF-8''#{upload.original_filename}|, } s3_object.expects(:presigned_url).with(:get, opts) @@ -452,9 +530,7 @@ RSpec.describe FileStore::S3Store do it "returns signed URL for a given path" do s3_helper.expects(:s3_bucket).returns(s3_bucket).at_least_once s3_bucket.expects(:object).with("special/optimized/file.png").returns(s3_object) - opts = { - expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds - } + opts = { expires_in: SiteSetting.s3_presigned_get_url_expires_after_seconds } s3_object.expects(:presigned_url).with(:get, opts) @@ -463,14 +539,17 @@ RSpec.describe FileStore::S3Store do it "does not prefix the s3_bucket_folder_path onto temporary upload prefixed keys" do SiteSetting.s3_upload_bucket = "s3-upload-bucket/folder_path" - uri = URI.parse(store.signed_url_for_path("#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}folder_path/uploads/default/blah/def.xyz")) + uri = + URI.parse( + store.signed_url_for_path( + "#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}folder_path/uploads/default/blah/def.xyz", + ), + ) expect(uri.path).to eq( - "/#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}folder_path/uploads/default/blah/def.xyz" + "/#{FileStore::BaseStore::TEMPORARY_UPLOAD_PREFIX}folder_path/uploads/default/blah/def.xyz", ) uri = URI.parse(store.signed_url_for_path("uploads/default/blah/def.xyz")) - expect(uri.path).to eq( - "/folder_path/uploads/default/blah/def.xyz" - ) + expect(uri.path).to eq("/folder_path/uploads/default/blah/def.xyz") end end @@ -485,7 +564,7 @@ RSpec.describe FileStore::S3Store do @fake_s3_bucket.put_object( key: upload_key, size: upload.filesize, - last_modified: upload.created_at + last_modified: upload.created_at, ) end end diff --git a/spec/lib/filter_best_posts_spec.rb b/spec/lib/filter_best_posts_spec.rb index 91aeddd55a..81133b8da6 100644 --- a/spec/lib/filter_best_posts_spec.rb +++ b/spec/lib/filter_best_posts_spec.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'filter_best_posts' -require 'topic_view' +require "filter_best_posts" +require "topic_view" RSpec.describe FilterBestPosts do - fab!(:topic) { Fabricate(:topic) } fab!(:coding_horror) { Fabricate(:coding_horror) } fab!(:first_poster) { topic.user } @@ -17,14 +16,13 @@ RSpec.describe FilterBestPosts do fab!(:admin) { Fabricate(:admin) } it "can find the best responses" do - filtered_posts = TopicView.new(topic.id, coding_horror, best: 2).filtered_posts best2 = FilterBestPosts.new(topic, filtered_posts, 2) expect(best2.posts.count).to eq(2) expect(best2.posts[0].id).to eq(p2.id) expect(best2.posts[1].id).to eq(p3.id) - topic.update_status('closed', true, Fabricate(:admin)) + topic.update_status("closed", true, Fabricate(:admin)) expect(topic.posts.count).to eq(4) end @@ -32,21 +30,23 @@ RSpec.describe FilterBestPosts do before { @filtered_posts = TopicView.new(topic.id, nil, best: 99).filtered_posts } it "should not get the status post" do - best = FilterBestPosts.new(topic, @filtered_posts, 99) expect(best.filtered_posts.size).to eq(3) expect(best.posts.map(&:id)).to match_array([p2.id, p3.id]) - end it "should get no results for trust level too low" do - - best = FilterBestPosts.new(topic, @filtered_posts, 99, min_trust_level: coding_horror.trust_level + 1) + best = + FilterBestPosts.new( + topic, + @filtered_posts, + 99, + min_trust_level: coding_horror.trust_level + 1, + ) expect(best.posts.count).to eq(0) end it "should filter out the posts with a score that is too low" do - best = FilterBestPosts.new(topic, @filtered_posts, 99, min_score: 99) expect(best.posts.count).to eq(0) end @@ -59,12 +59,26 @@ RSpec.describe FilterBestPosts do it "should punch through posts if the score is high enough" do p2.update_column(:score, 100) - best = FilterBestPosts.new(topic, @filtered_posts, 99, bypass_trust_level_score: 100, min_trust_level: coding_horror.trust_level + 1) + best = + FilterBestPosts.new( + topic, + @filtered_posts, + 99, + bypass_trust_level_score: 100, + min_trust_level: coding_horror.trust_level + 1, + ) expect(best.posts.count).to eq(1) end it "should bypass trust level score" do - best = FilterBestPosts.new(topic, @filtered_posts, 99, bypass_trust_level_score: 0, min_trust_level: coding_horror.trust_level + 1) + best = + FilterBestPosts.new( + topic, + @filtered_posts, + 99, + bypass_trust_level_score: 0, + min_trust_level: coding_horror.trust_level + 1, + ) expect(best.posts.count).to eq(0) end @@ -84,6 +98,5 @@ RSpec.describe FilterBestPosts do best = FilterBestPosts.new(topic, @filtered_posts, 99, only_moderator_liked: true) expect(best.posts.count).to eq(1) end - end end diff --git a/spec/lib/final_destination/http_spec.rb b/spec/lib/final_destination/http_spec.rb index fa9a7706e6..3388a2c835 100644 --- a/spec/lib/final_destination/http_spec.rb +++ b/spec/lib/final_destination/http_spec.rb @@ -9,9 +9,7 @@ describe FinalDestination::HTTP do Addrinfo.stubs(:getaddrinfo).never end - after do - WebMock.enable! - end + after { WebMock.enable! } def expect_tcp_and_abort(stub_addr, &blk) success = Class.new(StandardError) @@ -102,7 +100,10 @@ describe FinalDestination::HTTP do stub_ip_lookup("example.com", %w[1.1.1.1 2.2.2.2 3.3.3.3 4.4.4.4]) TCPSocket.stubs(:open).with { |addr| addr == "1.1.1.1" }.raises(Errno::ECONNREFUSED) TCPSocket.stubs(:open).with { |addr| addr == "2.2.2.2" }.raises(Errno::ECONNREFUSED) - TCPSocket.stubs(:open).with { |*args, **kwargs| kwargs[:open_timeout] == 0 }.raises(Errno::ETIMEDOUT) + TCPSocket + .stubs(:open) + .with { |*args, **kwargs| kwargs[:open_timeout] == 0 } + .raises(Errno::ETIMEDOUT) FinalDestination::HTTP.any_instance.stubs(:current_time).returns(0, 1, 5) expect do FinalDestination::HTTP.start("example.com", 80, open_timeout: 5) {} diff --git a/spec/lib/final_destination/resolver_spec.rb b/spec/lib/final_destination/resolver_spec.rb index 20ce673cd7..cdaa3d6c0d 100644 --- a/spec/lib/final_destination/resolver_spec.rb +++ b/spec/lib/final_destination/resolver_spec.rb @@ -28,9 +28,7 @@ describe FinalDestination::Resolver do expect(alive_thread_count).to eq(start_thread_count) - expect(FinalDestination::Resolver.lookup("example.com")).to eq( - %w[1.1.1.1 2.2.2.2], - ) + expect(FinalDestination::Resolver.lookup("example.com")).to eq(%w[1.1.1.1 2.2.2.2]) # Thread available for reuse after successful lookup expect(alive_thread_count).to eq(start_thread_count + 1) diff --git a/spec/lib/final_destination_spec.rb b/spec/lib/final_destination_spec.rb index a81be718ce..37ab6fef07 100644 --- a/spec/lib/final_destination_spec.rb +++ b/spec/lib/final_destination_spec.rb @@ -1,38 +1,21 @@ # frozen_string_literal: true -require 'final_destination' +require "final_destination" RSpec.describe FinalDestination do let(:opts) do { - ignore_redirects: ['https://ignore-me.com'], - - force_get_hosts: ['https://force.get.com', 'https://*.ihaveawildcard.com/'], - - preserve_fragment_url_hosts: ['https://eviltrout.com'], + ignore_redirects: ["https://ignore-me.com"], + force_get_hosts: %w[https://force.get.com https://*.ihaveawildcard.com/], + preserve_fragment_url_hosts: ["https://eviltrout.com"], } end - let(:doc_response) do - { - status: 200, - headers: { "Content-Type" => "text/html" } - } - end + let(:doc_response) { { status: 200, headers: { "Content-Type" => "text/html" } } } - let(:image_response) do - { - status: 200, - headers: { "Content-Type" => "image/jpeg" } - } - end + let(:image_response) { { status: 200, headers: { "Content-Type" => "image/jpeg" } } } - let(:body_response) do - { - status: 200, - body: "test" - } - end + let(:body_response) { { status: 200, body: "test" } } def fd_stub_request(method, url) uri = URI.parse(url) @@ -43,10 +26,11 @@ RSpec.describe FinalDestination do # In Excon we pass the IP in the URL, so we need to stub # that version as well uri.hostname = "HOSTNAME_PLACEHOLDER" - matcher = Regexp.escape(uri.to_s).sub( - "HOSTNAME_PLACEHOLDER", - "(#{Regexp.escape(host)}|#{Regexp.escape(ip)})" - ) + matcher = + Regexp.escape(uri.to_s).sub( + "HOSTNAME_PLACEHOLDER", + "(#{Regexp.escape(host)}|#{Regexp.escape(ip)})", + ) stub_request(method, /\A#{matcher}\z/).with(headers: { "Host" => host }) end @@ -54,37 +38,36 @@ RSpec.describe FinalDestination do def canonical_follow(from, dest) fd_stub_request(:get, from).to_return( status: 200, - body: "" + body: "", ) end def redirect_response(from, dest) - fd_stub_request(:head, from).to_return( - status: 302, - headers: { "Location" => dest } - ) + fd_stub_request(:head, from).to_return(status: 302, headers: { "Location" => dest }) end def fd(url) FinalDestination.new(url, opts) end - it 'correctly parses ignored hostnames' do - fd = FinalDestination.new('https://meta.discourse.org', - ignore_redirects: ['http://google.com', 'youtube.com', 'https://meta.discourse.org', '://bing.com'] - ) + it "correctly parses ignored hostnames" do + fd = + FinalDestination.new( + "https://meta.discourse.org", + ignore_redirects: %w[http://google.com youtube.com https://meta.discourse.org ://bing.com], + ) - expect(fd.ignored).to eq(['test.localhost', 'google.com', 'meta.discourse.org']) + expect(fd.ignored).to eq(%w[test.localhost google.com meta.discourse.org]) end - describe '.resolve' do + describe ".resolve" do it "has a ready status code before anything happens" do - expect(fd('https://eviltrout.com').status).to eq(:ready) + expect(fd("https://eviltrout.com").status).to eq(:ready) end it "returns nil for an invalid url" do expect(fd(nil).resolve).to be_nil - expect(fd('asdf').resolve).to be_nil + expect(fd("asdf").resolve).to be_nil end it "returns nil for unresolvable url" do @@ -100,37 +83,33 @@ RSpec.describe FinalDestination do it "returns nil when read timeouts" do Excon.expects(:public_send).raises(Excon::Errors::Timeout) - expect(fd('https://discourse.org').resolve).to eq(nil) + expect(fd("https://discourse.org").resolve).to eq(nil) end context "without redirects" do - before do - fd_stub_request(:head, "https://eviltrout.com/").to_return(doc_response) - end + before { fd_stub_request(:head, "https://eviltrout.com/").to_return(doc_response) } it "returns the final url" do - final = FinalDestination.new('https://eviltrout.com', opts) - expect(final.resolve.to_s).to eq('https://eviltrout.com') + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://eviltrout.com") expect(final.redirected?).to eq(false) expect(final.status).to eq(:resolved) end end it "ignores redirects" do - final = FinalDestination.new('https://ignore-me.com/some-url', opts) - expect(final.resolve.to_s).to eq('https://ignore-me.com/some-url') + final = FinalDestination.new("https://ignore-me.com/some-url", opts) + expect(final.resolve.to_s).to eq("https://ignore-me.com/some-url") expect(final.redirected?).to eq(false) expect(final.status).to eq(:resolved) end context "with underscores in URLs" do - before do - fd_stub_request(:head, 'https://some_thing.example.com').to_return(doc_response) - end + before { fd_stub_request(:head, "https://some_thing.example.com").to_return(doc_response) } it "doesn't raise errors with underscores in urls" do - final = FinalDestination.new('https://some_thing.example.com', opts) - expect(final.resolve.to_s).to eq('https://some_thing.example.com') + final = FinalDestination.new("https://some_thing.example.com", opts) + expect(final.resolve.to_s).to eq("https://some_thing.example.com") expect(final.redirected?).to eq(false) expect(final.status).to eq(:resolved) end @@ -144,8 +123,8 @@ RSpec.describe FinalDestination do end it "returns the final url" do - final = FinalDestination.new('https://eviltrout.com', opts) - expect(final.resolve.to_s).to eq('https://discourse.org') + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://discourse.org") expect(final.redirected?).to eq(true) expect(final.status).to eq(:resolved) end @@ -159,7 +138,7 @@ RSpec.describe FinalDestination do end it "returns the final url" do - final = FinalDestination.new('https://eviltrout.com', opts.merge(max_redirects: 1)) + final = FinalDestination.new("https://eviltrout.com", opts.merge(max_redirects: 1)) expect(final.resolve).to be_nil expect(final.redirected?).to eq(true) expect(final.status).to eq(:too_many_redirects) @@ -169,12 +148,18 @@ RSpec.describe FinalDestination do context "with a redirect to an internal IP" do before do redirect_response("https://eviltrout.com", "https://private-host.com") - FinalDestination::SSRFDetector.stubs(:lookup_and_filter_ips).with("eviltrout.com").returns(["1.2.3.4"]) - FinalDestination::SSRFDetector.stubs(:lookup_and_filter_ips).with("private-host.com").raises(FinalDestination::SSRFDetector::DisallowedIpError) + FinalDestination::SSRFDetector + .stubs(:lookup_and_filter_ips) + .with("eviltrout.com") + .returns(["1.2.3.4"]) + FinalDestination::SSRFDetector + .stubs(:lookup_and_filter_ips) + .with("private-host.com") + .raises(FinalDestination::SSRFDetector::DisallowedIpError) end it "returns the final url" do - final = FinalDestination.new('https://eviltrout.com', opts) + final = FinalDestination.new("https://eviltrout.com", opts) expect(final.resolve).to be_nil expect(final.redirected?).to eq(true) expect(final.status).to eq(:invalid_address) @@ -182,45 +167,56 @@ RSpec.describe FinalDestination do end context "with a redirect to login path" do - before do - redirect_response("https://eviltrout.com/t/xyz/1", "https://eviltrout.com/login") - end + before { redirect_response("https://eviltrout.com/t/xyz/1", "https://eviltrout.com/login") } it "does not follow redirect" do - final = FinalDestination.new('https://eviltrout.com/t/xyz/1', opts) - expect(final.resolve.to_s).to eq('https://eviltrout.com/t/xyz/1') + final = FinalDestination.new("https://eviltrout.com/t/xyz/1", opts) + expect(final.resolve.to_s).to eq("https://eviltrout.com/t/xyz/1") expect(final.redirected?).to eq(false) expect(final.status).to eq(:resolved) end end - it 'raises error when response is too big' do + it "raises error when response is too big" do stub_const(described_class, "MAX_REQUEST_SIZE_BYTES", 1) do fd_stub_request(:get, "https://codinghorror.com/blog").to_return(body_response) - final = FinalDestination.new('https://codinghorror.com/blog', opts.merge(follow_canonical: true)) - expect { final.resolve }.to raise_error(Excon::Errors::ExpectationFailed, "response size too big: https://codinghorror.com/blog") + final = + FinalDestination.new("https://codinghorror.com/blog", opts.merge(follow_canonical: true)) + expect { final.resolve }.to raise_error( + Excon::Errors::ExpectationFailed, + "response size too big: https://codinghorror.com/blog", + ) end end - it 'raises error when response is too slow' do - fd_stub_request(:get, "https://codinghorror.com/blog").to_return(lambda { |request| freeze_time(11.seconds.from_now) ; body_response }) - final = FinalDestination.new('https://codinghorror.com/blog', opts.merge(follow_canonical: true)) - expect { final.resolve }.to raise_error(Excon::Errors::ExpectationFailed, "connect timeout reached: https://codinghorror.com/blog") + it "raises error when response is too slow" do + fd_stub_request(:get, "https://codinghorror.com/blog").to_return( + lambda do |request| + freeze_time(11.seconds.from_now) + body_response + end, + ) + final = + FinalDestination.new("https://codinghorror.com/blog", opts.merge(follow_canonical: true)) + expect { final.resolve }.to raise_error( + Excon::Errors::ExpectationFailed, + "connect timeout reached: https://codinghorror.com/blog", + ) end - context 'when following canonical links' do - it 'resolves the canonical link as the final destination' do + context "when following canonical links" do + it "resolves the canonical link as the final destination" do canonical_follow("https://eviltrout.com", "https://codinghorror.com/blog") fd_stub_request(:head, "https://codinghorror.com/blog").to_return(doc_response) - final = FinalDestination.new('https://eviltrout.com', opts.merge(follow_canonical: true)) + final = FinalDestination.new("https://eviltrout.com", opts.merge(follow_canonical: true)) expect(final.resolve.to_s).to eq("https://codinghorror.com/blog") expect(final.redirected?).to eq(false) expect(final.status).to eq(:resolved) end - it 'resolves the canonical link when the URL is relative' do + it "resolves the canonical link when the URL is relative" do host = "https://codinghorror.com" canonical_follow("#{host}/blog", "/blog/canonical") @@ -233,7 +229,7 @@ RSpec.describe FinalDestination do expect(final.status).to eq(:resolved) end - it 'resolves the canonical link when the URL is relative and does not start with the / symbol' do + it "resolves the canonical link when the URL is relative and does not start with the / symbol" do host = "https://codinghorror.com" canonical_follow("#{host}/blog", "blog/canonical") fd_stub_request(:head, "#{host}/blog/canonical").to_return(doc_response) @@ -248,7 +244,7 @@ RSpec.describe FinalDestination do it "does not follow the canonical link if it's the same as the current URL" do canonical_follow("https://eviltrout.com", "https://eviltrout.com") - final = FinalDestination.new('https://eviltrout.com', opts.merge(follow_canonical: true)) + final = FinalDestination.new("https://eviltrout.com", opts.merge(follow_canonical: true)) expect(final.resolve.to_s).to eq("https://eviltrout.com") expect(final.redirected?).to eq(false) @@ -258,7 +254,7 @@ RSpec.describe FinalDestination do it "does not follow the canonical link if it's invalid" do canonical_follow("https://eviltrout.com", "") - final = FinalDestination.new('https://eviltrout.com', opts.merge(follow_canonical: true)) + final = FinalDestination.new("https://eviltrout.com", opts.merge(follow_canonical: true)) expect(final.resolve.to_s).to eq("https://eviltrout.com") expect(final.redirected?).to eq(false) @@ -268,7 +264,7 @@ RSpec.describe FinalDestination do context "when forcing GET" do it "will do a GET when forced" do - url = 'https://force.get.com/posts?page=4' + url = "https://force.get.com/posts?page=4" get_stub = fd_stub_request(:get, url) head_stub = fd_stub_request(:head, url) @@ -280,7 +276,7 @@ RSpec.describe FinalDestination do end it "will do a HEAD if not forced" do - url = 'https://eviltrout.com/posts?page=2' + url = "https://eviltrout.com/posts?page=2" get_stub = fd_stub_request(:get, url) head_stub = fd_stub_request(:head, url) @@ -292,7 +288,7 @@ RSpec.describe FinalDestination do end it "will do a GET when forced on a wildcard subdomain" do - url = 'https://any-subdomain.ihaveawildcard.com/some/other/content' + url = "https://any-subdomain.ihaveawildcard.com/some/other/content" get_stub = fd_stub_request(:get, url) head_stub = fd_stub_request(:head, url) @@ -304,7 +300,7 @@ RSpec.describe FinalDestination do end it "will do a HEAD if on a subdomain of a forced get domain without a wildcard" do - url = 'https://particularly.eviltrout.com/has/a/secret/plan' + url = "https://particularly.eviltrout.com/has/a/secret/plan" get_stub = fd_stub_request(:get, url) head_stub = fd_stub_request(:head, url) @@ -314,60 +310,63 @@ RSpec.describe FinalDestination do expect(get_stub).to_not have_been_requested expect(head_stub).to have_been_requested end - end context "when HEAD not supported" do before do - fd_stub_request(:get, 'https://eviltrout.com').to_return( + fd_stub_request(:get, "https://eviltrout.com").to_return( status: 301, headers: { - "Location" => 'https://discourse.org', - 'Set-Cookie' => 'evil=trout' - } + "Location" => "https://discourse.org", + "Set-Cookie" => "evil=trout", + }, ) - fd_stub_request(:head, 'https://discourse.org') + fd_stub_request(:head, "https://discourse.org") end context "when the status code is 405" do - before do - fd_stub_request(:head, 'https://eviltrout.com').to_return(status: 405) - end + before { fd_stub_request(:head, "https://eviltrout.com").to_return(status: 405) } it "will try a GET" do - final = FinalDestination.new('https://eviltrout.com', opts) - expect(final.resolve.to_s).to eq('https://discourse.org') + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://discourse.org") expect(final.status).to eq(:resolved) - expect(final.cookie).to eq('evil=trout') + expect(final.cookie).to eq("evil=trout") end end context "when the status code is 501" do - before do - fd_stub_request(:head, 'https://eviltrout.com').to_return(status: 501) - end + before { fd_stub_request(:head, "https://eviltrout.com").to_return(status: 501) } it "will try a GET" do - final = FinalDestination.new('https://eviltrout.com', opts) - expect(final.resolve.to_s).to eq('https://discourse.org') + final = FinalDestination.new("https://eviltrout.com", opts) + expect(final.resolve.to_s).to eq("https://discourse.org") expect(final.status).to eq(:resolved) - expect(final.cookie).to eq('evil=trout') + expect(final.cookie).to eq("evil=trout") end end it "correctly extracts cookies during GET" do fd_stub_request(:head, "https://eviltrout.com").to_return(status: 405) - fd_stub_request(:get, "https://eviltrout.com") - .to_return(status: 302, body: "" , headers: { + fd_stub_request(:get, "https://eviltrout.com").to_return( + status: 302, + body: "", + headers: { "Location" => "https://eviltrout.com", - "Set-Cookie" => ["foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", - "bar=1", - "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com"] - }) + "Set-Cookie" => [ + "foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", + "bar=1", + "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com", + ], + }, + ) - fd_stub_request(:head, "https://eviltrout.com") - .with(headers: { "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f" }) + fd_stub_request(:head, "https://eviltrout.com").with( + headers: { + "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f", + }, + ) final = FinalDestination.new("https://eviltrout.com", opts) expect(final.resolve.to_s).to eq("https://eviltrout.com") @@ -377,14 +376,20 @@ RSpec.describe FinalDestination do end it "should use the correct format for cookies when there is only one cookie" do - fd_stub_request(:head, "https://eviltrout.com") - .to_return(status: 302, headers: { + fd_stub_request(:head, "https://eviltrout.com").to_return( + status: 302, + headers: { "Location" => "https://eviltrout.com", - "Set-Cookie" => "foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com" - }) + "Set-Cookie" => + "foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", + }, + ) - fd_stub_request(:head, "https://eviltrout.com") - .with(headers: { "Cookie" => "foo=219ffwef9w0f" }) + fd_stub_request(:head, "https://eviltrout.com").with( + headers: { + "Cookie" => "foo=219ffwef9w0f", + }, + ) final = FinalDestination.new("https://eviltrout.com", opts) expect(final.resolve.to_s).to eq("https://eviltrout.com") @@ -393,16 +398,23 @@ RSpec.describe FinalDestination do end it "should use the correct format for cookies when there are multiple cookies" do - fd_stub_request(:head, "https://eviltrout.com") - .to_return(status: 302, headers: { + fd_stub_request(:head, "https://eviltrout.com").to_return( + status: 302, + headers: { "Location" => "https://eviltrout.com", - "Set-Cookie" => ["foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", - "bar=1", - "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com"] - }) + "Set-Cookie" => [ + "foo=219ffwef9w0f; expires=Mon, 19-Feb-2018 10:44:24 GMT; path=/; domain=eviltrout.com", + "bar=1", + "baz=2; expires=Tue, 19-Feb-2019 10:14:24 GMT; path=/; domain=eviltrout.com", + ], + }, + ) - fd_stub_request(:head, "https://eviltrout.com") - .with(headers: { "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f" }) + fd_stub_request(:head, "https://eviltrout.com").with( + headers: { + "Cookie" => "bar=1; baz=2; foo=219ffwef9w0f", + }, + ) final = FinalDestination.new("https://eviltrout.com", opts) expect(final.resolve.to_s).to eq("https://eviltrout.com") @@ -436,32 +448,38 @@ RSpec.describe FinalDestination do end end - describe '#get' do + describe "#get" do let(:fd) { FinalDestination.new("http://wikipedia.com", opts.merge(verbose: true)) } - before do - described_class.clear_https_cache!("wikipedia.com") - end + before { described_class.clear_https_cache!("wikipedia.com") } context "when there is a redirect" do before do - stub_request(:get, "http://wikipedia.com/"). - to_return(status: 302, body: "" , headers: { "location" => "https://wikipedia.com/" }) + stub_request(:get, "http://wikipedia.com/").to_return( + status: 302, + body: "", + headers: { + "location" => "https://wikipedia.com/", + }, + ) # webmock does not do chunks - stub_request(:get, "https://wikipedia.com/"). - to_return(status: 200, body: "" , headers: {}) + stub_request(:get, "https://wikipedia.com/").to_return( + status: 200, + body: "", + headers: { + }, + ) end - after do - WebMock.reset! - end + after { WebMock.reset! } it "correctly streams" do chunk = nil - result = fd.get do |resp, c| - chunk = c - throw :done - end + result = + fd.get do |resp, c| + chunk = c + throw :done + end expect(result).to eq("https://wikipedia.com/") expect(chunk).to eq("") @@ -471,12 +489,13 @@ RSpec.describe FinalDestination do context "when there is a timeout" do subject(:get) { fd.get {} } - before do - fd.stubs(:safe_session).raises(Timeout::Error) - end + before { fd.stubs(:safe_session).raises(Timeout::Error) } it "logs the exception" do - Rails.logger.expects(:warn).with(regexp_matches(/FinalDestination could not resolve URL \(timeout\)/)) + Rails + .logger + .expects(:warn) + .with(regexp_matches(/FinalDestination could not resolve URL \(timeout\)/)) get end @@ -488,9 +507,7 @@ RSpec.describe FinalDestination do context "when there is an SSL error" do subject(:get) { fd.get {} } - before do - fd.stubs(:safe_session).raises(OpenSSL::SSL::SSLError) - end + before { fd.stubs(:safe_session).raises(OpenSSL::SSL::SSLError) } it "logs the exception" do Rails.logger.expects(:warn).with(regexp_matches(/an error with ssl occurred/i)) @@ -505,24 +522,24 @@ RSpec.describe FinalDestination do describe ".validate_url_format" do it "supports http urls" do - expect(fd('http://eviltrout.com').validate_uri_format).to eq(true) + expect(fd("http://eviltrout.com").validate_uri_format).to eq(true) end it "supports https urls" do - expect(fd('https://eviltrout.com').validate_uri_format).to eq(true) + expect(fd("https://eviltrout.com").validate_uri_format).to eq(true) end it "doesn't support ftp urls" do - expect(fd('ftp://eviltrout.com').validate_uri_format).to eq(false) + expect(fd("ftp://eviltrout.com").validate_uri_format).to eq(false) end it "doesn't support IP urls" do - expect(fd('http://104.25.152.10').validate_uri_format).to eq(false) - expect(fd('https://[2001:abc:de:01:0:3f0:6a65:c2bf]').validate_uri_format).to eq(false) + expect(fd("http://104.25.152.10").validate_uri_format).to eq(false) + expect(fd("https://[2001:abc:de:01:0:3f0:6a65:c2bf]").validate_uri_format).to eq(false) end it "returns false for schemeless URL" do - expect(fd('eviltrout.com').validate_uri_format).to eq(false) + expect(fd("eviltrout.com").validate_uri_format).to eq(false) end it "returns false for nil URL" do @@ -530,50 +547,60 @@ RSpec.describe FinalDestination do end it "returns false for invalid ports" do - expect(fd('http://eviltrout.com:21').validate_uri_format).to eq(false) - expect(fd('https://eviltrout.com:8000').validate_uri_format).to eq(false) + expect(fd("http://eviltrout.com:21").validate_uri_format).to eq(false) + expect(fd("https://eviltrout.com:8000").validate_uri_format).to eq(false) end it "returns true for valid ports" do - expect(fd('http://eviltrout.com:80').validate_uri_format).to eq(true) - expect(fd('https://eviltrout.com:443').validate_uri_format).to eq(true) + expect(fd("http://eviltrout.com:80").validate_uri_format).to eq(true) + expect(fd("https://eviltrout.com:443").validate_uri_format).to eq(true) end end describe "https cache" do - it 'will cache https lookups' do - + it "will cache https lookups" do FinalDestination.clear_https_cache!("wikipedia.com") - fd_stub_request(:head, "http://wikipedia.com/image.png") - .to_return(status: 302, body: "", headers: { location: 'https://wikipedia.com/image.png' }) + fd_stub_request(:head, "http://wikipedia.com/image.png").to_return( + status: 302, + body: "", + headers: { + location: "https://wikipedia.com/image.png", + }, + ) fd_stub_request(:head, "https://wikipedia.com/image.png") - fd('http://wikipedia.com/image.png').resolve + fd("http://wikipedia.com/image.png").resolve fd_stub_request(:head, "https://wikipedia.com/image2.png") - fd('http://wikipedia.com/image2.png').resolve + fd("http://wikipedia.com/image2.png").resolve end end describe "#normalized_url" do it "correctly normalizes url" do - fragment_url = "https://eviltrout.com/2016/02/25/fixing-android-performance.html#discourse-comments" + fragment_url = + "https://eviltrout.com/2016/02/25/fixing-android-performance.html#discourse-comments" expect(fd(fragment_url).normalized_url.to_s).to eq(fragment_url) - expect(fd("https://eviltrout.com?s=180&d=mm&r=g").normalized_url.to_s) - .to eq("https://eviltrout.com?s=180&d=mm&%23038;r=g") + expect(fd("https://eviltrout.com?s=180&d=mm&r=g").normalized_url.to_s).to eq( + "https://eviltrout.com?s=180&d=mm&%23038;r=g", + ) - expect(fd("http://example.com/?a=\11\15").normalized_url.to_s).to eq("http://example.com/?a=%09%0D") + expect(fd("http://example.com/?a=\11\15").normalized_url.to_s).to eq( + "http://example.com/?a=%09%0D", + ) - expect(fd("https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE").normalized_url.to_s) - .to eq('https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE') + expect( + fd("https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE").normalized_url.to_s, + ).to eq("https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE") - expect(fd('https://ru.wikipedia.org/wiki/Свобо').normalized_url.to_s) - .to eq('https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE') + expect(fd("https://ru.wikipedia.org/wiki/Свобо").normalized_url.to_s).to eq( + "https://ru.wikipedia.org/wiki/%D0%A1%D0%B2%D0%BE%D0%B1%D0%BE", + ) end end end diff --git a/spec/lib/flag_settings_spec.rb b/spec/lib/flag_settings_spec.rb index 6dd895ede8..2d34e194d6 100644 --- a/spec/lib/flag_settings_spec.rb +++ b/spec/lib/flag_settings_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true -require 'flag_settings' +require "flag_settings" RSpec.describe FlagSettings do - let(:settings) { FlagSettings.new } - describe 'add' do - it 'will add a type' do + describe "add" do + it "will add a type" do settings.add(3, :off_topic) expect(settings.flag_types).to include(:off_topic) expect(settings.is_flag?(:off_topic)).to eq(true) @@ -18,26 +17,26 @@ RSpec.describe FlagSettings do expect(settings.auto_action_types).to be_empty end - it 'will add a topic type' do + it "will add a topic type" do settings.add(4, :inappropriate, topic_type: true) expect(settings.flag_types).to include(:inappropriate) expect(settings.topic_flag_types).to include(:inappropriate) expect(settings.without_custom_types).to include(:inappropriate) end - it 'will add a notify type' do + it "will add a notify type" do settings.add(3, :off_topic, notify_type: true) expect(settings.flag_types).to include(:off_topic) expect(settings.notify_types).to include(:off_topic) end - it 'will add an auto action type' do + it "will add an auto action type" do settings.add(7, :notify_moderators, auto_action_type: true) expect(settings.flag_types).to include(:notify_moderators) expect(settings.auto_action_types).to include(:notify_moderators) end - it 'will add a custom type' do + it "will add a custom type" do settings.add(7, :notify_user, custom_type: true) expect(settings.flag_types).to include(:notify_user) expect(settings.custom_types).to include(:notify_user) diff --git a/spec/lib/freedom_patches/schema_migration_details_spec.rb b/spec/lib/freedom_patches/schema_migration_details_spec.rb index 8a8317d703..7c8aba43d9 100644 --- a/spec/lib/freedom_patches/schema_migration_details_spec.rb +++ b/spec/lib/freedom_patches/schema_migration_details_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe FreedomPatches::SchemaMigrationDetails do - # we usually don't really need this model so lets not clutter up with it class SchemaMigrationDetail < ActiveRecord::Base end diff --git a/spec/lib/freedom_patches/translate_accelerator_spec.rb b/spec/lib/freedom_patches/translate_accelerator_spec.rb index c3a6592577..16c0c9e1ac 100644 --- a/spec/lib/freedom_patches/translate_accelerator_spec.rb +++ b/spec/lib/freedom_patches/translate_accelerator_spec.rb @@ -19,41 +19,39 @@ RSpec.describe "translate accelerator" do end it "supports raising if requested, and cache bypasses" do - expect { I18n.t('i_am_an_unknown_key99', raise: true) }.to raise_error(I18n::MissingTranslationData) + expect { I18n.t("i_am_an_unknown_key99", raise: true) }.to raise_error( + I18n::MissingTranslationData, + ) - orig = I18n.t('i_am_an_unknown_key99') + orig = I18n.t("i_am_an_unknown_key99") - expect(I18n.t('i_am_an_unknown_key99').object_id).to eq(orig.object_id) - expect(I18n.t('i_am_an_unknown_key99')).to eq("translation missing: en.i_am_an_unknown_key99") + expect(I18n.t("i_am_an_unknown_key99").object_id).to eq(orig.object_id) + expect(I18n.t("i_am_an_unknown_key99")).to eq("translation missing: en.i_am_an_unknown_key99") end it "returns the correct language" do - expect(I18n.t('foo', locale: :en)).to eq('Foo in :en') - expect(I18n.t('foo', locale: :de)).to eq('Foo in :de') + expect(I18n.t("foo", locale: :en)).to eq("Foo in :en") + expect(I18n.t("foo", locale: :de)).to eq("Foo in :de") - I18n.with_locale(:en) do - expect(I18n.t('foo')).to eq('Foo in :en') - end + I18n.with_locale(:en) { expect(I18n.t("foo")).to eq("Foo in :en") } - I18n.with_locale(:de) do - expect(I18n.t('foo')).to eq('Foo in :de') - end + I18n.with_locale(:de) { expect(I18n.t("foo")).to eq("Foo in :de") } end it "converts language keys to symbols" do - expect(I18n.t('foo', locale: :en)).to eq('Foo in :en') - expect(I18n.t('foo', locale: "en")).to eq('Foo in :en') + expect(I18n.t("foo", locale: :en)).to eq("Foo in :en") + expect(I18n.t("foo", locale: "en")).to eq("Foo in :en") expect(I18n.instance_variable_get(:@loaded_locales)).to contain_exactly(:en) end it "overrides for both string and symbol keys" do - key = 'user.email.not_allowed' - text_overridden = 'foobar' + key = "user.email.not_allowed" + text_overridden = "foobar" expect(I18n.t(key)).to be_present - override_translation('en', key, text_overridden) + override_translation("en", key, text_overridden) expect(I18n.t(key)).to eq(text_overridden) expect(I18n.t(key.to_sym)).to eq(text_overridden) @@ -61,17 +59,21 @@ RSpec.describe "translate accelerator" do describe ".overrides_by_locale" do it "should cache overrides for each locale" do - override_translation('en', 'got', 'summer') - override_translation('zh_TW', 'got', '冬季') + override_translation("en", "got", "summer") + override_translation("zh_TW", "got", "冬季") - I18n.overrides_by_locale('en') - I18n.overrides_by_locale('zh_TW') + I18n.overrides_by_locale("en") + I18n.overrides_by_locale("zh_TW") expect(I18n.instance_variable_get(:@overrides_by_site)).to eq( - 'default' => { - en: { 'got' => 'summer' }, - zh_TW: { 'got' => '冬季' } - } + "default" => { + en: { + "got" => "summer", + }, + zh_TW: { + "got" => "冬季", + }, + }, ) end end @@ -79,17 +81,18 @@ RSpec.describe "translate accelerator" do describe "plugins" do before do DiscoursePluginRegistry.register_locale( - 'foo', - name: 'Foo', - nativeName: 'Foo Bar', + "foo", + name: "Foo", + nativeName: "Foo Bar", plural: { - keys: [:one, :few, :other], - rule: lambda do |n| - return :one if n == 1 - return :few if n < 10 - :other - end - } + keys: %i[one few other], + rule: + lambda do |n| + return :one if n == 1 + return :few if n < 10 + :other + end, + }, ) LocaleSiteSetting.reset! @@ -104,10 +107,10 @@ RSpec.describe "translate accelerator" do it "loads plural rules from plugins" do I18n.locale = :foo - expect(I18n.t('i18n.plural.keys')).to eq([:one, :few, :other]) - expect(I18n.t('items', count: 1)).to eq('one item') - expect(I18n.t('items', count: 3)).to eq('some items') - expect(I18n.t('items', count: 20)).to eq('20 items') + expect(I18n.t("i18n.plural.keys")).to eq(%i[one few other]) + expect(I18n.t("items", count: 1)).to eq("one item") + expect(I18n.t("items", count: 3)).to eq("some items") + expect(I18n.t("items", count: 20)).to eq("20 items") end end @@ -115,124 +118,131 @@ RSpec.describe "translate accelerator" do before { I18n.locale = :en } it "returns the overridden key" do - override_translation('en', 'foo', 'Overwritten foo') - expect(I18n.t('foo')).to eq('Overwritten foo') + override_translation("en", "foo", "Overwritten foo") + expect(I18n.t("foo")).to eq("Overwritten foo") - override_translation('en', 'foo', 'new value') - expect(I18n.t('foo')).to eq('new value') + override_translation("en", "foo", "new value") + expect(I18n.t("foo")).to eq("new value") end it "returns the overridden key after switching the locale" do - override_translation('en', 'foo', 'Overwritten foo in EN') - override_translation('de', 'foo', 'Overwritten foo in DE') + override_translation("en", "foo", "Overwritten foo in EN") + override_translation("de", "foo", "Overwritten foo in DE") - expect(I18n.t('foo')).to eq('Overwritten foo in EN') + expect(I18n.t("foo")).to eq("Overwritten foo in EN") I18n.locale = :de - expect(I18n.t('foo')).to eq('Overwritten foo in DE') + expect(I18n.t("foo")).to eq("Overwritten foo in DE") end it "can be searched" do - override_translation('en', 'wat', 'Overwritten value') - expect(I18n.search('wat')).to include('wat' => 'Overwritten value') - expect(I18n.search('Overwritten')).to include('wat' => 'Overwritten value') + override_translation("en", "wat", "Overwritten value") + expect(I18n.search("wat")).to include("wat" => "Overwritten value") + expect(I18n.search("Overwritten")).to include("wat" => "Overwritten value") - override_translation('en', 'wat', 'Overwritten with (parentheses)') - expect(I18n.search('Overwritten with (')).to include('wat' => 'Overwritten with (parentheses)') + override_translation("en", "wat", "Overwritten with (parentheses)") + expect(I18n.search("Overwritten with (")).to include( + "wat" => "Overwritten with (parentheses)", + ) end it "supports disabling" do - orig_title = I18n.t('title') - override_translation('en', 'title', 'overridden title') + orig_title = I18n.t("title") + override_translation("en", "title", "overridden title") - I18n.overrides_disabled do - expect(I18n.t('title')).to eq(orig_title) - end + I18n.overrides_disabled { expect(I18n.t("title")).to eq(orig_title) } - expect(I18n.t('title')).to eq('overridden title') + expect(I18n.t("title")).to eq("overridden title") end it "supports interpolation" do - override_translation('en', 'world', 'my %{world}') - expect(I18n.t('world', world: 'foo')).to eq('my foo') + override_translation("en", "world", "my %{world}") + expect(I18n.t("world", world: "foo")).to eq("my foo") end it "supports interpolation named count" do - override_translation('en', 'wat', 'goodbye %{count}') - expect(I18n.t('wat', count: 123)).to eq('goodbye 123') + override_translation("en", "wat", "goodbye %{count}") + expect(I18n.t("wat", count: 123)).to eq("goodbye 123") end it "ignores interpolation named count if it is not applicable" do - override_translation('en', 'wat', 'bar') - expect(I18n.t('wat', count: 1)).to eq('bar') + override_translation("en", "wat", "bar") + expect(I18n.t("wat", count: 1)).to eq("bar") end it "supports one and other" do - override_translation('en', 'items.one', 'one fish') - override_translation('en', 'items.other', '%{count} fishies') - expect(I18n.t('items', count: 13)).to eq('13 fishies') - expect(I18n.t('items', count: 1)).to eq('one fish') + override_translation("en", "items.one", "one fish") + override_translation("en", "items.other", "%{count} fishies") + expect(I18n.t("items", count: 13)).to eq("13 fishies") + expect(I18n.t("items", count: 1)).to eq("one fish") end it "works with strings and symbols for non-pluralized string when count is given" do - override_translation('en', 'fish', 'trout') - expect(I18n.t(:fish, count: 1)).to eq('trout') - expect(I18n.t('fish', count: 1)).to eq('trout') + override_translation("en", "fish", "trout") + expect(I18n.t(:fish, count: 1)).to eq("trout") + expect(I18n.t("fish", count: 1)).to eq("trout") end it "supports one and other with fallback locale" do - override_translation('en_GB', 'items.one', 'one fish') - override_translation('en_GB', 'items.other', '%{count} fishies') + override_translation("en_GB", "items.one", "one fish") + override_translation("en_GB", "items.other", "%{count} fishies") I18n.with_locale(:en_GB) do - expect(I18n.t('items', count: 13)).to eq('13 fishies') - expect(I18n.t('items', count: 1)).to eq('one fish') + expect(I18n.t("items", count: 13)).to eq("13 fishies") + expect(I18n.t("items", count: 1)).to eq("one fish") end end it "supports one and other when only a single pluralization key is overridden" do - override_translation('en', 'keys.magic.other', 'no magic keys') - expect(I18n.t('keys.magic', count: 1)).to eq('one magic key') - expect(I18n.t('keys.magic', count: 2)).to eq('no magic keys') + override_translation("en", "keys.magic.other", "no magic keys") + expect(I18n.t("keys.magic", count: 1)).to eq("one magic key") + expect(I18n.t("keys.magic", count: 2)).to eq("no magic keys") end it "returns the overridden text when falling back" do - override_translation('en', 'got', 'summer') - expect(I18n.t('got')).to eq('summer') - expect(I18n.with_locale(:zh_TW) { I18n.t('got') }).to eq('summer') + override_translation("en", "got", "summer") + expect(I18n.t("got")).to eq("summer") + expect(I18n.with_locale(:zh_TW) { I18n.t("got") }).to eq("summer") - override_translation('en', 'throne', '%{title} is the new queen') - expect(I18n.t('throne', title: 'snow')).to eq('snow is the new queen') - expect(I18n.with_locale(:en) { I18n.t('throne', title: 'snow') }) - .to eq('snow is the new queen') + override_translation("en", "throne", "%{title} is the new queen") + expect(I18n.t("throne", title: "snow")).to eq("snow is the new queen") + expect(I18n.with_locale(:en) { I18n.t("throne", title: "snow") }).to eq( + "snow is the new queen", + ) end it "returns override if it exists before falling back" do - expect(I18n.t('got', default: '')).to eq('winter') - expect(I18n.with_locale(:ru) { I18n.t('got', default: '') }).to eq('winter') + expect(I18n.t("got", default: "")).to eq("winter") + expect(I18n.with_locale(:ru) { I18n.t("got", default: "") }).to eq("winter") - override_translation('ru', 'got', 'summer') - expect(I18n.t('got', default: '')).to eq('winter') - expect(I18n.with_locale(:ru) { I18n.t('got', default: '') }).to eq('summer') + override_translation("ru", "got", "summer") + expect(I18n.t("got", default: "")).to eq("winter") + expect(I18n.with_locale(:ru) { I18n.t("got", default: "") }).to eq("summer") end it "does not affect ActiveModel::Naming#human" do Fish = Class.new(ActiveRecord::Base) - override_translation('en', 'fish', 'fake fish') - expect(Fish.model_name.human).to eq('Fish') + override_translation("en", "fish", "fake fish") + expect(Fish.model_name.human).to eq("Fish") end it "works when the override contains an interpolation key" do expect(I18n.t("foo_with_variable")).to eq("Foo in :en with %{variable}") - I18n.with_locale(:de) { expect(I18n.t("foo_with_variable")).to eq("Foo in :de with %{variable}") } + I18n.with_locale(:de) do + expect(I18n.t("foo_with_variable")).to eq("Foo in :de with %{variable}") + end override_translation("en", "foo_with_variable", "Override in :en with %{variable}") expect(I18n.t("foo_with_variable")).to eq("Override in :en with %{variable}") - I18n.with_locale(:de) { expect(I18n.t("foo_with_variable")).to eq("Foo in :de with %{variable}") } + I18n.with_locale(:de) do + expect(I18n.t("foo_with_variable")).to eq("Foo in :de with %{variable}") + end override_translation("de", "foo_with_variable", "Override in :de with %{variable}") expect(I18n.t("foo_with_variable")).to eq("Override in :en with %{variable}") - I18n.with_locale(:de) { expect(I18n.t("foo_with_variable")).to eq("Override in :de with %{variable}") } + I18n.with_locale(:de) do + expect(I18n.t("foo_with_variable")).to eq("Override in :de with %{variable}") + end end end diff --git a/spec/lib/gaps_spec.rb b/spec/lib/gaps_spec.rb index aeab2f87e5..974886d4a1 100644 --- a/spec/lib/gaps_spec.rb +++ b/spec/lib/gaps_spec.rb @@ -1,24 +1,24 @@ # frozen_string_literal: true -require 'cache' +require "cache" RSpec.describe Gaps do - it 'returns no gaps for empty data' do + it "returns no gaps for empty data" do expect(Gaps.new(nil, nil)).to be_blank end - it 'returns no gaps with one element' do + it "returns no gaps with one element" do expect(Gaps.new([1], [1])).to be_blank end - it 'returns no gaps when all elements are present' do + it "returns no gaps when all elements are present" do expect(Gaps.new([1, 2, 3], [1, 2, 3])).to be_blank end describe "single element gap" do let(:gap) { Gaps.new([1, 3], [1, 2, 3]) } - it 'has a gap for post 3' do + it "has a gap for post 3" do expect(gap).not_to be_blank expect(gap.before[3]).to eq([2]) expect(gap.after).to be_blank @@ -28,7 +28,7 @@ RSpec.describe Gaps do describe "larger gap" do let(:gap) { Gaps.new([1, 2, 3, 6, 7], [1, 2, 3, 4, 5, 6, 7]) } - it 'has a gap for post 6' do + it "has a gap for post 6" do expect(gap).not_to be_blank expect(gap.before[6]).to eq([4, 5]) expect(gap.after).to be_blank @@ -38,7 +38,7 @@ RSpec.describe Gaps do describe "multiple gaps" do let(:gap) { Gaps.new([1, 5, 6, 7, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) } - it 'has both gaps' do + it "has both gaps" do expect(gap).not_to be_blank expect(gap.before[5]).to eq([2, 3, 4]) expect(gap.before[10]).to eq([8, 9]) @@ -49,7 +49,7 @@ RSpec.describe Gaps do describe "a gap in the beginning" do let(:gap) { Gaps.new([2, 3, 4], [1, 2, 3, 4]) } - it 'has the gap' do + it "has the gap" do expect(gap).not_to be_blank expect(gap.before[2]).to eq([1]) expect(gap.after).to be_blank @@ -59,7 +59,7 @@ RSpec.describe Gaps do describe "a gap in the ending" do let(:gap) { Gaps.new([1, 2, 3], [1, 2, 3, 4]) } - it 'has the gap' do + it "has the gap" do expect(gap).not_to be_blank expect(gap.before).to be_blank expect(gap.after[3]).to eq([4]) @@ -69,7 +69,7 @@ RSpec.describe Gaps do describe "a large gap in the ending" do let(:gap) { Gaps.new([1, 2, 3], [1, 2, 3, 4, 5, 6]) } - it 'has the gap' do + it "has the gap" do expect(gap).not_to be_blank expect(gap.before).to be_blank expect(gap.after[3]).to eq([4, 5, 6]) diff --git a/spec/lib/git_url_spec.rb b/spec/lib/git_url_spec.rb index 5d7ab3f964..a6cd1bca59 100644 --- a/spec/lib/git_url_spec.rb +++ b/spec/lib/git_url_spec.rb @@ -3,13 +3,13 @@ RSpec.describe GitUrl do it "handles the discourse github repo by ssh" do expect(GitUrl.normalize("git@github.com:discourse/discourse.git")).to eq( - "ssh://git@github.com/discourse/discourse.git" + "ssh://git@github.com/discourse/discourse.git", ) end it "handles the discourse github repo by https" do expect(GitUrl.normalize("https://github.com/discourse/discourse.git")).to eq( - "https://github.com/discourse/discourse.git" + "https://github.com/discourse/discourse.git", ) end end diff --git a/spec/lib/global_path_spec.rb b/spec/lib/global_path_spec.rb index 9846dca06f..61c0cecd7c 100644 --- a/spec/lib/global_path_spec.rb +++ b/spec/lib/global_path_spec.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -require 'global_path' +require "global_path" class GlobalPathInstance extend GlobalPath end RSpec.describe GlobalPath do - describe '.cdn_relative_path' do + describe ".cdn_relative_path" do def cdn_relative_path(p) GlobalPathInstance.cdn_relative_path(p) end @@ -27,16 +27,19 @@ RSpec.describe GlobalPath do end end - describe '.upload_cdn_path' do - it 'generates correctly when S3 bucket has a folder' do - global_setting :s3_access_key_id, 's3_access_key_id' - global_setting :s3_secret_access_key, 's3_secret_access_key' - global_setting :s3_bucket, 'file-uploads/folder' - global_setting :s3_region, 'us-west-2' - global_setting :s3_cdn_url, 'https://cdn-aws.com/folder' + describe ".upload_cdn_path" do + it "generates correctly when S3 bucket has a folder" do + global_setting :s3_access_key_id, "s3_access_key_id" + global_setting :s3_secret_access_key, "s3_secret_access_key" + global_setting :s3_bucket, "file-uploads/folder" + global_setting :s3_region, "us-west-2" + global_setting :s3_cdn_url, "https://cdn-aws.com/folder" - expect(GlobalPathInstance.upload_cdn_path("#{Discourse.store.absolute_base_url}/folder/upload.jpg")) - .to eq("https://cdn-aws.com/folder/upload.jpg") + expect( + GlobalPathInstance.upload_cdn_path( + "#{Discourse.store.absolute_base_url}/folder/upload.jpg", + ), + ).to eq("https://cdn-aws.com/folder/upload.jpg") end end end diff --git a/spec/lib/group_email_credentials_check_spec.rb b/spec/lib/group_email_credentials_check_spec.rb index 074b540285..603c1a44df 100644 --- a/spec/lib/group_email_credentials_check_spec.rb +++ b/spec/lib/group_email_credentials_check_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'net/smtp' -require 'net/imap' +require "net/smtp" +require "net/imap" RSpec.describe GroupEmailCredentialsCheck do fab!(:group1) { Fabricate(:group) } @@ -29,35 +29,43 @@ RSpec.describe GroupEmailCredentialsCheck do end it "returns an error message and the group ID if the group's SMTP settings error" do - EmailSettingsValidator.expects(:validate_smtp).raises( - Net::SMTPAuthenticationError.new("bad credentials") - ).then.returns(true).at_least_once + EmailSettingsValidator + .expects(:validate_smtp) + .raises(Net::SMTPAuthenticationError.new("bad credentials")) + .then + .returns(true) + .at_least_once EmailSettingsValidator.stubs(:validate_imap).returns(true) - expect(described_class.run).to eq([ - { - group_full_name: group2.full_name, - group_name: group2.name, - group_id: group2.id, - message: I18n.t("email_settings.smtp_authentication_error") - } - ]) + expect(described_class.run).to eq( + [ + { + group_full_name: group2.full_name, + group_name: group2.name, + group_id: group2.id, + message: I18n.t("email_settings.smtp_authentication_error"), + }, + ], + ) end it "returns an error message and the group ID if the group's IMAP settings error" do EmailSettingsValidator.stubs(:validate_smtp).returns(true) - EmailSettingsValidator.expects(:validate_imap).raises( - Net::IMAP::NoResponseError.new(stub(data: stub(text: "Invalid credentials"))) - ).once + EmailSettingsValidator + .expects(:validate_imap) + .raises(Net::IMAP::NoResponseError.new(stub(data: stub(text: "Invalid credentials")))) + .once - expect(described_class.run).to eq([ - { - group_full_name: group3.full_name, - group_name: group3.name, - group_id: group3.id, - message: I18n.t("email_settings.imap_authentication_error") - } - ]) + expect(described_class.run).to eq( + [ + { + group_full_name: group3.full_name, + group_name: group3.name, + group_id: group3.id, + message: I18n.t("email_settings.imap_authentication_error"), + }, + ], + ) end it "returns no imap errors if imap is disabled for the site" do diff --git a/spec/lib/guardian/topic_guardian_spec.rb b/spec/lib/guardian/topic_guardian_spec.rb index e014482120..94ff370848 100644 --- a/spec/lib/guardian/topic_guardian_spec.rb +++ b/spec/lib/guardian/topic_guardian_spec.rb @@ -12,109 +12,105 @@ RSpec.describe TopicGuardian do fab!(:private_topic) { Fabricate(:topic, category: private_category) } fab!(:private_message_topic) { Fabricate(:private_message_topic) } - before do - Guardian.enable_topic_can_see_consistency_check - end + before { Guardian.enable_topic_can_see_consistency_check } - after do - Guardian.disable_topic_can_see_consistency_check - end + after { Guardian.disable_topic_can_see_consistency_check } - describe '#can_create_shared_draft?' do - it 'when shared_drafts are disabled' do - SiteSetting.shared_drafts_min_trust_level = 'admin' + describe "#can_create_shared_draft?" do + it "when shared_drafts are disabled" do + SiteSetting.shared_drafts_min_trust_level = "admin" expect(Guardian.new(admin).can_create_shared_draft?).to eq(false) end - it 'when user is a moderator and access is set to admin' do + it "when user is a moderator and access is set to admin" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = 'admin' + SiteSetting.shared_drafts_min_trust_level = "admin" expect(Guardian.new(moderator).can_create_shared_draft?).to eq(false) end - it 'when user is a moderator and access is set to staff' do + it "when user is a moderator and access is set to staff" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = 'staff' + SiteSetting.shared_drafts_min_trust_level = "staff" expect(Guardian.new(moderator).can_create_shared_draft?).to eq(true) end - it 'when user is TL3 and access is set to TL2' do + it "when user is TL3 and access is set to TL2" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = '2' + SiteSetting.shared_drafts_min_trust_level = "2" expect(Guardian.new(tl3_user).can_create_shared_draft?).to eq(true) end end - describe '#can_see_shared_draft?' do - it 'when shared_drafts are disabled (existing shared drafts)' do - SiteSetting.shared_drafts_min_trust_level = 'admin' + describe "#can_see_shared_draft?" do + it "when shared_drafts are disabled (existing shared drafts)" do + SiteSetting.shared_drafts_min_trust_level = "admin" expect(Guardian.new(admin).can_see_shared_draft?).to eq(true) end - it 'when user is a moderator and access is set to admin' do + it "when user is a moderator and access is set to admin" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = 'admin' + SiteSetting.shared_drafts_min_trust_level = "admin" expect(Guardian.new(moderator).can_see_shared_draft?).to eq(false) end - it 'when user is a moderator and access is set to staff' do + it "when user is a moderator and access is set to staff" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = 'staff' + SiteSetting.shared_drafts_min_trust_level = "staff" expect(Guardian.new(moderator).can_see_shared_draft?).to eq(true) end - it 'when user is TL3 and access is set to TL2' do + it "when user is TL3 and access is set to TL2" do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = '2' + SiteSetting.shared_drafts_min_trust_level = "2" expect(Guardian.new(tl3_user).can_see_shared_draft?).to eq(true) end end - describe '#can_edit_topic?' do - context 'when the topic is a shared draft' do - let(:tl2_user) { Fabricate(:user, trust_level: TrustLevel[2]) } + describe "#can_edit_topic?" do + context "when the topic is a shared draft" do + let(:tl2_user) { Fabricate(:user, trust_level: TrustLevel[2]) } before do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = '2' + SiteSetting.shared_drafts_min_trust_level = "2" end - it 'returns false if the topic is a PM' do + it "returns false if the topic is a PM" do pm_with_draft = Fabricate(:private_message_topic, category: category) Fabricate(:shared_draft, topic: pm_with_draft) expect(Guardian.new(tl2_user).can_edit_topic?(pm_with_draft)).to eq(false) end - it 'returns false if the topic is archived' do + it "returns false if the topic is archived" do archived_topic = Fabricate(:topic, archived: true, category: category) Fabricate(:shared_draft, topic: archived_topic) expect(Guardian.new(tl2_user).can_edit_topic?(archived_topic)).to eq(false) end - it 'returns true if a shared draft exists' do + it "returns true if a shared draft exists" do Fabricate(:shared_draft, topic: topic) expect(Guardian.new(tl2_user).can_edit_topic?(topic)).to eq(true) end - it 'returns false if the user has a lower trust level' do + it "returns false if the user has a lower trust level" do tl1_user = Fabricate(:user, trust_level: TrustLevel[1]) Fabricate(:shared_draft, topic: topic) expect(Guardian.new(tl1_user).can_edit_topic?(topic)).to eq(false) end - it 'returns true if the shared_draft is from a different category' do + it "returns true if the shared_draft is from a different category" do topic = Fabricate(:topic, category: Fabricate(:category)) Fabricate(:shared_draft, topic: topic) @@ -123,8 +119,8 @@ RSpec.describe TopicGuardian do end end - describe '#can_review_topic?' do - it 'returns false for TL4 users' do + describe "#can_review_topic?" do + it "returns false for TL4 users" do tl4_user = Fabricate(:user, trust_level: TrustLevel[4]) topic = Fabricate(:topic) @@ -151,27 +147,26 @@ RSpec.describe TopicGuardian do # The test cases here are intentionally kept brief because majority of the cases are already handled by # `TopicGuardianCanSeeConsistencyCheck` which we run to ensure that the implementation between `TopicGuardian#can_see_topic_ids` # and `TopicGuardian#can_see_topic?` is consistent. - describe '#can_see_topic_ids' do - it 'returns the topic ids for the topics which a user is allowed to see' do - expect(Guardian.new.can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id])).to contain_exactly( - topic.id - ) + describe "#can_see_topic_ids" do + it "returns the topic ids for the topics which a user is allowed to see" do + expect( + Guardian.new.can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id]), + ).to contain_exactly(topic.id) - expect(Guardian.new(user).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id])).to contain_exactly( - topic.id - ) + expect( + Guardian.new(user).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id]), + ).to contain_exactly(topic.id) - expect(Guardian.new(moderator).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id])).to contain_exactly( - topic.id, - ) + expect( + Guardian.new(moderator).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id]), + ).to contain_exactly(topic.id) - expect(Guardian.new(admin).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id])).to contain_exactly( - topic.id, - private_message_topic.id - ) + expect( + Guardian.new(admin).can_see_topic_ids(topic_ids: [topic.id, private_message_topic.id]), + ).to contain_exactly(topic.id, private_message_topic.id) end - it 'returns the topic ids for topics which are deleted but user is a category moderator of' do + it "returns the topic ids for topics which are deleted but user is a category moderator of" do SiteSetting.enable_category_group_moderation = true category.update!(reviewable_by_group_id: group.id) @@ -182,20 +177,18 @@ RSpec.describe TopicGuardian do topic2 = Fabricate(:topic) user2 = Fabricate(:user) - expect(Guardian.new(user).can_see_topic_ids(topic_ids: [topic.id, topic2.id])).to contain_exactly( - topic.id, - topic2.id - ) + expect( + Guardian.new(user).can_see_topic_ids(topic_ids: [topic.id, topic2.id]), + ).to contain_exactly(topic.id, topic2.id) - expect(Guardian.new(user2).can_see_topic_ids(topic_ids: [topic.id, topic2.id])).to contain_exactly( - topic2.id, - ) + expect( + Guardian.new(user2).can_see_topic_ids(topic_ids: [topic.id, topic2.id]), + ).to contain_exactly(topic2.id) end end - describe '#filter_allowed_categories' do - - it 'allows admin access to categories without explicit access' do + describe "#filter_allowed_categories" do + it "allows admin access to categories without explicit access" do guardian = Guardian.new(admin) list = Topic.where(id: private_topic.id) list = guardian.filter_allowed_categories(list) @@ -203,12 +196,10 @@ RSpec.describe TopicGuardian do expect(list.count).to eq(1) end - context 'when SiteSetting.suppress_secured_categories_from_admin is true' do - before do - SiteSetting.suppress_secured_categories_from_admin = true - end + context "when SiteSetting.suppress_secured_categories_from_admin is true" do + before { SiteSetting.suppress_secured_categories_from_admin = true } - it 'does not allow admin access to categories without explicit access' do + it "does not allow admin access to categories without explicit access" do guardian = Guardian.new(admin) list = Topic.where(id: private_topic.id) list = guardian.filter_allowed_categories(list) @@ -216,6 +207,5 @@ RSpec.describe TopicGuardian do expect(list.count).to eq(0) end end - end end diff --git a/spec/lib/guardian/user_guardian_spec.rb b/spec/lib/guardian/user_guardian_spec.rb index 3b10a6fbe1..b6b0ab0e80 100644 --- a/spec/lib/guardian/user_guardian_spec.rb +++ b/spec/lib/guardian/user_guardian_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true RSpec.describe UserGuardian do - let :user do Fabricate(:user) end @@ -14,9 +13,7 @@ RSpec.describe UserGuardian do Fabricate(:admin) end - let(:user_avatar) do - Fabricate(:user_avatar, user: user) - end + let(:user_avatar) { Fabricate(:user_avatar, user: user) } let :users_upload do Upload.new(user_id: user_avatar.user_id, id: 1) @@ -32,20 +29,17 @@ RSpec.describe UserGuardian do Upload.new(user_id: 9999, id: 3) end - let(:moderator_upload) do - Upload.new(user_id: moderator.id, id: 4) - end + let(:moderator_upload) { Upload.new(user_id: moderator.id, id: 4) } let(:trust_level_1) { build(:user, trust_level: 1) } let(:trust_level_2) { build(:user, trust_level: 2) } - describe '#can_pick_avatar?' do - + describe "#can_pick_avatar?" do let :guardian do Guardian.new(user) end - context 'with anon user' do + context "with anon user" do let(:guardian) { Guardian.new } it "should return the right value" do @@ -53,32 +47,25 @@ RSpec.describe UserGuardian do end end - context 'with current user' do + context "with current user" do it "can not set uploads not owned by current user" do expect(guardian.can_pick_avatar?(user_avatar, users_upload)).to eq(true) expect(guardian.can_pick_avatar?(user_avatar, already_uploaded)).to eq(true) - UserUpload.create!( - upload_id: not_my_upload.id, - user_id: not_my_upload.user_id - ) + UserUpload.create!(upload_id: not_my_upload.id, user_id: not_my_upload.user_id) expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)).to eq(false) expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true) end it "can handle uploads that are associated but not directly owned" do - UserUpload.create!( - upload_id: not_my_upload.id, - user_id: user_avatar.user_id - ) + UserUpload.create!(upload_id: not_my_upload.id, user_id: user_avatar.user_id) - expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)) - .to eq(true) + expect(guardian.can_pick_avatar?(user_avatar, not_my_upload)).to eq(true) end end - context 'with moderator' do + context "with moderator" do let :guardian do Guardian.new(moderator) end @@ -92,7 +79,7 @@ RSpec.describe UserGuardian do end end - context 'with admin' do + context "with admin" do let :guardian do Guardian.new(admin) end @@ -153,7 +140,7 @@ RSpec.describe UserGuardian do Fabricate(:user_field), Fabricate(:user_field, show_on_profile: true), Fabricate(:user_field, show_on_user_card: true), - Fabricate(:user_field, show_on_user_card: true, show_on_profile: true) + Fabricate(:user_field, show_on_user_card: true, show_on_profile: true), ] end @@ -198,9 +185,7 @@ RSpec.describe UserGuardian do end context "when user created too many posts" do - before do - (User::MAX_STAFF_DELETE_POST_COUNT + 1).times { Fabricate(:post, user: user) } - end + before { (User::MAX_STAFF_DELETE_POST_COUNT + 1).times { Fabricate(:post, user: user) } } it "is allowed when user created the first post within delete_user_max_post_age days" do SiteSetting.delete_user_max_post_age = 2 @@ -214,9 +199,7 @@ RSpec.describe UserGuardian do end context "when user didn't create many posts" do - before do - (User::MAX_STAFF_DELETE_POST_COUNT - 1).times { Fabricate(:post, user: user) } - end + before { (User::MAX_STAFF_DELETE_POST_COUNT - 1).times { Fabricate(:post, user: user) } } it "is allowed when even when user created the first post before delete_user_max_post_age days" do SiteSetting.delete_user_max_post_age = 2 @@ -258,10 +241,15 @@ RSpec.describe UserGuardian do end it "is allowed when user responded to PM from system user" do - topic = Fabricate(:private_message_topic, user: Discourse.system_user, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: Discourse.system_user), - Fabricate.build(:topic_allowed_user, user: user) - ]) + topic = + Fabricate( + :private_message_topic, + user: Discourse.system_user, + topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: Discourse.system_user), + Fabricate.build(:topic_allowed_user, user: user), + ], + ) Fabricate(:post, user: user, topic: topic) expect(guardian.can_delete_user?(user)).to eq(true) @@ -271,9 +259,12 @@ RSpec.describe UserGuardian do end it "is allowed when user created multiple posts in PMs to themselves" do - topic = Fabricate(:private_message_topic, user: user, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: user) - ]) + topic = + Fabricate( + :private_message_topic, + user: user, + topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], + ) Fabricate(:post, user: user, topic: topic) Fabricate(:post, user: user, topic: topic) @@ -281,10 +272,15 @@ RSpec.describe UserGuardian do end it "isn't allowed when user created multiple posts in PMs sent to other users" do - topic = Fabricate(:private_message_topic, user: user, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: user), - Fabricate.build(:topic_allowed_user, user: Fabricate(:user)) - ]) + topic = + Fabricate( + :private_message_topic, + user: user, + topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: user), + Fabricate.build(:topic_allowed_user, user: Fabricate(:user)), + ], + ) Fabricate(:post, user: user, topic: topic) expect(guardian.can_delete_user?(user)).to eq(true) @@ -294,12 +290,16 @@ RSpec.describe UserGuardian do end it "isn't allowed when user created multiple posts in PMs sent to groups" do - topic = Fabricate(:private_message_topic, user: user, topic_allowed_users: [ - Fabricate.build(:topic_allowed_user, user: user) - ], topic_allowed_groups: [ - Fabricate.build(:topic_allowed_group, group: Fabricate(:group)), - Fabricate.build(:topic_allowed_group, group: Fabricate(:group)) - ]) + topic = + Fabricate( + :private_message_topic, + user: user, + topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], + topic_allowed_groups: [ + Fabricate.build(:topic_allowed_group, group: Fabricate(:group)), + Fabricate.build(:topic_allowed_group, group: Fabricate(:group)), + ], + ) Fabricate(:post, user: user, topic: topic) expect(guardian.can_delete_user?(user)).to eq(true) @@ -372,12 +372,12 @@ RSpec.describe UserGuardian do end describe "#can_see_review_queue?" do - it 'returns true when the user is a staff member' do + it "returns true when the user is a staff member" do guardian = Guardian.new(moderator) expect(guardian.can_see_review_queue?).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do guardian = Guardian.new(user) expect(guardian.can_see_review_queue?).to eq(false) end @@ -393,7 +393,7 @@ RSpec.describe UserGuardian do expect(guardian.can_see_review_queue?).to eq(true) end - it 'returns false if category group review is disabled' do + it "returns false if category group review is disabled" do group = Fabricate(:group) group.add(user) guardian = Guardian.new(user) @@ -404,7 +404,7 @@ RSpec.describe UserGuardian do expect(guardian.can_see_review_queue?).to eq(false) end - it 'returns false if the reviewable is under a read restricted category' do + it "returns false if the reviewable is under a read restricted category" do group = Fabricate(:group) group.add(user) guardian = Guardian.new(user) @@ -417,38 +417,38 @@ RSpec.describe UserGuardian do end end - describe 'can_upload_profile_header' do - it 'returns true if it is an admin' do + describe "can_upload_profile_header" do + it "returns true if it is an admin" do guardian = Guardian.new(admin) expect(guardian.can_upload_profile_header?(admin)).to eq(true) end - it 'returns true if the trust level of user matches site setting' do + it "returns true if the trust level of user matches site setting" do guardian = Guardian.new(trust_level_2) SiteSetting.min_trust_level_to_allow_profile_background = 2 expect(guardian.can_upload_profile_header?(trust_level_2)).to eq(true) end - it 'returns false if the trust level of user does not matches site setting' do + it "returns false if the trust level of user does not matches site setting" do guardian = Guardian.new(trust_level_1) SiteSetting.min_trust_level_to_allow_profile_background = 2 expect(guardian.can_upload_profile_header?(trust_level_1)).to eq(false) end end - describe 'can_upload_user_card_background' do - it 'returns true if it is an admin' do + describe "can_upload_user_card_background" do + it "returns true if it is an admin" do guardian = Guardian.new(admin) expect(guardian.can_upload_user_card_background?(admin)).to eq(true) end - it 'returns true if the trust level of user matches site setting' do + it "returns true if the trust level of user matches site setting" do guardian = Guardian.new(trust_level_2) SiteSetting.min_trust_level_to_allow_user_card_background = 2 expect(guardian.can_upload_user_card_background?(trust_level_2)).to eq(true) end - it 'returns false if the trust level of user does not matches site setting' do + it "returns false if the trust level of user does not matches site setting" do guardian = Guardian.new(trust_level_1) SiteSetting.min_trust_level_to_allow_user_card_background = 2 expect(guardian.can_upload_user_card_background?(trust_level_1)).to eq(false) diff --git a/spec/lib/guardian_spec.rb b/spec/lib/guardian_spec.rb index f386344328..4e5bf8d88f 100644 --- a/spec/lib/guardian_spec.rb +++ b/spec/lib/guardian_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Guardian do fab!(:trust_level_1) { Fabricate(:user, trust_level: 1) } fab!(:trust_level_2) { Fabricate(:user, trust_level: 2) } fab!(:trust_level_3) { Fabricate(:user, trust_level: 3) } - fab!(:trust_level_4) { Fabricate(:user, trust_level: 4) } + fab!(:trust_level_4) { Fabricate(:user, trust_level: 4) } fab!(:another_admin) { Fabricate(:admin) } fab!(:coding_horror) { Fabricate(:coding_horror) } @@ -30,38 +30,36 @@ RSpec.describe Guardian do Guardian.enable_topic_can_see_consistency_check end - after do - Guardian.disable_topic_can_see_consistency_check - end + after { Guardian.disable_topic_can_see_consistency_check } - it 'can be created without a user (not logged in)' do + it "can be created without a user (not logged in)" do expect { Guardian.new }.not_to raise_error end - it 'can be instantiated with a user instance' do + it "can be instantiated with a user instance" do expect { Guardian.new(user) }.not_to raise_error end describe "link_posting_access" do it "is none for anonymous users" do - expect(Guardian.new.link_posting_access).to eq('none') + expect(Guardian.new.link_posting_access).to eq("none") end it "is full for regular users" do - expect(Guardian.new(user).link_posting_access).to eq('full') + expect(Guardian.new(user).link_posting_access).to eq("full") end it "is none for a user of a low trust level" do user.trust_level = 0 SiteSetting.min_trust_to_post_links = 1 - expect(Guardian.new(user).link_posting_access).to eq('none') + expect(Guardian.new(user).link_posting_access).to eq("none") end it "is limited for a user of a low trust level with a allowlist" do - SiteSetting.allowed_link_domains = 'example.com' + SiteSetting.allowed_link_domains = "example.com" user.trust_level = 0 SiteSetting.min_trust_to_post_links = 1 - expect(Guardian.new(user).link_posting_access).to eq('limited') + expect(Guardian.new(user).link_posting_access).to eq("limited") end end @@ -85,20 +83,18 @@ RSpec.describe Guardian do end describe "allowlisted host" do - before do - SiteSetting.allowed_link_domains = host - end + before { SiteSetting.allowed_link_domains = host } it "allows a new user to post the link to the host" do user.trust_level = 0 SiteSetting.min_trust_to_post_links = 1 expect(Guardian.new(user).can_post_link?(host: host)).to eq(true) - expect(Guardian.new(user).can_post_link?(host: 'another-host.com')).to eq(false) + expect(Guardian.new(user).can_post_link?(host: "another-host.com")).to eq(false) end end end - describe '#post_can_act?' do + describe "#post_can_act?" do fab!(:user) { Fabricate(:user) } fab!(:post) { Fabricate(:post) } @@ -145,10 +141,8 @@ RSpec.describe Guardian do expect(Guardian.new(user).post_can_act?(staff_post, :spam)).to be_truthy end - describe 'when allow_flagging_staff is false' do - before do - SiteSetting.allow_flagging_staff = false - end + describe "when allow_flagging_staff is false" do + before { SiteSetting.allow_flagging_staff = false } it "doesn't allow flagging of staff posts" do expect(Guardian.new(user).post_can_act?(staff_post, :spam)).to eq(false) @@ -170,16 +164,32 @@ RSpec.describe Guardian do end it "returns false when you've already done it" do - expect(Guardian.new(user).post_can_act?(post, :like, opts: { - taken_actions: { PostActionType.types[:like] => 1 } - })).to be_falsey + expect( + Guardian.new(user).post_can_act?( + post, + :like, + opts: { + taken_actions: { + PostActionType.types[:like] => 1, + }, + }, + ), + ).to be_falsey end it "returns false when you already flagged a post" do PostActionType.notify_flag_types.each do |type, _id| - expect(Guardian.new(user).post_can_act?(post, :off_topic, opts: { - taken_actions: { PostActionType.types[type] => 1 } - })).to be_falsey + expect( + Guardian.new(user).post_can_act?( + post, + :off_topic, + opts: { + taken_actions: { + PostActionType.types[type] => 1, + }, + }, + ), + ).to be_falsey end end @@ -231,9 +241,7 @@ RSpec.describe Guardian do fab!(:moderator) { Fabricate(:moderator) } context "when enabled" do - before do - SiteSetting.enable_safe_mode = true - end + before { SiteSetting.enable_safe_mode = true } it "can be performed" do expect(Guardian.new.can_enable_safe_mode?).to eq(true) @@ -243,9 +251,7 @@ RSpec.describe Guardian do end context "when disabled" do - before do - SiteSetting.enable_safe_mode = false - end + before { SiteSetting.enable_safe_mode = false } it "can be performed" do expect(Guardian.new.can_enable_safe_mode?).to eq(false) @@ -255,8 +261,10 @@ RSpec.describe Guardian do end end - describe 'can_send_private_message' do - fab!(:suspended_user) { Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) } + describe "can_send_private_message" do + fab!(:suspended_user) do + Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) + end it "returns false when the user is nil" do expect(Guardian.new(nil).can_send_private_message?(user)).to be_falsey @@ -291,9 +299,7 @@ RSpec.describe Guardian do context "when personal_message_enabled_groups does not contain the user" do let(:group) { Fabricate(:group) } - before do - SiteSetting.personal_message_enabled_groups = group.id - end + before { SiteSetting.personal_message_enabled_groups = group.id } it "returns false if user is not staff member" do expect(Guardian.new(trust_level_4).can_send_private_message?(another_user)).to be_falsey @@ -312,7 +318,9 @@ RSpec.describe Guardian do end it "returns true for system user" do - expect(Guardian.new(Discourse.system_user).can_send_private_message?(another_user)).to be_truthy + expect( + Guardian.new(Discourse.system_user).can_send_private_message?(another_user), + ).to be_truthy end end @@ -356,7 +364,7 @@ RSpec.describe Guardian do group.update!(messageable_level: Group::ALIAS_LEVELS[level]) user_output = level == :everyone ? true : false admin_output = level != :nobody - mod_output = [:nobody, :only_admins].exclude?(level) + mod_output = %i[nobody only_admins].exclude?(level) expect(Guardian.new(user).can_send_private_message?(group)).to eq(user_output) expect(Guardian.new(admin).can_send_private_message?(group)).to eq(admin_output) @@ -398,30 +406,29 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_send_private_message?(group)).to eq(true) end - context 'when target user has private message disabled' do - before do - another_user.user_option.update!(allow_private_messages: false) - end + context "when target user has private message disabled" do + before { another_user.user_option.update!(allow_private_messages: false) } - context 'for a normal user' do - it 'should return false' do + context "for a normal user" do + it "should return false" do expect(Guardian.new(user).can_send_private_message?(another_user)).to eq(false) end end - context 'for a staff user' do - it 'should return true' do + context "for a staff user" do + it "should return true" do [admin, moderator].each do |staff_user| - expect(Guardian.new(staff_user).can_send_private_message?(another_user)) - .to eq(true) + expect(Guardian.new(staff_user).can_send_private_message?(another_user)).to eq(true) end end end end end - describe 'can_send_private_messages' do - fab!(:suspended_user) { Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) } + describe "can_send_private_messages" do + fab!(:suspended_user) do + Fabricate(:user, suspended_till: 1.week.from_now, suspended_at: 1.day.ago) + end it "returns false when the user is nil" do expect(Guardian.new(nil).can_send_private_messages?).to be_falsey @@ -441,9 +448,7 @@ RSpec.describe Guardian do context "when personal_message_enabled_groups does contain the user" do let(:group) { Fabricate(:group) } - before do - SiteSetting.personal_message_enabled_groups = group.id - end + before { SiteSetting.personal_message_enabled_groups = group.id } it "returns true" do expect(Guardian.new(user).can_send_private_messages?).to be_falsey @@ -455,9 +460,7 @@ RSpec.describe Guardian do context "when personal_message_enabled_groups does not contain the user" do let(:group) { Fabricate(:group) } - before do - SiteSetting.personal_message_enabled_groups = group.id - end + before { SiteSetting.personal_message_enabled_groups = group.id } it "returns false if user is not staff member" do expect(Guardian.new(trust_level_4).can_send_private_messages?).to be_falsey @@ -492,7 +495,7 @@ RSpec.describe Guardian do end end - describe 'can_reply_as_new_topic' do + describe "can_reply_as_new_topic" do fab!(:topic) { Fabricate(:topic) } fab!(:private_message) { Fabricate(:private_message_topic) } @@ -518,11 +521,10 @@ RSpec.describe Guardian do end end - describe 'can_see_post_actors?' do - + describe "can_see_post_actors?" do let(:topic) { Fabricate(:topic, user: coding_horror) } - it 'displays visibility correctly' do + it "displays visibility correctly" do guardian = Guardian.new(user) expect(guardian.can_see_post_actors?(nil, PostActionType.types[:like])).to be_falsey expect(guardian.can_see_post_actors?(topic, PostActionType.types[:like])).to be_truthy @@ -530,13 +532,14 @@ RSpec.describe Guardian do expect(guardian.can_see_post_actors?(topic, PostActionType.types[:spam])).to be_falsey expect(guardian.can_see_post_actors?(topic, PostActionType.types[:notify_user])).to be_falsey - expect(Guardian.new(moderator).can_see_post_actors?(topic, PostActionType.types[:notify_user])).to be_truthy + expect( + Guardian.new(moderator).can_see_post_actors?(topic, PostActionType.types[:notify_user]), + ).to be_truthy end - end - describe 'can_impersonate?' do - it 'allows impersonation correctly' do + describe "can_impersonate?" do + it "allows impersonation correctly" do expect(Guardian.new(admin).can_impersonate?(nil)).to be_falsey expect(Guardian.new.can_impersonate?(user)).to be_falsey expect(Guardian.new(coding_horror).can_impersonate?(user)).to be_falsey @@ -551,30 +554,30 @@ RSpec.describe Guardian do end describe "can_view_action_logs?" do - it 'is false for non-staff acting user' do + it "is false for non-staff acting user" do expect(Guardian.new(user).can_view_action_logs?(moderator)).to be_falsey end - it 'is false without a target user' do + it "is false without a target user" do expect(Guardian.new(moderator).can_view_action_logs?(nil)).to be_falsey end - it 'is true when target user is present' do + it "is true when target user is present" do expect(Guardian.new(moderator).can_view_action_logs?(user)).to be_truthy end end - describe 'can_invite_to_forum?' do + describe "can_invite_to_forum?" do fab!(:user) { Fabricate(:user) } fab!(:moderator) { Fabricate(:moderator) } - it 'returns true if user has sufficient trust level' do + it "returns true if user has sufficient trust level" do SiteSetting.min_trust_level_to_allow_invite = 2 expect(Guardian.new(trust_level_2).can_invite_to_forum?).to be_truthy expect(Guardian.new(moderator).can_invite_to_forum?).to be_truthy end - it 'returns false if user trust level does not have sufficient trust level' do + it "returns false if user trust level does not have sufficient trust level" do SiteSetting.min_trust_level_to_allow_invite = 2 expect(Guardian.new(trust_level_1).can_invite_to_forum?).to be_falsey end @@ -583,12 +586,12 @@ RSpec.describe Guardian do expect(Guardian.new.can_invite_to_forum?).to be_falsey end - it 'returns true when the site requires approving users' do + it "returns true when the site requires approving users" do SiteSetting.must_approve_users = true expect(Guardian.new(trust_level_2).can_invite_to_forum?).to be_truthy end - it 'returns false when max_invites_per_day is 0' do + it "returns false when max_invites_per_day is 0" do # let's also break it while here SiteSetting.max_invites_per_day = "a" @@ -597,7 +600,7 @@ RSpec.describe Guardian do expect(Guardian.new(moderator).can_invite_to_forum?).to be_truthy end - context 'with groups' do + context "with groups" do let(:groups) { [group, another_group] } before do @@ -605,14 +608,13 @@ RSpec.describe Guardian do group.add_owner(user) end - it 'returns false when user is not allowed to edit a group' do + it "returns false when user is not allowed to edit a group" do expect(Guardian.new(user).can_invite_to_forum?(groups)).to eq(false) - expect(Guardian.new(admin).can_invite_to_forum?(groups)) - .to eq(true) + expect(Guardian.new(admin).can_invite_to_forum?(groups)).to eq(true) end - it 'returns true when user is allowed to edit groups' do + it "returns true when user is allowed to edit groups" do another_group.add_owner(user) expect(Guardian.new(user).can_invite_to_forum?(groups)).to eq(true) @@ -620,8 +622,7 @@ RSpec.describe Guardian do end end - describe 'can_invite_to?' do - + describe "can_invite_to?" do describe "regular topics" do before do SiteSetting.min_trust_level_to_allow_invite = 2 @@ -631,11 +632,11 @@ RSpec.describe Guardian do fab!(:topic) { Fabricate(:topic) } fab!(:private_topic) { Fabricate(:topic, category: category) } fab!(:user) { topic.user } - let(:private_category) { Fabricate(:private_category, group: group) } + let(:private_category) { Fabricate(:private_category, group: group) } let(:group_private_topic) { Fabricate(:topic, category: private_category) } let(:group_owner) { group_private_topic.user.tap { |u| group.add_owner(u) } } - it 'handles invitation correctly' do + it "handles invitation correctly" do expect(Guardian.new(nil).can_invite_to?(topic)).to be_falsey expect(Guardian.new(moderator).can_invite_to?(nil)).to be_falsey expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy @@ -648,26 +649,26 @@ RSpec.describe Guardian do expect(Guardian.new(moderator).can_invite_to?(topic)).to be_truthy end - it 'returns false for normal user on private topic' do + it "returns false for normal user on private topic" do expect(Guardian.new(user).can_invite_to?(private_topic)).to be_falsey end - it 'returns false for admin on private topic' do + it "returns false for admin on private topic" do expect(Guardian.new(admin).can_invite_to?(private_topic)).to be(false) end - it 'returns true for a group owner' do + it "returns true for a group owner" do group_owner.update!(trust_level: SiteSetting.min_trust_level_to_allow_invite) expect(Guardian.new(group_owner).can_invite_to?(group_private_topic)).to be_truthy end - it 'return true for normal users even if must_approve_users' do + it "return true for normal users even if must_approve_users" do SiteSetting.must_approve_users = true expect(Guardian.new(user).can_invite_to?(topic)).to be_truthy expect(Guardian.new(admin).can_invite_to?(topic)).to be_truthy end - describe 'for a private category for automatic and non-automatic group' do + describe "for a private category for automatic and non-automatic group" do let(:category) do Fabricate(:category, read_restricted: true).tap do |category| category.groups << automatic_group @@ -677,21 +678,21 @@ RSpec.describe Guardian do let(:topic) { Fabricate(:topic, category: category) } - it 'should return true for an admin user' do + it "should return true for an admin user" do expect(Guardian.new(admin).can_invite_to?(topic)).to eq(true) end - it 'should return true for a group owner' do + it "should return true for a group owner" do group_owner.update!(trust_level: SiteSetting.min_trust_level_to_allow_invite) expect(Guardian.new(group_owner).can_invite_to?(topic)).to eq(true) end - it 'should return false for a normal user' do + it "should return false for a normal user" do expect(Guardian.new(user).can_invite_to?(topic)).to eq(false) end end - describe 'for a private category for automatic groups' do + describe "for a private category for automatic groups" do let(:category) do Fabricate(:private_category, group: automatic_group, read_restricted: true) end @@ -699,7 +700,7 @@ RSpec.describe Guardian do let(:group_owner) { Fabricate(:user).tap { |user| automatic_group.add_owner(user) } } let(:topic) { Fabricate(:topic, category: category) } - it 'should return false for all type of users' do + it "should return false for all type of users" do expect(Guardian.new(admin).can_invite_to?(topic)).to eq(false) expect(Guardian.new(group_owner).can_invite_to?(topic)).to eq(false) expect(Guardian.new(user).can_invite_to?(topic)).to eq(false) @@ -724,9 +725,7 @@ RSpec.describe Guardian do end context "when user does not belong to personal_message_enabled_groups" do - before do - SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:staff] - end + before { SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:staff] } it "doesn't allow a regular user to invite" do expect(Guardian.new(admin).can_invite_to?(pm)).to be_truthy @@ -735,9 +734,7 @@ RSpec.describe Guardian do end context "when PM has reached the maximum number of recipients" do - before do - SiteSetting.max_allowed_message_recipients = 2 - end + before { SiteSetting.max_allowed_message_recipients = 2 } it "doesn't allow a regular user to invite" do expect(Guardian.new(user).can_invite_to?(pm)).to be_falsey @@ -752,14 +749,14 @@ RSpec.describe Guardian do end end - describe 'can_invite_via_email?' do - it 'returns true for all (tl2 and above) users when sso is disabled, local logins are enabled, user approval is not required' do + describe "can_invite_via_email?" do + it "returns true for all (tl2 and above) users when sso is disabled, local logins are enabled, user approval is not required" do expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_truthy expect(Guardian.new(moderator).can_invite_via_email?(topic)).to be_truthy expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_truthy end - it 'returns true for all users when sso is enabled' do + it "returns true for all users when sso is enabled" do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true @@ -768,7 +765,7 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_truthy end - it 'returns false for all users when local logins are disabled' do + it "returns false for all users when local logins are disabled" do SiteSetting.enable_local_logins = false expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_falsey @@ -776,7 +773,7 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_invite_via_email?(topic)).to be_falsey end - it 'returns correct values when user approval is required' do + it "returns correct values when user approval is required" do SiteSetting.must_approve_users = true expect(Guardian.new(trust_level_2).can_invite_via_email?(topic)).to be_falsey @@ -785,23 +782,59 @@ RSpec.describe Guardian do end end - describe 'can_see?' do + describe "can_see_deleted_post?" do + fab!(:post) { Fabricate(:post) } - it 'returns false with a nil object' do + before { post.trash!(user) } + + it "returns false for post that is not deleted" do + post.recover! + expect(Guardian.new(admin).can_see_deleted_post?(post)).to be_falsey + end + + it "returns false for anon" do + expect(Guardian.new.can_see_deleted_post?(post)).to be_falsey + end + + it "returns true for admin" do + expect(Guardian.new(admin).can_see_deleted_post?(post)).to be_truthy + end + + it "returns true for mods" do + expect(Guardian.new(moderator).can_see_deleted_post?(post)).to be_truthy + end + + it "returns false for < TL4 users" do + user.update!(trust_level: TrustLevel[1]) + expect(Guardian.new(user).can_see_deleted_post?(post)).to be_falsey + end + + it "returns false if not ther person who deleted it" do + post.update!(deleted_by: another_user) + expect(Guardian.new(user).can_see_deleted_post?(post)).to be_falsey + end + + it "returns true for TL4 users' own posts" do + user.update!(trust_level: TrustLevel[4]) + expect(Guardian.new(user).can_see_deleted_post?(post)).to be_truthy + end + end + + describe "can_see?" do + it "returns false with a nil object" do expect(Guardian.new.can_see?(nil)).to be_falsey end - describe 'a Category' do - - it 'allows public categories' do + describe "a Category" do + it "allows public categories" do public_category = Fabricate(:category, read_restricted: false) expect(Guardian.new.can_see?(public_category)).to be_truthy end - it 'correctly handles secure categories' do + it "correctly handles secure categories" do normal_user = Fabricate(:user) staged_user = Fabricate(:user, staged: true) - admin_user = Fabricate(:user, admin: true) + admin_user = Fabricate(:user, admin: true) secure_category = Fabricate(:category, read_restricted: true) expect(Guardian.new(normal_user).can_see?(secure_category)).to be_falsey @@ -813,18 +846,25 @@ RSpec.describe Guardian do expect(Guardian.new(staged_user).can_see?(secure_category)).to be_falsey expect(Guardian.new(admin_user).can_see?(secure_category)).to be_truthy - secure_category = Fabricate(:category, read_restricted: true, email_in_allow_strangers: true) + secure_category = + Fabricate(:category, read_restricted: true, email_in_allow_strangers: true) expect(Guardian.new(normal_user).can_see?(secure_category)).to be_falsey expect(Guardian.new(staged_user).can_see?(secure_category)).to be_falsey expect(Guardian.new(admin_user).can_see?(secure_category)).to be_truthy - secure_category = Fabricate(:category, read_restricted: true, email_in: "foo2@bar.com", email_in_allow_strangers: true) + secure_category = + Fabricate( + :category, + read_restricted: true, + email_in: "foo2@bar.com", + email_in_allow_strangers: true, + ) expect(Guardian.new(normal_user).can_see?(secure_category)).to be_falsey expect(Guardian.new(staged_user).can_see?(secure_category)).to be_truthy expect(Guardian.new(admin_user).can_see?(secure_category)).to be_truthy end - it 'allows members of an authorized group' do + it "allows members of an authorized group" do secure_category = plain_category secure_category.set_permissions(group => :readonly) secure_category.save @@ -836,15 +876,14 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_see?(secure_category)).to be_truthy end - end - describe 'a Topic' do - it 'allows non logged in users to view topics' do + describe "a Topic" do + it "allows non logged in users to view topics" do expect(Guardian.new.can_see?(topic)).to be_truthy end - it 'correctly handles groups' do + it "correctly handles groups" do category = Fabricate(:category, read_restricted: true) category.set_permissions(group => :full) category.save @@ -914,7 +953,7 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_banner_topic?(topic)).to be_truthy end - it 'respects category group moderator settings' do + it "respects category group moderator settings" do group_user = Fabricate(:group_user) user_gm = group_user.user group = group_user.group @@ -934,8 +973,14 @@ RSpec.describe Guardian do expect(Guardian.new(user_gm).can_see?(topic)).to be_truthy end - it 'allows staged user to view own topic in restricted category when Category#email_in and Category#email_in_allow_strangers is set' do - secure_category = Fabricate(:category, read_restricted: true, email_in: "foo2@bar.com", email_in_allow_strangers: true) + it "allows staged user to view own topic in restricted category when Category#email_in and Category#email_in_allow_strangers is set" do + secure_category = + Fabricate( + :category, + read_restricted: true, + email_in: "foo2@bar.com", + email_in_allow_strangers: true, + ) topic_in_secure_category = Fabricate(:topic, category: secure_category, user: user) expect(Guardian.new(user).can_see?(topic_in_secure_category)).to eq(false) @@ -946,11 +991,11 @@ RSpec.describe Guardian do end end - describe 'a Post' do + describe "a Post" do fab!(:post) { Fabricate(:post) } fab!(:another_admin) { Fabricate(:admin) } - it 'correctly handles post visibility' do + it "correctly handles post visibility" do topic = post.topic expect(Guardian.new(user).can_see?(post)).to be_truthy @@ -968,7 +1013,7 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_see?(post)).to be_truthy end - it 'respects category group moderator settings' do + it "respects category group moderator settings" do group_user = Fabricate(:group_user) user_gm = group_user.user group = group_user.group @@ -985,7 +1030,7 @@ RSpec.describe Guardian do expect(Guardian.new(user_gm).can_see?(post)).to be_truthy end - it 'TL4 users can see their deleted posts' do + it "TL4 users can see their deleted posts" do user = Fabricate(:user, trust_level: 4) user2 = Fabricate(:user, trust_level: 4) post = Fabricate(:post, user: user, topic: Fabricate(:post).topic) @@ -996,7 +1041,7 @@ RSpec.describe Guardian do expect(Guardian.new(user2).can_see?(post)).to eq(false) end - it 'respects whispers' do + it "respects whispers" do SiteSetting.whispers_allowed_groups = "#{Group::AUTO_GROUPS[:staff]}|#{group.id}" regular_post = post @@ -1029,34 +1074,34 @@ RSpec.describe Guardian do end end - describe 'a PostRevision' do + describe "a PostRevision" do fab!(:post_revision) { Fabricate(:post_revision) } - context 'when edit_history_visible_to_public is true' do + context "when edit_history_visible_to_public is true" do before { SiteSetting.edit_history_visible_to_public = true } - it 'is false for nil' do + it "is false for nil" do expect(Guardian.new.can_see?(nil)).to be_falsey end - it 'is true if not logged in' do + it "is true if not logged in" do expect(Guardian.new.can_see?(post_revision)).to be_truthy end - it 'is true when logged in' do + it "is true when logged in" do expect(Guardian.new(user).can_see?(post_revision)).to be_truthy end end - context 'when edit_history_visible_to_public is false' do + context "when edit_history_visible_to_public is false" do before { SiteSetting.edit_history_visible_to_public = false } - it 'is true for staff' do + it "is true for staff" do expect(Guardian.new(admin).can_see?(post_revision)).to be_truthy expect(Guardian.new(moderator).can_see?(post_revision)).to be_truthy end - it 'is false for trust level equal or lower than 4' do + it "is false for trust level equal or lower than 4" do expect(Guardian.new(trust_level_3).can_see?(post_revision)).to be_falsey expect(Guardian.new(trust_level_4).can_see?(post_revision)).to be_falsey end @@ -1064,29 +1109,27 @@ RSpec.describe Guardian do end end - describe 'can_create?' do - - describe 'a Category' do - - it 'returns false when not logged in' do + describe "can_create?" do + describe "a Category" do + it "returns false when not logged in" do expect(Guardian.new.can_create?(Category)).to be_falsey end - it 'returns false when a regular user' do + it "returns false when a regular user" do expect(Guardian.new(user).can_create?(Category)).to be_falsey end - it 'returns false when a moderator' do + it "returns false when a moderator" do expect(Guardian.new(moderator).can_create?(Category)).to be_falsey end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_create?(Category)).to be_truthy end end - describe 'a Topic' do - it 'does not allow moderators to create topics in readonly categories' do + describe "a Topic" do + it "does not allow moderators to create topics in readonly categories" do category = plain_category category.set_permissions(everyone: :read) category.save @@ -1094,7 +1137,7 @@ RSpec.describe Guardian do expect(Guardian.new(moderator).can_create?(Topic, category)).to be_falsey end - it 'should check for full permissions' do + it "should check for full permissions" do category = plain_category category.set_permissions(everyone: :create_post) category.save @@ -1107,20 +1150,29 @@ RSpec.describe Guardian do it "is false if user has not met minimum trust level" do SiteSetting.min_trust_to_create_topic = 1 - expect(Guardian.new(Fabricate(:user, trust_level: 0)).can_create?(Topic, plain_category)).to be_falsey + expect( + Guardian.new(Fabricate(:user, trust_level: 0)).can_create?(Topic, plain_category), + ).to be_falsey end it "is true if user has met or exceeded the minimum trust level" do SiteSetting.min_trust_to_create_topic = 1 - expect(Guardian.new(Fabricate(:user, trust_level: 1)).can_create?(Topic, plain_category)).to be_truthy - expect(Guardian.new(Fabricate(:user, trust_level: 2)).can_create?(Topic, plain_category)).to be_truthy - expect(Guardian.new(Fabricate(:admin, trust_level: 0)).can_create?(Topic, plain_category)).to be_truthy - expect(Guardian.new(Fabricate(:moderator, trust_level: 0)).can_create?(Topic, plain_category)).to be_truthy + expect( + Guardian.new(Fabricate(:user, trust_level: 1)).can_create?(Topic, plain_category), + ).to be_truthy + expect( + Guardian.new(Fabricate(:user, trust_level: 2)).can_create?(Topic, plain_category), + ).to be_truthy + expect( + Guardian.new(Fabricate(:admin, trust_level: 0)).can_create?(Topic, plain_category), + ).to be_truthy + expect( + Guardian.new(Fabricate(:moderator, trust_level: 0)).can_create?(Topic, plain_category), + ).to be_truthy end end - describe 'a Post' do - + describe "a Post" do it "is false on readonly categories" do category = plain_category topic.category = category @@ -1135,7 +1187,7 @@ RSpec.describe Guardian do expect(Guardian.new.can_create?(Post, topic)).to be_falsey end - it 'is true for a regular user' do + it "is true for a regular user" do expect(Guardian.new(topic.user).can_create?(Post, topic)).to be_truthy end @@ -1144,16 +1196,14 @@ RSpec.describe Guardian do expect(Guardian.new(topic.user).can_create?(Post, topic)).to be_falsey end - context 'with closed topic' do - before do - topic.closed = true - end + context "with closed topic" do + before { topic.closed = true } it "doesn't allow new posts from regular users" do expect(Guardian.new(topic.user).can_create?(Post, topic)).to be_falsey end - it 'allows editing of posts' do + it "allows editing of posts" do expect(Guardian.new(topic.user).can_edit?(post)).to be_truthy end @@ -1170,17 +1220,15 @@ RSpec.describe Guardian do end end - context 'with archived topic' do - before do - topic.archived = true - end + context "with archived topic" do + before { topic.archived = true } - context 'with regular users' do + context "with regular users" do it "doesn't allow new posts from regular users" do expect(Guardian.new(coding_horror).can_create?(Post, topic)).to be_falsey end - it 'does not allow editing of posts' do + it "does not allow editing of posts" do expect(Guardian.new(coding_horror).can_edit?(post)).to be_falsey end end @@ -1195,9 +1243,7 @@ RSpec.describe Guardian do end context "with trashed topic" do - before do - topic.trash!(admin) - end + before { topic.trash!(admin) } it "doesn't allow new posts from regular users" do expect(Guardian.new(coding_horror).can_create?(Post, topic)).to be_falsey @@ -1213,14 +1259,14 @@ RSpec.describe Guardian do end context "with system message" do - fab!(:private_message) { + fab!(:private_message) do Fabricate( :topic, archetype: Archetype.private_message, - subtype: 'system_message', - category_id: nil + subtype: "system_message", + category_id: nil, ) - } + end before { user.save! } it "allows the user to reply to system messages" do @@ -1228,11 +1274,12 @@ RSpec.describe Guardian do SiteSetting.enable_system_message_replies = false expect(Guardian.new(user).can_create_post?(private_message)).to eq(false) end - end context "with private message" do - fab!(:private_message) { Fabricate(:topic, archetype: Archetype.private_message, category_id: nil) } + fab!(:private_message) do + Fabricate(:topic, archetype: Archetype.private_message, category_id: nil) + end before { user.save! } @@ -1257,17 +1304,14 @@ RSpec.describe Guardian do end end end # can_create? a Post - end - describe 'post_can_act?' do - + describe "post_can_act?" do it "isn't allowed on nil" do expect(Guardian.new(user).post_can_act?(nil, nil)).to be_falsey end - describe 'a Post' do - + describe "a Post" do let (:guardian) do Guardian.new(user) end @@ -1303,8 +1347,8 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_recover_topic?(topic)).to be_falsey end - context 'as a moderator' do - describe 'when post has been deleted' do + context "as a moderator" do + describe "when post has been deleted" do it "should return the right value" do expect(Guardian.new(moderator).can_recover_topic?(topic)).to be_falsey @@ -1315,7 +1359,7 @@ RSpec.describe Guardian do end describe "when post's user has been deleted" do - it 'should return the right value' do + it "should return the right value" do PostDestroyer.new(moderator, topic.first_post).destroy topic.first_post.user.destroy! @@ -1324,7 +1368,7 @@ RSpec.describe Guardian do end end - context 'when category group moderation is enabled' do + context "when category group moderation is enabled" do fab!(:group_user) { Fabricate(:group_user) } before do @@ -1346,7 +1390,6 @@ RSpec.describe Guardian do end describe "can_recover_post?" do - it "returns false for a nil user" do expect(Guardian.new(nil).can_recover_post?(post)).to be_falsey end @@ -1359,11 +1402,11 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_recover_post?(post)).to be_falsey end - context 'as a moderator' do + context "as a moderator" do fab!(:topic) { Fabricate(:topic, user: user) } fab!(:post) { Fabricate(:post, user: user, topic: topic) } - describe 'when post has been deleted' do + describe "when post has been deleted" do it "should return the right value" do expect(Guardian.new(moderator).can_recover_post?(post)).to be_falsey @@ -1373,7 +1416,7 @@ RSpec.describe Guardian do end describe "when post's user has been deleted" do - it 'should return the right value' do + it "should return the right value" do PostDestroyer.new(moderator, post).destroy post.user.destroy! @@ -1382,69 +1425,66 @@ RSpec.describe Guardian do end end end - end - describe '#can_convert_topic?' do - it 'returns false with a nil object' do + describe "#can_convert_topic?" do + it "returns false with a nil object" do expect(Guardian.new(user).can_convert_topic?(nil)).to be_falsey end - it 'returns false when not logged in' do + it "returns false when not logged in" do expect(Guardian.new.can_convert_topic?(topic)).to be_falsey end - it 'returns false when not staff' do + it "returns false when not staff" do expect(Guardian.new(trust_level_4).can_convert_topic?(topic)).to be_falsey end - it 'returns false for category definition topics' do + it "returns false for category definition topics" do c = plain_category topic = Topic.find_by(id: c.topic_id) expect(Guardian.new(admin).can_convert_topic?(topic)).to be_falsey end - it 'returns true when a moderator' do + it "returns true when a moderator" do expect(Guardian.new(moderator).can_convert_topic?(topic)).to be_truthy end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_convert_topic?(topic)).to be_truthy end - it 'returns false when user is not in personal_message_enabled_groups' do + it "returns false when user is not in personal_message_enabled_groups" do SiteSetting.personal_message_enabled_groups = Group::AUTO_GROUPS[:trust_level_4] expect(Guardian.new(user).can_convert_topic?(topic)).to be_falsey end end - describe 'can_edit?' do - - it 'returns false with a nil object' do + describe "can_edit?" do + it "returns false with a nil object" do expect(Guardian.new(user).can_edit?(nil)).to be_falsey end - describe 'a Post' do - - it 'returns false for silenced users' do + describe "a Post" do + it "returns false for silenced users" do post.user.silenced_till = 1.day.from_now expect(Guardian.new(post.user).can_edit?(post)).to be_falsey end - it 'returns false when not logged in' do + it "returns false when not logged in" do expect(Guardian.new.can_edit?(post)).to be_falsey end - it 'returns false when not logged in also for wiki post' do + it "returns false when not logged in also for wiki post" do post.wiki = true expect(Guardian.new.can_edit?(post)).to be_falsey end - it 'returns true if you want to edit your own post' do + it "returns true if you want to edit your own post" do expect(Guardian.new(post.user).can_edit?(post)).to be_truthy end - it 'returns false if you try to edit a locked post' do + it "returns false if you try to edit a locked post" do post.locked_by_id = moderator.id expect(Guardian.new(post.user).can_edit?(post)).to be_falsey end @@ -1474,33 +1514,33 @@ RSpec.describe Guardian do expect(Guardian.new(post.user).can_edit?(post)).to be_truthy end - it 'returns false if you are trying to edit a post you soft deleted' do + it "returns false if you are trying to edit a post you soft deleted" do post.user_deleted = true expect(Guardian.new(post.user).can_edit?(post)).to be_falsey end - it 'returns false if another regular user tries to edit a soft deleted wiki post' do + it "returns false if another regular user tries to edit a soft deleted wiki post" do post.wiki = true post.user_deleted = true expect(Guardian.new(coding_horror).can_edit?(post)).to be_falsey end - it 'returns false if you are trying to edit a deleted post' do + it "returns false if you are trying to edit a deleted post" do post.deleted_at = 1.day.ago expect(Guardian.new(post.user).can_edit?(post)).to be_falsey end - it 'returns false if another regular user tries to edit a deleted wiki post' do + it "returns false if another regular user tries to edit a deleted wiki post" do post.wiki = true post.deleted_at = 1.day.ago expect(Guardian.new(coding_horror).can_edit?(post)).to be_falsey end - it 'returns false if another regular user tries to edit your post' do + it "returns false if another regular user tries to edit your post" do expect(Guardian.new(coding_horror).can_edit?(post)).to be_falsey end - it 'returns true if another regular user tries to edit wiki post' do + it "returns true if another regular user tries to edit wiki post" do post.wiki = true expect(Guardian.new(coding_horror).can_edit?(post)).to be_truthy end @@ -1517,50 +1557,50 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_edit?(post)).to eq(false) end - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(post)).to be_truthy end - it 'returns true as a moderator, even if locked' do + it "returns true as a moderator, even if locked" do post.locked_by_id = admin.id expect(Guardian.new(moderator).can_edit?(post)).to be_truthy end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(post)).to be_truthy end - it 'returns true as a trust level 4 user' do + it "returns true as a trust level 4 user" do expect(Guardian.new(trust_level_4).can_edit?(post)).to be_truthy end - it 'returns false as a TL4 user if trusted_users_can_edit_others is false' do + it "returns false as a TL4 user if trusted_users_can_edit_others is false" do SiteSetting.trusted_users_can_edit_others = false expect(Guardian.new(trust_level_4).can_edit?(post)).to eq(false) end - it 'returns false when trying to edit a topic with no trust' do + it "returns false when trying to edit a topic with no trust" do SiteSetting.min_trust_to_edit_post = 2 post.user.trust_level = 1 expect(Guardian.new(topic.user).can_edit?(topic)).to be_falsey end - it 'returns false when trying to edit a post with no trust' do + it "returns false when trying to edit a post with no trust" do SiteSetting.min_trust_to_edit_post = 2 post.user.trust_level = 1 expect(Guardian.new(post.user).can_edit?(post)).to be_falsey end - it 'returns true when trying to edit a post with trust' do + it "returns true when trying to edit a post with trust" do SiteSetting.min_trust_to_edit_post = 1 post.user.trust_level = 1 expect(Guardian.new(post.user).can_edit?(post)).to be_truthy end - it 'returns false when another user has too low trust level to edit wiki post' do + it "returns false when another user has too low trust level to edit wiki post" do SiteSetting.min_trust_to_edit_wiki_post = 2 post.wiki = true coding_horror.trust_level = 1 @@ -1568,7 +1608,7 @@ RSpec.describe Guardian do expect(Guardian.new(coding_horror).can_edit?(post)).to be_falsey end - it 'returns true when another user has adequate trust level to edit wiki post' do + it "returns true when another user has adequate trust level to edit wiki post" do SiteSetting.min_trust_to_edit_wiki_post = 2 post.wiki = true coding_horror.trust_level = 2 @@ -1576,7 +1616,7 @@ RSpec.describe Guardian do expect(Guardian.new(coding_horror).can_edit?(post)).to be_truthy end - it 'returns true for post author even when he has too low trust level to edit wiki post' do + it "returns true for post author even when he has too low trust level to edit wiki post" do SiteSetting.min_trust_to_edit_wiki_post = 2 post.wiki = true post.user.trust_level = 1 @@ -1592,26 +1632,26 @@ RSpec.describe Guardian do before do SiteSetting.shared_drafts_category = category.id - SiteSetting.shared_drafts_min_trust_level = '2' + SiteSetting.shared_drafts_min_trust_level = "2" Fabricate(:shared_draft, topic: topic) end - it 'returns true if a shared draft exists' do + it "returns true if a shared draft exists" do expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(true) end - it 'returns false if the user has a lower trust level' do + it "returns false if the user has a lower trust level" do expect(Guardian.new(trust_level_1).can_edit_post?(post_with_draft)).to eq(false) end - it 'returns false if the draft is from a different category' do + it "returns false if the draft is from a different category" do topic.update!(category: Fabricate(:category)) expect(Guardian.new(trust_level_2).can_edit_post?(post_with_draft)).to eq(false) end end - context 'when category group moderation is enabled' do + context "when category group moderation is enabled" do fab!(:cat_mod_user) { Fabricate(:user) } before do @@ -1620,43 +1660,44 @@ RSpec.describe Guardian do post.topic.category.update!(reviewable_by_group_id: group.id) end - it 'returns true as a category group moderator user' do + it "returns true as a category group moderator user" do expect(Guardian.new(cat_mod_user).can_edit?(post)).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do expect(Guardian.new(another_user).can_edit?(post)).to eq(false) end end - describe 'post edit time limits' do - - context 'when post is older than post_edit_time_limit' do + describe "post edit time limits" do + context "when post is older than post_edit_time_limit" do let(:topic) { Fabricate(:topic) } - let(:old_post) { Fabricate(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) } + let(:old_post) do + Fabricate(:post, topic: topic, user: topic.user, created_at: 6.minutes.ago) + end before do - topic.user.update_columns(trust_level: 1) + topic.user.update_columns(trust_level: 1) SiteSetting.post_edit_time_limit = 5 end - it 'returns false to the author of the post' do + it "returns false to the author of the post" do expect(Guardian.new(old_post.user).can_edit?(old_post)).to be_falsey end - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(old_post)).to eq(true) end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(old_post)).to eq(true) end - it 'returns false for another regular user trying to edit your post' do + it "returns false for another regular user trying to edit your post" do expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey end - it 'returns true for another regular user trying to edit a wiki post' do + it "returns true for another regular user trying to edit a wiki post" do old_post.wiki = true expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy end @@ -1676,7 +1717,8 @@ RSpec.describe Guardian do it "returns false when the post topic's category allow_unlimited_owner_edits_on_first_post but the post is not the first in the topic" do old_post.topic.category.update(allow_unlimited_owner_edits_on_first_post: true) - new_post = Fabricate(:post, user: owner, topic: old_post.topic, created_at: 6.minutes.ago) + new_post = + Fabricate(:post, user: owner, topic: old_post.topic, created_at: 6.minutes.ago) expect(Guardian.new(owner).can_edit?(new_post)).to be_falsey end @@ -1687,31 +1729,33 @@ RSpec.describe Guardian do end end - context 'when post is older than tl2_post_edit_time_limit' do - let(:old_post) { Fabricate(:post, topic: topic, user: topic.user, created_at: 12.minutes.ago) } + context "when post is older than tl2_post_edit_time_limit" do + let(:old_post) do + Fabricate(:post, topic: topic, user: topic.user, created_at: 12.minutes.ago) + end before do topic.user.update_columns(trust_level: 2) SiteSetting.tl2_post_edit_time_limit = 10 end - it 'returns false to the author of the post' do + it "returns false to the author of the post" do expect(Guardian.new(old_post.user).can_edit?(old_post)).to be_falsey end - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(old_post)).to eq(true) end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(old_post)).to eq(true) end - it 'returns false for another regular user trying to edit your post' do + it "returns false for another regular user trying to edit your post" do expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_falsey end - it 'returns true for another regular user trying to edit a wiki post' do + it "returns true for another regular user trying to edit a wiki post" do old_post.wiki = true expect(Guardian.new(coding_horror).can_edit?(old_post)).to be_truthy end @@ -1731,25 +1775,26 @@ RSpec.describe Guardian do end end - describe 'a Topic' do - - it 'returns false when not logged in' do + describe "a Topic" do + it "returns false when not logged in" do expect(Guardian.new.can_edit?(topic)).to be_falsey end - it 'returns true for editing your own post' do + it "returns true for editing your own post" do expect(Guardian.new(topic.user).can_edit?(topic)).to eq(true) end - it 'returns false as a regular user' do + it "returns false as a regular user" do expect(Guardian.new(coding_horror).can_edit?(topic)).to be_falsey end - context 'when first post is hidden' do + context "when first post is hidden" do let!(:topic) { Fabricate(:topic, user: user) } - let!(:post) { Fabricate(:post, topic: topic, user: topic.user, hidden: true, hidden_at: Time.zone.now) } + let!(:post) do + Fabricate(:post, topic: topic, user: topic.user, hidden: true, hidden_at: Time.zone.now) + end - it 'returns false for editing your own post while inside the cooldown window' do + it "returns false for editing your own post while inside the cooldown window" do SiteSetting.cooldown_minutes_after_hiding_posts = 30 expect(Guardian.new(topic.user).can_edit?(topic)).to eq(false) @@ -1766,20 +1811,20 @@ RSpec.describe Guardian do end end - context 'when not archived' do - it 'returns true as a moderator' do + context "when not archived" do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(topic)).to eq(true) end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(topic)).to eq(true) end - it 'returns true at trust level 3' do + it "returns true at trust level 3" do expect(Guardian.new(trust_level_3).can_edit?(topic)).to eq(true) end - it 'is false at TL3, if `trusted_users_can_edit_others` is false' do + it "is false at TL3, if `trusted_users_can_edit_others` is false" do SiteSetting.trusted_users_can_edit_others = false expect(Guardian.new(trust_level_3).can_edit?(topic)).to eq(false) end @@ -1806,150 +1851,148 @@ RSpec.describe Guardian do end end - context 'with private message' do + context "with private message" do fab!(:private_message) { Fabricate(:private_message_topic) } - it 'returns false at trust level 3' do + it "returns false at trust level 3" do expect(Guardian.new(trust_level_3).can_edit?(private_message)).to eq(false) end - it 'returns false at trust level 4' do + it "returns false at trust level 4" do expect(Guardian.new(trust_level_4).can_edit?(private_message)).to eq(false) end end - context 'when archived' do + context "when archived" do let(:archived_topic) { Fabricate(:topic, user: user, archived: true) } - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(archived_topic)).to be_truthy end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(archived_topic)).to be_truthy end - it 'returns true at trust level 4' do + it "returns true at trust level 4" do expect(Guardian.new(trust_level_4).can_edit?(archived_topic)).to be_truthy end - it 'is false at TL4, if `trusted_users_can_edit_others` is false' do + it "is false at TL4, if `trusted_users_can_edit_others` is false" do SiteSetting.trusted_users_can_edit_others = false expect(Guardian.new(trust_level_4).can_edit?(archived_topic)).to eq(false) end - it 'returns false at trust level 3' do + it "returns false at trust level 3" do expect(Guardian.new(trust_level_3).can_edit?(archived_topic)).to be_falsey end - it 'returns false as a topic creator' do + it "returns false as a topic creator" do expect(Guardian.new(user).can_edit?(archived_topic)).to be_falsey end end - context 'when very old' do + context "when very old" do let(:old_topic) { Fabricate(:topic, user: user, created_at: 6.minutes.ago) } before { SiteSetting.post_edit_time_limit = 5 } - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(old_topic)).to be_truthy end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(old_topic)).to be_truthy end - it 'returns true at trust level 3' do + it "returns true at trust level 3" do expect(Guardian.new(trust_level_3).can_edit?(old_topic)).to be_truthy end - it 'returns false as a topic creator' do + it "returns false as a topic creator" do expect(Guardian.new(user).can_edit?(old_topic)).to be_falsey end end end - describe 'a Category' do - it 'returns false when not logged in' do + describe "a Category" do + it "returns false when not logged in" do expect(Guardian.new.can_edit?(plain_category)).to be_falsey end - it 'returns false as a regular user' do + it "returns false as a regular user" do expect(Guardian.new(plain_category.user).can_edit?(plain_category)).to be_falsey end - it 'returns false as a moderator' do + it "returns false as a moderator" do expect(Guardian.new(moderator).can_edit?(plain_category)).to be_falsey end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(plain_category)).to be_truthy end end - describe 'a User' do - - it 'returns false when not logged in' do + describe "a User" do + it "returns false when not logged in" do expect(Guardian.new.can_edit?(user)).to be_falsey end - it 'returns false as a different user' do + it "returns false as a different user" do expect(Guardian.new(coding_horror).can_edit?(user)).to be_falsey end - it 'returns true when trying to edit yourself' do + it "returns true when trying to edit yourself" do expect(Guardian.new(user).can_edit?(user)).to be_truthy end - it 'returns true as a moderator' do + it "returns true as a moderator" do expect(Guardian.new(moderator).can_edit?(user)).to be_truthy end - it 'returns true as an admin' do + it "returns true as an admin" do expect(Guardian.new(admin).can_edit?(user)).to be_truthy end end - end - describe '#can_moderate?' do - it 'returns false with a nil object' do + describe "#can_moderate?" do + it "returns false with a nil object" do expect(Guardian.new(user).can_moderate?(nil)).to be_falsey end - context 'when user is silenced' do - it 'returns false' do + context "when user is silenced" do + it "returns false" do user.update_column(:silenced_till, 1.year.from_now) expect(Guardian.new(user).can_moderate?(post)).to be(false) expect(Guardian.new(user).can_moderate?(topic)).to be(false) end end - context 'with a Topic' do - it 'returns false when not logged in' do + context "with a Topic" do + it "returns false when not logged in" do expect(Guardian.new.can_moderate?(topic)).to be_falsey end - it 'returns false when not a moderator' do + it "returns false when not a moderator" do expect(Guardian.new(user).can_moderate?(topic)).to be_falsey end - it 'returns true when a moderator' do + it "returns true when a moderator" do expect(Guardian.new(moderator).can_moderate?(topic)).to be_truthy end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_moderate?(topic)).to be_truthy end - it 'returns true when trust level 4' do + it "returns true when trust level 4" do expect(Guardian.new(trust_level_4).can_moderate?(topic)).to be_truthy end end end - describe '#can_see_flags?' do + describe "#can_see_flags?" do it "returns false when there is no post" do expect(Guardian.new(moderator).can_see_flags?(nil)).to be_falsey end @@ -1972,19 +2015,19 @@ RSpec.describe Guardian do end describe "#can_review_topic?" do - it 'returns false with a nil object' do + it "returns false with a nil object" do expect(Guardian.new(user).can_review_topic?(nil)).to eq(false) end - it 'returns true for a staff user' do + it "returns true for a staff user" do expect(Guardian.new(moderator).can_review_topic?(topic)).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do expect(Guardian.new(user).can_review_topic?(topic)).to eq(false) end - it 'returns true for a group member with reviewable status' do + it "returns true for a group member with reviewable status" do SiteSetting.enable_category_group_moderation = true GroupUser.create!(group_id: group.id, user_id: user.id) topic.category.update!(reviewable_by_group_id: group.id) @@ -1993,19 +2036,19 @@ RSpec.describe Guardian do end describe "#can_close_topic?" do - it 'returns false with a nil object' do + it "returns false with a nil object" do expect(Guardian.new(user).can_close_topic?(nil)).to eq(false) end - it 'returns true for a staff user' do + it "returns true for a staff user" do expect(Guardian.new(moderator).can_close_topic?(topic)).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do expect(Guardian.new(user).can_close_topic?(topic)).to eq(false) end - it 'returns true for a group member with reviewable status' do + it "returns true for a group member with reviewable status" do SiteSetting.enable_category_group_moderation = true GroupUser.create!(group_id: group.id, user_id: user.id) topic.category.update!(reviewable_by_group_id: group.id) @@ -2014,19 +2057,19 @@ RSpec.describe Guardian do end describe "#can_archive_topic?" do - it 'returns false with a nil object' do + it "returns false with a nil object" do expect(Guardian.new(user).can_archive_topic?(nil)).to eq(false) end - it 'returns true for a staff user' do + it "returns true for a staff user" do expect(Guardian.new(moderator).can_archive_topic?(topic)).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do expect(Guardian.new(user).can_archive_topic?(topic)).to eq(false) end - it 'returns true for a group member with reviewable status' do + it "returns true for a group member with reviewable status" do SiteSetting.enable_category_group_moderation = true GroupUser.create!(group_id: group.id, user_id: user.id) topic.category.update!(reviewable_by_group_id: group.id) @@ -2035,19 +2078,19 @@ RSpec.describe Guardian do end describe "#can_edit_staff_notes?" do - it 'returns false with a nil object' do + it "returns false with a nil object" do expect(Guardian.new(user).can_edit_staff_notes?(nil)).to eq(false) end - it 'returns true for a staff user' do + it "returns true for a staff user" do expect(Guardian.new(moderator).can_edit_staff_notes?(topic)).to eq(true) end - it 'returns false for a regular user' do + it "returns false for a regular user" do expect(Guardian.new(user).can_edit_staff_notes?(topic)).to eq(false) end - it 'returns true for a group member with reviewable status' do + it "returns true for a group member with reviewable status" do SiteSetting.enable_category_group_moderation = true GroupUser.create!(group_id: group.id, user_id: user.id) topic.category.update!(reviewable_by_group_id: group.id) @@ -2056,16 +2099,16 @@ RSpec.describe Guardian do end describe "#can_create_topic?" do - it 'returns true for staff user' do + it "returns true for staff user" do expect(Guardian.new(moderator).can_create_topic?(topic)).to eq(true) end - it 'returns false for user with insufficient trust level' do + it "returns false for user with insufficient trust level" do SiteSetting.min_trust_to_create_topic = 3 expect(Guardian.new(user).can_create_topic?(topic)).to eq(false) end - it 'returns true for user with sufficient trust level' do + it "returns true for user with sufficient trust level" do SiteSetting.min_trust_to_create_topic = 3 expect(Guardian.new(trust_level_4).can_create_topic?(topic)).to eq(true) end @@ -2078,7 +2121,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_create_topic?(topic)).to eq(false) end - it 'returns true when there is a category available for posting' do + it "returns true when there is a category available for posting" do SiteSetting.allow_uncategorized_topics = false plain_category.set_permissions(group => :full) @@ -2089,53 +2132,53 @@ RSpec.describe Guardian do end end - describe '#can_move_posts?' do - it 'returns false with a nil object' do + describe "#can_move_posts?" do + it "returns false with a nil object" do expect(Guardian.new(user).can_move_posts?(nil)).to be_falsey end - context 'with a Topic' do - it 'returns false when not logged in' do + context "with a Topic" do + it "returns false when not logged in" do expect(Guardian.new.can_move_posts?(topic)).to be_falsey end - it 'returns false when not a moderator' do + it "returns false when not a moderator" do expect(Guardian.new(user).can_move_posts?(topic)).to be_falsey end - it 'returns true when a moderator' do + it "returns true when a moderator" do expect(Guardian.new(moderator).can_move_posts?(topic)).to be_truthy end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_move_posts?(topic)).to be_truthy end end end - describe '#can_delete?' do - it 'returns false with a nil object' do + describe "#can_delete?" do + it "returns false with a nil object" do expect(Guardian.new(user).can_delete?(nil)).to be_falsey end - context 'with a Topic' do - it 'returns false when not logged in' do + context "with a Topic" do + it "returns false when not logged in" do expect(Guardian.new.can_delete?(topic)).to be_falsey end - it 'returns false when not a moderator' do + it "returns false when not a moderator" do expect(Guardian.new(Fabricate(:user)).can_delete?(topic)).to be_falsey end - it 'returns true when a moderator' do + it "returns true when a moderator" do expect(Guardian.new(moderator).can_delete?(topic)).to be_truthy end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_delete?(topic)).to be_truthy end - it 'returns false for static doc topics' do + it "returns false for static doc topics" do tos_topic = Fabricate(:topic, user: Discourse.system_user) SiteSetting.tos_topic_id = tos_topic.id expect(Guardian.new(admin).can_delete?(tos_topic)).to be_falsey @@ -2157,12 +2200,10 @@ RSpec.describe Guardian do expect(Guardian.new(topic.user).can_delete?(topic)).to be_falsey end - context 'when category group moderation is enabled' do + context "when category group moderation is enabled" do fab!(:group_user) { Fabricate(:group_user) } - before do - SiteSetting.enable_category_group_moderation = true - end + before { SiteSetting.enable_category_group_moderation = true } it "returns false if user is not a member of the appropriate group" do expect(Guardian.new(group_user.user).can_delete?(topic)).to be_falsey @@ -2176,12 +2217,10 @@ RSpec.describe Guardian do end end - context 'with a Post' do - before do - post.post_number = 2 - end + context "with a Post" do + before { post.post_number = 2 } - it 'returns false when not logged in' do + it "returns false when not logged in" do expect(Guardian.new.can_delete?(post)).to be_falsey end @@ -2192,7 +2231,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_delete?(post)).to be_falsey end - it 'returns true when trying to delete your own post' do + it "returns true when trying to delete your own post" do expect(Guardian.new(user).can_delete?(post)).to be_truthy expect(Guardian.new(trust_level_0).can_delete?(post)).to be_falsey @@ -2202,7 +2241,7 @@ RSpec.describe Guardian do expect(Guardian.new(trust_level_4).can_delete?(post)).to be_falsey end - it 'returns false when self deletions are disabled' do + it "returns false when self deletions are disabled" do SiteSetting.max_post_deletions_per_day = 0 expect(Guardian.new(user).can_delete?(post)).to be_falsey end @@ -2217,11 +2256,11 @@ RSpec.describe Guardian do expect(Guardian.new(moderator).can_delete?(post)).to be_falsey end - it 'returns true when a moderator' do + it "returns true when a moderator" do expect(Guardian.new(moderator).can_delete?(post)).to be_truthy end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_delete?(post)).to be_truthy end @@ -2234,7 +2273,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_delete?(post)).to eq(true) end - it 'returns false when post is first in a static doc topic' do + it "returns false when post is first in a static doc topic" do tos_topic = Fabricate(:topic, user: Discourse.system_user) SiteSetting.tos_topic_id = tos_topic.id post.update_attribute :post_number, 1 @@ -2242,10 +2281,8 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_delete?(post)).to be_falsey end - context 'when the topic is archived' do - before do - post.topic.archived = true - end + context "when the topic is archived" do + before { post.topic.archived = true } it "allows a staff member to delete it" do expect(Guardian.new(moderator).can_delete?(post)).to be_truthy @@ -2257,22 +2294,22 @@ RSpec.describe Guardian do end end - context 'with a Category' do + context "with a Category" do let(:category) { Fabricate(:category, user: moderator) } - it 'returns false when not logged in' do + it "returns false when not logged in" do expect(Guardian.new.can_delete?(category)).to be_falsey end - it 'returns false when a regular user' do + it "returns false when a regular user" do expect(Guardian.new(user).can_delete?(category)).to be_falsey end - it 'returns false when a moderator' do + it "returns false when a moderator" do expect(Guardian.new(moderator).can_delete?(category)).to be_falsey end - it 'returns true when an admin' do + it "returns true when an admin" do expect(Guardian.new(admin).can_delete?(category)).to be_truthy end @@ -2293,39 +2330,39 @@ RSpec.describe Guardian do end end - describe '#can_suspend?' do - it 'returns false when a user tries to suspend another user' do + describe "#can_suspend?" do + it "returns false when a user tries to suspend another user" do expect(Guardian.new(user).can_suspend?(coding_horror)).to be_falsey end - it 'returns true when an admin tries to suspend another user' do + it "returns true when an admin tries to suspend another user" do expect(Guardian.new(admin).can_suspend?(coding_horror)).to be_truthy end - it 'returns true when a moderator tries to suspend another user' do + it "returns true when a moderator tries to suspend another user" do expect(Guardian.new(moderator).can_suspend?(coding_horror)).to be_truthy end - it 'returns false when staff tries to suspend staff' do + it "returns false when staff tries to suspend staff" do expect(Guardian.new(admin).can_suspend?(moderator)).to be_falsey end end - context 'with a PostAction' do - let(:post_action) { + context "with a PostAction" do + let(:post_action) do user.id = 1 post.id = 1 a = PostAction.new(user: user, post: post, post_action_type_id: 2) a.created_at = 1.minute.ago a - } + end - it 'returns false when not logged in' do + it "returns false when not logged in" do expect(Guardian.new.can_delete?(post_action)).to be_falsey end - it 'returns false when not the user who created it' do + it "returns false when not the user who created it" do expect(Guardian.new(coding_horror).can_delete?(post_action)).to be_falsey end @@ -2347,7 +2384,7 @@ RSpec.describe Guardian do end end - describe '#can_approve?' do + describe "#can_approve?" do it "wont allow a non-logged in user to approve" do expect(Guardian.new.can_approve?(user)).to be_falsey end @@ -2375,7 +2412,7 @@ RSpec.describe Guardian do end end - describe '#can_grant_admin?' do + describe "#can_grant_admin?" do it "wont allow a non logged in user to grant an admin's access" do expect(Guardian.new.can_grant_admin?(another_admin)).to be_falsey end @@ -2384,7 +2421,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_grant_admin?(another_admin)).to be_falsey end - it 'wont allow an admin to grant their own access' do + it "wont allow an admin to grant their own access" do expect(Guardian.new(admin).can_grant_admin?(admin)).to be_falsey end @@ -2394,7 +2431,7 @@ RSpec.describe Guardian do expect(Guardian.new(admin).can_grant_admin?(user)).to be_truthy end - it 'should not allow an admin to grant admin access to a non real user' do + it "should not allow an admin to grant admin access to a non real user" do begin Discourse.system_user.update!(admin: false) expect(Guardian.new(admin).can_grant_admin?(Discourse.system_user)).to be(false) @@ -2404,7 +2441,7 @@ RSpec.describe Guardian do end end - describe '#can_revoke_admin?' do + describe "#can_revoke_admin?" do it "wont allow a non logged in user to revoke an admin's access" do expect(Guardian.new.can_revoke_admin?(another_admin)).to be_falsey end @@ -2413,7 +2450,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_revoke_admin?(another_admin)).to be_falsey end - it 'wont allow an admin to revoke their own access' do + it "wont allow an admin to revoke their own access" do expect(Guardian.new(admin).can_revoke_admin?(admin)).to be_falsey end @@ -2429,7 +2466,7 @@ RSpec.describe Guardian do end end - describe '#can_grant_moderation?' do + describe "#can_grant_moderation?" do it "wont allow a non logged in user to grant an moderator's access" do expect(Guardian.new.can_grant_moderation?(user)).to be_falsey end @@ -2438,11 +2475,11 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_grant_moderation?(moderator)).to be_falsey end - it 'will allow an admin to grant their own moderator access' do + it "will allow an admin to grant their own moderator access" do expect(Guardian.new(admin).can_grant_moderation?(admin)).to be_truthy end - it 'wont allow an admin to grant it to an already moderator' do + it "wont allow an admin to grant it to an already moderator" do expect(Guardian.new(admin).can_grant_moderation?(moderator)).to be_falsey end @@ -2460,7 +2497,7 @@ RSpec.describe Guardian do end end - describe '#can_revoke_moderation?' do + describe "#can_revoke_moderation?" do it "wont allow a non logged in user to revoke an moderator's access" do expect(Guardian.new.can_revoke_moderation?(moderator)).to be_falsey end @@ -2469,7 +2506,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_revoke_moderation?(moderator)).to be_falsey end - it 'wont allow a moderator to revoke their own moderator' do + it "wont allow a moderator to revoke their own moderator" do expect(Guardian.new(moderator).can_revoke_moderation?(moderator)).to be_falsey end @@ -2492,15 +2529,15 @@ RSpec.describe Guardian do end describe "#can_see_invite_details?" do - it 'is false without a logged in user' do + it "is false without a logged in user" do expect(Guardian.new(nil).can_see_invite_details?(user)).to be_falsey end - it 'is false without a user to look at' do + it "is false without a user to look at" do expect(Guardian.new(user).can_see_invite_details?(nil)).to be_falsey end - it 'is true when looking at your own invites' do + it "is true when looking at your own invites" do expect(Guardian.new(user).can_see_invite_details?(user)).to be_truthy end end @@ -2509,9 +2546,7 @@ RSpec.describe Guardian do let(:unapproved_user) { Fabricate(:user) } context "when must_approve_users is false" do - before do - SiteSetting.must_approve_users = false - end + before { SiteSetting.must_approve_users = false } it "returns true for a nil user" do expect(Guardian.new(nil).can_access_forum?).to be_truthy @@ -2522,10 +2557,8 @@ RSpec.describe Guardian do end end - context 'when must_approve_users is true' do - before do - SiteSetting.must_approve_users = true - end + context "when must_approve_users is true" do + before { SiteSetting.must_approve_users = true } it "returns false for a nil user" do expect(Guardian.new(nil).can_access_forum?).to be_falsey @@ -2565,7 +2598,9 @@ RSpec.describe Guardian do it "is true if user has no posts" do SiteSetting.delete_user_max_post_age = 10 - expect(Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago))).to be_truthy + expect( + Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago)), + ).to be_truthy end it "is true if user's first post is newer than delete_user_max_post_age days old" do @@ -2606,7 +2641,9 @@ RSpec.describe Guardian do it "is true if user has no posts" do SiteSetting.delete_user_max_post_age = 10 - expect(Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago))).to be_truthy + expect( + Guardian.new(actor).can_delete_all_posts?(Fabricate(:user, created_at: 100.days.ago)), + ).to be_truthy end it "is true if user's first post is newer than delete_user_max_post_age days old" do @@ -2685,33 +2722,33 @@ RSpec.describe Guardian do end end - describe 'can_grant_title?' do - it 'is false without a logged in user' do + describe "can_grant_title?" do + it "is false without a logged in user" do expect(Guardian.new(nil).can_grant_title?(user)).to be_falsey end - it 'is false for regular users' do + it "is false for regular users" do expect(Guardian.new(user).can_grant_title?(user)).to be_falsey end - it 'is true for moderators' do + it "is true for moderators" do expect(Guardian.new(moderator).can_grant_title?(user)).to be_truthy end - it 'is true for admins' do + it "is true for admins" do expect(Guardian.new(admin).can_grant_title?(user)).to be_truthy end - it 'is false without a user to look at' do + it "is false without a user to look at" do expect(Guardian.new(admin).can_grant_title?(nil)).to be_falsey end - context 'with title argument' do - fab!(:title_badge) { Fabricate(:badge, name: 'Helper', allow_title: true) } - fab!(:no_title_badge) { Fabricate(:badge, name: 'Writer', allow_title: false) } - fab!(:group) { Fabricate(:group, title: 'Groupie') } + context "with title argument" do + fab!(:title_badge) { Fabricate(:badge, name: "Helper", allow_title: true) } + fab!(:no_title_badge) { Fabricate(:badge, name: "Writer", allow_title: false) } + fab!(:group) { Fabricate(:group, title: "Groupie") } - it 'returns true if title belongs to a badge that user has' do + it "returns true if title belongs to a badge that user has" do BadgeGranter.grant(title_badge, user) expect(Guardian.new(user).can_grant_title?(user, title_badge.name)).to eq(true) end @@ -2725,7 +2762,7 @@ RSpec.describe Guardian do expect(Guardian.new(user).can_grant_title?(user, no_title_badge.name)).to eq(false) end - it 'returns true if title is from a group the user belongs to' do + it "returns true if title is from a group the user belongs to" do group.add(user) expect(Guardian.new(user).can_grant_title?(user, group.title)).to eq(true) end @@ -2735,66 +2772,66 @@ RSpec.describe Guardian do end it "returns true if the title is set to an empty string" do - expect(Guardian.new(user).can_grant_title?(user, '')).to eq(true) + expect(Guardian.new(user).can_grant_title?(user, "")).to eq(true) end end end - describe 'can_use_primary_group?' do - fab!(:group) { Fabricate(:group, title: 'Groupie') } + describe "can_use_primary_group?" do + fab!(:group) { Fabricate(:group, title: "Groupie") } - it 'is false without a logged in user' do + it "is false without a logged in user" do expect(Guardian.new(nil).can_use_primary_group?(user)).to be_falsey end - it 'is false with no group_id' do + it "is false with no group_id" do user.update(groups: [group]) expect(Guardian.new(user).can_use_primary_group?(user, nil)).to be_falsey end - it 'is false if the group does not exist' do + it "is false if the group does not exist" do user.update(groups: [group]) expect(Guardian.new(user).can_use_primary_group?(user, Group.last.id + 1)).to be_falsey end - it 'is false if the user is not a part of the group' do + it "is false if the user is not a part of the group" do user.update(groups: []) expect(Guardian.new(user).can_use_primary_group?(user, group.id)).to be_falsey end - it 'is false if the group is automatic' do - user.update(groups: [Group.new(name: 'autooo', automatic: true)]) + it "is false if the group is automatic" do + user.update(groups: [Group.new(name: "autooo", automatic: true)]) expect(Guardian.new(user).can_use_primary_group?(user, group.id)).to be_falsey end - it 'is true if the user is a part of the group, and the group is custom' do + it "is true if the user is a part of the group, and the group is custom" do user.update(groups: [group]) expect(Guardian.new(user).can_use_primary_group?(user, group.id)).to be_truthy end end - describe 'can_use_flair_group?' do - fab!(:group) { Fabricate(:group, title: 'Groupie', flair_icon: 'icon') } + describe "can_use_flair_group?" do + fab!(:group) { Fabricate(:group, title: "Groupie", flair_icon: "icon") } - it 'is false without a logged in user' do + it "is false without a logged in user" do expect(Guardian.new(nil).can_use_flair_group?(user)).to eq(false) end - it 'is false if the group does not exist' do + it "is false if the group does not exist" do expect(Guardian.new(user).can_use_flair_group?(user, nil)).to eq(false) expect(Guardian.new(user).can_use_flair_group?(user, Group.last.id + 1)).to eq(false) end - it 'is false if the user is not a part of the group' do + it "is false if the user is not a part of the group" do expect(Guardian.new(user).can_use_flair_group?(user, group.id)).to eq(false) end - it 'is false if the group does not have a flair' do + it "is false if the group does not have a flair" do group.update(flair_icon: nil) expect(Guardian.new(user).can_use_flair_group?(user, group.id)).to eq(false) end - it 'is true if the user is a part of the group and the group has a flair' do + it "is true if the user is a part of the group and the group has a flair" do user.update(groups: [group]) expect(Guardian.new(user).can_use_flair_group?(user, group.id)).to eq(true) end @@ -2814,9 +2851,7 @@ RSpec.describe Guardian do end context "when moderators_manage_categories_and_groups site setting is enabled" do - before do - SiteSetting.moderators_manage_categories_and_groups = true - end + before { SiteSetting.moderators_manage_categories_and_groups = true } it "returns true for moderators" do expect(Guardian.new(moderator).can_change_primary_group?(user, group)).to eq(true) @@ -2824,9 +2859,7 @@ RSpec.describe Guardian do end context "when moderators_manage_categories_and_groups site setting is disabled" do - before do - SiteSetting.moderators_manage_categories_and_groups = false - end + before { SiteSetting.moderators_manage_categories_and_groups = false } it "returns false for moderators" do expect(Guardian.new(moderator).can_change_primary_group?(user, group)).to eq(false) @@ -2834,32 +2867,37 @@ RSpec.describe Guardian do end end - describe 'can_change_trust_level?' do - - it 'is false without a logged in user' do + describe "can_change_trust_level?" do + it "is false without a logged in user" do expect(Guardian.new(nil).can_change_trust_level?(user)).to be_falsey end - it 'is false for regular users' do + it "is false for regular users" do expect(Guardian.new(user).can_change_trust_level?(user)).to be_falsey end - it 'is true for moderators' do + it "is true for moderators" do expect(Guardian.new(moderator).can_change_trust_level?(user)).to be_truthy end - it 'is true for admins' do + it "is true for admins" do expect(Guardian.new(admin).can_change_trust_level?(user)).to be_truthy end end describe "can_edit_username?" do it "is false without a logged in user" do - expect(Guardian.new(nil).can_edit_username?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(nil).can_edit_username?(Fabricate(:user, created_at: 1.minute.ago)), + ).to be_falsey end it "is false for regular users to edit another user's username" do - expect(Guardian.new(Fabricate(:user)).can_edit_username?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(Fabricate(:user)).can_edit_username?( + Fabricate(:user, created_at: 1.minute.ago), + ), + ).to be_falsey end shared_examples "staff can always change usernames" do @@ -2877,16 +2915,14 @@ RSpec.describe Guardian do end context "for anonymous user" do - before do - SiteSetting.allow_anonymous_posting = true - end + before { SiteSetting.allow_anonymous_posting = true } it "is false" do expect(Guardian.new(anonymous_user).can_edit_username?(anonymous_user)).to be_falsey end end - context 'for a new user' do + context "for a new user" do fab!(:target_user) { Fabricate(:user, created_at: 1.minute.ago) } include_examples "staff can always change usernames" @@ -2895,21 +2931,19 @@ RSpec.describe Guardian do end end - context 'for an old user' do - before do - SiteSetting.username_change_period = 3 - end + context "for an old user" do + before { SiteSetting.username_change_period = 3 } let(:target_user) { Fabricate(:user, created_at: 4.days.ago) } - context 'with no posts' do + context "with no posts" do include_examples "staff can always change usernames" it "is false for the user to change their own username" do expect(Guardian.new(target_user).can_edit_username?(target_user)).to be_falsey end end - context 'with posts' do + context "with posts" do before { target_user.stubs(:post_count).returns(1) } include_examples "staff can always change usernames" it "is false for the user to change their own username" do @@ -2918,10 +2952,8 @@ RSpec.describe Guardian do end end - context 'when editing is disabled in preferences' do - before do - SiteSetting.username_change_period = 0 - end + context "when editing is disabled in preferences" do + before { SiteSetting.username_change_period = 0 } include_examples "staff can always change usernames" @@ -2930,7 +2962,7 @@ RSpec.describe Guardian do end end - context 'when SSO username override is active' do + context "when SSO username override is active" do before do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true @@ -2952,15 +2984,11 @@ RSpec.describe Guardian do end describe "can_edit_email?" do - context 'when allowed in settings' do - before do - SiteSetting.email_editable = true - end + context "when allowed in settings" do + before { SiteSetting.email_editable = true } context "for anonymous user" do - before do - SiteSetting.allow_anonymous_posting = true - end + before { SiteSetting.allow_anonymous_posting = true } it "is false" do expect(Guardian.new(anonymous_user).can_edit_email?(anonymous_user)).to be_falsey @@ -2968,11 +2996,17 @@ RSpec.describe Guardian do end it "is false when not logged in" do - expect(Guardian.new(nil).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(nil).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago)), + ).to be_falsey end it "is false for regular users to edit another user's email" do - expect(Guardian.new(Fabricate(:user)).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(Fabricate(:user)).can_edit_email?( + Fabricate(:user, created_at: 1.minute.ago), + ), + ).to be_falsey end it "is true for a regular user to edit their own email" do @@ -2988,17 +3022,21 @@ RSpec.describe Guardian do end end - context 'when not allowed in settings' do - before do - SiteSetting.email_editable = false - end + context "when not allowed in settings" do + before { SiteSetting.email_editable = false } it "is false when not logged in" do - expect(Guardian.new(nil).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(nil).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago)), + ).to be_falsey end it "is false for regular users to edit another user's email" do - expect(Guardian.new(Fabricate(:user)).can_edit_email?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(Fabricate(:user)).can_edit_email?( + Fabricate(:user, created_at: 1.minute.ago), + ), + ).to be_falsey end it "is false for a regular user to edit their own email" do @@ -3014,7 +3052,7 @@ RSpec.describe Guardian do end end - context 'when SSO email override is active' do + context "when SSO email override is active" do before do SiteSetting.email_editable = false SiteSetting.discourse_connect_url = "https://www.example.com/sso" @@ -3036,121 +3074,115 @@ RSpec.describe Guardian do end end - describe 'can_edit_name?' do - it 'is false without a logged in user' do - expect(Guardian.new(nil).can_edit_name?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + describe "can_edit_name?" do + it "is false without a logged in user" do + expect( + Guardian.new(nil).can_edit_name?(Fabricate(:user, created_at: 1.minute.ago)), + ).to be_falsey end it "is false for regular users to edit another user's name" do - expect(Guardian.new(Fabricate(:user)).can_edit_name?(Fabricate(:user, created_at: 1.minute.ago))).to be_falsey + expect( + Guardian.new(Fabricate(:user)).can_edit_name?(Fabricate(:user, created_at: 1.minute.ago)), + ).to be_falsey end context "for anonymous user" do - before do - SiteSetting.allow_anonymous_posting = true - end + before { SiteSetting.allow_anonymous_posting = true } it "is false" do expect(Guardian.new(anonymous_user).can_edit_name?(anonymous_user)).to be_falsey end end - context 'for a new user' do + context "for a new user" do let(:target_user) { Fabricate(:user, created_at: 1.minute.ago) } - it 'is true for the user to change their own name' do + it "is true for the user to change their own name" do expect(Guardian.new(target_user).can_edit_name?(target_user)).to be_truthy end - it 'is true for moderators' do + it "is true for moderators" do expect(Guardian.new(moderator).can_edit_name?(user)).to be_truthy end - it 'is true for admins' do + it "is true for admins" do expect(Guardian.new(admin).can_edit_name?(user)).to be_truthy end end - context 'when name is disabled in preferences' do - before do - SiteSetting.enable_names = false - end + context "when name is disabled in preferences" do + before { SiteSetting.enable_names = false } - it 'is false for the user to change their own name' do + it "is false for the user to change their own name" do expect(Guardian.new(user).can_edit_name?(user)).to be_falsey end - it 'is false for moderators' do + it "is false for moderators" do expect(Guardian.new(moderator).can_edit_name?(user)).to be_falsey end - it 'is false for admins' do + it "is false for admins" do expect(Guardian.new(admin).can_edit_name?(user)).to be_falsey end end - context 'when name is enabled in preferences' do - before do - SiteSetting.enable_names = true - end + context "when name is enabled in preferences" do + before { SiteSetting.enable_names = true } - context 'when SSO is disabled' do + context "when SSO is disabled" do before do SiteSetting.enable_discourse_connect = false SiteSetting.auth_overrides_name = false end - it 'is true for admins' do + it "is true for admins" do expect(Guardian.new(admin).can_edit_name?(admin)).to be_truthy end - it 'is true for moderators' do + it "is true for moderators" do expect(Guardian.new(moderator).can_edit_name?(moderator)).to be_truthy end - it 'is true for users' do + it "is true for users" do expect(Guardian.new(user).can_edit_name?(user)).to be_truthy end end - context 'when SSO is enabled' do + context "when SSO is enabled" do before do SiteSetting.discourse_connect_url = "https://www.example.com/sso" SiteSetting.enable_discourse_connect = true end - context 'when SSO name override is active' do - before do - SiteSetting.auth_overrides_name = true - end + context "when SSO name override is active" do + before { SiteSetting.auth_overrides_name = true } - it 'is false for admins' do + it "is false for admins" do expect(Guardian.new(admin).can_edit_name?(admin)).to be_falsey end - it 'is false for moderators' do + it "is false for moderators" do expect(Guardian.new(moderator).can_edit_name?(moderator)).to be_falsey end - it 'is false for users' do + it "is false for users" do expect(Guardian.new(user).can_edit_name?(user)).to be_falsey end end - context 'when SSO name override is not active' do - before do - SiteSetting.auth_overrides_name = false - end + context "when SSO name override is not active" do + before { SiteSetting.auth_overrides_name = false } - it 'is true for admins' do + it "is true for admins" do expect(Guardian.new(admin).can_edit_name?(admin)).to be_truthy end - it 'is true for moderators' do + it "is true for moderators" do expect(Guardian.new(moderator).can_edit_name?(moderator)).to be_truthy end - it 'is true for users' do + it "is true for users" do expect(Guardian.new(user).can_edit_name?(user)).to be_truthy end end @@ -3158,38 +3190,36 @@ RSpec.describe Guardian do end end - describe '#can_export_entity?' do + describe "#can_export_entity?" do let(:anonymous_guardian) { Guardian.new } let(:user_guardian) { Guardian.new(user) } let(:moderator_guardian) { Guardian.new(moderator) } let(:admin_guardian) { Guardian.new(admin) } - it 'only allows admins to export user_list' do - expect(user_guardian.can_export_entity?('user_list')).to be_falsey - expect(moderator_guardian.can_export_entity?('user_list')).to be_falsey - expect(admin_guardian.can_export_entity?('user_list')).to be_truthy + it "only allows admins to export user_list" do + expect(user_guardian.can_export_entity?("user_list")).to be_falsey + expect(moderator_guardian.can_export_entity?("user_list")).to be_falsey + expect(admin_guardian.can_export_entity?("user_list")).to be_truthy end - it 'allow moderators to export other admin entities' do - expect(user_guardian.can_export_entity?('staff_action')).to be_falsey - expect(moderator_guardian.can_export_entity?('staff_action')).to be_truthy - expect(admin_guardian.can_export_entity?('staff_action')).to be_truthy + it "allow moderators to export other admin entities" do + expect(user_guardian.can_export_entity?("staff_action")).to be_falsey + expect(moderator_guardian.can_export_entity?("staff_action")).to be_truthy + expect(admin_guardian.can_export_entity?("staff_action")).to be_truthy end - it 'does not allow anonymous to export' do - expect(anonymous_guardian.can_export_entity?('user_archive')).to be_falsey + it "does not allow anonymous to export" do + expect(anonymous_guardian.can_export_entity?("user_archive")).to be_falsey end end - describe '#can_ignore_user?' do - before do - SiteSetting.min_trust_level_to_allow_ignore = 1 - end + describe "#can_ignore_user?" do + before { SiteSetting.min_trust_level_to_allow_ignore = 1 } let(:guardian) { Guardian.new(trust_level_2) } context "when ignored user is the same as guardian user" do - it 'does not allow ignoring user' do + it "does not allow ignoring user" do expect(guardian.can_ignore_user?(trust_level_2)).to eq(false) end end @@ -3197,52 +3227,51 @@ RSpec.describe Guardian do context "when ignored user is a staff user" do let!(:admin) { Fabricate(:user, admin: true) } - it 'does not allow ignoring user' do + it "does not allow ignoring user" do expect(guardian.can_ignore_user?(admin)).to eq(false) end end context "when ignored user is a normal user" do - it 'allows ignoring user' do + it "allows ignoring user" do expect(guardian.can_ignore_user?(another_user)).to eq(true) end end context "when ignorer is staff" do let(:guardian) { Guardian.new(admin) } - it 'allows ignoring user' do + it "allows ignoring user" do expect(guardian.can_ignore_user?(another_user)).to eq(true) end end context "when ignorer's trust level is below min_trust_level_to_allow_ignore" do let(:guardian) { Guardian.new(trust_level_0) } - it 'does not allow ignoring user' do + it "does not allow ignoring user" do expect(guardian.can_ignore_user?(another_user)).to eq(false) end end context "when ignorer's trust level is equal to min_trust_level_to_allow_ignore site setting" do let(:guardian) { Guardian.new(trust_level_1) } - it 'allows ignoring user' do + it "allows ignoring user" do expect(guardian.can_ignore_user?(another_user)).to eq(true) end end context "when ignorer's trust level is above min_trust_level_to_allow_ignore site setting" do let(:guardian) { Guardian.new(trust_level_3) } - it 'allows ignoring user' do + it "allows ignoring user" do expect(guardian.can_ignore_user?(another_user)).to eq(true) end end end - describe '#can_mute_user?' do - + describe "#can_mute_user?" do let(:guardian) { Guardian.new(trust_level_1) } context "when muted user is the same as guardian user" do - it 'does not allow muting user' do + it "does not allow muting user" do expect(guardian.can_mute_user?(trust_level_1)).to eq(false) end end @@ -3250,13 +3279,13 @@ RSpec.describe Guardian do context "when muted user is a staff user" do let!(:admin) { Fabricate(:user, admin: true) } - it 'does not allow muting user' do + it "does not allow muting user" do expect(guardian.can_mute_user?(admin)).to eq(false) end end context "when muted user is a normal user" do - it 'allows muting user' do + it "allows muting user" do expect(guardian.can_mute_user?(another_user)).to eq(true) end end @@ -3265,7 +3294,7 @@ RSpec.describe Guardian do let(:guardian) { Guardian.new(trust_level_0) } let!(:trust_level_0) { Fabricate(:user, trust_level: 0) } - it 'does not allow muting user' do + it "does not allow muting user" do expect(guardian.can_mute_user?(another_user)).to eq(false) end end @@ -3273,7 +3302,7 @@ RSpec.describe Guardian do context "when muter is staff" do let(:guardian) { Guardian.new(admin) } - it 'allows muting user' do + it "allows muting user" do expect(guardian.can_mute_user?(another_user)).to eq(true) end end @@ -3281,7 +3310,7 @@ RSpec.describe Guardian do context "when muters's trust level is tl1" do let(:guardian) { Guardian.new(trust_level_1) } - it 'allows muting user' do + it "allows muting user" do expect(guardian.can_mute_user?(another_user)).to eq(true) end end @@ -3306,9 +3335,8 @@ RSpec.describe Guardian do expect(guardian.allow_themes?([theme.id], include_preview: true)).to eq(true) - expect(guardian.allowed_theme_repo_import?('https://x.com/git')).to eq(true) - expect(guardian.allowed_theme_repo_import?('https:/evil.com/git')).to eq(false) - + expect(guardian.allowed_theme_repo_import?("https://x.com/git")).to eq(true) + expect(guardian.allowed_theme_repo_import?("https:/evil.com/git")).to eq(false) end end @@ -3316,8 +3344,12 @@ RSpec.describe Guardian do expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id])).to eq(false) expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id])).to eq(false) - expect(Guardian.new(moderator).allow_themes?([theme.id, theme2.id], include_preview: true)).to eq(true) - expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id], include_preview: true)).to eq(true) + expect( + Guardian.new(moderator).allow_themes?([theme.id, theme2.id], include_preview: true), + ).to eq(true) + expect(Guardian.new(admin).allow_themes?([theme.id, theme2.id], include_preview: true)).to eq( + true, + ) end it "only allows normal users to use user-selectable themes or default theme" do @@ -3351,10 +3383,10 @@ RSpec.describe Guardian do end end - describe 'can_wiki?' do + describe "can_wiki?" do let(:post) { Fabricate(:post, created_at: 1.minute.ago) } - it 'returns false for regular user' do + it "returns false for regular user" do expect(Guardian.new(coding_horror).can_wiki?(post)).to be_falsey end @@ -3368,36 +3400,36 @@ RSpec.describe Guardian do expect(Guardian.new(trust_level_2).can_wiki?(post)).to be_falsey end - it 'returns true when user satisfies trust level and owns the post' do + it "returns true when user satisfies trust level and owns the post" do SiteSetting.min_trust_to_allow_self_wiki = 2 own_post = Fabricate(:post, user: trust_level_2) expect(Guardian.new(trust_level_2).can_wiki?(own_post)).to be_truthy end - it 'returns true for admin user' do + it "returns true for admin user" do expect(Guardian.new(admin).can_wiki?(post)).to be_truthy end - it 'returns true for trust_level_4 user' do + it "returns true for trust_level_4 user" do expect(Guardian.new(trust_level_4).can_wiki?(post)).to be_truthy end - context 'when post is older than post_edit_time_limit' do + context "when post is older than post_edit_time_limit" do let(:old_post) { Fabricate(:post, user: trust_level_2, created_at: 6.minutes.ago) } before do SiteSetting.min_trust_to_allow_self_wiki = 2 SiteSetting.tl2_post_edit_time_limit = 5 end - it 'returns false when user satisfies trust level and owns the post' do + it "returns false when user satisfies trust level and owns the post" do expect(Guardian.new(trust_level_2).can_wiki?(old_post)).to be_falsey end - it 'returns true for admin user' do + it "returns true for admin user" do expect(Guardian.new(admin).can_wiki?(old_post)).to be_truthy end - it 'returns true for trust_level_4 user' do + it "returns true for trust_level_4 user" do expect(Guardian.new(trust_level_4).can_wiki?(post)).to be_truthy end end @@ -3405,9 +3437,7 @@ RSpec.describe Guardian do describe "Tags" do context "with tagging disabled" do - before do - SiteSetting.tagging_enabled = false - end + before { SiteSetting.tagging_enabled = false } it "can_create_tag returns false" do expect(Guardian.new(admin).can_create_tag?).to be_falsey @@ -3428,10 +3458,8 @@ RSpec.describe Guardian do SiteSetting.min_trust_level_to_tag_topics = 1 end - context 'when min_trust_to_create_tag is 3' do - before do - SiteSetting.min_trust_to_create_tag = 3 - end + context "when min_trust_to_create_tag is 3" do + before { SiteSetting.min_trust_to_create_tag = 3 } describe "can_create_tag" do it "returns false if trust level is too low" do @@ -3465,9 +3493,7 @@ RSpec.describe Guardian do end context 'when min_trust_to_create_tag is "staff"' do - before do - SiteSetting.min_trust_to_create_tag = 'staff' - end + before { SiteSetting.min_trust_to_create_tag = "staff" } it "returns false if not staff" do expect(Guardian.new(trust_level_4).can_create_tag?).to eq(false) @@ -3480,9 +3506,7 @@ RSpec.describe Guardian do end context 'when min_trust_to_create_tag is "admin"' do - before do - SiteSetting.min_trust_to_create_tag = 'admin' - end + before { SiteSetting.min_trust_to_create_tag = "admin" } it "returns false if not admin" do expect(Guardian.new(trust_level_4).can_create_tag?).to eq(false) @@ -3513,8 +3537,8 @@ RSpec.describe Guardian do end describe "#can_see_group?" do - it 'Correctly handles owner visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:owners]) + it "Correctly handles owner visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:owners]) group.add(member) group.save! @@ -3530,8 +3554,8 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_group?(group)).to eq(true) end - it 'Correctly handles staff visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:staff]) + it "Correctly handles staff visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:staff]) group.add(member) group.save! @@ -3547,8 +3571,8 @@ RSpec.describe Guardian do expect(Guardian.new.can_see_group?(group)).to eq(false) end - it 'Correctly handles member visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:members]) + it "Correctly handles member visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:members]) group.add(member) group.save! @@ -3564,8 +3588,8 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_group?(group)).to eq(true) end - it 'Correctly handles logged-on-user visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:logged_on_users]) + it "Correctly handles logged-on-user visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:logged_on_users]) group.add(member) group.save! @@ -3580,16 +3604,16 @@ RSpec.describe Guardian do expect(Guardian.new(another_user).can_see_group?(group)).to eq(true) end - it 'Correctly handles public groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:public]) + it "Correctly handles public groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:public]) expect(Guardian.new.can_see_group?(group)).to eq(true) end end describe "#can_see_group_members?" do - it 'Correctly handles group members visibility for owner' do - group = Group.new(name: 'group', members_visibility_level: Group.visibility_levels[:owners]) + it "Correctly handles group members visibility for owner" do + group = Group.new(name: "group", members_visibility_level: Group.visibility_levels[:owners]) group.add(member) group.save! @@ -3605,8 +3629,8 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_group_members?(group)).to eq(true) end - it 'Correctly handles group members visibility for staff' do - group = Group.new(name: 'group', members_visibility_level: Group.visibility_levels[:staff]) + it "Correctly handles group members visibility for staff" do + group = Group.new(name: "group", members_visibility_level: Group.visibility_levels[:staff]) group.add(member) group.save! @@ -3622,8 +3646,8 @@ RSpec.describe Guardian do expect(Guardian.new.can_see_group_members?(group)).to eq(false) end - it 'Correctly handles group members visibility for member' do - group = Group.new(name: 'group', members_visibility_level: Group.visibility_levels[:members]) + it "Correctly handles group members visibility for member" do + group = Group.new(name: "group", members_visibility_level: Group.visibility_levels[:members]) group.add(member) group.save! @@ -3639,8 +3663,12 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_group_members?(group)).to eq(true) end - it 'Correctly handles group members visibility for logged-on-user' do - group = Group.new(name: 'group', members_visibility_level: Group.visibility_levels[:logged_on_users]) + it "Correctly handles group members visibility for logged-on-user" do + group = + Group.new( + name: "group", + members_visibility_level: Group.visibility_levels[:logged_on_users], + ) group.add(member) group.save! @@ -3655,17 +3683,16 @@ RSpec.describe Guardian do expect(Guardian.new(another_user).can_see_group_members?(group)).to eq(true) end - it 'Correctly handles group members visibility for public' do - group = Group.new(name: 'group', members_visibility_level: Group.visibility_levels[:public]) + it "Correctly handles group members visibility for public" do + group = Group.new(name: "group", members_visibility_level: Group.visibility_levels[:public]) expect(Guardian.new.can_see_group_members?(group)).to eq(true) end - end - describe '#can_see_groups?' do - it 'correctly handles owner visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:owners]) + describe "#can_see_groups?" do + it "correctly handles owner visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:owners]) group.add(member) group.save! @@ -3681,9 +3708,9 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_groups?([group])).to eq(true) end - it 'correctly handles the case where the user does not own every group' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:owners]) - group2 = Group.new(name: 'group2', visibility_level: Group.visibility_levels[:owners]) + it "correctly handles the case where the user does not own every group" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:owners]) + group2 = Group.new(name: "group2", visibility_level: Group.visibility_levels[:owners]) group2.save! group.add(member) @@ -3700,8 +3727,8 @@ RSpec.describe Guardian do expect(Guardian.new(another_user).can_see_groups?([group, group2])).to eq(false) end - it 'correctly handles staff visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:staff]) + it "correctly handles staff visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:staff]) group.add(member) group.save! @@ -3717,8 +3744,8 @@ RSpec.describe Guardian do expect(Guardian.new(another_user).can_see_groups?([group])).to eq(false) end - it 'correctly handles member visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:members]) + it "correctly handles member visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:members]) group.add(member) group.save! @@ -3734,8 +3761,8 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_groups?([group])).to eq(true) end - it 'correctly handles logged-on-user visible groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:logged_on_users]) + it "correctly handles logged-on-user visible groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:logged_on_users]) group.add(member) group.save! @@ -3751,9 +3778,9 @@ RSpec.describe Guardian do expect(Guardian.new(another_user).can_see_groups?([group])).to eq(true) end - it 'correctly handles the case where the user is not a member of every group' do - group1 = Group.new(name: 'group', visibility_level: Group.visibility_levels[:members]) - group2 = Group.new(name: 'group2', visibility_level: Group.visibility_levels[:members]) + it "correctly handles the case where the user is not a member of every group" do + group1 = Group.new(name: "group", visibility_level: Group.visibility_levels[:members]) + group2 = Group.new(name: "group2", visibility_level: Group.visibility_levels[:members]) group2.save! group1.add(member) @@ -3769,21 +3796,21 @@ RSpec.describe Guardian do expect(Guardian.new(owner).can_see_groups?([group1, group2])).to eq(false) end - it 'correctly handles public groups' do - group = Group.new(name: 'group', visibility_level: Group.visibility_levels[:public]) + it "correctly handles public groups" do + group = Group.new(name: "group", visibility_level: Group.visibility_levels[:public]) expect(Guardian.new.can_see_groups?([group])).to eq(true) end - it 'correctly handles case where not every group is public' do - group1 = Group.new(name: 'group', visibility_level: Group.visibility_levels[:public]) - group2 = Group.new(name: 'group', visibility_level: Group.visibility_levels[:private]) + it "correctly handles case where not every group is public" do + group1 = Group.new(name: "group", visibility_level: Group.visibility_levels[:public]) + group2 = Group.new(name: "group", visibility_level: Group.visibility_levels[:private]) expect(Guardian.new.can_see_groups?([group1, group2])).to eq(false) end end - describe 'topic featured link category restriction' do + describe "topic featured link category restriction" do before { SiteSetting.topic_featured_link_enabled = true } let(:guardian) { Guardian.new(user) } let(:uncategorized) { Category.find(SiteSetting.uncategorized_category_id) } @@ -3804,15 +3831,15 @@ RSpec.describe Guardian do end end - context 'when exist' do + context "when exist" do fab!(:category) { Fabricate(:category, topic_featured_link_allowed: false) } fab!(:link_category) { Fabricate(:link_category) } - it 'returns true if the category is listed' do + it "returns true if the category is listed" do expect(guardian.can_edit_featured_link?(link_category.id)).to eq(true) end - it 'returns false if the category does not allow it' do + it "returns false if the category does not allow it" do expect(guardian.can_edit_featured_link?(category.id)).to eq(false) end end @@ -3824,9 +3851,7 @@ RSpec.describe Guardian do end context "with hide suspension reason enabled" do - before do - SiteSetting.hide_suspension_reasons = true - end + before { SiteSetting.hide_suspension_reasons = true } it "will not be shown to anonymous users" do expect(Guardian.new.can_see_suspension_reason?(user)).to eq(false) @@ -3842,15 +3867,14 @@ RSpec.describe Guardian do end end - describe '#can_remove_allowed_users?' do - context 'with staff users' do - it 'should be true' do - expect(Guardian.new(moderator).can_remove_allowed_users?(topic)) - .to eq(true) + describe "#can_remove_allowed_users?" do + context "with staff users" do + it "should be true" do + expect(Guardian.new(moderator).can_remove_allowed_users?(topic)).to eq(true) end end - context 'with trust_level >= 2 user' do + context "with trust_level >= 2 user" do fab!(:topic_creator) { Fabricate(:user, trust_level: 2) } fab!(:topic) { Fabricate(:topic, user: topic_creator) } @@ -3859,13 +3883,12 @@ RSpec.describe Guardian do topic.allowed_users << another_user end - it 'should be true' do - expect(Guardian.new(topic_creator).can_remove_allowed_users?(topic)) - .to eq(true) + it "should be true" do + expect(Guardian.new(topic_creator).can_remove_allowed_users?(topic)).to eq(true) end end - context 'with normal user' do + context "with normal user" do fab!(:topic) { Fabricate(:topic, user: Fabricate(:user, trust_level: 1)) } before do @@ -3873,40 +3896,37 @@ RSpec.describe Guardian do topic.allowed_users << another_user end - it 'should be false' do - expect(Guardian.new(user).can_remove_allowed_users?(topic)) - .to eq(false) + it "should be false" do + expect(Guardian.new(user).can_remove_allowed_users?(topic)).to eq(false) end - describe 'target_user is the user' do - describe 'when user is in a pm with another user' do - it 'should return true' do - expect(Guardian.new(user).can_remove_allowed_users?(topic, user)) - .to eq(true) + describe "target_user is the user" do + describe "when user is in a pm with another user" do + it "should return true" do + expect(Guardian.new(user).can_remove_allowed_users?(topic, user)).to eq(true) end end - describe 'when user is the creator of the topic' do - it 'should return false' do - expect(Guardian.new(topic.user).can_remove_allowed_users?(topic, topic.user)) - .to eq(false) + describe "when user is the creator of the topic" do + it "should return false" do + expect(Guardian.new(topic.user).can_remove_allowed_users?(topic, topic.user)).to eq( + false, + ) end end - describe 'when user is the only user in the topic' do - it 'should return false' do + describe "when user is the only user in the topic" do + it "should return false" do topic.remove_allowed_user(Discourse.system_user, another_user.username) - expect(Guardian.new(user).can_remove_allowed_users?(topic, user)) - .to eq(false) + expect(Guardian.new(user).can_remove_allowed_users?(topic, user)).to eq(false) end end end - describe 'target_user is not the user' do - it 'should return false' do - expect(Guardian.new(user).can_remove_allowed_users?(topic, moderator)) - .to eq(false) + describe "target_user is not the user" do + it "should return false" do + expect(Guardian.new(user).can_remove_allowed_users?(topic, moderator)).to eq(false) end end end @@ -3914,11 +3934,11 @@ RSpec.describe Guardian do context "with anonymous users" do fab!(:topic) { Fabricate(:topic) } - it 'should be false' do + it "should be false" do expect(Guardian.new.can_remove_allowed_users?(topic)).to eq(false) end - it 'should be false when the topic does not have a user (for example because the user was removed)' do + it "should be false when the topic does not have a user (for example because the user was removed)" do DB.exec("UPDATE topics SET user_id=NULL WHERE id=#{topic.id}") topic.reload @@ -3927,22 +3947,23 @@ RSpec.describe Guardian do end end - describe '#auth_token' do - it 'returns the correct auth token' do + describe "#auth_token" do + it "returns the correct auth token" do token = UserAuthToken.generate!(user_id: user.id) - cookie = create_auth_cookie( - token: token.unhashed_auth_token, - user_id: user.id, - trust_level: user.trust_level, - issued_at: 5.minutes.ago, - ) + cookie = + create_auth_cookie( + token: token.unhashed_auth_token, + user_id: user.id, + trust_level: user.trust_level, + issued_at: 5.minutes.ago, + ) env = create_request_env(path: "/").merge("HTTP_COOKIE" => "_t=#{cookie};") guardian = Guardian.new(user, ActionDispatch::Request.new(env)) expect(guardian.auth_token).to eq(token.auth_token) end - it 'supports v0 of auth cookie' do + it "supports v0 of auth cookie" do token = UserAuthToken.generate!(user_id: user.id) cookie = token.unhashed_auth_token env = create_request_env(path: "/").merge("HTTP_COOKIE" => "_t=#{cookie};") @@ -3960,9 +3981,7 @@ RSpec.describe Guardian do end context "when enabled" do - before do - SiteSetting.enable_page_publishing = true - end + before { SiteSetting.enable_page_publishing = true } it "is false for anonymous users" do expect(Guardian.new.can_publish_page?(topic)).to eq(false) @@ -4000,9 +4019,7 @@ RSpec.describe Guardian do describe "#can_see_site_contact_details?" do context "when login_required is enabled" do - before do - SiteSetting.login_required = true - end + before { SiteSetting.login_required = true } it "is false for anonymous users" do expect(Guardian.new.can_see_site_contact_details?).to eq(false) @@ -4014,9 +4031,7 @@ RSpec.describe Guardian do end context "when login_required is disabled" do - before do - SiteSetting.login_required = false - end + before { SiteSetting.login_required = false } it "is true for anonymous users" do expect(Guardian.new.can_see_site_contact_details?).to eq(true) @@ -4029,17 +4044,17 @@ RSpec.describe Guardian do end describe "#can_mention_here?" do - it 'returns false if disabled' do + it "returns false if disabled" do SiteSetting.max_here_mentioned = 0 expect(admin.guardian.can_mention_here?).to eq(false) end - it 'returns false if disabled' do - SiteSetting.here_mention = '' + it "returns false if disabled" do + SiteSetting.here_mention = "" expect(admin.guardian.can_mention_here?).to eq(false) end - it 'works with trust levels' do + it "works with trust levels" do SiteSetting.min_trust_level_for_here_mention = 2 expect(trust_level_0.guardian.can_mention_here?).to eq(false) @@ -4051,16 +4066,16 @@ RSpec.describe Guardian do expect(admin.guardian.can_mention_here?).to eq(true) end - it 'works with staff' do - SiteSetting.min_trust_level_for_here_mention = 'staff' + it "works with staff" do + SiteSetting.min_trust_level_for_here_mention = "staff" expect(trust_level_4.guardian.can_mention_here?).to eq(false) expect(moderator.guardian.can_mention_here?).to eq(true) expect(admin.guardian.can_mention_here?).to eq(true) end - it 'works with admin' do - SiteSetting.min_trust_level_for_here_mention = 'admin' + it "works with admin" do + SiteSetting.min_trust_level_for_here_mention = "admin" expect(trust_level_4.guardian.can_mention_here?).to eq(false) expect(moderator.guardian.can_mention_here?).to eq(false) @@ -4069,9 +4084,7 @@ RSpec.describe Guardian do end describe "#is_category_group_moderator" do - before do - SiteSetting.enable_category_group_moderation = true - end + before { SiteSetting.enable_category_group_moderation = true } fab!(:category) { Fabricate(:category) } diff --git a/spec/lib/has_errors_spec.rb b/spec/lib/has_errors_spec.rb index a63b31ef88..4765d50d77 100644 --- a/spec/lib/has_errors_spec.rb +++ b/spec/lib/has_errors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'has_errors' +require "has_errors" RSpec.describe HasErrors do class ErrorTestClass @@ -11,7 +11,7 @@ RSpec.describe HasErrors do let(:title_error) { "Title can't be blank" } # No title is invalid - let(:invalid_topic) { Fabricate.build(:topic, title: '') } + let(:invalid_topic) { Fabricate.build(:topic, title: "") } it "has no errors by default" do expect(error_test.errors).to be_blank @@ -35,7 +35,9 @@ RSpec.describe HasErrors do it "triggers a rollback" do invalid_topic.valid? - expect { error_test.rollback_from_errors!(invalid_topic) }.to raise_error(ActiveRecord::Rollback) + expect { error_test.rollback_from_errors!(invalid_topic) }.to raise_error( + ActiveRecord::Rollback, + ) expect(error_test.errors).to be_present expect(error_test.errors[:base]).to include(title_error) end @@ -43,12 +45,13 @@ RSpec.describe HasErrors do describe "rollback_with_error!" do it "triggers a rollback" do - - expect do - error_test.rollback_with!(invalid_topic, :too_many_users) - end.to raise_error(ActiveRecord::Rollback) + expect do error_test.rollback_with!(invalid_topic, :too_many_users) end.to raise_error( + ActiveRecord::Rollback, + ) expect(error_test.errors).to be_present - expect(error_test.errors[:base]).to include("You can only send warnings to one user at a time.") + expect(error_test.errors[:base]).to include( + "You can only send warnings to one user at a time.", + ) end end end diff --git a/spec/lib/highlight_js/highlight_js_spec.rb b/spec/lib/highlight_js/highlight_js_spec.rb index d3fd705886..c953ec56db 100644 --- a/spec/lib/highlight_js/highlight_js_spec.rb +++ b/spec/lib/highlight_js/highlight_js_spec.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true RSpec.describe HighlightJs do - it 'can list languages' do - expect(HighlightJs.languages).to include('thrift') + it "can list languages" do + expect(HighlightJs.languages).to include("thrift") end - it 'can generate a packed bundle' do - bundle = HighlightJs.bundle(["thrift", "http"]) + it "can generate a packed bundle" do + bundle = HighlightJs.bundle(%w[thrift http]) expect(bundle).to match(/thrift/) expect(bundle).to match(/http/) expect(bundle).not_to match(/applescript/) end - it 'can get a version string' do + it "can get a version string" do version1 = HighlightJs.version("http|cpp") version2 = HighlightJs.version("rust|cpp|fake") diff --git a/spec/lib/hijack_spec.rb b/spec/lib/hijack_spec.rb index 0e806fd7ab..87e1518a17 100644 --- a/spec/lib/hijack_spec.rb +++ b/spec/lib/hijack_spec.rb @@ -9,10 +9,7 @@ RSpec.describe Hijack do def initialize(env = {}) @io = StringIO.new - env.merge!( - "rack.hijack" => lambda { @io }, - "rack.input" => StringIO.new - ) + env.merge!("rack.hijack" => lambda { @io }, "rack.input" => StringIO.new) self.request = ActionController::TestRequest.new(env, nil, nil) @@ -23,7 +20,6 @@ RSpec.describe Hijack do def hijack_test(&blk) hijack(&blk) end - end let :tester do @@ -44,17 +40,14 @@ RSpec.describe Hijack do @calls = 0 end - after do - Middleware::RequestTracker.unregister_detailed_request_logger logger - end + after { Middleware::RequestTracker.unregister_detailed_request_logger logger } it "can properly track execution" do - app = lambda do |env| - tester = Hijack::Tester.new(env) - tester.hijack_test do - render body: "hello", status: 201 + app = + lambda do |env| + tester = Hijack::Tester.new(env) + tester.hijack_test { render body: "hello", status: 201 } end - end env = create_request_env(path: "/") middleware = Middleware::RequestTracker.new(app) @@ -81,24 +74,20 @@ RSpec.describe Hijack do it "handles cors" do SiteSetting.cors_origins = "www.rainbows.com" - app = lambda do |env| - tester = Hijack::Tester.new(env) - tester.hijack_test do - render body: "hello", status: 201 - end + app = + lambda do |env| + tester = Hijack::Tester.new(env) + tester.hijack_test { render body: "hello", status: 201 } - expect(tester.io.string).to include("Access-Control-Allow-Origin: www.rainbows.com") - end + expect(tester.io.string).to include("Access-Control-Allow-Origin: www.rainbows.com") + end env = {} middleware = Discourse::Cors.new(app) middleware.call(env) # it can do pre-flight - env = { - 'REQUEST_METHOD' => 'OPTIONS', - 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET' - } + env = { "REQUEST_METHOD" => "OPTIONS", "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "GET" } status, headers, _body = middleware.call(env) @@ -106,7 +95,8 @@ RSpec.describe Hijack do expected = { "Access-Control-Allow-Origin" => "www.rainbows.com", - "Access-Control-Allow-Headers" => "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization", + "Access-Control-Allow-Headers" => + "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization", "Access-Control-Allow-Credentials" => "true", "Access-Control-Allow-Methods" => "POST, PUT, GET, OPTIONS, DELETE", "Access-Control-Max-Age" => "7200", @@ -119,24 +109,20 @@ RSpec.describe Hijack do GlobalSetting.stubs(:enable_cors).returns(true) GlobalSetting.stubs(:cors_origin).returns("https://www.rainbows.com/") - app = lambda do |env| - tester = Hijack::Tester.new(env) - tester.hijack_test do - render body: "hello", status: 201 - end + app = + lambda do |env| + tester = Hijack::Tester.new(env) + tester.hijack_test { render body: "hello", status: 201 } - expect(tester.io.string).to include("Access-Control-Allow-Origin: https://www.rainbows.com") - end + expect(tester.io.string).to include("Access-Control-Allow-Origin: https://www.rainbows.com") + end env = {} middleware = Discourse::Cors.new(app) middleware.call(env) # it can do pre-flight - env = { - 'REQUEST_METHOD' => 'OPTIONS', - 'HTTP_ACCESS_CONTROL_REQUEST_METHOD' => 'GET' - } + env = { "REQUEST_METHOD" => "OPTIONS", "HTTP_ACCESS_CONTROL_REQUEST_METHOD" => "GET" } status, headers, _body = middleware.call(env) @@ -144,7 +130,8 @@ RSpec.describe Hijack do expected = { "Access-Control-Allow-Origin" => "https://www.rainbows.com", - "Access-Control-Allow-Headers" => "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization", + "Access-Control-Allow-Headers" => + "Content-Type, Cache-Control, X-Requested-With, X-CSRF-Token, Discourse-Present, User-Api-Key, User-Api-Client-Id, Authorization", "Access-Control-Allow-Credentials" => "true", "Access-Control-Allow-Methods" => "POST, PUT, GET, OPTIONS, DELETE", "Access-Control-Max-Age" => "7200", @@ -173,18 +160,14 @@ RSpec.describe Hijack do end it "renders non 200 status if asked for" do - tester.hijack_test do - render body: "hello world", status: 402 - end + tester.hijack_test { render body: "hello world", status: 402 } expect(tester.io.string).to include("402") expect(tester.io.string).to include("world") end it "handles send_file correctly" do - tester.hijack_test do - send_file __FILE__, disposition: nil - end + tester.hijack_test { send_file __FILE__, disposition: nil } expect(tester.io.string).to start_with("HTTP/1.1 200") end @@ -193,10 +176,11 @@ RSpec.describe Hijack do Process.stubs(:clock_gettime).returns(1.0) tester.hijack_test do Process.stubs(:clock_gettime).returns(2.0) - redirect_to 'http://awesome.com', allow_other_host: true + redirect_to "http://awesome.com", allow_other_host: true end - result = "HTTP/1.1 302 Found\r\nLocation: http://awesome.com\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 84\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\nYou are being redirected." + result = + "HTTP/1.1 302 Found\r\nLocation: http://awesome.com\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 84\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\nYou are being redirected." expect(tester.io.string).to eq(result) end @@ -207,7 +191,8 @@ RSpec.describe Hijack do render body: nil end - result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\n" + result = + "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\n" expect(tester.io.string).to eq(result) end @@ -218,7 +203,8 @@ RSpec.describe Hijack do render plain: "hello world" end - result = "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 11\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\nhello world" + result = + "HTTP/1.1 200 OK\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: 11\r\nConnection: close\r\nX-Runtime: 1.000000\r\n\r\nhello world" expect(tester.io.string).to eq(result) end @@ -226,7 +212,8 @@ RSpec.describe Hijack do Process.stubs(:clock_gettime).returns(1.0) tester.hijack_test - expected = "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 0.000000\r\n\r\n" + expected = + "HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: 0\r\nConnection: close\r\nX-Runtime: 0.000000\r\n\r\n" expect(tester.io.string).to eq(expected) end @@ -234,9 +221,7 @@ RSpec.describe Hijack do tester.io.close ran = false - tester.hijack_test do - ran = true - end + tester.hijack_test { ran = true } expect(ran).to eq(false) end diff --git a/spec/lib/html_prettify_spec.rb b/spec/lib/html_prettify_spec.rb index e9bd6c5138..4401c07a96 100644 --- a/spec/lib/html_prettify_spec.rb +++ b/spec/lib/html_prettify_spec.rb @@ -1,21 +1,22 @@ # frozen_string_literal: true -require 'html_prettify' +require "html_prettify" RSpec.describe HtmlPrettify do - def t(source, expected) expect(HtmlPrettify.render(source)).to eq(expected) end - it 'correctly prettifies html' do + it "correctly prettifies html" do t "

    All's well!

    ", "

    All’s well!

    " t "

    Eatin' Lunch'.

    ", "

    Eatin’ Lunch’.

    " - t "

    a 1/4. is a fraction but not 1/4/2000

    ", "

    a ¼. is a fraction but not 1/4/2000

    " + t "

    a 1/4. is a fraction but not 1/4/2000

    ", + "

    a ¼. is a fraction but not 1/4/2000

    " t "

    Well that'll be the day

    ", "

    Well that’ll be the day

    " - t %(

    "Quoted text"

    ), %(

    “Quoted text”

    ) + t %(

    "Quoted text"

    ), "

    “Quoted text”

    " t "

    I've been meaning to tell you ..

    ", "

    I’ve been meaning to tell you ..

    " - t "

    single `backticks` in HTML should be preserved

    ", "

    single `backticks` in HTML should be preserved

    " + t "

    single `backticks` in HTML should be preserved

    ", + "

    single `backticks` in HTML should be preserved

    " t "

    double hyphen -- ndash --- mdash

    ", "

    double hyphen – ndash — mdash

    " t "a long time ago...", "a long time ago…" t "is 'this a mistake'?", "is ‘this a mistake’?" @@ -27,7 +28,7 @@ RSpec.describe HtmlPrettify do t '\\\\mnt\\c', "\\\\mnt\\c" - t ERB::Util.html_escape(' yay'), "<img src=“test.png”> yay" + t ERB::Util.html_escape(' yay'), + "<img src=“test.png”> yay" end - end diff --git a/spec/lib/html_to_markdown_spec.rb b/spec/lib/html_to_markdown_spec.rb index 0666688077..c106b00b37 100644 --- a/spec/lib/html_to_markdown_spec.rb +++ b/spec/lib/html_to_markdown_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'html_to_markdown' +require "html_to_markdown" RSpec.describe HtmlToMarkdown do - def html_to_markdown(html, opts = {}) HtmlToMarkdown.new(html, opts).to_markdown end @@ -20,7 +19,9 @@ RSpec.describe HtmlToMarkdown do
    HTML - expect(html_to_markdown(html)).to eq("Hello,\n\nThis is the 1st paragraph.\n\nThis is another paragraph") + expect(html_to_markdown(html)).to eq( + "Hello,\n\nThis is the 1st paragraph.\n\nThis is another paragraph", + ) html = <<~HTML @@ -65,7 +66,6 @@ RSpec.describe HtmlToMarkdown do end it "doesn't error on non-inline elements like (aside, section)" do - html = <<~HTML