camaleon_cms 2.9.2 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +17 -4
  3. data/Rakefile +3 -16
  4. data/app/apps/plugins/attack/admin_controller.rb +9 -6
  5. data/app/apps/plugins/attack/attack_helper.rb +35 -25
  6. data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
  7. data/app/apps/plugins/front_cache/admin_controller.rb +15 -10
  8. data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
  9. data/app/apps/plugins/front_cache/front_cache_helper.rb +33 -9
  10. data/app/apps/plugins/visibility_post/config/config.json +1 -0
  11. data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
  12. data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
  13. data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
  14. data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
  15. data/app/apps/themes/default/assets/css/style.css.scss +24 -0
  16. data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
  17. data/app/apps/themes/default/default_helper.rb +2 -1
  18. data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
  19. data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
  20. data/app/apps/themes/new/custom_helper.rb +5 -9
  21. data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
  22. data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
  23. data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
  24. data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
  25. data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
  26. data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
  27. data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
  28. data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
  29. data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
  30. data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
  31. data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
  32. data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
  33. data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
  34. data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
  35. data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
  36. data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
  37. data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
  38. data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
  39. data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +76 -13
  40. data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
  41. data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
  42. data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
  43. data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
  44. data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
  45. data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
  46. data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
  47. data/app/controllers/camaleon_cms/frontend_controller.rb +140 -70
  48. data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
  49. data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
  50. data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
  51. data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
  52. data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
  53. data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
  54. data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
  55. data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
  56. data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
  57. data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
  58. data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
  59. data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
  60. data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
  61. data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
  62. data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
  63. data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
  64. data/app/decorators/camaleon_cms/post_decorator.rb +66 -50
  65. data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
  66. data/app/decorators/camaleon_cms/site_decorator.rb +29 -23
  67. data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
  68. data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
  69. data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
  70. data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
  71. data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
  72. data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
  73. data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
  74. data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
  75. data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
  76. data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
  77. data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
  78. data/app/helpers/camaleon_cms/content_helper.rb +13 -6
  79. data/app/helpers/camaleon_cms/email_helper.rb +7 -3
  80. data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
  81. data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
  82. data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
  83. data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
  84. data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
  85. data/app/helpers/camaleon_cms/html_helper.rb +58 -37
  86. data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
  87. data/app/helpers/camaleon_cms/session_helper.rb +188 -44
  88. data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
  89. data/app/helpers/camaleon_cms/site_helper.rb +34 -13
  90. data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
  91. data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
  92. data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
  93. data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
  94. data/app/models/camaleon_cms/ability.rb +15 -7
  95. data/app/models/camaleon_cms/category.rb +6 -6
  96. data/app/models/camaleon_cms/custom_field.rb +15 -6
  97. data/app/models/camaleon_cms/custom_field_group.rb +39 -22
  98. data/app/models/camaleon_cms/custom_fields_relationship.rb +133 -2
  99. data/app/models/camaleon_cms/media.rb +8 -2
  100. data/app/models/camaleon_cms/meta.rb +2 -2
  101. data/app/models/camaleon_cms/nav_menu.rb +5 -3
  102. data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
  103. data/app/models/camaleon_cms/plugin.rb +3 -4
  104. data/app/models/camaleon_cms/post.rb +113 -27
  105. data/app/models/camaleon_cms/post_comment.rb +10 -6
  106. data/app/models/camaleon_cms/post_default.rb +42 -12
  107. data/app/models/camaleon_cms/post_relationship.rb +6 -4
  108. data/app/models/camaleon_cms/post_tag.rb +4 -5
  109. data/app/models/camaleon_cms/post_type.rb +21 -13
  110. data/app/models/camaleon_cms/site.rb +39 -29
  111. data/app/models/camaleon_cms/term_relationship.rb +10 -7
  112. data/app/models/camaleon_cms/term_taxonomy.rb +52 -4
  113. data/app/models/camaleon_cms/theme.rb +2 -4
  114. data/app/models/camaleon_cms/user.rb +17 -5
  115. data/app/models/camaleon_cms/user_role.rb +66 -6
  116. data/app/models/camaleon_cms/widget/assigned.rb +11 -6
  117. data/app/models/camaleon_cms/widget/main.rb +12 -8
  118. data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
  119. data/app/models/camaleon_record.rb +89 -5
  120. data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +20 -38
  121. data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
  122. data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
  123. data/app/models/concerns/camaleon_cms/metas.rb +31 -6
  124. data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
  125. data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
  126. data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
  127. data/app/models/current_request.rb +8 -1
  128. data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
  129. data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
  130. data/app/uploaders/camaleon_cms_uploader.rb +23 -10
  131. data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
  132. data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
  133. data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
  134. data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
  135. data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
  136. data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
  137. data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
  138. data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
  139. data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
  140. data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
  141. data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
  142. data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
  143. data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
  144. data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
  145. data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
  146. data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
  147. data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
  148. data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
  149. data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
  150. data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
  151. data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
  152. data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
  153. data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
  154. data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
  155. data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
  156. data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
  157. data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
  158. data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
  159. data/config/initializers/action_view.rb +42 -5
  160. data/config/initializers/active_record_extension.rb +13 -27
  161. data/config/initializers/assets.rb +52 -23
  162. data/config/initializers/custom_initializers.rb +1 -1
  163. data/config/locales/camaleon_cms/admin/ar.yml +1 -0
  164. data/config/locales/camaleon_cms/admin/de.yml +1 -0
  165. data/config/locales/camaleon_cms/admin/en.yml +33 -0
  166. data/config/locales/camaleon_cms/admin/es.yml +1 -0
  167. data/config/locales/camaleon_cms/admin/fr.yml +1 -0
  168. data/config/locales/camaleon_cms/admin/it.yml +1 -0
  169. data/config/locales/camaleon_cms/admin/nl.yml +1 -0
  170. data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
  171. data/config/locales/camaleon_cms/admin/pt.yml +1 -0
  172. data/config/locales/camaleon_cms/admin/ru.yml +1 -0
  173. data/config/locales/camaleon_cms/admin/uk.yml +1 -0
  174. data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
  175. data/config/routes/admin.rb +33 -15
  176. data/config/routes/frontend.rb +21 -15
  177. data/lib/camaleon_cms/assets_precompile.rb +86 -0
  178. data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
  179. data/lib/camaleon_cms/content_security.rb +158 -0
  180. data/lib/camaleon_cms/engine.rb +47 -9
  181. data/lib/camaleon_cms/media_security_headers.rb +27 -0
  182. data/lib/camaleon_cms/setup_token.rb +69 -0
  183. data/lib/camaleon_cms/svg_content_checker.rb +123 -0
  184. data/lib/camaleon_cms/task_reporter.rb +14 -0
  185. data/lib/camaleon_cms/unsafe_markup.rb +207 -0
  186. data/lib/camaleon_cms/uploader_content_security.rb +248 -0
  187. data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
  188. data/lib/camaleon_cms/uploader_path_security.rb +146 -0
  189. data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
  190. data/lib/camaleon_cms/uploader_support.rb +86 -0
  191. data/lib/camaleon_cms/version.rb +1 -1
  192. data/lib/camaleon_cms.rb +11 -0
  193. data/lib/ext/hash.rb +28 -2
  194. data/lib/ext/string.rb +10 -5
  195. data/lib/ext/translator.rb +10 -2
  196. data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
  197. data/lib/generators/camaleon_cms/install_generator.rb +7 -4
  198. data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
  199. data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
  200. data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
  201. data/lib/plugin_routes.rb +412 -368
  202. data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
  203. data/lib/tasks/cross_site_field_groups.rake +74 -0
  204. data/lib/tasks/custom_fields_roles.rake +15 -13
  205. data/lib/tasks/orphaned_comments.rake +42 -0
  206. data/lib/tasks/private_upload_acls.rake +31 -0
  207. data/lib/tasks/site_custom_field_groups.rake +36 -0
  208. data/lib/tasks/unsafe_stored_content.rake +42 -0
  209. data/lib/tasks/unsafe_stored_uploads.rake +70 -0
  210. data/lib/tasks/user_field_groups.rake +27 -0
  211. metadata +123 -4
