decidim-core 0.30.7 → 0.30.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/html_cell.rb +1 -1
  3. data/app/cells/decidim/content_blocks/static_page/section_cell.rb +1 -1
  4. data/app/cells/decidim/content_blocks/static_page/summary_cell.rb +1 -1
  5. data/app/cells/decidim/content_blocks/static_page/two_pane_section_cell.rb +2 -2
  6. data/app/cells/decidim/data_consent/category.erb +5 -5
  7. data/app/cells/decidim/upload_modal_cell.rb +5 -0
  8. data/app/controllers/decidim/download_your_data_controller.rb +1 -1
  9. data/app/controllers/decidim/notifications_subscriptions_controller.rb +8 -0
  10. data/app/controllers/decidim/private_downloads_controller.rb +29 -0
  11. data/app/helpers/decidim/mailer_helper.rb +36 -0
  12. data/app/helpers/decidim/menu_helper.rb +2 -1
  13. data/app/helpers/decidim/newsletters_helper.rb +4 -22
  14. data/app/mailers/decidim/application_mailer.rb +4 -0
  15. data/app/models/decidim/attachment.rb +20 -2
  16. data/app/models/decidim/authorization.rb +7 -0
  17. data/app/models/decidim/private_download.rb +61 -0
  18. data/app/models/decidim/private_export.rb +6 -0
  19. data/app/packs/src/decidim/datepicker/datepicker_functions.js +26 -0
  20. data/app/packs/src/decidim/datepicker/generate_datepicker.js +2 -1
  21. data/app/packs/src/decidim/datepicker/generate_timepicker.js +9 -1
  22. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +234 -0
  23. data/app/packs/src/decidim/sw/push-permissions.js +47 -12
  24. data/app/presenters/decidim/menu_item_presenter.rb +7 -1
  25. data/app/services/decidim/notifications_subscriptions_persistor.rb +6 -0
  26. data/app/services/decidim/push_subscription_endpoint_validator.rb +34 -0
  27. data/app/services/decidim/send_push_notification.rb +5 -1
  28. data/app/views/decidim/devise/registrations/new.html.erb +1 -0
  29. data/app/views/decidim/devise/shared/_tos_fields.html.erb +3 -3
  30. data/app/views/decidim/notification_mailer/event_received.html.erb +3 -3
  31. data/app/views/decidim/notifications_settings/show.html.erb +5 -5
  32. data/config/locales/ca-IT.yml +1 -0
  33. data/config/locales/ca.yml +1 -0
  34. data/config/locales/cs.yml +5 -0
  35. data/config/locales/de.yml +13 -0
  36. data/config/locales/en.yml +1 -0
  37. data/config/locales/es-MX.yml +1 -0
  38. data/config/locales/es-PY.yml +1 -0
  39. data/config/locales/es.yml +1 -0
  40. data/config/locales/eu.yml +4 -0
  41. data/config/locales/fi-plain.yml +5 -0
  42. data/config/locales/fi.yml +7 -2
  43. data/config/locales/fr-CA.yml +1 -0
  44. data/config/locales/fr.yml +1 -0
  45. data/config/locales/it.yml +10 -0
  46. data/config/locales/pt-BR.yml +1 -1
  47. data/config/locales/sk.yml +1417 -0
  48. data/config/locales/sv.yml +1 -0
  49. data/config/routes.rb +1 -0
  50. data/lib/decidim/content_parsers/blob_parser.rb +2 -2
  51. data/lib/decidim/content_renderers/blob_renderer.rb +2 -2
  52. data/lib/decidim/core/version.rb +1 -1
  53. metadata +11 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5110ff22ed67aef95bf387023ec7d18d9602a92944c82c2ab9373e786c04393c
4
- data.tar.gz: b754a2b2ddb4a72ae07dcd384203d61454ced7f6a3a41a4f80a5bb501b3dc310
3
+ metadata.gz: a5817376e17111569ffa304fd748355de958fdb6020c5fec8f49d913707b43e9
4
+ data.tar.gz: '0785c351a8efe4e5740a7551bf6994a98db61257e29b8df325823a790061385b'
5
5
  SHA512:
6
- metadata.gz: 3659a2a34d2531735e1a03710329261952c724361d0661bb9cf02c7a2acc76c2516266e8030a82e4dcc40789361d67845f1421addfcbf0f1fe4e0743a1288572
7
- data.tar.gz: 9ec948751168cd3f4a8cc2b768be6a118cd22b47aecb0712fa64a1c01d58a5cdf2b7f2f20963fdd0eec2384a41753bbaec558016f533bfab7176227df6f3cadd
6
+ metadata.gz: cf18599b257a5ffea8442bcba6ea817eddecfa48e7a2150fb0ccf00ee024c267da283c333ee5c2eedb89441c7deea081bc89fe3f5af3ce55d5e27b13ab3a3484
7
+ data.tar.gz: f56a6474372376c00de0e4589d9dcc733a311a5302aedc4384064cdb4ec7048b0761360f616d5bd0d515068757776691c0bb951aec53df944742015678287042
@@ -8,7 +8,7 @@ module Decidim
8
8
  end
9
9
 
10
10
  def html_content
11
- translated_attribute(model.settings.html_content).html_safe
11
+ decidim_sanitize_editor_admin(translated_attribute(model.settings.html_content))
12
12
  end
13
13
  end
14
14
  end
@@ -5,7 +5,7 @@ module Decidim
5
5
  module StaticPage
6
6
  class SectionCell < Decidim::ViewModel
7
7
  def content
8
- translated_attribute(model.settings.content).html_safe
8
+ decidim_sanitize_editor_admin(translated_attribute(model.settings.content))
9
9
  end
10
10
  end
11
11
  end
@@ -5,7 +5,7 @@ module Decidim
5
5
  module StaticPage
6
6
  class SummaryCell < Decidim::ViewModel
7
7
  def content
8
- translated_attribute(model.settings.summary).html_safe
8
+ decidim_sanitize_editor_admin(translated_attribute(model.settings.summary))
9
9
  end
10
10
  end
11
11
  end
@@ -5,11 +5,11 @@ module Decidim
5
5
  module StaticPage
6
6
  class TwoPaneSectionCell < Decidim::ViewModel
7
7
  def left_column
8
- translated_attribute(model.settings.left_column).html_safe
8
+ decidim_sanitize_editor_admin(translated_attribute(model.settings.left_column))
9
9
  end
10
10
 
11
11
  def right_column
12
- translated_attribute(model.settings.right_column).html_safe
12
+ decidim_sanitize_editor_admin(translated_attribute(model.settings.right_column))
13
13
  end
14
14
  end
15
15
  end
@@ -13,12 +13,12 @@
13
13
  <%= icon "close-line", class: "cookies__category-toggle-icon" %>
14
14
  </label>
15
15
 
16
- <div id="accordion-trigger-<%= category[:slug] %>" data-controls="accordion-panel-<%= category[:slug] %>" aria-labelledby="accordion-title-<%= category[:slug] %>">
17
- <h3 id="accordion-title-<%= category[:slug] %>" class="cookies__category-trigger-title">
18
- <%= category[:title] %>
19
- </h3>
16
+ <h3 id="accordion-title-<%= category[:slug] %>" class="cookies__category-trigger-title">
17
+ <%= category[:title] %>
18
+ </h3>
20
19
 
21
- <span>
20
+ <div id="accordion-trigger-<%= category[:slug] %>" role="group" data-controls="accordion-panel-<%= category[:slug] %>" aria-labelledby="accordion-title-<%= category[:slug] %>">
21
+ <span aria-hidden="true">
22
22
  <%= icon "arrow-down-s-line", class: "cookies__category-trigger-arrow" %>
23
23
  <%= icon "arrow-up-s-line", class: "cookies__category-trigger-arrow" %>
