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,487 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
4
- require 'tempfile'
5
-
6
3
  module CamaleonCms
4
+ # Uploader entry point for views, ActiveJobs, and any standalone object that includes
5
+ # it (see config/initializers/custom_initializers.rb). The implementation lives in the
6
+ # shared modules below, which CamaleonCms::RuntimeUploaderConcern includes as well, so
7
+ # the two entry points cannot drift.
7
8
  module UploaderHelper
8
- UNSAFE_EVENT_PATTERNS = %w[
9
- onabort onafter onbefore onblur oncanplay onchange onclick oncontextmenu oncopy oncuechange oncut ondblclick
10
- ondrag ondrop ondurationchange onended onerror onfocus onhashchange oninvalid oninput onkey onload onmessage
11
- onmouse ononline onoffline onpagehide onpageshow onpage onpaste onpause onplay onpopstate onprogress
12
- onpropertychange onratechange onreadystatechange onreset onresize onscroll onsearch onseek onselect onshow
13
- onstalled onstorage onsuspend ontimeupdate ontoggle onunloadonsubmit onvolumechange onwaiting onwheel
14
- ].map { |pattern| /#{pattern}\w*\s*=/i }.freeze
15
-
16
- SUSPICIOUS_PATTERNS = (UNSAFE_EVENT_PATTERNS + [
17
- /<script[\s>]/i, # Script tags
18
- /javascript:/i, # JavaScript in href/src attributes
19
- /<iframe[\s>]/i, # Iframes
20
- /<object[\s>]/i, # Object tags
21
- /<embed[\s>]/i, # Embed tags
22
- /<base[\s>]/i, # Base tags (can be used to manipulate URLs)
23
- /data:/i # data: URLs (which can include scripts)
24
- ]).freeze
25
-
9
+ include UploaderContentSecurity
10
+ include UploaderPathSecurity
11
+ include UploaderPipeline
12
+ include UploaderImageProcessing
13
+ include UploaderSupport
26
14
  include ActionView::Helpers::NumberHelper
27
15
  include CamaleonCms::CamaleonHelper
28
16
 
29
- # upload a file into server
30
- # settings:
31
- # folder: Directory where the file will be saved (default: "")
32
- # sample: temporal => will save in /rails_path/public/temporal
33
- # generate_thumb: true, # generate thumb image if this is image format (default true)
34
- # maximum: maximum bytes permitted to upload (default: 1000MG)
35
- # dimension: dimension for the image (sample: 30x30 | x30 | 30x | 300x300?)
36
- # formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
37
- # remove_source: Boolean (delete source file after saved if this is true, default false)
38
- # same_name: Boolean (save the file with the same name if defined true, else search for a non used name)
39
- # versions: (String) Create additional multiple versions of the image uploaded,
40
- # sample: '300x300,505x350' ==> Will create two extra images with these dimensions
41
- # sample "test.png", versions: '200x200,450x450' will generate: thumb/test-png_200x200.png, test-png_450x450.png
42
- # thumb_size: String (redefine the dimensions of the thumbnail, sample: '100x100' ==> only for images)
43
- # temporal_time: if great than 0 seconds, then this file will expire (removed) in that time (default: 0)
44
- # To manage jobs, please check https://edgeguides.rubyonrails.org/active_job_basics.html
45
- # Note: if you are using temporal_time, you will need to copy the file to another directory later
46
- # sample: upload_file(params[:my_file], {formats: "images", folder: "temporal"})
47
- # sample: upload_file(params[:my_file], {formats: "jpg,png,gif,mp3,mp4", temporal_time: 10.minutes, maximum: 10.megabytes})
48
- def upload_file(uploaded_io, settings = {})
49
- cached_name = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.original_filename : nil
50
- return { error: 'File is empty', file: nil, size: nil } unless uploaded_io.present?
51
-
52
- if uploaded_io.is_a?(String) && uploaded_io.match(%r{^https?://}).present? # download url file
53
- tmp = cama_tmp_upload(uploaded_io)
54
- return tmp if tmp[:error].present?
55
-
56
- settings[:remove_source] = true
57
- uploaded_io = tmp[:file_path]
58
- end
59
- uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
60
- if settings[:dimension].present?
61
- uploaded_io = File.open(cama_resize_upload(uploaded_io.path, settings[:dimension]))
62
- end
63
-
64
- return { error: 'Potentially malicious content found!' } if file_content_unsafe?(uploaded_io)
65
-
66
- settings = settings.to_sym
67
- settings[:uploaded_io] = uploaded_io
68
- settings = {
69
- folder: '',
70
- maximum: current_site.get_option('filesystem_max_size', 100).to_f.megabytes,
71
- formats: '*',
72
- generate_thumb: true,
73
- temporal_time: 0,
74
- filename: begin
75
- cached_name || uploaded_io.original_filename
76
- rescue StandardError
77
- uploaded_io.path.split('/').last
78
- end.cama_fix_filename,
79
- file_size: File.size(uploaded_io.to_io),
80
- remove_source: false,
81
- same_name: false,
82
- versions: '',
83
- thumb_size: nil
84
- }.merge!(settings)
85
- hooks_run('before_upload', settings)
86
-
87
- # guard against path traversal
88
- return { error: 'Invalid file path' } unless cama_uploader.valid_folder_path?(settings[:folder])
89
-
90
- # formats validations
91
- err = validate_file_format_or_error(uploaded_io.path, settings[:formats])
92
- return err if err
93
-
94
- # file size validations
95
- if settings[:maximum] < settings[:file_size]
96
- max_size = number_to_human_size(settings[:maximum])
97
- return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
98
- end
99
- # save file
100
- key = File.join(settings[:folder], settings[:filename]).to_s.cama_fix_slash
101
- res = cama_uploader.add_file(settings[:uploaded_io], key, { same_name: settings[:same_name] })
102
-
103
- # generate image versions
104
- if res['file_type'] == 'image'
105
- settings[:versions].to_s.gsub(' ', '').split(',').each do |v|
106
- version_path = cama_resize_upload(settings[:uploaded_io].path, v, { replace: false })
107
- cama_uploader.add_file(version_path, cama_uploader.version_path(res['key'], v), is_thumb: true,
108
- same_name: true)
109
- FileUtils.rm_f(version_path)
110
- end
111
- end
112
-
113
- # generate thumb
114
- if settings[:generate_thumb] && res['thumb'].present?
115
- cama_uploader_generate_thumbnail(uploaded_io.path, res['key'], settings[:thumb_size],
116
- settings[:remove_source])
117
- end
118
- FileUtils.rm_f(uploaded_io.path) if settings[:remove_source] && File.exist?(uploaded_io.path)
119
-
120
- hooks_run('after_upload', settings)
121
-
122
- # temporal file upload (always put as local for temporal files)
123
- CamaleonCmsUploader.delete_block.call(settings, cama_uploader, key) if settings[:temporal_time] > 0
124
-
125
- res
126
- end
127
-
128
- # generate thumbnail of a existent image
129
- # key: key of the current file
130
- # the thumbnail will be saved in my_images/my_img.png => my_images/thumb/my_img.png
131
- def cama_uploader_generate_thumbnail(uploaded_io, key, thumb_size = nil, remove_source = false)
132
- w = thumb_size.present? ? thumb_size.split('x')[0] : cama_uploader.thumb[:w]
133
- h = thumb_size.present? ? thumb_size.split('x')[1] : cama_uploader.thumb[:h]
134
- uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
135
- path_thumb = cama_resize_and_crop(uploaded_io.path, w, h)
136
- thumb = cama_uploader.add_file(path_thumb, cama_uploader.version_path(key).sub('.svg', '.jpg'), is_thumb: true,
137
- same_name: true)
138
- FileUtils.rm_f(path_thumb) if remove_source
139
- thumb
140
- end
141
-
142
- # helper to find an available filename for file_path in that directory
143
- # sample: uploader_verify_name("/var/www/my_image.jpg")
144
- # return "/var/www/my_image_1.jpg" => if "/var/www/my_image.jpg" exist
145
- # return "/var/www/my_image.jpg" => if "/var/www/my_image.jpg" doesn't exist
146
- def uploader_verify_name(file_path)
147
- dir = File.dirname(file_path)
148
- filename = File.basename(file_path).to_s.cama_fix_filename
149
- files = Dir.entries(dir)
150
- if files.include?(filename)
151
- i = 1
152
- _filename = filename
153
- while files.include?(_filename)
154
- _filename = "#{File.basename(filename, File.extname(filename))}_#{i}#{File.extname(filename)}"
155
- i += 1
156
- end
157
- filename = _filename
158
- end
159
- "#{File.dirname(file_path)}/#{filename}"
160
- end
161
-
162
- # convert downloaded file path into public url
163
- def cama_file_path_to_url(file_path)
164
- file_path.sub(Rails.public_path.to_s, begin
165
- root_url
166
- rescue StandardError
167
- cama_root_url
168
- end)
169
- end
170
-
171
- # convert public url to file path
172
- def cama_url_to_file_path(url)
173
- File.join(Rails.public_path, URI(url.to_s).path)
174
- end
175
-
176
- # crop and image and saved as imagename_crop.ext
177
- # file: file path
178
- # w: new width
179
- # h: new height
180
- # w_offset: left offset
181
- # w_offset: top offset
182
- # resize: true/false
183
- # (true => resize the image to this dimension)
184
- # (false => crop the image with this dimension)
185
- # replace: Boolean (replace current image or create another file)
186
- def cama_crop_image(file_path, w = nil, h = nil, w_offset = 0, h_offset = 0, resize = false, replace = true)
187
- force = w.present? && h.present? && !w.include?('?') && !h.include?('?') ? '!' : ''
188
- img = MiniMagick::Image.open(file_path)
189
- w = clamp_to_image_dimension(w, img[:width])
190
- h = clamp_to_image_dimension(h, img[:height])
191
- data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, resize: resize, replace: replace }
192
- hooks_run('before_crop_image', data)
193
- data[:img].combine_options do |i|
194
- i.resize("#{w if w.present?}x#{h if h.present?}#{force}") if data[:resize]
195
- i.crop "#{w if w.present?}x#{h if h.present?}+#{w_offset}+#{h_offset}#{force}" unless data[:resize]
196
- end
197
-
198
- ext = File.extname(file_path)
199
- res = data[:replace] ? file_path : file_path.gsub(ext, "_crop#{ext}")
200
- data[:img].write res
201
- res
202
- end
203
-
204
- # resize and crop a file
205
- # SVGs are converted to JPEGs for editing
206
- # Params:
207
- # file: (String) File path
208
- # w: (Integer) width
209
- # h: (Integer) height
210
- # settings:
211
- # gravity: (Sym, default :north_east) Crop position: :north_west, :north, :north_east, :east, :south_east, :south, :south_west, :west, :center
212
- # overwrite: (Boolean, default true) true for overwrite current image with resized resolutions, false: create other file called with prefix "crop_"
213
- # output_name: (String, default prefixd name with crop_), permit to define the output name of the thumbnail if overwrite = true
214
- # Return: (String) file path where saved this cropped
215
- # sample: cama_resize_and_crop(my_file, 200, 200, {gravity: :north_east, overwrite: false})
216
- def cama_resize_and_crop(file, w, h, settings = {})
217
- settings = { gravity: :north_east, overwrite: true, output_name: +'' }.merge!(settings)
218
- img = MiniMagick::Image.open(file)
219
- if file.end_with? '.svg'
220
- img.format 'jpg'
221
- file.sub! '.svg', '.jpg'
222
- settings[:output_name]&.sub!('.svg', '.jpg')
223
- end
224
- w = clamp_to_image_dimension(w, img[:width])
225
- h = clamp_to_image_dimension(h, img[:height])
226
- w_original = img[:width].to_f
227
- h_original = img[:height].to_f
228
- w = w.to_i if w.present?
229
- h = h.to_i if h.present?
230
-
231
- # check proportions
232
- if w_original * h < h_original * w
233
- op_resize = "#{w.to_i}x"
234
- w_result = w
235
- h_result = (h_original * w / w_original)
236
- else
237
- op_resize = "x#{h.to_i}"
238
- w_result = (w_original * h / h_original)
239
- h_result = h
240
- end
241
-
242
- w_offset, h_offset = cama_crop_offsets_by_gravity(settings[:gravity], [w_result, h_result], [w, h])
243
- data = { img: img, w: w, h: h, w_offset: w_offset, h_offset: h_offset, op_resize: op_resize, settings: settings }
244
- hooks_run('before_resize_crop', data)
245
- data[:img].combine_options do |i|
246
- i.resize(data[:op_resize])
247
- i.gravity(settings[:gravity])
248
- i.crop "#{data[:w].to_i}x#{data[:h].to_i}+#{data[:w_offset]}+#{data[:h_offset]}!"
249
- end
250
-
251
- if settings[:overwrite]
252
- data[:img].write(file.sub('.svg', '.jpg'))
253
- elsif settings[:output_name].present?
254
- data[:img].write(file = File.join(File.dirname(file), settings[:output_name]).to_s)
255
- else
256
- data[:img].write(file = uploader_verify_name(File.join(File.dirname(file),
257
- "crop_#{File.basename(file.sub('.svg', '.jpg'))}")))
258
- end
259
- file
260
- end
261
-
262
- # upload tmp file
263
- # support for url and local path
264
- # sample:
265
- # cama_tmp_upload('https://camaleon.website/media/132/logo2.png') ==> /var/rails/my_project/public/tmp/1/logo2.png
266
- # cama_tmp_upload('/var/www/media/132/logo 2.png') ==> /var/rails/my_project/public/tmp/1/logo-2.png
267
- # accept args:
268
- # name: to indicate the name to use, sample: cama_tmp_upload('/var/www/media/132/logo 2.png', {name: 'owen.png', formats: 'images'})
269
- # formats: extensions permitted, sample: jpg,png,... or generic: images | videos | audios | documents (default *)
270
- # dimension: 20x30
271
- # return: {file_path, error}
272
- def cama_tmp_upload(uploaded_io, args = {})
273
- tmp_path = args[:path] || File.join(Rails.public_path, 'tmp', current_site.id.to_s).to_s
274
- FileUtils.mkdir_p(tmp_path) unless Dir.exist?(tmp_path)
275
- saved = false
276
- downloaded_tmp_file = nil
277
- if uploaded_io.is_a?(String) && uploaded_io.start_with?('data:') # create tmp file using base64 format
278
- _tmp_name = args[:name]
279
- return { error: cama_t('camaleon_cms.admin.media.name_required').to_s } unless params[:name].present?
280
-
281
- err = validate_file_format_or_error(_tmp_name, args[:formats])
282
- return err if err
283
-
284
- path = uploader_verify_name(File.join(tmp_path, _tmp_name))
285
- File.open(path, 'wb') { |f| f.write(Base64.decode64(uploaded_io.split(';base64,').last)) }
286
- uploaded_io = File.open(path)
287
- saved = true
288
- elsif uploaded_io.is_a?(String) && (uploaded_io.start_with?('http://') || uploaded_io.start_with?('https://'))
289
- err = validate_file_format_or_error(uploaded_io, args[:formats])
290
- return err if err
291
-
292
- if uploaded_io.include?(current_site.the_url(locale: nil))
293
- uploaded_io = File.join(Rails.public_path, uploaded_io.sub(current_site.the_url(locale: nil), '')).to_s
294
- else
295
- remote_file = cama_download_remote_file(uploaded_io)
296
- return remote_file if remote_file[:error].present?
297
-
298
- downloaded_tmp_file = remote_file[:file]
299
- uploaded_io = downloaded_tmp_file
300
- end
301
- _tmp_name = if uploaded_io.is_a?(String)
302
- uploaded_io.split('/').last.split('?').first
303
- else
304
- uploaded_io.path.split('/').last
305
- end
306
- args[:name] = args[:name] || _tmp_name
307
- end
308
- uploaded_io = File.open(uploaded_io) if uploaded_io.is_a?(String)
309
- err = validate_file_format_or_error(_tmp_name || uploaded_io.path, args[:formats])
310
- return err if err
311
-
312
- actual_size = begin
313
- uploaded_io.size
314
- rescue StandardError
315
- File.size(uploaded_io)
316
- end
317
- if args[:maximum].present? && args[:maximum] < actual_size
318
- max_size = number_to_human_size(args[:maximum])
319
- return { error: "#{ct('file_size_exceeded', default: 'File size exceeded')} (#{max_size})" }
320
- end
321
-
322
- name = args[:name] || uploaded_io&.original_filename || uploaded_io.path.split('/').last
323
- name = "#{File.basename(name, File.extname(name)).parameterize}#{File.extname(name)}"
324
- path ||= uploader_verify_name(File.join(tmp_path, name))
325
- File.open(path, 'wb') { |f| f.write(uploaded_io.read) } unless saved
326
- path = cama_resize_upload(path, args[:dimension]) if args[:dimension].present?
327
- { file_path: path, error: nil }
328
- ensure
329
- downloaded_tmp_file&.close!
330
- end
331
-
332
- # resize image if the format is correct
333
- # return resized file path
334
- def cama_resize_upload(image_path, dimension, args = {})
335
- if cama_uploader.class.validate_file_format(image_path, 'image') && dimension.present?
336
- dim_parts = dimension.split('x')
337
- r = { file: image_path, w: dim_parts[0], h: dim_parts[1], w_offset: 0, h_offset: 0,
338
- resize: !dim_parts[2] || dim_parts[2] == 'resize',
339
- replace: true, gravity: :north_east }.merge!(args)
340
- hooks_run('on_uploader_resize', r)
341
- image_path = if r[:w].present? && r[:h].present?
342
- cama_resize_and_crop(r[:file], r[:w], r[:h], { overwrite: r[:replace], gravity: r[:gravity] })
343
- else
344
- cama_crop_image(r[:file], r[:w], r[:h], r[:w_offset], r[:h_offset], r[:resize], r[:replace])
345
- end
346
- end
347
- image_path
348
- end
349
-
350
- # return the current uploader
351
- def cama_uploader
352
- @cama_uploader ||= lambda {
353
- thumb = current_site.get_option('filesystem_thumb_size', '100x100').split('x')
354
- args = {
355
- server: current_site.get_option('filesystem_type', 'local').downcase,
356
- thumb: { w: thumb[0], h: thumb[1] },
357
- aws_settings: {
358
- region: current_site.get_option('filesystem_region', 'us-west-2'),
359
- access_key: current_site.get_option('filesystem_s3_access_key'),
360
- secret_key: current_site.get_option('filesystem_s3_secret_key'),
361
- bucket: current_site.get_option('filesystem_s3_bucket_name'),
362
- cloud_front: current_site.get_option('filesystem_s3_cloudfront'),
363
- aws_file_upload_settings: ->(settings) { settings }, # permit to add your custom attributes for file_upload https://docs.aws.amazon.com/sdkforruby/api/Aws/S3/Object.html#upload_file-instance_method
364
- aws_file_read_settings: ->(data, _s3_file) { data } # permit to read custom attributes from aws file and add to file parsed object
365
- },
366
- custom_uploader: nil # possibility to use custom file uploader
367
- }
368
- hooks_run('on_uploader', args)
369
- return args[:custom_uploader] if args[:custom_uploader].present?
370
-
371
- base_args = { current_site: current_site, thumb: args[:thumb] }
372
- case args[:server]
373
- when 's3', 'aws'
374
- CamaleonCmsAwsUploader.new(base_args.merge(aws_settings: args[:aws_settings]), self)
375
- else
376
- CamaleonCmsLocalUploader.new(base_args, self)
377
- end
378
- }.call
17
+ # Message seam overrides (see CamaleonCms::UploaderPipeline). This entry point runs
18
+ # where `ct` and `cama_t` exist, so upload errors keep going through them: `ct` fires
19
+ # the `on_translation` hook, letting plugins override the text. The pipeline defaults
20
+ # go straight to I18n, which is all a controller can do.
21
+ def cama_uploader_ct(key, args = {})
22
+ ct(key, args)
379
23
  end
380
24
 
381
- def slugify(val)
382
- val.to_s.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '')
25
+ def cama_uploader_t(key, args = {})
26
+ cama_t(key, args)
383
27
  end
384
28
 
385
- def slugify_folder(val)
386
- split_folder = val.split('/')
387
- split_folder[-1] = slugify(split_folder[-1])
388
- split_folder.join('/')
389
- end
390
-
391
- private
392
-
393
- # Download remote files with SSRF guardrails: validate host/IP and reject redirects.
394
- # NOTE: UserUrlValidator.validate is intentionally called here even though the
395
- # crop_url controller path may have already validated the URL — this ensures
396
- # every caller of cama_tmp_upload is protected (defense-in-depth).
397
- def cama_download_remote_file(url)
398
- validation_result = UserUrlValidator.validate(url)
399
- return { error: validation_result.join(', ') } if validation_result.is_a?(Array)
400
-
401
- uri = URI.parse(url)
402
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
403
- http.open_timeout = 10
404
- http.read_timeout = 10
405
- http.request(Net::HTTP::Get.new(uri.request_uri.presence || '/'))
406
- end
407
-
408
- return { error: 'Redirects are not allowed for remote uploads.' } if response.is_a?(Net::HTTPRedirection)
409
-
410
- return { error: "Unable to download remote file (HTTP #{response.code})." } unless response.is_a?(Net::HTTPSuccess)
411
-
412
- # Enforce the site's maximum upload size to prevent memory exhaustion from oversized responses.
413
- max_bytes = current_site.get_option('filesystem_max_size', 100).to_f.megabytes
414
- body = response.body
415
- if body.bytesize > max_bytes
416
- return { error: "Remote file too large (max #{ActiveSupport::NumberHelper.number_to_human_size(max_bytes)})." }
417
- end
418
-
419
- ext = File.extname(uri.path.to_s)
420
- tempfile = Tempfile.new(['cama-upload-url', ext], binmode: true)
421
- tempfile.write(body)
422
- tempfile.rewind
423
- { file: tempfile, error: nil }
424
- rescue StandardError => e
425
- { error: "Unable to download remote file: #{ERB::Util.html_escape(e.message)}" }
426
- end
427
-
428
- def file_content_unsafe?(uploaded_io)
429
- file = uploaded_io.is_a?(ActionDispatch::Http::UploadedFile) ? uploaded_io.tempfile : uploaded_io
430
- file_content_unsafe = nil
431
-
432
- file.set_encoding(Encoding::BINARY) if file.respond_to?(:binmode) && file.respond_to?(:set_encoding)
433
-
434
- # Read the file for pattern scanning, then rewind so subsequent consumers
435
- # (e.g. upload handlers) can read the full content. Failing to rewind
436
- # resulted in 0-byte uploads when the file was a Tempfile (see report).
437
- file_content = file.read
438
- file.rewind if file.respond_to?(:rewind)
439
- SUSPICIOUS_PATTERNS.each do |pattern|
440
- if file_content =~ pattern
441
- Rails.logger.info { "Potentially malicious content found: #{pattern.inspect}" }
442
- break file_content_unsafe = pattern.inspect
443
- end
444
- end
445
-
446
- file_content_unsafe
447
- end
448
-
449
- # helper for resize and crop method
450
- def cama_crop_offsets_by_gravity(gravity, original_dimensions, cropped_dimensions)
451
- original_width, original_height = original_dimensions
452
- cropped_width, cropped_height = cropped_dimensions
453
-
454
- vertical_offset = case gravity
455
- when :north_west, :north, :north_east then 0
456
- when :center, :east, :west then [((original_height - cropped_height) / 2.0).to_i, 0].max
457
- when :south_west, :south, :south_east then (original_height - cropped_height).to_i
458
- end
459
-
460
- horizontal_offset = case gravity
461
- when :north_west, :west, :south_west then 0
462
- when :center, :north, :south then [((original_width - cropped_width) / 2.0).to_i, 0].max
463
- when :north_east, :east, :south_east then (original_width - cropped_width).to_i
464
- end
465
-
466
- [horizontal_offset, vertical_offset]
467
- end
468
-
469
- # convert file path into thumb path format
470
- # return the image name into thumb format: owewen.png into thumb/owen-png.png
471
- def cama_parse_for_thumb_name(file_path)
472
- "#{@fog_connection_hook_res[:thumb_folder_name]}/#{File.basename(file_path).parameterize}#{File.extname(file_path)}"
473
- end
474
-
475
- def clamp_to_image_dimension(value, img_size)
476
- return value unless value.present? && value.to_s.include?('?')
477
-
478
- img_size.to_f > value.sub('?', '').to_i ? value.sub('?', '') : img_size
479
- end
480
-
481
- def validate_file_format_or_error(file, formats)
482
- return if cama_uploader.class.validate_file_format(file, formats)
483
-
484
- { error: "#{ct('file_format_error')} (#{formats})" }
29
+ def cama_uploader_human_size(bytes)
30
+ number_to_human_size(bytes)
485
31
  end
486
32
  end
487
33
  end
@@ -1,15 +1,16 @@
1
1
  module CamaleonCms
2
2
  module UserRolesHelper
3
3
  def cama_get_roles_values
4
- @_cache_cama_get_roles_values ||= lambda {
4
+ @_cache_cama_get_roles_values ||= lambda do
5
5
  roles_list = CamaleonCms::UserRole::ROLES
6
- # permit to add custom roles to be listed in editing roles form
7
- # sample: args[:roles_list][:manager] << { key: 'my_role_key', label: "my_custom_permission", description: "lorem ipsum"}
6
+ # permit adding custom roles to be listed in editing roles form
7
+ # sample:
8
+ # args[:roles_list][:manager] << {key: 'my_role_key', label: "my_custom_permission", description: "lorem ipsum"}
8
9
  # authorize! :manage, :my_role_key
9
10
  args = { roles_list: roles_list }
10
11
  hooks_run('available_user_roles_list', args)
11
12
  args[:roles_list]
12
- }.call
13
+ end.call
13
14
  end
14
15
  end
15
16
  end
@@ -1,5 +1,5 @@
1
1
  module CamaleonCms
2
- class HtmlMailer < ActionMailer::Base
2
+ class HtmlMailer < ActionMailer::Base # rubocop:disable Rails/ApplicationMailer
3
3
  include CamaleonCms::SiteHelper
4
4
  include CamaleonCms::HooksHelper
5
5
  include CamaleonCms::PluginsHelper
@@ -23,7 +23,7 @@ module CamaleonCms
23
23
  layout_name: 'camaleon_cms/mailer',
24
24
  format: 'html'
25
25
  }.merge(data)
