From 4d731dcab673c153098b72cbcf921b2eddb430b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 18 Feb 2026 07:47:11 +0100 Subject: [PATCH] Replace raw and explicit html_safe calls --- .../activities/item_component.html.erb | 5 +- app/components/activities/item_component.rb | 3 +- .../item_subtitle_component.html.erb | 7 +- .../enterprise_edition/plan_for_feature.rb | 4 +- .../trial_teaser_component.rb | 2 +- .../show_page_header_component.html.erb | 6 +- .../projects/row_component.html.erb | 9 +- app/components/projects/row_component.rb | 24 +++-- .../projects/table_component.html.erb | 7 +- app/components/row_component.html.erb | 9 +- .../project_work_packages/header_component.rb | 2 +- .../invite_user_form_component.html.erb | 8 +- app/components/users/hover_card_component.rb | 4 +- .../users/show_page_header_component.html.erb | 3 +- .../journals/revision_component.html.erb | 18 ++-- .../date_picker/banner_component.rb | 24 ++--- .../exports/pdf/export_settings_component.rb | 9 +- app/controllers/account_controller.rb | 2 +- .../concerns/accounts/user_limits.rb | 28 +++--- .../work_packages/reminders_controller.rb | 4 +- ...write_readonly_attributes_caption.html.erb | 6 +- .../apiv3_cors_origins_caption.html.erb | 6 +- .../allowed_link_protocols_caption.html.erb | 4 +- .../security_badge_displayed_caption.html.erb | 4 +- app/forms/application_form.rb | 2 + app/forms/enterprise_trials/form.rb | 11 ++- app/forms/my/look_and_feel_form.rb | 4 +- .../creation_wizard/submission_form.rb | 2 +- .../settings/work_packages/activities/form.rb | 12 +-- app/forms/scim_clients/form.rb | 2 +- .../settings/authentication_settings_form.rb | 4 +- app/forms/settings/form_helper.rb | 4 +- app/forms/statuses/form.rb | 6 +- app/helpers/accessibility_helper.rb | 15 ++- app/helpers/application_helper.rb | 33 +++++-- app/helpers/breadcrumb_helper.rb | 9 +- app/helpers/error_message_helper.rb | 4 +- .../flash_messages_output_safety_helper.rb | 10 +- app/helpers/hook_helper.rb | 5 +- app/helpers/icons_helper.rb | 5 +- app/helpers/oauth_helper.rb | 2 +- app/helpers/pagination_helper.rb | 2 +- app/helpers/password_helper.rb | 11 ++- app/helpers/repositories_helper.rb | 97 +++++++++---------- app/helpers/search_helper.rb | 33 ++++--- app/helpers/settings_helper.rb | 41 +++++--- app/helpers/text_formatting_helper.rb | 9 +- app/helpers/users_helper.rb | 9 +- app/helpers/versions_helper.rb | 4 +- app/helpers/work_packages_helper.rb | 18 +--- .../activities/project_activity_provider.rb | 2 +- app/views/account/exit.html.erb | 8 +- app/views/activities/index.html.erb | 8 +- app/views/admin/backups/reset_token.html.erb | 8 +- app/views/admin/info.html.erb | 16 ++- .../_passwords.html.erb | 14 ++- .../authentication_settings/_sso.html.erb | 4 +- app/views/members/_member_form.html.erb | 6 +- app/views/news/show.html.erb | 6 +- .../placeholder_users/deletion_info.html.erb | 12 +-- .../copy_project_failed.html.erb | 2 +- app/views/repositories/_properties.html.erb | 39 ++++++++ app/views/repositories/changes.html.erb | 2 +- app/views/repositories/destroy_info.html.erb | 38 +++++--- app/views/repositories/diff.html.erb | 13 +-- app/views/repositories/show.html.erb | 2 +- .../shared_work_package.html.erb | 6 +- .../shared_work_package.text.erb | 2 +- .../activation_limit_reached.html.erb | 2 +- .../activation_limit_reached.text.erb | 2 +- app/views/users/_preferences.html.erb | 16 ++- .../work_packages/bulk/reassign.html.erb | 14 ++- app/views/workflows/copy.html.erb | 19 ++-- config/locales/en.yml | 88 ++++++++++------- config/locales/js-en.yml | 2 - config/static_links.yml | 2 + lib/custom_field_form_builder.rb | 13 ++- lib/open_project/object_linking.rb | 6 +- lib/open_project/page_hierarchy_helper.rb | 2 +- .../patches/action_view_accessible_errors.rb | 6 +- lib/redmine/menu_manager/menu_helper.rb | 10 +- lib/tabular_form_builder.rb | 4 +- lib_static/redmine/i18n.rb | 2 +- lookbook/docs/patterns/02-forms.md.erb | 8 +- .../saml/providers/confirm_destroy.html.erb | 5 +- .../views/avatars/users/_gravatars.html.erb | 2 +- .../job_status/dialog/body_component.html.erb | 2 +- .../destroy_info.html.erb | 6 +- .../synchronized_groups/destroy_info.html.erb | 4 +- modules/ldap_groups/config/locales/en.yml | 4 +- .../app/components/base_errors_component.rb | 2 +- .../app/controllers/meetings_controller.rb | 50 +++++----- .../recurring_meetings_controller.rb | 2 +- .../providers/confirm_destroy.html.erb | 6 +- modules/openid_connect/config/locales/en.yml | 4 +- .../app/views/recaptcha/admin/show.html.erb | 4 +- ...h_application_info_copy_component.html.erb | 6 +- .../oauth_application_info_copy_component.rb | 10 -- ...roy_confirmation_dialog_component.html.erb | 3 +- ...roy_confirmation_dialog_component.html.erb | 3 +- .../project_storage_members/index.html.erb | 4 +- modules/storages/config/locales/en.yml | 9 +- .../authentication/request_otp.html.erb | 4 +- .../_two_factor_authentication_self.html.erb | 4 +- .../config/locales/en.yml | 7 +- .../webhooks/outgoing/admin/index.html.erb | 6 +- modules/webhooks/config/locales/en.yml | 3 +- spec/features/auth/omniauth_spec.rb | 10 +- spec/helpers/error_message_helper_spec.rb | 10 +- spec/helpers/text_formatting_helper_spec.rb | 2 +- spec/lib/tabular_form_builder_spec.rb | 12 ++- 111 files changed, 607 insertions(+), 513 deletions(-) create mode 100644 app/views/repositories/_properties.html.erb diff --git a/app/components/activities/item_component.html.erb b/app/components/activities/item_component.html.erb index 00520787386..121c89daf3b 100644 --- a/app/components/activities/item_component.html.erb +++ b/app/components/activities/item_component.html.erb @@ -34,7 +34,10 @@ See COPYRIGHT and LICENSE files for more details. <% else %> <%= @event.event_title %> <% end %> - <%= project_suffix %> + <% suffix = project_suffix %> + <% if suffix.present? %> + (<%= suffix %>) + <% end %> <%= render( diff --git a/app/components/activities/item_component.rb b/app/components/activities/item_component.rb index 803fead8871..923e7415169 100644 --- a/app/components/activities/item_component.rb +++ b/app/components/activities/item_component.rb @@ -45,8 +45,7 @@ class Activities::ItemComponent < ViewComponent::Base return if activity_is_from_current_project? kind = activity_is_from_subproject? ? "subproject" : "project" - suffix = I18n.t("events.title.#{kind}", name: link_to(@event.project.name, @event.project)) - "(#{suffix})".html_safe # rubocop:disable Rails/OutputSafety + helpers.t("events.title.#{kind}_html", name: link_to(@event.project.name, @event.project)) end def display_user? diff --git a/app/components/activities/item_subtitle_component.html.erb b/app/components/activities/item_subtitle_component.html.erb index 4721b1d0aa2..02b65f3805d 100644 --- a/app/components/activities/item_subtitle_component.html.erb +++ b/app/components/activities/item_subtitle_component.html.erb @@ -29,10 +29,11 @@ See COPYRIGHT and LICENSE files for more details.
<%= + # OG: html_safe usage has been double-checked and would otherwise require a lot of i18n key change I18n.t( i18n_key, - user: user_html, - datetime: datetime_html - ).html_safe # OG: html_safe usage has been double-checked and would otherwise require a lot of i18n key change + user: h(user_html), + datetime: h(datetime_html) + ).html_safe # rubocop:disable Rails/OutputSafety %>
diff --git a/app/components/enterprise_edition/plan_for_feature.rb b/app/components/enterprise_edition/plan_for_feature.rb index 64d2bee683f..747708b6953 100644 --- a/app/components/enterprise_edition/plan_for_feature.rb +++ b/app/components/enterprise_edition/plan_for_feature.rb @@ -48,7 +48,7 @@ module EnterpriseEdition def description @description || begin if I18n.exists?(:description_html, scope: i18n_scope) - I18n.t(:description_html, scope: i18n_scope).html_safe + helpers.t(:description_html, scope: i18n_scope) else I18n.t(:description, scope: i18n_scope) end @@ -85,7 +85,7 @@ module EnterpriseEdition I18n.t("ee.upsell.plan_name", plan: plan.capitalize) end - I18n.t("ee.upsell.plan_text_html", plan_name:).html_safe + helpers.t("ee.upsell.plan_text_html", plan_name:) end end end diff --git a/app/components/enterprise_edition/trial_teaser_component.rb b/app/components/enterprise_edition/trial_teaser_component.rb index 016be7929e1..3af8233fa05 100644 --- a/app/components/enterprise_edition/trial_teaser_component.rb +++ b/app/components/enterprise_edition/trial_teaser_component.rb @@ -65,7 +65,7 @@ module EnterpriseEdition end def description - I18n.t("ee.teaser.description", trial_plan: plan_name).html_safe + helpers.t("ee.teaser.description_html", trial_plan: plan_name) end def plan_name diff --git a/app/components/placeholder_users/show_page_header_component.html.erb b/app/components/placeholder_users/show_page_header_component.html.erb index 80afe8f33a9..c04ab856621 100644 --- a/app/components/placeholder_users/show_page_header_component.html.erb +++ b/app/components/placeholder_users/show_page_header_component.html.erb @@ -28,7 +28,11 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title { "#{avatar @placeholder_user} #{h(@placeholder_user.name)}".html_safe } + header.with_title do + concat avatar(@placeholder_user) + concat @placeholder_user.name + end + header.with_breadcrumbs(breadcrumb_items) if @current_user.allowed_globally?(:manage_placeholder_user) diff --git a/app/components/projects/row_component.html.erb b/app/components/projects/row_component.html.erb index cff17907e37..d92127cfd5e 100644 --- a/app/components/projects/row_component.html.erb +++ b/app/components/projects/row_component.html.erb @@ -27,9 +27,10 @@ See COPYRIGHT and LICENSE files for more details. ++#%> - - <%= "class=\"#{row_css_class}\"".html_safe if row_css_class %>> +<%= content_tag(:tr, + id: row_css_id, + class: row_css_class + ) do %> <% columns.each do |column| %> <%= column_value(column) %> @@ -40,4 +41,4 @@ See COPYRIGHT and LICENSE files for more details. <%= link %> <% end %> - +<% end %> diff --git a/app/components/projects/row_component.rb b/app/components/projects/row_component.rb index 9d81d167966..e09c722ee59 100644 --- a/app/components/projects/row_component.rb +++ b/app/components/projects/row_component.rb @@ -211,17 +211,15 @@ module Projects def project_status return nil unless user_can_view_project_attributes? - content = "".html_safe - status_code = project.status_code - if status_code classes = helpers.project_status_css_class(status_code) - content << content_tag(:span, "", class: "project-status--bulb -inline #{classes}") - content << content_tag(:span, helpers.project_status_name(status_code), class: "project-status--name #{classes}") - end - content + capture do + concat content_tag(:span, "", class: "project-status--bulb -inline #{classes}") + concat content_tag(:span, helpers.project_status_name(status_code), class: "project-status--name #{classes}") + end + end end def status_explanation @@ -250,7 +248,7 @@ module Projects def row_css_class classes = %w[basics context-menu--reveal op-project-row-component] - classes << project_css_classes + classes += project_css_classes classes << row_css_level_classes classes.join(" ") @@ -269,13 +267,13 @@ module Projects end def project_css_classes - s = " project ".html_safe + output = ["project"] - s << " root" if project.root? - s << " child" if project.child? - s << (project.leaf? ? " leaf" : " parent") + output << "root" if project.root? + output << "child" if project.child? + output << (project.leaf? ? "leaf" : "parent") - s + output end def column_css_class(column) diff --git a/app/components/projects/table_component.html.erb b/app/components/projects/table_component.html.erb index ca4087a80e9..0de960bd03b 100644 --- a/app/components/projects/table_component.html.erb +++ b/app/components/projects/table_component.html.erb @@ -31,7 +31,10 @@ See COPYRIGHT and LICENSE files for more details.
- > + <%= content_tag :table, + id: table_id, + class: "generic-table", + data: { controller: "table-highlighting" } do %> <% columns.each do |column| %> > @@ -84,7 +87,7 @@ See COPYRIGHT and LICENSE files for more details. <% end %> <%= render_collection rows %> -
+ <% end %> <% if inline_create_link && show_inline_create %>
<%= inline_create_link %> diff --git a/app/components/row_component.html.erb b/app/components/row_component.html.erb index cff17907e37..592f4cbb23a 100644 --- a/app/components/row_component.html.erb +++ b/app/components/row_component.html.erb @@ -27,9 +27,10 @@ See COPYRIGHT and LICENSE files for more details. ++#%> - - <%= "class=\"#{row_css_class}\"".html_safe if row_css_class %>> +<%= content_tag(:tr, + id: row_css_id, + class: row_css_class + ) do %> <% columns.each do |column| %> <%= column_value(column) %> @@ -40,4 +41,4 @@ See COPYRIGHT and LICENSE files for more details. <%= link %> <% end %> - +<% end %> diff --git a/app/components/settings/project_work_packages/header_component.rb b/app/components/settings/project_work_packages/header_component.rb index abc9509c897..ff0a0d43137 100644 --- a/app/components/settings/project_work_packages/header_component.rb +++ b/app/components/settings/project_work_packages/header_component.rb @@ -104,7 +104,7 @@ module Settings private - def internal_comments_translation = t("ee.features.internal_comments").html_safe + def internal_comments_translation = t("ee.features.internal_comments") end end end diff --git a/app/components/shares/invite_user_form_component.html.erb b/app/components/shares/invite_user_form_component.html.erb index 9ad2d42ef4b..4996d652d5e 100644 --- a/app/components/shares/invite_user_form_component.html.erb +++ b/app/components/shares/invite_user_form_component.html.erb @@ -39,11 +39,11 @@ user_limit_row.with_column do render(Primer::Beta::Text.new(color: :danger)) do - I18n.t( + helpers.link_translate( "sharing.warning_user_limit_reached#{'_admin' if User.current.admin?}", - upgrade_url: OpenProject::Enterprise.upgrade_url, - entity: entity.model_name.human - ).html_safe + i18n_args: { entity: entity.model_name.human }, + links: { upgrade_url: OpenProject::Enterprise.upgrade_url } + ) end end end diff --git a/app/components/users/hover_card_component.rb b/app/components/users/hover_card_component.rb index 7b04ba87313..014639f1851 100644 --- a/app/components/users/hover_card_component.rb +++ b/app/components/users/hover_card_component.rb @@ -90,9 +90,9 @@ class Users::HoverCardComponent < ApplicationComponent remaining_count_link = link_to(t("users.groups.more", count: remaining_count), user_path(@user)) if remaining_count > 0 - t("users.groups.summary_with_more", names: summary_links, count_link: remaining_count_link).html_safe + t("users.groups.summary_with_more_html", names: summary_links, count_link: remaining_count_link) else - t("users.groups.summary", names: summary_links).html_safe + t("users.groups.summary_html", names: summary_links) end end end diff --git a/app/components/users/show_page_header_component.html.erb b/app/components/users/show_page_header_component.html.erb index 85dc0bb68ee..231ba6e2102 100644 --- a/app/components/users/show_page_header_component.html.erb +++ b/app/components/users/show_page_header_component.html.erb @@ -29,7 +29,8 @@ See COPYRIGHT and LICENSE files for more details. <%= render(Primer::OpenProject::PageHeader.new) do |header| header.with_title do - "#{avatar(@user, hover_card: { active: false })} #{h(@user.name)}".html_safe + concat avatar(@user, hover_card: { active: false }) + concat @user.name end header.with_breadcrumbs(breadcrumb_items) diff --git a/app/components/work_packages/activities_tab/journals/revision_component.html.erb b/app/components/work_packages/activities_tab/journals/revision_component.html.erb index ad622092be3..91c78d38890 100644 --- a/app/components/work_packages/activities_tab/journals/revision_component.html.erb +++ b/app/components/work_packages/activities_tab/journals/revision_component.html.erb @@ -42,13 +42,13 @@ target: "_blank" ) ) do - I18n.t("js.label_committed_link", revision_identifier: short_revision) + I18n.t(:label_committed_link, revision_identifier: short_revision) end I18n.t( - "js.label_committed_at", - committed_revision_link: committed_text.html_safe, + :label_committed_at_html, + committed_revision_link: committed_text, date: format_time(changeset.committed_on) - ).html_safe + ) end end end @@ -69,13 +69,13 @@ target: "_blank" ) ) do - I18n.t("js.label_committed_link", revision_identifier: short_revision) + I18n.t(:label_committed_link, revision_identifier: short_revision) end - I18n.t( - "js.label_committed_at", - committed_revision_link: committed_text.html_safe, + t( + :label_committed_at_html, + committed_revision_link: committed_text, date: format_time(changeset.committed_on) - ).html_safe + ) end end end diff --git a/app/components/work_packages/date_picker/banner_component.rb b/app/components/work_packages/date_picker/banner_component.rb index 8ece0b374a1..89a2c999926 100644 --- a/app/components/work_packages/date_picker/banner_component.rb +++ b/app/components/work_packages/date_picker/banner_component.rb @@ -96,15 +96,17 @@ module WorkPackages end def mobile_description - text = if @manually_scheduled - I18n.t("work_packages.datepicker_modal.banner.description.manual_mobile") - else - I18n.t("work_packages.datepicker_modal.banner.description.automatic_mobile") - end + text = + if @manually_scheduled + I18n.t("work_packages.datepicker_modal.banner.description.manual_mobile") + else + I18n.t("work_packages.datepicker_modal.banner.description.automatic_mobile") + end - "#{text} #{render(Primer::Beta::Link.new(tag: :a, href: link, target: '_blank', underline: true)) do - I18n.t('work_packages.datepicker_modal.show_relations') - end}".html_safe + capture do + concat text + concat render(Primer::Beta::Link.new(tag: :a, href: link, target: "_blank", underline: true)) { I18n.t("work_packages.datepicker_modal.show_relations") } + end end def overlapping_predecessor? @@ -122,7 +124,7 @@ module WorkPackages predecessor_work_packages.filter_map(&:due_date) .max - &.before?(@work_package.start_date - 2) + &.before?(@work_package.start_date - 2) end def predecessor_relations @@ -131,8 +133,8 @@ module WorkPackages def predecessor_work_packages @predecessor_work_packages ||= predecessor_relations - .includes(:to) - .map(&:to) + .includes(:to) + .map(&:to) end def children diff --git a/app/components/work_packages/exports/pdf/export_settings_component.rb b/app/components/work_packages/exports/pdf/export_settings_component.rb index 1d718420f0d..d7f549749e0 100644 --- a/app/components/work_packages/exports/pdf/export_settings_component.rb +++ b/app/components/work_packages/exports/pdf/export_settings_component.rb @@ -49,8 +49,13 @@ module WorkPackages end def gantt_chart_label - label = I18n.t("export.dialog.pdf.export_type.options.gantt.label") - gantt_chart_allowed? ? label : (label + enterprise_icon).html_safe # rubocop:disable Rails/OutputSafety + capture do + concat I18n.t("export.dialog.pdf.export_type.options.gantt.label") + + unless gantt_chart_allowed? + concat enterprise_icon + end + end end def pdf_export_types diff --git a/app/controllers/account_controller.rb b/app/controllers/account_controller.rb index 9d800c40375..50507eda3f0 100644 --- a/app/controllers/account_controller.rb +++ b/app/controllers/account_controller.rb @@ -283,7 +283,7 @@ class AccountController < ApplicationController ldap_auth_source_id: user.ldap_auth_source_id } - flash[:notice] = I18n.t("account.auth_source_login", login: user.login).html_safe + flash[:notice] = helpers.t("account.auth_source_login_html", login: user.login) redirect_to signin_path(username: user.login) end diff --git a/app/controllers/concerns/accounts/user_limits.rb b/app/controllers/concerns/accounts/user_limits.rb index 88a9ebea0ad..9814cc68337 100644 --- a/app/controllers/concerns/accounts/user_limits.rb +++ b/app/controllers/concerns/accounts/user_limits.rb @@ -91,18 +91,16 @@ module Accounts::UserLimits end def user_limit_warning - warning = if current_user.admin? - I18n.t( - :warning_user_limit_reached_admin, - upgrade_url: OpenProject::Enterprise.upgrade_url - ) - else - I18n.t( - :warning_user_limit_reached - ) - end - - warning.html_safe + if current_user.admin? + link_translate( + :warning_user_limit_reached_admin, + links: { upgrade_url: OpenProject::Enterprise.upgrade_url } + ) + else + I18n.t( + :warning_user_limit_reached + ) + end end def show_imminent_user_limit_warning!(flash_now: false) @@ -115,12 +113,10 @@ module Accounts::UserLimits # A warning for when the user limit has technically not been reached yet # but if all invited users were to activate their accounts it would be reached. def imminent_user_limit_warning - warning = I18n.t( + link_translate( :warning_imminent_user_limit, - upgrade_url: OpenProject::Enterprise.upgrade_url + links: { upgrade_url: OpenProject::Enterprise.upgrade_url } ) - - warning.html_safe end def user_limit_reached? diff --git a/app/controllers/work_packages/reminders_controller.rb b/app/controllers/work_packages/reminders_controller.rb index 8289236e51a..4d6bc949737 100644 --- a/app/controllers/work_packages/reminders_controller.rb +++ b/app/controllers/work_packages/reminders_controller.rb @@ -53,8 +53,8 @@ class WorkPackages::RemindersController < ApplicationController .call(reminder_params) if service_result.success? - message = I18n.t("work_package.reminders.create_success_message", - reminder_time: reminder_chosen_time(service_result.result)).html_safe + message = helpers.t("work_package.reminders.create_success_message_html", + reminder_time: reminder_chosen_time(service_result.result)) respond_with_success_flash_message(message:) else respond_with_error_modal_component(service_result) diff --git a/app/forms/admin/settings/api_settings_form/apiv3_write_readonly_attributes_caption.html.erb b/app/forms/admin/settings/api_settings_form/apiv3_write_readonly_attributes_caption.html.erb index 29f7e219749..e97978f18c7 100644 --- a/app/forms/admin/settings/api_settings_form/apiv3_write_readonly_attributes_caption.html.erb +++ b/app/forms/admin/settings/api_settings_form/apiv3_write_readonly_attributes_caption.html.erb @@ -9,9 +9,9 @@ <% end %> <%= render(Primer::Beta::Text.new(tag: :p, mb: 0)) do %> <%= - I18n.t( + helpers.link_translate( :setting_apiv3_write_readonly_attributes_additional, - api_documentation_link: static_link_to(:api_docs) - ).html_safe + links: { api_documentation_link: %i[api_docs] } + ) %> <% end %> diff --git a/app/forms/admin/settings/cors_form/apiv3_cors_origins_caption.html.erb b/app/forms/admin/settings/cors_form/apiv3_cors_origins_caption.html.erb index 6f35bc4993b..76c3f9742ab 100644 --- a/app/forms/admin/settings/cors_form/apiv3_cors_origins_caption.html.erb +++ b/app/forms/admin/settings/cors_form/apiv3_cors_origins_caption.html.erb @@ -3,8 +3,8 @@ <% end %> <%= render(Primer::Beta::Text.new(tag: :p)) do %> - <%= I18n.t( + <%= helpers.link_translate( :setting_apiv3_cors_origins_text_html, - origin_link: ::OpenProject::Static::Links.url_for(:origin_mdn_documentation) - ).html_safe %> + links: { docs_url: %i[origin_mdn_documentation] } + ) %> <% end %> diff --git a/app/forms/admin/settings/general_settings_form/allowed_link_protocols_caption.html.erb b/app/forms/admin/settings/general_settings_form/allowed_link_protocols_caption.html.erb index 1c785c65cd7..8984de8980b 100644 --- a/app/forms/admin/settings/general_settings_form/allowed_link_protocols_caption.html.erb +++ b/app/forms/admin/settings/general_settings_form/allowed_link_protocols_caption.html.erb @@ -1,10 +1,10 @@ <%= - I18n.t( + helpers.t( :setting_allowed_link_protocols_text_html, tel_code: content_tag(:code, "tel"), element_code: content_tag(:code, "element"), http_code: content_tag(:code, "http"), https_code: content_tag(:code, "https"), mailto_code: content_tag(:code, "mailto") - ).html_safe + ) %> diff --git a/app/forms/admin/settings/general_settings_form/security_badge_displayed_caption.html.erb b/app/forms/admin/settings/general_settings_form/security_badge_displayed_caption.html.erb index 887af818d17..e8c3d883ceb 100644 --- a/app/forms/admin/settings/general_settings_form/security_badge_displayed_caption.html.erb +++ b/app/forms/admin/settings/general_settings_form/security_badge_displayed_caption.html.erb @@ -1,8 +1,8 @@ <%= - I18n.t( + helpers.t( :text_notice_security_badge_displayed_html, information_panel_label: I18n.t(:label_information), more_info_url: ::OpenProject::Static::Links.url_for(:security_badge_documentation), information_panel_path: url_helpers.info_admin_index_path - ).html_safe + ) %> diff --git a/app/forms/application_form.rb b/app/forms/application_form.rb index c0e9938ecda..f74271ccbdc 100644 --- a/app/forms/application_form.rb +++ b/app/forms/application_form.rb @@ -37,6 +37,8 @@ class ApplicationForm < Primer::Forms::Base end end + delegate :helpers, to: :ApplicationController + def url_helpers Rails.application.routes.url_helpers end diff --git a/app/forms/enterprise_trials/form.rb b/app/forms/enterprise_trials/form.rb index a721f0eb0a8..ca34865d357 100644 --- a/app/forms/enterprise_trials/form.rb +++ b/app/forms/enterprise_trials/form.rb @@ -67,13 +67,20 @@ module EnterpriseTrials f.check_box( required: true, - label: I18n.t("ee.trial.consent_html").html_safe, + label: helpers.link_translate("ee.trial.consent", + links: { + tos_url: %i[terms_of_service], + privacy_url: %i[data_privacy] + }), name: :general_consent ) f.check_box( required: false, - label: I18n.t("ee.trial.receive_newsletter_html").html_safe, + label: helpers.link_translate("ee.trial.receive_newsletter", + links: { + newsletter_url: %i[newsletter] + }), name: :newsletter_consent ) end diff --git a/app/forms/my/look_and_feel_form.rb b/app/forms/my/look_and_feel_form.rb index f28a2312326..5d3904d930c 100644 --- a/app/forms/my/look_and_feel_form.rb +++ b/app/forms/my/look_and_feel_form.rb @@ -84,7 +84,7 @@ class My::LookAndFeelForm < ApplicationForm private def disable_keyboard_shortcuts_caption - attribute_name(:disable_keyboard_shortcuts_caption_html, - href: OpenProject::Static::Links.url_for(:shortcuts)).html_safe # rubocop:disable Rails/OutputSafety + helpers.link_translate(:"user_preferences.disable_keyboard_shortcuts_caption", + links: { docs_url: %i[shortcuts] }) end end diff --git a/app/forms/projects/settings/creation_wizard/submission_form.rb b/app/forms/projects/settings/creation_wizard/submission_form.rb index 655e9369b02..abba1d9013d 100644 --- a/app/forms/projects/settings/creation_wizard/submission_form.rb +++ b/app/forms/projects/settings/creation_wizard/submission_form.rb @@ -77,7 +77,7 @@ module Projects f.autocompleter( name: :project_creation_wizard_assignee_custom_field_id, label: I18n.t("settings.project_initiation_request.submission.assignee"), - caption: I18n.t("settings.project_initiation_request.submission.assignee_caption_html").html_safe, + caption: helpers.t("settings.project_initiation_request.submission.assignee_caption_html"), required: false, input_width: :large, autocomplete_options: { diff --git a/app/forms/projects/settings/work_packages/activities/form.rb b/app/forms/projects/settings/work_packages/activities/form.rb index 989253fd5d5..a78c73f459b 100644 --- a/app/forms/projects/settings/work_packages/activities/form.rb +++ b/app/forms/projects/settings/work_packages/activities/form.rb @@ -51,16 +51,8 @@ module Projects::Settings::WorkPackages::Activities private def caption_text - link = render( - Primer::Beta::Link.new( - href: OpenProject::Static::Links.url_for(:enterprise_features, :internal_comments), - underline: true - ) - ) do - I18n.t("label_learn_more") - end - - I18n.t("settings.work_packages.activities.helper_text", link:).html_safe + helpers.link_translate("settings.work_packages.activities.helper_text", + links: { docs_url: %i[enterprise_features internal_comments] }) end end end diff --git a/app/forms/scim_clients/form.rb b/app/forms/scim_clients/form.rb index f88d77f5194..c9c6f3e8833 100644 --- a/app/forms/scim_clients/form.rb +++ b/app/forms/scim_clients/form.rb @@ -59,7 +59,7 @@ module ScimClients client_form.select_list( name: :authentication_method, label: ScimClient.human_attribute_name(:authentication_method), - caption: I18n.t("admin.scim_clients.form.authentication_method_description_html").html_safe, + caption: helpers.t("admin.scim_clients.form.authentication_method_description_html"), input_width: :large, include_blank: false, disabled: model.persisted?, diff --git a/app/forms/settings/authentication_settings_form.rb b/app/forms/settings/authentication_settings_form.rb index 31d60214ab5..554e6312f75 100644 --- a/app/forms/settings/authentication_settings_form.rb +++ b/app/forms/settings/authentication_settings_form.rb @@ -91,13 +91,13 @@ module Settings f.text_field( name: :after_first_login_redirect_url, - caption: I18n.t(:setting_after_first_login_redirect_url_text_html).html_safe, + caption: helpers.t(:setting_after_first_login_redirect_url_text_html), input_width: :large ) f.text_field( name: :after_login_default_redirect_url, - caption: I18n.t(:setting_after_login_default_redirect_url_text_html).html_safe, + caption: helpers.t(:setting_after_login_default_redirect_url_text_html), input_width: :large ) diff --git a/app/forms/settings/form_helper.rb b/app/forms/settings/form_helper.rb index c203e84443e..10aefadb59d 100644 --- a/app/forms/settings/form_helper.rb +++ b/app/forms/settings/form_helper.rb @@ -56,8 +56,8 @@ module Settings # @param names [Array] The name(s) of the setting # @return [String] The translated HTML-safe caption def setting_caption(*names) - I18n.t("setting_#{names.join('_')}_caption_html", default: nil)&.html_safe \ - || I18n.t("setting_#{names.join('_')}_caption", default: nil) + ApplicationController.helpers.t("setting_#{names.join('_')}_caption_html", default: nil) || + I18n.t("setting_#{names.join('_')}_caption", default: nil) end # Retrieves the current value of a setting diff --git a/app/forms/statuses/form.rb b/app/forms/statuses/form.rb index cac0631dde9..d6550440bdf 100644 --- a/app/forms/statuses/form.rb +++ b/app/forms/statuses/form.rb @@ -79,7 +79,7 @@ module Statuses label: attribute_name(:is_readonly), name: :is_readonly, disabled: readonly_disabled?, - caption: I18n.t("statuses.edit.status_readonly_html").html_safe, + caption: helpers.t("statuses.edit.status_readonly_html"), data: { "admin--statuses-target": "isReadonlyCheckbox", restricted: readonly_work_packages_restricted? @@ -115,8 +115,8 @@ module Statuses end def percent_complete_field_caption - I18n.t("statuses.edit.status_percent_complete_text", - href: url_helpers.admin_settings_progress_tracking_path).html_safe + helpers.link_translate("statuses.edit.status_percent_complete_text", + links: { setting_url: url_helpers.admin_settings_progress_tracking_path }) end def already_default_status? diff --git a/app/helpers/accessibility_helper.rb b/app/helpers/accessibility_helper.rb index f363d62284a..f19ae4eef99 100644 --- a/app/helpers/accessibility_helper.rb +++ b/app/helpers/accessibility_helper.rb @@ -30,17 +30,16 @@ module AccessibilityHelper def you_are_here_info(condition = true, disabled = nil) - if condition && !disabled - "#{I18n.t(:description_current_position)}".html_safe - elsif condition && disabled - "".html_safe - else - "" - end + return "" unless condition + + content_tag(:span, + I18n.t(:description_current_position), + style: disabled ? "display: none" : "display: block", + class: "position-label sr-only") end def empty_element_tag - @empty_element_tag ||= ApplicationController.new.render_to_string(partial: "accessibility/empty_element_tag").html_safe + @empty_element_tag ||= ApplicationController.new.render_to_string(partial: "accessibility/empty_element_tag") end # Returns the locale :en for the given menu item if the user locale is diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 588b4668c23..9f7dbb97ed1 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -130,8 +130,8 @@ module ApplicationHelper class: :user_status_class) end - def labeled_check_box_tags(name, collection, options = {}) - collection.sort.map do |object| + def labeled_check_box_tags(name, collection, options = {}) # rubocop:disable Metrics/AbcSize + fields = collection.sort.map do |object| id = name.gsub(/[\[\]]+/, "_") + object.id.to_s object_options = options.inject({}) do |h, (k, v)| @@ -146,18 +146,30 @@ module ApplicationHelper styled_check_box_tag(name, object.id, false, id:) + object.to_s end end - end.join.html_safe + end + + safe_join(fields) end def authoring(created, author, options = {}) label = options[:label] || :label_added_time_by - I18n.t(label, author: link_to_user(author), age: time_tag(created)).html_safe + # Ensure we pass inputs here to html_escape + # which will respect html_safe? + author = ERB::Util.html_escape link_to_user(author) + age = ERB::Util.html_escape time_tag(created) + + # OG: html_safe is used here with explicitly escaped inputs except for the translation file + I18n.t(label, author:, age:).html_safe end def authoring_at(creation_date, author) return if author.nil? - I18n.t(:label_added_by_on, author: link_to_user(author), date: creation_date).html_safe + author = ERB::Util.html_escape link_to_user(author) + date = ERB::Util.html_escape creation_date + + # OG: html_safe is used here to avoid having to change this reusable key + I18n.t(:label_added_by_on, author:, date:).html_safe end def time_tag(time) @@ -196,7 +208,8 @@ module ApplicationHelper formats = capture(Redmine::Views::OtherFormatsBuilder.new(self), &) unless formats.nil? || formats.strip.empty? content_tag "p", class: "other-formats" do - (I18n.t(:label_export_to) + formats).html_safe + concat I18n.t(:label_export_to) + concat formats end end end @@ -251,6 +264,12 @@ module ApplicationHelper .sort_by(&:first) end + def blank_select_option + content_tag(:option, + "--- #{t(:actionview_instancetag_blank_option)} ---", + disabled: true) + end + def theme_options_for_select [ [I18n.t("themes.light"), "light"], @@ -424,7 +443,7 @@ module ApplicationHelper # @param [optional, String] content the content of the ROBOTS tag. # defaults to no index, follow, and no archive def robot_exclusion_tag(content = "NOINDEX,FOLLOW,NOARCHIVE") - "".html_safe + content_tag(:meta, name: "ROBOTS", content:) end def permitted_params diff --git a/app/helpers/breadcrumb_helper.rb b/app/helpers/breadcrumb_helper.rb index 1c1ed2d468e..139ad6809b8 100644 --- a/app/helpers/breadcrumb_helper.rb +++ b/app/helpers/breadcrumb_helper.rb @@ -30,10 +30,9 @@ module BreadcrumbHelper def nested_breadcrumb_element(section_header, title) - output = "".html_safe - output << "#{section_header}: " - output << content_tag(:b, title) - - output + capture do + concat "#{section_header}: " + concat content_tag(:b, title) + end end end diff --git a/app/helpers/error_message_helper.rb b/app/helpers/error_message_helper.rb index 01aa37297ce..63601b3d3bd 100644 --- a/app/helpers/error_message_helper.rb +++ b/app/helpers/error_message_helper.rb @@ -63,7 +63,7 @@ module ErrorMessageHelper list_of_messages(base_error_messages), text_header_invalid_fields(base_error_messages, fields_error_messages), list_of_messages(fields_error_messages) - ].compact, "
".html_safe) + ].compact, tag(:br)) end def text_header_invalid_fields(base_error_messages, fields_error_messages) @@ -77,6 +77,6 @@ module ErrorMessageHelper def list_of_messages(messages) return if messages.blank? - safe_join(messages, "
".html_safe) + safe_join(messages, tag(:br)) end end diff --git a/app/helpers/flash_messages_output_safety_helper.rb b/app/helpers/flash_messages_output_safety_helper.rb index af7ef4dce9b..3bbc7eb87d9 100644 --- a/app/helpers/flash_messages_output_safety_helper.rb +++ b/app/helpers/flash_messages_output_safety_helper.rb @@ -32,17 +32,15 @@ module FlashMessagesOutputSafetyHelper extend ActiveSupport::Concern - included do - # For .safe_join in join_flash_messages - include ActionView::Helpers::OutputSafetyHelper - end - ### # Joins individual flash messages with a Line Break Element. # # @param [String|Array] messages the flash messages to join. # @return [String] the joined messages as an HTML-safe string. def join_flash_messages(messages) - safe_join(Array(messages), "
".html_safe) + ApplicationController.helpers.safe_join( + Array(messages), + ApplicationController.helpers.tag(:br) + ) end end diff --git a/app/helpers/hook_helper.rb b/app/helpers/hook_helper.rb index 3449d862fe3..d13aa698013 100644 --- a/app/helpers/hook_helper.rb +++ b/app/helpers/hook_helper.rb @@ -57,7 +57,10 @@ module HookHelper default_context = { project: @project, hook_caller: self } default_context[:controller] = controller if respond_to?(:controller) default_context[:request] = request if respond_to?(:request) - OpenProject::Hook.call_hook(hook, default_context.merge(context)).join(" ").html_safe + ApplicationController.helpers.safe_join( + OpenProject::Hook.call_hook(hook, default_context.merge(context)), + " " + ) end end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 410f2d77a96..00d9117c2b0 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -30,12 +30,11 @@ module IconsHelper ## - # Create an tag with the given icon class names + # Create a tag with the given icon class names # and make it aria-hidden since screenreaders otherwise # output the css `content` of the icon. def op_icon(classnames, title: nil) - title = "title=\"#{h(title)}\"" unless title.nil? - %().html_safe + content_tag(:i, nil, class: classnames, title:, "aria-hidden": true) end ## diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb index f247e74c0d4..debabe77538 100644 --- a/app/helpers/oauth_helper.rb +++ b/app/helpers/oauth_helper.rb @@ -37,7 +37,7 @@ module OAuthHelper if strings.empty? I18n.t("oauth.scopes.api_v3") else - safe_join(strings.map { |scope| I18n.t("oauth.scopes.#{scope}", default: scope) }, "
".html_safe) + safe_join(strings.map { |scope| I18n.t("oauth.scopes.#{scope}", default: scope) }, tag(:br)) end end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index eaa03cd4796..808b6bc1d54 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -121,7 +121,7 @@ module PaginationHelper def pagination_options_section(paginator, params:, allowed_params:) per_page_options = Setting.per_page_options_array - return "".html_safe if per_page_options.empty? + return "" if per_page_options.empty? allowed_params ||= %w[filters sortBy] content_tag(:div, class: "op-pagination--options") do diff --git a/app/helpers/password_helper.rb b/app/helpers/password_helper.rb index fc14f525b8a..9f4c4112025 100644 --- a/app/helpers/password_helper.rb +++ b/app/helpers/password_helper.rb @@ -69,10 +69,13 @@ module PasswordHelper def render_password_complexity_hint rules = password_rules_description - s = OpenProject::Passwords::Evaluator.min_length_description - s += "
#{rules}" if rules.present? - - s.html_safe + capture do + concat OpenProject::Passwords::Evaluator.min_length_description + if rules.present? + concat tag(:br) + concat rules + end + end end private diff --git a/app/helpers/repositories_helper.rb b/app/helpers/repositories_helper.rb index 5e8343e159d..644feb9b452 100644 --- a/app/helpers/repositories_helper.rb +++ b/app/helpers/repositories_helper.rb @@ -47,16 +47,6 @@ module RepositoriesHelper text&.gsub(%r{^(.{#{length}}[^\n]*)\n.+$}m, '\\1...') end - def render_properties(properties) - unless properties.nil? || properties.empty? - content = +"" - properties.keys.sort.each do |property| - content << content_tag("li", raw("#{h property}: #{h properties[property]}")) - end - content_tag("ul", content.html_safe, class: "properties") - end - end - def render_changeset_changes changes = @changeset.file_changes.limit(1000).order(Arel.sql("path")).filter_map do |change| case change.action @@ -113,58 +103,59 @@ module RepositoriesHelper seen.size == 1 ? seen.first : :open end - def render_changes_tree(tree) - return "".html_safe if tree.nil? + # rubocop:disable Rails/HelperInstanceVariable + def render_changes_tree(tree) # rubocop:disable Metrics/AbcSize,Metrics/PerceivedComplexity + return "" if tree.nil? - items = tree.keys.sort.flat_map do |file| - style = "change" + items = tree.keys.sort.filter_map do |file| text = File.basename(file) if (s = tree[file][:s]) - style += " folder" path_param = without_leading_slash(to_path_param(@repository.relative_path(file))) - text = link_to(h(text), + link = link_to(text, show_revisions_path_project_repository_path(project_id: @project, repo_path: path_param, rev: @changeset.identifier), title: I18n.t(:label_folder)) - folder_li = content_tag(:li, text, - class: "#{style} icon icon-folder-#{calculate_folder_action(s)}") - [folder_li, render_changes_tree(s)] + content_tag(:li, safe_join([link, render_changes_tree(s)]), + class: "change folder icon icon-folder-#{calculate_folder_action(s)}") elsif (c = tree[file][:c]) - style += " change-#{c.action}" path_param = without_leading_slash(to_path_param(@repository.relative_path(c.path))) + parts = [] - text_parts = [] + parts << + if c.action == "D" + text + else + link_to(text, + entry_revision_project_repository_path(project_id: @project, + repo_path: path_param, + rev: @changeset.identifier), + title: changes_tree_change_title(c.action)) + end - unless c.action == "D" - text = link_to(h(text), - entry_revision_project_repository_path(project_id: @project, - repo_path: path_param, - rev: @changeset.identifier), - title: changes_tree_change_title(c.action)) - end - - text_parts << text - text_parts << " - " << h(c.revision) if c.revision.present? + parts << safe_join([" - ", c.revision]) if c.revision.present? if c.action == "M" - text_parts << " (" << link_to(I18n.t(:label_diff), - diff_revision_project_repository_path(project_id: @project, - repo_path: path_param, - rev: @changeset.identifier)) << ") " + diff_link = link_to(I18n.t(:label_diff), + diff_revision_project_repository_path(project_id: @project, + repo_path: path_param, + rev: @changeset.identifier)) + parts << safe_join([" (", diff_link, ") "]) end - text_parts << " " << content_tag(:span, c.from_path, class: "copied-from") if c.from_path.present? + parts << safe_join([" ", content_tag(:span, c.from_path, class: "copied-from")]) if c.from_path.present? - [changes_tree_li_element(c.action, safe_join(text_parts), style)] + changes_tree_li_element(c.action, safe_join(parts), "change change-#{c.action}") end - end.compact + end content_tag(:ul, safe_join(items)) end + # rubocop:enable Rails/HelperInstanceVariable + def to_utf8_for_repositories(str) return str if str.nil? @@ -203,14 +194,16 @@ module RepositoriesHelper if str.respond_to?(:force_encoding) str.force_encoding("UTF-8") - if !str.valid_encoding? - str = str.encode("US-ASCII", invalid: :replace, - undef: :replace, replace: "?").encode("UTF-8") + unless str.valid_encoding? + str = str.encode("US-ASCII", + invalid: :replace, + undef: :replace, replace: "?") + .encode("UTF-8") end else # removes invalid UTF8 sequences begin - (str + " ").encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")[0..-3] + "#{str} ".encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")[0..-3] rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError end end @@ -297,17 +290,15 @@ module RepositoriesHelper end def changes_tree_li_element(action, text, style) - icon_name = case action - when "A" then "icon-add" - when "D" then "icon-delete" - when "C" then "icon-copy" - when "R" then "icon-rename" - else - "icon-arrow-left-right" - end + icon_name = + case action + when "A" then "icon-add" + when "D" then "icon-delete" + when "C" then "icon-copy" + when "R" then "icon-rename" + else "icon-arrow-left-right" + end - content_tag(:li, text, - class: "#{style} icon #{icon_name}", - title: changes_tree_change_title(action)) + content_tag(:li, text, class: "#{style} icon #{icon_name}", title: changes_tree_change_title(action)) end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index e833fe92e78..b4b97f37411 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -34,21 +34,27 @@ module SearchHelper return nil unless split_text.length > 1 || text_on_not_found - result = +"" + parts = [] + total_length = 0 + split_text.each_with_index do |words, i| - if result.length > 1200 + if total_length > 1200 # maximum length of the preview reached - result << "..." + parts << "..." break end - result << if i.even? - abbreviated_text(words) - else - token_span(tokens, words) - end + part = if i.even? + abbreviated_text(words) + else + token_span(tokens, words) + end + + parts << part + total_length += part.length end - result.html_safe + + safe_join(parts) end def highlight_tokens_in_event(event, tokens) @@ -66,12 +72,11 @@ module SearchHelper def highlight_and_abbreviate_html(event_description, tokens) html = OpenProject::TextFormatting::Renderer.format_text(event_description) highlighted_html = highlight_tokens_in_html(html, tokens) - # rubocop:disable Rails/OutputSafety - abbreviated_html(highlighted_html).html_safe - # rubocop:enable Rails/OutputSafety + # This html_safe call is fine, as coming from our html pipeline + abbreviated_html(highlighted_html).html_safe # rubocop:disable Rails/OutputSafety end - def highlight_tokens_in_html(html, tokens) + def highlight_tokens_in_html(html, tokens) # rubocop:disable Metrics/AbcSize doc = Nokogiri::HTML::DocumentFragment.parse(html) tokens.each do |token| @@ -84,7 +89,7 @@ module SearchHelper t = (tokens.index(node.content.downcase) || 0) % 4 highlighted_text = escaped_text.gsub(/(#{escaped_token})/i) do - %{#{$1}} + content_tag(:span, $1, class: "search-highlight token-#{t}") end node.replace(Nokogiri::HTML::DocumentFragment.parse(highlighted_text)) diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index dd1fd314595..6343e0410ab 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -163,7 +163,7 @@ module SettingsHelper def setting_label(setting, options = {}) label = options[:label] - return "".html_safe if label == false + return "" if label == false styled_label_tag( "settings_#{setting}", options[:not_translated_label] || I18n.t(label || "setting_#{setting}"), @@ -192,18 +192,23 @@ module SettingsHelper def build_settings_matrix_head(settings, options = {}) content_tag(:tr, class: "form--matrix-header-row") do content_tag(:th, I18n.t(options[:label_choices] || :label_choices), - class: "form--matrix-header-cell") + - settings.map do |setting| - content_tag(:th, class: "form--matrix-header-cell") do - hidden_field_tag("settings[#{setting}][]", "") + - I18n.t("setting_#{setting}") - end - end.join.html_safe + class: "form--matrix-header-cell") + build_settings_matrix_head_values(settings) end end + def build_settings_matrix_head_values(settings) + values = settings.map do |setting| + content_tag(:th, class: "form--matrix-header-cell") do + hidden_field_tag("settings[#{setting}][]", "") + + I18n.t("setting_#{setting}") + end + end + + safe_join(values) + end + def build_settings_matrix_body(settings, choices) - choices.map do |choice| + body = choices.map do |choice| value = choice[:value] caption = choice[:caption] || value.to_s exceptions = Array(choice[:except]).compact @@ -211,11 +216,13 @@ module SettingsHelper content_tag(:td, caption, class: "form--matrix-cell") + settings_matrix_tds(settings, exceptions, value) end - end.join.html_safe # rubocop:disable Rails/OutputSafety + end + + safe_join(body) end def settings_matrix_tds(settings, exceptions, value) - settings.map do |setting| + tds = settings.map do |setting| content_tag(:td, class: "form--matrix-checkbox-cell") do unless exceptions.include?(setting) styled_check_box_tag("settings[#{setting}][]", value, @@ -223,14 +230,16 @@ module SettingsHelper disabled_setting_option(setting).merge(id: "#{setting}_#{value}")) end end - end.join.html_safe # rubocop:disable Rails/OutputSafety + end + + safe_join(tds) end - def setting_multiselect_choice(setting, choice, options) + def setting_multiselect_choice(setting, choice, options) # rubocop:disable Metrics/AbcSize text, value, choice_options = (choice.is_a?(Array) ? choice : [choice, choice]) choice_options = disabled_setting_option(setting) - .merge(choice_options || {}) - .merge(options.except(:id)) + .merge(choice_options || {}) + .merge(options.except(:id)) choice_options[:id] = "#{setting}_#{value}" content_tag(:label, class: "form--label-with-check-box") do @@ -255,7 +264,7 @@ module SettingsHelper if writable_setting?(setting) yield else - "".html_safe + ActiveSupport::SafeBuffer.new end end end diff --git a/app/helpers/text_formatting_helper.rb b/app/helpers/text_formatting_helper.rb index 52fe33bb1bf..e9967abd146 100644 --- a/app/helpers/text_formatting_helper.rb +++ b/app/helpers/text_formatting_helper.rb @@ -80,7 +80,6 @@ module TextFormattingHelper end def truncate_formatted_text(text, length: 120, replace_newlines: true) - # rubocop:disable Rails/OutputSafety stripped_text = strip_tags(format_text(text.to_s)) stripped_text = if length @@ -91,13 +90,13 @@ module TextFormattingHelper .strip if replace_newlines - stripped_text - .gsub(/[\r\n]+/, "
") + stripped_text.gsub!(/[\r\n]+/, "
") else stripped_text end - .html_safe - # rubocop:enable Rails/OutputSafety + + # Sanitized through strip_tags + stripped_text.html_safe # rubocop:disable Rails/OutputSafety end def truncate_multiline(string, length) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index e485e80ffe7..779cda7ff61 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -89,11 +89,12 @@ module UsersHelper # Create buttons to lock/unlock a user and reset failed logins def build_change_user_status_action(user) - result = "".html_safe - iterate_user_statusses(user) do |title, name| - result << ((yield title, name) + " ") # rubocop:disable Style/StringConcatenation + capture do + iterate_user_statusses(user) do |title, name| + concat yield(title, name) + concat " " + end end - result end def iterate_user_statusses(user) diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb index bbe9750bc07..359a3ab75d9 100644 --- a/app/helpers/versions_helper.rb +++ b/app/helpers/versions_helper.rb @@ -45,7 +45,7 @@ module VersionsHelper html_options = html_options.merge(id: link_to_version_id(version)) - link_name = options[:before_text].to_s.html_safe + format_version_name(version, options[:project] || @project) + link_name = format_version_name(version, options[:project] || @project) # rubocop:disable Rails/HelperInstanceVariable link_to_if version.visible?, link_name, { controller: "/versions", action: "show", id: version }, @@ -57,7 +57,7 @@ module VersionsHelper %i[start_date due_date] .filter { |attr| version.send(attr) } .map { |attr| "#{Version.human_attribute_name(attr)} #{format_date(version.send(attr))}" } - safe_join(formatted_dates, "
".html_safe) + safe_join(formatted_dates, tag(:br)) end def link_to_version_id(version) diff --git a/app/helpers/work_packages_helper.rb b/app/helpers/work_packages_helper.rb index 7a886b759d0..71aee969546 100644 --- a/app/helpers/work_packages_helper.rb +++ b/app/helpers/work_packages_helper.rb @@ -39,7 +39,7 @@ module WorkPackagesHelper # link_to_work_package(package, link_subject: true) # => Defect #6: This is the subject (everything within the link) # link_to_work_package(package, display_project: true) # => Foo - Defect #6: This is the subject def link_to_work_package(work_package, display_project: false, link_subject: false) # rubocop:disable Metrics/AbcSize - output = "".html_safe + output = ActiveSupport::SafeBuffer.new output << "#{work_package.project} - " if display_project && work_package.project_id link = link_to(work_package_path(work_package), @@ -101,22 +101,6 @@ module WorkPackagesHelper end end - def work_package_associations_to_address(associated) - ret = "".html_safe - - ret += content_tag(:p, I18n.t(:text_destroy_with_associated), class: "bold") - - ret += content_tag(:ul) do - associated.inject("".html_safe) do |list, associated_class| - list += content_tag(:li, associated_class.model_name.human, class: "decorated") - - list - end - end - - ret - end - def back_url_is_wp_show? route = Rails.application.routes.recognize_path(params[:back_url] || request.env["HTTP_REFERER"]) route[:controller] == "work_packages" && route[:action] == "index" && route[:state]&.match?(/^\d+/) diff --git a/app/models/activities/project_activity_provider.rb b/app/models/activities/project_activity_provider.rb index a9d2c98c53f..10675691426 100644 --- a/app/models/activities/project_activity_provider.rb +++ b/app/models/activities/project_activity_provider.rb @@ -51,7 +51,7 @@ class Activities::ProjectActivityProvider < Activities::BaseActivityProvider end def event_title(event) - I18n.t("events.title.project", name: event["project_name"]) + I18n.t("events.title.project_html", name: event["project_name"]) end def event_path(event) diff --git a/app/views/account/exit.html.erb b/app/views/account/exit.html.erb index 8b670699ccf..0232815f3c6 100644 --- a/app/views/account/exit.html.erb +++ b/app/views/account/exit.html.erb @@ -33,14 +33,10 @@ See COPYRIGHT and LICENSE files for more details. <% html_title t(:label_login) %> <%= call_hook :view_account_login_top %> -<% - signin_link = link_to I18n.t("label_here"), signin_path - instruction_text = I18n.t "instructions_#{instructions}", signin: signin_link -%> -

<%= I18n.t(:label_login) %>


-

<%= instruction_text.html_safe %>

+

<%= link_translate "instructions_#{instructions}", + links: { signin_url: signin_path } %>

<%= call_hook :view_account_login_bottom %> diff --git a/app/views/activities/index.html.erb b/app/views/activities/index.html.erb index b203eae5ebc..ca1e750ed55 100644 --- a/app/views/activities/index.html.erb +++ b/app/views/activities/index.html.erb @@ -31,7 +31,13 @@ See COPYRIGHT and LICENSE files for more details. <%= render Primer::OpenProject::PageHeader.new do |header| - header.with_title { (@author.nil? ? t(:label_activity) : t(:label_user_activity, value: link_to_user(@author))).html_safe } + header.with_title do + if @author.nil? + t(:label_activity) + else + t(:label_user_activity_html, value: link_to_user(@author)) + end + end header.with_description { t(:label_date_from_to, start: format_date(@date_to - @days), end: format_date(@date_to - 1)) } header.with_breadcrumbs(@project ? [{ href: project_overview_path(@project.id), text: @project.name }, t(:label_activity)] : nil) end diff --git a/app/views/admin/backups/reset_token.html.erb b/app/views/admin/backups/reset_token.html.erb index 1a4a4b2f24e..408dd7e4813 100644 --- a/app/views/admin/backups/reset_token.html.erb +++ b/app/views/admin/backups/reset_token.html.erb @@ -62,10 +62,12 @@ See COPYRIGHT and LICENSE files for more details.

<%= t( - "backup.reset_token.verification", - word: "#{t("backup.reset_token.verification_word_#{action}")}", + "backup.reset_token.verification_html", + word: content_tag(:em, + t("backup.reset_token.verification_word_#{action}"), + class: "danger-zone--expected-value"), action: t("backup.reset_token.verification_word_#{action}") - ).html_safe %> + ) %>

diff --git a/app/views/admin/info.html.erb b/app/views/admin/info.html.erb index 21d2e9821d2..e59a882016b 100644 --- a/app/views/admin/info.html.erb +++ b/app/views/admin/info.html.erb @@ -58,14 +58,12 @@ See COPYRIGHT and LICENSE files for more details. scheme: :warning, icon: :alert )) do - <<~STR.html_safe - Starting with OpenProject 16.0, PostgreSQL 16 is required to use OpenProject. - Your installation will remain functional with your current database, but anticipate incompatability - in future releases. -
- We have prepared #{static_link_to(:postgres_17_upgrade, label: 'upgrade guides for all installation methods')}. - You can perform the upgrade ahead of the next release at any time by following the guides. - STR + link_translate( + :"admin.info.database_deprecation_html", + links: { + upgrade_guide: %i[postgres_17_upgrade] + } + ) end component.with_attribute(key: "Deprecation warning", value:) end @@ -103,7 +101,7 @@ end %> component.with_attribute( key: t(:label_storage_for), - value: safe_join(entries[:labels], "
".html_safe) + value: safe_join(entries[:labels], tag(:br)) ) component.with_attribute( diff --git a/app/views/admin/settings/authentication_settings/_passwords.html.erb b/app/views/admin/settings/authentication_settings/_passwords.html.erb index d577d74850e..d6139ab465c 100644 --- a/app/views/admin/settings/authentication_settings/_passwords.html.erb +++ b/app/views/admin/settings/authentication_settings/_passwords.html.erb @@ -29,9 +29,9 @@ See COPYRIGHT and LICENSE files for more details. <%= settings_primer_form_with(scope: :settings, - url: admin_settings_authentication_path(tab: params[:tab]), - data: { turbo_method: :patch }, - method: :patch) do |f| + url: admin_settings_authentication_path(tab: params[:tab]), + data: { turbo_method: :patch }, + method: :patch) do |f| render_inline_settings_form(f) do |form| disabled = OpenProject::Configuration.disable_password_login? @@ -42,10 +42,8 @@ See COPYRIGHT and LICENSE files for more details. icon: :info, my: 2 ) do - I18n.t( - :note_password_login_disabled, - configuration: static_link_to(:disable_password_login, label: I18n.t('label_configuration')) - ).html_safe + link_translate(:note_password_login_disabled, + links: { configuration_url: %i[disable_password_login] }) end end end @@ -64,7 +62,7 @@ See COPYRIGHT and LICENSE files for more details. group.check_box(value:, label: I18n.t("label_password_rule_#{value}"), checked: OpenProject::Passwords::Evaluator.active_rule?(value), - ) + ) end end diff --git a/app/views/admin/settings/authentication_settings/_sso.html.erb b/app/views/admin/settings/authentication_settings/_sso.html.erb index 8bec979a71b..00010a692ff 100644 --- a/app/views/admin/settings/authentication_settings/_sso.html.erb +++ b/app/views/admin/settings/authentication_settings/_sso.html.erb @@ -37,10 +37,10 @@ See COPYRIGHT and LICENSE files for more details. name: :omniauth_direct_login_provider, input_width: :medium, label: I18n.t(:setting_omniauth_direct_login_provider), - caption: I18n.t( + caption: t( "settings.authentication.omniauth_direct_login_hint_html", internal_path: internal_signin_url - ).html_safe, + ), include_blank: I18n.t(:label_none_parentheses) ) do |select| AuthProvider diff --git a/app/views/members/_member_form.html.erb b/app/views/members/_member_form.html.erb index 0d9a4b267c8..5db509c6a23 100644 --- a/app/views/members/_member_form.html.erb +++ b/app/views/members/_member_form.html.erb @@ -102,7 +102,11 @@ See COPYRIGHT and LICENSE files for more details.
-

