Skip to content

Commit

Permalink
Merge pull request #2005 from tf/iframe-consent
Browse files Browse the repository at this point in the history
Consent opt-in for iframe embed element
  • Loading branch information
tf authored Sep 21, 2023
2 parents 7c84e0e + b26babe commit 92ca368
Show file tree
Hide file tree
Showing 38 changed files with 886 additions and 96 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
AllCops:
TargetRubyVersion: 2.3
TargetRubyVersion: 2.5

# Use double quotes only for interpolation.
Style/StringLiterals:
Expand Down
7 changes: 7 additions & 0 deletions app/assets/stylesheets/pageflow/editor/info_box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
margin-bottom: 1em;
}

&.error {
color: var(--ui-on-error-surface-color);
background-color: var(--ui-error-surface-color);
border-radius: rounded();
padding: space(3);
}

.shortcuts {
dt {
display: block;
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/pageflow/ui/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ textarea.short {
text-decoration: underline;
}

.input-hidden_via_binding {
.hidden_via_binding {
display: none;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ module EntryJsonSeedHelper

def scrolled_entry_editor_json_seed(json, scrolled_entry)
json.key_format!(camelize: :lower)
scrolled_entry_editor_legacy_typography_variants_seed(json, scrolled_entry)
entry_config = Pageflow.config_for(scrolled_entry)

scrolled_entry_editor_legacy_typography_variants_seed(json, entry_config)
scrolled_entry_editor_consent_vendor_host_matchers_seed(json, entry_config)

scrolled_entry_json_seed(json,
scrolled_entry,
Expand All @@ -19,14 +22,21 @@ def scrolled_entry_editor_json_seed(json, scrolled_entry)

private

def scrolled_entry_editor_legacy_typography_variants_seed(json, scrolled_entry)
def scrolled_entry_editor_legacy_typography_variants_seed(json, entry_config)
json.legacy_typography_variants(
Pageflow
.config_for(scrolled_entry)
entry_config
.legacy_typography_variants
.deep_transform_keys { |key| key.to_s.camelize(:lower) }
)
end

def scrolled_entry_editor_consent_vendor_host_matchers_seed(json, entry_config)
json.consent_vendor_host_matchers(
entry_config
.consent_vendor_host_matchers
.transform_keys { |regexp| regexp.inspect[1..-2] }
)
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
content_element_vendors =
entry_config.content_element_consent_vendors.by_content_element_id(entry)

json.content_element_consent_vendors(content_element_vendors)

I18n.with_locale(entry.locale) do
json.consent_vendors do
json.array!(content_element_vendors.values.uniq) do |name|
json.name name
json.display_name t("pageflow_scrolled.consent_vendors.#{name}.name")
json.description t("pageflow_scrolled.consent_vendors.#{name}.description")
json.opt_in_prompt t("pageflow_scrolled.consent_vendors.#{name}.opt_in_prompt")
json.paradigm 'lazy opt-in'
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ json.config do
self,
include_unused: options[:include_unused_additional_seed_data])
)

json.partial! 'pageflow_scrolled/entry_json_seed/consent_vendors',
entry: entry, entry_config: entry_config
end

unless options[:skip_i18n]
Expand Down
16 changes: 16 additions & 0 deletions entry_types/scrolled/config/locales/new/iframe_consent.de.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
de:
pageflow_scrolled:
editor:
content_elements:
iframeEmbed:
attributes:
requireConsent:
label: "Datenschutz-Einwilligung aktivieren"
inline_help: |-
iframe erst laden, nachdem Besucher der
Datenverarbeitung durch die eingebettete Seite
zugestimmt hat.
help_texts:
missingConsentVendor: |-
Für den Anbieter der angegeben URL stehen keine
Datenschutzangaben zur Verfügung.
15 changes: 15 additions & 0 deletions entry_types/scrolled/config/locales/new/iframe_consent.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
en:
pageflow_scrolled:
editor:
content_elements:
iframeEmbed:
attributes:
requireConsent:
label: "Display privacy opt-in"
inline_help: |
Only load iframe after the visitor has given
consent to the data processing by the embedded page.
help_texts:
missingConsentVendor: |-
No privacy policy information available for the provider
of the given URL.
40 changes: 40 additions & 0 deletions entry_types/scrolled/lib/pageflow_scrolled/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ class Configuration
# @since 15.7
attr_reader :additional_frontend_seed_data

# Determine which vendors a content element will require consent
# for. Based on the vendor name returned here, the following
# translations will be used in consent UI components.
#
# pageflow_scrolled.consent_vendors.#{name}.name
# pageflow_scrolled.consent_vendors.#{name}.description
# pageflow_scrolled.consent_vendors.#{name}.opt_in_prompt
#
# @example
#
# config.content_element_consent_vendors.register(
# lambda |configuration:| do
# if configuration['provider'] == 'youtube'
# 'youtube'
# else
# 'vimeo'
# end
# end,
# content_element_type_name: 'videoEmbed'
# )
#
# @return [ContentElementConsentVendors]
# @since edge
attr_reader :content_element_consent_vendors

# Mapping from URL hosts to consent vendor names. Used for iframe
# embed opt-in.
#
# @exmaple
#
# entry_type_config.consent_vendor_host_matchers = {
# /\.some-vendor\.com$/ => 'someVendor'
# }
#
# @return [Hash<RegExp, String>]
# @since edge
attr_accessor :consent_vendor_host_matchers

# Migrate typography variants to palette colors. Before palette
# colors for text blocks and headings were introduced, it was
# already possible to color text by defining typography variants
Expand Down Expand Up @@ -104,6 +142,8 @@ def initialize(*)
@additional_editor_packs = AdditionalPacks.new

@additional_frontend_seed_data = AdditionalSeedData.new
@content_element_consent_vendors = ContentElementConsentVendors.new
@consent_vendor_host_matchers = {}

@legacy_typography_variants = {}
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module PageflowScrolled
# Register consent vendors based on content element configuration
# data.
class ContentElementConsentVendors
# @api private
def initialize
@callables = {}
end

# Register callable to determine consent vendor from configuration
# attributes for a content element type.
#
# @param callable [#call]
# Receives configuration keyword argument and returns
# @param content_element_type_name [String]
def register(callable, content_element_type_name:)
@callables[content_element_type_name] = callable
end

# @api private
def by_content_element_id(entry)
content_elements_with_consent_vendor(entry).each_with_object({}) { |content_element, result|
next unless @callables[content_element.type_name]

result[content_element.id] = @callables[content_element.type_name].call(
entry: entry,
configuration: content_element.configuration
)
}.compact
end

private

def content_elements_with_consent_vendor(entry)
ContentElement.all_for_revision(entry.revision).where(type_name: @callables.keys)
end
end
end
18 changes: 18 additions & 0 deletions entry_types/scrolled/lib/pageflow_scrolled/plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,27 @@ def configure(config)
'frontendVersion',
FRONTEND_VERSION_SEED_DATA
)

c.content_element_consent_vendors.register(
IFRAME_EMBED_CONSENT_VENDOR,
content_element_type_name: 'iframeEmbed'
)
end
end

IFRAME_EMBED_CONSENT_VENDOR = lambda do |configuration:, entry:, **|
return unless configuration['requireConsent']

uri = URI.parse(configuration['source'])
host_matchers = Pageflow.config_for(entry).consent_vendor_host_matchers

host_matchers.detect do |matcher, _|
uri.host =~ matcher
end&.last
rescue URI::InvalidURIError
nil
end

FRONTEND_VERSION_SEED_DATA = lambda do |request:, entry:, **|
if request.params[:frontend] == 'v2' || entry.feature_state('frontend_v2')
2
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {ConsentVendors} from 'editor/models/ConsentVendors';

describe('ConsentVendors', () => {
describe('fromUrl', () => {
it('detects vendor from seed data host matcher', () => {
const consentVendors = new ConsentVendors({
hostMatchers: {
'\\.some-vendor.com$': 'someVendor'
}
});

expect(consentVendors.fromUrl('https://foo.some-vendor.com/abc'))
.toEqual('someVendor');
expect(consentVendors.fromUrl('https://other.com/abc'))
.toBeUndefined();
});
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {useContentElementConsentVendor} from 'entryState';

import {renderHookInEntry} from 'support';

describe('useContentElementConsentVendor', () => {
it('reads data from seed', () => {
const {result} = renderHookInEntry(
() => useContentElementConsentVendor({contentElementId: 10}), {
seed: {
consentVendors: [{name: 'someVendor', displayName: 'Some Vendor'}],
contentElementConsentVendors: {10: 'someVendor'},
contentElements: [{id: 10}]
}
}
);

const data = result.current;
expect(data).toMatchObject({name: 'someVendor', displayName: 'Some Vendor'});
});

it('returns undefined if content element does not have consent vendor', () => {
const {result} = renderHookInEntry(
() => useContentElementConsentVendor({contentElementId: 1}), {
seed: {
contentElements: [{id: 1}]
}
}
);

const data = result.current;
expect(data).toBeUndefined();
});
});
Loading

0 comments on commit 92ca368

Please sign in to comment.