From 23d49f9d380560f7adf994a095dd5d8dd7a22636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Mar 2024 09:26:22 +0100 Subject: [PATCH 01/12] Hide bundled plugins --- app/controllers/admin_controller.rb | 2 +- app/views/admin/plugins.html.erb | 5 +++-- config/locales/en.yml | 3 ++- lib/open_project/static/links.rb | 7 +++++++ lib/redmine/plugin.rb | 7 +++++++ modules/bim/lib/open_project/bim/engine.rb | 1 + 6 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 91dd292afc99..d670f34df98c 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -57,7 +57,7 @@ def projects end def plugins - @plugins = Redmine::Plugin.all.sort + @plugins = Redmine::Plugin.not_bundled.sort end def test_email diff --git a/app/views/admin/plugins.html.erb b/app/views/admin/plugins.html.erb index 2343204c7d44..5ad7fe0851f0 100644 --- a/app/views/admin/plugins.html.erb +++ b/app/views/admin/plugins.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% html_title t(:label_administration), t(:label_plugins) %> -<%= toolbar title: t(:label_modules_and_plugins) %> +<%= toolbar title: t(:label_plugins) %> <% if @plugins.any? %>
@@ -88,5 +88,6 @@ See COPYRIGHT and LICENSE files for more details.
<% else %> - <%= no_results_box %> + <%= no_results_box display_action: true, + action_url: OpenProject::Static::Links.url_for(:integrations) %> <% end %> diff --git a/config/locales/en.yml b/config/locales/en.yml index b11d56224dcb..a5c6f704c594 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -35,7 +35,8 @@ en: admin: plugins: - no_results_title_text: There are currently no plugins available. + no_results_title_text: There are currently no plugins installed. + no_results_content_text: See our integrations and plugins page for more information. custom_styles: color_theme: "Color theme" color_theme_custom: "(Custom)" diff --git a/lib/open_project/static/links.rb b/lib/open_project/static/links.rb index 601016792134..a1f7fb3b9f40 100644 --- a/lib/open_project/static/links.rb +++ b/lib/open_project/static/links.rb @@ -44,6 +44,10 @@ def links @links ||= static_links.merge(dynamic_links) end + def url_for(item) + links.dig(item, :href) + end + def has?(name) @links.key? name end @@ -277,6 +281,9 @@ def static_links }, ical_docs: { href: 'https://www.openproject.org/docs/user-guide/calendar/#subscribe-to-a-calendar' + }, + integrations: { + href: 'https://www.openproject.org/docs/system-admin-guide/integrations/' } } end diff --git a/lib/redmine/plugin.rb b/lib/redmine/plugin.rb index 4769847a8b6b..af0cac68c5ae 100644 --- a/lib/redmine/plugin.rb +++ b/lib/redmine/plugin.rb @@ -96,6 +96,7 @@ def def_field(*names) end end def_field :gem_name, :url, :author, :author_url, :version, :settings, :bundled + alias :bundled? :bundled attr_reader :id # Plugin constructor @@ -165,6 +166,12 @@ def self.all registered_plugins.values end + def self.not_bundled + registered_plugins + .values + .reject(&:bundled) + end + # Finds a plugin by its id # Returns a PluginNotFound exception if the plugin doesn't exist def self.find(id) diff --git a/modules/bim/lib/open_project/bim/engine.rb b/modules/bim/lib/open_project/bim/engine.rb index accd3e845abc..ea2a6356908b 100644 --- a/modules/bim/lib/open_project/bim/engine.rb +++ b/modules/bim/lib/open_project/bim/engine.rb @@ -36,6 +36,7 @@ class Engine < ::Rails::Engine register 'openproject-bim', author_url: 'https://www.openproject.org', + bundled: true, settings: { default: {} } do From 5c72ea0046a6b5230bf456f55a296ed6fd579535 Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Mon, 18 Mar 2024 13:10:36 +0100 Subject: [PATCH 02/12] switch to double quotes in most changed files with a lot of quotes --- Gemfile | 336 +++++++++---------- config/constants/settings/definition.rb | 402 +++++++++++------------ config/initializers/menus.rb | 372 ++++++++++----------- config/initializers/permissions.rb | 52 +-- config/routes.rb | 414 ++++++++++++------------ lib/open_project/version.rb | 26 +- 6 files changed, 801 insertions(+), 801 deletions(-) diff --git a/Gemfile b/Gemfile index 45910ccfdb70..f7059356a44d 100644 --- a/Gemfile +++ b/Gemfile @@ -26,361 +26,361 @@ # See COPYRIGHT and LICENSE files for more details. #++ -source 'https://rubygems.org' +source "https://rubygems.org" # TODO: Once packager.io and heroku buildpacks support bundler 2.4.22, # then we can use the new bundler syntax `ruby file: '.ruby-version'`. # https://github.com/heroku/heroku-buildpack-ruby/issues/1408#issuecomment-1841596215 -ruby File.read('.ruby-version').strip +ruby File.read(".ruby-version").strip -gem 'actionpack-xml_parser', '~> 2.0.0' -gem 'activemodel-serializers-xml', '~> 1.0.1' -gem 'activerecord-import', '~> 1.5.0' -gem 'activerecord-session_store', '~> 2.1.0' -gem 'ox' -gem 'rails', '~> 7.1.3' -gem 'responders', '~> 3.0' +gem "actionpack-xml_parser", "~> 2.0.0" +gem "activemodel-serializers-xml", "~> 1.0.1" +gem "activerecord-import", "~> 1.5.0" +gem "activerecord-session_store", "~> 2.1.0" +gem "ox" +gem "rails", "~> 7.1.3" +gem "responders", "~> 3.0" -gem 'ffi', '~> 1.15' +gem "ffi", "~> 1.15" -gem 'rdoc', '>= 2.4.2' +gem "rdoc", ">= 2.4.2" -gem 'doorkeeper', '~> 5.6.6' +gem "doorkeeper", "~> 5.6.6" # Maintain our own omniauth due to relative URL root issues # see upstream PR: https://github.com/omniauth/omniauth/pull/903 -gem 'omniauth', git: 'https://github.com/opf/omniauth', ref: 'fe862f986b2e846e291784d2caa3d90a658c67f0' -gem 'request_store', '~> 1.6.0' +gem "omniauth", git: "https://github.com/opf/omniauth", ref: "fe862f986b2e846e291784d2caa3d90a658c67f0" +gem "request_store", "~> 1.6.0" -gem 'warden', '~> 1.2' -gem 'warden-basic_auth', '~> 0.2.1' +gem "warden", "~> 1.2" +gem "warden-basic_auth", "~> 0.2.1" -gem 'will_paginate', '~> 4.0.0' +gem "will_paginate", "~> 4.0.0" -gem 'friendly_id', '~> 5.5.0' +gem "friendly_id", "~> 5.5.0" -gem 'acts_as_list', '~> 1.1.0' -gem 'acts_as_tree', '~> 2.9.0' -gem 'awesome_nested_set', '~> 3.6.0' -gem 'closure_tree', '~> 7.4.0' -gem 'rubytree', '~> 2.0.0' +gem "acts_as_list", "~> 1.1.0" +gem "acts_as_tree", "~> 2.9.0" +gem "awesome_nested_set", "~> 3.6.0" +gem "closure_tree", "~> 7.4.0" +gem "rubytree", "~> 2.0.0" # Only used in down migrations now. # Is to be removed once the referencing migrations have been squashed. -gem 'typed_dag', '~> 2.0.2', require: false +gem "typed_dag", "~> 2.0.2", require: false -gem 'addressable', '~> 2.8.0' +gem "addressable", "~> 2.8.0" # Remove whitespace from model input -gem 'auto_strip_attributes', '~> 2.5' +gem "auto_strip_attributes", "~> 2.5" # Provide timezone info for TZInfo used by AR -gem 'tzinfo-data', '~> 1.2024.1' +gem "tzinfo-data", "~> 1.2024.1" # to generate html-diffs (e.g. for wiki comparison) -gem 'htmldiff' +gem "htmldiff" # Generate url slugs with #to_url and other string niceties -gem 'stringex', '~> 2.8.5' +gem "stringex", "~> 2.8.5" # CommonMark markdown parser with GFM extension -gem 'commonmarker', '~> 1.0.3' +gem "commonmarker", "~> 1.0.3" # HTML pipeline for transformations on text formatter output # such as sanitization or additional features -gem 'html-pipeline', '~> 2.14.0' +gem "html-pipeline", "~> 2.14.0" # Tasklist parsing and renderer -gem 'deckar01-task_list', '~> 2.3.1' +gem "deckar01-task_list", "~> 2.3.1" # Requires escape-utils for faster escaping -gem 'escape_utils', '~> 1.3' +gem "escape_utils", "~> 1.3" # Syntax highlighting used in html-pipeline with rouge -gem 'rouge', '~> 4.2.0' +gem "rouge", "~> 4.2.0" # HTML sanitization used for html-pipeline -gem 'sanitize', '~> 6.1.0' +gem "sanitize", "~> 6.1.0" # HTML autolinking for mails and urls (replaces autolink) -gem 'rinku', '~> 2.0.4', require: %w[rinku rails_rinku] +gem "rinku", "~> 2.0.4", require: %w[rinku rails_rinku] # Version parsing with semver -gem 'semantic', '~> 1.6.1' +gem "semantic", "~> 1.6.1" # generates SVG Graphs # used for statistics on svn repositories -gem 'svg-graph', '~> 2.2.0' +gem "svg-graph", "~> 2.2.0" -gem 'date_validator', '~> 0.12.0' -gem 'email_validator', '~> 2.2.3' -gem 'json_schemer', '~> 2.2.0' -gem 'ruby-duration', '~> 3.2.0' +gem "date_validator", "~> 0.12.0" +gem "email_validator", "~> 2.2.3" +gem "json_schemer", "~> 2.2.0" +gem "ruby-duration", "~> 3.2.0" # `config/initializers/mail_starttls_patch.rb` has also been patched to # fix STARTTLS handling until https://github.com/mikel/mail/pull/1536 is # released. -gem 'mail', '= 2.8.1' +gem "mail", "= 2.8.1" # provide compatible filesystem information for available storage -gem 'sys-filesystem', '~> 1.4.0', require: false +gem "sys-filesystem", "~> 1.4.0", require: false -gem 'bcrypt', '~> 3.1.6' +gem "bcrypt", "~> 3.1.6" -gem 'multi_json', '~> 1.15.0' -gem 'oj', '~> 3.16.0' +gem "multi_json", "~> 1.15.0" +gem "oj", "~> 3.16.0" -gem 'daemons' -gem 'good_job', '~> 3.26.1' # update should be done manually in sync with saas-openproject version. +gem "daemons" +gem "good_job", "~> 3.26.1" # update should be done manually in sync with saas-openproject version. -gem 'rack-protection', '~> 3.2.0' +gem "rack-protection", "~> 3.2.0" # Rack::Attack is a rack middleware to protect your web app from bad clients. # It allows whitelisting, blacklisting, throttling, and tracking based # on arbitrary properties of the request. # https://github.com/kickstarter/rack-attack -gem 'rack-attack', '~> 6.7.0' +gem "rack-attack", "~> 6.7.0" # CSP headers -gem 'secure_headers', '~> 6.5.0' +gem "secure_headers", "~> 6.5.0" # Browser detection for incompatibility checks -gem 'browser', '~> 5.3.0' +gem "browser", "~> 5.3.0" # Providing health checks -gem 'okcomputer', '~> 1.18.1' +gem "okcomputer", "~> 1.18.1" -gem 'gon', '~> 6.4.0' +gem "gon", "~> 6.4.0" # Lograge to provide sane and non-verbose logging -gem 'lograge', '~> 0.14.0' +gem "lograge", "~> 0.14.0" # Structured warnings to selectively disable them in production -gem 'structured_warnings', '~> 0.4.0' +gem "structured_warnings", "~> 0.4.0" # catch exceptions and send them to any airbrake compatible backend # don't require by default, instead load on-demand when actually configured -gem 'airbrake', '~> 13.0.0', require: false +gem "airbrake", "~> 13.0.0", require: false -gem 'md_to_pdf', git: 'https://github.com/opf/md-to-pdf', ref: '8f14736a88ad0064d2a97be108fe7061ffbcee91' -gem 'prawn', '~> 2.4' +gem "md_to_pdf", git: "https://github.com/opf/md-to-pdf", ref: "8f14736a88ad0064d2a97be108fe7061ffbcee91" +gem "prawn", "~> 2.4" # prawn implicitly depends on matrix gem no longer in ruby core with 3.1 -gem 'matrix', '~> 0.4.2' +gem "matrix", "~> 0.4.2" -gem 'meta-tags', '~> 2.20.0' +gem "meta-tags", "~> 2.20.0" -gem 'paper_trail', '~> 15.1.0' +gem "paper_trail", "~> 15.1.0" -gem 'clamav-client', github: 'honestica/clamav-client', ref: '29e78ae94307cb34e79ddd29c5da79752239d8b7' +gem "clamav-client", github: "honestica/clamav-client", ref: "29e78ae94307cb34e79ddd29c5da79752239d8b7" group :production do # we use dalli as standard memcache client # requires memcached 1.4+ - gem 'dalli', '~> 3.2.0' - gem 'redis', '~> 5.1.0' + gem "dalli", "~> 3.2.0" + gem "redis", "~> 5.1.0" end -gem 'i18n-js', '~> 4.2.3' -gem 'rails-i18n', '~> 7.0.0' +gem "i18n-js", "~> 4.2.3" +gem "rails-i18n", "~> 7.0.0" -gem 'sprockets', '~> 3.7.2' # lock sprockets below 4.0 -gem 'sprockets-rails', '~> 3.4.2' +gem "sprockets", "~> 3.7.2" # lock sprockets below 4.0 +gem "sprockets-rails", "~> 3.4.2" -gem 'puma', '~> 6.4' -gem 'puma-plugin-statsd', '~> 2.0' -gem 'rack-timeout', '~> 0.6.3', require: 'rack/timeout/base' +gem "puma", "~> 6.4" +gem "puma-plugin-statsd", "~> 2.0" +gem "rack-timeout", "~> 0.6.3", require: "rack/timeout/base" -gem 'nokogiri', '~> 1.16.0' +gem "nokogiri", "~> 1.16.0" -gem 'carrierwave', '~> 1.3.1' -gem 'carrierwave_direct', '~> 2.1.0' -gem 'fog-aws' +gem "carrierwave", "~> 1.3.1" +gem "carrierwave_direct", "~> 2.1.0" +gem "fog-aws" -gem 'aws-sdk-core', '~> 3.107' +gem "aws-sdk-core", "~> 3.107" # File upload via fog + screenshots on travis -gem 'aws-sdk-s3', '~> 1.91' +gem "aws-sdk-s3", "~> 1.91" -gem 'openproject-token', '~> 4.0' +gem "openproject-token", "~> 4.0" -gem 'plaintext', '~> 0.3.2' +gem "plaintext", "~> 0.3.2" -gem 'ruby-progressbar', '~> 1.13.0', require: false +gem "ruby-progressbar", "~> 1.13.0", require: false -gem 'mini_magick', '~> 4.12.0', require: false +gem "mini_magick", "~> 4.12.0", require: false -gem 'validate_url' +gem "validate_url" # Storages support code -gem 'dry-container' +gem "dry-container" # ActiveRecord extension which adds typecasting to store accessors -gem 'store_attribute', '~> 1.0' +gem "store_attribute", "~> 1.0" # Appsignal integration -gem 'appsignal', '~> 3.0', require: false +gem "appsignal", "~> 3.0", require: false -gem 'view_component' +gem "view_component" # Lookbook -gem 'lookbook', '~> 2.2.1' +gem "lookbook", "~> 2.2.1" # Require factory_bot for usage with openproject plugins testing -gem 'factory_bot', '~> 6.4.0', require: false +gem "factory_bot", "~> 6.4.0", require: false # require factory_bot_rails for convenience in core development -gem 'factory_bot_rails', '~> 6.4.0', require: false +gem "factory_bot_rails", "~> 6.4.0", require: false -gem 'turbo-rails', '~> 2.0.0' +gem "turbo-rails", "~> 2.0.0" -gem 'httpx' +gem "httpx" group :test do - gem 'launchy', '~> 3.0.0' - gem 'rack-test', '~> 2.1.0' - gem 'shoulda-context', '~> 2.0' + gem "launchy", "~> 3.0.0" + gem "rack-test", "~> 2.1.0" + gem "shoulda-context", "~> 2.0" # Test prof provides factories from code # and other niceties - gem 'test-prof', '~> 1.3.0' - gem 'turbo_tests', github: 'crohr/turbo_tests', ref: 'fix/runtime-info' + gem "test-prof", "~> 1.3.0" + gem "turbo_tests", github: "crohr/turbo_tests", ref: "fix/runtime-info" - gem 'rack_session_access' - gem 'rspec', '~> 3.13.0' + gem "rack_session_access" + gem "rspec", "~> 3.13.0" # also add to development group, so 'spec' rake task gets loaded - gem 'rspec-rails', '~> 6.1.0', group: :development + gem "rspec-rails", "~> 6.1.0", group: :development # Retry failures within the same environment - gem 'retriable', '~> 3.1.1' - gem 'rspec-retry', '~> 0.6.1' + gem "retriable", "~> 3.1.1" + gem "rspec-retry", "~> 0.6.1" # Accessibility tests - gem 'axe-core-rspec' + gem "axe-core-rspec" # Modify ENV - gem 'climate_control' + gem "climate_control" # XML comparison tests - gem 'compare-xml', '~> 0.66', require: false + gem "compare-xml", "~> 0.66", require: false # PDF Export tests - gem 'pdf-inspector', '~> 1.2' + gem "pdf-inspector", "~> 1.2" # brings back testing for 'assigns' and 'assert_template' extracted in rails 5 - gem 'rails-controller-testing', '~> 1.0.2' + gem "rails-controller-testing", "~> 1.0.2" - gem 'capybara', '~> 3.40.0' - gem 'capybara_accessible_selectors', git: 'https://github.com/citizensadvice/capybara_accessible_selectors', branch: 'main' - gem 'capybara-screenshot', '~> 1.0.17' - gem 'cuprite', '~> 0.15.0' - gem 'selenium-devtools' - gem 'selenium-webdriver', '~> 4.18.0' + gem "capybara", "~> 3.40.0" + gem "capybara_accessible_selectors", git: "https://github.com/citizensadvice/capybara_accessible_selectors", branch: "main" + gem "capybara-screenshot", "~> 1.0.17" + gem "cuprite", "~> 0.15.0" + gem "selenium-devtools" + gem "selenium-webdriver", "~> 4.18.0" - gem 'fuubar', '~> 2.5.0' - gem 'timecop', '~> 0.9.0' + gem "fuubar", "~> 2.5.0" + gem "timecop", "~> 0.9.0" # Record your test suite's HTTP interactions and replay them during future test runs for fast, deterministic, accurate tests. - gem 'vcr' + gem "vcr" # Mock backend requests (for ruby tests) - gem 'webmock', '~> 3.12', require: false + gem "webmock", "~> 3.12", require: false # Mock selenium requests through proxy (for feature tests) - gem 'puffing-billy', '~> 4.0.0' - gem 'table_print', '~> 1.5.6' + gem "puffing-billy", "~> 4.0.0" + gem "table_print", "~> 1.5.6" - gem 'equivalent-xml', '~> 0.6' - gem 'json_spec', '~> 1.1.4' - gem 'shoulda-matchers', '~> 6.0', require: nil + gem "equivalent-xml", "~> 0.6" + gem "json_spec", "~> 1.1.4" + gem "shoulda-matchers", "~> 6.0", require: nil - gem 'parallel_tests', '~> 4.0' + gem "parallel_tests", "~> 4.0" end group :ldap do - gem 'net-ldap', '~> 0.19.0' + gem "net-ldap", "~> 0.19.0" end group :development do - gem 'listen', '~> 3.9.0' # Use for event-based reloaders + gem "listen", "~> 3.9.0" # Use for event-based reloaders - gem 'letter_opener' + gem "letter_opener" - gem 'spring' - gem 'spring-commands-rspec' - gem 'spring-commands-rubocop' + gem "spring" + gem "spring-commands-rspec" + gem "spring-commands-rubocop" - gem 'colored2' + gem "colored2" # git hooks manager - gem 'lefthook', require: false + gem "lefthook", require: false end group :development, :test do - gem 'dotenv-rails' + gem "dotenv-rails" # Tracing and profiling gems - gem 'flamegraph', require: false - gem 'rack-mini-profiler', require: false - gem 'ruby-prof', require: false - gem 'stackprof', require: false + gem "flamegraph", require: false + gem "rack-mini-profiler", require: false + gem "ruby-prof", require: false + gem "stackprof", require: false # REPL with debug commands - gem 'debug' + gem "debug" - gem 'pry-byebug', '~> 3.10.0', platforms: [:mri] - gem 'pry-doc' - gem 'pry-rails', '~> 0.3.6' - gem 'pry-rescue', '~> 1.6.0' + gem "pry-byebug", "~> 3.10.0", platforms: [:mri] + gem "pry-doc" + gem "pry-rails", "~> 0.3.6" + gem "pry-rescue", "~> 1.6.0" # ruby linting - gem 'rubocop', require: false - gem 'rubocop-inflector', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rails', require: false - gem 'rubocop-rspec', require: false + gem "rubocop", require: false + gem "rubocop-inflector", require: false + gem "rubocop-performance", require: false + gem "rubocop-rails", require: false + gem "rubocop-rspec", require: false # erb linting - gem 'erb_lint', require: false - gem 'erblint-github', require: false + gem "erb_lint", require: false + gem "erblint-github", require: false # Brakeman scanner - gem 'brakeman', '~> 6.1.0' + gem "brakeman", "~> 6.1.0" # i18n-tasks helps find and manage missing and unused translations. - gem 'i18n-tasks', '~> 1.0.13' + gem "i18n-tasks", "~> 1.0.13" end -gem 'bootsnap', '~> 1.18.0', require: false +gem "bootsnap", "~> 1.18.0", require: false # API gems -gem 'grape', '~> 2.0.0' -gem 'grape_logging', '~> 1.8.4' -gem 'roar', '~> 1.2.0' +gem "grape", "~> 2.0.0" +gem "grape_logging", "~> 1.8.4" +gem "roar", "~> 1.2.0" # CORS for API -gem 'rack-cors', '~> 2.0.2' +gem "rack-cors", "~> 2.0.2" # Gmail API -gem 'google-apis-gmail_v1', require: false -gem 'googleauth', require: false +gem "google-apis-gmail_v1", require: false +gem "googleauth", require: false # Required for contracts -gem 'disposable', '~> 0.6.2' +gem "disposable", "~> 0.6.2" platforms :mri, :mingw, :x64_mingw do group :postgres do - gem 'pg', '~> 1.5.0' + gem "pg", "~> 1.5.0" end # Support application loading when no database exists yet. - gem 'activerecord-nulldb-adapter', '~> 1.0.0' + gem "activerecord-nulldb-adapter", "~> 1.0.0" # Have application level locks on the database to have a mutex shared between workers/hosts. # We e.g. employ this to safeguard the creation of journals. - gem 'with_advisory_lock', '~> 5.1.0' + gem "with_advisory_lock", "~> 5.1.0" end # Load Gemfile.modules explicitly to allow dependabot to work -eval_gemfile './Gemfile.modules' +eval_gemfile "./Gemfile.modules" # Load Gemfile.local, Gemfile.plugins and custom Gemfiles -gemfiles = Dir.glob File.expand_path('{Gemfile.plugins,Gemfile.local}', __dir__) -gemfiles << ENV['CUSTOM_PLUGIN_GEMFILE'] unless ENV['CUSTOM_PLUGIN_GEMFILE'].nil? +gemfiles = Dir.glob File.expand_path("{Gemfile.plugins,Gemfile.local}", __dir__) +gemfiles << ENV["CUSTOM_PLUGIN_GEMFILE"] unless ENV["CUSTOM_PLUGIN_GEMFILE"].nil? gemfiles.each do |file| # We use send to allow dependabot to function # don't use eval_gemfile(file) here as it will break dependabot! send(:eval_gemfile, file) if File.readable?(file) end -gem 'openproject-octicons', '~>19.8.0' -gem 'openproject-octicons_helper', '~>19.8.0' -gem 'openproject-primer_view_components', '~>0.23.0' +gem "openproject-octicons", "~>19.8.0" +gem "openproject-octicons_helper", "~>19.8.0" +gem "openproject-primer_view_components", "~>0.23.0" diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index b71ba78e7a4b..c3ec3526f229 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -28,7 +28,7 @@ module Settings class Definition - ENV_PREFIX = 'OPENPROJECT_'.freeze + ENV_PREFIX = "OPENPROJECT_".freeze AR_BOOLEAN_TYPE = ActiveRecord::Type::Boolean.new DEFINITIONS = { activity_days_default: { @@ -36,34 +36,34 @@ class Definition }, after_first_login_redirect_url: { format: :string, - description: 'URL users logging in for the first time will be redirected to (e.g., a help screen)', + description: "URL users logging in for the first time will be redirected to (e.g., a help screen)", default: nil }, after_login_default_redirect_url: { - description: 'Override URL to which logged in users are redirected instead of the My page', + description: "Override URL to which logged in users are redirected instead of the My page", format: :string, default: nil }, apiv3_cors_enabled: { - description: 'Enable CORS headers for APIv3 server responses', + description: "Enable CORS headers for APIv3 server responses", default: false }, apiv3_cors_origins: { default: [] }, apiv3_docs_enabled: { - description: 'Enable interactive APIv3 documentation as part of the application', + description: "Enable interactive APIv3 documentation as part of the application", default: true }, apiv3_enable_basic_auth: { - description: 'Enable API token or global basic authentication for APIv3 requests', + description: "Enable API token or global basic authentication for APIv3 requests", default: true }, apiv3_max_page_size: { default: 1000 }, app_title: { - default: 'OpenProject' + default: "OpenProject" }, attachment_max_size: { default: 5120 @@ -76,41 +76,41 @@ class Definition # Carrierwave storage type. Possible values are, among others, :file and :fog. # The latter requires further configuration. attachments_storage: { - description: 'File storage configuration', + description: "File storage configuration", default: :file, format: :symbol, allowed: %i[file fog], writable: false }, attachments_storage_path: { - description: 'File storage disk location (only applicable for local file storage)', + description: "File storage disk location (only applicable for local file storage)", format: :string, default: nil, writable: false }, attachments_grace_period: { - description: 'Time in minutes to wait before uploaded files not attached to any container are removed', + description: "Time in minutes to wait before uploaded files not attached to any container are removed", default: 180 }, antivirus_scan_mode: { - description: 'Virus scanning option for files uploaded to OpenProject', + description: "Virus scanning option for files uploaded to OpenProject", format: :symbol, default: :disabled, allowed: %i[disabled clamav_socket clamav_host] }, antivirus_scan_target: { - description: 'The socket or hostname to connect to ClamAV', + description: "The socket or hostname to connect to ClamAV", format: :string, default: nil }, antivirus_scan_action: { - description: 'Virus scanning action for found infected files', + description: "Virus scanning action for found infected files", format: :symbol, default: :quarantine, allowed: %i[quarantine delete] }, auth_source_sso: { - description: 'Configuration for Header-based Single Sign-On', + description: "Configuration for Header-based Single Sign-On", format: :hash, default: nil, writable: false # config is cached globally so let's make it not writable @@ -123,7 +123,7 @@ class Definition # user: admin # password: 123456 authentication: { - description: 'Configuration options for global basic auth', + description: "Configuration options for global basic auth", format: :hash, default: nil }, @@ -138,12 +138,12 @@ class Definition allowed: [1, 7, 14, 30, 60, 90, 365] }, autologin_cookie_name: { - description: 'Cookie name for autologin cookie', - default: 'autologin' + description: "Cookie name for autologin cookie", + default: "autologin" }, autologin_cookie_path: { - description: 'Cookie path for autologin cookie', - default: '/' + description: "Cookie path for autologin cookie", + default: "/" }, available_languages: { format: :array, @@ -153,33 +153,33 @@ class Definition allowed: -> { Redmine::I18n.all_languages } }, avatar_link_expiry_seconds: { - description: 'Cache duration for avatar image API responses', + description: "Cache duration for avatar image API responses", default: 24.hours.to_i }, # Allow users with the required permissions to create backups via the web interface or API. backup_enabled: { - description: 'Enable application backups through the UI', + description: "Enable application backups through the UI", default: true }, backup_daily_limit: { - description: 'Maximum number of application backups allowed per day', + description: "Maximum number of application backups allowed per day", default: 3 }, backup_initial_waiting_period: { - description: 'Wait time before newly created backup tokens are usable', + description: "Wait time before newly created backup tokens are usable", default: 24.hours, format: :integer }, backup_include_attachments: { - description: 'Allow inclusion of attachments in application backups', + description: "Allow inclusion of attachments in application backups", default: true }, backup_attachment_size_max_sum_mb: { - description: 'Maximum limit of attachment size to include into application backups', + description: "Maximum limit of attachment size to include into application backups", default: 1024 }, blacklisted_routes: { - description: 'Blocked routes to prevent access to certain modules or pages', + description: "Blocked routes to prevent access to certain modules or pages", default: [], writable: false # used in initializer }, @@ -187,19 +187,19 @@ class Definition default: true }, boards_demo_data_available: { - description: 'Internal setting determining availability of demo seed data', + description: "Internal setting determining availability of demo seed data", default: false }, brute_force_block_minutes: { - description: 'Number of minutes to block users after presumed brute force attack', + description: "Number of minutes to block users after presumed brute force attack", default: 30 }, brute_force_block_after_failed_logins: { - description: 'Number of login attempts per user before assuming brute force attack', + description: "Number of login attempts per user before assuming brute force attack", default: 20 }, cache_expires_in_seconds: { - description: 'Expiration time for memcache entries, empty for no expiry be default', + description: "Expiration time for memcache entries, empty for no expiry be default", format: :integer, default: nil, writable: false @@ -209,40 +209,40 @@ class Definition }, # use dalli defaults for memcache cache_memcache_server: { - description: 'The memcache server host and IP', + description: "The memcache server host and IP", format: :string, default: nil, writable: false }, cache_redis_url: { - description: 'URL to the redis cache server', + description: "URL to the redis cache server", format: :string, default: nil, writable: false }, cache_namespace: { format: :string, - description: 'Namespace for cache keys, useful when multiple applications use a single memcache server', + description: "Namespace for cache keys, useful when multiple applications use a single memcache server", default: nil, writable: false }, commit_fix_done_ratio: { - description: 'Progress to apply when commit fixes work package', + description: "Progress to apply when commit fixes work package", default: 100 }, commit_fix_keywords: { - description: 'Keywords to look for in commit for fixing work packages', - default: 'fixes,closes' + description: "Keywords to look for in commit for fixing work packages", + default: "fixes,closes" }, commit_fix_status_id: { - description: 'Assigned status when fixing keyword is found', + description: "Assigned status when fixing keyword is found", format: :integer, default: nil, allowed: -> { Status.pluck(:id) + [nil] } }, commit_logs_encoding: { description: "Encoding used to convert commit logs to UTF-8", - default: 'UTF-8' + default: "UTF-8" }, commit_logtime_activity_id: { description: :setting_commit_logtime_activity_id, @@ -256,7 +256,7 @@ class Definition }, commit_ref_keywords: { description: "Keywords used in commits for referencing work packages", - default: 'refs,references,IssueID' + default: "refs,references,IssueID" }, consent_decline_mail: { format: :string, @@ -283,7 +283,7 @@ class Definition default: true }, database_cipher_key: { - description: 'Encryption key for repository credentials', + description: "Encryption key for repository credentials", format: :string, default: nil, writable: false @@ -292,28 +292,28 @@ class Definition format: :string, default: nil, allowed: [ - '%Y-%m-%d', - '%d/%m/%Y', - '%d.%m.%Y', - '%d-%m-%Y', - '%m/%d/%Y', - '%d %b %Y', - '%d %B %Y', - '%b %d, %Y', - '%B %d, %Y' + "%Y-%m-%d", + "%d/%m/%Y", + "%d.%m.%Y", + "%d-%m-%Y", + "%m/%d/%Y", + "%d %b %Y", + "%d %B %Y", + "%b %d, %Y", + "%B %d, %Y" ].freeze }, default_auto_hide_popups: { - description: 'Whether to automatically hide success notifications by default', + description: "Whether to automatically hide success notifications by default", default: true }, # user configuration default_comment_sort_order: { - description: 'Default sort order for activities', - default: 'asc' + description: "Default sort order for activities", + default: "asc" }, default_language: { - default: 'en', + default: "en", allowed: -> { Redmine::I18n.all_languages } }, default_projects_modules: { @@ -333,7 +333,7 @@ class Definition default: false }, development_highlight_enabled: { - description: 'Enable highlighting of development environment', + description: "Enable highlighting of development environment", default: -> { Rails.env.development? }, format: :boolean }, @@ -341,18 +341,18 @@ class Definition default: 1500 }, direct_uploads: { - description: 'Enable direct uploads to AWS S3. Only applicable with enabled Fog / AWS S3 configuration', + description: "Enable direct uploads to AWS S3. Only applicable with enabled Fog / AWS S3 configuration", default: true, writable: false }, disable_browser_cache: { - description: 'Prevent browser from caching any logged-in responses for security reasons', + description: "Prevent browser from caching any logged-in responses for security reasons", default: true, writable: false }, # allow to disable default modules disabled_modules: { - description: 'A list of module names to prevent access to in the application', + description: "A list of module names to prevent access to in the application", default: [], allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) }, writable: false # setting stored in global variable @@ -362,48 +362,48 @@ class Definition default: false }, disable_password_login: { - description: 'Disable internal logins and instead only allow SSO through OmniAuth.', + description: "Disable internal logins and instead only allow SSO through OmniAuth.", default: false }, display_subprojects_work_packages: { default: true }, drop_old_sessions_on_logout: { - description: 'Destroy all sessions for current_user on logout', + description: "Destroy all sessions for current_user on logout", default: true }, drop_old_sessions_on_login: { - description: 'Destroy all sessions for current_user on login', + description: "Destroy all sessions for current_user on login", default: false }, edition: { format: :string, - default: 'standard', - description: 'OpenProject edition mode', + default: "standard", + description: "OpenProject edition mode", writable: false, allowed: %w[standard bim] }, ee_manager_visible: { - description: 'Show or hide the Enterprise configuration page and enterprise banners', + description: "Show or hide the Enterprise configuration page and enterprise banners", default: true, writable: false }, enable_internal_assets_server: { - description: 'Serve assets through the Rails internal asset server', + description: "Serve assets through the Rails internal asset server", default: false, writable: false }, # email configuration email_delivery_configuration: { - default: 'inapp', + default: "inapp", allowed: %w[inapp legacy], writable: false, - env_alias: 'EMAIL_DELIVERY_CONFIGURATION' + env_alias: "EMAIL_DELIVERY_CONFIGURATION" }, email_delivery_method: { format: :symbol, default: nil, - env_alias: 'EMAIL_DELIVERY_METHOD' + env_alias: "EMAIL_DELIVERY_METHOD" }, emails_salutation: { allowed: %w[firstname name], @@ -411,12 +411,12 @@ class Definition }, emails_footer: { default: { - 'en' => '' + "en" => "" } }, emails_header: { default: { - 'en' => '' + "en" => "" } }, # use email address as login, hide login in registration form @@ -432,18 +432,18 @@ class Definition }, # Allow connections for trial creation and booking enterprise_trial_creation_host: { - description: 'Host for EE trial service', - default: 'https://start.openproject.com', + description: "Host for EE trial service", + default: "https://start.openproject.com", writable: false }, enterprise_chargebee_site: { - description: 'Site name for EE trial service', - default: 'openproject-enterprise', + description: "Site name for EE trial service", + default: "openproject-enterprise", writable: false }, enterprise_plan: { - description: 'Default EE selected plan', - default: 'enterprise-on-premises---euro---1-year', + description: "Default EE selected plan", + default: "enterprise-on-premises---euro---1-year", writable: false }, feeds_enabled: { @@ -463,54 +463,54 @@ class Definition allowed: [1, 4] }, fog: { - description: 'Configure fog, e.g. when using an S3 uploader', + description: "Configure fog, e.g. when using an S3 uploader", default: {} }, fog_download_url_expires_in: { - description: 'Expiration time in seconds of created shared presigned URLs', + description: "Expiration time in seconds of created shared presigned URLs", default: 21600 # 6h by default as 6 hours is max in S3 when using IAM roles }, # Additional / overridden help links force_help_link: { - description: 'You can set a custom URL for the help button in application header menu.', + description: "You can set a custom URL for the help button in application header menu.", format: :string, default: nil }, force_formatting_help_link: { - description: 'You can set a custom URL for the help button in the WYSIWYG editor.', + description: "You can set a custom URL for the help button in the WYSIWYG editor.", format: :string, default: nil }, forced_single_page_size: { - description: 'Forced page size for manually sorted work package views', + description: "Forced page size for manually sorted work package views", default: 250 }, good_job_queues: { - description: '', + description: "", format: :string, writable: false, - default: '*' + default: "*" }, good_job_max_threads: { - description: '', + description: "", format: :integer, writable: false, default: 20 }, good_job_max_cache: { - description: '', + description: "", format: :integer, writable: false, default: 10_000 }, good_job_enable_cron: { - description: '', + description: "", format: :boolean, writable: false, default: true }, good_job_cleanup_preserved_jobs_before_seconds_ago: { - description: '', + description: "", format: :integer, writable: false, default: 7.days @@ -520,40 +520,40 @@ class Definition }, # Health check configuration health_checks_authentication_password: { - description: 'Add an authentication challenge for the /health_check endpoint', + description: "Add an authentication challenge for the /health_check endpoint", format: :string, default: nil }, ## Maximum number of minutes that jobs have not yet run after their designated 'run_at' time health_checks_jobs_never_ran_minutes_ago: { - description: 'Set threshold of outstanding background jobs to fail health check', + description: "Set threshold of outstanding background jobs to fail health check", format: :integer, default: 5 }, ## Maximum number of unprocessed requests in puma's backlog. health_checks_backlog_threshold: { - description: 'Set threshold of outstanding HTTP requests to fail health check', + description: "Set threshold of outstanding HTTP requests to fail health check", format: :integer, default: 20 }, # Default gravatar image, set to something other than 404 # to ensure a default is returned gravatar_fallback_image: { - description: 'Set default gravatar image fallback', - default: '404' + description: "Set default gravatar image fallback", + default: "404" }, hidden_menu_items: { - description: 'Hide menu items in the menu sidebar for each main menu (such as Administration and Projects).', + description: "Hide menu items in the menu sidebar for each main menu (such as Administration and Projects).", default: {}, writable: false # cached in global variable }, impressum_link: { - description: 'Impressum link to be set, hidden by default', + description: "Impressum link to be set, hidden by default", format: :string, default: nil }, installation_type: { - default: 'manual', + default: "manual", writable: false }, installation_uuid: { @@ -561,7 +561,7 @@ class Definition default: nil }, internal_password_confirmation: { - description: 'Require password confirmations for certain administrative actions', + description: "Require password confirmations for certain administrative actions", default: true }, invitation_expiration_days: { @@ -571,20 +571,20 @@ class Definition default: 5 }, ldap_force_no_page: { - description: 'Force LDAP to respond as a single page, in case paged responses do not work with your server.', + description: "Force LDAP to respond as a single page, in case paged responses do not work with your server.", format: :string, default: nil }, ldap_groups_disable_sync_job: { - description: 'Deactivate regular synchronization job for groups in case scheduled as a separate cronjob', + description: "Deactivate regular synchronization job for groups in case scheduled as a separate cronjob", default: false }, ldap_users_disable_sync_job: { - description: 'Deactivate user attributes synchronization from LDAP', + description: "Deactivate user attributes synchronization from LDAP", default: false }, ldap_users_sync_status: { - description: 'Enable user status (locked/unlocked) synchronization from LDAP', + description: "Enable user status (locked/unlocked) synchronization from LDAP", format: :boolean, default: false }, @@ -594,8 +594,8 @@ class Definition writable: true }, log_level: { - description: 'Set the OpenProject logger level', - default: Rails.env.development? ? 'debug' : 'info', + description: "Set the OpenProject logger level", + default: Rails.env.development? ? "debug" : "info", allowed: %w[debug info warn error fatal], writable: false }, @@ -603,14 +603,14 @@ class Definition default: false }, lograge_enabled: { - description: 'Use lograge formatter for outputting logs', + description: "Use lograge formatter for outputting logs", default: true, format: :boolean, writable: false }, lograge_formatter: { - description: 'Lograge formatter to use for outputting logs', - default: 'key_value', + description: "Lograge formatter to use for outputting logs", + default: "key_value", format: :string, writable: false }, @@ -618,42 +618,42 @@ class Definition default: true }, lookbook_enabled: { - description: 'Enable the Lookbook component documentation tool. Discouraged for production environments.', + description: "Enable the Lookbook component documentation tool. Discouraged for production environments.", default: -> { Rails.env.development? }, format: :boolean }, lost_password: { - description: 'Activate or deactivate lost password form', + description: "Activate or deactivate lost password form", default: true }, mail_from: { - default: 'openproject@example.net' + default: "openproject@example.net" }, mail_handler_api_key: { format: :string, default: nil }, mail_handler_body_delimiters: { - default: '' + default: "" }, mail_handler_body_delimiter_regex: { - default: '' + default: "" }, mail_handler_ignore_filenames: { - default: 'signature.asc' + default: "signature.asc" }, mail_suffix_separators: { - default: '+' + default: "+" }, main_content_language: { - default: 'english', - description: 'Main content language for PostgreSQL full text features', + default: "english", + description: "Main content language for PostgreSQL full text features", writable: false, allowed: %w[danish dutch english finnish french german hungarian italian norwegian portuguese romanian russian simple spanish swedish turkish] }, migration_check_on_exceptions: { - description: 'Check for missing migrations in internal errors', + description: "Check for missing migrations in internal errors", default: true, writable: false }, @@ -671,12 +671,12 @@ class Definition default: 60000 }, oauth_allow_remapping_of_existing_users: { - description: 'When set to false, prevent users from other identity providers to take over accounts connected ' \ - 'to another identity provider.', + description: "When set to false, prevent users from other identity providers to take over accounts connected " \ + "to another identity provider.", default: true }, omniauth_direct_login_provider: { - description: 'Clicking on login sends a login request to the specified OmniAuth provider.', + description: "Clicking on login sends a login request to the specified OmniAuth provider.", format: :string, default: nil }, @@ -687,11 +687,11 @@ class Definition writable: false # this changes a global variable and must therefore not be writable at runtime }, onboarding_video_url: { - description: 'Onboarding guide instructional video URL', - default: 'https://player.vimeo.com/video/163426858?autoplay=1' + description: "Onboarding guide instructional video URL", + default: "https://player.vimeo.com/video/163426858?autoplay=1" }, onboarding_enabled: { - description: 'Enable or disable onboarding guided tour for new users', + description: "Enable or disable onboarding guided tour for new users", default: true }, password_active_rules: { @@ -714,7 +714,7 @@ class Definition # Requires a migration to be written # replace Setting#per_page_options_array per_page_options: { - default: '20, 100' + default: "20, 100" }, plain_text_mail: { default: false @@ -724,63 +724,63 @@ class Definition format: :string }, rails_asset_host: { - description: 'Custom asset hostname for serving assets (e.g., Cloudfront)', + description: "Custom asset hostname for serving assets (e.g., Cloudfront)", format: :string, default: nil, writable: false }, rails_cache_store: { - description: 'Set cache store implemenation to use with OpenProject', + description: "Set cache store implemenation to use with OpenProject", format: :symbol, default: :file_store, writable: false, allowed: %i[file_store memcache redis] }, rails_relative_url_root: { - description: 'Set a URL prefix / base path to run OpenProject under, e.g., host.tld/openproject', - default: '', + description: "Set a URL prefix / base path to run OpenProject under, e.g., host.tld/openproject", + default: "", writable: false }, https: { - description: 'Set assumed connection security for the Rails processes', + description: "Set assumed connection security for the Rails processes", format: :boolean, default: -> { Rails.env.production? }, writable: false }, hsts: { - description: 'Allow disabling of HSTS headers and http -> https redirects', + description: "Allow disabling of HSTS headers and http -> https redirects", format: :boolean, default: true, writable: false }, home_url: { - description: 'Override default link when clicking on the top menu logo (Homescreen by default).', + description: "Override default link when clicking on the top menu logo (Homescreen by default).", format: :string, default: nil }, httpx_connect_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_read_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_write_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), default: 3 }, httpx_keep_alive_timeout: { - description: '', + description: "", format: :float, writable: false, allowed: (0..), @@ -788,27 +788,27 @@ class Definition }, rate_limiting: { default: {}, - description: 'Configure rate limiting for various endpoint rules. See configuration documentation for details.' + description: "Configure rate limiting for various endpoint rules. See configuration documentation for details." }, registration_footer: { default: { - 'en' => '' + "en" => "" } }, remote_storage_upload_host: { format: :string, default: nil, writable: false, - description: 'Host the frontend uses to upload files to, which has to be added to the CSP.' + description: "Host the frontend uses to upload files to, which has to be added to the CSP." }, remote_storage_download_host: { format: :string, default: nil, writable: false, - description: 'Host the frontend uses to download files, which has to be added to the CSP.' + description: "Host the frontend uses to download files, which has to be added to the CSP." }, report_incoming_email_errors: { - description: 'Respond to incoming mails with error details', + description: "Respond to incoming mails with error details", default: true }, repositories_automatic_managed_vendor: { @@ -854,7 +854,7 @@ class Definition writable: false }, scm_local_checkout_path: { - default: 'repositories', # relative to OpenProject directory + default: "repositories", # relative to OpenProject directory writable: false }, scm_subversion_command: { @@ -867,32 +867,32 @@ class Definition default: true }, security_badge_url: { - description: 'URL of the update check badge', + description: "URL of the update check badge", default: "https://releases.openproject.com/v1/check.svg", writable: false }, seed_admin_user_password: { description: 'Password to set for the initially created admin user (Login remains "admin").', - default: 'admin', + default: "admin", writable: false }, seed_admin_user_mail: { - description: 'E-mail to set for the initially created admin user.', - default: 'admin@example.net', + description: "E-mail to set for the initially created admin user.", + default: "admin@example.net", writable: false }, seed_admin_user_name: { - description: 'Name to set for the initially created admin user.', - default: 'OpenProject Admin', + description: "Name to set for the initially created admin user.", + default: "OpenProject Admin", writable: false }, seed_admin_user_password_reset: { - description: 'Whether to force a password reset for the initially created admin user.', + description: "Whether to force a password reset for the initially created admin user.", default: true, writable: false }, seed_ldap: { - description: 'Provide an LDAP connection and sync settings through ENV', + description: "Provide an LDAP connection and sync settings through ENV", writable: false, default: nil, format: :hash @@ -901,12 +901,12 @@ class Definition default: 2 }, sendmail_arguments: { - description: 'Arguments to call sendmail with in case it is configured as outgoing email setup', + description: "Arguments to call sendmail with in case it is configured as outgoing email setup", format: :string, default: "-i" }, sendmail_location: { - description: 'Location of sendmail to call if it is configured as outgoing email setup', + description: "Location of sendmail to call if it is configured as outgoing email setup", format: :string, default: "/usr/sbin/sendmail" }, @@ -914,11 +914,11 @@ class Definition appsignal_frontend_key: { format: :string, default: nil, - description: 'Appsignal API key for JavaScript error reporting' + description: "Appsignal API key for JavaScript error reporting" }, session_cookie_name: { - description: 'Set session cookie name', - default: '_open_project_session' + description: "Set session cookie name", + default: "_open_project_session" }, session_ttl_enabled: { default: false @@ -927,44 +927,44 @@ class Definition default: 120 }, show_community_links: { - description: 'Enable or disable links to OpenProject community instances', + description: "Enable or disable links to OpenProject community instances", default: true }, show_product_version: { - description: 'Show product version information in the administration section', + description: "Show product version information in the administration section", default: true }, show_pending_migrations_warning: { - description: 'Enable or disable warning bar in case of pending migrations', + description: "Enable or disable warning bar in case of pending migrations", default: true, writable: false }, show_setting_mismatch_warning: { - description: 'Show mismatched protocol/hostname warning. In cases where they must differ this can be disabled', + description: "Show mismatched protocol/hostname warning. In cases where they must differ this can be disabled", default: true }, # Render storage information show_storage_information: { - description: 'Show available and taken storage information under administration / info', + description: "Show available and taken storage information under administration / info", default: true }, show_warning_bars: { - description: 'Render warning bars (pending migrations, deprecation, unsupported browsers)', + description: "Render warning bars (pending migrations, deprecation, unsupported browsers)", # Hide warning bars by default in tests as they might overlay other elements default: -> { !Rails.env.test? } }, smtp_authentication: { format: :string, - default: 'plain', - env_alias: 'SMTP_AUTHENTICATION' + default: "plain", + env_alias: "SMTP_AUTHENTICATION" }, smtp_enable_starttls_auto: { format: :boolean, default: false, - env_alias: 'SMTP_ENABLE_STARTTLS_AUTO' + env_alias: "SMTP_ENABLE_STARTTLS_AUTO" }, smtp_openssl_verify_mode: { - description: 'Globally set verify mode for OpenSSL. Careful: Setting to none will disable any SSL verification!', + description: "Globally set verify mode for OpenSSL. Careful: Setting to none will disable any SSL verification!", format: :string, default: "peer", allowed: %w[none peer client_once fail_if_no_peer_cert], @@ -973,43 +973,43 @@ class Definition smtp_ssl: { format: :boolean, default: false, - env_alias: 'SMTP_SSL' + env_alias: "SMTP_SSL" }, smtp_address: { format: :string, - default: '', - env_alias: 'SMTP_ADDRESS' + default: "", + env_alias: "SMTP_ADDRESS" }, smtp_domain: { format: :string, - default: 'your.domain.com', - env_alias: 'SMTP_DOMAIN' + default: "your.domain.com", + env_alias: "SMTP_DOMAIN" }, smtp_user_name: { format: :string, - default: '', - env_alias: 'SMTP_USER_NAME' + default: "", + env_alias: "SMTP_USER_NAME" }, smtp_port: { format: :integer, default: 587, - env_alias: 'SMTP_PORT' + env_alias: "SMTP_PORT" }, smtp_password: { format: :string, - default: '', - env_alias: 'SMTP_PASSWORD' + default: "", + env_alias: "SMTP_PASSWORD" }, software_name: { - description: 'Override software application name', - default: 'OpenProject' + description: "Override software application name", + default: "OpenProject" }, software_url: { - description: 'Override software application URL', - default: 'https://www.openproject.org/' + description: "Override software application URL", + default: "https://www.openproject.org/" }, sql_slow_query_threshold: { - description: 'Time limit in ms after which queries will be logged as slow queries', + description: "Time limit in ms after which queries will be logged as slow queries", default: 2000, writable: false }, @@ -1019,19 +1019,19 @@ class Definition allowed: [1, 6, 7] }, statsd: { - description: 'enable statsd metrics (currently puma only) by configuring host', + description: "enable statsd metrics (currently puma only) by configuring host", default: { - 'host' => nil, - 'port' => 8125 + "host" => nil, + "port" => 8125 }, writable: false }, sys_api_enabled: { - description: 'Enable internal system API for setting up managed repositories', + description: "Enable internal system API for setting up managed repositories", default: false }, sys_api_key: { - description: 'Internal system API key for setting up managed repositories', + description: "Internal system API key for setting up managed repositories", default: nil, format: :string }, @@ -1039,8 +1039,8 @@ class Definition format: :string, default: nil, allowed: [ - '%H:%M', - '%I:%M %p' + "%H:%M", + "%I:%M %p" ].freeze }, user_default_timezone: { @@ -1052,10 +1052,10 @@ class Definition default: false }, user_default_theme: { - default: 'light', + default: "light", format: :string, allowed: -> do - UserPreferences::Schema.schema.dig('definitions', 'UserPreferences', 'properties', 'theme', 'enum') + UserPreferences::Schema.schema.dig("definitions", "UserPreferences", "properties", "theme", "enum") end }, users_deletable_by_self: { @@ -1066,13 +1066,13 @@ class Definition allowed: -> { User::USER_FORMATS_STRUCTURE.keys } }, web: { - description: 'Web worker count and threads configuration', + description: "Web worker count and threads configuration", default: { - 'workers' => 2, - 'timeout' => 120, - 'wait_timeout' => 10, - 'min_threads' => 4, - 'max_threads' => 16 + "workers" => 2, + "timeout" => 120, + "wait_timeout" => 10, + "min_threads" => 4, + "max_threads" => 16 }, writable: false }, @@ -1088,7 +1088,7 @@ class Definition default: false }, work_package_done_ratio: { - default: 'field', + default: "field", allowed: %w[field status disabled] }, work_packages_projects_export_limit: { @@ -1105,7 +1105,7 @@ class Definition }, work_package_list_default_highlighting_mode: { format: :string, - default: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? 'inline' : 'none' }, + default: -> { EnterpriseToken.allows_to?(:conditional_highlighting) ? "inline" : "none" }, allowed: -> { Query::QUERY_HIGHLIGHTING_MODES.map(&:to_s) }, writable: -> { EnterpriseToken.allows_to?(:conditional_highlighting) } }, @@ -1117,14 +1117,14 @@ class Definition default: false }, working_days: { - description: 'Set working days of the week (Array of 1 to 7, where 1=Monday, 7=Sunday)', + description: "Set working days of the week (Array of 1 to 7, where 1=Monday, 7=Sunday)", format: :array, allowed: Array(1..7), default: Array(1..5) # Sat, Sun being non-working days, }, youtube_channel: { - description: 'Link to YouTube channel in help menu', - default: 'https://www.youtube.com/c/OpenProjectCommunity' + description: "Link to YouTube channel in help menu", + default: "https://www.youtube.com/c/OpenProjectCommunity" } }.freeze @@ -1289,7 +1289,7 @@ def all def file_config @file_config ||= begin - filename = Rails.root.join('config/configuration.yml') + filename = Rails.root.join("config/configuration.yml") file_config = {} @@ -1314,8 +1314,8 @@ def override_value(definition) end def override_value_from_file(definition) - envs = ['default', Rails.env] - envs.delete('default') if Rails.env.test? # The test setup should govern the configuration + envs = ["default", Rails.env] + envs.delete("default") if Rails.env.test? # The test setup should govern the configuration envs.each do |env| next unless (env_config = file_config[env]) next unless env_config.has_key?(definition.name) @@ -1379,7 +1379,7 @@ def path_to_hash(*path) end def unescape_underscores(path_segment) - path_segment.gsub '__', '_' + path_segment.gsub "__", "_" end def find_env_var_override(definition) @@ -1449,7 +1449,7 @@ def env_name_alias(definition) def extract_value_from_env(env_var_name, env_var_value) # YAML parses '' as false, but empty ENV variables will be passed as that. # To specify specific values, one can use !!str (-> '') or !!null (-> nil) - return env_var_value if env_var_value == '' + return env_var_value if env_var_value == "" parsed = load_yaml(env_var_value) diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index a5ce41ce3da2..ec7662bd4824 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -26,39 +26,39 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'redmine/menu_manager' +require "redmine/menu_manager" Redmine::MenuManager.map :top_menu do |menu| # projects menu will be added by # Redmine::MenuManager::TopMenuHelper#render_projects_top_menu_node menu.push :projects, - { controller: '/projects', project_id: nil, action: 'index' }, + { controller: "/projects", project_id: nil, action: "index" }, context: :modules, - caption: I18n.t('label_projects_menu'), - icon: 'projects', + caption: I18n.t("label_projects_menu"), + icon: "projects", if: Proc.new { User.current.logged? || !Setting.login_required? } menu.push :activity, - { controller: '/activities', action: 'index' }, + { controller: "/activities", action: "index" }, context: :modules, - icon: 'checkmark' + icon: "checkmark" menu.push :work_packages, - { controller: '/work_packages', project_id: nil, state: nil, action: 'index' }, + { controller: "/work_packages", project_id: nil, state: nil, action: "index" }, context: :modules, - caption: I18n.t('label_work_package_plural'), - icon: 'work-packages', + caption: I18n.t("label_work_package_plural"), + icon: "work-packages", if: Proc.new { (User.current.logged? || !Setting.login_required?) && User.current.allowed_in_any_work_package?(:view_work_packages) } menu.push :news, - { controller: '/news', project_id: nil, action: 'index' }, + { controller: "/news", project_id: nil, action: "index" }, context: :modules, - caption: I18n.t('label_news_plural'), - icon: 'news', + caption: I18n.t("label_news_plural"), + icon: "news", if: Proc.new { (User.current.logged? || !Setting.login_required?) && User.current.allowed_in_any_project?(:view_news) @@ -67,17 +67,17 @@ menu.push :help, OpenProject::Static::Links.help_link, last: true, - caption: '', - icon: 'help op-app-help--icon', + caption: "", + icon: "help op-app-help--icon", html: { accesskey: OpenProject::AccessKeys.key_for(:help), - title: I18n.t('label_help'), - target: '_blank' } + title: I18n.t("label_help"), + target: "_blank" } end Redmine::MenuManager.map :quick_add_menu do |menu| menu.push :new_project, Proc.new { |project| - { controller: '/projects', action: :new, project_id: nil, parent_id: project&.id } + { controller: "/projects", action: :new, project_id: nil, parent_id: project&.id } }, caption: ->(*) { Project.model_name.human }, icon: "add", @@ -93,32 +93,32 @@ menu.push :invite_user, nil, caption: :label_invite_user, - icon: 'user-plus', + icon: "user-plus", html: { - 'invite-user-modal-augment': 'invite-user-modal-augment' + "invite-user-modal-augment": "invite-user-modal-augment" }, if: Proc.new { User.current.allowed_in_any_project?(:manage_members) } end Redmine::MenuManager.map :account_menu do |menu| menu.push :timers, - { controller: '/my/timer', action: 'show' }, - partial: '/my/timer/menu' + { controller: "/my/timer", action: "show" }, + partial: "/my/timer/menu" menu.push :my_page, :my_page_path, - caption: I18n.t('js.my_page.label'), + caption: I18n.t("js.my_page.label"), if: Proc.new { User.current.logged? } menu.push :my_profile, - { controller: '/users', action: 'show', id: 'me' }, + { controller: "/users", action: "show", id: "me" }, caption: :label_my_activity, if: Proc.new { User.current.logged? } menu.push :my_account, - { controller: '/my', action: 'account' }, + { controller: "/my", action: "account" }, if: Proc.new { User.current.logged? } menu.push :administration, - { controller: '/admin', action: 'index' }, + { controller: "/admin", action: "index" }, if: Proc.new { - User.current.allowed_globally?({ controller: '/admin', action: 'index' }) + User.current.allowed_globally?({ controller: "/admin", action: "index" }) } menu.push :logout, :signout_path, @@ -128,53 +128,53 @@ Redmine::MenuManager.map :global_menu do |menu| # Homescreen menu.push :home, - { controller: '/homescreen', action: 'index' }, - icon: 'home', + { controller: "/homescreen", action: "index" }, + icon: "home", first: true # Projects menu.push :projects, - { controller: '/projects', project_id: nil, action: 'index' }, - caption: I18n.t('label_projects_menu'), - icon: 'projects', + { controller: "/projects", project_id: nil, action: "index" }, + caption: I18n.t("label_projects_menu"), + icon: "projects", after: :home, if: Proc.new { User.current.logged? || !Setting.login_required? } menu.push :projects_query_select, - { controller: '/projects', project_id: nil, action: 'index' }, + { controller: "/projects", project_id: nil, action: "index" }, parent: :projects, - partial: 'projects/menus/menu' + partial: "projects/menus/menu" # Activity menu.push :activity, - { controller: '/activities', action: 'index' }, - icon: 'checkmark', + { controller: "/activities", action: "index" }, + icon: "checkmark", after: :projects menu.push :activity_filters, - { controller: '/activities', action: 'index' }, + { controller: "/activities", action: "index" }, parent: :activity, - partial: 'activities/filters_menu' + partial: "activities/filters_menu" # Work packages menu.push :work_packages, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, caption: :label_work_package_plural, - icon: 'view-list', + icon: "view-list", after: :activity menu.push :work_packages_query_select, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, parent: :work_packages, - partial: 'work_packages/menu_query_select' + partial: "work_packages/menu_query_select" # News menu.push :news, - { controller: '/news', project_id: nil, action: 'index' }, - caption: I18n.t('label_news_plural'), - icon: 'news', + { controller: "/news", project_id: nil, action: "index" }, + caption: I18n.t("label_news_plural"), + icon: "news", after: :boards, if: Proc.new { (User.current.logged? || !Setting.login_required?) && @@ -184,194 +184,194 @@ Redmine::MenuManager.map :notifications_menu do |menu| menu.push :notification_grouping_select, - { controller: '/my', action: 'notifications' }, - partial: 'notifications/menu_notification_center' + { controller: "/my", action: "notifications" }, + partial: "notifications/menu_notification_center" end Redmine::MenuManager.map :my_menu do |menu| menu.push :account, - { controller: '/my', action: 'account' }, + { controller: "/my", action: "account" }, caption: :label_profile, - icon: 'user' + icon: "user" menu.push :settings, - { controller: '/my', action: 'settings' }, + { controller: "/my", action: "settings" }, caption: :label_setting_plural, - icon: 'settings2' + icon: "settings2" menu.push :password, - { controller: '/my', action: 'password' }, + { controller: "/my", action: "password" }, caption: :button_change_password, if: Proc.new { User.current.change_password_allowed? }, - icon: 'locked' + icon: "locked" menu.push :access_token, - { controller: '/my', action: 'access_token' }, - caption: I18n.t('my_account.access_tokens.access_tokens'), - icon: 'key' + { controller: "/my", action: "access_token" }, + caption: I18n.t("my_account.access_tokens.access_tokens"), + icon: "key" menu.push :sessions, - { controller: '/my/sessions', action: :index }, - caption: :'users.sessions.title', - icon: 'installation-services' + { controller: "/my/sessions", action: :index }, + caption: :"users.sessions.title", + icon: "installation-services" menu.push :notifications, - { controller: '/my', action: 'notifications' }, - caption: I18n.t('js.notifications.settings.title'), - icon: 'bell' + { controller: "/my", action: "notifications" }, + caption: I18n.t("js.notifications.settings.title"), + icon: "bell" menu.push :reminders, - { controller: '/my', action: 'reminders' }, - caption: I18n.t('js.reminders.settings.title'), - icon: 'email-alert' + { controller: "/my", action: "reminders" }, + caption: I18n.t("js.reminders.settings.title"), + icon: "email-alert" menu.push :delete_account, :delete_my_account_info_path, - caption: I18n.t('account.delete'), + caption: I18n.t("account.delete"), param: :user_id, if: Proc.new { Setting.users_deletable_by_self? }, last: :delete_account, - icon: 'delete' + icon: "delete" end Redmine::MenuManager.map :admin_menu do |menu| menu.push :admin_overview, - { controller: '/admin', action: :index }, + { controller: "/admin", action: :index }, if: Proc.new { User.current.admin? }, caption: :label_overview, - icon: 'home', + icon: "home", first: true menu.push :users, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { !User.current.admin? && (User.current.allowed_globally?(:manage_user) || User.current.allowed_globally?(:create_user)) }, caption: :label_user_plural, - icon: 'group' + icon: "group" menu.push :placeholder_users, - { controller: '/placeholder_users' }, + { controller: "/placeholder_users" }, if: Proc.new { !User.current.admin? && User.current.allowed_globally?(:manage_placeholder_user) }, caption: :label_placeholder_user_plural, - icon: 'group' + icon: "group" menu.push :users_and_permissions, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { User.current.admin? }, caption: :label_user_and_permission, - icon: 'group' + icon: "group" menu.push :user_settings, - { controller: '/admin/settings/users_settings', action: :show }, + { controller: "/admin/settings/users_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_users_settings, parent: :users_and_permissions menu.push :users, - { controller: '/users' }, + { controller: "/users" }, if: Proc.new { User.current.admin? }, caption: :label_user_plural, parent: :users_and_permissions menu.push :placeholder_users, - { controller: '/placeholder_users' }, + { controller: "/placeholder_users" }, if: Proc.new { User.current.admin? }, caption: :label_placeholder_user_plural, parent: :users_and_permissions, - enterprise_feature: 'placeholder_users' + enterprise_feature: "placeholder_users" menu.push :groups, - { controller: '/groups' }, + { controller: "/groups" }, if: Proc.new { User.current.admin? }, caption: :label_group_plural, parent: :users_and_permissions menu.push :roles, - { controller: '/roles' }, + { controller: "/roles" }, if: Proc.new { User.current.admin? }, caption: :label_role_and_permissions, parent: :users_and_permissions menu.push :permissions_report, - { controller: '/roles', action: 'report' }, + { controller: "/roles", action: "report" }, if: Proc.new { User.current.admin? }, caption: :label_permissions_report, parent: :users_and_permissions menu.push :user_avatars, - { controller: '/admin/settings', action: 'show_plugin', id: :openproject_avatars }, + { controller: "/admin/settings", action: "show_plugin", id: :openproject_avatars }, if: Proc.new { User.current.admin? }, caption: :label_avatar_plural, parent: :users_and_permissions menu.push :admin_work_packages, - { controller: '/admin/settings/work_packages_settings', action: :show }, + { controller: "/admin/settings/work_packages_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_work_package_plural, - icon: 'view-list' + icon: "view-list" menu.push :work_packages_setting, - { controller: '/admin/settings/work_packages_settings', action: :show }, + { controller: "/admin/settings/work_packages_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_work_packages menu.push :types, - { controller: '/types' }, + { controller: "/types" }, if: Proc.new { User.current.admin? }, caption: :label_type_plural, parent: :admin_work_packages menu.push :statuses, - { controller: '/statuses' }, + { controller: "/statuses" }, if: Proc.new { User.current.admin? }, caption: :label_status, parent: :admin_work_packages, - html: { class: 'statuses' } + html: { class: "statuses" } menu.push :workflows, - { controller: '/workflows', action: 'edit' }, + { controller: "/workflows", action: "edit" }, if: Proc.new { User.current.admin? }, caption: Proc.new { Workflow.model_name.human }, parent: :admin_work_packages menu.push :custom_fields, - { controller: '/custom_fields' }, + { controller: "/custom_fields" }, if: Proc.new { User.current.admin? }, caption: :label_custom_field_plural, - icon: 'custom-fields', - html: { class: 'custom_fields' } + icon: "custom-fields", + html: { class: "custom_fields" } menu.push :custom_actions, - { controller: '/custom_actions' }, + { controller: "/custom_actions" }, if: Proc.new { User.current.admin? }, - caption: :'custom_actions.plural', + caption: :"custom_actions.plural", parent: :admin_work_packages, - enterprise_feature: 'custom_actions' + enterprise_feature: "custom_actions" menu.push :attribute_help_texts, - { controller: '/attribute_help_texts' }, - caption: :'attribute_help_texts.label_plural', - icon: 'help2', + { controller: "/attribute_help_texts" }, + caption: :"attribute_help_texts.label_plural", + icon: "help2", if: Proc.new { User.current.allowed_globally?(:edit_attribute_help_texts) } menu.push :attachments, - { controller: '/admin/settings/attachments_settings', action: :show }, - caption: :'attributes.attachments', - icon: 'attachment', + { controller: "/admin/settings/attachments_settings", action: :show }, + caption: :"attributes.attachments", + icon: "attachment", if: Proc.new { User.current.admin? } menu.push :attachments_settings, - { controller: '/admin/settings/attachments_settings', action: :show }, + { controller: "/admin/settings/attachments_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :attachments menu.push :virus_scanning_settings, - { controller: '/admin/settings/virus_scanning_settings', action: :show }, - caption: :'settings.antivirus.title', + { controller: "/admin/settings/virus_scanning_settings", action: :show }, + caption: :"settings.antivirus.title", parent: :attachments, - enterprise_feature: 'virus_scanning', + enterprise_feature: "virus_scanning", if: Proc.new { User.current.admin? } menu.push :attachment_quarantine, - { controller: '/admin/attachments/quarantined_attachments', action: :index }, - caption: :'antivirus_scan.quarantined_attachments.title', + { controller: "/admin/attachments/quarantined_attachments", action: :index }, + caption: :"antivirus_scan.quarantined_attachments.title", parent: :attachments, if: Proc.new { User.current.admin? && @@ -379,176 +379,176 @@ } menu.push :enumerations, - { controller: '/enumerations' }, + { controller: "/enumerations" }, if: Proc.new { User.current.admin? }, - icon: 'enumerations' + icon: "enumerations" menu.push :calendars_and_dates, - { controller: '/admin/settings/working_days_settings', action: :show }, + { controller: "/admin/settings/working_days_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_calendars_and_dates, - icon: 'calendar' + icon: "calendar" menu.push :working_days, - { controller: '/admin/settings/working_days_settings', action: :show }, + { controller: "/admin/settings/working_days_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_working_days, parent: :calendars_and_dates menu.push :date_format, - { controller: '/admin/settings/date_format_settings', action: :show }, + { controller: "/admin/settings/date_format_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_date_format, parent: :calendars_and_dates menu.push :icalendar, - { controller: '/admin/settings/icalendar_settings', action: :show }, + { controller: "/admin/settings/icalendar_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_calendar_subscriptions, parent: :calendars_and_dates menu.push :settings, - { controller: '/admin/settings/general_settings', action: :show }, + { controller: "/admin/settings/general_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_system_settings, - icon: 'settings2' + icon: "settings2" SettingsHelper.system_settings_tabs.each do |node| menu.push :"settings_#{node[:name]}", { controller: node[:controller], action: :show }, caption: node[:label], - if: Proc.new { User.current.admin? && node[:name] != 'experimental' }, + if: Proc.new { User.current.admin? && node[:name] != "experimental" }, parent: :settings end menu.push :mail_and_notifications, - { controller: '/admin/settings/aggregation_settings', action: :show }, + { controller: "/admin/settings/aggregation_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.mails_and_notifications', - icon: 'mail1' + caption: :"menus.admin.mails_and_notifications", + icon: "mail1" menu.push :notification_settings, - { controller: '/admin/settings/aggregation_settings', action: :show }, + { controller: "/admin/settings/aggregation_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.aggregation', + caption: :"menus.admin.aggregation", parent: :mail_and_notifications menu.push :mail_notifications, - { controller: '/admin/settings/mail_notifications_settings', action: :show }, + { controller: "/admin/settings/mail_notifications_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.mail_notification', + caption: :"menus.admin.mail_notification", parent: :mail_and_notifications menu.push :incoming_mails, - { controller: '/admin/settings/incoming_mails_settings', action: :show }, + { controller: "/admin/settings/incoming_mails_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_incoming_emails, parent: :mail_and_notifications menu.push :api_and_webhooks, - { controller: '/admin/settings/api_settings', action: :show }, + { controller: "/admin/settings/api_settings", action: :show }, if: Proc.new { User.current.admin? }, - caption: :'menus.admin.api_and_webhooks', - icon: 'relations' + caption: :"menus.admin.api_and_webhooks", + icon: "relations" menu.push :api, - { controller: '/admin/settings/api_settings', action: :show }, + { controller: "/admin/settings/api_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_api_access_key_type, parent: :api_and_webhooks menu.push :authentication, - { controller: '/admin/settings/authentication_settings', action: :show }, + { controller: "/admin/settings/authentication_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_authentication, - icon: 'two-factor-authentication' + icon: "two-factor-authentication" menu.push :authentication_settings, - { controller: '/admin/settings/authentication_settings', action: :show }, + { controller: "/admin/settings/authentication_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :authentication menu.push :ldap_authentication, - { controller: '/ldap_auth_sources', action: 'index' }, + { controller: "/ldap_auth_sources", action: "index" }, if: Proc.new { User.current.admin? && !OpenProject::Configuration.disable_password_login? }, parent: :authentication, caption: :label_ldap_auth_source_plural, - html: { class: 'server_authentication' }, + html: { class: "server_authentication" }, last: true menu.push :oauth_applications, - { controller: '/oauth/applications', action: 'index' }, + { controller: "/oauth/applications", action: "index" }, if: Proc.new { User.current.admin? }, parent: :authentication, - caption: :'oauth.application.plural', - html: { class: 'oauth_applications' } + caption: :"oauth.application.plural", + html: { class: "oauth_applications" } menu.push :announcements, - { controller: '/announcements', action: 'edit' }, + { controller: "/announcements", action: "edit" }, if: Proc.new { User.current.admin? }, caption: :label_announcement, - icon: 'news' + icon: "news" menu.push :plugins, - { controller: '/admin', action: 'plugins' }, + { controller: "/admin", action: "plugins" }, if: Proc.new { User.current.admin? }, last: true, - icon: 'plugins' + icon: "plugins" menu.push :backups, - { controller: '/admin/backups', action: 'show' }, + { controller: "/admin/backups", action: "show" }, if: Proc.new { OpenProject::Configuration.backup_enabled? && User.current.allowed_globally?(Backup.permission) }, caption: :label_backup, last: true, - icon: 'save' + icon: "save" menu.push :info, - { controller: '/admin', action: 'info' }, + { controller: "/admin", action: "info" }, if: Proc.new { User.current.admin? }, caption: :label_information_plural, last: true, - icon: 'info1' + icon: "info1" menu.push :custom_style, - { controller: '/custom_styles', action: :show }, + { controller: "/custom_styles", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_custom_style, - icon: 'design', - enterprise_feature: 'define_custom_style' + icon: "design", + enterprise_feature: "define_custom_style" menu.push :colors, - { controller: '/colors', action: 'index' }, + { controller: "/colors", action: "index" }, if: Proc.new { User.current.admin? }, caption: :label_color_plural, - icon: 'status' + icon: "status" menu.push :enterprise, - { controller: '/enterprises', action: :show }, + { controller: "/enterprises", action: :show }, caption: :label_enterprise_edition, - icon: 'enterprise-addons', + icon: "enterprise-addons", if: proc { User.current.admin? && OpenProject::Configuration.ee_manager_visible? } menu.push :admin_costs, - { controller: '/admin/settings', action: 'show_plugin', id: :costs }, + { controller: "/admin/settings", action: "show_plugin", id: :costs }, if: Proc.new { User.current.admin? }, caption: :project_module_costs, - icon: 'budget' + icon: "budget" menu.push :costs_setting, - { controller: '/admin/settings', action: 'show_plugin', id: :costs }, + { controller: "/admin/settings", action: "show_plugin", id: :costs }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_costs menu.push :admin_backlogs, - { controller: '/backlogs_settings', action: :show }, + { controller: "/backlogs_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_backlogs, - icon: 'backlogs' + icon: "backlogs" menu.push :backlogs_settings, - { controller: '/backlogs_settings', action: :show }, + { controller: "/backlogs_settings", action: :show }, if: Proc.new { User.current.admin? }, caption: :label_setting_plural, parent: :admin_backlogs @@ -556,72 +556,72 @@ Redmine::MenuManager.map :project_menu do |menu| menu.push :activity, - { controller: '/activities', action: 'index' }, - if: Proc.new { |p| p.module_enabled?('activity') }, - icon: 'checkmark' + { controller: "/activities", action: "index" }, + if: Proc.new { |p| p.module_enabled?("activity") }, + icon: "checkmark" menu.push :activity_filters, - { controller: '/activities', action: 'index' }, - if: Proc.new { |p| p.module_enabled?('activity') }, + { controller: "/activities", action: "index" }, + if: Proc.new { |p| p.module_enabled?("activity") }, parent: :activity, - partial: 'activities/filters_menu' + partial: "activities/filters_menu" menu.push :roadmap, - { controller: '/versions', action: 'index' }, + { controller: "/versions", action: "index" }, if: Proc.new { |p| p.shared_versions.any? }, - icon: 'roadmap' + icon: "roadmap" menu.push :work_packages, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, caption: :label_work_package_plural, - if: Proc.new { |p| p.module_enabled?('work_package_tracking') }, - icon: 'view-list', + if: Proc.new { |p| p.module_enabled?("work_package_tracking") }, + icon: "view-list", html: { - id: 'main-menu-work-packages', - 'wp-query-menu': 'wp-query-menu' + id: "main-menu-work-packages", + "wp-query-menu": "wp-query-menu" } menu.push :work_packages_query_select, - { controller: '/work_packages', action: 'index' }, + { controller: "/work_packages", action: "index" }, parent: :work_packages, - partial: 'work_packages/menu_query_select', + partial: "work_packages/menu_query_select", last: true, caption: :label_all_open_wps menu.push :news, - { controller: '/news', action: 'index' }, + { controller: "/news", action: "index" }, caption: :label_news_plural, - icon: 'news' + icon: "news" menu.push :forums, - { controller: '/forums', action: 'index', id: nil }, + { controller: "/forums", action: "index", id: nil }, caption: :label_forum_plural, - icon: 'ticket-note' + icon: "ticket-note" menu.push :repository, - { controller: '/repositories', action: :show }, + { controller: "/repositories", action: :show }, if: Proc.new { |p| p.repository && !p.repository.new_record? }, - icon: 'folder-open' + icon: "folder-open" # Wiki menu items are added by WikiMenuItemHelper menu.push :members, - { controller: '/members', action: 'index' }, + { controller: "/members", action: "index" }, caption: :label_member_plural, before: :settings, - icon: 'group' + icon: "group" menu.push :members_menu, - { controller: '/members', action: 'index' }, + { controller: "/members", action: "index" }, parent: :members, - partial: 'members/menus/menu', + partial: "members/menus/menu", caption: :label_member_plural menu.push :settings, - { controller: '/projects/settings/general', action: :show }, + { controller: "/projects/settings/general", action: :show }, caption: :label_project_settings, last: true, - icon: 'settings2', + icon: "settings2", allow_deeplink: true { @@ -636,7 +636,7 @@ storage: :label_required_disk_storage }.each do |key, caption| menu.push :"settings_#{key}", - { controller: "/projects/settings/#{key}", action: 'show' }, + { controller: "/projects/settings/#{key}", action: "show" }, caption:, parent: :settings end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index d76e3c19f52e..4098e318acf5 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -37,7 +37,7 @@ map.permission :archive_project, { - 'projects/archive': %i[create] + "projects/archive": %i[create] }, permissible_on: :project, require: :member @@ -45,7 +45,7 @@ map.permission :create_backup, { admin: %i[index], - 'admin/backups': %i[delete_token perform_token_reset reset_token show] + "admin/backups": %i[delete_token perform_token_reset reset_token show] }, permissible_on: :global, require: :loggedin, @@ -54,7 +54,7 @@ map.permission :create_user, { users: %i[index show new create resend_invitation], - 'users/memberships': %i[create], + "users/memberships": %i[create], admin: %i[index] }, permissible_on: :global, @@ -64,7 +64,7 @@ map.permission :manage_user, { users: %i[index show edit update change_status change_status_info], - 'users/memberships': %i[create update destroy], + "users/memberships": %i[create update destroy], admin: %i[index] }, permissible_on: :global, @@ -74,7 +74,7 @@ map.permission :manage_placeholder_user, { placeholder_users: %i[index show new create edit update deletion_info destroy], - 'placeholder_users/memberships': %i[create update destroy], + "placeholder_users/memberships": %i[create update destroy], admin: %i[index] }, permissible_on: :global, @@ -93,10 +93,10 @@ map.permission :edit_project, { - 'projects/settings/general': %i[show], - 'projects/settings/storage': %i[show], - 'projects/templated': %i[create destroy], - 'projects/identifier': %i[show update] + "projects/settings/general": %i[show], + "projects/settings/storage": %i[show], + "projects/templated": %i[create destroy], + "projects/identifier": %i[show update] }, permissible_on: :project, require: :member, @@ -104,7 +104,7 @@ map.permission :select_project_modules, { - 'projects/settings/modules': %i[show update] + "projects/settings/modules": %i[show update] }, permissible_on: :project, require: :member @@ -112,7 +112,7 @@ map.permission :manage_members, { members: %i[index new create update destroy autocomplete_for_member menu], - 'members/menus': %i[show] + "members/menus": %i[show] }, permissible_on: :project, require: :member, @@ -122,14 +122,14 @@ map.permission :view_members, { members: %i[index menu], - 'members/menus': %i[show] + "members/menus": %i[show] }, permissible_on: :project, contract_actions: { members: %i[read] } map.permission :manage_versions, { - 'projects/settings/versions': [:show], + "projects/settings/versions": [:show], versions: %i[new create edit update close_completed destroy] }, permissible_on: :project, @@ -137,14 +137,14 @@ map.permission :manage_types, { - 'projects/settings/types': %i[show update] + "projects/settings/types": %i[show update] }, permissible_on: :project, require: :member map.permission :select_custom_fields, { - 'projects/settings/custom_fields': %i[show update] + "projects/settings/custom_fields": %i[show update] }, permissible_on: :project, require: :member @@ -179,7 +179,7 @@ journals: %i[index], work_packages: %i[show index], work_packages_api: [:get], - 'work_packages/reports': %i[report report_details] + "work_packages/reports": %i[report report_details] }, permissible_on: %i[work_package project], contract_actions: { work_packages: %i[read] } @@ -192,7 +192,7 @@ wpt.permission :edit_work_packages, { - 'work_packages/bulk': %i[edit update] + "work_packages/bulk": %i[edit update] }, permissible_on: %i[work_package project], require: :member, @@ -200,7 +200,7 @@ contract_actions: { work_packages: %i[update] } wpt.permission :move_work_packages, - { 'work_packages/moves': %i[new create] }, + { "work_packages/moves": %i[new create] }, permissible_on: :project, require: :loggedin, dependencies: :view_work_packages, @@ -242,7 +242,7 @@ # WorkPackage categories wpt.permission :manage_categories, { - 'projects/settings/categories': [:show], + "projects/settings/categories": [:show], categories: %i[new create edit update destroy] }, permissible_on: :project, @@ -258,7 +258,7 @@ wpt.permission :delete_work_packages, { work_packages: :destroy, - 'work_packages/bulk': :destroy + "work_packages/bulk": :destroy }, permissible_on: :project, require: :member, @@ -304,8 +304,8 @@ map.permission :share_work_packages, { - 'work_packages/shares': %i[index create destroy update resend_invite], - 'work_packages/shares/bulk': %i[update destroy] + "work_packages/shares": %i[index create destroy update resend_invite], + "work_packages/shares/bulk": %i[update destroy] }, permissible_on: :project, dependencies: %i[edit_work_packages view_shared_work_packages], @@ -313,7 +313,7 @@ map.permission :view_shared_work_packages, { - 'work_packages/shares': %i[index] + "work_packages/shares": %i[index] }, permissible_on: :project, require: :member, @@ -350,13 +350,13 @@ news.permission :manage_news, { news: %i[new create edit update destroy preview], - 'news/comments': [:destroy] + "news/comments": [:destroy] }, permissible_on: :project, require: :member news.permission :comment_news, - { 'news/comments': :create }, + { "news/comments": :create }, permissible_on: :project end @@ -429,7 +429,7 @@ repo.permission :manage_repository, { repositories: %i[edit create update committers destroy_info destroy], - 'projects/settings/repository': :show + "projects/settings/repository": :show }, permissible_on: :project, require: :member diff --git a/config/routes.rb b/config/routes.rb index bbf68291a86a..3112563eb962 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -27,74 +27,74 @@ #++ Rails.application.routes.draw do - root to: 'homescreen#index', as: 'home' - rails_relative_url_root = OpenProject::Configuration['rails_relative_url_root'] || '' + root to: "homescreen#index", as: "home" + rails_relative_url_root = OpenProject::Configuration["rails_relative_url_root"] || "" # Route for error pages - get '/404', to: "errors#not_found" - get '/422', to: "errors#unacceptable" - get '/500', to: "errors#internal_error" + get "/404", to: "errors#not_found" + get "/422", to: "errors#unacceptable" + get "/500", to: "errors#internal_error" # Route for health_checks - get '/health_check' => 'ok_computer/ok_computer#show', check: 'web' + get "/health_check" => "ok_computer/ok_computer#show", check: "web" # Override the default `all` checks route to return the full check - get '/health_checks/all' => 'ok_computer/ok_computer#show', check: 'full' + get "/health_checks/all" => "ok_computer/ok_computer#show", check: "full" mount OkComputer::Engine, at: "/health_checks" - get "/api/docs" => 'api_docs#index' + get "/api/docs" => "api_docs#index" # Redirect deprecated issue links to new work packages uris - get '/issues(/)' => redirect("#{rails_relative_url_root}/work_packages") + get "/issues(/)" => redirect("#{rails_relative_url_root}/work_packages") # The URI.escape doesn't escape / unless you ask it to. # see https://github.com/rails/rails/issues/5688 - get '/issues/*rest' => redirect { |params, _req| + get "/issues/*rest" => redirect { |params, _req| "#{rails_relative_url_root}/work_packages/#{URI::RFC2396_Parser.new.escape(params[:rest])}" } # Respond with 410 gone for APIV2 calls - match '/api/v2(/*unmatched_route)', to: proc { [410, {}, ['']] }, via: :all - match '/assets/compiler.js.map', to: proc { [404, {}, ['']] }, via: :all + match "/api/v2(/*unmatched_route)", to: proc { [410, {}, [""]] }, via: :all + match "/assets/compiler.js.map", to: proc { [404, {}, [""]] }, via: :all # Redirect wp short url for work packages to full URL - get '/wp(/)' => redirect("#{rails_relative_url_root}/work_packages") - get '/wp/*rest' => redirect { |params, _req| + get "/wp(/)" => redirect("#{rails_relative_url_root}/work_packages") + get "/wp/*rest" => redirect { |params, _req| "#{rails_relative_url_root}/work_packages/#{URI::RFC2396_Parser.new.escape(params[:rest])}" } # Add catch method for Rack OmniAuth to allow route helpers # Note: This renders a 404 in rails but is caught by omniauth in Rack before - get '/auth/failure', to: 'account#omniauth_failure' - get '/auth/:provider', to: proc { [404, {}, ['']] }, as: 'omniauth_start' - match '/auth/:provider/callback', to: 'account#omniauth_login', as: 'omniauth_login', via: %i[get post] + get "/auth/failure", to: "account#omniauth_failure" + get "/auth/:provider", to: proc { [404, {}, [""]] }, as: "omniauth_start" + match "/auth/:provider/callback", to: "account#omniauth_login", as: "omniauth_login", via: %i[get post] # In case assets are actually delivered by a node server (e.g. in test env) # forward requests to the proxy if FrontendAssetHelper.assets_proxied? - match '/assets/frontend/*appendix', + match "/assets/frontend/*appendix", to: redirect("#{FrontendAssetHelper.cli_proxy}/assets/frontend/%{appendix}", status: 307), format: false, via: :all end - scope controller: 'account' do - get '/account/force_password_change', action: 'force_password_change' - post '/account/change_password', action: 'change_password' - match '/account/lost_password', action: 'lost_password', via: %i[get post] - match '/account/register', action: 'register', via: %i[get post patch] - get '/account/activate', action: 'activate' + scope controller: "account" do + get "/account/force_password_change", action: "force_password_change" + post "/account/change_password", action: "change_password" + match "/account/lost_password", action: "lost_password", via: %i[get post] + match "/account/register", action: "register", via: %i[get post patch] + get "/account/activate", action: "activate" - match '/login', action: 'login', as: 'signin', via: %i[get post] - get '/login/internal', action: 'internal_login', as: 'internal_signin' - get '/logout', action: 'logout', as: 'signout' + match "/login", action: "login", as: "signin", via: %i[get post] + get "/login/internal", action: "internal_login", as: "internal_signin" + get "/logout", action: "logout", as: "signout" - get '/sso', action: 'auth_source_sso_failed', as: 'sso_failure' + get "/sso", action: "auth_source_sso_failed", as: "sso_failure" - get '/login/:stage/failure', action: 'stage_failure', as: 'stage_failure' - get '/login/:stage/:secret', action: 'stage_success', as: 'stage_success' + get "/login/:stage/failure", action: "stage_failure", as: "stage_failure" + get "/login/:stage/:secret", action: "stage_success", as: "stage_success" - get '/account/consent', action: 'consent', as: 'account_consent' - get '/account/decline_consent', action: 'decline_consent', as: 'account_decline_consent' - post '/account/confirm_consent', action: 'confirm_consent', as: 'account_confirm_consent' + get "/account/consent", action: "consent", as: "account_consent" + get "/account/decline_consent", action: "decline_consent", as: "account_decline_consent" + post "/account/confirm_consent", action: "confirm_consent", as: "account_confirm_consent" end # Because of https://github.com/intridea/grape/pull/853/files this has to be @@ -102,7 +102,7 @@ # returned for all routes for which the v3 has also resources. Grape does # remove the prefix (v3) before checking whether the method is supported. I # don't understand why that should make sense. - mount API::Root => '/api' + mount API::Root => "/api" # OAuth authorization routes use_doorkeeper do @@ -110,45 +110,45 @@ skip_controllers :applications, :authorized_applications end - get '/roles/workflow/:id/:role_id/:type_id' => 'roles#workflow' + get "/roles/workflow/:id/:role_id/:type_id" => "roles#workflow" - get '/types/:id/edit/:tab' => "types#edit", + get "/types/:id/edit/:tab" => "types#edit", as: "edit_type_tab" - match '/types/:id/update/:tab' => "types#update", + match "/types/:id/update/:tab" => "types#update", as: "update_type_tab", via: %i[post patch] resources :types do - post 'move/:id', action: 'move', on: :collection + post "move/:id", action: "move", on: :collection end resources :statuses, except: :show do collection do - post 'update_work_package_done_ratio' + post "update_work_package_done_ratio" end end - get 'custom_style/:digest/logo/:filename' => 'custom_styles#logo_download', - as: 'custom_style_logo', + get "custom_style/:digest/logo/:filename" => "custom_styles#logo_download", + as: "custom_style_logo", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/export_logo/:filename' => 'custom_styles#export_logo_download', - as: 'custom_style_export_logo', + get "custom_style/:digest/export_logo/:filename" => "custom_styles#export_logo_download", + as: "custom_style_export_logo", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/export_cover/:filename' => 'custom_styles#export_cover_download', - as: 'custom_style_export_cover', + get "custom_style/:digest/export_cover/:filename" => "custom_styles#export_cover_download", + as: "custom_style_export_cover", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/favicon/:filename' => 'custom_styles#favicon_download', - as: 'custom_style_favicon', + get "custom_style/:digest/favicon/:filename" => "custom_styles#favicon_download", + as: "custom_style_favicon", constraints: { filename: /[^\/]*/ } - get 'custom_style/:digest/touch-icon/:filename' => 'custom_styles#touch_icon_download', - as: 'custom_style_touch_icon', + get "custom_style/:digest/touch-icon/:filename" => "custom_styles#touch_icon_download", + as: "custom_style_touch_icon", constraints: { filename: /[^\/]*/ } - get 'highlighting/styles(/:version_tag)' => 'highlighting#styles', - as: 'highlighting_css_styles' + get "highlighting/styles(/:version_tag)" => "highlighting#styles", + as: "highlighting_css_styles" resources :custom_fields, except: :show do member do @@ -158,25 +158,25 @@ end end - get '(projects/:project_id)/search' => 'search#index', as: 'search' + get "(projects/:project_id)/search" => "search#index", as: "search" # only providing routes for journals when there are multiple subclasses of journals # all subclasses will look for the journals routes resources :journals, only: :index do - get 'diff/:field', action: :diff, on: :member, as: 'diff' + get "diff/:field", action: :diff, on: :member, as: "diff" end # REVIEW: review those wiki routes - scope 'projects/:project_id/wiki/:id' do + scope "projects/:project_id/wiki/:id" do resource :wiki_menu_item, only: %i[edit update] end # generic route for adding/removing watchers. # Models declared as acts_as_watchable will be automatically added to # OpenProject::Acts::Watchable::Routes.watched - scope ':object_type/:object_id', constraints: OpenProject::Acts::Watchable::Routes do - post '/watch' => 'watchers#watch' - delete '/unwatch' => 'watchers#unwatch' + scope ":object_type/:object_id", constraints: OpenProject::Acts::Watchable::Routes do + post "/watch" => "watchers#watch" + delete "/unwatch" => "watchers#unwatch" end namespace :projects do @@ -185,32 +185,32 @@ end resources :projects, except: %i[show edit create update] do - scope module: 'projects' do - namespace 'settings' do - resource :general, only: %i[show], controller: 'general' + scope module: "projects" do + namespace "settings" do + resource :general, only: %i[show], controller: "general" resource :modules, only: %i[show update] resource :types, only: %i[show update] resource :custom_fields, only: %i[show update] - resource :repository, only: %i[show], controller: 'repository' + resource :repository, only: %i[show], controller: "repository" resource :versions, only: %i[show] resource :categories, only: %i[show update] - resource :storage, only: %i[show], controller: 'storage' + resource :storage, only: %i[show], controller: "storage" end - resource :templated, only: %i[create destroy], controller: 'templated' - resource :archive, only: %i[create destroy], controller: 'archive' - resource :identifier, only: %i[show update], controller: 'identifier' + resource :templated, only: %i[create destroy], controller: "templated" + resource :archive, only: %i[create destroy], controller: "archive" + resource :identifier, only: %i[show update], controller: "identifier" end member do - get "settings", to: redirect('projects/%{id}/settings/general/') + get "settings", to: redirect("projects/%{id}/settings/general/") get :copy patch :types # Destroy uses a get request to prompt the user before the actual DELETE request - get :destroy_info, as: 'confirm_destroy' + get :destroy_info, as: "confirm_destroy" end resources :versions, only: %i[new create] do @@ -222,7 +222,7 @@ # this is only another name for versions#index # For nice "road in the url for the index action # this could probably be rewritten with a resource as: 'roadmap' - get '/roadmap' => 'versions#index' + get "/roadmap" => "versions#index" resources :news, only: %i[index new create] @@ -233,25 +233,25 @@ constraints: { id: /([^\/]+(?=\.markdown)|[^\/]+)/ }, except: %i[index create] do collection do - post '/new' => 'wiki#create', as: 'create' + post "/new" => "wiki#create", as: "create" get :export - get '/index' => 'wiki#index' + get "/index" => "wiki#index" get :menu end member do - get '/new' => 'wiki#new_child', as: 'new_child' - get '/diff/:version/vs/:version_from' => 'wiki#diff', as: 'wiki_diff_compare' - get '/diff(/:version)' => 'wiki#diff', as: 'wiki_diff' - get '/annotate/:version' => 'wiki#annotate', as: 'wiki_annotate' - get '/toc' => 'wiki#index' + get "/new" => "wiki#new_child", as: "new_child" + get "/diff/:version/vs/:version_from" => "wiki#diff", as: "wiki_diff_compare" + get "/diff(/:version)" => "wiki#diff", as: "wiki_diff" + get "/annotate/:version" => "wiki#annotate", as: "wiki_annotate" + get "/toc" => "wiki#index" match :rename, via: %i[get patch] - get :parent_page, action: 'edit_parent_page' - patch :parent_page, action: 'update_parent_page' + get :parent_page, action: "edit_parent_page" + patch :parent_page, action: "update_parent_page" get :history post :protect - get :select_main_menu_item, to: 'wiki_menu_items#select_main_menu_item' - post :replace_main_menu_item, to: 'wiki_menu_items#replace_main_menu_item' + get :select_main_menu_item, to: "wiki_menu_items#select_main_menu_item" + post :replace_main_menu_item, to: "wiki_menu_items#replace_main_menu_item" get :menu end end @@ -260,24 +260,24 @@ # it is necessary to define the show action later # than any other route as it otherwise would # work as a catchall for everything under /wiki - get 'wiki' => 'wiki#show' + get "wiki" => "wiki#show" resources :work_packages, only: [] do collection do - get '/report/:detail' => 'work_packages/reports#report_details' - get '/report' => 'work_packages/reports#report' + get "/report/:detail" => "work_packages/reports#report_details" + get "/report" => "work_packages/reports#report" end # states managed by client-side routing on work_package#index - get '(/*state)' => 'work_packages#index', on: :collection, as: '' - get '/create_new' => 'work_packages#index', on: :collection, as: 'new_split' - get '/new' => 'work_packages#index', on: :collection, as: 'new' + get "(/*state)" => "work_packages#index", on: :collection, as: "" + get "/create_new" => "work_packages#index", on: :collection, as: "new_split" + get "/new" => "work_packages#index", on: :collection, as: "new" # state for show view in project context - get '(/*state)' => 'work_packages#show', on: :member, as: '' + get "(/*state)" => "work_packages#show", on: :member, as: "" end - resources :activity, :activities, only: :index, controller: 'activities' do + resources :activity, :activities, only: :index, controller: "activities" do collection do get :menu end @@ -303,7 +303,7 @@ resource :menu, only: %[show] end - resource :repository, controller: 'repositories', except: [:new] do + resource :repository, controller: "repositories", except: [:new] do # Destroy uses a get request to prompt the user before the actual DELETE request get :destroy_info get :committers @@ -311,36 +311,36 @@ get :graph get :revisions - get '/statistics', action: :stats, as: 'stats' + get "/statistics", action: :stats, as: "stats" - get '(/revisions/:rev)/diff.:format', action: :diff - get '(/revisions/:rev)/diff(/*repo_path)', + get "(/revisions/:rev)/diff.:format", action: :diff + get "(/revisions/:rev)/diff(/*repo_path)", action: :diff, - format: 'html', + format: "html", constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ } - get '(/revisions/:rev)/:format/*repo_path', + get "(/revisions/:rev)/:format/*repo_path", action: :entry, format: /raw/, rev: /[\w0-9.\-_]+/ %w{diff annotate changes entry browse}.each do |action| get "(/revisions/:rev)/#{action}(/*repo_path)", - format: 'html', + format: "html", action:, constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, as: "#{action}_revision" end - get '/revision(/:rev)', rev: /[\w0-9.\-_]+/, + get "/revision(/:rev)", rev: /[\w0-9.\-_]+/, action: :revision, - as: 'show_revision' + as: "show_revision" - get '(/revisions/:rev)(/*repo_path)', + get "(/revisions/:rev)(/*repo_path)", action: :show, - format: 'html', + format: "html", constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, - as: 'show_revisions_path' + as: "show_revisions_path" end end @@ -352,52 +352,52 @@ end end - scope 'admin' do + scope "admin" do resource :announcements, only: %i[edit update] constraints(Constraints::Enterprise) do resource :enterprise, only: %i[show create destroy] - scope controller: 'enterprises' do - post 'enterprise/save_trial_key' => 'enterprises#save_trial_key' - delete 'enterprise/delete_trial_key' => 'enterprises#delete_trial_key' + scope controller: "enterprises" do + post "enterprise/save_trial_key" => "enterprises#save_trial_key" + delete "enterprise/delete_trial_key" => "enterprises#delete_trial_key" end end resources :enumerations do - post 'move/:id', action: 'move', on: :collection + post "move/:id", action: "move", on: :collection end - delete 'design/logo' => 'custom_styles#logo_delete', as: 'custom_style_logo_delete' - delete 'design/export_logo' => 'custom_styles#export_logo_delete', as: 'custom_style_export_logo_delete' - delete 'design/export_cover' => 'custom_styles#export_cover_delete', as: 'custom_style_export_cover_delete' - delete 'design/favicon' => 'custom_styles#favicon_delete', as: 'custom_style_favicon_delete' - delete 'design/touch_icon' => 'custom_styles#touch_icon_delete', as: 'custom_style_touch_icon_delete' - get 'design/upsale' => 'custom_styles#upsale', as: 'custom_style_upsale' - post 'design/colors' => 'custom_styles#update_colors', as: 'update_design_colors' - post 'design/themes' => 'custom_styles#update_themes', as: 'update_design_themes' - post 'design/export_cover_text_color' => 'custom_styles#update_export_cover_text_color', - as: 'update_custom_style_export_cover_text_color' + delete "design/logo" => "custom_styles#logo_delete", as: "custom_style_logo_delete" + delete "design/export_logo" => "custom_styles#export_logo_delete", as: "custom_style_export_logo_delete" + delete "design/export_cover" => "custom_styles#export_cover_delete", as: "custom_style_export_cover_delete" + delete "design/favicon" => "custom_styles#favicon_delete", as: "custom_style_favicon_delete" + delete "design/touch_icon" => "custom_styles#touch_icon_delete", as: "custom_style_touch_icon_delete" + get "design/upsale" => "custom_styles#upsale", as: "custom_style_upsale" + post "design/colors" => "custom_styles#update_colors", as: "update_design_colors" + post "design/themes" => "custom_styles#update_themes", as: "update_design_themes" + post "design/export_cover_text_color" => "custom_styles#update_export_cover_text_color", + as: "update_custom_style_export_cover_text_color" - resource :custom_style, only: %i[update show create], path: 'design' + resource :custom_style, only: %i[update show create], path: "design" resources :attribute_help_texts, only: %i(index new create edit update destroy) do - get :upsale, to: 'attribute_help_texts#upsale', on: :collection, as: :upsale + get :upsale, to: "attribute_help_texts#upsale", on: :collection, as: :upsale end resources :groups, except: %i[show] do member do # this should be put into it's own resource - post '/members' => 'groups#add_users', as: 'members_of' - delete '/members/:user_id' => 'groups#remove_user', as: 'member_of' + post "/members" => "groups#add_users", as: "members_of" + delete "/members/:user_id" => "groups#remove_user", as: "member_of" # this should be put into it's own resource - patch '/memberships/:membership_id' => 'groups#edit_membership', as: 'membership_of' - put '/memberships/:membership_id' => 'groups#edit_membership' - delete '/memberships/:membership_id' => 'groups#destroy_membership' - post '/memberships' => 'groups#create_memberships', as: 'memberships_of' + patch "/memberships/:membership_id" => "groups#edit_membership", as: "membership_of" + put "/memberships/:membership_id" => "groups#edit_membership" + delete "/memberships/:membership_id" => "groups#destroy_membership" + post "/memberships" => "groups#create_memberships", as: "memberships_of" end end resources :roles, except: %i[show] do collection do - put '/' => 'roles#bulk_update' + put "/" => "roles#bulk_update" get :report end end @@ -422,37 +422,37 @@ patch tab[:name], controller: tab[:controller], action: :update, as: "update_#{tab[:name]}" end - resource :authentication, controller: '/admin/settings/authentication_settings', only: %i[show update] - resource :attachments, controller: '/admin/settings/attachments_settings', only: %i[show update] - resource :virus_scanning, controller: '/admin/settings/virus_scanning_settings', only: %i[show update] do + resource :authentication, controller: "/admin/settings/authentication_settings", only: %i[show update] + resource :attachments, controller: "/admin/settings/attachments_settings", only: %i[show update] + resource :virus_scanning, controller: "/admin/settings/virus_scanning_settings", only: %i[show update] do collection do get :av_form end end - resource :incoming_mails, controller: '/admin/settings/incoming_mails_settings', only: %i[show update] - resource :aggregation, controller: '/admin/settings/aggregation_settings', only: %i[show update] - resource :mail_notifications, controller: '/admin/settings/mail_notifications_settings', only: %i[show update] - resource :api, controller: '/admin/settings/api_settings', only: %i[show update] - resource :work_packages, controller: '/admin/settings/work_packages_settings', only: %i[show update] - resource :working_days, controller: '/admin/settings/working_days_settings', only: %i[show update] - resource :users, controller: '/admin/settings/users_settings', only: %i[show update] - resource :date_format, controller: '/admin/settings/date_format_settings', only: %i[show update] - resource :icalendar, controller: '/admin/settings/icalendar_settings', only: %i[show update] + resource :incoming_mails, controller: "/admin/settings/incoming_mails_settings", only: %i[show update] + resource :aggregation, controller: "/admin/settings/aggregation_settings", only: %i[show update] + resource :mail_notifications, controller: "/admin/settings/mail_notifications_settings", only: %i[show update] + resource :api, controller: "/admin/settings/api_settings", only: %i[show update] + resource :work_packages, controller: "/admin/settings/work_packages_settings", only: %i[show update] + resource :working_days, controller: "/admin/settings/working_days_settings", only: %i[show update] + resource :users, controller: "/admin/settings/users_settings", only: %i[show update] + resource :date_format, controller: "/admin/settings/date_format_settings", only: %i[show update] + resource :icalendar, controller: "/admin/settings/icalendar_settings", only: %i[show update] # Redirect /settings to general settings - get '/', to: redirect('/admin/settings/general') + get "/", to: redirect("/admin/settings/general") # Plugin settings - get 'plugin/:id', action: :show_plugin - post 'plugin/:id', action: :update_plugin + get "plugin/:id", action: :show_plugin + post "plugin/:id", action: :update_plugin end resources :quarantined_attachments, - controller: '/admin/attachments/quarantined_attachments', + controller: "/admin/attachments/quarantined_attachments", only: %i[index destroy] - resource :backups, controller: '/admin/backups', only: %i[show] do + resource :backups, controller: "/admin/backups", only: %i[show] do collection do get :reset_token post :reset_token, action: :perform_token_reset @@ -465,48 +465,48 @@ resource :workflows, only: %i[edit update show] do member do # We should fix this crappy routing (split up and rename controller methods) - match 'copy', action: 'copy', via: %i[get post] + match "copy", action: "copy", via: %i[get post] end end namespace :work_packages do - match 'auto_complete' => 'auto_completes#index', via: %i[get post] - resource :bulk, controller: 'bulk', only: %i[edit update destroy] + match "auto_complete" => "auto_completes#index", via: %i[get post] + resource :bulk, controller: "bulk", only: %i[edit update destroy] # FIXME: this is kind of evil!! We need to remove this soonest and # cover the functionality. Route is being used in work-package-service.js:331 - get '/bulk' => 'bulk#destroy' + get "/bulk" => "bulk#destroy" resources :shares, only: %i[destroy update] end resources :work_packages, only: [:index] do # move bulk of wps - get 'move/new' => 'work_packages/moves#new', on: :collection, as: 'new_move' - post 'move' => 'work_packages/moves#create', on: :collection, as: 'move' + get "move/new" => "work_packages/moves#new", on: :collection, as: "new_move" + post "move" => "work_packages/moves#create", on: :collection, as: "move" # move individual wp - resource :move, controller: 'work_packages/moves', only: %i[new create] + resource :move, controller: "work_packages/moves", only: %i[new create] # states managed by client-side routing on work_package#index - get 'details/*state' => 'work_packages#index', on: :collection, as: :details + get "details/*state" => "work_packages#index", on: :collection, as: :details # Rails managed sharing route - resources :shares, controller: 'work_packages/shares', only: %i[index create] do + resources :shares, controller: "work_packages/shares", only: %i[index create] do member do - post 'resend_invite' => 'work_packages/shares#resend_invite' + post "resend_invite" => "work_packages/shares#resend_invite" end collection do - resource :bulk, controller: 'work_packages/shares/bulk', only: %i[update destroy], as: :shares_bulk + resource :bulk, controller: "work_packages/shares/bulk", only: %i[update destroy], as: :shares_bulk end end # states managed by client-side (angular) routing on work_package#show - get '/' => 'work_packages#index', on: :collection, as: 'index' - get '/create_new' => 'work_packages#index', on: :collection, as: 'new_split' - get '/new' => 'work_packages#index', on: :collection, as: 'new', state: 'new' + get "/" => "work_packages#index", on: :collection, as: "index" + get "/create_new" => "work_packages#index", on: :collection, as: "new_split" + get "/new" => "work_packages#index", on: :collection, as: "new", state: "new" # We do not want to match the work package export routes - get '(/*state)' => 'work_packages#show', on: :member, as: '', constraints: { id: /\d+/ } - get '/share_upsale' => 'work_packages#index', on: :collection, as: 'share_upsale' - get '/edit' => 'work_packages#show', on: :member, as: 'edit' + get "(/*state)" => "work_packages#show", on: :member, as: "", constraints: { id: /\d+/ } + get "/share_upsale" => "work_packages#index", on: :collection, as: "share_upsale" + get "/edit" => "work_packages#show", on: :member, as: "edit" end resources :versions, only: %i[show edit update destroy] do @@ -515,18 +515,18 @@ end end - resources :activity, :activities, only: :index, controller: 'activities' do + resources :activity, :activities, only: :index, controller: "activities" do collection do get :menu end end resources :users, constraints: { id: /(\d+|me)/ }, except: :edit do - resources :memberships, controller: 'users/memberships', only: %i[update create destroy] + resources :memberships, controller: "users/memberships", only: %i[update create destroy] member do - get '/edit(/:tab)' => 'users#edit', as: 'edit' - get '/change_status/:change_action' => 'users#change_status_info', as: 'change_status_info' + get "/edit(/:tab)" => "users#edit", as: "edit" + get "/change_status/:change_action" => "users#change_status_info", as: "change_status_info" post :change_status post :resend_invitation get :deletion_info @@ -534,10 +534,10 @@ end resources :placeholder_users, except: :edit do - resources :memberships, controller: 'placeholder_users/memberships', only: %i[update create destroy] + resources :memberships, controller: "placeholder_users/memberships", only: %i[update create destroy] member do - get '/edit(/:tab)' => 'placeholder_users#edit', as: 'edit' + get "/edit(/:tab)" => "placeholder_users#edit", as: "edit" get :deletion_info end end @@ -546,33 +546,33 @@ resources :groups, only: %i[show], as: :show_group resources :forums, only: [] do - resources :topics, controller: 'messages', except: [:index], shallow: true do + resources :topics, controller: "messages", except: [:index], shallow: true do member do get :quote - post :reply, as: 'reply_to' + post :reply, as: "reply_to" end end end resources :news, only: %i[index destroy update edit show] do - resources :comments, controller: 'news/comments', only: %i[create destroy], shallow: true + resources :comments, controller: "news/comments", only: %i[create destroy], shallow: true end # redirect for backwards compatibility - scope 'attachments', + scope "attachments", constraints: { id: /\d+/, filename: /[^\/]*/ }, format: false do - get '/download/:id/:filename', + get "/download/:id/:filename", to: redirect("#{rails_relative_url_root}/attachments/%{id}/%{filename}") - get '/download/:id', + get "/download/:id", to: redirect("#{rails_relative_url_root}/attachments/%{id}") - scope ':id' do - get '(/:filename)', + scope ":id" do + get "(/:filename)", to: redirect("#{rails_relative_url_root}/api/v3/attachments/%{id}/content") - delete '', + delete "", to: redirect("#{rails_relative_url_root}/api/v3/attachments/%{id}") end end @@ -584,45 +584,45 @@ end end - scope controller: 'sys' do - match '/sys/repo_auth', action: 'repo_auth', via: %i[get post] - get '/sys/projects', action: 'projects' - get '/sys/fetch_changesets', action: 'fetch_changesets' - get '/sys/projects/:id/repository/update_storage', action: 'update_required_storage' + scope controller: "sys" do + match "/sys/repo_auth", action: "repo_auth", via: %i[get post] + get "/sys/projects", action: "projects" + get "/sys/fetch_changesets", action: "fetch_changesets" + get "/sys/projects/:id/repository/update_storage", action: "update_required_storage" end # alternate routes for the current user - scope 'my' do - get '/deletion_info' => 'users#deletion_info', as: 'delete_my_account_info' - post '/oauth/revoke_application/:application_id' => 'oauth/grants#revoke_application', as: 'revoke_my_oauth_application' - delete '/storage_token/:id' => 'my#delete_storage_token', as: 'storage_token_delete' + scope "my" do + get "/deletion_info" => "users#deletion_info", as: "delete_my_account_info" + post "/oauth/revoke_application/:application_id" => "oauth/grants#revoke_application", as: "revoke_my_oauth_application" + delete "/storage_token/:id" => "my#delete_storage_token", as: "storage_token_delete" - resources :sessions, controller: 'my/sessions', as: 'my_sessions', only: %i[index show destroy] - resources :auto_login_tokens, controller: 'my/auto_login_tokens', as: 'my_auto_login_tokens', only: %i[destroy] + resources :sessions, controller: "my/sessions", as: "my_sessions", only: %i[index show destroy] + resources :auto_login_tokens, controller: "my/auto_login_tokens", as: "my_auto_login_tokens", only: %i[destroy] end - scope controller: 'my' do - get '/my/password', action: 'password' - post '/my/change_password', action: 'change_password' + scope controller: "my" do + get "/my/password", action: "password" + post "/my/change_password", action: "change_password" - get '/my/account', action: 'account' - get '/my/settings', action: 'settings' - get '/my/notifications', action: 'notifications' - get '/my/reminders', action: 'reminders' + get "/my/account", action: "account" + get "/my/settings", action: "settings" + get "/my/notifications", action: "notifications" + get "/my/reminders", action: "reminders" - patch '/my/account', action: 'update_account' - patch '/my/settings', action: 'update_settings' + patch "/my/account", action: "update_account" + patch "/my/settings", action: "update_settings" - post '/my/generate_rss_key', action: 'generate_rss_key' - delete '/my/revoke_rss_key', action: 'revoke_rss_key' - post '/my/generate_api_key', action: 'generate_api_key' - delete '/my/revoke_api_key', action: 'revoke_api_key' - delete '/my/revoke_ical_token', action: 'revoke_ical_token' - get '/my/access_token', action: 'access_token' + post "/my/generate_rss_key", action: "generate_rss_key" + delete "/my/revoke_rss_key", action: "revoke_rss_key" + post "/my/generate_api_key", action: "generate_api_key" + delete "/my/revoke_api_key", action: "revoke_api_key" + delete "/my/revoke_ical_token", action: "revoke_ical_token" + get "/my/access_token", action: "access_token" end - scope controller: 'onboarding' do - patch 'user_settings', action: 'user_settings' + scope controller: "onboarding" do + patch "user_settings", action: "user_settings" end resources :colors do @@ -633,18 +633,18 @@ end end - get '/robots' => 'homescreen#robots', defaults: { format: :txt } + get "/robots" => "homescreen#robots", defaults: { format: :txt } - root to: 'account#login' + root to: "account#login" scope :notifications do - get '(/*state)', to: 'angular#notifications_layout', as: :notifications_center + get "(/*state)", to: "angular#notifications_layout", as: :notifications_center end # OAuthClient needs a "callback" URL that Nextcloud calls with a "code" (see OAuth2 RFC) - scope 'oauth_clients/:oauth_client_id' do - get 'callback', controller: 'oauth_clients', action: :callback - get 'ensure_connection', controller: 'oauth_clients', action: :ensure_connection, as: 'oauth_clients_ensure_connection' + scope "oauth_clients/:oauth_client_id" do + get "callback", controller: "oauth_clients", action: :callback + get "ensure_connection", controller: "oauth_clients", action: :ensure_connection, as: "oauth_clients_ensure_connection" end if OpenProject::Configuration.lookbook_enabled? @@ -652,6 +652,6 @@ end if Rails.env.development? - mount GoodJob::Engine => 'good_job' + mount GoodJob::Engine => "good_job" end end diff --git a/lib/open_project/version.rb b/lib/open_project/version.rb index 69c9ca44612d..b545805e3995 100644 --- a/lib/open_project/version.rb +++ b/lib/open_project/version.rb @@ -26,8 +26,8 @@ # See COPYRIGHT and LICENSE files for more details. #++ -require 'rexml/document' -require 'open3' +require "rexml/document" +require "open3" module OpenProject module VERSION # :nodoc: @@ -48,7 +48,7 @@ class << self # # 2.0.0debian-2 def special - '' + "" end def revision @@ -57,31 +57,31 @@ def revision def core_sha cached_or_block(:@core_sha) do - read_optional 'CORE_VERSION' + read_optional "CORE_VERSION" end end def core_url cached_or_block(:@core_url) do - read_optional 'CORE_URL' + read_optional "CORE_URL" end end def product_sha cached_or_block(:@product_sha) do - read_optional 'PRODUCT_VERSION' + read_optional "PRODUCT_VERSION" end end def product_url cached_or_block(:@product_url) do - read_optional 'PRODUCT_URL' + read_optional "PRODUCT_URL" end end def builder_sha cached_or_block(:@builder_sha) do - read_optional 'BUILDER_VERSION' + read_optional "BUILDER_VERSION" end end @@ -98,14 +98,14 @@ def to_a; ARRAY end def to_s; STRING end def to_semver - [MAJOR, MINOR, PATCH].join('.') + special + [MAJOR, MINOR, PATCH].join(".") + special end private def release_date_from_file cached_or_block(:@release_date_from_file) do - path = Rails.root.join('RELEASE_DATE') + path = Rails.root.join("RELEASE_DATE") if File.exist? path s = File.read(path) Date.parse(s) @@ -115,7 +115,7 @@ def release_date_from_file def release_date_from_git cached_or_block(:@release_date_from_git) do - date, = Open3.capture3('git', 'log', '-1', '--format=%cd', '--date=short') + date, = Open3.capture3("git", "log", "-1", "--format=%cd", "--date=short") Date.parse(date) if date end end @@ -128,7 +128,7 @@ def revision_from_core_sha def revision_from_git cached_or_block(:@revision) do - revision, = Open3.capture3('git', 'rev-parse', 'HEAD') + revision, = Open3.capture3("git", "rev-parse", "HEAD") if revision.present? revision.strip[0..8] end @@ -157,6 +157,6 @@ def cached_or_block(variable) REVISION = revision ARRAY = [MAJOR, MINOR, PATCH, REVISION].compact - STRING = ARRAY.join('.') + STRING = ARRAY.join(".") end end From ddb6b767645204580d0f0890543046aad3757c5b Mon Sep 17 00:00:00 2001 From: Ivan Kuchin Date: Mon, 18 Mar 2024 13:13:30 +0100 Subject: [PATCH 03/12] add switching to double quotes in most changed files to .git-blame-ignore-revs --- .git-blame-ignore-revs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b2eac6ca41e7..364045d10d63 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -5,3 +5,5 @@ 21a696ef9b170e14ad2daf53364a4c2113822c2f # Update copyright information for 2023 c795874f7f281297bbd3bad2fdb58b24cb4ce624 +# switch to double quotes in most changed files with a lot of quotes +5c72ea0046a6b5230bf456f55a296ed6fd579535 From 20370871cdeea9ea9a6f15f81d96f62e499f8451 Mon Sep 17 00:00:00 2001 From: Christophe Bliard Date: Mon, 18 Mar 2024 14:21:25 +0100 Subject: [PATCH 04/12] Fix overlapping character ranges in regex `\w` is equivalent to `[a-zA-Z0-9_]`, and already includes `0-9` and `_`. --- config/routes.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 3112563eb962..89c4963a26d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -317,29 +317,29 @@ get "(/revisions/:rev)/diff(/*repo_path)", action: :diff, format: "html", - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ } + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ } get "(/revisions/:rev)/:format/*repo_path", action: :entry, format: /raw/, - rev: /[\w0-9.\-_]+/ + rev: /[\w.\-]+/ %w{diff annotate changes entry browse}.each do |action| get "(/revisions/:rev)/#{action}(/*repo_path)", format: "html", action:, - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ }, as: "#{action}_revision" end - get "/revision(/:rev)", rev: /[\w0-9.\-_]+/, + get "/revision(/:rev)", rev: /[\w.\-]+/, action: :revision, as: "show_revision" get "(/revisions/:rev)(/*repo_path)", action: :show, format: "html", - constraints: { rev: /[\w0-9.\-_]+/, repo_path: /.*/ }, + constraints: { rev: /[\w.\-]+/, repo_path: /.*/ }, as: "show_revisions_path" end end From 4c7512a8ef45ecfc7d9654d3434556befaf34b3f Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Mon, 18 Mar 2024 19:42:49 +0100 Subject: [PATCH 05/12] [#53429] Project storage menu links does not include prefix. https://community.openproject.org/work_packages/53429 --- .../storages/admin/submit_or_cancel_form.rb | 2 +- .../app/models/storages/project_storage.rb | 2 +- .../spec/features/storages_menu_links_spec.rb | 40 ++++++++++++++----- .../support/ensure_connection_path_helper.rb | 5 ++- 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb index e6a8b227aca0..3204742b2f18 100644 --- a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb +++ b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb @@ -58,7 +58,7 @@ def default_cancel_button_options name: :cancel, scheme: :default, tag: :a, - href: Rails.application.routes.url_helpers.admin_settings_storages_path, + href: OpenProject::StaticRouting::StaticRouter.new.admin_settings_storages_path, label: I18n.t('button_cancel') } end diff --git a/modules/storages/app/models/storages/project_storage.rb b/modules/storages/app/models/storages/project_storage.rb index 3ae6dcb71e4e..8e9563377f06 100644 --- a/modules/storages/app/models/storages/project_storage.rb +++ b/modules/storages/app/models/storages/project_storage.rb @@ -92,7 +92,7 @@ def open(user) def open_with_connection_ensured return unless storage.configured? - url_helpers = Rails.application.routes.url_helpers + url_helpers = OpenProject::StaticRouting::StaticRouter.new.url_helpers open_project_storage_url = url_helpers.open_project_storage_url( host: Setting.host_name, protocol: 'https', diff --git a/modules/storages/spec/features/storages_menu_links_spec.rb b/modules/storages/spec/features/storages_menu_links_spec.rb index 969b12c2ad73..63d4347af018 100644 --- a/modules/storages/spec/features/storages_menu_links_spec.rb +++ b/modules/storages/spec/features/storages_menu_links_spec.rb @@ -31,23 +31,25 @@ require 'spec_helper' require_module_spec_helper -RSpec.describe 'Storage links in project menu', :js, :with_cuprite do +RSpec.describe 'Storage links in project menu' do include EnsureConnectionPathHelper - let!(:storage_configured_linked1) { create(:nextcloud_storage_configured, :as_automatically_managed, name: "Storage 1") } - let!(:project_storage1) { create(:project_storage, :as_automatically_managed, project:, storage: storage_configured_linked1) } - let!(:storage_configured_linked2) { create(:nextcloud_storage_configured, name: "Storage 2") } - let!(:project_storage2) do + shared_let(:project) { create(:project, enabled_module_names: %i[storages]) } + shared_let(:storage_configured_linked1) { create(:nextcloud_storage_configured, :as_automatically_managed, name: "Storage 1") } + shared_let(:project_storage1) do + create(:project_storage, :as_automatically_managed, project:, storage: storage_configured_linked1) + end + shared_let(:storage_configured_linked2) { create(:nextcloud_storage_configured, name: "Storage 2") } + shared_let(:project_storage2) do create(:project_storage, project_folder_mode: 'inactive', project:, storage: storage_configured_linked2) end - let!(:storage_configured_linked3) { create(:nextcloud_storage_configured, name: "Storage 3") } - let!(:project_storage3) do + shared_let(:storage_configured_linked3) { create(:nextcloud_storage_configured, name: "Storage 3") } + shared_let(:project_storage3) do create(:project_storage, project_folder_mode: 'manual', project:, storage: storage_configured_linked3) end - let!(:storage_configured_unlinked) { create(:nextcloud_storage_configured, name: "Storage 4") } - let!(:storage_unconfigured_linked) { create(:nextcloud_storage, name: "Storage 5") } - let!(:project_storage4) { create(:project_storage, project:, storage: storage_unconfigured_linked) } - let!(:project) { create(:project, enabled_module_names: %i[storages]) } + shared_let(:storage_configured_unlinked) { create(:nextcloud_storage_configured, name: "Storage 4") } + shared_let(:storage_unconfigured_linked) { create(:nextcloud_storage, name: "Storage 5") } + shared_let(:project_storage4) { create(:project_storage, project:, storage: storage_unconfigured_linked) } let(:user) { create(:user, member_with_permissions: { project => permissions }) } before do @@ -82,6 +84,22 @@ expect(page).to have_no_link(storage_configured_unlinked.name) expect(page).to have_no_link(storage_unconfigured_linked.name) end + + context 'when OP has been installed behind prefix' do + let(:prefix) { '/qwerty' } + + before { allow(OpenProject::Configuration).to receive(:rails_relative_url_root).and_return(prefix) } + + it 'has all links prefixed' do + visit(project_path(id: project.id)) + + expect(page).to have_link(storage_configured_linked1.name, href: ensure_connection_path(project_storage1)) + expect(page).to have_link(storage_configured_linked2.name, href: ensure_connection_path(project_storage2)) + expect(page).to have_link(storage_configured_linked3.name, href: ensure_connection_path(project_storage3)) + expect(page).to have_no_link(storage_configured_unlinked.name) + expect(page).to have_no_link(storage_unconfigured_linked.name) + end + end end context 'read_files' do diff --git a/modules/storages/spec/support/ensure_connection_path_helper.rb b/modules/storages/spec/support/ensure_connection_path_helper.rb index 82d621094f03..58d1baecda45 100644 --- a/modules/storages/spec/support/ensure_connection_path_helper.rb +++ b/modules/storages/spec/support/ensure_connection_path_helper.rb @@ -30,10 +30,11 @@ module EnsureConnectionPathHelper def ensure_connection_path(project_storage) - oauth_clients_ensure_connection_path( + url_helpers = OpenProject::StaticRouting::StaticRouter.new.url_helpers + url_helpers.oauth_clients_ensure_connection_path( oauth_client_id: project_storage.storage.oauth_client.client_id, storage_id: project_storage.storage.id, - destination_url: open_project_storage_url( + destination_url: url_helpers.open_project_storage_url( protocol: 'https', project_id: project_storage.project.identifier, id: project_storage.id From 6b54372fea22fe33492e50f80f6629fc72af6914 Mon Sep 17 00:00:00 2001 From: Pavel Balashou Date: Tue, 19 Mar 2024 10:56:12 +0100 Subject: [PATCH 06/12] Use StaticRouter correctly. --- .../storages/app/forms/storages/admin/submit_or_cancel_form.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb index 3204742b2f18..064961e92fc4 100644 --- a/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb +++ b/modules/storages/app/forms/storages/admin/submit_or_cancel_form.rb @@ -58,7 +58,7 @@ def default_cancel_button_options name: :cancel, scheme: :default, tag: :a, - href: OpenProject::StaticRouting::StaticRouter.new.admin_settings_storages_path, + href: OpenProject::StaticRouting::StaticRouter.new.url_helpers.admin_settings_storages_path, label: I18n.t('button_cancel') } end From 240e03f8cbdac223478a2aa48edb771f1fd2a1c1 Mon Sep 17 00:00:00 2001 From: Maya Berdygylyjova Date: Tue, 19 Mar 2024 12:58:27 +0100 Subject: [PATCH 07/12] [#53045] update file storages docs (#15021) * [#53045] update file storages docs [#53045] 13.4 Documentation: OneDrive/SharePoint documentation update https://community.openproject.org/work_packages/53045 * Update docs/user-guide/projects/project-settings/file-storages/README.md Co-authored-by: dominic-braeunlein --------- Co-authored-by: dominic-braeunlein --- .../project-settings/file-storages/README.md | 8 +++++++- .../file-storages-oauth-nudge-nextcloud.png | Bin 0 -> 22800 bytes 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png diff --git a/docs/user-guide/projects/project-settings/file-storages/README.md b/docs/user-guide/projects/project-settings/file-storages/README.md index 189f945c6680..e5dc87166037 100644 --- a/docs/user-guide/projects/project-settings/file-storages/README.md +++ b/docs/user-guide/projects/project-settings/file-storages/README.md @@ -60,6 +60,8 @@ The SharePoint file storage is now available to all work packages in this projec > **Note:** Please refer to the [OneDrive/SharePoint user guide](../../../file-management/one-drive-integration) for further instructions on using the integration at a user level. + + ## Add a Nextcloud storage to a project If you have selected the Nextcloud option in the previous step of storage selection, you will now see the **Project folder** options. @@ -80,7 +82,11 @@ Click on **Add** to add your new Nextcloud file storage to this project. The Nextcloud file storage is now available to all work packages in this project. -> **Note:** For information on how to use the file storage (link Nextcloud user accounts at a user level, link files to a work package, view and download linked files, unlink files), please read our [Nextcloud integration user guide](../../../file-management/nextcloud-integration/). +> **Note:** For information on how to use the file storage (link Nextcloud user accounts at a user level, link files to a work package, view and download linked files, unlink files), please read our [Nextcloud integration user guide](../../../file-management/nextcloud-integration/). + +If you do not yet have an access token for the file storage in a project, you will be prompted to log into your file storage. You can choose to login immediately to establish the connection or to do it later. + +![OAuth for file storages in OpenProject project settings](file-storages-oauth-nudge-nextcloud.png) ### Project folder member connection status diff --git a/docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png b/docs/user-guide/projects/project-settings/file-storages/file-storages-oauth-nudge-nextcloud.png new file mode 100644 index 0000000000000000000000000000000000000000..80a6224cb596d958cb3f4806f15d6e4d709e1a33 GIT binary patch literal 22800 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 Jex>CU{eQD-v=;yX literal 0 HcmV?d00001 From 33b0f44e630f6b49313af308b4ba95a0b4758177 Mon Sep 17 00:00:00 2001 From: Maya Berdygylyjova Date: Tue, 19 Mar 2024 13:07:25 +0100 Subject: [PATCH 08/12] Fixed a typo in 13.4 docs (#15037) --- docs/getting-started/my-account/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/my-account/README.md b/docs/getting-started/my-account/README.md index a47a2ae730cf..59d704e31575 100644 --- a/docs/getting-started/my-account/README.md +++ b/docs/getting-started/my-account/README.md @@ -116,7 +116,7 @@ In order to register a new device for two-factor authentication, click the green - Mobile phone - App-based authenticator -- WebAuth +- WebAuthn ![](openproject_my_account_authentication_options.png) From 63e6918ae4cf2c0975a8b36cbd942a8b03409c21 Mon Sep 17 00:00:00 2001 From: ulferts Date: Tue, 19 Mar 2024 13:58:04 +0100 Subject: [PATCH 09/12] handle line break only long text field in project list --- .../common/attribute_component.rb | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/components/open_project/common/attribute_component.rb b/app/components/open_project/common/attribute_component.rb index 12d1584a7b6e..e77a2f9892b5 100644 --- a/app/components/open_project/common/attribute_component.rb +++ b/app/components/open_project/common/attribute_component.rb @@ -57,7 +57,7 @@ def full_text end def display_expand_button_value - multi_type? || text_ast.xpath('html/body').children.length > 1 ? :block : :none + multi_type? || body_children.length > 1 ? :block : :none end def text_color @@ -67,18 +67,26 @@ def text_color private def first_paragraph - @first_paragraph ||= text_ast - .xpath('html/body') - .children - .first - .inner_html - .html_safe # rubocop:disable Rails/OutputSafety + @first_paragraph ||= if body_children.any? + body_children + .first + .inner_html + .html_safe # rubocop:disable Rails/OutputSafety + else + '' + end end def text_ast @text_ast ||= Nokogiri::HTML(full_text) end + def body_children + text_ast + .xpath('html/body') + .children + end + def multi_type? first_paragraph.include?('figure') || first_paragraph.include?('macro') end From 3cb6c4d65e3af2683ccdd9aebc87cd62b07456b7 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 19 Mar 2024 14:52:51 +0100 Subject: [PATCH 10/12] Extract relying party construction into a concern --- .../webauthn_relying_party.rb | 19 +++++++++ .../authentication_controller.rb | 19 ++++----- .../base_controller.rb | 42 +++++++------------ 3 files changed, 42 insertions(+), 38 deletions(-) create mode 100644 modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb diff --git a/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb new file mode 100644 index 000000000000..8fcde41b375c --- /dev/null +++ b/modules/two_factor_authentication/app/controllers/concerns/two_factor_authentication/webauthn_relying_party.rb @@ -0,0 +1,19 @@ +module ::TwoFactorAuthentication + module WebauthnRelyingParty + extend ActiveSupport::Concern + + protected + + def webauthn_relying_party + @webauthn_relying_party ||= begin + origin = "#{Setting.protocol}://#{Setting.host_name}" + + WebAuthn::RelyingParty.new( + origin:, + id: URI(origin).host, + name: Setting.app_title + ) + end + end + end +end diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb index d5d82c6266c8..bb770fbe1ea4 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb @@ -4,8 +4,10 @@ class AuthenticationController < ApplicationController include ::TwoFactorAuthentication::RememberToken # Backup tokens functionality include ::TwoFactorAuthentication::BackupCodes + # Webauthn relying party based on domain + include ::TwoFactorAuthentication::WebauthnRelyingParty # Include global layout helper - layout 'no_menu' + layout "no_menu" # User is not yet logged in, so skip login required check skip_before_action :check_if_login_required @@ -29,7 +31,7 @@ def request_otp session[:authenticated_user_force_2fa] = service.needs_registration? if service.needs_registration? - flash[:info] = I18n.t('two_factor_authentication.forced_registration.required_to_add_device') + flash[:info] = I18n.t("two_factor_authentication.forced_registration.required_to_add_device") redirect_to new_forced_2fa_device_path elsif !service.requires_token? complete_stage_redirect @@ -142,9 +144,9 @@ def render_login_otp(service) @active_devices = @user.otp_devices.get_active if params["back_url"] - render action: 'request_otp', back_url: params["back_url"] + render action: "request_otp", back_url: params["back_url"] else - render action: 'request_otp' + render action: "request_otp" end end @@ -206,7 +208,7 @@ def manager # In case of mis-configuration, block all logins def ensure_valid_configuration if manager.invalid_configuration? - render_500 message: I18n.t('two_factor_authentication.error_is_enforced_not_active') + render_500 message: I18n.t("two_factor_authentication.error_is_enforced_not_active") false end end @@ -221,12 +223,5 @@ def complete_stage_redirect def failure_stage_redirect redirect_to authentication_stage_failure_path :two_factor_authentication end - - def webauthn_relying_party - @webauthn_relying_party ||= WebAuthn::RelyingParty.new( - origin: "#{Setting.protocol}://#{Setting.host_name}", - name: Setting.app_title - ) - end end end diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb index 0f9bfb8bf995..bf5ca78a4af8 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/base_controller.rb @@ -1,5 +1,7 @@ module ::TwoFactorAuthentication class BaseController < ApplicationController + include ::TwoFactorAuthentication::WebauthnRelyingParty + # Ensure 2FA authentication is enabled before_action :ensure_enabled_2fa @@ -10,17 +12,17 @@ class BaseController < ApplicationController helper_method :optional_webauthn_challenge_url - layout 'no_menu' + layout "no_menu" def new if params[:type] @device_type = params[:type].to_sym @device = new_device_type! @device_type - render 'two_factor_authentication/two_factor_devices/new' + render "two_factor_authentication/two_factor_devices/new" else @available_devices = available_devices - render 'two_factor_authentication/two_factor_devices/new_type' + render "two_factor_authentication/two_factor_devices/new_type" end end @@ -32,7 +34,7 @@ def make_default if @device.make_default! flash[:notice] = t(:notice_successful_update) else - flash[:error] = t('two_factor_authentication.devices.make_default_failed') + flash[:error] = t("two_factor_authentication.devices.make_default_failed") end redirect_to index_path @@ -42,14 +44,14 @@ def make_default # Destroy the given device if its not the default def destroy if @device.default && strategy_manager.enforced? - render_400 message: t('two_factor_authentication.devices.is_default_cannot_delete') + render_400 message: t("two_factor_authentication.devices.is_default_cannot_delete") return end if @device.destroy flash[:notice] = t(:notice_successful_delete) else - flash[:error] = t('two_factor_authentication.devices.failed_to_delete') + flash[:error] = t("two_factor_authentication.devices.failed_to_delete") Rails.logger.error "Failed to delete #{@device.id} of user#{target_user.id}. Errors: #{@device.errors.full_messages.join(' ')}" end @@ -89,8 +91,8 @@ def request_device_confirmation_token request_token_for_device( @device, confirm_path: url_for(action: :confirm, device_id: @device.id), - title: I18n.t('two_factor_authentication.devices.confirm_device'), - message: I18n.t('two_factor_authentication.devices.text_confirm_to_complete_html', identifier: @device.identifier) + title: I18n.t("two_factor_authentication.devices.confirm_device"), + message: I18n.t("two_factor_authentication.devices.text_confirm_to_complete_html", identifier: @device.identifier) ) end @@ -112,15 +114,15 @@ def validate_device_token # rubocop:disable Metrics/AbcSize def confirm_and_save(result) if result.success? && @device.confirm_registration_and_save - flash[:notice] = t('two_factor_authentication.devices.registration_complete') + flash[:notice] = t("two_factor_authentication.devices.registration_complete") true elsif !result.success? flash[:notice] = nil - flash[:error] = t('two_factor_authentication.devices.registration_failed_token_invalid') + flash[:error] = t("two_factor_authentication.devices.registration_failed_token_invalid") false else flash[:notice] = nil - flash[:error] = t('two_factor_authentication.devices.registration_failed_update') + flash[:error] = t("two_factor_authentication.devices.registration_failed_update") false end end @@ -133,10 +135,10 @@ def request_token_for_device(device, locals) flash[:notice] = transmit.result if transmit.result.present? # Request confirmation from user as in the regular login flow - render 'two_factor_authentication/two_factor_devices/confirm', layout: 'base', locals: + render "two_factor_authentication/two_factor_devices/confirm", layout: "base", locals: else error = transmit.errors.full_messages.join(". ") - default_message = t('two_factor_authentication.devices.confirm_send_failed') + default_message = t("two_factor_authentication.devices.confirm_send_failed") flash[:error] = "#{default_message} #{error}" redirect_to registration_failure_path @@ -195,18 +197,6 @@ def verify_webauthn_credential false end - def webauthn_relying_party - @webauthn_relying_party ||= begin - origin = "#{Setting.protocol}://#{Setting.host_name}" - - WebAuthn::RelyingParty.new( - origin:, - id: URI(origin).host, - name: Setting.app_title - ) - end - end - def logout_other_sessions if current_user == target_user Rails.logger.info { "First 2FA device registered for #{target_user}, terminating other logged in sessions." } @@ -257,7 +247,7 @@ def show_local_breadcrumb end def default_breadcrumb - t('two_factor_authentication.label_devices') + t("two_factor_authentication.label_devices") end def available_devices From 40501bd958950130c7218a9013c7bad7689c373c Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 19 Mar 2024 14:54:25 +0100 Subject: [PATCH 11/12] Allowed devices should be an array --- .../app/models/two_factor_authentication/device/webauthn.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb index 7379befcd3fc..c4aefe990b44 100644 --- a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb +++ b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb @@ -22,14 +22,14 @@ def options_for_create(relying_party) @options_for_create ||= relying_party.options_for_registration( user: { id: user.webauthn_id, name: user.name }, exclude: TwoFactorAuthentication::Device::Webauthn.where(user:).pluck(:webauthn_external_id), - authenticator_selection: { user_verification: 'discouraged' } + authenticator_selection: { user_verification: "discouraged" } ) end def options_for_get(relying_party) @options_for_get ||= relying_party.options_for_authentication( - user_verification: 'discouraged', # we do not require user verification - allow: webauthn_external_id # TODO: Maybe also allow all other tokens? Let's see + user_verification: "discouraged", # we do not require user verification + allow: [webauthn_external_id] # TODO: Maybe also allow all other tokens? Let's see ) end From f66825d45824fe9b41bb087fb4325dfcbf09b021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Tue, 19 Mar 2024 15:44:31 +0100 Subject: [PATCH 12/12] use the remembered device when switching (#15042) * use the remembered device when switching * fixup! use the remembered device when switching * Remove discouraged user verification --------- Co-authored-by: Klaus Zanders --- .../authentication_controller.rb | 14 ++++++++------ .../two_factor_authentication/device/webauthn.rb | 4 +--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb index bb770fbe1ea4..a16a2a070608 100644 --- a/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb +++ b/modules/two_factor_authentication/app/controllers/two_factor_authentication/authentication_controller.rb @@ -84,7 +84,7 @@ def successful_2fa_transmission(service, transmit) ## # Create a token service for the current user # with an optional override to use a non-default channel - def otp_service(user, use_channel: nil, use_device: nil) + def otp_service(user, use_channel: nil, use_device: remembered_device(user)) session[:two_factor_authentication_device_id] = use_device.try(:id) ::TwoFactorAuthentication::TokenService.new user:, use_channel:, use_device: end @@ -92,16 +92,18 @@ def otp_service(user, use_channel: nil, use_device: nil) ## # Get the used device for verification def otp_service_for_verification(user) - use_device = - if session[:two_factor_authentication_device_id] - user.otp_devices.find(session[:two_factor_authentication_device_id]) - end - otp_service(user, use_device:) + otp_service(user, use_device: remembered_device(user)) rescue ActiveRecord::RecordNotFound render_404 false end + def remembered_device(user) + if session[:two_factor_authentication_device_id] + user.otp_devices.find(session[:two_factor_authentication_device_id]) + end + end + ## # Detect overridden channel or device from params when trying to resend def service_from_resend_params diff --git a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb index c4aefe990b44..d36848dbac00 100644 --- a/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb +++ b/modules/two_factor_authentication/app/models/two_factor_authentication/device/webauthn.rb @@ -21,14 +21,12 @@ def self.device_type def options_for_create(relying_party) @options_for_create ||= relying_party.options_for_registration( user: { id: user.webauthn_id, name: user.name }, - exclude: TwoFactorAuthentication::Device::Webauthn.where(user:).pluck(:webauthn_external_id), - authenticator_selection: { user_verification: "discouraged" } + exclude: TwoFactorAuthentication::Device::Webauthn.where(user:).pluck(:webauthn_external_id) ) end def options_for_get(relying_party) @options_for_get ||= relying_party.options_for_authentication( - user_verification: "discouraged", # we do not require user verification allow: [webauthn_external_id] # TODO: Maybe also allow all other tokens? Let's see ) end