camaleon_cms 2.9.2 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -4
  3. data/Rakefile +3 -16
  4. data/app/apps/plugins/attack/admin_controller.rb +9 -6
  5. data/app/apps/plugins/attack/attack_helper.rb +35 -25
  6. data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
  7. data/app/apps/plugins/front_cache/admin_controller.rb +15 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/front_cache_helper.rb +33 -9
  10. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  11. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  12. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  13. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  14. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  15. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  16. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  17. data/app/apps/themes/default/default_helper.rb +2 -1
  18. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  19. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  20. data/app/apps/themes/new/custom_helper.rb +5 -9
  21. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  22. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  23. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  24. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  25. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  26. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
  27. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  28. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
  29. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
  30. data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
  31. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  32. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  33. data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
  34. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  35. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
  36. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  37. data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
  38. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
  39. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +76 -13
  40. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
  41. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
  42. data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
  43. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
  44. data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
  45. data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
  46. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  47. data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
  48. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
  49. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  50. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  51. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  52. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  53. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  54. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  55. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  56. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  57. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  58. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  59. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  60. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  61. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  62. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  63. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  64. data/app/decorators/camaleon_cms/post_decorator.rb +66 -50
  65. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  66. data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
  67. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  68. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  69. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  70. data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
  71. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  72. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  73. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  74. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
  75. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  76. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  77. data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
  78. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  79. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  80. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  81. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
  82. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  83. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  84. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  85. data/app/helpers/camaleon_cms/html_helper.rb +58 -37
  86. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  87. data/app/helpers/camaleon_cms/session_helper.rb +188 -44
  88. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  89. data/app/helpers/camaleon_cms/site_helper.rb +34 -13
  90. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  91. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
  92. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  93. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  94. data/app/models/camaleon_cms/ability.rb +15 -7
  95. data/app/models/camaleon_cms/category.rb +6 -6
  96. data/app/models/camaleon_cms/custom_field.rb +15 -6
  97. data/app/models/camaleon_cms/custom_field_group.rb +39 -22
  98. data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
  99. data/app/models/camaleon_cms/media.rb +8 -2
  100. data/app/models/camaleon_cms/meta.rb +2 -2
  101. data/app/models/camaleon_cms/nav_menu.rb +5 -3
  102. data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
  103. data/app/models/camaleon_cms/plugin.rb +3 -4
  104. data/app/models/camaleon_cms/post.rb +113 -27
  105. data/app/models/camaleon_cms/post_comment.rb +10 -6
  106. data/app/models/camaleon_cms/post_default.rb +42 -12
  107. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  108. data/app/models/camaleon_cms/post_tag.rb +4 -5
  109. data/app/models/camaleon_cms/post_type.rb +21 -13
  110. data/app/models/camaleon_cms/site.rb +39 -29
  111. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  112. data/app/models/camaleon_cms/term_taxonomy.rb +52 -4
  113. data/app/models/camaleon_cms/theme.rb +2 -4
  114. data/app/models/camaleon_cms/user.rb +17 -5
  115. data/app/models/camaleon_cms/user_role.rb +66 -6
  116. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  117. data/app/models/camaleon_cms/widget/main.rb +12 -8
  118. data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
  119. data/app/models/camaleon_record.rb +89 -5
  120. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
  121. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  122. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
  123. data/app/models/concerns/camaleon_cms/metas.rb +31 -6
  124. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  125. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  126. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  127. data/app/models/current_request.rb +8 -1
  128. data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
  129. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  130. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  131. data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
  132. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  133. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  134. data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
  135. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  136. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  137. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  138. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
  139. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  140. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  141. data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
  142. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  143. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  144. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  145. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  146. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
  147. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  148. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
  149. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  150. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  151. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  152. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
  153. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  154. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  155. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  156. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  157. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  158. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  159. data/config/initializers/action_view.rb +42 -5
  160. data/config/initializers/active_record_extension.rb +13 -27
  161. data/config/initializers/assets.rb +52 -23
  162. data/config/initializers/custom_initializers.rb +1 -1
  163. data/config/locales/camaleon_cms/admin/ar.yml +1 -0
  164. data/config/locales/camaleon_cms/admin/de.yml +1 -0
  165. data/config/locales/camaleon_cms/admin/en.yml +33 -0
  166. data/config/locales/camaleon_cms/admin/es.yml +1 -0
  167. data/config/locales/camaleon_cms/admin/fr.yml +1 -0
  168. data/config/locales/camaleon_cms/admin/it.yml +1 -0
  169. data/config/locales/camaleon_cms/admin/nl.yml +1 -0
  170. data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
  171. data/config/locales/camaleon_cms/admin/pt.yml +1 -0
  172. data/config/locales/camaleon_cms/admin/ru.yml +1 -0
  173. data/config/locales/camaleon_cms/admin/uk.yml +1 -0
  174. data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
  175. data/config/routes/admin.rb +33 -15
  176. data/config/routes/frontend.rb +21 -15
  177. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  178. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  179. data/lib/camaleon_cms/content_security.rb +158 -0
  180. data/lib/camaleon_cms/engine.rb +47 -9
  181. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  182. data/lib/camaleon_cms/setup_token.rb +69 -0
  183. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  184. data/lib/camaleon_cms/task_reporter.rb +14 -0
  185. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  186. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  187. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  188. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  189. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  190. data/lib/camaleon_cms/uploader_support.rb +86 -0
  191. data/lib/camaleon_cms/version.rb +1 -1
  192. data/lib/camaleon_cms.rb +11 -0
  193. data/lib/ext/hash.rb +28 -2
  194. data/lib/ext/string.rb +10 -5
  195. data/lib/ext/translator.rb +10 -2
  196. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  197. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  198. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  199. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  200. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  201. data/lib/plugin_routes.rb +412 -368
  202. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  203. data/lib/tasks/cross_site_field_groups.rake +74 -0
  204. data/lib/tasks/custom_fields_roles.rake +15 -13
  205. data/lib/tasks/orphaned_comments.rake +42 -0
  206. data/lib/tasks/private_upload_acls.rake +31 -0
  207. data/lib/tasks/site_custom_field_groups.rake +36 -0
  208. data/lib/tasks/unsafe_stored_content.rake +42 -0
  209. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  210. data/lib/tasks/user_field_groups.rake +27 -0
  211. metadata +123 -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
