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
@@ -0,0 +1,27 @@
1
+ <div class="login-body">
2
+ <h3 class="login-box-msg"><%= t('camaleon_cms.admin.logout.confirm_title', default: 'Log out?') %></h3>
3
+ <%= render "camaleon_cms/flash_messages" %>
4
+ <p>
5
+ <%# Security (audit M6): reached over GET (an old bookmark, or a theme still linking the
6
+ path) — the session ends only through the POST below, so a forged GET cannot end it. %>
7
+ <%= t('camaleon_cms.admin.logout.confirm_instructions',
8
+ default: 'You are about to end your session. Continue?') %>
9
+ </p>
10
+ <div class="row">
11
+ <div class="col-xs-6">
12
+ <%# Cancel returns to the vetted return_to when a theme supplied one (see
13
+ #prepare_logout_confirmation), else the dashboard -- never blindly into the admin. %>
14
+ <a href="<%= @logout_cancel_url %>" class="btn btn-default btn-block btn-flat"><%= t('camaleon_cms.admin.button.cancel', default: 'Cancel') %></a>
15
+ </div>
16
+ <div class="col-xs-6">
17
+ <%# `full` (impersonation), `return_to` (ecosystem themes link logout with a return-to-site
18
+ destination) and `locale` are carried through to the POST -- precomputed as scalars in
19
+ the controller so a hash-shaped param cannot crash url generation; return_to is then
20
+ vetted by cama_logout_user's safe-redirect check like on any logout. %>
21
+ <%= button_to t('camaleon_cms.admin.logout.logout', default: 'Logout'),
22
+ cama_admin_logout_path(full: @logout_full, return_to: @logout_return_to,
23
+ locale: @logout_locale),
24
+ method: :post, class: 'btn btn-danger btn-block btn-flat' %>
25
+ </div>
26
+ </div>
27
+ </div>
@@ -49,7 +49,10 @@
49
49
  var form = modal.find('form');
50
50
  form.validate({submitHandler: function(){
51
51
  showLoading();
52
- $.get(link.attr('href'), form.serialize(), function(){ hideLoading(); }).complete(function(){ modal.modal('hide'); }).error(function(r){ $.fn.alert({title: r.responseText, type: 'error'}) });
52
+ // Security (audit M6): test_email sends mail, so the route is POST-only;
53
+ // jquery_ujs' prefilter attaches the CSRF token to non-GET ajax.
54
+ // .always/.fail (not the .complete/.error jqXHR aliases removed in jQuery 3).
55
+ $.post(link.attr('href'), form.serialize(), function(){ hideLoading(); }).always(function(){ modal.modal('hide'); }).fail(function(r){ $.fn.alert({title: r.responseText, type: 'error'}) });
53
56
  }});
54
57
  }});
55
58
  return false;
@@ -3,7 +3,7 @@
3
3
  @item_options_value = @item_options_value || {}
4
4
  @item_id = @item_value[:id] || ''
5
5
  @index = "#{@item_id}_#{Time.now.to_i}_#{rand(999...99999)}"
6
- @field_config = cama_custom_field_elements[@key.to_sym]
6
+ @field_config = @key.present? ? cama_custom_field_elements[@key.to_sym] : nil
7
7
  @panel_id = "panel-field#{@index}"
8
8
  colors = ['primary','success','warning','colorful','info','danger']
9
9
  unless @field_config.nil?
@@ -15,13 +15,24 @@
15
15
  </div>
16
16
  <% end %>
17
17
  <div class="fields_container">
18
- <% fields.decorate.each do |field| is_disabled = field.options[:disabled].present? || field.options[:readonly].present?; group_field_data[field.slug] = {default_value: field.options[:default_value], is_disabled: is_disabled, id: field.id, multiple: field.options[:multiple].to_s.to_bool && !is_disabled, kind: field.get_option('field_key')} %>
19
- <div class="item-custom-field c-field-<%= field.options[:field_key] %>" data-field-key="<%= field.slug %>">
18
+ <% fields.decorate.each do |field|
19
+ field_key = field.get_option('field_key').presence || field.options[:field_key].presence || field.options['field_key'].presence
20
+ normalized_field_key = field_key.presence || 'text_box'
21
+ is_disabled = field.get_option('disabled').present? || field.get_option('readonly').present?
22
+ required_field = field.get_option('required').to_s.to_bool
23
+ group_field_data[field.slug] = {
24
+ default_value: field.get_option('default_value'),
25
+ is_disabled: is_disabled,
26
+ id: field.id,
27
+ multiple: field.get_option('multiple').to_s.to_bool && !is_disabled,
28
+ kind: normalized_field_key
29
+ } %>
30
+ <div class="item-custom-field c-field-<%= normalized_field_key %>" data-field-key="<%= field.slug %>">
20
31
  <input name="<%= field_name %>[<%= field.slug %>][id]" type="hidden" value="<%= field.id %>"/>
21
32
  <input name="<%= field_name %>[<%= field.slug %>][group_number]" class="cama_custom_group_number" type="hidden" value="0"/>
22
33
  <label>
23
34
  <%= field.name %>
24
- <%= raw "<em class='text-danger'>*</em>" if field.options[:required].to_s.to_bool %>
35
+ <%= raw "<em class='text-danger'>*</em>" if required_field %>
25
36
  <% if current_site.get_option('custom_fields_show_shortcodes') && ["post", "posttype", "category", "postTag", "site", "user", "navmenu", "theme"].include?(obj_class) %>
26
37
  <small class="shortcode_field"><br><%= cama_shortcode_print("[data field='#{field.slug}' #{"object='#{obj_class}' #{"id='#{record.id}'" if obj_class != "Theme" }" unless record.new_record?}]") %></small>
27
38
  <% end %>
@@ -31,7 +42,11 @@
31
42
  <% end %>
32
43
  <div class="editor-custom-fields content-field-<%= field.id %>" data-add_field_title="<%= t('camaleon_cms.admin.button.add_new_field', default: 'Add new field')%>">
33
44
  <%= begin
34
- render(field.get_option('render') || (cama_custom_field_elements[field.get_option('field_key').to_sym][:render] rescue nil) || "camaleon_cms/admin/settings/custom_fields/fields/#{field.options[:field_key]}", field: field, values: nil, is_disabled: is_disabled, default_use: default_use, field_name: field_name)
45
+ custom_field_render = (cama_custom_field_elements[normalized_field_key.to_sym][:render] rescue nil)
46
+ render(
47
+ field.get_option('render') || custom_field_render || "camaleon_cms/admin/settings/custom_fields/fields/#{normalized_field_key}",
48
+ field: field, values: nil, is_disabled: is_disabled, default_use: default_use, field_name: field_name
49
+ )
35
50
  rescue => e
36
51
  Rails.logger.error "Camaleon CMS - Render Custom Fields Error: Custom field template was not found => #{e.message}: #{$!.backtrace}".cama_log_style(:red)
37
52
  e.message
@@ -2,7 +2,7 @@
2
2
  def _render_category_option_html(item, pos = 0)
3
3
  category = item.decorate
4
4
  value = "Category_Post,#{category.id}" %>
5
- <option value="<%= value %>" data-help="<%= "#{t('camaleon_cms.admin.settings.tooltip.add_custom_field_posts')}"+ category.the_title %>"><%= "#{('—' * pos)} #{category.the_title}" %></option>
5
+ <option value="<%= value %>" data-help="<%= safe_join([t('camaleon_cms.admin.settings.tooltip.add_custom_field_posts'), category.the_title]) %>"><%= safe_join([('—' * pos), ' ', category.the_title]) %></option>
6
6
  <% end
7
7
 
8
8
  def _render_categories(categories, _level = 0)
@@ -64,13 +64,14 @@
64
64
  </optgroup>
65
65
 
66
66
  <optgroup label="<%= t('camaleon_cms.admin.settings.others') %>">
67
- <% value = "#{PluginRoutes.static_system_info['user_model'].presence || 'User'},#{current_site.id}" %>
67
+ <%# Demodulized: user groups are stored and read under the association's scope name %>
68
+ <% value = "#{PluginRoutes.get_user_class_name.demodulize},#{current_site.id}" %>
68
69
  <option value="<%= value %>" data-help="<%= t('camaleon_cms.admin.settings.tooltip.add_custom_field_users') %>"><%= t('camaleon_cms.admin.sidebar.users') %></option>
69
70
  <% value = "Site,#{current_site.id}" %>
70
71
  <option value="<%= value %>" data-help="<%= t('camaleon_cms.admin.settings.tooltip.add_custom_field_sites') %>"><%= t('camaleon_cms.admin.sidebar.site_settings') %></option>
71
- <% f_args={models: @_extra_models_for_fields || []}; hooks_run('custom_field_custom_models', f_args); f_args[:models].each do |model| %>
72
+ <% cf_extra_models_for_fields.each do |model| %>
72
73
  <% value = "#{model.name},#{current_site.id}" %>
73
- <option value="<%= value %>" data-help=""><%= model.name %></option><%= model.name %>
74
+ <option value="<%= value %>" data-help=""><%= model.name %></option>
74
75
  <% end %>
75
76
  </optgroup>
76
77
 
@@ -13,17 +13,17 @@
13
13
  </tr>
14
14
  </thead>
15
15
  <tbody>
16
- <% @_shortcodes.each do |code| %>
16
+ <% shortcodes_list.each do |code| %>
17
17
  <tr>
18
18
  <td>
19
19
  <code>[<%= code %>]</code>
20
20
  </td>
21
- <td class="col_descr"><%= @_shortcodes_descr[code] %></td>
21
+ <td class="col_descr"><%= shortcodes_descriptions[code] %></td>
22
22
  </tr>
23
23
  <% end %>
24
24
  </tbody>
25
25
  </table>
26
- <%= content_tag("div", t('camaleon_cms.admin.message.data_found_list'), class: "alert alert-warning") if @_shortcodes.empty? %>
26
+ <%= content_tag("div", t('camaleon_cms.admin.message.data_found_list'), class: "alert alert-warning") if shortcodes_list.empty? %>
27
27
  </div>
28
28
  </div>
29
29
  </div>
@@ -34,4 +34,4 @@
34
34
  $(this).html($(this).html().replace(/\n/g, "<br>"));
35
35
  });
36
36
  })
37
- </script>
37
+ </script>
@@ -3,7 +3,7 @@
3
3
  <div class="page-content-wrap">
4
4
  <div class="panel panel-default">
5
5
  <div class="panel-heading">
6
- <h4><span class="fa fa-cog"></span> <%= "#{t('camaleon_cms.admin.settings.configuration_site')}: #{@site.the_title} " %> </h4>
6
+ <h4><span class="fa fa-cog"></span> <%= safe_join([t('camaleon_cms.admin.settings.configuration_site'), ': ', @site.the_title, ' ']) %> </h4>
7
7
  </div>
8
8
  <%= form_for @site, as: "site", url: {action: :site_saved}, html: {id: "site_settings_form", class: "panel-body cama_ajax_request validate"} do |f| %>
9
9
  <div class="tabs">
@@ -5,7 +5,7 @@
5
5
  <div class="col-md-8 col-md-offset-2">
6
6
  <div class="panel panel-default">
7
7
  <div class="panel-heading">
8
- <h4><span class="fa fa-cog"></span> <%= @site.new_record? ? t('camaleon_cms.admin.page_title.new_site') : "#{t('camaleon_cms.admin.page_title.edit_site')}: #{@site.the_title} "%> </h4>
8
+ <h4><span class="fa fa-cog"></span> <%= @site.new_record? ? t('camaleon_cms.admin.page_title.new_site') : safe_join([t('camaleon_cms.admin.page_title.edit_site'), ': ', @site.the_title, ' ']) %> </h4>
9
9
  </div>
10
10
  <div class="panel-body">
11
11
  <%= render partial: 'layouts/camaleon_cms/admin/form_error', locals: {data: @site} %>
@@ -17,6 +17,26 @@
17
17
  <%= render partial: 'layouts/camaleon_cms/admin/form_error', locals: {data: @user_role} %>
18
18
  <% rol_values_post_type = @user_role.new_record? ? {} : @user_role.get_meta("_post_type_#{current_site.id.to_s}") %>
19
19
  <% rol_values_manager = @user_role.new_record? ? {} : @user_role.get_meta("_manager_#{current_site.id.to_s}") %>
20
+ <%# A role whose users are administrators holds every permission through `Ability`'s %>
21
+ <%# `can :manage, :all`, which returns before either meta above is read. Their stored %>
22
+ <%# metas are only what `set_default_user_roles` seeded when the site was created, so %>
23
+ <%# every key added to ROLES afterwards rendered unchecked on existing sites and read %>
24
+ <%# as "denied to administrators", which it never was. Deriving the state from the %>
25
+ <%# role keeps it true for keys added later, with no backfill task to run and none to %>
26
+ <%# write again next time. %>
27
+ <%# %>
28
+ <%# Such a role's boxes are also locked, and `UserRolesController` declines to write %>
29
+ <%# its metas — the two conditions are the same predicate, so a locked box, which the %>
30
+ <%# browser never submits, cannot silently clear what is stored. %>
31
+ <% grants_all = @user_role.grants_all_permissions? %>
32
+ <% permissions_editable = @user_role.permissions_editable? %>
33
+ <%# Without this the locked, all-checked boxes look like a bug rather than a statement %>
34
+ <%# about what an administrator is. %>
35
+ <% if grants_all %>
36
+ <div class="alert alert-info">
37
+ <span class="fa fa-info-circle"></span>&nbsp;<%= t('camaleon_cms.admin.users.admin_holds_all_permissions', default: 'Users with this role are administrators, so they hold every permission below regardless of what is selected. These settings cannot be changed.') %>
38
+ </div>
39
+ <% end %>
20
40
  <div id="contents-checkbox" class="form-horizontal">
21
41
  <div class="">
22
42
  <table class="table table-responsive table-hover table-striped">
@@ -49,7 +69,7 @@
49
69
  values = [] if values == 0
50
70
  %>
51
71
  <% if visible %>
52
- <input type="checkbox" name="rol_values[post_type][<%= value[:key] %>][]" value="<%= pt.id %>" <%= "checked" if values.include?(pt.id) %> <%= "disabled" unless @user_role.editable? %> onclick="<%= "cama_user_roles_do_all_publish(this, '#{value[:color]}', '#{value[:key]}')" %>" >
72
+ <input type="checkbox" name="rol_values[post_type][<%= value[:key] %>][]" value="<%= pt.id %>" <%= "checked" if grants_all || values.include?(pt.id) %> <%= "disabled" unless permissions_editable %> onclick="<%= "cama_user_roles_do_all_publish(this, '#{value[:color]}', '#{value[:key]}')" %>" >
53
73
  <% end %>
54
74
  </td>
55
75
  <% end %>
@@ -64,9 +84,10 @@
64
84
  <h5><%= t('camaleon_cms.admin.users.other_permissions')%></h5>
65
85
  <div class="row">
66
86
  <% values = rol_values_manager || {} %>
87
+ <%# Same rule as the grid above: see `grants_all` where it is assigned. %>
67
88
  <% cama_get_roles_values[:manager].each do |value| %>
68
89
  <div class="col-md-4">
69
- <label><input type="checkbox" name="rol_values[manager][<%= value[:key] %>]" value="1" <%= "checked" if values[value[:key].to_sym].present? %> <%= "disabled" unless @user_role.editable? %> <%= 'data-danger-select-eval=1' if value[:key].to_s == 'select_eval' %> >&nbsp; <%= raw value[:label] %></label>
90
+ <label><input type="checkbox" name="rol_values[manager][<%= value[:key] %>]" value="1" <%= "checked" if grants_all || values[value[:key].to_sym].present? %> <%= "disabled" unless permissions_editable %> <%= 'data-danger-select-eval=1' if value[:key].to_s == 'select_eval' %> >&nbsp; <%= raw value[:label] %></label>
70
91
  &nbsp;<%= raw cama_html_tooltip(value[:description], 'right') if value[:description].present? %>
71
92
  </div>
72
93
  <% end %>
@@ -77,7 +98,7 @@
77
98
 
78
99
 
79
100
  </div>
80
- <% if @user_role.editable? %>
101
+ <% if permissions_editable %>
81
102
  <div id="checked-actions" class="panel-footer text-right">
82
103
  <a class="btn btn-default btn-sm" data-type="all" href="#"><%= t('camaleon_cms.admin.button.select_all') %></a>
83
104
  <a class="btn btn-default btn-sm" data-type="none" href="#"><%= t('camaleon_cms.admin.button.select_none') %></a>
@@ -13,6 +13,9 @@
13
13
 
14
14
  <div class="row">
15
15
  <%= form_for @user, as: "user", url:{action: @user.new_record? ? :create : :update, id: @user.id}, html:{class: 'validate-user form-horizontal cama_ajax_request', id: "user_form"} do |f| %>
16
+ <%# A non-admin may not change an admin's password or recovery identifiers (email/username), so
17
+ the enforcement in UsersController#user_params/#updated_ajax is mirrored here (H10). %>
18
+ <% credentials_locked = !cama_current_user.may_edit_credentials?(@user) %>
16
19
  <div class="col-md-3">
17
20
  <div class="panel panel-default">
18
21
  <div class="panel-body">
@@ -35,11 +38,11 @@
35
38
  </div>
36
39
  <div class="">
37
40
  <label class="control-label"><%= t('camaleon_cms.admin.table.login')%></label>
38
- <%= f.text_field :username, :class => "form-control required" %>
41
+ <%= f.text_field :username, :class => "form-control required", disabled: credentials_locked %>
39
42
  </div>
40
43
  <div class="">
41
44
  <label class="control-label"><%= t('camaleon_cms.admin.table.e_mail')%></label>
42
- <%= f.email_field :email, :class => "form-control required email"%>
45
+ <%= f.email_field :email, :class => "form-control required email", disabled: credentials_locked %>
43
46
  </div>
44
47
  <% if @user.new_record? %>
45
48
  <div class="">
@@ -54,14 +57,15 @@
54
57
  <div class="">
55
58
  <%= f.label "#{t('camaleon_cms.admin.table.role')}", class: "control-label" %>
56
59
  <div class="">
57
- <%= f.select :role,current_site.user_roles.reorder(:name).decorate.map{|role| [role.the_title, role.slug]}, {}, {:class => "form-control required", disabled: !cama_current_user.role_grantor?(@user)}%>
60
+ <%# Only offer the admin role to someone who may actually grant it (H10); keep it listed when editing a user who already holds it so their role still displays. %>
61
+ <%= f.select :role, current_site.user_roles.reorder(:name).decorate.reject { |role| role.slug == 'admin' && !cama_current_user.role_grantor?(@user, 'admin') && @user&.role.to_s != 'admin' }.map{|role| [role.the_title, role.slug]}, {}, {:class => "form-control required", disabled: !cama_current_user.role_grantor?(@user)}%>
58
62
  </div>
59
63
  </div>
60
64
  <hr>
61
65
  <div class="">
62
66
  <button type="submit" class="btn btn-success btn-block" ><%= @user.new_record? ? t('camaleon_cms.admin.button.create') : t('camaleon_cms.admin.button.update') %></button>
63
67
  </div>
64
- <% if !@user.new_record? %>
68
+ <% if !@user.new_record? && !credentials_locked %>
65
69
  <div class="">
66
70
  <a href="#" class="btn btn-warning btn-block btn-rounded" data-toggle="modal" data-target="#modal_change_password"><%= t('camaleon_cms.admin.button.change_password') %></a>
67
71
  </div>
@@ -45,7 +45,7 @@
45
45
  <%= link_to raw('<i class="fa fa-times"></i>'), { action: :destroy, id: f.id },
46
46
  method: :delete, data: { confirm: t('camaleon_cms.admin.message.delete_item') }, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.delete')}" %>
47
47
  <% end %>
48
- <%= link_to raw('<i class="fa fa-retweet"></i>'), {action: :impersonate, id: f.id }, class: "btn btn-default btn-xs cama_ajax_request", title: "#{cama_t('camaleon_cms.admin.users.impersonate')}" if can? :impersonate, f %>
48
+ <%= link_to raw('<i class="fa fa-retweet"></i>'), {action: :impersonate, id: f.id }, method: :post, class: "btn btn-default btn-xs cama_ajax_request", title: "#{cama_t('camaleon_cms.admin.users.impersonate')}" if can? :impersonate, f %>
49
49
  </td>
50
50
  </tr>
51
51
  <% end %>
@@ -41,9 +41,15 @@
41
41
  %>
42
42
  <p><%= post.decorate.the_title if post.present? %></p>
43
43
  <% when 'field_attrs'
44
- post = JSON.parse(value) rescue {}
44
+ attrs = JSON.parse(value) rescue nil
45
45
  %>
46
- <p><%= raw("<strong>#{post['attr']}: </strong>#{post['attr']}") if post.present? %></p>
46
+ <% if attrs.is_a?(Hash) && attrs.present? %>
47
+ <%# Security (scan-and-reject policy): attr/value are gated at save like editor %>
48
+ <%# values -- CustomFieldsRelationship scans the DECODED JSON members (any shape) -- %>
49
+ <%# so stored values equal authored values and render verbatim. Guard on Hash so a %>
50
+ <%# non-object JSON value (array/scalar) renders nothing instead of raising a 500. %>
51
+ <p><strong><%= raw attrs['attr'] %>: </strong><%= raw attrs['value'] %></p>
52
+ <% end %>
47
53
  <% else %>
48
54
  <p><%= value %></p>
49
55
  <% end %>
@@ -6,7 +6,7 @@
6
6
  <li>
7
7
  <h3><a href="<%= ptype.the_url %>"><%= ptype.the_title %></a></h3>
8
8
  <% if ptype.manage_categories? %>
9
- <%= raw(cama_sitemap_cats_generator(ptype.the_categories)) %>
9
+ <%= raw(cama_sitemap_cats_generator(ptype.the_categories, @r)) %>
10
10
  <% else %>
11
11
  <ul>
12
12
  <% ptype.the_posts.decorate.each do |post| next if @r[:skip_post_ids].include?(post.id) %>
@@ -57,7 +57,7 @@ xml.urlset 'xmlns' => 'https://www.sitemaps.org/schemas/sitemap/0.9' do
57
57
  @r[:custom].each_value do |item|
58
58
  xml.url do
59
59
  xml.loc item[:url]
60
- xml.lastmod item[:lastmod] || Date.today.to_s
60
+ xml.lastmod item[:lastmod] || Date.current.to_s
61
61
  xml.changefreq item[:changefreq] || 'monthly'
62
62
  xml.priority item[:priority] || '0.5'
63
63
  end
@@ -38,7 +38,7 @@
38
38
  <a href="<%= cama_admin_profile_edit_path %>" class="btn btn-primary cama_ajax_request"><%= t("camaleon_cms.admin.users.profile") %></a>
39
39
  </div>
40
40
  <div class="pull-right">
41
- <a href="<%= cama_admin_logout_path %>" class="btn btn-danger"><%= t("camaleon_cms.admin.logout.logout") %></a>
41
+ <%= button_to t("camaleon_cms.admin.logout.logout"), cama_admin_logout_path, method: :post, class: "btn btn-danger" %>
42
42
  </div>
43
43
  </li>
44
44
  </ul>
@@ -7,11 +7,14 @@ module ActionView
7
7
  # fix to add camaleon prefix to search partials and layouts
8
8
  def find(name, prefixes = [], partial = false, keys = [], options = {})
9
9
  if use_camaleon_partial_prefixes.present?
10
- if !partial && !prefixes.present? && File.exist?(name) # fix for windows ==> render file: '....'
10
+ scoped_prefixes = cama_theme_scoped_prefixes(prefixes)
11
+ theme_scoped = scoped_prefixes.present?
12
+ prefixes = scoped_prefixes if theme_scoped
13
+ if !partial && prefixes.blank? && File.exist?(name) # fix for windows ==> render file: '....'
11
14
  # puts "rendering specific file (render file: '....')"
12
15
  else
13
- prefixes = [''] unless prefixes.present?
14
- prefixes = (self.prefixes + prefixes).uniq if prefixes.is_a?(Array)
16
+ prefixes = [''] if prefixes.blank?
17
+ prefixes = (self.prefixes + prefixes).uniq if prefixes.is_a?(Array) && !theme_scoped
15
18
  end
16
19
  end
17
20
  @view_paths.find(*cama_args_for_lookup(name, prefixes, partial, keys, options))
@@ -21,8 +24,11 @@ module ActionView
21
24
  # fix to add camaleon prefixes on verify template exist
22
25
  def exists?(name, prefixes = [], partial = false, keys = [], **options)
23
26
  if use_camaleon_partial_prefixes.present?
24
- prefixes = [''] unless prefixes.present?
25
- prefixes = (prefixes + self.prefixes).uniq if prefixes.is_a?(Array)
27
+ scoped_prefixes = cama_theme_scoped_prefixes(prefixes)
28
+ theme_scoped = scoped_prefixes.present?
29
+ prefixes = scoped_prefixes if theme_scoped
30
+ prefixes = [''] if prefixes.blank?
31
+ prefixes = (prefixes + self.prefixes).uniq if prefixes.is_a?(Array) && !theme_scoped
26
32
  end
27
33
  @view_paths.exists?(*cama_args_for_lookup(name, prefixes, partial, keys, options))
28
34
  end
@@ -43,6 +49,37 @@ module ActionView
43
49
  [name, prefixes, partial || false, details, details_key, keys]
44
50
  end
45
51
  end
52
+
53
+ # Detects a lookup whose explicit prefixes are *entirely* scoped to the
54
+ # current (frontend) theme -- i.e. its `themes/<slug>/views...` prefixes
55
+ # and/or the gem `camaleon_cms/default_theme` fallback, and nothing else
56
+ # (e.g. a partial rendered from within a theme, or a theme preview). In
57
+ # that case the lookup is restricted to that theme and the global
58
+ # `self.prefixes` are NOT merged, so a different theme / per-site override
59
+ # does not leak in.
60
+ #
61
+ # When the explicit prefixes also include non-theme prefixes (e.g. a
62
+ # plugin front controller rendering its own `plugins/<name>/...`
63
+ # templates), it returns a blank list, signalling the caller to merge
64
+ # `self.prefixes` as before -- this keeps the plugin's own
65
+ # `plugins/<name>/views/...` lookup prefix reachable.
66
+ #
67
+ # Known limitation (regression audit M30, deliberately kept): an explicit
68
+ # `render prefixes:` whose list is *entirely* theme-scoped but whose target
69
+ # template actually lives under a controller prefix raises MissingTemplate,
70
+ # because `self.prefixes` are not merged in that case. Merging them would
71
+ # reopen the per-site-override / cross-theme leak this guard exists to close,
72
+ # and no stock or ecosystem caller hits it, so the isolation is kept.
73
+ def cama_theme_scoped_prefixes(prefixes)
74
+ theme = CurrentRequest.frontend_current_theme
75
+ slug = theme&.slug
76
+ return if slug.blank? || prefixes.blank? || !prefixes.is_a?(Array)
77
+
78
+ selected = prefixes.select do |prefix|
79
+ prefix.include?("themes/#{slug}/views") || prefix == 'camaleon_cms/default_theme'
80
+ end
81
+ selected if selected.length == prefixes.length
82
+ end
46
83
  end
47
84
  end
48
85
  end
@@ -1,25 +1,3 @@
1
- module ActiveRecordExtras
2
- module Relation
3
- extend ActiveSupport::Concern
4
-
5
- module ClassMethods
6
- def update_or_create(attributes = {})
7
- assign_or_new(attributes).save
8
- end
9
-
10
- def update_or_create!(attributes = {})
11
- assign_or_new(attributes).save!
12
- end
13
-
14
- def assign_or_new(attributes)
15
- obj = first || new
16
- obj.assign_attributes(attributes)
17
- obj
18
- end
19
- end
20
- end
21
- end
22
-
23
1
  ActiveRecord::Associations::CollectionProxy.class_eval do
24
2
  # order a collection by custom fields
25
3
  # Arguments:
@@ -28,9 +6,17 @@ ActiveRecord::Associations::CollectionProxy.class_eval do
28
6
  # sample: CamaleonCms::Site.first.posts.sort_by_field("untitled-field-attributes", "desc")
29
7
  def sort_by_field(key, order = 'ASC')
30
8
  cfr_table = CamaleonCms::CustomFieldsRelationship.table_name
31
- joins("LEFT OUTER JOIN #{cfr_table} ON #{cfr_table}.objectid = #{build.class.table_name}.id").where(
32
- "#{cfr_table}.custom_field_slug = ? and #{cfr_table}.object_class = ?", key, build.class.name.parseCamaClass
33
- ).reorder("#{cfr_table}.value #{order}")
9
+ # Security (audit 2026-08-11, Tier-2 #8): sort_by_field is a public API themes/plugins reach as
10
+ # sort_by_field(key, params[:order]), so interpolating `order` straight into ORDER BY was an
11
+ # injection sink. Whitelist the direction to ASC/DESC and order by a quoted Arel column, so a
12
+ # hostile direction can neither append extra ORDER BY terms nor raise -- without relying on
13
+ # ActiveRecord's implicit raw-SQL guard. The whitelist reads the leading token so previously
14
+ # working spellings (' desc ', 'DESC NULLS LAST') keep their direction; punctuation sticks to
15
+ # the token ('DESC;...' != 'DESC'), so hostile strings still fail closed to ascending.
16
+ direction = order.to_s.strip[/\A\S+/]&.casecmp?('DESC') ? :desc : :asc
17
+ joins("LEFT OUTER JOIN #{cfr_table} ON #{cfr_table}.objectid = #{klass.table_name}.id").where(
18
+ "#{cfr_table}.custom_field_slug = ? and #{cfr_table}.object_class = ?", key, klass.name.parseCamaClass
19
+ ).reorder(CamaleonCms::CustomFieldsRelationship.arel_table[:value].public_send(direction))
34
20
  end
35
21
 
36
22
  # Filter by custom field values
@@ -43,8 +29,8 @@ ActiveRecord::Associations::CollectionProxy.class_eval do
43
29
  # .where("#{CamaleonCms::CustomFieldsRelationship.table_name}.value=?", "my_value_for_field")
44
30
  def filter_by_field(key, args = {})
45
31
  cfr_table = CamaleonCms::CustomFieldsRelationship.table_name
46
- res = joins("LEFT OUTER JOIN #{cfr_table} ON #{cfr_table}.objectid = #{build.class.table_name}.id").where(
47
- "#{cfr_table}.custom_field_slug = ? and #{cfr_table}.object_class = ?", key, build.class.name.parseCamaClass
32
+ res = joins("LEFT OUTER JOIN #{cfr_table} ON #{cfr_table}.objectid = #{klass.table_name}.id").where(
33
+ "#{cfr_table}.custom_field_slug = ? and #{cfr_table}.object_class = ?", key, klass.name.parseCamaClass
48
34
  )
49
35
  res = res.where("#{cfr_table}.value = ?", args[:value]) if args[:value]
50
36
  res
@@ -1,34 +1,63 @@
1
1
  # Be sure to restart your server when you modify this file.
2
2
 
3
3
  # Version of your assets, change this if you want to expire all your assets.
4
- Rails.application.config.assets.version = '1.0'
4
+ Rails.application.config.assets.version = '1.1'
5
5
 
6
6
  Rails.application.config.tinymce.install = :copy
7
7
 
8
- # Add additional assets to the asset load path
9
- Rails.application.config.assets.precompile += %w[camaleon_cms/*]
10
- # Rails.application.config.assets.precompile += %w( themes/*/assets/* )
8
+ # TinyMCE editor icons in development.
9
+ #
10
+ # sprockets-rails >= 3.5 registers `Sprockets::Rails::AssetUrlProcessor`, a `text/css`
11
+ # post-processor that rewrites every relative `url(...)` reference to a digested asset
12
+ # path. TinyMCE's bundled skin (`tinymce/skins/lightgray/skin.min.css`) references its
13
+ # icon font with directory-relative urls such as `url('fonts/tinymce.woff')`. Sprockets
14
+ # resolves `url()` paths against the asset load roots (not the referencing file's own
15
+ # directory), so it cannot find `fonts/tinymce.woff` (its real logical path is
16
+ # `tinymce/skins/lightgray/fonts/...`) and rewrites it to an invalid root path
17
+ # (`/fonts/tinymce.woff`) which 404s, leaving the editor toolbar without icons.
18
+ #
19
+ # In production this never happens: `tinymce.install = :copy` ships the skin as raw static
20
+ # files under `public/assets`, bypassing Sprockets (and this processor) entirely. The bug
21
+ # only surfaces in environments that compile assets on the fly (development).
22
+ #
23
+ # We must NOT disable the processor globally: other stylesheets (e.g. Bootstrap, whose
24
+ # glyphicon @font-face urls are relative) rely on it to produce resolvable digested paths.
25
+ # Instead, swap it for a thin subclass that, for the TinyMCE skin only, first expands the
26
+ # skin's directory-relative font urls into full logical asset paths (prefixing them with the
27
+ # skin's own logical directory) before delegating to the original processor. The original
28
+ # then resolves and digests them correctly. Every other stylesheet is processed unchanged.
29
+ if defined?(Sprockets::Rails::AssetUrlProcessor) && Rails.application.config.assets.compile &&
30
+ Sprockets.respond_to?(:unregister_postprocessor) && Sprockets.respond_to?(:register_postprocessor)
31
+ class TinymceSkinSafeAssetUrlProcessor < Sprockets::Rails::AssetUrlProcessor
32
+ SKIN_MARKER = 'tinymce/skins/'.freeze
33
+ # Directory-relative `url(...)` references that are not absolute/external/data urls.
34
+ RELATIVE_URL = %r{url\(\s*["']?(?!(?:#|data|http|/))(?:\./)?(?<path>[^"'\s)]+)\s*["']?\)}
11
35
 
12
- # This will precompile any assets, not just JavaScript (.js, .swf, .css, .scss)
13
-
14
- sprockets_3 = !Sprockets.const_defined?(:BabelProcessor)
15
- if sprockets_3
16
- Rails.application.config.assets.precompile << proc do |path|
17
- res = false
18
- dirname = File.dirname(path)
19
- if dirname.start_with?('plugins/') || dirname.start_with?('themes/')
20
- name = File.basename(path)
21
- content_type = begin
22
- MIME::Types.type_for(name).first.content_type
23
- rescue StandardError
24
- ''
25
- end
26
- if (path =~ /\.(css|js|svg|ttf|woff|eot|swf|pdf|png|jpg|gif)\z/ ||
27
- content_type.scan(%r{(javascript|image/|audio|video|font)}).any?) &&
28
- !name.start_with?('_') && !path.include?('/views/')
29
- res = true
36
+ def self.call(input)
37
+ filename = input[:filename].to_s
38
+ return super unless filename.include?(SKIN_MARKER)
39
+
40
+ logical_dir = File.dirname(filename[filename.index('tinymce/')..])
41
+ rewritten = input[:data].gsub(RELATIVE_URL) do
42
+ "url(#{logical_dir}/#{Regexp.last_match(:path)})"
30
43
  end
44
+ super(input.merge(data: rewritten))
31
45
  end
32
- res
33
46
  end
47
+
48
+ Sprockets.unregister_postprocessor('text/css', Sprockets::Rails::AssetUrlProcessor)
49
+ Sprockets.register_postprocessor('text/css', TinymceSkinSafeAssetUrlProcessor)
34
50
  end
51
+
52
+ # Add additional assets to the asset load path
53
+ Rails.application.config.assets.precompile += %w[camaleon_cms/*]
54
+ # Rails.application.config.assets.precompile += %w( themes/*/assets/* )
55
+
56
+ # Precompile plugin/theme assets that live under app/apps/{plugins,themes}/.../assets.
57
+ # Rails' default precompile rules only cover app/assets, so without this plugin/theme
58
+ # assets are never declared as precompiled and `javascript_include_tag` /
59
+ # `stylesheet_link_tag` raise `AssetNotPrecompiledError` on Sprockets >= 4 when
60
+ # `unknown_asset_fallback` is disabled. Sprockets 4 only resolves exact logical paths
61
+ # (globs/procs are not honored by `asset_precompiled?`), so we enumerate them as strings.
62
+ require 'camaleon_cms/assets_precompile'
63
+ Rails.application.config.assets.precompile += CamaleonCms::AssetsPrecompile.logical_paths
@@ -1,7 +1,7 @@
1
1
  # load all custom initializers of plugins or themes
2
2
  Rails.application.config.to_prepare do |_config|
3
3
  PluginRoutes.all_apps.each do |ap|
4
- next unless ap['path'].present?
4
+ next if ap['path'].blank?
5
5
 
6
6
  f = File.join(ap['path'], 'config', 'initializer.rb')
7
7
  load f if File.exist?(f)
@@ -136,6 +136,7 @@ ar:
136
136
  message:
137
137
  success: 'Welcome!!!'
138
138
  fail: 'Username or Password incorrect'
139
+ too_many_attempts: 'محاولات فاشلة كثيرة جدًا. يرجى المحاولة مرة أخرى لاحقًا.'
139
140
  forgot_url_incorrect: 'URL incorrect'
140
141
  forgot_expired: 'Reset expired.'
141
142
  hello: 'Hello'
@@ -135,6 +135,7 @@ de:
135
135
  message:
136
136
  success: 'Willkommen!!!'
137
137
  fail: 'Benutzername oder Passwort falsch'
138
+ too_many_attempts: 'Zu viele fehlgeschlagene Versuche. Bitte versuchen Sie es später erneut.'
138
139
  forgot_url_incorrect: 'URL inkorrekt'
139
140
  forgot_expired: 'Resetfunktion abgelaufen'
140
141
  hello: 'Hallo'