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.
Files changed (216) 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 +14 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
  10. data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
  11. data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
  12. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  13. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  14. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  15. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  16. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  17. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  18. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  19. data/app/apps/themes/default/default_helper.rb +2 -1
  20. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  21. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  22. data/app/apps/themes/new/custom_helper.rb +5 -9
  23. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  24. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  25. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  26. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  27. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  28. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
  29. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  30. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
  31. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
  32. data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
  33. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  34. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  35. data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
  36. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  37. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
  38. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  39. data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
  40. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
  41. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
  42. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
  43. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
  44. data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
  45. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
  46. data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
  47. data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
  48. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  49. data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
  50. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
  51. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  52. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  53. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  54. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  55. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  56. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  57. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  58. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  59. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  60. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  61. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  62. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  63. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  64. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  65. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  66. data/app/decorators/camaleon_cms/post_decorator.rb +68 -50
  67. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  68. data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
  69. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  70. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  71. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  72. data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
  73. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  74. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  75. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  76. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
  77. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  78. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  79. data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
  80. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  81. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  82. data/app/helpers/camaleon_cms/frontend/application_helper.rb +6 -1
  83. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  84. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
  85. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  86. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  87. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  88. data/app/helpers/camaleon_cms/html_helper.rb +58 -37
  89. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  90. data/app/helpers/camaleon_cms/session_helper.rb +188 -44
  91. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  92. data/app/helpers/camaleon_cms/site_helper.rb +34 -13
  93. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  94. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
  95. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  96. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  97. data/app/models/camaleon_cms/ability.rb +15 -7
  98. data/app/models/camaleon_cms/category.rb +6 -6
  99. data/app/models/camaleon_cms/custom_field.rb +15 -6
  100. data/app/models/camaleon_cms/custom_field_group.rb +39 -22
  101. data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -2
  102. data/app/models/camaleon_cms/media.rb +8 -2
  103. data/app/models/camaleon_cms/meta.rb +2 -2
  104. data/app/models/camaleon_cms/nav_menu.rb +5 -3
  105. data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
  106. data/app/models/camaleon_cms/plugin.rb +3 -4
  107. data/app/models/camaleon_cms/post.rb +137 -27
  108. data/app/models/camaleon_cms/post_comment.rb +10 -6
  109. data/app/models/camaleon_cms/post_default.rb +42 -12
  110. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  111. data/app/models/camaleon_cms/post_tag.rb +4 -5
  112. data/app/models/camaleon_cms/post_type.rb +21 -13
  113. data/app/models/camaleon_cms/site.rb +39 -29
  114. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  115. data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
  116. data/app/models/camaleon_cms/theme.rb +2 -4
  117. data/app/models/camaleon_cms/user.rb +17 -5
  118. data/app/models/camaleon_cms/user_role.rb +81 -6
  119. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  120. data/app/models/camaleon_cms/widget/main.rb +12 -8
  121. data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
  122. data/app/models/camaleon_record.rb +89 -5
  123. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
  124. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  125. data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
  126. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
  127. data/app/models/concerns/camaleon_cms/metas.rb +31 -6
  128. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  129. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  130. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  131. data/app/models/current_request.rb +8 -1
  132. data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
  133. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  134. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  135. data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
  136. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  137. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  138. data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
  139. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  140. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  141. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  142. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
  143. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  144. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  145. data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
  146. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  147. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  148. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  149. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  150. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
  151. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  152. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
  153. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  154. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  155. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  156. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
  157. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  158. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  159. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  160. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  161. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  162. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  163. data/config/initializers/action_view.rb +42 -5
  164. data/config/initializers/active_record_extension.rb +13 -27
  165. data/config/initializers/assets.rb +52 -23
  166. data/config/initializers/custom_initializers.rb +1 -1
  167. data/config/locales/camaleon_cms/admin/ar.yml +1 -0
  168. data/config/locales/camaleon_cms/admin/de.yml +1 -0
  169. data/config/locales/camaleon_cms/admin/en.yml +36 -0
  170. data/config/locales/camaleon_cms/admin/es.yml +1 -0
  171. data/config/locales/camaleon_cms/admin/fr.yml +1 -0
  172. data/config/locales/camaleon_cms/admin/it.yml +1 -0
  173. data/config/locales/camaleon_cms/admin/nl.yml +1 -0
  174. data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
  175. data/config/locales/camaleon_cms/admin/pt.yml +1 -0
  176. data/config/locales/camaleon_cms/admin/ru.yml +1 -0
  177. data/config/locales/camaleon_cms/admin/uk.yml +1 -0
  178. data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
  179. data/config/routes/admin.rb +33 -15
  180. data/config/routes/frontend.rb +21 -15
  181. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  182. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  183. data/lib/camaleon_cms/content_security.rb +158 -0
  184. data/lib/camaleon_cms/engine.rb +45 -21
  185. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  186. data/lib/camaleon_cms/setup_token.rb +69 -0
  187. data/lib/camaleon_cms/shortcode_registry.rb +102 -0
  188. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  189. data/lib/camaleon_cms/task_reporter.rb +14 -0
  190. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  191. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  192. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  193. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  194. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  195. data/lib/camaleon_cms/uploader_support.rb +86 -0
  196. data/lib/camaleon_cms/version.rb +1 -1
  197. data/lib/camaleon_cms.rb +23 -0
  198. data/lib/ext/hash.rb +28 -2
  199. data/lib/ext/string.rb +10 -5
  200. data/lib/ext/translator.rb +10 -2
  201. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  202. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  203. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  204. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  205. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  206. data/lib/plugin_routes.rb +412 -368
  207. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  208. data/lib/tasks/cross_site_field_groups.rake +74 -0
  209. data/lib/tasks/custom_fields_roles.rake +15 -13
  210. data/lib/tasks/orphaned_comments.rake +42 -0
  211. data/lib/tasks/private_upload_acls.rake +31 -0
  212. data/lib/tasks/site_custom_field_groups.rake +36 -0
  213. data/lib/tasks/unsafe_stored_content.rake +42 -0
  214. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  215. data/lib/tasks/user_field_groups.rake +27 -0
  216. metadata +125 -4
