decidim-core 0.31.6 → 0.31.8

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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/card_metadata_cell.rb +1 -1
  3. data/app/cells/decidim/comments_button_cell.rb +9 -1
  4. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
  5. data/app/cells/decidim/data_consent/category.erb +20 -33
  6. data/app/cells/decidim/following_cell.rb +1 -1
  7. data/app/cells/decidim/footer_topics/show.erb +5 -8
  8. data/app/cells/decidim/nav_links/show.erb +3 -1
  9. data/app/cells/decidim/participatory_space_dropdown_metadata/links.erb +1 -1
  10. data/app/cells/decidim/participatory_space_private_user/show.erb +1 -1
  11. data/app/cells/decidim/share_text_widget/modal.erb +1 -0
  12. data/app/cells/decidim/share_widget/modal.erb +1 -0
  13. data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
  14. data/app/commands/decidim/create_omniauth_registration.rb +17 -2
  15. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
  16. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
  17. data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
  18. data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
  19. data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
  20. data/app/controllers/concerns/decidim/locale_switcher.rb +62 -11
  21. data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
  22. data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
  23. data/app/controllers/concerns/decidim/paginable.rb +7 -3
  24. data/app/controllers/concerns/decidim/payload_info.rb +2 -0
  25. data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
  26. data/app/controllers/decidim/application_controller.rb +0 -6
  27. data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
  28. data/app/controllers/decidim/components/base_controller.rb +2 -0
  29. data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
  30. data/app/controllers/decidim/favicon_controller.rb +9 -0
  31. data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
  32. data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
  33. data/app/controllers/decidim/homepage_controller.rb +4 -1
  34. data/app/controllers/decidim/likes_controller.rb +1 -1
  35. data/app/controllers/decidim/links_controller.rb +2 -0
  36. data/app/controllers/decidim/manifests_controller.rb +2 -0
  37. data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
  38. data/app/controllers/decidim/newsletters_controller.rb +4 -0
  39. data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
  40. data/app/controllers/decidim/offline_controller.rb +5 -1
  41. data/app/controllers/decidim/open_data_controller.rb +5 -1
  42. data/app/controllers/decidim/pages_controller.rb +31 -3
  43. data/app/controllers/decidim/private_downloads_controller.rb +2 -0
  44. data/app/controllers/decidim/profiles_controller.rb +10 -0
  45. data/app/controllers/decidim/qr_controller.rb +3 -1
  46. data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
  47. data/app/controllers/decidim/searches_controller.rb +2 -0
  48. data/app/controllers/decidim/short_links_controller.rb +4 -0
  49. data/app/controllers/decidim/static_map_controller.rb +2 -0
  50. data/app/controllers/decidim/timeouts_controller.rb +4 -0
  51. data/app/controllers/decidim/tos_controller.rb +2 -0
  52. data/app/controllers/decidim/upload_validations_controller.rb +4 -2
  53. data/app/controllers/decidim/user_activities_controller.rb +2 -0
  54. data/app/events/decidim/amendable/amendment_base_event.rb +7 -1
  55. data/app/forms/decidim/upload_validation_form.rb +16 -10
  56. data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
  57. data/app/helpers/decidim/menu_helper.rb +0 -2
  58. data/app/helpers/decidim/sanitize_helper.rb +11 -1
  59. data/app/jobs/decidim/application_job.rb +4 -0
  60. data/app/models/decidim/authorization.rb +24 -0
  61. data/app/models/decidim/oauth_application.rb +1 -0
  62. data/app/models/decidim/user.rb +3 -2
  63. data/app/models/decidim/user_base_entity.rb +1 -1
  64. data/app/packs/src/decidim/callout.js +10 -0
  65. data/app/packs/src/decidim/callout.test.js +32 -0
  66. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +260 -42
  67. data/app/packs/src/decidim/controllers/accordion/controller.js +136 -17
  68. data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
  69. data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -0
  70. data/app/packs/src/decidim/controllers/password_toggler/controller.js +22 -1
  71. data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
  72. data/app/packs/src/decidim/datepicker/datepicker_functions.js +34 -23
  73. data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
  74. data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
  75. data/app/packs/src/decidim/direct_uploads/upload_modal.js +2 -1
  76. data/app/packs/src/decidim/map/controller.js +10 -1
  77. data/app/packs/src/decidim/refactor/moved/focus_guard.js +12 -12
  78. data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
  79. data/app/packs/src/decidim/utilities/dom.js +2 -2
  80. data/app/packs/stylesheets/decidim/_cards.scss +1 -1
  81. data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
  82. data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
  83. data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
  84. data/app/packs/stylesheets/decidim/_documents.scss +1 -1
  85. data/app/packs/stylesheets/decidim/_footer.scss +33 -1
  86. data/app/packs/stylesheets/decidim/_modal.scss +16 -8
  87. data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
  88. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +12 -2
  89. data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
  90. data/app/packs/stylesheets/decidim/_typography.scss +5 -2
  91. data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
  92. data/app/permissions/decidim/permissions.rb +20 -0
  93. data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
  94. data/app/presenters/decidim/footer_menu_presenter.rb +2 -4
  95. data/app/presenters/decidim/log/base_presenter.rb +10 -1
  96. data/app/presenters/decidim/log/user_presenter.rb +2 -2
  97. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -0
  98. data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
  99. data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
  100. data/app/services/decidim/svg_content_validator.rb +554 -0
  101. data/app/uploaders/decidim/application_uploader.rb +2 -2
  102. data/app/validators/uploader_image_content_validator.rb +278 -0
  103. data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
  104. data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
  105. data/app/views/decidim/searches/_filters.html.erb +1 -1
  106. data/app/views/decidim/shared/_login_modal.html.erb +12 -14
  107. data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
  108. data/app/views/layouts/decidim/footer/_main.html.erb +1 -1
  109. data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
  110. data/config/initializers/rack_attack.rb +10 -0
  111. data/config/locales/ar.yml +0 -20
  112. data/config/locales/bg.yml +0 -20
  113. data/config/locales/ca-IT.yml +31 -24
  114. data/config/locales/ca.yml +31 -24
  115. data/config/locales/cs.yml +20 -22
  116. data/config/locales/de.yml +59 -26
  117. data/config/locales/el.yml +0 -18
  118. data/config/locales/en.yml +31 -24
  119. data/config/locales/es-MX.yml +31 -24
  120. data/config/locales/es-PY.yml +31 -24
  121. data/config/locales/es.yml +31 -24
  122. data/config/locales/eu.yml +178 -176
  123. data/config/locales/fi-plain.yml +29 -24
  124. data/config/locales/fi.yml +29 -24
  125. data/config/locales/fr-CA.yml +10 -20
  126. data/config/locales/fr.yml +10 -20
  127. data/config/locales/gl.yml +1440 -51
  128. data/config/locales/hu.yml +0 -18
  129. data/config/locales/id-ID.yml +0 -18
  130. data/config/locales/is-IS.yml +0 -6
  131. data/config/locales/it.yml +0 -17
  132. data/config/locales/ja.yml +29 -24
  133. data/config/locales/lb.yml +0 -18
  134. data/config/locales/lt.yml +0 -19
  135. data/config/locales/lv.yml +0 -18
  136. data/config/locales/nl.yml +0 -18
  137. data/config/locales/no.yml +0 -18
  138. data/config/locales/pl.yml +2 -20
  139. data/config/locales/pt-BR.yml +0 -26
  140. data/config/locales/pt.yml +0 -18
  141. data/config/locales/ro-RO.yml +0 -19
  142. data/config/locales/ru.yml +0 -8
  143. data/config/locales/sk.yml +2 -26
  144. data/config/locales/sv.yml +45 -29
  145. data/config/locales/tr-TR.yml +0 -20
  146. data/config/locales/uk.yml +0 -6
  147. data/config/locales/zh-CN.yml +0 -16
  148. data/config/locales/zh-TW.yml +0 -19
  149. data/db/migrate/20250217192438_convert_user_groups_into_users.rb +3 -2
  150. data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
  151. data/decidim-core.gemspec +3 -0
  152. data/lib/decidim/core/test/shared_examples/amendable/amendment_event_examples.rb +2 -2
  153. data/lib/decidim/core/test/shared_examples/amendable/amendment_promoted_event_examples.rb +3 -3
  154. data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
  155. data/lib/decidim/core/test/shared_examples/has_space_in_mcell_examples.rb +2 -1
  156. data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
  157. data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +85 -6
  158. data/lib/decidim/core/test/shared_examples/resource_liked_event_examples.rb +3 -3
  159. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
  160. data/lib/decidim/core/version.rb +1 -1
  161. data/lib/decidim/core.rb +10 -1
  162. data/lib/decidim/has_upload_validations.rb +1 -0
  163. data/lib/decidim/legacy_form_builder.rb +4 -0
  164. data/lib/decidim/map/dynamic_map.rb +3 -1
  165. data/lib/decidim/query_extensions.rb +2 -0
  166. metadata +31 -6
