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 1404 additions and 1554 deletions
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PageModel;
class Page extends BaseController
{
/**
* @var \App\Entities\Page|null
*/
protected $page;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->page = (new PageModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
function list()
{
$data = [
'pages' => (new PageModel())->findAll(),
];
return view('admin/page/list', $data);
}
function view()
{
return view('admin/page/view', ['page' => $this->page]);
}
function create()
{
helper('form');
return view('admin/page/create');
}
function attemptCreate()
{
$page = new \App\Entities\Page([
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'content' => $this->request->getPost('content'),
]);
$pageModel = new PageModel();
if (!$pageModel->insert($page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()
->route('page-list')
->with(
'message',
lang('Page.messages.createSuccess', [
'pageTitle' => $page->title,
])
);
}
function edit()
{
helper('form');
replace_breadcrumb_params([0 => $this->page->title]);
return view('admin/page/edit', ['page' => $this->page]);
}
function attemptEdit()
{
$this->page->title = $this->request->getPost('title');
$this->page->slug = $this->request->getPost('slug');
$this->page->content = $this->request->getPost('content');
$pageModel = new PageModel();
if (!$pageModel->update($this->page->id, $this->page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()->route('page-list');
}
public function delete()
{
(new PageModel())->delete($this->page->id);
return redirect()->route('page-list');
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\CategoryModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use Config\Services;
class Podcast extends BaseController
{
/**
* @var \App\Entities\Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function list()
{
if (!has_permission('podcasts-list')) {
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts(user()->id),
];
} else {
$data = ['podcasts' => (new PodcastModel())->findAll()];
}
return view('admin/podcast/list', $data);
}
public function view()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/view', $data);
}
public function viewAnalytics()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/index', $data);
}
public function viewAnalyticsWebpages()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/webpages', $data);
}
public function viewAnalyticsLocations()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/locations', $data);
}
public function viewAnalyticsUniqueListeners()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/unique_listeners', $data);
}
public function viewAnalyticsListeningTime()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/listening_time', $data);
}
public function viewAnalyticsTimePeriods()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/time_periods', $data);
}
public function viewAnalyticsPlayers()
{
$data = ['podcast' => $this->podcast];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/analytics/players', $data);
}
public function create()
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language(
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
),
];
return view('admin/podcast/create', $data);
}
public function attemptCreate()
{
$rules = [
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$podcast = new \App\Entities\Podcast([
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description_markdown' => $this->request->getPost('description'),
'image' => $this->request->getFile('image'),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'payment_pointer' => $this->request->getPost('payment_pointer'),
'is_blocked' => $this->request->getPost('is_blocked') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes',
'is_locked' => $this->request->getPost('lock') === 'yes',
'created_by' => user(),
'updated_by' => user(),
]);
$podcastModel = new PodcastModel();
$db = \Config\Database::connect();
$db->transStart();
if (!($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(
user()->id,
$newPodcastId,
$podcastAdminGroup->id
);
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$newPodcastId,
$this->request->getPost('other_categories')
);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function edit()
{
helper('form');
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'podcast' => $this->podcast,
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/edit', $data);
}
public function attemptEdit()
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->podcast->title = $this->request->getPost('title');
$this->podcast->name = $this->request->getPost('name');
$this->podcast->description_markdown = $this->request->getPost(
'description'
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->podcast->image = $image;
}
$this->podcast->language_code = $this->request->getPost('language');
$this->podcast->category_id = $this->request->getPost('category');
$this->podcast->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer'
);
$this->podcast->is_blocked =
$this->request->getPost('is_blocked') === 'yes';
$this->podcast->is_completed =
$this->request->getPost('complete') === 'yes';
$this->podcast->is_lock = $this->request->getPost('lock') === 'yes';
$this->updated_by = user();
$db = \Config\Database::connect();
$db->transStart();
$podcastModel = new PodcastModel();
if (!$podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$this->podcast->id,
$this->request->getPost('other_categories')
);
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function latestEpisodes(int $limit, int $podcast_id)
{
$episodes = (new EpisodeModel())
->where('podcast_id', $podcast_id)
->orderBy('created_at', 'desc')
->findAll($limit);
return view('admin/podcast/latest_episodes', ['episodes' => $episodes]);
}
public function delete()
{
(new PodcastModel())->delete($this->podcast->id);
return redirect()->route('podcast-list');
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\CategoryModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use App\Models\PlatformModel;
use Config\Services;
use League\HTMLToMarkdown\HtmlConverter;
class PodcastImport extends BaseController
{
/**
* @var \App\Entities\Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function index()
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language(
$this->request->getServer('HTTP_ACCEPT_LANGUAGE')
),
];
return view('admin/podcast/import', $data);
}
public function attemptImport()
{
helper(['media', 'misc']);
$rules = [
'imported_feed_url' => 'required|validate_url',
'season_number' => 'is_natural_no_zero|permit_empty',
'max_episodes' => 'is_natural_no_zero|permit_empty',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
try {
$feed = simplexml_load_file(
$this->request->getPost('imported_feed_url')
);
} catch (\ErrorException $ex) {
return redirect()
->back()
->withInput()
->with('errors', [
$ex->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$nsItunes = $feed->channel[0]->children(
'http://www.itunes.com/dtds/podcast-1.0.dtd'
);
$nsPodcast = $feed->channel[0]->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'
);
if ((string) $nsPodcast->locked === 'yes') {
return redirect()
->back()
->withInput()
->with('errors', [lang('PodcastImport.lock_import')]);
}
$converter = new HtmlConverter();
$channelDescriptionHtml = $feed->channel[0]->description;
try {
$podcast = new \App\Entities\Podcast([
'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost(
'imported_feed_url'
),
'new_feed_url' => base_url(
route_to('podcast_feed', $this->request->getPost('name'))
),
'title' => $feed->channel[0]->title,
'description_markdown' => $converter->convert(
$channelDescriptionHtml
),
'description_html' => $channelDescriptionHtml,
'image' => $nsItunes->image && !empty($nsItunes->image->attributes())
? download_file($nsItunes->image->attributes())
: ($feed->channel[0]->image && !empty($feed->channel[0]->image->url)
? download_file($feed->channel[0]->image->url)
: null),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' => empty($nsItunes->explicit)
? null
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
: (in_array($nsItunes->explicit, ['no', 'false'])
? 'clean'
: null)),
'owner_name' => $nsItunes->owner->name,
'owner_email' => $nsItunes->owner->email,
'publisher' => $nsItunes->author,
'type' => empty($nsItunes->type) ? 'episodic' : $nsItunes->type,
'copyright' => $feed->channel[0]->copyright,
'is_blocked' => empty($nsItunes->block)
? false
: $nsItunes->block === 'yes',
'is_completed' => empty($nsItunes->complete)
? false
: $nsItunes->complete === 'yes',
'created_by' => user(),
'updated_by' => user(),
]);
} catch (\ErrorException $ex) {
return redirect()
->back()
->withInput()
->with('errors', [
$ex->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$podcastModel = new PodcastModel();
$db = \Config\Database::connect();
$db->transStart();
if (!($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(
user()->id,
$newPodcastId,
$podcastAdminGroup->id
);
$platformModel = new PlatformModel();
$podcastsPlatformsData = [];
foreach ($nsPodcast->id as $podcastingPlatform) {
$slug = $podcastingPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'podcasting');
array_push($podcastsPlatformsData, [
'platform_slug' => $slug,
'podcast_id' => $newPodcastId,
'link_url' => $podcastingPlatform->attributes()['url'],
'link_content' => $podcastingPlatform->attributes()['id'],
'is_visible' => false,
]);
}
foreach ($nsPodcast->social as $socialPlatform) {
$slug = $socialPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'social');
array_push($podcastsPlatformsData, [
'platform_slug' => $socialPlatform->attributes()['platform'],
'podcast_id' => $newPodcastId,
'link_url' => $socialPlatform->attributes()['url'],
'link_content' => $socialPlatform,
'is_visible' => false,
]);
}
foreach ($nsPodcast->funding as $fundingPlatform) {
$slug = $fundingPlatform->attributes()['platform'];
$platformModel->getOrCreatePlatform($slug, 'funding');
array_push($podcastsPlatformsData, [
'platform_slug' => $fundingPlatform->attributes()['platform'],
'podcast_id' => $newPodcastId,
'link_url' => $fundingPlatform->attributes()['url'],
'link_content' => $fundingPlatform->attributes()['id'],
'is_visible' => false,
]);
}
if (count($podcastsPlatformsData) > 1) {
$platformModel->createPodcastPlatforms(
$newPodcastId,
$podcastsPlatformsData
);
}
$numberItems = $feed->channel[0]->item->count();
$lastItem =
!empty($this->request->getPost('max_episodes')) &&
$this->request->getPost('max_episodes') < $numberItems
? $this->request->getPost('max_episodes')
: $numberItems;
$slugs = [];
// For each Episode:
for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) {
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
$nsItunes = $item->children(
'http://www.itunes.com/dtds/podcast-1.0.dtd'
);
$slug = slugify(
$this->request->getPost('slug_field') === 'title'
? $item->title
: basename($item->link)
);
if (in_array($slug, $slugs)) {
$slugNumber = 2;
while (in_array($slug . '-' . $slugNumber, $slugs)) {
$slugNumber++;
}
$slug = $slug . '-' . $slugNumber;
}
$slugs[] = $slug;
$itemDescriptionHtml =
$this->request->getPost('description_field') === 'summary'
? $nsItunes->summary
: ($this->request->getPost('description_field') ===
'subtitle_summary'
? $nsItunes->subtitle . '<br/>' . $nsItunes->summary
: $item->description);
$newEpisode = new \App\Entities\Episode([
'podcast_id' => $newPodcastId,
'guid' => empty($item->guid) ? null : $item->guid,
'title' => $item->title,
'slug' => $slug,
'enclosure' => download_file($item->enclosure->attributes()),
'description_markdown' => $converter->convert(
$itemDescriptionHtml
),
'description_html' => $itemDescriptionHtml,
'image' =>
!$nsItunes->image || empty($nsItunes->image->attributes())
? null
: download_file($nsItunes->image->attributes()),
'parental_advisory' => empty($nsItunes->explicit)
? null
: (in_array($nsItunes->explicit, ['yes', 'true'])
? 'explicit'
: (in_array($nsItunes->explicit, ['no', 'false'])
? 'clean'
: null)),
'number' =>
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
: (!empty($nsItunes->episode)
? $nsItunes->episode
: null),
'season_number' => empty(
$this->request->getPost('season_number')
)
? (!empty($nsItunes->season)
? $nsItunes->season
: null)
: $this->request->getPost('season_number'),
'type' => empty($nsItunes->episodeType)
? 'full'
: $nsItunes->episodeType,
'is_blocked' => empty($nsItunes->block)
? false
: $nsItunes->block === 'yes',
'created_by' => user(),
'updated_by' => user(),
'published_at' => strtotime($item->pubDate),
]);
$episodeModel = new EpisodeModel();
if (!$episodeModel->insert($newEpisode)) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
}
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use Config\Services;
class PodcastPlatform extends BaseController
{
/**
* @var \App\Entities\Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (
!($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[0]);
return $this->$method(...$params);
}
public function index()
{
return view('admin/podcast/platforms/dashboard');
}
public function platforms($platformType)
{
helper('form');
$data = [
'podcast' => $this->podcast,
'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks(
$this->podcast->id,
$platformType
),
];
replace_breadcrumb_params([0 => $this->podcast->title]);
return view('admin/podcast/platforms', $data);
}
public function attemptPlatformsUpdate($platformType)
{
$platformModel = new PlatformModel();
$validation = Services::validation();
$podcastsPlatformsData = [];
foreach (
$this->request->getPost('platforms')
as $platformSlug => $podcastPlatform
) {
$podcastPlatformUrl = $podcastPlatform['url'];
if (
!empty($podcastPlatformUrl) &&
$validation->check($podcastPlatformUrl, 'validate_url')
) {
array_push($podcastsPlatformsData, [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'link_content' => $podcastPlatform['content'],
'is_visible' => array_key_exists(
'visible',
$podcastPlatform
)
? $podcastPlatform['visible'] == 'yes'
: false,
]);
}
}
$platformModel->savePodcastPlatforms(
$this->podcast->id,
$platformType,
$podcastsPlatformsData
);
return redirect()
->back()
->with('message', lang('Platforms.messages.updateSuccess'));
}
public function removePodcastPlatform($platformSlug)
{
(new PlatformModel())->removePodcastPlatform(
$this->podcast->id,
$platformSlug
);
return redirect()
->back()
->with('message', lang('Platforms.messages.removeLinkSuccess'));
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Authorization\GroupModel;
use App\Models\UserModel;
use Config\Services;
class User extends BaseController
{
/**
* @var \App\Entities\User|null
*/
protected $user;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->user = (new UserModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function list()
{
$data = ['users' => (new UserModel())->findAll()];
return view('admin/user/list', $data);
}
public function view()
{
$data = ['user' => $this->user];
replace_breadcrumb_params([0 => $this->user->username]);
return view('admin/user/view', $data);
}
public function create()
{
helper('form');
$data = [
'roles' => (new GroupModel())->getUserRoles(),
];
return view('admin/user/create', $data);
}
public function attemptCreate()
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules(['only' => ['username']]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
]
);
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new \App\Entities\User($this->request->getPost());
// Activate user
$user->activate();
// Force user to reset his password on first connection
$user->forcePasswordReset();
if (!$userModel->insert($user)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.createSuccess', [
'username' => $user->username,
])
);
}
public function edit()
{
helper('form');
$roles = (new GroupModel())->getUserRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->name] = lang('User.roles.' . $role->name);
return $result;
},
[]
);
$data = [
'user' => $this->user,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([0 => $this->user->username]);
return view('admin/user/edit', $data);
}
public function attemptEdit()
{
$authorize = Services::authorization();
$roles = $this->request->getPost('roles');
$authorize->setUserGroups($this->user->id, $roles);
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.rolesEditSuccess', [
'username' => $this->user->username,
])
);
}
public function forcePassReset()
{
$userModel = new UserModel();
$this->user->forcePasswordReset();
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.forcePassResetSuccess', [
'username' => $this->user->username,
])
);
}
public function ban()
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.banSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
$userModel = new UserModel();
// TODO: add ban reason?
$this->user->ban('');
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.banSuccess', [
'username' => $this->user->username,
])
);
}
public function unBan()
{
$userModel = new UserModel();
$this->user->unBan();
if (!$userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.unbanSuccess', [
'username' => $this->user->username,
])
);
}
public function delete()
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
(new UserModel())->delete($this->user->id);
return redirect()
->back()
->with(
'message',
lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
])
);
}
}
<?php
/**
* Class Analytics
* Creates Analytics controller
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use CodeIgniter\Controller;
class Analytics 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 array
*/
protected $helpers = ['analytics'];
/**
* Constructor.
*/
public function initController(
\CodeIgniter\HTTP\RequestInterface $request,
\CodeIgniter\HTTP\ResponseInterface $response,
\Psr\Log\LoggerInterface $logger
) {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
//--------------------------------------------------------------------
// Preload any models, libraries, etc, here.
//--------------------------------------------------------------------
// E.g.:
// $this->session = \Config\Services::session();
set_user_session_deny_list_ip();
set_user_session_location();
set_user_session_player();
}
// Add one hit to this episode:
public function hit($base64EpisodeData, ...$filename)
{
helper('media', 'analytics');
$serviceName = isset($_GET['_from']) ? $_GET['_from'] : '';
$episodeData = unpack(
'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate',
base64_url_decode($base64EpisodeData)
);
podcast_hit(
$episodeData['podcastId'],
$episodeData['episodeId'],
$episodeData['bytesThreshold'],
$episodeData['fileSize'],
$episodeData['duration'],
$episodeData['publicationDate'],
$serviceName
);
return redirect()->to(media_base_url($filename));
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\User;
class Auth extends \Myth\Auth\Controllers\AuthController
{
/**
* An array of helpers to be automatically loaded
* upon class instantiation.
*
* @var array
*/
protected $helpers = ['components'];
/**
* Attempt to register a new user.
*/
public function attemptRegister()
{
// Check if registration is allowed
if (!$this->config->allowRegistration) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.registerDisabled'));
}
$users = model('UserModel');
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'username' =>
'required|alpha_numeric_space|min_length[3]|is_unique[users.username]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
// Save the user
$allowedPostFields = array_merge(
['password'],
$this->config->validFields,
$this->config->personalFields
);
$user = new User($this->request->getPost($allowedPostFields));
$this->config->requireActivation !== false
? $user->generateActivateHash()
: $user->activate();
// Ensure default group gets assigned if set
if (!empty($this->config->defaultUserGroup)) {
$users = $users->withGroup($this->config->defaultUserGroup);
}
if (!$users->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
if ($this->config->requireActivation !== false) {
$activator = service('activator');
$sent = $activator->send($user);
if (!$sent) {
return redirect()
->back()
->withInput()
->with(
'error',
$activator->error() ?? lang('Auth.unknownError')
);
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.activationSuccess'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.registerSuccess'));
}
/**
* Verifies the code with the email and saves the new password,
* if they all pass validation.
*
* @return mixed
*/
public function attemptReset()
{
if ($this->config->activeResetter === false) {
return redirect()
->route('login')
->with('error', lang('Auth.forgotDisabled'));
}
$users = model('UserModel');
// First things first - log the reset attempt.
$users->logResetAttempt(
$this->request->getPost('email'),
$this->request->getPost('token'),
$this->request->getIPAddress(),
(string) $this->request->getUserAgent()
);
$rules = [
'token' => 'required',
'email' => 'required|valid_email',
'password' => 'required|strong_password',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
$user = $users
->where('email', $this->request->getPost('email'))
->where('reset_hash', $this->request->getPost('token'))
->first();
if (is_null($user)) {
return redirect()
->back()
->with('error', lang('Auth.forgotNoUser'));
}
// Reset token still valid?
if (
!empty($user->reset_expires) &&
time() > $user->reset_expires->getTimestamp()
) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.resetTokenExpired'));
}
// Success! Save the new password, and cleanup the reset hash.
$user->password = $this->request->getPost('password');
$user->reset_hash = null;
$user->reset_at = date('Y-m-d H:i:s');
$user->reset_expires = null;
$user->force_pass_reset = false;
$users->save($user);
return redirect()
->route('login')
->with('message', lang('Auth.resetSuccess'));
}
}
<?php <?php
/** declare(strict_types=1);
* Class BaseController
*
* 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
*
* For security be sure to declare any new methods as protected or private.
*
* @package CodeIgniter
*/
namespace App\Controllers; namespace App\Controllers;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Override;
use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
class BaseController extends Controller /**
* 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
*
* For security be sure to declare any new methods as protected or private.
*/
abstract class BaseController extends Controller
{ {
/** /**
* An array of helpers to be loaded automatically upon * An array of helpers to be loaded automatically upon
* class instantiation. These helpers will be available * class instantiation. These helpers will be available
* to all other controllers that extend BaseController. * to all other controllers that extend BaseController.
* *
* @var array * @var list<string>
*/ */
protected $helpers = ['analytics', 'svg', 'components', 'misc']; protected $helpers = [];
/** /**
* Constructor. * 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(
\CodeIgniter\HTTP\RequestInterface $request, RequestInterface $request,
\CodeIgniter\HTTP\ResponseInterface $response, ResponseInterface $response,
\Psr\Log\LoggerInterface $logger LoggerInterface $logger,
) { ): void {
$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);
//-------------------------------------------------------------------- Theme::setTheme('app');
// Preload any models, libraries, etc, here.
//--------------------------------------------------------------------
// E.g.:
// $this->session = \Config\Services::session();
set_user_session_deny_list_ip();
set_user_session_browser();
set_user_session_referer();
set_user_session_entry_page();
}
protected static function triggerWebpageHit($podcastId)
{
webpage_hit($podcastId);
} }
} }
<?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);
}
}
<?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\Page;
use App\Models\CreditModel;
use App\Models\PodcastModel;
class CreditsController extends BaseController
{
public function index(): string
{
$locale = service('request')
->getLocale();
$cacheName = implode(
'_',
array_filter(['page', 'credits', $locale, auth()->loggedIn() ? 'authenticated' : null]),
);
if (! ($found = cache($cacheName))) {
$page = new Page([
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'content_markdown' => '',
]);
$allPodcasts = (new PodcastModel())->findAll();
$allCredits = (new CreditModel())->findAll();
// Unlike the carpenter, we make a tree from a table:
$personGroup = null;
$personId = null;
$personRole = null;
$credits = [];
foreach ($allCredits as $credit) {
if ($personGroup !== $credit->person_group) {
$personGroup = $credit->person_group;
$personId = $credit->person_id;
$personRole = $credit->person_role;
$credits[$personGroup] = [
'group_label' => $credit->group_label,
'persons' => [
$personId => [
'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
'information_url' => $credit->person->information_url,
'roles' => [
$personRole => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' › '
: '') .
esc($credit->episode->title) .
episode_numbering(
$credit->episode
->number,
$credit->episode
->season_number,
'text-xs ml-2',
true,
)
: esc($credit->podcast->title),
],
],
],
],
],
],
];
} elseif ($personId !== $credit->person_id) {
$personId = $credit->person_id;
$personRole = $credit->person_role;
$credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'),
'information_url' => $credit->person->information_url,
'roles' => [
$personRole => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' › '
: '') .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode
->season_number,
'text-xs ml-2',
true,
)
: esc($credit->podcast->title),
],
],
],
],
];
} elseif ($personRole !== $credit->person_role) {
$personRole = $credit->person_role;
$credits[$personGroup]['persons'][$personId]['roles'][
$personRole
] = [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' › '
: '') .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: esc($credit->podcast->title),
],
],
];
} else {
$credits[$personGroup]['persons'][$personId]['roles'][
$personRole
]['is_in'][] = [
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' › '
: '') .
esc($credit->episode->title) .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: esc($credit->podcast->title),
];
}
}
set_page_metatags($page);
$data = [
'page' => $page,
'credits' => $credits,
];
$found = view('pages/credits', $data);
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
}
<?php
/**
* @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\EpisodeModel;
use App\Models\PodcastModel;
class Episode extends BaseController
{
/**
* @var \App\Entities\Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode|null
*/
protected $episode;
public function _remap($method, ...$params)
{
$this->podcast = (new PodcastModel())->getPodcastByName($params[0]);
if (
count($params) > 1 &&
!($this->episode = (new EpisodeModel())->getEpisodeBySlug(
$this->podcast->id,
$params[1]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return $this->$method();
}
public function index()
{
self::triggerWebpageHit($this->episode->podcast_id);
$locale = service('request')->getLocale();
$cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_{$locale}";
if (!($cachedView = cache($cacheName))) {
$episodeModel = new EpisodeModel();
$previousNextEpisodes = $episodeModel->getPreviousNextEpisodes(
$this->episode,
$this->podcast->type
);
$data = [
'previousEpisode' => $previousNextEpisodes['previous'],
'nextEpisode' => $previousNextEpisodes['next'],
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode(
$this->podcast->id
);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
}
<?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);
}
}
<?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\EpisodeComment;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class EpisodeCommentController extends BaseController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
protected Episode $episode;
protected EpisodeComment $comment;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 3) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $podcast->actor;
if (
! ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if (
! ($comment = (new EpisodeCommentModel())->getCommentById($params[2])) instanceof EpisodeComment
) {
throw PageNotFoundException::forPageNotFound();
}
$this->comment = $comment;
unset($params[2]);
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"episode#{$this->episode->id}",
"comment#{$this->comment->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_comment_metatags($this->comment);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'episode' => $this->episode,
'comment' => $this->comment,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('episode/comment', $data);
}
return view('episode/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function commentObject(): ResponseInterface
{
$commentObject = new CommentObject($this->comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function replies(): ResponseInterface
{
/**
* get comment replies
*/
$commentReplies = model(EpisodeCommentModel::class, false)
->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
if ($paginatedReplies !== null) {
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function likeAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
public function replyAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
}
<?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\Libraries\NoteObject;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
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
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Episode $episode;
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(): string
{
$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,
]);
}
return $cachedView;
}
public function activity(): string
{
$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 = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/activity', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function chapters(): string
{
$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);
}
// 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,
]);
}
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,
]);
}
return $cachedView;
}
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');
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,
'themeData' => $themeData,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function oembedJSON(): ResponseInterface
{
return $this->response->setJSON([
'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="' .
$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_width' => config('Images')
->podcastCoverSizes['og']['width'],
'thumbnail_height' => config('Images')
->podcastCoverSizes['og']['height'],
]);
}
public function oembedXML(): ResponseInterface
{
$oembed = new SimpleXMLElement("<?xml version='1.0' encoding='utf-8' standalone='yes'?><oembed></oembed>");
$oembed->addChild('type', 'rich');
$oembed->addChild('version', '1.0');
$oembed->addChild('title', $this->episode->title);
$oembed->addChild('provider_name', $this->podcast->title);
$oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->cover->og_url);
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['og']['width']);
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['og']['height']);
$oembed->addChild(
'html',
htmlspecialchars(
'<iframe src="' .
$this->episode->embed_url .
'" width="100%" height="' . config(
Embed::class,
)->height . '" frameborder="0" scrolling="no"></iframe>',
),
);
$oembed->addChild('width', (string) config('Embed')->width);
$oembed->addChild('height', (string) config('Embed')->height);
// @phpstan-ignore-next-line
return $this->response->setXML($oembed);
}
public function episodeObject(): ResponseInterface
{
$podcastObject = new PodcastEpisode($this->episode);
return $this->response
->setContentType('application/json')
->setBody($podcastObject->toJSON());
}
public function comments(): ResponseInterface
{
/**
* get comments: aggregated replies from posts referring to the episode
*/
$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');
if ($pageNumber < 1) {
$episodeComments->paginate(12);
$pager = $episodeComments->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedComments = $episodeComments->paginate(12, 'default', $pageNumber);
$pager = $episodeComments->pager;
$orderedItems = [];
if ($paginatedComments !== null) {
foreach ($paginatedComments as $comment) {
$orderedItems[] = (new NoteObject($comment))->toArray();
}
}
// @phpstan-ignore-next-line
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setHeader('Access-Control-Allow-Origin', '*')
->setBody($collection->toJSON());
}
}
<?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);
}
}
<?php
/**
* @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\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
class Feed extends Controller
{
public function index($podcastName)
{
helper('rss');
$podcast = (new PodcastModel())->where('name', $podcastName)->first();
if (!$podcast) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$serviceSlug = '';
try {
$service = \Opawg\UserAgentsPhp\UserAgentsRSS::find(
$_SERVER['HTTP_USER_AGENT']
);
if ($service) {
$serviceSlug = $service['slug'];
}
} catch (\Exception $e) {
// If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $e);
}
$cacheName =
"podcast{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
if (!($found = cache($cacheName))) {
$found = get_rss_feed($podcast, $serviceSlug);
// 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(
$podcast->id
);
cache()->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE
);
}
return $this->response->setXML($found);
}
}
<?php
declare(strict_types=1);
/**
* @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 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
{
$podcast = (new PodcastModel())->where('handle', $podcastHandle)
->first();
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(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());
}
$serviceSlug = '';
if ($service) {
$serviceSlug = $service['slug'];
}
$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, $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(
$podcast->id,
);
cache()
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $this->response->setXML($found);
}
}
<?php
/**
* @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\PodcastModel;
class Home extends BaseController
{
public function index()
{
$model = new PodcastModel();
$allPodcasts = $model->findAll();
// check if there's only one podcast to redirect user to it
if (count($allPodcasts) == 1) {
return redirect()->route('podcast', [$allPodcasts[0]->name]);
}
// default behavior: list all podcasts on home page
$data = ['podcasts' => $allPodcasts];
return view('home', $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\Models\PodcastModel;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Media\FileManagers\FileManagerInterface;
class HomeController extends BaseController
{
public function index(): RedirectResponse | string
{
$sortOptions = ['activity', 'created_desc', 'created_asc'];
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet(
'sort',
) : 'activity';
$allPodcasts = (new PodcastModel())->getAllPodcasts($sortBy);
// check if there's only one podcast to redirect user to it
if (count($allPodcasts) === 1) {
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
}
set_home_metatags();
// default behavior: list all podcasts on home page
$data = [
'podcasts' => $allPodcasts,
'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!',
]);
}
}
<?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\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface;
class MapController extends BaseController
{
public function index(): string
{
$cacheName = implode(
'_',
array_filter([
'page',
'map',
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
return view('pages/map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
public function getEpisodesMarkers(): ResponseInterface
{
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = (new EpisodeModel())
->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,
'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,
'podcast_title' => esc($episode->podcast->title),
'episode_title' => esc($episode->title),
];
}
// The page cache is set to a decade so it is deleted manually upon episode update
cache()
->save($cacheName, $found, DECADE);
}
return $this->response->setJSON($found);
}
}