@@ -1,10 +1,49 @@
1
1
  module CamaleonCms
2
2
  class Post < CamaleonCms::PostDefault
3
3
  include CamaleonCms::CategoriesTagsForPosts
4
+ include CamaleonCms::ContentShortcodeGate
5
+
6
+ # Structural, non-executable markup that long-form post content legitimately uses but the
7
+ # sanitizer default drops. Superset of the default so upstream security additions are inherited.
8
+ SANITIZE_EXTRA_TAGS = %w[table thead tbody tfoot tr td th caption col colgroup
9
+ figure figcaption u s hr].freeze
10
+ # These attributes are allowed knowing what they cost, none of which is script execution:
11
+ # `style` is CSS-scrubbed by the gate's scrubber (expression(), url(javascript:) and friends
12
+ # register as removals) but permits absolutely-positioned overlays; `id` widens DOM clobbering,
13
+ # which the scrubber's own default `name` already opened; and `target` without a forced
14
+ # `rel=noopener` is reverse tabnabbing on browsers predating the implicit default. The trade
15
+ # is deliberate: an untrusted author whose content needs more than this list has the save
16
+ # refused, and the role that wants more grants post_content_unfiltered_html.
17
+ SANITIZE_EXTRA_ATTRIBUTES = %w[id style target rel colspan rowspan].freeze
18
+ CONTENT_ALLOWED_TAGS = (ActionController::Base.helpers.sanitizer_vendor.safe_list_sanitizer
19
+ .allowed_tags.to_a + SANITIZE_EXTRA_TAGS).uniq.freeze
20
+ CONTENT_ALLOWED_ATTRIBUTES = (ActionController::Base.helpers.sanitizer_vendor.safe_list_sanitizer
21
+ .allowed_attributes.to_a + SANITIZE_EXTRA_ATTRIBUTES).uniq.freeze
22
+
23
+ # Opt-out for trusted server-side pipelines (imports, seeds, plugin code) that would otherwise
24
+ # be sanitized by the fail-closed default. Exposed as a reader plus a bang enabler and NO
25
+ # `unfiltered_content=` writer, so `assign_attributes`/mass assignment cannot reach it — only
26
+ # explicit server-side code calling `post.unfiltered_content!` can.
27
+ #
28
+ # It is sticky for the lifetime of the object, not for one save: an instance that has been
29
+ # opted out stays opted out for every subsequent save of that instance. That is what a
30
+ # pipeline staging a record over several steps wants; code that needs the opt-out to apply
31
+ # once should not reuse the instance afterwards.
32
+ attr_reader :unfiltered_content
33
+
34
+ def unfiltered_content!
35
+ @unfiltered_content = true
36
+ self
37
+ end
4
38
 
5
39
  alias_attribute :post_type_id, :taxonomy_id
6
40
  default_scope -> { where(post_class: 'Post').order(post_order: :asc, created_at: :desc) }
7
41
 
42
+ validate :reject_untrusted_dangerous_content, on: %i[create update]
43
+ # Shortcodes in post content are expanded by do_shortcode at render (PostDecorator#the_content),
44
+ # an end-run around post_content_unfiltered_html; gate authorship behind content_shortcodes.
45
+ gate_content_shortcodes :content
46
+
8
47
  # DEPRECATED
