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
@@ -5,7 +5,7 @@ module CamaleonCms
5
5
 
6
6
  slug_array = record.slug.to_s.translations_array
7
7
  ptype = record.post_type
8
- return unless ptype.present? # only for posts that belongs to a post type model
8
+ return if ptype.blank? # only for posts that belongs to a post type model
9
9
 
10
10
  post_table = CamaleonCms::Post.table_name
11
11
 
@@ -27,7 +27,7 @@ module CamaleonCms
27
27
  .where.not(id: record.id)
28
28
  .where.not(status: %i[draft draft_child trash])
29
29
  unless posts.empty?
30
- record.errors[:base] <<
30
+ message =
31
31
  if slug_array.size > 1
32
32
  "#{I18n.t('camaleon_cms.admin.post.message.requires_different_slug')}: #{posts.pluck(:slug).map do |slug|
33
33
  record.slug.to_s.translations.map do |lng, r_slug|
@@ -37,14 +37,15 @@ module CamaleonCms
37
37
  else
38
38
  "#{I18n.t('camaleon_cms.admin.post.message.requires_different_slug')}: #{record.slug} "
39
39
  end
40
+ record.errors.add(:base, message)
40
41
  end
41
42
 
42
43
  # avoid recursive page parent
43
44
  return unless record.post_parent.present? && ptype.manage_hierarchy? &&
44
45
  record.parents.cama_pluck(:id).include?(record.id)
45
46
 
46
- record.errors[:base] << I18n.t('camaleon_cms.admin.post.message.recursive_hierarchy',
47
- default: 'Parent Post Recursive')
47
+ record.errors.add(:base, I18n.t('camaleon_cms.admin.post.message.recursive_hierarchy',
48
+ default: 'Parent Post Recursive'))
48
49
  end
49
50
  end
50
51
  end
@@ -11,7 +11,7 @@ module CamaleonCms
11
11
 
12
12
  return unless slug_exists
13
13
 
14
- record.errors[:base] << I18n.t('camaleon_cms.admin.post.message.requires_different_slug').to_s
14
+ record.errors.add(:base, I18n.t('camaleon_cms.admin.post.message.requires_different_slug').to_s)
15
15
  end
16
16
  end
17
17
  end
@@ -24,20 +24,32 @@
24
24
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
25
  # SOFTWARE.
26
26
 
27
- # require 'resolv'
28
- require 'ipaddress'
27
+ require 'ipaddr'
28
+ require 'resolv'
29
29
  require 'addressable/uri'
30
30
 
31
31
  module CamaleonCms
32
32
  class UserUrlValidator
33
+ HTTPS_SCHEME = 'https'
33
34
  LOCAL_IPS = %w[0.0.0.0 ::].freeze
35
+ LINK_LOCAL_NETMASK = IPAddr.new('169.254.0.0/16').freeze
36
+ SHARED_ADDR_NETMASK = IPAddr.new('100.64.0.0/10').freeze
37
+ IPV6_SITELOCAL = IPAddr.new('fec0::/10').freeze
38
+ IPV6_UNIQUE_LOCAL = IPAddr.new('fc00::/7').freeze
34
39
 
35
40
  def self.validate(...)
36
41
  new.validate(...)
37
42
  end
38
43
 
44
+ def self.validate_external_https(...)
45
+ new.validate_external_https(...)
46
+ end
47
+
48
+ attr_reader :resolved_ip
49
+
39
50
  def initialize
40
51
  @errors = []
52
+ @resolved_ip = nil
41
53
  end
42
54
 
43
55
  # Validates the given url according to the constraints specified by the received arguments.
@@ -46,31 +58,65 @@ module CamaleonCms
46
58
  # allow_local_network - Registers error if URL resolves to a link-local address and argument is false.
47
59
  # enforce_user - Registers error if URL user doesn't start with alphanumeric characters and argument is true.
48
60
  # enforce_sanitizing - Registers error if URL includes any HTML/CSS/JS tags and argument is true.
61
+ # resolve - When true (default), performs DNS resolution to check the IP.
62
+ # When false, only validates URL structure and checks IP literals statically
63
+ # (no DNS resolution, allowing unresolvable hostnames like custom scheme URLs).
64
+ # reject_path_traversal - When true, checks if URI path contains path traversal sequences.
49
65
  #
50
- # Returns an array with [<uri>, <original-hostname>].
51
- def validate(url, allow_localhost: false, allow_local_network: false, enforce_user: true, enforce_sanitizing: true)
52
- return invalid_url unless url.present?
66
+ # Returns an array with error messages, or true if valid.
67
+ def validate(url, allow_localhost: false, allow_local_network: false, enforce_user: true, enforce_sanitizing: true,
68
+ resolve: true, reject_path_traversal: false)
69
+ return true if skip_validation?
70
+ return invalid_url if url.blank?
53
71
 
