decidim-core 0.32.0 → 0.32.2

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 (176) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/comments_button_cell.rb +9 -1
  3. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
  4. data/app/cells/decidim/data_consent/category.erb +20 -33
  5. data/app/cells/decidim/following_cell.rb +1 -1
  6. data/app/cells/decidim/footer_topics/show.erb +5 -8
  7. data/app/cells/decidim/member/show.erb +1 -1
  8. data/app/cells/decidim/nav_links/show.erb +3 -1
  9. data/app/cells/decidim/share_text_widget/modal.erb +1 -0
  10. data/app/cells/decidim/share_widget/modal.erb +1 -0
  11. data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
  12. data/app/commands/decidim/create_omniauth_registration.rb +17 -2
  13. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
  14. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
  15. data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
  16. data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
  17. data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
  18. data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
  19. data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
  20. data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
  21. data/app/controllers/concerns/decidim/paginable.rb +7 -3
  22. data/app/controllers/concerns/decidim/payload_info.rb +2 -0
  23. data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
  24. data/app/controllers/decidim/application_controller.rb +0 -6
  25. data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
  26. data/app/controllers/decidim/components/base_controller.rb +2 -0
  27. data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
  28. data/app/controllers/decidim/favicon_controller.rb +9 -0
  29. data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
  30. data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
  31. data/app/controllers/decidim/homepage_controller.rb +4 -1
  32. data/app/controllers/decidim/likes_controller.rb +1 -1
  33. data/app/controllers/decidim/links_controller.rb +2 -0
  34. data/app/controllers/decidim/manifests_controller.rb +2 -0
  35. data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
  36. data/app/controllers/decidim/newsletters_controller.rb +4 -0
  37. data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
  38. data/app/controllers/decidim/offline_controller.rb +5 -1
  39. data/app/controllers/decidim/open_data_controller.rb +5 -1
  40. data/app/controllers/decidim/pages_controller.rb +31 -3
  41. data/app/controllers/decidim/private_downloads_controller.rb +2 -0
  42. data/app/controllers/decidim/profiles_controller.rb +19 -1
  43. data/app/controllers/decidim/qr_controller.rb +3 -1
  44. data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
  45. data/app/controllers/decidim/searches_controller.rb +2 -0
  46. data/app/controllers/decidim/short_links_controller.rb +4 -0
  47. data/app/controllers/decidim/static_map_controller.rb +2 -0
  48. data/app/controllers/decidim/timeouts_controller.rb +4 -0
  49. data/app/controllers/decidim/tos_controller.rb +2 -0
  50. data/app/controllers/decidim/upload_validations_controller.rb +4 -2
  51. data/app/controllers/decidim/user_activities_controller.rb +3 -0
  52. data/app/forms/decidim/upload_validation_form.rb +16 -10
  53. data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
  54. data/app/helpers/decidim/decidim_form_helper.rb +1 -1
  55. data/app/helpers/decidim/menu_helper.rb +0 -2
  56. data/app/helpers/decidim/sanitize_helper.rb +11 -1
  57. data/app/jobs/decidim/application_job.rb +4 -0
  58. data/app/models/decidim/authorization.rb +24 -0
  59. data/app/models/decidim/oauth_application.rb +1 -0
  60. data/app/models/decidim/user.rb +3 -2
  61. data/app/models/decidim/user_base_entity.rb +1 -1
  62. data/app/packs/src/decidim/callout.js +10 -0
  63. data/app/packs/src/decidim/callout.test.js +32 -0
  64. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +260 -42
  65. data/app/packs/src/decidim/controllers/accordion/controller.js +136 -17
  66. data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
  67. data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
  68. data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
  69. data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
  70. data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
  71. data/app/packs/src/decidim/controllers/main_menu/controller.js +4 -0
  72. data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +23 -0
  73. data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
  74. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
  75. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
  76. data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
  77. data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
  78. data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
  79. data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
  80. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
  81. data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
  82. data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
  83. data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
  84. data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
  85. data/app/packs/src/decidim/editor/index.js +4 -0
  86. data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
  87. data/app/packs/src/decidim/map/controller.js +10 -1
  88. data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
  89. data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
  90. data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
  91. data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
  92. data/app/packs/src/decidim/utilities/dom.js +2 -2
  93. data/app/packs/stylesheets/decidim/_cards.scss +1 -1
  94. data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
  95. data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
  96. data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
  97. data/app/packs/stylesheets/decidim/_documents.scss +1 -1
  98. data/app/packs/stylesheets/decidim/_footer.scss +33 -1
  99. data/app/packs/stylesheets/decidim/_modal.scss +16 -8
  100. data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
  101. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +12 -2
  102. data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
  103. data/app/packs/stylesheets/decidim/_typography.scss +5 -2
  104. data/app/packs/stylesheets/decidim/application.scss +1 -0
  105. data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
  106. data/app/permissions/decidim/permissions.rb +20 -0
  107. data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
  108. data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
  109. data/app/presenters/decidim/log/base_presenter.rb +10 -1
  110. data/app/presenters/decidim/log/user_presenter.rb +2 -2
  111. data/app/presenters/decidim/menu_item_presenter.rb +1 -5
  112. data/app/presenters/decidim/resource_presenter.rb +11 -0
  113. data/app/resolvers/decidim/meta_image_url_resolver.rb +14 -1
  114. data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
  115. data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
  116. data/app/services/decidim/svg_content_validator.rb +554 -0
  117. data/app/uploaders/decidim/application_uploader.rb +2 -2
  118. data/app/validators/uploader_image_content_validator.rb +276 -0
  119. data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
  120. data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
  121. data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
  122. data/app/views/decidim/searches/_filters.html.erb +1 -1
  123. data/app/views/decidim/shared/_login_modal.html.erb +12 -14
  124. data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
  125. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  126. data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
  127. data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
  128. data/config/initializers/rack_attack.rb +10 -0
  129. data/config/locales/bg.yml +0 -1
  130. data/config/locales/ca-IT.yml +9 -1
  131. data/config/locales/ca.yml +16 -6
  132. data/config/locales/cs.yml +31 -1
  133. data/config/locales/de.yml +99 -3
  134. data/config/locales/en.yml +11 -1
  135. data/config/locales/es-MX.yml +11 -1
  136. data/config/locales/es-PY.yml +11 -1
  137. data/config/locales/es.yml +15 -5
  138. data/config/locales/eu.yml +166 -161
  139. data/config/locales/fi-plain.yml +9 -1
  140. data/config/locales/fi.yml +9 -1
  141. data/config/locales/fr-CA.yml +12 -1
  142. data/config/locales/fr.yml +12 -1
  143. data/config/locales/gl.yml +1485 -33
  144. data/config/locales/hu.yml +0 -1
  145. data/config/locales/ja.yml +28 -1
  146. data/config/locales/pl.yml +2 -1
  147. data/config/locales/pt-BR.yml +0 -1
  148. data/config/locales/ro-RO.yml +0 -1
  149. data/config/locales/sk.yml +2 -1
  150. data/config/locales/sv.yml +68 -12
  151. data/config/locales/tr-TR.yml +0 -2
  152. data/config/routes.rb +11 -2
  153. data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
  154. data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
  155. data/decidim-core.gemspec +4 -1
  156. data/lib/decidim/api/functions/user_entity_list.rb +1 -0
  157. data/lib/decidim/api/types/user_moderation_type.rb +2 -2
  158. data/lib/decidim/content_parsers/user_parser.rb +1 -1
  159. data/lib/decidim/content_parsers.rb +0 -1
  160. data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
  161. data/lib/decidim/content_renderers/user_renderer.rb +15 -4
  162. data/lib/decidim/core/test/shared_examples/access_mode_restricted_participatory_spaces.rb +8 -5
  163. data/lib/decidim/core/test/shared_examples/access_mode_transparent_participatory_spaces.rb +14 -11
  164. data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
  165. data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
  166. data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
  167. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
  168. data/lib/decidim/core/version.rb +1 -1
  169. data/lib/decidim/core.rb +10 -1
  170. data/lib/decidim/form_builder.rb +9 -1
  171. data/lib/decidim/has_upload_validations.rb +1 -0
  172. data/lib/decidim/legacy_form_builder.rb +4 -0
  173. data/lib/decidim/map/dynamic_map.rb +3 -1
  174. metadata +33 -7
  175. data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
  176. data/lib/tasks/upgrade/decidim_active_storage_migration_tasks.rake +0 -79
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This validator ensures that files declared as images actually contain valid
4
+ # image data of the declared format. It does so by checking the leading bytes
5
+ # (magic bytes / file signature) of the file against the known signatures of
6
+ # the supported image formats, and by comparing the detected format with the
7
+ # content type declared by the file.
8
+ #
9
+ # This prevents non-image files (e.g. HTML, MATLAB, or arbitrary binary data)
10
+ # from being uploaded with an image extension and a spoofed image content type.
11
+ # Such files would later cause image processing to fail (e.g. in ImageMagick or
12
+ # in libvips when generating a variant) and could lead to a denial of service.
13
+ #
14
+ # Comparing the detected format with the declared content type also prevents
15
+ # files whose contents do not match their declaration (e.g. a JPEG file
16
+ # declared as image/png), so that the stored MIME type is consistent with the
17
+ # file contents.
18
+ #
19
+ # SVG documents are XML-based and can contain scripts and other active content,
20
+ # which is executed when the document is rendered by the browser. Because of
21
+ # that, the contents of files detected as SVG are additionally validated by
22
+ # Decidim::SvgContentValidator, so that documents which could execute scripts
23
+ # or read external resources are rejected instead of being stored as they are.
24
+ class UploaderImageContentValidator < ActiveModel::Validations::FileContentTypeValidator
25
+ # The number of leading bytes to read from the file to detect its signature.
26
+ SIGNATURE_LENGTH = 128
27
+
28
+ # The known signatures of the supported binary image formats. Each entry
29
+ # associates a format with a list of [offset, bytes] pairs that must all
30
+ # match at their respective offsets for the file to be considered of that
31
+ # format.
32
+ BINARY_IMAGE_SIGNATURES = [
33
+ [:png, [[0, "\x89PNG\r\n\x1A\n".b]]],
34
+ [:jpeg, [[0, "\xFF\xD8\xFF".b]]],
35
+ [:gif, [[0, "GIF87a".b]]],
36
+ [:gif, [[0, "GIF89a".b]]],
37
+ [:bmp, [[0, "BM".b]]],
38
+ [:tiff, [[0, "II*\x00".b]]],
39
+ [:tiff, [[0, "MM\x00*".b]]],
40
+ [:webp, [[0, "RIFF".b], [8, "WEBP".b]]]
41
+ ].freeze
42
+
43
+ # The major brands of the ISOBMFF "ftyp" box that identify HEIF and AVIF
44
+ # images. These formats share the same leading layout as other "ftyp" files
45
+ # (e.g. MP4 video), so the brand at offset 8 is used to tell images apart.
46
+ # The brands are grouped per format family, because HEIF files may use any
47
+ # of the HEIF brands regardless of their extension (e.g. a ".heic" file
48
+ # with the "mif1" major brand).
49
+ FTYPE_IMAGE_BRANDS = {
50
+ heif: %w(heic heix hevc hevx mif1 msf1).map(&:b).freeze,
51
+ avif: %w(avif avis).map(&:b).freeze
52
+ }.freeze
53
+
54
+ # The leading markup of an SVG document, which is XML-based and therefore has
55
+ # no fixed binary signature.
56
+ SVG_SIGNATURES = ["<svg".b, "<?xml".b, "<!DOCTYPE svg".b].freeze
57
+
58
+ # The content types accepted for each image format detected from the file
59
+ # signature. The content type declared by the file must be one of these, so
60
+ # that the stored MIME type is consistent with the file contents. The lists
61
+ # include the aliases commonly used in the wild for the same format.
62
+ FORMAT_CONTENT_TYPES = {
63
+ png: %w(image/png),
64
+ jpeg: %w(image/jpeg image/jpg image/pjpeg),
65
+ gif: %w(image/gif),
66
+ bmp: %w(image/bmp image/x-ms-bmp image/windows-bmp),
67
+ tiff: %w(image/tiff image/x-tiff),
68
+ webp: %w(image/webp),
69
+ heif: %w(image/heic image/heif image/heic-sequence image/heif-sequence),
70
+ avif: %w(image/avif image/avis),
71
+ svg: %w(image/svg+xml)
72
+ }.freeze
73
+
74
+ def validate_each(record, attribute, value)
75
+ begin
76
+ values = parse_values(value)
77
+ rescue JSON::ParserError
78
+ record.errors.add attribute, :invalid
79
+ return
80
+ end
81
+
82
+ return if values.empty?
83
+
84
+ values.each do |val|
85
+ validate_image_content(record, attribute, val)
86
+ end
87
+ end
88
+
89
+ def check_validity!; end
90
+
91
+ private
92
+
93
+ def validate_image_content(record, attribute, file)
94
+ content_type = normalize_content_type(declared_content_type(file))
95
+ return unless content_type.start_with?("image/")
96
+
97
+ signature = file_signature(file)
98
+ return if signature.nil?
99
+
100
+ detected = image_format(signature)
101
+ return add_invalid_image(record, attribute) unless valid_image_format?(detected, content_type)
102
+
103
+ validate_svg_content(record, attribute, file) if detected == :svg
104
+ end
105
+
106
+ # The content type declared by the file, either through the blob of an
107
+ # attached file or through the upload itself.
108
+ def declared_content_type(file)
109
+ if file.is_a?(ActiveStorage::Attached)
110
+ file.blob&.content_type
111
+ else
112
+ file.try(:content_type)
113
+ end
114
+ end
115
+
116
+ # Strips the content type parameters (e.g. "image/png;charset=binary") and
117
+ # normalizes the casing, as content types are case insensitive.
118
+ def normalize_content_type(content_type)
119
+ content_type.to_s.split(";", 2).first.to_s.strip.downcase
120
+ end
121
+
122
+ # Whether the format detected from the file signature is one of the
123
+ # supported image formats and is consistent with the content type declared
124
+ # by the file.
125
+ def valid_image_format?(detected, content_type)
126
+ return false if detected.nil?
127
+
128
+ FORMAT_CONTENT_TYPES.fetch(detected).include?(content_type)
129
+ end
130
+
131
+ # Validates the contents of a file detected as SVG, which can contain active
132
+ # content. The validation is delegated to Decidim::SvgContentValidator, so
133
+ # that the SVG specific logic is kept out of this validator. Documents which
134
+ # are not well formed SVG documents are rejected as invalid images, and the
135
+ # ones which could execute scripts or read external resources when they are
136
+ # rendered are rejected as unsafe files.
137
+ def validate_svg_content(record, attribute, file)
138
+ case Decidim::SvgContentValidator.validate(file_content(file))
139
+ when :invalid
140
+ add_invalid_image(record, attribute)
141
+ when :unsafe
142
+ add_unsafe_svg(record, attribute)
143
+ end
144
+ end
145
+
146
+ def add_invalid_image(record, attribute)
147
+ record.errors.add attribute, I18n.t("decidim.errors.files.file_is_not_a_valid_image")
148
+ end
149
+
150
+ def add_unsafe_svg(record, attribute)
151
+ record.errors.add attribute, I18n.t("decidim.errors.files.file_contains_unsafe_content")
152
+ end
153
+
154
+ # Reads the leading bytes (signature) of the file. Returns nil when the file
155
+ # cannot be read, in which case the validation is skipped. Empty files yield
156
+ # an empty signature, which is rejected.
157
+ def file_signature(file)
158
+ read_file(file, SIGNATURE_LENGTH)
159
+ end
160
+
161
+ # Reads the whole contents of the file. Returns nil when the file cannot be
162
+ # read.
163
+ def file_content(file)
164
+ read_file(file)
165
+ end
166
+
167
+ # Reads the contents of the file, or only its leading bytes when a maximum
168
+ # length is given.
169
+ def read_file(file, max_length = nil)
170
+ if uploaded_file?(file)
171
+ File.open(file.path, "rb") { |io| read_io(io, max_length) }
172
+ elsif file.is_a?(ActiveStorage::Attached)
173
+ read_attached(file, max_length)
174
+ end
175
+ rescue ActiveStorage::Error, Errno::ENOENT, IOError
176
+ nil
177
+ end
178
+
179
+ # Reads the contents of an IO, or only its leading bytes when a maximum
180
+ # length is given. IO#read returns nil at the end of the file, so an empty
181
+ # file would otherwise be indistinguishable from an unreadable one and skip
182
+ # the validation. Normalizing it to an empty content keeps empty files
183
+ # rejected, while nil is kept for files which cannot be read at all.
184
+ def read_io(io, max_length = nil)
185
+ (max_length ? io.read(max_length) : io.read) || ""
186
+ end
187
+
188
+ # Reads the contents of an attached file. Blobs are only uploaded once the
189
+ # record is saved, so for still unpersisted blobs (e.g. an image assigned to
190
+ # a new record) the bytes are read from the pending attachable. Otherwise a
191
+ # spoofed image could be saved without being checked.
192
+ def read_attached(attached, max_length = nil)
193
+ blob = attached.blob
194
+ return read_blob(blob, max_length) if blob&.persisted?
195
+
196
+ read_pending_attachable(attached, max_length)
197
+ end
198
+
199
+ # ActiveStorage keeps the attachable of a pending attachment in the record's
200
+ # attachment changes until the blob is uploaded when the record is saved, so
201
+ # its contents can already be read during the validation.
202
+ def read_pending_attachable(attached, max_length = nil)
203
+ attachable = pending_attachable(attached)
204
+ if uploaded_file?(attachable) || attachable.is_a?(File)
205
+ File.open(attachable.path, "rb") { |io| read_io(io, max_length) }
206
+ elsif attachable.is_a?(Pathname)
207
+ File.open(attachable.to_path, "rb") { |io| read_io(io, max_length) }
208
+ elsif attachable.is_a?(Hash)
209
+ read_io_without_consuming(attachable[:io], max_length)
210
+ end
211
+ end
212
+
213
+ def pending_attachable(attached)
214
+ change = attached.record.try(:attachment_changes)&.[](attached.name.to_s)
215
+ change.try(:attachable)
216
+ end
217
+
218
+ # Reads the contents without consuming the IO, so ActiveStorage can still
219
+ # upload it after the validation.
220
+ def read_io_without_consuming(io, max_length = nil)
221
+ return unless io.respond_to?(:read) && io.respond_to?(:rewind)
222
+
223
+ io.rewind
224
+ content = read_io(io, max_length)
225
+ io.rewind
226
+ content
227
+ end
228
+
229
+ # Reads the whole contents of the blob, or only its leading bytes when a
230
+ # maximum length is given to keep the operation cheap for large files. The
231
+ # services return nil when the blob is empty, which is normalized to an
232
+ # empty content so that empty blobs are rejected. Falls back to opening the
233
+ # whole blob when the service does not support ranged downloads.
234
+ def read_blob(blob, max_length = nil)
235
+ return blob.download if max_length.nil?
236
+
237
+ blob.download_chunk(0...max_length) || ""
238
+ rescue NotImplementedError
239
+ blob.open { |io| read_io(io, max_length) }
240
+ end
241
+
242
+ # The image format detected from the file signature, or nil when the
243
+ # signature does not match any of the supported image formats.
244
+ def image_format(signature)
245
+ return nil if signature.blank?
246
+
247
+ binary_image_format(signature) || ftyp_image_format(signature) || svg_format(signature)
248
+ end
249
+
250
+ def binary_image_format(signature)
251
+ BINARY_IMAGE_SIGNATURES.find do |_format, pairs|
252
+ pairs.all? { |offset, bytes| signature[offset, bytes.bytesize] == bytes }
253
+ end&.first
254
+ end
255
+
256
+ # The format of an ISOBMFF-based image (HEIF or AVIF). These files start with
257
+ # a "ftyp" box at offset 4 followed by a major brand at offset 8.
258
+ def ftyp_image_format(signature)
259
+ return nil unless signature[4, 4] == "ftyp".b
260
+
261
+ FTYPE_IMAGE_BRANDS.find { |_format, brands| brands.include?(signature[8, 4]) }&.first
262
+ end
263
+
264
+ def svg_format(signature)
265
+ content = signature.sub(/\A\xEF\xBB\xBF/n, "").lstrip
266
+ return nil unless SVG_SIGNATURES.any? { |svg_signature| content.start_with?(svg_signature) }
267
+
268
+ :svg
269
+ end
270
+
271
+ def uploaded_file?(file)
272
+ return true if defined?(Rack::Test::UploadedFile) && file.is_a?(Rack::Test::UploadedFile)
273
+
274
+ file.is_a?(ActionDispatch::Http::UploadedFile)
275
+ end
276
+ end
@@ -12,7 +12,7 @@
12
12
  </label>
