From e1f65cd3b53353a30d4ab6eb5312393cf04a1676 Mon Sep 17 00:00:00 2001 From: Benjamin Bellamy Date: Thu, 29 Oct 2020 17:27:16 +0100 Subject: [PATCH] feat(episodes): replace all audio file URL parameters with base64 encoded data --- app/Config/Routes.php | 13 +++--- app/Controllers/Analytics.php | 28 ++++++------ ...10000_add_analytics_podcasts_procedure.php | 5 ++- app/Entities/Episode.php | 44 ++++++++++++------- app/Helpers/analytics_helper.php | 22 +++++++++- 5 files changed, 71 insertions(+), 41 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index ce41ca17..f435cc76 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -31,6 +31,7 @@ $routes->setAutoRoute(false); $routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}'); $routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}'); +$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}'); /** * -------------------------------------------------------------------- @@ -59,14 +60,10 @@ $routes->group(config('App')->installGateway, function ($routes) { ]); }); -// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/duration/podcast_folder/filename.mp3) -$routes->add( - 'audio/(:num)/(:num)/(:num)/(:num)/(:num)/(:any)', - 'Analytics::hit/$1/$2/$3/$4/$5/$6', - [ - 'as' => 'analytics_hit', - ] -); +// Route for podcast audio file analytics (/audio/pack(podcast_id,episode_id,bytes_threshold,filesize,duration,date)/podcast_folder/filename.mp3) +$routes->add('audio/(:base64)/(:any)', 'Analytics::hit/$1/$2', [ + 'as' => 'analytics_hit', +]); // Show the Unknown UserAgents $routes->get('.well-known/unknown-useragents', 'UnknownUserAgents'); diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index 7db0bee0..ccaa92f6 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -46,24 +46,24 @@ class Analytics extends Controller } // Add one hit to this episode: - public function hit( - $podcastId, - $episodeId, - $bytesThreshold, - $fileSize, - $duration, - ...$filename - ) { - helper('media'); + public function hit($base64EpisodeData, ...$filename) + { + helper('media', 'analytics'); $serviceName = isset($_GET['_from']) ? $_GET['_from'] : ''; + $episodeData = unpack( + 'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate', + base64_url_decode($base64EpisodeData) + ); + podcast_hit( - $podcastId, - $episodeId, - $bytesThreshold, - $fileSize, - $duration, + $episodeData['podcastId'], + $episodeData['episodeId'], + $episodeData['bytesThreshold'], + $episodeData['fileSize'], + $episodeData['duration'], + $episodeData['publicationDate'], $serviceName ); return redirect()->to(media_base_url($filename)); diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php index 01c9ab3b..d77a5d19 100644 --- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php +++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_procedure.php @@ -35,6 +35,7 @@ CREATE PROCEDURE `{$prefix}analytics_podcasts` ( IN `p_bot` TINYINT(1) UNSIGNED, IN `p_filesize` INT UNSIGNED, IN `p_duration` INT UNSIGNED, + IN `p_age` INT UNSIGNED, IN `p_new_listener` TINYINT(1) UNSIGNED ) MODIFIES SQL DATA DETERMINISTIC @@ -57,8 +58,8 @@ IF NOT `p_bot` THEN INSERT INTO `{$prefix}analytics_podcasts_by_hour`(`podcast_id`, `date`, `hour`) VALUES (p_podcast_id, @current_date, @current_hour) ON DUPLICATE KEY UPDATE `hits`=`hits`+1; - INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) - SELECT p_podcast_id, p_episode_id, @current_date, datediff(@current_datetime,`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id + INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) + VALUES (p_podcast_id, p_episode_id, @current_date, p_age) ON DUPLICATE KEY UPDATE `hits`=`hits`+1; INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`) VALUES (p_podcast_id, p_country_code, @current_date) diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 79c45a7a..454849e1 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -179,25 +179,37 @@ class Episode extends Entity public function getEnclosureUrl() { + helper('analytics'); + return base_url( route_to( 'analytics_hit', - $this->attributes['podcast_id'], - $this->attributes['id'], - // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics - // - if file is shorter than 60sec, then it's enclosure_filesize - // - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds - $this->attributes['enclosure_duration'] <= 60 - ? $this->attributes['enclosure_filesize'] - : $this->attributes['enclosure_headersize'] + - floor( - (($this->attributes['enclosure_filesize'] - - $this->attributes['enclosure_headersize']) / - $this->attributes['enclosure_duration']) * - 60 - ), - $this->attributes['enclosure_filesize'], - $this->attributes['enclosure_duration'], + base64_url_encode( + pack( + 'I*', + $this->attributes['podcast_id'], + $this->attributes['id'], + // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics + // - if file is shorter than 60sec, then it's enclosure_filesize + // - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds + $this->attributes['enclosure_duration'] <= 60 + ? $this->attributes['enclosure_filesize'] + : $this->attributes['enclosure_headersize'] + + floor( + (($this->attributes['enclosure_filesize'] - + $this->attributes[ + 'enclosure_headersize' + ]) / + $this->attributes[ + 'enclosure_duration' + ]) * + 60 + ), + $this->attributes['enclosure_filesize'], + $this->attributes['enclosure_duration'], + strtotime($this->attributes['published_at']) + ) + ), $this->attributes['enclosure_uri'] ) ); diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index 998f94a3..7439c1e4 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -30,6 +30,22 @@ if (!function_exists('getallheaders')) { } } +/** + * Encode Base64 for URLs + */ +function base64_url_encode($input) +{ + return strtr(base64_encode($input), '+/=', '._-'); +} + +/** + * Decode Base64 from URL + */ +function base64_url_decode($input) +{ + return base64_decode(strtr($input, '._-', '+/=')); +} + /** * Set user country in session variable, for analytics purpose */ @@ -245,6 +261,7 @@ function podcast_hit( $bytesThreshold, $fileSize, $duration, + $publicationDate, $serviceName ) { $session = \Config\Services::session(); @@ -311,6 +328,8 @@ function podcast_hit( $db = \Config\Database::connect(); $procedureName = $db->prefixTable('analytics_podcasts'); + $age = intdiv(time() - $publicationDate, 86400); + // We create a sha1 hash for this IP_Address+User_Agent+Podcast_ID (used to count unique listeners): $listenerHashId = '_IpUaPo_' . @@ -337,7 +356,7 @@ function podcast_hit( cache()->save($listenerHashId, $downloadsByUser, $midnightTTL); $db->query( - "CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?);", + "CALL $procedureName(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);", [ $podcastId, $episodeId, @@ -352,6 +371,7 @@ function podcast_hit( $session->get('player')['bot'], $fileSize, $duration, + $age, $newListener, ] );