@@ -0,0 +1,278 @@
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
+ [:ico, [[0, "\x00\x00\x01\x00".b]]]
42
+ ].freeze
43
+
44
+ # The major brands of the ISOBMFF "ftyp" box that identify HEIF and AVIF
45
+ # images. These formats share the same leading layout as other "ftyp" files
46
+ # (e.g. MP4 video), so the brand at offset 8 is used to tell images apart.
47
+ # The brands are grouped per format family, because HEIF files may use any
48
+ # of the HEIF brands regardless of their extension (e.g. a ".heic" file
49
+ # with the "mif1" major brand).
50
+ FTYPE_IMAGE_BRANDS = {
51
+ heif: %w(heic heix hevc hevx mif1 msf1).map(&:b).freeze,
52
+ avif: %w(avif avis).map(&:b).freeze
53
+ }.freeze
54
+
55
+ # The leading markup of an SVG document, which is XML-based and therefore has
56
+ # no fixed binary signature.
57
+ SVG_SIGNATURES = ["<svg".b, "<?xml".b, "<!DOCTYPE svg".b].freeze
58
+
59
+ # The content types accepted for each image format detected from the file
60
+ # signature. The content type declared by the file must be one of these, so
61
+ # that the stored MIME type is consistent with the file contents. The lists
62
+ # include the aliases commonly used in the wild for the same format.
63
+ FORMAT_CONTENT_TYPES = {
64
+ png: %w(image/png),
65
+ jpeg: %w(image/jpeg image/jpg image/pjpeg),
66
+ gif: %w(image/gif),
67
+ bmp: %w(image/bmp image/x-ms-bmp image/windows-bmp),
68
+ tiff: %w(image/tiff image/x-tiff),
69
+ webp: %w(image/webp),
70
+ ico: %w(image/vnd.microsoft.icon image/x-icon image/ico image/icon),
71
+ heif: %w(image/heic image/heif image/heic-sequence image/heif-sequence),
72
+ avif: %w(image/avif image/avis),
73
+ svg: %w(image/svg+xml)
74
+ }.freeze
75
+
76
+ def validate_each(record, attribute, value)
77
+ begin
78
+ values = parse_values(value)
79
+ rescue JSON::ParserError
80
+ record.errors.add attribute, :invalid
81
+ return
82
+ end
83
+
84
+ return if values.empty?
85
+
86
+ values.each do |val|
87
+ validate_image_content(record, attribute, val)
88
+ end
89
+ end
90
+
91
+ def check_validity!; end
92
+
93
+ private
94
+
95
+ def validate_image_content(record, attribute, file)
96
+ content_type = normalize_content_type(declared_content_type(file))
97
+ return unless content_type.start_with?("image/")
98
+
99
+ signature = file_signature(file)
100
+ return if signature.nil?
101
+
102
+ detected = image_format(signature)
103
+ return add_invalid_image(record, attribute) unless valid_image_format?(detected, content_type)
104
+
105
+ validate_svg_content(record, attribute, file) if detected == :svg
106
+ end
107
+
108
+ # The content type declared by the file, either through the blob of an
109
+ # attached file or through the upload itself.
110
+ def declared_content_type(file)
111
+ if file.is_a?(ActiveStorage::Attached)
112
+ file.blob&.content_type
113
+ else
114
+ file.try(:content_type)
115
+ end
116
+ end
117
+
118
+ # Strips the content type parameters (e.g. "image/png;charset=binary") and
119
+ # normalizes the casing, as content types are case insensitive.
120
+ def normalize_content_type(content_type)
121
+ content_type.to_s.split(";", 2).first.to_s.strip.downcase
122
+ end
123
+
124
+ # Whether the format detected from the file signature is one of the
125
+ # supported image formats and is consistent with the content type declared
126
+ # by the file.
127
+ def valid_image_format?(detected, content_type)
128
+ return false if detected.nil?
129
+
130
+ FORMAT_CONTENT_TYPES.fetch(detected).include?(content_type)
131
+ end
132
+
133
+ # Validates the contents of a file detected as SVG, which can contain active
134
+ # content. The validation is delegated to Decidim::SvgContentValidator, so
135
+ # that the SVG specific logic is kept out of this validator. Documents which
136
+ # are not well formed SVG documents are rejected as invalid images, and the
137
+ # ones which could execute scripts or read external resources when they are
138
+ # rendered are rejected as unsafe files.
139
+ def validate_svg_content(record, attribute, file)
140
+ case Decidim::SvgContentValidator.validate(file_content(file))
141
+ when :invalid
142
+ add_invalid_image(record, attribute)
143
+ when :unsafe
144
+ add_unsafe_svg(record, attribute)
145
+ end
146
+ end
147
+
148
+ def add_invalid_image(record, attribute)
149
+ record.errors.add attribute, I18n.t("decidim.errors.files.file_is_not_a_valid_image")
150
+ end
151
+
152
+ def add_unsafe_svg(record, attribute)
153
+ record.errors.add attribute, I18n.t("decidim.errors.files.file_contains_unsafe_content")
154
+ end
155
+
156
+ # Reads the leading bytes (signature) of the file. Returns nil when the file
157
+ # cannot be read, in which case the validation is skipped. Empty files yield
158
+ # an empty signature, which is rejected.
159
+ def file_signature(file)
160
+ read_file(file, SIGNATURE_LENGTH)
161
+ end
162
+
163
+ # Reads the whole contents of the file. Returns nil when the file cannot be
164
+ # read.
165
+ def file_content(file)
166
+ read_file(file)
167
+ end
168
+
169
+ # Reads the contents of the file, or only its leading bytes when a maximum
170
+ # length is given.
171
+ def read_file(file, max_length = nil)
172
+ if uploaded_file?(file)
173
+ File.open(file.path, "rb") { |io| read_io(io, max_length) }
174
+ elsif file.is_a?(ActiveStorage::Attached)
175
+ read_attached(file, max_length)
176
+ end
177
+ rescue ActiveStorage::Error, Errno::ENOENT, IOError
178
+ nil
179
+ end
180
+
181
+ # Reads the contents of an IO, or only its leading bytes when a maximum
182
+ # length is given. IO#read returns nil at the end of the file, so an empty
183
+ # file would otherwise be indistinguishable from an unreadable one and skip
184
+ # the validation. Normalizing it to an empty content keeps empty files
185
+ # rejected, while nil is kept for files which cannot be read at all.
186
+ def read_io(io, max_length = nil)
187
+ (max_length ? io.read(max_length) : io.read) || ""
188
+ end
189
+
190
+ # Reads the contents of an attached file. Blobs are only uploaded once the
191
+ # record is saved, so for still unpersisted blobs (e.g. an image assigned to
192
+ # a new record) the bytes are read from the pending attachable. Otherwise a
193
+ # spoofed image could be saved without being checked.
194
+ def read_attached(attached, max_length = nil)
195
+ blob = attached.blob
196
+ return read_blob(blob, max_length) if blob&.persisted?
197
+
198
+ read_pending_attachable(attached, max_length)
199
+ end
200
+
201
+ # ActiveStorage keeps the attachable of a pending attachment in the record's
202
+ # attachment changes until the blob is uploaded when the record is saved, so
203
+ # its contents can already be read during the validation.
204
+ def read_pending_attachable(attached, max_length = nil)
205
+ attachable = pending_attachable(attached)
206
+ if uploaded_file?(attachable) || attachable.is_a?(File)
207
+ File.open(attachable.path, "rb") { |io| read_io(io, max_length) }
208
+ elsif attachable.is_a?(Pathname)
209
+ File.open(attachable.to_path, "rb") { |io| read_io(io, max_length) }
210
+ elsif attachable.is_a?(Hash)
211
+ read_io_without_consuming(attachable[:io], max_length)
212
+ end
213
+ end
214
+
215
+ def pending_attachable(attached)
216
+ change = attached.record.try(:attachment_changes)&.[](attached.name.to_s)
217
+ change.try(:attachable)
218
+ end
219
+
220
+ # Reads the contents without consuming the IO, so ActiveStorage can still
221
+ # upload it after the validation.
222
+ def read_io_without_consuming(io, max_length = nil)
223
+ return unless io.respond_to?(:read) && io.respond_to?(:rewind)
224
+
225
+ io.rewind
226
+ content = read_io(io, max_length)
227
+ io.rewind
228
+ content
229
+ end
230
+
231
+ # Reads the whole contents of the blob, or only its leading bytes when a
232
+ # maximum length is given to keep the operation cheap for large files. The
233
+ # services return nil when the blob is empty, which is normalized to an
234
+ # empty content so that empty blobs are rejected. Falls back to opening the
235
+ # whole blob when the service does not support ranged downloads.
236
+ def read_blob(blob, max_length = nil)
237
+ return blob.download if max_length.nil?
238
+
239
+ blob.download_chunk(0...max_length) || ""
240
+ rescue NotImplementedError
241
+ blob.open { |io| read_io(io, max_length) }
242
+ end
243
+
244
+ # The image format detected from the file signature, or nil when the
245
+ # signature does not match any of the supported image formats.
246
+ def image_format(signature)
247
+ return nil if signature.blank?
248
+
249
+ binary_image_format(signature) || ftyp_image_format(signature) || svg_format(signature)
250
+ end
251
+
252
+ def binary_image_format(signature)
253
+ BINARY_IMAGE_SIGNATURES.find do |_format, pairs|
254
+ pairs.all? { |offset, bytes| signature[offset, bytes.bytesize] == bytes }
255
+ end&.first
256
+ end
257
+
258
+ # The format of an ISOBMFF-based image (HEIF or AVIF). These files start with
259
+ # a "ftyp" box at offset 4 followed by a major brand at offset 8.
260
+ def ftyp_image_format(signature)
261
+ return nil unless signature[4, 4] == "ftyp".b
262
+
263
+ FTYPE_IMAGE_BRANDS.find { |_format, brands| brands.include?(signature[8, 4]) }&.first
264
+ end
265
+
266
+ def svg_format(signature)
267
+ content = signature.sub(/\A\xEF\xBB\xBF/n, "").lstrip
268
+ return nil unless SVG_SIGNATURES.any? { |svg_signature| content.start_with?(svg_signature) }
269
+
270
+ :svg
271
+ end
272
+
273
+ def uploaded_file?(file)
274
+ return true if defined?(Rack::Test::UploadedFile) && file.is_a?(Rack::Test::UploadedFile)
275
+
276
+ file.is_a?(ActionDispatch::Http::UploadedFile)
277
+ end
278
+ 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>
@@ -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>
@@ -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
@@ -544,13 +544,9 @@ ar:
544
544
  affected_user:
