Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add privacy setting allowing to randomise config ID on the backend #22952

Merged
merged 21 commits into from
Feb 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
586af7f
Implement initial config ID randomisation
michalkleiner Jan 14, 2025
1820a2e
Build dist files
michalkleiner Jan 14, 2025
2422497
Fix conditional logic
michalkleiner Jan 15, 2025
822c7f9
Initialize system test and test fixture
michalkleiner Jan 15, 2025
468c46f
Fix tests
michalkleiner Jan 20, 2025
71fd4ec
Directly set request metadata and return early when randomising confi…
michalkleiner Jan 20, 2025
2b5f11d
fix failing test
sgiehl Jan 21, 2025
e34cffb
fix phpcs
sgiehl Jan 21, 2025
ed07bf5
Use type-loose comparison in tests
michalkleiner Jan 22, 2025
e18f28e
Put UI and randomisation mechanism behind a feature flag
michalkleiner Jan 24, 2025
daf31bf
Fix model variable name in vue component
michalkleiner Jan 24, 2025
3def27b
Update system test to reflect using feature flag
michalkleiner Jan 27, 2025
6d09589
Only use feature flag to toggle UI controls
michalkleiner Jan 27, 2025
b9998c6
Remove unused use statements
michalkleiner Jan 27, 2025
b1f4396
Tweak UI tests to show both enabled and disabled feature flag for UI …
michalkleiner Jan 27, 2025
24d0b46
Add feature flag note it's only for the UI
michalkleiner Jan 27, 2025
9951901
Move static datetimes from test to fixture for better reuse
michalkleiner Jan 28, 2025
1a6e256
Apply PR feedback
michalkleiner Jan 29, 2025
6c19fea
Simplify config hash generation
michalkleiner Jan 29, 2025
d0a93c1
Use just the date portion to prevent binding error in tests
michalkleiner Jan 31, 2025
ecc191a
Use positional parameters in test SQL queries
mneudert Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions core/Tracker/Settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __construct($isSameFingerprintsAcrossWebsites)
$this->isSameFingerprintsAcrossWebsites = $isSameFingerprintsAcrossWebsites;
}

public function getConfigId(Request $request, $ipAddress)
public function getConfigId(Request $request, $ipAddress): string
{
list($plugin_Flash, $plugin_Java, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF,
$plugin_WindowsMedia, $plugin_Silverlight, $plugin_Cookie) = $request->getPlugins();
Expand Down Expand Up @@ -115,6 +115,11 @@ public function getConfigId(Request $request, $ipAddress)
);
}

public function getRandomConfigId(): string
{
return $this->getRandomConfigHash();
}