13
13
 
14
14
  <div data-controller="accordion" class="radio-accordion-section content-block__description editor-content">
15
- <div id="panel-title--<%= panel_id %>" aria-hidden="true">
15
+ <div id="panel-title--<%= panel_id %>" aria-hidden="true" inert>
16
16
  <%= yield %>
17
17
  </div>
18
18
 
@@ -21,10 +21,6 @@
21
21
  <%= show_more_text %>
22
22
  </span>
23
23
  <%= icon "arrow-down-s-line" %>
24
- <span>
25
- <%= show_less_text %>
26
- </span>
27
- <%= icon "arrow-up-s-line" %>
28
24
  </button>
29
25
  </div>
30
26
  </div>
@@ -11,7 +11,7 @@
11
11
  spellcheck="false"
12
12
  autocomplete="off"
13
13
  placeholder="<%= t("add_users_placeholder", scope: "decidim.messaging.conversations.add_conversation_users") %>"
14
- data-noresults="<%= t("no_results", scope: "decidim.messaging.conversations.add_conversation_users") %>"
14
+ data-no_results="<%= t("no_results", scope: "decidim.messaging.conversations.add_conversation_users") %>"
15
15
  data-selected="conversation__modal-results"
16
16
  contenteditable="true"
17
17
  data-direct-messages-disabled="<%= t("participant_with_disabled_message_reception", scope: "decidim.messaging.conversations.add_conversation_users") %>">