24
24
  </span>
@@ -177,6 +177,11 @@ module Decidim
177
177
 
178
178
  def file_attachment_path(attachment)
179
179
  return unless attachment
180
+
181
+ if attachment.respond_to?(:record) && attachment.record.is_a?(Decidim::Authorization) && attachment.name.to_s == "verification_attachment"
182
+ return decidim.private_download_path(Decidim::PrivateDownload.for(attachment.record, attachment_name: attachment.name).token)
183
+ end
184
+
180
185
  return Rails.application.routes.url_helpers.rails_blob_url(attachment, only_path: true) if attachment.is_a? ActiveStorage::Blob
181
186
 
182
187
  if attachment.try(:attached?)
@@ -50,7 +50,7 @@ module Decidim
50
50
  flash[:error] = t("decidim.account.download_your_data_export.export_expired")
51
51
  redirect_to download_your_data_path
52
52
  elsif private_export.file.attached?
53
- redirect_to Rails.application.routes.url_helpers.rails_blob_url(private_export.file.blob, only_path: true)
53
+ redirect_to private_download_path(Decidim::PrivateDownload.for(private_export, attachment_name: :file).token)
54
54
  else
55
55
  flash[:error] = t("decidim.account.download_your_data_export.file_no_exists")
56
56
  redirect_to download_your_data_path
@@ -3,6 +3,8 @@
3
3
  module Decidim
4
4
  # The controller to handle the subscriptions to push notifications
5
5
  class NotificationsSubscriptionsController < Decidim::ApplicationController
6
+ rescue_from Decidim::NotificationsSubscriptionsPersistor::UnsupportedPushSubscriptionEndpointError, with: :unsupported_browser
7
+
6
8
  def create
7
9
  Decidim::NotificationsSubscriptionsPersistor.new(current_user).add_subscription(params)
8
10
  head :ok
@@ -12,5 +14,11 @@ module Decidim
12
14
  Decidim::NotificationsSubscriptionsPersistor.new(current_user).delete_subscription(params[:auth])
13
15
  head :ok
14
16
  end
17
+
18
+ private
19
+
20
+ def unsupported_browser
21
+ render json: { error: I18n.t("notifications_settings.show.push_notifications_unsupported_browser", scope: "decidim") }, status: :unprocessable_entity
22
+ end
15
23
  end
