From 78fdb32af8612126f9a275dd700463f53b2b641f Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 11:29:53 +0100 Subject: [PATCH 1/8] Add range section cache (#26) * add initial code * reorg code * rename test * add unit tests * add pod * update readme --- README.pod | 25 +- lib/GDPR/IAB/TCFv2.pm | 33 +- lib/GDPR/IAB/TCFv2/RangeSection.pm | 83 ++-- t/01-parse.t | 670 +++++++++++++++------------- t/{02-json.t => 02-json-bitfield.t} | 335 +++++++------- 5 files changed, 648 insertions(+), 498 deletions(-) rename t/{02-json.t => 02-json-bitfield.t} (69%) diff --git a/README.pod b/README.pod index 2b7df78..6a1a83d 100644 --- a/README.pod +++ b/README.pod @@ -114,14 +114,33 @@ or date_format => '%Y%m%d', # yyymmdd }, strict => 1, + prefetch => 284, ); -Parse may receive an optional hash of parameters: C (boolean) and C (hashref with the following properties): +Parse may receive an optional hash with the following parameters: =over =item * +On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). + +The C mode is disabled by default. + +=item * + +The C option receives one (as scalar) or more (as arrayref) vendor ids. + +This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id. + +=item * + +C is hashref with the following properties used to customize the json format: + +=over + +=item * + C changes the json encoding. By default we omit some false values such as C to create a compact json representation. With C we will present everything. See L for more details. @@ -150,9 +169,7 @@ except if the option C is true. =back -On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). - -The C mode is disabled by default. +=back =head1 METHODS diff --git a/lib/GDPR/IAB/TCFv2.pm b/lib/GDPR/IAB/TCFv2.pm index e82d769..b7f0511 100644 --- a/lib/GDPR/IAB/TCFv2.pm +++ b/lib/GDPR/IAB/TCFv2.pm @@ -106,6 +106,14 @@ sub Parse { $options{json}->{date_format} ||= DATE_FORMAT_ISO_8601; $options{json}->{boolean_values} ||= [ _json_false(), _json_true() ]; + if ( exists $opts{prefetch} ) { + my $prefetch = $opts{prefetch}; + + $prefetch = [$prefetch] if ref($prefetch) ne ref( [] ); + + $options{prefetch} = $prefetch; + } + my $self = { core_data => $segments->{core_data}, publisher_tc_data => $segments->{publisher_tc}, @@ -740,14 +748,33 @@ or date_format => '%Y%m%d', # yyymmdd }, strict => 1, + prefetch => 284, ); -Parse may receive an optional hash of parameters: C (boolean) and C (hashref with the following properties): +Parse may receive an optional hash with the following parameters: =over =item * +On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). + +The C mode is disabled by default. + +=item * + +The C option receives one (as scalar) or more (as arrayref) vendor ids. + +This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id. + +=item * + +C is hashref with the following properties used to customize the json format: + +=over + +=item * + C changes the json encoding. By default we omit some false values such as C to create a compact json representation. With C we will present everything. See L for more details. @@ -776,9 +803,7 @@ except if the option C is true. =back -On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). - -The C mode is disabled by default. +=back =head1 METHODS diff --git a/lib/GDPR/IAB/TCFv2/RangeSection.pm b/lib/GDPR/IAB/TCFv2/RangeSection.pm index 3203df5..3aedcf8 100644 --- a/lib/GDPR/IAB/TCFv2/RangeSection.pm +++ b/lib/GDPR/IAB/TCFv2/RangeSection.pm @@ -29,41 +29,58 @@ sub Parse { "a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size" if $data_size < 31; - my ( $num_entries, $next_offset ) = get_uint12( $data, $offset ); + my %prefetch; + my %cache; - my @ranges; + if ( exists $options->{prefetch} ) { + my $vendor_ids = $options->{prefetch}; - foreach my $i ( 1 .. $num_entries ) { - my $range; - ( $range, $next_offset ) = _parse_range( - $data, - $data_size, - $next_offset, - $max_id, - $options, - ); - - push @ranges, $range; + foreach my $vendor_id ( @{$vendor_ids} ) { + $prefetch{$vendor_id} = 1; + $cache{$vendor_id} = 0; + } } my $self = { - ranges => \@ranges, + ranges => [], + cache => \%cache, max_id => $max_id, options => $options, }; bless $self, $klass; + my $next_offset = $self->_parse( $data, $data_size, $offset, \%prefetch ); + return ( $self, $next_offset ); } +sub _parse { + my ( $self, $data, $data_size, $offset, $prefetch ) = @_; + + my ( $num_entries, $next_offset ) = get_uint12( $data, $offset ); + + foreach my $i ( 1 .. $num_entries ) { + $next_offset = $self->_parse_range( + $data, + $data_size, + $next_offset, + $prefetch, + ); + } + + return $next_offset; +} + sub _parse_range { - my ( $data, $data_size, $offset, $max_id, $options ) = @_; + my ( $self, $data, $data_size, $offset, $prefetch ) = @_; croak "bit $offset was suppose to start a new range entry, but the consent string was only $data_size bytes long" if $data_size <= $offset / 8; + my $max_id = $self->{max_id}; + # If the first bit is set, it's a Range of IDs my ( $is_range, $next_offset ) = is_set $data, $offset; if ($is_range) { @@ -82,8 +99,14 @@ sub _parse_range { croak "start $start can't be bigger than end $end" if $start > $end; - return [ $start, $end ], - $next_offset; + push @{ $self->{ranges} }, [ $start, $end ]; + + foreach my $vendor_id ( keys %{$prefetch} ) { + $self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} ) + if $start <= $vendor_id && $vendor_id <= $end; + } + + return $next_offset; } my $vendor_id; @@ -94,7 +117,12 @@ sub _parse_range { "bit $offset range entry exclusion vendor $vendor_id, but only vendors [1, $max_id] are valid" if 1 > $vendor_id || $vendor_id > $max_id; - return [ $vendor_id, $vendor_id ], $next_offset; + push @{ $self->{ranges} }, [ $vendor_id, $vendor_id ]; + + $self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} ) + if exists $prefetch->{$vendor_id}; + + return $next_offset; } sub max_id { @@ -109,6 +137,8 @@ sub contains { croak "invalid vendor id $id: must be positive integer bigger than 0" if $id < 1; + return $self->{cache}->{$id} if exists $self->{cache}->{$id}; + return if $id > $self->{max_id}; foreach my $range ( @{ $self->{ranges} } ) { @@ -132,15 +162,7 @@ sub all { sub TO_JSON { my $self = shift; - if ( !!$self->{options}->{json}->{compact} ) { - my @vendors; - - foreach my $range ( @{ $self->{ranges} } ) { - push @vendors, $range->[0] .. $range->[1]; - } - - return \@vendors; - } + return $self->all if !!$self->{options}->{json}->{compact}; my ( $false, $true ) = @{ $self->{options}->{json}->{boolean_values} }; @@ -174,13 +196,14 @@ GDPR::IAB::TCFv2::RangeSection - Transparency & Consent String version 2 range s data_size => length($data), offset => 230, # offset for vendor ranges max_id => $max_id_consent, + prefetch => 284, # will cache the result of vendor id 284 ); say "range section contains id 284" if $range_section->contains(284); =head1 CONSTRUCTOR -Constructor C receives an hash of 5 parameters: +Constructor C receives an hash parameters: =over @@ -204,6 +227,10 @@ Key C is the max id (used to validate the ranges if all data is between Key C is the L options (includes the C field to modify the L method output. +=item * + +Key C is an optional arrayref of vendor ids to populate the result as cache. + =back Will die if any parameter is missing. diff --git a/t/01-parse.t b/t/01-parse.t index bfd68b5..f938ee6 100644 --- a/t/01-parse.t +++ b/t/01-parse.t @@ -6,223 +6,312 @@ use Test::Exception; use GDPR::IAB::TCFv2; -subtest "valid tcf v2 consent string using bitfield" => sub { - my $consent; +subtest "bitfield" => sub { + subtest "valid tcf v2 consent string using bitfield" => sub { + my $consent; - my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); + } + 'should not throw exception'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; - is $consent->tc_string, $tc_string, 'should return the original tc string'; + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - is $consent->version, 2, 'should return version 2'; + is $consent->version, 2, 'should return version 2'; - is $consent->created, 1228644257, - 'should return the creation epoch 07/12/2008'; + is $consent->created, 1228644257, + 'should return the creation epoch 07/12/2008'; - { - my ( $seconds, $nanoseconds ) = $consent->created; - is $seconds, 1228644257, - 'should return the creation epoch 07/12/2008 on list context'; - is $nanoseconds, 700000000, - 'should return the 700000000 nanoseconds of epoch on list context'; - } + { + my ( $seconds, $nanoseconds ) = $consent->created; + is $seconds, 1228644257, + 'should return the creation epoch 07/12/2008 on list context'; + is $nanoseconds, 700000000, + 'should return the 700000000 nanoseconds of epoch on list context'; + } - is $consent->last_updated, 1326215413, - 'should return the last update epoch 10/01/2012'; + is $consent->last_updated, 1326215413, + 'should return the last update epoch 10/01/2012'; - { - my ( $seconds, $nanoseconds ) = $consent->last_updated; - is $seconds, 1326215413, - 'should return the last updated epoch 07/12/2008 on list context'; - is $nanoseconds, 400000000, - 'should return the 400000000 nanoseconds of epoch on list context'; - } + { + my ( $seconds, $nanoseconds ) = $consent->last_updated; + is $seconds, 1326215413, + 'should return the last updated epoch 07/12/2008 on list context'; + is $nanoseconds, 400000000, + 'should return the 400000000 nanoseconds of epoch on list context'; + } - is $consent->cmp_id, 21, 'should return the cmp id 21'; + is $consent->cmp_id, 21, 'should return the cmp id 21'; - is $consent->cmp_version, 7, 'should return the cmp version 7'; + is $consent->cmp_version, 7, 'should return the cmp version 7'; - is $consent->consent_screen, 2, 'should return the consent screen 2'; + is $consent->consent_screen, 2, 'should return the consent screen 2'; - is $consent->consent_language, "EN", - 'should return the consent language "EN"'; + is $consent->consent_language, "EN", + 'should return the consent language "EN"'; - is $consent->vendor_list_version, 23, - 'should return the vendor list version 23'; + is $consent->vendor_list_version, 23, + 'should return the vendor list version 23'; - is $consent->policy_version, 2, - 'should return the policy version 2'; + is $consent->policy_version, 2, + 'should return the policy version 2'; - ok $consent->is_service_specific, - 'should return true for service specific'; + ok $consent->is_service_specific, + 'should return true for service specific'; - ok !$consent->use_non_standard_stacks, - 'should return false for use non standard stacks'; + ok !$consent->use_non_standard_stacks, + 'should return false for use non standard stacks'; - ok !$consent->purpose_one_treatment, - 'should return false for use purpose one treatment'; + ok !$consent->purpose_one_treatment, + 'should return false for use purpose one treatment'; - is $consent->publisher_country_code, "KM", - 'should return the publisher country code "KM"'; + is $consent->publisher_country_code, "KM", + 'should return the publisher country code "KM"'; - is $consent->max_vendor_id_consent, 115, "max vendor id consent 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"; + is $consent->max_vendor_id_legitimate_interest, 113, + "max vendor id legitimate interest is 113"; - subtest "check purpose consent ids" => sub { - plan tests => 24; + subtest "check purpose consent ids" => sub { + plan tests => 24; - my %allowed_purposes = map { $_ => 1 } ( 1, 3, 9, 10 ); + my %allowed_purposes = map { $_ => 1 } ( 1, 3, 9, 10 ); - foreach my $id ( 1 .. 24 ) { - is !!$consent->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking purpose id $id for consent"; - } - }; + foreach my $id ( 1 .. 24 ) { + is !!$consent->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking purpose id $id for consent"; + } + }; - subtest "check purpose legitimate interest ids" => sub { - plan tests => 24; + subtest "check purpose legitimate interest ids" => sub { + plan tests => 24; - my %allowed_purposes = map { $_ => 1 } ( 3, 4, 5, 8, 9, 10 ); + my %allowed_purposes = map { $_ => 1 } ( 3, 4, 5, 8, 9, 10 ); - foreach my $id ( 1 .. 24 ) { - is !!$consent->is_purpose_legitimate_interest_allowed($id), - !!$allowed_purposes{$id}, - "checking purpose id $id for legitimate interest"; - } - }; + foreach my $id ( 1 .. 24 ) { + is !!$consent->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking purpose id $id for legitimate interest"; + } + }; - subtest "check special feature opt in" => sub { - plan tests => 12; + subtest "check special feature opt in" => sub { + plan tests => 12; - my %special_feature_opt_in = ( - 2 => 1, - ); + my %special_feature_opt_in = ( + 2 => 1, + ); - foreach my $id ( 1 .. 12 ) { - is !!$consent->is_special_feature_opt_in($id), - !!$special_feature_opt_in{$id}, - "checking special feature id $id opt in"; - } - }; + foreach my $id ( 1 .. 12 ) { + is !!$consent->is_special_feature_opt_in($id), + !!$special_feature_opt_in{$id}, + "checking special feature id $id opt in"; + } + }; - subtest "check vendor consent ids" => sub { - plan tests => 120; - - my %allowed_vendors = - map { $_ => 1 } ( - 2, 3, 6, 7, 8, 10, 12, 13, 14, 15, 16, 21, 25, 27, 30, 31, 34, 35, - 37, 38, 39, 42, 43, 49, 52, 54, 55, 56, 57, 59, 60, 63, 64, 65, - 66, 67, 68, 69, 73, 74, 76, 78, 83, 86, 87, 89, 90, 92, 96, 99, - 100, 106, 109, 110, 114, 115 - ); - - foreach my $id ( 1 .. 120 ) { - is !!$consent->vendor_consent($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for consent"; - } - }; + subtest "check vendor consent ids" => sub { + plan tests => 120; + + my %allowed_vendors = + map { $_ => 1 } ( + 2, 3, 6, 7, 8, 10, 12, 13, 14, 15, 16, 21, 25, 27, 30, 31, 34, + 35, + 37, 38, 39, 42, 43, 49, 52, 54, 55, 56, 57, 59, 60, 63, 64, 65, + 66, 67, 68, 69, 73, 74, 76, 78, 83, 86, 87, 89, 90, 92, 96, 99, + 100, 106, 109, 110, 114, 115 + ); + + foreach my $id ( 1 .. 120 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - subtest "check vendor legitimate interest ids" => sub { - plan tests => 120; + subtest "check vendor legitimate interest ids" => sub { + plan tests => 120; - my %allowed_vendors = - map { $_ => 1 } ( 1, 9, 26, 27, 30, 36, 37, 43, 86, 97, 110, 113 ); + my %allowed_vendors = + map { $_ => 1 } + ( 1, 9, 26, 27, 30, 36, 37, 43, 86, 97, 110, 113 ); - foreach my $id ( 1 .. 120 ) { - is !!$consent->vendor_legitimate_interest($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for legitimate interest"; - } + foreach my $id ( 1 .. 120 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; + + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + + my $publisher_tc = $consent->publisher_tc; + + ok !defined($publisher_tc), "should not return publisher_tc"; + + done_testing; }; - ok !$consent->check_publisher_restriction( 1, 0, 284 ), - "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; - my $publisher_tc = $consent->publisher_tc; + subtest + "valid tcf v2 consent string using bitfield with publisher TC section" + => sub { - ok !defined($publisher_tc), "should not return publisher_tc"; + subtest "without custom purposes" => sub { + my $consent; - done_testing; -}; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.argAC0gAAAAAAAAAAAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); + } + 'should not throw exception'; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; -subtest - "valid tcf v2 consent string using bitfield with publisher TC section" => - sub { + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - subtest "without custom purposes" => sub { - my $consent; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.argAC0gAAAAAAAAAAAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + is $consent->version, 2, 'should return version 2'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + my $publisher_tc = $consent->publisher_tc; - is $consent->tc_string, $tc_string, - 'should return the original tc string'; + ok defined($publisher_tc), "should return publisher_tc"; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + is $publisher_tc->num_custom_purposes, 0, + "should not have any custom purposes"; - is $consent->version, 2, 'should return version 2'; + subtest "check publisher purpose consent ids" => sub { + plan tests => 24; - my $publisher_tc = $consent->publisher_tc; + my %allowed_purposes = map { $_ => 1 } ( 2, 4, 6, 8, 9, 10 ); - ok defined($publisher_tc), "should return publisher_tc"; + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for consent"; + } + }; - is $publisher_tc->num_custom_purposes, 0, - "should not have any custom purposes"; + subtest "check publisher purpose legitimate interest ids" => sub { + plan tests => 24; - subtest "check publisher purpose consent ids" => sub { - plan tests => 24; + my %allowed_purposes = map { $_ => 1 } ( 2, 4, 5, 7, 10 ); - my %allowed_purposes = map { $_ => 1 } ( 2, 4, 6, 8, 9, 10 ); + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc + ->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for legitimate interest"; + } + }; - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for consent"; - } + done_testing; }; - subtest "check publisher purpose legitimate interest ids" => sub { - plan tests => 24; - - my %allowed_purposes = map { $_ => 1 } ( 2, 4, 5, 7, 10 ); + subtest "with custom purposes" => sub { + my $consent; - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_legitimate_interest_allowed( - $id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for legitimate interest"; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.YAAAAAAAAXA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); } + 'should not throw exception'; + + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + + is $consent->tc_string, $tc_string, + 'should return the original tc string'; + + is "${consent}", $tc_string, + 'should return the original tc string in string context'; + + is $consent->version, 2, 'should return version 2'; + + my $publisher_tc = $consent->publisher_tc; + + ok defined($publisher_tc), "should return publisher_tc"; + + is $publisher_tc->num_custom_purposes, 2, + "should have 2 custom purposes"; + + subtest "check publisher purpose consent ids" => sub { + plan tests => 24; + + my %allowed_purposes; + + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for consent"; + } + }; + + subtest "check publisher purpose legitimate interest ids" => sub { + plan tests => 24; + + my %allowed_purposes; + + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc + ->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for legitimate interest"; + } + }; + + + subtest "check publisher custom purpose consent ids" => sub { + plan tests => 2; + + ok $publisher_tc->is_custom_purpose_consent_allowed(1), + "should have custom purpose 1 allowed"; + ok $publisher_tc->is_custom_purpose_consent_allowed(2), + "should have custom purpose 2 allowed"; + }; + + subtest + "check publisher custom purpose legitimate interest ids" => sub { + plan tests => 2; + + ok $publisher_tc + ->is_custom_purpose_legitimate_interest_allowed(1), + "should have custom purpose 1 allowed"; + ok !$publisher_tc + ->is_custom_purpose_legitimate_interest_allowed(2), + "should not have custom purpose 2 allowed"; + }; + + done_testing; }; done_testing; - }; + }; + done_testing; - subtest "with custom purposes" => sub { +}; + +subtest "range" => sub { + subtest "valid tcf v2 consent string using range" => sub { my $consent; my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.YAAAAAAAAXA'; + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; lives_ok { $consent = GDPR::IAB::TCFv2->Parse($tc_string); } @@ -238,201 +327,182 @@ subtest is $consent->version, 2, 'should return version 2'; - my $publisher_tc = $consent->publisher_tc; - - ok defined($publisher_tc), "should return publisher_tc"; - - is $publisher_tc->num_custom_purposes, 2, - "should have 2 custom purposes"; - - subtest "check publisher purpose consent ids" => sub { - plan tests => 24; - - my %allowed_purposes; - - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for consent"; - } - }; - - subtest "check publisher purpose legitimate interest ids" => sub { - plan tests => 24; - - my %allowed_purposes; - - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_legitimate_interest_allowed( - $id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for legitimate interest"; - } - }; + is $consent->created, 1587946020, + 'should return the creation epoch 27/04/2020 on scalar context'; + { + my ( $seconds, $nanoseconds ) = $consent->created; + is $seconds, 1587946020, + 'should return the creation epoch 27/04/2020 on list context'; + is $nanoseconds, 0, + 'should return the 0 nanoseconds of epoch on list context'; + } - subtest "check publisher custom purpose consent ids" => sub { - plan tests => 2; + is $consent->last_updated, 1587946020, + 'should return the last update epoch 27/04/2020'; - ok $publisher_tc->is_custom_purpose_consent_allowed(1), - "should have custom purpose 1 allowed"; - ok $publisher_tc->is_custom_purpose_consent_allowed(2), - "should have custom purpose 2 allowed"; - }; - - subtest "check publisher custom purpose legitimate interest ids" => - sub { - plan tests => 2; + { + my ( $seconds, $nanoseconds ) = $consent->last_updated; + is $seconds, 1587946020, + 'should return the last update epoch 27/04/2020 on list context'; + is $nanoseconds, 0, + 'should return the 0 nanoseconds of epoch on list context'; + } - ok $publisher_tc->is_custom_purpose_legitimate_interest_allowed(1), - "should have custom purpose 1 allowed"; - ok !$publisher_tc->is_custom_purpose_legitimate_interest_allowed( - 2), "should not have custom purpose 2 allowed"; - }; + is $consent->cmp_id, 3, 'should return the cmp id 3'; - done_testing; - }; + is $consent->cmp_version, 2, 'should return the cmp version 2'; - done_testing; - }; + is $consent->consent_screen, 7, 'should return the consent screen 7'; + is $consent->consent_language, "EN", + 'should return the consent language "EN"'; -subtest "valid tcf v2 consent string using range" => sub { - my $consent; + is $consent->vendor_list_version, 48, + 'should return the vendor list version 23'; - my $tc_string = - 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + is $consent->policy_version, 2, + 'should return the policy version 2'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + ok !$consent->is_service_specific, + 'should return true for service specific'; - is $consent->tc_string, $tc_string, 'should return the original tc string'; + ok !$consent->use_non_standard_stacks, + 'should return false for use non standard stacks'; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + ok !$consent->purpose_one_treatment, + 'should return false for use purpose one treatment'; - is $consent->version, 2, 'should return version 2'; + is $consent->publisher_country_code, "AA", + 'should return the publisher country code "AA"'; - is $consent->created, 1587946020, - 'should return the creation epoch 27/04/2020 on scalar context'; + is $consent->max_vendor_id_consent, 626, + "max vendor id consent is 626"; - { - my ( $seconds, $nanoseconds ) = $consent->created; - is $seconds, 1587946020, - 'should return the creation epoch 27/04/2020 on list context'; - is $nanoseconds, 0, - 'should return the 0 nanoseconds of epoch on list context'; - } + is $consent->max_vendor_id_legitimate_interest, 628, + "max vendor id legitimate interest is 628"; - is $consent->last_updated, 1587946020, - 'should return the last update epoch 27/04/2020'; + subtest "check purpose consent ids" => sub { + plan tests => 24; - { - my ( $seconds, $nanoseconds ) = $consent->last_updated; - is $seconds, 1587946020, - 'should return the last update epoch 27/04/2020 on list context'; - is $nanoseconds, 0, - 'should return the 0 nanoseconds of epoch on list context'; - } + foreach my $id ( 1 .. 24 ) { + ok !$consent->is_purpose_consent_allowed($id), + "checking purpose id $id for consent"; + } + }; - is $consent->cmp_id, 3, 'should return the cmp id 3'; + subtest "check purpose legitimate interest ids" => sub { + plan tests => 24; - is $consent->cmp_version, 2, 'should return the cmp version 2'; + foreach my $id ( 1 .. 24 ) { + ok !$consent->is_purpose_legitimate_interest_allowed($id), + "checking purpose id $id for legitimate interest"; + } + }; - is $consent->consent_screen, 7, 'should return the consent screen 7'; + subtest "check special feature opt in" => sub { + plan tests => 12; - is $consent->consent_language, "EN", - 'should return the consent language "EN"'; + foreach my $id ( 1 .. 12 ) { + ok !$consent->is_special_feature_opt_in($id), + "checking special feature id $id opt in"; + } + }; - is $consent->vendor_list_version, 48, - 'should return the vendor list version 23'; + subtest "check vendor consent ids" => sub { + plan tests => 626; - is $consent->policy_version, 2, - 'should return the policy version 2'; + my %allowed_vendors = + map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); - ok !$consent->is_service_specific, - 'should return true for service specific'; + foreach my $id ( 1 .. 626 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - ok !$consent->use_non_standard_stacks, - 'should return false for use non standard stacks'; + subtest "check vendor legitimate interest ids" => sub { + plan tests => 628; - ok !$consent->purpose_one_treatment, - 'should return false for use purpose one treatment'; + my %allowed_vendors = + map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); - is $consent->publisher_country_code, "AA", - 'should return the publisher country code "AA"'; + foreach my $id ( 1 .. 628 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; - is $consent->max_vendor_id_consent, 626, "max vendor id consent is 626"; + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; - is $consent->max_vendor_id_legitimate_interest, 628, - "max vendor id legitimate interest is 628"; + my $publisher_tc = $consent->publisher_tc; - subtest "check purpose consent ids" => sub { - plan tests => 24; + ok !defined($publisher_tc), "should not return publisher_tc"; - foreach my $id ( 1 .. 24 ) { - ok !$consent->is_purpose_consent_allowed($id), - "checking purpose id $id for consent"; - } + done_testing; }; + subtest + "valid tcf v2 consent string using range and prefetch some vendor ids" + => sub { + my $consent; - subtest "check purpose legitimate interest ids" => sub { - plan tests => 24; - - foreach my $id ( 1 .. 24 ) { - ok !$consent->is_purpose_legitimate_interest_allowed($id), - "checking purpose id $id for legitimate interest"; + my $tc_string = + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse( + $tc_string, + prefetch => [ 1, 20 .. 24, 284, 626, 268 ] + ); } - }; + 'should not throw exception'; - subtest "check special feature opt in" => sub { - plan tests => 12; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; - foreach my $id ( 1 .. 12 ) { - ok !$consent->is_special_feature_opt_in($id), - "checking special feature id $id opt in"; - } - }; + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - subtest "check vendor consent ids" => sub { - plan tests => 626; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - my %allowed_vendors = - map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); + is $consent->version, 2, 'should return version 2'; - foreach my $id ( 1 .. 626 ) { - is !!$consent->vendor_consent($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for consent"; - } - }; - subtest "check vendor legitimate interest ids" => sub { - plan tests => 628; + subtest "check vendor consent ids" => sub { + plan tests => 626; - my %allowed_vendors = - map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); + my %allowed_vendors = + map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); - foreach my $id ( 1 .. 628 ) { - is !!$consent->vendor_legitimate_interest($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for legitimate interest"; - } - }; + foreach my $id ( 1 .. 626 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - ok !$consent->check_publisher_restriction( 1, 0, 284 ), - "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + subtest "check vendor legitimate interest ids" => sub { + plan tests => 628; - my $publisher_tc = $consent->publisher_tc; + my %allowed_vendors = + map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); - ok !defined($publisher_tc), "should not return publisher_tc"; + foreach my $id ( 1 .. 628 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + + done_testing; + }; done_testing; }; - subtest "check publisher restriction" => sub { subtest "check publisher restriction #1" => sub { my $consent; diff --git a/t/02-json.t b/t/02-json-bitfield.t similarity index 69% rename from t/02-json.t rename to t/02-json-bitfield.t index 3cb6f34..f30129e 100644 --- a/t/02-json.t +++ b/t/02-json-bitfield.t @@ -9,160 +9,199 @@ use GDPR::IAB::TCFv2; # use DateTime; # use DateTimeX::TO_JSON formatter => 'DateTime::Format::RFC3339'; -subtest - "should convert data to json using compact flag and 0/1 as booleans" => sub { - subtest "should convert data to json using yyyymmdd as date format" => +subtest "bitfield" => sub { + subtest + "should convert data to json using compact flag and 0/1 as booleans" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - date_format => '%Y%m%d', # yyymmdd - }, - ); - - - my $got = $consent->TO_JSON(); - my $expected = _fixture_compact( - created => 20081207, - last_updated => 20120110, - ); - is_deeply $got, $expected, "must return the json hashref"; - - done_testing; - }; + subtest "should convert data to json using yyyymmdd as date format" => + sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + date_format => '%Y%m%d', # yyymmdd + }, + ); + + + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_compact( + created => 20081207, + last_updated => 20120110, + ); + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "should convert data to json using epoch date format" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 1, + boolean_values => [ 0, 1 ], + }, + ); - subtest "should convert data to json using epoch date format" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 1, - boolean_values => [ 0, 1 ], - }, - ); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_compact( + created => 1228644257, + last_updated => 1326215413, + ); - my $got = $consent->TO_JSON(); - my $expected = _fixture_compact( - created => 1228644257, - last_updated => 1326215413, - ); + is_deeply $got, $expected, "must return the json hashref"; - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; done_testing; - }; + }; - done_testing; - }; + subtest + "should convert data to json using default (non-compact) and 0/1 as booleans" + => sub { + + subtest "default non verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); -subtest - "should convert data to json using default (non-compact) and 0/1 as booleans" - => sub { + ok $consent->vendor_consent(27); - subtest "default non verbose, date as iso 8601" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 0, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - ok $consent->vendor_consent(27); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_default(); + is_deeply $got, $expected, "must return the json hashref"; - my $got = $consent->TO_JSON(); - my $expected = _fixture_default(); - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; - done_testing; - }; + subtest "default verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 1, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); - subtest "default non verbose, date as iso 8601" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 1, - compact => 0, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); + ok $consent->vendor_consent(27); - ok $consent->vendor_consent(27); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_verbose(); + is_deeply $got, $expected, "must return the json hashref"; - my $got = $consent->TO_JSON(); - my $expected = _fixture_verbose(); - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; done_testing; - }; - - done_testing; - }; - + }; -subtest "publisher section" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); + subtest "publisher section" => sub { + subtest "publisher section without publisher_tc" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); - my $got = $consent->TO_JSON; - my $expected = - { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } }; + my $got = $consent->TO_JSON; + my $expected = + { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } + }; - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; - done_testing; -}; -subtest "publisher section with publisher_tc" => sub { - subtest "without custom purposes" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - my $got = $consent->TO_JSON; - my $expected = { - "publisher" => { - "consents" => [ 2, 4, 6, 8, 9, 10 ], - "legitimate_interests" => [ 2, 4, 5, 7, 10 ], - "custom_purposes" => { - "consents" => [], - "legitimate_interests" => [], - }, - "restrictions" => { "7" => { "32" => 1 } } - } + done_testing; + }; + subtest "publisher section with publisher_tc" => sub { + subtest "without custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [ 2, 4, 6, 8, 9, 10 ], + "legitimate_interests" => [ 2, 4, 5, 7, 10 ], + "custom_purposes" => { + "consents" => [], + "legitimate_interests" => [], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + subtest "with custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [], + "legitimate_interests" => [], + "custom_purposes" => { + "consents" => [ 1, 2 ], + "legitimate_interests" => [1], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + done_testing; }; - - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; done_testing; }; - subtest "with custom purposes" => sub { + subtest "TO_JSON method should return the same hashref " => sub { my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', json => { verbose => 0, compact => 1, @@ -171,50 +210,22 @@ subtest "publisher section with publisher_tc" => sub { }, ); - my $got = $consent->TO_JSON; - my $expected = { - "publisher" => { - "consents" => [], - "legitimate_interests" => [], - "custom_purposes" => { - "consents" => [ 1, 2 ], - "legitimate_interests" => [1], - }, - "restrictions" => { "7" => { "32" => 1 } } - } - }; - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; + my $got1 = $consent->TO_JSON(); + my $got2 = $consent->TO_JSON(); + + is_deeply $got1, $got2, "must return the same hashref"; done_testing; }; done_testing; -}; -subtest "TO_JSON method should return the same hashref " => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - - my $got1 = $consent->TO_JSON(); - my $got2 = $consent->TO_JSON(); - is_deeply $got1, $got2, "must return the same hashref"; - - done_testing; }; done_testing; -sub _fixture_compact { +sub _fixture_bitfield_compact { my (%extra) = @_; return { @@ -332,7 +343,7 @@ sub _fixture_compact { }; } -sub _fixture_default { +sub _fixture_bitfield_default { my (%extra) = @_; return { @@ -450,7 +461,7 @@ sub _fixture_default { }; } -sub _fixture_verbose { +sub _fixture_bitfield_verbose { my (%extra) = @_; return { From 7f191b75d22e77044756a15adaf782338f9fcea5 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 11:44:10 +0100 Subject: [PATCH 2/8] increase tests --- t/03-json-range.t | 1682 ++++++++++++++++++++++++++++++++++++ t/{03-bugs.t => 90-bugs.t} | 0 2 files changed, 1682 insertions(+) create mode 100644 t/03-json-range.t rename t/{03-bugs.t => 90-bugs.t} (100%) diff --git a/t/03-json-range.t b/t/03-json-range.t new file mode 100644 index 0000000..d25283d --- /dev/null +++ b/t/03-json-range.t @@ -0,0 +1,1682 @@ +use strict; +use warnings; + +use Test::More; + +use GDPR::IAB::TCFv2; + +# use JSON; +# use DateTime; +# use DateTimeX::TO_JSON formatter => 'DateTime::Format::RFC3339'; + +subtest "range" => sub { + subtest + "should convert data to json using compact flag and 0/1 as booleans" => + sub { + subtest "should convert data to json using yyyymmdd as date format" => + sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + date_format => '%Y%m%d', # yyymmdd + }, + ); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_compact( + created => 20200427, + last_updated => 20200427, + ); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "should convert data to json using epoch date format" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 1, + boolean_values => [ 0, 1 ], + }, + ); + + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_compact( + created => 1587946020, + last_updated => 1587946020, + ); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + done_testing; + }; + + subtest + "should convert data to json using default (non-compact) and 0/1 as booleans" + => sub { + + subtest "default non verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + ok $consent->vendor_consent(23); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_default(); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "default verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 1, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + ok $consent->vendor_consent(23); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_verbose(); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + done_testing; + }; + + subtest "publisher section" => sub { + subtest "publisher section without publisher_tc" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = + { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + subtest "publisher section with publisher_tc" => sub { + subtest "without custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [ 2, 4, 6, 8, 9, 10 ], + "legitimate_interests" => [ 2, 4, 5, 7, 10 ], + "custom_purposes" => { + "consents" => [], + "legitimate_interests" => [], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + subtest "with custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [], + "legitimate_interests" => [], + "custom_purposes" => { + "consents" => [ 1, 2 ], + "legitimate_interests" => [1], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + done_testing; + }; + + done_testing; + }; + + subtest "TO_JSON method should return the same hashref " => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + + my $got1 = $consent->TO_JSON(); + my $got2 = $consent->TO_JSON(); + + is_deeply $got1, $got2, "must return the same hashref"; + + done_testing; + }; + + done_testing; + +}; + +done_testing; + +sub _fixture_range_compact { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => [], + 'legitimate_interests' => [] + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => [], + 'last_updated' => '20200427', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '20200427', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => [ + 23, + 42, + 126, + 127, + 128, + 587, + 613, + 626 + ], + 'legitimate_interests' => [ + 24, + 44, + 129, + 130, + 131, + 591, + 614, + 628 + ] + }, + %extra + }; +} + +sub _fixture_range_default { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => {}, + 'legitimate_interests' => {} + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => {}, + 'last_updated' => '2020-04-27T00:07:00Z', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '2020-04-27T00:07:00Z', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => { + '128' => 1, + '613' => 1, + '127' => 1, + '626' => 1, + '42' => 1, + '23' => 1, + '126' => 1, + '587' => 1 + }, + 'legitimate_interests' => { + '628' => 1, + '591' => 1, + '130' => 1, + '131' => 1, + '24' => 1, + '129' => 1, + '44' => 1, + '614' => 1 + } + }, + %extra + }; +} + +sub _fixture_range_verbose { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => { + '11' => 0, + '21' => 0, + '7' => 0, + '17' => 0, + '2' => 0, + '22' => 0, + '1' => 0, + '18' => 0, + '23' => 0, + '16' => 0, + '13' => 0, + '6' => 0, + '3' => 0, + '9' => 0, + '12' => 0, + '20' => 0, + '14' => 0, + '15' => 0, + '8' => 0, + '4' => 0, + '24' => 0, + '19' => 0, + '10' => 0, + '5' => 0 + }, + 'legitimate_interests' => { + '11' => 0, + '21' => 0, + '7' => 0, + '17' => 0, + '2' => 0, + '22' => 0, + '1' => 0, + '18' => 0, + '23' => 0, + '16' => 0, + '13' => 0, + '6' => 0, + '3' => 0, + '9' => 0, + '12' => 0, + '20' => 0, + '14' => 0, + '15' => 0, + '8' => 0, + '4' => 0, + '24' => 0, + '19' => 0, + '10' => 0, + '5' => 0 + } + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => { + '6' => 0, + '11' => 0, + '3' => 0, + '7' => 0, + '9' => 0, + '12' => 0, + '2' => 0, + '8' => 0, + '1' => 0, + '4' => 0, + '10' => 0, + '5' => 0 + }, + 'last_updated' => '2020-04-27T00:07:00Z', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '2020-04-27T00:07:00Z', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => { + '559' => 0, + '127' => 1, + '32' => 0, + '443' => 0, + '206' => 0, + '118' => 0, + '71' => 0, + '358' => 0, + '331' => 0, + '560' => 0, + '580' => 0, + '84' => 0, + '512' => 0, + '437' => 0, + '463' => 0, + '194' => 0, + '517' => 0, + '458' => 0, + '451' => 0, + '220' => 0, + '454' => 0, + '31' => 0, + '578' => 0, + '378' => 0, + '325' => 0, + '29' => 0, + '572' => 0, + '350' => 0, + '540' => 0, + '226' => 0, + '58' => 0, + '211' => 0, + '153' => 0, + '15' => 0, + '527' => 0, + '431' => 0, + '382' => 0, + '337' => 0, + '101' => 0, + '340' => 0, + '76' => 0, + '311' => 0, + '62' => 0, + '571' => 0, + '139' => 0, + '389' => 0, + '129' => 0, + '548' => 0, + '495' => 0, + '418' => 0, + '236' => 0, + '218' => 0, + '168' => 0, + '135' => 0, + '14' => 0, + '348' => 0, + '145' => 0, + '49' => 0, + '178' => 0, + '285' => 0, + '124' => 0, + '234' => 0, + '594' => 0, + '23' => 1, + '388' => 0, + '364' => 0, + '96' => 0, + '486' => 0, + '509' => 0, + '160' => 0, + '569' => 0, + '367' => 0, + '8' => 0, + '98' => 0, + '43' => 0, + '485' => 0, + '391' => 0, + '21' => 0, + '523' => 0, + '288' => 0, + '193' => 0, + '460' => 0, + '119' => 0, + '586' => 0, + '453' => 0, + '324' => 0, + '180' => 0, + '244' => 0, + '351' => 0, + '410' => 0, + '595' => 0, + '246' => 0, + '488' => 0, + '61' => 0, + '430' => 0, + '447' => 0, + '536' => 0, + '379' => 0, + '415' => 0, + '113' => 0, + '152' => 0, + '189' => 0, + '452' => 0, + '342' => 0, + '579' => 0, + '295' => 0, + '480' => 0, + '341' => 0, + '438' => 0, + '107' => 0, + '535' => 0, + '87' => 0, + '77' => 0, + '444' => 0, + '541' => 0, + '508' => 0, + '221' => 0, + '39' => 0, + '64' => 0, + '558' => 0, + '417' => 0, + '12' => 0, + '312' => 0, + '45' => 0, + '507' => 0, + '405' => 0, + '260' => 0, + '573' => 0, + '237' => 0, + '370' => 0, + '309' => 0, + '567' => 0, + '1' => 0, + '506' => 0, + '136' => 0, + '116' => 0, + '416' => 0, + '144' => 0, + '380' => 0, + '100' => 0, + '300' => 0, + '286' => 0, + '120' => 0, + '381' => 0, + '581' => 0, + '308' => 0, + '392' => 0, + '254' => 0, + '177' => 0, + '496' => 0, + '605' => 0, + '373' => 0, + '607' => 0, + '205' => 0, + '42' => 1, + '22' => 0, + '399' => 0, + '235' => 0, + '301' => 0, + '436' => 0, + '213' => 0, + '94' => 0, + '51' => 0, + '456' => 0, + '568' => 0, + '296' => 0, + '265' => 0, + '493' => 0, + '171' => 0, + '386' => 0, + '445' => 0, + '200' => 0, + '366' => 0, + '329' => 0, + '525' => 0, + '27' => 0, + '272' => 0, + '161' => 0, + '582' => 0, + '534' => 0, + '400' => 0, + '20' => 0, + '109' => 0, + '151' => 0, + '557' => 0, + '468' => 0, + '287' => 0, + '475' => 0, + '441' => 0, + '78' => 0, + '413' => 0, + '294' => 0, + '349' => 0, + '275' => 0, + '515' => 0, + '197' => 0, + '138' => 0, + '606' => 0, + '137' => 0, + '60' => 0, + '432' => 0, + '519' => 0, + '346' => 0, + '17' => 0, + '427' => 0, + '82' => 0, + '110' => 0, + '333' => 0, + '590' => 0, + '323' => 0, + '69' => 0, + '112' => 0, + '545' => 0, + '191' => 0, + '224' => 0, + '187' => 0, + '588' => 0, + '446' => 0, + '262' => 0, + '617' => 0, + '79' => 0, + '212' => 0, + '352' => 0, + '126' => 1, + '426' => 0, + '251' => 0, + '542' => 0, + '369' => 0, + '279' => 0, + '176' => 0, + '498' => 0, + '483' => 0, + '256' => 0, + '372' => 0, + '574' => 0, + '170' => 0, + '33' => 0, + '428' => 0, + '7' => 0, + '26' => 0, + '227' => 0, + '99' => 0, + '566' => 0, + '526' => 0, + '72' => 0, + '500' => 0, + '264' => 0, + '255' => 0, + '533' => 0, + '359' => 0, + '182' => 0, + '108' => 0, + '604' => 0, + '556' => 0, + '462' => 0, + '414' => 0, + '232' => 0, + '477' => 0, + '225' => 0, + '330' => 0, + '142' => 0, + '207' => 0, + '263' => 0, + '394' => 0, + '167' => 0, + '48' => 0, + '360' => 0, + '610' => 0, + '514' => 0, + '513' => 0, + '615' => 0, + '50' => 0, + '476' => 0, + '510' => 0, + '393' => 0, + '449' => 0, + '293' => 0, + '274' => 0, + '549' => 0, + '322' => 0, + '469' => 0, + '353' => 0, + '575' => 0, + '375' => 0, + '128' => 1, + '28' => 0, + '310' => 0, + '40' => 0, + '589' => 0, + '303' => 0, + '192' => 0, + '250' => 0, + '614' => 0, + '501' => 0, + '215' => 0, + '278' => 0, + '490' => 0, + '150' => 0, + '130' => 0, + '155' => 0, + '387' => 0, + '53' => 0, + '245' => 0, + '626' => 1, + '543' => 0, + '267' => 0, + '354' => 0, + '461' => 0, + '583' => 0, + '257' => 0, + '85' => 0, + '332' => 0, + '9' => 0, + '425' => 0, + '591' => 0, + '34' => 0, + '539' => 0, + '603' => 0, + '90' => 0, + '276' => 0, + '620' => 0, + '565' => 0, + '102' => 0, + '520' => 0, + '532' => 0, + '16' => 0, + '55' => 0, + '233' => 0, + '57' => 0, + '259' => 0, + '368' => 0, + '424' => 0, + '316' => 0, + '163' => 0, + '395' => 0, + '89' => 0, + '611' => 0, + '175' => 0, + '584' => 0, + '35' => 0, + '11' => 0, + '492' => 0, + '208' => 0, + '347' => 0, + '511' => 0, + '434' => 0, + '93' => 0, + '292' => 0, + '291' => 0, + '374' => 0, + '114' => 0, + '199' => 0, + '442' => 0, + '429' => 0, + '73' => 0, + '409' => 0, + '67' => 0, + '241' => 0, + '198' => 0, + '489' => 0, + '585' => 0, + '327' => 0, + '320' => 0, + '280' => 0, + '273' => 0, + '471' => 0, + '622' => 0, + '202' => 0, + '249' => 0, + '361' => 0, + '465' => 0, + '184' => 0, + '24' => 0, + '140' => 0, + '104' => 0, + '131' => 0, + '181' => 0, + '412' => 0, + '385' => 0, + '502' => 0, + '307' => 0, + '314' => 0, + '154' => 0, + '355' => 0, + '553' => 0, + '159' => 0, + '479' => 0, + '326' => 0, + '555' => 0, + '47' => 0, + '619' => 0, + '37' => 0, + '335' => 0, + '270' => 0, + '5' => 0, + '195' => 0, + '621' => 0, + '538' => 0, + '524' => 0, + '554' => 0, + '552' => 0, + '521' => 0, + '598' => 0, + '162' => 0, + '433' => 0, + '74' => 0, + '240' => 0, + '334' => 0, + '440' => 0, + '230' => 0, + '115' => 0, + '299' => 0, + '377' => 0, + '103' => 0, + '602' => 0, + '201' => 0, + '423' => 0, + '612' => 0, + '91' => 0, + '266' => 0, + '467' => 0, + '174' => 0, + '474' => 0, + '481' => 0, + '214' => 0, + '422' => 0, + '564' => 0, + '563' => 0, + '97' => 0, + '41' => 0, + '52' => 0, + '302' => 0, + '229' => 0, + '503' => 0, + '593' => 0, + '68' => 0, + '188' => 0, + '315' => 0, + '402' => 0, + '338' => 0, + '576' => 0, + '616' => 0, + '222' => 0, + '25' => 0, + '83' => 0, + '484' => 0, + '305' => 0, + '623' => 0, + '544' => 0, + '217' => 0, + '328' => 0, + '239' => 0, + '122' => 0, + '143' => 0, + '158' => 0, + '269' => 0, + '281' => 0, + '464' => 0, + '363' => 0, + '46' => 0, + '6' => 0, + '562' => 0, + '36' => 0, + '518' => 0, + '183' => 0, + '497' => 0, + '472' => 0, + '362' => 0, + '439' => 0, + '317' => 0, + '608' => 0, + '132' => 0, + '169' => 0, + '411' => 0, + '478' => 0, + '384' => 0, + '398' => 0, + '546' => 0, + '537' => 0, + '407' => 0, + '18' => 0, + '376' => 0, + '522' => 0, + '125' => 0, + '599' => 0, + '44' => 0, + '609' => 0, + '587' => 1, + '190' => 0, + '95' => 0, + '298' => 0, + '601' => 0, + '313' => 0, + '243' => 0, + '231' => 0, + '551' => 0, + '529' => 0, + '148' => 0, + '343' => 0, + '504' => 0, + '397' => 0, + '106' => 0, + '157' => 0, + '65' => 0, + '203' => 0, + '261' => 0, + '81' => 0, + '321' => 0, + '459' => 0, + '624' => 0, + '86' => 0, + '284' => 0, + '247' => 0, + '371' => 0, + '204' => 0, + '165' => 0, + '289' => 0, + '2' => 0, + '435' => 0, + '401' => 0, + '186' => 0, + '147' => 0, + '339' => 0, + '228' => 0, + '531' => 0, + '268' => 0, + '345' => 0, + '596' => 0, + '172' => 0, + '319' => 0, + '223' => 0, + '404' => 0, + '613' => 1, + '516' => 0, + '282' => 0, + '420' => 0, + '121' => 0, + '344' => 0, + '487' => 0, + '494' => 0, + '238' => 0, + '577' => 0, + '253' => 0, + '561' => 0, + '448' => 0, + '209' => 0, + '216' => 0, + '357' => 0, + '117' => 0, + '63' => 0, + '455' => 0, + '600' => 0, + '80' => 0, + '336' => 0, + '457' => 0, + '179' => 0, + '383' => 0, + '297' => 0, + '277' => 0, + '92' => 0, + '10' => 0, + '550' => 0, + '505' => 0, + '419' => 0, + '133' => 0, + '290' => 0, + '625' => 0, + '592' => 0, + '149' => 0, + '123' => 0, + '304' => 0, + '547' => 0, + '210' => 0, + '406' => 0, + '258' => 0, + '396' => 0, + '482' => 0, + '173' => 0, + '530' => 0, + '56' => 0, + '499' => 0, + '66' => 0, + '19' => 0, + '54' => 0, + '365' => 0, + '306' => 0, + '70' => 0, + '470' => 0, + '166' => 0, + '88' => 0, + '141' => 0, + '30' => 0, + '570' => 0, + '403' => 0, + '252' => 0, + '466' => 0, + '156' => 0, + '134' => 0, + '75' => 0, + '283' => 0, + '618' => 0, + '59' => 0, + '421' => 0, + '450' => 0, + '271' => 0, + '491' => 0, + '219' => 0, + '318' => 0, + '13' => 0, + '105' => 0, + '473' => 0, + '185' => 0, + '3' => 0, + '597' => 0, + '248' => 0, + '390' => 0, + '146' => 0, + '111' => 0, + '38' => 0, + '356' => 0, + '408' => 0, + '4' => 0, + '528' => 0, + '164' => 0, + '196' => 0, + '242' => 0 + }, + 'legitimate_interests' => { + '559' => 0, + '127' => 0, + '32' => 0, + '443' => 0, + '206' => 0, + '118' => 0, + '71' => 0, + '358' => 0, + '331' => 0, + '560' => 0, + '580' => 0, + '84' => 0, + '512' => 0, + '437' => 0, + '463' => 0, + '194' => 0, + '517' => 0, + '458' => 0, + '451' => 0, + '220' => 0, + '454' => 0, + '31' => 0, + '578' => 0, + '378' => 0, + '325' => 0, + '29' => 0, + '572' => 0, + '350' => 0, + '540' => 0, + '226' => 0, + '58' => 0, + '211' => 0, + '153' => 0, + '15' => 0, + '527' => 0, + '431' => 0, + '382' => 0, + '337' => 0, + '101' => 0, + '340' => 0, + '76' => 0, + '311' => 0, + '62' => 0, + '571' => 0, + '139' => 0, + '389' => 0, + '129' => 1, + '548' => 0, + '495' => 0, + '418' => 0, + '236' => 0, + '218' => 0, + '168' => 0, + '135' => 0, + '14' => 0, + '348' => 0, + '145' => 0, + '49' => 0, + '178' => 0, + '285' => 0, + '124' => 0, + '627' => 0, + '234' => 0, + '594' => 0, + '23' => 0, + '388' => 0, + '364' => 0, + '96' => 0, + '486' => 0, + '509' => 0, + '160' => 0, + '569' => 0, + '367' => 0, + '8' => 0, + '98' => 0, + '43' => 0, + '485' => 0, + '391' => 0, + '21' => 0, + '523' => 0, + '288' => 0, + '193' => 0, + '460' => 0, + '119' => 0, + '586' => 0, + '453' => 0, + '324' => 0, + '180' => 0, + '244' => 0, + '351' => 0, + '410' => 0, + '595' => 0, + '246' => 0, + '488' => 0, + '61' => 0, + '430' => 0, + '447' => 0, + '536' => 0, + '379' => 0, + '415' => 0, + '113' => 0, + '152' => 0, + '189' => 0, + '452' => 0, + '342' => 0, + '579' => 0, + '295' => 0, + '480' => 0, + '341' => 0, + '438' => 0, + '107' => 0, + '535' => 0, + '87' => 0, + '77' => 0, + '444' => 0, + '541' => 0, + '508' => 0, + '221' => 0, + '39' => 0, + '64' => 0, + '558' => 0, + '417' => 0, + '12' => 0, + '312' => 0, + '45' => 0, + '507' => 0, + '405' => 0, + '260' => 0, + '573' => 0, + '237' => 0, + '370' => 0, + '309' => 0, + '567' => 0, + '1' => 0, + '506' => 0, + '136' => 0, + '116' => 0, + '416' => 0, + '144' => 0, + '380' => 0, + '100' => 0, + '300' => 0, + '286' => 0, + '120' => 0, + '381' => 0, + '581' => 0, + '308' => 0, + '392' => 0, + '254' => 0, + '177' => 0, + '496' => 0, + '605' => 0, + '373' => 0, + '607' => 0, + '205' => 0, + '42' => 0, + '22' => 0, + '399' => 0, + '235' => 0, + '301' => 0, + '436' => 0, + '213' => 0, + '94' => 0, + '51' => 0, + '456' => 0, + '568' => 0, + '296' => 0, + '265' => 0, + '493' => 0, + '171' => 0, + '386' => 0, + '445' => 0, + '200' => 0, + '366' => 0, + '329' => 0, + '525' => 0, + '27' => 0, + '272' => 0, + '161' => 0, + '582' => 0, + '534' => 0, + '400' => 0, + '20' => 0, + '109' => 0, + '151' => 0, + '557' => 0, + '468' => 0, + '287' => 0, + '475' => 0, + '441' => 0, + '78' => 0, + '413' => 0, + '294' => 0, + '349' => 0, + '275' => 0, + '515' => 0, + '197' => 0, + '138' => 0, + '606' => 0, + '137' => 0, + '60' => 0, + '432' => 0, + '519' => 0, + '346' => 0, + '17' => 0, + '427' => 0, + '82' => 0, + '110' => 0, + '333' => 0, + '590' => 0, + '323' => 0, + '69' => 0, + '112' => 0, + '545' => 0, + '191' => 0, + '224' => 0, + '187' => 0, + '588' => 0, + '446' => 0, + '262' => 0, + '617' => 0, + '79' => 0, + '212' => 0, + '352' => 0, + '126' => 0, + '426' => 0, + '251' => 0, + '542' => 0, + '369' => 0, + '279' => 0, + '176' => 0, + '498' => 0, + '483' => 0, + '256' => 0, + '372' => 0, + '574' => 0, + '170' => 0, + '33' => 0, + '428' => 0, + '7' => 0, + '26' => 0, + '227' => 0, + '99' => 0, + '566' => 0, + '526' => 0, + '72' => 0, + '500' => 0, + '264' => 0, + '255' => 0, + '533' => 0, + '359' => 0, + '182' => 0, + '108' => 0, + '604' => 0, + '556' => 0, + '462' => 0, + '414' => 0, + '232' => 0, + '477' => 0, + '225' => 0, + '330' => 0, + '142' => 0, + '207' => 0, + '263' => 0, + '394' => 0, + '167' => 0, + '48' => 0, + '360' => 0, + '610' => 0, + '514' => 0, + '513' => 0, + '615' => 0, + '50' => 0, + '476' => 0, + '510' => 0, + '393' => 0, + '449' => 0, + '293' => 0, + '274' => 0, + '549' => 0, + '322' => 0, + '469' => 0, + '353' => 0, + '575' => 0, + '375' => 0, + '128' => 0, + '28' => 0, + '310' => 0, + '40' => 0, + '589' => 0, + '303' => 0, + '192' => 0, + '250' => 0, + '614' => 1, + '501' => 0, + '215' => 0, + '278' => 0, + '490' => 0, + '150' => 0, + '130' => 1, + '155' => 0, + '387' => 0, + '53' => 0, + '245' => 0, + '626' => 0, + '543' => 0, + '267' => 0, + '354' => 0, + '461' => 0, + '583' => 0, + '257' => 0, + '85' => 0, + '332' => 0, + '9' => 0, + '425' => 0, + '591' => 1, + '34' => 0, + '539' => 0, + '603' => 0, + '90' => 0, + '276' => 0, + '620' => 0, + '565' => 0, + '102' => 0, + '520' => 0, + '532' => 0, + '16' => 0, + '55' => 0, + '233' => 0, + '57' => 0, + '259' => 0, + '368' => 0, + '424' => 0, + '316' => 0, + '163' => 0, + '395' => 0, + '89' => 0, + '611' => 0, + '175' => 0, + '584' => 0, + '35' => 0, + '11' => 0, + '492' => 0, + '208' => 0, + '347' => 0, + '511' => 0, + '434' => 0, + '93' => 0, + '292' => 0, + '291' => 0, + '374' => 0, + '114' => 0, + '199' => 0, + '442' => 0, + '429' => 0, + '73' => 0, + '409' => 0, + '67' => 0, + '241' => 0, + '198' => 0, + '489' => 0, + '585' => 0, + '327' => 0, + '320' => 0, + '280' => 0, + '273' => 0, + '471' => 0, + '622' => 0, + '202' => 0, + '249' => 0, + '361' => 0, + '465' => 0, + '184' => 0, + '24' => 1, + '140' => 0, + '104' => 0, + '131' => 1, + '181' => 0, + '412' => 0, + '385' => 0, + '502' => 0, + '307' => 0, + '314' => 0, + '154' => 0, + '355' => 0, + '553' => 0, + '159' => 0, + '479' => 0, + '326' => 0, + '555' => 0, + '47' => 0, + '619' => 0, + '37' => 0, + '335' => 0, + '270' => 0, + '5' => 0, + '195' => 0, + '621' => 0, + '538' => 0, + '524' => 0, + '554' => 0, + '552' => 0, + '521' => 0, + '598' => 0, + '162' => 0, + '433' => 0, + '74' => 0, + '240' => 0, + '334' => 0, + '440' => 0, + '230' => 0, + '115' => 0, + '299' => 0, + '377' => 0, + '103' => 0, + '602' => 0, + '201' => 0, + '423' => 0, + '612' => 0, + '91' => 0, + '266' => 0, + '467' => 0, + '174' => 0, + '474' => 0, + '481' => 0, + '214' => 0, + '422' => 0, + '564' => 0, + '563' => 0, + '97' => 0, + '41' => 0, + '52' => 0, + '302' => 0, + '229' => 0, + '503' => 0, + '593' => 0, + '68' => 0, + '188' => 0, + '315' => 0, + '402' => 0, + '338' => 0, + '576' => 0, + '616' => 0, + '222' => 0, + '25' => 0, + '83' => 0, + '484' => 0, + '305' => 0, + '623' => 0, + '544' => 0, + '217' => 0, + '328' => 0, + '239' => 0, + '122' => 0, + '143' => 0, + '628' => 1, + '158' => 0, + '269' => 0, + '281' => 0, + '464' => 0, + '363' => 0, + '46' => 0, + '6' => 0, + '562' => 0, + '36' => 0, + '518' => 0, + '183' => 0, + '497' => 0, + '472' => 0, + '362' => 0, + '439' => 0, + '317' => 0, + '608' => 0, + '132' => 0, + '169' => 0, + '411' => 0, + '478' => 0, + '384' => 0, + '398' => 0, + '546' => 0, + '537' => 0, + '407' => 0, + '18' => 0, + '376' => 0, + '522' => 0, + '125' => 0, + '599' => 0, + '44' => 1, + '609' => 0, + '587' => 0, + '190' => 0, + '95' => 0, + '298' => 0, + '601' => 0, + '313' => 0, + '243' => 0, + '231' => 0, + '551' => 0, + '529' => 0, + '148' => 0, + '343' => 0, + '504' => 0, + '397' => 0, + '106' => 0, + '157' => 0, + '65' => 0, + '203' => 0, + '261' => 0, + '81' => 0, + '321' => 0, + '459' => 0, + '624' => 0, + '86' => 0, + '284' => 0, + '247' => 0, + '371' => 0, + '204' => 0, + '165' => 0, + '289' => 0, + '2' => 0, + '435' => 0, + '401' => 0, + '186' => 0, + '147' => 0, + '339' => 0, + '228' => 0, + '531' => 0, + '268' => 0, + '345' => 0, + '596' => 0, + '172' => 0, + '319' => 0, + '223' => 0, + '404' => 0, + '613' => 0, + '516' => 0, + '282' => 0, + '420' => 0, + '121' => 0, + '344' => 0, + '487' => 0, + '494' => 0, + '238' => 0, + '577' => 0, + '253' => 0, + '561' => 0, + '448' => 0, + '209' => 0, + '216' => 0, + '357' => 0, + '117' => 0, + '63' => 0, + '455' => 0, + '600' => 0, + '80' => 0, + '336' => 0, + '457' => 0, + '179' => 0, + '383' => 0, + '297' => 0, + '277' => 0, + '92' => 0, + '10' => 0, + '550' => 0, + '505' => 0, + '419' => 0, + '133' => 0, + '290' => 0, + '625' => 0, + '592' => 0, + '149' => 0, + '123' => 0, + '304' => 0, + '547' => 0, + '210' => 0, + '406' => 0, + '258' => 0, + '396' => 0, + '482' => 0, + '173' => 0, + '530' => 0, + '56' => 0, + '499' => 0, + '66' => 0, + '19' => 0, + '54' => 0, + '365' => 0, + '306' => 0, + '70' => 0, + '470' => 0, + '166' => 0, + '88' => 0, + '141' => 0, + '30' => 0, + '570' => 0, + '403' => 0, + '252' => 0, + '466' => 0, + '156' => 0, + '134' => 0, + '75' => 0, + '283' => 0, + '618' => 0, + '59' => 0, + '421' => 0, + '450' => 0, + '271' => 0, + '491' => 0, + '219' => 0, + '318' => 0, + '13' => 0, + '105' => 0, + '473' => 0, + '185' => 0, + '3' => 0, + '597' => 0, + '248' => 0, + '390' => 0, + '146' => 0, + '111' => 0, + '38' => 0, + '356' => 0, + '408' => 0, + '4' => 0, + '528' => 0, + '164' => 0, + '196' => 0, + '242' => 0 + } + }, + %extra + }; +} diff --git a/t/03-bugs.t b/t/90-bugs.t similarity index 100% rename from t/03-bugs.t rename to t/90-bugs.t From b4747507afab320b814b2b01bb654171ea5cbf0e Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 13:49:14 +0100 Subject: [PATCH 3/8] Refactor publisher restrictions (#27) * add named parameters on check_publisher_restriction method * add new method * improve test * Revert "improve test" This reverts commit ae7274e49ad0b767b158beadd9ce73e07a5eb836. * fix format * fix pod * Update Publisher.pm Remove char * Update PublisherRestrictions.pm Remove char * tidy test * remove bad chars * try remove all bad chars --- README.pod | 11 ++++ lib/GDPR/IAB/TCFv2.pm | 45 +++++++++++++--- lib/GDPR/IAB/TCFv2/Publisher.pm | 29 ++++++++--- lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm | 57 +++++++++++++++++---- t/00-load.t | 4 +- t/01-parse.t | 44 +++++++++++++++- 6 files changed, 163 insertions(+), 27 deletions(-) diff --git a/README.pod b/README.pod index 6a1a83d..cb450a9 100644 --- a/README.pod +++ b/README.pod @@ -325,6 +325,13 @@ It true, there is a publisher restriction of certain type, for a given purpose i # with restriction type 0 'Purpose Flatly Not Allowed by Publisher' my $ok = $instance->check_publisher_restriction(1, 0, 284); +or + + my $ok = $instance->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, + vendor_id => 284); + Version 2.0 of the Framework introduced the ability for publishers to signal restrictions on how vendors may process personal data. Restrictions can be of two types: =over @@ -357,6 +364,10 @@ 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_restrictions + +Similar to L but return an hashref of purpose => { restriction type => bool } for a given vendor. + =head2 publisher_tc If the consent string has a C section, we will decode this section as an instance of L. diff --git a/lib/GDPR/IAB/TCFv2.pm b/lib/GDPR/IAB/TCFv2.pm index b7f0511..ceb4c39 100644 --- a/lib/GDPR/IAB/TCFv2.pm +++ b/lib/GDPR/IAB/TCFv2.pm @@ -333,10 +333,28 @@ sub vendor_legitimate_interest { } sub check_publisher_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my $self = shift; + + my ( $purpose_id, $restriction_type, $vendor_id ); + + if ( scalar(@_) == 6 ) { + my (%opts) = @_; + + $purpose_id = $opts{purpose_id}; + $restriction_type = $opts{restriction_type}; + $vendor_id = $opts{vendor_id}; + } + + ( $purpose_id, $restriction_type, $vendor_id ) = @_; return $self->{publisher} - ->check_restriction( $purpose_id, $restrict_type, $vendor ); + ->check_restriction( $purpose_id, $restriction_type, $vendor_id ); +} + +sub publisher_restrictions { + my ( $self, $vendor_id ) = @_; + + return $self->{publisher}->restrictions($vendor_id); } sub publisher_tc { @@ -501,10 +519,10 @@ sub _parse_vendor_section { # parse vendor legitimate interest - my $pub_restrict_offset = + my $pub_restriction_offset = $self->_parse_vendor_legitimate_interests($legitimate_interest_offset); - return $pub_restrict_offset; + return $pub_restriction_offset; } sub _parse_vendor_consents { @@ -523,22 +541,22 @@ sub _parse_vendor_consents { sub _parse_vendor_legitimate_interests { my ( $self, $legitimate_interest_offset ) = @_; - my ( $vendor_legitimate_interests, $pub_restrict_offset ) = + my ( $vendor_legitimate_interests, $pub_restriction_offset ) = $self->_parse_bitfield_or_range( $legitimate_interest_offset, ); $self->{vendor_legitimate_interests} = $vendor_legitimate_interests; - return $pub_restrict_offset; + return $pub_restriction_offset; } sub _parse_publisher_section { - my ( $self, $pub_restrict_offset ) = @_; + my ( $self, $pub_restriction_offset ) = @_; # parse public restrictions - my $core_data = substr( $self->{core_data}, $pub_restrict_offset ); + my $core_data = substr( $self->{core_data}, $pub_restriction_offset ); my $core_data_size = length( $self->{core_data} ); my $publisher = GDPR::IAB::TCFv2::Publisher->Parse( @@ -959,6 +977,13 @@ It true, there is a publisher restriction of certain type, for a given purpose i # with restriction type 0 'Purpose Flatly Not Allowed by Publisher' my $ok = $instance->check_publisher_restriction(1, 0, 284); +or + + my $ok = $instance->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, + vendor_id => 284); + Version 2.0 of the Framework introduced the ability for publishers to signal restrictions on how vendors may process personal data. Restrictions can be of two types: =over @@ -991,6 +1016,10 @@ 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_restrictions + +Similar to L but return an hashref of purpose => { restriction type => bool } for a given vendor. + =head2 publisher_tc If the consent string has a C section, we will decode this section as an instance of L. diff --git a/lib/GDPR/IAB/TCFv2/Publisher.pm b/lib/GDPR/IAB/TCFv2/Publisher.pm index 136f0e1..14af9b0 100644 --- a/lib/GDPR/IAB/TCFv2/Publisher.pm +++ b/lib/GDPR/IAB/TCFv2/Publisher.pm @@ -51,10 +51,16 @@ sub Parse { } sub check_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my ( $self, $purpose_id, $restriction_type, $vendor_id ) = @_; return $self->{restrictions} - ->check_restriction( $purpose_id, $restrict_type, $vendor ); + ->check_restriction( $purpose_id, $restriction_type, $vendor_id ); +} + +sub restrictions { + my ( $self, $vendor_id ) = @_; + + return $self->{restrictions}->restrictions($vendor_id); } sub publisher_tc { @@ -95,7 +101,7 @@ Combines the creation of L and L { json => ... }, ); - say "there is publisher restriction on purpose id 1, type 0 on vendor 284" + say "there is publisher restriction on purpose id 1, type 0 on vendor_id 284" if $publisher->check_restriction(1, 0, 284); =head1 CONSTRUCTOR @@ -126,12 +132,21 @@ Key C is the L options (includes the C field to =head2 check_restriction -Return true for a given combination of purpose id, restriction type and vendor +Return true for a given combination of purpose id, restriction type and vendor_id my $purpose_id = 1; my $restriction_type = 0; - my $vendor = 284; - $ok = $range->check_restriction($purpose_id, $restriction_type, $vendor); + my $vendor_id = 284; + $ok = $publisher->check_restriction($purpose_id, $restriction_type, $vendor_id); + +=head2 restrictions + +Return a hashref of purpose => { restriction type => bool } for a given vendor id. + +Example, by parsing the consent C we can generate this. + + my $restrictions = $publisher->restrictions(32); + # returns { 7 => { 1 => 1 } } =head2 publisher_tc @@ -155,7 +170,7 @@ Returns a hashref with the following format: # 0 - Not Allowed # 1 - Require Consent # 2 - Require Legitimate Interest - '[vendor id]' => 1, + '[vendor_id id]' => 1, }, } } diff --git a/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm b/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm index f77826f..fbdb05f 100644 --- a/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm +++ b/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm @@ -66,14 +66,44 @@ sub Parse { return $self; } +sub restrictions { + my ( $self, $vendor_id ) = @_; + + my %restrictions; + + foreach my $purpose_id ( keys %{ $self->{restrictions} } ) { + foreach my $restriction_type ( + keys %{ $self->{restrictions}->{$purpose_id} } ) + { + if ( $self->{restrictions}->{$purpose_id}->{$restriction_type} + ->contains($vendor_id) ) + { + $restrictions{$purpose_id} ||= {}; + $restrictions{$purpose_id}->{$restriction_type} = 1; + } + } + } + + return \%restrictions; +} + sub check_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my $self = shift; + + my $nargs = scalar(@_); + + croak "missing arguments: purpose id, restriction type and vendor id" + if $nargs == 0; + croak "missing arguments: restriction type and vendor id" if $nargs == 1; + croak "missing argument: vendor id" if $nargs == 2; + + my ( $purpose_id, $restriction_type, $vendor_id ) = @_; return 0 - unless exists $self->{restrictions}->{$purpose_id}->{$restrict_type}; + unless exists $self->{restrictions}->{$purpose_id}->{$restriction_type}; - return $self->{restrictions}->{$purpose_id}->{$restrict_type} - ->contains($vendor); + return $self->{restrictions}->{$purpose_id}->{$restriction_type} + ->contains($vendor_id); } sub TO_JSON { @@ -86,11 +116,11 @@ sub TO_JSON { my %purpose_restrictions; - foreach my $restrict_type ( keys %{$restriction_map} ) { - my $vendors = $restriction_map->{$restrict_type}->all; + foreach my $restriction_type ( keys %{$restriction_map} ) { + my $vendors = $restriction_map->{$restriction_type}->all; foreach my $vendor ( @{$vendors} ) { - $purpose_restrictions{$vendor} = int($restrict_type); + $purpose_restrictions{$vendor} = int($restriction_type); } } @@ -146,8 +176,17 @@ Return true for a given combination of purpose id, restriction type and vendor my $purpose_id = 1; my $restriction_type = 0; - my $vendor = 284; - $ok = $range->check_restriction($purpose_id, $restriction_type, $vendor); + my $vendor_id = 284; + my $ok = $object->check_restriction($purpose_id, $restriction_type, $vendor_id); + +=head2 restrictions + +Return a hashref of purpose => { restriction type => bool } for a given vendor id. + +Example, by parsing the consent C we can generate this. + + my $restrictions = $object->restrictions(32); + # returns { 7 => { 1 => 1 } } =head2 TO_JSON diff --git a/t/00-load.t b/t/00-load.t index 999318c..d578a3f 100644 --- a/t/00-load.t +++ b/t/00-load.t @@ -42,9 +42,9 @@ subtest "check interfaces" => sub { @role_decoder_methods, qw; can_ok 'GDPR::IAB::TCFv2::PublisherRestrictions', @role_base_methods, - qw; + qw; can_ok 'GDPR::IAB::TCFv2::Publisher', @role_base_methods, - qw; + qw; can_ok 'GDPR::IAB::TCFv2::PublisherTC', @role_base_methods, qw sub { }; ok !$consent->check_publisher_restriction( 1, 0, 284 ), - "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher' when called with positional parameters"; + + ok !$consent->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, vendor_id => 284 + ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher' when called with named parameters"; + + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; my $publisher_tc = $consent->publisher_tc; @@ -438,6 +448,10 @@ subtest "range" => sub { ok !$consent->check_publisher_restriction( 1, 0, 284 ), "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + my $publisher_tc = $consent->publisher_tc; ok !defined($publisher_tc), "should not return publisher_tc"; @@ -499,6 +513,10 @@ subtest "range" => sub { ok !$consent->check_publisher_restriction( 1, 0, 284 ), "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + done_testing; }; done_testing; @@ -529,6 +547,14 @@ subtest "check publisher restriction" => sub { ok !$consent->check_publisher_restriction( 5, 1, 32 ), "must have publisher restriction to vendor 32 regarding purpose id 5 of type 1 'Require Consent'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + + $restrictions = $consent->publisher_restrictions(32); + is_deeply $restrictions, { 7 => { 1 => 1 } }, + "should return the restriction purpose id => restriction map type map"; + done_testing; }; @@ -569,6 +595,18 @@ subtest "check publisher restriction" => sub { ok $consent->check_publisher_restriction( 2, 1, 32 ); ok !$consent->check_publisher_restriction( 2, 1, 42 ); + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + + $restrictions = $consent->publisher_restrictions(32); + is_deeply $restrictions, + { 1 => { 0 => 1 }, 2 => { 0 => 1, 1 => 1 }, 7 => { 0 => 1, 1 => 1 }, + 10 => { 0 => 1, 1 => 1 } + }, + "should return the restriction purpose id => restriction map type map"; + + done_testing; }; @@ -588,6 +626,10 @@ subtest "check publisher restriction" => sub { ok !$consent->check_publisher_restriction( 1, 0, 284 ), "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + done_testing; }; From 42c39e2e1566040b658d5947fb45a2ca445ae5ef Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 13:51:43 +0100 Subject: [PATCH 4/8] add missing changes --- Changes | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changes b/Changes index c72f9ce..f0499f0 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,7 @@ + - validate parameters on method check_publisher_restriction + - add method publisher_restrictions by vendor id + - add prefetch option to cache vendor ids when the consent string is range based + 0.100 - parse publisher tc section if available - add strict mode (disabled by default) to validate the consent string version From 6565601bdfc45145cb1f3f3640a272a4e7cef54d Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 14:09:09 +0100 Subject: [PATCH 5/8] fix issue #25 --- Changes | 5 +- lib/GDPR/IAB/TCFv2/Constants/Purpose.pm | 55 ++++++++++++------- .../IAB/TCFv2/Constants/RestrictionType.pm | 24 +++++--- .../IAB/TCFv2/Constants/SpecialFeature.pm | 22 +++++--- 4 files changed, 70 insertions(+), 36 deletions(-) diff --git a/Changes b/Changes index f0499f0..a4ecad8 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,9 @@ + - refactor constants, stop use dualvars - validate parameters on method check_publisher_restriction - add method publisher_restrictions by vendor id - - add prefetch option to cache vendor ids when the consent string is range based + - add prefetch option to cache vendor ids when the consent string is range based. +It is 2x faster check consent on a prefetched vendor than a regular one if the consent is range based. +Without increase the Parsing time. 0.100 - parse publisher tc section if available diff --git a/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm b/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm index 4feb2ef..ec932da 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm @@ -1,30 +1,38 @@ package GDPR::IAB::TCFv2::Constants::Purpose; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - InfoStorageAccess => - dualvar( 1, "Store and/or access information on a device" ), - BasicAdserving => dualvar( 2, "Use limited data to select advertising" ), - PersonalizationProfile => - dualvar( 3, "Create profiles for personalised advertising" ), + InfoStorageAccess => 1, + BasicAdserving => 2, + PersonalizationProfile => 3, + PersonalizationSelection => 4, + ContentProfile => 5, + ContentSelection => 6, + AdPerformance => 7, + ContentPerformance => 8, + MarketResearch => 9, + DevelopImprove => 10, + SelectContent => 11, +}; + +use constant PurposeDescription => { + InfoStorageAccess => "Store and/or access information on a device", + BasicAdserving => "Use limited data to select advertising", + PersonalizationProfile => "Create profiles for personalised advertising", PersonalizationSelection => - dualvar( 4, "Use profiles to select personalised advertising" ), - ContentProfile => dualvar( 5, "Create profiles to personalise content" ), - ContentSelection => - dualvar( 6, "Use profiles to select personalised content" ), - AdPerformance => dualvar( 7, "Measure advertising performance" ), - ContentPerformance => dualvar( 8, "Measure content performance" ), - MarketResearch => dualvar( - 9, - "Understand audiences through statistics or combinations of data from different sources" - ), - DevelopImprove => dualvar( 10, "Develop and improve services" ), - SelectContent => dualvar( 11, "Use limited data to select content" ), + "Use profiles to select personalised advertising", + ContentProfile => "Create profiles to personalise content", + ContentSelection => "Use profiles to select personalised content", + AdPerformance => "Measure advertising performance", + ContentPerformance => "Measure content performance", + MarketResearch => + "Understand audiences through statistics or combinations of data from different sources", + DevelopImprove => "Develop and improve services", + SelectContent => "Use limited data to select content", }; our @EXPORT_OK = qw< @@ -39,6 +47,7 @@ our @EXPORT_OK = qw< MarketResearch DevelopImprove SelectContent + PurposeDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -58,15 +67,15 @@ GDPR::IAB::TCFv2::Constants::Purpose - TCF v2.2 purposes use feature 'say'; - say "Purpose id is ". (0+InfoStorageAccess), ", and it means " . InfoStorageAccess; + say "Purpose id is ", InfoStorageAccess , ", and it means " . PurposeDescription->{InfoStorageAccess}; # Output: # Purpose id is 1, and it means Store and/or access information on a device =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L =head2 InfoStorageAccess @@ -289,3 +298,7 @@ A travel magazine has published an article on its website about the new online c A sports news mobile app has started a new section of articles covering the most recent football games. Each article includes videos hosted by a separate streaming platform showcasing the highlights of each match. If you fast-forward a video, this information may be used to select a shorter video to play next. =back + +=head2 PurposeDescription + +Returns a hashref with a mapping between all purpose ids and their description. diff --git a/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm b/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm index 4c6562f..e43bacd 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm @@ -1,21 +1,27 @@ package GDPR::IAB::TCFv2::Constants::RestrictionType; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - NotAllowed => dualvar( 0, "Purpose Flatly Not Allowed by Publisher" ), - RequireConsent => dualvar( 1, "Require Consent" ), - RequireLegitimateInterest => dualvar( 2, "Require Legitimate Interest" ), + NotAllowed => 0, + RequireConsent => 1, + RequireLegitimateInterest => 2, +}; + +use constant RestrictionTypeDescription => { + NotAllowed => "Purpose Flatly Not Allowed by Publisher", + RequireConsent =>, "Require Consent", + RequireLegitimateInterest => "Require Legitimate Interest", }; our @EXPORT_OK = qw< NotAllowed RequireConsent RequireLegitimateInterest + RestrictionTypeDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -37,15 +43,15 @@ GDPR::IAB::TCFv2::Constants::RestrictionType - TCF v2.2 publisher restriction ty use feature 'say'; - say "Restriction type id is ". (0+NotAllowed), ", and it means " . NotAllowed; + say "Restriction type id is ", NotAllowed, ", and it means " , RestrictionTypeDescription->{NotAllowed}; # Output: # Restriction type id is 0, and it means Purpose Flatly Not Allowed by Publisher =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L =head2 NotAllowed @@ -59,6 +65,10 @@ Restriction type id 1: Require Consent (if Vendor has declared the Purpose IDs l Restriction type id 2: Require Legitimate Interest (if Vendor has declared the Purpose IDs legal basis as Consent and flexible) +=head2 RestrictionTypeDescription + +Returns a hashref with a mapping between all restriction types and their description. + =head1 NOTE Vendors must always respect a 0 (Not Allowed) regardless of whether or not they have not declared that Purpose to be "flexible". Values 1 and 2 are in accordance with a vendor's declared flexibility. Eg. if a vendor has Purpose 2 declared as Legitimate Interest but also declares that Purpose as flexible and this field is set to 1, they must then check for the "consent" signal in the VendorConsents section to make a determination on whether they have the legal basis for processing user personal data under that Purpose. diff --git a/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm b/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm index 1e2772a..f989ef5 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm @@ -1,20 +1,24 @@ package GDPR::IAB::TCFv2::Constants::SpecialFeature; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - Geolocation => dualvar( 1, "Use precise geolocation data" ), - DeviceScan => - dualvar( 2, "Actively scan device characteristics for identification" ) + Geolocation => 1, + DeviceScan => 2 +}; + +use constant SpecialFeatureDescription => { + Geolocation => "Use precise geolocation data", + DeviceScan => "Actively scan device characteristics for identification" }; our @EXPORT_OK = qw< Geolocation DeviceScan + SpecialFeatureDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -36,15 +40,15 @@ GDPR::IAB::TCFv2::Constants::SpecialFeature - TCF v2.2 special features use feature 'say'; - say "Special feature id is ". (0+Geolocation), ", and it means " . Geolocation; + say "Special feature id is ", Geolocation, ", and it means " , SpecialFeatureDescription->{Geolocation}; # Output: # Special feature id is 1, and it means Use precise geolocation data =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L. =head2 Geolocation @@ -58,3 +62,7 @@ Special feature id 2: Actively scan device characteristics for identification With your acceptance, certain characteristics specific to your device might be requested and used to distinguish it from other devices (such as the installed fonts or plugins, the resolution of your screen) in support of the purposes explained in this notice. "description": + +=head2 SpecialFeatureDescription + +Returns a hashref with a mapping between all restriction types and their description. From 7e4ad09848c24e5adb2e3e13d515ea7c0e166c57 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 14:11:18 +0100 Subject: [PATCH 6/8] promote new version --- Changes | 1 + README.pod | 2 +- lib/GDPR/IAB/TCFv2.pm | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Changes b/Changes index a4ecad8..e098a25 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,4 @@ +0.200 - refactor constants, stop use dualvars - validate parameters on method check_publisher_restriction - add method publisher_restrictions by vendor id diff --git a/README.pod b/README.pod index cb450a9..5896d94 100644 --- a/README.pod +++ b/README.pod @@ -26,7 +26,7 @@ GDPR::IAB::TCFv2 - Transparency & Consent String version 2 parser =head1 VERSION -Version 0.100 +Version 0.200 =head1 SYNOPSIS diff --git a/lib/GDPR/IAB/TCFv2.pm b/lib/GDPR/IAB/TCFv2.pm index ceb4c39..1367283 100644 --- a/lib/GDPR/IAB/TCFv2.pm +++ b/lib/GDPR/IAB/TCFv2.pm @@ -22,7 +22,7 @@ use GDPR::IAB::TCFv2::BitUtils qw { @@ -678,7 +678,7 @@ GDPR::IAB::TCFv2 - Transparency & Consent String version 2 parser =head1 VERSION -Version 0.100 +Version 0.200 =head1 SYNOPSIS From 8b7301c69c2932564afd3b3a0077fce56e9b8c20 Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 14:13:07 +0100 Subject: [PATCH 7/8] update manifest --- MANIFEST | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST b/MANIFEST index 90466be..ca770df 100644 --- a/MANIFEST +++ b/MANIFEST @@ -17,6 +17,8 @@ MANIFEST This list of files README.pod t/00-load.t t/01-parse.t +t/02-json-bitfield.t t/02-json.t -t/03-bugs.t +t/03-json-range.t +t/90-bugs.t t/99-pod.t From a7126a3eb6eb9102e20e97d0528eae7d59e8bdcb Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 14:14:53 +0100 Subject: [PATCH 8/8] fix manifest --- MANIFEST | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MANIFEST b/MANIFEST index ca770df..d726902 100644 --- a/MANIFEST +++ b/MANIFEST @@ -13,12 +13,11 @@ lib/GDPR/IAB/TCFv2/PublisherTC.pm lib/GDPR/IAB/TCFv2/RangeSection.pm LICENSE Makefile.PL -MANIFEST This list of files +MANIFEST README.pod t/00-load.t t/01-parse.t t/02-json-bitfield.t -t/02-json.t t/03-json-range.t t/90-bugs.t t/99-pod.t