decidim-core 0.32.1 → 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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
  3. data/app/cells/decidim/following_cell.rb +1 -1
  4. data/app/cells/decidim/member/show.erb +1 -1
  5. data/app/cells/decidim/nav_links/show.erb +3 -1
  6. data/app/cells/decidim/share_text_widget/modal.erb +1 -0
  7. data/app/cells/decidim/share_widget/modal.erb +1 -0
  8. data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
  9. data/app/commands/decidim/create_omniauth_registration.rb +17 -2
  10. data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
  11. data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
  12. data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
  13. data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
  14. data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
  15. data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
  16. data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
  17. data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
  18. data/app/controllers/concerns/decidim/paginable.rb +7 -3
  19. data/app/controllers/concerns/decidim/payload_info.rb +2 -0
  20. data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
  21. data/app/controllers/decidim/application_controller.rb +0 -6
  22. data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
  23. data/app/controllers/decidim/components/base_controller.rb +2 -0
  24. data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
  25. data/app/controllers/decidim/favicon_controller.rb +9 -0
  26. data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
  27. data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
  28. data/app/controllers/decidim/homepage_controller.rb +4 -1
  29. data/app/controllers/decidim/likes_controller.rb +1 -1
  30. data/app/controllers/decidim/links_controller.rb +2 -0
  31. data/app/controllers/decidim/manifests_controller.rb +2 -0
  32. data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
  33. data/app/controllers/decidim/newsletters_controller.rb +4 -0
  34. data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
  35. data/app/controllers/decidim/offline_controller.rb +5 -1
  36. data/app/controllers/decidim/open_data_controller.rb +5 -1
  37. data/app/controllers/decidim/pages_controller.rb +31 -3
  38. data/app/controllers/decidim/private_downloads_controller.rb +2 -0
  39. data/app/controllers/decidim/profiles_controller.rb +19 -1
  40. data/app/controllers/decidim/qr_controller.rb +3 -1
  41. data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
  42. data/app/controllers/decidim/searches_controller.rb +2 -0
  43. data/app/controllers/decidim/short_links_controller.rb +4 -0
  44. data/app/controllers/decidim/static_map_controller.rb +2 -0
  45. data/app/controllers/decidim/timeouts_controller.rb +4 -0
  46. data/app/controllers/decidim/tos_controller.rb +2 -0
  47. data/app/controllers/decidim/upload_validations_controller.rb +4 -2
  48. data/app/controllers/decidim/user_activities_controller.rb +3 -0
  49. data/app/forms/decidim/upload_validation_form.rb +16 -10
  50. data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
  51. data/app/helpers/decidim/decidim_form_helper.rb +1 -1
  52. data/app/helpers/decidim/sanitize_helper.rb +11 -1
  53. data/app/jobs/decidim/application_job.rb +4 -0
  54. data/app/models/decidim/authorization.rb +24 -0
  55. data/app/models/decidim/oauth_application.rb +1 -0
  56. data/app/models/decidim/user.rb +1 -0
  57. data/app/models/decidim/user_base_entity.rb +1 -1
  58. data/app/packs/src/decidim/callout.js +10 -0
  59. data/app/packs/src/decidim/callout.test.js +32 -0
  60. data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
  61. data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
  62. data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
  63. data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
  64. data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
  65. data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
  66. data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
  67. data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
  68. data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
  69. data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
  70. data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
  71. data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
  72. data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
  73. data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
  74. data/app/packs/src/decidim/editor/index.js +4 -0
  75. data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
  76. data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
  77. data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
  78. data/app/packs/src/decidim/utilities/dom.js +2 -2
  79. data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
  80. data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
  81. data/app/packs/stylesheets/decidim/_modal.scss +16 -8
  82. data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
  83. data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
  84. data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
  85. data/app/packs/stylesheets/decidim/_typography.scss +5 -2
  86. data/app/packs/stylesheets/decidim/application.scss +1 -0
  87. data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
  88. data/app/permissions/decidim/permissions.rb +20 -0
  89. data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
  90. data/app/presenters/decidim/log/base_presenter.rb +10 -1
  91. data/app/presenters/decidim/log/user_presenter.rb +2 -2
  92. data/app/presenters/decidim/menu_item_presenter.rb +1 -5
  93. data/app/presenters/decidim/resource_presenter.rb +11 -0
  94. data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -1
  95. data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
  96. data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
  97. data/app/services/decidim/svg_content_validator.rb +554 -0
  98. data/app/uploaders/decidim/application_uploader.rb +2 -2
  99. data/app/validators/uploader_image_content_validator.rb +276 -0
  100. data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
  101. data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
  102. data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
  103. data/app/views/decidim/shared/_login_modal.html.erb +12 -14
  104. data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
  105. data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
  106. data/config/initializers/rack_attack.rb +10 -0
  107. data/config/locales/bg.yml +0 -1
  108. data/config/locales/ca-IT.yml +3 -1
  109. data/config/locales/ca.yml +10 -6
  110. data/config/locales/cs.yml +15 -1
  111. data/config/locales/de.yml +99 -3
  112. data/config/locales/en.yml +5 -1
  113. data/config/locales/es-MX.yml +5 -1
  114. data/config/locales/es-PY.yml +5 -1
  115. data/config/locales/es.yml +9 -5
  116. data/config/locales/eu.yml +161 -160
  117. data/config/locales/fi-plain.yml +3 -1
  118. data/config/locales/fi.yml +3 -1
  119. data/config/locales/fr-CA.yml +5 -1
  120. data/config/locales/fr.yml +5 -1
  121. data/config/locales/gl.yml +1485 -33
  122. data/config/locales/hu.yml +0 -1
  123. data/config/locales/ja.yml +3 -1
  124. data/config/locales/pl.yml +0 -1
  125. data/config/locales/pt-BR.yml +0 -1
  126. data/config/locales/ro-RO.yml +0 -1
  127. data/config/locales/sk.yml +2 -1
  128. data/config/locales/sv.yml +68 -12
  129. data/config/locales/tr-TR.yml +0 -2
  130. data/config/routes.rb +11 -2
  131. data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
  132. data/decidim-core.gemspec +1 -1
  133. data/lib/decidim/api/functions/user_entity_list.rb +1 -0
  134. data/lib/decidim/api/types/user_moderation_type.rb +2 -2
  135. data/lib/decidim/content_parsers/user_parser.rb +1 -1
  136. data/lib/decidim/content_parsers.rb +0 -1
  137. data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
  138. data/lib/decidim/content_renderers/user_renderer.rb +15 -4
  139. data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
  140. data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
  141. data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
  142. data/lib/decidim/core/version.rb +1 -1
  143. data/lib/decidim/core.rb +9 -0
  144. data/lib/decidim/form_builder.rb +9 -1
  145. data/lib/decidim/has_upload_validations.rb +1 -0
  146. data/lib/decidim/legacy_form_builder.rb +4 -0
  147. metadata +12 -7
  148. data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
  149. 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) %>";
