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