9
48
  has_many :post_relationships, class_name: 'CamaleonCms::PostRelationship', foreign_key: :objectid,
10
49
  dependent: :destroy, inverse_of: :post
@@ -14,16 +53,17 @@ module CamaleonCms
14
53
  has_many :term_relationships, foreign_key: :objectid, dependent: :destroy, inverse_of: :object
15
54
  has_many :categories, class_name: 'CamaleonCms::Category', through: :term_relationships, source: :term_taxonomy
16
55
  has_many :post_tags, class_name: 'CamaleonCms::PostTag', through: :term_relationships, source: :term_taxonomy
17
- has_many :comments, class_name: 'CamaleonCms::PostComment', foreign_key: :post_id, dependent: :destroy
18
- has_many :drafts, lambda {
19
- where(status: 'draft_child')
20
- }, class_name: 'CamaleonCms::Post', foreign_key: :post_parent, dependent: :destroy
56
+ has_many :comments, class_name: 'CamaleonCms::PostComment', dependent: :destroy
57
+ has_many :drafts, -> { where(status: 'draft_child') }, class_name: 'CamaleonCms::Post', inverse_of: :parent,
58
+ foreign_key: :post_parent, dependent: :destroy
21
59
  has_many :children, class_name: 'CamaleonCms::Post', foreign_key: :post_parent, dependent: :destroy,
22
- primary_key: :id
60
+ primary_key: :id, inverse_of: :parent
23
61
 
24
- belongs_to :owner, class_name: CamaManager.get_user_class_name, foreign_key: :user_id, required: false
25
- belongs_to :parent, class_name: 'CamaleonCms::Post', foreign_key: :post_parent, required: false
26
- belongs_to :post_type, foreign_key: :taxonomy_id, inverse_of: :posts, required: false
62
+ belongs_to :owner, class_name: CamaManager.get_user_class_name.to_s, foreign_key: :user_id, optional: true,
63
+ inverse_of: :all_posts
64
+ belongs_to :parent, class_name: 'CamaleonCms::Post', foreign_key: :post_parent, optional: true,
65
+ inverse_of: :children
66
+ belongs_to :post_type, foreign_key: :taxonomy_id, inverse_of: :posts, optional: true
27
67
 
28
68
  scope :visible_frontend, -> { where(status: 'published') }
