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 1216 additions and 378 deletions
<?php
// app/Config/Vite.php
declare(strict_types=1);
namespace Config;
use CodeIgniterVite\Config\Vite as ViteConfig;
class Vite extends ViteConfig
{
public function __construct()
{
parent::__construct();
$adminGateway = config('Admin')
->gateway;
$installGateway = config('Install')
->gateway;
$this->routesAssets = [
[
'routes' => ['*'],
'exclude' => [$adminGateway . '*', $installGateway . '*'],
'assets' => ['styles/site.css', 'js/app.ts', 'js/podcast.ts', 'js/audio-player.ts'],
],
[
'routes' => ['/map'],
'assets' => ['js/map.ts'],
],
[
'routes' => ['/' . $adminGateway . '*'],
'assets' => ['styles/admin.css', 'js/admin.ts', 'js/admin-audio-player.ts'],
],
[
'routes' => [$installGateway . '*'],
'assets' => ['styles/install.css'],
],
];
}
}
...@@ -3,28 +3,15 @@ ...@@ -3,28 +3,15 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Podlibre * @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use CodeIgniter\Controller; use Modules\Fediverse\Controllers\ActivityPubController as FediverseActivityPubController;
use CodeIgniter\HTTP\Response;
class ActivityPubController extends Controller class ActivityPubController extends FediverseActivityPubController
{ {
/**
* @noRector ReturnTypeDeclarationRector
*/
public function preflight(): Response
{
return $this->response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
->setHeader('Access-Control-Max-Age', '86400')
->setHeader('Cache-Control', 'public, max-age=86400')
->setStatusCode(200);
}
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -18,31 +18,22 @@ class ActorController extends FediverseActorController ...@@ -18,31 +18,22 @@ class ActorController extends FediverseActorController
use AnalyticsTrait; use AnalyticsTrait;
/** /**
* @var string[] * @var list<string>
*/ */
protected $helpers = ['auth', 'svg', 'components', 'misc']; protected $helpers = ['svg', 'components', 'misc', 'seo'];
public function follow(): string public function followView(): string
{ {
// Prevent analytics hit when authenticated // @phpstan-ignore-next-line
if (! can_user_interact()) { $this->registerPodcastWebpageHit($this->actor->podcast->id);
// @phpstan-ignore-next-line
$this->registerPodcastWebpageHit($this->actor->podcast->id); helper(['form', 'components', 'svg']);
} // @phpstan-ignore-next-line
set_follow_metatags($this->actor);
$cacheName = "page_podcast-{$this->actor->username}_follow"; $data = [
if (! ($cachedView = cache($cacheName))) { 'actor' => $this->actor,
helper(['form', 'components', 'svg']); ];
$data = [
'actor' => $this->actor, return view('podcast/follow', $data);
];
return view('podcast/follow', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
} }
} }
...@@ -7,28 +7,40 @@ namespace App\Controllers; ...@@ -7,28 +7,40 @@ namespace App\Controllers;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Override;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use ViewThemes\Theme; use ViewThemes\Theme;
/** /**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all * BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. Extend this class in any new controllers: class Home extends BaseController * your controllers. Extend this class in any new controllers: class Home extends BaseController
* *
* For security be sure to declare any new methods as protected or private. * For security be sure to declare any new methods as protected or private.
*/ */
class BaseController extends Controller abstract class BaseController extends Controller
{ {
/** /**
* Constructor. * An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available
* to all other controllers that extend BaseController.
*
* @var list<string>
*/ */
protected $helpers = [];
/**
* Be sure to declare properties for any property fetch you initialized.
* The creation of dynamic property is deprecated in PHP 8.2.
*/
// protected $session;
#[Override]
public function initController( public function initController(
RequestInterface $request, RequestInterface $request,
ResponseInterface $response, ResponseInterface $response,
LoggerInterface $logger LoggerInterface $logger,
): void { ): void {
$this->helpers = array_merge($this->helpers, ['auth', 'svg', 'components', 'misc']); $this->helpers = [...$this->helpers, 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
// Do Not Edit This Line // Do Not Edit This Line
parent::initController($request, $response, $logger); parent::initController($request, $response, $logger);
......
<?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 CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
class ColorsController extends Controller
{
public function index(): ResponseInterface
{
$cacheName = 'colors.css';
if (
! ($colorsCssBody = cache($cacheName))
) {
$colorThemes = config('Colors')
->themes;
$colorsCssBody = '';
foreach ($colorThemes as $name => $color) {
$colorsCssBody .= ".theme-{$name} {";
foreach ($color as $variable => $value) {
$colorsCssBody .= "--color-{$variable}: {$value[0]} {$value[1]}% {$value[2]}%;";
}
$colorsCssBody .= '}';
}
cache()
->save($cacheName, $colorsCssBody, DECADE);
}
return $this->response->setHeader('Content-Type', 'text/css')
->setHeader('charset', 'UTF-8')
->setBody($colorsCssBody);
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -20,16 +20,20 @@ class CreditsController extends BaseController ...@@ -20,16 +20,20 @@ class CreditsController extends BaseController
{ {
$locale = service('request') $locale = service('request')
->getLocale(); ->getLocale();
$allPodcasts = (new PodcastModel())->findAll();
$cacheName = "page_credits_{$locale}"; $cacheName = implode(
'_',
array_filter(['page', 'credits', $locale, auth()->loggedIn() ? 'authenticated' : null]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$page = new Page([ $page = new Page([
'title' => lang('Person.credits', [], $locale), 'title' => lang('Person.credits', [], $locale),
'slug' => 'credits', 'slug' => 'credits',
'content_markdown' => '', 'content_markdown' => '',
]); ]);
$allPodcasts = (new PodcastModel())->findAll();
$allCredits = (new CreditModel())->findAll(); $allCredits = (new CreditModel())->findAll();
// Unlike the carpenter, we make a tree from a table: // Unlike the carpenter, we make a tree from a table:
...@@ -44,27 +48,24 @@ class CreditsController extends BaseController ...@@ -44,27 +48,24 @@ class CreditsController extends BaseController
$personRole = $credit->person_role; $personRole = $credit->person_role;
$credits[$personGroup] = [ $credits[$personGroup] = [
'group_label' => $credit->group_label, 'group_label' => $credit->group_label,
'persons' => [ 'persons' => [
$personId => [ $personId => [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => 'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
$credit->person->image->thumbnail_url, 'information_url' => $credit->person->information_url,
'information_url' => 'roles' => [
$credit->person->information_url,
'roles' => [
$personRole => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title}" ? esc($credit->podcast->title) . ''
: '') . : '') .
$credit->episode esc($credit->episode->title) .
->title .
episode_numbering( episode_numbering(
$credit->episode $credit->episode
->number, ->number,
...@@ -73,7 +74,7 @@ class CreditsController extends BaseController ...@@ -73,7 +74,7 @@ class CreditsController extends BaseController
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: $credit->podcast->title, : esc($credit->podcast->title),
], ],
], ],
], ],
...@@ -85,23 +86,22 @@ class CreditsController extends BaseController ...@@ -85,23 +86,22 @@ class CreditsController extends BaseController
$personId = $credit->person_id; $personId = $credit->person_id;
$personRole = $credit->person_role; $personRole = $credit->person_role;
$credits[$personGroup]['persons'][$personId] = [ $credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => 'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
$credit->person->image->thumbnail_url,
'information_url' => $credit->person->information_url, 'information_url' => $credit->person->information_url,
'roles' => [ 'roles' => [
$personRole => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title}" ? esc($credit->podcast->title) . ''
: '') . : '') .
$credit->episode->title . esc($credit->episode->title) .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode $credit->episode
...@@ -109,7 +109,7 @@ class CreditsController extends BaseController ...@@ -109,7 +109,7 @@ class CreditsController extends BaseController
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: $credit->podcast->title, : esc($credit->podcast->title),
], ],
], ],
], ],
...@@ -121,23 +121,23 @@ class CreditsController extends BaseController ...@@ -121,23 +121,23 @@ class CreditsController extends BaseController
$personRole $personRole
] = [ ] = [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title}" ? esc($credit->podcast->title) . ''
: '') . : '') .
$credit->episode->title . esc($credit->episode->title) .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode->season_number, $credit->episode->season_number,
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: $credit->podcast->title, : esc($credit->podcast->title),
], ],
], ],
]; ];
...@@ -150,26 +150,27 @@ class CreditsController extends BaseController ...@@ -150,26 +150,27 @@ class CreditsController extends BaseController
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? "{$credit->podcast->title}" ? esc($credit->podcast->title) . ''
: '') . : '') .
$credit->episode->title . esc($credit->episode->title) .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode->season_number, $credit->episode->season_number,
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: $credit->podcast->title, : esc($credit->podcast->title),
]; ];
} }
} }
set_page_metatags($page);
$data = [ $data = [
'page' => $page, 'page' => $page,
'credits' => $credits, 'credits' => $credits,
]; ];
$found = view('credits', $data); $found = view('pages/credits', $data);
cache() cache()
->save($cacheName, $found, DECADE); ->save($cacheName, $found, DECADE);
......
<?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\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use Modules\Analytics\Config\Analytics;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Override;
use Psr\Log\LoggerInterface;
class EpisodeAudioController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend Analytics.
*
* @var list<string>
*/
protected $helpers = ['analytics'];
protected Podcast $podcast;
protected Episode $episode;
protected Analytics $analyticsConfig;
#[Override]
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger,
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
set_user_session_deny_list_ip();
set_user_session_location();
set_user_session_player();
$this->analyticsConfig = config('Analytics');
}
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): RedirectResponse | ResponseInterface
{
// check if episode is premium?
$subscription = null;
// check if podcast is already unlocked before any token validation
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
$this->episode->podcast->handle,
)) instanceof Subscription) {
// look for token as GET parameter
if (($token = $this->request->getGet('token')) === null) {
return $this->response->setStatusCode(401)
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Episode is premium, you must provide a token to unlock it.',
],
]);
}
// check if there's a valid subscription for the provided token
if (! ($subscription = (new SubscriptionModel())->validateSubscription(
$this->episode->podcast->handle,
$token,
)) instanceof Subscription) {
return $this->response->setStatusCode(401, 'Invalid token!')
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Invalid token!',
],
]);
}
}
$session = service('session');
$serviceName = '';
if ($this->request->getGet('_from')) {
$serviceName = $this->request->getGet('_from');
} elseif ($session->get('embed_domain') !== null) {
$serviceName = $session->get('embed_domain');
} elseif ($session->get('referer') !== null && $session->get('referer') !== '- Direct -') {
$serviceName = parse_url((string) $session->get('referer'), PHP_URL_HOST);
}
$audioFileSize = $this->episode->audio->file_size;
$audioFileHeaderSize = $this->episode->audio->header_size;
$audioDuration = $this->episode->audio->duration;
// bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
// - if audio is less than or equal to 60s, then take the audio file_size
// - if audio is more than 60s, then take the audio file_header_size + 60s
$bytesThreshold = $audioDuration <= 60
? $audioFileSize
: $audioFileHeaderSize +
(int) floor((($audioFileSize - $audioFileHeaderSize) / $audioDuration) * 60);
podcast_hit(
$this->episode->podcast_id,
$this->episode->id,
$bytesThreshold,
$audioFileSize,
$audioDuration,
$this->episode->published_at->getTimestamp(),
$serviceName,
$subscription instanceof Subscription ? $subscription->id : null,
);
$audioFileURI = new URI(service('file_manager')->getUrl($this->episode->audio->file_key));
$queryParams = [];
foreach ($this->request->getGet() as $key => $value) {
// do not include token in query params
if ($key !== 'token') {
$queryParams[$key] = $value;
}
}
$audioFileURI->setQueryArray($queryParams);
return redirect()->to((string) $audioFileURI);
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -19,7 +19,7 @@ use App\Models\EpisodeModel; ...@@ -19,7 +19,7 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait; use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Entities\Actor; use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Objects\OrderedCollectionObject; use Modules\Fediverse\Objects\OrderedCollectionObject;
...@@ -44,7 +44,7 @@ class EpisodeCommentController extends BaseController ...@@ -44,7 +44,7 @@ class EpisodeCommentController extends BaseController
} }
if ( if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null ! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -53,15 +53,15 @@ class EpisodeCommentController extends BaseController ...@@ -53,15 +53,15 @@ class EpisodeCommentController extends BaseController
$this->actor = $podcast->actor; $this->actor = $podcast->actor;
if ( if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null ! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
$this->episode = $episode; $this->episode = $episode;
if ( if (
($comment = (new EpisodeCommentModel())->getCommentById($params[2])) === null ! ($comment = (new EpisodeCommentModel())->getCommentById($params[2])) instanceof EpisodeComment
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -77,37 +77,38 @@ class EpisodeCommentController extends BaseController ...@@ -77,37 +77,38 @@ class EpisodeCommentController extends BaseController
public function view(): string public function view(): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->podcast->id);
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode( $cacheName = implode(
'_', '_',
array_filter([ array_filter([
'page', 'page',
"episode#{$this->episode->id}",
"comment#{$this->comment->id}", "comment#{$this->comment->id}",
service('request') service('request')
->getLocale(), ->getLocale(),
can_user_interact() ? '_authenticated' : null, auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_episode_comment_metatags($this->comment);
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'actor' => $this->actor, 'actor' => $this->actor,
'episode' => $this->episode, 'episode' => $this->episode,
'comment' => $this->comment, 'comment' => $this->comment,
]; ];
// if user is logged in then send to the authenticated activity view // if user is logged in then send to the authenticated activity view
if (can_user_interact()) { if (auth()->loggedIn()) {
helper('form'); helper('form');
return view('podcast/comment_authenticated', $data); return view('episode/comment', $data);
} }
return view('podcast/comment', $data, [
'cache' => DECADE, return view('episode/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -115,10 +116,7 @@ class EpisodeCommentController extends BaseController ...@@ -115,10 +116,7 @@ class EpisodeCommentController extends BaseController
return $cachedView; return $cachedView;
} }
/** public function commentObject(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function commentObject(): Response
{ {
$commentObject = new CommentObject($this->comment); $commentObject = new CommentObject($this->comment);
...@@ -127,15 +125,12 @@ class EpisodeCommentController extends BaseController ...@@ -127,15 +125,12 @@ class EpisodeCommentController extends BaseController
->setBody($commentObject->toJSON()); ->setBody($commentObject->toJSON());
} }
/** public function replies(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function replies(): Response
{ {
/** /**
* get comment replies * get comment replies
*/ */
$commentReplies = model('CommentModel', false) $commentReplies = model(EpisodeCommentModel::class, false)
->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes()) ->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes())
->orderBy('created_at', 'ASC'); ->orderBy('created_at', 'ASC');
...@@ -165,18 +160,26 @@ class EpisodeCommentController extends BaseController ...@@ -165,18 +160,26 @@ class EpisodeCommentController extends BaseController
->setBody($collection->toJSON()); ->setBody($collection->toJSON());
} }
public function attemptLike(): RedirectResponse public function likeAction(): RedirectResponse
{ {
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel') model('LikeModel')
->toggleLike(interact_as_actor(), $this->comment); ->toggleLike($interactAsActor, $this->comment);
return redirect()->back(); return redirect()->back();
} }
public function attemptReply(): RedirectResponse public function replyAction(): RedirectResponse
{ {
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel') model('LikeModel')
->toggleLike(interact_as_actor(), $this->comment); ->toggleLike($interactAsActor, $this->comment);
return redirect()->back(); return redirect()->back();
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -18,12 +18,12 @@ use App\Models\EpisodeModel; ...@@ -18,12 +18,12 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Config\Services; use Config\Embed;
use Modules\Analytics\AnalyticsTrait; use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject; use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage; use Modules\Fediverse\Objects\OrderedCollectionPage;
use Modules\Media\FileManagers\FileManagerInterface;
use SimpleXMLElement; use SimpleXMLElement;
class EpisodeController extends BaseController class EpisodeController extends BaseController
...@@ -41,7 +41,7 @@ class EpisodeController extends BaseController ...@@ -41,7 +41,7 @@ class EpisodeController extends BaseController
} }
if ( if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null ! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -49,8 +49,8 @@ class EpisodeController extends BaseController ...@@ -49,8 +49,8 @@ class EpisodeController extends BaseController
$this->podcast = $podcast; $this->podcast = $podcast;
if ( if (
($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null ! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -64,18 +64,70 @@ class EpisodeController extends BaseController ...@@ -64,18 +64,70 @@ class EpisodeController extends BaseController
public function index(): string public function index(): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->episode->podcast_id);
if (! can_user_interact()) {
$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 = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/comments', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
} }
$locale = service('request') return $cachedView;
->getLocale(); }
$cacheName =
"page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" . public function activity(): string
(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))) { if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episode' => $this->episode, 'episode' => $this->episode,
...@@ -85,15 +137,15 @@ class EpisodeController extends BaseController ...@@ -85,15 +137,15 @@ class EpisodeController extends BaseController
$this->podcast->id, $this->podcast->id,
); );
if (can_user_interact()) { if (auth()->loggedIn()) {
helper('form'); helper('form');
return view('podcast/episode_authenticated', $data);
return view('episode/activity', $data);
} }
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('podcast/episode', $data, [ return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -101,33 +153,156 @@ class EpisodeController extends BaseController ...@@ -101,33 +153,156 @@ class EpisodeController extends BaseController
return $cachedView; return $cachedView;
} }
public function embeddablePlayer(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 // The page cache is set to a decade so it is deleted manually upon podcast update
if (! can_user_interact()) { return view('episode/chapters', $data, [
$this->registerPodcastWebpageHit($this->episode->podcast_id); 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
} }
$session = Services::session(); return $cachedView;
$session->start(); }
if (isset($_SERVER['HTTP_REFERER'])) {
$session->set('embeddable_player_domain', parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)); 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') return $cachedView;
->getLocale(); }
public function embed(string $theme = 'light-transparent'): string
{
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$session = service('session');
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}"; 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))) { if (! ($cachedView = cache($cacheName))) {
$themeData = EpisodeModel::$themes[$theme]; $themeData = EpisodeModel::$themes[$theme];
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episode' => $this->episode, 'episode' => $this->episode,
'theme' => $theme, 'theme' => $theme,
'themeData' => $themeData, 'themeData' => $themeData,
]; ];
...@@ -136,10 +311,8 @@ class EpisodeController extends BaseController ...@@ -136,10 +311,8 @@ class EpisodeController extends BaseController
); );
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('embeddable_player', $data, [ return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -150,24 +323,25 @@ class EpisodeController extends BaseController ...@@ -150,24 +323,25 @@ class EpisodeController extends BaseController
public function oembedJSON(): ResponseInterface public function oembedJSON(): ResponseInterface
{ {
return $this->response->setJSON([ return $this->response->setJSON([
'type' => 'rich', 'type' => 'rich',
'version' => '1.0', 'version' => '1.0',
'title' => $this->episode->title, 'title' => $this->episode->title,
'provider_name' => $this->podcast->title, 'provider_name' => $this->podcast->title,
'provider_url' => $this->podcast->link, 'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title, 'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link, 'author_url' => $this->podcast->link,
'html' => 'html' => '<iframe src="' .
'<iframe src="' . $this->episode->embed_url .
$this->episode->embeddable_player_url . '" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>',
'" width="100%" height="144" frameborder="0" scrolling="no"></iframe>', 'width' => config('Embed')
'width' => 600, ->width,
'height' => 144, 'height' => config('Embed')
'thumbnail_url' => $this->episode->image->large_url, ->height,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_width' => config('Images') 'thumbnail_width' => config('Images')
->largeSize, ->podcastCoverSizes['og']['width'],
'thumbnail_height' => config('Images') 'thumbnail_height' => config('Images')
->largeSize, ->podcastCoverSizes['og']['height'],
]); ]);
} }
...@@ -182,27 +356,27 @@ class EpisodeController extends BaseController ...@@ -182,27 +356,27 @@ class EpisodeController extends BaseController
$oembed->addChild('provider_url', $this->podcast->link); $oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title); $oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link); $oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->image->large_url); $oembed->addChild('thumbnail', $this->episode->cover->og_url);
$oembed->addChild('thumbnail_width', config('Images')->largeSize); $oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['og']['width']);
$oembed->addChild('thumbnail_height', config('Images')->largeSize); $oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['og']['height']);
$oembed->addChild( $oembed->addChild(
'html', 'html',
htmlentities( htmlspecialchars(
'<iframe src="' . '<iframe src="' .
$this->episode->embeddable_player_url . $this->episode->embed_url .
'" width="100%" height="144" frameborder="0" scrolling="no"></iframe>', '" width="100%" height="' . config(
Embed::class,
)->height . '" frameborder="0" scrolling="no"></iframe>',
), ),
); );
$oembed->addChild('width', '600'); $oembed->addChild('width', (string) config('Embed')->width);
$oembed->addChild('height', '144'); $oembed->addChild('height', (string) config('Embed')->height);
return $this->response->setXML((string) $oembed); // @phpstan-ignore-next-line
return $this->response->setXML($oembed);
} }
/** public function episodeObject(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function episodeObject(): Response
{ {
$podcastObject = new PodcastEpisode($this->episode); $podcastObject = new PodcastEpisode($this->episode);
...@@ -211,21 +385,16 @@ class EpisodeController extends BaseController ...@@ -211,21 +385,16 @@ class EpisodeController extends BaseController
->setBody($podcastObject->toJSON()); ->setBody($podcastObject->toJSON());
} }
/** public function comments(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function comments(): Response
{ {
/** /**
* get comments: aggregated replies from posts referring to the episode * get comments: aggregated replies from posts referring to the episode
*/ */
$episodeComments = model('PostModel') $episodeComments = model('PostModel')
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder { ->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id')
return $builder->select('id') ->from('fediverse_posts')
->from(config('Fediverse')->tablesPrefix . 'posts') ->where('episode_id', $this->episode->id))
->where('episode_id', $this->episode->id); ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
})
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page'); $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 @@ ...@@ -3,36 +3,58 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Exception; use Exception;
use Opawg\UserAgentsPhp\UserAgentsRSS; use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Opawg\UserAgentsV2Php\UserAgentsRSS;
class FeedController extends Controller class FeedController extends Controller
{ {
/**
* Instance of the main Request object.
*
* @var IncomingRequest
*/
protected $request;
public function index(string $podcastHandle): ResponseInterface public function index(string $podcastHandle): ResponseInterface
{ {
helper('rss');
$podcast = (new PodcastModel())->where('handle', $podcastHandle) $podcast = (new PodcastModel())->where('handle', $podcastHandle)
->first(); ->first();
if (! $podcast) { if (! $podcast instanceof Podcast) {
throw PageNotFoundException::forPageNotFound(); 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; $service = null;
try { try {
$service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']); $service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT'));
} catch (Exception $exception) { } catch (Exception $exception) {
// If things go wrong the show must go on and the user must be able to download the file // If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $exception->getMessage()); log_message('critical', $exception->getMessage());
...@@ -43,11 +65,24 @@ class FeedController extends Controller ...@@ -43,11 +65,24 @@ class FeedController extends Controller
$serviceSlug = $service['slug']; $serviceSlug = $service['slug'];
} }
$cacheName = $subscription = null;
"podcast#{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : ''); $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))) { 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 // 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( $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
...@@ -55,13 +90,7 @@ class FeedController extends Controller ...@@ -55,13 +90,7 @@ class FeedController extends Controller
); );
cache() cache()
->save( ->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $this->response->setXML($found); return $this->response->setXML($found);
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -11,34 +11,73 @@ declare(strict_types=1); ...@@ -11,34 +11,73 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use Config\Services; use CodeIgniter\HTTP\ResponseInterface;
use Modules\Media\FileManagers\FileManagerInterface;
class HomeController extends BaseController class HomeController extends BaseController
{ {
public function index(): RedirectResponse | string public function index(): RedirectResponse | string
{ {
$db = db_connect(); $sortOptions = ['activity', 'created_desc', 'created_asc'];
if ($db->getDatabase() === '' || ! $db->tableExists('podcasts')) { $sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
// Database connection has not been set or could not find the podcasts table 'sort',
// Redirecting to install page because it is likely that Castopod Host has not been installed yet. ) : 'activity';
// 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);
}
$allPodcasts = (new PodcastModel())->findAll(); $allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
// check if there's only one podcast to redirect user to it // check if there's only one podcast to redirect user to it
if (count($allPodcasts) === 1) { if (count($allPodcasts) === 1) {
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]); return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
} }
set_home_metatags();
// default behavior: list all podcasts on home page // default behavior: list all podcasts on home page
$data = [ $data = [
'podcasts' => $allPodcasts, 'podcasts' => $allPodcasts,
'sortBy' => $sortBy,
]; ];
return view('home', $data); 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!',
]);
}
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -13,19 +13,29 @@ namespace App\Controllers; ...@@ -13,19 +13,29 @@ namespace App\Controllers;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
class MapMarkerController extends BaseController class MapController extends BaseController
{ {
public function index(): string public function index(): string
{ {
$locale = service('request') $cacheName = implode(
->getLocale(); '_',
$cacheName = "page_map_{$locale}"; array_filter([
'page',
'map',
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = view('map', [], [ return view('pages/map', [], [
'cache' => DECADE, 'cache' => DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
return $found; return $found;
} }
...@@ -34,27 +44,29 @@ class MapMarkerController extends BaseController ...@@ -34,27 +44,29 @@ class MapMarkerController extends BaseController
$cacheName = 'episodes_markers'; $cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$episodes = (new EpisodeModel()) $episodes = (new EpisodeModel())
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->where('location_geo is not', null) ->where('location_geo is not', null)
->findAll(); ->findAll();
$found = []; $found = [];
foreach ($episodes as $episode) { foreach ($episodes as $episode) {
$found[] = [ $found[] = [
'latitude' => $episode->location->latitude, 'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude, 'longitude' => $episode->location->longitude,
'location_name' => $episode->location->name, 'location_name' => esc($episode->location->name),
'location_url' => $episode->location->url, 'location_url' => $episode->location->url,
'episode_link' => $episode->link, 'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link, 'podcast_link' => $episode->podcast->link,
'image_path' => $episode->image->thumbnail_url, 'cover_url' => $episode->cover->thumbnail_url,
'podcast_title' => $episode->podcast->title, 'podcast_title' => esc($episode->podcast->title),
'episode_title' => $episode->title, 'episode_title' => esc($episode->title),
]; ];
} }
// The page cache is set to a decade so it is deleted manually upon episode update // The page cache is set to a decade so it is deleted manually upon episode update
cache() cache()
->save($cacheName, $found, DECADE); ->save($cacheName, $found, DECADE);
} }
return $this->response->setJSON($found); return $this->response->setJSON($found);
} }
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -24,9 +24,8 @@ class PageController extends BaseController ...@@ -24,9 +24,8 @@ class PageController extends BaseController
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
if ( $page = (new PageModel())->where('slug', $params[0])->first();
($page = (new PageModel())->where('slug', $params[0])->first()) === null if (! $page instanceof Page) {
) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -37,13 +36,25 @@ class PageController extends BaseController ...@@ -37,13 +36,25 @@ class PageController extends BaseController
public function index(): string public function index(): string
{ {
$cacheName = "page-{$this->page->slug}"; $cacheName = implode(
'_',
array_filter([
'page',
$this->page->slug,
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
set_page_metatags($this->page);
$data = [ $data = [
'page' => $this->page, 'page' => $this->page,
]; ];
$found = view('page', $data); $found = view('pages/page', $data);
// The page cache is set to a decade so it is deleted manually upon page update // The page cache is set to a decade so it is deleted manually upon page update
cache() cache()
......
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @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());
}
}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -17,7 +17,7 @@ use App\Models\EpisodeModel; ...@@ -17,7 +17,7 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response; use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait; use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject; use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage; use Modules\Fediverse\Objects\OrderedCollectionPage;
...@@ -35,7 +35,7 @@ class PodcastController extends BaseController ...@@ -35,7 +35,7 @@ class PodcastController extends BaseController
} }
if ( if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) === null ! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -47,10 +47,7 @@ class PodcastController extends BaseController ...@@ -47,10 +47,7 @@ class PodcastController extends BaseController
return $this->{$method}(...$params); return $this->{$method}(...$params);
} }
/** public function podcastActor(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function podcastActor(): Response
{ {
$podcastActor = new PodcastActor($this->podcast); $podcastActor = new PodcastActor($this->podcast);
...@@ -61,10 +58,7 @@ class PodcastController extends BaseController ...@@ -61,10 +58,7 @@ class PodcastController extends BaseController
public function activity(): string public function activity(): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->podcast->id);
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode( $cacheName = implode(
'_', '_',
...@@ -74,20 +68,24 @@ class PodcastController extends BaseController ...@@ -74,20 +68,24 @@ class PodcastController extends BaseController
'activity', 'activity',
service('request') service('request')
->getLocale(), ->getLocale(),
can_user_interact() ? '_authenticated' : null, is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_podcast_metatags($this->podcast, 'activity');
$data = [ $data = [
'podcast' => $this->podcast, '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 user is logged in then send to the authenticated activity view
if (can_user_interact()) { if (auth()->loggedIn()) {
helper('form'); helper('form');
return view('podcast/activity_authenticated', $data);
return view('podcast/activity', $data);
} }
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode( $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
...@@ -95,9 +93,7 @@ class PodcastController extends BaseController ...@@ -95,9 +93,7 @@ class PodcastController extends BaseController
); );
return view('podcast/activity', $data, [ return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -105,13 +101,57 @@ class PodcastController extends BaseController ...@@ -105,13 +101,57 @@ class PodcastController extends BaseController
return $cachedView; return $cachedView;
} }
public function episodes(): string public function about(): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->podcast->id);
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id); $cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'about',
service('request')
->getLocale(),
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 = [
'podcast' => $this->podcast,
'stats' => $stats,
];
// // if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('podcast/about', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
} }
return $cachedView;
}
public function episodes(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$yearQuery = $this->request->getGet('year'); $yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season'); $seasonQuery = $this->request->getGet('season');
...@@ -136,7 +176,9 @@ class PodcastController extends BaseController ...@@ -136,7 +176,9 @@ class PodcastController extends BaseController
$seasonQuery ? 'season' . $seasonQuery : null, $seasonQuery ? 'season' . $seasonQuery : null,
service('request') service('request')
->getLocale(), ->getLocale(),
can_user_interact() ? '_authenticated' : null, is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
...@@ -152,18 +194,17 @@ class PodcastController extends BaseController ...@@ -152,18 +194,17 @@ class PodcastController extends BaseController
$isActive = $yearQuery === $year['year']; $isActive = $yearQuery === $year['year'];
if ($isActive) { if ($isActive) {
$activeQuery = [ $activeQuery = [
'type' => 'year', 'type' => 'year',
'value' => $year['year'], 'value' => $year['year'],
'label' => $year['year'], 'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'], 'number_of_episodes' => $year['number_of_episodes'],
]; ];
} }
$episodesNavigation[] = [ $episodesNavigation[] = [
'label' => $year['year'], 'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'], 'number_of_episodes' => $year['number_of_episodes'],
'route' => 'route' => route_to('podcast-episodes', $this->podcast->handle) .
route_to('podcast-episodes', $this->podcast->handle) .
'?year=' . '?year=' .
$year['year'], $year['year'],
'is_active' => $isActive, 'is_active' => $isActive,
...@@ -174,7 +215,7 @@ class PodcastController extends BaseController ...@@ -174,7 +215,7 @@ class PodcastController extends BaseController
$isActive = $seasonQuery === $season['season_number']; $isActive = $seasonQuery === $season['season_number'];
if ($isActive) { if ($isActive) {
$activeQuery = [ $activeQuery = [
'type' => 'season', 'type' => 'season',
'value' => $season['season_number'], 'value' => $season['season_number'],
'label' => lang('Podcast.season', [ 'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'], 'seasonNumber' => $season['season_number'],
...@@ -188,19 +229,19 @@ class PodcastController extends BaseController ...@@ -188,19 +229,19 @@ class PodcastController extends BaseController
'seasonNumber' => $season['season_number'], 'seasonNumber' => $season['season_number'],
]), ]),
'number_of_episodes' => $season['number_of_episodes'], 'number_of_episodes' => $season['number_of_episodes'],
'route' => 'route' => route_to('podcast-episodes', $this->podcast->handle) .
route_to('podcast-episodes', $this->podcast->handle) .
'?season=' . '?season=' .
$season['season_number'], $season['season_number'],
'is_active' => $isActive, 'is_active' => $isActive,
]; ];
} }
set_podcast_metatags($this->podcast, 'episodes');
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation, 'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery, 'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes( 'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id, $this->podcast->id,
$this->podcast->type, $this->podcast->type,
$yearQuery, $yearQuery,
...@@ -208,18 +249,15 @@ class PodcastController extends BaseController ...@@ -208,18 +249,15 @@ class PodcastController extends BaseController
), ),
]; ];
if (auth()->loggedIn()) {
return view('podcast/episodes', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode( $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id, $this->podcast->id,
); );
// if user is logged in then send to the authenticated episodes view
if (can_user_interact()) {
return view('podcast/episodes_authenticated', $data);
}
return view('podcast/episodes', $data, [ return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode 'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -227,19 +265,16 @@ class PodcastController extends BaseController ...@@ -227,19 +265,16 @@ class PodcastController extends BaseController
return $cachedView; return $cachedView;
} }
/** public function episodeCollection(): ResponseInterface
* @noRector ReturnTypeDeclarationRector
*/
public function episodeCollection(): Response
{ {
if ($this->podcast->type === 'serial') { if ($this->podcast->type === 'serial') {
// podcast is serial // podcast is serial
$episodes = model('EpisodeModel') $episodes = model('EpisodeModel')
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('season_number DESC, number ASC'); ->orderBy('season_number DESC, number ASC');
} else { } else {
$episodes = model('EpisodeModel') $episodes = model('EpisodeModel')
->where('`published_at` <= NOW()', null, false) ->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'DESC'); ->orderBy('published_at', 'DESC');
} }
...@@ -268,4 +303,12 @@ class PodcastController extends BaseController ...@@ -268,4 +303,12 @@ class PodcastController extends BaseController
->setContentType('application/activity+json') ->setContentType('application/activity+json')
->setBody($collection->toJSON()); ->setBody($collection->toJSON());
} }
public function links(): string
{
set_podcast_metatags($this->podcast, 'links');
return view('podcast/links', [
'podcast' => $this->podcast,
]);
}
} }
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
...@@ -22,7 +22,7 @@ use CodeIgniter\HTTP\URI; ...@@ -22,7 +22,7 @@ use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait; use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController; use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Modules\Fediverse\Entities\Post as FediversePost; use Override;
class PostController extends FediversePostController class PostController extends FediversePostController
{ {
...@@ -33,14 +33,21 @@ class PostController extends FediversePostController ...@@ -33,14 +33,21 @@ class PostController extends FediversePostController
protected Actor $actor; protected Actor $actor;
/** /**
* @var string[] * @var CastopodPost
*/ */
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc']; protected $post;
/**
* @var list<string>
*/
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
#[Override]
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
if ( if (
($podcast = (new PodcastModel())->getPodcastByHandle($params[0],)) === null ! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
...@@ -48,25 +55,29 @@ class PostController extends FediversePostController ...@@ -48,25 +55,29 @@ class PostController extends FediversePostController
$this->podcast = $podcast; $this->podcast = $podcast;
$this->actor = $this->podcast->actor; $this->actor = $this->podcast->actor;
if (count($params) <= 1) {
unset($params[0]);
return $this->{$method}(...$params);
}
if ( if (
count($params) > 1 && ! ($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
($post = (new PostModel())->getPostById($params[1])) !== null
) { ) {
$this->post = $post; throw PageNotFoundException::forPageNotFound();
unset($params[0]);
unset($params[1]);
} }
$this->post = $post;
unset($params[0]);
unset($params[1]);
return $this->{$method}(...$params); return $this->{$method}(...$params);
} }
public function view(): string public function view(): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->podcast->id);
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode( $cacheName = implode(
'_', '_',
...@@ -75,23 +86,26 @@ class PostController extends FediversePostController ...@@ -75,23 +86,26 @@ class PostController extends FediversePostController
"post#{$this->post->id}", "post#{$this->post->id}",
service('request') service('request')
->getLocale(), ->getLocale(),
can_user_interact() ? '_authenticated' : null, auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_post_metatags($this->post);
$data = [ $data = [
'post' => $this->post, 'post' => $this->post,
'podcast' => $this->podcast, 'podcast' => $this->podcast,
]; ];
// if user is logged in then send to the authenticated activity view // if user is logged in then send to the authenticated activity view
if (can_user_interact()) { if (auth()->loggedIn()) {
helper('form'); helper('form');
return view('podcast/post_authenticated', $data); return view('post/post', $data);
} }
return view('podcast/post', $data, [
'cache' => DECADE, return view('post/post', $data, [
'cache' => DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
...@@ -99,11 +113,12 @@ class PostController extends FediversePostController ...@@ -99,11 +113,12 @@ class PostController extends FediversePostController
return $cachedView; return $cachedView;
} }
public function attemptCreate(): RedirectResponse #[Override]
public function createAction(): RedirectResponse
{ {
$rules = [ $rules = [
'message' => 'required|max_length[500]', 'message' => 'required|max_length[500]',
'episode_url' => 'valid_url|permit_empty', 'episode_url' => 'valid_url_strict|permit_empty',
]; ];
if (! $this->validate($rules)) { if (! $this->validate($rules)) {
...@@ -113,16 +128,18 @@ class PostController extends FediversePostController ...@@ -113,16 +128,18 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$message = $this->request->getPost('message'); $validData = $this->validator->getValidated();
$message = $validData['message'];
$newPost = new CastopodPost([ $newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(), 'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(), 'published_at' => Time::now(),
'created_by' => user_id(), 'created_by' => user_id(),
]); ]);
// get episode if episodeUrl has been set // get episode if episodeUrl has been set
$episodeUri = $this->request->getPost('episode_url'); $episodeUri = $validData['episode_url'];
if ( if (
$episodeUri && $episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) && ($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
...@@ -148,7 +165,8 @@ class PostController extends FediversePostController ...@@ -148,7 +165,8 @@ class PostController extends FediversePostController
return redirect()->back(); return redirect()->back();
} }
public function attemptReply(): RedirectResponse #[Override]
public function replyAction(): RedirectResponse
{ {
$rules = [ $rules = [
'message' => 'required|max_length[500]', 'message' => 'required|max_length[500]',
...@@ -161,14 +179,20 @@ class PostController extends FediversePostController ...@@ -161,14 +179,20 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$newPost = new FediversePost([ $validData = $this->validator->getValidated();
'actor_id' => interact_as_actor_id(),
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id, 'in_reply_to_id' => $this->post->id,
'message' => $this->request->getPost('message'), 'message' => $validData['message'],
'published_at' => Time::now(), 'published_at' => Time::now(),
'created_by' => user_id(), 'created_by' => user_id(),
]); ]);
if ($this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
$postModel = new PostModel(); $postModel = new PostModel();
if (! $postModel->addReply($newPost)) { if (! $postModel->addReply($newPost)) {
return redirect() return redirect()
...@@ -181,21 +205,23 @@ class PostController extends FediversePostController ...@@ -181,21 +205,23 @@ class PostController extends FediversePostController
return redirect()->back(); return redirect()->back();
} }
public function attemptFavourite(): RedirectResponse #[Override]
public function favouriteAction(): RedirectResponse
{ {
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post); model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back(); return redirect()->back();
} }
public function attemptReblog(): RedirectResponse #[Override]
public function reblogAction(): RedirectResponse
{ {
(new PostModel())->toggleReblog(interact_as_actor(), $this->post); (new PostModel())->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back(); return redirect()->back();
} }
public function attemptAction(): RedirectResponse public function action(): RedirectResponse
{ {
$rules = [ $rules = [
'action' => 'required|in_list[favourite,reblog,reply]', 'action' => 'required|in_list[favourite,reblog,reply]',
...@@ -208,46 +234,35 @@ class PostController extends FediversePostController ...@@ -208,46 +234,35 @@ class PostController extends FediversePostController
->with('errors', $this->validator->getErrors()); ->with('errors', $this->validator->getErrors());
} }
$action = $this->request->getPost('action'); $validData = $this->validator->getValidated();
$action = $validData['action'];
return match ($action) { return match ($action) {
'favourite' => $this->attemptFavourite(), 'favourite' => $this->favouriteAction(),
'reblog' => $this->attemptReblog(), 'reblog' => $this->reblogAction(),
'reply' => $this->attemptReply(), 'reply' => $this->replyAction(),
default => redirect() default => redirect()
->back() ->back()
->withInput() ->withInput()
->with('errors', 'error'), ->with('errors', 'error'),
}; };
} }
public function remoteAction(string $action): string public function remoteActionView(string $action): string
{ {
// Prevent analytics hit when authenticated $this->registerPodcastWebpageHit($this->podcast->id);
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id); set_remote_actions_metatags($this->post, $action);
} $data = [
'podcast' => $this->podcast,
$cacheName = implode( 'actor' => $this->actor,
'_', 'post' => $this->post,
array_filter(['page', "post#{$this->post->id}", "remote_{$action}", service('request') ->getLocale()]), 'action' => $action,
); ];
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
helper('form');
return view('podcast/post_remote_action', $data, [ helper('form');
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return (string) $cachedView; // NO VIEW CACHING: form has a CSRF token which should change on each request
return view('post/remote_action', $data);
} }
} }
<?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\Entities\Podcast;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
class WebmanifestController extends Controller
{
/**
* @var array<string, array<string, string>>
*/
final public const array THEME_COLORS = [
'pine' => [
'theme' => '#009486',
'background' => '#F0F9F8',
],
'lake' => [
'theme' => '#00ACE0',
'background' => '#F0F7F9',
],
'jacaranda' => [
'theme' => '#562CDD',
'background' => '#F2F0F9',
],
'crimson' => [
'theme' => '#F24562',
'background' => '#F9F0F2',
],
'amber' => [
'theme' => '#FF6224',
'background' => '#F9F3F0',
],
'onyx' => [
'theme' => '#040406',
'background' => '#F3F3F7',
],
];
public function index(): ResponseInterface
{
helper('misc');
$webmanifest = [
'name' => esc(service('settings') ->get('App.siteName')),
'description' => esc(service('settings') ->get('App.siteDescription')),
'lang' => service('request')
->getLocale(),
'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' => [
[
'src' => get_site_icon_url('192'),
'type' => 'image/png',
'sizes' => '192x192',
],
[
'src' => get_site_icon_url('512'),
'type' => 'image/png',
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
public function podcastManifest(string $podcastHandle): ResponseInterface
{
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($podcastHandle)) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$webmanifest = [
'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' => [
[
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'sizes' => '192x192',
],
[
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
}
...@@ -5,39 +5,40 @@ declare(strict_types=1); ...@@ -5,39 +5,40 @@ declare(strict_types=1);
/** /**
* Class AddCategories Creates categories table in database * Class AddCategories Creates categories table in database
* *
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Database\Migrations; namespace App\Database\Migrations;
use CodeIgniter\Database\Migration; use Override;
class AddCategories extends Migration class AddCategories extends BaseMigration
{ {
#[Override]
public function up(): void public function up(): void
{ {
$this->forge->addField([ $this->forge->addField([
'id' => [ 'id' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
], ],
'parent_id' => [ 'parent_id' => [
'type' => 'INT', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
'null' => true, 'null' => true,
], ],
'code' => [ 'code' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 32, 'constraint' => 32,
], ],
'apple_category' => [ 'apple_category' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 32, 'constraint' => 32,
], ],
'google_category' => [ 'google_category' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 32, 'constraint' => 32,
], ],
]); ]);
...@@ -47,6 +48,7 @@ class AddCategories extends Migration ...@@ -47,6 +48,7 @@ class AddCategories extends Migration
$this->forge->createTable('categories'); $this->forge->createTable('categories');
} }
#[Override]
public function down(): void public function down(): void
{ {
$this->forge->dropTable('categories'); $this->forge->dropTable('categories');
......
...@@ -5,27 +5,28 @@ declare(strict_types=1); ...@@ -5,27 +5,28 @@ declare(strict_types=1);
/** /**
* Class AddLanguages Creates languages table in database * Class AddLanguages Creates languages table in database
* *
* @copyright 2020 Podlibre * @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Database\Migrations; namespace App\Database\Migrations;
use CodeIgniter\Database\Migration; use Override;
class AddLanguages extends Migration class AddLanguages extends BaseMigration
{ {
#[Override]
public function up(): void public function up(): void
{ {
$this->forge->addField([ $this->forge->addField([
'code' => [ 'code' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code', 'comment' => 'ISO 639-1 language code',
'constraint' => 2, 'constraint' => 2,
], ],
'native_name' => [ 'native_name' => [
'type' => 'VARCHAR', 'type' => 'VARCHAR',
'constraint' => 128, 'constraint' => 128,
], ],
]); ]);
...@@ -33,6 +34,7 @@ class AddLanguages extends Migration ...@@ -33,6 +34,7 @@ class AddLanguages extends Migration
$this->forge->createTable('languages'); $this->forge->createTable('languages');
} }
#[Override]
public function down(): void public function down(): void
{ {
$this->forge->dropTable('languages'); $this->forge->dropTable('languages');
......