@@ -1,3 +1 @@
1
- $("#messages").append("<%= j(render "messages", sender: conversation.messages.first.sender, messages: [conversation.messages.first]).html_safe %>");
2
- $("div[data-add-message]").replaceWith("<%= j(render "reply", form:, conversation:).html_safe %>");
3
- window.initFoundation($("#new_message_"));
1
+ window.location.href = "<%= conversation_path(conversation) %>";
@@ -16,7 +16,7 @@
16
16
  </button>
17
17
  <% end %>
18
18
  </div>
19
- <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true" aria-expanded="true">
19
+ <button id="dropdown-trigger-search" data-controller="dropdown" data-target="dropdown-menu-search" data-auto-close="true">
20
20
  <%= content_tag :span, t("decidim.searches.filters_small_view.filter_by"), class: "#{"is-active" if params.dig(:filter, :with_resource_type) == nil}" %>
21
21
  <% @blocks.each do |elements| %>
22
22
  <% elements.each do |type, results| %>
@@ -9,11 +9,9 @@
9
9
  %>
10
10
 
11
11
  <%= decidim_form_for(Decidim::User.new, namespace: "login", as: :user, url: path, html: { class: nil }) do |f| %>
12
- <div>
13
- <div class="flex flex-row">
14
- <%= icon "user-line", class: "w-6 h-6 text-gray fill-current" %>
15
- <h3 id="dialog-title-loginModal" class="h3 ml-1"><%= t(".please_log_in") %></h3>
16
- </div>
12
+ <div data-dialog-container>
13
+ <%= icon "user-line" %>
14
+ <h3 id="dialog-title-loginModal" tabindex="-1" data-dialog-title><%= t(".please_log_in") %></h3>
17
15
 