29
69
  scope :public_posts, lambda {
@@ -38,11 +78,18 @@ module CamaleonCms
38
78
  scope :drafts, -> { where(status: %w[draft draft_child]) }
39
79
  scope :pending, -> { where(status: 'pending') }
40
80
  scope :latest, -> { reorder(created_at: :desc) }
81
+ # `preload`, not `includes`: frontend listings run `the_posts` through this scope. If it were
82
+ # `includes`, a caller that later chains a join/`eager_load`/hash-`where` on one of these tables
83
+ # (e.g. PostDecorator#the_related_posts joins :categories) would flip the relation to
84
+ # `eager_loading?` -- one multi-way LEFT JOIN that loads a PARTIAL categories set per post and
85
+ # duplicates rows in `pluck`/`count`. `preload` issues separate queries, never merged into a join.
86
+ scope :with_eager, -> { preload(:metas, :categories, :post_tags, post_type: :metas) }
41
87
 
42
88
  validates_with CamaleonCms::PostUniqValidator
43
89
  attr_accessor :show_title_with_parent
44
90
 
45
- before_create :fix_post_order, if: ->(p) { !p.post_order.present? || p.post_order == 0 }
91
+ before_create :fix_post_order, if: ->(p) { p.post_order.blank? || p.post_order == 0 }
92
+ before_save :set_published_at_on_publish
46
93
 
47
94
  # return all parents for current page hierarchy ordered bottom to top
48
95
  def parents
@@ -66,22 +113,22 @@ module CamaleonCms
66
113
  end
67
114
  end
68
115
 
69
- # return the post type of this post (DEPRECATED)
116
+ # Return the post type of this post (DEPRECATED), though used yet in migration, so let it stay
70
117
  def get_post_type_depre
71
118
  post_types.reorder(nil).first
72
119
  end
73
120
 
74
- # check if this post was published
121
+ # Check if this post was published
75
122
  def published?
76
123
  status == 'published'
77
124
  end
78
125
 
79
- # check if this is in pending status
126
+ # Check if this post is in the pending status
80
127
  def pending?
81
128
  status == 'pending'
82
129
  end
83
130
 
84
- # check if this is in draft status
131
+ # Check if this post is in the draft status
85
132
  def draft?
86
133
  %w[draft draft_child].include?(status)
87
134
  end
@@ -90,12 +137,12 @@ module CamaleonCms
90
137
  status == 'draft_child'
91
138
  end
92
139
 
93
- # check if this is in trash status
140
+ # Check if this post is in the trash status
94
141
  def trash?
95
142
  status == 'trash'
96
143
  end
97
144
 
98
- # check if current post can manage content
145
+ # Check if the current post can manage content
99
146
  # return boolean
100
147
  def manage_content?(posttype = nil)
101
148
  get_option('has_content', (posttype || post_type).get_option('has_content', true))
@@ -106,34 +153,35 @@ module CamaleonCms
106
153
  get_option('has_layout', (posttype || post_type).get_option('has_layout', false))
107
154
  end
108
155
 
109
- # check if current post can manage template
156
+ # Check if current post can manage template
110
157
  # return boolean
111
158
  def manage_template?(posttype = nil)
112
159
  get_option('has_template', (posttype || post_type).get_option('has_template', true))
113
160
  end
114
161
 
115
- # check if current post can manage summary
162
+ # Check if the current post can manage summary
116
163
  # return boolean
117
164
  def manage_summary?(posttype = nil)
118
165
  get_option('has_summary', (posttype || post_type).get_option('has_summary', true))
119
166
  end
120
167
 
121
- # check if current post can manage picture
168
+ # Check if the current post can manage picture
122
169
  # return boolean
123
170
  def manage_picture?(posttype = nil)
124
171
  get_option('has_picture', (posttype || post_type).get_option('has_picture', true))
125
172
  end
126
173
 
127
- # check if current post can manage comments
174
+ # Check if the current post can manage comments
128
175
  # return boolean
129
176
  def manage_comments?(posttype = nil)
130
177
  get_option('has_comments', (posttype || post_type).get_option('has_comments', false))
131
178
  end
132
179
 
133
- # check if the post can be commented
180
+ # Check if the post can be commented
134
181
  # sample: @post.can_commented?
135
- # return Boolean (true/false)
136
- # to enable comments for current post, use this: post.set_meta('has_comments', '1'). Note: Parent PostType should be enabled for comments too: post_type.set_option('has_comments', true)
182
+ # to enable comments for current post, use this: post.set_meta('has_comments', '1').
183
+ # Note: Parent PostType should be enabled for comments too: post_type.set_option('has_comments', true)
184
+ # @return [TrueClass, FalseClass]
137
185
  def can_commented?
138
186
  manage_comments? && get_meta('has_comments').to_s == '1'
139
187
  end
@@ -151,10 +199,15 @@ module CamaleonCms
151
199
  # has_picture, boolean (default true)
152
200
  # has_template, boolean (default false)
153
201
  # has_comments, boolean (default false)
154
- # default_layout: (string) (default layout) # this is still used if post type was inactivated layout and overwritten by dropdown in post view
155
- # default_template: (string) (default template) # this is still used if post type was inactivated template and overwritten by dropdown in post view
202
+ #
203
+ # the following is still used if post type was inactivated layout and overwritten by dropdown in post view
204
+ # default_layout: (string) (default layout)
205
+ #
206
+ # the following is still used if post type was inactivated template and overwritten by dropdown in post view
207
+ # default_template: (string) (default template)
156
208
  # has_layout: (boolean) (default false)
157
- # skip_fields: (array) (default empty) array of custom field keys to avoid for this post, sample: ["subtitle", "icon"]
209
+ # skip_fields: (array) (default empty) array of custom field keys to avoid for this post,
210
+ # sample: ["subtitle", "icon"]
158
211
  # val: value for the setting
159
212
  def set_setting(key, val)
160
213
  set_option(key, val)
@@ -171,7 +224,7 @@ module CamaleonCms
171
224
  # new_order_position: (Integer) position number
172
225
  # return nil
173
226
  def set_position(new_order_position)
174
- update_column('post_order', new_order_position)
227
+ update_column(:post_order, new_order_position) # rubocop:disable Rails/SkipsModelValidations
175
228
  end
176
229
 
177
230
  # save the summary for current post
@@ -247,10 +300,67 @@ module CamaleonCms
247
300
 
248
301
  private
249
302
 
303
+ def trusted_for_unfiltered_html?
304
+ user = CurrentRequest.user
305
+ site = CurrentRequest.site
306
+ # Fail closed (sanitize) when either user or site context is missing - e.g. background jobs, rake tasks
307
+ # or the console. Guarding the site too avoids a NoMethodError from Ability#initialize (which dereferences
308
+ # the site for non-admin users) that would otherwise abort the whole save.
309
+ return false if user.blank? || site.blank?
310
+
311
+ CamaleonCms::Ability.new(user, site).can?(:post_content_unfiltered_html, post_type)
312
+ end
313
+
314
+ # Security (scan-and-reject policy): content an untrusted author is not permitted to write is
315
+ # refused with a validation error -- never sanitized or rewritten -- so stored content always
316
+ # equals authored content and the frontend may render it verbatim. The guard on
317
+ # `content_changed?` deliberately leaves pre-gate stored content editable (a title fix on an
318
+ # old post must not brick it); `rake camaleon_cms:security:scan_content` lists such content.
319
+ def reject_untrusted_dangerous_content
320
+ return unless new_record? || content_changed?
321
+ return if content.blank?
322
+ # The explicit pipeline opt-out short-circuits before the Ability lookup below.
323
+ return if unfiltered_content
324
+ # Check trust only once we know there is content to gate, so unchanged/blank updates never pay for
325
+ # building an Ability (a role-meta DB lookup) on every save.
326
+ return if trusted_for_unfiltered_html?
327
+
328
+ # Distinguish an over-size refusal from a disallowed-markup refusal (audit M16): an over-size
329
+ # value can be perfectly clean, so the markup message (scripts/handlers/embeds) would misdescribe
330
+ # it and send the author hunting for markup that is not there.
331
+ if CamaleonCms::UnsafeMarkup.too_large?(content)
332
+ errors.add(:content, cama_content_rejection_message('content_too_large'))
333
+ elsif CamaleonCms::UnsafeMarkup.unsafe_html?(
334
+ content, tags: CONTENT_ALLOWED_TAGS, attributes: CONTENT_ALLOWED_ATTRIBUTES
335
+ )
336
+ errors.add(:content, cama_content_rejection_message('content_rejected'))
337
+ end
338
+ end
339
+
340
+ # Only en.yml carries these keys while the process locale follows the current admin/site
341
+ # language — fall back to English rather than emit "translation missing".
342
+ def cama_content_rejection_message(key)
343
+ full_key = "camaleon_cms.admin.post.message.#{key}"
344
+ I18n.t(full_key, default: I18n.t(full_key, locale: :en))
345
+ end
346
+
250
347
  # calculate a post order when it is empty
251
348
  def fix_post_order
252
349
  last_post = post_type.posts.where.not(id: nil).last
253
- self.post_order = last_post.present? ? last_post.post_order + 1 : 1
350
+ self.post_order = last_post.present? ? (last_post.post_order || 0) + 1 : 1
351
+ end
352
+
353
+ # Stamp the publish time at the moment a post becomes published without an explicit date --
354
+ # the admin Publish action, or any save that transitions the status to 'published'. Keyed to
355
+ # the status change (or the record being born published), not to every save, so a post created
356
+ # as a draft keeps a nil published_at until it is actually published, a caller-supplied date
357
+ # (e.g. a scheduled post) is preserved, and later edits of an already-published post keep the
358
+ # original date.
359
+ def set_published_at_on_publish
360
+ return unless status == 'published' && published_at.blank?
361
+ return unless new_record? || will_save_change_to_status?
362
+
363
+ self.published_at = Time.current.utc
254
364
  end
255
365
  end
256
366
  end
@@ -6,16 +6,20 @@ module CamaleonCms
6
6
  extend CamaleonCms::NormalizeAttrs
7
7
 
8
8
  self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}comments"
