Skip to content

Commit

Permalink
Add privacy setting allowing to randomise config ID on the backend (#…
Browse files Browse the repository at this point in the history
…22952)

* Implement initial config ID randomisation
* Initialise system test and test fixture
* Put UI control behind a feature flag
* Build dist files
  • Loading branch information
michalkleiner authored Feb 1, 2025
1 parent dfff2d0 commit 74e65ea
Show file tree
Hide file tree
Showing 18 changed files with 442 additions and 45 deletions.
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) {
// 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

0 comments on commit 74e65ea

Please sign in to comment.