mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-09-27 20:21:59 +02:00
feat(analytics): add current date and secret salt to analytics hash for improved privacy
This commit is contained in:
parent
d021abb52f
commit
6f2e7c009c
@ -213,6 +213,24 @@ if (! function_exists('podcast_uuid')) {
|
|||||||
|
|
||||||
//--------------------------------------------------------------------
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (! function_exists('generate_random_salt')) {
|
||||||
|
function generate_random_salt(int $length = 64): string
|
||||||
|
{
|
||||||
|
$salt = '';
|
||||||
|
while (strlen($salt) < $length) {
|
||||||
|
$charNumber = random_int(33, 126);
|
||||||
|
// Exclude " ' \ `
|
||||||
|
if (! in_array($charNumber, [34, 39, 92, 96], true)) {
|
||||||
|
$salt .= chr($charNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $salt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//--------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
if (! function_exists('file_upload_max_size')) {
|
if (! function_exists('file_upload_max_size')) {
|
||||||
|
|
||||||
|
@ -25,6 +25,19 @@ class Analytics extends BaseConfig
|
|||||||
'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
|
'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
* Secret Salt
|
||||||
|
* --------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* The secret salt is a string of random characters that is used when hashing data.
|
||||||
|
* Each Castopod instance has its own secret salt so keys will never be the same.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* Z&|qECKBrwgaaD>~;U/tXG1U%tSe_oi5Tzy)h>}5NC2npSrjvM0w_Q>cs=0o=H]*
|
||||||
|
*/
|
||||||
|
public string $salt = '';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the full audio file url
|
* get the full audio file url
|
||||||
*
|
*
|
||||||
|
@ -241,12 +241,12 @@ if (! function_exists('podcast_hit')) {
|
|||||||
* Counting podcast episode downloads for analytic purposes ✅ No IP address is ever stored on the server. ✅ Only
|
* Counting podcast episode downloads for analytic purposes ✅ No IP address is ever stored on the server. ✅ Only
|
||||||
* aggregate data is stored in the database. We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
|
* aggregate data is stored in the database. We follow IAB Podcast Measurement Technical Guidelines Version 2.0:
|
||||||
* https://iabtechlab.com/standards/podcast-measurement-guidelines/
|
* https://iabtechlab.com/standards/podcast-measurement-guidelines/
|
||||||
* https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf ✅ Rolling 24-hour
|
* https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf ✅ 24-hour window ✅
|
||||||
* window ✅ Castopod does not do pre-load ✅ IP deny list https://github.com/client9/ipcat ✅ User-agent
|
* Castopod does not do pre-load ✅ IP deny list https://github.com/client9/ipcat ✅ User-agent Filtering
|
||||||
* Filtering https://github.com/opawg/user-agents ✅ RSS User-agent https://github.com/opawg/podcast-rss-useragents
|
* https://github.com/opawg/user-agents ✅ RSS User-agent https://github.com/opawg/podcast-rss-useragents ✅
|
||||||
* ✅ Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app) ✅ In case of partial
|
* Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app) ✅ In case of partial content,
|
||||||
* content, adds up all requests to check >1mn was downloaded ✅ Identifying Uniques is done with a combination of
|
* adds up all requests to check >1mn was downloaded ✅ Identifying Uniques is done with a combination of IP
|
||||||
* IP Address and User Agent
|
* Address and User Agent
|
||||||
*
|
*
|
||||||
* @param integer $podcastId The podcast ID
|
* @param integer $podcastId The podcast ID
|
||||||
* @param integer $episodeId The Episode ID
|
* @param integer $episodeId The Episode ID
|
||||||
@ -280,19 +280,25 @@ if (! function_exists('podcast_hit')) {
|
|||||||
? $_SERVER['HTTP_RANGE']
|
? $_SERVER['HTTP_RANGE']
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// We create a sha1 hash for this IP_Address+User_Agent+Episode_ID (used to count only once multiple episode downloads):
|
$salt = config('Analytics')
|
||||||
$episodeHashId =
|
->salt;
|
||||||
'_IpUaEp_' .
|
// We create a sha1 hash for this Salt+Current_Date+IP_Address+User_Agent+Episode_ID (used to count only once multiple episode downloads):
|
||||||
sha1($_SERVER['REMOTE_ADDR'] . '_' . $_SERVER['HTTP_USER_AGENT'] . '_' . $episodeId);
|
$episodeListenerHashId =
|
||||||
|
'Analytics_Episode_' .
|
||||||
|
sha1(
|
||||||
|
$salt . '_' . date(
|
||||||
|
'Y-m-d'
|
||||||
|
) . '_' . $_SERVER['REMOTE_ADDR'] . '_' . $_SERVER['HTTP_USER_AGENT'] . '_' . $episodeId
|
||||||
|
);
|
||||||
// Was this episode downloaded in the past 24h:
|
// Was this episode downloaded in the past 24h:
|
||||||
$downloadedBytes = cache($episodeHashId);
|
$downloadedBytes = cache($episodeListenerHashId);
|
||||||
// Rolling window is 24 hours (86400 seconds):
|
// Rolling window is 24 hours (86400 seconds):
|
||||||
$rollingTTL = 86400;
|
$rollingTTL = 86400;
|
||||||
if ($downloadedBytes) {
|
if ($downloadedBytes) {
|
||||||
// In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
|
// In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download):
|
||||||
$rollingTTL =
|
$rollingTTL =
|
||||||
cache()
|
cache()
|
||||||
->getMetadata($episodeHashId)['expire'] - time();
|
->getMetadata($episodeListenerHashId)['expire'] - time();
|
||||||
} else {
|
} else {
|
||||||
// If it was never downloaded that means that zero byte were downloaded:
|
// If it was never downloaded that means that zero byte were downloaded:
|
||||||
$downloadedBytes = 0;
|
$downloadedBytes = 0;
|
||||||
@ -320,7 +326,7 @@ if (! function_exists('podcast_hit')) {
|
|||||||
|
|
||||||
// We save the number of downloaded bytes for this user and this episode:
|
// We save the number of downloaded bytes for this user and this episode:
|
||||||
cache()
|
cache()
|
||||||
->save($episodeHashId, $downloadedBytes, $rollingTTL);
|
->save($episodeListenerHashId, $downloadedBytes, $rollingTTL);
|
||||||
|
|
||||||
// If more that 1mn was downloaded, that's a hit, we send that to the database:
|
// If more that 1mn was downloaded, that's a hit, we send that to the database:
|
||||||
if ($downloadedBytes >= $bytesThreshold) {
|
if ($downloadedBytes >= $bytesThreshold) {
|
||||||
@ -329,13 +335,17 @@ if (! function_exists('podcast_hit')) {
|
|||||||
|
|
||||||
$age = intdiv(time() - $publicationTime, 86400);
|
$age = intdiv(time() - $publicationTime, 86400);
|
||||||
|
|
||||||
// We create a sha1 hash for this IP_Address+User_Agent+Podcast_ID (used to count unique listeners):
|
// We create a sha1 hash for this Salt+Current_Date+IP_Address+User_Agent+Podcast_ID (used to count unique listeners):
|
||||||
$listenerHashId =
|
$podcastListenerHashId =
|
||||||
'_IpUaPo_' .
|
'Analytics_Podcast_' .
|
||||||
sha1($_SERVER['REMOTE_ADDR'] . '_' . $_SERVER['HTTP_USER_AGENT'] . '_' . $podcastId);
|
sha1(
|
||||||
|
$salt . '_' . date(
|
||||||
|
'Y-m-d'
|
||||||
|
) . '_' . $_SERVER['REMOTE_ADDR'] . '_' . $_SERVER['HTTP_USER_AGENT'] . '_' . $podcastId
|
||||||
|
);
|
||||||
$newListener = 1;
|
$newListener = 1;
|
||||||
// Has this listener already downloaded an episode today:
|
// Has this listener already downloaded an episode today:
|
||||||
$downloadsByUser = cache($listenerHashId);
|
$downloadsByUser = cache($podcastListenerHashId);
|
||||||
// We add one download
|
// We add one download
|
||||||
if ($downloadsByUser) {
|
if ($downloadsByUser) {
|
||||||
$newListener = 0;
|
$newListener = 0;
|
||||||
@ -348,7 +358,7 @@ if (! function_exists('podcast_hit')) {
|
|||||||
$midnightTTL = strtotime('tomorrow') - time();
|
$midnightTTL = strtotime('tomorrow') - time();
|
||||||
// We save the download count for this user until midnight:
|
// We save the download count for this user until midnight:
|
||||||
cache()
|
cache()
|
||||||
->save($listenerHashId, $downloadsByUser, $midnightTTL);
|
->save($podcastListenerHashId, $downloadsByUser, $midnightTTL);
|
||||||
|
|
||||||
$db->query(
|
$db->query(
|
||||||
"CALL {$procedureName}(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
|
"CALL {$procedureName}(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);",
|
||||||
|
@ -31,7 +31,7 @@ class InstallController extends Controller
|
|||||||
/**
|
/**
|
||||||
* @var string[]
|
* @var string[]
|
||||||
*/
|
*/
|
||||||
protected $helpers = ['form', 'components', 'svg'];
|
protected $helpers = ['form', 'components', 'svg', 'misc'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
@ -72,7 +72,7 @@ class InstallController extends Controller
|
|||||||
// Check if the created .env file is writable to continue install process
|
// Check if the created .env file is writable to continue install process
|
||||||
if (is_really_writable(ROOTPATH . '.env')) {
|
if (is_really_writable(ROOTPATH . '.env')) {
|
||||||
try {
|
try {
|
||||||
$dotenv->required(['app.baseURL', 'admin.gateway', 'auth.gateway']);
|
$dotenv->required(['app.baseURL', 'analytics.salt', 'admin.gateway', 'auth.gateway']);
|
||||||
} catch (ValidationException) {
|
} catch (ValidationException) {
|
||||||
// form to input instance configuration
|
// form to input instance configuration
|
||||||
return $this->instanceConfig();
|
return $this->instanceConfig();
|
||||||
@ -99,6 +99,7 @@ class InstallController extends Controller
|
|||||||
try {
|
try {
|
||||||
$dotenv->required([
|
$dotenv->required([
|
||||||
'app.baseURL',
|
'app.baseURL',
|
||||||
|
'analytics.salt',
|
||||||
'admin.gateway',
|
'admin.gateway',
|
||||||
'auth.gateway',
|
'auth.gateway',
|
||||||
'database.default.hostname',
|
'database.default.hostname',
|
||||||
@ -169,6 +170,7 @@ class InstallController extends Controller
|
|||||||
'app.baseURL' => $baseUrl,
|
'app.baseURL' => $baseUrl,
|
||||||
'app.mediaBaseURL' =>
|
'app.mediaBaseURL' =>
|
||||||
$mediaBaseUrl === '' ? $baseUrl : $mediaBaseUrl,
|
$mediaBaseUrl === '' ? $baseUrl : $mediaBaseUrl,
|
||||||
|
'analytics.salt' => generate_random_salt(64),
|
||||||
'admin.gateway' => $this->request->getPost('admin_gateway'),
|
'admin.gateway' => $this->request->getPost('admin_gateway'),
|
||||||
'auth.gateway' => $this->request->getPost('auth_gateway'),
|
'auth.gateway' => $this->request->getPost('auth_gateway'),
|
||||||
]);
|
]);
|
||||||
|
Loading…
Reference in New Issue
Block a user