9
- # attr_accessible :user_id, :post_id, :content, :author, :author_email, :author_url, :author_IP, :approved, :agent, :agent, :typee, :comment_parent, :is_anonymous
9
+ # attr_accessible :user_id, :post_id, :content, :author, :author_email, :author_url, :author_IP, :approved, :agent,
10
+ # :agent, :typee, :comment_parent, :is_anonymous
10
11
  attr_accessor :is_anonymous
11
12
 
12
13
  # default_scope order('comments.created_at ASC')
13
14
  # approved: approved | pending | spam
14
15
 
15
- has_many :children, class_name: 'CamaleonCms::PostComment', foreign_key: :comment_parent, dependent: :destroy
16
- belongs_to :post, required: false
17
- belongs_to :parent, class_name: 'CamaleonCms::PostComment', foreign_key: :comment_parent, required: false
18
- belongs_to :user, class_name: CamaManager.get_user_class_name, foreign_key: :user_id, required: false
16
+ has_many :children, class_name: 'CamaleonCms::PostComment', foreign_key: :comment_parent, dependent: :destroy,
17
+ inverse_of: :parent
18
+
19
+ belongs_to :post, optional: true
20
+ belongs_to :parent, class_name: 'CamaleonCms::PostComment', foreign_key: :comment_parent, optional: true,
21
+ inverse_of: :children
22
+ belongs_to :user, class_name: CamaManager.get_user_class_name.to_s, optional: true
19
23
 