18
16
  <div>
19
17
  <% if current_organization.sign_in_enabled? %>
@@ -44,16 +42,16 @@
44
42
  <%= render partial: "decidim/devise/shared/omniauth_buttons", locals: { is_horizontal: true } %>
45
43
  <% end %>
46
44
  <% end %>
47
- <% if current_organization.sign_in_enabled? %>
48
- <div data-dialog-actions>
49
- <div class="form__wrapper-block w-full mb-4">
50
- <button type="submit" class="button button__lg button__secondary">
51
- <%= t("devise.sessions.new.log_in") %>
52
- </button>
53
- </div>
54
- </div>
55
- <% end %>
56
45
  </div>
57
46
  </div>
47
+ <% if current_organization.sign_in_enabled? %>
48
+ <div data-dialog-actions>
49
+ <div class="form__wrapper-block w-full mb-4">
50
+ <button type="submit" class="button button__lg button__secondary">
51
+ <%= t("devise.sessions.new.log_in") %>
52
+ </button>
53
+ </div>
54
+ </div>
55
+ <% end %>
58
56
  <% end %>
59
57
  <% end %>
@@ -8,7 +8,7 @@
8
8
  <% if organization.logo.attached? %>
9
9
  <%= image_tag(
10
10
  organization.attached_uploader(:logo).variant_url(:medium, host: organization.host),
11
- style: "max-height: 50px",
11
+ style: "max-height: 50px; display: inline-block",
12
12
  alt: organization_name(organization)
13
13
  ) %>