/**
* Returns a 64-bit hash that attempts to identify a user.
* Maintaining some privacy by default, eg. prevents the merging of several Piwik serve together for matching across instances..
Expand Down Expand Up @@ -151,7 +156,7 @@ protected function getConfigHash(
$ip,
$browserLang,
$fingerprintHash
) {
): string {
// prevent the config hash from being the same, across different Piwik instances
// (limits ability of different Piwik instances to cross-match users)
$salt = SettingsPiwik::getSalt();
Expand All @@ -170,6 +175,16 @@ protected function getConfigHash(
$configString .= $request->getIdSite();
}

return $this->createHashOfConfigString($configString);
}

protected function getRandomConfigHash(): string
{
return $this->createHashOfConfigString(random_bytes(64));
}

private function createHashOfConfigString(string $configString): string
{
$hash = md5($configString, $raw_output = true);

return substr($hash, 0, Tracker::LENGTH_BINARY_ID);
Expand Down
10 changes: 10 additions & 0 deletions plugins/CoreHome/Tracker/VisitRequestProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,16 @@ public function processRequestParams(VisitProperties $visitProperties, Request $

$privacyConfig = new PrivacyManagerConfig();

if ($privacyConfig->randomizeConfigId) {
mneudert marked this conversation as resolved.
Show resolved Hide resolved
// always new visit when randomising config id
$request->setMetadata('CoreHome', 'visitorId', $this->userSettings->getRandomConfigId());
$request->setMetadata('CoreHome', 'isVisitorKnown', false);
$request->setMetadata('CoreHome', 'isNewVisit', true);
$request->setMetadata('CoreHome', 'lastKnownVisit', false);

return false;
}

$ip = $request->getIpString();
if ($privacyConfig->useAnonymizedIpForVisitEnrichment) {
$ip = $visitProperties->getProperty('location_ip');
Expand Down
6 changes: 5 additions & 1 deletion plugins/PrivacyManager/API.php
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ private function formatAvailableColumnsToAnonymize($columns)
/**
* @internal
*/
public function setAnonymizeIpSettings($anonymizeIPEnable, $maskLength, $useAnonymizedIpForVisitEnrichment, $anonymizeUserId = false, $anonymizeOrderId = false, $anonymizeReferrer = '', $forceCookielessTracking = false)
public function setAnonymizeIpSettings($anonymizeIPEnable, $maskLength, $useAnonymizedIpForVisitEnrichment, $anonymizeUserId = false, $anonymizeOrderId = false, $anonymizeReferrer = '', $forceCookielessTracking = false, $randomizeConfigId = false)
{
Piwik::checkUserHasSuperUserAccess();

Expand Down Expand Up @@ -238,6 +238,10 @@ public function setAnonymizeIpSettings($anonymizeIPEnable, $maskLength, $useAnon
Piwik::postEvent('CustomJsTracker.updateTracker');
}

if (false !== $randomizeConfigId) {
$privacyConfig->randomizeConfigId = (bool) $randomizeConfigId;
}

return true;
}

Expand Down
2 changes: 2 additions & 0 deletions plugins/PrivacyManager/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* @property int $anonymizeUserId If enabled, it will pseudo anonymize the User ID
* @property int $anonymizeOrderId If enabled, it will anonymize the Order ID
* @property string $anonymizeReferrer Whether the referrer should be anonymized and how it much it should be anonymized
* @property bool $randomizeConfigId If enabled, Matomo will generate a new random Config ID (fingerprint) for each tracking request
*/
class Config
{
Expand All @@ -40,6 +41,7 @@ class Config
'anonymizeUserId' => array('type' => 'boolean', 'default' => false),
'anonymizeOrderId' => array('type' => 'boolean', 'default' => false),
'anonymizeReferrer' => array('type' => 'string', 'default' => ''),
'randomizeConfigId' => array('type' => 'boolean', 'default' => false),
);

public function __set($name, $value)
Expand Down
10 changes: 9 additions & 1 deletion plugins/PrivacyManager/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
use Piwik\Piwik;
use Piwik\Plugin\Manager;
use Piwik\Plugins\CustomJsTracker\File;
use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\LanguagesManager\API as APILanguagesManager;
use Piwik\Plugins\PrivacyManager\FeatureFlags\ConfigIdRandomisation;
use Piwik\Plugins\SitesManager\SiteContentDetection\ConsentManagerDetectionAbstract;
use Piwik\Plugins\SitesManager\SiteContentDetection\SiteContentDetectionAbstract;
use Piwik\SiteContentDetector;
Expand All @@ -46,11 +48,15 @@ class Controller extends \Piwik\Plugin\ControllerAdmin
/** @var SiteContentDetector */
private $siteContentDetector;

public function __construct(ReferrerAnonymizer $referrerAnonymizer, SiteContentDetector $siteContentDetector)
/** @var FeatureFlagManager */
private $featureFlagManager;

public function __construct(ReferrerAnonymizer $referrerAnonymizer, SiteContentDetector $siteContentDetector, FeatureFlagManager $featureFlagManager)
{
parent::__construct();
$this->referrerAnonymizer = $referrerAnonymizer;
$this->siteContentDetector = $siteContentDetector;
$this->featureFlagManager = $featureFlagManager;
}

private function checkDataPurgeAdminSettingsIsEnabled()
Expand Down Expand Up @@ -246,6 +252,7 @@ public function privacySettings()
$view->dbUser = PiwikConfig::getInstance()->database['username'];
$view->deactivateNonce = Nonce::getNonce(self::DEACTIVATE_DNT_NONCE);
$view->activateNonce = Nonce::getNonce(self::ACTIVATE_DNT_NONCE);
$view->configRandomisationFeatureFlag = $this->featureFlagManager->isFeatureActive(ConfigIdRandomisation::class);