@@ -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 %>
@@ -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"
@@ -1710,6 +1711,8 @@ ca-IT:
1710
1711
  login_modal:
1711
1712
  please_log_in: Si us plau, inicia la sessió
1712
1713
  sign_up: Crea un compte
1714
+ mentions:
1715
+ search_prompt: Escriu aquí per cercar entre les participants
1713
1716
  mentions_modal:
1714
1717
  remove_recipient: Eliminar destinatària %{name}
1715
1718
  progress: Progrés
@@ -2092,7 +2095,6 @@ ca-IT:
2092
2095
  layouts:
2093
2096
  decidim:
2094
2097
  announcements:
2095
- view_less: Mostra menys
2096
2098
  view_more: Més informació
2097
2099
  data_consent:
2098
2100
  details:
@@ -252,6 +252,7 @@ ca:
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"
@@ -855,6 +856,8 @@ ca:
855
856
  errors:
856
857
  files:
857
858
  file_cannot_be_processed: No s'ha pogut processar el fitxer
859
+ file_contains_unsafe_content: El fitxer conté contingut no segur
860
+ file_is_not_a_valid_image: El fitxer no és una imatge vàlida
858
861
  file_resolution_too_large: La resolució de l'arxiu és massa gran
859
862
  not_inside_organization: L'arxiu no està vinculat a cap organització.
860
863
  internal_server_error:
@@ -960,12 +963,12 @@ ca:
960
963
  member_added:
961
964
  published:
962
965
  email_intro: Se t'ha afegit com a membre a un espai de participació.