54
- # Param url can be a string, URI or Addressable::URI
55
72
  return invalid_url unless (uri = parse_url(url))
56
73
 
57
74
  validate_uri(uri: uri, enforce_sanitizing: enforce_sanitizing, enforce_user: enforce_user)
58
- return @errors if @errors.any?
59
75
 
60
- address_info = get_address_info(uri)
76
+ validate_path_traversal(uri) if reject_path_traversal
61
77
  return @errors if @errors.any?
62
78
 
63
- validate_local_request(
64
- address_info: address_info,
65
- allow_localhost: allow_localhost,
66
- allow_local_network: allow_local_network
67
- )
79
+ if resolve
80
+ return @errors if @errors.any?
81
+
82
+ address_info = get_address_info(uri)
83
+ return @errors if @errors.any?
84
+
85
+ @resolved_ip = address_info.first&.ip_address
86
+
87
+ validate_local_request(
88
+ address_info: address_info,
89
+ allow_localhost: allow_localhost,
90
+ allow_local_network: allow_local_network
91
+ )
92
+ else
93
+ validate_static_ip(uri.hostname, allow_localhost: allow_localhost, allow_local_network: allow_local_network)
94
+ end
68
95
 
69
96
  @errors.empty? || @errors
70
97
  end
71
98
 
99
+ def validate_external_https(url)
100
+ return true if skip_validation?
101
+
102
+ uri = parse_url(url)
103
+ return [I18n.t('camaleon_cms.admin.validate.url')] if uri.nil? || uri.scheme.blank? || uri.hostname.blank?
104
+ return [I18n.t('camaleon_cms.admin.validate.https_only_url')] unless uri.scheme&.downcase == HTTPS_SCHEME
105
+
106
+ validate(uri, allow_localhost: false, allow_local_network: false)
107
+ end
108
+
72
109
  private
73
110
 
111
+ # Operator escape hatch: when ENV['CAMALEON_SKIP_URL_VALIDATION'] is set, all upload-URL
112
+ # validation (scheme, host, SSRF/link-local, path traversal, HTML sanitization) is bypassed.
113
+ # It is meant only for trusted, isolated environments (e.g. a local import job behind no
114
+ # network); leaving it set in production removes the SSRF and open-fetch protections around
115
+ # crop/crop_url. Off unless the variable is explicitly present.
116
+ def skip_validation?
117
+ ENV['CAMALEON_SKIP_URL_VALIDATION'].present?
118
+ end
119
+
74
120
  def validate_uri(uri:, enforce_sanitizing:, enforce_user:)
75
121
  validate_html_tags(uri) if enforce_sanitizing
76
122
 
@@ -78,19 +124,23 @@ module CamaleonCms
78
124
  validate_hostname(uri.hostname)
79
125
  end
80
126
 
81
- # @param uri [Addressable::URI]
82
- # @return [Array<Addrinfo>] addrinfo object for the URI
83
127
  def get_address_info(uri)
84
- Addrinfo.getaddrinfo(uri.hostname, get_port(uri), nil, :STREAM).map do |addr|
128
+ return @errors << I18n.t('camaleon_cms.admin.validate.host_invalid') if uri.hostname.blank?
129
+
130
+ Addrinfo.getaddrinfo(hostname_for_resolution(uri.hostname), get_port(uri), nil, :STREAM).map do |addr|
85
131
  addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr
86
132
  end
133
+ rescue SocketError
134
+ if valid_ip?(uri.hostname)
135
+ addr = Addrinfo.new(Socket.pack_sockaddr_in(get_port(uri), uri.hostname))
136
+ [addr.ipv6_v4mapped? ? addr.ipv6_to_ipv4 : addr]
137
+ else
138
+ @errors << I18n.t('camaleon_cms.admin.validate.host_invalid')
139
+ end
87
140
  rescue ArgumentError => e
88
- # Addrinfo.getaddrinfo errors if the domain exceeds 1024 characters.
89
141
  @errors << I18n.t('camaleon_cms.admin.validate.hostname_long') if e.message.include?('hostname too long')
90
142
 
91
143
  @errors << "#{e.message}: #{I18n.t('camaleon_cms.admin.validate.url')}" if @errors.blank?
92
- rescue SocketError
93
- @errors << I18n.t('camaleon_cms.admin.validate.host_invalid')
94
144
  end
95
145
 
96
146
  def validate_local_request(address_info:, allow_localhost:, allow_local_network:)
@@ -113,14 +163,21 @@ module CamaleonCms
113
163
  uri.port || uri.default_port
114
164
  end
115
165
 
166
+ def hostname_for_resolution(hostname)
167
+ return hostname if hostname.blank?
168
+ return hostname if hostname.end_with?('.')
169
+ return hostname if valid_ip?(hostname)
170
+ return hostname unless hostname.match?(/\.[a-zA-Z]/)
171
+
172
+ "#{hostname}."
173
+ end
174
+
116
175
  def validate_html_tags(uri)
117
176
  uri_str = uri.to_s
118
177
  sanitized_uri = ActionController::Base.helpers.sanitize(uri_str, tags: [])
119
178
  @errors << I18n.t('camaleon_cms.admin.validate.html_tags') unless sanitized_uri == uri_str
120
179
  end
121
180
 
122
- # @param [String, Addressable::URI, #to_str] url The URL string to parse
123
- # @return [Addressable::URI, nil] URI object based on the parsed string, or `nil` if the `url` is invalid
124
181
  def parse_url(url)
125
182
  invalid = nil
126
183
  uri = Addressable::URI.parse(url).tap do |parsed_url|
@@ -136,8 +193,7 @@ module CamaleonCms
136
193
  def multiline_blocked?(parsed_url)
137
194
  url = parsed_url.to_s
138
195
 
139
- return true if url =~ /[\n\r]/
140
- # Google Cloud Storage uses a multi-line, encoded Signature query string
196
+ return true if /[\n\r]/.match?(url)
141
197
  return false if %w[http https].include?(parsed_url.scheme&.downcase)
142
198
 
143
199
  CGI.unescape(url) =~ /[\n\r]/
@@ -145,19 +201,44 @@ module CamaleonCms
145
201
 
146
202
  def validate_user(value)
147
203
  return if value.blank?
148
- return if value =~ /\A\p{Alnum}/
204
+ return if /\A\p{Alnum}/.match?(value)
149
205
 
150
206
  @errors << I18n.t('camaleon_cms.admin.validate.username_alphanumeric')
151
207
  end
152
208
 
153
209
  def validate_hostname(value)
154
210
  return if value.blank?
155
- return if IPAddress.valid?(value)
156
- return if value =~ /\A\p{Alnum}/
211
+ return @errors << I18n.t('camaleon_cms.admin.validate.host_invalid') if invalid_decimal_ipv4_hostname?(value)
212
+ return if valid_ip?(value)
213
+ return if /\A\p{Alnum}/.match?(value)
157
214
 
158
215
  @errors << I18n.t('camaleon_cms.admin.validate.host_or_ip_invalid')
159
216
  end
160
217
 
218
+ def invalid_decimal_ipv4_hostname?(value)
219
+ return false unless /\A(?:0|[1-9]\d*)(?:\.(?:0|[1-9]\d*))+\z/.match?(value)
220
+
221
+ # An all-numeric dotted hostname is only ever meant to be a dotted-quad
222
+ # IPv4 literal. Reject it when it has too many octets or any out-of-range
223
+ # octet (regardless of octet count) so an out-of-range shorthand form such
224
+ # as 192.168.257 cannot slip through as a resolvable name. In-range
225
+ # shorthand (e.g. 192.168.1) is left to the resolver/category checks.
226
+ octets = value.split('.')
227
+ octets.length > 4 || octets.any? { |octet| octet.to_i > 255 }
228
+ end
229
+
230
+ def valid_ip?(value)
231
+ str = value.to_s
232
+ return false if str.blank?
233
+ return false if str.include?('/')
234
+ return false unless Resolv::IPv4::Regex.match?(str) || Resolv::IPv6::Regex.match?(str)
235
+
236
+ IPAddr.new(str)
237
+ true
238
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
239
+ false
240
+ end
241
+
161
242
  def validate_localhost(addrs_info)
162
243
  return if (Socket.ip_address_list.map(&:ip_address).concat(LOCAL_IPS) & addrs_info.map(&:ip_address)).empty?
163
244
 
@@ -165,41 +246,139 @@ module CamaleonCms
165
246
  end
166
247
 
167
248
  def validate_loopback(addrs_info)
168
- return unless addrs_info.any? { |addr| addr.ipv4_loopback? || addr.ipv6_loopback? }
249
+ return unless addrs_info.any? { |addr| loopback_ip?(ip_from_addrinfo(addr)) }
169
250
 
170
251
  @errors << I18n.t('camaleon_cms.admin.validate.no_loopback_requests')
171
252
  end
172
253
 
173
254
  def validate_local_network(addrs_info)
174
- return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? || addr.ipv6_unique_local? }
255
+ return unless addrs_info.any? { |addr| local_network_ip?(ip_from_addrinfo(addr)) }
175
256
 
176
257
  @errors << I18n.t('camaleon_cms.admin.validate.no_local_net_requests')
177
258
  end
178
259
 
179
260
  def validate_link_local(addrs_info)
180
- netmask = IPAddr.new('169.254.0.0/16')
181
- return unless addrs_info.any? { |addr| addr.ipv6_linklocal? || netmask.include?(addr.ip_address) }
261
+ return unless addrs_info.any? { |addr| link_local_ip?(ip_from_addrinfo(addr)) }
182
262
 
183
263
  @errors << I18n.t('camaleon_cms.admin.validate.no_link_local_net_requests')
