diff --git a/.github/workflows/brakeman-scan-core.yml b/.github/workflows/brakeman-scan-core.yml index 998d0bedbdf..3b6079a64cc 100644 --- a/.github/workflows/brakeman-scan-core.yml +++ b/.github/workflows/brakeman-scan-core.yml @@ -18,7 +18,7 @@ jobs: permissions: contents: read # for actions/checkout to fetch code security-events: write # for github/codeql-action/upload-sarif to upload SARIF results - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' name: Brakeman Scan runs-on: ubuntu-latest env: diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml index f59458f4394..ab97db75464 100644 --- a/.github/workflows/continuous-delivery.yml +++ b/.github/workflows/continuous-delivery.yml @@ -12,7 +12,7 @@ jobs: trigger_downstream_workflow: permissions: contents: none - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' runs-on: ubuntu-latest steps: - name: Trigger Flavours workflow diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 31bbd3061d6..0c52141f129 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -11,7 +11,7 @@ on: jobs: danger: - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' runs-on: [ubuntu-latest] timeout-minutes: 10 steps: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b837e4a68fd..403adfee4d7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -116,7 +116,7 @@ jobs: docker_tags: ${{ steps.extract_version.outputs.docker_tags }} registry_image: ${{ steps.extract_version.outputs.registry_image }} build: - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' needs: - setup runs-on: diff --git a/.github/workflows/downstream-ci.yml b/.github/workflows/downstream-ci.yml index 223c7a3b414..69eff29affb 100644 --- a/.github/workflows/downstream-ci.yml +++ b/.github/workflows/downstream-ci.yml @@ -25,7 +25,7 @@ jobs: trigger_saas_tests: permissions: contents: none - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' name: SaaS tests runs-on: ubuntu-latest steps: diff --git a/.github/workflows/i18n-tasks.yml b/.github/workflows/i18n-tasks.yml index 07dcacf8cb2..99550db20b8 100644 --- a/.github/workflows/i18n-tasks.yml +++ b/.github/workflows/i18n-tasks.yml @@ -23,7 +23,7 @@ jobs: i18n-tasks: permissions: contents: read - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' name: I18n inconsistency check runs-on: ubuntu-latest steps: diff --git a/.github/workflows/openapi.yaml b/.github/workflows/openapi.yaml index 387f37ee40d..bd2218b39d8 100644 --- a/.github/workflows/openapi.yaml +++ b/.github/workflows/openapi.yaml @@ -15,7 +15,7 @@ on: jobs: api-spec: name: APIv3 specification (OpenAPI 3.0) - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' runs-on: [ubuntu-latest] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/packager.yml b/.github/workflows/packager.yml index 1ecdfccec05..a58cedcb2aa 100644 --- a/.github/workflows/packager.yml +++ b/.github/workflows/packager.yml @@ -1,5 +1,5 @@ name: Package -on: +on: push: branches: - packaging/* @@ -11,7 +11,7 @@ on: jobs: build: - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' name: ${{ matrix.target }} runs-on: ubuntu-latest services: diff --git a/.github/workflows/pullpreview.yml b/.github/workflows/pullpreview.yml index 010d595be18..70c9318e0ef 100644 --- a/.github/workflows/pullpreview.yml +++ b/.github/workflows/pullpreview.yml @@ -20,7 +20,7 @@ jobs: pull-requests: write # to remove labels statuses: write # to create commit status - if: github.repository == 'opf/openproject' && ( github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') ) + if: github.repository_owner == 'opf' && ( github.event_name == 'schedule' || github.event.label.name == 'pullpreview' || contains(github.event.pull_request.labels.*.name, 'pullpreview') ) runs-on: ubuntu-latest timeout-minutes: 60 steps: diff --git a/.github/workflows/test-core.yml b/.github/workflows/test-core.yml index 6b8bf82295f..fb9a5cf3d5c 100644 --- a/.github/workflows/test-core.yml +++ b/.github/workflows/test-core.yml @@ -24,7 +24,7 @@ permissions: jobs: all: name: Units + Features - if: github.repository == 'opf/openproject' + if: github.repository_owner == 'opf' runs-on: labels: "runs-on=${{ github.run_id }}/image=ubuntu24-full-x64/family=m7+c7+r7+i7+r8/ram=128+256/cpu=32" timeout-minutes: 40 diff --git a/app/components/projects/wizard/page_component.sass b/app/components/projects/wizard/page_component.sass index ed745052600..59352545cc9 100644 --- a/app/components/projects/wizard/page_component.sass +++ b/app/components/projects/wizard/page_component.sass @@ -59,3 +59,11 @@ @media screen and (max-width: $breakpoint-md) grid-template-columns: 170px 1fr + + @media screen and (max-width: $breakpoint-sm) + grid-template-areas: "main" + grid-template-columns: 1fr + + &--sidebar, + &--help + display: none diff --git a/app/seeders/standard.yml b/app/seeders/standard.yml index 9e2ca11be2f..3922e8bacde 100644 --- a/app/seeders/standard.yml +++ b/app/seeders/standard.yml @@ -91,6 +91,7 @@ projects: - board_view - team_planner_view - meetings + - documents news: - t_title: Welcome to your demo project t_summary: | @@ -389,6 +390,7 @@ projects: - work_package_tracking - gantt - board_view + - documents news: - t_title: Welcome to your Scrum demo project t_summary: | diff --git a/config/constants/settings/definition.rb b/config/constants/settings/definition.rb index b0253672993..ae7fb0a9d91 100644 --- a/config/constants/settings/definition.rb +++ b/config/constants/settings/definition.rb @@ -346,7 +346,14 @@ module Settings allowed: -> { Redmine::I18n.all_languages } }, default_projects_modules: { - default: %w[calendar board_view work_package_tracking gantt news costs wiki], + default: -> { + base_modules = %w[calendar board_view work_package_tracking gantt news costs wiki] + if Setting.real_time_text_collaboration_enabled? + base_modules + %w[documents] + else + base_modules + end + }, allowed: -> { OpenProject::AccessControl.available_project_modules.map(&:to_s) } }, default_projects_public: { @@ -575,7 +582,10 @@ module Settings }, real_time_text_collaboration_enabled: { description: "Enable real-time collaborative editing of text fields using BlockNoteJS and Hocuspocus server.", - default: true + default: -> { + Setting.collaborative_editing_hocuspocus_url.present? && + Setting.collaborative_editing_hocuspocus_secret.present? + } }, collaborative_editing_hocuspocus_url: { format: :string, diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index 5d3daefdaf2..7d86bd53802 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -145,14 +145,13 @@ Rails.application.config.action_controller.raise_on_open_redirects = true # https://guides.rubyonrails.org/configuring.html#config-action-dispatch-default-headers # Change the default headers to disable browsers' flawed legacy XSS protection. -# Rails.application.config.action_dispatch.default_headers = { -# "X-Frame-Options" => "SAMEORIGIN", -# "X-XSS-Protection" => "0", -# "X-Content-Type-Options" => "nosniff", -# "X-Download-Options" => "noopen", -# "X-Permitted-Cross-Domain-Policies" => "none", -# "Referrer-Policy" => "strict-origin-when-cross-origin" -# } +Rails.application.config.action_dispatch.default_headers = { + "X-Frame-Options" => "SAMEORIGIN", + "X-Content-Type-Options" => "nosniff", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" +} # https://guides.rubyonrails.org/configuring.html#config-active-support-cache-format-version # ** Please read carefully, this must be configured in config/application.rb ** diff --git a/db/migrate/20260106151226_add_documents_to_default_projects_modules.rb b/db/migrate/20260106151226_add_documents_to_default_projects_modules.rb new file mode 100644 index 00000000000..a6eaf0a2629 --- /dev/null +++ b/db/migrate/20260106151226_add_documents_to_default_projects_modules.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +#-- 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. +#++ + +class AddDocumentsToDefaultProjectsModules < ActiveRecord::Migration[8.0] + def up + return unless Setting.exists?(:real_time_text_collaboration_enabled) + return unless Setting.real_time_text_collaboration_enabled? + + # Only update if setting exists in DB (avoid updating on new installations - seeder handles that) + setting = Setting.find_by(name: "default_projects_modules") + return unless setting + + current_modules = setting.value || [] + return if current_modules.include?("documents") + + Setting.default_projects_modules = current_modules + ["documents"] + end + + def down + # No-op + end +end diff --git a/docs/release-notes/16-6-2/README.md b/docs/release-notes/16-6-2/README.md index e3a6479f65b..d7a31b9a398 100644 --- a/docs/release-notes/16-6-2/README.md +++ b/docs/release-notes/16-6-2/README.md @@ -11,9 +11,43 @@ release_date: 2025-12-02 Release date: 2025-12-02 We released OpenProject [OpenProject 16.6.2](https://community.openproject.org/versions/2243). -The release contains several bug fixes and we recommend updating to the newest version. +The release contains security relevant bug fixes and we strongly urge updating to the newest version. Below you will find a complete list of all changes and bug fixes. +The reported vulnerabilities have been reported as part of a Pentest by [Mantodea Security GmbH](https://mantodeasecurity.de/). +Thank you for your cooperation and responsible disclosure of the vulnerabilities + +### CVE-2026-22601 - Code Execution in E-Mail function + +For OpenProject version 16.6.1 and below, a registered administrator can execute arbitrary command by configuring sendmail binary path and sending a test email. + +This vulnerability was assigned to the CVE CVE-2026-22601. +For more information, please see the [GitHub Advisory GHSA-9vrv-7h26-c7jc)](https://github.com/opf/openproject/security/advisories/GHSA-9vrv-7h26-c7jc). + +### CVE-2026-22602 - User Enumeration via User ID + +A low‑privileged logged-in user can view the full names of other users. The full name corresponding to any arbitrary user ID can be retrieved via the following URL, even if the requesting account has only minimal permissions: + +This vulnerability was assigned to the CVE CVE-2026-22602. +For more information, please see the [GitHub Advisory GHSA-7fvx-9h6h-g82j](https://github.com/opf/openproject/security/advisories/GHSA-7fvx-9h6h-g82j). + + +### CVE-2026-22603 - No protection against brute-force attacks in the Change Password function + +OpenProject’s unauthenticated password-change endpoint (/account/change_password) was not protected by the same brute-force safeguards that apply to the normal login form. +In affected versions, an attacker who can guess or enumerate user IDs can send unlimited password-change requests for a given account without triggering lockout or other rate-limiting controls. + +This vulnerability was assigned to the CVE CVE-2026-22603. +For more information, please see the [GitHub Advisory GHSA-93x5-prx9-x239](https://github.com/opf/openproject/security/advisories/GHSA-93x5-prx9-x239). + +### CVE-2026-22604 - User enumeration via the change password function + +When sending a POST request to the /account/change_password endpoint with an arbitrary User ID as the password_change_user_id parameter, the resulting error page would show the username for the requested user. Since this endpoint is intended to be called without being authenticated, this allows to enumerate the user names of all accounts registered in an OpenProject instance. + +This vulnerability was assigned to the CVE CVE-2026-22604. +For more information, please see the [GitHub Advisory GHSA-q7qp-p3vw-j2fh](https://github.com/opf/openproject/security/advisories/GHSA-q7qp-p3vw-j2fh). + + ## Bug fixes and changes diff --git a/docs/release-notes/16-6-3/README.md b/docs/release-notes/16-6-3/README.md index 419f815a408..4c224e06094 100644 --- a/docs/release-notes/16-6-3/README.md +++ b/docs/release-notes/16-6-3/README.md @@ -11,9 +11,18 @@ release_date: 2025-12-11 Release date: 2025-12-11 We released OpenProject [OpenProject 16.6.3](https://community.openproject.org/versions/2247). -The release contains several bug fixes and we recommend updating to the newest version. +The release contains security relevant bug fixes and we strongly urge updating to the newest version. Below you will find a complete list of all changes and bug fixes. +### CVE-2026-22605 - Insecure Direct Object Reference in Meetings + +OpenProject versions <= 16.6.2 allows users with the View Meetings permission on any project, to access meeting agenda and section titles, notes, and text outcomes of meetings that belonged to projects, the user does not have access to. Linked work packages to projects the user is not allowed to see, are not affected. + +This vulnerability was assigned to the CVE CVE-2026-22605. +For more information, please see the [GitHub Advisory GHSA-fq4m-pxvm-8x2j](https://github.com/opf/openproject/security/advisories/GHSA-fq4m-pxvm-8x2j). + +This vulnerability was reported as part of the [YesWeHack.com OpenProject Bug Bounty program](https://yeswehack.com/programs/openproject), sponsored by the European Commission. + ## Bug fixes and changes diff --git a/docs/release-notes/16-6-4/README.md b/docs/release-notes/16-6-4/README.md new file mode 100644 index 00000000000..e748d492e6a --- /dev/null +++ b/docs/release-notes/16-6-4/README.md @@ -0,0 +1,37 @@ +--- +title: OpenProject 16.6.4 +sidebar_navigation: + title: 16.6.4 +release_version: 16.6.4 +release_date: 2026-01-08 +--- + +# OpenProject 16.6.4 + +Release date: 2026-01-08 + +We released OpenProject [OpenProject 16.6.4](https://community.openproject.org/versions/2248). + +The release contains security relevant bug fixes and we strongly urge updating to the newest version. +Below you will find a complete list of all changes and bug fixes. + +### CVE-2026-22600 - Arbitrary File Read via ImageMagick SVG Coder + +A Local File Read (LFR) vulnerability exists in the work package PDF export functionality of OpenProject < 16.6.4 . By uploading a specially crafted SVG file (disguised as a PNG) as a work package attachment, an attacker can exploit the backend image processing engine (ImageMagick). When the work package is exported to PDF, the backend attempts to resize the image, triggering the ImageMagick text: coder. This allows an attacker to read arbitrary local files that the application user has permissions to access (e.g., /etc/passwd, all project configuration files, private project data, etc.) + +This vulnerability was assigned to the CVE CVE-2026-22605. +For more information, please see the [GitHub Advisory GHSA-m8f2-cwpq-vvhh)](https://github.com/opf/openproject/security/advisories/GHSA-m8f2-cwpq-vvhh). + +The vulnerability has been responsibly disclosed through the [YesWeHack bounty program for OpenProject](https://yeswehack.com/programs/openproject) by user [syndrome_imposter](https://yeswehack.com/hunters/syndrome-impostor). This bug bounty program is being sponsored by the European Commission. + + + +## Bug fixes and changes + + + + +- Bugfix: SVG attachments are interpreted as PNG \[[#70349](https://community.openproject.org/wp/70349)\] + + + diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md index 8b7c4fc474a..ffec0255e2e 100644 --- a/docs/release-notes/README.md +++ b/docs/release-notes/README.md @@ -13,6 +13,13 @@ Stay up to date and get an overview of the new features included in the releases +## 16.6.4 + +Release date: 2026-01-08 + +[Release Notes](16-6-4/) + + ## 16.6.3 Release date: 2025-12-11 diff --git a/frontend/src/global_styles/content/_widget_box.sass b/frontend/src/global_styles/content/_widget_box.sass index 9b4c03dd49c..a0200155831 100644 --- a/frontend/src/global_styles/content/_widget_box.sass +++ b/frontend/src/global_styles/content/_widget_box.sass @@ -39,6 +39,7 @@ $widget-box--enumeration-width: 20px .widget-box flex: 1 flex-basis: 32% + max-height: 750px // Avoid that individual widgets blow the whole grid display: flex flex-direction: column diff --git a/lib/api/helpers/attachment_renderer.rb b/lib/api/helpers/attachment_renderer.rb index 0c75202e3bd..74a7f9e32b2 100644 --- a/lib/api/helpers/attachment_renderer.rb +++ b/lib/api/helpers/attachment_renderer.rb @@ -92,6 +92,9 @@ module API content_type attachment_content_type(attachment) header["Content-Disposition"] = attachment.content_disposition + # Ensure we set nosniff on attachments served from our app + # so that browsers do not reinterpret the content + header["X-Content-Type-Options"] = "nosniff" env["api.format"] = :binary sendfile attachment.diskfile.path end diff --git a/modules/bim/app/seeders/bim.yml b/modules/bim/app/seeders/bim.yml index b26e1f2c4cf..648ffa197b5 100644 --- a/modules/bim/app/seeders/bim.yml +++ b/modules/bim/app/seeders/bim.yml @@ -247,6 +247,7 @@ projects: - wiki - board_view - team_planner_view + - documents news: - t_title: Welcome to your demo project t_summary: | @@ -395,6 +396,7 @@ projects: - wiki - board_view - team_planner_view + - documents news: - t_title: Welcome to your demo project t_summary: | diff --git a/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb b/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb index 2803933a8fa..832a5f43a47 100644 --- a/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb +++ b/modules/bim/spec/seeders/root_seeder_bim_edition_spec.rb @@ -53,7 +53,7 @@ RSpec.describe RootSeeder, it "creates the BIM demo data" do expect(Project.count).to eq 4 - expect(EnabledModule.count).to eq 27 + expect(EnabledModule.count).to eq 29 expect(WorkPackage.count).to eq 76 expect(Wiki.count).to eq 3 expect(Query.count).to eq 29 diff --git a/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.html.erb b/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.html.erb index 36c491852f3..a58251ba0d2 100644 --- a/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.html.erb +++ b/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.html.erb @@ -42,11 +42,11 @@ concat( flex_layout do |flex_container| flex_container.with_column do - render Primer::Beta::AvatarStack.new( + render Primer::OpenProject::AvatarStack.new( data: { "documents--live-events-target": "users" } ) do |component| users.map do |user| - component.with_avatar(src: avatar_url(user), alt: user.name) + component.with_avatar_with_fallback(**avatar_options_for(user)) end end end @@ -67,14 +67,12 @@ component.with_body(caret: :top_left) do flex_layout do |container| users.each do |user| - container.with_row do - render Users::AvatarComponent.new( - user: user, - show_name: true, - link: false, - hover_card: { active: false }, - size: :mini - ) + container.with_column do + render Primer::OpenProject::AvatarWithFallback.new(**avatar_options_for(user)) + end + + container.with_column(ml: 2) do + render(Primer::Beta::Text.new(color: :subtle)) { user.name } end end end diff --git a/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.rb b/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.rb index a66fd82e442..bf8533589b4 100644 --- a/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.rb +++ b/modules/documents/app/components/documents/show_edit_view/page_header/live_users_component.rb @@ -49,6 +49,14 @@ module Documents def active_editors I18n.t("documents.active_editors_count", count: users.count).html_safe end + + def avatar_options_for(user) + { + src: avatar_url(user), + alt: user.name, + unique_id: user.id + } + end end end end diff --git a/modules/documents/spec/controllers/documents_controller_spec.rb b/modules/documents/spec/controllers/documents_controller_spec.rb index 485d911a762..ba1ee55edcd 100644 --- a/modules/documents/spec/controllers/documents_controller_spec.rb +++ b/modules/documents/spec/controllers/documents_controller_spec.rb @@ -187,7 +187,10 @@ RSpec.describe DocumentsController do end describe "generate_oauth_token", - with_config: { collaborative_editing_hocuspocus_secret: "secret1234" } do + with_config: { + collaborative_editing_hocuspocus_url: "wss://hocuspocus.local", + collaborative_editing_hocuspocus_secret: "secret1234" + } do let(:manage_role) { create(:project_role, permissions: %i[view_documents manage_documents]) } let(:view_only_role) { create(:project_role, permissions: [:view_documents]) } let(:user_with_manage) { create(:user) } diff --git a/modules/documents/spec/features/attachment_upload_spec.rb b/modules/documents/spec/features/attachment_upload_spec.rb index a31a4b9898e..9c3298987ac 100644 --- a/modules/documents/spec/features/attachment_upload_spec.rb +++ b/modules/documents/spec/features/attachment_upload_spec.rb @@ -214,7 +214,7 @@ RSpec.describe "Upload attachment to documents", end end - context "for collaborative documents" do + context "for collaborative documents", with_settings: { real_time_text_collaboration_enabled: true } do let(:document) { create(:document, project:) } let(:editor) { FormFields::Primerized::BlockNoteEditorInput.new } let(:attachments_list) { Components::AttachmentsList.new } diff --git a/modules/documents/spec/features/block_note_editor_spec.rb b/modules/documents/spec/features/block_note_editor_spec.rb index a284f6f43d3..c99eb5d198c 100644 --- a/modules/documents/spec/features/block_note_editor_spec.rb +++ b/modules/documents/spec/features/block_note_editor_spec.rb @@ -30,7 +30,7 @@ require "rails_helper" -RSpec.describe "BlockNote editor rendering", :js do +RSpec.describe "BlockNote editor rendering", :js, with_settings: { real_time_text_collaboration_enabled: true } do let(:admin) { create(:admin) } let(:type) { create(:document_type, :experimental) } let(:document) { create(:document, type:) } diff --git a/modules/documents/spec/features/documents/admin/settings/document_collaboration_settings_spec.rb b/modules/documents/spec/features/documents/admin/settings/document_collaboration_settings_spec.rb index b7d93dc1524..ee274a274d0 100644 --- a/modules/documents/spec/features/documents/admin/settings/document_collaboration_settings_spec.rb +++ b/modules/documents/spec/features/documents/admin/settings/document_collaboration_settings_spec.rb @@ -30,7 +30,9 @@ require "spec_helper" -RSpec.describe "Document collaboration settings admin", :js, :settings_reset do +RSpec.describe "Document collaboration settings admin", + :js, + :settings_reset do include Flash::Expectations current_user { create(:admin) } @@ -39,6 +41,16 @@ RSpec.describe "Document collaboration settings admin", :js, :settings_reset do it "can configure hocuspocus url and secret" do visit admin_settings_document_collaboration_settings_path + within_test_selector("collaboration-settings-disabled-notice") do + expect(page).to have_heading("Real-time collaboration is not enabled") + expect(page).to have_content("Once enabled, multiple users will be able to work together on a " \ + "document at the same time. All new documents will be based on a new " \ + "editor (BlockNote) and will require a working connection to a Hocuspocus server.") + click_on "Enable real-time collaboration" + end + + expect_and_dismiss_flash(message: "Real-time collaboration has been enabled.") + expect(page).to have_field("Hocuspocus server URL", with: "") expect(page).to have_field("Client secret", with: "") @@ -72,22 +84,12 @@ RSpec.describe "Document collaboration settings admin", :js, :settings_reset do end expect_and_dismiss_flash(message: "Real-time collaboration has been disabled.") - - within_test_selector("collaboration-settings-disabled-notice") do - expect(page).to have_heading("Real-time collaboration is not enabled") - expect(page).to have_content("Once enabled, multiple users will be able to work together on a " \ - "document at the same time. All new documents will be based on a new " \ - "editor (BlockNote) and will require a working connection to a Hocuspocus server.") - click_on "Enable real-time collaboration" - end - - expect_and_dismiss_flash(message: "Real-time collaboration has been enabled.") - expect(Setting.real_time_text_collaboration_enabled?).to be(true) end end context "with hocuspocus url set via environment variable", - with_env: { "OPENPROJECT_COLLABORATIVE_EDITING_HOCUSPOCUS_URL" => "wss://env-hocuspocus.example.com" } do + with_env: { "OPENPROJECT_COLLABORATIVE_EDITING_HOCUSPOCUS_URL" => "wss://env-hocuspocus.example.com" }, + with_settings: { collaborative_editing_hocuspocus_secret: "secret1234" } do before do reset(:collaborative_editing_hocuspocus_url) visit admin_settings_document_collaboration_settings_path @@ -99,11 +101,16 @@ RSpec.describe "Document collaboration settings admin", :js, :settings_reset do expect(page).to have_field("Hocuspocus server URL", with: "wss://env-hocuspocus.example.com", disabled: true) + + expect(page).to have_field("Client secret", + with: "", + disabled: false) end end context "with secret set via environment variable", - with_env: { "OPENPROJECT_COLLABORATIVE_EDITING_HOCUSPOCUS_SECRET" => "envsupersecret" } do + with_env: { "OPENPROJECT_COLLABORATIVE_EDITING_HOCUSPOCUS_SECRET" => "envsupersecret" }, + with_settings: { collaborative_editing_hocuspocus_url: "wss://env-hocuspocus.example.com" } do before do reset(:collaborative_editing_hocuspocus_secret) visit admin_settings_document_collaboration_settings_path @@ -112,6 +119,9 @@ RSpec.describe "Document collaboration settings admin", :js, :settings_reset do it "shows the secret as read-only" do expect(page).to have_content("Some values are configured via environment variables and cannot be edited here.") + expect(page).to have_field("Hocuspocus server URL", + with: "wss://env-hocuspocus.example.com", + disabled: false) expect(page).to have_field("Client secret", with: "", disabled: true) diff --git a/modules/documents/spec/features/documents/project/create_document_spec.rb b/modules/documents/spec/features/documents/project/create_document_spec.rb index 47a8518852f..f7e4c1086aa 100644 --- a/modules/documents/spec/features/documents/project/create_document_spec.rb +++ b/modules/documents/spec/features/documents/project/create_document_spec.rb @@ -47,7 +47,7 @@ RSpec.describe "Create Document", current_user { manager } - context "for collaborative documents" do + context "for collaborative documents", with_settings: { real_time_text_collaboration_enabled: true } do it "creates a new document via +Document buttons" do index_page.visit! diff --git a/modules/documents/spec/features/documents/project/show_edit_document_spec.rb b/modules/documents/spec/features/documents/project/show_edit_document_spec.rb index c29a83732eb..d0a1fa8870f 100644 --- a/modules/documents/spec/features/documents/project/show_edit_document_spec.rb +++ b/modules/documents/spec/features/documents/project/show_edit_document_spec.rb @@ -52,7 +52,8 @@ RSpec.describe "Show/Edit Document View", # rubocop:enable RSpec/AnyInstance end - it "renders a collaborative document" do + it "renders a collaborative document", + with_settings: { real_time_text_collaboration_enabled: true } do visit document_path(document) expect(page).to have_content("Collaborative document") diff --git a/modules/meeting/app/controllers/meetings_controller.rb b/modules/meeting/app/controllers/meetings_controller.rb index 48823ad6a45..19a6a074bab 100644 --- a/modules/meeting/app/controllers/meetings_controller.rb +++ b/modules/meeting/app/controllers/meetings_controller.rb @@ -460,7 +460,8 @@ class MeetingsController < ApplicationController @meeting = scope .visible - .includes([:project, :author, { participants: :user }, :sections, { agenda_items: :outcomes }]) + .includes([:project, :author, { participants: :user }, { agenda_items: :outcomes }]) + .preload(:sections) .find(params[:id]) end diff --git a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb index 9eda5e35bd2..57d995fe841 100644 --- a/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb +++ b/modules/meeting/spec/features/structured_meetings/structured_meeting_crud_spec.rb @@ -513,5 +513,28 @@ RSpec.describe "Meetings CRUD", show_page.expect_blankslate end end + + it "maintains section order when rendering" do + section1 = create(:meeting_section, meeting:, title: "Section A") + section2 = create(:meeting_section, meeting:, title: "Section B") + section3 = create(:meeting_section, meeting:, title: "Section C") + + show_page.visit! + + expect(show_page.section_headers) + .to eq(["Section A", "Section B", "Section C"]) + + show_page.select_section_action(section3, "Move up") + + wait_for { [section1, section2, section3].map { |s| s.reload.position } } + .to eq([1, 3, 2]) + wait_for { show_page.section_headers } + .to eq(["Section A", "Section C", "Section B"]) + + show_page.reload! + + expect(show_page.section_headers) + .to eq(["Section A", "Section C", "Section B"]) + end end end diff --git a/modules/meeting/spec/support/pages/meetings/show.rb b/modules/meeting/spec/support/pages/meetings/show.rb index 9077b7c3254..4c58a7f454d 100644 --- a/modules/meeting/spec/support/pages/meetings/show.rb +++ b/modules/meeting/spec/support/pages/meetings/show.rb @@ -780,5 +780,10 @@ module Pages::Meetings element["data-reference-value"] != old_reference_value end end + + def section_headers + page.all(".op-meeting-section-container[data-test-selector^='meeting-section-header-container-']") + .map(&:text) + end end end diff --git a/publiccode.yml b/publiccode.yml index bbef0e39600..23b501bbb84 100644 --- a/publiccode.yml +++ b/publiccode.yml @@ -7,8 +7,8 @@ name: OpenProject applicationSuite: openDesk url: 'https://github.com/opf/openproject' roadmap: 'https://www.openproject.org/roadmap' -releaseDate: '2025-12-11' -softwareVersion: '16.6.3' +releaseDate: '2026-01-08' +softwareVersion: '16.6.4' developmentStatus: stable softwareType: standalone/web logo: 'publiccode_logo.svg' diff --git a/spec/constants/settings/definition_spec.rb b/spec/constants/settings/definition_spec.rb index 4447e9f37a0..262dfa95d2f 100644 --- a/spec/constants/settings/definition_spec.rb +++ b/spec/constants/settings/definition_spec.rb @@ -38,7 +38,7 @@ RSpec.describe Settings::Definition, :settings_reset do described_class.add_all - expect(described_class.all.keys).to eq(described_class::DEFINITIONS.keys) + expect(described_class.all.keys).to match_array(described_class::DEFINITIONS.keys) end it "does not add any plugin/feature settings if they were removed for some reason" do diff --git a/spec/migrations/add_documents_to_default_projects_modules_spec.rb b/spec/migrations/add_documents_to_default_projects_modules_spec.rb new file mode 100644 index 00000000000..14e7bf64b8d --- /dev/null +++ b/spec/migrations/add_documents_to_default_projects_modules_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +#-- 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. +#++ + +require "spec_helper" +require Rails.root.join("db/migrate/20260106151226_add_documents_to_default_projects_modules") + +RSpec.describe AddDocumentsToDefaultProjectsModules, type: :model do + let(:base_modules) { %w[calendar board_view work_package_tracking gantt news costs wiki] } + + before do + # Ensure a clean state + Setting.find_by(name: "default_projects_modules")&.destroy + Setting.clear_cache + end + + context "when real_time_text_collaboration is enabled", + with_settings: { real_time_text_collaboration_enabled: true } do + context "when default_projects_modules setting exists in DB" do + before do + Setting.default_projects_modules = base_modules + end + + it "adds documents to the default modules" do + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + Setting.clear_cache + expect(Setting.default_projects_modules).to include("documents") + end + + it "preserves existing modules" do + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + Setting.clear_cache + expect(Setting.default_projects_modules).to include(*base_modules) + end + + context "when documents is already in the default modules" do + before do + Setting.default_projects_modules = base_modules + ["documents"] + end + + it "does not duplicate documents" do + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + Setting.clear_cache + expect(Setting.default_projects_modules.count("documents")).to eq(1) + end + end + end + + context "when default_projects_modules setting does not exist in DB" do + it "does not create the setting (seeder handles new installations)" do + expect(Setting.find_by(name: "default_projects_modules")).to be_nil + + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + # Setting should still not exist - seeder will handle it + expect(Setting.find_by(name: "default_projects_modules")).to be_nil + end + end + end + + context "when real_time_text_collaboration is disabled", + with_settings: { real_time_text_collaboration_enabled: false } do + before do + Setting.default_projects_modules = base_modules + end + + it "does not modify the default modules" do + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + Setting.clear_cache + expect(Setting.default_projects_modules).not_to include("documents") + expect(Setting.default_projects_modules).to match_array(base_modules) + end + end + + context "when real_time_text_collaboration_enabled setting does not exist" do + before do + Setting.default_projects_modules = base_modules + allow(Setting).to receive(:exists?).and_call_original + allow(Setting).to receive(:exists?).with(:real_time_text_collaboration_enabled).and_return(false) + end + + it "does not modify the default modules" do + ActiveRecord::Migration.suppress_messages { described_class.migrate(:up) } + + Setting.clear_cache + expect(Setting.default_projects_modules).not_to include("documents") + expect(Setting.default_projects_modules).to match_array(base_modules) + end + end +end diff --git a/spec/models/setting_spec.rb b/spec/models/setting_spec.rb index c8ffa4ac183..48e35f0333c 100644 --- a/spec/models/setting_spec.rb +++ b/spec/models/setting_spec.rb @@ -603,4 +603,31 @@ RSpec.describe Setting do end end end + + describe "default_projects_modules conditional default" do + shared_examples "base modules unchanged" do + it "includes the base modules" do + base_modules = %w[calendar board_view work_package_tracking gantt news costs wiki] + expect(Settings::Definition[:default_projects_modules].default).to include(*base_modules) + end + end + + context "when real_time_text_collaboration is enabled", + with_settings: { real_time_text_collaboration_enabled: true } do + it "includes documents in the default modules" do + expect(Settings::Definition[:default_projects_modules].default).to include("documents") + end + + it_behaves_like "base modules unchanged" + end + + context "when real_time_text_collaboration is disabled", + with_settings: { real_time_text_collaboration_enabled: false } do + it "does not include documents in the default modules" do + expect(Settings::Definition[:default_projects_modules].default).not_to include("documents") + end + + it_behaves_like "base modules unchanged" + end + end end diff --git a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb index 5391a8a539a..5052404d970 100644 --- a/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb +++ b/spec/requests/api/v3/attachments/attachment_resource_shared_examples.rb @@ -469,6 +469,10 @@ RSpec.shared_examples "an APIv3 attachment resource", content_type: :json, type: expect(expires_time > Time.now.utc + max_age - 60).to be_truthy end + it "includes X-Content-Type-Options nosniff header to prevent content type sniffing" do + expect(subject.headers["X-Content-Type-Options"]).to eq "nosniff" + end + it "sends the file in binary" do expect(subject.body) .to match(mock_file.read) diff --git a/spec/requests/dynamic_content_security_policy_spec.rb b/spec/requests/dynamic_content_security_policy_spec.rb index ef6339340bf..1e8f13fa983 100644 --- a/spec/requests/dynamic_content_security_policy_spec.rb +++ b/spec/requests/dynamic_content_security_policy_spec.rb @@ -60,5 +60,12 @@ RSpec.describe "" do end end end + + it "includes X-Content-Type-Options nosniff header to prevent content type sniffing" do + get "/" + + expect(last_response).to have_http_status(200) + expect(last_response.headers["X-Content-Type-Options"]).to eq "nosniff" + end end end diff --git a/spec/seeders/root_seeder_standard_edition_spec.rb b/spec/seeders/root_seeder_standard_edition_spec.rb index d3e7bd0f3eb..326491fc278 100644 --- a/spec/seeders/root_seeder_standard_edition_spec.rb +++ b/spec/seeders/root_seeder_standard_edition_spec.rb @@ -53,7 +53,7 @@ RSpec.describe RootSeeder, it "creates the demo data" do # rubocop:disable RSpec/MultipleExpectations expect(Project.count).to eq 2 - expect(EnabledModule.count).to eq 13 + expect(EnabledModule.count).to eq 15 expect(WorkPackage.count).to eq 36 expect(Wiki.count).to eq 2 expect(Query.having_views.count).to eq 8