545
545
  email_intro: 'تم قبول التعديل لمدة %{amendable_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
546
546
  email_outro: لقد تلقيت هذا الإشعار لأنك مؤلف برقم %{amendable_title}.
547
- email_subject: تم قبول التعديل من %{amendable_title} من %{emendation_author_nickname}
548
- notification_title: تم قبول التعديل <a href="%{emendation_path}"></a> تم إنشاؤه بواسطة <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> لمدة <a href="%{amendable_path}">%{amendable_title}</a>.
549
547
  follower:
550
548
  email_intro: 'تم قبول التعديل لمدة %{amendable_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
551
549
  email_outro: لقد تلقيت هذا الإشعار لأنك تتابع %{amendable_title}. يمكنك إيقاف تلقي الإخطارات باتباع الرابط السابق.
552
- email_subject: تم قبول التعديل من %{amendable_title} من %{emendation_author_nickname}
553
- notification_title: تم قبول التعديل <a href="%{emendation_path}"></a> تم إنشاؤه بواسطة <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> لمدة <a href="%{amendable_path}">%{amendable_title}</a>.
554
550
  amendment_created:
555
551
  affected_user:
556
552
  email_intro: 'تم إنشاء تعديل جديد لـ %{amendable_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
@@ -566,29 +562,20 @@ ar:
566
562
  affected_user:
567
563
  email_intro: 'تم نشر تعديل مرفوض لـ %{amendable_title} باعتباره %{amendable_type}جديد. يمكنك أن ترى ذلك من هذه الصفحة:'
568
564
  email_outro: لقد تلقيت هذا الإشعار لأنك مؤلف برقم %{amendable_title}.
569
- email_subject: تم نشر تعديل من %{emendation_author_nickname} كـ %{amendable_type}جديد
570
- notification_title: A <a href="%{emendation_path}">رفض التعديل</a> لل <a href="%{amendable_path}">%{amendable_title}</a> تم نشره باعتباره الجديدة %{amendable_type} من <a href="%{emendation_author_path}">%{emendation_author_nickname}</a>.
571
565
  follower:
572
566
  email_intro: 'تم نشر تعديل مرفوض لـ %{amendable_title} باعتباره %{amendable_type}جديد. يمكنك أن ترى ذلك من هذه الصفحة:'
573
567
  email_outro: لقد تلقيت هذا الإشعار لأنك تتابع %{amendable_title}. يمكنك إيقاف تلقي الإخطارات باتباع الرابط السابق.
574
- email_subject: تم نشر تعديل من %{emendation_author_nickname} كـ %{amendable_type}جديد
575
- notification_title: A <a href="%{emendation_path}">رفض التعديل</a> لل <a href="%{amendable_path}">%{amendable_title}</a> تم نشره باعتباره الجديدة %{amendable_type} من <a href="%{emendation_author_path}">%{emendation_author_nickname}</a>.
576
568
  amendment_rejected:
