consently 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/MIT-LICENSE +20 -0
- data/README.md +279 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/consently.css +252 -0
- data/app/controllers/consently/application_controller.rb +4 -0
- data/app/controllers/consently/consents_controller.rb +44 -0
- data/app/helpers/consently/application_helper.rb +4 -0
- data/app/helpers/consently/tags_helper.rb +176 -0
- data/app/javascript/consently/banner_controller.js +190 -0
- data/app/models/consently/application_record.rb +5 -0
- data/app/models/consently/consent_record.rb +18 -0
- data/app/views/consently/_banner.html.erb +93 -0
- data/app/views/consently/_policy.html.erb +85 -0
- data/config/importmap.rb +3 -0
- data/config/locales/cs.yml +33 -0
- data/config/locales/de.yml +33 -0
- data/config/locales/en.yml +33 -0
- data/config/locales/es.yml +33 -0
- data/config/locales/fr.yml +33 -0
- data/config/locales/hu.yml +33 -0
- data/config/locales/it.yml +33 -0
- data/config/locales/nl.yml +33 -0
- data/config/locales/pl.yml +33 -0
- data/config/locales/sk.yml +33 -0
- data/config/routes.rb +5 -0
- data/lib/consently/configuration.rb +150 -0
- data/lib/consently/consent.rb +62 -0
- data/lib/consently/cookie.rb +12 -0
- data/lib/consently/engine.rb +33 -0
- data/lib/consently/providers/base.rb +95 -0
- data/lib/consently/providers/clarity.rb +34 -0
- data/lib/consently/providers/custom.rb +34 -0
- data/lib/consently/providers/google_ads.rb +32 -0
- data/lib/consently/providers/google_analytics.rb +45 -0
- data/lib/consently/providers/google_tag_manager.rb +34 -0
- data/lib/consently/providers/hotjar.rb +39 -0
- data/lib/consently/providers/meta_pixel.rb +37 -0
- data/lib/consently/providers/plausible.rb +30 -0
- data/lib/consently/providers.rb +9 -0
- data/lib/consently/script.rb +17 -0
- data/lib/consently/version.rb +3 -0
- data/lib/consently.rb +74 -0
- data/lib/generators/consently/consent_log/consent_log_generator.rb +28 -0
- data/lib/generators/consently/consent_log/templates/create_consently_consent_records.rb +19 -0
- data/lib/generators/consently/install/install_generator.rb +44 -0
- data/lib/generators/consently/install/templates/consently.rb +63 -0
- data/lib/generators/consently/views/views_generator.rb +15 -0
- data/lib/tasks/consently_tasks.rake +4 -0
- metadata +121 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
en:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "We use cookies to run this site, and - with your consent - to understand how it is used and to show you relevant offers."
|
|
5
|
+
policy_link: "Read our cookie policy."
|
|
6
|
+
accept_all: "Accept all"
|
|
7
|
+
reject_all: "Reject all"
|
|
8
|
+
preferences: "Settings"
|
|
9
|
+
save: "Save choices"
|
|
10
|
+
cancel: "Cancel"
|
|
11
|
+
preferences_link: "Cookie settings"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "you agreed"
|
|
14
|
+
denied: "not agreed"
|
|
15
|
+
no_tags: "Nothing in this category."
|
|
16
|
+
no_cookies: "Sets no cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Kept for"
|
|
19
|
+
duration_days: "%{count} days"
|
|
20
|
+
duration_session: "until you close the browser"
|
|
21
|
+
own_heading: "The consent cookie itself"
|
|
22
|
+
own_body: "Your choice is kept in one cookie in your browser: the categories you agreed to, the version of this policy and the time. Nothing about you."
|
|
23
|
+
own_version: "Policy version"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Necessary"
|
|
27
|
+
description: "Needed for the site to work. They cannot be switched off."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Analytics"
|
|
30
|
+
description: "Help us see how the site is used, so we can improve it."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Let us show you offers that fit what you are interested in."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
es:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Usamos cookies para que este sitio funcione y, con tu consentimiento, para entender cómo se usa y mostrarte ofertas relevantes."
|
|
5
|
+
policy_link: "Lee nuestra política de cookies."
|
|
6
|
+
accept_all: "Aceptar todo"
|
|
7
|
+
reject_all: "Rechazar todo"
|
|
8
|
+
preferences: "Ajustes"
|
|
9
|
+
save: "Guardar selección"
|
|
10
|
+
cancel: "Cancelar"
|
|
11
|
+
preferences_link: "Ajustes de cookies"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "consentido"
|
|
14
|
+
denied: "sin consentimiento"
|
|
15
|
+
no_tags: "Nada en esta categoría."
|
|
16
|
+
no_cookies: "No instala cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Se conserva"
|
|
19
|
+
duration_days: "%{count} días"
|
|
20
|
+
duration_session: "hasta que cierres el navegador"
|
|
21
|
+
own_heading: "La propia cookie de consentimiento"
|
|
22
|
+
own_body: "Tu elección se guarda en una cookie de tu navegador: las categorías que aceptaste, la versión de esta política y la hora. Nada sobre ti."
|
|
23
|
+
own_version: "Versión de la política"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Necesarias"
|
|
27
|
+
description: "Imprescindibles para que el sitio funcione. No se pueden desactivar."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Analíticas"
|
|
30
|
+
description: "Nos muestran cómo se usa el sitio para poder mejorarlo."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Nos permiten mostrarte ofertas acordes con tus intereses."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
fr:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Nous utilisons des cookies pour faire fonctionner ce site et, avec votre accord, pour comprendre comment il est utilisé et vous proposer des offres pertinentes."
|
|
5
|
+
policy_link: "Consulter notre politique de cookies."
|
|
6
|
+
accept_all: "Tout accepter"
|
|
7
|
+
reject_all: "Tout refuser"
|
|
8
|
+
preferences: "Paramètres"
|
|
9
|
+
save: "Enregistrer mes choix"
|
|
10
|
+
cancel: "Annuler"
|
|
11
|
+
preferences_link: "Paramètres des cookies"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "accepté"
|
|
14
|
+
denied: "non accepté"
|
|
15
|
+
no_tags: "Rien dans cette catégorie."
|
|
16
|
+
no_cookies: "Ne dépose aucun cookie."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Conservé"
|
|
19
|
+
duration_days: "%{count} jours"
|
|
20
|
+
duration_session: "jusqu'à la fermeture du navigateur"
|
|
21
|
+
own_heading: "Le cookie de consentement lui-même"
|
|
22
|
+
own_body: "Votre choix tient dans un cookie de votre navigateur : les catégories acceptées, la version de cette politique et l'heure. Rien sur vous."
|
|
23
|
+
own_version: "Version de la politique"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Nécessaires"
|
|
27
|
+
description: "Indispensables au fonctionnement du site. Ils ne peuvent pas être désactivés."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Mesure d'audience"
|
|
30
|
+
description: "Nous montrent comment le site est utilisé, afin de l'améliorer."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Nous permettent de vous proposer des offres correspondant à vos centres d'intérêt."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
hu:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Sütiket használunk, hogy az oldal működjön, és - a hozzájárulásoddal - hogy megértsük, hogyan használod, illetve releváns ajánlatokat mutassunk."
|
|
5
|
+
policy_link: "Olvasd el a süti szabályzatunkat."
|
|
6
|
+
accept_all: "Összes elfogadása"
|
|
7
|
+
reject_all: "Összes elutasítása"
|
|
8
|
+
preferences: "Beállítások"
|
|
9
|
+
save: "Választás mentése"
|
|
10
|
+
cancel: "Mégse"
|
|
11
|
+
preferences_link: "Süti beállítások"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "hozzájárultál"
|
|
14
|
+
denied: "nincs hozzájárulás"
|
|
15
|
+
no_tags: "Ebben a kategóriában nincs semmi."
|
|
16
|
+
no_cookies: "Nem helyez el sütit."
|
|
17
|
+
cookie_name: "Süti"
|
|
18
|
+
cookie_duration: "Megőrzés"
|
|
19
|
+
duration_days: "%{count} nap"
|
|
20
|
+
duration_session: "a böngésző bezárásáig"
|
|
21
|
+
own_heading: "Maga a hozzájárulási süti"
|
|
22
|
+
own_body: "A választásod egyetlen sütiben van a böngésződben: az elfogadott kategóriák, e szabályzat verziója és az idő. Rólad semmi."
|
|
23
|
+
own_version: "Szabályzat verziója"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Szükséges"
|
|
27
|
+
description: "Az oldal működéséhez kellenek. Nem kapcsolhatók ki."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Statisztikai"
|
|
30
|
+
description: "Megmutatják, hogyan használod az oldalt, hogy fejleszthessük."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Segítenek olyan ajánlatokat mutatni, amelyek érdekelhetnek."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
it:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Usiamo i cookie per far funzionare questo sito e, con il tuo consenso, per capire come viene usato e mostrarti offerte pertinenti."
|
|
5
|
+
policy_link: "Leggi la nostra cookie policy."
|
|
6
|
+
accept_all: "Accetta tutto"
|
|
7
|
+
reject_all: "Rifiuta tutto"
|
|
8
|
+
preferences: "Impostazioni"
|
|
9
|
+
save: "Salva le scelte"
|
|
10
|
+
cancel: "Annulla"
|
|
11
|
+
preferences_link: "Impostazioni cookie"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "consenso dato"
|
|
14
|
+
denied: "nessun consenso"
|
|
15
|
+
no_tags: "Niente in questa categoria."
|
|
16
|
+
no_cookies: "Non imposta cookie."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Conservato"
|
|
19
|
+
duration_days: "%{count} giorni"
|
|
20
|
+
duration_session: "fino alla chiusura del browser"
|
|
21
|
+
own_heading: "Il cookie di consenso"
|
|
22
|
+
own_body: "La tua scelta sta in un cookie nel tuo browser: le categorie accettate, la versione di questa policy e l'ora. Nulla su di te."
|
|
23
|
+
own_version: "Versione della policy"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Necessari"
|
|
27
|
+
description: "Servono al funzionamento del sito. Non possono essere disattivati."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Statistici"
|
|
30
|
+
description: "Ci mostrano come viene usato il sito, così possiamo migliorarlo."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Ci permettono di mostrarti offerte in linea con i tuoi interessi."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
nl:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "We gebruiken cookies om deze site te laten werken en, met jouw toestemming, om te begrijpen hoe de site gebruikt wordt en je relevante aanbiedingen te tonen."
|
|
5
|
+
policy_link: "Lees ons cookiebeleid."
|
|
6
|
+
accept_all: "Alles accepteren"
|
|
7
|
+
reject_all: "Alles weigeren"
|
|
8
|
+
preferences: "Instellingen"
|
|
9
|
+
save: "Keuze opslaan"
|
|
10
|
+
cancel: "Annuleren"
|
|
11
|
+
preferences_link: "Cookie-instellingen"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "toegestaan"
|
|
14
|
+
denied: "niet toegestaan"
|
|
15
|
+
no_tags: "Niets in deze categorie."
|
|
16
|
+
no_cookies: "Plaatst geen cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Bewaard"
|
|
19
|
+
duration_days: "%{count} dagen"
|
|
20
|
+
duration_session: "tot je de browser sluit"
|
|
21
|
+
own_heading: "De toestemmingscookie zelf"
|
|
22
|
+
own_body: "Je keuze staat in één cookie in je browser: de categorieën die je toestond, de versie van dit beleid en het tijdstip. Niets over jou."
|
|
23
|
+
own_version: "Versie van het beleid"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Noodzakelijk"
|
|
27
|
+
description: "Nodig om de site te laten werken. Deze kun je niet uitzetten."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Statistieken"
|
|
30
|
+
description: "Laten ons zien hoe de site gebruikt wordt, zodat we hem kunnen verbeteren."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketing"
|
|
33
|
+
description: "Hiermee kunnen we je aanbiedingen tonen die bij je interesses passen."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
pl:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Używamy plików cookie, żeby ta strona działała, a za Twoją zgodą także po to, by rozumieć, jak z niej korzystasz, i pokazywać Ci trafne oferty."
|
|
5
|
+
policy_link: "Przeczytaj naszą politykę cookies."
|
|
6
|
+
accept_all: "Akceptuję wszystkie"
|
|
7
|
+
reject_all: "Odrzuć wszystkie"
|
|
8
|
+
preferences: "Ustawienia"
|
|
9
|
+
save: "Zapisz wybór"
|
|
10
|
+
cancel: "Anuluj"
|
|
11
|
+
preferences_link: "Ustawienia cookies"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "zgoda udzielona"
|
|
14
|
+
denied: "brak zgody"
|
|
15
|
+
no_tags: "Nic w tej kategorii."
|
|
16
|
+
no_cookies: "Nie ustawia ciasteczek."
|
|
17
|
+
cookie_name: "Ciasteczko"
|
|
18
|
+
cookie_duration: "Przechowywane"
|
|
19
|
+
duration_days: "%{count} dni"
|
|
20
|
+
duration_session: "do zamknięcia przeglądarki"
|
|
21
|
+
own_heading: "Samo ciasteczko zgody"
|
|
22
|
+
own_body: "Twój wybór trzymamy w jednym ciasteczku w Twojej przeglądarce: kategorie, na które się zgodziłeś, wersja tej polityki i czas. Nic o Tobie."
|
|
23
|
+
own_version: "Wersja polityki"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Niezbędne"
|
|
27
|
+
description: "Potrzebne do działania strony. Nie można ich wyłączyć."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Analityczne"
|
|
30
|
+
description: "Pokazują nam, jak korzystasz ze strony, dzięki czemu możemy ją ulepszać."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketingowe"
|
|
33
|
+
description: "Pozwalają pokazywać Ci oferty dopasowane do Twoich zainteresowań."
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
sk:
|
|
2
|
+
consently:
|
|
3
|
+
banner:
|
|
4
|
+
message: "Používame súbory cookie, aby táto stránka fungovala, a s vaším súhlasom aj na to, aby sme rozumeli, ako ju používate, a mohli vám ukazovať relevantné ponuky."
|
|
5
|
+
policy_link: "Prečítajte si naše zásady cookies."
|
|
6
|
+
accept_all: "Prijať všetko"
|
|
7
|
+
reject_all: "Odmietnuť všetko"
|
|
8
|
+
preferences: "Nastavenia"
|
|
9
|
+
save: "Uložiť voľbu"
|
|
10
|
+
cancel: "Zrušiť"
|
|
11
|
+
preferences_link: "Nastavenia cookies"
|
|
12
|
+
policy:
|
|
13
|
+
granted: "súhlas udelený"
|
|
14
|
+
denied: "bez súhlasu"
|
|
15
|
+
no_tags: "V tejto kategórii nič nie je."
|
|
16
|
+
no_cookies: "Nenastavuje žiadne cookies."
|
|
17
|
+
cookie_name: "Cookie"
|
|
18
|
+
cookie_duration: "Doba uloženia"
|
|
19
|
+
duration_days: "%{count} dní"
|
|
20
|
+
duration_session: "do zatvorenia prehliadača"
|
|
21
|
+
own_heading: "Samotná cookie so súhlasom"
|
|
22
|
+
own_body: "Vaša voľba je uložená v jednej cookie vo vašom prehliadači: kategórie, s ktorými ste súhlasili, verzia týchto zásad a čas. Nič o vás."
|
|
23
|
+
own_version: "Verzia zásad"
|
|
24
|
+
categories:
|
|
25
|
+
necessary:
|
|
26
|
+
name: "Nevyhnutné"
|
|
27
|
+
description: "Potrebné na fungovanie stránky. Nedajú sa vypnúť."
|
|
28
|
+
analytics:
|
|
29
|
+
name: "Analytické"
|
|
30
|
+
description: "Ukazujú nám, ako stránku používate, aby sme ju mohli zlepšovať."
|
|
31
|
+
marketing:
|
|
32
|
+
name: "Marketingové"
|
|
33
|
+
description: "Umožňujú nám ukazovať vám ponuky, ktoré zodpovedajú vašim záujmom."
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# Everything the host application sets in config/initializers/consently.rb.
|
|
3
|
+
class Configuration
|
|
4
|
+
DEFAULT_CATEGORIES = %i[necessary analytics marketing].freeze
|
|
5
|
+
|
|
6
|
+
# Where the visitor's choice is kept. It is read by JavaScript, so it is a
|
|
7
|
+
# plain cookie rather than a signed one.
|
|
8
|
+
attr_accessor :cookie_name, :cookie_max_age, :cookie_path
|
|
9
|
+
|
|
10
|
+
# Bump this whenever the policy changes: an older consent stops counting
|
|
11
|
+
# and the banner asks again.
|
|
12
|
+
attr_accessor :consent_version
|
|
13
|
+
|
|
14
|
+
# true, false, or a callable taking the request - e.g.
|
|
15
|
+
# c.enabled = ->(request) { Rails.env.production? }
|
|
16
|
+
attr_accessor :enabled
|
|
17
|
+
|
|
18
|
+
# Emits Google's consent mode v2 defaults (everything denied) before any
|
|
19
|
+
# Google tag, and updates them when the visitor chooses. Leave it on if
|
|
20
|
+
# you use any Google product.
|
|
21
|
+
attr_accessor :google_consent_mode
|
|
22
|
+
|
|
23
|
+
# Store a row per decision, as proof of consent. Needs the engine mounted
|
|
24
|
+
# and the migration from `rails g consently:consent_log`.
|
|
25
|
+
attr_accessor :log_consents
|
|
26
|
+
|
|
27
|
+
# Whether consently_tags links the banner's stylesheet. Turn it off if
|
|
28
|
+
# you have taken the views over and styled them yourself.
|
|
29
|
+
attr_accessor :stylesheet
|
|
30
|
+
|
|
31
|
+
# Honour the browser's Do Not Track header as a rejection. Off by default:
|
|
32
|
+
# DNT is advisory and widely ignored, so treating it as a legal signal is
|
|
33
|
+
# your call, not the gem's.
|
|
34
|
+
attr_accessor :respect_do_not_track
|
|
35
|
+
|
|
36
|
+
# Global Privacy Control, which - unlike DNT - is a binding opt-out signal
|
|
37
|
+
# in California and Colorado. On by default, and a visitor who sends it is
|
|
38
|
+
# never shown the banner: their answer already arrived.
|
|
39
|
+
attr_accessor :respect_global_privacy_control
|
|
40
|
+
|
|
41
|
+
# Whether this visitor has to be asked at all. false means no banner and
|
|
42
|
+
# everything runs - the answer for traffic outside the EU when your legal
|
|
43
|
+
# advice says so:
|
|
44
|
+
#
|
|
45
|
+
# c.consent_required = ->(request) { EU_COUNTRIES.include?(request.headers["CF-IPCountry"]) }
|
|
46
|
+
#
|
|
47
|
+
# Careful: this switches tags on without asking, so it is opt-in.
|
|
48
|
+
attr_accessor :consent_required
|
|
49
|
+
|
|
50
|
+
# Reload the page once a choice is made. Off by default - releasing the
|
|
51
|
+
# blocked tags in place is the whole point, and a reload throws away
|
|
52
|
+
# whatever the visitor was doing. Turn it on when the page itself renders
|
|
53
|
+
# differently depending on consent (an embedded map, a video, a status
|
|
54
|
+
# list) and you would rather let the server decide again.
|
|
55
|
+
attr_accessor :reload_after_choice
|
|
56
|
+
|
|
57
|
+
# Who made the decision, for the consent log. A callable taking the
|
|
58
|
+
# request; return anything that identifies the visitor in your own system
|
|
59
|
+
# (a global id, "User#42", an account number). Left nil the log stays
|
|
60
|
+
# anonymous, which is the right default for a public site.
|
|
61
|
+
attr_accessor :consent_subject
|
|
62
|
+
|
|
63
|
+
# Where the cookie policy lives. A string, or a callable taking the view
|
|
64
|
+
# context - handy when the URL is locale dependent.
|
|
65
|
+
attr_accessor :policy_url
|
|
66
|
+
|
|
67
|
+
# Which scope a request belongs to, e.g. ->(request) { request.host }.
|
|
68
|
+
# Nil means every request sees the default configuration.
|
|
69
|
+
attr_accessor :scope_resolver
|
|
70
|
+
|
|
71
|
+
attr_reader :categories
|
|
72
|
+
|
|
73
|
+
def initialize
|
|
74
|
+
@cookie_name = "consently"
|
|
75
|
+
@cookie_max_age = 60 * 60 * 24 * 180 # six months, the usual guidance
|
|
76
|
+
@cookie_path = "/"
|
|
77
|
+
@consent_version = 1
|
|
78
|
+
@enabled = true
|
|
79
|
+
@google_consent_mode = true
|
|
80
|
+
@stylesheet = true
|
|
81
|
+
@log_consents = false
|
|
82
|
+
@respect_do_not_track = false
|
|
83
|
+
@respect_global_privacy_control = true
|
|
84
|
+
@consent_required = true
|
|
85
|
+
@reload_after_choice = false
|
|
86
|
+
@consent_subject = nil
|
|
87
|
+
@policy_url = nil
|
|
88
|
+
@scope_resolver = nil
|
|
89
|
+
@categories = DEFAULT_CATEGORIES.dup
|
|
90
|
+
@scopes = {}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# A category of your own, shown in the preferences panel alongside the
|
|
94
|
+
# built-in ones. Give it a name in your locale file.
|
|
95
|
+
def category(name)
|
|
96
|
+
name = name.to_sym
|
|
97
|
+
@categories << name unless @categories.include?(name)
|
|
98
|
+
name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def optional_categories
|
|
102
|
+
categories - [ Consent::NECESSARY ]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# c.tag :google_analytics, id: "G-XXXX"
|
|
106
|
+
def tag(key, **options)
|
|
107
|
+
default_scope.tag(key, **options)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Tags for one shop, host or whatever your scope_resolver returns. Falls
|
|
111
|
+
# back to the tags declared outside any scope, and may override them by
|
|
112
|
+
# declaring the same provider again.
|
|
113
|
+
#
|
|
114
|
+
# c.scope "trixbrix" do |s|
|
|
115
|
+
# s.tag :google_analytics, id: "G-TRIX"
|
|
116
|
+
# end
|
|
117
|
+
def scope(name)
|
|
118
|
+
scope = (@scopes[name.to_s] ||= Scope.new)
|
|
119
|
+
yield scope if block_given?
|
|
120
|
+
scope
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def tags_for(scope_name = nil)
|
|
124
|
+
tags = default_scope.tags.dup
|
|
125
|
+
tags.merge!(@scopes[scope_name.to_s].tags) if scope_name && @scopes.key?(scope_name.to_s)
|
|
126
|
+
tags.values
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def default_scope
|
|
130
|
+
@default_scope ||= Scope.new
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# A named bundle of tags. Same API as the top-level `tag`, which is what
|
|
134
|
+
# makes the block form read the way it does.
|
|
135
|
+
class Scope
|
|
136
|
+
def initialize
|
|
137
|
+
@tags = {}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def tag(key, **options)
|
|
141
|
+
provider = Providers::Base.build(key, **options)
|
|
142
|
+
@tags[provider.key] = provider
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def tags
|
|
146
|
+
@tags
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# What the visitor has agreed to, as read from the cookie.
|
|
3
|
+
#
|
|
4
|
+
# The cookie is deliberately tiny and readable by JavaScript - the banner
|
|
5
|
+
# writes it in the browser and the server only reads it, so a page rendered
|
|
6
|
+
# after the choice can emit the real tags instead of blocked ones.
|
|
7
|
+
#
|
|
8
|
+
# {"v":1,"c":["analytics"],"t":"2026-08-12T10:00:00Z"}
|
|
9
|
+
class Consent
|
|
10
|
+
NECESSARY = :necessary
|
|
11
|
+
|
|
12
|
+
attr_reader :categories, :version, :recorded_at
|
|
13
|
+
|
|
14
|
+
def self.none
|
|
15
|
+
new(given: false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Never raises: a cookie can be truncated, hand-edited or left over from an
|
|
19
|
+
# older format, and none of that should take a page down.
|
|
20
|
+
def self.from_cookie(raw, version:)
|
|
21
|
+
return none if raw.blank?
|
|
22
|
+
|
|
23
|
+
data = begin
|
|
24
|
+
JSON.parse(raw.to_s)
|
|
25
|
+
rescue JSON::ParserError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
return none unless data.is_a?(Hash)
|
|
29
|
+
|
|
30
|
+
# A consent given against an older policy version counts as no consent:
|
|
31
|
+
# the banner asks again and nothing runs in the meantime.
|
|
32
|
+
return none unless data["v"].to_s == version.to_s
|
|
33
|
+
|
|
34
|
+
new(
|
|
35
|
+
categories: Array(data["c"]).map { |category| category.to_s.to_sym },
|
|
36
|
+
version: data["v"],
|
|
37
|
+
recorded_at: data["t"]
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(categories: [], version: nil, recorded_at: nil, given: true)
|
|
42
|
+
@categories = Array(categories).map(&:to_sym).freeze
|
|
43
|
+
@version = version
|
|
44
|
+
@recorded_at = recorded_at
|
|
45
|
+
@given = given
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def given?
|
|
49
|
+
@given
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Necessary tags are the ones the site cannot work without, so they never
|
|
53
|
+
# wait for a click.
|
|
54
|
+
def granted?(category)
|
|
55
|
+
category.to_sym == NECESSARY || categories.include?(category.to_sym)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
{ "v" => version, "c" => categories.map(&:to_s), "t" => recorded_at }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
# One cookie a tag is known to set. A cookie policy has to name them and say
|
|
3
|
+
# how long they last, which is exactly the part everyone copies from a
|
|
4
|
+
# competitor's site and gets wrong.
|
|
5
|
+
#
|
|
6
|
+
# `days` is nil for a session cookie.
|
|
7
|
+
Cookie = Struct.new(:name, :days, :provider, keyword_init: true) do
|
|
8
|
+
def session?
|
|
9
|
+
days.nil?
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
class Engine < ::Rails::Engine
|
|
3
|
+
isolate_namespace Consently
|
|
4
|
+
|
|
5
|
+
initializer "consently.helpers" do
|
|
6
|
+
ActiveSupport.on_load(:action_view) do
|
|
7
|
+
include Consently::TagsHelper
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# The engine's app/assets comes along on its own; app/javascript does not,
|
|
12
|
+
# and Sprockets needs the two entries named before it will compile them.
|
|
13
|
+
initializer "consently.assets" do |app|
|
|
14
|
+
if app.config.respond_to?(:assets)
|
|
15
|
+
app.config.assets.paths << root.join("app/javascript")
|
|
16
|
+
if app.config.assets.respond_to?(:precompile)
|
|
17
|
+
app.config.assets.precompile += %w[consently.css consently/banner_controller.js]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The banner controller gets pinned for the host application, so
|
|
23
|
+
# `import "consently/banner_controller"` resolves without anyone editing
|
|
24
|
+
# config/importmap.rb. Our paths have to be in place before importmap-rails
|
|
25
|
+
# draws them, hence `before:`.
|
|
26
|
+
initializer "consently.importmap", before: "importmap" do |app|
|
|
27
|
+
if app.config.respond_to?(:importmap)
|
|
28
|
+
app.config.importmap.paths << root.join("config/importmap.rb")
|
|
29
|
+
app.config.importmap.cache_sweepers << root.join("app/javascript")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
# A tag vendor: what scripts it needs and which consent category it falls
|
|
4
|
+
# under. Subclasses describe the snippet as data (see Consently::Script)
|
|
5
|
+
# so the view can render it live or blocked from the same description.
|
|
6
|
+
class Base
|
|
7
|
+
# Ids are interpolated straight into JavaScript, so they are held to a
|
|
8
|
+
# shape that cannot carry a quote or a bracket out of the string.
|
|
9
|
+
SAFE_OPTION = /\A[\w\-.:\/]+\z/
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def registry
|
|
13
|
+
@@registry ||= {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def register(key, klass)
|
|
17
|
+
registry[key.to_sym] = klass
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def build(key, **options)
|
|
21
|
+
klass = registry[key.to_sym] or raise UnknownProvider, key
|
|
22
|
+
klass.new(**options)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Set by each subclass; where a vendor is unambiguous (Plausible does
|
|
26
|
+
# not touch cookies) it can say :necessary and load right away.
|
|
27
|
+
attr_accessor :default_category
|
|
28
|
+
|
|
29
|
+
def provider_key
|
|
30
|
+
@provider_key
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def provider_key=(key)
|
|
34
|
+
@provider_key = key.to_sym
|
|
35
|
+
register(@provider_key, self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# What this vendor stores in the visitor's browser, for the policy
|
|
39
|
+
# page. `days: nil` means a session cookie.
|
|
40
|
+
def cookie(name, days: nil)
|
|
41
|
+
cookie_manifest << { name: name, days: days }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cookie_manifest
|
|
45
|
+
@cookie_manifest ||= []
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :options, :category, :key
|
|
50
|
+
|
|
51
|
+
def initialize(**options)
|
|
52
|
+
@options = options
|
|
53
|
+
@category = (options[:category] || self.class.default_category).to_sym
|
|
54
|
+
@key = (options[:as] || self.class.provider_key).to_sym
|
|
55
|
+
validate!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Array<Consently::Script>
|
|
59
|
+
def scripts
|
|
60
|
+
[]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Array<Consently::Cookie> - what this tag leaves in the browser.
|
|
64
|
+
def cookies
|
|
65
|
+
self.class.cookie_manifest.map { |attributes| Cookie.new(**attributes, provider: key) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Rendered inside <body>, for vendors that still ship a <noscript>
|
|
69
|
+
# fallback (Google Tag Manager, Meta). HTML string or nil.
|
|
70
|
+
def noscript
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def to_s
|
|
75
|
+
"#{key} (#{category})"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def validate!
|
|
81
|
+
# Subclasses call require_option for what they cannot work without.
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def require_option(name)
|
|
85
|
+
value = options[name]
|
|
86
|
+
raise MissingOption, "Consently: #{self.class.provider_key} needs a #{name}:" if value.blank?
|
|
87
|
+
unless value.to_s.match?(SAFE_OPTION)
|
|
88
|
+
raise MissingOption, "Consently: #{name}: #{value.inspect} for #{self.class.provider_key} contains characters that are not allowed in a tag id"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
value.to_s
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Consently
|
|
2
|
+
module Providers
|
|
3
|
+
# Microsoft Clarity: session recordings and heatmaps.
|
|
4
|
+
class Clarity < Base
|
|
5
|
+
self.provider_key = :clarity
|
|
6
|
+
self.default_category = :analytics
|
|
7
|
+
|
|
8
|
+
cookie "_clck", days: 365
|
|
9
|
+
cookie "_clsk", days: 1
|
|
10
|
+
cookie "CLID", days: 365
|
|
11
|
+
cookie "MUID", days: 390
|
|
12
|
+
cookie "ANONCHK", days: nil
|
|
13
|
+
|
|
14
|
+
def scripts
|
|
15
|
+
[ Script.new(inline: <<~JS.strip) ]
|
|
16
|
+
(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
17
|
+
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
18
|
+
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
19
|
+
})(window, document, "clarity", "script", "#{id}");
|
|
20
|
+
JS
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def id
|
|
26
|
+
@id ||= require_option(:id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validate!
|
|
30
|
+
id
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|