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.
- checksums.yaml +4 -4
- data/README.md +17 -4
- data/Rakefile +3 -16
- data/app/apps/plugins/attack/admin_controller.rb +9 -6
- data/app/apps/plugins/attack/attack_helper.rb +35 -25
- data/app/apps/plugins/authoring_post/authoring_post_helper.rb +23 -16
- data/app/apps/plugins/front_cache/admin_controller.rb +14 -10
- data/app/apps/plugins/front_cache/config/initializer.rb +1 -1
- data/app/apps/plugins/front_cache/config/locales/translation.yml +0 -7
- data/app/apps/plugins/front_cache/front_cache_helper.rb +109 -37
- data/app/apps/plugins/front_cache/views/admin/settings.html.erb +0 -5
- data/app/apps/plugins/visibility_post/config/config.json +1 -0
- data/app/apps/plugins/visibility_post/config/routes_front.txt +1 -0
- data/app/apps/plugins/visibility_post/front_controller.rb +23 -0
- data/app/apps/plugins/visibility_post/visibility_post_helper.rb +141 -70
- data/app/apps/themes/camaleon_first/main_helper.rb +18 -12
- data/app/apps/themes/default/assets/css/style.css.scss +24 -0
- data/app/apps/themes/default/assets/genericons/genericons.css +3 -8
- data/app/apps/themes/default/default_helper.rb +2 -1
- data/app/apps/themes/default/views/layouts/index.html.erb +3 -1
- data/app/apps/themes/new/assets/css/styles.css.scss +24 -0
- data/app/apps/themes/new/custom_helper.rb +5 -9
- data/app/apps/themes/new/views/layouts/_header.html.erb +3 -1
- data/app/assets/javascripts/camaleon_cms/admin/_data.js +16 -0
- data/app/assets/javascripts/camaleon_cms/admin/_post.js +5 -2
- data/app/assets/javascripts/camaleon_cms/admin/nav_menu.js +14 -3
- data/app/assets/javascripts/camaleon_cms/admin/uploader/_media_manager.js +5 -0
- data/app/controllers/camaleon_cms/admin/appearances/nav_menus_controller.rb +8 -6
- data/app/controllers/camaleon_cms/admin/appearances/themes_controller.rb +8 -5
- data/app/controllers/camaleon_cms/admin/appearances/widgets/assign_controller.rb +7 -2
- data/app/controllers/camaleon_cms/admin/appearances/widgets/main_controller.rb +2 -1
- data/app/controllers/camaleon_cms/admin/categories_controller.rb +19 -4
- data/app/controllers/camaleon_cms/admin/comments_controller.rb +0 -1
- data/app/controllers/camaleon_cms/admin/installers_controller.rb +32 -1
- data/app/controllers/camaleon_cms/admin/media_controller.rb +66 -14
- data/app/controllers/camaleon_cms/admin/plugins_controller.rb +1 -3
- data/app/controllers/camaleon_cms/admin/post_tags_controller.rb +10 -4
- data/app/controllers/camaleon_cms/admin/posts/drafts_controller.rb +40 -11
- data/app/controllers/camaleon_cms/admin/posts_controller.rb +55 -22
- data/app/controllers/camaleon_cms/admin/sessions_controller.rb +237 -44
- data/app/controllers/camaleon_cms/admin/settings/custom_fields_controller.rb +80 -13
- data/app/controllers/camaleon_cms/admin/settings/post_types_controller.rb +5 -2
- data/app/controllers/camaleon_cms/admin/settings/sites_controller.rb +26 -3
- data/app/controllers/camaleon_cms/admin/settings_controller.rb +5 -3
- data/app/controllers/camaleon_cms/admin/user_roles_controller.rb +7 -5
- data/app/controllers/camaleon_cms/admin/users_controller.rb +78 -11
- data/app/controllers/camaleon_cms/admin_controller.rb +136 -26
- data/app/controllers/camaleon_cms/camaleon_controller.rb +37 -92
- data/app/controllers/camaleon_cms/frontend_controller.rb +146 -74
- data/app/controllers/concerns/camaleon_cms/admin/custom_fields_concern.rb +20 -5
- data/app/controllers/concerns/camaleon_cms/frontend_concern.rb +46 -17
- data/app/controllers/concerns/camaleon_cms/frontend_visited_state_concern.rb +72 -0
- data/app/controllers/concerns/camaleon_cms/hook_lifecycle_concern.rb +104 -0
- data/app/controllers/concerns/camaleon_cms/request_context_concern.rb +113 -0
- data/app/controllers/concerns/camaleon_cms/runtime_admin_menu_concern.rb +237 -0
- data/app/controllers/concerns/camaleon_cms/runtime_captcha_image_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_html_content_concern.rb +143 -0
- data/app/controllers/concerns/camaleon_cms/runtime_shortcode_theme_concern.rb +246 -0
- data/app/controllers/concerns/camaleon_cms/runtime_state_concern.rb +13 -0
- data/app/controllers/concerns/camaleon_cms/runtime_uploader_concern.rb +54 -0
- data/app/controllers/concerns/camaleon_cms/session_captcha_runtime_concern.rb +19 -0
- data/app/controllers/concerns/camaleon_cms/session_runtime_concern.rb +40 -0
- data/app/decorators/camaleon_cms/application_decorator.rb +11 -8
- data/app/decorators/camaleon_cms/category_decorator.rb +3 -2
- data/app/decorators/camaleon_cms/post_comment_decorator.rb +13 -5
- data/app/decorators/camaleon_cms/post_decorator.rb +68 -50
- data/app/decorators/camaleon_cms/post_type_decorator.rb +1 -1
- data/app/decorators/camaleon_cms/site_decorator.rb +33 -25
- data/app/decorators/camaleon_cms/term_taxonomy_decorator.rb +21 -10
- data/app/decorators/camaleon_cms/theme_decorator.rb +3 -3
- data/app/decorators/camaleon_cms/user_decorator.rb +26 -5
- data/app/helpers/camaleon_cms/admin/application_helper.rb +5 -8
- data/app/helpers/camaleon_cms/admin/breadcrumb_helper.rb +1 -1
- data/app/helpers/camaleon_cms/admin/custom_fields_helper.rb +24 -23
- data/app/helpers/camaleon_cms/admin/menus_helper.rb +249 -139
- data/app/helpers/camaleon_cms/admin/post_type_helper.rb +34 -17
- data/app/helpers/camaleon_cms/camaleon_helper.rb +31 -39
- data/app/helpers/camaleon_cms/captcha_helper.rb +73 -53
- data/app/helpers/camaleon_cms/comment_helper.rb +19 -7
- data/app/helpers/camaleon_cms/content_helper.rb +13 -6
- data/app/helpers/camaleon_cms/email_helper.rb +7 -3
- data/app/helpers/camaleon_cms/frontend/application_helper.rb +6 -1
- data/app/helpers/camaleon_cms/frontend/content_select_helper.rb +34 -20
- data/app/helpers/camaleon_cms/frontend/nav_menu_helper.rb +143 -66
- data/app/helpers/camaleon_cms/frontend/seo_helper.rb +39 -28
- data/app/helpers/camaleon_cms/frontend/site_helper.rb +32 -12
- data/app/helpers/camaleon_cms/hooks_helper.rb +14 -4
- data/app/helpers/camaleon_cms/html_helper.rb +58 -37
- data/app/helpers/camaleon_cms/plugins_helper.rb +23 -24
- data/app/helpers/camaleon_cms/session_helper.rb +188 -44
- data/app/helpers/camaleon_cms/short_code_helper.rb +181 -85
- data/app/helpers/camaleon_cms/site_helper.rb +34 -13
- data/app/helpers/camaleon_cms/theme_helper.rb +35 -19
- data/app/helpers/camaleon_cms/uploader_helper.rb +19 -473
- data/app/helpers/camaleon_cms/user_roles_helper.rb +5 -4
- data/app/mailers/camaleon_cms/html_mailer.rb +4 -4
- data/app/models/camaleon_cms/ability.rb +15 -7
- data/app/models/camaleon_cms/category.rb +6 -6
- data/app/models/camaleon_cms/custom_field.rb +15 -6
- data/app/models/camaleon_cms/custom_field_group.rb +39 -22
- data/app/models/camaleon_cms/custom_fields_relationship.rb +140 -2
- data/app/models/camaleon_cms/media.rb +8 -2
- data/app/models/camaleon_cms/meta.rb +2 -2
- data/app/models/camaleon_cms/nav_menu.rb +5 -3
- data/app/models/camaleon_cms/nav_menu_item.rb +16 -8
- data/app/models/camaleon_cms/plugin.rb +3 -4
- data/app/models/camaleon_cms/post.rb +137 -27
- data/app/models/camaleon_cms/post_comment.rb +10 -6
- data/app/models/camaleon_cms/post_default.rb +42 -12
- data/app/models/camaleon_cms/post_relationship.rb +6 -4
- data/app/models/camaleon_cms/post_tag.rb +4 -5
- data/app/models/camaleon_cms/post_type.rb +21 -13
- data/app/models/camaleon_cms/site.rb +39 -29
- data/app/models/camaleon_cms/term_relationship.rb +10 -7
- data/app/models/camaleon_cms/term_taxonomy.rb +58 -4
- data/app/models/camaleon_cms/theme.rb +2 -4
- data/app/models/camaleon_cms/user.rb +17 -5
- data/app/models/camaleon_cms/user_role.rb +81 -6
- data/app/models/camaleon_cms/widget/assigned.rb +11 -6
- data/app/models/camaleon_cms/widget/main.rb +12 -8
- data/app/models/camaleon_cms/widget/sidebar.rb +3 -8
- data/app/models/camaleon_record.rb +89 -5
- data/app/models/concerns/camaleon_cms/categories_tags_for_posts.rb +36 -42
- data/app/models/concerns/camaleon_cms/common_relationships.rb +18 -5
- data/app/models/concerns/camaleon_cms/content_shortcode_gate.rb +73 -0
- data/app/models/concerns/camaleon_cms/custom_fields_read.rb +111 -54
- data/app/models/concerns/camaleon_cms/metas.rb +31 -6
- data/app/models/concerns/camaleon_cms/normalize_attrs.rb +2 -8
- data/app/models/concerns/camaleon_cms/site_default_settings.rb +120 -16
- data/app/models/concerns/camaleon_cms/user_methods.rb +44 -19
- data/app/models/current_request.rb +8 -1
- data/app/uploaders/camaleon_cms_aws_uploader.rb +31 -10
- data/app/uploaders/camaleon_cms_local_uploader.rb +76 -19
- data/app/uploaders/camaleon_cms_uploader.rb +23 -10
- data/app/validators/camaleon_cms/post_uniq_validator.rb +5 -4
- data/app/validators/camaleon_cms/uniq_validator.rb +1 -1
- data/app/validators/camaleon_cms/user_url_validator.rb +215 -36
- data/app/views/camaleon_cms/admin/categories/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/installers/form.html.erb +5 -0
- data/app/views/camaleon_cms/admin/installers/welcome.html.erb +30 -6
- data/app/views/camaleon_cms/admin/plugins/_plugins_list.html.erb +2 -2
- data/app/views/camaleon_cms/admin/post_tags/index.html.erb +2 -2
- data/app/views/camaleon_cms/admin/posts/_sidebar.html.erb +1 -1
- data/app/views/camaleon_cms/admin/posts/form.html.erb +3 -2
- data/app/views/camaleon_cms/admin/posts/index.html.erb +5 -5
- data/app/views/camaleon_cms/admin/sessions/back_to_parent.html.erb +39 -0
- data/app/views/camaleon_cms/admin/sessions/logout_confirm.html.erb +27 -0
- data/app/views/camaleon_cms/admin/settings/_email_settings.html.erb +4 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_get_items.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render.html.erb +19 -4
- data/app/views/camaleon_cms/admin/settings/custom_fields/_render_category_simple.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/custom_fields/form.html.erb +4 -3
- data/app/views/camaleon_cms/admin/settings/shortcodes.html.erb +4 -4
- data/app/views/camaleon_cms/admin/settings/site.html.erb +1 -1
- data/app/views/camaleon_cms/admin/settings/sites/form.html.erb +1 -1
- data/app/views/camaleon_cms/admin/user_roles/form.html.erb +24 -3
- data/app/views/camaleon_cms/admin/users/form.html.erb +8 -4
- data/app/views/camaleon_cms/admin/users/index.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/partials/_render_custom_field.html.erb +8 -2
- data/app/views/camaleon_cms/default_theme/sitemap.html.erb +1 -1
- data/app/views/camaleon_cms/default_theme/sitemap.xml.builder +1 -1
- data/app/views/layouts/camaleon_cms/admin/_header.html.erb +1 -1
- data/config/initializers/action_view.rb +42 -5
- data/config/initializers/active_record_extension.rb +13 -27
- data/config/initializers/assets.rb +52 -23
- data/config/initializers/custom_initializers.rb +1 -1
- data/config/locales/camaleon_cms/admin/ar.yml +1 -0
- data/config/locales/camaleon_cms/admin/de.yml +1 -0
- data/config/locales/camaleon_cms/admin/en.yml +36 -0
- data/config/locales/camaleon_cms/admin/es.yml +1 -0
- data/config/locales/camaleon_cms/admin/fr.yml +1 -0
- data/config/locales/camaleon_cms/admin/it.yml +1 -0
- data/config/locales/camaleon_cms/admin/nl.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt-BR.yml +1 -0
- data/config/locales/camaleon_cms/admin/pt.yml +1 -0
- data/config/locales/camaleon_cms/admin/ru.yml +1 -0
- data/config/locales/camaleon_cms/admin/uk.yml +1 -0
- data/config/locales/camaleon_cms/admin/zh-CH.yml +1 -0
- data/config/routes/admin.rb +33 -15
- data/config/routes/frontend.rb +21 -15
- data/lib/camaleon_cms/assets_precompile.rb +86 -0
- data/lib/camaleon_cms/captcha_image_generation.rb +65 -0
- data/lib/camaleon_cms/content_security.rb +158 -0
- data/lib/camaleon_cms/engine.rb +45 -21
- data/lib/camaleon_cms/media_security_headers.rb +27 -0
- data/lib/camaleon_cms/setup_token.rb +69 -0
- data/lib/camaleon_cms/shortcode_registry.rb +102 -0
- data/lib/camaleon_cms/svg_content_checker.rb +123 -0
- data/lib/camaleon_cms/task_reporter.rb +14 -0
- data/lib/camaleon_cms/unsafe_markup.rb +207 -0
- data/lib/camaleon_cms/uploader_content_security.rb +248 -0
- data/lib/camaleon_cms/uploader_image_processing.rb +166 -0
- data/lib/camaleon_cms/uploader_path_security.rb +146 -0
- data/lib/camaleon_cms/uploader_pipeline.rb +350 -0
- data/lib/camaleon_cms/uploader_support.rb +86 -0
- data/lib/camaleon_cms/version.rb +1 -1
- data/lib/camaleon_cms.rb +23 -0
- data/lib/ext/hash.rb +28 -2
- data/lib/ext/string.rb +10 -5
- data/lib/ext/translator.rb +10 -2
- data/lib/generators/camaleon_cms/gem_plugin_template/app/controllers/plugins/my_plugin/admin_controller.rb +5 -1
- data/lib/generators/camaleon_cms/install_generator.rb +7 -4
- data/lib/generators/camaleon_cms/install_template/plugin_routes.rb +5 -12
- data/lib/generators/camaleon_cms/theme_generator.rb +5 -10
- data/lib/generators/camaleon_cms/theme_template/views/layouts/index.html.erb +3 -1
- data/lib/plugin_routes.rb +412 -368
- data/lib/tasks/camaleon_cms/camaleon_tasks.rake +2 -1
- data/lib/tasks/cross_site_field_groups.rake +74 -0
- data/lib/tasks/custom_fields_roles.rake +15 -13
- data/lib/tasks/orphaned_comments.rake +42 -0
- data/lib/tasks/private_upload_acls.rake +31 -0
- data/lib/tasks/site_custom_field_groups.rake +36 -0
- data/lib/tasks/unsafe_stored_content.rake +42 -0
- data/lib/tasks/unsafe_stored_uploads.rake +70 -0
- data/lib/tasks/user_field_groups.rake +27 -0
- 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
|
|
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
|
-
|
|
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
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
28
|
-
require '
|
|
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
|
|
51
|
-
def validate(url, allow_localhost: false, allow_local_network: false, enforce_user: true, enforce_sanitizing: true
|
|
52
|
-
|
|
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
|
-
|
|
76
|
+
validate_path_traversal(uri) if reject_path_traversal
|
|
61
77
|
return @errors if @errors.any?
|
|
62
78
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
156
|
-
return if value
|
|
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|
|
|
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|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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> <%=
|
|
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:
|
|
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
|
|
2
|
+
<h3><%= t('camaleon_cms.admin.installer.welcome_title', default: 'Your site was created successfully.') %></h3>
|
|
3
3
|
<hr>
|
|
4
|
-
<p
|
|
5
|
-
<p
|
|
6
|
-
<
|
|
7
|
-
<p
|
|
8
|
-
|
|
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> <%=
|
|
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:
|
|
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
|
-
<%=
|
|
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
|
-
|
|
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>
|