diff --git a/.gitignore b/.gitignore index 48b8bf9..0b565f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ vendor/ + +.idea/ +.vscode/ \ No newline at end of file diff --git a/api/vendorlist.go b/api/vendorlist.go index ffa6d6b..eaf2785 100644 --- a/api/vendorlist.go +++ b/api/vendorlist.go @@ -34,4 +34,6 @@ type Vendor interface { LegitimateInterestStrict(purposeID consentconstants.Purpose) (hasLegitimateInterest bool) // SpecialPurpose returns true if this vendor claims a need for the given special purpose SpecialPurpose(purposeID consentconstants.Purpose) (hasSpecialPurpose bool) + // SpecialFeature returns true if this vendor claims a need for the given special feature + SpecialFeature(featureID consentconstants.SpecialFeature) (hasSpecialFeature bool) } diff --git a/consentconstants/features.go b/consentconstants/features.go new file mode 100644 index 0000000..f7f300f --- /dev/null +++ b/consentconstants/features.go @@ -0,0 +1,6 @@ +package consentconstants + +// SpecialFeature is one of the IAB GDPR special features. These appear in: +// 1. `root.specialFeatures[i]` of the vendor list: https://vendorlist.consensu.org/vendorlist.json +// 2. SpecialFeatureOptIns of the Consent string: https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/Consent%20string%20and%20vendor%20list%20formats%20v1.1%20Final.md#vendor-consent-string-format- +type SpecialFeature uint8 \ No newline at end of file diff --git a/consentconstants/tcf2/features.go b/consentconstants/tcf2/features.go new file mode 100644 index 0000000..deef4a0 --- /dev/null +++ b/consentconstants/tcf2/features.go @@ -0,0 +1,12 @@ +package consentconstants + +import base "github.com/prebid/go-gdpr/consentconstants" + +// TCF 2.0 Special Features: +const ( + // Use precise geolocation data to select and deliver an ad in the moment, without storing it. + Geolocation base.SpecialFeature = 1 + + // Identify a device by actively scanning device characteristics in order to select an ad in the moment. + DeviceScan base.SpecialFeature = 2 +) diff --git a/vendorlist/eager-parsing.go b/vendorlist/eager-parsing.go index 2a72aeb..b84bf95 100644 --- a/vendorlist/eager-parsing.go +++ b/vendorlist/eager-parsing.go @@ -106,11 +106,16 @@ func (l parsedVendor) LegitimateInterestStrict(purposeID consentconstants.Purpos return } -// V1 vedndor list does not support special purposes. +// V1 vendor list does not support special purposes. func (l parsedVendor) SpecialPurpose(purposeID consentconstants.Purpose) bool { return false } +// V1 vendor list does not support special features. +func (l parsedVendor) SpecialFeature(featureID consentconstants.SpecialFeature) bool { + return false +} + type vendorListContract struct { Version uint16 `json:"vendorListVersion"` Vendors []vendorListVendorContract `json:"vendors"` diff --git a/vendorlist/lazy-parsing.go b/vendorlist/lazy-parsing.go index 4ba5e8b..bdf061c 100644 --- a/vendorlist/lazy-parsing.go +++ b/vendorlist/lazy-parsing.go @@ -63,11 +63,16 @@ func (l lazyVendor) LegitimateInterestStrict(purposeID consentconstants.Purpose) return idExists(l, int(purposeID), "legIntPurposeIds") } -// V1 vedndor list does not support special purposes. +// V1 vendor list does not support special purposes. func (l lazyVendor) SpecialPurpose(purposeID consentconstants.Purpose) bool { return false } +// V1 vendor list does not support special features. +func (l lazyVendor) SpecialFeature(featureID consentconstants.SpecialFeature) bool { + return false +} + // Returns false unless "id" exists in an array located at "data.key". func idExists(data []byte, id int, key string) bool { hasID := false diff --git a/vendorlist2/eager-parsing.go b/vendorlist2/eager-parsing.go index 88b0acf..718ea98 100644 --- a/vendorlist2/eager-parsing.go +++ b/vendorlist2/eager-parsing.go @@ -40,16 +40,17 @@ func ParseEagerly(data []byte) (api.VendorList, error) { func parseVendor(contract vendorListVendorContract) parsedVendor { parsed := parsedVendor{ - purposes: mapify(contract.Purposes), - legitimateInterests: mapify(contract.LegitimateInterests), - flexiblePurposes: mapify(contract.FlexiblePurposes), - specialPurposes: mapify(contract.SpecialPurposes), + purposes: mapifyPurpose(contract.Purposes), + legitimateInterests: mapifyPurpose(contract.LegitimateInterests), + flexiblePurposes: mapifyPurpose(contract.FlexiblePurposes), + specialPurposes: mapifyPurpose(contract.SpecialPurposes), + specialFeatures: mapifySpecialFeature(contract.SpecialFeatures), } return parsed } -func mapify(input []uint8) map[consentconstants.Purpose]struct{} { +func mapifyPurpose(input []uint8) map[consentconstants.Purpose]struct{} { m := make(map[consentconstants.Purpose]struct{}, len(input)) var s struct{} for _, value := range input { @@ -58,6 +59,15 @@ func mapify(input []uint8) map[consentconstants.Purpose]struct{} { return m } +func mapifySpecialFeature(input []uint8) map[consentconstants.SpecialFeature]struct{} { + m := make(map[consentconstants.SpecialFeature]struct{}, len(input)) + var s struct{} + for _, value := range input { + m[consentconstants.SpecialFeature(value)] = s + } + return m +} + type parsedVendorList struct { version uint16 vendors map[uint16]parsedVendor @@ -80,6 +90,7 @@ type parsedVendor struct { legitimateInterests map[consentconstants.Purpose]struct{} flexiblePurposes map[consentconstants.Purpose]struct{} specialPurposes map[consentconstants.Purpose]struct{} + specialFeatures map[consentconstants.SpecialFeature]struct{} } func (l parsedVendor) Purpose(purposeID consentconstants.Purpose) (hasPurpose bool) { @@ -120,6 +131,12 @@ func (l parsedVendor) SpecialPurpose(purposeID consentconstants.Purpose) (hasSpe return } +// SpecialFeature returns true if this vendor claims a need for the given special feature +func (l parsedVendor) SpecialFeature(featureID consentconstants.SpecialFeature) (hasSpecialFeature bool) { + _, hasSpecialFeature = l.specialFeatures[featureID] + return +} + type vendorListContract struct { Version uint16 `json:"vendorListVersion"` Vendors map[string]vendorListVendorContract `json:"vendors"` @@ -131,4 +148,5 @@ type vendorListVendorContract struct { LegitimateInterests []uint8 `json:"legIntPurposes"` FlexiblePurposes []uint8 `json:"flexiblePurposes"` SpecialPurposes []uint8 `json:"specialPurposes"` + SpecialFeatures []uint8 `json:"specialFeatures"` } diff --git a/vendorlist2/lazy-parsing.go b/vendorlist2/lazy-parsing.go index fe5e8ba..ec5dd22 100644 --- a/vendorlist2/lazy-parsing.go +++ b/vendorlist2/lazy-parsing.go @@ -70,6 +70,11 @@ func (l lazyVendor) SpecialPurpose(purposeID consentconstants.Purpose) (hasSpeci return idExists(l, int(purposeID), "specialPurposes") } +// SpecialFeature returns true if this vendor claims a need for the given special feature +func (l lazyVendor) SpecialFeature(featureID consentconstants.SpecialFeature) (hasSpecialFeature bool) { + return idExists(l, int(featureID), "specialFeatures") +} + // Returns false unless "id" exists in an array located at "data.key". func idExists(data []byte, id int, key string) bool { hasID := false diff --git a/vendorlist2/shared_test.go b/vendorlist2/shared_test.go index 49f498b..1eb3f43 100644 --- a/vendorlist2/shared_test.go +++ b/vendorlist2/shared_test.go @@ -48,6 +48,10 @@ func vendorTester(parser func(data []byte) api.VendorList) func(*testing.T) { assertBoolsEqual(t, true, v.SpecialPurpose(2)) assertBoolsEqual(t, false, v.SpecialPurpose(3)) // Does not exist yet + assertBoolsEqual(t, true, v.SpecialFeature(1)) + assertBoolsEqual(t, true, v.SpecialFeature(2)) + assertBoolsEqual(t, false, v.SpecialFeature(3)) // Does not exist yet + v = list.Vendor(80) assertBoolsEqual(t, true, v.Purpose(1)) assertBoolsEqual(t, true, v.PurposeStrict(1)) @@ -72,6 +76,10 @@ func vendorTester(parser func(data []byte) api.VendorList) func(*testing.T) { assertBoolsEqual(t, false, v.SpecialPurpose(1)) assertBoolsEqual(t, false, v.SpecialPurpose(2)) assertBoolsEqual(t, false, v.SpecialPurpose(3)) // Does not exist yet + + assertBoolsEqual(t, false, v.SpecialFeature(1)) + assertBoolsEqual(t, false, v.SpecialFeature(2)) + assertBoolsEqual(t, false, v.SpecialFeature(3)) // Does not exist yet } } @@ -91,7 +99,7 @@ const testData = ` "flexiblePurposes": [2, 9], "specialPurposes": [1, 2], "features": [1, 2], - "specialFeatures": [], + "specialFeatures": [1, 2], "policyUrl": "https://www.emerse.com/privacy-policy/" }, "80": {