14
14
  <% else %>
@@ -3,7 +3,7 @@
3
3
  <div class="lg:w-1/3">
4
4
  <%= render partial: "layouts/decidim/footer/main_intro" %>
5
5
  </div>
6
- <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 gap-y-10 lg:grid-cols-4 text-md text-white">
6
+ <div class="lg:w-2/3 grid grid-cols-1 gap-x-4 lg:grid-cols-4 text-md text-white">
7
7
  <%= render partial: "layouts/decidim/footer/main_links" %>
8
8
  </div>
9
9
  </div>
@@ -1,31 +1,59 @@
1
- <%= footer_menu.render %>
1
+ <div data-controller="accordion" data-multiselectable="true" class="contents">
2
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.decidim_title") %>">
3
+ <button type="button" id="panel-footer-menu-trigger" data-controls="panel-footer-menu" data-open-lg="true">
4
+ <span class="h4"><%= t("layouts.decidim.footer.decidim_title") %></span>
5
+ <%= icon "arrow-down-s-line" %>
6
+ </button>
7
+ <div id="panel-footer-menu" aria-hidden="true">
8
+ <%= footer_menu.render_menu_items %>
9
+ </div>
10
+ </nav>
2
11
 
3
- <nav role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
4
- <h2 class="h4 mb-4"><%= t("layouts.decidim.user_menu.profile") %></h2>
5
- <ul class="space-y-4 break-inside-avoid">
6
- <% if current_user %>
7
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
8
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
9
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
10
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
11
- <% else %>
12
- <% if current_organization.sign_up_enabled? %>
13
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
14
- <% end %>
15
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
16
- <% end %>
17
- </ul>
18
- </nav>
12
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.user_menu.profile") %>">
13
+ <button type="button" id="panel-profile-trigger" data-controls="panel-profile" data-open-lg="true">
14
+ <span class="h4"><%= t("layouts.decidim.user_menu.profile") %></span>
15
+ <%= icon "arrow-down-s-line" %>
16
+ </button>
17
+ <div id="panel-profile" aria-hidden="true">
18
+ <ul>
19
+ <% if current_user %>
20
+ <li><%= link_to t("layouts.decidim.user_menu.configuration"), decidim.account_path %></li>
21
+ <li><%= link_to t("layouts.decidim.user_menu.public_profile"), decidim.profile_path(current_user.nickname) %></li>
22
+ <li><%= link_to t("layouts.decidim.user_menu.notifications"), decidim.notifications_path %></li>
23
+ <li><%= link_to t("layouts.decidim.user_menu.conversations"), decidim.conversations_path %></li>
24
+ <% else %>
25
+ <% if current_organization.sign_up_enabled? %>
26
+ <li><%= link_to t("layouts.decidim.footer.sign_up"), decidim.new_user_registration_path %></li>
27
+ <% end %>
28
+ <li><%= link_to t("layouts.decidim.footer.log_in"), decidim.new_user_session_path %></li>
29
+ <% end %>
30
+ </ul>
31
+ </div>
32
+ </nav>
19
33
 