20
24
  default_scope { order("#{CamaleonCms::PostComment.table_name}.created_at DESC") }
21
25
 
@@ -26,7 +30,7 @@ module CamaleonCms
26
30
  normalize_attrs(:content)
27
31
 
28
32
  validates :content, presence: true
29
- validates_presence_of :author, :author_email, if: proc { |c| c.is_anonymous.present? }
33
+ validates :author, :author_email, presence: { if: proc { |c| c.is_anonymous.present? } }
30
34
  after_create :update_counter
31
35
  after_destroy :update_counter
32
36
 
@@ -11,9 +11,42 @@ module CamaleonCms
11
11
  end
12
12
  end
13
13
 
14
+ # Point Rails to use the post_class column for Native STI
15
+ self.inheritance_column = :post_class
16
+
17
+ # Controls Writing: Strip 'CamaleonCms::' when saving to DB
18
+ # "CamaleonCms::Widget::Assigned" -> "Widget::Assigned"
19
+ # "CamaleonCms::Post" -> "Post"
20
+ def self.sti_name
21
+ name.sub(/^CamaleonCms::/, '')
22
+ end
23
+
24
+ # Controls Reading: Prepend 'CamaleonCms::' when pulling from DB
25
+ # "Widget::Assigned" -> "CamaleonCms::Widget::Assigned"
26
+ # "Post" -> "CamaleonCms::Post"
27
+ def self.find_sti_class(type_name)
28
+ full_class_name = type_name.start_with?('CamaleonCms::') ? type_name : "CamaleonCms::#{type_name}"
29
+ klass = begin
30
+ full_class_name.constantize
31
+ rescue NameError
32
+ nil
33
+ end
34
+ return klass if klass && klass <= base_class
35
+
36
+ # `super` resolves fully-qualified external classes (plugin STI outside the
37
+ # CamaleonCms namespace) and enforces ancestry; values it cannot place load as
38
+ # the base class, as every row did before native STI.
39
+ begin
40
+ super
41
+ rescue ActiveRecord::SubclassNotFound
42
+ base_class
43
+ end
44
+ end
45
+
14
46
  self.table_name = "#{PluginRoutes.static_system_info['db_prefix']}posts"
15
47
 
16
- # attr_accessible :user_id, :title, :slug, :content, :content_filtered, :status, :visibility, :visibility_value, :post_order, :post_type_key, :taxonomy_id, :published_at, :post_parent, :post_order, :is_feature
48
+ # attr_accessible :user_id, :title, :slug, :content, :content_filtered, :status, :visibility, :visibility_value,
49
+ # :post_order, :post_type_key, :taxonomy_id, :published_at, :post_parent, :post_order, :is_feature
17
50
  attr_accessor :draft_id
18
51
 
19
52
  # attr_accessible :data_options
@@ -21,10 +54,12 @@ module CamaleonCms
21
54
  cattr_accessor :current_user
22
55
  cattr_accessor :current_site
23
56
 
24
- has_many :term_relationships, foreign_key: :objectid, dependent: :destroy, primary_key: :id
25
- has_many :children, lambda {
26
- where(post_class: 'PostDefault')
27
- }, class_name: 'CamaleonCms::PostDefault', foreign_key: :post_parent, dependent: :destroy, primary_key: :id
57
+ has_many :children, -> { where(post_class: 'PostDefault') },
58
+ class_name: 'CamaleonCms::PostDefault', foreign_key: :post_parent, dependent: :destroy, inverse_of: :parent
59
+
60
+ belongs_to :parent, class_name: 'CamaleonCms::PostDefault', foreign_key: :post_parent, optional: true,
61
+ inverse_of: :children
62
+
28
63
  scope :featured, -> { where(is_feature: true) }
29
64
 
30
65
  validates :title, :slug, presence: true
@@ -45,11 +80,6 @@ module CamaleonCms
45
80
  res.take
46
81
  end
47
82
 
48
- # return the parent of a post (support for sub contents or tree of posts)
49
- def parent
50
- CamaleonCms::Post.where(id: post_parent).first
51
- end
52
-
53
83
  # return the author of this Content
54
84
  def author
55
85
  CamaleonCms::User.find(user_id)
@@ -62,7 +92,7 @@ module CamaleonCms
62
92
  CamaleonCms::NavMenuItem.where(url: id, kind: 'post')
