Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to publisher tc #22

Merged
merged 13 commits into from
Dec 15, 2023
1 change: 1 addition & 0 deletions Changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- parse publisher tc section if available
- add strict mode (disabled by default) to validate the consent string version

0.084
Expand Down
27 changes: 26 additions & 1 deletion README.pod
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,12 @@ For the avoidance of doubt:

In case a vendor has declared flexibility for a purpose and there is no legal basis restriction signal it must always apply the default legal basis under which the purpose was registered aside from being registered as flexible. That means if a vendor declared a purpose as legitimate interest and also declared that purpose as flexible it may not apply a "consent" signal without a legal basis restriction signal to require consent.

=head2 publisher_tc

If the consent string has a C<Publisher TC> section, we will decode this section as an instance of L<GDPR::IAB::TCFv2::PublisherTC>.

Will return undefined if there is no C<Publisher TC> section.

=head2 TO_JSON

Will serialize the consent object into a hash reference. The objective is to be used by L<JSON> package.
Expand All @@ -356,7 +362,7 @@ With option C<convert_blessed>, the encoder will call this method.
use GDPR::IAB::TCFv2;

my $consent = GDPR::IAB::TCFv2->Parse(
'COyiILmOyiILmADACHENAPCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAAAAAA',
'COyiILmOyiILmADACHENAPCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAAAAAA.argAC0gAAAAAAAAAAAA',
json => {
compact => 1,
date_format => sub { # can be omitted, with DateTimeX::TO_JSON
Expand Down Expand Up @@ -385,6 +391,25 @@ Outputs:
"cmp_id" : 3,
"purpose_one_treatment" : false,
"publisher" : {
"consents" : [
2,
4,
6,
8,
9,
10
],
"legitimate_interests" : [
2,
4,
5,
7,
10
],
"custom_purpose" : {
"consents" : [],
"legitimate_interests" : []
},
"restrictions" : {}
},
"special_features_opt_in" : [],
Expand Down
145 changes: 83 additions & 62 deletions lib/GDPR/IAB/TCFv2.pm
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use GDPR::IAB::TCFv2::BitUtils qw<is_set
get_uint36
get_char6_pair
>;
use GDPR::IAB::TCFv2::PublisherRestrictions;
use GDPR::IAB::TCFv2::Publisher;
use GDPR::IAB::TCFv2::RangeSection;

our $VERSION = "0.084";
Expand All @@ -31,7 +31,6 @@ use constant {
MIN_BYTE_SIZE => 29,
},
EXPECTED_TCF_V2_VERSION => 2,
ASSUMED_MAX_VENDOR_ID => 0x7FFF, # 32767 or (1 << 15) -1
MAX_SPECIAL_FEATURE_ID => 12,
MAX_PURPOSE_ID => 24,
DATE_FORMAT_ISO_8601 => '%Y-%m-%dT%H:%M:%SZ',
Expand Down Expand Up @@ -117,7 +116,7 @@ sub Parse {

vendor_consents => undef,
vendor_legitimate_interests => undef,
publisher_restrictions => undef,
publisher => undef,
};

bless $self, $klass;
Expand Down Expand Up @@ -332,8 +331,14 @@ sub vendor_legitimate_interest {
sub check_publisher_restriction {
my ( $self, $purpose_id, $restrict_type, $vendor ) = @_;

return $self->{publisher_restrictions}
->contains( $purpose_id, $restrict_type, $vendor );
return $self->{publisher}
->check_restriction( $purpose_id, $restrict_type, $vendor );
}

sub publisher_tc {
my $self = shift;

return $self->{publisher}->publisher_tc;
}

sub _format_date {
Expand Down Expand Up @@ -422,16 +427,22 @@ sub TO_JSON {
legitimate_interests =>
$self->{vendor_legitimate_interests}->TO_JSON,
},
publisher => {
restrictions => $self->{publisher_restrictions}->TO_JSON,
},
publisher => $self->{publisher}->TO_JSON,
};
}

sub _decode_tc_string_segments {
my $tc_string = shift;

my (@parts) = split CONSENT_STRING_TCF_V2->{SEPARATOR}, $tc_string;
my ( $core, @parts ) = split CONSENT_STRING_TCF_V2->{SEPARATOR},
$tc_string;

my $core_data = _validate_and_decode_base64($core);
my $core_data_size = length($core_data) / 8;

croak
"vendor consent strings are at least @{[ CONSENT_STRING_TCF_V2->{MIN_BYTE_SIZE} ]} bytes long (got ${core_data_size} bytes)"
if $core_data_size < CONSENT_STRING_TCF_V2->{MIN_BYTE_SIZE};

my %segments;

Expand All @@ -443,19 +454,9 @@ sub _decode_tc_string_segments {
$segments{$segment_type} = $decoded;
}

croak "missing core section"
unless exists $segments{ SEGMENT_TYPES->{CORE} };

my $core_data = $segments{ SEGMENT_TYPES->{CORE} };
my $disclosed_vendors = $segments{ SEGMENT_TYPES->{DISCLOSED_VENDORS} };
my $publisher_tc = $segments{ SEGMENT_TYPES->{PUBLISHER_TC} };

my $core_data_size = length($core_data) / 8;

croak
"vendor consent strings are at least @{[ CONSENT_STRING_TCF_V2->{MIN_BYTE_SIZE} ]} bytes long (got ${core_data_size} bytes)"
if $core_data_size < CONSENT_STRING_TCF_V2->{MIN_BYTE_SIZE};

# return hashref
return {
core_data => $core_data,
Expand Down Expand Up @@ -531,75 +532,70 @@ sub _parse_vendor_legitimate_interests {
return $pub_restrict_offset;
}

sub _parse_publisher_section {
my ( $self, $pub_restrict_offset ) = @_;

# parse public restrictions

my $core_data = substr( $self->{core_data}, $pub_restrict_offset );
my $core_data_size = length( $self->{core_data} );

my $publisher = GDPR::IAB::TCFv2::Publisher->Parse(
core_data => $core_data,
core_data_size => $core_data_size,
publisher_tc_data => $self->{publisher_tc_data},
options => $self->{options},
);

$self->{publisher} = $publisher;
}

sub _parse_disclosed_vendors {
my $self = shift;

# TODO parse section disclosed vendors if available

return unless defined $self->{disclosed_vendors_data}; # if avaliable

# my $disclosed_vendors = $self->_parse_bitfield_or_range(0, 'disclosed_vendors_data');

# $self->{disclosed_vendors} = $disclosed_vendors;
}

sub _parse_bitfield_or_range {
my ( $self, $offset ) = @_;
my ( $self, $offset, $section ) = @_;

$section ||= q<core_data>;

my $something;

my ( $max_id, $next_offset ) = get_uint16( $self->{core_data}, $offset );
my ( $max_id, $next_offset ) = get_uint16( $self->{$section}, $offset );

my $is_range;

( $is_range, $next_offset ) = is_set(
$self->{core_data},
$self->{$section},
$next_offset,
);

if ($is_range) {
( $something, $next_offset ) = $self->_parse_range_section(
$max_id,
$next_offset,
$section,
);
}
else {
( $something, $next_offset ) = $self->_parse_bitfield(
$max_id,
$next_offset,
$section,
);
}

return wantarray ? ( $something, $next_offset ) : $something;
}

sub _parse_publisher_section {
my ( $self, $pub_restrict_offset ) = @_;

$self->_parse_publisher_restrictions($pub_restrict_offset);

# TODO parse section publisher_tc if available

# $self->{publisher_tc_data}; # if avaliable
}

sub _parse_publisher_restrictions {
my ( $self, $pub_restrict_offset ) = @_;

my $data = substr(
$self->{core_data}, $pub_restrict_offset,
ASSUMED_MAX_VENDOR_ID
);

my ( $publisher_restrictions, $relative_next_offset ) =
GDPR::IAB::TCFv2::PublisherRestrictions->Parse(
data => $data,
data_size => length( $self->{core_data} ),
max_id => ASSUMED_MAX_VENDOR_ID,
options => $self->{options},
);

$self->{publisher_restrictions} = $publisher_restrictions;

return $pub_restrict_offset + $relative_next_offset;
}

sub _parse_disclosed_vendors {
my $self = shift;

# TODO parse section disclosed vendors if available

# $self->{disclosed_vendors_data}; # if avaliable
}

sub _parse_range_section {
my ( $self, $max_id, $range_section_start_offset, $section ) = @_;

Expand Down Expand Up @@ -995,6 +991,12 @@ For the avoidance of doubt:

In case a vendor has declared flexibility for a purpose and there is no legal basis restriction signal it must always apply the default legal basis under which the purpose was registered aside from being registered as flexible. That means if a vendor declared a purpose as legitimate interest and also declared that purpose as flexible it may not apply a "consent" signal without a legal basis restriction signal to require consent.

=head2 publisher_tc

If the consent string has a C<Publisher TC> section, we will decode this section as an instance of L<GDPR::IAB::TCFv2::PublisherTC>.

Will return undefined if there is no C<Publisher TC> section.

=head2 TO_JSON

Will serialize the consent object into a hash reference. The objective is to be used by L<JSON> package.
Expand All @@ -1011,7 +1013,7 @@ With option C<convert_blessed>, the encoder will call this method.
use GDPR::IAB::TCFv2;

my $consent = GDPR::IAB::TCFv2->Parse(
'COyiILmOyiILmADACHENAPCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAAAAAA',
'COyiILmOyiILmADACHENAPCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAAAAAA.argAC0gAAAAAAAAAAAA',
json => {
compact => 1,
date_format => sub { # can be omitted, with DateTimeX::TO_JSON
Expand Down Expand Up @@ -1040,6 +1042,25 @@ Outputs:
"cmp_id" : 3,
"purpose_one_treatment" : false,
"publisher" : {
"consents" : [
2,
4,
6,
8,
9,
10
],
"legitimate_interests" : [
2,
4,
5,
7,
10
],
"custom_purpose" : {
"consents" : [],
"legitimate_interests" : []
},
"restrictions" : {}
},
"special_features_opt_in" : [],
Expand Down
60 changes: 33 additions & 27 deletions lib/GDPR/IAB/TCFv2/BitField.pm
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use Carp qw<croak>;
sub Parse {
my ( $klass, %args ) = @_;

croak "missing 'data'" unless defined $args{data};
croak "missing 'data'" unless defined $args{data};
croak "missing 'data_size'" unless defined $args{data_size};
croak "missing 'max_id'"
unless defined $args{max_id};

Expand Down Expand Up @@ -80,24 +81,6 @@ sub TO_JSON {
};
}

sub _format_json_subsection2 {
my ( $self, $data, $max ) = @_;

my ( $false, $true ) = @{ $self->{options}->{json}->{boolean_values} };

if ( !!$self->{options}->{json}->{compact} ) {
return [
grep { $data->{$_} } 1 .. $max,
];
}

my $verbose = !!$self->{options}->{json}->{verbose};

return $data if $verbose;

return { map { $_ => $true } grep { $data->{$_} } keys %{$data} };
}

1;
__END__

Expand All @@ -112,22 +95,37 @@ GDPR::IAB::TCFv2::BitField - Transparency & Consent String version 2 bitfield pa
my $max_id_consent = << get 16 bits from $data offset 213 >>

my $bit_field = GDPR::IAB::TCFv2::BitField->Parse(
data => $data,
offset => 230, # offset for vendor consents
max_id => $max_id_consent,
data => substr($data, OFFSET),
data_size => length($data),
max_id => $max_id_consent,
options => { json => ... },
);

if $bit_field->contains(284) { ... }
say "bit field contains id 284" if $bit_field->contains(284);

=head1 CONSTRUCTOR

Constructor C<Parse> receive 3 parameters: data (as sequence of bits), start bit offset and vendor bits required (max vendor id).
Constructor C<Parse> receives an hash of 4 parameters:

=over

Will die if any parameter is missing.
=item *

Will die if data does not contain all bits required.
Key C<data> is the binary data

Will return an array of two elements: the object itself and the next offset.
=item *

Key C<data_size> is the original binary data size

=item *

Key C<max_id> is the max id (used to validate the ranges if all data is between 1 and C<max_id>)

=item *

Key C<options> is the L<GDPR::IAB::TCFv2> options (includes the C<json> field to modify the L</TO_JSON> method output.

=back

=head1 METHODS

Expand All @@ -145,3 +143,11 @@ Returns the max vendor id.
=head2 all

Returns an array of all vendors mapped with the bit enabled.

=head2 TO_JSON

By default it returns an hashref mapping id to a boolean, that represent if the id is active or not in the bitfield.

The json option C<verbose> controls if all ids between 1 to L</max_id> will be present on the C<json> or only the ones that are true.

The json option C<compact> change the response, will return an arrayref of all ids active on the bitfield.
Loading