camaleon_cms 2.9.2 → 2.9.4
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/README.md +17 -4
- data/Rakefile +3 -16
- data/app/apps/plugins/attack/admin_controller.rb +9 -6
- data/app/apps/plugins/attack/attack_helper.rb +35 -25
- data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
- data/app/apps/plugins/front_cache/admin_controller.rb +14 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
- data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
- data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
- data/app/apps/plugins/visibility_post/config/config.json +1 -0
- data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
- data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
- data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
- data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
- data/app/apps/themes/default/assets/css/style.css.scss +24 -0
- data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
- data/app/apps/themes/default/default_helper.rb +2 -1
- data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
- data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
- data/app/apps/themes/new/custom_helper.rb +5 -9
- data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
- data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
- data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
- data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
- data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
- data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
- data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
- data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
- data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
- data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
- data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
- data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
- data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
- data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
- data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
- data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
- data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
- data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
- data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
- data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
- data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
- data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
- data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
- data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
- data/app/decorators/camaleon_cms/post_decorator.rb +68 -50
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
- data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
- data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
- data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
- data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
- data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
- data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
- data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
- data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
- data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
- data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
- data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
- data/app/helpers/camaleon_cms/content_helper.rb +13 -6
- data/app/helpers/camaleon_cms/email_helper.rb +7 -3
- data/app/helpers/camaleon_cms/frontend/application_helper.rb +6 -1
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
- data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
- data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
- data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
- data/app/helpers/camaleon_cms/html_helper.rb +58 -37
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +188 -44
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +34 -13
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
- data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
- data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
- data/app/models/camaleon_cms/ability.rb +15 -7
- data/app/models/camaleon_cms/category.rb +6 -6
- data/app/models/camaleon_cms/custom_field.rb +15 -6
- data/app/models/camaleon_cms/custom_field_group.rb +39 -22
- data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -2
- data/app/models/camaleon_cms/media.rb +8 -2
- data/app/models/camaleon_cms/meta.rb +2 -2
- data/app/models/camaleon_cms/nav_menu.rb +5 -3
- data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
- data/app/models/camaleon_cms/plugin.rb +3 -4
- data/app/models/camaleon_cms/post.rb +137 -27
- data/app/models/camaleon_cms/post_comment.rb +10 -6
- data/app/models/camaleon_cms/post_default.rb +42 -12
- data/app/models/camaleon_cms/post_relationship.rb +6 -4
- data/app/models/camaleon_cms/post_tag.rb +4 -5
- data/app/models/camaleon_cms/post_type.rb +21 -13
- data/app/models/camaleon_cms/site.rb +39 -29
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
- data/app/models/camaleon_cms/theme.rb +2 -4
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +81 -6
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +12 -8
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
- data/app/models/camaleon_record.rb +89 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
- data/app/models/concerns/camaleon_cms/metas.rb +31 -6
- data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
- data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
- data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
- data/app/models/current_request.rb +8 -1
- data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
- data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
- data/app/uploaders/camaleon_cms_uploader.rb +23 -10
- data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
- data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
- data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
- data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
- data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
- data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
- data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
- data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
- data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
- data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
- data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
- data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
- data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
- data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
- data/config/initializers/action_view.rb +42 -5
- data/config/initializers/active_record_extension.rb +13 -27
- data/config/initializers/assets.rb +52 -23
- data/config/initializers/custom_initializers.rb +1 -1
- data/config/locales/camaleon_cms/admin/ar.yml +1 -0
- data/config/locales/camaleon_cms/admin/de.yml +1 -0
- data/config/locales/camaleon_cms/admin/en.yml +36 -0
- data/config/locales/camaleon_cms/admin/es.yml +1 -0
- data/config/locales/camaleon_cms/admin/fr.yml +1 -0
- data/config/locales/camaleon_cms/admin/it.yml +1 -0
- data/config/locales/camaleon_cms/admin/nl.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt.yml +1 -0
- data/config/locales/camaleon_cms/admin/ru.yml +1 -0
- data/config/locales/camaleon_cms/admin/uk.yml +1 -0
- data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- data/lib/camaleon_cms/assets_precompile.rb +86 -0
- data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
- data/lib/camaleon_cms/content_security.rb +158 -0
- data/lib/camaleon_cms/engine.rb +45 -21
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/shortcode_registry.rb +102 -0
- data/lib/camaleon_cms/svg_content_checker.rb +123 -0
- data/lib/camaleon_cms/task_reporter.rb +14 -0
- data/lib/camaleon_cms/unsafe_markup.rb +207 -0
- data/lib/camaleon_cms/uploader_content_security.rb +248 -0
- data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
- data/lib/camaleon_cms/uploader_path_security.rb +146 -0
- data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
- data/lib/camaleon_cms/uploader_support.rb +86 -0
- data/lib/camaleon_cms/version.rb +1 -1
- data/lib/camaleon_cms.rb +23 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +10 -5
- data/lib/ext/translator.rb +10 -2
- data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
- data/lib/generators/camaleon_cms/install_generator.rb +7 -4
- data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
- data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
- data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
- data/lib/plugin_routes.rb +412 -368
- data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
- data/lib/tasks/cross_site_field_groups.rake +74 -0
- data/lib/tasks/custom_fields_roles.rake +15 -13
- data/lib/tasks/orphaned_comments.rake +42 -0
- data/lib/tasks/private_upload_acls.rake +31 -0
- data/lib/tasks/site_custom_field_groups.rake +36 -0
- data/lib/tasks/unsafe_stored_content.rake +42 -0
- data/lib/tasks/unsafe_stored_uploads.rake +70 -0
- data/lib/tasks/user_field_groups.rake +27 -0
- metadata +125 -4
|
@@ -5,16 +5,22 @@ module CamaleonCms
|
|
|
5
5
|
alias_attribute :site_id, :parent_id
|
|
6
6
|
|
|
7
7
|
default_scope do
|
|
8
|
-
where("object_class != '_fields'")
|
|
9
|
-
.reorder("#{CamaleonCms::CustomField.table_name}.field_order ASC")
|
|
8
|
+
where("object_class != '_fields'").reorder("#{CamaleonCms::CustomField.table_name}.field_order ASC")
|
|
10
9
|
end
|
|
11
10
|
|
|
11
|
+
# Scope metas by object_class so a group's numeric id colliding with another model's id does
|
|
12
|
+
# not read the wrong meta rows (meta rows are keyed by both objectid and object_class).
|
|
13
|
+
# rubocop:disable Rails/InverseOf
|
|
12
14
|
has_many :metas, -> { where(object_class: 'CustomFieldGroup') }, foreign_key: :objectid, dependent: :destroy
|
|
15
|
+
# rubocop:enable Rails/InverseOf
|
|
16
|
+
|
|
13
17
|
has_many :fields, -> { where(object_class: '_fields') }, class_name: 'CamaleonCms::CustomField',
|
|
14
|
-
foreign_key: :parent_id, dependent: :destroy
|
|
15
|
-
|
|
18
|
+
foreign_key: :parent_id, dependent: :destroy,
|
|
19
|
+
inverse_of: :custom_field_group
|
|
20
|
+
belongs_to :site, foreign_key: :parent_id, optional: true, inverse_of: :custom_field_groups
|
|
21
|
+
belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
validates :slug, uniqueness: { scope: %i[object_class objectid parent_id] }
|
|
18
24
|
|
|
19
25
|
before_validation :before_validating
|
|
20
26
|
|
|
@@ -52,9 +58,9 @@ module CamaleonCms
|
|
|
52
58
|
end
|
|
53
59
|
alias add_field add_manual_field
|
|
54
60
|
|
|
55
|
-
# return a field with slug = slug from current group
|
|
61
|
+
# return a field with slug = slug from the current group
|
|
56
62
|
def get_field(slug)
|
|
57
|
-
fields.
|
|
63
|
+
fields.find_by_slug(slug) # rubocop:disable Rails/DynamicFindBy
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
# only used by form on admin panel (protected)
|
|
@@ -76,7 +82,7 @@ module CamaleonCms
|
|
|
76
82
|
if id_val.present? && (field_item = fields.find_by(id: id_val)).present?
|
|
77
83
|
# If this is an existing select_eval field (or the incoming data would
|
|
78
84
|
# make it a select_eval) ensure the current actor has explicit
|
|
79
|
-
# permission. For updates we preserve the form-like
|
|
85
|
+
# permission. For updates, we preserve the form-like behaviour by
|
|
80
86
|
# collecting an error-like non-persisted field in errors_saved and
|
|
81
87
|
# skipping the update when unauthorized.
|
|
82
88
|
existing_key = (field_item.options || {})[:field_key].to_s
|
|
@@ -94,7 +100,7 @@ module CamaleonCms
|
|
|
94
100
|
# Check if the incoming options request creation of select_eval
|
|
95
101
|
incoming_key = (options[:field_key] || item[:field_key]).to_s
|
|
96
102
|
if incoming_key == 'select_eval' && !can?(:manage, :select_eval)
|
|
97
|
-
# Add an error-like non-persisted field to errors_saved to preserve
|
|
103
|
+
# Add an error-like non-persisted field to errors_saved to preserve behaviour
|
|
98
104
|
field_item = fields.new(item)
|
|
99
105
|
field_item.errors.add(:base, 'Not authorized to create select_eval field')
|
|
100
106
|
errors_saved << field_item
|
|
@@ -116,6 +122,11 @@ module CamaleonCms
|
|
|
116
122
|
end
|
|
117
123
|
|
|
118
124
|
# generate the caption for this group
|
|
125
|
+
# The caption is rendered with `raw` by admin/settings/custom_fields/index.html.erb, so every
|
|
126
|
+
# interpolated value is escaped here, at the source: `the_title` already returns an escaped
|
|
127
|
+
# SafeBuffer, and the plain attributes below are escaped explicitly. `object_class` is included
|
|
128
|
+
# because the placement check admits any class name paired with the current site's id, so it is
|
|
129
|
+
# attacker-settable text like the model names are.
|
|
119
130
|
def get_caption
|
|
120
131
|
caption = ''
|
|
121
132
|
begin
|
|
@@ -126,16 +137,19 @@ module CamaleonCms
|
|
|
126
137
|
caption = "Fields for Categories in <b>#{site.post_types.find(objectid).decorate.the_title}</b>"
|
|
127
138
|
when 'PostType_PostTag'
|
|
128
139
|
caption = "Fields for Post tags in <b>#{site.post_types.find(objectid).decorate.the_title}</b>"
|
|
129
|
-
when '
|
|
130
|
-
|
|
140
|
+
when 'Main'
|
|
141
|
+
widget_name = CamaleonCms::Widget::Main.find(objectid).name.translate
|
|
142
|
+
caption = "Fields for Widget <b>(#{ERB::Util.html_escape(widget_name)})</b>"
|
|
131
143
|
when 'Theme'
|
|
132
|
-
|
|
144
|
+
theme_name = begin
|
|
133
145
|
site.themes.find(objectid).name
|
|
134
146
|
rescue StandardError
|
|
135
147
|
objectid
|
|
136
|
-
end
|
|
148
|
+
end
|
|
149
|
+
caption = "Field settings for Theme <b>(#{ERB::Util.html_escape(theme_name)})</b>"
|
|
137
150
|
when 'NavMenu'
|
|
138
|
-
|
|
151
|
+
menu_name = CamaleonCms::NavMenu.find(objectid).name
|
|
152
|
+
caption = "Field settings for Menus <b>(#{ERB::Util.html_escape(menu_name)})</b>"
|
|
139
153
|
when 'Site'
|
|
140
154
|
caption = 'Field settings the site'
|
|
141
155
|
when 'PostType'
|
|
@@ -144,25 +158,28 @@ module CamaleonCms
|
|
|
144
158
|
p = CamaleonCms::Post.find(objectid).decorate
|
|
145
159
|
caption = "Fields for content <b>(#{p.the_title})</b>"
|
|
146
160
|
else # 'Plugin' or other class
|
|
147
|
-
caption = "Fields for <b>#{object_class}</b>"
|
|
161
|
+
caption = "Fields for <b>#{ERB::Util.html_escape(object_class)}</b>"
|
|
148
162
|
end
|
|
149
163
|
rescue StandardError => e
|
|
150
164
|
Rails.logger.debug "Camaleon CMS - Menu Item Error: #{e.message} ==> Attrs: #{attributes}"
|
|
151
165
|
end
|
|
152
|
-
|
|
166
|
+
# rubocop:disable Rails/OutputSafety -- every interpolated value above is escaped at the source
|
|
167
|
+
caption.html_safe
|
|
168
|
+
# rubocop:enable Rails/OutputSafety
|
|
153
169
|
end
|
|
154
170
|
|
|
155
171
|
private
|
|
156
172
|
|
|
157
173
|
def before_validating
|
|
158
|
-
self.slug = "_group-#{name.to_s.parameterize}"
|
|
174
|
+
self.slug = "_group-#{name.to_s.parameterize}" if slug.blank?
|
|
159
175
|
end
|
|
160
176
|
|
|
161
177
|
# auto save the default field values
|
|
162
178
|
def auto_save_default_values(field, options)
|
|
163
179
|
class_name = object_class.split('_').first
|
|
164
|
-
return unless %w[Post Category Plugin
|
|
165
|
-
|
|
180
|
+
return unless %w[Post Category Plugin Theme].include?(class_name) &&
|
|
181
|
+
objectid &&
|
|
182
|
+
(options[:default_value].present? || options[:default_values].present?)
|
|
166
183
|
|
|
167
184
|
owner = if class_name == 'Theme'
|
|
168
185
|
"CamaleonCms::#{class_name}".constantize.find(objectid) # owner model
|
|
@@ -170,13 +187,13 @@ module CamaleonCms
|
|
|
170
187
|
begin
|
|
171
188
|
"CamaleonCms::#{class_name}".constantize.find(objectid)
|
|
172
189
|
rescue StandardError
|
|
173
|
-
"CamaleonCms::#{class_name}".constantize.
|
|
190
|
+
"CamaleonCms::#{class_name}".constantize.find_by_slug(objectid) # rubocop:disable Rails/DynamicFindBy
|
|
174
191
|
end
|
|
175
192
|
end
|
|
176
193
|
(options[:default_values] || [options[:default_value]] || []).each do |value|
|
|
177
194
|
if owner.present?
|
|
178
|
-
owner.custom_field_values
|
|
179
|
-
|
|
195
|
+
owner.custom_field_values
|
|
196
|
+
.create!(custom_field_id: field.id, custom_field_slug: field.slug, value: fix_meta_value(value))
|
|
180
197
|
end
|
|
181
198
|
end
|
|
182
199
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
class CustomFieldsRelationship < CamaleonRecord
|
|
3
|
+
include CamaleonCms::ContentShortcodeGate
|
|
4
|
+
|
|
3
5
|
self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}custom_fields_relationships"
|
|
4
6
|
|
|
5
7
|
# attr_accessible :objectid, :custom_field_id, :term_order, :value, :object_class,
|
|
@@ -7,23 +9,159 @@ module CamaleonCms
|
|
|
7
9
|
default_scope { order("#{CamaleonCms::CustomFieldsRelationship.table_name}.term_order ASC") }
|
|
8
10
|
|
|
9
11
|
# relations
|
|
10
|
-
belongs_to :custom_field, class_name: 'CamaleonCms::CustomField',
|
|
12
|
+
belongs_to :custom_field, class_name: 'CamaleonCms::CustomField', optional: true
|
|
13
|
+
belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
|
|
11
14
|
|
|
12
15
|
# validates :objectid, :custom_field_id, presence: true
|
|
13
16
|
validates :custom_field_id, presence: true # error on clone model
|
|
14
17
|
|
|
18
|
+
# Rendered positions that need a save-time gate (audit M17, scan-and-reject policy). An
|
|
19
|
+
# `editor` value is emitted as markup (`raw`); a `field_attrs` value is a JSON pair whose
|
|
20
|
+
# members are emitted as markup; the URI field types are emitted into href/src. Every other
|
|
21
|
+
# field type's value renders through escaping ERB as element content, where markup cannot
|
|
22
|
+
# execute, so it needs no gate.
|
|
23
|
+
MARKUP_FIELD_KEYS = %w[editor].freeze
|
|
24
|
+
JSON_MARKUP_FIELD_KEYS = %w[field_attrs].freeze
|
|
25
|
+
URI_FIELD_KEYS = %w[url image audio video file].freeze
|
|
26
|
+
GATED_FIELD_KEYS = (MARKUP_FIELD_KEYS + JSON_MARKUP_FIELD_KEYS + URI_FIELD_KEYS).freeze
|
|
27
|
+
|
|
28
|
+
validate :reject_untrusted_dangerous_value
|
|
29
|
+
# Any custom-field value is expanded by do_shortcode at render (CustomFieldsConcern#the_field
|
|
30
|
+
# and friends), regardless of field type, so gate a shortcode in ANY value behind
|
|
31
|
+
# content_shortcodes -- broader than the HTML gate above, which only covers markup/URI field
|
|
32
|
+
# types.
|
|
33
|
+
gate_content_shortcodes :value
|
|
34
|
+
|
|
15
35
|
after_save :set_parent_slug
|
|
16
36
|
after_save :update_model_owner # TODO: convert this model into polymorphic
|
|
17
37
|
|
|
38
|
+
# Opt-out for trusted server-side pipelines (imports, seeds, plugin code), mirroring
|
|
39
|
+
# Post#unfiltered_content!: a reader plus a bang enabler and NO writer, so mass assignment
|
|
40
|
+
# cannot reach it. Sticky for the lifetime of the instance.
|
|
41
|
+
attr_reader :unfiltered_value
|
|
42
|
+
|
|
43
|
+
def unfiltered_value!
|
|
44
|
+
@unfiltered_value = true
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Whether this field type's value is emitted into a markup or URL position (so it is gated).
|
|
50
|
+
def gated_field_key?(field_key)
|
|
51
|
+
GATED_FIELD_KEYS.include?(field_key.to_s)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Why the save-time gate would refuse this value for the field type, or nil if it passes:
|
|
55
|
+
# :too_large (over the parse ceiling), :html (disallowed markup), :uri (script-capable URL).
|
|
56
|
+
# Ignores author trust, so the validation (trust-gated) and the scan_content rake task (which
|
|
57
|
+
# reports every stored row) share one dispatch and cannot drift.
|
|
58
|
+
def gate_rejection_reason(field_key, value)
|
|
59
|
+
return if value.blank?
|
|
60
|
+
|
|
61
|
+
key = field_key.to_s
|
|
62
|
+
if MARKUP_FIELD_KEYS.include?(key)
|
|
63
|
+
markup_rejection_reason(value)
|
|
64
|
+
elsif JSON_MARKUP_FIELD_KEYS.include?(key)
|
|
65
|
+
return :too_large if CamaleonCms::UnsafeMarkup.too_large?(value)
|
|
66
|
+
|
|
67
|
+
:html if json_member_values(value).any? { |member| markup_unsafe?(member) }
|
|
68
|
+
elsif URI_FIELD_KEYS.include?(key)
|
|
69
|
+
:uri if CamaleonCms::UnsafeMarkup.dangerous_uri?(value)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def markup_rejection_reason(value)
|
|
76
|
+
return :too_large if CamaleonCms::UnsafeMarkup.too_large?(value)
|
|
77
|
+
|
|
78
|
+
:html if markup_unsafe?(value)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def markup_unsafe?(value)
|
|
82
|
+
CamaleonCms::UnsafeMarkup.unsafe_html?(
|
|
83
|
+
value, tags: CamaleonCms::Post::CONTENT_ALLOWED_TAGS,
|
|
84
|
+
attributes: CamaleonCms::Post::CONTENT_ALLOWED_ATTRIBUTES
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A field_attrs value stores JSON whose string members render verbatim. Scan every DECODED
|
|
89
|
+
# member (Hash, Array or nested), not the stored bytes: the Rails JSON encoder unicode-escapes
|
|
90
|
+
# angle brackets (storing the escape instead of a literal bracket), so a byte-level scan of the
|
|
91
|
+
# stored string would pass markup that JSON.parse restores at render. Unparseable values are
|
|
92
|
+
# scanned as-is (fail closed on whatever the renderer would fall back to).
|
|
93
|
+
def json_member_values(value)
|
|
94
|
+
flatten_json_strings(JSON.parse(value))
|
|
95
|
+
rescue JSON::ParserError
|
|
96
|
+
[value.to_s]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def flatten_json_strings(node)
|
|
100
|
+
case node
|
|
101
|
+
when Hash then node.values.flat_map { |member| flatten_json_strings(member) }
|
|
102
|
+
when Array then node.flat_map { |member| flatten_json_strings(member) }
|
|
103
|
+
else [node.to_s]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
18
108
|
private
|
|
19
109
|
|
|
110
|
+
# Security (audit M17): the frontend renders an `editor`/`field_attrs` value verbatim and
|
|
111
|
+
# URI-type values into URL positions, so a value an untrusted author may not write is refused --
|
|
112
|
+
# never rewritten. Stored values therefore always equal authored values. The scan/dispatch lives
|
|
113
|
+
# in the class-level gate_rejection_reason so the scan_content rake task reuses it verbatim.
|
|
114
|
+
def reject_untrusted_dangerous_value
|
|
115
|
+
return if value.blank? || unfiltered_value
|
|
116
|
+
|
|
117
|
+
field_key = custom_field&.options&.[](:field_key).to_s
|
|
118
|
+
return unless self.class.gated_field_key?(field_key)
|
|
119
|
+
return if author_trusted_for_unfiltered_value?
|
|
120
|
+
|
|
121
|
+
reason = self.class.gate_rejection_reason(field_key, value)
|
|
122
|
+
return unless reason
|
|
123
|
+
|
|
124
|
+
errors.add(:base, cama_rejection_message(rejection_message_key(reason)))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def rejection_message_key(reason)
|
|
128
|
+
case reason
|
|
129
|
+
when :too_large then 'value_too_large'
|
|
130
|
+
when :uri then 'value_rejected_uri'
|
|
131
|
+
else 'value_rejected_html'
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The message must never be swallowed by a missing translation: only en.yml carries these keys,
|
|
136
|
+
# while the process locale follows the current admin/site language — fall back to English.
|
|
137
|
+
def cama_rejection_message(key)
|
|
138
|
+
full_key = "camaleon_cms.admin.custom_field.message.#{key}"
|
|
139
|
+
I18n.t(full_key, slug: custom_field_slug,
|
|
140
|
+
default: I18n.t(full_key, slug: custom_field_slug, locale: :en))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Trust follows the post-content model: an admin may write anything; a post's field values may
|
|
144
|
+
# skip the gate when the saver holds post_content_unfiltered_html for the post type. Fails
|
|
145
|
+
# closed (the gate applies) without a request context — benign values still pass the scan.
|
|
146
|
+
def author_trusted_for_unfiltered_value?
|
|
147
|
+
user = CurrentRequest.user
|
|
148
|
+
site = CurrentRequest.site
|
|
149
|
+
return false if user.blank? || site.blank?
|
|
150
|
+
return true if user.admin?
|
|
151
|
+
|
|
152
|
+
parent = owner
|
|
153
|
+
return false unless parent.is_a?(CamaleonCms::Post) && parent.post_type.present?
|
|
154
|
+
|
|
155
|
+
CamaleonCms::Ability.new(user, site).can?(:post_content_unfiltered_html, parent.post_type)
|
|
156
|
+
end
|
|
157
|
+
|
|
20
158
|
def set_parent_slug
|
|
21
159
|
# self.update_column('custom_field_slug', self.custom_fields.slug)
|
|
22
160
|
end
|
|
23
161
|
|
|
24
162
|
# touch owner model
|
|
25
163
|
def update_model_owner
|
|
26
|
-
"CamaleonCms::#{object_class}".constantize.find(objectid).touch
|
|
164
|
+
"CamaleonCms::#{object_class}".constantize.find(objectid).touch # rubocop:disable Rails/SkipsModelValidations
|
|
27
165
|
rescue StandardError
|
|
28
166
|
nil
|
|
29
167
|
end
|
|
@@ -2,7 +2,7 @@ module CamaleonCms
|
|
|
2
2
|
class Media < CamaleonRecord
|
|
3
3
|
self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}media"
|
|
4
4
|
|
|
5
|
-
belongs_to :site,
|
|
5
|
+
belongs_to :site, optional: true
|
|
6
6
|
validates :name, uniqueness: {
|
|
7
7
|
scope: %i[site_id is_folder folder_path is_public],
|
|
8
8
|
message: 'Duplicates not allowed'
|
|
@@ -23,7 +23,7 @@ module CamaleonCms
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
# search file or folder by key
|
|
26
|
-
def self.
|
|
26
|
+
def self.by_key(key)
|
|
27
27
|
key = key.cama_fix_media_key
|
|
28
28
|
if key == '/'
|
|
29
29
|
where(folder_path: File.dirname(key))
|
|
@@ -32,6 +32,12 @@ module CamaleonCms
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# legacy public name (pre-rename callers); the media table has no `key` column, so
|
|
36
|
+
# without the alias this falls through to Rails' dynamic finder and raises
|
|
37
|
+
class << self
|
|
38
|
+
alias find_by_key by_key
|
|
39
|
+
end
|
|
40
|
+
|
|
35
41
|
# return all items of current folder
|
|
36
42
|
def items
|
|
37
43
|
coll = is_public ? site.public_media : site.private_media
|
|
@@ -3,8 +3,8 @@ module CamaleonCms
|
|
|
3
3
|
self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}metas"
|
|
4
4
|
# attr_accessible :objectid, :key, :value, :object_class
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
extend CamaleonCms::NormalizeAttrs
|
|
9
9
|
end
|
|
10
10
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
class NavMenu < CamaleonCms::TermTaxonomy
|
|
3
|
-
normalize_attrs(:
|
|
3
|
+
normalize_attrs(:description)
|
|
4
|
+
|
|
5
|
+
# deterministic external iteration order (2.9.2 parity); render paths reorder(:term_order)
|
|
6
|
+
default_scope { order(id: :asc) }
|
|
4
7
|
|
|
5
|
-
default_scope { where(taxonomy: :nav_menu).order(id: :asc) }
|
|
6
8
|
alias_attribute :site_id, :parent_id
|
|
7
9
|
|
|
8
10
|
has_many :children, class_name: 'CamaleonCms::NavMenuItem', foreign_key: :parent_id, dependent: :destroy,
|
|
9
11
|
inverse_of: :parent
|
|
10
|
-
belongs_to :site, foreign_key: :parent_id, inverse_of: :nav_menus,
|
|
12
|
+
belongs_to :site, foreign_key: :parent_id, inverse_of: :nav_menus, optional: true
|
|
11
13
|
|
|
12
14
|
# add menu item for current menu
|
|
13
15
|
# value: (Hash) is a hash object that contains label, type, link
|
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
class NavMenuItem < CamaleonCms::TermTaxonomy
|
|
3
|
+
# NavMenuItem#name (the menu label) is the one "name" field that is rendered as raw/trusted HTML
|
|
4
|
+
# (see CamaleonCms::Frontend::NavMenuHelper#cama_menu_draw_items), unlike other model names which are
|
|
5
|
+
# ERB-auto-escaped. It therefore keeps save-time sanitization: safe formatting (icons, spans) is preserved
|
|
6
|
+
# while scripts and event handlers are stripped, preventing stored XSS from menu managers.
|
|
3
7
|
normalize_attrs(:name, :description)
|
|
4
8
|
|
|
9
|
+
# deterministic external iteration order (2.9.2 parity); render paths reorder(:term_order)
|
|
10
|
+
default_scope { order(id: :asc) }
|
|
11
|
+
|
|
5
12
|
alias_attribute :site_id, :term_group
|
|
6
13
|
alias_attribute :label, :name
|
|
7
14
|
alias_attribute :url, :description
|
|
8
15
|
alias_attribute :kind, :slug
|
|
9
16
|
alias_attribute :target, :status
|
|
17
|
+
|
|
10
18
|
# attr_accessible :label, :url, :kind
|
|
11
|
-
#
|
|
12
|
-
default_scope { where(taxonomy: :nav_menu_item).order(id: :asc) }
|
|
13
19
|
|
|
14
|
-
belongs_to :parent, class_name: 'CamaleonCms::NavMenu', inverse_of: :children,
|
|
20
|
+
belongs_to :parent, class_name: 'CamaleonCms::NavMenu', inverse_of: :children, optional: true
|
|
15
21
|
belongs_to :parent_item, class_name: 'CamaleonCms::NavMenuItem', foreign_key: :parent_id, inverse_of: :children,
|
|
16
|
-
|
|
22
|
+
optional: true
|
|
17
23
|
has_many :children, class_name: 'CamaleonCms::NavMenuItem', foreign_key: :parent_id, dependent: :destroy,
|
|
18
24
|
inverse_of: :parent_item
|
|
19
25
|
|
|
@@ -56,10 +62,12 @@ module CamaleonCms
|
|
|
56
62
|
private
|
|
57
63
|
|
|
58
64
|
def update_count
|
|
59
|
-
parent&.update_column(
|
|
60
|
-
parent_item&.update_column(
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
parent&.update_column(:count, parent.children.size) # rubocop:disable Rails/SkipsModelValidations
|
|
66
|
+
parent_item&.update_column(:count, parent_item.children.size) # rubocop:disable Rails/SkipsModelValidations
|
|
67
|
+
# update group and position
|
|
68
|
+
update_columns( # rubocop:disable Rails/SkipsModelValidations
|
|
69
|
+
term_group: main_menu.parent_id, term_order: CamaleonCms::NavMenuItem.where(parent_id: parent_id).count
|
|
70
|
+
)
|
|
63
71
|
end
|
|
64
72
|
|
|
65
73
|
# fast access from site to menu items
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module CamaleonCms
|
|
2
2
|
class Plugin < CamaleonCms::TermTaxonomy
|
|
3
|
-
normalize_attrs(:
|
|
3
|
+
normalize_attrs(:description)
|
|
4
4
|
|
|
5
5
|
# attrs:
|
|
6
6
|
# term_group => status active (1, nil)
|
|
@@ -9,9 +9,8 @@ module CamaleonCms
|
|
|
9
9
|
|
|
10
10
|
attr_accessor :error
|
|
11
11
|
|
|
12
|
-
belongs_to :site, foreign_key: :parent_id,
|
|
12
|
+
belongs_to :site, foreign_key: :parent_id, optional: true, inverse_of: :plugins
|
|
13
13
|
|
|
14
|
-
default_scope { where(taxonomy: :plugin) }
|
|
15
14
|
scope :active, -> { where(term_group: 1) }
|
|
16
15
|
|
|
17
16
|
before_validation :set_default
|
|
@@ -71,7 +70,7 @@ module CamaleonCms
|
|
|
71
70
|
private
|
|
72
71
|
|
|
73
72
|
def set_default
|
|
74
|
-
self.name = slug
|
|
73
|
+
self.name = slug if name.blank?
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
def destroy_custom_fields
|