20
- <nav role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
21
- <h2 class="h4 mb-4"><%= t("layouts.decidim.footer.resources") %></h2>
22
- <ul class="space-y-4 break-inside-avoid">
23
- <li class="font-semibold underline"><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
24
- <% if Decidim.module_installed?(:meetings) %>
25
- <li class="font-semibold underline"><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
26
- <% end %>
27
- <li class="font-semibold underline"><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
28
- </ul>
29
- </nav>
34
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.resources") %>">
35
+ <button type="button" id="panel-resources-trigger" data-controls="panel-resources" data-open-lg="true">
36
+ <span class="h4"><%= t("layouts.decidim.footer.resources") %></span>
37
+ <%= icon "arrow-down-s-line" %>
38
+ </button>
39
+ <div id="panel-resources" aria-hidden="true">
40
+ <ul>
41
+ <li><%= link_to t("decidim.profiles.show.activity"), decidim.last_activities_path %></li>
42
+ <% if Decidim.module_installed?(:meetings) %>
43
+ <li><%= link_to t("decidim.pages.home.extended.meetings"), Decidim::Meetings::DirectoryEngine.routes.url_helpers.root_path %></li>
44
+ <% end %>
45
+ <li><%= link_to t("layouts.decidim.footer.open_data"), decidim.open_data_path %></li>
46
+ </ul>
47
+ </div>
48
+ </nav>
30
49
 
