diff --git a/lib/GDPR/IAB/TCFv2.pm b/lib/GDPR/IAB/TCFv2.pm index fcc0f5e..627ea6d 100644 --- a/lib/GDPR/IAB/TCFv2.pm +++ b/lib/GDPR/IAB/TCFv2.pm @@ -49,11 +49,11 @@ sub Parse { if ( $self->_is_vendor_consent_range_encoding ) { ( $vendor_consents, $legitimate_interest_start ) = - $self->_parseRangeSection( $self->max_vendor_id, 230 ); + $self->_parseRangeSection( $self->max_vendor_id_consent, 230 ); } else { ( $vendor_consents, $legitimate_interest_start ) = - $self->_parseBitField( $self->max_vendor_id, 230 ); + $self->_parseBitField( $self->max_vendor_id_consent, 230 ); } $self->{vendor_consents} = $vendor_consents; @@ -61,29 +61,30 @@ sub Parse { my $legitimate_interest_max_vendor = get_uint16( $self->{data}, $legitimate_interest_start ); + $self->{legitimate_interest_max_vendor} = $legitimate_interest_max_vendor; + croak "invalid consent data: no legitimate interest start position (got $legitimate_interest_start +16 but @{[ length( $self->{data} ) ]})" if $legitimate_interest_start + 16 > length( $self->{data} ); - my $is_vendor_legitimate_interest_range = is_set( $data, $legitimate_interest_start + 16 ); - - $self->{legitimate_interest_start} = $legitimate_interest_start + 17; + my $is_vendor_legitimate_interest_range = + is_set( $data, $legitimate_interest_start + 16 ); my $vendor_legitimate_interests; my $pub_restrict_start; - if ( $is_vendor_legitimate_interest_range ) { + if ($is_vendor_legitimate_interest_range) { ( $vendor_legitimate_interests, $pub_restrict_start ) = $self->_parseRangeSection( - $legitimate_interest_max_vendor, - $self->{legitimate_interest_start} + $self->max_vendor_id_legitimate_interest, + $legitimate_interest_start + 17 ); } else { ( $vendor_legitimate_interests, $pub_restrict_start ) = $self->_parseBitField( - $legitimate_interest_max_vendor, - $self->{legitimate_interest_start} + $self->max_vendor_id_legitimate_interest, + $legitimate_interest_start + 17 ); } @@ -218,22 +219,28 @@ sub publisher_country_code { return get_char6_sequence( $self->{data}, 201, 2 ); } -sub max_vendor_id { +sub max_vendor_id_consent { my $self = shift; return get_uint16( $self->{data}, 213 ); } +sub max_vendor_id_legitimate_interest { + my $self = shift; + + return $self->{legitimate_interest_max_vendor}; +} + sub vendor_consent { my ( $self, $id ) = @_; - return $self->{vendor_consents}->vendor_consent($id); + return $self->{vendor_consents}->contains($id); } sub vendor_legitimate_interest { my ( $self, $id ) = @_; - return $self->{vendor_legitimate_interests}->vendor_consent($id); + return $self->{vendor_legitimate_interests}->contains($id); } sub _is_vendor_consent_range_encoding { @@ -245,12 +252,6 @@ sub _is_vendor_consent_range_encoding { sub _parseRangeSection { my ( $self, $vendor_bits_required, $start_bit ) = @_; - my $data_size = length( $self->{data} ); - - croak - "a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size" - if $data_size < 32; - my $range_section = GDPR::IAB::TCFv2::RangeSection->new( data => $self->{data}, start_bit => $start_bit, @@ -263,15 +264,6 @@ sub _parseRangeSection { sub _parseBitField { my ( $self, $vendor_bits_required, $start_bit ) = @_; - my $data_size = length( $self->{data} ); - - # add 7 to force rounding to next integer value - my $bytes_required = ( $vendor_bits_required + $start_bit + 7 ) / 8; - - croak - "a BitField for $vendor_bits_required requires a consent string of $bytes_required bytes. This consent string had $data_size" - if $data_size < $bytes_required; - my $bitfield = GDPR::IAB::TCFv2::BitField->new( data => $self->{data}, start_bit => $start_bit, @@ -382,6 +374,47 @@ Version of the GVL used to create this TC String. The user's consent value for each Purpose established on the legal basis of consent. + my $ok = $instance->is_purpose_consent_allowed(1); + +=head2 is_purpose_legitimate_interest_allowed + +The user's consent value for each Purpose established on the legal basis of legitimate interest. + + my $ok = $instance->is_purpose_legitimate_interest_allowed(1); + +=head2 purpose_one_treatment + +CMPs can use the PublisherCC field to indicate the legal jurisdiction the publisher is under to help vendors determine whether the vendor needs consent for Purpose 1. + +Returns true if Purpose 1 was NOT disclosed at all. + +Returns false if Purpose 1 was disclosed commonly as consent as expected by the L. + +=head2 publisher_country_code + +Two-letter L language code of the country that determines legislation of reference. +Commonly, this corresponds to the country in which the publisher’s business entity is established. + +=head2 max_vendor_id_consent + +The maximum Vendor ID that is represented in the following bit field or range encoding. + +Because this section can be a variable length, this indicates the last ID of the section so that a decoder will know when it has reached the end. + +=head2 vendor_consent + +The consent value for each Vendor ID + +=head2 max_vendor_id_legitimate_interest + +The maximum Vendor ID that is represented in the following bit field or range encoding. + +Because this section can be a variable length, this indicates the last ID of the section so that a decoder will know when it has reached the end. + +=head2 vendor_legitimate_interest + +The legitimate interest value for each Vendor ID + =head1 FUNCTIONS =head2 looksLikeIsConsentVersion2 diff --git a/lib/GDPR/IAB/TCFv2/BitField.pm b/lib/GDPR/IAB/TCFv2/BitField.pm index eac19ca..7561afb 100644 --- a/lib/GDPR/IAB/TCFv2/BitField.pm +++ b/lib/GDPR/IAB/TCFv2/BitField.pm @@ -15,6 +15,15 @@ sub new { my $vendor_bits_required = $args{vendor_bits_required} or croak "missing 'vendor_bits_required'"; + my $data_size = length($data); + + # add 7 to force rounding to next integer value + my $bytes_required = ( $vendor_bits_required + $start_bit + 7 ) / 8; + + croak + "a BitField for $vendor_bits_required requires a consent string of $bytes_required bytes. This consent string had $data_size" + if $data_size < $bytes_required; + my $self = { data => substr( $data, $start_bit ), vendor_bits_required => $vendor_bits_required, @@ -31,7 +40,7 @@ sub max_vendor_id { return $self->{vendor_bits_required}; } -sub vendor_consent { +sub contains { my ( $self, $id ) = @_; croak "invalid vendor id $id: must be positive integer bigger than 0" @@ -43,3 +52,43 @@ sub vendor_consent { } 1; +__END__ + +=head1 NAME + +GDPR::IAB::TCFv2::BitField - Transparency & Consent String version 2 bitfield parser + +=head1 SYNOPSIS + + my $data = unpack "B*", decode_base64url('tcf v2 consent string base64 encoded'); + + my $max_vendor_id_consent = << get 16 bits from $data offset 213 >> + + my $bit_field = GDPR::IAB::TCFv2::BitField->new( + data => $data, + start_bit => 230, # offset for vendor consents + vendor_bits_required => $max_vendor_id_consent + ); + + if $bit_field->contains(284) { ... } + +=head1 CONSTRUCTOR + +Receive 3 parameters: data (as sequence of bits), start bit offset and vendor bits required (max vendor id). + +Will die if any parameter is missing. + +Will die if data does not contain all bits required. + +=head1 METHODS + +=head2 contains + +Return the vendor id bit status (if enable or not) from the bit field. +Will return false if id is bigger than max vendor id. + + my $ok = $bit_field->contains(284); + +=head2 max_vendor_id + +Returns the max vendor id. diff --git a/lib/GDPR/IAB/TCFv2/BitUtils.pm b/lib/GDPR/IAB/TCFv2/BitUtils.pm index f9812a3..52c8e50 100644 --- a/lib/GDPR/IAB/TCFv2/BitUtils.pm +++ b/lib/GDPR/IAB/TCFv2/BitUtils.pm @@ -11,7 +11,6 @@ use base qw; our @EXPORT_OK = qw [ qw; + + my $data = unpack "B*", decode_base64url('tcf v2 consent string base64 encoded'); + + my $max_vendor_id_consent = get_uint16($data, 213); + +=head1 FUNCTIONS + +=head2 is_set + +Receive two parameters: data and bit offset. + +Will return true if the bit present on bit offset is 1. + + my $is_service_specific = is_set( $data, 138 ); + +=head2 get_uint6 + +Receive two parameters: data and bit offset. + +Will fetch 6 bits from data since bit offset and convert it an unsigned int. + + my $version = get_uint6( $data, 0 ); + +=head2 get_char6 + +Similar to L but perform increment the value with the ascii value of "A" letter and convert to a character. + +=head2 get_char6_sequence + +Receives the data, bit offset and sequence size n. + +Returns a string of size n by concantenating L calls. + + my $consent_language = get_char6_sequence($data, 108, 2) # returns two letter country encoded as ISO_639-1 + +=head2 get_uint12 + +Receives the data and bit offset. + +Will fetch 12 bits from data since bit offset and convert it an unsigned int (short). + + my $cmp_id = get_uint12( $data, 78 ); + +=head2 get_uint16 + +Receives the data and bit offset. + +Will fetch 16 bits from data since bit offset and convert it an unsigned int (short). + + my $max_vendor_id_consent = get_uint16( $data, 213 ); + +=head2 get_uint36 + +Receives the data and bit offset. + +Will fetch 36 bits from data since bit offset and convert it an unsigned int (long). + + my $deciseconds = get_uint36( $data, 6 ); + my $created = $deciseconds/2; + diff --git a/lib/GDPR/IAB/TCFv2/RangeConsent.pm b/lib/GDPR/IAB/TCFv2/RangeConsent.pm index 33285c0..757ed0b 100644 --- a/lib/GDPR/IAB/TCFv2/RangeConsent.pm +++ b/lib/GDPR/IAB/TCFv2/RangeConsent.pm @@ -31,3 +31,33 @@ sub contains { } 1; +__END__ + +=head1 NAME + +GDPR::IAB::TCFv2::RangeConsent - Transparency & Consent String version 2 range consent pair + +=head1 SYNOPSIS + + my $range = GDPR::IAB::TCFv2::RangeConsent->new( + start => 10, + end => 20, + ); + + die "ops" unless $range->contains(15); + +=head1 CONSTRUCTOR + +Receive 2 parameters: start and end. + +Will die if any parameter is missing. + +Will die if start is bigger than end. + +=head1 METHODS + +=head2 contains + +Return true if the id is present on the range [start, end] + + my $ok = $range->contains(15); diff --git a/lib/GDPR/IAB/TCFv2/RangeSection.pm b/lib/GDPR/IAB/TCFv2/RangeSection.pm index 564da49..f390b19 100644 --- a/lib/GDPR/IAB/TCFv2/RangeSection.pm +++ b/lib/GDPR/IAB/TCFv2/RangeSection.pm @@ -17,7 +17,12 @@ sub new { my $vendor_bits_required = $args{vendor_bits_required} or croak "missing 'vendor_bits_required'"; - # TODO add parse range consent + my $data_size = length($data); + + croak + "a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size" + if $data_size < 32; + my $num_entries = get_uint12( $data, $start_bit ); my $current_offset = $start_bit + 12; @@ -25,9 +30,10 @@ sub new { my @consents; foreach my $i ( 1 .. $num_entries ) { - my ( $consent, $bits_consumed ) = - _parse_range_consent( $data, $current_offset, - $vendor_bits_required ); + my ( $consent, $bits_consumed ) = _parse_range_consent( + $data, $current_offset, + $vendor_bits_required + ); push @consents, $consent; @@ -63,8 +69,11 @@ sub _parse_range_consent { "bit $initial_bit range entry exclusion ends at $end, but the max vendor ID is $max_vendor_id" if $end > $max_vendor_id; - return GDPR::IAB::TCFv2::RangeConsent->new( start => $start, - end => $end ), 33; + return GDPR::IAB::TCFv2::RangeConsent->new( + start => $start, + end => $end + ), + 33; } my $vendor_id = get_uint16( $data, $initial_bit + 1 ); @@ -73,8 +82,11 @@ sub _parse_range_consent { "bit $initial_bit range entry excludes vendor $vendor_id, but only vendors [1, $max_vendor_id] are valid" if $vendor_id > $max_vendor_id; - return GDPR::IAB::TCFv2::RangeConsent->new( start => $vendor_id, - end => $vendor_id ), 17; + return GDPR::IAB::TCFv2::RangeConsent->new( + start => $vendor_id, + end => $vendor_id + ), + 17; } sub current_offset { @@ -89,7 +101,7 @@ sub max_vendor_id { return $self->{vendor_bits_required}; } -sub vendor_consent { +sub contains { my ( $self, $id ) = @_; croak "invalid vendor id $id: must be positive integer bigger than 0" @@ -101,3 +113,46 @@ sub vendor_consent { } 1; +__END__ + +=head1 NAME + +GDPR::IAB::TCFv2::RangeSection - Transparency & Consent String version 2 range section parser + +=head1 SYNOPSIS + + my $data = unpack "B*", decode_base64url('tcf v2 consent string base64 encoded'); + + my $max_vendor_id_consent = << get 16 bits from $data offset 213 >> + + my $range_section = GDPR::IAB::TCFv2::RangeSection->new( + data => $data, + start_bit => 230, # offset for vendor consents + vendor_bits_required => $max_vendor_id_consent + ); + + if $range_section->contains(284) { ... } + +=head1 CONSTRUCTOR + +Receive 3 parameters: data (as sequence of bits), start bit offset and vendor bits required (max vendor id). + +Will die if any parameter is missing. + +Will die if data does not contain all bits required. + +Will die if the range sections are malformed. + +=head1 METHODS + +=head2 contains + +Return the vendor id bit status (if enable or not) from one of the range sections. + +Will return false if id is bigger than max vendor id. + + my $ok = $range_section->contains(284); + +=head2 max_vendor_id + +Returns the max vendor id. diff --git a/t/00-load.t b/t/00-load.t index 969658e..222369c 100644 --- a/t/00-load.t +++ b/t/00-load.t @@ -16,7 +16,7 @@ require_ok 'GDPR::IAB::TCFv2'; isa_ok 'GDPR::IAB::TCFv2::BitUtils', 'Exporter'; -my @methods = qw; +my @methods = qw; can_ok 'GDPR::IAB::TCFv2::BitField', @methods; can_ok 'GDPR::IAB::TCFv2::RangeSection', @methods; diff --git a/t/01-parse.t b/t/01-parse.t index e020ea3..0f705aa 100644 --- a/t/01-parse.t +++ b/t/01-parse.t @@ -4,7 +4,7 @@ use Test::Exception; use GDPR::IAB::TCFv2; subtest "valid tcf v2 consent string using bitfield" => sub { - plan tests => 21; + plan tests => 22; my $consent; @@ -52,7 +52,10 @@ subtest "valid tcf v2 consent string using bitfield" => sub { is $consent->publisher_country_code, "KM", 'should return the publisher country code "KM"'; - is $consent->max_vendor_id, 115, "max vendor id is 115"; + is $consent->max_vendor_id_consent, 115, "max vendor id consent is 115"; + + is $consent->max_vendor_id_legitimate_interest, 113, + "max vendor id legitimate interest is 113"; subtest "check purpose consent ids" => sub { plan tests => 24; @@ -125,7 +128,7 @@ subtest "valid tcf v2 consent string using bitfield" => sub { }; subtest "valid tcf v2 consent string using range" => sub { - plan tests => 21; + plan tests => 22; my $consent; @@ -173,7 +176,10 @@ subtest "valid tcf v2 consent string using range" => sub { is $consent->publisher_country_code, "AA", 'should return the publisher country code "AA"'; - is $consent->max_vendor_id, 626, "max vendor id is 626"; + is $consent->max_vendor_id_consent, 626, "max vendor id consent is 626"; + + is $consent->max_vendor_id_legitimate_interest, 628, + "max vendor id legitimate interest is 628"; subtest "check purpose consent ids" => sub { plan tests => 24;