<%= I18n.t("warning_user_limit_reached#{'_admin' if current_user.admin?}", upgrade_url: OpenProject::Enterprise.upgrade_url).html_safe %>

+

<%= + link_translate( + "warning_user_limit_reached#{'_admin' if current_user.admin?}", + links: { upgrade_url: OpenProject::Enterprise.upgrade_url } + ) %>

<% end %> diff --git a/app/views/news/show.html.erb b/app/views/news/show.html.erb index 11306d8f317..701412bedb5 100644 --- a/app/views/news/show.html.erb +++ b/app/views/news/show.html.erb @@ -29,7 +29,11 @@ See COPYRIGHT and LICENSE files for more details. <%= render Primer::OpenProject::PageHeader.new do |header| - header.with_title { "#{avatar(@news.author)} #{h @news.title}".html_safe } + header.with_title do + concat avatar(@news.author) + concat @news.title + end + header.with_breadcrumbs( [ { href: project_overview_path(@project.id), text: @project.name }, diff --git a/app/views/placeholder_users/deletion_info.html.erb b/app/views/placeholder_users/deletion_info.html.erb index 2ccce86ce66..c8efb773b70 100644 --- a/app/views/placeholder_users/deletion_info.html.erb +++ b/app/views/placeholder_users/deletion_info.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <% name = @placeholder_user.name %> -<% html_title(t(:label_administration), t("placeholder_users.deletion_info.heading", name: name).to_s) -%> +<% html_title(t(:label_administration), t("placeholder_users.deletion_info.heading_html", name:).to_s) -%> <%= labelled_tabular_form_for( :placeholder_user, @@ -40,11 +40,7 @@ See COPYRIGHT and LICENSE files for more details.

- <%= t( - "placeholder_users.deletion_info.heading", - name: content_tag(:em, name) - ) - .html_safe %> + <%= t("placeholder_users.deletion_info.heading_html", name: content_tag(:em, name)) %>

@@ -56,9 +52,9 @@ See COPYRIGHT and LICENSE files for more details.

<%= t( - "placeholder_users.deletion_info.confirmation", + "placeholder_users.deletion_info.confirmation_html", name: content_tag(:em, name, class: "danger-zone--expected-value") - ).html_safe %> + ) %>

diff --git a/app/views/project_mailer/copy_project_failed.html.erb b/app/views/project_mailer/copy_project_failed.html.erb index a47494ba1fe..ad978bfa6c7 100644 --- a/app/views/project_mailer/copy_project_failed.html.erb +++ b/app/views/project_mailer/copy_project_failed.html.erb @@ -31,4 +31,4 @@ See COPYRIGHT and LICENSE files for more details.

<%= t("copy_project.text.failed", source_project_name: @source_project.name, target_project_name: @target_project_name) %>

-<%= @errors.join("
".html_safe) %> +<%= safe_join @errors, tag(:br) %> diff --git a/app/views/repositories/_properties.html.erb b/app/views/repositories/_properties.html.erb new file mode 100644 index 00000000000..d7e3afb59bd --- /dev/null +++ b/app/views/repositories/_properties.html.erb @@ -0,0 +1,39 @@ +<%#-- copyright +OpenProject is an open source project management software. +Copyright (C) the OpenProject GmbH + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License version 3. + +OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +Copyright (C) 2006-2013 Jean-Philippe Lang +Copyright (C) 2010-2013 the ChiliProject Team + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +See COPYRIGHT and LICENSE files for more details. + +++#%> + +<% if @properties.present? %> +
    + <% @properties.keys.sort.each do |property| %> +
  • + <%= property %>: + <%= @properties[property] %> +
  • + <% end %> +
+<% end %> diff --git a/app/views/repositories/changes.html.erb b/app/views/repositories/changes.html.erb index 79c5ac7f0db..255628fcbfb 100644 --- a/app/views/repositories/changes.html.erb +++ b/app/views/repositories/changes.html.erb @@ -32,7 +32,7 @@ See COPYRIGHT and LICENSE files for more details.

<%= render partial: "link_to_functions" %>

-<%= render_properties(@properties) %> +<%= render partial: "properties" %> <%= unless @changesets.empty? render( diff --git a/app/views/repositories/destroy_info.html.erb b/app/views/repositories/destroy_info.html.erb index 7695ef97592..ea64225adb2 100644 --- a/app/views/repositories/destroy_info.html.erb +++ b/app/views/repositories/destroy_info.html.erb @@ -42,12 +42,15 @@ See COPYRIGHT and LICENSE files for more details. <%= styled_form_tag(project_repository_path(@project), method: :delete, class: "danger-zone") do %>

- <%= t("repositories.destroy.title", repository_type: "#{h(@repository.repository_type)} - #{t(:project_module_repository)}").html_safe %> + <%= t("repositories.destroy.title_html", repository_type: content_tag(:em, "#{@repository.repository_type} - #{t(:project_module_repository)}")) %>

- <%= t("repositories.destroy.subtitle", repository_type: "#{h(@repository.repository_type)} - #{t(:project_module_repository)}", project_name: h(@project.identifier)).html_safe %>
+ <%= t("repositories.destroy.subtitle", + repository_type: "#{@repository.repository_type} - #{t(:project_module_repository)}", + project_name: @project.identifier) %> +
<%= t("repositories.destroy.confirmation") %> -
+
<%= t("repositories.destroy.managed_path_note", path: @repository.root_url) %>

@@ -55,7 +58,8 @@ See COPYRIGHT and LICENSE files for more details. <%= t("repositories.destroy.info") %>

- <%= t("repositories.destroy.repository_verification", identifier: "#{h(@project.identifier)}").html_safe %> + <%= t("repositories.destroy.repository_verification_html", + identifier: content_tag(:em, @project.identifier, class: "danger-zone--expected-value")) %>

@@ -66,13 +70,13 @@ See COPYRIGHT and LICENSE files for more details. title: t(:button_delete), disabled: true, class: "-primary" do %> - <%= op_icon("button--icon icon-delete") %> - <%= t(:button_delete) %> + <%= op_icon("button--icon icon-delete") %> + <%= t(:button_delete) %> <% end %> <%= link_to project_settings_repository_path(@project), title: t(:button_cancel), class: "button -with-icon icon-cancel" do %> - <%= t(:button_cancel) %> + <%= t(:button_cancel) %> <% end %>
@@ -80,14 +84,20 @@ See COPYRIGHT and LICENSE files for more details. <% else %>
-

<%= t("repositories.destroy.title_not_managed", repository_type: "#{h(@repository.repository_type)} - #{t(:project_module_repository)}").html_safe %>

+

+ + <%= t("repositories.destroy.title_not_managed", + repository_type: content_tag(:em, "#{@repository.repository_type} - #{t(:project_module_repository)}")) %> + +
+

<%= t( - "repositories.destroy.subtitle_not_managed", - repository_type: "#{h(@repository.repository_type)} - #{t(:project_module_repository)}", - project_name: h(@project.identifier), - url: h(@repository.url) - ).html_safe %>
+ "repositories.destroy.subtitle_not_managed_html", + repository_type: "#{@repository.repository_type} - #{t(:project_module_repository)}", + project_name: @project.identifier, + url: @repository.url + ) %>

<%= t("repositories.destroy.info_not_managed") %> @@ -106,7 +116,7 @@ See COPYRIGHT and LICENSE files for more details. <%= link_to project_settings_repository_path(@project), title: t(:button_cancel), class: "button -with-icon icon-cancel" do %> - <%= t(:button_cancel) %> + <%= t(:button_cancel) %> <% end %>

diff --git a/app/views/repositories/diff.html.erb b/app/views/repositories/diff.html.erb index e598558ae2a..587cc82e30b 100644 --- a/app/views/repositories/diff.html.erb +++ b/app/views/repositories/diff.html.erb @@ -65,14 +65,11 @@ See COPYRIGHT and LICENSE files for more details. <%= render partial: 'common/diff', locals: { diff: @diff, diff_type: @diff_type } %> <% end -%> <%= other_formats_links do |f| %> - <% unidiff_link = f.link_to 'Diff', url: permitted_params.repository_diff.to_h, caption: 'Unified diff' %> - <% if !@path.blank? %> - <% unidiff_link.gsub!('?', '&') %> - <% end %> - <% wrong_url = CGI::escapeHTML(CGI.escape(with_leading_slash(@path))).concat('.diff') %> - <% good_url = '.diff'.concat('?repo_path=', CGI.escape(without_leading_slash(@path)).gsub('&', '%26')) %> - <% unidiff_link.gsub!(wrong_url, good_url) %> - <%= unidiff_link.html_safe %> + <% + unidiff_params = permitted_params.repository_diff.to_h.merge(format: "diff") + unidiff_params[:repo_path] = without_leading_slash(@path) if @path.present? + %> + <%= f.link_to "Diff", url: unidiff_params, caption: "Unified diff" %> <% end %> <% html_title(h(with_leading_slash(@path)), 'Diff') -%> diff --git a/app/views/repositories/show.html.erb b/app/views/repositories/show.html.erb index 13b4865fa08..2a2a433311b 100644 --- a/app/views/repositories/show.html.erb +++ b/app/views/repositories/show.html.erb @@ -34,7 +34,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render partial: "dir_list" %> <% end %> -<%= render_properties(@properties) %> +<%= render partial: "properties" %>
<%# rev => nil prevents overwriting the rev parameter queried for in the form with the parameter from the request %> diff --git a/app/views/sharing_mailer/shared_work_package.html.erb b/app/views/sharing_mailer/shared_work_package.html.erb index f1a3da18d3a..2f15ffa749e 100644 --- a/app/views/sharing_mailer/shared_work_package.html.erb +++ b/app/views/sharing_mailer/shared_work_package.html.erb @@ -36,12 +36,12 @@ <% - formatted_actions = @allowed_work_package_actions.map do |action| - "#{action.downcase}" + allowed_actions = @allowed_work_package_actions.map do |action| + content_tag(:span, action.downcase, class: "-bold") end.to_sentence %> - <%= t("mail.sharing.work_packages.allowed_actions", allowed_actions: formatted_actions).html_safe %> + <%= t("mail.sharing.work_packages.allowed_actions_html", allowed_actions:) %> diff --git a/app/views/sharing_mailer/shared_work_package.text.erb b/app/views/sharing_mailer/shared_work_package.text.erb index 5fdaff428a9..4e48dd1f14a 100644 --- a/app/views/sharing_mailer/shared_work_package.text.erb +++ b/app/views/sharing_mailer/shared_work_package.text.erb @@ -23,7 +23,7 @@ <%= "=" * (("# " + @work_package.id.to_s + @work_package.subject).length + 4) %> <%= I18n.t("mail.work_packages.reason.shared") %>: -<%= t("mail.sharing.work_packages.allowed_actions", allowed_actions: @allowed_work_package_actions.to_sentence).html_safe %> +<%= t("mail.sharing.work_packages.allowed_actions_html", allowed_actions: @allowed_work_package_actions.to_sentence) %> <%= t("mail.sharing.work_packages.open_work_package") %> <%= @url %> diff --git a/app/views/user_mailer/activation_limit_reached.html.erb b/app/views/user_mailer/activation_limit_reached.html.erb index f26ca7e8c75..4066916bdfd 100644 --- a/app/views/user_mailer/activation_limit_reached.html.erb +++ b/app/views/user_mailer/activation_limit_reached.html.erb @@ -30,7 +30,7 @@ See COPYRIGHT and LICENSE files for more details.

<% mail_link = content_tag(:a, @email, href: "mailto:#{@email}") %> <% host_link = content_tag(:a, Setting.app_title, href: Rails.application.root_url) %> - <%= t("mail_user_activation_limit_reached.message", email: mail_link, host: host_link).html_safe %> + <%= t("mail_user_activation_limit_reached.message_html", email: mail_link, host: host_link) %>

<%= t("mail_user_activation_limit_reached.steps.label") %> diff --git a/app/views/user_mailer/activation_limit_reached.text.erb b/app/views/user_mailer/activation_limit_reached.text.erb index 1c184667b63..39d8afaa668 100644 --- a/app/views/user_mailer/activation_limit_reached.text.erb +++ b/app/views/user_mailer/activation_limit_reached.text.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -<%= t("mail_user_activation_limit_reached.message", email: @email, host: Rails.application.root_url) %> +<%= t("mail_user_activation_limit_reached.message_html", email: @email, host: Rails.application.root_url) %> <%= t("mail_user_activation_limit_reached.steps.label") %> - <%= t("mail_user_activation_limit_reached.steps.a").sub(link_regex, OpenProject::Enterprise.upgrade_url) %> diff --git a/app/views/users/_preferences.html.erb b/app/views/users/_preferences.html.erb index 0968a94229c..02be01dfd76 100644 --- a/app/views/users/_preferences.html.erb +++ b/app/views/users/_preferences.html.erb @@ -28,11 +28,11 @@ See COPYRIGHT and LICENSE files for more details. ++#%> <%= fields_for :pref, @user.pref, builder: TabularFormBuilder, lang: current_language do |pref_fields| %> <%= render Settings::TimeZoneSettingComponent.new( - "time_zone", - form: pref_fields, - include_blank: false, - container_class: defined?(input_size) ? "-#{input_size}" : "-wide" - ) %> + "time_zone", + form: pref_fields, + include_blank: false, + container_class: defined?(input_size) ? "-#{input_size}" : "-wide" + ) %>
<%= pref_fields.select :theme, theme_options_for_select, container_class: "-middle" %>
@@ -44,10 +44,8 @@ See COPYRIGHT and LICENSE files for more details. <%= pref_fields.check_box :disable_keyboard_shortcuts, label: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts") %> - <%= I18n.t( - "activerecord.attributes.user_preference.disable_keyboard_shortcuts_caption_html", - href: OpenProject::Static::Links.url_for(:shortcuts) - ).html_safe %> + <%= link_translate(:"user_preferences.disable_keyboard_shortcuts_caption", + links: { docs_url: %i[shortcuts] }) %>
<% end %> diff --git a/app/views/work_packages/bulk/reassign.html.erb b/app/views/work_packages/bulk/reassign.html.erb index afd297f59f2..62dd5e752bd 100644 --- a/app/views/work_packages/bulk/reassign.html.erb +++ b/app/views/work_packages/bulk/reassign.html.erb @@ -48,8 +48,12 @@ See COPYRIGHT and LICENSE files for more details. <%= t("work_package.destroy.title") %> - <%= work_package_associations_to_address(associated) %> - +

<%= t(:text_destroy_with_associated) %>

+
    + <% associated.each do |associated_class| %> +
  • <%= associated_class.model_name.human %>
  • + <% end %> +

<%= t(:text_destroy_what_to_do) %>

@@ -108,9 +112,9 @@ See COPYRIGHT and LICENSE files for more details. <% end %>
<%= styled_button_tag title: t(:button_delete), class: "-primary" do - concat content_tag :i, "", class: "button--icon icon-delete" - concat content_tag :span, t(:button_delete), class: "button--text" - end %> + concat content_tag :i, "", class: "button--icon icon-delete" + concat content_tag :span, t(:button_delete), class: "button--text" + end %> <%= link_to_function t(:button_cancel), "history.back()", title: t(:button_cancel), diff --git a/app/views/workflows/copy.html.erb b/app/views/workflows/copy.html.erb index f925c158e06..b07841c4eec 100644 --- a/app/views/workflows/copy.html.erb +++ b/app/views/workflows/copy.html.erb @@ -39,18 +39,19 @@ See COPYRIGHT and LICENSE files for more details. <%= select_tag( "source_type_id", - "".html_safe + - "".html_safe + - options_from_collection_for_select(@types, "id", "name", @source_type && @source_type.id), class: "form--select" + (blank_select_option + + content_tag(:option, "--- #{t(:label_copy_same_as_target)} ---") + + options_from_collection_for_select(@types, "id", "name", @source_type && @source_type.id)), + class: "form--select" ) %>
<%= select_tag( "source_role_id", - "".html_safe + - "".html_safe + - options_from_collection_for_select(@roles, "id", "name", @source_role && @source_role.id), class: "form--select" + (blank_select_option + + content_tag(:option, "--- #{t(:label_copy_same_as_target)} ---") + + options_from_collection_for_select(@roles, "id", "name", @source_type && @source_type.id)), ) %>
@@ -62,16 +63,14 @@ See COPYRIGHT and LICENSE files for more details.
<%= select_tag "target_type_ids", - "".html_safe + - options_from_collection_for_select(@types, "id", "name", @target_types && @target_types.map(&:id)), + blank_select_option + options_from_collection_for_select(@types, "id", "name", @target_types && @target_types.map(&:id)), multiple: true, size: 20, class: "form--select" %>
<%= select_tag "target_role_ids", - "".html_safe + - options_from_collection_for_select(@roles, "id", "name", @target_roles && @target_roles.map(&:id)), + blank_select_option + options_from_collection_for_select(@roles, "id", "name", @target_roles && @target_roles.map(&:id)), multiple: true, size: 20, class: "form--select" %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 53810ac3649..b0a4b9d921e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -436,6 +436,15 @@ en: default_transitions: "Default transitions" user_author: "User is author" user_assignee: "User is assignee" + info: + database_deprecation_html: > + Starting with OpenProject 16.0, PostgreSQL 16 is required to use OpenProject. + Your installation will remain functional with your current database, but anticipate incompatability + in future releases. +
+ We have prepared [upgrade guides for all installation methods](upgrade_guide). + You can perform the upgrade ahead of the next release at any time by following the guides. + authentication: login_and_registration: "Login and registration" @@ -1101,9 +1110,9 @@ en: groups: member_in_these_groups: "This user is currently a member of the following groups:" no_results_title_text: This user is currently not a member in any group. - summary_with_more: Member of %{names} and %{count_link}. + summary_with_more_html: Member of %{names} and %{count_link}. more: "%{count} more" - summary: Member of %{names}. + summary_html: Member of %{names}. memberships: no_results_title_text: This user is currently not a member of a project. open_profile: "Open profile" @@ -1219,6 +1228,9 @@ en: work_days_count: one: "1 working day" other: "%{count} working days" + user_preferences: + disable_keyboard_shortcuts_caption: > + You can choose to disable default [keyboard shortcuts](docs_url) if you use a screen reader or want to avoid accidentally triggering an action with a shortcut. page: text: "Text" placeholder_users: @@ -1227,7 +1239,7 @@ en: You do not have the right to manage members for all projects that the placeholder user is a member of. delete_tooltip: "Delete placeholder user" deletion_info: - heading: "Delete placeholder user %{name}" + heading_html: "Delete placeholder user %{name}" data_consequences: > All occurrences of the placeholder user (e.g., as assignee, responsible or other user values) will be reassigned to an account called "Deleted user". @@ -1236,7 +1248,7 @@ en: it will not be possible to distinguish the data the user created from the data of another deleted account. irreversible: "This action is irreversible" - confirmation: "Enter the placeholder user name %{name} to confirm the deletion." + confirmation_html: "Enter the placeholder user name %{name} to confirm the deletion." priorities: edit: priority_color_text: | @@ -1272,7 +1284,7 @@ en: Check this option to exclude work packages with this status from totals of Work, Remaining work, and % Complete in a hierarchy. status_percent_complete_text: |- - In status-based progress calculation mode, the % Complete of a work + In [status-based progress calculation mode](setting_url), the % Complete of a work package is automatically set to this value when this status is selected. Ignored in work-based mode. status_readonly_html: | @@ -1560,7 +1572,7 @@ en: account for you or change the self registration limit for this provider. login_with_auth_provider: "or sign in with your existing account" signup_with_auth_provider: "or sign up using" - auth_source_login: Please login as %{login} to activate your account. + auth_source_login_html: Please login as %{login} to activate your account. omniauth_login: Please login to activate your account. actionview_instancetag_blank_option: "Please select" @@ -1853,8 +1865,6 @@ en: button_update_user_information: "Update profile" comments_sorting: "Display work package activity sorted by" disable_keyboard_shortcuts: "Disable keyboard shortcuts" - disable_keyboard_shortcuts_caption_html: |- - You can choose to disable default keyboard shortcuts if you use a screen reader or want to avoid accidentally triggering an action with a shortcut. dismissed_enterprise_banners: "Hidden enterprise banners" impaired: "Accessibility mode" auto_hide_popups: "Automatically hide success banners" @@ -2618,7 +2628,7 @@ en: You will need to generate a backup token to be able to create a backup. Each time you want to request a backup you will have to provide this token. You can delete the backup token to disable backups for this user. - verification: > + verification_html: > Enter %{word} to confirm you want to %{action} the backup token. verification_word_reset: reset verification_word_create: create @@ -3077,7 +3087,7 @@ en: title: one: "One day left of %{trial_plan} trial token" other: "%{count} days left of %{trial_plan} trial token" - description: "You have access to all %{trial_plan} features." + description_html: "You have access to all %{trial_plan} features." trial: not_found: "You have requested a trial token, but that request is no longer available. Please try again." wait_for_confirmation: "We sent you an email to confirm your address in order to retrieve a trial token." @@ -3096,11 +3106,8 @@ en: confirmation_subline: > Please, check your inbox and follow the steps to start your 14-day free trial. domain_caption: The token will be valid for your currently configured host name. - receive_newsletter_html: > - I want to receive the OpenProject newsletter. - consent_html: > - I agree with the terms of service - and the privacy policy. + receive_newsletter: "I want to receive the OpenProject [newsletter](newsletter_url)." + consent: "I agree with the [terms of service](tos_url) and the [privacy policy](privacy_url)." email_calendar_updates: state: @@ -3195,8 +3202,8 @@ en: work_package_edit: "Work Package edited" work_package_note: "Work Package note added" title: - project: "Project: %{name}" - subproject: "Subproject: %{name}" + project_html: "Project: %{name}" + subproject_html: "Subproject: %{name}" export: dialog: @@ -3466,9 +3473,9 @@ en: configuration_guide: "Configuration guide" get_in_touch: "You have questions? Get in touch with us." - instructions_after_registration: "You can sign in as soon as your account has been activated by clicking %{signin}." - instructions_after_logout: "You can sign in again by clicking %{signin}." - instructions_after_error: "You can try to sign in again by clicking %{signin}. If the error persists, ask your admin for help." + instructions_after_registration: "You can sign in as soon as your account has been activated by clicking [here](signin_url)." + instructions_after_logout: "You can sign in again by clicking [here](signin_url)." + instructions_after_error: "You can try to sign in again by clicking [here](signin_url). If the error persists, ask your admin for help." menus: admin: @@ -3664,6 +3671,8 @@ en: label_calendar_show: "Show Calendar" label_category: "Category" label_completed: Completed + label_committed_at_html: "%{committed_revision_link} at %{date}" + label_committed_link: "committed revision %{revision_identifier}" label_consent_settings: "User Consent" label_wiki_menu_item: Wiki menu item label_select_main_menu_item: Select new main menu item @@ -4189,7 +4198,7 @@ en: label_user: "User" label_user_and_permission: "Users and permissions" label_user_named: "User %{name}" - label_user_activity: "%{value}'s activity" + label_user_activity_html: "%{value}'s activity" label_user_anonymous: "Anonymous" label_user_menu: "User menu" label_user_new: "New user" @@ -4373,7 +4382,7 @@ en: note: "Note: “%{note}”" sharing: work_packages: - allowed_actions: "You may %{allowed_actions} this work package. This can change depending on your project role and permissions." + allowed_actions_html: "You may %{allowed_actions} this work package. This can change depending on your project role and permissions." create_account: "To access this work package, you will need to create and activate an account on %{instance}." open_work_package: "Open work package" subject: "Work package #%{id} was shared with you" @@ -4476,7 +4485,7 @@ en: mail_user_activation_limit_reached: subject: User activation limit reached - message: | + message_html: | A new user (%{email}) tried to create an account on an OpenProject environment that you manage (%{host}). The user cannot activate their account since the user limit has been reached. steps: @@ -4832,10 +4841,10 @@ en: info: "Deleting the repository is an irreversible action." info_not_managed: "Note: This will NOT delete the contents of this repository, as it is not managed by OpenProject." managed_path_note: "The following directory will be erased: %{path}" - repository_verification: "Enter the project's identifier %{identifier} to verify the deletion of its repository." + repository_verification_html: "Enter the project's identifier %{identifier} to verify the deletion of its repository." subtitle: "Do you really want to delete the %{repository_type} of the project %{project_name}?" - subtitle_not_managed: "Do you really want to remove the linked %{repository_type} %{url} from the project %{project_name}?" - title: "Delete the %{repository_type}" + subtitle_not_managed_html: "Do you really want to remove the linked %{repository_type} %{url} from the project %{project_name}?" + title_html: "Delete the %{repository_type}" title_not_managed: "Remove the linked %{repository_type}?" errors: build_failed: "Unable to create the repository with the selected configuration. %{reason}" @@ -4946,7 +4955,7 @@ en: setting_apiv3_cors_origins_text_html: > If CORS is enabled, these are the origins that are allowed to access OpenProject API.
- Please check the Documentation on the Origin header on how to specify the expected values. + Please check the [Documentation on the Origin header](docs_url) on how to specify the expected values. setting_apiv3_write_readonly_attributes: "Write access to read-only attributes" setting_apiv3_write_readonly_attributes_instructions: > If enabled, the API will allow administrators to write static read-only attributes during creation, @@ -4956,7 +4965,7 @@ en: administrators to impersonate the creation of items as other users. All creation requests are being logged however with the true author. setting_apiv3_write_readonly_attributes_additional: > - For more information on attributes and supported resources, please see the %{api_documentation_link}. + For more information on attributes and supported resources, please see the [API documentation](api_documentation_link). setting_apiv3_max_page_size: "Maximum API page size" setting_apiv3_max_page_size_instructions: > Set the maximum page size the API will respond with. @@ -5366,7 +5375,10 @@ en: not_allowed_text: "You do not have the necessary permissions to view this page." activities: enable_internal_comments: "Enable internal comments" - helper_text: "Internal comments allow an internal team to communicate amongst themselves privately. These are only visible to selected roles that have the necessary permissions and will not be visible publicly. %{link}" + helper_text: > + Internal comments allow an internal team to communicate amongst themselves privately. + These are only visible to selected roles that have the necessary permissions and will not be visible publicly. + [Click here to learn more](docs_url) text_formatting: markdown: "Markdown" @@ -5647,21 +5659,23 @@ en: version_status_open: "open" note: Note - note_password_login_disabled: "Password login has been disabled by %{configuration}." + note_password_login_disabled: "Password login has been disabled through a [configuration setting](configuration_url)." warning: Warning warning_attachments_not_saved: "%{count} file(s) could not be saved." warning_imminent_user_limit: > You invited more users than are supported by your current plan. Invited users may not be able to join your OpenProject environment. - Please upgrade your plan or block existing + Please [upgrade your plan](upgrade_url) or block existing users in order to allow invited and registered users to join. warning_registration_token_expired: | The activation email has expired. We sent you a new one to %{email}. Please click the link inside of it to activate your account. warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this instance. + Adding additional users will exceed the current limit. + Please contact an administrator to increase the user limit to ensure external users are able to access this instance. warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this instance. + Adding additional users will exceed the current limit. + Please [upgrade your plan](upgrade_url) to be able to ensure external users are able to access this instance. warning_user_limit_reached_instructions: > You reached your user limit (%{current}/%{max} active users). Please contact sales@openproject.com to upgrade your Enterprise edition plan and add additional users. @@ -5730,7 +5744,7 @@ en: reminders: label_remind_at: "Date" note_placeholder: "Why are you setting this reminder?" - create_success_message: "Reminder set successfully. You will receive a notification for this work package %{reminder_time}." + create_success_message_html: "Reminder set successfully. You will receive a notification for this work package %{reminder_time}." success_update_message: "Reminder updated successfully." success_deletion_message: "Reminder deleted successfully." sharing: @@ -5760,9 +5774,11 @@ en: text_user_limit_reached: "Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}." text_user_limit_reached_admins: 'Adding additional users will exceed the current limit. Please upgrade your plan to be able to add more users.' warning_user_limit_reached: > - Adding additional users will exceed the current limit. Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. + Adding additional users will exceed the current limit. + Please contact an administrator to increase the user limit to ensure external users are able to access this %{entity}. warning_user_limit_reached_admin: > - Adding additional users will exceed the current limit. Please upgrade your plan to be able to ensure external users are able to access this %{entity}. + Adding additional users will exceed the current limit. + Please [upgrade your plan](upgrade_url) to be able to ensure external users are able to access this %{entity}. warning_no_selected_user: "Please select users to share this %{entity} with" warning_locked_user: "The user %{user} is locked and cannot be shared with" user_details: diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 084a63c3edd..6e8a9765fc0 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -389,8 +389,6 @@ en: label_collapse_all: "Collapse all" label_collapse_text: "Collapse text" label_comment: "Comment" - label_committed_at: "%{committed_revision_link} at %{date}" - label_committed_link: "committed revision %{revision_identifier}" label_contains: "contains" label_created_on: "created on" label_edit_comment: "Edit this comment" diff --git a/config/static_links.yml b/config/static_links.yml index 7708b89c816..03c503d8288 100644 --- a/config/static_links.yml +++ b/config/static_links.yml @@ -201,6 +201,8 @@ sysadmin_docs: href: https://www.openproject.org/docs/system-admin-guide/authentication/scim/#step-3-choose-an-authentication-method scim_jwt_authetication_method: href: https://www.openproject.org/docs/system-admin-guide/authentication/scim/#c-jwt-from-identity-provider +terms_of_service: + href: https://www.openproject.org/terms-of-service/ text_formatting: href: https://www.openproject.org/docs/user-guide/wysiwyg/ label: :setting_text_formatting diff --git a/lib/custom_field_form_builder.rb b/lib/custom_field_form_builder.rb index 836361c992d..74b455567a1 100644 --- a/lib/custom_field_form_builder.rb +++ b/lib/custom_field_form_builder.rb @@ -142,15 +142,14 @@ class CustomFieldFormBuilder < TabularFormBuilder for: custom_field_field_id, class: classes, title: custom_field.name do - output = "".html_safe - output += custom_field.name + capture do + concat custom_field.name - # Render a help text icon - if options[:help_text] - output += content_tag("attribute-help-text", "", data: options[:help_text]) + # Render a help text icon + if options[:help_text] + concat content_tag("attribute-help-text", "", data: options[:help_text]) + end end - - output end end end diff --git a/lib/open_project/object_linking.rb b/lib/open_project/object_linking.rb index 584285675e6..58b4f628451 100644 --- a/lib/open_project/object_linking.rb +++ b/lib/open_project/object_linking.rb @@ -167,7 +167,11 @@ module OpenProject def project_link_name(project, show_icon) if show_icon && User.current.member_of?(project) - icon_wrapper("icon-context icon-star", I18n.t(:description_my_project).html_safe + " ".html_safe) + project.name + label = ActiveSupport::SafeBuffer.new + label << I18n.t(:description_my_project) + label << " ".html_safe + + icon_wrapper("icon-context icon-star", label) + project.name else project.name end diff --git a/lib/open_project/page_hierarchy_helper.rb b/lib/open_project/page_hierarchy_helper.rb index b102cadb240..0f1d3efa46c 100644 --- a/lib/open_project/page_hierarchy_helper.rb +++ b/lib/open_project/page_hierarchy_helper.rb @@ -39,7 +39,7 @@ module OpenProject concat render_page_hierarchy(pages, page.id, options) if is_parent end end - chunks.join.html_safe + safe_join(chunks) end end diff --git a/lib/open_project/patches/action_view_accessible_errors.rb b/lib/open_project/patches/action_view_accessible_errors.rb index cc9aa38cd11..8eef9d9f0be 100644 --- a/lib/open_project/patches/action_view_accessible_errors.rb +++ b/lib/open_project/patches/action_view_accessible_errors.rb @@ -44,7 +44,11 @@ module ActionView def wrap_with_error_span(html_tag, object, method) object_identifier = erroneous_object_identifier(object.object_id.to_s, method) - "#{html_tag}".html_safe + helpers = ActionController::Base.helpers + + helpers.content_tag(:span, id: object_identifier, class: "errorSpan") do + helpers.content_tag(:a, "", name: object_identifier) + html_tag + end end def erroneous_object_identifier(id, method) diff --git a/lib/redmine/menu_manager/menu_helper.rb b/lib/redmine/menu_manager/menu_helper.rb index 9b8384173ba..b369d892415 100644 --- a/lib/redmine/menu_manager/menu_helper.rb +++ b/lib/redmine/menu_manager/menu_helper.rb @@ -180,10 +180,10 @@ module Redmine::MenuManager::MenuHelper caption, url, selected = extract_node_details(item, project) shown_in_main_menu = menu_class == "op-menu" - link_text = "".html_safe + link_text = ActiveSupport::SafeBuffer.new if item.icon(project).present? - link_text += render(Primer::Beta::Octicon.new( + link_text << render(Primer::Beta::Octicon.new( icon: item.icon, mr: shown_in_main_menu ? 3 : 0, size: shown_in_main_menu ? :small : :medium @@ -192,7 +192,7 @@ module Redmine::MenuManager::MenuHelper badge_class = item.badge(project:).present? ? " #{menu_class}--item-title_has-badge" : "" - link_text += content_tag(:span, + link_text << content_tag(:span, class: "#{menu_class}--item-title#{badge_class}", lang: menu_item_locale(item)) do title_text = content_tag(:span, caption, class: "ellipsis") + badge_for(item) @@ -205,7 +205,7 @@ module Redmine::MenuManager::MenuHelper end if item.icon_after.present? - link_text += render(Primer::Beta::Octicon.new(icon: item.icon_after, classes: "trailing-icon")) + link_text << render(Primer::Beta::Octicon.new(icon: item.icon_after, classes: "trailing-icon")) end html_options = item.html_options(selected:) @@ -424,8 +424,6 @@ module Redmine::MenuManager::MenuHelper key = item.badge(project: @project) if key.present? content_tag("span", I18n.t(key), class: "main-item--badge") - else - "".html_safe end end diff --git a/lib/tabular_form_builder.rb b/lib/tabular_form_builder.rb index 4c55d02171c..9147756edd6 100644 --- a/lib/tabular_form_builder.rb +++ b/lib/tabular_form_builder.rb @@ -207,7 +207,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder if prefix ret.prepend content_tag(:span, - prefix.html_safe, + prefix, class: "form--field-affix", id: options[:prefix_id], "aria-hidden": true) @@ -215,7 +215,7 @@ class TabularFormBuilder < ActionView::Helpers::FormBuilder if suffix ret.concat content_tag(:span, - suffix.html_safe, + suffix, class: "form--field-affix", id: options[:suffix_id], "aria-hidden": true) diff --git a/lib_static/redmine/i18n.rb b/lib_static/redmine/i18n.rb index e6b739df5a0..597a25b87ed 100644 --- a/lib_static/redmine/i18n.rb +++ b/lib_static/redmine/i18n.rb @@ -290,7 +290,7 @@ module Redmine href:, target:, underline:, - data: { allow_external_link: true }, + data: { allow_external_link: true } ) component.with_trailing_visual_icon(icon: :"link-external") if external component.with_content(text) diff --git a/lookbook/docs/patterns/02-forms.md.erb b/lookbook/docs/patterns/02-forms.md.erb index 47f84f5bb0e..63c317a9891 100644 --- a/lookbook/docs/patterns/02-forms.md.erb +++ b/lookbook/docs/patterns/02-forms.md.erb @@ -439,8 +439,7 @@ class Admin::Settings::GeneralSettingsForm < ApplicationForm name: :per_page_options, label: I18n.t("setting_per_page_options"), value: Setting[:per_page_options], - caption: "#{I18n.t(:text_comma_separated)}
" \ - "#{I18n.t(:text_notice_too_many_values_are_inperformant)}".html_safe) + caption: safe_join [I18n.t(:text_comma_separated), helpers.tag(:br), I18n.t(:text_notice_too_many_values_are_inperformant)], disabled: !Setting.per_page_options_writable? ) general_form.text_field( @@ -492,8 +491,9 @@ class Admin::Settings::GeneralSettingsForm < ApplicationForm settings_form do |general_form| general_form.text_field(name: :app_title) general_form.text_field(name: :per_page_options, - caption: "#{I18n.t(:text_comma_separated)}
" \ - "#{I18n.t(:text_notice_too_many_values_are_inperformant)}".html_safe) + caption: safe_join [I18n.t(:text_comma_separated), + helpers.tag(:br), + I18n.t(:text_notice_too_many_values_are_inperformant)]) general_form.text_field(name: :activity_days_default, type: :number) general_form.text_field(name: :host_name, diff --git a/modules/auth_saml/app/views/saml/providers/confirm_destroy.html.erb b/modules/auth_saml/app/views/saml/providers/confirm_destroy.html.erb index a062532c586..57788a906b8 100644 --- a/modules/auth_saml/app/views/saml/providers/confirm_destroy.html.erb +++ b/modules/auth_saml/app/views/saml/providers/confirm_destroy.html.erb @@ -36,7 +36,7 @@ See COPYRIGHT and LICENSE files for more details. <%= t("saml.delete_title") %>

- <%= t("provider.delete_warning.provider", name: content_tag(:strong, @provider.display_name)).html_safe %> + <%= t("provider.delete_warning.provider_html", name: content_tag(:strong, @provider.display_name)) %>

  • <%= t("provider.delete_warning.delete_result_1") %>
  • @@ -50,7 +50,8 @@ See COPYRIGHT and LICENSE files for more details. <%= t("provider.delete_warning.irreversible_notice") %>

    - <%= t("provider.delete_warning.input_delete_confirmation", name: "#{h(@provider.display_name)}").html_safe %> + <%= t("provider.delete_warning.input_delete_confirmation_html", + name: content_tag(:em, @provider.display_name, class: "danger-zone--expected-value")) %>

    <%= text_field_tag :delete_confirmation %> diff --git a/modules/avatars/app/views/avatars/users/_gravatars.html.erb b/modules/avatars/app/views/avatars/users/_gravatars.html.erb index db817e81675..9fb4fa4c4f8 100644 --- a/modules/avatars/app/views/avatars/users/_gravatars.html.erb +++ b/modules/avatars/app/views/avatars/users/_gravatars.html.erb @@ -10,7 +10,7 @@ <% if @user == current_user %>

    <%= t "note" %>: - <%= t("avatars.text_change_gravatar_html", gravatar_url: link_to("gravatar.com", "https://www.gravatar.com")).html_safe %> + <%= t("avatars.text_change_gravatar_html", gravatar_url: link_to("gravatar.com", "https://www.gravatar.com")) %>

    <% end %> diff --git a/modules/job_status/app/components/job_status/dialog/body_component.html.erb b/modules/job_status/app/components/job_status/dialog/body_component.html.erb index 69dfd0e149c..42cc38c0de0 100644 --- a/modules/job_status/app/components/job_status/dialog/body_component.html.erb +++ b/modules/job_status/app/components/job_status/dialog/body_component.html.erb @@ -43,7 +43,7 @@ download: mime_type_pdf? ? nil : "download", data: { "job-status-polling-target": "download" - }) + } ) end end diff --git a/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/destroy_info.html.erb b/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/destroy_info.html.erb index f4e3bf2da74..1dfc6e42304 100644 --- a/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/destroy_info.html.erb +++ b/modules/ldap_groups/app/views/ldap_groups/synchronized_filters/destroy_info.html.erb @@ -52,9 +52,9 @@ <% end %>

    <%= t( - "ldap_groups.synchronized_filters.destroy.verification", - name: "#{h(@filter.name)}" - ).html_safe %> + "ldap_groups.synchronized_filters.destroy.verification_html", + name: content_tag(:em, @filter.name, class: "danger-zone--expected-value") + ) %>

    diff --git a/modules/ldap_groups/app/views/ldap_groups/synchronized_groups/destroy_info.html.erb b/modules/ldap_groups/app/views/ldap_groups/synchronized_groups/destroy_info.html.erb index c0d14bbf6b3..ecd9aa25b38 100644 --- a/modules/ldap_groups/app/views/ldap_groups/synchronized_groups/destroy_info.html.erb +++ b/modules/ldap_groups/app/views/ldap_groups/synchronized_groups/destroy_info.html.erb @@ -20,8 +20,8 @@
    <%= t( "ldap_groups.synchronized_groups.destroy.verification", - name: "#{h(@group.dn)}" - ).html_safe %> + name: content_tag(:em, @group.dn, class: "danger-zone--expected-value") + ) %>

    diff --git a/modules/ldap_groups/config/locales/en.yml b/modules/ldap_groups/config/locales/en.yml index 168e4714cad..fce21491f55 100644 --- a/modules/ldap_groups/config/locales/en.yml +++ b/modules/ldap_groups/config/locales/en.yml @@ -48,7 +48,7 @@ en: title: 'Remove synchronized filter %{name}' confirmation: "If you continue, the synchronized filter %{name} and all groups %{groups_count} created through it will be removed." removed_groups: "Warning: This will remove the following groups from OpenProject and remove it from all projects!" - verification: "Enter the filter name %{name} to verify the deletion." + verification_html: "Enter the filter name %{name} to verify the deletion." form: group_name_attribute_text: 'Enter the attribute of the LDAP group used for setting the OpenProject group name.' filter_string_text: 'Enter the RFC4515 LDAP filter that returns groups in your LDAP to synchronize with OpenProject.' @@ -63,7 +63,7 @@ en: title: 'Remove synchronized group %{name}' confirmation: "If you continue, the synchronized group %{name} and all %{users_count} users synchronized through it will be removed." info: "Note: The OpenProject group itself and members added outside this LDAP synchronization will not be removed." - verification: "Enter the group's name %{name} to verify the deletion." + verification_html: "Enter the group's name %{name} to verify the deletion." help_text_html: | This module allows you to set up a synchronization between LDAP and OpenProject groups. It depends on LDAP groups need to use the groupOfNames / memberOf attribute set to be working with OpenProject. diff --git a/modules/meeting/app/components/base_errors_component.rb b/modules/meeting/app/components/base_errors_component.rb index bfa134ec95f..744d95832fe 100644 --- a/modules/meeting/app/components/base_errors_component.rb +++ b/modules/meeting/app/components/base_errors_component.rb @@ -52,6 +52,6 @@ class BaseErrorsComponent < ApplicationComponent def joined_messages messages = @keys.map { |key| @errors.full_messages_for(key) }.flatten - helpers.safe_join(messages, "
    ".html_safe) + helpers.safe_join(messages, helpers.tag(:br)) end end diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 3180d9a62f5..182e98c7f40 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -119,13 +119,16 @@ class MeetingsController < ApplicationController @meeting = call.result if call.success? - text = I18n.t(:notice_successful_create) + text = ActiveSupport::SafeBuffer.new + text << I18n.t(:notice_successful_create) unless User.current.pref.time_zone? link = I18n.t(:notice_timezone_missing, zone: formatted_time_zone_offset) - text += " #{view_context.link_to(link, { controller: '/my', action: :locale, anchor: 'pref_time_zone' }, - class: 'link_to_profile')}" + text << " " + text << view_context.link_to(link, + { controller: "/my", action: :locale, anchor: "pref_time_zone" }, + class: "link_to_profile") end - flash[:notice] = text.html_safe # rubocop:disable Rails/OutputSafety + flash[:notice] = text redirect_to status: :see_other, action: "show", id: @meeting else @@ -317,7 +320,7 @@ class MeetingsController < ApplicationController .call .on_failure { |call| render_500(message: call.message) } .on_success do |call| - send_data call.result, filename: filename_for_content_disposition("#{@meeting.title}.ics") + send_data call.result, filename: filename_for_content_disposition("#{@meeting.title}.ics") end end @@ -379,8 +382,8 @@ class MeetingsController < ApplicationController def exit_draft_mode call = ::Meetings::UpdateService - .new(user: current_user, model: @meeting) - .call({ state: "open", notify: meeting_params[:notify] == "1" }) + .new(user: current_user, model: @meeting) + .call({ state: "open", notify: meeting_params[:notify] == "1" }) if call.success? deliver_invitation_mails @@ -419,11 +422,11 @@ class MeetingsController < ApplicationController .participants .invited .find_each do |participant| - MeetingMailer.invited( - @meeting, - participant.user, - User.current - ).deliver_later + MeetingMailer.invited( + @meeting, + participant.user, + User.current + ).deliver_later end end @@ -646,12 +649,13 @@ class MeetingsController < ApplicationController service = MeetingNotificationService.new(@meeting) result = service.call(:invited) - message = if result.success? - I18n.t(:notice_successful_notification) - else - I18n.t(:error_notification_with_errors, - recipients: result.errors.map(&:name).join("; ")) - end + message = + if result.success? + I18n.t(:notice_successful_notification) + else + I18n.t(:error_notification_with_errors, + recipients: result.errors.map(&:name).join("; ")) + end if type == :notify flash[result.success? ? :notice : :error] = message @@ -669,11 +673,11 @@ class MeetingsController < ApplicationController .participants .invited .find_each do |participant| - MeetingSeriesMailer.invited( - recurring_meeting, - participant.user, - User.current - ).deliver_later + MeetingSeriesMailer.invited( + recurring_meeting, + participant.user, + User.current + ).deliver_later end render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_notification)) diff --git a/modules/meeting/app/controllers/recurring_meetings_controller.rb b/modules/meeting/app/controllers/recurring_meetings_controller.rb index 751ae75c715..ba41ab814df 100644 --- a/modules/meeting/app/controllers/recurring_meetings_controller.rb +++ b/modules/meeting/app/controllers/recurring_meetings_controller.rb @@ -86,7 +86,7 @@ class RecurringMeetingsController < ApplicationController @recurring_meeting = call.result if call.success? - flash[:notice] = I18n.t(:notice_successful_create).html_safe + flash[:notice] = I18n.t(:notice_successful_create) redirect_to project_meeting_path(@recurring_meeting.project, @recurring_meeting.template), status: :see_other else diff --git a/modules/openid_connect/app/views/openid_connect/providers/confirm_destroy.html.erb b/modules/openid_connect/app/views/openid_connect/providers/confirm_destroy.html.erb index 6dc53156dae..a5457d66dba 100644 --- a/modules/openid_connect/app/views/openid_connect/providers/confirm_destroy.html.erb +++ b/modules/openid_connect/app/views/openid_connect/providers/confirm_destroy.html.erb @@ -36,7 +36,8 @@ See COPYRIGHT and LICENSE files for more details. <%= t("openid_connect.delete_title") %>

    - <%= t("provider.delete_warning.provider", name: content_tag(:strong, @provider.display_name)).html_safe %> + <%= t("provider.delete_warning.provider_html", + name: content_tag(:strong, @provider.display_name)) %>

    • <%= t("provider.delete_warning.delete_result_1") %>
    • @@ -50,7 +51,8 @@ See COPYRIGHT and LICENSE files for more details. <%= t("provider.delete_warning.irreversible_notice") %>

      - <%= t("provider.delete_warning.input_delete_confirmation", name: "#{h(@provider.display_name)}").html_safe %> + <%= t("provider.delete_warning.input_delete_confirmation_html", + name: content_tag(:em, @provider.display_name, class: "danger-zone--expected-value")) %>

      <%= text_field_tag :delete_confirmation %> diff --git a/modules/openid_connect/config/locales/en.yml b/modules/openid_connect/config/locales/en.yml index c870826f8d9..e8b0d23c425 100644 --- a/modules/openid_connect/config/locales/en.yml +++ b/modules/openid_connect/config/locales/en.yml @@ -51,9 +51,9 @@ en: provider: delete_warning: - input_delete_confirmation: Enter the provider name %{name} to confirm deletion. + input_delete_confirmation_html: Enter the provider name %{name} to confirm deletion. irreversible_notice: Deleting an SSO provider is an irreversible action. - provider: 'Are you sure you want to delete the SSO provider %{name}? To confirm this action please enter the name of the provider in the field below, this will:' + provider_html: 'Are you sure you want to delete the SSO provider %{name}? To confirm this action please enter the name of the provider in the field below, this will:' delete_result_1: Remove the provider from the list of available providers. delete_result_user_count: zero: No users are currently using this provider. No further action is required. diff --git a/modules/recaptcha/app/views/recaptcha/admin/show.html.erb b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb index 6b4bae905f8..565d4d2ec80 100644 --- a/modules/recaptcha/app/views/recaptcha/admin/show.html.erb +++ b/modules/recaptcha/app/views/recaptcha/admin/show.html.erb @@ -27,12 +27,12 @@ container_class: "-middle" %>
      - <%= I18n.t( + <%= t( "recaptcha.settings.captcha_description_html", hcaptcha_link: link_to("https://docs.hcaptcha.com/switch/", "https://docs.hcaptcha.com/switch/", target: "_blank"), recaptcha_link: link_to("https://www.google.com/recaptcha", "https://www.google.com/recaptcha", target: "_blank"), turnstile_link: link_to("https://developers.cloudflare.com/turnstile/", "https://developers.cloudflare.com/turnstile/", target: "_blank") - ).html_safe %> + ) %>
    diff --git a/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb index dd4f22a0162..2f4efc22692 100644 --- a/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb +++ b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.html.erb @@ -4,7 +4,11 @@ flex_layout(flex_items: :center) do |credentials_row| credentials_row.with_row(mb: 3, test_selector: "storage-openproject_oauth_application_warning") do render(Primer::Alpha::Banner.new(icon: :alert, scheme: :warning)) do - I18n.t("storages.instructions.oauth_application_details", oauth_application_details_link: oauth_application_details_link).html_safe + helpers.link_translate( + "storages.instructions.oauth_application_details", + links: { oauth_application_details_link: ::Storages::UrlBuilder.url(storage.uri, "settings/admin/openproject") }, + external: true + ) end end diff --git a/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb index cb5979463d5..67510d85b20 100644 --- a/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb +++ b/modules/storages/app/components/storages/admin/oauth_application_info_copy_component.rb @@ -40,16 +40,6 @@ module Storages::Admin def self.wrapper_key = :storage_openproject_oauth_section - def oauth_application_details_link - render( - Primer::Beta::Link.new( - href: ::Storages::UrlBuilder.url(storage.uri, "settings/admin/openproject"), - data: { allow_external_link: true }, - target: "_blank" - ) - ) { I18n.t("storages.instructions.oauth_application_details_link_text") } - end - def submit_button_options { scheme: :primary, diff --git a/modules/storages/app/components/storages/admin/storages/destroy_confirmation_dialog_component.html.erb b/modules/storages/app/components/storages/admin/storages/destroy_confirmation_dialog_component.html.erb index 26e88291817..931912bb0b1 100644 --- a/modules/storages/app/components/storages/admin/storages/destroy_confirmation_dialog_component.html.erb +++ b/modules/storages/app/components/storages/admin/storages/destroy_confirmation_dialog_component.html.erb @@ -43,7 +43,8 @@ See COPYRIGHT and LICENSE files for more details. <% dialog.with_additional_details do %> <% render(Primer::OpenProject::FlexLayout.new) do |flex| %> <% flex.with_row do %> - <%= t("storages.delete_warning.storage", file_storage: "#{h(@storage.name)}").html_safe %> + <%= t("storages.delete_warning.storage_html", + file_storage: content_tag(:strong, @storage.name)) %> <% end %> <% flex.with_row do %>
      diff --git a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb index 932ac083f03..dd2c020e3de 100644 --- a/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb +++ b/modules/storages/app/components/storages/project_storages/destroy_confirmation_dialog_component.html.erb @@ -44,7 +44,8 @@ See COPYRIGHT and LICENSE files for more details. <% dialog.with_additional_details do %> <% render(Primer::OpenProject::FlexLayout.new) do |flex| %> <% flex.with_row do %> - <%= t("storages.delete_warning.project_storage", file_storage: "#{h(@project_storage.storage.name)}").html_safe %> + <%= t("storages.delete_warning.project_storage_html", + file_storage: content_tag(:strong, @project_storage.storage.name)) %> <% end %> <% flex.with_row do %>
        diff --git a/modules/storages/app/views/storages/project_settings/project_storage_members/index.html.erb b/modules/storages/app/views/storages/project_settings/project_storage_members/index.html.erb index 8367a04d372..26814125f77 100644 --- a/modules/storages/app/views/storages/project_settings/project_storage_members/index.html.erb +++ b/modules/storages/app/views/storages/project_settings/project_storage_members/index.html.erb @@ -44,9 +44,9 @@ See COPYRIGHT and LICENSE files for more details. <%= content_tag( :p, t( - "storages.page_titles.project_storage_members.subtitle", + "storages.page_titles.project_storage_members.subtitle_html", storage_name_link: link_to(@storage.name, edit_admin_settings_storage_path(@storage)) - ).html_safe + ) ) %> <%= render(::Storages::ProjectStorages::Members::TableComponent.new(rows: @project_users, storage: @storage)) %> diff --git a/modules/storages/config/locales/en.yml b/modules/storages/config/locales/en.yml index 49b2a4cd140..4937ebfbb72 100644 --- a/modules/storages/config/locales/en.yml +++ b/modules/storages/config/locales/en.yml @@ -227,13 +227,13 @@ en: confirm_replace_oauth_application: This action will reset the current OAuth credentials. After confirming you will have to reenter the credentials at the storage provider and all remote users will have to authorize against OpenProject again. Are you sure you want to proceed? confirm_replace_oauth_client: This action will reset the current OAuth credentials. After confirming you will have to enter new credentials from the storage provider and all users will have to authorize against %{provider_type} again. Are you sure you want to proceed? delete_warning: - project_storage: Are you sure you want to remove %{file_storage} from this project? project_storage_delete_result_1: All links to corresponding files and folders will be removed project_storage_delete_result_2: The automatically-managed project folder and all files in it will be deleted - storage: Are you sure you want to delete %{file_storage} as an external file storage? + project_storage_html: Are you sure you want to remove %{file_storage} from this project? storage_delete_result_1: The storage will be removed from all projects currently using it storage_delete_result_2: All links to corresponding files and folders will be removed storage_delete_result_3: The automatically-managed project folder and all files in it will be deleted + storage_html: Are you sure you want to delete %{file_storage} as an external file storage? dependencies: nextcloud: group_folders_app: Team Folders @@ -413,8 +413,7 @@ en: no_specific_folder: By default, each user will start at their own home folder when they upload a file. no_storage_set_up: There are no file storages set up yet. not_logged_into_storage: To select a project folder, please first login - oauth_application_details: The client secret value will not be accessible again after you close this window. Please copy these values into the %{oauth_application_details_link}. - oauth_application_details_link_text: Nextcloud OpenProject Integration settings + oauth_application_details: The client secret value will not be accessible again after you close this window. Please copy these values into the [Nextcloud OpenProject Integration settings](oauth_application_details_link). one_drive: application_link_text: Azure portal copy_redirect_uri: Copy redirect URI @@ -522,7 +521,7 @@ en: members_connection_status: Members connection status new: Add a file storage to this project project_storage_members: - subtitle: Check the connection status for the storage %{storage_name_link} of all project members. + subtitle_html: Check the connection status for the storage %{storage_name_link} of all project members. title: Members connection status project_storages: delete: Remove file storage from project? diff --git a/modules/two_factor_authentication/app/views/two_factor_authentication/authentication/request_otp.html.erb b/modules/two_factor_authentication/app/views/two_factor_authentication/authentication/request_otp.html.erb index d00b885bbf2..d1ab0c2db62 100644 --- a/modules/two_factor_authentication/app/views/two_factor_authentication/authentication/request_otp.html.erb +++ b/modules/two_factor_authentication/app/views/two_factor_authentication/authentication/request_otp.html.erb @@ -20,7 +20,7 @@

        <%= t "two_factor_authentication.label_two_factor_authentication" %>

        <% identifier = @used_device.request_2fa_identifier(@strategy.channel) %> <% if @used_device.input_based? %> -

        <%= raw I18n.t("two_factor_authentication.devices.2fa_from_input", device_name: identifier) %>

        +

        <%= t("two_factor_authentication.devices.2fa_from_input_html", device_name: identifier) %>


        <%= styled_label_tag "otp", t(:field_otp) %> @@ -29,7 +29,7 @@
    <% else %> -

    <%= raw I18n.t("two_factor_authentication.devices.2fa_from_webauthn", device_name: identifier) %>

    +

    <%= t("two_factor_authentication.devices.2fa_from_webauthn_html", device_name: identifier) %>


    <%= hidden_field_tag "webauthn_credential", nil, "data-two-factor-authentication-target": "webauthnCredential" %>
    diff --git a/modules/two_factor_authentication/app/views/users/_two_factor_authentication_self.html.erb b/modules/two_factor_authentication/app/views/users/_two_factor_authentication_self.html.erb index b16dbd53cf2..67ebfffe800 100644 --- a/modules/two_factor_authentication/app/views/users/_two_factor_authentication_self.html.erb +++ b/modules/two_factor_authentication/app/views/users/_two_factor_authentication_self.html.erb @@ -6,8 +6,8 @@

    - <% self_link = link_to t("two_factor_authentication.admin.self_edit_link_name"), my_2fa_devices_path %> - <%= t("two_factor_authentication.admin.self_edit_path", self_edit_link: self_link).html_safe %> + <%= link_translate("two_factor_authentication.admin.self_edit_path", + links: { self_edit_link: my_2fa_devices_path }) %>

    diff --git a/modules/two_factor_authentication/config/locales/en.yml b/modules/two_factor_authentication/config/locales/en.yml index 1110c45f4f5..b27585bd76b 100644 --- a/modules/two_factor_authentication/config/locales/en.yml +++ b/modules/two_factor_authentication/config/locales/en.yml @@ -75,8 +75,7 @@ en: error_invalid_settings: "The 2FA strategies you selected are invalid" failed_to_save_settings: "Failed to update 2FA settings: %{message}" admin: - self_edit_path: "To add or modify your own 2FA devices, please go to %{self_edit_link}" - self_edit_link_name: "Two-factor authentication on your account page" + self_edit_path: "To add or modify your own 2FA devices, please go to the [Two-factor authentication on your account page](self_edit_link)" self_edit_forbidden: "You may not edit your own 2FA devices on this path. Go to My Account > Two factor authentication instead." no_devices_for_user: "No 2FA device has been registered for this user." all_devices_deleted: "All 2FA devices of this user have been deleted" @@ -124,8 +123,8 @@ en: failed_to_delete: "Failed to delete 2FA device." is_default_cannot_delete: "The device is marked as default and cannot be deleted due to an active security policy. Mark another device as default before deleting." not_existing: "No 2FA device has been registered for your account." - 2fa_from_input: Please enter the code from your %{device_name} to verify your identity. - 2fa_from_webauthn: Please provide the WebAuthn device %{device_name}. If it is USB based make sure to plug it in and touch it. Then click the sign in button. + 2fa_from_input_html: Please enter the code from your %{device_name} to verify your identity. + 2fa_from_webauthn_html: Please provide the WebAuthn device %{device_name}. If it is USB based make sure to plug it in and touch it. Then click the sign in button. webauthn: title: "WebAuthn" description: Register a FIDO2 device (like YubiKey) or the secure enclave of your mobile device. diff --git a/modules/webhooks/app/views/webhooks/outgoing/admin/index.html.erb b/modules/webhooks/app/views/webhooks/outgoing/admin/index.html.erb index ba283113c9e..1079824d619 100644 --- a/modules/webhooks/app/views/webhooks/outgoing/admin/index.html.erb +++ b/modules/webhooks/app/views/webhooks/outgoing/admin/index.html.erb @@ -10,10 +10,10 @@ ) header.with_description do - t( + link_translate( "webhooks.outgoing.explanation.text", - link: link_to(t(:"webhooks.outgoing.explanation.link"), admin_settings_aggregation_path, target: "_blank", rel: "noopener") - ).html_safe + links: { aggregation_path: admin_settings_aggregation_path } + ) end end %> diff --git a/modules/webhooks/config/locales/en.yml b/modules/webhooks/config/locales/en.yml index 69ec1fd5c2a..fc9d7263f97 100644 --- a/modules/webhooks/config/locales/en.yml +++ b/modules/webhooks/config/locales/en.yml @@ -40,8 +40,7 @@ en: explanation: text: > Upon the occurrence of an event like the creation of a work package or an update on a project, OpenProject will send a POST request to the configured web endpoints. - Oftentimes, the event is sent after the %{link} has passed. - link: configured aggregation period + Oftentimes, the event is sent after the [configured aggregation period](aggregation_path) has passed. status: enabled: 'Webhook is enabled' disabled: 'Webhook is disabled' diff --git a/spec/features/auth/omniauth_spec.rb b/spec/features/auth/omniauth_spec.rb index 80a3c2e2b75..18d13165a57 100644 --- a/spec/features/auth/omniauth_spec.rb +++ b/spec/features/auth/omniauth_spec.rb @@ -62,12 +62,6 @@ RSpec.describe "Omniauth authentication" do OmniAuth.config.logger = @omniauth_logger end - ## - # Returns a given translation up until the first occurrence of a parameter (exclusive). - def translation_substring(translation) - translation.scan(/(^.*) %\{/).first.first - end - describe "existing user sign in" do it "redirects to back url" do visit account_lost_password_path @@ -121,7 +115,7 @@ RSpec.describe "Omniauth authentication" do visit signout_path expect(page).to have_content(I18n.t(:notice_logged_out)) - expect(page).to have_content translation_substring(I18n.t(:instructions_after_logout)) + expect(page).to have_content I18n.t(:instructions_after_logout) end it "sign-in after previous sign-out shows my page" do @@ -268,7 +262,7 @@ RSpec.describe "Omniauth authentication" do it_behaves_like "omniauth signin error" do let(:login_path) { signin_path } - let(:instructions) { translation_substring I18n.t(:instructions_after_error) } + let(:instructions) { I18n.t(:instructions_after_error) } end end end diff --git a/spec/helpers/error_message_helper_spec.rb b/spec/helpers/error_message_helper_spec.rb index 788a851e21b..42643d6ddba 100644 --- a/spec/helpers/error_message_helper_spec.rb +++ b/spec/helpers/error_message_helper_spec.rb @@ -42,7 +42,7 @@ RSpec.describe ErrorMessageHelper do Array(attributes) .flat_map { |attribute| errors.full_messages_for(attribute) } .map { CGI.escapeHTML it } - .join("
    ") + .join("
    ") end before do @@ -66,7 +66,7 @@ RSpec.describe ErrorMessageHelper do expect(description).to be_html_safe.and eq([ t("errors.header_invalid_fields", count: 1), join_escaped_errors(:title) - ].join("
    ")) + ].join("
    ")) end end @@ -83,7 +83,7 @@ RSpec.describe ErrorMessageHelper do expect(description).to be_html_safe.and eq([ t("errors.header_invalid_fields", count: 2), join_escaped_errors(%i[title author]) - ].join("
    ")) + ].join("
    ")) end end @@ -114,7 +114,7 @@ RSpec.describe ErrorMessageHelper do join_escaped_errors(:base), t("errors.header_additional_invalid_fields", count: 1), join_escaped_errors(:title) - ].join("
    ")) + ].join("
    ")) end end @@ -134,7 +134,7 @@ RSpec.describe ErrorMessageHelper do join_escaped_errors(:base), t("errors.header_additional_invalid_fields", count: 2), join_escaped_errors(%i[title author]) - ].join("
    ")) + ].join("
    ")) end end end diff --git a/spec/helpers/text_formatting_helper_spec.rb b/spec/helpers/text_formatting_helper_spec.rb index e3efd6d59e4..870798db830 100644 --- a/spec/helpers/text_formatting_helper_spec.rb +++ b/spec/helpers/text_formatting_helper_spec.rb @@ -108,7 +108,7 @@ RSpec.describe TextFormattingHelper do "Lorem ipsum dolor sit \namet, consetetur sadipscing elitr, sed diam nonumy eirmod\n tempor invidunt" end let(:text_html) do - "Lorem ipsum dolor sit
    amet, consetetur sadipscing elitr, sed diam nonumy eirmod
    tempor invidunt" + "Lorem ipsum dolor sit
    amet, consetetur sadipscing elitr, sed diam nonumy eirmod
    tempor invidunt" end it "replaces escaped line breaks with html line breaks and should be html_safe" do diff --git a/spec/lib/tabular_form_builder_spec.rb b/spec/lib/tabular_form_builder_spec.rb index 4096ee1a6b6..010ea5e3840 100644 --- a/spec/lib/tabular_form_builder_spec.rb +++ b/spec/lib/tabular_form_builder_spec.rb @@ -91,7 +91,9 @@ RSpec.describe TabularFormBuilder do end context "with a prefix" do - let(:options) { { title: "Name", prefix: %{Prefix} } } + let(:options) do + { title: "Name", prefix: ApplicationController.helpers.content_tag(:span, "Prefix", style: "color:red") } + end it "outputs elements" do expect(output).to be_html_eql(%{ @@ -121,7 +123,9 @@ RSpec.describe TabularFormBuilder do end context "with a suffix" do - let(:options) { { title: "Name", suffix: %{Suffix} } } + let(:options) do + { title: "Name", suffix: ApplicationController.helpers.content_tag(:span, "Suffix", style: "color:blue") } + end it "outputs elements" do expect(output).to be_html_eql(%{ @@ -147,8 +151,8 @@ RSpec.describe TabularFormBuilder do let(:options) do { title: "Name", - prefix: %{PREFIX}, - suffix: %{SUFFIX} + prefix: ApplicationController.helpers.content_tag(:span, "PREFIX", style: "color:yellow"), + suffix: ApplicationController.helpers.content_tag(:span, "SUFFIX", style: "color:green") } end