963
- email_outro: Heu rebut aquesta notificació perquè una administradora us ha afegit a <a href="%{resource_url}">"%{resource_title}"</a>. El vostre perfil es mostrarà a la <a href="%{members_page}">llista de membres</a> d'aquest espai.<br> Si l'accés a aquest espai és restringit, hi podràs accedir sempre que hagis iniciat la teva sessió amb el teu compte.
966
+ email_outro: Has rebut aquesta notificació perquè una administradora t'ha afegit a <a href="%{resource_url}">"%{resource_title}"</a>. El teu perfil es mostrarà a la <a href="%{members_page}">llista de membres</a> d'aquest espai.<br> Si l'accés a aquest espai és restringit, hi podràs accedir sempre que hagis iniciat la teva sessió amb el teu compte.
964
967
  email_subject: Ara tens accés a "%{resource_title}".
965
968
  notification_title: Ara tens accés a <a href="%{resource_url}">"%{resource_title}"</a>.
966
969
  unpublished:
967
970
  email_intro: Se t'ha afegit com a membre a un espai de participació.
968
- email_outro: Heu rebut aquesta notificació perquè una administradora us ha afegit a <a href="%{resource_url}">"%{resource_title}"</a>. Si l'accés a aquest espai és restringit, hi podràs accedir sempre que hagis iniciat la teva sessió amb el teu compte.
971
+ email_outro: Has rebut aquesta notificació perquè una administradora t'ha afegit a <a href="%{resource_url}">"%{resource_title}"</a>. Si l'accés a aquest espai és restringit, hi podràs accedir sempre que hagis iniciat la teva sessió amb el teu compte.
969
972
  email_subject: Ara tens accés a "%{resource_title}".
970
973
  notification_title: Ara tens accés a <a href="%{resource_url}">"%{resource_title}"</a>.
971
974
  reports:
@@ -1562,7 +1565,7 @@ ca:
1562
1565
  participatory_spaces:
1563
1566
  show:
1564
1567
  restricted_space: Aquest és un espai restringit. Només membres i administradores el poden veure i participar-hi.
1565
- transparent_space: Aquest és un espai restringit però transparent. Qualsevol pot veure'n el contingut, però només les membres i administradores hi poden participar.
1568
+ transparent_space: Aquest és un espai transparent. Qualsevol pot veure'n el contingut, però només les membres i administradores hi poden participar.
1566
1569
  passwords:
1567
1570
  update:
1568
1571
  error: Hi ha hagut un problema en actualitzar la contrasenya.
@@ -1710,6 +1713,8 @@ ca:
1710
1713
  login_modal:
1711
1714
  please_log_in: Si us plau, inicia la sessió
1712
1715
  sign_up: Crea un compte
1716
+ mentions:
1717
+ search_prompt: Escriu aquí per cercar entre les participants
1713
1718
  mentions_modal:
1714
1719
  remove_recipient: Eliminar destinatària %{name}
1715
1720
  progress: Progrés
@@ -1880,10 +1885,10 @@ ca:
1880
1885
  ignore: |-
1881
1886
  Si no vols acceptar la invitació, si us plau, ignora aquest correu electrònic.<br /> El teu compte no es crearà fins que no accedeixis a l'enllaç anterior i estableixis el teu àlies i contrasenya.
1882
1887
  invited_you_as_admin: "%{invited_by} t'ha convidat com a administradora de %{application}. Pots acceptar-ho a través de l'enllaç següent."
1883
- invited_you_as_member: "%{invited_by} t'ha convidat com a membre de %{application}. Pots acceptar-ho a través de l'enllaç següent."
1888
+ invited_you_as_member: "%{invited_by} t'ha convidat com a membre de %{application}. Pots acceptar-ho a través del següent enllaç."
1884
1889
  someone_invited_you: Algú t'ha convidat a %{application}. Pots acceptar-ho a través de l'enllaç següent.
1885
1890
  someone_invited_you_as_admin: Algú t'ha convidat com a administradora de %{application}, pots acceptar-lo a través de l'enllaç següent.
1886
- someone_invited_you_as_member: Algú t'ha convidat com a membre de %{application}, pots acceptar-ho a través de l'enllaç següent.
1891
+ someone_invited_you_as_member: Algú t'ha convidat com a membre de %{application}, pots acceptar-ho a través del següent enllaç.
1887
1892
  subject: Instruccions d'invitació
