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
@@ -12,15 +12,18 @@ module CamaleonCms
12
12
  # return the excerpt of this post
13
13
  def the_excerpt(qty_chars = 200)
14
14
  excerpt = object.get_meta('summary').to_s.translate(get_locale)
15
- # r = {content: (excerpt.present? ? excerpt : object.content_filtered.to_s.translate(get_locale).strip_tags.gsub(/
|\n/, " ").truncate(qty_chars)), post: object}
15
+ # r = {
16
+ # content: (
17
+ # (excerpt if excerpt.present?) ||
18
+ # object.content_filtered.to_s.translate(get_locale).strip_tags.gsub(/
|\n/, " ").truncate(qty_chars)
19
+ # ),
20
+ # post: object
21
+ # }
16
22
  r = {
17
- content: (if excerpt.present?
18
- excerpt
19
- else
20
- h.cama_strip_shortcodes(object.content_filtered.to_s.translate(get_locale).strip_tags.gsub(
21
- /
|\n/, ' '
22
- ).truncate(qty_chars))
23
- end), post: object
23
+ content:
24
+ excerpt.presence || h.cama_strip_shortcodes(object.content_filtered.to_s.translate(get_locale)
25
+ .strip_tags.gsub(/
|\n/, ' ').truncate(qty_chars)),
26
+ post: object
24
27
  }
25
28
  h.hooks_run('post_the_excerpt', r)
26
29
  r[:content]
@@ -33,17 +36,13 @@ module CamaleonCms
33
36
  h.do_shortcode(r[:content], self)
34
37
  end
35
38
 
36
- # return thumbnail image for this post
39
+ # Return the thumbnail image for this post
37
40
  # default: default image if thumbails not exist
38
41
  # if default is empty, post_type default thumb will be returned
39
42
  def the_thumb_url(default = nil)
40
43
  th = object.get_meta('thumb')
41
- if th.present?
42
- th
43
- else
44
- default || object.post_type.get_option('default_thumb',
45
- nil) || h.asset_url('camaleon_cms/image-not-found.png')
46
- end
44
+ th.presence || default || object.post_type.get_option('default_thumb', nil) ||
45
+ h.asset_url('camaleon_cms/image-not-found.png')
47
46
  end
48
47
  alias the_image_url the_thumb_url
49
48
 
@@ -93,7 +92,7 @@ module CamaleonCms
93
92
  args[:label_cat] = I18n.t('routes.category', default: 'category')
94
93
  args[:category_id] = cat.id
95
94
  args[:title] = cat.the_title(args[:locale]).parameterize
96
- args[:title] = cat.the_slug unless args[:title].present?
95
+ args[:title] = cat.the_slug if args[:title].blank?
97
96
  else
98
97
  p_url_format = 'post'
99
98
  end
@@ -110,7 +109,7 @@ module CamaleonCms
110
109
  args[:post_type_title] = ptype.the_title(args[:locale]).parameterize.presence || ptype.the_slug
111
110
  args[:category_id] = cat.id
112
111
  args[:title] = cat.the_title(args[:locale]).parameterize
113
- args[:title] = cat.the_slug unless args[:title].present?
112
+ args[:title] = cat.the_slug if args[:title].blank?
114
113
  else
115
114
  p_url_format = 'post'
116
115
  end
@@ -130,12 +129,10 @@ module CamaleonCms
130
129
  # sample: {es: 'https://mydomain.com/es/articulo-3.html', en: 'https://mydomain.com/en/post-3.html'}
131
130
  def the_urls(*args)
132
131
  args = args.extract_options!
133
- res = {}
134
- h.current_site.the_languages.each do |l|
132
+ h.current_site.the_languages.each_with_object({}) do |l, hsh|
135
133
  args[:locale] = l
136
- res[l] = the_url(args.clone)
134
+ hsh[l] = the_url(args.clone)
137
135
  end
138
- res
139
136
  end
140
137
 
141
138
  # return edit url for this post
@@ -148,10 +145,10 @@ module CamaleonCms
148
145
  # return html link
149
146
  # attrs: Hash of link tag attributes, sample: {id: "myid", class: "sss" }
150
147
  def the_edit_link(title = nil, attrs = {})
151
- return '' unless h.cama_current_user.present?
148
+ return '' if h.cama_current_user.blank?
152
149
 
153
- attrs = { target: '_blank', style: 'font-size:11px !important;cursor:pointer;' }.merge(attrs)
154
- h.link_to("→ #{title || h.ct('edit', default: 'Edit')}".html_safe, the_edit_url, attrs)
150
+ attrs = { target: '_blank', style: 'font-size:11px !important;cursor:pointer;' }.merge!(attrs)
151
+ h.link_to(h.safe_join(['→ ', title || h.ct('edit', default: 'Edit')]), the_edit_url, attrs)
155
152
  end
156
153
 
157
154
  # show thumbnail image as html
@@ -162,7 +159,7 @@ module CamaleonCms
162
159
  end
163
160
 
164
161
  # show link and thumbnail included as html
165
- # link_args: html attributes for link
162
+ # link_args: html attributes for the link
166
163
  # img_args: html attributes for image
167
164
  def the_link_thumb(link_args = {}, img_args = {})
168
165
  h.link_to(the_thumb(img_args), the_url, link_args)
@@ -186,7 +183,15 @@ module CamaleonCms
186
183
  color = 'default'
187
184
  status = self.status
188
185
  end
189
- "<span class='label label-#{color} label-form'>#{status.titleize}</span>"
186
+ # The `else` arm above returns the raw column value, which is not validated and is writable at
187
+ # contributor privilege, and three admin views render this label through `raw`. Escaping the
188
+ # interpolated value here -- at the source, as `the_title` does -- is what makes those sinks
189
+ # safe. `titleize` is not a mitigation: HTML tag names and hostnames are case-insensitive.
190
+ # `content_tag` is deliberately not used: it emits double-quoted attributes and would change
191
+ # the byte output for every legitimate status, which downstream themes may match on.
192
+ # rubocop:disable Rails/OutputSafety -- only the escaped status is interpolated into fixed markup
193
+ "<span class='label label-#{color} label-form'>#{h.h(status.titleize)}</span>".html_safe
194
+ # rubocop:enable Rails/OutputSafety
190
195
  end
191
196
 
192
197
  # return the user object who created this post
@@ -209,7 +214,7 @@ module CamaleonCms
209
214
  object.comments.main.approveds.eager_load(:user)
210
215
  end
211
216
 
212
- # check if the post can be visited by current visitor
217
+ # check if the post can be visited by the current user
213
218
  def can_visit?
214
219
  r = { flag: true, post: object }
215
220
  h.hooks_run('post_can_visit', r)
@@ -228,7 +233,7 @@ module CamaleonCms
228
233
  p_type.decorate.generate_breadcrumb(add_post_type, true)
229
234
  end
230
235
  if object.post_parent.present? && p_type.manage_hierarchy?
231
- object.parents.reverse.each do |p|
236
+ object.parents.reverse_each do |p|
232
237
  p = p.decorate
233
238
  h.breadcrumb_add(p.the_title, p.published? ? p.the_url : nil)
234
239
  end
@@ -238,50 +243,63 @@ module CamaleonCms
238
243
 
239
244
  # return the post type of this post
240
245
  def the_post_type
241
- object.post_type.decorate
246
+ @the_post_type ||= object.post_type.decorate
242
247
  end
243
248
 
244
- # looks for the next post item related to parent element based on post_order attribute
245
- # @param _parent: parent decorated model, like: (PostType *default), Category, PostTag, Site
249
+ # It looks for the next post item related to the parent element based on the post_order attribute
250
+ # @param parent: parent decorated model, like: (PostType *default), Category, PostTag, Site
246
251
  # @samples: my_post.the_next_post(), my_post.the_next_post(@category), my_post.the_next_post(current_site)
247
- def the_next_post(_parent = nil)
248
- (_parent.presence || the_post_type).the_posts.where(
249
- "#{CamaleonCms::Post.table_name}.post_order > :position OR (#{CamaleonCms::Post.table_name}.post_order = :position and #{CamaleonCms::Post.table_name}.created_at > :created_at)", {
250
- position: object.post_order, created_at: object.created_at
251
- }
252
+ def the_next_post(parent = nil)
253
+ # eager: false -- next/prev resolve a single sibling used only for its title/url
254
+ h.verify_front_visibility((parent.presence || the_post_type).posts, eager: false).where(
255
+ post_order_predicate_gt(object.post_order, object.created_at)
252
256
  ).where.not(id: object.id).take.try(:decorate)
253
257
  end
254
258
 
255
- # looks for the next post item related to parent element based on post_order attribute
256
- # @param _parent: parent decorated model, like: (PostType *default), Category, PostTag, Site
259
+ # looks for the next post item related to the parent element based on post_order attribute
260
+ # @param parent: parent decorated model, like: (PostType *default), Category, PostTag, Site
257
261
  # @samples: my_post.the_prev_post(), my_post.the_prev_post(@category), my_post.the_prev_post(current_site)
258
- def the_prev_post(_parent = nil)
259
- (_parent.presence || the_post_type).the_posts.where("#{CamaleonCms::Post.table_name}.post_order < :position OR (#{CamaleonCms::Post.table_name}.post_order = :position and #{CamaleonCms::Post.table_name}.created_at < :created_at)", { position: object.post_order, created_at: object.created_at }).where.not(id: object.id).reorder(
260
- post_order: :asc, created_at: :asc
261
- ).last.try(:decorate)
262
+ def the_prev_post(parent = nil)
263
+ # eager: false -- next/prev resolve a single sibling used only for its title/url
264
+ h.verify_front_visibility((parent.presence || the_post_type).posts, eager: false)
265
+ .where(post_order_predicate_lt(object.post_order, object.created_at))
266
+ .where.not(id: object.id).reorder(post_order: :asc, created_at: :asc).last.try(:decorate)
262
267
  end
263
268
 
264
269
  # return the title with hierarchy prefixed
265
- # sample: title paren 1 - title parent 2 -.. -...
270
+ # sample: title paren 1 - title parent 2 -... -...
266
271
  # if add_parent_title: true will add parent title like: —— item 1.1.1 | item 1.1
267
272
  def the_hierarchy_title
268
- return the_title unless object.post_parent.present?
273
+ return the_title if object.post_parent.blank?
269
274
 
270
- res = '&#8212;' * object.parents.count
271
- res << " #{the_title}"
272
- res << " | #{object.parent.decorate.the_title}" if object.show_title_with_parent
273
- res.html_safe
275
+ parts = [h.raw('&#8212;' * object.parents.count), h.sanitize(" #{the_title}")]
276
+ parts << h.sanitize(" | #{object.parent.decorate.the_title}") if object.show_title_with_parent
277
+ h.safe_join(parts)
274
278
  end
275
279
 
276
- # return all related posts of current post
280
+ # return all related posts of the current post
277
281
  def the_related_posts
278
282
  ptype = the_post_type
279
- ptype.the_posts.joins(:categories).where(CamaleonCms::TermRelationship.table_name.to_s => { term_taxonomy_id: the_categories.pluck(:id) }).distinct
283
+ ptype.the_posts.joins(:categories).where(
284
+ CamaleonCms::TermRelationship.table_name.to_s => { term_taxonomy_id: the_categories.pluck(:id) }
285
+ ).distinct
280
286
  end
281
287
 
282
288
  # fix for "Using Draper::Decorator without inferred source class"
283
289
  def self.object_class_name
284
290
  'CamaleonCms::Post'
285
291
  end
292
+
293
+ private
294
+
295
+ def post_order_predicate_gt(position, created_at)
296
+ t = CamaleonCms::Post.arel_table
297
+ t[:post_order].gt(position).or(t[:post_order].eq(position).and(t[:created_at].gt(created_at)))
298
+ end
299
+
300
+ def post_order_predicate_lt(position, created_at)
301
+ t = CamaleonCms::Post.arel_table
302
+ t[:post_order].lt(position).or(t[:post_order].eq(position).and(t[:created_at].lt(created_at)))
303
+ end
286
304
  end
287
305
  end
@@ -71,7 +71,7 @@ module CamaleonCms
71
71
  def the_category(slug_or_id)
72
72
  return the_categories.where(id: slug_or_id).first if slug_or_id.is_a?(Integer)
73
73
 
74
- the_categories.find_by_slug(slug_or_id) if slug_or_id.is_a?(String)
74
+ the_categories.find_by_slug(slug_or_id) if slug_or_id.is_a?(String) # rubocop:disable Rails/DynamicFindBy
75
75
  end
76
76
 
77
77
  # return all post_tags for the post_type (active_record) filtered by permissions + hidden posts + roles + etc...
@@ -53,22 +53,26 @@ module CamaleonCms
53
53
  # slug_or_id: (Array) array of post ids, return multiple posts
54
54
  # return post model or nil
55
55
  def the_post(slug_or_id)
56
+ # eager: false -- a single-record lookup must not pay the listing preloads (with_eager)
57
+ rel = h.verify_front_visibility(object.posts, eager: false)
56
58
  if slug_or_id.is_a?(Integer)
57
59
  post = begin
58
- the_posts.find(slug_or_id)
60
+ rel.find(slug_or_id)
59
61
  rescue StandardError
60
62
  nil
61
63
  end
62
64
  end
63
65
  if slug_or_id.is_a?(Array)
64
66
  post = begin
65
- the_posts.find(slug_or_id)
67
+ rel.find(slug_or_id)
66
68
  rescue StandardError
67
69
  nil
68
70
  end
69
71
  end
70
- post = the_posts.find_by_slug(slug_or_id) if slug_or_id.is_a?(String) # id
71
- post.present? ? post.decorate : nil
72
+ # use find_by_slug (multi-language aware) so posts with localized slugs
73
+ # (e.g. "<!--:en-->sample-post<!--:-->...") are matched instead of returning nil
74
+ post = rel.find_by_slug(slug_or_id) if slug_or_id.is_a?(String) # rubocop:disable Rails/DynamicFindBy
75
+ post&.decorate
72
76
  end
73
77
 
74
78
  # return a collection of categories
@@ -81,7 +85,7 @@ module CamaleonCms
81
85
  def the_categories(slug_or_id = nil)
82
86
  return the_post_type(slug_or_id).the_categories if slug_or_id.present?
83
87
 
84
- object.categories unless slug_or_id.present?
88
+ object.categories if slug_or_id.blank?
85
89
  end
86
90
 
87
91
  # return the category object with id or slug = slug_or_id from this site
@@ -96,7 +100,7 @@ module CamaleonCms
96
100
  return unless slug_or_id.is_a?(String)
97
101
 
98
102
  begin
99
- the_full_categories.find_by_slug(slug_or_id).decorate
103
+ the_full_categories.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
100
104
  rescue StandardError
101
105
  nil
102
106
  end
@@ -126,7 +130,7 @@ module CamaleonCms
126
130
  return unless slug_or_id.is_a?(String)
127
131
 
128
132
  begin
129
- object.post_tags.find_by_slug(slug_or_id).decorate
133
+ object.post_tags.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
130
134
  rescue StandardError
131
135
  nil
132
136
  end
@@ -144,7 +148,7 @@ module CamaleonCms
144
148
  return unless id_or_username.is_a?(String)
145
149
 
146
150
  begin
147
- object.users.find_by_username(id_or_username).decorate
151
+ object.users.find_by_username(id_or_username).decorate # rubocop:disable Rails/DynamicFindBy
148
152
  rescue StandardError
149
153
  nil
150
154
  end
@@ -165,14 +169,14 @@ module CamaleonCms
165
169
  def the_post_type(slug_or_id)
166
170
  if slug_or_id.is_a?(String)
167
171
  begin
168
- return object.post_types.find_by_slug(slug_or_id).decorate
172
+ return object.post_types.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
169
173
  rescue StandardError
170
174
  nil
171
175
  end
172
176
  end
173
177
  if slug_or_id.is_a?(Array)
174
178
  begin
175
- return object.post_types.find_by_slug(slug_or_id).decorate
179
+ return object.post_types.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
176
180
  rescue StandardError
177
181
  nil
178
182
  end
@@ -195,21 +199,25 @@ module CamaleonCms
195
199
  lan = object.get_languages
196
200
  return if lan.size < 2
197
201
 
198
- res = ["<ul class='#{list_class}'>"]
199
- lan.each do |lang|
200
- path = "#{lang}.png"
201
- label = (if block
202
- h.capture(lang, I18n.locale.to_s == lang.to_s,
203
- &block)
204
- else
205
- "<img src='#{h.asset_path("camaleon_cms/language/#{path}")}'/>"
206
- end)
207
- res << "<li class='#{current_class if I18n.locale.to_s == lang.to_s}'> <a href='#{h.cama_url_to_fixed(
208
- current_page ? 'url_for' : 'cama_root_url', { locale: lang, cama_set_language: lang }
209
- )}'>#{label}</a> </li>"
202
+ h.content_tag(:ul, class: list_class) do
203
+ h.safe_join(lan.map do |lang|
204
+ label = if block
205
+ h.capture(lang, I18n.locale.to_s == lang.to_s, &block)
206
+ else
207
+ h.tag.img(src: h.asset_path("camaleon_cms/language/#{lang}.png"))
208
+ end
209
+
210
+ h.content_tag(:li, class: (current_class if I18n.locale.to_s == lang.to_s)) do
211
+ h.link_to(
212
+ label,
213
+ h.cama_url_to_fixed(
214
+ current_page ? 'url_for' : 'cama_root_url',
215
+ { locale: lang, cama_set_language: lang }
216
+ )
217
+ )
218
+ end
219
+ end)
210
220
  end
211
- res << '</ul>'
212
- res.join('').html_safe
213
221
  end
214
222
 
215
223
  # return Array of frontend languages configured for this site
@@ -245,7 +253,7 @@ module CamaleonCms
245
253
  postfix = 'url'
246
254
  postfix = 'path' if args.delete(:as_path)
247
255
  skip_relative_url_root = args.delete(:skip_relative_url_root)
248
- h.cama_current_site_host_port(args) unless args.keys.include?(:host)
256
+ h.cama_current_site_host_port(args) unless args.key?(:host)
249
257
  res = h.cama_url_to_fixed("cama_root_#{postfix}", args)
250
258
  if skip_relative_url_root && PluginRoutes.static_system_info['relative_url_root'].present?
251
259
  res = res.sub("/#{PluginRoutes.static_system_info['relative_url_root']}",
@@ -4,8 +4,12 @@ module CamaleonCms
4
4
  delegate_all
5
5
 
6
6
  # return the title for current locale
7
+ # The name is HTML-escaped here (like PostDecorator#the_title) because taxonomy
8
+ # names are plain text yet are rendered through `raw`/`html_safe` sinks such as
9
+ # the breadcrumb builder and the nav menu, so escaping at the source prevents
10
+ # stored XSS via category/tag/post_type/site names.
7
11
  def the_title(locale = nil)
8
- r = { title: object.name.translate(get_locale(locale)), object: object }
12
+ r = { title: h.h(object.name.to_s.translate(get_locale(locale))), object: object }
9
13
  begin
10
14
  h.hooks_run('taxonomy_the_title', r)
11
15
  rescue StandardError
@@ -26,12 +30,19 @@ module CamaleonCms
26
30
  h.do_shortcode(r[:content], self)
27
31
  end
28
32
 
33
+ # Same shape as PostDecorator#the_status: markup built by interpolation and rendered through
34
+ # `raw` at admin/search.html.erb. Only I18n strings reach it today, so there is nothing to
35
+ # exploit here -- escaping at the source is what keeps that true if the interpolated value ever
36
+ # changes.
29
37
  def the_status
30
- if status.to_s.to_bool
31
- "<span class='label label-success'> #{I18n.t('camaleon_cms.admin.button.actived')} </span>"
32
- else
33
- "<span class='label label-default'> #{I18n.t('camaleon_cms.admin.button.not_actived')} </span>"
34
- end
38
+ color, label = if status.to_s.to_bool
39
+ ['success', I18n.t('camaleon_cms.admin.button.actived')]
40
+ else
41
+ ['default', I18n.t('camaleon_cms.admin.button.not_actived')]
42
+ end
43
+ # rubocop:disable Rails/OutputSafety -- only the escaped label is interpolated into fixed markup
44
+ "<span class='label label-#{color}'> #{h.h(label)} </span>".html_safe
45
+ # rubocop:enable Rails/OutputSafety
35
46
  end
36
47
 
37
48
  # return excerpt for this post type
@@ -55,7 +66,7 @@ module CamaleonCms
55
66
 
56
67
  # search a post with id (integer) or slug (string)
57
68
  def the_post(slug_or_id)
58
- return nil unless slug_or_id.present?
69
+ return nil if slug_or_id.blank?
59
70
 
60
71
  if slug_or_id.is_a?(Integer)
61
72
  begin
@@ -67,7 +78,7 @@ module CamaleonCms
67
78
  return unless slug_or_id.is_a?(String)
68
79
 
69
80
  begin
70
- object.posts.find_by_slug(slug_or_id).decorate
81
+ object.posts.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
71
82
  rescue StandardError
72
83
  nil
73
84
  end
@@ -94,10 +105,10 @@ module CamaleonCms
94
105
  # return html link
95
106
  # attrs: Hash of link tag attributes, sample: {id: "myid", class: "sss" }
96
107
  def the_edit_link(title = nil, attrs = {})
97
- return '' unless h.cama_current_user.present?
108
+ return '' if h.cama_current_user.blank?
98
109
 
99
110
  attrs = { target: '_blank', style: 'font-size:11px !important;cursor:pointer;' }.merge(attrs)
100
- h.link_to("&rarr; #{title || h.ct('edit', default: 'Edit')}".html_safe, the_edit_url, attrs)
111
+ h.link_to(h.safe_join(['→ ', title || h.ct('edit', default: 'Edit')]), the_edit_url, attrs)
101
112
  end
102
113
 
103
114
  # return the user owner of this item
@@ -11,11 +11,11 @@ module CamaleonCms
11
11
  h.cama_admin_settings_theme_url(args)
12
12
  end
13
13
 
14
- def the_settings_link
15
- return '' unless h.cama_current_user.present?
14
+ def the_settings_link(title = nil, attrs = {})
15
+ return '' if h.cama_current_user.blank?
16
16
 
17
17
  attrs = { target: '_blank', style: 'font-size:11px !important;cursor:pointer;' }.merge(attrs)
18
- h.link_to("&rarr; #{title || h.ct('edit', default: 'Edit')}".html_safe, the_settings_url, attrs)
18
+ h.link_to(h.safe_join(['→ ', title || h.ct('edit', default: 'Edit')]), the_settings_url, attrs)
19
19
  end
20
20
  end
21
21
  end
@@ -20,7 +20,9 @@ module CamaleonCms
20
20
 
21
21
  # return the avatar for this user, default: assets/admin/img/no_image.jpg
22
22
  def the_avatar(default_avatar = nil)
23
- avatar_exists? ? object.get_meta('avatar') : (default_avatar || h.asset_url('camaleon_cms/admin/img/no_image.jpg'))
23
+ return object.get_meta('avatar') if avatar_exists?
24
+
25
+ default_avatar || h.asset_url('camaleon_cms/admin/img/no_image.jpg')
24
26
  end
25
27
 
26
28
  # return the slogan for this user, default: Hello World
@@ -34,7 +36,7 @@ module CamaleonCms
34
36
  args[:label] = I18n.t('routes.profile', default: 'profile')
35
37
  args[:user_id] = the_id
36
38
  args[:user_name] = the_name.parameterize
37
- args[:user_name] = the_username unless args[:user_name].present?
39
+ args[:user_name] = the_username if args[:user_name].blank?
38
40
  args[:locale] = get_locale unless args.include?(:locale)
39
41
  args[:format] = args[:format] || 'html'
40
42
  as_path = args.delete(:as_path)
@@ -52,8 +54,26 @@ module CamaleonCms
52
54
  h.current_site.posts.where(user_id: object.id)
53
55
  end
54
56
 
55
- def role_grantor?(other_user)
56
- h.can?(:manage, :users) && (other_user.nil? || id != other_user.id)
57
+ # Whether this user may set another user's role. Only an admin may grant the `admin` role (the one
58
+ # `User#admin?` tests) or change the role of a user who is already an admin — so holding
59
+ # `:manage, :users` is neither a path to minting an admin (escalation) nor to stripping one (H10).
60
+ def role_grantor?(other_user, new_role = nil)
61
+ return false unless h.can?(:manage, :users) && (other_user.nil? || id != other_user.id)
62
+ return admin? if new_role.to_s == 'admin' || other_user&.role.to_s == 'admin'
63
+
64
+ true
65
+ end
66
+
67
+ # Whether this user may edit +other_user+'s login credential (password) or recovery identifiers
68
+ # (email, username). Only an admin may edit an admin's account: a `:manage, :users` holder who could
69
+ # reset an admin's password would sign in as them, and one who could repoint an admin's email would
70
+ # hijack a password-reset link — either is a path to superadmin that would make `role_grantor?`'s
71
+ # guard of the privileged set moot (H10). Editing one's own account, or any non-admin, is unrestricted
72
+ # here; the controller's `:manage, :users` (or self) authorization still applies first.
73
+ def may_edit_credentials?(other_user)
74
+ return true unless other_user&.admin?
75
+
76
+ admin?
57
77
  end
58
78
 
59
79
  def self.object_class_name
@@ -65,7 +85,8 @@ module CamaleonCms
65
85
  def avatar_exists?
66
86
  # TODO: change verification
67
87
  # if object.get_meta('avatar').present?
68
- # File.exist?(h.cama_url_to_file_path(object.get_meta('avatar'))) || Faraday.head(object.get_meta('avatar')).status == 200
88
+ # File.exist?(h.cama_url_to_file_path(object.get_meta('avatar'))) ||
89
+ # Faraday.head(object.get_meta('avatar')).status == 200
69
90
  # else
70
91
  # false
71
92
  # end
@@ -32,16 +32,13 @@ module CamaleonCms
32
32
  end
33
33
  end
34
34
 
35
- # return the locale for frontend translations initialized in admin controller
36
- # used by models like posts, categories, ..., sample: my_post.the_url
37
- # fix for https://github.com/owen2345/camaleon-cms/issues/233#issuecomment-215385432
38
- def cama_get_i18n_frontend
39
- @cama_i18n_frontend
40
- end
41
-
42
35
  # print code with auto copy
43
36
  def cama_shortcode_print(code)
44
- content_tag(:input, nil, class: 'code_style', readonly: true, onmousedown: 'this.clicked = 1;', onfocus: 'if (!this.clicked) this.select(); else this.clicked = 2;', onclick: 'if (this.clicked == 2) this.select(); this.clicked = 0;', tabindex: '-1', value: code)
37
+ content_tag(
38
+ :input, nil, class: 'code_style', readonly: true, onmousedown: 'this.clicked = 1;',
39
+ onfocus: 'if (!this.clicked) this.select(); else this.clicked = 2;',
40
+ onclick: 'if (this.clicked == 2) this.select(); this.clicked = 0;', tabindex: '-1', value: code
41
+ )
45
42
  end
46
43
  end
47
44
  end
@@ -4,7 +4,7 @@ module CamaleonCms
4
4
  # draw the title for the admin admin panel according the breadcrumb
5
5
  def cama_admin_title_draw
6
6
  res = [t('camaleon_cms.admin.sidebar_top.admin_panel')]
7
- breadcrumbs.reverse.slice(0, 2).reverse.each { |b| res << (b.is_a?(Hash) ? b[:name] : b.name) }
7
+ breadcrumbs.reverse.slice(0, 2).reverse_each { |b| res << (b.is_a?(Hash) ? b[:name] : b.name) }
8
8
  res.join(' &raquo; ')
9
9
  end
10
10
 
@@ -2,31 +2,19 @@ module CamaleonCms
2
2
  module Admin
3
3
  module CustomFieldsHelper
4
4
  def cama_custom_field_elements
5
- return @_cama_custom_field_elements if @_cama_custom_field_elements.present?
5
+ return CurrentRequest.custom_field_elements if CurrentRequest.custom_field_elements.present?
6
6
 
7
7
  items = {}
8
8
  items[:text_box] = {
9
9
  key: 'text_box',
10
10
  label: t('camaleon_cms.admin.custom_field.fields.text_box'),
11
- options: {
12
- required: true,
13
- multiple: true,
14
- translate: true,
15
- default_value: '',
16
- show_frontend: true
17
- }
11
+ options: { required: true, multiple: true, translate: true, default_value: '', show_frontend: true }
18
12
  }
19
13
 
20
14
  items[:text_area] = {
21
15
  key: 'text_area',
22
16
  label: t('camaleon_cms.admin.custom_field.fields.text_area'),
23
- options: {
24
- required: true,
25
- multiple: true,
26
- translate: true,
27
- default_value: '',
28
- show_frontend: true
29
- }
17
+ options: { required: true, multiple: true, translate: true, default_value: '', show_frontend: true }
30
18
  }
31
19
  items[:select] = {
32
20
  key: 'select',
@@ -35,10 +23,7 @@ module CamaleonCms
35
23
  required: true,
36
24
  multiple: true,
37
25
  translate: true,
38
- multiple_options: {
39
- label: t('camaleon_cms.admin.settings.options_select'),
40
- default: 'radio'
41
- },
26
+ multiple_options: { label: t('camaleon_cms.admin.settings.options_select'), default: 'radio' },
42
27
  show_frontend: true
43
28
  }
44
29
  }
@@ -298,7 +283,8 @@ module CamaleonCms
298
283
  }
299
284
 
300
285
  # evaluate the content of command value on listing
301
- # sample command: options_from_collection_for_select(current_site.the_posts("commerce").decorate, :id, :the_title)
286
+ # sample command:
287
+ # options_from_collection_for_select(current_site.the_posts("commerce").decorate, :id, :the_title)
302
288
  items[:select_eval] = {
303
289
  key: 'select_eval',
304
290
  label: t('camaleon_cms.admin.custom_field.fields.select_eval'),
@@ -344,17 +330,32 @@ module CamaleonCms
344
330
  }
345
331
  r = { fields: items }
346
332
  hooks_run('extra_custom_fields', r)
347
- @_cama_custom_field_elements = r[:fields]
333
+ CurrentRequest.custom_field_elements = r[:fields]
348
334
  end
349
335
 
350
336
  # add your model class into custom fields editor
351
337
  # Note: to use custom fields on your model, you need the following:
352
- # - add: belongs_to :site (in your model) //don't forget multi site support, i.e.: you need site_id attribute in your table
338
+ # - add: belongs_to :site (in your model)
339
+ # //don't forget multi site support, i.e.: you need site_id attribute in your table
353
340
  # - add: include CamaleonCms::CustomFieldsRead (in your model)
354
341
  # ==> With this, you can manage your model like a plugin. Check api -> custom fields section into docs)
355
342
  # model_class: class name (Product)
356
343
  def cf_add_model(model_class)
357
- @_extra_models_for_fields << model_class
344
+ CurrentRequest.extra_models_for_fields ||= []
345
+ CurrentRequest.extra_models_for_fields << model_class
346
+ end
347
+
348
+ # Models offered in the field-group placement dropdown: those registered via cf_add_model plus any
349
+ # a custom_field_custom_models hook appends. Seeded from the legacy @_extra_models_for_fields ivar
350
+ # so a controller still assigning it keeps working. The dropdown used to read that ivar while
351
+ # cf_add_model wrote CurrentRequest, so registered models never appeared. Regression M18.
352
+ # The seed and the hook payload are copies: hook appends must reach only this read, not
353
+ # accumulate in the request store or leak into the controller ivar.
354
+ def cf_extra_models_for_fields
355
+ CurrentRequest.extra_models_for_fields ||= (instance_variable_get(:@_extra_models_for_fields) || []).dup
356
+ f_args = { models: CurrentRequest.extra_models_for_fields.dup }
357
+ hooks_run('custom_field_custom_models', f_args)
358
+ f_args[:models]
358
359
  end
359
360
  end
360
361
  end