fivo_cookie_consent 0.2.0

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.
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsCookiesGdpr
4
+ # Main helper module for GDPR cookie consent functionality
5
+ module ApplicationHelper
6
+ # Renders the GDPR cookie consent banner with server-side cookie data
7
+ # @return [String] HTML markup for the cookie consent banner and modal
8
+ def gdpr_cookie_banner
9
+ render partial: 'rails_cookies_gdpr/banner'
10
+ end
11
+
12
+ # Detects server-side cookies that JavaScript cannot access (HttpOnly)
13
+ # @return [Hash] Hash of categorized server-side cookies
14
+ def detect_server_side_cookies
15
+ cookies_data = {
16
+ necessary: []
17
+ }
18
+
19
+ return cookies_data unless respond_to?(:request) && request
20
+
21
+ # Ensure Rails sets a real session cookie for this response.
22
+ # Accessing +session.id+ forces session creation if it does not exist yet.
23
+ session.id
24
+
25
+ # Determine the session cookie key configured for the app
26
+ session_key = Rails.application.config.session_options[:key] || '_session'
27
+ session_keys = [session_key, '_session'].uniq
28
+
29
+ session_keys.each do |key|
30
+ cookies_data[:necessary] << {
31
+ name: key,
32
+ duration: 'Session',
33
+ description: 'Speichert Sitzungsdaten für die Funktionalität der Website',
34
+ server_side: true
35
+ }
36
+ end
37
+
38
+ # Detect CSRF token cookie (only if present)
39
+ csrf_cookie_name = 'csrf_token'
40
+ if request.cookies[csrf_cookie_name]
41
+ cookies_data[:necessary] << {
42
+ name: csrf_cookie_name,
43
+ duration: 'Session',
44
+ description: 'Schutz vor Cross-Site Request Forgery (CSRF) Angriffen',
45
+ server_side: true
46
+ }
47
+ end
48
+
49
+ # Check for other common Rails cookies if they already exist
50
+ rails_cookies = [
51
+ { name: '_rails_session', description: 'Rails Sitzungsdaten' },
52
+ { name: 'authenticity_token', description: 'Rails Authentifizierungstoken' },
53
+ { name: '_session_id', description: 'Sitzungs-Identifikator' }
54
+ ]
55
+
56
+ rails_cookies.each do |cookie_info|
57
+ next unless request.cookies[cookie_info[:name]]
58
+
59
+ cookies_data[:necessary] << {
60
+ name: cookie_info[:name],
61
+ duration: 'Session',
62
+ description: cookie_info[:description],
63
+ server_side: true
64
+ }
65
+ end
66
+
67
+ cookies_data
68
+ end
69
+
70
+ # Returns a JSON string of detected server-side cookies that is safe to embed
71
+ # inside an HTML attribute. Rely on Rails’ automatic HTML escaping instead
72
+ # of marking the string as +html_safe+, otherwise the attribute would break
73
+ # because the quotes inside the JSON would not be escaped, resulting in
74
+ # invalid markup and a subsequent JSON.parse() error in the browser.
75
+ #
76
+ # @return [String] Escaped JSON string
77
+ def server_cookies_json
78
+ detect_server_side_cookies.to_json
79
+ end
80
+
81
+ def gdpr_consent_mode_defaults(functionality: true, security: true)
82
+ analytics_default = functionality ? 'granted' : 'denied'
83
+ functionality_default = functionality ? 'granted' : 'denied'
84
+ security_default = security ? 'granted' : 'denied'
85
+
86
+ script = <<~JS
87
+ window.dataLayer = window.dataLayer || [];
88
+ function gtag(){ dataLayer.push(arguments); }
89
+ gtag('consent', 'default', {
90
+ ad_storage: 'denied',
91
+ analytics_storage: '#{analytics_default}',
92
+ functionality_storage: '#{functionality_default}',
93
+ security_storage: '#{security_default}',
94
+ personalization_storage: 'denied'
95
+ });
96
+ document.addEventListener('gdpr:accept', function(event) {
97
+ var detail = event && event.detail || {};
98
+ var categories = detail.categories || [];
99
+ var analyticsGranted = categories.indexOf('analytics') !== -1;
100
+ var marketingGranted = categories.indexOf('marketing') !== -1;
101
+ var functionalGranted = categories.indexOf('functional') !== -1;
102
+ gtag('consent', 'update', {
103
+ ad_storage: marketingGranted ? 'granted' : 'denied',
104
+ analytics_storage: analyticsGranted ? 'granted' : 'denied',
105
+ functionality_storage: functionalGranted ? 'granted' : 'denied'
106
+ });
107
+ });
108
+ document.addEventListener('gdpr:decline', function() {
109
+ gtag('consent', 'update', {
110
+ ad_storage: 'denied',
111
+ analytics_storage: 'denied',
112
+ functionality_storage: 'denied'
113
+ });
114
+ });
115
+ JS
116
+
117
+ content_tag(:script, script.html_safe)
118
+ end
119
+
120
+ def gdpr_lazy_load_tag(name, **options)
121
+ name_sym = name.to_sym
122
+ config = RailsCookiesGdpr.configuration
123
+ registered = config.respond_to?(:registered_scripts) ? RailsCookiesGdpr.registered_scripts[name_sym] : {}
124
+ merged = (registered || {}).merge(options)
125
+
126
+ category = (merged[:category] || :analytics).to_s
127
+ attrs = { type: 'text/plain', 'data-gdpr-category': category }
128
+
129
+ case name_sym
130
+ when :gtm
131
+ gtm_id = merged[:id] || merged[:gtm_id]
132
+ return '' unless gtm_id
133
+ attrs['data-gdpr-src'] = "https://www.googletagmanager.com/gtm.js?id=#{gtm_id}"
134
+ attrs['data-gdpr-async'] = 'true'
135
+ when :ga4, :gtag
136
+ measurement_id = merged[:id] || merged[:measurement_id]
137
+ return '' unless measurement_id
138
+ attrs['data-gdpr-src'] = "https://www.googletagmanager.com/gtag/js?id=#{measurement_id}"
139
+ attrs['data-gdpr-async'] = 'true'
140
+ else
141
+ src = merged[:src]
142
+ attrs['data-gdpr-src'] = src if src
143
+ attrs['data-gdpr-async'] = 'true' if merged[:async]
144
+ attrs['data-gdpr-defer'] = 'true' if merged[:defer]
145
+ end
146
+
147
+ inline_code = merged[:inline_code]
148
+
149
+ if inline_code
150
+ content_tag(:script, inline_code.html_safe, attrs)
151
+ else
152
+ content_tag(:script, '', attrs)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,27 @@
1
+ <div id="gdpr-cookie-banner" class="gdpr-banner" style="display: none;" data-server-cookies="<%= server_cookies_json %>">
2
+ <div class="gdpr-banner__container">
3
+ <div class="gdpr-banner__content">
4
+ <div class="gdpr-banner__text">
5
+ <h3 class="gdpr-banner__title">Cookie-Einstellungen</h3>
6
+ <p class="gdpr-banner__description">
7
+ Wir verwenden Cookies, um Ihnen die bestmögliche Nutzung unserer Website zu ermöglichen.
8
+ Einige Cookies sind für das Funktionieren der Website erforderlich, während andere uns helfen,
9
+ die Website zu verbessern und Ihnen personalisierte Inhalte anzuzeigen.
10
+ </p>
11
+ </div>
12
+ <div class="gdpr-banner__actions">
13
+ <button type="button" class="gdpr-banner__btn gdpr-banner__btn--secondary" data-action="decline">
14
+ Alles ablehnen
15
+ </button>
16
+ <button type="button" class="gdpr-banner__btn gdpr-banner__btn--link" data-action="settings">
17
+ Einstellungen ändern
18
+ </button>
19
+ <button type="button" class="gdpr-banner__btn gdpr-banner__btn--primary" data-action="accept">
20
+ Alle akzeptieren
21
+ </button>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+
27
+ <%= render partial: "rails_cookies_gdpr/modal" %>
@@ -0,0 +1,123 @@
1
+ <div id="gdpr-cookie-modal" class="gdpr-modal" style="display: none;" role="dialog" aria-labelledby="gdpr-modal-title" aria-modal="true">
2
+ <div class="gdpr-modal__backdrop" data-action="close-modal"></div>
3
+ <div class="gdpr-modal__container">
4
+ <div class="gdpr-modal__header">
5
+ <h2 id="gdpr-modal-title" class="gdpr-modal__title">Cookie-Einstellungen verwalten</h2>
6
+ <button type="button" class="gdpr-modal__close" data-action="close-modal" aria-label="Modal schließen">
7
+ <span aria-hidden="true">&times;</span>
8
+ </button>
9
+ </div>
10
+
11
+ <div class="gdpr-modal__body">
12
+ <p class="gdpr-modal__description">
13
+ Wählen Sie aus, welche Cookies Sie zulassen möchten. Sie können diese Einstellungen jederzeit ändern.
14
+ Weitere Informationen finden Sie in unserer
15
+ <a href="<%= RailsCookiesGdpr.configuration.privacy_url %>" target="_blank" rel="noopener">Datenschutzerklärung</a>.
16
+ </p>
17
+
18
+ <div class="gdpr-modal__categories">
19
+ <!-- Necessary cookies (always enabled) -->
20
+ <div class="gdpr-modal__category" data-category-section="necessary">
21
+ <div class="gdpr-modal__category-header" data-category="necessary">
22
+ <div class="gdpr-modal__category-title-wrapper">
23
+ <span class="gdpr-modal__chevron" aria-expanded="false" role="button" tabindex="0" aria-label="Cookie-Liste anzeigen">▶</span>
24
+ <h3 class="gdpr-modal__category-title">Notwendige Cookies</h3>
25
+ </div>
26
+ <div class="gdpr-modal__toggle gdpr-modal__toggle--disabled">
27
+ <input type="checkbox" id="gdpr-necessary" checked disabled>
28
+ <label for="gdpr-necessary" class="gdpr-modal__toggle-label">
29
+ <span class="gdpr-modal__toggle-slider"></span>
30
+ </label>
31
+ </div>
32
+ </div>
33
+ <p class="gdpr-modal__category-description">
34
+ Diese Cookies sind für das ordnungsgemäße Funktionieren der Website erforderlich und können nicht deaktiviert werden.
35
+ </p>
36
+ <div class="gdpr-modal__empty-state" style="display: none;">
37
+ <p class="gdpr-modal__empty-text">Keine Cookies gesetzt</p>
38
+ </div>
39
+ <div class="gdpr-modal__cookie-list" style="display: none;"></div>
40
+ </div>
41
+
42
+ <!-- Analytics cookies -->
43
+ <div class="gdpr-modal__category" data-category-section="analytics">
44
+ <div class="gdpr-modal__category-header" data-category="analytics">
45
+ <div class="gdpr-modal__category-title-wrapper">
46
+ <span class="gdpr-modal__chevron" aria-expanded="false" role="button" tabindex="0" aria-label="Cookie-Liste anzeigen">▶</span>
47
+ <h3 class="gdpr-modal__category-title"><%= RailsCookiesGdpr.configuration.analytics_label %></h3>
48
+ </div>
49
+ <div class="gdpr-modal__toggle">
50
+ <input type="checkbox" id="gdpr-analytics" data-category="analytics">
51
+ <label for="gdpr-analytics" class="gdpr-modal__toggle-label">
52
+ <span class="gdpr-modal__toggle-slider"></span>
53
+ </label>
54
+ </div>
55
+ </div>
56
+ <p class="gdpr-modal__category-description">
57
+ Diese Cookies helfen uns zu verstehen, wie Besucher mit unserer Website interagieren,
58
+ indem sie Informationen anonym sammeln und melden.
59
+ </p>
60
+ <div class="gdpr-modal__empty-state" style="display: none;">
61
+ <p class="gdpr-modal__empty-text">Keine Cookies gesetzt</p>
62
+ </div>
63
+ <div class="gdpr-modal__cookie-list" style="display: none;"></div>
64
+ </div>
65
+
66
+ <!-- Marketing cookies -->
67
+ <div class="gdpr-modal__category" data-category-section="marketing">
68
+ <div class="gdpr-modal__category-header" data-category="marketing">
69
+ <div class="gdpr-modal__category-title-wrapper">
70
+ <span class="gdpr-modal__chevron" aria-expanded="false" role="button" tabindex="0" aria-label="Cookie-Liste anzeigen">▶</span>
71
+ <h3 class="gdpr-modal__category-title"><%= RailsCookiesGdpr.configuration.marketing_label %></h3>
72
+ </div>
73
+ <div class="gdpr-modal__toggle">
74
+ <input type="checkbox" id="gdpr-marketing" data-category="marketing">
75
+ <label for="gdpr-marketing" class="gdpr-modal__toggle-label">
76
+ <span class="gdpr-modal__toggle-slider"></span>
77
+ </label>
78
+ </div>
79
+ </div>
80
+ <p class="gdpr-modal__category-description">
81
+ Diese Cookies werden verwendet, um Ihnen relevante Werbung und Marketing-Kommunikation zu zeigen.
82
+ </p>
83
+ <div class="gdpr-modal__empty-state" style="display: none;">
84
+ <p class="gdpr-modal__empty-text">Keine Cookies gesetzt</p>
85
+ </div>
86
+ <div class="gdpr-modal__cookie-list" style="display: none;"></div>
87
+ </div>
88
+
89
+ <!-- Functional cookies -->
90
+ <div class="gdpr-modal__category" data-category-section="functional">
91
+ <div class="gdpr-modal__category-header" data-category="functional">
92
+ <div class="gdpr-modal__category-title-wrapper">
93
+ <span class="gdpr-modal__chevron" aria-expanded="false" role="button" tabindex="0" aria-label="Cookie-Liste anzeigen">▶</span>
94
+ <h3 class="gdpr-modal__category-title"><%= RailsCookiesGdpr.configuration.functional_label %></h3>
95
+ </div>
96
+ <div class="gdpr-modal__toggle">
97
+ <input type="checkbox" id="gdpr-functional" data-category="functional">
98
+ <label for="gdpr-functional" class="gdpr-modal__toggle-label">
99
+ <span class="gdpr-modal__toggle-slider"></span>
100
+ </label>
101
+ </div>
102
+ </div>
103
+ <p class="gdpr-modal__category-description">
104
+ Diese Cookies ermöglichen erweiterte Funktionalitäten und Personalisierung der Website.
105
+ </p>
106
+ <div class="gdpr-modal__empty-state" style="display: none;">
107
+ <p class="gdpr-modal__empty-text">Keine Cookies gesetzt</p>
108
+ </div>
109
+ <div class="gdpr-modal__cookie-list" style="display: none;"></div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <div class="gdpr-modal__footer">
115
+ <button type="button" class="gdpr-modal__btn gdpr-modal__btn--secondary" data-action="decline-all">
116
+ Alles ablehnen
117
+ </button>
118
+ <button type="button" class="gdpr-modal__btn gdpr-modal__btn--primary" data-action="save-preferences">
119
+ Einstellungen speichern
120
+ </button>
121
+ </div>
122
+ </div>
123
+ </div>
@@ -0,0 +1,6 @@
1
+ # SQLite. Versions 3.8.0 and up are supported.
2
+ test:
3
+ adapter: sqlite3
4
+ database: db/test.sqlite3
5
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
6
+ timeout: 5000
data/db/test.sqlite3 ADDED
Binary file
Binary file
File without changes
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FivoCookieConsent
4
+ # Configuration class for customizing cookie consent settings
5
+ class Configuration
6
+ attr_accessor :analytics_label, :marketing_label, :functional_label, :privacy_url, :cookie_lifespan,
7
+ :cookie_patterns, :registered_scripts
8
+
9
+ def initialize
10
+ @analytics_label = 'Analyse-Cookies'
11
+ @marketing_label = 'Marketing-Cookies'
12
+ @functional_label = 'Präferenz-Cookies'
13
+ @privacy_url = '/datenschutz'
14
+ @cookie_lifespan = 365 # days
15
+ @cookie_patterns = default_cookie_patterns
16
+ @registered_scripts = default_registered_scripts
17
+ end
18
+
19
+ private
20
+
21
+ # Default cookie patterns for categorization
22
+ def default_cookie_patterns
23
+ {
24
+ necessary: [
25
+ /^_session_/,
26
+ /^session_/,
27
+ /^csrf_token/,
28
+ /^authenticity_token/,
29
+ /^_rails_/,
30
+ /^gdpr_cookie_consent$/
31
+ ],
32
+ analytics: [
33
+ /^_ga/,
34
+ /^_gid/,
35
+ /^_gat/,
36
+ /^_gtag/,
37
+ /^__utm/,
38
+ /^_dc_gtm_/,
39
+ /^_hjid/,
40
+ /^_hjFirstSeen/,
41
+ /^_hj/
42
+ ],
43
+ marketing: [
44
+ /^_fbp/,
45
+ /^_fbc/,
46
+ /^fr$/,
47
+ /^tr$/,
48
+ /^_pinterest_/,
49
+ /^_pin_unauth/,
50
+ /^__Secure-3PAPISID/,
51
+ /^__Secure-3PSID/,
52
+ /^NID$/,
53
+ /^IDE$/
54
+ ],
55
+ functional: [
56
+ /^_locale/,
57
+ /^language/,
58
+ /^theme/,
59
+ /^preferences/,
60
+ /^_user_settings/,
61
+ /^_ui_/
62
+ ]
63
+ }
64
+ end
65
+
66
+ def default_registered_scripts
67
+ FivoCookieConsent.default_registered_scripts
68
+ end
69
+ end
70
+
71
+ class << self
72
+ attr_writer :configuration
73
+
74
+ # Get current configuration instance
75
+ # @return [Configuration] The current configuration
76
+ def configuration
77
+ @configuration ||= Configuration.new
78
+ end
79
+
80
+ # Configure the gem with a block
81
+ # @yield [Configuration] The configuration instance
82
+ # @example
83
+ # FivoCookieConsent.configure do |config|
84
+ # config.analytics_label = "Custom Analytics"
85
+ # end
86
+ def configure
87
+ yield(configuration)
88
+ end
89
+
90
+ def register_script(name, options = {})
91
+ configuration.registered_scripts ||= {}
92
+ configuration.registered_scripts[name.to_sym] = options
93
+ end
94
+
95
+ def registered_scripts
96
+ configuration.registered_scripts || {}
97
+ end
98
+
99
+ def default_registered_scripts
100
+ {
101
+ gtm: { category: :analytics },
102
+ ga4: { category: :analytics },
103
+ gtag: { category: :analytics },
104
+ recaptcha: { category: :functional },
105
+ hotjar: { category: :analytics },
106
+ fb_pixel: { category: :marketing }
107
+ }
108
+ end
109
+ end
110
+ end
111
+
112
+ # Alias for backwards compatibility with the expected namespace
113
+ module RailsCookiesGdpr
114
+ class << self
115
+ # Configure method for RailsCookiesGdpr namespace
116
+ # @yield [FivoCookieConsent::Configuration] The configuration instance
117
+ def configure(&block)
118
+ FivoCookieConsent.configure(&block)
119
+ end
120
+
121
+ # Get configuration from FivoCookieConsent
122
+ # @return [FivoCookieConsent::Configuration] The current configuration
123
+ def configuration
124
+ FivoCookieConsent.configuration
125
+ end
126
+
127
+ # Get cookie patterns for categorization
128
+ # @return [Hash] Cookie patterns hash with categories as keys
129
+ def cookie_patterns
130
+ configuration.cookie_patterns
131
+ end
132
+
133
+ def register_script(name, options = {})
134
+ FivoCookieConsent.register_script(name, options)
135
+ end
136
+
137
+ def registered_scripts
138
+ FivoCookieConsent.registered_scripts
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Fix for Rails 7.0 Logger compatibility issue
4
+ require 'logger'
5
+
6
+ # Ensure Logger is available before ActiveSupport tries to use it
7
+ Logger.class unless defined?(Logger)
8
+ begin
9
+ require 'rails/engine'
10
+ rescue LoadError
11
+ # Rails not available
12
+ return
13
+ end
14
+
15
+ module FivoCookieConsent
16
+ class Engine < ::Rails::Engine
17
+ isolate_namespace RailsCookiesGdpr
18
+
19
+ # Configure paths for autoloading
20
+ config.autoload_paths += %W[
21
+ #{root}/app/controllers
22
+ #{root}/app/helpers
23
+ #{root}/app/models
24
+ ]
25
+
26
+ # Include helpers only after code is loaded (works with Rails 7.0+)
27
+ config.to_prepare do
28
+ ActionController::Base.helper RailsCookiesGdpr::ApplicationHelper if defined?(ActionController::Base)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'rails/railtie'
5
+ rescue LoadError
6
+ return
7
+ end
8
+
9
+ module FivoCookieConsent
10
+ class Railtie < Rails::Railtie
11
+ # Load helpers into ActionController AFTER ActionController is ready
12
+ initializer 'fivo_cookie_consent.load_helpers' do
13
+ ActiveSupport.on_load :action_controller do
14
+ helper RailsCookiesGdpr::ApplicationHelper
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FivoCookieConsent
4
+ VERSION = '0.2.0'
5
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require_relative 'fivo_cookie_consent/version'
6
+ require_relative 'fivo_cookie_consent/configuration'
7
+
8
+ # Only load Rails components when Rails is available
9
+ if defined?(Rails)
10
+ require_relative 'fivo_cookie_consent/railtie'
11
+ require_relative 'fivo_cookie_consent/engine'
12
+ end
13
+
14
+ module FivoCookieConsent
15
+ class Error < StandardError; end
16
+ end