1888
1893
  invite_admin:
1889
1894
  subject: T'han convidat a administrar %{organization}
@@ -2092,7 +2097,6 @@ ca:
2092
2097
  layouts:
2093
2098
  decidim:
2094
2099
  announcements:
2095
- view_less: Mostra menys
2096
2100
  view_more: Més informació
2097
2101
  data_consent:
2098
2102
  details:
@@ -988,6 +988,12 @@ cs:
988
988
  member_added:
989
989
  published:
990
990
  email_intro: Byli jste přidáni jako člen do participačního prostoru.
991
+ email_subject: Nyní máte přístup k %{resource_title}.
992
+ notification_title: Nyní máte přístup k <a href="%{resource_url}">%{resource_title}</a>.
993
+ unpublished:
994
+ email_intro: Byli jste přidáni jako člen do participačního prostoru.
995
+ email_outro: Obdrželi jste toto oznámení, protože vás správce přidal do <a href="%{resource_url}">%{resource_title}</a>. Pokud je přístup do tohoto prostoru omezený, budete mít přístup k němu pomocí vašeho účtu.
996
+ email_subject: Nyní máte přístup k %{resource_title}.
991
997
  reports:
992
998
  parent_hidden:
993
999
  email_outro: Obdrželi jste toto oznámení, protože jste autorem ovlivněného %{resource_type}.
@@ -1189,6 +1195,7 @@ cs:
1189
1195
  access_modes:
1190
1196
  restricted: Omezený
1191
1197
  transparent: Transparentní
1198
+ not_found: Přístupový režim %{id} nebyl nalezen
1192
1199
  area_presenter:
1193
1200
  not_found: 'Oblast nebyla nalezena v databázi (ID: %{id})'
1194
1201
  area_type_presenter:
@@ -1207,6 +1214,8 @@ cs:
1207
1214
  dynamic:
1208
1215
  screen_reader_explanation: Následující prvek je mapa, která prezentuje položky na této stránce jako mapové body. Prvek lze použít s čtečkou obrazovky, ale může být těžko srozumitelný.
1209
1216
  skip_button: Přeskočit mapu
1217
+ zoom_in: Přiblížit
1218
+ zoom_out: Oddálit
1210
1219
  static:
1211
1220
  latlng_text: 'zeměpisná šířka: %{latitude}, zeměpisná délka: %{longitude}'
1212
1221
  map_service_brand: OpenStreetMap
@@ -1543,6 +1552,10 @@ cs:
1543
1552
  never_logged_in: nikdy se nepřihlásil
1544
1553
  subject: Upozornění na neaktivní účet
1545
1554
  participatory_space:
1555
+ labels:
1556
+ reference: Odkaz na prostor
1557
+ restricted: Omezený prostor
1558
+ transparent: Transparentní prostor
1546
1559
  pages:
1547
1560
  user_profile:
1548
1561
  member_of:
@@ -1839,6 +1852,7 @@ cs:
1839
1852
  nickname_help: Vaše přezdívka v %{organization}. Může obsahovat pouze písmena, číslice, '-' a '_'.
1840
1853
  submit_button: Uložit
1841
1854
  subtitle: Pokud přijmete pozvánku, nastavte svou přezdívku a heslo.
1855
+ subtitle_no_password: Pokud přijmete pozvánku, nastavte prosím svou přezdívku.
1842
1856
  invitation_removed: Vaše pozvánka byla odstraněna.
1843
1857
  invitation_token_invalid: Předložený token je neplatný!
1844
1858
  new:
@@ -1846,6 +1860,7 @@ cs:
1846
1860
  submit_button: Odeslat pozvánku
1847
1861
  no_invitations_remaining: Žádné zbývající pozvánky
1848
1862
  send_instructions: Pozvánka byla odeslána na %{email}.
1863
+ updated: Pozvánka byla úspěšně přijata. Nyní jste přihlášeni.
1849
1864
  updated_not_active: Pozvánka úspěšně přijata.
1850
1865
  mailer:
1851
1866
  confirmation_instructions:
@@ -2082,7 +2097,6 @@ cs:
2082
2097
  layouts:
2083
2098
  decidim:
2084
2099
  announcements:
2085
- view_less: Zobrazit méně
2086
2100
  view_more: Více informací
2087
2101
  data_consent:
2088
2102
  details: