diff --git a/.github/workflows/brakeman-scan-core.yml b/.github/workflows/brakeman-scan-core.yml
index 1b585f33991b..3160d431daf2 100644
--- a/.github/workflows/brakeman-scan-core.yml
+++ b/.github/workflows/brakeman-scan-core.yml
@@ -29,11 +29,6 @@ jobs:
- name: Setup Ruby
uses: ruby/setup-ruby@v1
- with:
- # FIXME: remove the ruby version once '3.2.2' is released.
- # This is set to head to fix ruby segfaulting when brakeman is
- # used. See https://bugs.ruby-lang.org/issues/19433
- ruby-version: 'head'
- name: Setup Brakeman
run: |
diff --git a/.github/workflows/continuous-delivery.yml b/.github/workflows/continuous-delivery.yml
index c28326a5fb47..b8554bbbf4bc 100644
--- a/.github/workflows/continuous-delivery.yml
+++ b/.github/workflows/continuous-delivery.yml
@@ -19,9 +19,8 @@ jobs:
TOKEN: ${{ secrets.OPENPROJECT_CI_TOKEN }}
REPOSITORY: opf/openproject-flavours
WORKFLOW_ID: ci.yml
- CORE_REF: ${{ github.ref_name }}
run: |
curl -i --fail-with-body -H"authorization: Bearer $TOKEN" \
-XPOST -H"Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/$REPOSITORY/actions/workflows/$WORKFLOW_ID/dispatches \
- -d '{"ref": "dev", "inputs": { "ref" : "'$CORE_REF'" }}'
+ -d '{"ref": "dev", "inputs": { "ref" : "${{ github.ref_name }}" }}'
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
new file mode 100644
index 000000000000..8b30f3873e49
--- /dev/null
+++ b/.github/workflows/danger.yml
@@ -0,0 +1,26 @@
+name: migration-warning-on-release-branches
+
+on:
+ pull_request:
+ branches:
+ - release/*
+ paths:
+ - 'db/migrate/**.rb'
+ - 'modules/**/db/migrate/*.rb'
+
+jobs:
+ danger:
+ if: github.repository == 'opf/openproject'
+ runs-on: [ubuntu-latest]
+ timeout-minutes: 10
+ steps:
+ - uses: actions/checkout@v4
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.2.3'
+ - uses: MeilCli/danger-action@v5
+ with:
+ danger_file: 'Dangerfile'
+ danger_id: 'danger-pr'
+ env:
+ DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/Dangerfile b/Dangerfile
new file mode 100644
index 000000000000..3e464b81f625
--- /dev/null
+++ b/Dangerfile
@@ -0,0 +1,9 @@
+CORE_OR_MODULE_MIGRATIONS_REGEX = %r{(modules/.*)?db/migrate/.*\.rb}
+
+def added_or_modified_migrations?
+ (git.modified_files + git.added_files).grep(CORE_OR_MODULE_MIGRATIONS_REGEX)
+end
+
+if added_or_modified_migrations?
+ warn "This PR has migration-related changes on a release branch. Ping @opf/operations"
+end
diff --git a/Gemfile b/Gemfile
index 2c9db7c6e658..8af268ad236f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -382,4 +382,4 @@ end
gem 'openproject-octicons', '~>19.8.0'
gem 'openproject-octicons_helper', '~>19.8.0'
-gem 'openproject-primer_view_components', '~>0.20.0'
+gem 'openproject-primer_view_components', '~>0.22.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index 720531b1b0cd..5f00834bbea0 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -238,35 +238,35 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.0)
- actioncable (7.1.3)
- actionpack (= 7.1.3)
- activesupport (= 7.1.3)
+ actioncable (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.1.3)
- actionpack (= 7.1.3)
- activejob (= 7.1.3)
- activerecord (= 7.1.3)
- activestorage (= 7.1.3)
- activesupport (= 7.1.3)
+ actionmailbox (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ activejob (= 7.1.3.2)
+ activerecord (= 7.1.3.2)
+ activestorage (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
mail (>= 2.7.1)
net-imap
net-pop
net-smtp
- actionmailer (7.1.3)
- actionpack (= 7.1.3)
- actionview (= 7.1.3)
- activejob (= 7.1.3)
- activesupport (= 7.1.3)
+ actionmailer (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ actionview (= 7.1.3.2)
+ activejob (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
mail (~> 2.5, >= 2.5.4)
net-imap
net-pop
net-smtp
rails-dom-testing (~> 2.2)
- actionpack (7.1.3)
- actionview (= 7.1.3)
- activesupport (= 7.1.3)
+ actionpack (7.1.3.2)
+ actionview (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4)
@@ -277,31 +277,31 @@ GEM
actionpack-xml_parser (2.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
- actiontext (7.1.3)
- actionpack (= 7.1.3)
- activerecord (= 7.1.3)
- activestorage (= 7.1.3)
- activesupport (= 7.1.3)
+ actiontext (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ activerecord (= 7.1.3.2)
+ activestorage (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.1.3)
- activesupport (= 7.1.3)
+ actionview (7.1.3.2)
+ activesupport (= 7.1.3.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (7.1.3)
- activesupport (= 7.1.3)
+ activejob (7.1.3.2)
+ activesupport (= 7.1.3.2)
globalid (>= 0.3.6)
- activemodel (7.1.3)
- activesupport (= 7.1.3)
+ activemodel (7.1.3.2)
+ activesupport (= 7.1.3.2)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
- activerecord (7.1.3)
- activemodel (= 7.1.3)
- activesupport (= 7.1.3)
+ activerecord (7.1.3.2)
+ activemodel (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
timeout (>= 0.4.0)
activerecord-import (1.5.1)
activerecord (>= 4.2)
@@ -314,13 +314,13 @@ GEM
multi_json (~> 1.11, >= 1.11.2)
rack (>= 2.0.8, < 4)
railties (>= 6.1)
- activestorage (7.1.3)
- actionpack (= 7.1.3)
- activejob (= 7.1.3)
- activerecord (= 7.1.3)
- activesupport (= 7.1.3)
+ activestorage (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ activejob (= 7.1.3.2)
+ activerecord (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
marcel (~> 1.0)
- activesupport (7.1.3)
+ activesupport (7.1.3.2)
base64
bigdecimal
concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -756,7 +756,7 @@ GEM
actionview
openproject-octicons (= 19.8.0)
railties
- openproject-primer_view_components (0.20.0)
+ openproject-primer_view_components (0.22.2)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
openproject-octicons (>= 19.8.0)
@@ -824,7 +824,7 @@ GEM
puma (>= 5.0, < 7)
raabro (1.4.0)
racc (1.7.3)
- rack (2.2.8)
+ rack (2.2.8.1)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (6.7.0)
@@ -854,20 +854,20 @@ GEM
rackup (1.0.0)
rack (< 3)
webrick
- rails (7.1.3)
- actioncable (= 7.1.3)
- actionmailbox (= 7.1.3)
- actionmailer (= 7.1.3)
- actionpack (= 7.1.3)
- actiontext (= 7.1.3)
- actionview (= 7.1.3)
- activejob (= 7.1.3)
- activemodel (= 7.1.3)
- activerecord (= 7.1.3)
- activestorage (= 7.1.3)
- activesupport (= 7.1.3)
+ rails (7.1.3.2)
+ actioncable (= 7.1.3.2)
+ actionmailbox (= 7.1.3.2)
+ actionmailer (= 7.1.3.2)
+ actionpack (= 7.1.3.2)
+ actiontext (= 7.1.3.2)
+ actionview (= 7.1.3.2)
+ activejob (= 7.1.3.2)
+ activemodel (= 7.1.3.2)
+ activerecord (= 7.1.3.2)
+ activestorage (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
bundler (>= 1.15.0)
- railties (= 7.1.3)
+ railties (= 7.1.3.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -882,9 +882,9 @@ GEM
rails-i18n (7.0.8)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
- railties (7.1.3)
- actionpack (= 7.1.3)
- activesupport (= 7.1.3)
+ railties (7.1.3.2)
+ actionpack (= 7.1.3.2)
+ activesupport (= 7.1.3.2)
irb
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -1227,7 +1227,7 @@ DEPENDENCIES
openproject-octicons (~> 19.8.0)
openproject-octicons_helper (~> 19.8.0)
openproject-openid_connect!
- openproject-primer_view_components (~> 0.20.0)
+ openproject-primer_view_components (~> 0.22.2)
openproject-recaptcha!
openproject-reporting!
openproject-storages!
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 1f77a494fd07..564973057f70 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -220,7 +220,15 @@ def reset_i18n_fallbacks
end
def set_localization
- SetLocalizationService.new(User.current, request.env['HTTP_ACCEPT_LANGUAGE']).call
+ # 1. Use completely autheticated user
+ # 2. Use user with some authenticated stages not compelted.
+ # In this case user is not considered logged in, but identified.
+ # It covers localization for extra authentication stages(like :consent, for example)
+ # 3. Use anonymous instance.
+ user = RequestStore[:current_user] ||
+ (session[:authenticated_user_id].present? && User.find_by(id: session[:authenticated_user_id])) ||
+ User.anonymous
+ SetLocalizationService.new(user, request.env['HTTP_ACCEPT_LANGUAGE']).call
end
def deny_access(not_found: false)
diff --git a/app/controllers/concerns/accounts/authentication_stages.rb b/app/controllers/concerns/accounts/authentication_stages.rb
index 1ddb7bf5fbd4..c2524f58e16d 100644
--- a/app/controllers/concerns/accounts/authentication_stages.rb
+++ b/app/controllers/concerns/accounts/authentication_stages.rb
@@ -1,3 +1,31 @@
+#-- copyright
+# OpenProject is an open source project management software.
+# Copyright (C) 2012-2024 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.
+#++
+
module Accounts::AuthenticationStages
def successful_authentication(user, reset_stages: true, just_registered: false)
stages = authentication_stages after_activation: just_registered, reset: reset_stages
diff --git a/app/controllers/concerns/accounts/user_consent.rb b/app/controllers/concerns/accounts/user_consent.rb
index 74cf7f9d00ca..a2603dfae3ef 100644
--- a/app/controllers/concerns/accounts/user_consent.rb
+++ b/app/controllers/concerns/accounts/user_consent.rb
@@ -33,8 +33,8 @@ module Accounts::UserConsent
include ::UserConsentHelper
def consent
- if consent_required?
- render 'account/consent', locals: { consenting_user: }
+ if user_consent_required? && consenting_user&.consent_expired?
+ render 'account/consent'
else
consent_finished
end
@@ -50,14 +50,6 @@ def confirm_consent
end
end
- def consent_required?
- # Ensure consent is enabled and a text is provided
- return false unless user_consent_required?
-
- # Require the user to consent if he hasn't already
- consent_expired?
- end
-
def decline_consent
message = I18n.t('consent.decline_warning_message') + "\n"
message <<
@@ -71,19 +63,6 @@ def decline_consent
redirect_to authentication_stage_failure_path :consent
end
- def consent_expired?
- consented_at = consenting_user.try(:consented_at)
-
- # Always if the user has not consented
- return true if consented_at.blank?
-
- # Did not expire if no consent_time set, but user has consented at some point
- return false if Setting.consent_time.blank?
-
- # Otherwise, expires when consent_time is newer than last consented_at
- consented_at < Setting.consent_time
- end
-
def consenting_user
User.find_by id: session[:authenticated_user_id]
end
diff --git a/app/helpers/user_consent_helper.rb b/app/helpers/user_consent_helper.rb
index dbf945c51c1d..66d5a6dafbe8 100644
--- a/app/helpers/user_consent_helper.rb
+++ b/app/helpers/user_consent_helper.rb
@@ -37,16 +37,14 @@ def user_consent_required?
end
##
- # Gets consent instructions for the given user.
+ # Gets consent instructions.
#
- # @param user [User] The user to get instructions for.
# @param locale [String] ISO-639-1 code for the desired locale (e.g. de, en, fr).
# `I18n.locale` is set for each request individually depending
# among other things on the user's Accept-Language headers.
# @return [String] Instructions in the respective language.
- def user_consent_instructions(_user, locale: I18n.locale)
+ def user_consent_instructions(locale)
all = Setting.consent_info
-
all.fetch(locale.to_s) { all.values.first }
end
@@ -54,6 +52,8 @@ def consent_checkbox_label(locale: I18n.locale)
I18n.t('consent.checkbox_label', locale:)
end
+ private
+
def consent_configured?
if Setting.consent_info.count == 0
Rails.logger.error 'Instance is configured to require consent, but no consent_info has been set.'
diff --git a/app/models/role.rb b/app/models/role.rb
index f2c06f8a24d1..a30278a45b02 100644
--- a/app/models/role.rb
+++ b/app/models/role.rb
@@ -77,7 +77,16 @@ def copy_from_role(source_role)
inclusion: { in: ->(*) { Role.subclasses.map(&:to_s) } }
def self.givable
- where.not(builtin: [BUILTIN_NON_MEMBER, BUILTIN_ANONYMOUS])
+ where
+ .not(
+ builtin: [
+ Role::BUILTIN_NON_MEMBER,
+ Role::BUILTIN_ANONYMOUS,
+ Role::BUILTIN_WORK_PACKAGE_VIEWER,
+ Role::BUILTIN_WORK_PACKAGE_COMMENTER,
+ Role::BUILTIN_WORK_PACKAGE_EDITOR
+ ]
+ )
.order(Arel.sql('position'))
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 22e7c63f525a..806f40ebddcc 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -470,6 +470,17 @@ def anonymous?
!logged?
end
+ def consent_expired?
+ # Always if the user has not consented
+ return true if consented_at.blank?
+
+ # Did not expire if no consent_time set, but user has consented at some point
+ return false if Setting.consent_time.blank?
+
+ # Otherwise, expires when consent_time is newer than last consented_at
+ consented_at < Setting.consent_time
+ end
+
# Cheap version of Project.visible.count
def number_of_known_projects
if admin?
diff --git a/app/seeders/demo_data/global_query_seeder.rb b/app/seeders/demo_data/global_query_seeder.rb
index b6e535ab5942..4c918b2387f9 100644
--- a/app/seeders/demo_data/global_query_seeder.rb
+++ b/app/seeders/demo_data/global_query_seeder.rb
@@ -33,6 +33,10 @@ def seed_data!
end
end
+ def applicable?
+ Query.global.none?
+ end
+
private
def seed_global_queries
diff --git a/app/seeders/development_data/shared_work_packages_seeder.rb b/app/seeders/development_data/shared_work_packages_seeder.rb
index 6abe5062e5a7..5bfccbcc581d 100644
--- a/app/seeders/development_data/shared_work_packages_seeder.rb
+++ b/app/seeders/development_data/shared_work_packages_seeder.rb
@@ -81,8 +81,8 @@ def work_package_attributes
reference: :save_gotham,
description: "Gotham is in trouble. It's your job to save it!",
status: seed_data.find_reference(:default_status_new),
- type: seed_data.find_reference(:default_type_epic),
- priority: seed_data.find_reference(:default_priority_immediate)
+ type: seed_data.find_reference(:default_type_epic, :default_type_phase),
+ priority: seed_data.find_reference(:default_priority_immediate, :default_priority_high)
},
{
project:,
@@ -92,7 +92,7 @@ def work_package_attributes
description: 'Must be stopped before Gotham is doomed.',
status: seed_data.find_reference(:default_status_new),
type: seed_data.find_reference(:default_type_task),
- priority: seed_data.find_reference(:default_priority_immediate)
+ priority: seed_data.find_reference(:default_priority_immediate, :default_priority_high)
},
{
project:,
diff --git a/app/seeders/root_seeder.rb b/app/seeders/root_seeder.rb
index 0712ee730088..e95fb44f0a4c 100644
--- a/app/seeders/root_seeder.rb
+++ b/app/seeders/root_seeder.rb
@@ -29,10 +29,13 @@
# Seeds the minimum data required to run OpenProject (BasicDataSeeder, AdminUserSeeder)
# as well as optional demo data (DemoDataSeeder) to give a user some orientation.
class RootSeeder < Seeder
- def initialize(seed_development_data: Rails.env.development?)
+ attr_reader :raise_on_unknown_language
+
+ def initialize(seed_development_data: Rails.env.development?, raise_on_unknown_language: false)
super()
@seed_development_data = seed_development_data
+ @raise_on_unknown_language = raise_on_unknown_language
load_available_seeders
end
@@ -174,7 +177,14 @@ def seed_plugins_data
def desired_lang
desired_lang = ENV.fetch('OPENPROJECT_SEED_LOCALE', Setting.default_language)
- raise "Locale #{desired_lang} is not supported" if Redmine::I18n.all_languages.exclude?(desired_lang)
+
+ if Redmine::I18n.all_languages.exclude?(desired_lang)
+ if raise_on_unknown_language
+ raise "Locale #{desired_lang} is not supported"
+ else
+ desired_lang = :en
+ end
+ end
desired_lang
end
diff --git a/app/seeders/source/seed_data.rb b/app/seeders/source/seed_data.rb
index 61da39acbcd2..82540ce6a9fe 100644
--- a/app/seeders/source/seed_data.rb
+++ b/app/seeders/source/seed_data.rb
@@ -43,15 +43,25 @@ def store_reference(reference, record)
registry[reference] = record
end
- def find_reference(reference, default: :__unset__)
+ # Finds and returns the value associated with the given reference.
+ #
+ # @param reference [Symbol] The reference to search for.
+ # @param fallbacks [Array
- <%= render partial: 'account/user_consent_check', locals: { consenting_user: @user } %>
+ <%= render partial: 'account/user_consent_check' %>
<% end %>
<%= render partial: 'account/auth_providers', locals: { omniauth_title: I18n.t('account.signup_with_auth_provider'), wide: true } %>
diff --git a/app/views/account/_user_consent_check.html.erb b/app/views/account/_user_consent_check.html.erb
index c59978007828..26020ae4bd9c 100644
--- a/app/views/account/_user_consent_check.html.erb
+++ b/app/views/account/_user_consent_check.html.erb
@@ -1,6 +1,6 @@
").replace(/\r?\n/g,"
").replace(/\t/g," ").replace(/^\s/," ").replace(/\s$/," ").replace(/\s\s/g," ")).includes("
")||i.includes("
"))&&(i=`
${i}
`),e=i),r=this.editor.data.htmlProcessor.toView(e)}var i;const s=new d(this,"inputTransformation");this.fire(s,{content:r,dataTransfer:n,targetRanges:t.targetRanges,method:t.method}),s.stop.called&&e.stop(),o.scrollToTheSelection()}),{priority:"low"}),this.listenTo(this,"inputTransformation",((e,o)=>{if(o.content.isEmpty)return;const n=this.editor.data.toModel(o.content,"$clipboardHolder");0!=n.childCount&&(e.stop(),t.change((()=>{this.fire("contentInsertion",{content:n,method:o.method,dataTransfer:o.dataTransfer,targetRanges:o.targetRanges})})))}),{priority:"low"}),this.listenTo(this,"contentInsertion",((e,o)=>{o.resultRange=t.insertContent(o.content)}),{priority:"low"})}_setupCopyCut(){const e=this.editor,t=e.model.document,o=e.editing.view.document,n=(n,r)=>{const i=r.dataTransfer;r.preventDefault();const s=e.data.toView(e.model.getSelectedContent(t.selection));o.fire("clipboardOutput",{dataTransfer:i,content:s,method:n.name})};this.listenTo(o,"copy",n,{priority:"low"}),this.listenTo(o,"cut",((t,o)=>{e.model.canEditAt(e.model.document.selection)?n(t,o):o.preventDefault()}),{priority:"low"}),this.listenTo(o,"clipboardOutput",((o,n)=>{n.content.isEmpty||(n.dataTransfer.setData("text/html",this.editor.data.htmlProcessor.toData(n.content)),n.dataTransfer.setData("text/plain",Tb(n.content))),"cut"==n.method&&e.model.deleteContent(t.selection)}),{priority:"low"})}}var Ib=o(390),Pb={attributes:{"data-cke":!0}};Pb.setAttributes=Wr(),Pb.insert=qr().bind(null,"head"),Pb.domAPI=Lr(),Pb.insertStyleElement=Ur();Or()(Ib.Z,Pb);Ib.Z&&Ib.Z.locals&&Ib.Z.locals;class Rb extends Br{static get pluginName(){return"DragDrop"}static get requires(){return[Bb,eb]}init(){const e=this.editor,t=e.editing.view;this._draggedRange=null,this._draggingUid="",this._draggableElement=null,this._updateDropMarkerThrottled=fh((e=>this._updateDropMarker(e)),40),this._removeDropMarkerDelayed=xr((()=>this._removeDropMarker()),40),this._clearDraggableAttributesDelayed=xr((()=>this._clearDraggableAttributes()),40),e.plugins.has("DragDropExperimental")?this.forceDisabled("DragDropExperimental"):(t.addObserver(Db),t.addObserver(yu),this._setupDragging(),this._setupContentInsertionIntegration(),this._setupClipboardInputIntegration(),this._setupDropMarker(),this._setupDraggableAttributeHandling(),this.listenTo(e,"change:isReadOnly",((e,t,o)=>{o?this.forceDisabled("readOnlyMode"):this.clearForceDisabled("readOnlyMode")})),this.on("change:isEnabled",((e,t,o)=>{o||this._finalizeDragging(!1)})),n.isAndroid&&this.forceDisabled("noAndroidSupport"))}destroy(){return this._draggedRange&&(this._draggedRange.detach(),this._draggedRange=null),this._updateDropMarkerThrottled.cancel(),this._removeDropMarkerDelayed.cancel(),this._clearDraggableAttributesDelayed.cancel(),super.destroy()}_setupDragging(){const e=this.editor,t=e.model,o=t.document,r=e.editing.view,i=r.document;this.listenTo(i,"dragstart",((n,r)=>{const s=o.selection;if(r.target&&r.target.is("editableElement"))return void r.preventDefault();const a=r.target?Nb(r.target):null;if(a){const o=e.editing.mapper.toModelElement(a);if(this._draggedRange=Kl.fromRange(t.createRangeOn(o)),e.plugins.has("WidgetToolbarRepository")){e.plugins.get("WidgetToolbarRepository").forceDisabled("dragDrop")}}else if(!i.selection.isCollapsed){const e=i.selection.getSelectedElement();e&&$m(e)||(this._draggedRange=Kl.fromRange(s.getFirstRange()))}if(!this._draggedRange)return void r.preventDefault();this._draggingUid=h();const l=this.isEnabled&&e.model.canEditAt(this._draggedRange);r.dataTransfer.effectAllowed=l?"copyMove":"copy",r.dataTransfer.setData("application/ckeditor5-dragging-uid",this._draggingUid);const c=t.createSelection(this._draggedRange.toRange()),d=e.data.toView(t.getSelectedContent(c));i.fire("clipboardOutput",{dataTransfer:r.dataTransfer,content:d,method:"dragstart"}),l||(this._draggedRange.detach(),this._draggedRange=null,this._draggingUid="")}),{priority:"low"}),this.listenTo(i,"dragend",((e,t)=>{this._finalizeDragging(!t.dataTransfer.isCanceled&&"move"==t.dataTransfer.dropEffect)}),{priority:"low"}),this.listenTo(i,"dragenter",(()=>{this.isEnabled&&r.focus()})),this.listenTo(i,"dragleave",(()=>{this._removeDropMarkerDelayed()})),this.listenTo(i,"dragging",((t,o)=>{if(!this.isEnabled)return void(o.dataTransfer.dropEffect="none");this._removeDropMarkerDelayed.cancel();const r=zb(e,o.targetRanges,o.target);e.model.canEditAt(r)?(this._draggedRange||(o.dataTransfer.dropEffect="copy"),n.isGecko||("copy"==o.dataTransfer.effectAllowed?o.dataTransfer.dropEffect="copy":["all","copyMove"].includes(o.dataTransfer.effectAllowed)&&(o.dataTransfer.dropEffect="move")),r&&this._updateDropMarkerThrottled(r)):o.dataTransfer.dropEffect="none"}),{priority:"low"})}_setupClipboardInputIntegration(){const e=this.editor,t=e.editing.view.document;this.listenTo(t,"clipboardInput",((t,o)=>{if("drop"!=o.method)return;const n=zb(e,o.targetRanges,o.target);if(this._removeDropMarker(),!n||!e.model.canEditAt(n))return this._finalizeDragging(!1),void t.stop();this._draggedRange&&this._draggingUid!=o.dataTransfer.getData("application/ckeditor5-dragging-uid")&&(this._draggedRange.detach(),this._draggedRange=null,this._draggingUid="");if("move"==Mb(o.dataTransfer)&&this._draggedRange&&this._draggedRange.containsRange(n,!0))return this._finalizeDragging(!1),void t.stop();o.targetRanges=[e.editing.mapper.toViewRange(n)]}),{priority:"high"})}_setupContentInsertionIntegration(){const e=this.editor.plugins.get(Bb);e.on("contentInsertion",((e,t)=>{if(!this.isEnabled||"drop"!==t.method)return;const o=t.targetRanges.map((e=>this.editor.editing.mapper.toModelRange(e)));this.editor.model.change((e=>e.setSelection(o)))}),{priority:"high"}),e.on("contentInsertion",((e,t)=>{if(!this.isEnabled||"drop"!==t.method)return;const o="move"==Mb(t.dataTransfer),n=!t.resultRange||!t.resultRange.isCollapsed;this._finalizeDragging(n&&o)}),{priority:"lowest"})}_setupDraggableAttributeHandling(){const e=this.editor,t=e.editing.view,o=t.document;this.listenTo(o,"mousedown",((r,i)=>{if(n.isAndroid||!i)return;this._clearDraggableAttributesDelayed.cancel();let s=Nb(i.target);if(n.isBlink&&!s&&!o.selection.isCollapsed){const e=o.selection.getSelectedElement();if(!e||!$m(e)){const e=o.selection.editableElement;e&&!e.isReadOnly&&(s=e)}}s&&(t.change((e=>{e.setAttribute("draggable","true",s)})),this._draggableElement=e.editing.mapper.toModelElement(s))})),this.listenTo(o,"mouseup",(()=>{n.isAndroid||this._clearDraggableAttributesDelayed()}))}_clearDraggableAttributes(){const e=this.editor.editing;e.view.change((t=>{this._draggableElement&&"$graveyard"!=this._draggableElement.root.rootName&&t.removeAttribute("draggable",e.mapper.toViewElement(this._draggableElement)),this._draggableElement=null}))}_setupDropMarker(){const e=this.editor;e.conversion.for("editingDowncast").markerToHighlight({model:"drop-target",view:{classes:["ck-clipboard-drop-target-range"]}}),e.conversion.for("editingDowncast").markerToElement({model:"drop-target",view:(t,{writer:o})=>{if(e.model.schema.checkChild(t.markerRange.start,"$text"))return o.createUIElement("span",{class:"ck ck-clipboard-drop-target-position"},(function(e){const t=this.toDomElement(e);return t.append("",e.createElement("span"),""),t}))}})}_updateDropMarker(e){const t=this.editor,o=t.model.markers;t.model.change((t=>{o.has("drop-target")?o.get("drop-target").getRange().isEqual(e)||t.updateMarker("drop-target",{range:e}):t.addMarker("drop-target",{range:e,usingOperation:!1,affectsData:!1})}))}_removeDropMarker(){const e=this.editor.model;this._removeDropMarkerDelayed.cancel(),this._updateDropMarkerThrottled.cancel(),e.markers.has("drop-target")&&e.change((e=>{e.removeMarker("drop-target")}))}_finalizeDragging(e){const t=this.editor,o=t.model;if(this._removeDropMarker(),this._clearDraggableAttributes(),t.plugins.has("WidgetToolbarRepository")){t.plugins.get("WidgetToolbarRepository").clearForceDisabled("dragDrop")}this._draggingUid="",this._draggedRange&&(e&&this.isEnabled&&o.deleteContent(o.createSelection(this._draggedRange),{doNotAutoparagraph:!0}),this._draggedRange.detach(),this._draggedRange=null)}}function zb(e,t,o){const r=e.model,i=e.editing.mapper;let s=null;const a=t?t[0].start:null;if(o.is("uiElement")&&(o=o.parent),s=function(e,t){const o=e.model,n=e.editing.mapper;if($m(t))return o.createRangeOn(n.toModelElement(t));if(!t.is("editableElement")){const e=t.findAncestor((e=>$m(e)||e.is("editableElement")));if($m(e))return o.createRangeOn(n.toModelElement(e))}return null}(e,o),s)return s;const l=function(e,t){const o=e.editing.mapper,n=e.editing.view,r=o.toModelElement(t);if(r)return r;const i=n.createPositionBefore(t),s=o.findMappedViewAncestor(i);return o.toModelElement(s)}(e,o),c=a?i.toModelPosition(a):null;return c?(s=function(e,t,o){const n=e.model;if(!n.schema.checkChild(o,"$block"))return null;const r=n.createPositionAt(o,0),i=t.path.slice(0,r.path.length),s=n.createPositionFromPath(t.root,i),a=s.nodeAfter;if(a&&n.schema.isObject(a))return n.createRangeOn(a);return null}(e,c,l),s||(s=r.schema.getNearestSelectionRange(c,n.isGecko?"forward":"backward"),s||function(e,t){const o=e.model;let n=t;for(;n;){if(o.schema.isObject(n))return o.createRangeOn(n);n=n.parent}return null}(e,c.parent))):function(e,t){const o=e.model,n=o.schema,r=o.createPositionAt(t,0);return n.getNearestSelectionRange(r,"forward")}(e,l)}function Mb(e){return n.isGecko?e.dropEffect:["all","copyMove"].includes(e.effectAllowed)?"move":"copy"}function Nb(e){if(e.is("editableElement"))return null;if(e.hasClass("ck-widget__selection-handle"))return e.findAncestor($m);if($m(e))return e;const t=e.findAncestor((e=>$m(e)||e.is("editableElement")));return $m(t)?t:null}class Fb extends Br{static get pluginName(){return"PastePlainText"}static get requires(){return[Bb]}init(){const e=this.editor,t=e.model,o=e.editing.view,n=o.document,r=t.document.selection;let i=!1;o.addObserver(Db),this.listenTo(n,"keydown",((e,t)=>{i=t.shiftKey})),e.plugins.get(Bb).on("contentInsertion",((e,o)=>{(i||function(e,t){if(e.childCount>1)return!1;const o=e.getChild(0);if(t.isObject(o))return!1;return 0==Array.from(o.getAttributeKeys()).length}(o.content,t.schema))&&t.change((e=>{const n=Array.from(r.getAttributes()).filter((([e])=>t.schema.getAttributeProperties(e).isFormatting));r.isCollapsed||t.deleteContent(r,{doNotAutoparagraph:!0}),n.push(...r.getAttributes());const i=e.createRangeIn(o.content);for(const t of i.getItems())t.is("$textProxy")&&e.setAttributes(n,t)}))}))}}class Ob extends Br{static get pluginName(){return"Clipboard"}static get requires(){return[Bb,Rb,Fb]}}Hn("px");class Vb extends Pr{constructor(e){super(e),this._stack=[],this._createdBatches=new WeakSet,this.refresh(),this._isEnabledBasedOnSelection=!1,this.listenTo(e.data,"set",((e,t)=>{t[1]={...t[1]};const o=t[1];o.batchType||(o.batchType={isUndoable:!1})}),{priority:"high"}),this.listenTo(e.data,"set",((e,t)=>{t[1].batchType.isUndoable||this.clearStack()}))}refresh(){this.isEnabled=this._stack.length>0}get createdBatches(){return this._createdBatches}addBatch(e){const t=this.editor.model.document.selection,o={ranges:t.hasOwnRange?Array.from(t.getRanges()):[],isBackward:t.isBackward};this._stack.push({batch:e,selection:o}),this.refresh()}clearStack(){this._stack=[],this.refresh()}_restoreSelection(e,t,o){const n=this.editor.model,r=n.document,i=[],s=e.map((e=>e.getTransformedByOperations(o))),a=s.flat();for(const e of s){const t=e.filter((e=>e.root!=r.graveyard)).filter((e=>!jb(e,a)));t.length&&(Lb(t),i.push(t[0]))}i.length&&n.change((e=>{e.setSelection(i,{backward:t})}))}_undo(e,t){const o=this.editor.model,n=o.document;this._createdBatches.add(t);const r=e.operations.slice().filter((e=>e.isDocumentOperation));r.reverse();for(const e of r){const r=e.baseVersion+1,i=Array.from(n.history.getOperations(r)),s=Dd([e.getReversed()],i,{useRelations:!0,document:this.editor.model.document,padWithNoOps:!1,forceWeakRemove:!0}).operationsA;for(let r of s){const i=r.affectedSelectable;i&&!o.canEditAt(i)&&(r=new bd(r.baseVersion)),t.addOperation(r),o.applyOperation(r),n.history.setOperationAsUndone(e,r)}}}}function Lb(e){e.sort(((e,t)=>e.start.isBefore(t.start)?-1:1));for(let t=1;t