Skip to content

Commit 3a45a8b

Browse files
committed
Introduce new event PrivacyManager.deleteDataSubjectsForDeletedSites
1 parent fbe43a4 commit 3a45a8b

File tree

4 files changed

+99
-4
lines changed

4 files changed

+99
-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: 52 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,9 @@ public function setUp(): void
3437

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

39-
public function testLogDataPurgingRemovesBotRequests(): void
40-
{
41+
Db::query('TRUNCATE TABLE ' . BotRequestsDao::getPrefixedTableName());
42+
4143
// track some bot requests
4244
$t = Fixture::getTracker(1, '2025-02-02 12:00:00');
4345
$t->setUserAgent('Mozilla/5.0 (compatible; ChatGPT-User/1.0)');
@@ -56,7 +58,10 @@ public function testLogDataPurgingRemovesBotRequests(): void
5658
$t->setUrl('https://matomo.org/faq/576');
5759
$t->setCustomTrackingParameter('recMode', '1');
5860
Fixture::checkResponse($t->doTrackPageView(''));
61+
}
5962

63+
public function testLogDataPurgingRemovesBotRequests(): void
64+
{
6065
// check that all requests were tracked
6166
$tableName = BotRequestsDao::getPrefixedTableName();
6267
$sql = "SELECT COUNT(*) FROM `{$tableName}`";
@@ -69,10 +74,53 @@ public function testLogDataPurgingRemovesBotRequests(): void
6974
$purger->purgeData($days, true);
7075

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

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)