@getquick/content 0.0.0-stage → 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.
- package/README.md +142 -2
- package/consent-migrations/0001_consent_records.sql +34 -0
- package/dist/bearer-DMOCoSci.js +16 -0
- package/dist/client-config-B3LNSOxl.js +4 -0
- package/dist/configuration-C_JGygQI.d.ts +259 -0
- package/dist/consent-astro.d.ts +36 -0
- package/dist/consent-astro.js +94 -0
- package/dist/consent-client.d.ts +37 -0
- package/dist/consent-client.js +321 -0
- package/dist/consent-migrations.d.ts +8 -0
- package/dist/consent-migrations.js +10 -0
- package/dist/consent-server.d.ts +77 -0
- package/dist/consent-server.js +292 -0
- package/dist/consent-testing.d.ts +96 -0
- package/dist/consent-testing.js +188 -0
- package/dist/consent.d.ts +114 -0
- package/dist/consent.js +5 -0
- package/dist/embeds-p9SyGNZI.js +344 -0
- package/dist/formatting-wOk2jz6J.js +276 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +20 -0
- package/dist/legal-C5Xiq59W.d.ts +48 -0
- package/dist/migrations.d.ts +4 -0
- package/dist/migrations.js +6 -0
- package/dist/paths-Cmzzof3v.js +5 -0
- package/dist/records-CACA2O0Q.d.ts +56 -0
- package/dist/records-Dumrz759.js +74 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +6368 -0
- package/dist/state-BYWc5TUB.js +559 -0
- package/dist/state-Dzd52F3c.d.ts +77 -0
- package/dist/version-Dn6VLZJO.js +6 -0
- package/dist/wordpress-CPL40-vq.d.ts +318 -0
- package/migrations/0001_publications.sql +31 -0
- package/migrations/0002_entries.sql +14 -0
- package/migrations/0003_publication_events.sql +36 -0
- package/migrations/0004_withdrawals.sql +30 -0
- package/migrations/0005_reconciliation.sql +45 -0
- package/package.json +56 -3
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { C as hostMatches, S as embedService, T as isServiceId, _ as SERVICE_CATALOG, a as MEASUREMENT_ID, b as catalogService, c as CONSENT_CATEGORIES, d as ConsentService, f as ConsentStorage, g as OptionalCategory, h as OPTIONAL_CATEGORIES, i as ConsentConfigurationInput, l as COPY_LANGUAGES, m as CopyLanguage, n as CONSENT_COPY_REVISION, o as consentConfiguration, p as Copy, r as ConsentConfiguration, s as needsBanner, t as AnalyticsSettings, u as ConsentCategory, v as SERVICE_IDS, w as hostService, x as copyLanguage, y as ServiceId } from "./configuration-C_JGygQI.js";
|
|
2
|
+
import { a as REQUIRED_LEGAL_ROLES, i as LegalRole, n as LegalContract, o as legalRevisions, r as LegalLanguage, s as parseLegalLanguage, t as LEGAL_ROLES } from "./legal-C5Xiq59W.js";
|
|
3
|
+
import { _ as serializeConsentState, a as ConsentChoice, c as DECISION_VALIDITY_MS, d as denyAll, f as grantedCategories, g as readConsentCookie, h as promptReason, i as ConsentAction, l as consentCookie, m as parseConsentState, n as CONSENT_COOKIE, o as ConsentDecisions, p as isConsentId, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as ConsentState, t as CONSENT_ACTIONS, u as decide, v as serviceGranted } from "./state-Dzd52F3c.js";
|
|
4
|
+
import { a as isLegalRoleName, i as consentRecordRequest, n as ConsentRecordRequest, o as isRecordToken, r as MAX_RECORD_BYTES, s as parseConsentRecordRequest, t as ConsentRecord } from "./records-CACA2O0Q.js";
|
|
5
|
+
//#region frontend/src/consent/copy.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Runtime-owned copy (CN-C3, CN-C4): Sites restyle it but do not edit it.
|
|
8
|
+
* Drafted for CONSENT-1 and pending the legal review recorded in ADR 0008's
|
|
9
|
+
* open items; changing what it says about processing bumps
|
|
10
|
+
* CONSENT_COPY_REVISION.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConsentCopy {
|
|
13
|
+
bannerTitle: string;
|
|
14
|
+
bannerText: string;
|
|
15
|
+
acceptAll: string;
|
|
16
|
+
rejectAll: string;
|
|
17
|
+
customise: string;
|
|
18
|
+
save: string;
|
|
19
|
+
close: string;
|
|
20
|
+
preferencesTitle: string;
|
|
21
|
+
preferencesText: string;
|
|
22
|
+
cookieSettings: string;
|
|
23
|
+
cookiePolicy: string;
|
|
24
|
+
consentId: string;
|
|
25
|
+
noDecision: string;
|
|
26
|
+
provider: string;
|
|
27
|
+
purpose: string;
|
|
28
|
+
storage: string;
|
|
29
|
+
retention: string;
|
|
30
|
+
thirdPartyStorage: string;
|
|
31
|
+
alwaysOn: string;
|
|
32
|
+
categories: Record<ConsentCategory, {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
}>;
|
|
36
|
+
placeholderText: (service: string, provider: string) => string;
|
|
37
|
+
loadOnce: string;
|
|
38
|
+
alwaysAllow: string;
|
|
39
|
+
blockedText: (host: string) => string;
|
|
40
|
+
legal: Record<string, string>;
|
|
41
|
+
legalNavigation: string;
|
|
42
|
+
}
|
|
43
|
+
export declare const CONSENT_COPY: Record<CopyLanguage, ConsentCopy>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region frontend/src/consent/embeds.d.ts
|
|
46
|
+
/** Where rewriting runs: the Site's consent configuration and the page's language. */
|
|
47
|
+
export interface EmbedContext {
|
|
48
|
+
configuration: ConsentConfiguration;
|
|
49
|
+
/** The page's `lang` (BCP 47); placeholders follow it (CN-C3). */
|
|
50
|
+
language: string;
|
|
51
|
+
/**
|
|
52
|
+
* Hosts that are the Site itself (its Frontend and its own CMS/media origin).
|
|
53
|
+
* Relative URLs are always first party.
|
|
54
|
+
*/
|
|
55
|
+
firstPartyHosts?: readonly string[];
|
|
56
|
+
}
|
|
57
|
+
/** An embed rewriting withheld for good: from an unknown host, an inactive service, or a script. */
|
|
58
|
+
export interface BlockedEmbed {
|
|
59
|
+
tag: "iframe" | "script" | "embed" | "object";
|
|
60
|
+
/** The host it would have contacted; `inline` for an inline script. */
|
|
61
|
+
host: string;
|
|
62
|
+
service: ServiceId | null;
|
|
63
|
+
reason: "unknown-host" | "inactive-service" | "script";
|
|
64
|
+
}
|
|
65
|
+
export interface RewrittenHtml {
|
|
66
|
+
html: string;
|
|
67
|
+
/** Embeds turned into service placeholders, loadable after consent. */
|
|
68
|
+
placeholders: Array<{
|
|
69
|
+
service: ServiceId;
|
|
70
|
+
host: string;
|
|
71
|
+
}>;
|
|
72
|
+
/** Embeds blocked for good (CN-R3), for diagnostics and readiness. */
|
|
73
|
+
blocked: BlockedEmbed[];
|
|
74
|
+
}
|
|
75
|
+
/** Text safe inside an attribute value or element content. */
|
|
76
|
+
export declare function escapeHtml(value: string): string;
|
|
77
|
+
export interface PlaceholderInput {
|
|
78
|
+
service: ServiceId;
|
|
79
|
+
/** The embed's URL; it is only an attribute value until consent. */
|
|
80
|
+
src: string;
|
|
81
|
+
/** The page's `lang`. */
|
|
82
|
+
language: string;
|
|
83
|
+
title?: string;
|
|
84
|
+
width?: string | number;
|
|
85
|
+
height?: string | number;
|
|
86
|
+
/** Further iframe attributes (allow, allowfullscreen, class…), applied when it loads. */
|
|
87
|
+
attributes?: Record<string, string>;
|
|
88
|
+
/** A decorative embed (a background video): no notice or buttons; loads only on a grant. */
|
|
89
|
+
decorative?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A placeholder for a third-party embed (CN-R13): no live `src`, naming the
|
|
93
|
+
* provider and purpose and offering "Load once" and "Always allow external
|
|
94
|
+
* media". The client runtime replaces it with the iframe after consent.
|
|
95
|
+
* Without JavaScript it never loads. Site components use it for their own
|
|
96
|
+
* embeds (a contact map); rewriteEmbeds uses it for WordPress HTML.
|
|
97
|
+
*/
|
|
98
|
+
export declare function embedPlaceholder(input: PlaceholderInput): string;
|
|
99
|
+
/** The generic placeholder for an embed that may never load (CN-R3). */
|
|
100
|
+
export declare function blockedPlaceholder(host: string, tag: BlockedEmbed["tag"], language: string): string;
|
|
101
|
+
/**
|
|
102
|
+
* Rewrites published HTML so its initial form contacts no third party
|
|
103
|
+
* (CN-R1, CN-R3, CN-R13): iframes of active catalog services become
|
|
104
|
+
* placeholders; other third-party iframes, embeds and objects, and every
|
|
105
|
+
* third-party or inline executable script, are blocked and reported.
|
|
106
|
+
* First-party and non-executable content is left byte for byte.
|
|
107
|
+
*/
|
|
108
|
+
export declare function rewriteEmbeds(html: string, context: EmbedContext): RewrittenHtml;
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region frontend/src/consent/version.d.ts
|
|
111
|
+
/** The UI/adapter version recorded with each decision (CN-R7). */
|
|
112
|
+
export declare const CONSENT_UI_VERSION: string;
|
|
113
|
+
//#endregion
|
|
114
|
+
export { AnalyticsSettings, CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY_REVISION, COPY_LANGUAGES, ConsentAction, ConsentCategory, ConsentChoice, ConsentConfiguration, ConsentConfigurationInput, ConsentDecisions, ConsentRecord, ConsentRecordRequest, ConsentService, ConsentState, ConsentStorage, Copy, CopyLanguage, DECISION_VALIDITY_MS, LEGAL_ROLES, type LegalContract, type LegalLanguage, type LegalRole, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, OptionalCategory, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, ServiceId, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedService, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, serializeConsentState, serviceGranted };
|
package/dist/consent.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { C as SERVICE_IDS, D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage, _ as consentConfiguration, a as consentCookie, b as COPY_LANGUAGES, c as grantedCategories, d as promptReason, f as readConsentCookie, g as MEASUREMENT_ID, h as CONSENT_COPY_REVISION, i as DECISION_VALIDITY_MS, k as isServiceId, l as isConsentId, m as serviceGranted, n as CONSENT_COOKIE, o as decide, p as serializeConsentState, r as CONSENT_COOKIE_MAX_AGE_SECONDS, s as denyAll, t as CONSENT_ACTIONS, u as parseConsentState, v as needsBanner, w as catalogService, x as OPTIONAL_CATEGORIES, y as CONSENT_CATEGORIES } from "./state-BYWc5TUB.js";
|
|
2
|
+
import { a as parseConsentRecordRequest, i as isRecordToken, n as consentRecordRequest, r as isLegalRoleName, t as MAX_RECORD_BYTES } from "./records-Dumrz759.js";
|
|
3
|
+
import { a as CONSENT_COPY, c as legalRevisions, i as rewriteEmbeds, l as parseLegalLanguage, n as embedPlaceholder, o as LEGAL_ROLES, r as escapeHtml, s as REQUIRED_LEGAL_ROLES, t as blockedPlaceholder } from "./embeds-p9SyGNZI.js";
|
|
4
|
+
import { t as CONSENT_UI_VERSION } from "./version-Dn6VLZJO.js";
|
|
5
|
+
export { CONSENT_ACTIONS, CONSENT_CATEGORIES, CONSENT_COOKIE, CONSENT_COOKIE_MAX_AGE_SECONDS, CONSENT_COPY, CONSENT_COPY_REVISION, CONSENT_UI_VERSION, COPY_LANGUAGES, DECISION_VALIDITY_MS, LEGAL_ROLES, MAX_RECORD_BYTES, MEASUREMENT_ID, OPTIONAL_CATEGORIES, REQUIRED_LEGAL_ROLES, SERVICE_CATALOG, SERVICE_IDS, blockedPlaceholder, catalogService, consentConfiguration, consentCookie, consentRecordRequest, copyLanguage, decide, denyAll, embedPlaceholder, embedService, escapeHtml, grantedCategories, hostMatches, hostService, isConsentId, isLegalRoleName, isRecordToken, isServiceId, legalRevisions, needsBanner, parseConsentRecordRequest, parseConsentState, parseLegalLanguage, promptReason, readConsentCookie, rewriteEmbeds, serializeConsentState, serviceGranted };
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { D as hostMatches, E as embedService, O as hostService, S as SERVICE_CATALOG, T as copyLanguage } from "./state-BYWc5TUB.js";
|
|
2
|
+
import { i as isRecordToken, r as isLegalRoleName } from "./records-Dumrz759.js";
|
|
3
|
+
//#region frontend/src/consent/legal.ts
|
|
4
|
+
/**
|
|
5
|
+
* LEGAL-1's Legal Contract, as gq-config publishes it (fixed in the consent
|
|
6
|
+
* execution record): per language, each assigned role's public URL, title and
|
|
7
|
+
* revision, plus the controller's identity.
|
|
8
|
+
*/
|
|
9
|
+
const LEGAL_ROLES = [
|
|
10
|
+
"privacy_policy",
|
|
11
|
+
"cookie_policy",
|
|
12
|
+
"terms_of_use",
|
|
13
|
+
"legal_notice",
|
|
14
|
+
"terms_of_sale",
|
|
15
|
+
"returns_policy",
|
|
16
|
+
"shipping_policy",
|
|
17
|
+
"complaints_book",
|
|
18
|
+
"dispute_resolution"
|
|
19
|
+
];
|
|
20
|
+
/** The roles every Site needs; commerce and Portuguese-consumer roles come from gq-config's `missingRequired`. */
|
|
21
|
+
const REQUIRED_LEGAL_ROLES = ["privacy_policy", "cookie_policy"];
|
|
22
|
+
const text = (value) => typeof value === "string" && value.trim() ? value : null;
|
|
23
|
+
function safeUrl(value) {
|
|
24
|
+
if (typeof value !== "string") return null;
|
|
25
|
+
if (value.startsWith("/") && !value.startsWith("//")) return value;
|
|
26
|
+
try {
|
|
27
|
+
const url = new URL(value);
|
|
28
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url.href : null;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One language's Contract from untrusted input (GraphQL or the storefront
|
|
35
|
+
* payload): unknown roles, unsafe URLs and malformed entries are dropped.
|
|
36
|
+
*/
|
|
37
|
+
function parseLegalLanguage(value) {
|
|
38
|
+
if (!value || typeof value !== "object") return null;
|
|
39
|
+
const raw = value;
|
|
40
|
+
const controller = raw.controller ?? {};
|
|
41
|
+
const roles = [];
|
|
42
|
+
for (const entry of Array.isArray(raw.roles) ? raw.roles : []) {
|
|
43
|
+
if (!entry || typeof entry !== "object") continue;
|
|
44
|
+
const role = entry;
|
|
45
|
+
const url = safeUrl(role.url);
|
|
46
|
+
if (typeof role.role !== "string" || !LEGAL_ROLES.includes(role.role)) continue;
|
|
47
|
+
if (!url || roles.some((each) => each.role === role.role)) continue;
|
|
48
|
+
roles.push({
|
|
49
|
+
role: role.role,
|
|
50
|
+
url,
|
|
51
|
+
title: text(role.title),
|
|
52
|
+
revision: text(role.revision),
|
|
53
|
+
isFallback: role.isFallback === true
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
roles.sort((a, b) => LEGAL_ROLES.indexOf(a.role) - LEGAL_ROLES.indexOf(b.role));
|
|
57
|
+
return {
|
|
58
|
+
controller: {
|
|
59
|
+
name: text(controller.name),
|
|
60
|
+
privacyEmail: text(controller.privacyEmail),
|
|
61
|
+
dpoContact: text(controller.dpoContact)
|
|
62
|
+
},
|
|
63
|
+
roles,
|
|
64
|
+
missingRequired: (Array.isArray(raw.missingRequired) ? raw.missingRequired : []).filter(isLegalRoleName)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The LEGAL-1 revisions in force on a page, recorded with each decision
|
|
69
|
+
* (CN-R7). A revision the record schema can't hold is left out rather than
|
|
70
|
+
* making every record invalid.
|
|
71
|
+
*/
|
|
72
|
+
function legalRevisions(legal) {
|
|
73
|
+
return Object.fromEntries((legal?.roles ?? []).flatMap((role) => isRecordToken(role.revision) ? [[role.role, role.revision]] : []));
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region frontend/src/consent/copy.ts
|
|
77
|
+
const CONSENT_COPY = {
|
|
78
|
+
en: {
|
|
79
|
+
bannerTitle: "Your privacy choices",
|
|
80
|
+
bannerText: "We use what is strictly necessary for this website to work. With your consent we also use the services listed in the settings, for the purposes shown there. You can accept or reject them all, choose by purpose, and change your mind at any time in Cookie settings.",
|
|
81
|
+
acceptAll: "Accept all",
|
|
82
|
+
rejectAll: "Reject all",
|
|
83
|
+
customise: "Choose by purpose",
|
|
84
|
+
save: "Save choices",
|
|
85
|
+
close: "Close",
|
|
86
|
+
preferencesTitle: "Cookie settings",
|
|
87
|
+
preferencesText: "Choose which purposes you allow. Nothing optional is used until you allow it, and you can withdraw at any time.",
|
|
88
|
+
cookieSettings: "Cookie settings",
|
|
89
|
+
cookiePolicy: "Cookie policy",
|
|
90
|
+
consentId: "Your consent ID",
|
|
91
|
+
noDecision: "No choice recorded yet",
|
|
92
|
+
provider: "Provider",
|
|
93
|
+
purpose: "Purpose",
|
|
94
|
+
storage: "Storage",
|
|
95
|
+
retention: "Retention",
|
|
96
|
+
thirdPartyStorage: "set by the provider; this website cannot remove it",
|
|
97
|
+
alwaysOn: "Always on",
|
|
98
|
+
categories: {
|
|
99
|
+
necessary: {
|
|
100
|
+
name: "Strictly necessary",
|
|
101
|
+
description: "Needed for the website to work and to remember your choices."
|
|
102
|
+
},
|
|
103
|
+
preferences: {
|
|
104
|
+
name: "Preferences",
|
|
105
|
+
description: "Remembers settings that make the website more convenient."
|
|
106
|
+
},
|
|
107
|
+
analytics: {
|
|
108
|
+
name: "Analytics",
|
|
109
|
+
description: "Helps us understand, in aggregate, how the website is used."
|
|
110
|
+
},
|
|
111
|
+
marketing: {
|
|
112
|
+
name: "Marketing",
|
|
113
|
+
description: "Measures and personalises advertising."
|
|
114
|
+
},
|
|
115
|
+
external_media: {
|
|
116
|
+
name: "External media",
|
|
117
|
+
description: "Loads videos, maps and other content from other providers."
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
placeholderText: (service, provider) => `This content is hosted by ${service} (${provider}). Loading it sends your IP address to that provider and may let it store data on your device.`,
|
|
121
|
+
loadOnce: "Load once",
|
|
122
|
+
alwaysAllow: "Always allow external media",
|
|
123
|
+
blockedText: (host) => `Content from ${host} is blocked: it isn't a service this website uses.`,
|
|
124
|
+
legal: {
|
|
125
|
+
privacy_policy: "Privacy policy",
|
|
126
|
+
cookie_policy: "Cookie policy",
|
|
127
|
+
terms_of_use: "Terms of use",
|
|
128
|
+
legal_notice: "Legal notice",
|
|
129
|
+
terms_of_sale: "Terms of sale",
|
|
130
|
+
returns_policy: "Returns policy",
|
|
131
|
+
shipping_policy: "Shipping policy",
|
|
132
|
+
complaints_book: "Complaints book",
|
|
133
|
+
dispute_resolution: "Dispute resolution"
|
|
134
|
+
},
|
|
135
|
+
legalNavigation: "Legal information"
|
|
136
|
+
},
|
|
137
|
+
"pt-PT": {
|
|
138
|
+
bannerTitle: "As suas escolhas de privacidade",
|
|
139
|
+
bannerText: "Usamos o estritamente necessário para que este website funcione. Com o seu consentimento, usamos também os serviços indicados nas definições, para as finalidades aí descritas. Pode aceitar ou rejeitar todos, escolher por finalidade e mudar de ideias a qualquer momento nas Definições de cookies.",
|
|
140
|
+
acceptAll: "Aceitar todos",
|
|
141
|
+
rejectAll: "Rejeitar todos",
|
|
142
|
+
customise: "Escolher por finalidade",
|
|
143
|
+
save: "Guardar escolhas",
|
|
144
|
+
close: "Fechar",
|
|
145
|
+
preferencesTitle: "Definições de cookies",
|
|
146
|
+
preferencesText: "Escolha as finalidades que autoriza. Nada opcional é usado até o autorizar, e pode retirar o consentimento a qualquer momento.",
|
|
147
|
+
cookieSettings: "Definições de cookies",
|
|
148
|
+
cookiePolicy: "Política de cookies",
|
|
149
|
+
consentId: "O seu ID de consentimento",
|
|
150
|
+
noDecision: "Ainda não há escolha registada",
|
|
151
|
+
provider: "Fornecedor",
|
|
152
|
+
purpose: "Finalidade",
|
|
153
|
+
storage: "Armazenamento",
|
|
154
|
+
retention: "Conservação",
|
|
155
|
+
thirdPartyStorage: "definido pelo fornecedor; este website não o pode remover",
|
|
156
|
+
alwaysOn: "Sempre ativo",
|
|
157
|
+
categories: {
|
|
158
|
+
necessary: {
|
|
159
|
+
name: "Estritamente necessários",
|
|
160
|
+
description: "Necessários para o funcionamento do website e para memorizar as suas escolhas."
|
|
161
|
+
},
|
|
162
|
+
preferences: {
|
|
163
|
+
name: "Preferências",
|
|
164
|
+
description: "Memorizam definições que tornam o website mais conveniente."
|
|
165
|
+
},
|
|
166
|
+
analytics: {
|
|
167
|
+
name: "Estatísticas",
|
|
168
|
+
description: "Ajudam-nos a perceber, de forma agregada, como o website é usado."
|
|
169
|
+
},
|
|
170
|
+
marketing: {
|
|
171
|
+
name: "Marketing",
|
|
172
|
+
description: "Medem e personalizam a publicidade."
|
|
173
|
+
},
|
|
174
|
+
external_media: {
|
|
175
|
+
name: "Conteúdos externos",
|
|
176
|
+
description: "Carregam vídeos, mapas e outros conteúdos de outros fornecedores."
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
placeholderText: (service, provider) => `Este conteúdo está alojado em ${service} (${provider}). Carregá-lo envia o seu endereço IP a esse fornecedor e pode permitir-lhe guardar dados no seu dispositivo.`,
|
|
180
|
+
loadOnce: "Carregar uma vez",
|
|
181
|
+
alwaysAllow: "Permitir sempre conteúdos externos",
|
|
182
|
+
blockedText: (host) => `O conteúdo de ${host} está bloqueado: não é um serviço usado por este website.`,
|
|
183
|
+
legal: {
|
|
184
|
+
privacy_policy: "Política de privacidade",
|
|
185
|
+
cookie_policy: "Política de cookies",
|
|
186
|
+
terms_of_use: "Termos de utilização",
|
|
187
|
+
legal_notice: "Informação legal",
|
|
188
|
+
terms_of_sale: "Termos e condições de venda",
|
|
189
|
+
returns_policy: "Política de devoluções",
|
|
190
|
+
shipping_policy: "Política de envios",
|
|
191
|
+
complaints_book: "Livro de reclamações",
|
|
192
|
+
dispute_resolution: "Resolução alternativa de litígios"
|
|
193
|
+
},
|
|
194
|
+
legalNavigation: "Informação legal"
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region frontend/src/consent/embeds.ts
|
|
199
|
+
const ATTRIBUTE = /([^\s"'<>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
200
|
+
const ATTRIBUTES = `((?:[^>"']|"[^"]*"|'[^']*')*)`;
|
|
201
|
+
const ELEMENT = new RegExp(`<(iframe|object)\\b${ATTRIBUTES}>[\\s\\S]*?<\\/\\1\\s*>|<(script)\\b${ATTRIBUTES}>([\\s\\S]*?)<\\/script\\s*>|<(iframe|embed)\\b${ATTRIBUTES}\\/?>`, "gi");
|
|
202
|
+
const NON_EXECUTABLE = /^(?:application\/(?:ld\+)?json|text\/template|text\/x-template|importmap)$/i;
|
|
203
|
+
const RUNTIME_HOOKS = /\bdata-gq-(consent-[a-z-]*|embed|decorative|loaded|once)\b/gi;
|
|
204
|
+
function codePoint(value) {
|
|
205
|
+
return Number.isInteger(value) && value >= 0 && value <= 1114111 ? String.fromCodePoint(value) : "�";
|
|
206
|
+
}
|
|
207
|
+
function decodeEntities(value) {
|
|
208
|
+
return value.replace(/&#x([0-9a-f]{1,8});/gi, (_, hex) => codePoint(parseInt(hex, 16))).replace(/&#(\d{1,10});/g, (_, decimal) => codePoint(Number(decimal))).replaceAll(""", "\"").replaceAll("'", "'").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
209
|
+
}
|
|
210
|
+
/** Text safe inside an attribute value or element content. */
|
|
211
|
+
function escapeHtml(value) {
|
|
212
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
213
|
+
}
|
|
214
|
+
function attributesOf(source) {
|
|
215
|
+
const attributes = {};
|
|
216
|
+
for (const match of source.matchAll(ATTRIBUTE)) {
|
|
217
|
+
const name = match[1].toLowerCase();
|
|
218
|
+
if (Object.hasOwn(attributes, name)) continue;
|
|
219
|
+
attributes[name] = decodeEntities(match[2] ?? match[3] ?? match[4] ?? "");
|
|
220
|
+
}
|
|
221
|
+
return attributes;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* What a reference contacts: the Site itself (relative, first-party host,
|
|
225
|
+
* about:blank), a third-party URL, or an opaque document (data:, javascript:,
|
|
226
|
+
* blob:…) whose own requests can't be known, treated as never loadable.
|
|
227
|
+
*/
|
|
228
|
+
function classify(reference, firstPartyHosts) {
|
|
229
|
+
const value = reference.trim();
|
|
230
|
+
if (!value || /^about:blank$/i.test(value)) return { kind: "first" };
|
|
231
|
+
let url;
|
|
232
|
+
try {
|
|
233
|
+
url = new URL(value, "https://first-party.invalid/");
|
|
234
|
+
} catch {
|
|
235
|
+
return { kind: "opaque" };
|
|
236
|
+
}
|
|
237
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return { kind: "opaque" };
|
|
238
|
+
if (url.hostname === "first-party.invalid") return { kind: "first" };
|
|
239
|
+
if (firstPartyHosts.some((host) => hostMatches(url.hostname, host.toLowerCase()))) return { kind: "first" };
|
|
240
|
+
return {
|
|
241
|
+
kind: "third",
|
|
242
|
+
url
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A placeholder for a third-party embed (CN-R13): no live `src`, naming the
|
|
247
|
+
* provider and purpose and offering "Load once" and "Always allow external
|
|
248
|
+
* media". The client runtime replaces it with the iframe after consent.
|
|
249
|
+
* Without JavaScript it never loads. Site components use it for their own
|
|
250
|
+
* embeds (a contact map); rewriteEmbeds uses it for WordPress HTML.
|
|
251
|
+
*/
|
|
252
|
+
function embedPlaceholder(input) {
|
|
253
|
+
const service = SERVICE_CATALOG[input.service];
|
|
254
|
+
const copy = CONSENT_COPY[copyLanguage(input.language)];
|
|
255
|
+
const attrs = {
|
|
256
|
+
...input.attributes,
|
|
257
|
+
src: input.src
|
|
258
|
+
};
|
|
259
|
+
if (input.title !== void 0) attrs.title = input.title;
|
|
260
|
+
if (input.width !== void 0) attrs.width = String(input.width);
|
|
261
|
+
if (input.height !== void 0) attrs.height = String(input.height);
|
|
262
|
+
const embed = escapeHtml(JSON.stringify({
|
|
263
|
+
tag: "iframe",
|
|
264
|
+
attrs
|
|
265
|
+
}));
|
|
266
|
+
const ratio = Number(attrs.width) > 0 && Number(attrs.height) > 0 ? ` style="aspect-ratio: ${Number(attrs.width)} / ${Number(attrs.height)}"` : "";
|
|
267
|
+
const data = `data-gq-consent-embed="${input.service}" data-gq-category="${service.category}" data-gq-embed="${embed}"`;
|
|
268
|
+
if (input.decorative) return `<div class="gq-consent-embed gq-consent-embed--decorative" ${data} data-gq-decorative aria-hidden="true"${ratio}></div>`;
|
|
269
|
+
return `<div class="gq-consent-embed" ${data} role="group" aria-label="${escapeHtml(attrs.title ? `${service.name}: ${attrs.title}` : service.name)}"${ratio}><div class="gq-consent-embed__notice"><p class="gq-consent-embed__text">${escapeHtml(copy.placeholderText(service.name, service.provider))}</p><p class="gq-consent-embed__purpose">${escapeHtml(service.purpose[copyLanguage(input.language)])}</p><div class="gq-consent-embed__actions"><button type="button" class="gq-consent-button" data-gq-consent-load-once>${escapeHtml(copy.loadOnce)}</button><button type="button" class="gq-consent-button" data-gq-consent-allow="${service.category}">${escapeHtml(copy.alwaysAllow)}</button></div></div></div>`;
|
|
270
|
+
}
|
|
271
|
+
/** The generic placeholder for an embed that may never load (CN-R3). */
|
|
272
|
+
function blockedPlaceholder(host, tag, language) {
|
|
273
|
+
const copy = CONSENT_COPY[copyLanguage(language)];
|
|
274
|
+
if (tag === "script") return `<span hidden data-gq-consent-blocked="${escapeHtml(host)}" data-gq-tag="script"></span>`;
|
|
275
|
+
return `<div class="gq-consent-blocked" data-gq-consent-blocked="${escapeHtml(host)}" data-gq-tag="${tag}" role="note"><p>${escapeHtml(copy.blockedText(host))}</p></div>`;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Rewrites published HTML so its initial form contacts no third party
|
|
279
|
+
* (CN-R1, CN-R3, CN-R13): iframes of active catalog services become
|
|
280
|
+
* placeholders; other third-party iframes, embeds and objects, and every
|
|
281
|
+
* third-party or inline executable script, are blocked and reported.
|
|
282
|
+
* First-party and non-executable content is left byte for byte.
|
|
283
|
+
*/
|
|
284
|
+
function rewriteEmbeds(html, context) {
|
|
285
|
+
const firstParty = context.firstPartyHosts ?? [];
|
|
286
|
+
const placeholders = [];
|
|
287
|
+
const blocked = [];
|
|
288
|
+
const block = (tag, host, service, reason) => {
|
|
289
|
+
blocked.push({
|
|
290
|
+
tag,
|
|
291
|
+
host,
|
|
292
|
+
service,
|
|
293
|
+
reason
|
|
294
|
+
});
|
|
295
|
+
return blockedPlaceholder(host, tag, context.language);
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
html: html.replace(RUNTIME_HOOKS, "data-gq-published-$1").replace(ELEMENT, (whole, pairedTag, pairedAttrs, scriptTag, scriptAttrs, scriptBody, voidTag, voidAttrs) => {
|
|
299
|
+
if (scriptTag) {
|
|
300
|
+
const attrs = attributesOf(scriptAttrs ?? "");
|
|
301
|
+
if (attrs.type && NON_EXECUTABLE.test(attrs.type.trim())) return whole;
|
|
302
|
+
if (attrs.src !== void 0) {
|
|
303
|
+
const target = classify(attrs.src, firstParty);
|
|
304
|
+
if (target.kind === "first") return whole;
|
|
305
|
+
return target.kind === "third" ? block("script", target.url.hostname, hostService(target.url.hostname), "script") : block("script", "inline", null, "script");
|
|
306
|
+
}
|
|
307
|
+
return scriptBody?.trim() ? block("script", "inline", null, "script") : whole;
|
|
308
|
+
}
|
|
309
|
+
const tag = (pairedTag ?? voidTag).toLowerCase();
|
|
310
|
+
const attrs = attributesOf((pairedAttrs ?? voidAttrs) || "");
|
|
311
|
+
const reference = tag === "object" ? attrs.data : attrs.src;
|
|
312
|
+
if (reference === void 0) {
|
|
313
|
+
if (tag === "iframe" && attrs.srcdoc === void 0) return whole;
|
|
314
|
+
return block(tag, "inline", null, "unknown-host");
|
|
315
|
+
}
|
|
316
|
+
const target = classify(reference, firstParty);
|
|
317
|
+
if (target.kind === "first") return whole;
|
|
318
|
+
if (target.kind === "opaque") return block(tag, "inline", null, "unknown-host");
|
|
319
|
+
const url = target.url;
|
|
320
|
+
const service = tag === "iframe" ? embedService(url) : null;
|
|
321
|
+
if (!service) return block(tag, url.hostname, hostService(url.hostname), "unknown-host");
|
|
322
|
+
if (!context.configuration.services.includes(service)) return block(tag, url.hostname, service, "inactive-service");
|
|
323
|
+
placeholders.push({
|
|
324
|
+
service,
|
|
325
|
+
host: url.hostname
|
|
326
|
+
});
|
|
327
|
+
const { src: _src, title, width, height, ...rest } = attrs;
|
|
328
|
+
return embedPlaceholder({
|
|
329
|
+
service,
|
|
330
|
+
src: url.href,
|
|
331
|
+
language: context.language,
|
|
332
|
+
...title !== void 0 ? { title } : {},
|
|
333
|
+
...width !== void 0 ? { width } : {},
|
|
334
|
+
...height !== void 0 ? { height } : {},
|
|
335
|
+
attributes: rest,
|
|
336
|
+
decorative: /\bwp-block-getquick-design-video-hero__video\b/.test(attrs.class ?? "")
|
|
337
|
+
});
|
|
338
|
+
}),
|
|
339
|
+
placeholders,
|
|
340
|
+
blocked
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
export { CONSENT_COPY as a, legalRevisions as c, rewriteEmbeds as i, parseLegalLanguage as l, embedPlaceholder as n, LEGAL_ROLES as o, escapeHtml as r, REQUIRED_LEGAL_ROLES as s, blockedPlaceholder as t };
|