From da0f0472819007e02e5da37399f2377772c618b9 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Thu, 2 Jul 2020 10:08:32 +0000
Subject: [PATCH] feat(cache): add podcast and episode pages to cache + clear
 them after insert or update

- throw not found page error if no podcast in podcast controller
- delete unnecessary unknownuseragents view
---
 app/Config/Routes.php                             |  4 ++--
 app/Controllers/Episode.php                       |  7 +++++--
 app/Controllers/Podcast.php                       | 14 +++++++++++---
 app/Controllers/UnknownUserAgents.php             | 11 ++---------
 .../Migrations/2020-06-05-170000_add_episodes.php | 12 +-----------
 app/Models/EpisodeModel.php                       | 15 ++++++---------
 app/Models/PodcastModel.php                       | 12 +++++++++---
 app/Models/UnknownUserAgentsModel.php             |  4 ++--
 app/Views/json/unknownuseragents.php              |  5 -----
 9 files changed, 38 insertions(+), 46 deletions(-)
 delete mode 100644 app/Views/json/unknownuseragents.php

diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 4bb4f1cb4b..5db584e1c8 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -22,8 +22,8 @@ $routes->setDefaultMethod('index');
 $routes->setTranslateURIDashes(false);
 $routes->set404Override();
 $routes->setAutoRoute(false);
-$routes->addPlaceholder('podcastName', '[a-z0-9\_]{1,191}');
-$routes->addPlaceholder('episodeSlug', '[a-z0-9\-]{1,191}');
+$routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,191}');
+$routes->addPlaceholder('episodeSlug', '[a-zA-Z0-9\-]{1,191}');
 
 /**
  * --------------------------------------------------------------------
diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php
index 8be56caf8f..fc14e8957c 100644
--- a/app/Controllers/Episode.php
+++ b/app/Controllers/Episode.php
@@ -49,7 +49,7 @@ class Episode extends BaseController
                 'image' =>
                     'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
                 'title' => 'required',
-                'slug' => 'required',
+                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
                 'description' => 'required',
                 'type' => 'required',
             ])
@@ -105,7 +105,7 @@ class Episode extends BaseController
                 'image' =>
                     'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
                 'title' => 'required',
-                'slug' => 'required',
+                'slug' => 'required|regex_match[[a-zA-Z0-9\-]{1,191}]',
                 'description' => 'required',
                 'type' => 'required',
             ])
@@ -164,6 +164,9 @@ class Episode extends BaseController
 
     public function view()
     {
+        // The page cache is set to a decade so it is deleted manually upon podcast update
+        $this->cachePage(DECADE);
+
         self::triggerWebpageHit($this->podcast->id);
 
         $data = [
diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php
index 8e51850885..081f7b9faf 100644
--- a/app/Controllers/Podcast.php
+++ b/app/Controllers/Podcast.php
@@ -18,7 +18,12 @@ class Podcast extends BaseController
     {
         if (count($params) > 0) {
             $podcast_model = new PodcastModel();
-            $this->podcast = $podcast_model->where('name', $params[0])->first();
+            if (
+                !($podcast = $podcast_model->where('name', $params[0])->first())
+            ) {
+                throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
+            }
+            $this->podcast = $podcast;
         }
 
         return $this->$method();
@@ -32,7 +37,7 @@ class Podcast extends BaseController
         if (
             !$this->validate([
                 'title' => 'required',
-                'name' => 'required|regex_match[^[a-z0-9\_]{1,191}$]',
+                'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
                 'description' => 'required|max_length[4000]',
                 'image' =>
                     'uploaded[image]|is_image[image]|ext_in[image,jpg,png]',
@@ -91,7 +96,7 @@ class Podcast extends BaseController
         if (
             !$this->validate([
                 'title' => 'required',
-                'name' => 'required|regex_match[^[a-z0-9\_]{1,191}$]',
+                'name' => 'required|regex_match[[a-zA-Z0-9\_]{1,191}]',
                 'description' => 'required|max_length[4000]',
                 'image' =>
                     'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty',
@@ -150,6 +155,9 @@ class Podcast extends BaseController
 
     public function view()
     {
+        // The page cache is set to a decade so it is deleted manually upon podcast update
+        $this->cachePage(DECADE);
+
         self::triggerWebpageHit($this->podcast->id);
 
         $data = [
diff --git a/app/Controllers/UnknownUserAgents.php b/app/Controllers/UnknownUserAgents.php
index 220daf8650..82e1a2985b 100644
--- a/app/Controllers/UnknownUserAgents.php
+++ b/app/Controllers/UnknownUserAgents.php
@@ -3,17 +3,10 @@ use CodeIgniter\Controller;
 
 class UnknownUserAgents extends Controller
 {
-    public function index($p_id = 0)
+    public function index($last_known_id = 0)
     {
         $model = new \App\Models\UnknownUserAgentsModel();
 
-        $data = [
-            'useragents' => $model->getUserAgents($p_id),
-        ];
-
-        $this->response->setContentType('application/json');
-        $this->response->setStatusCode(\CodeIgniter\HTTP\Response::HTTP_OK);
-
-        echo view('json/unknownuseragents', $data);
+        return $this->response->setJSON($model->getUserAgents($last_known_id));
     }
 }
diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
index 11cf98d780..4a4fc52894 100644
--- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php
+++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php
@@ -58,13 +58,6 @@ class AddEpisodes extends Migration
                 'comment' =>
                     'An episode description. Description is text containing one or more sentences describing your episode to potential listeners. You can specify up to 4000 characters. You can use rich text formatting and some HTML (<p>, <ol>, <ul>, <li>, <a>) if wrapped in the <CDATA> tag. To include links in your description or rich HTML, adhere to the following technical guidelines: enclose all portions of your XML that contain embedded HTML in a CDATA section to prevent formatting issues, and to ensure proper link functionality.',
             ],
-            'duration' => [
-                'type' => 'INT',
-                'constraint' => 10,
-                'unsigned' => true,
-                'comment' =>
-                    'The duration of an episode. Different duration formats are accepted however it is recommended to convert the length of the episode into seconds.',
-            ],
             'image_uri' => [
                 'type' => 'VARCHAR',
                 'constraint' => 1024,
@@ -90,7 +83,7 @@ class AddEpisodes extends Migration
                 'type' => 'INT',
                 'constraint' => 10,
                 'unsigned' => true,
-                'null' => true,
+                'default' => 1,
                 'comment' =>
                     'The episode season number. If an episode is within a season use this tag. Where season is a non-zero integer (1, 2, 3, etc.) representing your season number. To allow the season feature for shows containing a single season, if only one season exists in the RSS feed, Apple Podcasts doesn’t display a season number. When you add a second season to the RSS feed, Apple Podcasts displays the season numbers.',
             ],
@@ -136,9 +129,6 @@ class AddEpisodes extends Migration
         $this->forge->addKey('id', true);
         $this->forge->addUniqueKey(['podcast_id', 'slug']);
 
-        // FIXME: as season_number can be null, the unique key constraint is useless when it is
-        // the majority of RDBMS behave that way
-        // possible solution: remove the null constraint on the season_number and set a default
         $this->forge->addUniqueKey(['podcast_id', 'season_number', 'number']);
         $this->forge->addForeignKey('podcast_id', 'podcasts', 'id');
         $this->forge->createTable('episodes');
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 84f2388af3..bd29dbf608 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -39,14 +39,8 @@ class EpisodeModel extends Model
     protected $useSoftDeletes = true;
     protected $useTimestamps = true;
 
-    protected $afterInsert = [
-        'writeEnclosureMetadata',
-        'clearPodcastFeedCache',
-    ];
-    protected $afterUpdate = [
-        'writeEnclosureMetadata',
-        'clearPodcastFeedCache',
-    ];
+    protected $afterInsert = ['writeEnclosureMetadata', 'clearCache'];
+    protected $afterUpdate = ['writeEnclosureMetadata', 'clearCache'];
 
     protected function writeEnclosureMetadata(array $data)
     {
@@ -61,7 +55,7 @@ class EpisodeModel extends Model
         return $data;
     }
 
-    protected function clearPodcastFeedCache(array $data)
+    protected function clearCache(array $data)
     {
         $episode = $this->find(
             is_array($data['id']) ? $data['id'][0] : $data['id']
@@ -69,6 +63,9 @@ class EpisodeModel extends Model
 
         $cache = \Config\Services::cache();
 
+        // delete cache for rss feed, podcast and episode pages
         $cache->delete(md5($episode->podcast->feed_url));
+        $cache->delete(md5($episode->podcast->link));
+        $cache->delete(md5($episode->link));
     }
 }
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 36d3770410..fba25f5ce7 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -40,10 +40,10 @@ class PodcastModel extends Model
 
     protected $useTimestamps = true;
 
-    protected $afterInsert = ['clearPodcastFeedCache'];
-    protected $afterUpdate = ['clearPodcastFeedCache'];
+    protected $afterInsert = ['clearCache'];
+    protected $afterUpdate = ['clearCache'];
 
-    protected function clearPodcastFeedCache(array $data)
+    protected function clearCache(array $data)
     {
         $podcast = $this->find(
             is_array($data['id']) ? $data['id'][0] : $data['id']
@@ -51,6 +51,12 @@ class PodcastModel extends Model
 
         $cache = \Config\Services::cache();
 
+        // delete cache for rss feed and podcast pages
         $cache->delete(md5($podcast->feed_url));
+        $cache->delete(md5($podcast->link));
+        // TODO: clear cache for every podcast's episode page?
+        // foreach ($podcast->episodes as $episode) {
+        //     $cache->delete(md5($episode->link));
+        // }
     }
 }
diff --git a/app/Models/UnknownUserAgentsModel.php b/app/Models/UnknownUserAgentsModel.php
index ba6e69ef10..5cd0b71b16 100644
--- a/app/Models/UnknownUserAgentsModel.php
+++ b/app/Models/UnknownUserAgentsModel.php
@@ -16,8 +16,8 @@ class UnknownUserAgentsModel extends Model
 
     protected $allowedFields = [];
 
-    public function getUserAgents($p_id = 0)
+    public function getUserAgents($last_known_id = 0)
     {
-        return $this->where('id>', $p_id)->findAll();
+        return $this->where('id>', $last_known_id)->findAll();
     }
 }
diff --git a/app/Views/json/unknownuseragents.php b/app/Views/json/unknownuseragents.php
deleted file mode 100644
index 9014c0df08..0000000000
--- a/app/Views/json/unknownuseragents.php
+++ /dev/null
@@ -1,5 +0,0 @@
-<?php
-if (!empty($useragents) && is_array($useragents)) {
-    echo json_encode($useragents);
-}
-?>
-- 
GitLab