Compare commits

..

4 Commits

Author SHA1 Message Date
Yassine Doghri edbee96db5 chore: change rector and ecs configs + update devcontainer to php 8.2 2024-04-24 17:33:02 +00:00
Yassine Doghri f9a939471d chore: update codeigniter to 4.5.1 + other dependencies to latest 2024-04-24 15:42:17 +00:00
Yassine Doghri 303a900f66 refactor(platforms): move platforms data in code instead of database
refs #457
2024-04-24 14:47:05 +00:00
Guy Martin (Dwev) 57e459e187 feat: support podcast:txt tag with verify use case
closes #468
2024-04-24 10:03:20 +00:00
80 changed files with 9003 additions and 7693 deletions

View File

@ -4,7 +4,7 @@
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
FROM php:8.1-fpm
FROM php:8.2-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"

View File

@ -83,7 +83,7 @@ class App extends BaseConfig
* DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
*
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-';
public string $permittedURIChars = 'a-z 0-9~%.:_\-@';
/**
* --------------------------------------------------------------------------

View File

@ -49,6 +49,7 @@ class Autoload extends AutoloadConfig
'Modules\Install' => ROOTPATH . 'modules/Install/',
'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
'Modules\Update' => ROOTPATH . 'modules/Update/',
@ -61,7 +62,6 @@ class Autoload extends AutoloadConfig
/**
* -------------------------------------------------------------------
* Class Map
* -------------------------------------------------------------------
* The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;

View File

@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace Config;
/**

View File

@ -15,7 +15,6 @@ use CodeIgniter\Router\RouteCollection;
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder(

View File

@ -29,6 +29,7 @@ class Routing extends BaseRouting
ROOTPATH . 'modules/Auth/Config/Routes.php',
ROOTPATH . 'modules/Fediverse/Config/Routes.php',
ROOTPATH . 'modules/Install/Config/Routes.php',
ROOTPATH . 'modules/Platforms/Config/Routes.php',
ROOTPATH . 'modules/PodcastImport/Config/Routes.php',
ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php',
];

View File

@ -37,8 +37,8 @@ class Services extends BaseService
return static::getSharedInstance('router', $routes, $request);
}
$routes = $routes ?? static::routes();
$request = $request ?? static::request();
$routes ??= static::routes();
$request ??= static::request();
return new Router($routes, $request);
}
@ -53,7 +53,7 @@ class Services extends BaseService
return static::getSharedInstance('negotiator', $request);
}
$request = $request ?? static::request();
$request ??= static::request();
return new Negotiate($request);
}

View File

@ -13,8 +13,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* 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
*

View File

@ -106,9 +106,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -157,9 +155,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -218,9 +214,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -284,9 +278,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -339,9 +331,7 @@ class EpisodeController extends BaseController
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -420,11 +410,9 @@ class EpisodeController extends BaseController
* get comments: aggregated replies from posts referring to the episode
*/
$episodeComments = model(PostModel::class)
->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
return $builder->select('id')
->from('fediverse_posts')
->where('episode_id', $this->episode->id);
})
->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');

View File

@ -79,13 +79,7 @@ class FeedController extends Controller
);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $this->response->setXML($found);

View File

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
/*
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{
public function index(): ResponseInterface
{
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}

View File

@ -96,9 +96,7 @@ class PodcastController extends BaseController
);
return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -148,9 +146,7 @@ class PodcastController extends BaseController
);
return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
@ -270,9 +266,7 @@ class PodcastController extends BaseController
$this->podcast->id,
);
return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}

View File

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsVerifyTxtField adds 1 field to podcast table in database to support podcast:txt tag
*
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
class AddPodcastsVerifyTxtField extends BaseMigration
{
public function up(): void
{
$fields = [
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
];
$this->forge->addColumn('podcasts', $fields);
}
public function down(): void
{
$this->forge->dropColumn('podcasts', 'verify_txt');
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class RefactorPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
'after' => 'podcast_id',
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE', 'platforms_podcast_id_foreign');
$this->forge->addUniqueKey(['podcast_id', 'type', 'slug']);
$this->forge->createTable('platforms_temp');
$platformsData = $this->db->table('podcasts_platforms')
->select('podcasts_platforms.*, type')
->join('platforms', 'platforms.slug = podcasts_platforms.platform_slug')
->get()
->getResultArray();
$data = [];
foreach ($platformsData as $platformData) {
$data[] = [
'podcast_id' => $platformData['podcast_id'],
'type' => $platformData['type'],
'slug' => $platformData['platform_slug'],
'link_url' => $platformData['link_url'],
'account_id' => $platformData['account_id'],
'is_visible' => $platformData['is_visible'],
];
}
if ($data !== []) {
$this->db->table('platforms_temp')
->insertBatch($data);
}
$this->forge->dropTable('platforms');
$this->forge->dropTable('podcasts_platforms');
$this->forge->renameTable('platforms_temp', 'platforms');
}
public function down(): void
{
// delete platforms
$this->forge->dropTable('platforms');
// recreate platforms and podcasts_platforms tables
$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,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()'
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug', 'CASCADE');
$this->forge->createTable('podcasts_platforms');
}
}

View File

@ -20,6 +20,5 @@ class AppSeeder extends Seeder
{
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('PlatformSeeder');
}
}

View File

@ -20,7 +20,6 @@ class DevSeeder extends Seeder
{
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('PlatformSeeder');
$this->call('DevSuperadminSeeder');
}
}

View File

@ -77,34 +77,32 @@ class FakePodcastsAnalyticsSeeder extends Seeder
for (
$lineNumber = 0;
$lineNumber < rand(1, (int) $probability1);
$lineNumber < random_int(1, (int) $probability1);
++$lineNumber
) {
$probability2 = floor(exp(6 - $age / 20)) + 10;
$player =
$jsonUserAgents[
rand(1, count($jsonUserAgents) - 1)
random_int(1, count($jsonUserAgents) - 1)
];
$service =
$jsonRSSUserAgents[
rand(1, count($jsonRSSUserAgents) - 1)
random_int(1, count($jsonRSSUserAgents) - 1)
]['slug'];
$app = isset($player['app']) ? $player['app'] : '';
$device = isset($player['device'])
? $player['device']
: '';
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
$app = $player['app'] ?? '';
$device = $player['device'] ?? '';
$os = $player['os'] ?? '';
$isBot = $player['bot'] ?? 0;
$fakeIp =
rand(0, 255) .
random_int(0, 255) .
'.' .
rand(0, 255) .
random_int(0, 255) .
'.' .
rand(0, 255) .
random_int(0, 255) .
'.' .
rand(0, 255);
random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
@ -115,9 +113,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
try {
$city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null
? 'N/A'
: $city->country->isoCode;
$countryCode = $city->country->isoCode ?? 'N/A';
$regionCode = $city->subdivisions === []
? 'N/A'
@ -128,20 +124,20 @@ class FakePodcastsAnalyticsSeeder extends Seeder
//Bad luck, bad IP, nothing to do.
}
$hits = rand(0, (int) $probability2);
$hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600),
'bandwidth' => rand(1000000, 10000000),
'duration' => random_int(60, 3600),
'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits,
'unique_listeners' => $hits,
];
$analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'hour' => rand(0, 23),
'hour' => random_int(0, 23),
'hits' => $hits,
];
$analyticsPodcastsByCountry[] = [

View File

@ -216,23 +216,23 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
for (
$lineNumber = 0;
$lineNumber < rand(1, $probability1);
$lineNumber < random_int(1, $probability1);
++$lineNumber
) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$domain =
$this->domains[rand(0, count($this->domains) - 1)];
$this->domains[random_int(0, count($this->domains) - 1)];
$keyword =
$this->keywords[
rand(0, count($this->keywords) - 1)
random_int(0, count($this->keywords) - 1)
];
$browser =
$this->browsers[
rand(0, count($this->browsers) - 1)
random_int(0, count($this->browsers) - 1)
];
$hits = rand(0, $probability2);
$hits = random_int(0, $probability2);
$websiteByBrowser[] = [
'podcast_id' => $podcast->id,

View File

@ -483,7 +483,7 @@ class Episode extends Entity
public function setGuid(?string $guid = null): static
{
$this->attributes['guid'] = $guid === null ? $this->getLink() : $guid;
$this->attributes['guid'] = $guid ?? $this->getLink();
return $this;
}

View File

@ -15,7 +15,6 @@ use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
@ -32,6 +31,8 @@ use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException;
@ -528,7 +529,7 @@ class Podcast extends Entity
}
if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting');
$this->podcasting_platforms = (new PlatformModel())->getPlatforms($this->id, 'podcasting');
}
return $this->podcasting_platforms;
@ -546,7 +547,7 @@ class Podcast extends Entity
}
if ($this->social_platforms === null) {
$this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social');
$this->social_platforms = (new PlatformModel())->getPlatforms($this->id, 'social');
}
return $this->social_platforms;
@ -564,7 +565,7 @@ class Podcast extends Entity
}
if ($this->funding_platforms === null) {
$this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding');
$this->funding_platforms = (new PlatformModel())->getPlatforms($this->id, 'funding');
}
return $this->funding_platforms;

View File

@ -42,24 +42,16 @@ if (! function_exists('write_audio_file_tags')) {
// populate data array
$TagData = [
'title' => [esc($episode->title)],
'artist' => [
$episode->podcast->publisher === null
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'title' => [esc($episode->title)],
'artist' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
'album' => [esc($episode->podcast->title)],
'year' => [$episode->published_at instanceof Time ? $episode->published_at->format('Y') : ''],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [(string) $episode->number],
'copyright_message' => [$episode->podcast->copyright],
'publisher' => [
$episode->podcast->publisher === null
? esc($episode->podcast->owner_name)
: $episode->podcast->publisher,
],
'encoded_by' => ['Castopod'],
'publisher' => [$episode->podcast->publisher ?? esc($episode->podcast->owner_name)],
'encoded_by' => ['Castopod'],
// TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it
// 'website' => [$podcast_url],

View File

@ -164,7 +164,7 @@ if (! function_exists('parse_size')) {
$size = (float) preg_replace('~[^0-9\.]~', '', $size); // Remove the non-numeric characters from the size.
if ($unit !== '') {
// Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
return round($size * pow(1024, (float) stripos('bkmgtpezy', $unit[0])));
return round($size * 1024 ** ((float) stripos('bkmgtpezy', $unit[0])));
}
return round($size);
@ -183,7 +183,7 @@ if (! function_exists('format_bytes')) {
$pow = floor(($bytes ? log($bytes) : 0) / log($is_binary ? 1024 : 1000));
$pow = min($pow, count($units) - 1);
$bytes /= pow($is_binary ? 1024 : 1000, $pow);
$bytes /= ($is_binary ? 1024 : 1000) ** $pow;
return round($bytes, $precision) . $units[$pow];
}

View File

@ -121,6 +121,12 @@ if (! function_exists('get_rss_feed')) {
->addAttribute('owner', $podcast->owner_email);
}
if ($podcast->verify_txt !== null) {
$channel
->addChild('txt', $podcast->verify_txt, $podcastNamespace)
->addAttribute('purpose', 'verify');
}
if ($podcast->imported_feed_url !== null) {
$channel->addChild('previousUrl', $podcast->imported_feed_url, $podcastNamespace);
}
@ -254,12 +260,7 @@ if (! function_exists('get_rss_feed')) {
$itunesNamespace,
);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunesNamespace,
false
);
$channel->addChild('author', $podcast->publisher ?: $podcast->owner_name, $itunesNamespace, false);
$channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunesNamespace);
@ -348,7 +349,7 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('episodeType', $episode->type, $itunesNamespace);
// If episode is of type trailer, add podcast:trailer tag on channel level
if ($episode->type == 'trailer') {
if ($episode->type === 'trailer') {
$trailer = $channel->addChild('trailer', $episode->title, $podcastNamespace);
$trailer->addAttribute('pubdate', $episode->published_at->format(DATE_RFC2822));
$trailer->addAttribute(

View File

@ -48,7 +48,7 @@ class Router extends CodeIgniterRouter
$matchedKey = $routeKey;
// Are we dealing with a locale?
if (strpos($routeKey, '{locale}') !== false) {
if (str_contains($routeKey, '{locale}')) {
$routeKey = str_replace('{locale}', '[^/]+', $routeKey);
}
@ -73,7 +73,7 @@ class Router extends CodeIgniterRouter
// Store our locale so CodeIgniter object can
// assign it to the Request.
if (strpos($matchedKey, '{locale}') !== false) {
if (str_contains($matchedKey, '{locale}')) {
preg_match(
'#^' . str_replace('{locale}', '(?<locale>[^/]+)', $matchedKey) . '$#u',
$uri,
@ -183,13 +183,13 @@ class Router extends CodeIgniterRouter
[$controller] = explode('::', (string) $handler);
// Checks `/` in controller name
if (strpos($controller, '/') !== false) {
if (str_contains($controller, '/')) {
throw RouterException::forInvalidControllerName($handler);
}
if (strpos((string) $handler, '$') !== false && strpos($routeKey, '(') !== false) {
if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
// Checks dynamic controller
if (strpos($controller, '$') !== false) {
if (str_contains($controller, '$')) {
throw RouterException::forDynamicController($handler);
}

View File

@ -7,8 +7,6 @@ namespace ViewComponents;
use CodeIgniter\View\ViewDecoratorInterface;
/**
* Class Decorator
*
* Enables rendering of View Components into the views.
*
* Borrowed and adapted from https://github.com/lonnieezell/Bonfire2/

View File

@ -19,7 +19,7 @@ class Theme
protected static $defaultTheme = 'app';
/**
* @var string
* @var ?string
*/
protected static $currentTheme;
@ -71,9 +71,7 @@ class Theme
*/
public static function current(): string
{
return static::$currentTheme !== null
? static::$currentTheme
: static::$defaultTheme;
return static::$currentTheme ?? static::$defaultTheme;
}
/**

View File

@ -272,13 +272,7 @@ class EpisodeModel extends UuidModel
$secondsToNextUnpublishedEpisode = $this->getSecondsToNextUnpublishedEpisode($podcastId);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $found;

View File

@ -1,205 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class PlatformModel Model for platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Models;
use App\Entities\Platform;
use CodeIgniter\Model;
use Config\App;
class PlatformModel extends Model
{
/**
* @var string
*/
protected $table = 'platforms';
/**
* @var string
*/
protected $primaryKey = 'slug';
/**
* @var list<string>
*/
protected $allowedFields = ['slug', 'type', 'label', 'home_url', 'submit_url'];
/**
* @var string
*/
protected $returnType = Platform::class;
/**
* @var bool
*/
protected $useSoftDeletes = false;
/**
* @var bool
*/
protected $useTimestamps = false;
/**
* @return Platform[]
*/
public function getPlatforms(): array
{
if (! ($found = cache('platforms'))) {
$baseUrl = rtrim(config(App::class)->baseURL, '/');
$found = $this->select(
"*, CONCAT('{$baseUrl}/assets/images/platforms/',`type`,'/',`slug`,'.svg') as icon",
)->findAll();
cache()
->save('platforms', $found, DECADE);
}
return $found;
}
public function getPlatform(string $slug): ?Platform
{
$cacheName = "platform-{$slug}";
if (! ($found = cache($cacheName))) {
$found = $this->where('slug', $slug)
->first();
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
public function createPlatform(
string $slug,
string $type,
string $label,
string $homeUrl,
string $submitUrl = null
): bool {
$data = [
'slug' => $slug,
'type' => $type,
'label' => $label,
'home_url' => $homeUrl,
'submit_url' => $submitUrl,
];
return $this->insert($data, false);
}
/**
* @return Platform[]
*/
public function getPlatformsWithLinks(int $podcastId, string $platformType): array
{
if (
! ($found = cache("podcast#{$podcastId}_platforms_{$platformType}_withLinks"))
) {
$found = $this->select(
'platforms.*, podcasts_platforms.link_url, podcasts_platforms.account_id, podcasts_platforms.is_visible, podcasts_platforms.is_on_embed',
)
->join(
'podcasts_platforms',
"podcasts_platforms.platform_slug = platforms.slug AND podcasts_platforms.podcast_id = {$podcastId}",
'left',
)
->where('platforms.type', $platformType)
->findAll();
cache()
->save("podcast#{$podcastId}_platforms_{$platformType}_withLinks", $found, DECADE);
}
return $found;
}
/**
* @return Platform[]
*/
public function getPodcastPlatforms(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
if (! ($found = cache($cacheName))) {
$found = $this->select(
'platforms.*, podcasts_platforms.link_url, podcasts_platforms.account_id, podcasts_platforms.is_visible, podcasts_platforms.is_on_embed',
)
->join('podcasts_platforms', 'podcasts_platforms.platform_slug = platforms.slug')
->where('podcasts_platforms.podcast_id', $podcastId)
->where('platforms.type', $platformType)
->findAll();
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @param mixed[] $podcastsPlatformsData
*
* @return int|false Number of rows inserted or FALSE on failure
*/
public function savePodcastPlatforms(
int $podcastId,
string $platformType,
array $podcastsPlatformsData
): int | false {
$this->clearCache($podcastId);
$podcastsPlatformsTable = $this->db->prefixTable('podcasts_platforms');
$platformsTable = $this->db->prefixTable('platforms');
$deleteJoinQuery = <<<SQL
DELETE {$podcastsPlatformsTable}
FROM {$podcastsPlatformsTable}
INNER JOIN {$platformsTable} ON {$platformsTable}.slug = {$podcastsPlatformsTable}.platform_slug
WHERE `podcast_id` = ? AND `type` = ?
SQL;
$this->db->query($deleteJoinQuery, [$podcastId, $platformType]);
if ($podcastsPlatformsData === []) {
// no rows inserted
return 0;
}
return $this->db
->table('podcasts_platforms')
->insertBatch($podcastsPlatformsData);
}
public function removePodcastPlatform(int $podcastId, string $platformSlug): bool | string
{
$this->clearCache($podcastId);
return $this->db->table('podcasts_platforms')
->delete([
'podcast_id' => $podcastId,
'platform_slug' => $platformSlug,
]);
}
public function clearCache(int $podcastId): void
{
cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
// delete localized podcast page cache
cache()
->deleteMatching("page_podcast#{$podcastId}*");
// delete post and episode comments pages cache
cache()
->deleteMatching('page_post*');
cache()
->deleteMatching('page_episode#*');
}
}

View File

@ -61,6 +61,7 @@ class PodcastModel extends Model
'location_name',
'location_geo',
'location_osm',
'verify_txt',
'payment_pointer',
'custom_rss',
'is_published_on_hubs',
@ -258,13 +259,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $found;
@ -294,13 +289,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode($podcastId);
cache()
->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $found;
@ -334,11 +323,7 @@ class PodcastModel extends Model
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId);
cache()
->save(
$cacheName,
$defaultQuery,
$secondsToNextUnpublishedEpisode ? $secondsToNextUnpublishedEpisode : DECADE
);
->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
}
return $defaultQuery;

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0,0H24V24H0Z" fill="none" />
<path d="M13.3,13.07a1.91,1.91,0,0,1,.48,0,2.51,2.51,0,0,1,1.88,1.63c.08.26.19.66.26.93l.09.37h.38a5.49,5.49,0,0,0,5.38-5.46c0-4.52-3.77-8.28-9.54-8.28A10.16,10.16,0,0,0,5.9,4.56,10,10,0,0,0,2.23,12.3a10.64,10.64,0,0,0,.28,2.35,10,10,0,0,0,9.72,7.65,11.07,11.07,0,0,0,1.3-.08A10,10,0,0,0,20,18.6l.1-.12-.2-.73-.38.1a14.32,14.32,0,0,1-3.72.48,14.14,14.14,0,0,1-3-.31,2.51,2.51,0,0,1-1.87-1.65,2.51,2.51,0,0,1,.48-2.44A2.5,2.5,0,0,1,13.3,13.07ZM12.23,3.24c5.34,0,8.6,3.42,8.6,7.34a4.55,4.55,0,0,1-4.1,4.5c-.06-.22-.13-.45-.18-.62a15.25,15.25,0,0,0-9.36-9.7A8.79,8.79,0,0,1,12.23,3.24ZM3.17,12.3A9,9,0,0,1,6.3,5.45a14.35,14.35,0,0,1,8.38,7A3.26,3.26,0,0,0,14,12.2a15.86,15.86,0,0,0-3.17-.33,15.65,15.65,0,0,0-7.51,1.94A9.33,9.33,0,0,1,3.17,12.3Zm9.44,6.64a15.9,15.9,0,0,0,3.19.33,16.77,16.77,0,0,0,2.46-.19,9,9,0,0,1-4.6,2.17,13.91,13.91,0,0,1-1.73-2.52A3.83,3.83,0,0,0,12.61,18.94Zm-.07,2.42h-.31a8.82,8.82,0,0,1-4.16-1A14.24,14.24,0,0,1,9.88,16a4.07,4.07,0,0,0,.16.71A15.68,15.68,0,0,0,12.54,21.36Zm-1.83-8a15.6,15.6,0,0,0-3.48,6.55,9,9,0,0,1-3.72-5.1,14.27,14.27,0,0,1,7.29-1.95h.44A3.57,3.57,0,0,0,10.71,13.31Z" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0,0H24V24H0Z" fill="none" />
<path d="M3.22,10.08A1.24,1.24,0,0,0,2,11.31V12.7a1.22,1.22,0,0,0,2.44,0h0V11.31A1.17,1.17,0,0,0,3.22,10.08Zm17.56,0a1.23,1.23,0,0,0-1.22,1.23V12.7a1.21,1.21,0,0,0,1.22,1.22A1.22,1.22,0,0,0,22,12.7h0V11.31A1.24,1.24,0,0,0,20.78,10.08ZM7.56,14.2a1.24,1.24,0,0,0-1.23,1.23v1.4a1.23,1.23,0,0,0,2.45,0h0v-1.4A1.17,1.17,0,0,0,7.56,14.2ZM7.56,6A1.24,1.24,0,0,0,6.33,7.24V11.7h0a1.23,1.23,0,0,0,2.45,0h0V7.24A1.17,1.17,0,0,0,7.56,6Zm8.88,0a1.24,1.24,0,0,0-1.22,1.23V8.63a1.23,1.23,0,0,0,2.45,0h0V7.24A1.24,1.24,0,0,0,16.44,6ZM12,2a1.23,1.23,0,0,0-1.22,1.23V4.62a1.22,1.22,0,1,0,2.44,0h0V3.23A1.23,1.23,0,0,0,12,2Zm0,16.16a1.22,1.22,0,0,0-1.22,1.22v1.4a1.22,1.22,0,1,0,2.44,0h0v-1.4A1.26,1.26,0,0,0,12,18.16Zm4.44-7a1.23,1.23,0,0,0-1.22,1.22v4.47a1.23,1.23,0,0,0,2.45,0h0V12.36A1.23,1.23,0,0,0,16.44,11.14ZM13.22,8.41a1.22,1.22,0,0,0-2.44,0h0v7.36h0a1.22,1.22,0,1,0,2.44,0h0V8.41Z" />
</svg>

Before

Width:  |  Height:  |  Size: 991 B

View File

@ -42,7 +42,7 @@ class Alert extends Component
$this->variant = 'default';
}
$glyph = icon(($this->glyph === null ? $variants[$this->variant]['glyph'] : $this->glyph), 'flex-shrink-0 mr-2 text-lg');
$glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), 'flex-shrink-0 mr-2 text-lg');
$title = $this->title === null ? '' : '<div class="font-semibold">' . $this->title . '</div>';
$class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class;

View File

@ -28,17 +28,18 @@ class Button extends Component
public function render(): string
{
$baseClass =
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent';
'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full focus:ring-accent';
$variantClass = [
'default' => 'text-black bg-gray-300 hover:bg-gray-400',
'primary' => 'text-accent-contrast bg-accent-base hover:bg-accent-hover',
'secondary' => 'border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'success' => 'text-white bg-pine-500 hover:bg-pine-800',
'danger' => 'text-white bg-red-600 hover:bg-red-700',
'warning' => 'text-black bg-yellow-500 hover:bg-yellow-600',
'info' => 'text-white bg-blue-500 hover:bg-blue-600',
'disabled' => 'text-black bg-gray-300 cursor-not-allowed',
'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400',
'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover',
'secondary' => 'shadow-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800',
'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700',
'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600',
'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600',
'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed',
'link' => 'text-accent-base bg-transparent underline hover:no-underline',
];
$sizeClass = [

2
builds
View File

@ -1,6 +1,8 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
define('LATEST_RELEASE', '^4.0');
define('GITHUB_URL', 'https://github.com/codeigniter4/codeigniter4');

View File

@ -9,7 +9,7 @@
"php": "^8.1",
"adaures/ipcat-php": "^v1.0.0",
"adaures/podcast-persons-taxonomy": "^v1.0.1",
"aws/aws-sdk-php": "^3.305.0",
"aws/aws-sdk-php": "^3.305.1",
"chrisjean/php-ico": "^1.0.4",
"cocur/slugify": "^v4.5.1",
"codeigniter4/framework": "v4.5.1",
@ -35,10 +35,10 @@
"mikey179/vfsstream": "^v1.6.11",
"phpstan/extension-installer": "^1.3.1",
"phpstan/phpstan": "^1.10.67",
"phpunit/phpunit": "^10.5.19",
"phpunit/phpunit": "^10.5.20",
"rector/rector": "^1.0.4",
"symplify/coding-standard": "^12.0.7",
"symplify/easy-coding-standard": "^12.0.13"
"symplify/coding-standard": "^12.1.4",
"symplify/easy-coding-standard": "^12.1.14"
},
"autoload": {
"psr-4": {

252
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1fd50f9d5d2d6f953e192d7ead95050c",
"content-hash": "214fb8dbb7be84011c98ac2ab2df7c9d",
"packages": [
{
"name": "adaures/ipcat-php",
@ -75,16 +75,16 @@
},
{
"name": "aws/aws-crt-php",
"version": "v1.2.4",
"version": "v1.2.5",
"source": {
"type": "git",
"url": "https://github.com/awslabs/aws-crt-php.git",
"reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2"
"reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/eb0c6e4e142224a10b08f49ebf87f32611d162b2",
"reference": "eb0c6e4e142224a10b08f49ebf87f32611d162b2",
"url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b",
"reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b",
"shasum": ""
},
"require": {
@ -114,22 +114,22 @@
"keywords": ["amazon", "aws", "crt", "sdk"],
"support": {
"issues": "https://github.com/awslabs/aws-crt-php/issues",
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.4"
"source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5"
},
"time": "2023-11-08T00:42:13+00:00"
"time": "2024-04-19T21:30:56+00:00"
},
{
"name": "aws/aws-sdk-php",
"version": "3.305.0",
"version": "3.305.1",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "6992342acf7dd4501163c6cddabe76c74f2020ad"
"reference": "3af1c6925b95a0f4303a1859dd56aa8374560c42"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/6992342acf7dd4501163c6cddabe76c74f2020ad",
"reference": "6992342acf7dd4501163c6cddabe76c74f2020ad",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3af1c6925b95a0f4303a1859dd56aa8374560c42",
"reference": "3af1c6925b95a0f4303a1859dd56aa8374560c42",
"shasum": ""
},
"require": {
@ -205,9 +205,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.305.0"
"source": "https://github.com/aws/aws-sdk-php/tree/3.305.1"
},
"time": "2024-04-22T18:07:07+00:00"
"time": "2024-04-23T18:10:07+00:00"
},
{
"name": "brick/math",
@ -4347,16 +4347,16 @@
},
{
"name": "phpunit/phpunit",
"version": "10.5.19",
"version": "10.5.20",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "c726f0de022368f6ed103e452a765d3304a996a4"
"reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c726f0de022368f6ed103e452a765d3304a996a4",
"reference": "c726f0de022368f6ed103e452a765d3304a996a4",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/547d314dc24ec1e177720d45c6263fb226cc2ae3",
"reference": "547d314dc24ec1e177720d45c6263fb226cc2ae3",
"shasum": ""
},
"require": {
@ -4416,7 +4416,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.19"
"source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.20"
},
"funding": [
{
@ -4432,7 +4432,7 @@
"type": "tidelift"
}
],
"time": "2024-04-17T14:06:18+00:00"
"time": "2024-04-24T06:32:35+00:00"
},
{
"name": "psr/container",
@ -5536,47 +5536,46 @@
},
{
"name": "symfony/console",
"version": "v6.4.6",
"version": "v7.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f"
"reference": "fde915cd8e7eb99b3d531d3d5c09531429c3f9e5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/a2708a5da5c87d1d0d52937bdeac625df659e11f",
"reference": "a2708a5da5c87d1d0d52937bdeac625df659e11f",
"url": "https://api.github.com/repos/symfony/console/zipball/fde915cd8e7eb99b3d531d3d5c09531429c3f9e5",
"reference": "fde915cd8e7eb99b3d531d3d5c09531429c3f9e5",
"shasum": ""
},
"require": {
"php": ">=8.1",
"symfony/deprecation-contracts": "^2.5|^3",
"php": ">=8.2",
"symfony/polyfill-mbstring": "~1.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/string": "^5.4|^6.0|^7.0"
"symfony/string": "^6.4|^7.0"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/dotenv": "<5.4",
"symfony/event-dispatcher": "<5.4",
"symfony/lock": "<5.4",
"symfony/process": "<5.4"
"symfony/dependency-injection": "<6.4",
"symfony/dotenv": "<6.4",
"symfony/event-dispatcher": "<6.4",
"symfony/lock": "<6.4",
"symfony/process": "<6.4"
},
"provide": {
"psr/log-implementation": "1.0|2.0|3.0"
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0",
"symfony/lock": "^5.4|^6.0|^7.0",
"symfony/messenger": "^5.4|^6.0|^7.0",
"symfony/process": "^5.4|^6.0|^7.0",
"symfony/stopwatch": "^5.4|^6.0|^7.0",
"symfony/var-dumper": "^5.4|^6.0|^7.0"
"symfony/lock": "^6.4|^7.0",
"symfony/messenger": "^6.4|^7.0",
"symfony/process": "^6.4|^7.0",
"symfony/stopwatch": "^6.4|^7.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -5601,7 +5600,7 @@
"homepage": "https://symfony.com",
"keywords": ["cli", "command-line", "console", "terminal"],
"support": {
"source": "https://github.com/symfony/console/tree/v6.4.6"
"source": "https://github.com/symfony/console/tree/v7.0.6"
},
"funding": [
{
@ -5617,28 +5616,28 @@
"type": "tidelift"
}
],
"time": "2024-03-29T19:07:53+00:00"
"time": "2024-04-01T11:04:53+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v6.4.3",
"version": "v7.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef"
"reference": "834c28d533dd0636f910909d01b9ff45cc094b5e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/ae9d3a6f3003a6caf56acd7466d8d52378d44fef",
"reference": "ae9d3a6f3003a6caf56acd7466d8d52378d44fef",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/834c28d533dd0636f910909d01b9ff45cc094b5e",
"reference": "834c28d533dd0636f910909d01b9ff45cc094b5e",
"shasum": ""
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"symfony/event-dispatcher-contracts": "^2.5|^3"
},
"conflict": {
"symfony/dependency-injection": "<5.4",
"symfony/dependency-injection": "<6.4",
"symfony/service-contracts": "<2.5"
},
"provide": {
@ -5647,13 +5646,13 @@
},
"require-dev": {
"psr/log": "^1|^2|^3",
"symfony/config": "^5.4|^6.0|^7.0",
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
"symfony/error-handler": "^5.4|^6.0|^7.0",
"symfony/expression-language": "^5.4|^6.0|^7.0",
"symfony/http-foundation": "^5.4|^6.0|^7.0",
"symfony/config": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
"symfony/error-handler": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/service-contracts": "^2.5|^3",
"symfony/stopwatch": "^5.4|^6.0|^7.0"
"symfony/stopwatch": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -5677,7 +5676,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v6.4.3"
"source": "https://github.com/symfony/event-dispatcher/tree/v7.0.3"
},
"funding": [
{
@ -5693,7 +5692,7 @@
"type": "tidelift"
}
],
"time": "2024-01-23T14:51:35+00:00"
"time": "2024-01-23T15:02:46+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@ -5771,20 +5770,20 @@
},
{
"name": "symfony/filesystem",
"version": "v6.4.6",
"version": "v7.0.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3"
"reference": "408105dff4c104454100730bdfd1a9cdd993f04d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/9919b5509ada52cc7f66f9a35c86a4a29955c9d3",
"reference": "9919b5509ada52cc7f66f9a35c86a4a29955c9d3",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/408105dff4c104454100730bdfd1a9cdd993f04d",
"reference": "408105dff4c104454100730bdfd1a9cdd993f04d",
"shasum": ""
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-mbstring": "~1.8"
},
@ -5810,7 +5809,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/filesystem/tree/v6.4.6"
"source": "https://github.com/symfony/filesystem/tree/v7.0.6"
},
"funding": [
{
@ -5826,27 +5825,27 @@
"type": "tidelift"
}
],
"time": "2024-03-21T19:36:20+00:00"
"time": "2024-03-21T19:37:36+00:00"
},
{
"name": "symfony/finder",
"version": "v6.4.0",
"version": "v7.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce"
"reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/11d736e97f116ac375a81f96e662911a34cd50ce",
"reference": "11d736e97f116ac375a81f96e662911a34cd50ce",
"url": "https://api.github.com/repos/symfony/finder/zipball/6e5688d69f7cfc4ed4a511e96007e06c2d34ce56",
"reference": "6e5688d69f7cfc4ed4a511e96007e06c2d34ce56",
"shasum": ""
},
"require": {
"php": ">=8.1"
"php": ">=8.2"
},
"require-dev": {
"symfony/filesystem": "^6.0|^7.0"
"symfony/filesystem": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -5870,7 +5869,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v6.4.0"
"source": "https://github.com/symfony/finder/tree/v7.0.0"
},
"funding": [
{
@ -5886,24 +5885,24 @@
"type": "tidelift"
}
],
"time": "2023-10-31T17:30:12+00:00"
"time": "2023-10-31T17:59:56+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v6.4.0",
"version": "v7.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "22301f0e7fdeaacc14318928612dee79be99860e"
"reference": "700ff4096e346f54cb628ea650767c8130f1001f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e",
"reference": "22301f0e7fdeaacc14318928612dee79be99860e",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f",
"reference": "700ff4096e346f54cb628ea650767c8130f1001f",
"shasum": ""
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
@ -5929,7 +5928,7 @@
"homepage": "https://symfony.com",
"keywords": ["config", "configuration", "options"],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v6.4.0"
"source": "https://github.com/symfony/options-resolver/tree/v7.0.0"
},
"funding": [
{
@ -5945,7 +5944,7 @@
"type": "tidelift"
}
],
"time": "2023-08-08T10:16:24+00:00"
"time": "2023-08-08T10:20:21+00:00"
},
{
"name": "symfony/polyfill-intl-grapheme",
@ -6163,20 +6162,20 @@
},
{
"name": "symfony/process",
"version": "v6.4.4",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "710e27879e9be3395de2b98da3f52a946039f297"
"reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/710e27879e9be3395de2b98da3f52a946039f297",
"reference": "710e27879e9be3395de2b98da3f52a946039f297",
"url": "https://api.github.com/repos/symfony/process/zipball/0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
"reference": "0e7727191c3b71ebec6d529fa0e50a01ca5679e9",
"shasum": ""
},
"require": {
"php": ">=8.1"
"php": ">=8.2"
},
"type": "library",
"autoload": {
@ -6200,7 +6199,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v6.4.4"
"source": "https://github.com/symfony/process/tree/v7.0.4"
},
"funding": [
{
@ -6216,7 +6215,7 @@
"type": "tidelift"
}
],
"time": "2024-02-20T12:31:00+00:00"
"time": "2024-02-22T20:27:20+00:00"
},
{
"name": "symfony/service-contracts",
@ -6298,20 +6297,20 @@
},
{
"name": "symfony/stopwatch",
"version": "v6.4.3",
"version": "v7.0.3",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
"reference": "416596166641f1f728b0a64f5b9dd07cceb410c1"
"reference": "983900d6fddf2b0cbaacacbbad07610854bd8112"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/416596166641f1f728b0a64f5b9dd07cceb410c1",
"reference": "416596166641f1f728b0a64f5b9dd07cceb410c1",
"url": "https://api.github.com/repos/symfony/stopwatch/zipball/983900d6fddf2b0cbaacacbbad07610854bd8112",
"reference": "983900d6fddf2b0cbaacacbbad07610854bd8112",
"shasum": ""
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"symfony/service-contracts": "^2.5|^3"
},
"type": "library",
@ -6336,7 +6335,7 @@
"description": "Provides a way to profile code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/stopwatch/tree/v6.4.3"
"source": "https://github.com/symfony/stopwatch/tree/v7.0.3"
},
"funding": [
{
@ -6352,24 +6351,24 @@
"type": "tidelift"
}
],
"time": "2024-01-23T14:35:58+00:00"
"time": "2024-01-23T15:02:46+00:00"
},
{
"name": "symfony/string",
"version": "v6.4.4",
"version": "v7.0.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9"
"reference": "f5832521b998b0bec40bee688ad5de98d4cf111b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9",
"reference": "4e465a95bdc32f49cf4c7f07f751b843bbd6dcd9",
"url": "https://api.github.com/repos/symfony/string/zipball/f5832521b998b0bec40bee688ad5de98d4cf111b",
"reference": "f5832521b998b0bec40bee688ad5de98d4cf111b",
"shasum": ""
},
"require": {
"php": ">=8.1",
"php": ">=8.2",
"symfony/polyfill-ctype": "~1.8",
"symfony/polyfill-intl-grapheme": "~1.0",
"symfony/polyfill-intl-normalizer": "~1.0",
@ -6379,11 +6378,11 @@
"symfony/translation-contracts": "<2.5"
},
"require-dev": {
"symfony/error-handler": "^5.4|^6.0|^7.0",
"symfony/http-client": "^5.4|^6.0|^7.0",
"symfony/intl": "^6.2|^7.0",
"symfony/error-handler": "^6.4|^7.0",
"symfony/http-client": "^6.4|^7.0",
"symfony/intl": "^6.4|^7.0",
"symfony/translation-contracts": "^2.5|^3.0",
"symfony/var-exporter": "^5.4|^6.0|^7.0"
"symfony/var-exporter": "^6.4|^7.0"
},
"type": "library",
"autoload": {
@ -6409,7 +6408,7 @@
"homepage": "https://symfony.com",
"keywords": ["grapheme", "i18n", "string", "unicode", "utf-8", "utf8"],
"support": {
"source": "https://github.com/symfony/string/tree/v6.4.4"
"source": "https://github.com/symfony/string/tree/v7.0.4"
},
"funding": [
{
@ -6425,41 +6424,38 @@
"type": "tidelift"
}
],
"time": "2024-02-01T13:16:41+00:00"
"time": "2024-02-01T13:17:36+00:00"
},
{
"name": "symplify/coding-standard",
"version": "12.0.7",
"version": "12.1.4",
"source": {
"type": "git",
"url": "https://github.com/symplify/coding-standard.git",
"reference": "199f1e5e2b2bd67eac392ab1d3c6c04bf7ebce3c"
"reference": "1a591e18c64e7367b243729a43a70c2e92025468"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symplify/coding-standard/zipball/199f1e5e2b2bd67eac392ab1d3c6c04bf7ebce3c",
"reference": "199f1e5e2b2bd67eac392ab1d3c6c04bf7ebce3c",
"url": "https://api.github.com/repos/symplify/coding-standard/zipball/1a591e18c64e7367b243729a43a70c2e92025468",
"reference": "1a591e18c64e7367b243729a43a70c2e92025468",
"shasum": ""
},
"require": {
"friendsofphp/php-cs-fixer": "^3.40",
"friendsofphp/php-cs-fixer": "^3.49",
"nette/utils": "^3.2",
"php": ">=8.1",
"php": ">=8.2",
"symplify/rule-doc-generator-contracts": "^11.1"
},
"require-dev": {
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10.26",
"phpunit/phpunit": "^10.2",
"rector/rector": "^0.17.7",
"squizlabs/php_codesniffer": "^3.7.2",
"symplify/easy-ci": "^11.3",
"symplify/easy-coding-standard": "^12.0.11",
"symplify/phpstan-extensions": "^11.2",
"phpstan/phpstan": "^1.10.58",
"phpunit/phpunit": "^10.5",
"rector/rector": "^1.0",
"squizlabs/php_codesniffer": "^3.8.1",
"symplify/easy-coding-standard": "^12.1",
"symplify/phpstan-extensions": "^11.4",
"symplify/rule-doc-generator": "^12.0",
"tomasvotruba/class-leak": "^0.2",
"tomasvotruba/type-coverage": "^0.2",
"tomasvotruba/unused-public": "^0.3",
"tracy/tracy": "^2.10"
},
"type": "library",
@ -6473,7 +6469,7 @@
"description": "Set of Symplify rules for PHP_CodeSniffer and PHP CS Fixer.",
"support": {
"issues": "https://github.com/symplify/coding-standard/issues",
"source": "https://github.com/symplify/coding-standard/tree/12.0.7"
"source": "https://github.com/symplify/coding-standard/tree/12.1.4"
},
"funding": [
{
@ -6485,29 +6481,29 @@
"type": "github"
}
],
"time": "2023-12-07T09:18:34+00:00"
"time": "2024-02-23T13:07:31+00:00"
},
{
"name": "symplify/easy-coding-standard",
"version": "12.1.1",
"version": "12.1.14",
"source": {
"type": "git",
"url": "https://github.com/easy-coding-standard/easy-coding-standard.git",
"reference": "d082c6f90e83ae85f7e63b03ea06780dcf765834"
"reference": "e3c4a241ee36704f7cf920d5931f39693e64afd5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/d082c6f90e83ae85f7e63b03ea06780dcf765834",
"reference": "d082c6f90e83ae85f7e63b03ea06780dcf765834",
"url": "https://api.github.com/repos/easy-coding-standard/easy-coding-standard/zipball/e3c4a241ee36704f7cf920d5931f39693e64afd5",
"reference": "e3c4a241ee36704f7cf920d5931f39693e64afd5",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"conflict": {
"friendsofphp/php-cs-fixer": "<3.0",
"phpcsstandards/php_codesniffer": "<3.6",
"symplify/coding-standard": "<11.3"
"friendsofphp/php-cs-fixer": "<3.46",
"phpcsstandards/php_codesniffer": "<3.8",
"symplify/coding-standard": "<12.1"
},
"bin": ["bin/ecs"],
"type": "library",
@ -6520,7 +6516,7 @@
"keywords": ["Code style", "automation", "fixer", "static analysis"],
"support": {
"issues": "https://github.com/easy-coding-standard/easy-coding-standard/issues",
"source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.1.1"
"source": "https://github.com/easy-coding-standard/easy-coding-standard/tree/12.1.14"
},
"funding": [
{
@ -6532,7 +6528,7 @@
"type": "github"
}
],
"time": "2024-01-05T13:20:55+00:00"
"time": "2024-02-23T13:10:40+00:00"
},
{
"name": "symplify/rule-doc-generator-contracts",

View File

@ -303,11 +303,8 @@ You may skip this section if you go through the install wizard (go to
# Populates all Languages
php spark db:seed LanguageSeeder
# Populates all podcasts platforms
php spark db:seed PlatformSeeder
# Adds a superadmin with [admin@castopod.local / castopod] credentials
php spark db:seed PlatformSeeder
php spark db:seed DevSuperadminSeeder
```
3. (optionnal) Populate the database with test data:

30
ecs.php
View File

@ -10,26 +10,20 @@ use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;
use Symplify\CodingStandard\Fixer\Naming\StandardizeHereNowDocKeywordFixer;
use Symplify\CodingStandard\Fixer\Spacing\MethodChainingNewlineFixer;
use Symplify\EasyCodingStandard\Config\ECSConfig;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
return static function (ECSConfig $ecsConfig): void {
// alternative to CLI arguments, easier to maintain and extend
$ecsConfig->paths([
return ECSConfig::configure()
->withPaths([
__DIR__ . '/app',
__DIR__ . '/modules',
__DIR__ . '/themes',
__DIR__ . '/tests',
__DIR__ . '/public',
__DIR__ . '/builds',
__DIR__ . '/ecs.php',
__DIR__ . '/preload.php',
__DIR__ . '/rector.php',
__DIR__ . '/spark',
]);
$ecsConfig->sets([SetList::CLEAN_CODE, SetList::COMMON, SetList::SYMPLIFY, SetList::PSR_12]);
$ecsConfig->skip([
])
->withRootFiles()
->withPreparedSets(cleanCode: true, common: true, symplify: true, strict: true, psr12: true)
->withSkip([
// skip specific generated files
__DIR__ . '/modules/Admin/Language/*/PersonsTaxonomy.php',
@ -40,11 +34,7 @@ return static function (ECSConfig $ecsConfig): void {
__DIR__ . '/app/Helpers/components_helper.php',
],
LineLengthFixer::class => [
__DIR__ . '/app/Views/*',
__DIR__ . '/modules/**/Views/*',
__DIR__ . '/themes/*',
],
LineLengthFixer::class => [__DIR__ . '/app/Views/*', __DIR__ . '/modules/**/Views/*', __DIR__ . '/themes/*'],
IndentationTypeFixer::class => [
__DIR__ . '/app/Views/*',
@ -65,11 +55,9 @@ return static function (ECSConfig $ecsConfig): void {
BinaryOperatorSpacesFixer::class => [__DIR__ . '/app/Language/*', __DIR__ . '/modules/**/Language/*'],
AssignmentInConditionSniff::class,
]);
$ecsConfig->ruleWithConfiguration(BinaryOperatorSpacesFixer::class, [
])
->withConfiguredRule(BinaryOperatorSpacesFixer::class, [
'operators' => [
'=>' => 'align_single_space_minimal',
],
]);
};

View File

@ -511,48 +511,6 @@ $routes->group(
});
});
});
$routes->group('platforms', static function ($routes): void {
$routes->get(
'/',
'PodcastPlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'social',
'PodcastPlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'funding',
'PodcastPlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->post(
'save/(:platformType)',
'PodcastPlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'(:slug)/podcast-platform-remove',
'PodcastPlatformController::removePodcastPlatform/$1/$2',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast#.manage-platforms',
],
);
});
// Podcast notifications
$routes->group('notifications', static function ($routes): void {
$routes->get('/', 'NotificationController::list/$1', [

View File

@ -12,8 +12,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* 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
*

View File

@ -326,12 +326,8 @@ class EpisodeController extends BaseController
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->episode->number = $this->request->getPost('episode_number')
? $this->request->getPost('episode_number')
: null;
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null;
$this->episode->number = $this->request->getPost('episode_number') ?: null;
$this->episode->season_number = $this->request->getPost('season_number') ?: null;
$this->episode->type = $this->request->getPost('type');
$this->episode->is_blocked = $this->request->getPost('block') === 'yes';
$this->episode->custom_rss_string = $this->request->getPost('custom_rss');

View File

@ -17,7 +17,7 @@ use CodeIgniter\HTTP\RedirectResponse;
class PageController extends BaseController
{
protected ?Page $page;
protected ?Page $page = null;
public function _remap(string $method, string ...$params): mixed
{

View File

@ -18,7 +18,7 @@ use Modules\Media\Models\MediaModel;
class PersonController extends BaseController
{
protected ?Person $person;
protected ?Person $person = null;
public function _remap(string $method, string ...$params): mixed
{

View File

@ -224,6 +224,7 @@ class PodcastController extends BaseController
'location' => $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
),
'verify_txt' => $this->request->getPost('verify_txt'),
'custom_rss_string' => $this->request->getPost('custom_rss'),
'is_blocked' => $this->request->getPost('block') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes',
@ -320,6 +321,9 @@ class PodcastController extends BaseController
$this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
);
$this->podcast->verify_txt = $this->request->getPost('verify_txt') === '' ? null : $this->request->getPost(
'verify_txt'
);
$this->podcast->custom_rss_string = $this->request->getPost('custom_rss');
$this->podcast->new_feed_url = $this->request->getPost('new_feed_url') === '' ? null : $this->request->getPost(
'new_feed_url'

View File

@ -138,6 +138,9 @@ return [
'If you need RSS tags that Castopod does not handle, set them here.',
'custom_rss' => 'Custom RSS tags for the podcast',
'custom_rss_hint' => 'This will be injected within the ❬channel❭ tag.',
'verify_txt' => 'Ownership verification TXT',
'verify_txt_hint' => 'Rather than relying on email, certain third-party services may confirm your podcast ownership by requesting you to embed a verification text within your feed.',
'verify_txt_helper' => 'This text is injected into a <podcast:txt purpose="verify"> tag.',
'new_feed_url' => 'New feed URL',
'new_feed_url_hint' => 'Use this field when you move to another domain or podcast hosting platform. By default, the value is set to the current RSS URL if the podcast is imported.',
'old_feed_url' => 'Old feed URL',

View File

@ -30,9 +30,7 @@ trait AnalyticsTrait
$referer = $session->get('referer');
$domain =
parse_url((string) $referer, PHP_URL_HOST) === null
? '- Direct -'
: parse_url((string) $referer, PHP_URL_HOST);
parse_url((string) $referer, PHP_URL_HOST) ?? '- Direct -';
parse_str((string) parse_url((string) $referer, PHP_URL_QUERY), $queries);
$keywords = $queries['q'] ?? null;

View File

@ -2,7 +2,6 @@
declare(strict_types=1);
use CodeIgniter\Router\RouteCollection;
use Modules\Analytics\Config\Analytics;
/**

View File

@ -85,14 +85,10 @@ if (! function_exists('set_user_session_location')) {
$city = $cityReader->city(client_ip());
$location = [
'countryCode' => $city->country->isoCode === null
? 'N/A'
: $city->country->isoCode,
'regionCode' => $city->subdivisions[0]->isoCode === null
? 'N/A'
: $city->subdivisions[0]->isoCode,
'latitude' => round($city->location->latitude, 3),
'longitude' => round($city->location->longitude, 3),
'countryCode' => $city->country->isoCode ?? 'N/A',
'regionCode' => $city->subdivisions[0]->isoCode ?? 'N/A',
'latitude' => round($city->location->latitude, 3),
'longitude' => round($city->location->longitude, 3),
];
// If things go wrong the show must go on and the user must be able to download the file
} catch (Exception) {
@ -179,9 +175,7 @@ if (! function_exists('set_user_session_referer')) {
{
$session = Services::session();
$newreferer = isset($_SERVER['HTTP_REFERER'])
? $_SERVER['HTTP_REFERER']
: '- Direct -';
$newreferer = $_SERVER['HTTP_REFERER'] ?? '- Direct -';
$newreferer =
parse_url((string) $newreferer, PHP_URL_HOST) ===
parse_url(current_url(false), PHP_URL_HOST)
@ -250,9 +244,7 @@ if (! function_exists('podcast_hit')) {
}
//We get the HTTP header field `Range`:
$httpRange = isset($_SERVER['HTTP_RANGE'])
? $_SERVER['HTTP_RANGE']
: null;
$httpRange = $_SERVER['HTTP_RANGE'] ?? null;
$salt = config(Analytics::class)
->salt;

View File

@ -65,7 +65,7 @@ class EpisodeController extends Controller
return $this->failNotFound('Episode not found');
}
return $this->respond($this->mapEpisode($episode));
return $this->respond(static::mapEpisode($episode));
}
protected static function mapEpisode(Episode $episode): Episode

View File

@ -36,7 +36,7 @@ class ApiFilter implements FilterInterface
}
$authHeader = $request->getHeaderLine('Authorization');
if (substr($authHeader, 0, 6) !== 'Basic ') {
if (! str_starts_with($authHeader, 'Basic ')) {
$response->setStatusCode(401);
return $response;
@ -44,7 +44,7 @@ class ApiFilter implements FilterInterface
$auth_token = base64_decode(substr($authHeader, 6), true);
list($username, $password) = explode(':', (string) $auth_token);
[$username, $password] = explode(':', (string) $auth_token);
if (! ($username === $restApiConfig->basicAuthUsername && $password === $restApiConfig->basicAuthPassword)) {
$response->setStatusCode(401);

View File

@ -11,8 +11,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Class ActionController
*
* A generic controller to handle Authentication Actions.
*/
class ActionController extends ShieldActionController

View File

@ -22,7 +22,7 @@ class ContributorController extends BaseController
{
protected Podcast $podcast;
protected ?User $contributor;
protected ?User $contributor = null;
public function _remap(string $method, string ...$params): mixed
{

View File

@ -8,8 +8,6 @@ use CodeIgniter\Controller;
use CodeIgniter\HTTP\RedirectResponse;
/**
* Class ActionController
*
* A generic controller to handle Authentication Actions.
*/
class InteractController extends Controller

View File

@ -11,8 +11,6 @@ use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/**
* Class RegisterController
*
* Handles displaying registration form, and handling actual registration flow.
*/
class RegisterController extends ShieldRegisterController

View File

@ -22,7 +22,7 @@ use Modules\Auth\Models\UserModel;
class UserController extends BaseController
{
protected ?User $user;
protected ?User $user = null;
public function _remap(string $method, string ...$params): mixed
{

View File

@ -102,9 +102,10 @@ if (! function_exists('add_podcast_group')) {
if (! function_exists('get_instance_group')) {
function get_instance_group(User $user): ?string
{
$instanceGroups = array_filter($user->getGroups() ?? [], static function ($group): bool {
return ! str_starts_with($group, 'podcast#');
});
$instanceGroups = array_filter(
$user->getGroups() ?? [],
static fn ($group): bool => ! str_starts_with((string) $group, 'podcast#')
);
if ($instanceGroups === []) {
return null;
@ -138,9 +139,10 @@ if (! function_exists('set_instance_group')) {
if (! function_exists('get_podcast_group')) {
function get_podcast_group(User $user, int $podcastId, bool $removePrefix = true): ?string
{
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group) use ($podcastId): bool {
return str_starts_with($group, "podcast#{$podcastId}-");
});
$podcastGroups = array_filter(
$user->getGroups() ?? [],
static fn ($group): bool => str_starts_with((string) $group, "podcast#{$podcastId}-")
);
if ($podcastGroups === []) {
return null;
@ -180,9 +182,10 @@ if (! function_exists('get_podcast_groups')) {
*/
function get_user_podcast_ids(User $user): array
{
$podcastGroups = array_filter($user->getGroups() ?? [], static function ($group): bool {
return str_starts_with($group, 'podcast#');
});
$podcastGroups = array_filter(
$user->getGroups() ?? [],
static fn ($group): bool => str_starts_with((string) $group, 'podcast#')
);
$userPodcastIds = [];
// extract all podcast ids from groups

View File

@ -42,9 +42,7 @@ abstract class AbstractObject
}
// removes all NULL, FALSE and Empty Strings but leaves 0 (zero) values
return array_filter($array, static function ($value): bool {
return $value !== null && $value !== false && $value !== '';
});
return array_filter($array, static fn ($value): bool => $value !== null && $value !== false && $value !== '');
}
public function toJSON(): string

View File

@ -175,12 +175,10 @@ if (! function_exists('create_preview_card_from_url')) {
// Check that, at least, the url and title are set
$newPreviewCard = new PreviewCard([
'url' => $mediaUrl,
'title' => $media['title'] ?? '',
'description' => $media['description'] ?? '',
'type' => isset($typeMapping[$media['type']])
? $typeMapping[$media['type']]
: 'link',
'url' => $mediaUrl,
'title' => $media['title'] ?? '',
'description' => $media['description'] ?? '',
'type' => $typeMapping[$media['type']] ?? 'link',
'author_name' => $media['author_name'] ?? null,
'author_url' => $media['author_url'] ?? null,
'provider_name' => $media['provider_name'] ?? '',

View File

@ -134,7 +134,7 @@ class PostModel extends UuidModel
$secondsToNextUnpublishedPost = $this->getSecondsToNextUnpublishedPosts($actorId);
cache()
->save($cacheName, $found, $secondsToNextUnpublishedPost ? $secondsToNextUnpublishedPost : DECADE);
->save($cacheName, $found, $secondsToNextUnpublishedPost ?: DECADE);
}
return $found;

View File

@ -160,9 +160,7 @@ class InstallController extends Controller
if (! $this->validate($rules)) {
return redirect()
->to(
(host_url() === null ? config(App::class) ->baseURL : host_url()) . config(Install::class)->gateway
)
->to((host_url() ?? config(App::class) ->baseURL) . config(Install::class)->gateway)
->withInput()
->with('errors', $this->validator->getErrors());
}

View File

@ -85,16 +85,12 @@ class Transcript extends BaseMedia
}
$transcript_format = $this->file->getExtension();
switch ($transcript_format) {
case 'vtt':
$transcriptJson = $transcriptParser->loadString($transcriptContent)
->parseVtt();
break;
case 'srt':
default:
$transcriptJson = $transcriptParser->loadString($transcriptContent)
->parseSrt();
}
$transcriptJson = match ($transcript_format) {
'vtt' => $transcriptParser->loadString($transcriptContent)
->parseVtt(),
default => $transcriptParser->loadString($transcriptContent)
->parseSrt(),
};
$tempFilePath = WRITEPATH . 'uploads/' . $this->file->getRandomName();
file_put_contents($tempFilePath, $transcriptJson);

View File

@ -543,9 +543,9 @@ class VideoClipper
# find unique color
do {
$r = rand(0, 255);
$g = rand(0, 255);
$b = rand(0, 255);
$r = random_int(0, 255);
$g = random_int(0, 255);
$b = random_int(0, 255);
} while (imagecolorexact($src, $r, $g, $b) < 0);
$ns = $s * $q;

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Modules\PremiumPodcasts\Config;
use CodeIgniter\Router\RouteCollection;
use Modules\Admin\Config\Admin;
$routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
/** @var RouteCollection $routes */
// Admin routes for subscriptions
$routes->group(
config(Admin::class)
->gateway,
[
'namespace' => 'Modules\Platforms\Controllers',
],
static function ($routes): void {
$routes->group('podcasts/(:num)/platforms', static function ($routes): void {
$routes->get(
'/',
'PlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'social',
'PlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'funding',
'PlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->post(
'save/(:platformType)',
'PlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast#.manage-platforms',
],
);
$routes->get(
'(:platformType)/(:slug)/podcast-platform-remove',
'PlatformController::removePlatform/$1/$2/$3',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast#.manage-platforms',
],
);
});
}
);

View File

@ -8,18 +8,19 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace Modules\Admin\Controllers;
namespace Modules\Platforms\Controllers;
use App\Entities\Podcast;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use Modules\Admin\Controllers\BaseController;
use Modules\Platforms\Models\PlatformModel;
class PodcastPlatformController extends BaseController
class PlatformController extends BaseController
{
protected ?Podcast $podcast;
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
@ -28,18 +29,20 @@ class PodcastPlatformController extends BaseController
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) instanceof Podcast
! ($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) instanceof Podcast
) {
unset($params[0]);
return $this->{$method}(...$params);
throw PageNotFoundException::forPageNotFound();
}
throw PageNotFoundException::forPageNotFound();
$this->podcast = $podcast;
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): string
{
return view('podcast/platforms\dashboard');
return view('podcast/platforms/dashboard');
}
public function platforms(string $platformType): string
@ -49,7 +52,7 @@ class PodcastPlatformController extends BaseController
$data = [
'podcast' => $this->podcast,
'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks($this->podcast->id, $platformType),
'platforms' => (new PlatformModel())->getPlatformsWithData($this->podcast->id, $platformType),
];
replace_breadcrumb_params([
@ -64,8 +67,7 @@ class PodcastPlatformController extends BaseController
$platformModel = new PlatformModel();
$validation = Services::validation();
$podcastsPlatformsData = [];
$platformsData = [];
foreach (
$this->request->getPost('platforms')
as $platformSlug => $podcastPlatform
@ -80,30 +82,27 @@ class PodcastPlatformController extends BaseController
}
$podcastPlatformAccountId = trim((string) $podcastPlatform['account_id']);
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'account_id' => $podcastPlatformAccountId === '' ? null : $podcastPlatformAccountId,
'is_visible' => array_key_exists('visible', $podcastPlatform) &&
$platformsData[] = [
'podcast_id' => $this->podcast->id,
'type' => $platformType,
'slug' => $platformSlug,
'link_url' => $podcastPlatformUrl,
'account_id' => $podcastPlatformAccountId === '' ? null : $podcastPlatformAccountId,
'is_visible' => array_key_exists('visible', $podcastPlatform) &&
$podcastPlatform['visible'] === 'yes',
'is_on_embed' => array_key_exists(
'on_embed',
$podcastPlatform
) && $podcastPlatform['on_embed'] === 'yes',
];
}
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType, $podcastsPlatformsData);
$platformModel->savePlatforms($this->podcast->id, $platformType, $platformsData);
return redirect()
->back()
->with('message', lang('Platforms.messages.updateSuccess'));
}
public function removePodcastPlatform(string $platformSlug): RedirectResponse
public function removePlatform(string $platformType, string $platformSlug): RedirectResponse
{
(new PlatformModel())->removePodcastPlatform($this->podcast->id, $platformSlug);
(new PlatformModel())->removePlatform($this->podcast->id, $platformType, $platformSlug);
return redirect()
->back()

View File

@ -8,20 +8,20 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
namespace App\Entities;
namespace Modules\Platforms\Entities;
use CodeIgniter\Entity\Entity;
/**
* @property int $podcast_id
* @property string $slug
* @property string $type
* @property string $label
* @property string $link_url
* @property string|null $account_id
* @property bool $is_visible
* @property string $home_url
* @property string|null $submit_url
* @property string|null $link_url
* @property string|null $account_id
* @property bool|null $is_visible
* @property bool|null $is_on_embed
*/
class Platform extends Entity
{
@ -29,14 +29,14 @@ class Platform extends Entity
* @var array<string, string>
*/
protected $casts = [
'slug' => 'string',
'type' => 'string',
'label' => 'string',
'home_url' => 'string',
'submit_url' => '?string',
'link_url' => '?string',
'account_id' => '?string',
'is_visible' => '?boolean',
'is_on_embed' => '?boolean',
'podcast_id' => 'int',
'slug' => 'string',
'type' => 'string',
'label' => 'string',
'link_url' => 'string',
'account_id' => '?string',
'is_visible' => 'boolean',
'home_url' => 'string',
'submit_url' => '?string',
];
}

View File

@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
/**
* Class PlatformModel Model for platforms table in database
*
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Modules\Platforms\Models;
use CodeIgniter\Model;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Platforms;
class PlatformModel extends Model
{
/**
* @var string
*/
protected $table = 'platforms';
/**
* @var string
*/
protected $primaryKey = 'id';
/**
* @var string[]
*/
protected $allowedFields = ['podcast_id', 'type', 'slug', 'link_url', 'account_id', 'is_visible'];
/**
* @var string
*/
protected $returnType = Platform::class;
/**
* @var bool
*/
protected $useSoftDeletes = false;
/**
* @var bool
*/
protected $useTimestamps = false;
/**
* @return Platform[]
*/
public function getPlatformsWithData(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}_withData";
if (! ($found = cache($cacheName))) {
$platforms = new Platforms();
$found = $this->getPlatforms($podcastId, $platformType);
$platformsData = $platforms->getPlatformsByType($platformType);
$knownSlugs = [];
foreach ($found as $podcastPlatform) {
$knownSlugs[] = $podcastPlatform->slug;
}
foreach ($platformsData as $slug => $platform) {
if (! in_array($slug, $knownSlugs, true)) {
$found[] = new Platform([
'podcast_id' => $podcastId,
'slug' => $slug,
'type' => $platformType,
'label' => $platform['label'],
'home_url' => $platform['home_url'],
'submit_url' => $platform['submit_url'],
'link_url' => '',
'account_id' => null,
'is_visible' => false,
]);
}
}
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return Platform[]
*/
public function getPlatforms(int $podcastId, string $platformType): array
{
$cacheName = "podcast#{$podcastId}_platforms_{$platformType}";
if (! ($found = cache($cacheName))) {
$platforms = new Platforms();
/** @var Platform[] $found */
$found = $this
->where('podcast_id', $podcastId)
->where('type', $platformType)
->orderBy('slug')
->findAll();
foreach ($found as $platform) {
$platformData = $platforms->findPlatformBySlug($platformType, $platform->slug);
if ($platformData === null) {
// delete platform, it does not correspond to any existing one
$this->delete($platform->id);
continue;
}
$platform->type = $platformType;
$platform->label = $platformData['label'];
$platform->home_url = $platformData['home_url'];
$platform->submit_url = $platformData['submit_url'];
}
cache()
->save($cacheName, $found, DECADE);
}
return $found;
}
/**
* @return int|false Number of rows inserted or FALSE on failure
*/
public function savePlatforms(int $podcastId, string $platformType, array $data): int | false
{
$this->clearCache($podcastId);
$platforms = new Platforms();
$platformsData = $platforms->getPlatformsByType($platformType);
$this->builder()
->whereIn('slug', array_keys($platformsData))
->delete();
if ($data === []) {
// no rows inserted
return 0;
}
return $this->insertBatch($data);
}
public function removePlatform(int $podcastId, string $platformType, string $platformSlug): bool | string
{
$this->clearCache($podcastId);
return $this->builder()
->delete([
'podcast_id' => $podcastId,
'type' => $platformType,
'slug' => $platformSlug,
]);
}
public function clearCache(int $podcastId): void
{
cache()->deleteMatching("podcast#{$podcastId}_platforms_*");
// delete localized podcast page cache
cache()
->deleteMatching("page_podcast#{$podcastId}*");
// delete post and episode comments pages cache
cache()
->deleteMatching('page_post*');
cache()
->deleteMatching('page_episode#*');
}
}

View File

@ -8,11 +8,9 @@ use AdAures\PodcastPersonsTaxonomy\ReversedTaxonomy;
use App\Entities\Episode;
use App\Entities\Location;
use App\Entities\Person;
use App\Entities\Platform;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
@ -23,6 +21,8 @@ use Exception;
use League\HTMLToMarkdown\HtmlConverter;
use Modules\Auth\Config\AuthGroups;
use Modules\Auth\Models\UserModel;
use Modules\Platforms\Models\PlatformModel;
use Modules\Platforms\Platforms;
use Modules\PodcastImport\Entities\PodcastImportTask;
use Modules\PodcastImport\Entities\TaskStatus;
use PodcastFeed\PodcastFeed;
@ -52,9 +52,9 @@ class PodcastImport extends BaseCommand
$importQueue = get_import_tasks();
$currentImport = current(array_filter($importQueue, static function ($task): bool {
return $task->status === TaskStatus::Running;
}));
$currentImport = current(
array_filter($importQueue, static fn ($task): bool => $task->status === TaskStatus::Running)
);
if ($currentImport instanceof PodcastImportTask) {
$currentImport->syncWithProcess();
@ -68,9 +68,7 @@ class PodcastImport extends BaseCommand
}
// Get the next queued import
$queuedImports = array_filter($importQueue, static function ($task): bool {
return $task->status === TaskStatus::Queued;
});
$queuedImports = array_filter($importQueue, static fn ($task): bool => $task->status === TaskStatus::Queued);
$nextImport = end($queuedImports);
if (! $nextImport instanceof PodcastImportTask) {
@ -392,27 +390,32 @@ class PodcastImport extends BaseCommand
],
];
$platforms = new Platforms();
$platformModel = new PlatformModel();
foreach ($platformTypes as $platformType) {
$podcastsPlatformsData = [];
$platformsData = [];
$currPlatformStep = 1; // for progress
CLI::write($platformType['name'] . ' - ' . $platformType['count'] . ' elements');
foreach ($platformType['elements'] as $platform) {
CLI::showProgress($currPlatformStep++, $platformType['count']);
$platformLabel = $platform->getAttribute('platform');
$platformSlug = slugify((string) $platformLabel);
if ($platformModel->getPlatform($platformSlug) instanceof Platform) {
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $platform->getAttribute($platformType['account_url_key']),
'account_id' => $platform->getAttribute($platformType['account_id_key']),
'is_visible' => false,
];
$platformSlug = $platform->getAttribute('platform');
$platformData = $platforms->findPlatformBySlug($platformType['name'], $platformSlug);
if ($platformData === null) {
continue;
}
$platformsData[] = [
'podcast_id' => $this->podcast->id,
'type' => $platformType['name'],
'slug' => $platformSlug,
'link_url' => $platform->getAttribute($platformType['account_url_key']),
'account_id' => $platform->getAttribute($platformType['account_id_key']),
'is_visible' => false,
];
}
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType['name'], $podcastsPlatformsData);
$platformModel->savePlatforms($this->podcast->id, $platformType['name'], $platformsData);
CLI::showProgress(false);
}
}
@ -522,9 +525,7 @@ class PodcastImport extends BaseCommand
->get()
->getResultArray();
return array_map(static function (array $element) {
return $element['guid'];
}, $result);
return array_map(static fn (array $element) => $element['guid'], $result);
}
/**

View File

@ -26,11 +26,10 @@ if (! function_exists('get_import_tasks')) {
}
if ($podcastHandle !== null) {
$podcastImportsQueue = array_filter($podcastImportsQueue, static function ($importTask) use (
$podcastHandle
): bool {
return $importTask->handle === $podcastHandle;
});
$podcastImportsQueue = array_filter(
$podcastImportsQueue,
static fn ($importTask): bool => $importTask->handle === $podcastHandle
);
}
usort($podcastImportsQueue, static function (PodcastImportTask $a, PodcastImportTask $b): int {

View File

@ -30,16 +30,16 @@
"dependencies": {
"@amcharts/amcharts4": "^4.10.38",
"@amcharts/amcharts4-geodata": "^4.1.28",
"@codemirror/commands": "^6.3.3",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/commands": "^6.5.0",
"@codemirror/lang-xml": "^6.1.0",
"@codemirror/language": "^6.10.1",
"@codemirror/state": "^6.4.1",
"@codemirror/view": "^6.24.1",
"@codemirror/view": "^6.26.3",
"@floating-ui/dom": "^1.6.3",
"@github/clipboard-copy-element": "^1.3.0",
"@github/hotkey": "^3.1.0",
"@github/markdown-toolbar-element": "^2.2.1",
"@github/relative-time-element": "^4.3.1",
"@github/markdown-toolbar-element": "^2.2.3",
"@github/relative-time-element": "^4.4.0",
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
"@vime/core": "^5.4.1",
"choices.js": "^10.2.0",
@ -47,28 +47,28 @@
"flatpickr": "^4.6.13",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"lit": "^3.1.2",
"marked": "^12.0.0",
"wavesurfer.js": "^7.7.3",
"lit": "^3.1.3",
"marked": "^12.0.2",
"wavesurfer.js": "^7.7.11",
"xml-formatter": "^3.6.2"
},
"devDependencies": {
"@commitlint/cli": "^19.0.3",
"@commitlint/config-conventional": "^19.0.3",
"@csstools/css-tokenizer": "^2.2.3",
"@commitlint/cli": "^19.3.0",
"@commitlint/config-conventional": "^19.2.2",
"@csstools/css-tokenizer": "^2.2.4",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"@semantic-release/gitlab": "^13.0.3",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@types/leaflet": "^1.9.8",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@tailwindcss/typography": "^0.5.12",
"@types/leaflet": "^1.9.12",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"all-contributors-cli": "^6.26.1",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"cssnano": "^6.0.3",
"cssnano": "^6.1.2",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
@ -76,25 +76,25 @@
"husky": "^9.0.11",
"is-ci": "^3.0.1",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"postcss-import": "^16.0.1",
"postcss-nesting": "^12.0.4",
"postcss-preset-env": "^9.4.0",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"postcss-nesting": "^12.1.2",
"postcss-preset-env": "^9.5.9",
"postcss-reporter": "^7.1.0",
"prettier": "3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"semantic-release": "^23.0.2",
"stylelint": "^16.2.1",
"semantic-release": "^23.0.8",
"stylelint": "^16.4.0",
"stylelint-config-standard": "^36.0.0",
"svgo": "^3.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.4",
"vite-plugin-pwa": "^0.19.2",
"workbox-build": "^7.0.0",
"workbox-core": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0"
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.10",
"vite-plugin-pwa": "^0.19.8",
"workbox-build": "^7.1.0",
"workbox-core": "^7.1.0",
"workbox-routing": "^7.1.0",
"workbox-strategies": "^7.1.0"
},
"lint-staged": {
"*.{js,ts,css,md,json}": "prettier --write",

View File

@ -1,7 +1,7 @@
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
bootstrap="system/Test/bootstrap.php"
bootstrap="vendor/codeigniter4/framework/system/Test/bootstrap.php"
backupGlobals="false"
beStrictAboutOutputDuringTests="true"
colors="true"
@ -57,13 +57,4 @@
<env name="database.tests.DBPrefix" value="tests_"/>
<env name="restapi.enabled" value="true"/>
</php>
<source>
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory suffix=".php">./app/Views</directory>
<file>./app/Config/Routes.php</file>
</exclude>
</source>
</phpunit>

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
declare(strict_types=1);
use Rector\CodeQuality\Rector\ClassMethod\ExplicitReturnNullRector;
use Rector\CodingStyle\Rector\Encapsed\EncapsedStringsToSprintfRector;
use Rector\CodingStyle\Rector\Stmt\NewlineAfterStatementRector;
use Rector\CodingStyle\Rector\String_\SymplifyQuoteEscapeRector;
@ -12,31 +13,22 @@ use Rector\EarlyReturn\Rector\If_\ChangeAndIfToEarlyReturnRector;
use Rector\EarlyReturn\Rector\If_\ChangeOrIfContinueToMultiContinueRector;
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
use Rector\Php71\Rector\FuncCall\RemoveExtraParametersRector;
use Rector\Set\ValueObject\SetList;
use Rector\ValueObject\PhpVersion;
return static function (RectorConfig $rectorConfig): void {
$rectorConfig->paths([__DIR__ . '/app', __DIR__ . '/modules', __DIR__ . '/tests', __DIR__ . '/public']);
// do you need to include constants, class aliases or custom autoloader? files listed will be executed
$rectorConfig->bootstrapFiles([__DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php']);
// Define what rule sets will be applied
$rectorConfig->sets([
SetList::PHP_81,
SetList::TYPE_DECLARATION,
SetList::CODE_QUALITY,
SetList::CODING_STYLE,
SetList::EARLY_RETURN,
SetList::DEAD_CODE,
]);
// auto import fully qualified class names
$rectorConfig->importNames();
$rectorConfig->phpVersion(PhpVersion::PHP_81);
$rectorConfig->skip([
return RectorConfig::configure()
->withPaths([__DIR__ . '/app', __DIR__ . '/modules', __DIR__ . '/tests', __DIR__ . '/public'])
->withBootstrapFiles([__DIR__ . '/vendor/codeigniter4/framework/system/Test/bootstrap.php'])
->withPhpVersion(PhpVersion::PHP_81)
->withPhpSets(php81: true)
->withPreparedSets(
typeDeclarations: true,
codeQuality: true,
codingStyle: true,
earlyReturn: true,
deadCode: true,
)
->withImportNames(true, true, true, true)
->withSkip([
// .mp3 files were somehow processed by rector, so skip all media files
__DIR__ . '/public/media/*',
@ -50,6 +42,7 @@ return static function (RectorConfig $rectorConfig): void {
EncapsedStringsToSprintfRector::class,
RemoveExtraParametersRector::class,
UnwrapFutureCompatibleIfPhpVersionRector::class,
ExplicitReturnNullRector::class,
// skip rule in specific directory
StringClassNameToClassConstantRector::class => [
@ -65,9 +58,5 @@ return static function (RectorConfig $rectorConfig): void {
],
ChangeAndIfToEarlyReturnRector::class => [__DIR__ . '/modules/Install/Controllers/InstallController.php'],
]);
// Path to phpstan with extensions, that PHPStan in Rector uses to determine types
$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon');
$rectorConfig->phpstanConfigs(['vendor/codeigniter/phpstan-codeigniter/extension.neon']);
};
])
->withPHPStanConfigs([__DIR__ . '/phpstan.neon', 'vendor/codeigniter/phpstan-codeigniter/extension.neon']);

2
spark
View File

@ -1,6 +1,8 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* This file is part of CodeIgniter 4 framework.
*

View File

@ -117,6 +117,7 @@ module.exports = {
cards: "repeat(auto-fill, minmax(14rem, 1fr))",
latestEpisodes: "repeat(5, 1fr)",
colorButtons: "repeat(auto-fill, minmax(4rem, 1fr))",
platforms: "repeat(auto-fill, minmax(18rem, 1fr))",
},
gridTemplateRows: {
admin: "40px 1fr",

View File

@ -36,8 +36,6 @@ namespace Tests\Support\Libraries;
use Config\App;
/**
* Class ConfigReader
*
* An extension of BaseConfig that prevents the constructor from loading external values. Used to read actual local
* values from a config file.
*/

View File

@ -205,7 +205,16 @@
as="XMLEditor"
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>" />
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>"
rows="8" />
<Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
hint="<?= esc(lang('Podcast.form.verify_txt_hint')) ?>"
helper="<?= esc(lang('Podcast.form.verify_txt_helper')) ?>"
rows="5" />
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="true" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?>

View File

@ -239,25 +239,36 @@
name="custom_rss"
label="<?= esc(lang('Podcast.form.custom_rss')) ?>"
hint="<?= esc(lang('Podcast.form.custom_rss_hint')) ?>"
content="<?= esc($podcast->custom_rss_string) ?>" />
content="<?= esc($podcast->custom_rss_string) ?>"
rows="8" />
<Forms.Field
name="new_feed_url"
type="url"
label="<?= esc(lang('Podcast.form.new_feed_url')) ?>"
hint="<?= esc(lang('Podcast.form.new_feed_url_hint')) ?>"
value="<?= esc($podcast->new_feed_url) ?>"
/>
<Forms.Field
as="Textarea"
name="verify_txt"
label="<?= esc(lang('Podcast.form.verify_txt')) ?>"
hint="<?= esc(lang('Podcast.form.verify_txt_hint')) ?>"
helper="<?= esc(lang('Podcast.form.verify_txt_helper')) ?>"
value="<?= esc($podcast->verify_txt) ?>"
rows="5" />
<Forms.Field
name="new_feed_url"
type="url"
label="<?= esc(lang('Podcast.form.new_feed_url')) ?>"
hint="<?= esc(lang('Podcast.form.new_feed_url_hint')) ?>"
value="<?= esc($podcast->new_feed_url) ?>"
/>
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="<?= $podcast->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="<?= $podcast->is_completed ? 'true' : 'false' ?>">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.lock_hint')) ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="<?= $podcast->is_blocked ? 'true' : 'false' ?>" hint="<?= esc(lang('Podcast.form.block_hint')) ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="<?= $podcast->is_completed ? 'true' : 'false' ?>">
<?= lang('Podcast.form.complete') ?>
</Forms.Toggler>
</Forms.Section>
</div>

View File

@ -14,9 +14,10 @@
<?= $this->section('content') ?>
<form id="platforms-form" action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="flex flex-col max-w-md gap-y-8">
<form id="platforms-form" action="<?= route_to('platforms-save', $podcast->id, $platformType) ?>" method="POST" class="grid w-full gap-4 lg:gap-8 grid-cols-platforms">
<?= csrf_field() ?>
<?php foreach ($platforms as $platform): ?>
<div class="relative flex-col items-start p-4 rounded-lg bg-elevated border-3 <?= $platform->link_url ? 'border-accent-base' : 'border-subtle' ?>">
@ -24,7 +25,8 @@
route_to(
'podcast-platform-remove',
$podcast->id,
esc($platform->slug),
$platform->type,
$platform->slug,
),
icon('delete-bin', 'mx-auto'),
[
@ -36,7 +38,7 @@
],
)
: '' ?>
<div class="flex items-center gap-x-4">
<div class="flex items-center gap-x-2">
<?= icon(
esc($platform->slug),
'text-skin-muted text-4xl',
@ -45,29 +47,15 @@
<h2 class="text-xl font-semibold"><?= $platform->label ?></h2>
</div>
<div class="flex flex-col flex-1 mt-4">
<div class="inline-flex ml-12 gap-x-2">
<?= anchor($platform->home_url, icon('external-link', 'mx-auto') . lang('Platforms.website'), [
'class' => 'gap-x-1 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent px-2 py-1 text-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'target' => '_blank',
'rel' => 'noopener noreferrer',
'data-tooltip' => 'bottom',
'title' => lang('Platforms.home_url', [
<div class="inline-flex ml-8 -mt-6 gap-x-1">
<Button uri="<?= $platform->home_url ?>" variant="link" size="small" target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.home_url', [
'platformName' => $platform->label,
]) ?>" data-tooltip="bottom"><?= lang('Platforms.website') ?></Button>
<?php if ($platform->submit_url !== null): ?>
<Button uri="<?= $platform->submit_url ?>" variant="link" size="small" target="_blank" rel="noopener noreferrer" title="<?= lang('Platforms.submit_url', [
'platformName' => $platform->label,
]),
]) ?>
<?= $platform->submit_url ? anchor(
$platform->submit_url,
icon('add') . lang('Platforms.register'),
[
'class' => 'gap-x-1 flex-shrink-0 inline-flex items-center justify-center font-semibold shadow-xs rounded-full focus:ring-accent px-2 py-1 text-sm border-2 border-accent-base text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover',
'target' => '_blank',
'rel' => 'noopener noreferrer',
'data-tooltip' => 'bottom',
'title' => lang('Platforms.submit_url', [
'platformName' => $platform->label,
]),
]
) : '' ?>
]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></Button>
<?php endif; ?>
</div>
<fieldset>
<Forms.Field