From b4747507afab320b814b2b01bb654171ea5cbf0e Mon Sep 17 00:00:00 2001 From: Tiago Peczenyj Date: Sun, 17 Dec 2023 13:49:14 +0100 Subject: [PATCH] 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; };