31
- <%= cell("decidim/footer_topics", nil) %>
50
+ <nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.help") %>">
51
+ <button type="button" id="panel-help-trigger" data-controls="panel-help" data-open-lg="true">
52
+ <span class="h4"><%= t("layouts.decidim.footer.help") %></span>
53
+ <%= icon "arrow-down-s-line" %>
54
+ </button>
55
+ <div id="panel-help" aria-hidden="true">
56
+ <%= cell("decidim/footer_topics", nil) %>
57
+ </div>
58
+ </nav>
59
+ </div>
@@ -2,6 +2,7 @@
2
2
  <span><%= t("decidim.menu.home") %></span>
3
3
  <% end %>
4
4
  <% truncatable_items = breadcrumb_items.compact_blank.drop(1) %>
5
+ <% current_page_item = breadcrumb_items.compact_blank.select { |breadcrumb_item| breadcrumb_item[:active] }.last %>
5
6
  <% breadcrumb_items.each do |item| %>
6
7
  <% next if item.blank? %>
7
8
 
@@ -16,7 +17,7 @@
16
17
  <% end %>
17
18
  <span aria-hidden="true"><%= icon "arrow-right-s-line", class: "fill-white" %></span>
18
19
  <% if item[:url].present? && !is_active_link?(item[:url], :exclusive) %>
19
- <%= link_to item[:url], class: trigger_classes.join(" "), data: (truncatable ? { breadcrumb_truncate_target: "item" } : {}), "aria-current": (item[:active] ? "page" : nil) do %>
20
+ <%= link_to item[:url], class: trigger_classes.join(" "), data: (truncatable ? { breadcrumb_truncate_target: "item" } : {}), "aria-current": (item == current_page_item ? "page" : nil) do %>
20
21
  <% if truncatable %>
21
22
  <span class="menu-bar__breadcrumb-desktop__label--truncatable">
22
23
  <span class="menu-bar__breadcrumb-text" data-breadcrumb-truncate-target="text"><%= item_label %></span>
@@ -27,7 +28,7 @@
27
28
  <% end %>
28
29
  <% else %>