26
- data[:cc_to] = [data[:cc_to]] if data[:cc_to].is_a?(String) || !data[:cc_to].present?
26
+ data[:cc_to] = [data[:cc_to]] if data[:cc_to].is_a?(String) || data[:cc_to].blank?
27
27
 
28
28
  mail_data = { to: email, subject: subject }
29
29
  if @current_site.get_option('mailer_enabled') == 1
@@ -72,7 +72,7 @@ module CamaleonCms
72
72
  end
73
73
  end
74
74
 
75
- layout = data[:layout_name].present? ? data[:layout_name] : false
75
+ layout = data[:layout_name].presence || false
76
76
  if data[:template_name].present? # render email with template
77
77
  if data[:format] == 'html'
78
78
  mail(mail_data) do |format|
@@ -92,7 +92,7 @@ module CamaleonCms
92
92
  mail(mail_data) { |format| format.html { render inline: @html, layout: layout } } if data[:format] == 'html'
93
93
  mail(mail_data) { |format| format.text { render inline: @html, layout: layout } } if data[:format] == 'txt'
94
94
  end
95
- mail(mail_data) unless data[:format].present?
95
+ mail(mail_data) if data[:format].blank?
96
96
  end
97
97
 
98
98
  private
@@ -54,6 +54,10 @@ module CamaleonCms
54
54
  safe_can :post_tags, CamaleonCms::PostType do |pt|
55
55
  @roles_post_type[:manage_tags].to_i.include?(pt.id)
56
56
  end
57
+ safe_can :post_content_unfiltered_html, CamaleonCms::PostType do |pt|
58
+ ids_unfiltered = @roles_post_type[:post_content_unfiltered_html] || []
59
+ ids_unfiltered.to_i.include?(pt.id)
60
+ end
57
61
 
58
62
  safe_can :update, CamaleonCms::Post do |post|
59
63
  pt_id = post.post_type.id
@@ -84,12 +88,7 @@ module CamaleonCms
84
88
  end
85
89
 
86
90
  # others
87
- %i[media comments themes widgets nav_menu plugins users settings custom_fields select_eval].each do |manager_key|
88
- safe_can :manage, manager_key if @roles_manager[manager_key]
89
- end
90
- @roles_manager.try(:each) do |rol_manage_key, val_role|
91
- safe_can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
92
- end
91
+ define_manage_rules
93
92
  end
94
93
  cannot :impersonate, CamaleonCms::User do |u|
95
94
  u.id == user.id
@@ -119,7 +118,7 @@ module CamaleonCms
119
118
  def safe_can(action, subject, &block)
120
119
  if block_given?
121
120
  can(action, subject) do |resource|
122
- safely_false { block.call(resource) }
121
+ safely_false { yield(resource) }
123
122
  end
124
123
  else
125
124
  # No block: can(action, subject) does not evaluate user logic; safe to call directly.
@@ -134,5 +133,14 @@ module CamaleonCms
134
133
  rescue StandardError
135
134
  false
136
135
  end
136
+
137
+ def define_manage_rules
138
+ %i[media comments themes widgets nav_menu plugins users settings custom_fields select_eval].each do |resource|
139
+ safe_can :manage, resource if @roles_manager[resource]
140
+ end
141
+ @roles_manager.each do |rol_manage_key, val_role|
142
+ safe_can :manage, rol_manage_key.to_sym if val_role.to_s.cama_true?
143
+ end
144
+ end
137
145
  end
138
146
  end
@@ -1,21 +1,21 @@
1
1
  module CamaleonCms
2
2
  class Category < CamaleonCms::TermTaxonomy
3
- normalize_attrs(:name, :description)
3
+ normalize_attrs(:description)
4
4
 
5
5
  alias_attribute :site_id, :term_group
6
6
  alias_attribute :post_type_id, :status
7
7
 
8
- default_scope { where(taxonomy: :category) }
9
8
  scope :no_empty, -> { where('count > 0') } # return all categories that contains at least one post
10
9
  scope :empty, -> { where(count: [0, nil]) } # return all categories that does not contain any post
11
10
  # scope :parents, -> { where("term_taxonomy.parent_id IS NULL") }
12
11
 
13
12
  has_many :posts, foreign_key: :objectid, through: :term_relationships, source: :object
14
- has_many :children, class_name: 'CamaleonCms::Category', foreign_key: :parent_id, dependent: :destroy
15
- belongs_to :parent, class_name: 'CamaleonCms::Category', foreign_key: :parent_id, required: false
13
+ has_many :children, class_name: 'CamaleonCms::Category', foreign_key: :parent_id, dependent: :destroy,
14
+ inverse_of: :parent
15
+ belongs_to :parent, class_name: 'CamaleonCms::Category', optional: true
16
16
  belongs_to :post_type_parent, class_name: 'CamaleonCms::PostType', foreign_key: :parent_id,
17
- inverse_of: :categories, required: false
18
- belongs_to :site, required: false
17
+ inverse_of: :categories, optional: true
18
+ belongs_to :site, optional: true
19
19
 
20
20
  before_save :set_site
21
21
  before_destroy :set_posts_in_default_category
@@ -14,15 +14,24 @@ module CamaleonCms
14
14
  # status: nil -> visible on list group fields
15
15
  # attr_accessible :object_class, :objectid, :description, :parent_id, :count, :name, :slug,
16
16
  # :field_order, :status, :is_repeat
17
+
18
+ # Scope metas by object_class. Meta rows are keyed by (objectid, object_class), so a custom
19
+ # field whose numeric id collides with another model's id (e.g. a Post) would otherwise read
20
+ # the wrong "_default" meta and lose its field_key. See regression: TinyMCE editor field
21
+ # rendered as a plain text_box on Theme settings.
22
+ # rubocop:disable Rails/InverseOf
17
23
  has_many :metas, -> { where(object_class: 'CustomField') }, foreign_key: :objectid, dependent: :destroy
18
- has_many :values, class_name: 'CamaleonCms::CustomFieldsRelationship',
19
- foreign_key: :custom_field_id, dependent: :destroy
20
- belongs_to :custom_field_group, required: false
21
- belongs_to :parent, class_name: 'CamaleonCms::CustomField', foreign_key: :parent_id, required: false
24
+ # rubocop:enable Rails/InverseOf
25
+ has_many :values, class_name: 'CamaleonCms::CustomFieldsRelationship', dependent: :destroy
26
+
27
+ belongs_to :custom_field_group, class_name: 'CamaleonCms::CustomFieldGroup', foreign_key: :parent_id,
28
+ optional: true, inverse_of: :fields
29
+ belongs_to :parent, class_name: 'CamaleonCms::CustomField', optional: true
30
+ belongs_to :owner, polymorphic: true, foreign_key: :objectid, foreign_type: :object_class, optional: true
22
31
 
23
32
  validates :name, :object_class, presence: true
24
- validates_uniqueness_of :slug, scope: %i[parent_id object_class],
25
- unless: ->(o) { o.is_a?(CamaleonCms::CustomFieldGroup) }
33
+ validates :slug, uniqueness: { scope: %i[parent_id object_class],
34
+ unless: ->(o) { o.is_a?(CamaleonCms::CustomFieldGroup) } }
26
35
 
27
36
  before_validation :before_validating
28
37
  before_update :check_select_eval_authorization