- belongs_to :site, foreign_key: :parent_id, required: false
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
- validates_uniqueness_of :slug, scope: %i[object_class objectid parent_id]
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.find_by(slug: slug)
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 behavior by
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 behavior
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 'Widget::Main'
130
- caption = "Fields for Widget <b>(#{CamaleonCms::Widget::Main.find(objectid).name.translate})</b>"
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
- caption = "Field settings for Theme <b>(#{begin
144
+ theme_name = begin
133
145
  site.themes.find(objectid).name
134
146
  rescue StandardError
135
147
  objectid
136
- end})</b>"
148
+ end
149
+ caption = "Field settings for Theme <b>(#{ERB::Util.html_escape(theme_name)})</b>"
137
150
  when 'NavMenu'
138
- caption = "Field settings for Menus <b>(#{CamaleonCms::NavMenu.find(objectid).name})</b>"
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
- caption
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}" unless slug.present?
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
- Theme].include?(class_name) && objectid && (options[:default_value].present? || options[:default_values].present?)
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.find_by(slug: objectid)
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.create!(custom_field_id: field.id, custom_field_slug: field.slug,
179
- value: fix_meta_value(value))
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
@@ -7,23 +7,154 @@ module CamaleonCms
7
7
  default_scope { order("#{CamaleonCms::CustomFieldsRelationship.table_name}.term_order ASC") }
8
8
 
9
9
  # relations
10
- belongs_to :custom_field, class_name: 'CamaleonCms::CustomField', required: false
10
+ belongs_to :custom_field, class_name: 'CamaleonCms::CustomField', optional: true
11
+ belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
11
12
 
12
13
  # validates :objectid, :custom_field_id, presence: true
13
14
  validates :custom_field_id, presence: true # error on clone model
14
15
 
16
+ # Rendered positions that need a save-time gate (audit M17, scan-and-reject policy). An
17
+ # `editor` value is emitted as markup (`raw`); a `field_attrs` value is a JSON pair whose
18
+ # members are emitted as markup; the URI field types are emitted into href/src. Every other
19
+ # field type's value renders through escaping ERB as element content, where markup cannot
20
+ # execute, so it needs no gate.
21
+ MARKUP_FIELD_KEYS = %w[editor].freeze
22
+ JSON_MARKUP_FIELD_KEYS = %w[field_attrs].freeze
23
+ URI_FIELD_KEYS = %w[url image audio video file].freeze
24
+ GATED_FIELD_KEYS = (MARKUP_FIELD_KEYS + JSON_MARKUP_FIELD_KEYS + URI_FIELD_KEYS).freeze
25
+
26
+ validate :reject_untrusted_dangerous_value
27
+
15
28
  after_save :set_parent_slug
16
29
  after_save :update_model_owner # TODO: convert this model into polymorphic
17
30
 
31
+ # Opt-out for trusted server-side pipelines (imports, seeds, plugin code), mirroring
32
+ # Post#unfiltered_content!: a reader plus a bang enabler and NO writer, so mass assignment
33
+ # cannot reach it. Sticky for the lifetime of the instance.
34
+ attr_reader :unfiltered_value
35
+
36
+ def unfiltered_value!
37
+ @unfiltered_value = true
38
+ self
39
+ end
40
+
41
+ class << self
42
+ # Whether this field type's value is emitted into a markup or URL position (so it is gated).
43
+ def gated_field_key?(field_key)
44
+ GATED_FIELD_KEYS.include?(field_key.to_s)
45
+ end
46
+
47
+ # Why the save-time gate would refuse this value for the field type, or nil if it passes:
48
+ # :too_large (over the parse ceiling), :html (disallowed markup), :uri (script-capable URL).
49
+ # Ignores author trust, so the validation (trust-gated) and the scan_content rake task (which
50
+ # reports every stored row) share one dispatch and cannot drift.
51
+ def gate_rejection_reason(field_key, value)
52
+ return if value.blank?
53
+
54
+ key = field_key.to_s
55
+ if MARKUP_FIELD_KEYS.include?(key)
56
+ markup_rejection_reason(value)
57
+ elsif JSON_MARKUP_FIELD_KEYS.include?(key)
58
+ return :too_large if CamaleonCms::UnsafeMarkup.too_large?(value)
59
+
60
+ :html if json_member_values(value).any? { |member| markup_unsafe?(member) }
61
+ elsif URI_FIELD_KEYS.include?(key)
62
+ :uri if CamaleonCms::UnsafeMarkup.dangerous_uri?(value)
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def markup_rejection_reason(value)
69
+ return :too_large if CamaleonCms::UnsafeMarkup.too_large?(value)
70
+
71
+ :html if markup_unsafe?(value)
72
+ end
73
+
74
+ def markup_unsafe?(value)
75
+ CamaleonCms::UnsafeMarkup.unsafe_html?(
76
+ value, tags: CamaleonCms::Post::CONTENT_ALLOWED_TAGS,
77
+ attributes: CamaleonCms::Post::CONTENT_ALLOWED_ATTRIBUTES
78
+ )
79
+ end
80
+
81
+ # A field_attrs value stores JSON whose string members render verbatim. Scan every DECODED
82
+ # member (Hash, Array or nested), not the stored bytes: the Rails JSON encoder unicode-escapes
83
+ # angle brackets (storing the escape instead of a literal bracket), so a byte-level scan of the
84
+ # stored string would pass markup that JSON.parse restores at render. Unparseable values are
85
+ # scanned as-is (fail closed on whatever the renderer would fall back to).
86
+ def json_member_values(value)
87
+ flatten_json_strings(JSON.parse(value))
88
+ rescue JSON::ParserError
89
+ [value.to_s]
90
+ end
91
+
92
+ def flatten_json_strings(node)
93
+ case node
94
+ when Hash then node.values.flat_map { |member| flatten_json_strings(member) }
95
+ when Array then node.flat_map { |member| flatten_json_strings(member) }
96
+ else [node.to_s]
97
+ end
98
+ end
99
+ end
100
+
18
101
  private