29
30
  <%# This block is executed if the condition is false %>
30
- <%= content_tag :span, class: [*trigger_classes, "no-interactive"].join(" "), data: (truncatable ? { breadcrumb_truncate_target: "item" } : {}), "aria-current": (item[:active] ? "page" : nil) do %>
31
+ <%= content_tag :span, class: [*trigger_classes, "no-interactive"].join(" "), data: (truncatable ? { breadcrumb_truncate_target: "item" } : {}), "aria-current": (item == current_page_item ? "page" : nil) do %>
31
32
  <% if truncatable %>
32
33
  <span class="menu-bar__breadcrumb-desktop__label--truncatable">
33
34
  <span class="menu-bar__breadcrumb-text" data-breadcrumb-truncate-target="text"><%= item_label %></span>
@@ -34,6 +34,16 @@ if Rails.env.production? || Rails.env.test?
34
34
  Rack::Attack.throttle("limit password recovery attempts per email", limit: 5, period: 60.seconds) do |request|
35
35
  request.params["user"]["email"] if request.path == "/users/password" && request.post?
36
36
  end
37
+
38
+ # Throttle SMS verification confirmation attempts per IP to 10 reqs/minute
39
+ Rack::Attack.throttle("limit sms verification attempts per ip", limit: 10, period: 60.seconds) do |request|
40
+ request.ip if request.path.match?(%r{^/[^/]+/sms/authorizations(?:\.[^/]+)?$}) && request.put?
41
+ end
42
+
43
+ # Throttle postal letter verification confirmation attempts per IP to 10 reqs/minute
44
+ Rack::Attack.throttle("limit postal letter verification attempts per ip", limit: 10, period: 60.seconds) do |request|
45
+ request.ip if request.path.match?(%r{^/[^/]+/postal_letter/authorizations(?:\.[^/]+)?$}) && request.put?
46
+ end
37
47
  end
38
48
  end
39
49
  end
@@ -1502,7 +1502,6 @@ bg:
1502
1502
  layouts:
1503
1503
  decidim:
1504
1504
  announcements:
1505
- view_less: Покажи по-малко
1506
1505
  view_more: Още информация
1507
1506
  data_consent:
1508
1507
  details:
@@ -252,6 +252,7 @@ ca-IT:
252
252
  create: "%{user_name} ha afegit el component %{resource_name} a l'espai %{space_name}"
253
253
  delete: "%{user_name} ha eliminat el component %{resource_name} de l'espai %{space_name}"
254
254
  export_component: "%{user_name} ha exportat %{resource_name}%{component_name} de %{space_name} com %{format_name}"
255
+ menu_hidden: "%{user_name} ha amagat el component %{resource_name} del menú de l'espai de participació: %{space_name}"
255
256
  publish: "%{user_name} ha publicat el component %{resource_name} a l'espai %{space_name}"
256
257
  restore: "%{user_name} ha restaurat el component %{resource_name} a l'espai %{space_name}"
257
258
  soft_delete: "%{user_name} ha mogut el component %{resource_name} a l'espai %{space_name} a la paperera"
@@ -1205,6 +1206,8 @@ ca-IT:
1205
1206
  dynamic:
1206
1207
  screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
1207
1208
  skip_button: Saltar el mapa
1209
+ zoom_in: Ampliar
1210
+ zoom_out: Allunyar
1208
1211
  static:
1209
1212
  latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
1210
1213
  map_service_brand: OpenStreetMap
@@ -1546,6 +1549,10 @@ ca-IT:
1546
1549
  never_logged_in: mai ha iniciat sessió
1547
1550
  subject: Avís de compte inactiu
1548
1551
  participatory_space:
1552
+ labels:
1553
+ reference: Referència de l'espai de participació
1554
+ restricted: Espai de participació restringit
1555
+ transparent: Espai de participació transparent
1549
1556
  pages:
1550
1557
  user_profile:
1551
1558
  member_of:
@@ -1704,6 +1711,8 @@ ca-IT:
1704
1711
  login_modal:
1705
1712
  please_log_in: Si us plau, inicia la sessió
1706
1713
  sign_up: Crea un compte
1714
+ mentions:
1715
+ search_prompt: Escriu aquí per cercar entre les participants
1707
1716
  mentions_modal:
1708
1717
  remove_recipient: Eliminar destinatària %{name}
1709
1718
  progress: Progrés
@@ -2086,7 +2095,6 @@ ca-IT:
2086
2095
  layouts:
2087
2096
  decidim:
2088
2097
  announcements:
2089
- view_less: Mostra menys
2090
2098
  view_more: Més informació
2091
2099
  data_consent:
2092
2100
  details: