The source headers, LICENSE files, and license metadata had drifted apart. Align the entire project to MIT: - Convert every source-file header to the MIT text across all comment styles (Go, TS, TSX, JS, MJS, SQL, CSS, GraphQL, shell), including SPDX-License-Identifier tags - Set the root and cookie-banner LICENSE files to the MIT text with a "MIT License" title line - Switch the package.json license fields, Docker image label, and cookie-banner README to MIT - Update docs and the genmodels header generator accordingly - Normalize copyright lines to a single format (Copyright (c) <year(s)> Probo Inc <hello@probo.com>.): unify the hello@getprobo.com and hello@probo.inc emails to hello@probo.com and the comma-separated years to a hyphenated range Genuine third-party references are intentionally left untouched: the Lucide icon attributions (Lucide is ISC) and the trivy dependency license allowlist. Signed-off-by: Sacha Al Himdani <sacha@probo.com>
195 lines
5.9 KiB
Go
195 lines
5.9 KiB
Go
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
|
|
package cookiebanner
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"reflect"
|
|
"slices"
|
|
|
|
"go.probo.inc/probo/pkg/coredata"
|
|
"go.probo.inc/probo/pkg/gid"
|
|
)
|
|
|
|
// resolveTranslations converts raw DB translations into the resolved map
|
|
// used by buildBannerConfig at serve time. Categories are filtered and sorted
|
|
// in snapshot order so the positional category translations align.
|
|
func resolveTranslations(
|
|
translations coredata.CookieBannerTranslations,
|
|
categories coredata.CookieCategories,
|
|
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
|
|
sortConsentCategories(categories)
|
|
return buildSnapshotTranslations(translations, categories)
|
|
}
|
|
|
|
// snapshotsEqual reports whether two version snapshots are visitor-identical.
|
|
// buildSnapshot already normalises empty slices and nil maps, so reflect.DeepEqual
|
|
// is sufficient and is the single chokepoint we'd extend if we ever wanted to
|
|
// ignore particular fields.
|
|
func snapshotsEqual(a, b coredata.CookieBannerVersionSnapshot) bool {
|
|
return reflect.DeepEqual(a, b)
|
|
}
|
|
|
|
// snapshotCategoryKindOrder returns a stable weight per Kind so the snapshot
|
|
// keeps the visitor-facing layout invariants (NECESSARY first, then NORMAL
|
|
// sorted by ID) without depending on the admin-controlled rank.
|
|
func snapshotCategoryKindOrder(k coredata.CookieCategoryKind) int {
|
|
switch k {
|
|
case coredata.CookieCategoryKindNecessary:
|
|
return 0
|
|
case coredata.CookieCategoryKindNormal:
|
|
return 1
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
|
|
// sortConsentCategories sorts categories in snapshot order: NECESSARY first,
|
|
// then NORMAL sorted by ID. The caller is expected to have already excluded
|
|
// UNCATEGORISED at load time.
|
|
func sortConsentCategories(categories coredata.CookieCategories) {
|
|
slices.SortStableFunc(categories, func(a, b *coredata.CookieCategory) int {
|
|
if d := snapshotCategoryKindOrder(a.Kind) - snapshotCategoryKindOrder(b.Kind); d != 0 {
|
|
return d
|
|
}
|
|
|
|
return bytes.Compare(a.ID[:], b.ID[:])
|
|
})
|
|
}
|
|
|
|
func buildSnapshot(
|
|
banner *coredata.CookieBanner,
|
|
categories coredata.CookieCategories,
|
|
allPatterns coredata.TrackerPatterns,
|
|
) coredata.CookieBannerVersionSnapshot {
|
|
sortConsentCategories(categories)
|
|
|
|
cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
|
|
|
|
for _, p := range allPatterns {
|
|
cookiesByCategory[p.CookieCategoryID] = append(
|
|
cookiesByCategory[p.CookieCategoryID],
|
|
coredata.CookieItem{
|
|
Name: p.DisplayName,
|
|
TrackerType: p.TrackerType,
|
|
MaxAgeSeconds: p.MaxAgeSeconds,
|
|
Description: p.Description,
|
|
},
|
|
)
|
|
}
|
|
|
|
snapshotCategories := make([]coredata.CookieBannerVersionSnapshotCategory, len(categories))
|
|
for i, c := range categories {
|
|
cookies := cookiesByCategory[c.ID]
|
|
if cookies == nil {
|
|
cookies = coredata.CookieItems{}
|
|
}
|
|
|
|
gcmConsentTypes := c.GCMConsentTypes
|
|
if gcmConsentTypes == nil {
|
|
gcmConsentTypes = []string{}
|
|
}
|
|
|
|
snapshotCategories[i] = coredata.CookieBannerVersionSnapshotCategory{
|
|
Name: c.Name,
|
|
Slug: c.Slug,
|
|
Description: c.Description,
|
|
Kind: c.Kind,
|
|
Cookies: cookies,
|
|
GCMConsentTypes: gcmConsentTypes,
|
|
PostHogConsent: c.PostHogConsent,
|
|
}
|
|
}
|
|
|
|
return coredata.CookieBannerVersionSnapshot{
|
|
PrivacyPolicyURL: banner.PrivacyPolicyURL,
|
|
CookiePolicyURL: banner.CookiePolicyURL,
|
|
ConsentExpiryDays: banner.ConsentExpiryDays,
|
|
DefaultLanguage: banner.DefaultLanguage,
|
|
Categories: snapshotCategories,
|
|
}
|
|
}
|
|
|
|
func buildSnapshotTranslations(
|
|
translations coredata.CookieBannerTranslations,
|
|
categories coredata.CookieCategories,
|
|
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
|
|
if len(translations) == 0 {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]coredata.CookieBannerVersionSnapshotTranslation, len(translations))
|
|
|
|
for _, t := range translations {
|
|
var raw struct {
|
|
Categories map[string]struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
} `json:"categories"`
|
|
}
|
|
|
|
_ = json.Unmarshal(t.Translations, &raw)
|
|
|
|
ui := make(map[string]string)
|
|
|
|
var flat map[string]json.RawMessage
|
|
|
|
_ = json.Unmarshal(t.Translations, &flat)
|
|
for k, v := range flat {
|
|
if k == "categories" || k == "cookies" {
|
|
continue
|
|
}
|
|
|
|
var s string
|
|
if json.Unmarshal(v, &s) == nil {
|
|
ui[k] = s
|
|
}
|
|
}
|
|
|
|
catTranslations := make([]coredata.CookieBannerVersionSnapshotCategoryTranslation, len(categories))
|
|
for i, c := range categories {
|
|
if raw.Categories != nil {
|
|
if ct, ok := raw.Categories[c.ID.String()]; ok {
|
|
catTranslations[i] = coredata.CookieBannerVersionSnapshotCategoryTranslation{
|
|
Name: ct.Name,
|
|
Description: ct.Description,
|
|
}
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
catTranslations[i] = coredata.CookieBannerVersionSnapshotCategoryTranslation{
|
|
Name: c.Name,
|
|
Description: c.Description,
|
|
}
|
|
}
|
|
|
|
result[t.Language] = coredata.CookieBannerVersionSnapshotTranslation{
|
|
UI: ui,
|
|
Categories: catTranslations,
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|