577
569
  affected_user:
578
570
  email_intro: 'تم رفض التعديل لمدة %{amendable_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
579
571
  email_outro: لقد تلقيت هذا الإشعار لأنك مؤلف برقم %{amendable_title}.
580
- email_subject: تم رفض التعديل لـ %{amendable_title} من %{emendation_author_nickname}
581
- notification_title: تم رفض التعديل <a href="%{emendation_path}"></a> تم إنشاؤه بواسطة <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> لـ <a href="%{amendable_path}">%{amendable_title}</a>.
582
572
  follower:
583
573
  email_intro: 'تم رفض التعديل لمدة %{amendable_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
584
574
  email_outro: لقد تلقيت هذا الإشعار لأنك تتابع %{amendable_title}. يمكنك إيقاف تلقي الإخطارات باتباع الرابط السابق.
585
- email_subject: تم رفض التعديل لـ %{amendable_title} من %{emendation_author_nickname}
586
- notification_title: تم رفض التعديل <a href="%{emendation_path}"></a> تم إنشاؤه بواسطة <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> لـ <a href="%{amendable_path}">%{amendable_title}</a>.
587
575
  emendation_promoted:
588
576
  follower:
589
577
  email_intro: 'تم نشر تعديل لـ %{amendable_title}. يمكنك رؤيته من هذه الصفحة:'
