Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • adaures/castopod
  • mkljczk/castopod-host
  • spaetz/castopod-host
  • PatrykMis/castopod
  • jonas/castopod
  • ajeremias/castopod
  • misuzu/castopod
  • KrzysztofDomanczyk/castopod
  • Behel/castopod
  • nebulon/castopod
  • ewen/castopod
  • NeoluxConsulting/castopod
  • nateritter/castopod-og
  • prcutler/castopod
14 results
Show changes
Showing
with 797 additions and 516 deletions
......@@ -16,11 +16,10 @@ use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\LikeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Objects\OrderedCollectionObject;
......@@ -45,7 +44,7 @@ class EpisodeCommentController extends BaseController
}
if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -54,15 +53,15 @@ class EpisodeCommentController extends BaseController
$this->actor = $podcast->actor;
if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null
) {
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if (
($comment = (new EpisodeCommentModel())->getCommentById($params[2])) === null
! ($comment = (new EpisodeCommentModel())->getCommentById($params[2])) instanceof EpisodeComment
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -78,10 +77,7 @@ class EpisodeCommentController extends BaseController
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
......@@ -91,27 +87,28 @@ class EpisodeCommentController extends BaseController
"comment#{$this->comment->id}",
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_comment_metatags($this->comment);
$data = [
'metatags' => get_episode_comment_metatags($this->comment),
'podcast' => $this->podcast,
'actor' => $this->actor,
'actor' => $this->actor,
'episode' => $this->episode,
'comment' => $this->comment,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/comment', $data);
}
return view('episode/comment', $data, [
'cache' => DECADE,
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -119,10 +116,7 @@ class EpisodeCommentController extends BaseController
return $cachedView;
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function commentObject(): Response
public function commentObject(): ResponseInterface
{
$commentObject = new CommentObject($this->comment);
......@@ -131,10 +125,7 @@ class EpisodeCommentController extends BaseController
->setBody($commentObject->toJSON());
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function replies(): Response
public function replies(): ResponseInterface
{
/**
* get comment replies
......@@ -169,18 +160,26 @@ class EpisodeCommentController extends BaseController
->setBody($collection->toJSON());
}
public function attemptLike(): RedirectResponse
public function likeAction(): RedirectResponse
{
model(LikeModel::class)
->toggleLike(interact_as_actor(), $this->comment);
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
public function attemptReply(): RedirectResponse
public function replyAction(): RedirectResponse
{
model(LikeModel::class)
->toggleLike(interact_as_actor(), $this->comment);
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
......
......@@ -16,15 +16,14 @@ use App\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\Embed;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
use Modules\Media\FileManagers\FileManagerInterface;
use SimpleXMLElement;
class EpisodeController extends BaseController
......@@ -42,7 +41,7 @@ class EpisodeController extends BaseController
}
if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -50,8 +49,8 @@ class EpisodeController extends BaseController
$this->podcast = $podcast;
if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null
) {
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -65,20 +64,25 @@ class EpisodeController extends BaseController
public function index(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$locale = service('request')
->getLocale();
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
(can_user_interact() ? '_authenticated' : '');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];
......@@ -87,7 +91,7 @@ class EpisodeController extends BaseController
$this->podcast->id,
);
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/comments', $data);
......@@ -95,9 +99,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -107,20 +109,26 @@ class EpisodeController extends BaseController
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$locale = service('request')
->getLocale();
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_activity_{$locale}" .
(can_user_interact() ? '_authenticated' : '');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'activity',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'metatags' => get_episode_metatags($this->episode),
'podcast' => $this->podcast,
'episode' => $this->episode,
];
......@@ -129,7 +137,7 @@ class EpisodeController extends BaseController
$this->podcast->id,
);
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('episode/activity', $data);
......@@ -137,9 +145,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -147,33 +153,156 @@ class EpisodeController extends BaseController
return $cachedView;
}
public function embed(string $theme = 'light-transparent'): string
public function chapters(): string
{
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'chapters',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get chapters from json file
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/chapters', $data);
}
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
$session = Services::session();
$session->start();
if (isset($_SERVER['HTTP_REFERER'])) {
$session->set('embed_domain', parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST));
return $cachedView;
}
public function transcript(): string
{
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'transcript',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get transcript from json file
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key,
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/transcript', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
$locale = service('request')
->getLocale();
return $cachedView;
}
public function embed(string $theme = 'light-transparent'): string
{
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embed_{$theme}_{$locale}";
$session = service('session');
if (service('superglobals')->server('HTTP_REFERER') !== null) {
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'embed',
$theme,
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$themeData = EpisodeModel::$themes[$theme];
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
'themeData' => $themeData,
];
......@@ -183,9 +312,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -196,22 +323,21 @@ class EpisodeController extends BaseController
public function oembedJSON(): ResponseInterface
{
return $this->response->setJSON([
'type' => 'rich',
'version' => '1.0',
'title' => $this->episode->title,
'type' => 'rich',
'version' => '1.0',
'title' => $this->episode->title,
'provider_name' => $this->podcast->title,
'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link,
'html' =>
'<iframe src="' .
'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link,
'html' => '<iframe src="' .
$this->episode->embed_url .
'" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
'width' => config('Embed')
->width,
'height' => config('Embed')
->height,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_width' => config('Images')
->podcastCoverSizes['og']['width'],
'thumbnail_height' => config('Images')
......@@ -238,7 +364,9 @@ class EpisodeController extends BaseController
htmlspecialchars(
'<iframe src="' .
$this->episode->embed_url .
'" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
'" width="100%" height="' . config(
Embed::class,
)->height . '" frameborder="0" scrolling="no"></iframe>',
),
);
$oembed->addChild('width', (string) config('Embed')->width);
......@@ -248,10 +376,7 @@ class EpisodeController extends BaseController
return $this->response->setXML($oembed);
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeObject(): Response
public function episodeObject(): ResponseInterface
{
$podcastObject = new PodcastEpisode($this->episode);
......@@ -260,21 +385,16 @@ class EpisodeController extends BaseController
->setBody($podcastObject->toJSON());
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function comments(): Response
public function comments(): ResponseInterface
{
/**
* get comments: aggregated replies from posts referring to the episode
*/
$episodeComments = model(PostModel::class)
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
return $builder->select('id')
->from(config('Fediverse')->tablesPrefix . 'posts')
->where('episode_id', $this->episode->id);
})
->where('`published_at` <= NOW()', null, false)
$episodeComments = model('PostModel')
->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
->from('fediverse_posts')
->where('episode_id', $this->episode->id))
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
......
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use Modules\Media\FileManagers\FileManagerInterface;
class EpisodePreviewController extends BaseController
{
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 1) {
throw PageNotFoundException::forPageNotFound();
}
// find episode by previewUUID
$episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
if (! $episode instanceof Episode) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if ($episode->publication_status === 'published') {
// redirect to episode page
return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
}
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): string
{
helper('form');
return view('episode/preview-comments', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function activity(): string
{
helper('form');
return view('episode/preview-activity', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function chapters(): string
{
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
helper('form');
return view('episode/preview-chapters', $data);
}
public function transcript(): string
{
// get transcript from json file
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key,
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
helper('form');
return view('episode/preview-transcript', $data);
}
}
......@@ -3,36 +3,58 @@
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface;
use Exception;
use Opawg\UserAgentsPhp\UserAgentsRSS;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Opawg\UserAgentsV2Php\UserAgentsRSS;
class FeedController extends Controller
{
/**
* Instance of the main Request object.
*
* @var IncomingRequest
*/
protected $request;
public function index(string $podcastHandle): ResponseInterface
{
helper('rss');
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
->first();
if (! $podcast) {
if (! $podcast instanceof Podcast) {
throw PageNotFoundException::forPageNotFound();
}
// 301 redirect to new feed?
$redirectToNewFeed = service('settings')
->get('Podcast.redirect_to_new_feed', 'podcast:' . $podcast->id);
if ($redirectToNewFeed && $podcast->new_feed_url !== null && filter_var(
$podcast->new_feed_url,
FILTER_VALIDATE_URL,
) && $podcast->new_feed_url !== current_url()) {
return redirect()->to($podcast->new_feed_url, 301);
}
helper(['rss', 'premium_podcasts', 'misc']);
$service = null;
try {
$service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']);
$service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT'));
} catch (Exception $exception) {
// If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $exception->getMessage());
......@@ -43,11 +65,24 @@ class FeedController extends Controller
$serviceSlug = $service['slug'];
}
$cacheName =
"podcast#{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
$subscription = null;
$token = $this->request->getGet('token');
if ($token) {
$subscription = (new SubscriptionModel())->validateSubscription($podcastHandle, $token);
}
$cacheName = implode(
'_',
array_filter([
"podcast#{$podcast->id}",
'feed',
$service ? $serviceSlug : null,
$subscription instanceof Subscription ? "subscription#{$subscription->id}" : null,
]),
);
if (! ($found = cache($cacheName))) {
$found = get_rss_feed($podcast, $serviceSlug);
$found = get_rss_feed($podcast, $serviceSlug, $subscription, $token);
// The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
......@@ -55,13 +90,7 @@ class FeedController extends Controller
);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $this->response->setXML($found);
......
......@@ -11,25 +11,18 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Models\PodcastModel;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Media\FileManagers\FileManagerInterface;
class HomeController extends BaseController
{
public function index(): RedirectResponse | string
{
$db = db_connect();
if ($db->getDatabase() === '' || ! $db->tableExists('podcasts')) {
// Database connection has not been set or could not find the podcasts table
// Redirecting to install page because it is likely that Castopod has not been installed yet.
// NB: as base_url wouldn't have been defined here, redirect to install wizard manually
$route = Services::routes()->reverseRoute('install');
return redirect()->to(rtrim(host_url(), '/') . $route);
}
$sortOptions = ['activity', 'created_desc', 'created_asc'];
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
'sort'
'sort',
) : 'activity';
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
......@@ -39,13 +32,52 @@ class HomeController extends BaseController
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
}
set_home_metatags();
// default behavior: list all podcasts on home page
$data = [
'metatags' => get_home_metatags(),
'podcasts' => $allPodcasts,
'sortBy' => $sortBy,
'sortBy' => $sortBy,
];
return view('home', $data);
}
public function health(): ResponseInterface
{
$errors = [];
try {
db_connect();
} catch (DatabaseException) {
$errors[] = 'Unable to connect to the database.';
}
// --- Can Castopod connect to the cache handler
if (config('Cache')->handler !== 'dummy' && cache()->getCacheInfo() === null) {
$errors[] = 'Unable connect to the cache handler.';
}
// --- Can Castopod write to storage?
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager', false);
if (! $fileManager->isHealthy()) {
$errors[] = 'Problem with file manager.';
}
if ($errors !== []) {
return $this->response->setStatusCode(503)
->setJSON([
'code' => 503,
'errors' => $errors,
]);
}
return $this->response->setStatusCode(200)
->setJSON([
'code' => 200,
'message' => '✨ All good!',
]);
}
}
......@@ -24,13 +24,14 @@ class MapController extends BaseController
'map',
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
$found = view('pages/map', [], [
'cache' => DECADE,
return view('pages/map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -43,19 +44,19 @@ class MapController extends BaseController
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = (new EpisodeModel())
->where('`published_at` <= NOW()', null, false)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->where('location_geo is not', null)
->findAll();
$found = [];
foreach ($episodes as $episode) {
$found[] = [
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'location_name' => esc($episode->location->name),
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'cover_url' => $episode->cover->thumbnail_url,
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'cover_url' => $episode->cover->thumbnail_url,
'podcast_title' => esc($episode->podcast->title),
'episode_title' => esc($episode->title),
];
......
......@@ -24,9 +24,8 @@ class PageController extends BaseController
throw PageNotFoundException::forPageNotFound();
}
if (
($page = (new PageModel())->where('slug', $params[0])->first()) === null
) {
$page = (new PageModel())->where('slug', $params[0])->first();
if (! $page instanceof Page) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -44,13 +43,14 @@ class PageController extends BaseController
$this->page->slug,
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
set_page_metatags($this->page);
$data = [
'metatags' => get_page_metatags($this->page),
'page' => $this->page,
];
......
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
/*
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{
public function index(): ResponseInterface
{
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}
......@@ -17,7 +17,7 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
......@@ -35,7 +35,7 @@ class PodcastController extends BaseController
}
if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -47,10 +47,7 @@ class PodcastController extends BaseController
return $this->{$method}(...$params);
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function podcastActor(): Response
public function podcastActor(): ResponseInterface
{
$podcastActor = new PodcastActor($this->podcast);
......@@ -61,10 +58,7 @@ class PodcastController extends BaseController
public function activity(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
......@@ -74,19 +68,21 @@ class PodcastController extends BaseController
'activity',
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_podcast_metatags($this->podcast, 'activity');
$data = [
'metatags' => get_podcast_metatags($this->podcast, 'activity'),
'podcast' => $this->podcast,
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/activity', $data);
......@@ -97,9 +93,7 @@ class PodcastController extends BaseController
);
return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -109,10 +103,7 @@ class PodcastController extends BaseController
public function about(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
......@@ -122,21 +113,23 @@ class PodcastController extends BaseController
'about',
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$stats = (new EpisodeModel())->getPodcastStats($this->podcast->id);
set_podcast_metatags($this->podcast, 'about');
$data = [
'metatags' => get_podcast_metatags($this->podcast, 'about'),
'podcast' => $this->podcast,
'stats' => $stats,
'stats' => $stats,
];
// // if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('podcast/about', $data);
......@@ -147,9 +140,7 @@ class PodcastController extends BaseController
);
return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -159,10 +150,7 @@ class PodcastController extends BaseController
public function episodes(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season');
......@@ -188,7 +176,9 @@ class PodcastController extends BaseController
$seasonQuery ? 'season' . $seasonQuery : null,
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
......@@ -204,18 +194,17 @@ class PodcastController extends BaseController
$isActive = $yearQuery === $year['year'];
if ($isActive) {
$activeQuery = [
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
];
}
$episodesNavigation[] = [
'label' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->handle) .
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?year=' .
$year['year'],
'is_active' => $isActive,
......@@ -226,7 +215,7 @@ class PodcastController extends BaseController
$isActive = $seasonQuery === $season['season_number'];
if ($isActive) {
$activeQuery = [
'type' => 'season',
'type' => 'season',
'value' => $season['season_number'],
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
......@@ -240,20 +229,19 @@ class PodcastController extends BaseController
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->handle) .
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?season=' .
$season['season_number'],
'is_active' => $isActive,
];
}
set_podcast_metatags($this->podcast, 'episodes');
$data = [
'metatags' => get_podcast_metatags($this->podcast, 'episodes'),
'podcast' => $this->podcast,
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id,
$this->podcast->type,
$yearQuery,
......@@ -261,7 +249,7 @@ class PodcastController extends BaseController
),
];
if (can_user_interact()) {
if (auth()->loggedIn()) {
return view('podcast/episodes', $data);
}
......@@ -269,9 +257,7 @@ class PodcastController extends BaseController
$this->podcast->id,
);
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -279,19 +265,16 @@ class PodcastController extends BaseController
return $cachedView;
}
/**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeCollection(): Response
public function episodeCollection(): ResponseInterface
{
if ($this->podcast->type === 'serial') {
// podcast is serial
$episodes = model(EpisodeModel::class)
->where('`published_at` <= NOW()', null, false)
$episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('season_number DESC, number ASC');
} else {
$episodes = model(EpisodeModel::class)
->where('`published_at` <= NOW()', null, false)
$episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'DESC');
}
......@@ -320,4 +303,12 @@ class PodcastController extends BaseController
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function links(): string
{
set_podcast_metatags($this->podcast, 'links');
return view('podcast/links', [
'podcast' => $this->podcast,
]);
}
}
......@@ -22,7 +22,7 @@ use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Modules\Fediverse\Models\FavouriteModel;
use Override;
class PostController extends FediversePostController
{
......@@ -38,14 +38,16 @@ class PostController extends FediversePostController
protected $post;
/**
* @var string[]
* @var list<string>
*/
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo'];
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
#[Override]
public function _remap(string $method, string ...$params): mixed
{
if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0],)) === null
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
......@@ -53,26 +55,29 @@ class PostController extends FediversePostController
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (count($params) <= 1) {
unset($params[0]);
return $this->{$method}(...$params);
}
if (
count($params) > 1 &&
($post = (new PostModel())->getPostById($params[1])) !== null
! ($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
) {
/** @var CastopodPost $post */
$this->post = $post;
unset($params[0]);
unset($params[1]);
throw PageNotFoundException::forPageNotFound();
}
$this->post = $post;
unset($params[0]);
unset($params[1]);
return $this->{$method}(...$params);
}
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
......@@ -81,25 +86,26 @@ class PostController extends FediversePostController
"post#{$this->post->id}",
service('request')
->getLocale(),
can_user_interact() ? 'authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_post_metatags($this->post);
$data = [
'metatags' => get_post_metatags($this->post),
'post' => $this->post,
'post' => $this->post,
'podcast' => $this->podcast,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
if (auth()->loggedIn()) {
helper('form');
return view('post/post', $data);
}
return view('post/post', $data, [
'cache' => DECADE,
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
......@@ -107,10 +113,11 @@ class PostController extends FediversePostController
return $cachedView;
}
public function attemptCreate(): RedirectResponse
#[Override]
public function createAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url_strict|permit_empty',
];
......@@ -121,16 +128,18 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors());
}
$message = $this->request->getPost('message');
$validData = $this->validator->getValidated();
$message = $validData['message'];
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $this->request->getPost('episode_url');
$episodeUri = $validData['episode_url'];
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
......@@ -156,7 +165,8 @@ class PostController extends FediversePostController
return redirect()->back();
}
public function attemptReply(): RedirectResponse
#[Override]
public function replyAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
......@@ -169,15 +179,17 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $this->request->getPost('message'),
'published_at' => Time::now(),
'created_by' => user_id(),
'message' => $validData['message'],
'published_at' => Time::now(),
'created_by' => user_id(),
]);
if ($this->post->in_reply_to_id === null && $this->post->episode_id !== null) {
if ($this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
......@@ -193,21 +205,23 @@ class PostController extends FediversePostController
return redirect()->back();
}
public function attemptFavourite(): RedirectResponse
#[Override]
public function favouriteAction(): RedirectResponse
{
model(FavouriteModel::class)->toggleFavourite(interact_as_actor(), $this->post);
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back();
}
public function attemptReblog(): RedirectResponse
#[Override]
public function reblogAction(): RedirectResponse
{
(new PostModel())->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back();
}
public function attemptAction(): RedirectResponse
public function action(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
......@@ -220,47 +234,35 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors());
}
$action = $this->request->getPost('action');
$validData = $this->validator->getValidated();
$action = $validData['action'];
return match ($action) {
'favourite' => $this->attemptFavourite(),
'reblog' => $this->attemptReblog(),
'reply' => $this->attemptReply(),
default => redirect()
'favourite' => $this->favouriteAction(),
'reblog' => $this->reblogAction(),
'reply' => $this->replyAction(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteAction(string $action): string
public function remoteActionView(string $action): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter(['page', "post#{$this->post->id}", "remote_{$action}", service('request') ->getLocale()]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'metatags' => get_remote_actions_metatags($this->post, $action),
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
helper('form');
$this->registerPodcastWebpageHit($this->podcast->id);
set_remote_actions_metatags($this->post, $action);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
return view('post/remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
helper('form');
return (string) $cachedView;
// NO VIEW CACHING: form has a CSRF token which should change on each request
return view('post/remote_action', $data);
}
}
......@@ -10,6 +10,7 @@ declare(strict_types=1);
namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
......@@ -20,56 +21,56 @@ class WebmanifestController extends Controller
/**
* @var array<string, array<string, string>>
*/
public const THEME_COLORS = [
final public const array THEME_COLORS = [
'pine' => [
'theme' => '#009486',
'theme' => '#009486',
'background' => '#F0F9F8',
],
'lake' => [
'theme' => '#00ACE0',
'theme' => '#00ACE0',
'background' => '#F0F7F9',
],
'jacaranda' => [
'theme' => '#562CDD',
'theme' => '#562CDD',
'background' => '#F2F0F9',
],
'crimson' => [
'theme' => '#F24562',
'theme' => '#F24562',
'background' => '#F9F0F2',
],
'amber' => [
'theme' => '#FF6224',
'theme' => '#FF6224',
'background' => '#F9F3F0',
],
'onyx' => [
'theme' => '#040406',
'theme' => '#040406',
'background' => '#F3F3F7',
],
];
public function index(): ResponseInterface
{
helper('misc');
$webmanifest = [
'name' => esc(service('settings') ->get('App.siteName')),
'name' => esc(service('settings') ->get('App.siteName')),
'description' => esc(service('settings') ->get('App.siteDescription')),
'lang' => service('request')
'lang' => service('request')
->getLocale(),
'start_url' => base_url(),
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'start_url' => base_url(),
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
'icons' => [
[
'src' => service('settings')
->get('App.siteIcon')['192'],
'type' => 'image/png',
'src' => get_site_icon_url('192'),
'type' => 'image/png',
'sizes' => '192x192',
],
[
'src' => service('settings')
->get('App.siteIcon')['512'],
'type' => 'image/png',
'src' => get_site_icon_url('512'),
'type' => 'image/png',
'sizes' => '512x512',
],
],
......@@ -81,31 +82,31 @@ class WebmanifestController extends Controller
public function podcastManifest(string $podcastHandle): ResponseInterface
{
if (
($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) === null
! ($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$webmanifest = [
'name' => esc($podcast->title),
'short_name' => '@' . esc($podcast->handle),
'description' => $podcast->description,
'lang' => $podcast->language_code,
'start_url' => $podcast->link,
'scope' => '/@' . esc($podcast->handle),
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'name' => esc($podcast->title),
'short_name' => $podcast->at_handle,
'description' => $podcast->description,
'lang' => $podcast->language_code,
'start_url' => $podcast->link,
'scope' => '/' . $podcast->at_handle,
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
'icons' => [
[
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'sizes' => '192x192',
],
[
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'sizes' => '512x512',
],
],
......
......@@ -12,32 +12,33 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddCategories extends Migration
class AddCategories extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'code' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
......@@ -47,6 +48,7 @@ class AddCategories extends Migration
$this->forge->createTable('categories');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('categories');
......
......@@ -12,20 +12,21 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddLanguages extends Migration
class AddLanguages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
......@@ -33,6 +34,7 @@ class AddLanguages extends Migration
$this->forge->createTable('languages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('languages');
......
......@@ -12,32 +12,33 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddPodcasts extends Migration
class AddPodcasts extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'guid' => [
'type' => 'CHAR',
'type' => 'CHAR',
'constraint' => 36,
],
'actor_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'handle' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
......@@ -47,50 +48,50 @@ class AddPodcasts extends Migration
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'banner_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'language_code' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'default' => 0,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'null' => true,
],
'owner_name' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
......@@ -101,91 +102,94 @@ class AddPodcasts extends Migration
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
'comment' => 'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
'comment' => 'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'null' => true,
],
'is_premium_by_default' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'updated_at' => [
'created_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'updated_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
......@@ -194,7 +198,7 @@ class AddPodcasts extends Migration
$this->forge->addUniqueKey('handle');
$this->forge->addUniqueKey('guid');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey('actor_id', config('Fediverse')->tablesPrefix . 'actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('cover_id', 'media', 'id');
$this->forge->addForeignKey('banner_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('category_id', 'categories', 'id');
......@@ -204,6 +208,7 @@ class AddPodcasts extends Migration
$this->forge->createTable('podcasts');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts');
......
......@@ -12,36 +12,37 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddEpisodes extends Migration
class AddEpisodes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
],
'audio_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'description_markdown' => [
......@@ -51,90 +52,95 @@ class AddEpisodes extends Migration
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'transcript_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'transcript_remote_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'null' => true,
],
'chapters_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'chapters_remote_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'null' => true,
],
'number' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'posts_count' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'default' => 0,
'default' => 0,
],
'comments_count' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'default' => 0,
'default' => 0,
],
'is_premium' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
......@@ -147,10 +153,6 @@ class AddEpisodes extends Migration
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
......@@ -162,8 +164,17 @@ class AddEpisodes extends Migration
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
// Add Full-Text Search index on title and description_markdown
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
ADD FULLTEXT title (title, description_markdown);
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episodes');
......
......@@ -12,41 +12,45 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddPlatforms extends Migration
class AddPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'null' => true,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField('`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()');
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()',
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('platforms');
......
......@@ -12,39 +12,40 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddPodcastsPlatforms extends Migration
class AddPodcastsPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
'default' => 0,
],
]);
......@@ -54,6 +55,7 @@ class AddPodcastsPlatforms extends Migration
$this->forge->createTable('podcasts_platforms');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_platforms');
......
......@@ -12,70 +12,69 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddEpisodeComments extends Migration
class AddEpisodeComments extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'BINARY',
'type' => 'BINARY',
'constraint' => 16,
],
'uri' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 255,
],
'episode_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'actor_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'in_reply_to_id' => [
'type' => 'BINARY',
'type' => 'BINARY',
'constraint' => 16,
'null' => true,
'null' => true,
],
'message' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 5000,
],
'message_html' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 6000,
],
'likes_count' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'created_by' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
'null' => true,
'null' => true,
],
]);
$fediverseTablesPrefix = config('Fediverse')
->tablesPrefix;
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', $fediverseTablesPrefix . 'actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->createTable('episode_comments');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episode_comments');
......
......@@ -12,33 +12,32 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddLikes extends Migration
class AddLikes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'actor_id' => [
'type' => 'INT',
'type' => 'INT',
'unsigned' => true,
],
'comment_id' => [
'type' => 'BINARY',
'type' => 'BINARY',
'constraint' => 16,
],
]);
$fediverseTablesPrefix = config('Fediverse')
->tablesPrefix;
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()');
$this->forge->addPrimaryKey(['actor_id', 'comment_id']);
$this->forge->addForeignKey('actor_id', $fediverseTablesPrefix . 'actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('comment_id', 'episode_comments', 'id', '', 'CASCADE');
$this->forge->createTable('likes');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('likes');
......
......@@ -12,26 +12,27 @@ declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class AddPages extends Migration
class AddPages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'type' => 'VARCHAR',
'constraint' => 128,
'unique' => true,
'unique' => true,
],
'content_markdown' => [
'type' => 'TEXT',
......@@ -45,15 +46,12 @@ class AddPages extends Migration
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('pages');
......