16
24
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ class PrivateDownloadsController < Decidim::ApplicationController
5
+ before_action :authenticate_user!
6
+
7
+ def show
8
+ return head :not_found unless private_download.attached?
9
+ return head :not_found unless private_download.authorized_for?(current_user)
10
+
11
+ disposition = private_download.attachment.content_type.start_with?("image/") ? :inline : :attachment
12
+
13
+ send_data(
14
+ private_download.attachment.download,
15
+ filename: private_download.attachment.filename.to_s,
16
+ type: private_download.attachment.content_type,
17
+ disposition:
18
+ )
19
+ rescue Decidim::PrivateDownload::InvalidTokenError
20
+ head :not_found
21
+ end
22
+
23
+ private
24
+
25
+ def private_download
26
+ @private_download ||= Decidim::PrivateDownload.from_token(params[:id])
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ # Helper that provides methods to render order selector and links
5
+ module MailerHelper
6
+ # Transforms relative image URLs in HTML content to absolute URLs using the provided host.
7
+ # This is used in emails (newsletters and notifications) to ensure images display correctly
8
+ # in email clients.
9
+ #
10
+ # @param content [String] - HTML content with img tags
11
+ # @param host [String] - the Decidim::Organization host to use for the root URL
12
+ #
13
+ # @return [String] - the content with transformed image URLs
14
+ def decidim_transform_image_urls(content, host)
15
+ return content if host.blank? || content.blank?
16
+
17
+ root_url = if Rails.application.secrets.dig(:storage, :cdn_host).present?
18
+ Rails.application.secrets.dig(:storage, :cdn_host).chomp("/")
19
+ else
20
+ Decidim::EngineRouter.new("decidim", {}).root_url(host:).chomp("/")
21
+ end
22
+
23
+ content.gsub(/src\s*=\s*(['"])([^'"]*)\1/) do
24
+ quote = Regexp.last_match(1)
25
+ src_value = Regexp.last_match(2)
26
+
27
+ if src_value.blank? || src_value.start_with?("http://", "https://", "data:", "//", "cid:")
28
+ %(src=#{quote}#{src_value}#{quote})
29
+ else
30
+ normalized_src = src_value.start_with?("/") ? src_value : "/#{src_value}"
31
+ %(src=#{quote}#{root_url}#{normalized_src}#{quote})
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -57,7 +57,8 @@ module Decidim
57
57
  self,
58
58
  element_class: "font-semibold underline",
59
59
  active_class: "is-active",
60
- container_options: { class: "space-y-4 break-inside-avoid", role: :menu },
60
+ role: false,
61
+ container_options: { class: "space-y-4 break-inside-avoid" },
61
62
  label: t("layouts.decidim.footer.decidim_title")
62
63
  )
63
64
  end
@@ -3,6 +3,9 @@
3
3
  module Decidim
4
4
  # Helper that provides methods to render links with utm codes, and replaced name
5
5
  module NewslettersHelper
6
+ include Decidim::SanitizeHelper
7
+ include Decidim::MailerHelper
8
+
6
9
  # If the newsletter body there are some links and the Decidim.track_newsletter_links = true
7
10
  # it will be replaced with the utm_codes method described below.
8
11
  # for example transform "https://es.lipsum.com/" to "https://es.lipsum.com/?utm_source=localhost&utm_campaign=newsletter_11"
@@ -19,7 +22,7 @@ module Decidim
19
22
 
20
23
  content = interpret_name(content, user)
21
24
  content = track_newsletter_links(content, id, host)
22
- transform_image_urls(content, host)
25
+ decidim_transform_image_urls(content, host)
23
26
  end
24
27
 
25
28
  # this method is used to generate the root link on mail with the utm_codes
@@ -67,27 +70,6 @@ module Decidim
67
70
  content.gsub("%{name}", user.name)
68
71
  end
69
72
 
70
- # Find each img HTML tag with relative path in src attribute
71
- # For each URL, prepends the decidim.root_url
72
- # If host is not defined it returns full content
73
- #
74
- # @param content [String] - the string to convert
75
- # @param host [String] - the Decidim::Organization host to replace
76
- #
77
- # @return [String] - the content converted
78
- #
79
- def transform_image_urls(content, host)
80
- return content if host.blank?
81
-
82
- content.scan(/src\s*=\s*"([^"]*)"/).each do |src|
83
- root_url = decidim.root_url(host:)[0..-2]
84
- src_replaced = "#{root_url}#{src.first}"
85
- content = content.gsub(/src\s*=\s*"([^"]*#{src.first})"/, %(src="#{src_replaced}"))
86
- end
87
-
88
- content
89
- end
90
-
91
73
  # Add tracking query params to each links
92
74
  #
93
75
  # @param content [String] - the string to convert
@@ -7,9 +7,13 @@ module Decidim
7
7
  include LocalisedMailer
8
8
  include MultitenantAssetHost
9
9
  include Decidim::SanitizeHelper
10
+ include Decidim::MailerHelper
10
11
  include Decidim::OrganizationHelper
11
12
  helper_method :organization_name, :decidim_escape_translated, :decidim_sanitize_translated, :translated_attribute, :decidim_sanitize, :decidim_sanitize_newsletter
12
13
 
14
+ helper Decidim::SanitizeHelper
15
+ helper Decidim::MailerHelper
16
+
13
17
  after_action :set_smtp
14
18
  after_action :set_from
15
19
 
@@ -88,7 +88,7 @@ module Decidim
88
88
  # Returns String.
89
89
  def file_type
90
90
  if file?
91
- url&.split(".")&.last&.split("&")&.first&.downcase
91
+ file.filename.extension&.downcase
92
92
  elsif link?
93
93
  "link"
94
94
  end
@@ -100,7 +100,13 @@ module Decidim
100
100
  def url
101
101
  @url ||=
102
102
  if file?
103
- attached_uploader(:file).url
103
+ if private_download_required?
104
+ Decidim::Core::Engine.routes.url_helpers.private_download_path(
105
+ Decidim::PrivateDownload.for(self, attachment_name: :file).token
106
+ )
107
+ else
108
+ attached_uploader(:file).url
109
+ end
104
110
  elsif link?
105
111
  link
106
112
  end
@@ -144,5 +150,17 @@ module Decidim
144
150
 
145
151
  attached_to.can_participate?(user)
146
152
  end
153
+
154
+ def private_download_authorized?(user, requested_attachment_name)
155
+ return false unless requested_attachment_name.to_s == "file"
156
+
157
+ can_participate?(user)
158
+ end
159
+
160
+ def private_download_required?
161
+ return attached_to.private_space? if attached_to.respond_to?(:private_space?)
162
+
163
+ attached_to.respond_to?(:component) && attached_to.component&.private_non_transparent_space?
164
+ end
147
165
  end
148
166
  end
@@ -91,6 +91,13 @@ module Decidim
91
91
  Decidim::AuthorizationTransfer.perform!(self, handler)
92
92
  end
93
93
 
94
+ def private_download_authorized?(user, requested_attachment_name)
95
+ return false unless requested_attachment_name.to_s == "verification_attachment"
96
+ return true if user&.admin? && user.organization == organization
97
+
98
+ user == self.user
99
+ end
100
+
94
101
  private
95
102
 
96
103
  def active_handler?
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ class PrivateDownload
5
+ class InvalidTokenError < StandardError; end
6
+
7
+ VERIFIER_PURPOSE = :private_download
8
+
9
+ def self.for(record, attachment_name:)
10
+ new(record:, attachment_name:)
11
+ end
12
+
13
+ def self.from_token(token)
14
+ payload = verifier.verify(token, purpose: VERIFIER_PURPOSE).with_indifferent_access
15
+ record = GlobalID::Locator.locate(payload[:gid])
16
+
17
+ raise InvalidTokenError if record.blank?
18
+
19
+ new(record:, attachment_name: payload[:attachment_name])
20
+ rescue ActiveSupport::MessageVerifier::InvalidSignature, TypeError
21
+ raise InvalidTokenError
22
+ end
23
+
24
+ def self.verifier
25
+ @verifier ||= ActiveSupport::MessageVerifier.new(Rails.application.secret_key_base, serializer: JSON)
26
+ end
27
+
28
+ def initialize(record:, attachment_name:)
29
+ @record = record
30
+ @attachment_name = attachment_name.to_s
31
+ end
32
+
33
+ def token
34
+ self.class.verifier.generate(
35
+ {
36
+ gid: record.to_global_id.to_s,
37
+ attachment_name:
38
+ },
39
+ purpose: VERIFIER_PURPOSE
40
+ )
41
+ end
42
+
43
+ def attachment
44
+ record.public_send(attachment_name)
45
+ end
46
+
47
+ def attached?
48
+ attachment.respond_to?(:attached?) && attachment.attached?
49
+ end
50
+
51
+ def authorized_for?(user)
52
+ return false unless record.respond_to?(:private_download_authorized?)
53
+
54
+ record.private_download_authorized?(user, attachment_name)
55
+ end
56
+
57
+ private
58
+
59
+ attr_reader :record, :attachment_name
60
+ end
61
+ end
@@ -24,5 +24,11 @@ module Decidim
24
24
  self.content_type = file.content_type
25
25
  self.file_size = file.byte_size
26
26
  end
27
+
28
+ def private_download_authorized?(user, requested_attachment_name)
29
+ return false unless requested_attachment_name.to_s == "file"
30
+
31
+ attached_to == user
32
+ end
27
33
  end
28
34
  end
@@ -1,5 +1,31 @@
1
1
  // Utility helper functions for the date and time picker functionality
2
2
 
3
+ export const adjustPickerPosition = (input, datePickerContainer, selector) => {
4
+ const parent = input.closest(selector);
5
+
6
+ if (getComputedStyle(parent).position === "static") {
7
+ parent.style.position = "relative";
8
+ }
9
+
10
+ const rect = input.getBoundingClientRect();
11
+ const calendarHeight = datePickerContainer.offsetHeight;
12
+ const spaceAbove = rect.top;
13
+ const spaceBelow = window.innerHeight - rect.bottom;
14
+ const openBelow = spaceBelow >= calendarHeight || spaceBelow >= spaceAbove;
15
+
16
+ if (openBelow) {
17
+ // Open below
18
+ datePickerContainer.style.top = `${input.offsetHeight}px`;
19
+ datePickerContainer.style.bottom = "";
20
+ } else {
21
+ // Open above
22
+ datePickerContainer.style.top = "";
23
+ datePickerContainer.style.bottom = `${input.offsetHeight}px`;
24
+ }
25
+
26
+ datePickerContainer.style.right = "0px";
27
+ };
28
+
3
29
  export const setHour = (value, format) => {
4
30
  const hour = value.split(":")[0];
5
31
  if (format === 12) {
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable require-jsdoc */
2
2
  import icon from "src/decidim/icon"
3
- import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos } from "src/decidim/datepicker/datepicker_functions"
3
+ import { dateToPicker, formatDate, displayDate, formatTime, calculateDatepickerPos, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
4
4
  import { dateKeyDownListener, dateBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners"
5
5
  import { getDictionary } from "src/decidim/i18n"
6
6
 
@@ -130,6 +130,7 @@ export default function generateDatePicker(input, row, formats) {
130
130
  };
131
131
  pickedDate = null;
132
132
  datePickerContainer.style.display = "block";
133
+ adjustPickerPosition(date, datePickerContainer, ".datepicker__date-column");
133
134
 
134
135
  document.addEventListener("click", datePickerDisplay);
135
136
 
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable require-jsdoc */
2
+ /* eslint max-lines: ["error", 310] */
3
+
2
4
  import icon from "src/decidim/icon"
3
- import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue } from "src/decidim/datepicker/datepicker_functions"
5
+ import { changeHourDisplay, changeMinuteDisplay, formatDate, hourDisplay, minuteDisplay, formatTime, setHour, setMinute, updateTimeValue, updateInputValue, adjustPickerPosition } from "src/decidim/datepicker/datepicker_functions"
4
6
  import { timeKeyDownListener, timeBeforeInputListener } from "src/decidim/datepicker/datepicker_listeners";
5
7
  import { getDictionary } from "src/decidim/i18n";
6
8
 
@@ -21,6 +23,10 @@ export default function generateTimePicker(input, row, formats) {
21
23
  clock.setAttribute("type", "button");
22
24
  clock.setAttribute("aria-label", input.dataset.buttonTimeLabel);
23
25
 
26
+ if (input.attributes.disabled) {
27
+ clock.setAttribute("disabled", input.attributes.disabled);
28
+ };
29
+
24
30
  timeColumn.appendChild(time);
25
31
  timeColumn.appendChild(clock);
26
32
 
@@ -270,6 +276,8 @@ export default function generateTimePicker(input, row, formats) {
270
276
  event.preventDefault();
271
277
  timePicker.style.display = "block";
272
278
  document.addEventListener("click", timePickerDisplay);
279
+ adjustPickerPosition(time, timePicker, ".datepicker__time-column")
280
+
273
281
  hours.value = hourDisplay(hour);
274
282
  minutes.value = minuteDisplay(minute);
275
283
  });