184
264
  end
185
265
 
186
266
  def validate_shared_address(addrs_info)
187
- netmask = IPAddr.new('100.64.0.0/10')
188
- return unless addrs_info.any? { |addr| netmask.include?(addr.ip_address) }
267
+ return unless addrs_info.any? { |addr| shared_address_ip?(ip_from_addrinfo(addr)) }
189
268
 
190
269
  @errors << I18n.t('camaleon_cms.admin.validate.no_shared_address_requests')
191
270
  end
192
271
 
193
- # Registers an error if any IP in `addrs_info` is the limited broadcast address.
194
- # https://datatracker.ietf.org/doc/html/rfc919#section-7
195
272
  def validate_limited_broadcast_address(addrs_info)
196
- blocked_ips = ['255.255.255.255']
273
+ return unless addrs_info.any? { |addr| limited_broadcast_ip?(ip_from_addrinfo(addr)) }
274
+
275
+ @errors << I18n.t('camaleon_cms.admin.validate.no_limited_broadcast_address_requests')
276
+ end
277
+
278
+ def validate_static_ip(hostname, allow_localhost:, allow_local_network:)
279
+ return if hostname.blank?
280
+
281
+ if !allow_localhost && (hostname.casecmp?('localhost') || hostname.casecmp?('localhost.localdomain'))
282
+ @errors << I18n.t('camaleon_cms.admin.validate.no_localhost_requests')
283
+ return
284
+ end
285
+
286
+ return unless valid_ip?(hostname)
287
+
288
+ ip = IPAddr.new(hostname)
289
+ ip = ip.native if ip.ipv4_mapped?
290
+
291
+ validate_static_localhost(hostname, ip) unless allow_localhost
292
+ return if allow_local_network
197
293
 
198
- return if (blocked_ips & addrs_info.map(&:ip_address)).empty?
294
+ validate_static_local_network(ip)
295
+ validate_static_link_local(ip)
296
+ validate_static_shared_address(ip)
297
+ validate_static_limited_broadcast(ip)
298
+ end
299
+
300
+ def validate_static_localhost(hostname, ip)
301
+ if hostname.in?(LOCAL_IPS) || ip.to_s == '::'
302
+ @errors << I18n.t('camaleon_cms.admin.validate.no_localhost_requests')
303
+ elsif ip.loopback?
304
+ @errors << I18n.t('camaleon_cms.admin.validate.no_loopback_requests')
305
+ end
306
+ end
307
+
308
+ def validate_static_local_network(ip)
309
+ return unless local_network_ip?(ip)
310
+
311
+ @errors << I18n.t('camaleon_cms.admin.validate.no_local_net_requests')
312
+ end
313
+
314
+ def validate_static_link_local(ip)
315
+ return unless link_local_ip?(ip)
316
+
317
+ @errors << I18n.t('camaleon_cms.admin.validate.no_link_local_net_requests')
318
+ end
319
+
320
+ def validate_static_shared_address(ip)
321
+ return unless shared_address_ip?(ip)
322
+
323
+ @errors << I18n.t('camaleon_cms.admin.validate.no_shared_address_requests')
324
+ end
325
+
326
+ def validate_static_limited_broadcast(ip)
327
+ return unless limited_broadcast_ip?(ip)
199
328
 
200
329
  @errors << I18n.t('camaleon_cms.admin.validate.no_limited_broadcast_address_requests')
201
330
  end
202
331
 
332
+ def validate_path_traversal(uri)
333
+ # Fully percent-decode the path (so both %2e%2e and multiply-encoded forms
334
+ # such as %252e%252e are caught) then look for actual ".." path segments.
335
+ # Comparing uri.path to uri.normalized_path would instead flag legitimate
336
+ # URLs whose encoding merely differs (e.g. %7E -> ~).
337
+ decoded = uri.path.to_s
338
+ 5.times do
339
+ unencoded = Addressable::URI.unencode(decoded)
340
+ break if unencoded == decoded
341
+
342
+ decoded = unencoded
343
+ end
344
+ return unless decoded.split(%r{[/\\]}).include?('..')
345
+
346
+ @errors << I18n.t('camaleon_cms.admin.validate.path_traversal')
347
+ end
348
+
349
+ def ip_from_addrinfo(addr)
350
+ ip = IPAddr.new(addr.ip_address)
351
+ ip.ipv4_mapped? ? ip.native : ip
352
+ end
353
+
354
+ def loopback_ip?(ip)
355
+ ip.loopback?
356
+ end
357
+
358
+ def local_network_ip?(ip)
359
+ ip.private? || ipv6_sitelocal?(ip) || ipv6_unique_local?(ip)
360
+ end
361
+
362
+ def link_local_ip?(ip)
363
+ ip.link_local? || LINK_LOCAL_NETMASK.include?(ip)
364
+ end
365
+
366
+ def shared_address_ip?(ip)
367
+ SHARED_ADDR_NETMASK.include?(ip)
368
+ end
369
+
370
+ def limited_broadcast_ip?(ip)
371
+ ip.to_s == '255.255.255.255'
372
+ end
373
+
374
+ def ipv6_sitelocal?(ip)
375
+ IPV6_SITELOCAL.include?(ip)
376
+ end
377
+
378
+ def ipv6_unique_local?(ip)
379
+ IPV6_UNIQUE_LOCAL.include?(ip)
380
+ end
381
+
203
382
  def invalid_url
204
383
  @errors << I18n.t('camaleon_cms.admin.validate.url')
205
384
  end
@@ -12,7 +12,7 @@
12
12
  <div class="panel panel-default">
13
13
  <div class="panel-heading ui-draggable-handle">
14
14
  <h4>
15
- <span class="fa fa-folder"></span> <%= "#{@post_type.decorate.the_title} / " if @post_type.present? %> <%= t('camaleon_cms.admin.post_type.categories', default: 'Categories') %>
15
+ <span class="fa fa-folder"></span> <%= safe_join([@post_type.decorate.the_title, ' / ']) if @post_type.present? %> <%= t('camaleon_cms.admin.post_type.categories', default: 'Categories') %>
16
16
  </h4>
17
17
  </div>
18
18
  <div class="panel-body">
@@ -39,7 +39,7 @@
39
39
  <%= r = {html: "", post_type: @post_type, category: item}; hooks_run("category_list_body", r); raw(r[:html]); %>
40
40
  <td>
41
41
  <%= link_to raw('<i class="fa fa-eye"></i>'), item.the_url, target: '_blank', class: "btn btn-info btn-xs", title: "#{t('camaleon_cms.common.visit')}" %>
42
- <%= link_to raw('<i class="fa fa-list"></i>'), cama_admin_post_type_taxonomy_posts_path(@post_type.id, "category", item.id), class: "btn btn-default btn-xs cama_ajax_request", title: "#{cama_pluralize_text(@post_type.decorate.the_title)} - #{item.the_title.to_s.titleize}" if @post_type.present? && (can? :posts, @post_type) %>
42
+ <%= link_to raw('<i class="fa fa-list"></i>'), cama_admin_post_type_taxonomy_posts_path(@post_type.id, "category", item.id), class: "btn btn-default btn-xs cama_ajax_request", title: safe_join([cama_pluralize_text(@post_type.decorate.the_title), ' - ', item.name.to_s.translate(item.get_locale).titleize]) if @post_type.present? && (can? :posts, @post_type) %>
43
43
  <%= link_to raw('<i class="fa fa-pencil"></i>'), {action: :edit, id: item.id }, class: "btn btn-default btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.edit')}" %>
44
44
  <%= link_to raw('<i class="fa fa-times"></i>'), { action: :destroy, id: item.id },method: :delete, data: { confirm: t('camaleon_cms.admin.message.delete') }, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.delete')}" unless item.get_option('not_deleted', false) %>
45
45
  </td>
@@ -13,6 +13,11 @@
13
13
  <%= f.label t('camaleon_cms.admin.post_type.template') %><br>
14
14
  <%= select_tag :theme, options_for_select(PluginRoutes.all_themes.map{|theme| [theme["name"], theme["key"]] }), :class => "form-control required" %>
15
15
  </div>
16
+ <div class="form-group">
17
+ <%= label_tag :setup_token, t('camaleon_cms.admin.installer.setup_token', default: 'Setup token') %><br>
18
+ <%= text_field_tag :setup_token, nil, class: "form-control", autocomplete: "off" %>
19
+ <small style="color: #999;"><%= t('camaleon_cms.admin.installer.setup_token_hint', default: 'Required for remote installations; leave blank when installing locally. Printed in the server log and saved to tmp/camaleon_setup_token on first run.') %></small>
20
+ </div>
16
21
  <div class="form-group">
17
22
  <button class="btn btn-primary pull-right" type="submit"><%= t('camaleon_cms.admin.button.submit') %></button>
18
23
  </div>
@@ -1,8 +1,32 @@
1
1
  <div class="well">
2
- <h3>Your site was created successfully.</h3>
2
+ <h3><%= t('camaleon_cms.admin.installer.welcome_title', default: 'Your site was created successfully.') %></h3>
3
3
  <hr>
4
- <p>Vist Frontend: <%= link_to("Here.", cama_root_url, target: "_blank") %></p>
5
- <p>Visit Admin panel <%= link_to("Here.", cama_admin_login_url, target: "_blank") %></p>
6
- <p>user: admin</p>
7
- <p>pass: admin123</p>
8
- </div>
4
+ <p><%= t('camaleon_cms.admin.installer.visit_frontend', default: 'Visit Frontend:') %> <%= link_to(t('camaleon_cms.admin.installer.here', default: 'Here.'), cama_root_url, target: "_blank") %></p>
5
+ <p><%= t('camaleon_cms.admin.installer.visit_admin', default: 'Visit Admin panel') %> <%= link_to(t('camaleon_cms.admin.installer.here', default: 'Here.'), cama_admin_login_url, target: "_blank") %></p>
6
+ <hr>
7
+ <p><%= t('camaleon_cms.admin.installer.user_label', default: 'User:') %> <strong>admin</strong></p>
8
+ <% if @generated_admin_password.present? %>
9
+ <p>
10
+ <%= t('camaleon_cms.admin.installer.password_label', default: 'Password:') %>
11
+ <strong><span id="cama_generated_password"><%= @generated_admin_password %></span></strong>
12
+ <button type="button" class="btn btn-xs btn-default" onclick="cama_copy_generated_password(this)">
13
+ <%= t('camaleon_cms.admin.installer.copy', default: 'Copy') %>
14
+ </button>
15
+ </p>
16
+ <p style="color: #c0392b;"><%= t('camaleon_cms.admin.installer.must_change_notice', default: 'Save this password now — it is shown only once. You will be asked to change it when you first sign in.') %></p>
17
+ <script>
18
+ function cama_copy_generated_password(btn) {
19
+ var node = document.getElementById('cama_generated_password');
20
+ var done = function () { btn.textContent = '<%= j t('camaleon_cms.admin.installer.copied', default: 'Copied!') %>'; };
21
+ if (navigator.clipboard && navigator.clipboard.writeText) {
22
+ navigator.clipboard.writeText(node.textContent).then(done, function () {});
23
+ } else {
24
+ var range = document.createRange(); range.selectNode(node);
25
+ var sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range);
26
+ try { document.execCommand('copy'); done(); } catch (e) {}
27
+ sel.removeAllRanges();
28
+ }
29
+ }
30
+ </script>
31
+ <% end %>
32
+ </div>
@@ -30,8 +30,8 @@
30
30
  <td><%= t("camaleon_cms.admin.plugins.status_#{status}") %></td>
31
31
  <td>
32
32
  <% msg = "#{status ? t('camaleon_cms.admin.button.disable_plugin') : t('camaleon_cms.admin.button.activate_plugin') }" %>
33
- <%= link_to msg, {action: :toggle, id: plugin["key"], status: status }, class: "btn btn-#{status ? 'danger' : 'primary'} btn-xs", data: { confirm: msg } %>
34
- <%= link_to("<i class='fa fa-arrow-circle-o-up'></i>".html_safe, cama_admin_plugin_upgrade_path(plugin["key"]), class: "btn btn-info btn-xs", title: "#{t("camaleon_cms.admin.plugins.upgrade_to")} #{plugin["version"]}") if status && current_site.get_plugin(plugin["key"]).old_version? %>
33
+ <%= link_to msg, {action: :toggle, id: plugin["key"], status: status }, method: :patch, class: "btn btn-#{status ? 'danger' : 'primary'} btn-xs", data: { confirm: msg } %>
34
+ <%= link_to("<i class='fa fa-arrow-circle-o-up'></i>".html_safe, cama_admin_plugin_upgrade_path(plugin["key"]), method: :post, class: "btn btn-info btn-xs", title: "#{t("camaleon_cms.admin.plugins.upgrade_to")} #{plugin["version"]}") if status && current_site.get_plugin(plugin["key"]).old_version? %>
35
35
  </td>
36
36
  </tr>
37
37
  <% end %>
@@ -3,7 +3,7 @@
3
3
  <div class="col-md-4">
4
4
  <div class="panel panel-default">
5
5
  <div class="panel-heading ui-draggable-handle">
6
- <h4><span class="fa fa-folder"></span> <%= "#{cama_pluralize_text(@post_type.decorate.the_title)} / " if @post_type.present? %> <%= cama_pluralize_text(t('camaleon_cms.admin.post_type.tags').to_s.titleize) %></h4>
6
+ <h4><span class="fa fa-folder"></span> <%= safe_join([cama_pluralize_text(@post_type.decorate.the_title), ' / ']) if @post_type.present? %> <%= cama_pluralize_text(t('camaleon_cms.admin.post_type.tags').to_s.titleize) %></h4>
7
7
  </div>
8
8
  <%= render partial: 'form' %>
9
9
  </div>
@@ -37,7 +37,7 @@
37
37
  <td><%= tag.count %></td>
38
38
  <td>
39
39
  <%= link_to raw('<i class="fa fa-eye"></i>'), tag.the_url, target: '_blank', class: "btn btn-info btn-xs btn_view", title: "#{t('camaleon_cms.common.visit')}" %>
40
- <%= link_to raw('<i class="fa fa-list"></i>'), cama_admin_post_type_taxonomy_posts_path(@post_type.id, "post_tag", tag.id), class: "btn btn-default btn-xs cama_ajax_request btn_posts", title: "#{cama_pluralize_text(@post_type.decorate.the_title)}-#{tag.slug.to_s.titleize}" if @post_type.present? %>
40
+ <%= link_to raw('<i class="fa fa-list"></i>'), cama_admin_post_type_taxonomy_posts_path(@post_type.id, "post_tag", tag.id), class: "btn btn-default btn-xs cama_ajax_request btn_posts", title: safe_join([cama_pluralize_text(@post_type.decorate.the_title), '-', tag.slug.to_s.titleize]) if @post_type.present? %>
41
41
  <%= link_to raw('<i class="fa fa-pencil"></i>'), {action: :edit, id: tag.id }, class: "btn btn-default btn-xs cama_ajax_request btn_edit", title: "#{t('camaleon_cms.admin.button.edit')}" %>
42
42
  <%= link_to raw('<i class="fa fa-times"></i>'), { action: :destroy, id: tag.id }, method: :delete, data: { confirm: t('camaleon_cms.admin.message.delete') }, class: "btn btn-danger btn-xs cama_ajax_request btn_del", title: "#{t('camaleon_cms.admin.button.delete')}" unless tag.get_option('not_deleted', false) %>
43
43
  </td>
@@ -14,7 +14,7 @@
14
14
  </div>
15
15
  <div class="panel-footer">
16
16
  <div class="row">
17
- <div class="col-md-6 "><%= link_to t('camaleon_cms.admin.button.move_trash'), {action: :trash, post_id: @post.id}, class: "text-center text-danger btn-xs btn" unless @post.new_record? %></div>
17
+ <div class="col-md-6 "><%= link_to t('camaleon_cms.admin.button.move_trash'), {action: :trash, post_id: @post.id}, method: :patch, data: {confirm: t('camaleon_cms.admin.message.delete')}, class: "text-center text-danger btn-xs btn" unless @post.new_record? %></div>
18
18
  <div class="col-md-6">
19
19
  <% if @post.draft? %>
20
20
  <a class="btn btn-default btn-xs pull-right" href="#" data-type="pending" onclick="$('#form-post').submit(); return false;" style="<%= 'display: none' unless @post.pending? %>" ><%= t('camaleon_cms.admin.post_type.save_pending')%> </a>
@@ -9,9 +9,10 @@
9
9
  <h4>
10
10
  <span class="fa fa-file-o"></span>
11
11
  <% if !@post.new_record? %>
12
- <%= "#{t('camaleon_cms.admin.page_title.edit')} #{@post_type.the_title}: #{@post_decorate.the_title}" %> <%= raw @post_decorate.the_status.html_safe %>
12
+ <%= safe_join([t('camaleon_cms.admin.page_title.edit'), ' ', @post_type.the_title, ': ', @post_decorate.the_title]) %> <%= raw @post_decorate.the_status.html_safe %>
13
13
  <% end %>
14
- <%= link_to raw("#{t('camaleon_cms.admin.button.view_draft')}"), {action: :edit, id: @post.drafts.pluck('id')}, id: "view_draft", class: "label label-warning label-form", title: "#{t('camaleon_cms.admin.button.view_draft')}", target: '_blank' if @post.drafts.present? %>
14
+ <% cama_own_draft = @post.drafts.where(user_id: cama_current_user.id).first %>
15
+ <%= link_to raw("#{t('camaleon_cms.admin.button.view_draft')}"), {action: :edit, id: cama_own_draft.id}, id: "view_draft", class: "label label-warning label-form", title: "#{t('camaleon_cms.admin.button.view_draft')}", target: '_blank' if cama_own_draft %>
15
16
  <%= t('camaleon_cms.admin.page_title.create') + " " + @post_type.the_title.to_s if @post.new_record? %>
16
17
  </h4>
17
18
 
@@ -60,10 +60,10 @@
60
60
  <td><%= link_to raw(f.the_status), {action: :index, s: f.status}, class: "cama_ajax_request" %> </td>
61
61
  <td><%= f.author.fullname %></td>
62
62
  <% if @post_type.manage_categories? %>
63
- <td><%= post_type_list_taxonomy(f.categories, "success") %></td>
63
+ <td><%= post_type_list_taxonomy(f.categories, "success", @post_type) %></td>
64
64
  <% end %>
65
65
  <% if @post_type.manage_tags? %>
66
- <td><%= post_type_list_taxonomy(f.post_tags, "default") %></td>
66
+ <td><%= post_type_list_taxonomy(f.post_tags, "default", @post_type) %></td>
67
67
  <% end %>
68
68
  <% extra_column = {post: f, post_type: @post_type, content: "", from_body: true}; hooks_run("list_post_extra_columns", extra_column) %>
69
69
  <%= raw extra_column[:content] %>
@@ -88,18 +88,18 @@
88
88
  <% end %>
89
89
 
90
90
  <% if f.trash? %>
91
- <%= link_to raw('<i class="fa fa-check"></i>'), {action: :restore, post_id: f.id, s: params[:s]}, class: "btn btn-success btn-xs", title: "#{t('camaleon_cms.admin.button.restore')}" if can? :update, f %>
91
+ <%= link_to raw('<i class="fa fa-check"></i>'), {action: :restore, post_id: f.id, s: params[:s]}, method: :patch, class: "btn btn-success btn-xs", title: "#{t('camaleon_cms.admin.button.restore')}" if can? :update, f %>
92
92
  <%= link_to raw('<i class="fa fa-times"></i>'), {action: :destroy, id: f.id, s: params[:s]},
93
93
  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_complete', default: 'Delete completely')}" if can? :destroy, f %>
94
94
  <% elsif f.draft? %>
95
95
  <%#= link_to raw('<i class="fa fa-check"></i>'), {action: :edit, id: f.id, recover_draft: 'true' }, class: "btn btn-success btn-xs", title: "#{t('camaleon_cms.admin.button.recover')}" if can? :update, f %>
96
96
  <%= link_to raw('<i class="fa fa-pencil"></i>'), {action: :edit, id: f.id}, class: "btn btn-default btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.edit')}" if can? :update, f %>
97
97
  <%= link_to raw('<i class="fa fa-trash"></i>'), {action: :trash, post_id: f.id, s: params[:s]},
98
- data: {confirm: t('camaleon_cms.admin.message.delete')}, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.move_trash')}" if can? :destroy, f %>
98
+ method: :patch, data: {confirm: t('camaleon_cms.admin.message.delete')}, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.move_trash')}" if can? :destroy, f %>
99
99
  <% else %>
100
100
  <%= link_to raw('<i class="fa fa-pencil"></i>'), {action: :edit, id: f.id}, class: "btn btn-default btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.edit')}" if can? :update, f %>
101
101
  <%= link_to raw('<i class="fa fa-trash"></i>'), {action: :trash, post_id: f.id, s: params[:s]},
102
- data: {confirm: t('camaleon_cms.admin.message.delete')}, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.move_trash')}" if can? :destroy, f %>
102
+ method: :patch, data: {confirm: t('camaleon_cms.admin.message.delete')}, class: "btn btn-danger btn-xs cama_ajax_request", title: "#{t('camaleon_cms.admin.button.move_trash')}" if can? :destroy, f %>
103
103
  <% end %>
104
104
  </td>
105
105
  </tr>
@@ -0,0 +1,39 @@
1
+ <div class="login-body">
2
+ <h3 class="login-box-msg"><%= t('camaleon_cms.admin.login.back_to_parent.title', default: 'Return to your account') %></h3>
3
+ <%= render "camaleon_cms/flash_messages" %>
4
+ <p>
5
+ <%# The parent admin's username is deliberately not shown: whoever holds an abandoned
6
+ impersonation must not learn which admin account to attack (or phish). %>
7
+ <%= t('camaleon_cms.admin.login.back_to_parent.instructions',
8
+ default: 'You are currently working as %{impersonated}. Enter the password for your own account to return to it, or log out completely.',
9
+ impersonated: @impersonated_user.username) %>
10
+ </p>
11
+ <%= form_tag cama_admin_back_to_parent_path, method: "post", role: "form", id: "back_to_parent", class: "form-horizontal cama_form_validate" do %>
12
+ <div class="form-group">
13
+ <div class="col-md-12">
14
+ <%= password_field_tag :password, nil, class: "form-control required", autofocus: true,
15
+ placeholder: t('camaleon_cms.admin.login.password', default: 'Password') %>
16
+ </div>
17
+ </div>
18
+ <div class="form-group">
19
+ <div class="col-md-12">
20
+ <%= raw cama_captcha_tags_if_under_attack("login") %>
21
+ </div>
22
+ </div>
23
+ <% end %>
24
+ <%# Security (audit M6): the full-logout button is its OWN button_to form, a sibling of the
25
+ re-auth form above -- never nested inside it. A nested <form> is dropped by the HTML parser
26
+ (the button would submit the outer form), and with per-form CSRF tokens the two tokens would
27
+ collide, breaking both submits. The "Return to my account" button stays a submit of the
28
+ re-auth form via the HTML5 `form` attribute. %>
29
+ <div class="row">
30
+ <div class="col-xs-6">
31
+ <%= button_to t('camaleon_cms.admin.login.back_to_parent.logout', default: 'Log out completely'),
32
+ cama_admin_logout_path(full: 1), method: :post,
33
+ class: 'btn btn-default btn-block btn-flat' %>
34
+ </div>
35
+ <div class="col-xs-6">
36
+ <button type="submit" form="back_to_parent" class="btn btn-primary btn-block btn-flat"><%= t('camaleon_cms.admin.login.back_to_parent.submit', default: 'Return to my account') %></button>
37
+ </div>
38
+ </div>
39
+ </div>