590
578
  email_outro: لقد تلقيت هذا الإشعار لأنك مؤلف %{amendable_title}.
591
- email_subject: تم نشر تعديل من قبل %{emendation_author_nickname} كـ %{amendable_type} جديد
592
579
  attachments:
593
580
  attachment_created:
594
581
  email_intro: 'تمت إضافة مستند جديد إلى %{resource_title}. يمكنك أن ترى ذلك من هذه الصفحة:'
@@ -619,16 +606,9 @@ ar:
619
606
  notification_title: |-
620
607
  قام مسؤول بإزالة %{resource_type} لأنه تم الإبلاغ عنه كـ %{report_reasons}.</br><i>%{resource_content}</i>
621
608
  users:
622
- profile_updated:
623
- email_intro: و <a href="%{resource_url}">صفحة الملف الشخصي</a> من %{name} (%{nickname})، الذي كنت التالية، وقد تم تحديثها.
624
- email_outro: لقد تلقيت هذا الإشعار لأنك تتابع %{nickname}. يمكنك إيقاف تلقي الإخطارات باتباع الرابط السابق.
625
- email_subject: "%{nickname} تحديث ملفهم الشخصي"
626
- notification_title: و <a href="%{resource_path}">صفحة الملف الشخصي</a> من %{name} (%{nickname})، الذي كنت التالية، وقد تم تحديثها.
627
609
  user_officialized:
628
- email_intro: تم إضفاء الطابع الرسمي على المشارك %{name} (%{nickname}).
629
610
  email_outro: تلقيت هذا الإشعار لأنك مدير في هذه المنظمة.
630
611
  email_subject: "تم إضفاء الطابع الرسمي على %{name}"
631
- notification_title: تم إضفاء الطابع الرسمي على المشارك %{name} (%{nickname}).
632
612
  export_mailer:
633
613
  download_your_data_export:
634
614
  download: تحميل
@@ -643,13 +643,9 @@ bg:
643
643
  affected_user:
644
644
  email_intro: 'Приета е поправка в %{amendable_title}. Можете да я видите на тази страница:'
645
645
  email_outro: Получавате това известие, защото сте автор на %{amendable_title}.
646
- email_subject: Приета е поправка в %{amendable_title} от %{emendation_author_nickname}
647
- notification_title: <a href="%{emendation_path}">Поправката</a> направена от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> е приета за <a href="%{amendable_path}">%{amendable_title}</a>.
648
646
  follower:
649
647
  email_intro: 'Приета е поправка в %{amendable_title}. Можете да я видите на тази страница:'
650
648
  email_outro: Получавате това известие, защото следвате %{amendable_title}. Може да прекратите известията чрез предната връзка.
651
- email_subject: Приета е поправка в %{amendable_title} от %{emendation_author_nickname}
652
- notification_title: <a href="%{emendation_path}">Поправката</a> направена от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> е приета за <a href="%{amendable_path}">%{amendable_title}</a>.
653
649
  amendment_created:
654
650
  affected_user:
655
651
  email_intro: 'Нова поправка е създадена в %{amendable_title}. Можете да я видите на тази страница:'
@@ -665,24 +661,16 @@ bg:
665
661
  affected_user:
