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
Select Git revision
  • alpha
  • beta
  • develop
  • docs/fix-readme
  • docs/update-vitepress
  • draft/rss-feed
  • feat/dashboard
  • feat/episodes-page-ux
  • feat/generator-user-agent
  • feat/headliner
  • feat/new-languages
  • feat/plugins
  • fix/federation
  • fix/forms-ux
  • i18n
  • main
  • next
  • refactor/transcripts
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
118 results

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
Select Git revision
  • alpha
  • beta
  • develop
  • docs/update-vitepress
  • feat/dashboard
  • feat/op3
  • i18n
  • main
  • v1.0.0
  • v1.0.0-alpha.1
  • v1.0.0-alpha.10
  • v1.0.0-alpha.11
  • v1.0.0-alpha.12
  • v1.0.0-alpha.13
  • v1.0.0-alpha.14
  • v1.0.0-alpha.15
  • v1.0.0-alpha.16
  • v1.0.0-alpha.17
  • v1.0.0-alpha.18
  • v1.0.0-alpha.19
  • v1.0.0-alpha.2
  • v1.0.0-alpha.20
  • v1.0.0-alpha.21
  • v1.0.0-alpha.22
  • v1.0.0-alpha.23
  • v1.0.0-alpha.24
  • v1.0.0-alpha.25
  • v1.0.0-alpha.26
  • v1.0.0-alpha.27
  • v1.0.0-alpha.28
  • v1.0.0-alpha.29
  • v1.0.0-alpha.3
  • v1.0.0-alpha.30
  • v1.0.0-alpha.31
  • v1.0.0-alpha.32
  • v1.0.0-alpha.33
  • v1.0.0-alpha.34
  • v1.0.0-alpha.35
  • v1.0.0-alpha.36
  • v1.0.0-alpha.37
  • v1.0.0-alpha.38
  • v1.0.0-alpha.39
  • v1.0.0-alpha.4
  • v1.0.0-alpha.40
  • v1.0.0-alpha.41
  • v1.0.0-alpha.42
  • v1.0.0-alpha.43
  • v1.0.0-alpha.44
  • v1.0.0-alpha.45
  • v1.0.0-alpha.46
  • v1.0.0-alpha.47
  • v1.0.0-alpha.48
  • v1.0.0-alpha.49
  • v1.0.0-alpha.5
  • v1.0.0-alpha.50
  • v1.0.0-alpha.51
  • v1.0.0-alpha.52
  • v1.0.0-alpha.53
  • v1.0.0-alpha.54
  • v1.0.0-alpha.55
  • v1.0.0-alpha.56
  • v1.0.0-alpha.57
  • v1.0.0-alpha.58
  • v1.0.0-alpha.59
  • v1.0.0-alpha.6
  • v1.0.0-alpha.60
  • v1.0.0-alpha.61
  • v1.0.0-alpha.62
  • v1.0.0-alpha.63
  • v1.0.0-alpha.64
  • v1.0.0-alpha.65
  • v1.0.0-alpha.66
  • v1.0.0-alpha.67
  • v1.0.0-alpha.68
  • v1.0.0-alpha.69
  • v1.0.0-alpha.7
  • v1.0.0-alpha.70
  • v1.0.0-alpha.71
  • v1.0.0-alpha.72
  • v1.0.0-alpha.73
  • v1.0.0-alpha.74
  • v1.0.0-alpha.75
  • v1.0.0-alpha.76
  • v1.0.0-alpha.77
  • v1.0.0-alpha.78
  • v1.0.0-alpha.79
  • v1.0.0-alpha.8
  • v1.0.0-alpha.80
  • v1.0.0-alpha.9
  • v1.0.0-beta.1
  • v1.0.0-beta.10
  • v1.0.0-beta.11
  • v1.0.0-beta.12
  • v1.0.0-beta.13
  • v1.0.0-beta.14
  • v1.0.0-beta.15
  • v1.0.0-beta.16
  • v1.0.0-beta.17
  • v1.0.0-beta.18
  • v1.0.0-beta.19
  • v1.0.0-beta.2
  • v1.0.0-beta.20
  • v1.0.0-beta.21
  • v1.0.0-beta.22
  • v1.0.0-beta.23
  • v1.0.0-beta.24
  • v1.0.0-beta.3
  • v1.0.0-beta.4
108 results
Show changes
Showing
with 920 additions and 1886 deletions
<?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-activity', [
$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
/**
* @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\UserModel;
use CodeIgniter\Controller;
use Config\Services;
use Dotenv\Dotenv;
class Install extends Controller
{
protected $helpers = ['form', 'components', 'svg'];
/**
* 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);
}
/**
* Every operation goes through this method to handle
* the install logic.
*
* If all required actions have already been performed,
* the install route will show a 404 page.
*/
public function index()
{
try {
// Check if .env is created and has all required fields
$dotenv = Dotenv::createUnsafeImmutable(ROOTPATH);
$dotenv->load();
} catch (\Throwable $e) {
$this->createEnv();
}
// Check if the created .env file is writable to continue install process
if (is_really_writable(ROOTPATH . '.env')) {
try {
$dotenv->required([
'app.baseURL',
'app.adminGateway',
'app.authGateway',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
// form to input instance configuration
return $this->instanceConfig();
}
try {
$dotenv->required([
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
return $this->databaseConfig();
}
try {
$dotenv->required('cache.handler');
} catch (\Dotenv\Exception\ValidationException $e) {
return $this->cacheConfig();
}
} else {
try {
$dotenv->required([
'app.baseURL',
'app.adminGateway',
'app.authGateway',
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
'cache.handler',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
return view('install/manual_config');
}
}
try {
$db = db_connect();
// Check if superadmin has been created, meaning migrations and seeds have passed
if (
$db->tableExists('users') &&
(new UserModel())->countAll() > 0
) {
// if so, show a 404 page
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
// Could not connect to the database
// show database config view to fix value
session()->setFlashdata(
'error',
lang('Install.messages.databaseConnectError'),
);
return view('install/database_config');
}
// migrate if no user has been created
$this->migrate();
// Check if all seeds have succeeded
$this->seed();
return $this->createSuperAdmin();
}
/**
* Returns the form to generate the .env config file for the instance.
*/
public function createEnv()
{
// create empty .env file
try {
$envFile = fopen(ROOTPATH . '.env', 'w');
fclose($envFile);
} catch (\Throwable $e) {
// Could not create the .env file, redirect to a view with manual instructions on how to add it
return view('install/manual_config');
}
}
public function instanceConfig()
{
return view('install/instance_config');
}
public function attemptInstanceConfig()
{
$rules = [
'hostname' => 'required|validate_url',
'media_base_url' => 'permit_empty|validate_url',
'admin_gateway' => 'required',
'auth_gateway' => 'required|differs[admin_gateway]',
];
if (!$this->validate($rules)) {
return redirect()
->to(
(empty(host_url()) ? config('App')->baseURL : host_url()) .
config('App')->installGateway,
)
->withInput()
->with('errors', $this->validator->getErrors());
}
$baseUrl = $this->request->getPost('hostname');
$mediaBaseUrl = $this->request->getPost('media_base_url');
self::writeEnv([
'app.baseURL' => $baseUrl,
'app.mediaBaseURL' => empty($mediaBaseUrl)
? $baseUrl
: $mediaBaseUrl,
'app.adminGateway' => $this->request->getPost('admin_gateway'),
'app.authGateway' => $this->request->getPost('auth_gateway'),
]);
helper('text');
// redirect to full install url with new baseUrl input
return redirect(0)->to(
reduce_double_slashes(
$baseUrl . '/' . config('App')->installGateway,
),
);
}
public function databaseConfig()
{
return view('install/database_config');
}
public function attemptDatabaseConfig()
{
$rules = [
'db_hostname' => 'required',
'db_name' => 'required',
'db_username' => 'required',
'db_password' => 'required',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'database.default.hostname' => $this->request->getPost(
'db_hostname',
),
'database.default.database' => $this->request->getPost('db_name'),
'database.default.username' => $this->request->getPost(
'db_username',
),
'database.default.password' => $this->request->getPost(
'db_password',
),
'database.default.DBPrefix' => $this->request->getPost('db_prefix'),
]);
return redirect()->back();
}
public function cacheConfig()
{
return view('install/cache_config');
}
public function attemptCacheConfig()
{
$rules = [
'cache_handler' => 'required',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'cache.handler' => $this->request->getPost('cache_handler'),
]);
return redirect()->back();
}
/**
* Runs all database migrations required for instance.
*/
public function migrate()
{
$migrations = \Config\Services::migrations();
!$migrations->setNamespace('Myth\Auth')->latest();
!$migrations->setNamespace('ActivityPub')->latest();
!$migrations->setNamespace('Analytics')->latest();
!$migrations->setNamespace(APP_NAMESPACE)->latest();
}
/**
* Runs all database seeds required for instance.
*/
public function seed()
{
$seeder = \Config\Database::seeder();
// Seed database
$seeder->call('AppSeeder');
}
/**
* Returns the form to create a the first superadmin user for the instance.
*/
public function createSuperAdmin()
{
return view('install/create_superadmin');
}
/**
* Creates the first superadmin user or redirects back to form if any error.
*
* After creation, user is redirected to login page to input its credentials.
*/
public function attemptCreateSuperAdmin()
{
$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();
$db = \Config\Database::connect();
$db->transStart();
if (!($userId = $userModel->insert($user, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// add newly created user to superadmin group
$authorization = Services::authorization();
$authorization->addUserToGroup($userId, 'superadmin');
$db->transComplete();
// Success!
// set redirect_url session as admin area to go to after login
session()->set('redirect_url', route_to('admin'));
return redirect()
->route('login')
->with('message', lang('Install.messages.createSuperAdminSuccess'));
}
/**
* writes config values in .env file
* overwrites any existing key and appends new ones
*
* @param array $data key/value config pairs
*
* @return void
*/
public static function writeEnv($configData)
{
$envData = file(ROOTPATH . '.env'); // reads an array of lines
foreach ($configData as $key => $value) {
$replaced = false;
$keyVal = $key . '="' . $value . '"' . PHP_EOL;
$envData = array_map(function ($line) use (
$key,
$keyVal,
&$replaced
) {
if (strpos($line, $key) === 0) {
$replaced = true;
return $keyVal;
}
return $line;
},
$envData);
if (!$replaced) {
array_push($envData, $keyVal);
}
}
file_put_contents(ROOTPATH . '.env', implode('', $envData));
}
}
<?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);
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
class Note extends \ActivityPub\Controllers\NoteController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast
*/
protected $podcast;
protected $helpers = ['auth', 'activitypub', 'svg', 'components', 'misc'];
public function _remap($method, ...$params)
{
if (
!($this->podcast = (new PodcastModel())->getPodcastByName(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$this->actor = $this->podcast->actor;
if (count($params) > 1) {
if (!($this->note = model('NoteModel')->getNoteById($params[1]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
unset($params[0]);
unset($params[1]);
return $this->$method(...$params);
}
public function index()
{
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"note#{$this->note->id}",
service('request')->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($cachedView = cache($cacheName))) {
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'persons' => $persons,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/note_authenticated', $data);
} else {
return view('podcast/note', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
}
return $cachedView;
}
public function attemptCreate()
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url|permit_empty',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$message = $this->request->getPost('message');
$newNote = new \App\Entities\Note([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $this->request->getPost('episode_url');
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri)))
) {
if (
$episode = (new EpisodeModel())->getEpisodeBySlug(
$params['podcastName'],
$params['episodeSlug'],
)
) {
$newNote->episode_id = $episode->id;
}
}
$newNote->message = $message;
if (
!model('NoteModel')->addNote(
$newNote,
$newNote->episode_id ? false : true,
true,
)
) {
return redirect()
->back()
->withInput()
->with('errors', model('NoteModel')->errors());
}
// Note has been successfully created
return redirect()->back();
}
public function attemptReply()
{
$rules = [
'message' => 'required|max_length[500]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$newNote = new \ActivityPub\Entities\Note([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->note->id,
'message' => $this->request->getPost('message'),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
if (!model('NoteModel')->addReply($newNote)) {
return redirect()
->back()
->withInput()
->with('errors', model('NoteModel')->errors());
}
// Reply note without preview card has been successfully created
return redirect()->back();
}
public function attemptFavourite()
{
model('FavouriteModel')->toggleFavourite(
interact_as_actor(),
$this->note,
);
return redirect()->back();
}
public function attemptReblog()
{
model('NoteModel')->toggleReblog(interact_as_actor(), $this->note);
return redirect()->back();
}
public function attemptAction()
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (!$this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
switch ($this->request->getPost('action')) {
case 'favourite':
return $this->attemptFavourite();
case 'reblog':
return $this->attemptReblog();
case 'reply':
return $this->attemptReply();
}
}
public function remoteAction($action)
{
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"note#{$this->note->id}",
"remote_{$action}",
service('request')->getLocale(),
]),
);
if (!($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'note' => $this->note,
'action' => $action,
];
helper('form');
return view('podcast/note_remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
}
<?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\PageModel;
use App\Models\CreditModel;
use App\Models\PodcastModel;
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())
->where('slug', $params[0])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
}
return $this->$method();
}
public function index()
{
$cacheName = "page@{$this->page->slug}";
if (!($found = cache($cacheName))) {
$data = [
'page' => $this->page,
];
$found = view('page', $data);
// The page cache is set to a decade so it is deleted manually upon page update
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
public function credits()
{
$locale = service('request')->getLocale();
$allPodcasts = (new PodcastModel())->findAll();
$cacheName = "page_credits_{$locale}";
if (!($found = cache($cacheName))) {
$page = new \App\Entities\Page([
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'content' => '',
]);
$allCredits = (new CreditModel())->findAll();
// Unlike the carpenter, we make a tree from a table:
$person_group = null;
$person_id = null;
$person_role = null;
$credits = [];
foreach ($allCredits as $credit) {
if ($person_group !== $credit->person_group) {
$person_group = $credit->person_group;
$person_id = $credit->person_id;
$person_role = $credit->person_role;
$credits[$person_group] = [
'group_label' => $credit->group_label,
'persons' => [
$person_id => [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' =>
$credit->person->information_url,
'roles' => [
$person_role => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} ▸ "
: '') .
$credit->episode
->title .
episode_numbering(
$credit->episode
->number,
$credit->episode
->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
],
],
],
],
],
],
];
} elseif ($person_id !== $credit->person_id) {
$person_id = $credit->person_id;
$person_role = $credit->person_role;
$credits[$person_group]['persons'][$person_id] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' => $credit->person->information_url,
'roles' => [
$person_role => [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title} ▸ "
: '') .
$credit->episode->title .
episode_numbering(
$credit->episode->number,
$credit->episode
->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
],
],
],
],
];
} elseif ($person_role !== $credit->person_role) {
$person_role = $credit->person_role;
$credits[$person_group]['persons'][$person_id]['roles'][
$person_role
] = [
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title} ▸ "
: '') .
$credit->episode->title .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
],
],
];
} else {
$credits[$person_group]['persons'][$person_id]['roles'][
$person_role
]['is_in'][] = [
'link' => $credit->episode
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
? (count($allPodcasts) > 1
? "{$credit->podcast->title} ▸ "
: '') .
$credit->episode->title .
episode_numbering(
$credit->episode->number,
$credit->episode->season_number,
'text-xs ml-2',
true,
)
: $credit->podcast->title,
];
}
}
$data = [
'page' => $page,
'credits' => $credits,
];
$found = view('credits', $data);
cache()->save($cacheName, $found, DECADE);
}
return $found;
}
}
<?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\PageModel;
use CodeIgniter\Exceptions\PageNotFoundException;
class PageController extends BaseController
{
protected Page $page;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
$page = (new PageModel())->where('slug', $params[0])->first();
if (! $page instanceof Page) {
throw PageNotFoundException::forPageNotFound();
}
$this->page = $page;
return $this->{$method}();
}
public function index(): string
{
$cacheName = implode(
'_',
array_filter([
'page',
$this->page->slug,
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
set_page_metatags($this->page);
$data = [
'page' => $this->page,
];
$found = view('pages/page', $data);
// The page cache is set to a decade so it is deleted manually upon page update
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 CodeIgniter\Controller;
/*
* Provide public access to all platforms so that they can be exported
*/
class Platform extends Controller
{
public function index()
{
$model = new \App\Models\PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\NoteModel;
class Podcast extends BaseController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastByName(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[0]);
}
return $this->$method(...$params);
}
public function activity()
{
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'activity',
service('request')->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($cachedView = cache($cacheName))) {
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'notes' => (new NoteModel())->getActorPublishedNotes(
$this->podcast->actor_id,
),
'persons' => $persons,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/activity_authenticated', $data);
} else {
return view('podcast/activity', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
}
return $cachedView;
}
public function episodes()
{
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season');
if (!$yearQuery and !$seasonQuery) {
$defaultQuery = (new PodcastModel())->getDefaultQuery(
$this->podcast->id,
);
if ($defaultQuery) {
if ($defaultQuery['type'] == 'season') {
$seasonQuery = $defaultQuery['data']['season_number'];
} elseif ($defaultQuery['type'] == 'year') {
$yearQuery = $defaultQuery['data']['year'];
}
}
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'episodes',
$yearQuery ? 'year' . $yearQuery : null,
$seasonQuery ? 'season' . $seasonQuery : null,
service('request')->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (!($cachedView = cache($cacheName))) {
// Build navigation array
$podcastModel = new PodcastModel();
$years = $podcastModel->getYears($this->podcast->id);
$seasons = $podcastModel->getSeasons($this->podcast->id);
$episodesNavigation = [];
$activeQuery = null;
foreach ($years as $year) {
$isActive = $yearQuery == $year['year'];
if ($isActive) {
$activeQuery = [
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
];
}
array_push($episodesNavigation, [
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'?year=' .
$year['year'],
'is_active' => $isActive,
]);
}
foreach ($seasons as $season) {
$isActive = $seasonQuery == $season['season_number'];
if ($isActive) {
$activeQuery = [
'type' => 'season',
'value' => $season['season_number'],
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
];
}
array_push($episodesNavigation, [
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'?season=' .
$season['season_number'],
'is_active' => $isActive,
]);
}
helper('persons');
$persons = [];
construct_person_array($this->podcast->persons, $persons);
$data = [
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id,
$this->podcast->type,
$yearQuery,
$seasonQuery,
),
'persons' => $persons,
];
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$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);
} else {
return view('podcast/episodes', $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\Podcast;
use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class PodcastController extends BaseController
{
use AnalyticsTrait;
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if ($params === []) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
unset($params[0]);
return $this->{$method}(...$params);
}
public function podcastActor(): ResponseInterface
{
$podcastActor = new PodcastActor($this->podcast);
return $this->response
->setContentType('application/activity+json')
->setBody($podcastActor->toJSON());
}
public function activity(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'activity',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_podcast_metatags($this->podcast, 'activity');
$data = [
'podcast' => $this->podcast,
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('podcast/activity', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function about(): string
{
$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');
$seasonQuery = $this->request->getGet('season');
if (! $yearQuery && ! $seasonQuery) {
$defaultQuery = (new PodcastModel())->getDefaultQuery($this->podcast->id);
if ($defaultQuery) {
if ($defaultQuery['type'] === 'season') {
$seasonQuery = $defaultQuery['data']['season_number'];
} elseif ($defaultQuery['type'] === 'year') {
$yearQuery = $defaultQuery['data']['year'];
}
}
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'episodes',
$yearQuery ? 'year' . $yearQuery : null,
$seasonQuery ? 'season' . $seasonQuery : null,
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
// Build navigation array
$podcastModel = new PodcastModel();
$years = $podcastModel->getYears($this->podcast->id);
$seasons = $podcastModel->getSeasons($this->podcast->id);
$episodesNavigation = [];
$activeQuery = null;
foreach ($years as $year) {
$isActive = $yearQuery === $year['year'];
if ($isActive) {
$activeQuery = [
'type' => 'year',
'value' => $year['year'],
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
];
}
$episodesNavigation[] = [
'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'],
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?year=' .
$year['year'],
'is_active' => $isActive,
];
}
foreach ($seasons as $season) {
$isActive = $seasonQuery === $season['season_number'];
if ($isActive) {
$activeQuery = [
'type' => 'season',
'value' => $season['season_number'],
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
];
}
$episodesNavigation[] = [
'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'],
]),
'number_of_episodes' => $season['number_of_episodes'],
'route' => route_to('podcast-episodes', $this->podcast->handle) .
'?season=' .
$season['season_number'],
'is_active' => $isActive,
];
}
set_podcast_metatags($this->podcast, 'episodes');
$data = [
'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery,
'episodes' => (new EpisodeModel())->getPodcastEpisodes(
$this->podcast->id,
$this->podcast->type,
$yearQuery,
$seasonQuery,
),
];
if (auth()->loggedIn()) {
return view('podcast/episodes', $data);
}
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
$this->podcast->id,
);
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function episodeCollection(): ResponseInterface
{
if ($this->podcast->type === 'serial') {
// podcast is serial
$episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('season_number DESC, number ASC');
} else {
$episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'DESC');
}
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$episodes->paginate(12);
$pager = $episodes->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedEpisodes = $episodes->paginate(12, 'default', $pageNumber);
$pager = $episodes->pager;
$orderedItems = [];
if ($paginatedEpisodes !== null) {
foreach ($paginatedEpisodes as $episode) {
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
}
}
// @phpstan-ignore-next-line
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function links(): string
{
set_podcast_metatags($this->podcast, 'links');
return view('podcast/links', [
'podcast' => $this->podcast,
]);
}
}
<?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\Actor;
use App\Entities\Podcast;
use App\Entities\Post as CastopodPost;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Override;
class PostController extends FediversePostController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
/**
* @var CastopodPost
*/
protected $post;
/**
* @var list<string>
*/
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
#[Override]
public function _remap(string $method, string ...$params): mixed
{
if (
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (count($params) <= 1) {
unset($params[0]);
return $this->{$method}(...$params);
}
if (
! ($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
) {
throw PageNotFoundException::forPageNotFound();
}
$this->post = $post;
unset($params[0]);
unset($params[1]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"post#{$this->post->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_post_metatags($this->post);
$data = [
'post' => $this->post,
'podcast' => $this->podcast,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('post/post', $data);
}
return view('post/post', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
#[Override]
public function createAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url_strict|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$message = $validData['message'];
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $validData['episode_url'];
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
) {
$newPost->episode_id = $episode->id;
}
$newPost->message = $message;
$postModel = new PostModel();
if (
! $postModel
->addPost($newPost, ! (bool) $newPost->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Post has been successfully created
return redirect()->back();
}
#[Override]
public function replyAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $validData['message'],
'published_at' => Time::now(),
'created_by' => user_id(),
]);
if ($this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
$postModel = new PostModel();
if (! $postModel->addReply($newPost)) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Reply post without preview card has been successfully created
return redirect()->back();
}
#[Override]
public function favouriteAction(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back();
}
#[Override]
public function reblogAction(): RedirectResponse
{
(new PostModel())->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back();
}
public function action(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$action = $validData['action'];
return match ($action) {
'favourite' => $this->favouriteAction(),
'reblog' => $this->reblogAction(),
'reply' => $this->replyAction(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteActionView(string $action): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
set_remote_actions_metatags($this->post, $action);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
helper('form');
// 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);
}
}
<?php
/**
* Class AddCategories
* Creates categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCategories extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
],
'code' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('code');
$this->forge->addForeignKey('parent_id', 'categories', 'id');
$this->forge->createTable('categories');
}
public function down()
{
$this->forge->dropTable('categories');
}
}
<?php
/**
* Class AddLanguages
* Creates languages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddLanguages extends Migration
{
public function up()
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
$this->forge->addPrimaryKey('code');
$this->forge->createTable('languages');
}
public function down()
{
$this->forge->dropTable('languages');
}
}
<?php
/**
* Class AddPodcasts
* Creates podcasts table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcasts extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osmid' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('name');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey(
'actor_id',
'activitypub_actors',
'id',
false,
'CASCADE',
);
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('podcasts');
}
public function down()
{
$this->forge->dropTable('podcasts');
}
}
<?php
/**
* Class AddEpisodes
* Creates episodes table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodes extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
],
'audio_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_duration' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'Playtime in seconds',
],
'audio_file_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'File size in bytes',
],
'audio_file_header_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'Header size in bytes',
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
],
'transcript_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'transcript_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'chapters_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'chapters_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osmid' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'favourites_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'reblogs_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'notes_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
$this->forge->addForeignKey(
'podcast_id',
'podcasts',
'id',
false,
'CASCADE',
);
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
}
public function down()
{
$this->forge->dropTable('episodes');
}
}
<?php
/**
* Class AddSoundbites
* Creates soundbites table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddSoundbites extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'start_time' => [
'type' => 'FLOAT',
],
'duration' => [
'type' => 'FLOAT',
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']);
$this->forge->addForeignKey(
'podcast_id',
'podcasts',
'id',
false,
'CASCADE',
);
$this->forge->addForeignKey(
'episode_id',
'episodes',
'id',
false,
'CASCADE',
);
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('soundbites');
}
public function down()
{
$this->forge->dropTable('soundbites');
}
}
<?php
/**
* Class AddPlatforms
* Creates platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPlatforms extends Migration
{
public function up()
{
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'default' => null,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()'
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
public function down()
{
$this->forge->dropTable('platforms');
}
}
<?php
/**
* Class AddPodcastUsers
* Creates podcast_users table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsUsers extends Migration
{
public function up()
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'user_id' => [
'type' => 'INT',
'unsigned' => true,
],
'group_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id', false, 'CASCADE');
$this->forge->addForeignKey(
'podcast_id',
'podcasts',
'id',
false,
'CASCADE',
);
$this->forge->addForeignKey(
'group_id',
'auth_groups',
'id',
false,
'CASCADE',
);
$this->forge->createTable('podcasts_users');
}
public function down()
{
$this->forge->dropTable('podcasts_users');
}
}
<?php
/**
* Class AddPages
* Creates pages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPages extends Migration
{
public function up()
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
'unique' => true,
],
'content' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
public function down()
{
$this->forge->dropTable('pages');
}
}