$view->maskLengthOptions = [
['key' => '1',
Expand Down Expand Up @@ -363,6 +370,7 @@ private function getAnonymizeIPInfo()
if (!$anonymizeIP["useAnonymizedIpForVisitEnrichment"]) {
$anonymizeIP["useAnonymizedIpForVisitEnrichment"] = '0';
}
$anonymizeIP["randomizeConfigId"] = $privacyConfig->randomizeConfigId;

return $anonymizeIP;
}
Expand Down
31 changes: 31 additions & 0 deletions plugins/PrivacyManager/FeatureFlags/ConfigIdRandomisation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

declare(strict_types=1);

namespace Piwik\Plugins\PrivacyManager\FeatureFlags;

use Piwik\Plugins\FeatureFlags\FeatureFlagInterface;

/**
* PLEASE NOTE!
*
* This feature flag only controls if the Config ID randomisation setting is visible in the Privacy settings.
*
* Disabling the feature flag once the privacy setting was enabled won't stop the config ID randomisation unless
* disabled, either through the UI with the feature flag enabled or by removing the option from the db.
*
*/
class ConfigIdRandomisation implements FeatureFlagInterface
{
public function getName(): string
{
return 'ConfigIdRandomisation';
}
}
2 changes: 2 additions & 0 deletions plugins/PrivacyManager/PrivacyManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,8 @@ public function getClientSideTranslationKeys(&$translationKeys)
$translationKeys[] = 'Overlay_Location';
$translationKeys[] = 'General_UserId';
$translationKeys[] = 'General_Done';
$translationKeys[] = 'PrivacyManager_UseRandomizeConfigId';
$translationKeys[] = 'PrivacyManager_RandomizeConfigIdNote';
}

public function setTrackerCacheGeneral(&$cacheContent)
Expand Down
6 changes: 4 additions & 2 deletions plugins/PrivacyManager/lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,8 @@
"FindMatchingDataSubjects": "Find matching data subjects",
"ConsentManager": "Consent Manager",
"ConsentManagerDetected": "%1$s consent manager was detected on your website. To learn about configuring Matomo to work with %1$s please read %2$sthis guide%3$s",
"ConsentManagerConnected": "%1$s appears to already be configured to work with Matomo."
"ConsentManagerConnected": "%1$s appears to already be configured to work with Matomo.",
"UseRandomizeConfigId": "Randomize config_ID for enhanced privacy",
"RandomizeConfigIdNote": "By randomizing the config_ID for every request, this feature ensures that visitor tracking is compliant with the strictest privacy interpretations. This setting disables mechanisms that rely on user-agent data, cookies, or other identifiers that require explicit consent, allowing for anonymized tracking while respecting user privacy."
}
}
}
2 changes: 2 additions & 0 deletions plugins/PrivacyManager/templates/privacySettings.twig
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
schedule-deletion-options="{{ scheduleDeletionOptions|default(null)|json_encode }}"
anonymizations="{{ anonymizations|json_encode }}"
is-super-user="{{ isSuperUser|json_encode }}"
randomize-config-id="{{ anonymizeIP.randomizeConfigId|json_encode }}"
config-randomisation-feature-flag="{{ configRandomisationFeatureFlag|json_encode }}"
></div>

{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php

/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

namespace Piwik\Plugins\PrivacyManager\tests\Fixtures;

use Piwik\Date;
use Piwik\Option;
use Piwik\Plugins\PrivacyManager\Config as PrivacyManagerConfig;
use Piwik\Plugins\PrivacyManager\PrivacyManager;
use Piwik\Tests\Framework\Fixture;
use Piwik\Tracker\Cache;

class RandomizedConfigIdVisitsFixture extends Fixture
{
public static $dateTimeNormalConfig = '2015-01-01 01:00:00';
public static $dateTimeRandomizedConfig = '2015-02-01 01:00:00'; // as above + 1 month

public $dateTime;
public $idSite = 1;

/** @var PrivacyManagerConfig */
private $privacyManagerConfig;

public function setUp(): void
{
$this->dateTime = self::$dateTimeNormalConfig;

Option::set(PrivacyManager::OPTION_USERID_SALT, 'simpleuseridsalt1');
Cache::clearCacheGeneral();

$this->privacyManagerConfig = new PrivacyManagerConfig();

$this->setUpWebsite();

// config off
// should NOT randomize
$this->trackVisits(false);

// config on
// should randomize
$this->dateTime = self::$dateTimeRandomizedConfig;
$this->trackVisits(true);
}

public function tearDown(): void
{
// empty
}

private function setConfigIdRandomisationPrivacyConfig(bool $config)
{
$this->privacyManagerConfig->randomizeConfigId = $config;
}

private function addHour()
{
$this->dateTime = Date::factory($this->dateTime)->addPeriod(1, 'hour')->getDatetime();
}

private function setUpWebsite()
{
if (!self::siteCreated($this->idSite)) {
$idSite = self::createWebsite($this->dateTime, $ecommerce = 1);
$this->assertSame($this->idSite, $idSite);
}
}

protected function trackStandardVisits(int $visits)
{
$t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
$t->setUrl('http://example.com/');
for ($v = 1; $v <= $visits; $v++) {
$dt = Date::factory($this->dateTime)->addPeriod($v, 'minute')->getDatetime();
$t->setForceVisitDateTime($dt);
self::checkResponse($t->doTrackPageView("Standard visit - $dt"));
}
}

protected function trackVisitsWithMultipleActions(int $visits, int $actions)
{
for ($v = 1; $v <= $visits; $v++) {
$t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
$t->setUrl('http://example.com/');
$t->setForceVisitDateTime(Date::factory($this->dateTime)->addPeriod($v, 'minute')->getDatetime());

self::checkResponse($t->doTrackPageView("Visit with actions - $v"));
for ($a = 1; $a <= $actions; $a++) {
$dt = Date::factory($this->dateTime)
->addPeriod($v, 'minute')
->addPeriod($a, 'second')
->getDatetime();
$t->setForceVisitDateTime($dt);
self::checkResponse($t->doTrackAction("http://example.com/$dt", 'link'));
}
}
}

protected function trackVisitsWithUserId(int $visits)
{
$t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
$t->setUserId('foobar');
$t->setUrl('http://example.com/');
for ($v = 1; $v <= $visits; $v++) {
$dt = Date::factory($this->dateTime)->addPeriod($v, 'minute')->getDatetime();
$t->setForceVisitDateTime($dt);
self::checkResponse($t->doTrackPageView("Visit with user ID set - $dt"));
}
}

protected function trackEcommerceOrder(int $orders)
{
$t = self::getTracker($this->idSite, $this->dateTime, $defaultInit = true);
$t->setUrl('http://example.com/myorder');
self::checkResponse($t->doTrackPageView('Visit with ecommerce order'));

for ($o = 1; $o <= $orders; $o++) {
$dt = Date::factory($this->dateTime)->addPeriod($o, 'second')->getDatetime();
$t->setForceVisitDateTime($dt);
$t->doTrackEcommerceOrder('Ecommerce order ID - ' . $dt, 10 * $o, 7, 2, 1, 0);
}
}

protected function trackVisits(bool $randomizeConfigId)
{
$this->setConfigIdRandomisationPrivacyConfig($randomizeConfigId);

// track visits
$this->trackStandardVisits(2);
$this->addHour();

// track visits with multiple actions
$this->trackVisitsWithMultipleActions(3, 2);
$this->addHour();

// track visits with set UserID
$this->trackVisitsWithUserId(2);
$this->addHour();

// track ecommerce order
$this->trackEcommerceOrder(3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ public function testSetTrackerCacheContent()
'PrivacyManager.anonymizeReferrer' => '',
'PrivacyManager.useAnonymizedIpForVisitEnrichment' => false,
'PrivacyManager.forceCookielessTracking' => false,
'PrivacyManager.randomizeConfigId' => false,
);

$this->assertEquals($expected, $content);
Expand Down
Loading
Loading