decidim-core 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/cells/decidim/comments_button_cell.rb +9 -1
- data/app/cells/decidim/content_blocks/participatory_space_main_data/title.erb +1 -14
- data/app/cells/decidim/data_consent/category.erb +20 -33
- data/app/cells/decidim/following_cell.rb +1 -1
- data/app/cells/decidim/footer_topics/show.erb +5 -8
- 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/menu_helper.rb +0 -2
- 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 +3 -2
- 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 +260 -42
- data/app/packs/src/decidim/controllers/accordion/controller.js +136 -17
- data/app/packs/src/decidim/controllers/autofocus/controller.js +30 -0
- data/app/packs/src/decidim/controllers/dropdown/controller.js +6 -0
- data/app/packs/src/decidim/controllers/dropdown/dropdown.test.js +28 -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/main_menu/controller.js +4 -0
- data/app/packs/src/decidim/controllers/main_menu/main_menu.test.js +23 -0
- 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/controllers/password_toggler/controller.js +22 -1
- data/app/packs/src/decidim/controllers/password_toggler/password_toggler.test.js +11 -20
- 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/direct_uploads/upload_modal.js +2 -1
- 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/map/controller.js +10 -1
- 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/refactor/moved/focus_guard.js +12 -12
- data/app/packs/src/decidim/refactor/moved/focus_guard.test.js +136 -0
- data/app/packs/src/decidim/utilities/dom.js +2 -2
- data/app/packs/stylesheets/decidim/_cards.scss +1 -1
- data/app/packs/stylesheets/decidim/_content_blocks.scss +4 -10
- data/app/packs/stylesheets/decidim/_conversations.scss +0 -8
- data/app/packs/stylesheets/decidim/_cookies.scss +13 -5
- data/app/packs/stylesheets/decidim/_documents.scss +1 -1
- data/app/packs/stylesheets/decidim/_footer.scss +33 -1
- 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 +12 -2
- 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/footer_menu_presenter.rb +2 -4
- 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 +14 -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/searches/_filters.html.erb +1 -1
- 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/footer/_main.html.erb +1 -1
- data/app/views/layouts/decidim/footer/_main_links.html.erb +56 -28
- 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 +9 -1
- data/config/locales/ca.yml +16 -6
- data/config/locales/cs.yml +31 -1
- data/config/locales/de.yml +99 -3
- data/config/locales/en.yml +11 -1
- data/config/locales/es-MX.yml +11 -1
- data/config/locales/es-PY.yml +11 -1
- data/config/locales/es.yml +15 -5
- data/config/locales/eu.yml +166 -161
- data/config/locales/fi-plain.yml +9 -1
- data/config/locales/fi.yml +9 -1
- data/config/locales/fr-CA.yml +12 -1
- data/config/locales/fr.yml +12 -1
- data/config/locales/gl.yml +1485 -33
- data/config/locales/hu.yml +0 -1
- data/config/locales/ja.yml +28 -1
- data/config/locales/pl.yml +2 -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/20250217192438_convert_user_groups_into_users.rb +3 -2
- data/db/migrate/20260909000000_add_failed_attempts_and_locked_at_to_decidim_authorizations.rb +8 -0
- data/decidim-core.gemspec +4 -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/access_mode_restricted_participatory_spaces.rb +8 -5
- data/lib/decidim/core/test/shared_examples/access_mode_transparent_participatory_spaces.rb +14 -11
- data/lib/decidim/core/test/shared_examples/comments_examples.rb +26 -0
- 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 +10 -1
- 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
- data/lib/decidim/map/dynamic_map.rb +3 -1
- metadata +33 -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) %>";
|
|
@@ -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"
|
|
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
|
-
|
|
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 %>
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
<%=
|
|
50
|
+
<nav class="footer__nav" role="navigation" aria-label="<%= t("layouts.decidim.footer.help") %>">
|
|
51
|
+
<button type="button" id="panel-help-trigger" data-controls="panel-help" data-open-lg="true">
|
|
52
|
+
<span class="h4"><%= t("layouts.decidim.footer.help") %></span>
|
|
53
|
+
<%= icon "arrow-down-s-line" %>
|
|
54
|
+
</button>
|
|
55
|
+
<div id="panel-help" aria-hidden="true">
|
|
56
|
+
<%= cell("decidim/footer_topics", nil) %>
|
|
57
|
+
</div>
|
|
58
|
+
</nav>
|
|
59
|
+
</div>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
<span><%= t("decidim.menu.home") %></span>
|
|
3
3
|
<% end %>
|
|
4
4
|
<% truncatable_items = breadcrumb_items.compact_blank.drop(1) %>
|
|
5
|
+
<% current_page_item = breadcrumb_items.compact_blank.select { |breadcrumb_item| breadcrumb_item[:active] }.last %>
|
|
5
6
|
<% breadcrumb_items.each do |item| %>
|
|
6
7
|
<% next if item.blank? %>
|
|
7
8
|
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
<% end %>
|
|
17
18
|
<span aria-hidden="true"><%= icon "arrow-right-s-line", class: "fill-white" %></span>
|
|
18
19
|
<% if item[:url].present? && !is_active_link?(item[:url], :exclusive) %>
|
|
19
|
-
<%= link_to item[:url], class: trigger_classes.join(" "), data: (truncatable ? { breadcrumb_truncate_target: "item" } : {}), "aria-current": (item
|
|
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"
|
|
@@ -1205,6 +1206,8 @@ ca-IT:
|
|
|
1205
1206
|
dynamic:
|
|
1206
1207
|
screen_reader_explanation: El següent element és un mapa que presenta els components d'aquesta pàgina com a punts al mapa. L'element es pot fer servir amb un lector de pantalla però pot ser difícil d'entendre.
|
|
1207
1208
|
skip_button: Saltar el mapa
|
|
1209
|
+
zoom_in: Ampliar
|
|
1210
|
+
zoom_out: Allunyar
|
|
1208
1211
|
static:
|
|
1209
1212
|
latlng_text: 'latitud: %{latitude}, longitud: %{longitude}'
|
|
1210
1213
|
map_service_brand: OpenStreetMap
|
|
@@ -1546,6 +1549,10 @@ ca-IT:
|
|
|
1546
1549
|
never_logged_in: mai ha iniciat sessió
|
|
1547
1550
|
subject: Avís de compte inactiu
|
|
1548
1551
|
participatory_space:
|
|
1552
|
+
labels:
|
|
1553
|
+
reference: Referència de l'espai de participació
|
|
1554
|
+
restricted: Espai de participació restringit
|
|
1555
|
+
transparent: Espai de participació transparent
|
|
1549
1556
|
pages:
|
|
1550
1557
|
user_profile:
|
|
1551
1558
|
member_of:
|
|
@@ -1704,6 +1711,8 @@ ca-IT:
|
|
|
1704
1711
|
login_modal:
|
|
1705
1712
|
please_log_in: Si us plau, inicia la sessió
|
|
1706
1713
|
sign_up: Crea un compte
|
|
1714
|
+
mentions:
|
|
1715
|
+
search_prompt: Escriu aquí per cercar entre les participants
|
|
1707
1716
|
mentions_modal:
|
|
1708
1717
|
remove_recipient: Eliminar destinatària %{name}
|
|
1709
1718
|
progress: Progrés
|
|
@@ -2086,7 +2095,6 @@ ca-IT:
|
|
|
2086
2095
|
layouts:
|
|
2087
2096
|
decidim:
|
|
2088
2097
|
announcements:
|
|
2089
|
-
view_less: Mostra menys
|
|
2090
2098
|
view_more: Més informació
|
|
2091
2099
|
data_consent:
|
|
2092
2100
|
details:
|