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.
- checksums.yaml +4 -4
- data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/member/show.erb +1 -1
- data/app/cells/decidim/nav_links/show.erb +3 -1
- data/app/cells/decidim/share_text_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/modal.erb +1 -0
- data/app/cells/decidim/share_widget/qr_code_modal.erb +2 -4
- data/app/commands/decidim/create_omniauth_registration.rb +17 -2
- data/app/controllers/concerns/decidim/devise_authentication_methods.rb +4 -0
- data/app/controllers/concerns/decidim/devise_controllers.rb +2 -0
- data/app/controllers/concerns/decidim/disable_redirection_to_external_host.rb +2 -0
- data/app/controllers/concerns/decidim/filter_resource.rb +2 -0
- data/app/controllers/concerns/decidim/impersonate_users.rb +2 -2
- data/app/controllers/concerns/decidim/locale_switcher.rb +12 -10
- data/app/controllers/concerns/decidim/needs_permission.rb +22 -10
- data/app/controllers/concerns/decidim/onboarding_action_methods.rb +2 -0
- data/app/controllers/concerns/decidim/paginable.rb +7 -3
- data/app/controllers/concerns/decidim/payload_info.rb +2 -0
- data/app/controllers/concerns/decidim/resource_versions_concern.rb +14 -0
- data/app/controllers/decidim/application_controller.rb +0 -6
- data/app/controllers/decidim/authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/components/base_controller.rb +2 -0
- data/app/controllers/decidim/devise/passwords_controller.rb +8 -0
- data/app/controllers/decidim/favicon_controller.rb +9 -0
- data/app/controllers/decidim/free_resource_authorization_modals_controller.rb +2 -0
- data/app/controllers/decidim/gamification/badges_controller.rb +2 -0
- data/app/controllers/decidim/homepage_controller.rb +4 -1
- data/app/controllers/decidim/likes_controller.rb +1 -1
- data/app/controllers/decidim/links_controller.rb +2 -0
- data/app/controllers/decidim/manifests_controller.rb +2 -0
- data/app/controllers/decidim/messaging/conversations_controller.rb +10 -4
- data/app/controllers/decidim/newsletters_controller.rb +4 -0
- data/app/controllers/decidim/notifications_subscriptions_controller.rb +4 -0
- data/app/controllers/decidim/offline_controller.rb +5 -1
- data/app/controllers/decidim/open_data_controller.rb +5 -1
- data/app/controllers/decidim/pages_controller.rb +31 -3
- data/app/controllers/decidim/private_downloads_controller.rb +2 -0
- data/app/controllers/decidim/profiles_controller.rb +19 -1
- data/app/controllers/decidim/qr_controller.rb +3 -1
- data/app/controllers/decidim/resource_autocomplete_controller.rb +2 -0
- data/app/controllers/decidim/searches_controller.rb +2 -0
- data/app/controllers/decidim/short_links_controller.rb +4 -0
- data/app/controllers/decidim/static_map_controller.rb +2 -0
- data/app/controllers/decidim/timeouts_controller.rb +4 -0
- data/app/controllers/decidim/tos_controller.rb +2 -0
- data/app/controllers/decidim/upload_validations_controller.rb +4 -2
- data/app/controllers/decidim/user_activities_controller.rb +3 -0
- data/app/forms/decidim/upload_validation_form.rb +16 -10
- data/app/helpers/concerns/decidim/user_role_checker.rb +35 -10
- data/app/helpers/decidim/decidim_form_helper.rb +1 -1
- data/app/helpers/decidim/sanitize_helper.rb +11 -1
- data/app/jobs/decidim/application_job.rb +4 -0
- data/app/models/decidim/authorization.rb +24 -0
- data/app/models/decidim/oauth_application.rb +1 -0
- data/app/models/decidim/user.rb +1 -0
- data/app/models/decidim/user_base_entity.rb +1 -1
- data/app/packs/src/decidim/callout.js +10 -0
- data/app/packs/src/decidim/callout.test.js +32 -0
- data/app/packs/src/decidim/controllers/accordion/accordion.test.js +64 -0
- data/app/packs/src/decidim/controllers/accordion/controller.js +43 -0
- data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
- data/app/packs/src/decidim/controllers/language_change/controller.js +21 -8
- data/app/packs/src/decidim/controllers/language_change/language_change.test.js +0 -1
- data/app/packs/src/decidim/controllers/mention/controller.js +32 -3
- data/app/packs/src/decidim/controllers/mention/input_mentions.test.js +8 -5
- data/app/packs/src/decidim/controllers/multiple_mentions/input_multiple_mentions.test.js +1 -1
- data/app/packs/src/decidim/datepicker/datepicker_functions.js +41 -25
- data/app/packs/src/decidim/datepicker/generate_datepicker.js +4 -4
- data/app/packs/src/decidim/datepicker/test/datepicker_functions_adjust_picker_position.test.js +64 -0
- data/app/packs/src/decidim/datepicker/test/time.test.js +39 -24
- data/app/packs/src/decidim/editor/common/suggestion.js +32 -22
- data/app/packs/src/decidim/editor/extensions/mention/index.js +14 -3
- data/app/packs/src/decidim/editor/index.js +4 -0
- data/app/packs/src/decidim/editor/test/extensions/mention.test.js +32 -9
- data/app/packs/src/decidim/refactor/implementation/autofocus.js +22 -0
- data/app/packs/src/decidim/refactor/implementation/autofocus.test.js +171 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_modal.scss +16 -8
- data/app/packs/stylesheets/decidim/_modal_share.scss +0 -22
- data/app/packs/stylesheets/decidim/_participatory_spaces.scss +11 -1
- data/app/packs/stylesheets/decidim/_rich_text.scss +12 -9
- data/app/packs/stylesheets/decidim/_typography.scss +5 -2
- data/app/packs/stylesheets/decidim/application.scss +1 -0
- data/app/packs/stylesheets/decidim/legacy/email.scss +0 -5
- data/app/permissions/decidim/permissions.rb +20 -0
- data/app/presenters/decidim/admin_log/component_presenter.rb +1 -1
- data/app/presenters/decidim/log/base_presenter.rb +10 -1
- data/app/presenters/decidim/log/user_presenter.rb +2 -2
- data/app/presenters/decidim/menu_item_presenter.rb +1 -5
- data/app/presenters/decidim/resource_presenter.rb +11 -0
- data/app/resolvers/decidim/meta_image_url_resolver.rb +7 -1
- data/app/serializers/decidim/exporters/open_data_blocked_user_serializer.rb +11 -2
- data/app/serializers/decidim/exporters/open_data_moderation_serializer.rb +25 -1
- data/app/services/decidim/svg_content_validator.rb +554 -0
- data/app/uploaders/decidim/application_uploader.rb +2 -2
- data/app/validators/uploader_image_content_validator.rb +276 -0
- data/app/views/decidim/application/_radio_accordion.html.erb +1 -5
- data/app/views/decidim/messaging/conversations/_add_conversation_users.html.erb +1 -1
- data/app/views/decidim/messaging/conversations/create.js.erb +1 -3
- data/app/views/decidim/shared/_login_modal.html.erb +12 -14
- data/app/views/layouts/decidim/_mailer_logo.html.erb +1 -1
- data/app/views/layouts/decidim/header/_menu_breadcrumb_desktop.html.erb +3 -2
- data/config/initializers/rack_attack.rb +10 -0
- data/config/locales/bg.yml +0 -1
- data/config/locales/ca-IT.yml +3 -1
- data/config/locales/ca.yml +10 -6
- data/config/locales/cs.yml +15 -1
- data/config/locales/de.yml +99 -3
- data/config/locales/en.yml +5 -1
- data/config/locales/es-MX.yml +5 -1
- data/config/locales/es-PY.yml +5 -1
- data/config/locales/es.yml +9 -5
- data/config/locales/eu.yml +161 -160
- data/config/locales/fi-plain.yml +3 -1
- data/config/locales/fi.yml +3 -1
- data/config/locales/fr-CA.yml +5 -1
- data/config/locales/fr.yml +5 -1
- data/config/locales/gl.yml +1485 -33
- data/config/locales/hu.yml +0 -1
- data/config/locales/ja.yml +3 -1
- data/config/locales/pl.yml +0 -1
- data/config/locales/pt-BR.yml +0 -1
- data/config/locales/ro-RO.yml +0 -1
- data/config/locales/sk.yml +2 -1
- data/config/locales/sv.yml +68 -12
- data/config/locales/tr-TR.yml +0 -2
- data/config/routes.rb +11 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +1 -1
- data/lib/decidim/api/functions/user_entity_list.rb +1 -0
- data/lib/decidim/api/types/user_moderation_type.rb +2 -2
- data/lib/decidim/content_parsers/user_parser.rb +1 -1
- data/lib/decidim/content_parsers.rb +0 -1
- data/lib/decidim/content_renderers/mention_resource_renderer.rb +18 -4
- data/lib/decidim/content_renderers/user_renderer.rb +15 -4
- data/lib/decidim/core/test/shared_examples/participatory_space_members_page_examples.rb +77 -0
- data/lib/decidim/core/test/shared_examples/participatory_space_members_shared_examples.rb +58 -0
- data/lib/decidim/core/test/shared_examples/versions_controller_examples.rb +23 -1
- data/lib/decidim/core/version.rb +1 -1
- data/lib/decidim/core.rb +9 -0
- data/lib/decidim/form_builder.rb +9 -1
- data/lib/decidim/has_upload_validations.rb +1 -0
- data/lib/decidim/legacy_form_builder.rb +4 -0
- metadata +12 -7
- data/lib/decidim/content_parsers/inline_images_parser.rb +0 -68
- 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-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
data/config/locales/bg.yml
CHANGED
data/config/locales/ca-IT.yml
CHANGED
|
@@ -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:
|
data/config/locales/ca.yml
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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:
|
data/config/locales/cs.yml
CHANGED
|
@@ -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:
|