@@ -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,61 @@ 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
+ (parent.presence || the_post_type).the_posts.where(
254
+ post_order_predicate_gt(object.post_order, object.created_at)
252
255
  ).where.not(id: object.id).take.try(:decorate)
253
256
  end
254
257
 
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
258
+ # looks for the next post item related to the parent element based on post_order attribute
259
+ # @param parent: parent decorated model, like: (PostType *default), Category, PostTag, Site
257
260
  # @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)
261
+ def the_prev_post(parent = nil)
262
+ (parent.presence || the_post_type).the_posts
263
+ .where(post_order_predicate_lt(object.post_order, object.created_at))
264
+ .where.not(id: object.id).reorder(post_order: :asc, created_at: :asc).last.try(:decorate)
262
265
  end
263
266
 
264
267
  # return the title with hierarchy prefixed
265
- # sample: title paren 1 - title parent 2 -.. -...
268
+ # sample: title paren 1 - title parent 2 -... -...
266
269
  # if add_parent_title: true will add parent title like: —— item 1.1.1 | item 1.1
267
270
  def the_hierarchy_title
268
- return the_title unless object.post_parent.present?
271
+ return the_title if object.post_parent.blank?
269
272
 
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
273
+ parts = [h.raw('&#8212;' * object.parents.count), h.sanitize(" #{the_title}")]
274
+ parts << h.sanitize(" | #{object.parent.decorate.the_title}") if object.show_title_with_parent
275
+ h.safe_join(parts)
274
276
  end
275
277
 
276
- # return all related posts of current post
278
+ # return all related posts of the current post
277
279
  def the_related_posts
278
280
  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
281
+ ptype.the_posts.joins(:categories).where(
282
+ CamaleonCms::TermRelationship.table_name.to_s => { term_taxonomy_id: the_categories.pluck(:id) }
283
+ ).distinct
280
284
  end
281
285
 
282
286
  # fix for "Using Draper::Decorator without inferred source class"
283
287
  def self.object_class_name
284
288
  'CamaleonCms::Post'
285
289
  end
290
+
291
+ private
292
+
293
+ def post_order_predicate_gt(position, created_at)
294
+ t = CamaleonCms::Post.arel_table
295
+ t[:post_order].gt(position).or(t[:post_order].eq(position).and(t[:created_at].gt(created_at)))
296
+ end
297
+
298
+ def post_order_predicate_lt(position, created_at)
299
+ t = CamaleonCms::Post.arel_table
300
+ t[:post_order].lt(position).or(t[:post_order].eq(position).and(t[:created_at].lt(created_at)))
301
+ end
286
302
  end
287
303
  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...
@@ -67,8 +67,10 @@ module CamaleonCms
67
67
  nil
68
68
  end
69
69
  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
70
+ # use find_by_slug (multi-language aware) so posts with localized slugs
71
+ # (e.g. "<!--:en-->sample-post<!--:-->...") are matched instead of returning nil
72
+ post = the_posts.find_by_slug(slug_or_id) if slug_or_id.is_a?(String) # rubocop:disable Rails/DynamicFindBy
73
+ post&.decorate
72
74
  end
73
75
 
74
76
  # return a collection of categories
@@ -81,7 +83,7 @@ module CamaleonCms
81
83
  def the_categories(slug_or_id = nil)
82
84
  return the_post_type(slug_or_id).the_categories if slug_or_id.present?
83
85
 
84
- object.categories unless slug_or_id.present?
86
+ object.categories if slug_or_id.blank?
85
87
  end
86
88
 
87
89
  # return the category object with id or slug = slug_or_id from this site
@@ -96,7 +98,7 @@ module CamaleonCms
96
98
  return unless slug_or_id.is_a?(String)
97
99
 
98
100
  begin
99
- the_full_categories.find_by_slug(slug_or_id).decorate
101
+ the_full_categories.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
100
102
  rescue StandardError
101
103
  nil
102
104
  end
@@ -126,7 +128,7 @@ module CamaleonCms
126
128
  return unless slug_or_id.is_a?(String)
127
129
 
128
130
  begin
129
- object.post_tags.find_by_slug(slug_or_id).decorate
131
+ object.post_tags.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
130
132
  rescue StandardError
131
133
  nil
132
134
  end
@@ -144,7 +146,7 @@ module CamaleonCms
144
146
  return unless id_or_username.is_a?(String)
145
147
 
146
148
  begin
147
- object.users.find_by_username(id_or_username).decorate
149
+ object.users.find_by_username(id_or_username).decorate # rubocop:disable Rails/DynamicFindBy
148
150
  rescue StandardError
149
151
  nil
150
152
  end
@@ -165,14 +167,14 @@ module CamaleonCms
165
167
  def the_post_type(slug_or_id)
166
168
  if slug_or_id.is_a?(String)
167
169
  begin
168
- return object.post_types.find_by_slug(slug_or_id).decorate
170
+ return object.post_types.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
169
171
  rescue StandardError
170
172
  nil
171
173
  end
172
174
  end
173
175
  if slug_or_id.is_a?(Array)
174
176
  begin
175
- return object.post_types.find_by_slug(slug_or_id).decorate
177
+ return object.post_types.find_by_slug(slug_or_id).decorate # rubocop:disable Rails/DynamicFindBy
176
178
  rescue StandardError
177
179
  nil
178
180
  end
@@ -195,21 +197,25 @@ module CamaleonCms
195
197
  lan = object.get_languages
196
198
  return if lan.size < 2
197
199
 
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>"
200
+ h.content_tag(:ul, class: list_class) do
201
+ h.safe_join(lan.map do |lang|
202
+ label = if block
203
+ h.capture(lang, I18n.locale.to_s == lang.to_s, &block)
204
+ else
205
+ h.tag.img(src: h.asset_path("camaleon_cms/language/#{lang}.png"))
206
+ end
207
+
208
+ h.content_tag(:li, class: (current_class if I18n.locale.to_s == lang.to_s)) do
209
+ h.link_to(
210
+ label,
211
+ h.cama_url_to_fixed(
212
+ current_page ? 'url_for' : 'cama_root_url',
213
+ { locale: lang, cama_set_language: lang }
214
+ )
215
+ )
216
+ end
217
+ end)
210
218
  end
211
- res << '</ul>'
212
- res.join('').html_safe
213
219
  end
214
220
 
215
221
  # return Array of frontend languages configured for this site
@@ -245,7 +251,7 @@ module CamaleonCms
245
251
  postfix = 'url'
246
252
  postfix = 'path' if args.delete(:as_path)
247
253
  skip_relative_url_root = args.delete(:skip_relative_url_root)
248
- h.cama_current_site_host_port(args) unless args.keys.include?(:host)
254
+ h.cama_current_site_host_port(args) unless args.key?(:host)
249
255
  res = h.cama_url_to_fixed("cama_root_#{postfix}", args)
250
256
  if skip_relative_url_root && PluginRoutes.static_system_info['relative_url_root'].present?
251
257
  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