Skip to content

Commit 786befb

Browse files
committed
Introduce new event PrivacyManager.deleteDataSubjectsForDeletedSites
1 parent e85df81 commit 786befb

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

plugins/BotTracking/BotTracking.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public function registerEvents(): array
4040
{
4141
return [
4242
'PrivacyManager.deleteLogsOlderThan' => 'deleteLogsOlderThan',
43+
'PrivacyManager.deleteDataSubjectsForDeletedSites' => 'deleteDataSubjectsForDeletedSites',
4344
'Tracker.isBotRequest' => 'isBotRequest',
4445
];
4546
}
@@ -65,6 +66,12 @@ public function deleteLogsOlderThan(Date $dateUpperLimit): void
6566
(new BotRequestsDao())->deleteOldRecords($dateUpperLimit);
6667
}
6768

69+
public function deleteDataSubjectsForDeletedSites(array &$result, array $idSitesNoLongerExisting): void
70+
{
71+
$dao = new BotRequestsDao();
72+
$result[$dao::getTableName()] = $dao->deleteRecordsForIdSites($idSitesNoLongerExisting);
73+
}
74+
6875
/**
6976
* @todo Remove, once Device Detector is able to detect all known ai bots
7077
*/

plugins/BotTracking/Dao/BotRequestsDao.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,25 @@ public function deleteOldRecords(Date $date): int
122122

123123
return (int)Db::get()->rowCount($result);
124124
}
125+
126+
/**
127+
* Delete bot telemetry records for specific sites
128+
*
129+
* @param array<int|string> $siteIds Delete records older than this date
130+
* @return int Number of deleted records
131+
*/
132+
public function deleteRecordsForIdSites(array $siteIds): int
133+
{
134+
$tableName = self::getPrefixedTableName();
135+
$siteIds = array_map('intval', $siteIds);
136+
137+
$sql = sprintf(
138+
'DELETE FROM `%s` WHERE idsite IN (' . implode(', ', $siteIds) . ') LIMIT 25000',
139+
$tableName
140+
);
141+
142+
$result = Db::query($sql);
143+
144+
return (int)Db::get()->rowCount($result);
145+
}
125146
}

plugins/BotTracking/tests/System/PurgeLogDataTest.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,16 @@
1111

1212
namespace Piwik\Plugins\BotTracking\tests\System;
1313

14+
use Piwik\Container\StaticContainer;
1415
use Piwik\DataAccess\RawLogDao;
1516
use Piwik\Date;
1617
use Piwik\Db;
1718
use Piwik\LogDeleter;
1819
use Piwik\Plugin\Dimension\DimensionMetadataProvider;
1920
use Piwik\Plugins\BotTracking\Dao\BotRequestsDao;
2021
use Piwik\Plugins\PrivacyManager\LogDataPurger;
22+
use Piwik\Plugins\PrivacyManager\Model\DataSubjects;
23+
use Piwik\Plugins\SitesManager\API as SitesManagerAPI;
2124
use Piwik\Tests\Framework\Fixture;
2225
use Piwik\Tests\Framework\Mock\Plugin\LogTablesProvider;
2326
use Piwik\Tests\Framework\TestCase\SystemTestCase;
@@ -34,10 +37,7 @@ public function setUp(): void
3437

3538
Fixture::createSuperUser();
3639
Fixture::createWebsite('2014-02-04');
37-
}
3840

39-
public function testLogDataPurgingRemovesBotRequests(): void
40-
{
4141
// track some bot requests
4242
$t = Fixture::getTracker(1, '2025-02-02 12:00:00');
4343
$t->setUserAgent('Mozilla/5.0 (compatible; ChatGPT-User/1.0)');
@@ -56,7 +56,10 @@ public function testLogDataPurgingRemovesBotRequests(): void
5656
$t->setUrl('https://matomo.org/faq/576');
5757
$t->setCustomTrackingParameter('recMode', '1');
5858
Fixture::checkResponse($t->doTrackPageView(''));
59+
}
5960

61+
public function testLogDataPurgingRemovesBotRequests(): void
62+
{
6063
// check that all requests were tracked
6164
$tableName = BotRequestsDao::getPrefixedTableName();
6265
$sql = "SELECT COUNT(*) FROM `{$tableName}`";
@@ -69,10 +72,53 @@ public function testLogDataPurgingRemovesBotRequests(): void
6972
$purger->purgeData($days, true);
7073

7174
// ensure that two bot requests were removed
72-
$tableName = BotRequestsDao::getPrefixedTableName();
7375
$sql = "SELECT * FROM `{$tableName}`";
7476
$bots = Db::fetchAll($sql);
7577
self::assertCount(1, $bots);
7678
self::assertEquals('MistralAI-User', $bots[0]['bot_name']);
7779
}
80+
81+
public function testDeleteDataSubjectsForDeletedSitesRemovesBotRequests(): void
82+
{
83+
// track a normal visit that gets removed, otherwise bot requests won't be removed
84+
$t = Fixture::getTracker(1, '2025-02-02 12:00:00');
85+
$t->setUrl('https://matomo.org/faq/123');
86+
Fixture::checkResponse($t->doTrackPageView(''));
87+
88+
// track request for another site
89+
Fixture::createWebsite('2014-02-04');
90+
91+
// track some bot requests
92+
$t = Fixture::getTracker(2, '2025-02-02 12:00:00');
93+
$t->setUserAgent('Mozilla/5.0 (compatible; ChatGPT-User/1.0)');
94+
$t->setUrl('https://matomo.org/faq/123');
95+
$t->setCustomTrackingParameter('recMode', '1');
96+
Fixture::checkResponse($t->doTrackPageView(''));
97+
98+
// remove site 1
99+
SitesManagerAPI::getInstance()->deleteSite(1);
100+
101+
// check that all requests still exist
102+
$tableName = BotRequestsDao::getPrefixedTableName();
103+
$sql = "SELECT COUNT(*) FROM `{$tableName}`";
104+
self::assertEquals(4, Db::fetchOne($sql));
105+
106+
$logTablesProvider = StaticContainer::get('Piwik\Plugin\LogTablesProvider');
107+
$dataSubjects = new DataSubjects($logTablesProvider);
108+
$result = $dataSubjects->deleteDataSubjectsForDeletedSites([2]); // idsite 2 still exists
109+
$this->assertEquals([
110+
'log_visit' => 1,
111+
'log_link_visit_action' => 1,
112+
'log_conversion_item' => 0,
113+
'log_conversion' => 0,
114+
'log_bot_request' => 3,
115+
], $result);
116+
117+
// check that requests were correctly removed
118+
$sql = "SELECT COUNT(*) FROM `{$tableName}` WHERE `idsite` = 1";
119+
self::assertEquals(0, Db::fetchOne($sql));
120+
121+
$sql = "SELECT COUNT(*) FROM `{$tableName}` WHERE `idsite` = 2";
122+
self::assertEquals(1, Db::fetchOne($sql));
123+
}
78124
}

plugins/PrivacyManager/Model/DataSubjects.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ public function deleteDataSubjectsForDeletedSites($allExistingIdSites)
8686
}
8787
}
8888

89+
/**
90+
* Lets you delete data subjects to make your plugin GDPR compliant.
91+
* This can be useful if you have developed a plugin which stores any data for specific sites, not bound to a visit but doesn't
92+
* use any core logic to store this data. If core API's are used, for example log tables, then the data may
93+
* be deleted automatically.
94+
*
95+
* **Example**
96+
*
97+
* public function deleteDataSubjectsForDeletedSites(&$result, $idSitesNoLongerExisting)
98+
* {
99+
* $numDeletes = $this->deleteDataForSites($idSitesNoLongerExisting)
100+
* $result['myplugin'] = $numDeletes;
101+
* }
102+
*
103+
* @param array &$results An array storing the result of how much data was deleted for.
104+
* @param array &$idSitesNoLongerExisting An array with multiple site ids that were removed
105+
*/
106+
Piwik::postEvent('PrivacyManager.deleteDataSubjectsForDeletedSites', [&$results, $idSitesNoLongerExisting]);
107+
89108
krsort($results); // make sure test results are always in same order
90109
return $results;
91110
}

0 commit comments

Comments
 (0)