63
93
  end
64
94
 
65
- # Set the meta, field values and the post keywords here
95
+ # Set the meta-field values and the post keywords here
66
96
  def set_params(meta, custom_fields, options)
67
97
  set_metas(meta)
68
98
  set_field_values(custom_fields)
@@ -78,7 +108,7 @@ module CamaleonCms
78
108
 
79
109
  # do all before actions to save the content
80
110
  def before_saved
81
- self.title = 'Untitled' unless title.present?
111
+ self.title = 'Untitled' if title.blank?
82
112
  self.content_filtered = if content.to_s.include?('<!--:-->')
83
113
  content.translations.transform_values do |value|
84
114
  value.squish.strip_tags
@@ -5,11 +5,11 @@ module CamaleonCms
5
5
  # attr_accessible :objectid, :term_taxonomy_id, :term_order
6
6
  default_scope -> { order(term_order: :asc) }
7
7
 
8
- belongs_to :post_type, class_name: 'CamaleonCms::PostType', foreign_key: :term_taxonomy_id,
9
- inverse_of: :post_relationships, required: false
8
+ belongs_to :post_type, class_name: 'CamaleonCms::PostType', foreign_key: :term_taxonomy_id, inverse_of: false,
9
+ optional: true
10
10
  belongs_to :post, lambda {
11
11
  order("#{CamaleonCms::Post.table_name}.id DESC")
12
- }, foreign_key: :objectid, inverse_of: :post_relationships, dependent: :destroy, required: false
12
+ }, foreign_key: :objectid, inverse_of: :post_relationships, optional: true
13
13
 
14
14
  # callbacks
15
15
  after_create :update_count
@@ -18,7 +18,9 @@ module CamaleonCms
18
18
  private
19
19
 
20
20
  def update_count
21
- post_type.update_column('count', post_type.posts.size) if post_type.present?
21
+ return if post_type.blank?
22
+
23
+ post_type.update_column(:count, post_type.posts.size) # rubocop:disable Rails/SkipsModelValidations
22
24
  end
23
25
  end
24
26
  end
@@ -1,11 +1,10 @@
1
1
  module CamaleonCms
2
2
  class PostTag < CamaleonCms::TermTaxonomy
3
- normalize_attrs(:name, :description)
4
-
5
- default_scope { where(taxonomy: :post_tag) }
3
+ normalize_attrs(:description)
6
4
 
7
5
  has_many :posts, foreign_key: :objectid, through: :term_relationships, source: :object
8
- belongs_to :post_type, foreign_key: :parent_id, inverse_of: :post_tags, required: false
9
- belongs_to :owner, class_name: CamaManager.get_user_class_name, foreign_key: :user_id, required: false
6
+ belongs_to :post_type, foreign_key: :parent_id, inverse_of: :post_tags, optional: true
7
+ belongs_to :owner, class_name: CamaManager.get_user_class_name.to_s, foreign_key: :user_id, optional: true,
8
+ inverse_of: false
10
9
  end
11
10
  end
@@ -1,9 +1,8 @@
1
1
  module CamaleonCms
2
2
  class PostType < CamaleonCms::TermTaxonomy
3
- normalize_attrs(:name, :description)
3
+ normalize_attrs(:description)
4
4
 
5
5
  alias_attribute :site_id, :parent_id
6
- default_scope { where(taxonomy: :post_type) }
7
6
 
8
7
  has_many :categories, foreign_key: :parent_id, dependent: :destroy, inverse_of: :post_type_parent
9
8
  has_many :post_tags, foreign_key: :parent_id, dependent: :destroy, inverse_of: :post_type
@@ -12,12 +11,12 @@ module CamaleonCms
12
11
  has_many :posts_through_categories, foreign_key: :objectid, through: :term_relationships, source: :object
13
12
  has_many :posts_draft, class_name: 'CamaleonCms::Post', foreign_key: :taxonomy_id, dependent: :destroy,
14
13
  inverse_of: :post_type
15
- has_many :field_group_taxonomy, lambda {
16
- where('object_class LIKE ?', 'PostType_%')
17
- }, class_name: 'CamaleonCms::CustomField', foreign_key: :objectid, dependent: :destroy
14
+ has_many :field_group_taxonomy, -> { where('object_class LIKE ?', 'PostType_%') },
15
+ class_name: 'CamaleonCms::CustomField', foreign_key: :objectid, dependent: :destroy, inverse_of: :owner
18
16
 
19
- belongs_to :owner, class_name: CamaManager.get_user_class_name, foreign_key: :user_id, required: false
20
- belongs_to :site, foreign_key: :parent_id, required: false
17
+ belongs_to :owner, class_name: CamaManager.get_user_class_name.to_s, foreign_key: :user_id, optional: true,
18
+ inverse_of: false
19
+ belongs_to :site, foreign_key: :parent_id, optional: true, inverse_of: :post_types
21
20
 
22
21
  scope :visible_menu, -> { where(term_group: nil) }
23
22
  scope :hidden_menu, -> { where(term_group: -1) }
@@ -92,7 +91,7 @@ module CamaleonCms
92
91
  def default_category
93
92
  return unless manage_categories?
94
93
 
95
- cat = categories.find_by_slug('uncategorized')
94
+ cat = categories.find_by_slug('uncategorized') # rubocop:disable Rails/DynamicFindBy
96
95
  unless cat
97
96
  cat = categories.create({ name: 'Uncategorized', slug: 'uncategorized', parent_id: id })
98
97
  cat.set_option('not_deleted', true)
@@ -110,10 +109,17 @@ module CamaleonCms
110
109
  # summary: String resume (optional) => default (empty)
111
110
  # post_order: Integer to define the order position in the list (optional)
112
111
  # fields: Hash of values for custom fields, sample => fields: {subtitle: 'abc', icon: 'test' } (optional)
113
- # settings: Hash of post settings, sample => settings:
114
- # {has_content: false, has_summary: true, default_layout: 'my_layout', default_template: 'my_template' } (optional, see more in post.set_setting(...))
112
+ # settings: Hash of post settings, sample => settings: (optional, see more in post.set_setting(...))
113
+ # {has_content: false, has_summary: true, default_layout: 'my_layout', default_template: 'my_template' }
115
114
  # data_metas: {template: "", layout: ""}
116
- # sample: my_posttype.add_post(title: "My Title", post_order: 5, content: 'lorem_ipsum', settings: {default_template: "home/counters", has_content: false, has_seo: false, skip_fields: ["sub_tite", 'banner']}, fields: {pattern: true, bg: 'https://www.reallusion.com/de/images/3dx5/whatsnew/3dx5_features_banner_bg_02.jpg'})
115
+ # sample:
116
+ # my_posttype.add_post(
117
+ # title: "My Title", post_order: 5, content: 'lorem_ipsum',
118
+ # settings: {
119
+ # default_template: "home/counters", has_content: false, has_seo: false, skip_fields: ["sub_tite", 'banner']
120
+ # },
121
+ # fields: { pattern: true, bg: 'https://www.reallusion.com/de/images/3dx5/whatsnew/3dx5_features_banner_bg_02.jpg' }
122
+ # )
117
123
  # More samples here: https://gist.github.com/owen2345/eba9691585ed78ad6f7b52e9591357bf
118
124
  # return created post if it was created, else return errors
119
125
  def add_post(args)
@@ -125,7 +131,7 @@ module CamaleonCms
125
131
  args[:data_tags] = args.delete(:tags)
126
132
  _thumb = args.delete(:thumb)
127
133
  p = posts.new(args)
128
- p.slug = site.get_valid_post_slug(p.title.parameterize) unless p.slug.present?
134
+ p.slug = site.get_valid_post_slug(p.title.parameterize) if p.slug.blank?
129
135
  if p.save!
130
136
  _settings.each { |k, v| p.set_setting(k, v) } if _settings.present?
131
137
  p.set_position(_order_position) if _order_position.present?
@@ -139,6 +145,7 @@ module CamaleonCms
139
145
  end
140
146
 
141
147
  # return all available route formats of this post type for content posts
148
+ # rubocop:disable Layout/LineLength
142
149
  def contents_route_formats
143
150
  {
144
151
  'post_of_post_type' => '<code>/group/:post_type_id-:title/:slug</code><br> (Sample: https://localhost.com/group/17-services/myservice.html)',
@@ -149,6 +156,7 @@ module CamaleonCms
149
156
  'hierarchy_post' => '<code>/:parent1_slug/:parent2_slug/.../:slug</code><br> (Sample: https://localhost.com/item-1/item-1-1/item-111.html)'
150
157
  }
151
158
  end
159
+ # rubocop:enable Layout/LineLength
152
160
 
153
161
  # return the configuration of routes for post contents
154
162
  def contents_route_format
@@ -181,7 +189,7 @@ module CamaleonCms
181
189
 
182
190
  # destroy all custom field groups assigned to this post type
183
191
  def destroy_field_groups
184
- if !destroyed_by_association.present? && %w[post page].include?(slug)
192
+ if destroyed_by_association.blank? && %w[post page].include?(slug)
185
193
  errors.add(:base, 'This post type can not be deleted.')
186
194
  return false
187
195
  end