19
102
 
103
+ # Security (audit M17): the frontend renders an `editor`/`field_attrs` value verbatim and
104
+ # URI-type values into URL positions, so a value an untrusted author may not write is refused --
105
+ # never rewritten. Stored values therefore always equal authored values. The scan/dispatch lives
106
+ # in the class-level gate_rejection_reason so the scan_content rake task reuses it verbatim.
107
+ def reject_untrusted_dangerous_value
108
+ return if value.blank? || unfiltered_value
109
+
110
+ field_key = custom_field&.options&.[](:field_key).to_s
111
+ return unless self.class.gated_field_key?(field_key)
112
+ return if author_trusted_for_unfiltered_value?
113
+
114
+ reason = self.class.gate_rejection_reason(field_key, value)
115
+ return unless reason
116
+
117
+ errors.add(:base, cama_rejection_message(rejection_message_key(reason)))
118
+ end
119
+
120
+ def rejection_message_key(reason)
121
+ case reason
122
+ when :too_large then 'value_too_large'
123
+ when :uri then 'value_rejected_uri'
124
+ else 'value_rejected_html'
125
+ end
126
+ end
127
+
128
+ # The message must never be swallowed by a missing translation: only en.yml carries these keys,
129
+ # while the process locale follows the current admin/site language — fall back to English.
130
+ def cama_rejection_message(key)
131
+ full_key = "camaleon_cms.admin.custom_field.message.#{key}"
132
+ I18n.t(full_key, slug: custom_field_slug,
133
+ default: I18n.t(full_key, slug: custom_field_slug, locale: :en))
134
+ end
135
+
136
+ # Trust follows the post-content model: an admin may write anything; a post's field values may
137
+ # skip the gate when the saver holds post_content_unfiltered_html for the post type. Fails
138
+ # closed (the gate applies) without a request context — benign values still pass the scan.
139
+ def author_trusted_for_unfiltered_value?
140
+ user = CurrentRequest.user
141
+ site = CurrentRequest.site
142
+ return false if user.blank? || site.blank?
143
+ return true if user.admin?
144
+
145
+ parent = owner
146
+ return false unless parent.is_a?(CamaleonCms::Post) && parent.post_type.present?
147
+
148
+ CamaleonCms::Ability.new(user, site).can?(:post_content_unfiltered_html, parent.post_type)
149
+ end
150
+
20
151
  def set_parent_slug
21
152
  # self.update_column('custom_field_slug', self.custom_fields.slug)
22
153
  end
23
154
 
24
155
  # touch owner model
25
156
  def update_model_owner
26
- "CamaleonCms::#{object_class}".constantize.find(objectid).touch
157
+ "CamaleonCms::#{object_class}".constantize.find(objectid).touch # rubocop:disable Rails/SkipsModelValidations
27
158
  rescue StandardError
28
159
  nil
29
160
  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, required: false
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.find_by_key(key)
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
- extend CamaleonCms::NormalizeAttrs
6
+ belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
7
7
 
8
- normalize_attrs(:value)
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(:name, :description)
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, required: false
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, required: false
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
- required: false
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('count', parent.children.size)
60
- parent_item&.update_column('count', parent_item.children.size)
61
- update_column(:term_group, main_menu.parent_id)
62
- update_column(:term_order, CamaleonCms::NavMenuItem.where(parent_id: parent_id).count) # update position
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(:name, :description)
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, required: false
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 unless name.present?
73
+ self.name = slug if name.blank?
75
74
  end
76
75
 
77
76
  def destroy_custom_fields