Skip to content

Commit c02891b

Browse files
authored
Add Ecommerce restricted mode for CNIL (#23860)
* Add filter/test for ecommerce segments * Move EcommerceRestricted to plugins directory * Update compliance check for ecomm restricted / text * Update UI screenshots * Update screenshots
1 parent ac0a05e commit c02891b

25 files changed

+351
-95
lines changed

plugins/Ecommerce/Ecommerce.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@
1212
use Piwik\Columns\ComputedMetricFactory;
1313
use Piwik\Columns\MetricsList;
1414
use Piwik\Common;
15+
use Piwik\Container\StaticContainer;
1516
use Piwik\Plugin\ArchivedMetric;
1617
use Piwik\Plugin\ComputedMetric;
1718
use Piwik\Plugins\Ecommerce\Columns\ProductCategory;
19+
use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
20+
use Piwik\Plugins\PrivacyManager\FeatureFlags\PrivacyCompliance;
21+
use Piwik\Plugins\SegmentEditor\Settings\LimitSegments;
22+
use Piwik\Segment\SegmentsList;
1823

1924
/**
2025
*
@@ -30,6 +35,7 @@ public function registerEvents()
3035
'Translate.getClientSideTranslationKeys' => 'getClientSideTranslationKeys',
3136
'Metric.addComputedMetrics' => 'addComputedMetrics',
3237
'Actions.getCustomActionDimensionFieldsAndJoins' => 'provideActionDimensionFields',
38+
'Segment.filterSegments' => 'filterSegments',
3339
];
3440
}
3541

@@ -83,4 +89,26 @@ public function addComputedMetrics(MetricsList $list, ComputedMetricFactory $com
8389
}
8490
}
8591
}
92+
93+
public function filterSegments(SegmentsList &$list, array $idSites)
94+
{
95+
$featureFlagManager = StaticContainer::get(FeatureFlagManager::class);
96+
if ($featureFlagManager->isFeatureActive(PrivacyCompliance::class)) {
97+
$limitSegmentsSettingEnabled = false;
98+
if (empty($idSites)) {
99+
$limitSegmentsSettingEnabled = LimitSegments::getInstance()->getValue();
100+
} else {
101+
foreach ($idSites as $idsite) {
102+
$limitSegmentsSettingEnabled |= LimitSegments::getInstance($idsite)->getValue();
103+
}
104+
}
105+
if ($limitSegmentsSettingEnabled) {
106+
$list->remove('Goals_Ecommerce', 'orderId');
107+
$list->remove('Goals_Ecommerce', 'revenueOrder');
108+
$list->remove('Goals_Ecommerce', 'productPrice');
109+
$list->remove('Goals_Ecommerce', 'productName');
110+
$list->remove('Goals_Ecommerce', 'productSku');
111+
}
112+
}
113+
}
86114
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\Ecommerce\Settings;
11+
12+
use Piwik\Piwik;
13+
use Piwik\Plugins\PrivacyManager\Settings\CompliancePolicyEnforcedSetting;
14+
use Piwik\Policy\CnilPolicy;
15+
use Piwik\Site;
16+
use Piwik\Url;
17+
18+
class EcommerceRestricted extends CompliancePolicyEnforcedSetting
19+
{
20+
public static function getTitle(): string
21+
{
22+
return Piwik::translate('Ecommerce_EcommercePolicySettingTitle');
23+
}
24+
25+
public static function getComplianceRequirementNote(?int $idSite = null): string
26+
{
27+
$idSites = self::getIdSitesToCheck($idSite);
28+
29+
if (!self::hasEcommerceEnabledSite($idSites)) {
30+
return self::getCompliantMessage($idSite, $idSites);
31+
}
32+
33+
if (self::getInstance($idSite)->getValue() === false) {
34+
return Piwik::translate('Ecommerce_EcommercePolicySettingNonCompliantNote');
35+
}
36+
37+
$manageUrl = 'index.php' . Url::getCurrentQueryStringWithParametersModified(self::getManageParams($idSite));
38+
39+
return Piwik::translate('Ecommerce_EcommercePolicySettingRequirementNote', ['<a href="' . $manageUrl . '">', '</a>']);
40+
}
41+
42+
private static function getManageParams(?int $idSite): array
43+
{
44+
$params = [
45+
'module' => 'SitesManager',
46+
'action' => 'index',
47+
];
48+
49+
if ($idSite !== null) {
50+
$params['idSite'] = $idSite;
51+
}
52+
53+
return $params;
54+
}
55+
56+
public static function isCompliant(string $policy, ?int $idSite = null): bool
57+
{
58+
$policyValues = static::getPolicyRequirements();
59+
if (!array_key_exists($policy, $policyValues)) {
60+
return true;
61+
}
62+
63+
$currentValue = self::getInstance($idSite)->getValue();
64+
65+
$idSites = self::getIdSitesToCheck($idSite);
66+
67+
return $currentValue === $policyValues[$policy] || !self::hasEcommerceEnabledSite($idSites);
68+
}
69+
70+
public static function getPolicyRequirements(): array
71+
{
72+
return [
73+
CnilPolicy::class => true,
74+
];
75+
}
76+
77+
private static function getIdSitesToCheck(?int $idSite): array
78+
{
79+
if ($idSite === null) {
80+
return Site::getIdSitesFromIdSitesString('all');
81+
}
82+
83+
$ids = Site::getIdSitesFromIdSitesString((string) $idSite);
84+
85+
return empty($ids) ? [$idSite] : $ids;
86+
}
87+
88+
private static function hasEcommerceEnabledSite(array $idSites): bool
89+
{
90+
foreach ($idSites as $siteId) {
91+
if (Site::isEcommerceEnabledFor((int) $siteId)) {
92+
return true;
93+
}
94+
}
95+
96+
return false;
97+
}
98+
99+
private static function getCompliantMessage(?int $idSite, array $idSites): string
100+
{
101+
if ($idSite !== null && count($idSites) === 1) {
102+
return Piwik::translate('Ecommerce_EcommercePolicySettingCompliantSingle');
103+
}
104+
105+
return Piwik::translate('Ecommerce_EcommercePolicySettingCompliantAll');
106+
}
107+
}

plugins/Ecommerce/lang/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
"EcommerceOverviewSubcategoryHelp1": "The Ecommerce Overview section is the best place to get a high-level view of your online store’s performance. At a glance, you can see how many sales you’re making, how much revenue you are generating, and your website’s conversion rate.",
2424
"EcommerceOverviewSubcategoryHelp2": "Click on an individual metric within the sparkline chart to focus on it within the full-sized evolution graph.",
2525
"EcommerceOverviewSubcategoryHelp3": "Learn more in our Ecommerce guide here.",
26+
"EcommercePolicySettingRequirementNote": "Ecommerce analytics is enabled, but specific segments are disabled (Order ID, Order revenue, Product price, Product name, Product SKU). If you prefer, you can completely disable Ecommerce tracking %1$shere%2$s.",
27+
"EcommercePolicySettingTitle": "Ecommerce Restricted",
28+
"EcommercePolicySettingCompliantSingle": "Compliant because ecommerce isn't active for this measurable.",
29+
"EcommercePolicySettingCompliantAll": "Compliant because ecommerce isn't active for any sites.",
30+
"EcommercePolicySettingNonCompliantNote": "Non compliant because ecommerce analytics is enabled unrestricted.",
2631
"SalesSubcategoryHelp1": "This section contains an extensive collection of reports to help you analyse the different conditions that most commonly lead to sales, such as the traffic and campaign sources, user time and location and devices used to access them.",
2732
"SalesSubcategoryHelp2": "You can also learn exactly how revenue is associated with each dimension, such as specific traffic types or tracked campaigns.",
2833
"EcommerceLogSubcategoryHelp1": "The Ecommerce log provides granular session-level data so you can look at the full session for each user that either made a purchase or abandoned their cart. This can help you understand what users do before and after purchasing to reveal optimisation opportunities.",
2934
"EcommerceLogSubcategoryHelp2": "Data on this page is updated in real-time.",
3035
"ProductSubcategoryHelp": "The Products view can help you identify products and categories that are over-performing or under-performing to reveal trends and opportunities related to your product selection and store pages."
3136
}
32-
}
37+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\Ecommerce\tests\Integration;
11+
12+
use Piwik\Policy\CnilPolicy;
13+
use Piwik\Plugins\Ecommerce\Settings\EcommerceRestricted;
14+
use Piwik\Tests\Framework\Fixture;
15+
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
16+
17+
class EcommerceRestrictedTest extends IntegrationTestCase
18+
{
19+
private $ecommerceSite;
20+
private $nonEcommerceSite;
21+
22+
public function setUp(): void
23+
{
24+
parent::setUp();
25+
26+
Fixture::createSuperUser();
27+
$this->ecommerceSite = Fixture::createWebsite('2024-01-01 00:00:00', $ecommerce = 1);
28+
$this->nonEcommerceSite = Fixture::createWebsite('2024-01-02 00:00:00', $ecommerce = 0);
29+
}
30+
31+
public function tearDown(): void
32+
{
33+
CnilPolicy::setActiveStatus(null, false);
34+
parent::tearDown();
35+
}
36+
37+
public function testReturnsCompliantMessageWhenEcommerceDisabled(): void
38+
{
39+
$note = EcommerceRestricted::getComplianceRequirementNote($this->nonEcommerceSite);
40+
41+
$this->assertEquals('Ecommerce_EcommercePolicySettingCompliantSingle', $note);
42+
}
43+
44+
public function testReturnsNonCompliantMessageWhenPolicyNotEnforced(): void
45+
{
46+
CnilPolicy::setActiveStatus(null, false);
47+
$note = EcommerceRestricted::getComplianceRequirementNote($this->ecommerceSite);
48+
49+
$this->assertEquals('Ecommerce_EcommercePolicySettingNonCompliantNote', $note);
50+
}
51+
52+
public function testReturnsRequirementNoteWithLinkWhenPolicyEnforced(): void
53+
{
54+
CnilPolicy::setActiveStatus(null, true);
55+
$_GET = [];
56+
57+
$note = EcommerceRestricted::getComplianceRequirementNote($this->ecommerceSite);
58+
59+
$this->assertEquals('Ecommerce_EcommercePolicySettingRequirementNote', $note);
60+
}
61+
62+
public function testIsCompliantWhenEcommerceDisabled(): void
63+
{
64+
CnilPolicy::setActiveStatus(null, true);
65+
66+
$this->assertTrue(
67+
EcommerceRestricted::isCompliant(CnilPolicy::class, $this->nonEcommerceSite)
68+
);
69+
}
70+
71+
public function testIsCompliantWhenEcommerceEnabledAndPolicyEnforced(): void
72+
{
73+
CnilPolicy::setActiveStatus(null, true);
74+
75+
$this->assertTrue(
76+
EcommerceRestricted::isCompliant(CnilPolicy::class, $this->ecommerceSite)
77+
);
78+
}
79+
80+
public function testIsCompliantWhenEcommerceEnabledAndPolicyNotEnforced(): void
81+
{
82+
CnilPolicy::setActiveStatus(null, false);
83+
84+
$this->assertFalse(
85+
EcommerceRestricted::isCompliant(CnilPolicy::class, $this->ecommerceSite)
86+
);
87+
}
88+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/**
4+
* Matomo - free/libre analytics platform
5+
*
6+
* @link https://matomo.org
7+
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
8+
*/
9+
10+
namespace Piwik\Plugins\Ecommerce\tests\Integration;
11+
12+
use Piwik\Cache;
13+
use Piwik\Config;
14+
use Piwik\Plugins\PrivacyManager\FeatureFlags\PrivacyCompliance;
15+
use Piwik\Policy\CnilPolicy;
16+
use Piwik\Segment\SegmentsList;
17+
use Piwik\Tests\Framework\Fixture;
18+
use Piwik\Tests\Framework\TestCase\IntegrationTestCase;
19+
20+
/**
21+
* @group Ecommerce
22+
* @group EcommerceSegmentsFilter
23+
* @group Integration
24+
*/
25+
class SegmentsFilterTest extends IntegrationTestCase
26+
{
27+
private $idSite;
28+
29+
public function setUp(): void
30+
{
31+
parent::setUp();
32+
33+
Fixture::createSuperUser();
34+
$this->idSite = Fixture::createWebsite('2023-01-01 00:00:00', $ecommerce = 1);
35+
}
36+
37+
public function tearDown(): void
38+
{
39+
$this->disablePrivacyCompliance();
40+
unset($_GET['idSite'], $_POST['idSite']);
41+
Cache::getTransientCache()->flushAll();
42+
43+
parent::tearDown();
44+
}
45+
46+
public function testSegmentsNotFilteredWhenComplianceDisabled(): void
47+
{
48+
$this->disablePrivacyCompliance();
49+
50+
$segmentsList = $this->getSegmentsListForSite();
51+
52+
$this->assertNotNull($segmentsList->getSegment('orderId'));
53+
$this->assertNotNull($segmentsList->getSegment('revenueOrder'));
54+
$this->assertNotNull($segmentsList->getSegment('productPrice'));
55+
$this->assertNotNull($segmentsList->getSegment('productName'));
56+
$this->assertNotNull($segmentsList->getSegment('productSku'));
57+
}
58+
59+
public function testSegmentsFilteredWhenComplianceEnabled(): void
60+
{
61+
$this->enablePrivacyCompliance();
62+
63+
$segmentsList = $this->getSegmentsListForSite();
64+
65+
$this->assertNull($segmentsList->getSegment('orderId'));
66+
$this->assertNull($segmentsList->getSegment('revenueOrder'));
67+
$this->assertNull($segmentsList->getSegment('productPrice'));
68+
$this->assertNull($segmentsList->getSegment('productName'));
69+
$this->assertNull($segmentsList->getSegment('productSku'));
70+
}
71+
72+
private function getSegmentsListForSite(): SegmentsList
73+
{
74+
Cache::getTransientCache()->flushAll();
75+
76+
$_GET['idSite'] = $this->idSite;
77+
78+
return SegmentsList::get();
79+
}
80+
81+
private function enablePrivacyCompliance(): void
82+
{
83+
$featureFlag = new PrivacyCompliance();
84+
Config::getInstance()->FeatureFlags = [$featureFlag->getName() . '_feature' => 'enabled'];
85+
CnilPolicy::setActiveStatus(null, true);
86+
}
87+
88+
private function disablePrivacyCompliance(): void
89+
{
90+
$featureFlag = new PrivacyCompliance();
91+
Config::getInstance()->FeatureFlags = [$featureFlag->getName() . '_feature' => 'disabled'];
92+
CnilPolicy::setActiveStatus(null, false);
93+
}
94+
}

plugins/PrivacyManager/stylesheets/compliance.less

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
line-height: 1.5rem;
1111
padding-left: 0.5rem;
1212
padding-right: 0.5rem;
13+
white-space: nowrap;
1314

1415
&.compliant {
1516
color: #5D9E52;

plugins/PrivacyManager/tests/System/expected/test___PrivacyManager.getComplianceStatus.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@
33
<complianceModeEnforced>0</complianceModeEnforced>
44
<complianceConfigControlled>0</complianceConfigControlled>
55
<complianceRequirements>
6-
<row>
7-
<name>Ecommerce</name>
8-
<value>non_compliant</value>
9-
<notes>Ecommerce must be disabled</notes>
10-
</row>
116
<row>
127
<name>Device model detection disabled</name>
138
<value>non_compliant</value>
@@ -19,6 +14,11 @@
1914
<notes>Only major OS and browser versions are stored.</notes>
2015
</row>
2116
<row>
17+
<name>Ecommerce Restricted</name>
18+
<value>non_compliant</value>
19+
<notes>Non compliant because ecommerce analytics is enabled unrestricted.</notes>
20+
</row>
21+
<row>
2222
<name>PII data filtered</name>
2323
<value>non_compliant</value>
2424
<notes>PII data must be filtered by the Matomo-recommended PII exclusion list.</notes>

0 commit comments

Comments
 (0)