666
662
  email_intro: 'Отхвърлена поправка в %{amendable_title} е публикувана като нов %{amendable_type}. Можете да видите на тази страница:'
667
663
  email_outro: Получавате това известие, защото сте автор на %{amendable_title}.
668
- email_subject: Поправка на %{emendation_author_nickname} е публикувана като нов %{amendable_type}
669
- notification_title: Отхвърлената <a href="%{emendation_path}">поправка</a> в <a href="%{amendable_path}">%{amendable_title}</a> беше публикувана като нов %{amendable_type} от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a>.
670
664
  follower:
671
665
  email_intro: 'Отхвърлена поправка в %{amendable_title} е публикувана като нов %{amendable_type}. Можете да видите на тази страница:'
672
666
  email_outro: Получавате това известие, защото следвате %{amendable_title}. Може да прекратите известията чрез предната връзка.
673
- email_subject: Поправка на %{emendation_author_nickname} е публикувана като нов %{amendable_type}
674
- notification_title: Отхвърлената <a href="%{emendation_path}">поправка</a> в <a href="%{amendable_path}">%{amendable_title}</a> беше публикувана като нов %{amendable_type} от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a>.
675
667
  amendment_rejected:
676
668
  affected_user:
677
669
  email_intro: 'Поправка е отхвърлена в %{amendable_title}. Можете да я видите на тази страница:'
678
670
  email_outro: Получавате това известие, защото сте автор на %{amendable_title}.
679
- email_subject: Отхвърлена е поправка в %{amendable_title} от %{emendation_author_nickname}
680
- notification_title: <a href="%{emendation_path}">Поправката</a> направена от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> е отхвърлена за <a href="%{amendable_path}">%{amendable_title}</a>.
681
671
  follower:
682
672
  email_intro: 'Поправка е отхвърлена в %{amendable_title}. Можете да я видите на тази страница:'
683
673
  email_outro: Получавате това известие, защото следвате %{amendable_title}. Може да прекратите известията чрез предната връзка.
684
- email_subject: Отхвърлена е поправка в %{amendable_title} от %{emendation_author_nickname}
685
- notification_title: <a href="%{emendation_path}">Поправката</a> направена от <a href="%{emendation_author_path}">%{emendation_author_nickname}</a> е отхвърлена за <a href="%{amendable_path}">%{amendable_title}</a>.
686
674
  attachments:
687
675
  attachment_created:
688
676
  email_intro: 'Нов документ е създаден в %{resource_title}. Можете да го видите на тази страница:'
@@ -724,16 +712,9 @@ bg:
724
712
  Администратор премахна вашия %{resource_type}, защото е докладван като %{report_reasons}.</br>
725
713
  <i>%{resource_content}</i>
726
714
  users:
727
- profile_updated:
728
- email_intro: Профилната <a href="%{resource_url}">страница</a> на %{name} (%{nickname}), който следвате е обновена.
729
- email_outro: Получавате това известие, защото следвате %{nickname}. Може да прекратите известията чрез предната връзка.
730
- email_subject: "%{nickname} обнови своя профил"
731
- notification_title: Профилната <a href="%{resource_path}">страница</a> на %{name} (%{nickname}), който следвате е обновена.
732
715
  user_officialized:
733
- email_intro: Участник %{name} (%{nickname}) е официален.
734
716
  email_outro: Получихте това известие, защото сте администратор на организацията.
735
717
  email_subject: "%{name} е официализиран"
736
- notification_title: Участник %{name} (%{nickname}) е официализиран.
737
718
  export_mailer:
738
719
  download_your_data_export:
739
720
  download: Сваляне
@@ -1527,7 +1508,6 @@ bg:
1527
1508
  layouts:
1528
1509
  decidim:
1529
1510
  announcements:
1530
- view_less: Покажи по-малко
1531
1511
  view_more: Още информация
1532
1512
  data_consent:
1533
1513
  details: