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;