refactor: add rector to enforce type declarations, code quality + style and remove dead code

- update CI process to include quality stage (tests + code review)
- add captainhook to install git pre-commit & pre-push hooks
- remove .devcontainer Dockerfile to use project's docker-compose services: all
services can now be started automatically using vscode
- update docs/setup-development.md
This commit is contained in:
Yassine Doghri 2021-05-06 14:00:48 +00:00
parent a54a5964c3
commit 5c5c6da4be
No known key found for this signature in database
GPG Key ID: 3E7F89498B960C9F
302 changed files with 8813 additions and 3685 deletions

View File

@ -1,12 +0,0 @@
FROM php:7.3-fpm
COPY --from=composer /usr/bin/composer /usr/bin/composer
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && \
apt-get install -y nodejs
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git vim

View File

@ -1,8 +1,11 @@
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/docker-existing-dockerfile
{
"name": "Existing Dockerfile",
"dockerFile": "./Dockerfile",
"name": "Castopod Host dev",
"dockerComposeFile": ["../docker-compose.yml"],
"service": "app",
"workspaceFolder": "/castopod-host",
"postCreateCommand": "cron && php spark serve --host 0.0.0.0",
"settings": {
"terminal.integrated.shell.linux": "/bin/bash",
"editor.formatOnSave": true,
@ -13,18 +16,18 @@
"color-highlight.markerType": "dot-before"
},
"extensions": [
"mikestead.dotenv",
"bmewburn.vscode-intelephense-client",
"streetsidesoftware.code-spell-checker",
"naumovs.color-highlight",
"heybourn.headwind",
"wayou.vscode-todo-highlight",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"jamesbirtles.svelte-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"wongjn.php-sniffer",
"eamodio.gitlens"
]
"mikestead.dotenv",
"bmewburn.vscode-intelephense-client",
"streetsidesoftware.code-spell-checker",
"naumovs.color-highlight",
"heybourn.headwind",
"wayou.vscode-todo-highlight",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"jamesbirtles.svelte-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"wongjn.php-sniffer",
"eamodio.gitlens"
]
}

View File

@ -1,6 +1,7 @@
image: php:7.3-fpm
stages:
- quality
- bundle
- release
@ -31,16 +32,30 @@ before_script:
- curl -sL https://deb.nodesource.com/setup_12.x | bash -
- apt-get update && apt-get install -y nodejs
# Install php and js dependencies
- php composer.phar install --no-dev --ignore-platform-reqs
# Install all php and js dependencies
- php composer.phar install --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs
- npm install
# build all UI assets
- npm run build
tests:
stage: quality
script:
- vendor/bin/phpunit
code-review:
stage: quality
script:
# run rector
- vendor/bin/rector process --dry-run
bundle_app:
stage: bundle
script:
# remove dev dependencies using the --no-dev option
- php composer.phar install --no-dev --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs
# build all UI assets
- npm run build
# download GeoLite2-City archive and extract it to writable/uploads
- wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/
@ -65,8 +80,8 @@ release_app:
- apt-get install jq -y
- apt-get install zip -y
# make prepare-release.sh executable
- chmod +x ./prepare-release.sh
# make scripts/prepare-release.sh executable
- chmod +x ./scripts/prepare-release.sh
# IMPORTANT: delete local git tags before release to prevent eventual script failure (ie. tag already exists)
- git tag | xargs git tag -d

View File

@ -11,7 +11,7 @@
[
"@semantic-release/exec",
{
"prepareCmd": "./prepare-release.sh ${nextRelease.version}"
"prepareCmd": "./scripts/prepare-release.sh ${nextRelease.version}"
}
],
"@semantic-release/npm",

View File

@ -1,8 +1,31 @@
####################################################
# Castopod Host development Docker file
####################################################
# NOT optimized for production
# should be used only for development purposes
####################################################
FROM php:7.3-fpm
LABEL maintainer="Yassine Doghri<yassine@podlibre.org>"
COPY . /castopod-host
WORKDIR /castopod-host
# Install composer
COPY --from=composer /usr/bin/composer /usr/bin/composer
# Install npm
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash -
RUN apt-get update && \
apt-get install -y nodejs
# Install git + vim
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y git vim
### Install CodeIgniter's server requirements
#-- https://github.com/codeigniter4/appstarter#server-requirements

View File

@ -4,31 +4,13 @@ namespace App\Authorization;
class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
{
//--------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------
/**
* Checks a group to see if they have the specified permission.
*
* @param int|string $permission
* @param int $groupId
*
* @return mixed
*/
public function groupHasPermission($permission, int $groupId)
public function groupHasPermission($permission, int $groupId): bool
{
if (
empty($permission) ||
(!is_string($permission) && !is_numeric($permission))
) {
return null;
}
if (empty($groupId) || !is_numeric($groupId)) {
return null;
}
// Get the Permission ID
$permissionId = $this->getPermissionID($permission);
@ -36,36 +18,23 @@ class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
return false;
}
if (
$this->permissionModel->doesGroupHavePermission(
$groupId,
(int) $permissionId
)
) {
return true;
}
return false;
return (bool) $this->permissionModel->doesGroupHavePermission(
$groupId,
$permissionId,
);
}
/**
* Makes user part of given groups.
*
* @param $userId
* @param array|null $groups // Either collection of ID or names
*
* @return bool
* @param array $groups Either collection of ID or names
*/
public function setUserGroups(int $userId, $groups)
public function setUserGroups(int $userId, array $groups = []): bool
{
if (empty($userId) || !is_numeric($userId)) {
return null;
}
// remove user from all groups before resetting it in new groups
$this->groupModel->removeUserFromAllGroups($userId);
if (empty($groups)) {
if ($groups = []) {
return true;
}

View File

@ -4,14 +4,20 @@ namespace App\Authorization;
class GroupModel extends \Myth\Auth\Authorization\GroupModel
{
public function getContributorRoles()
/**
* @return mixed[]
*/
public function getContributorRoles(): array
{
return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after')
->findAll();
}
public function getUserRoles()
/**
* @return mixed[]
*/
public function getUserRoles(): array
{
return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after')

View File

@ -7,11 +7,6 @@ class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
/**
* Checks to see if a user, or one of their groups,
* has a specific permission.
*
* @param $userId
* @param $permissionId
*
* @return bool
*/
public function doesGroupHavePermission(
int $groupId,
@ -33,9 +28,7 @@ class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
* id => name
* ]
*
* @param int $groupId
*
* @return array
* @return array<int, string>
*/
public function getPermissionsForGroup(int $groupId): array
{

View File

@ -26,7 +26,10 @@ class Analytics extends AnalyticsBase
$this->gateway = config('App')->adminGateway . '/analytics';
}
public function getAudioFileUrl($audioFilePath)
/**
* get the full audio file url
*/
public function getAudioFileUrl(string $audioFilePath): string
{
helper('media');

View File

@ -2,6 +2,7 @@
namespace Config;
use CodeIgniter\Session\Handlers\FileHandler;
use CodeIgniter\Config\BaseConfig;
class App extends BaseConfig
@ -34,6 +35,8 @@ class App extends BaseConfig
* WITH a trailing slash:
*
* http://cdn.example.com/
*
* @var string
*/
public $mediaBaseURL = 'http://127.0.0.2:8080/';
@ -163,7 +166,7 @@ class App extends BaseConfig
*
* @var string
*/
public $sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler';
public $sessionDriver = FileHandler::class;
/**
* --------------------------------------------------------------------------
@ -480,6 +483,8 @@ class App extends BaseConfig
* Media root folder
* --------------------------------------------------------------------------
* Defines the root folder for media files storage
*
* @var string
*/
public $mediaRoot = 'media';
@ -488,6 +493,8 @@ class App extends BaseConfig
* Admin gateway
* --------------------------------------------------------------------------
* Defines a base route for all admin pages
*
* @var string
*/
public $adminGateway = 'cp-admin';
@ -496,6 +503,8 @@ class App extends BaseConfig
* Auth gateway
* --------------------------------------------------------------------------
* Defines a base route for all authentication related pages
*
* @var string
*/
public $authGateway = 'cp-auth';
@ -504,6 +513,8 @@ class App extends BaseConfig
* Install gateway
* --------------------------------------------------------------------------
* Defines a base route for instance installation
*
* @var string
*/
public $installGateway = 'cp-install';
}

View File

@ -4,10 +4,13 @@ namespace Config;
class Auth extends \Myth\Auth\Config\Auth
{
//--------------------------------------------------------------------
// Views used by Auth Controllers
//--------------------------------------------------------------------
/**
* --------------------------------------------------------------------------
* Views used by Auth Controllers
* --------------------------------------------------------------------------
*
* @var array<string, string>
*/
public $views = [
'login' => 'auth/login',
'register' => 'auth/register',
@ -17,26 +20,35 @@ class Auth extends \Myth\Auth\Config\Auth
'emailActivation' => 'auth/emails/activation',
];
//--------------------------------------------------------------------
// Layout for the views to extend
//--------------------------------------------------------------------
/**
* --------------------------------------------------------------------------
* Layout for the views to extend
* --------------------------------------------------------------------------
*
* @var string
*/
public $viewLayout = 'auth/_layout';
//--------------------------------------------------------------------
// Allow User Registration
//--------------------------------------------------------------------
// When enabled (default) any unregistered user may apply for a new
// account. If you disable registration you may need to ensure your
// controllers and views know not to offer registration.
//
/**
* --------------------------------------------------------------------------
* Allow User Registration
* --------------------------------------------------------------------------
* When enabled (default) any unregistered user may apply for a new
* account. If you disable registration you may need to ensure your
* controllers and views know not to offer registration.
*
* @var bool
*/
public $allowRegistration = false;
//--------------------------------------------------------------------
// Require confirmation registration via email
//--------------------------------------------------------------------
// When enabled, every registered user will receive an email message
// with a special link he have to confirm to activate his account.
//
/**
* --------------------------------------------------------------------------
* Require confirmation registration via email
* --------------------------------------------------------------------------
* When enabled, every registered user will receive an email message
* with a special link he have to confirm to activate his account.
*
* @var bool
*/
public $requireActivation = false;
}

View File

@ -32,7 +32,7 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var string|null
*/
public $reportURI = null;
public $reportURI;
/**
* Instructs user agents to rewrite URL schemes, changing
@ -53,7 +53,7 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var string|string[]|null
*/
public $defaultSrc = null;
public $defaultSrc;
/**
* Lists allowed scripts' URLs.
@ -83,7 +83,7 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var string|string[]|null
*/
public $baseURI = null;
public $baseURI;
/**
* Lists the URLs for workers and embedded frame contents
@ -105,7 +105,7 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var string|string[]
*/
public $fontSrc = null;
public $fontSrc;
/**
* Lists valid endpoints for submission from `<form>` tags.
@ -122,14 +122,14 @@ class ContentSecurityPolicy extends BaseConfig
*
* @var string|string[]|null
*/
public $frameAncestors = null;
public $frameAncestors;
/**
* Restricts the origins allowed to deliver video and audio.
*
* @var string|string[]|null
*/
public $mediaSrc = null;
public $mediaSrc;
/**
* Allows control over Flash and other plugins.
@ -141,19 +141,19 @@ class ContentSecurityPolicy extends BaseConfig
/**
* @var string|string[]|null
*/
public $manifestSrc = null;
public $manifestSrc;
/**
* Limits the kinds of plugins a page may invoke.
*
* @var string|string[]|null
*/
public $pluginTypes = null;
public $pluginTypes;
/**
* List of actions allowed.
*
* @var string|string[]|null
*/
public $sandbox = null;
public $sandbox;
}

View File

@ -62,6 +62,7 @@ class Database extends Config
'username' => '',
'password' => '',
'database' => ':memory:',
/** @noRector StringClassNameToClassConstantRector */
'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
'pConnect' => false,

View File

@ -52,7 +52,7 @@ Events::on('pre_system', function () {
}
});
Events::on('login', function ($user) {
Events::on('login', function ($user): void {
helper('auth');
// set interact_as_actor_id value
@ -62,7 +62,7 @@ Events::on('login', function ($user) {
}
});
Events::on('logout', function ($user) {
Events::on('logout', function ($user): void {
helper('auth');
// remove user's interact_as_actor session
@ -75,7 +75,7 @@ Events::on('logout', function ($user) {
* --------------------------------------------------------------------
* Update episode metadata counts
*/
Events::on('on_note_add', function ($note) {
Events::on('on_note_add', function ($note): void {
if ($note->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
@ -87,7 +87,7 @@ Events::on('on_note_add', function ($note) {
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
});
Events::on('on_note_remove', function ($note) {
Events::on('on_note_remove', function ($note): void {
if ($note->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
@ -106,7 +106,7 @@ Events::on('on_note_remove', function ($note) {
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_note_reblog', function ($actor, $note) {
Events::on('on_note_reblog', function ($actor, $note): void {
if ($episodeId = $note->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
@ -125,7 +125,7 @@ Events::on('on_note_reblog', function ($actor, $note) {
}
});
Events::on('on_note_undo_reblog', function ($reblogNote) {
Events::on('on_note_undo_reblog', function ($reblogNote): void {
$note = $reblogNote->reblog_of_note;
if ($episodeId = $note->episode_id) {
model('EpisodeModel')
@ -147,21 +147,21 @@ Events::on('on_note_undo_reblog', function ($reblogNote) {
}
});
Events::on('on_note_reply', function ($reply) {
Events::on('on_note_reply', function ($reply): void {
$note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_reply_remove', function ($reply) {
Events::on('on_reply_remove', function ($reply): void {
$note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->id}*");
});
Events::on('on_note_favourite', function ($actor, $note) {
Events::on('on_note_favourite', function ($actor, $note): void {
if ($note->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
@ -176,7 +176,7 @@ Events::on('on_note_favourite', function ($actor, $note) {
}
});
Events::on('on_note_undo_favourite', function ($actor, $note) {
Events::on('on_note_undo_favourite', function ($actor, $note): void {
if ($note->episode_id) {
model('EpisodeModel')
->where('id', $note->episode_id)
@ -191,22 +191,22 @@ Events::on('on_note_undo_favourite', function ($actor, $note) {
}
});
Events::on('on_block_actor', function ($actorId) {
Events::on('on_block_actor', function ($actorId): void {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_unblock_actor', function ($actorId) {
Events::on('on_unblock_actor', function ($actorId): void {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_block_domain', function ($domainName) {
Events::on('on_block_domain', function ($domainName): void {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});
Events::on('on_unblock_domain', function ($domainName) {
Events::on('on_unblock_domain', function ($domainName): void {
cache()->deleteMatching('page_podcast*');
cache()->deleteMatching('page_note*');
});

View File

@ -2,6 +2,10 @@
namespace Config;
use Myth\Auth\Filters\LoginFilter;
use Myth\Auth\Filters\RoleFilter;
use App\Filters\PermissionFilter;
use ActivityPub\Filters\ActivityPubFilter;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar;
@ -19,10 +23,10 @@ class Filters extends BaseConfig
'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class,
'login' => \Myth\Auth\Filters\LoginFilter::class,
'role' => \Myth\Auth\Filters\RoleFilter::class,
'permission' => \App\Filters\PermissionFilter::class,
'activity-pub' => \ActivityPub\Filters\ActivityPubFilter::class,
'login' => LoginFilter::class,
'role' => RoleFilter::class,
'permission' => PermissionFilter::class,
'activity-pub' => ActivityPubFilter::class,
];
/**

View File

@ -2,6 +2,8 @@
namespace Config;
use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\FormatterInterface;
@ -40,9 +42,9 @@ class Format extends BaseConfig
* @var array<string, string>
*/
public $formatters = [
'application/json' => 'CodeIgniter\Format\JSONFormatter',
'application/xml' => 'CodeIgniter\Format\XMLFormatter',
'text/xml' => 'CodeIgniter\Format\XMLFormatter',
'application/json' => JSONFormatter::class,
'application/xml' => XMLFormatter::class,
'text/xml' => XMLFormatter::class,
];
/**
@ -62,17 +64,12 @@ class Format extends BaseConfig
];
//--------------------------------------------------------------------
/**
* A Factory method to return the appropriate formatter for the given mime type.
*
* @param string $mime
*
* @return FormatterInterface
*
* @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
*/
public function getFormatter(string $mime)
public function getFormatter(string $mime): FormatterInterface
{
return Services::format()->getFormatter($mime);
}

View File

@ -23,39 +23,70 @@ class Kint extends BaseConfig
|--------------------------------------------------------------------------
*/
public $plugins = null;
public $plugins;
/**
* @var int
*/
public $maxDepth = 6;
/**
* @var bool
*/
public $displayCalledFrom = true;
/**
* @var bool
*/
public $expanded = false;
/*
|--------------------------------------------------------------------------
| RichRenderer Settings
|--------------------------------------------------------------------------
*/
|--------------------------------------------------------------------------
| RichRenderer Settings
|--------------------------------------------------------------------------
*/
/**
* @var string
*/
public $richTheme = 'aante-light.css';
/**
* @var bool
*/
public $richFolder = false;
/**
* @var int
*/
public $richSort = Renderer::SORT_FULL;
public $richObjectPlugins = null;
public $richObjectPlugins;
public $richTabPlugins = null;
public $richTabPlugins;
/*
|--------------------------------------------------------------------------
| CLI Settings
|--------------------------------------------------------------------------
*/
|--------------------------------------------------------------------------
| CLI Settings
|--------------------------------------------------------------------------
*/
/**
* @var bool
*/
public $cliColors = true;
/**
* @var bool
*/
public $cliForceUTF8 = false;
/**
* @var bool
*/
public $cliDetectWidth = true;
/**
* @var int
*/
public $cliMinWidth = 40;
}

View File

@ -2,6 +2,7 @@
namespace Config;
use CodeIgniter\Log\Handlers\FileHandler;
use CodeIgniter\Config\BaseConfig;
class Logger extends BaseConfig
@ -82,7 +83,7 @@ class Logger extends BaseConfig
* File Handler
* --------------------------------------------------------------------
*/
'CodeIgniter\Log\Handlers\FileHandler' => [
FileHandler::class => [
/*
* The log levels that this handler will handle.
*/

View File

@ -22,7 +22,7 @@ class Mimes
/**
* Map of extensions to mime types.
*
* @var array
* @var array<string, string>
*/
public static $mimes = [
'hqx' => [
@ -321,11 +321,9 @@ class Mimes
/**
* Attempts to determine the best mime type for the given file extension.
*
* @param string $extension
*
* @return string|null The mime type found, or none if unable to determine.
*/
public static function guessTypeFromExtension(string $extension)
public static function guessTypeFromExtension(string $extension): ?string
{
$extension = trim(strtolower($extension), '. ');
@ -341,15 +339,13 @@ class Mimes
/**
* Attempts to determine the best file extension for a given mime type.
*
* @param string $type
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type)
*
* @return string|null The extension determined, or null if unable to match.
*/
public static function guessExtensionFromType(
string $type,
string $proposedExtension = null
) {
): ?string {
$type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension));

View File

@ -54,7 +54,7 @@ $routes->addPlaceholder(
$routes->get('/', 'Home::index', ['as' => 'home']);
// Install Wizard route
$routes->group(config('App')->installGateway, function ($routes) {
$routes->group(config('App')->installGateway, function ($routes): void {
$routes->get('/', 'Install', ['as' => 'install']);
$routes->post('instance-config', 'Install::attemptInstanceConfig', [
'as' => 'instance-config',
@ -76,12 +76,12 @@ $routes->get('.well-known/platforms', 'Platform');
$routes->group(
config('App')->adminGateway,
['namespace' => 'App\Controllers\Admin'],
function ($routes) {
function ($routes): void {
$routes->get('/', 'Home', [
'as' => 'admin',
]);
$routes->group('persons', function ($routes) {
$routes->group('persons', function ($routes): void {
$routes->get('/', 'Person', [
'as' => 'person-list',
'filter' => 'permission:person-list',
@ -93,7 +93,7 @@ $routes->group(
$routes->post('new', 'Person::attemptCreate', [
'filter' => 'permission:person-create',
]);
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'Person::view/$1', [
'as' => 'person-view',
'filter' => 'permission:person-view',
@ -113,7 +113,7 @@ $routes->group(
});
// Podcasts
$routes->group('podcasts', function ($routes) {
$routes->group('podcasts', function ($routes): void {
$routes->get('/', 'Podcast::list', [
'as' => 'podcast-list',
]);
@ -134,7 +134,7 @@ $routes->group(
// Podcast
// Use ids in admin area to help permission and group lookups
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'Podcast::view/$1', [
'as' => 'podcast-view',
'filter' => 'permission:podcasts-view,podcast-view',
@ -151,7 +151,7 @@ $routes->group(
'filter' => 'permission:podcasts-delete',
]);
$routes->group('persons', function ($routes) {
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PodcastPerson/$1', [
'as' => 'podcast-person-manage',
'filter' => 'permission:podcast-edit',
@ -170,7 +170,7 @@ $routes->group(
);
});
$routes->group('analytics', function ($routes) {
$routes->group('analytics', function ($routes): void {
$routes->get('/', 'Podcast::viewAnalytics/$1', [
'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view',
@ -226,7 +226,7 @@ $routes->group(
});
// Podcast episodes
$routes->group('episodes', function ($routes) {
$routes->group('episodes', function ($routes): void {
$routes->get('/', 'Episode::list/$1', [
'as' => 'episode-list',
'filter' =>
@ -241,7 +241,7 @@ $routes->group(
]);
// Episode
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'Episode::view/$1/$2', [
'as' => 'episode-view',
'filter' =>
@ -349,7 +349,7 @@ $routes->group(
],
);
$routes->group('persons', function ($routes) {
$routes->group('persons', function ($routes): void {
$routes->get('/', 'EpisodePerson/$1/$2', [
'as' => 'episode-person-manage',
'filter' => 'permission:podcast_episodes-edit',
@ -376,7 +376,7 @@ $routes->group(
});
// Podcast contributors
$routes->group('contributors', function ($routes) {
$routes->group('contributors', function ($routes): void {
$routes->get('/', 'Contributor::list/$1', [
'as' => 'contributor-list',
'filter' =>
@ -391,7 +391,7 @@ $routes->group(
]);
// Contributor
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'Contributor::view/$1/$2', [
'as' => 'contributor-view',
'filter' =>
@ -418,7 +418,7 @@ $routes->group(
});
});
$routes->group('platforms', function ($routes) {
$routes->group('platforms', function ($routes): void {
$routes->get(
'/',
'PodcastPlatform::platforms/$1/podcasting',
@ -464,7 +464,7 @@ $routes->group(
});
// Instance wide Fediverse config
$routes->group('fediverse', function ($routes) {
$routes->group('fediverse', function ($routes): void {
$routes->get('/', 'Fediverse::dashboard', [
'as' => 'fediverse-dashboard',
]);
@ -479,7 +479,7 @@ $routes->group(
});
// Pages
$routes->group('pages', function ($routes) {
$routes->group('pages', function ($routes): void {
$routes->get('/', 'Page::list', ['as' => 'page-list']);
$routes->get('new', 'Page::create', [
'as' => 'page-create',
@ -489,7 +489,7 @@ $routes->group(
'filter' => 'permission:pages-manage',
]);
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'Page::view/$1', ['as' => 'page-view']);
$routes->get('edit', 'Page::edit/$1', [
'as' => 'page-edit',
@ -507,7 +507,7 @@ $routes->group(
});
// Users
$routes->group('users', function ($routes) {
$routes->group('users', function ($routes): void {
$routes->get('/', 'User::list', [
'as' => 'user-list',
'filter' => 'permission:users-list',
@ -521,7 +521,7 @@ $routes->group(
]);
// User
$routes->group('(:num)', function ($routes) {
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'User::view/$1', [
'as' => 'user-view',
'filter' => 'permission:users-view',
@ -553,7 +553,7 @@ $routes->group(
});
// My account
$routes->group('my-account', function ($routes) {
$routes->group('my-account', function ($routes): void {
$routes->get('/', 'MyAccount', [
'as' => 'my-account',
]);
@ -568,7 +568,7 @@ $routes->group(
/**
* Overwriting Myth:auth routes file
*/
$routes->group(config('App')->authGateway, function ($routes) {
$routes->group(config('App')->authGateway, function ($routes): void {
// Login/out
$routes->get('login', 'Auth::login', ['as' => 'login']);
$routes->post('login', 'Auth::attemptLogin');
@ -600,7 +600,7 @@ $routes->group(config('App')->authGateway, function ($routes) {
});
// Podcast's Public routes
$routes->group('@(:podcastName)', function ($routes) {
$routes->group('@(:podcastName)', function ($routes): void {
$routes->get('/', 'Podcast::activity/$1', [
'as' => 'podcast-activity',
]);
@ -621,7 +621,7 @@ $routes->group('@(:podcastName)', function ($routes) {
$routes->get('episodes', 'Podcast::episodes/$1', [
'as' => 'podcast-episodes',
]);
$routes->group('episodes/(:slug)', function ($routes) {
$routes->group('episodes/(:slug)', function ($routes): void {
$routes->get('/', 'Episode/$1/$2', [
'as' => 'episode',
]);
@ -631,7 +631,7 @@ $routes->group('@(:podcastName)', function ($routes) {
$routes->get('oembed.xml', 'Episode::oembedXML/$1/$2', [
'as' => 'episode-oembed-xml',
]);
$routes->group('embeddable-player', function ($routes) {
$routes->group('embeddable-player', function ($routes): void {
$routes->get('/', 'Episode::embeddablePlayer/$1/$2', [
'as' => 'embeddable-player',
]);
@ -661,13 +661,13 @@ $routes->post('interact-as-actor', 'Auth::attemptInteractAsActor', [
/**
* Overwriting ActivityPub routes file
*/
$routes->group('@(:podcastName)', function ($routes) {
$routes->group('@(:podcastName)', function ($routes): void {
$routes->post('notes/new', 'Note::attemptCreate/$1', [
'as' => 'note-attempt-create',
'filter' => 'permission:podcast-manage_publications',
]);
// Note
$routes->group('notes/(:uuid)', function ($routes) {
$routes->group('notes/(:uuid)', function ($routes): void {
$routes->get('/', 'Note/$1/$2', [
'as' => 'note',
'alternate-content' => [

View File

@ -23,7 +23,7 @@ class Actor extends \ActivityPub\Controllers\ActorController
$this->registerPodcastWebpageHit($this->actor->podcast->id);
}
$cacheName = "page_podcast@{$this->actor->username}_follow";
$cacheName = "page_podcast-{$this->actor->username}_follow";
if (!($cachedView = cache($cacheName))) {
helper(['form', 'components', 'svg']);
$data = [

View File

@ -31,16 +31,12 @@ class BaseController extends Controller
/**
* Constructor.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param LoggerInterface $logger
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
) {
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);

View File

@ -8,6 +8,10 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Entities\User;
use CodeIgniter\Exceptions\PageNotFoundException;
use Exception;
use App\Authorization\GroupModel;
use App\Models\PodcastModel;
use App\Models\UserModel;
@ -15,12 +19,12 @@ use App\Models\UserModel;
class Contributor extends BaseController
{
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
/**
* @var \App\Entities\User|null
* @var User|null
*/
protected $user;
@ -28,18 +32,20 @@ class Contributor extends BaseController
{
$this->podcast = (new PodcastModel())->getPodcastById($params[0]);
if (count($params) > 1) {
if (
!($this->user = (new UserModel())->getPodcastContributor(
$params[1],
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) <= 1) {
return $this->$method();
}
return $this->$method();
if (
$this->user = (new UserModel())->getPodcastContributor(
$params[1],
$params[0],
)
) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function list()
@ -57,7 +63,7 @@ class Contributor extends BaseController
$data = [
'contributor' => (new UserModel())->getPodcastContributor(
$this->user->id,
$this->podcast->id
$this->podcast->id,
),
];
@ -79,7 +85,7 @@ class Contributor extends BaseController
$result[$user->id] = $user->username;
return $result;
},
[]
[],
);
$roles = (new GroupModel())->getContributorRoles();
@ -89,7 +95,7 @@ class Contributor extends BaseController
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[]
[],
);
$data = [
@ -108,9 +114,9 @@ class Contributor extends BaseController
(new PodcastModel())->addPodcastContributor(
$this->request->getPost('user'),
$this->podcast->id,
$this->request->getPost('role')
$this->request->getPost('role'),
);
} catch (\Exception $e) {
} catch (Exception $exception) {
return redirect()
->back()
->withInput()
@ -133,7 +139,7 @@ class Contributor extends BaseController
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[]
[],
);
$data = [
@ -141,7 +147,7 @@ class Contributor extends BaseController
'user' => $this->user,
'contributorGroupId' => (new PodcastModel())->getContributorGroupId(
$this->user->id,
$this->podcast->id
$this->podcast->id,
),
'roleOptions' => $roleOptions,
];
@ -158,7 +164,7 @@ class Contributor extends BaseController
(new PodcastModel())->updatePodcastContributor(
$this->user->id,
$this->podcast->id,
$this->request->getPost('role')
$this->request->getPost('role'),
);
return redirect()->route('contributor-list', [$this->podcast->id]);
@ -178,7 +184,7 @@ class Contributor extends BaseController
if (
!$podcastModel->removePodcastContributor(
$this->user->id,
$this->podcast->id
$this->podcast->id,
)
) {
return redirect()
@ -193,7 +199,7 @@ class Contributor extends BaseController
lang('Contributor.messages.removeContributorSuccess', [
'username' => $this->user->username,
'podcastTitle' => $this->podcast->title,
])
]),
);
}
}

View File

@ -8,6 +8,7 @@
namespace App\Controllers\Admin;
use App\Entities\Episode as EpisodeEntity;
use App\Entities\Note;
use App\Models\EpisodeModel;
use App\Models\NoteModel;
@ -18,17 +19,17 @@ use CodeIgniter\I18n\Time;
class Episode extends BaseController
{
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode|null
* @var Episode|null
*/
protected $episode;
/**
* @var \App\Entities\Soundbite|null
* @var Soundbite|null
*/
protected $soundbites;
@ -123,7 +124,7 @@ class Episode extends BaseController
->with('errors', $this->validator->getErrors());
}
$newEpisode = new \App\Entities\Episode([
$newEpisode = new EpisodeEntity([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),

View File

@ -8,6 +8,9 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Entities\Episode;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\EpisodePersonModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
@ -16,42 +19,39 @@ use App\Models\PersonModel;
class EpisodePerson extends BaseController
{
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode
* @var Episode
*/
protected $episode;
public function _remap($method, ...$params)
{
if (count($params) > 1) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (
!($this->episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
} else {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
if (count($params) <= 2) {
throw PageNotFoundException::forPageNotFound();
}
unset($params[1]);
unset($params[0]);
return $this->$method(...$params);
if (
($this->podcast = (new PodcastModel())->getPodcastById(
$params[0],
)) &&
($this->episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
unset($params[1]);
unset($params[0]);
return $this->$method(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index()

View File

@ -8,8 +8,6 @@
namespace App\Controllers\Admin;
use ActivityPub\Models\BlockedDomainModel;
class Fediverse extends BaseController
{
public function dashboard()

View File

@ -8,24 +8,28 @@
namespace App\Controllers\Admin;
use App\Entities\Page as EntitiesPage;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PageModel;
class Page extends BaseController
{
/**
* @var \App\Entities\Page|null
* @var Page|null
*/
protected $page;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->page = (new PageModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if ($this->page = (new PageModel())->find($params[0])) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
function list()
@ -51,7 +55,7 @@ class Page extends BaseController
function attemptCreate()
{
$page = new \App\Entities\Page([
$page = new EntitiesPage([
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'content' => $this->request->getPost('content'),
@ -72,7 +76,7 @@ class Page extends BaseController
'message',
lang('Page.messages.createSuccess', [
'pageTitle' => $page->title,
])
]),
);
}

View File

@ -8,28 +8,28 @@
namespace App\Controllers\Admin;
use App\Entities\Person as EntitiesPerson;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PersonModel;
class Person extends BaseController
{
/**
* @var \App\Entities\Person|null
* @var Person|null
*/
protected $person;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->person = (new PersonModel())->getPersonById(
$params[0]
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if ($this->person = (new PersonModel())->getPersonById($params[0])) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function index()
@ -68,7 +68,7 @@ class Person extends BaseController
->with('errors', $this->validator->getErrors());
}
$person = new \App\Entities\Person([
$person = new EntitiesPerson([
'full_name' => $this->request->getPost('full_name'),
'unique_name' => $this->request->getPost('unique_name'),
'information_url' => $this->request->getPost('information_url'),
@ -118,14 +118,14 @@ class Person extends BaseController
$this->person->full_name = $this->request->getPost('full_name');
$this->person->unique_name = $this->request->getPost('unique_name');
$this->person->information_url = $this->request->getPost(
'information_url'
'information_url',
);
$image = $this->request->getFile('image');
if ($image->isValid()) {
$this->person->image = $image;
}
$this->updated_by = user()->id;
$this->person->updated_by = user()->id;
$personModel = new PersonModel();
if (!$personModel->update($this->person->id, $this->person)) {

View File

@ -8,6 +8,9 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast as EntitiesPodcast;
use CodeIgniter\Exceptions\PageNotFoundException;
use Config\Database;
use App\Models\CategoryModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
@ -17,23 +20,21 @@ use Config\Services;
class Podcast extends BaseController
{
/**
* @var \App\Entities\Podcast|null
* @var Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function list()
@ -145,7 +146,7 @@ class Podcast extends BaseController
->with('errors', $this->validator->getErrors());
}
$podcast = new \App\Entities\Podcast([
$podcast = new EntitiesPodcast([
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description_markdown' => $this->request->getPost('description'),
@ -175,7 +176,7 @@ class Podcast extends BaseController
]);
$podcastModel = new PodcastModel();
$db = \Config\Database::connect();
$db = Database::connect();
$db->transStart();
@ -271,18 +272,18 @@ class Podcast extends BaseController
);
$this->podcast->partner_id = $this->request->getPost('partner_id');
$this->podcast->partner_link_url = $this->request->getPost(
'partner_link_url'
'partner_link_url',
);
$this->podcast->partner_image_url = $this->request->getPost(
'partner_image_url'
'partner_image_url',
);
$this->podcast->is_blocked = $this->request->getPost('block') === 'yes';
$this->podcast->is_completed =
$this->request->getPost('complete') === 'yes';
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
$this->updated_by = user()->id;
$this->podcast->updated_by = user()->id;
$db = \Config\Database::connect();
$db = Database::connect();
$db->transStart();
$podcastModel = new PodcastModel();

View File

@ -8,6 +8,13 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use CodeIgniter\Exceptions\PageNotFoundException;
use ErrorException;
use Config\Database;
use Podlibre\PodcastNamespace\ReversedTaxonomy;
use App\Entities\PodcastPerson;
use App\Entities\Episode;
use App\Models\CategoryModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
@ -22,23 +29,21 @@ use League\HTMLToMarkdown\HtmlConverter;
class PodcastImport extends BaseController
{
/**
* @var \App\Entities\Podcast|null
* @var Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function index()
@ -80,12 +85,12 @@ class PodcastImport extends BaseController
$feed = simplexml_load_file(
$this->request->getPost('imported_feed_url'),
);
} catch (\ErrorException $ex) {
} catch (ErrorException $errorException) {
return redirect()
->back()
->withInput()
->with('errors', [
$ex->getMessage() .
$errorException->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
@ -115,7 +120,7 @@ class PodcastImport extends BaseController
$channelDescriptionHtml = (string) $feed->channel[0]->description;
try {
$podcast = new \App\Entities\Podcast([
$podcast = new Podcast([
'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost(
'imported_feed_url',
@ -157,9 +162,9 @@ class PodcastImport extends BaseController
'is_completed' => empty($nsItunes->complete)
? false
: $nsItunes->complete === 'yes',
'location_name' => !$nsPodcast->location
? null
: (string) $nsPodcast->location,
'location_name' => $nsPodcast->location
? (string) $nsPodcast->location
: null,
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
@ -173,7 +178,7 @@ class PodcastImport extends BaseController
'created_by' => user()->id,
'updated_by' => user()->id,
]);
} catch (\ErrorException $ex) {
} catch (ErrorException $ex) {
return redirect()
->back()
->withInput()
@ -188,7 +193,7 @@ class PodcastImport extends BaseController
}
$podcastModel = new PodcastModel();
$db = \Config\Database::connect();
$db = Database::connect();
$db->transStart();
@ -221,13 +226,13 @@ class PodcastImport extends BaseController
$platformLabel = $platform->attributes()['platform'];
$platformSlug = slugify($platformLabel);
if ($platformModel->getPlatform($platformSlug)) {
array_push($podcastsPlatformsData, [
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $newPodcastId,
'link_url' => $platform->attributes()['url'],
'link_content' => $platform->attributes()['id'],
'is_visible' => false,
]);
];
}
}
}
@ -243,24 +248,22 @@ class PodcastImport extends BaseController
$newPersonId = null;
if ($newPerson = $personModel->getPerson($podcastPerson)) {
$newPersonId = $newPerson->id;
} else {
if (
!($newPersonId = $personModel->createPerson(
$podcastPerson,
$podcastPerson->attributes()['href'],
$podcastPerson->attributes()['img'],
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
} elseif (
!($newPersonId = $personModel->createPerson(
$podcastPerson,
$podcastPerson->attributes()['href'],
$podcastPerson->attributes()['img'],
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
$personGroup = empty($podcastPerson->attributes()['group'])
? ['slug' => '']
: \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
: ReversedTaxonomy::$taxonomy[
(string) $podcastPerson->attributes()['group']
];
$personRole =
@ -270,7 +273,7 @@ class PodcastImport extends BaseController
: $personGroup['roles'][
strval($podcastPerson->attributes()['role'])
];
$newPodcastPerson = new \App\Entities\PodcastPerson([
$newPodcastPerson = new PodcastPerson([
'podcast_id' => $newPodcastId,
'person_id' => $newPersonId,
'person_group' => $personGroup['slug'],
@ -297,7 +300,7 @@ class PodcastImport extends BaseController
//////////////////////////////////////////////////////////////////
// For each Episode:
for ($itemNumber = 1; $itemNumber <= $lastItem; $itemNumber++) {
for ($itemNumber = 1; $itemNumber <= $lastItem; ++$itemNumber) {
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
$nsItunes = $item->children(
@ -318,7 +321,7 @@ class PodcastImport extends BaseController
if (in_array($slug, $slugs)) {
$slugNumber = 2;
while (in_array($slug . '-' . $slugNumber, $slugs)) {
$slugNumber++;
++$slugNumber;
}
$slug = $slug . '-' . $slugNumber;
}
@ -340,7 +343,7 @@ class PodcastImport extends BaseController
$itemDescriptionHtml = $item->description;
}
$newEpisode = new \App\Entities\Episode([
$newEpisode = new Episode([
'podcast_id' => $newPodcastId,
'guid' => empty($item->guid) ? null : $item->guid,
'title' => $item->title,
@ -366,15 +369,15 @@ class PodcastImport extends BaseController
'number' =>
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
: (!empty($nsItunes->episode)
? $nsItunes->episode
: null),
: (empty($nsItunes->episode)
? null
: $nsItunes->episode),
'season_number' => empty(
$this->request->getPost('season_number')
)
? (!empty($nsItunes->season)
? $nsItunes->season
: null)
? (empty($nsItunes->season)
? null
: $nsItunes->season)
: $this->request->getPost('season_number'),
'type' => empty($nsItunes->episodeType)
? 'full'
@ -382,9 +385,9 @@ class PodcastImport extends BaseController
'is_blocked' => empty($nsItunes->block)
? false
: $nsItunes->block === 'yes',
'location_name' => !$nsPodcast->location
? null
: $nsPodcast->location,
'location_name' => $nsPodcast->location
? $nsPodcast->location
: null,
'location_geo' =>
!$nsPodcast->location ||
empty($nsPodcast->location->attributes()['geo'])
@ -415,24 +418,22 @@ class PodcastImport extends BaseController
$newPersonId = null;
if ($newPerson = $personModel->getPerson($episodePerson)) {
$newPersonId = $newPerson->id;
} else {
if (
!($newPersonId = $personModel->createPerson(
$episodePerson,
$episodePerson->attributes()['href'],
$episodePerson->attributes()['img'],
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
} elseif (
!($newPersonId = $personModel->createPerson(
$episodePerson,
$episodePerson->attributes()['href'],
$episodePerson->attributes()['img'],
))
) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
$personGroup = empty($episodePerson->attributes()['group'])
? ['slug' => '']
: \Podlibre\PodcastNamespace\ReversedTaxonomy::$taxonomy[
: ReversedTaxonomy::$taxonomy[
strval($episodePerson->attributes()['group'])
];
$personRole =
@ -442,7 +443,7 @@ class PodcastImport extends BaseController
: $personGroup['roles'][
strval($episodePerson->attributes()['role'])
];
$newEpisodePerson = new \App\Entities\PodcastPerson([
$newEpisodePerson = new PodcastPerson([
'podcast_id' => $newPodcastId,
'episode_id' => $newEpisodeId,
'person_id' => $newPersonId,

View File

@ -8,6 +8,8 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PodcastPersonModel;
use App\Models\PodcastModel;
use App\Models\PersonModel;
@ -15,26 +17,22 @@ use App\Models\PersonModel;
class PodcastPerson extends BaseController
{
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastById(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
} else {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound();
}
unset($params[0]);
return $this->$method(...$params);
if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
unset($params[0]);
return $this->$method(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index()

View File

@ -8,6 +8,8 @@
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use Config\Services;
@ -15,20 +17,22 @@ use Config\Services;
class PodcastPlatform extends BaseController
{
/**
* @var \App\Entities\Podcast|null
* @var Podcast|null
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (
!($this->podcast = (new PodcastModel())->getPodcastById($params[0]))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
if (count($params) === 0) {
return $this->$method();
}
unset($params[0]);
return $this->$method(...$params);
if ($this->podcast = (new PodcastModel())->getPodcastById($params[0])) {
unset($params[0]);
return $this->$method(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index()
@ -45,7 +49,7 @@ class PodcastPlatform extends BaseController
'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks(
$this->podcast->id,
$platformType
$platformType,
),
];
@ -65,36 +69,35 @@ class PodcastPlatform extends BaseController
as $platformSlug => $podcastPlatform
) {
$podcastPlatformUrl = $podcastPlatform['url'];
if (
!empty($podcastPlatformUrl) &&
$validation->check($podcastPlatformUrl, 'validate_url')
) {
array_push($podcastsPlatformsData, [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'link_content' => $podcastPlatform['content'],
'is_visible' => array_key_exists(
'visible',
$podcastPlatform
)
? $podcastPlatform['visible'] == 'yes'
: false,
'is_on_embeddable_player' => array_key_exists(
'on_embeddable_player',
$podcastPlatform
)
? $podcastPlatform['on_embeddable_player'] == 'yes'
: false,
]);
if (empty($podcastPlatformUrl)) {
continue;
}
if (!$validation->check($podcastPlatformUrl, 'validate_url')) {
continue;
}
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'link_content' => $podcastPlatform['content'],
'is_visible' =>
array_key_exists('visible', $podcastPlatform) &&
$podcastPlatform['visible'] == 'yes',
'is_on_embeddable_player' =>
array_key_exists(
'on_embeddable_player',
$podcastPlatform,
) && $podcastPlatform['on_embeddable_player'] == 'yes',
];
return redirect()
->back()
->with('message', lang('Platforms.messages.updateSuccess'));
}
$platformModel->savePodcastPlatforms(
$this->podcast->id,
$platformType,
$podcastsPlatformsData
$podcastsPlatformsData,
);
return redirect()
@ -106,7 +109,7 @@ class PodcastPlatform extends BaseController
{
(new PlatformModel())->removePodcastPlatform(
$this->podcast->id,
$platformSlug
$platformSlug,
);
return redirect()

View File

@ -8,26 +8,30 @@
namespace App\Controllers\Admin;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Authorization\GroupModel;
use App\Entities\User as EntitiesUser;
use App\Models\UserModel;
use Config\Services;
class User extends BaseController
{
/**
* @var \App\Entities\User|null
* @var User|null
*/
protected $user;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (!($this->user = (new UserModel())->find($params[0]))) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if ($this->user = (new UserModel())->find($params[0])) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function list()
@ -67,7 +71,7 @@ class User extends BaseController
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
]
],
);
if (!$this->validate($rules)) {
@ -78,7 +82,7 @@ class User extends BaseController
}
// Save the user
$user = new \App\Entities\User($this->request->getPost());
$user = new EntitiesUser($this->request->getPost());
// Activate user
$user->activate();
@ -100,7 +104,7 @@ class User extends BaseController
'message',
lang('User.messages.createSuccess', [
'username' => $user->username,
])
]),
);
}
@ -115,7 +119,7 @@ class User extends BaseController
$result[$role->name] = lang('User.roles.' . $role->name);
return $result;
},
[]
[],
);
$data = [
@ -141,7 +145,7 @@ class User extends BaseController
'message',
lang('User.messages.rolesEditSuccess', [
'username' => $this->user->username,
])
]),
);
}
@ -163,7 +167,7 @@ class User extends BaseController
'message',
lang('User.messages.forcePassResetSuccess', [
'username' => $this->user->username,
])
]),
);
}
@ -196,7 +200,7 @@ class User extends BaseController
'message',
lang('User.messages.banSuccess', [
'username' => $this->user->username,
])
]),
);
}
@ -217,7 +221,7 @@ class User extends BaseController
'message',
lang('User.messages.unbanSuccess', [
'username' => $this->user->username,
])
]),
);
}
@ -242,7 +246,7 @@ class User extends BaseController
'message',
lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
])
]),
);
}
}

View File

@ -8,9 +8,11 @@
namespace App\Controllers;
use Myth\Auth\Controllers\AuthController;
use App\Entities\User;
use CodeIgniter\HTTP\RedirectResponse;
class Auth extends \Myth\Auth\Controllers\AuthController
class Auth extends AuthController
{
/**
* An array of helpers to be automatically loaded
@ -104,10 +106,8 @@ class Auth extends \Myth\Auth\Controllers\AuthController
/**
* Verifies the code with the email and saves the new password,
* if they all pass validation.
*
* @return mixed
*/
public function attemptReset()
public function attemptReset(): RedirectResponse
{
if ($this->config->activeResetter === false) {
return redirect()

View File

@ -30,16 +30,12 @@ class BaseController extends Controller
/**
* Constructor.
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @param LoggerInterface $logger
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
) {
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);

View File

@ -11,6 +11,7 @@ namespace App\Controllers;
use Analytics\AnalyticsTrait;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use SimpleXMLElement;
class Episode extends BaseController
@ -18,31 +19,41 @@ class Episode extends BaseController
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode|null
* @var Episode
*/
protected $episode;
public function _remap($method, ...$params)
{
$this->podcast = (new PodcastModel())->getPodcastByName($params[0]);
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
count($params) > 1 &&
!($this->episode = (new EpisodeModel())->getEpisodeBySlug(
$this->podcast->id,
$params[1],
!($this->podcast = (new PodcastModel())->getPodcastByName(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
throw PageNotFoundException::forPageNotFound();
}
unset($params[1]);
unset($params[0]);
return $this->$method(...$params);
if (
$this->episode = (new EpisodeModel())->getEpisodeBySlug(
$this->podcast->id,
$params[1],
)
) {
unset($params[1]);
unset($params[0]);
return $this->$method(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index()

View File

@ -8,32 +8,34 @@
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Exceptions\PageNotFoundException;
use Opawg\UserAgentsPhp\UserAgentsRSS;
use Exception;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
class Feed extends Controller
{
public function index($podcastName)
public function index($podcastName): ResponseInterface
{
helper('rss');
$podcast = (new PodcastModel())->where('name', $podcastName)->first();
if (!$podcast) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
throw PageNotFoundException::forPageNotFound();
}
$serviceSlug = '';
try {
$service = \Opawg\UserAgentsPhp\UserAgentsRSS::find(
$_SERVER['HTTP_USER_AGENT'],
);
$service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']);
if ($service) {
$serviceSlug = $service['slug'];
}
} catch (\Exception $e) {
} catch (Exception $exception) {
// If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $e);
log_message('critical', $exception);
}
$cacheName =

View File

@ -12,6 +12,9 @@ use App\Models\PodcastModel;
class Home extends BaseController
{
/**
* @return mixed
*/
public function index()
{
$model = new PodcastModel();

View File

@ -8,6 +8,15 @@
namespace App\Controllers;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
use Throwable;
use Dotenv\Exception\ValidationException;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\Database\Exceptions\DatabaseException;
use Config\Database;
use App\Entities\User;
use App\Models\UserModel;
use CodeIgniter\Controller;
use Config\Services;
@ -15,16 +24,19 @@ use Dotenv\Dotenv;
class Install extends Controller
{
/**
* @var string[]
*/
protected $helpers = ['form', 'components', 'svg'];
/**
* Constructor.
*/
public function initController(
\CodeIgniter\HTTP\RequestInterface $request,
\CodeIgniter\HTTP\ResponseInterface $response,
\Psr\Log\LoggerInterface $logger
) {
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
}
@ -36,14 +48,14 @@ class Install extends Controller
* If all required actions have already been performed,
* the install route will show a 404 page.
*/
public function index()
public function index(): string
{
try {
// Check if .env is created and has all required fields
$dotenv = Dotenv::createUnsafeImmutable(ROOTPATH);
$dotenv->load();
} catch (\Throwable $e) {
} catch (Throwable $e) {
$this->createEnv();
}
@ -55,7 +67,7 @@ class Install extends Controller
'app.adminGateway',
'app.authGateway',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
} catch (ValidationException $e) {
// form to input instance configuration
return $this->instanceConfig();
}
@ -68,13 +80,13 @@ class Install extends Controller
'database.default.password',
'database.default.DBPrefix',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
} catch (ValidationException $validationException) {
return $this->databaseConfig();
}
try {
$dotenv->required('cache.handler');
} catch (\Dotenv\Exception\ValidationException $e) {
} catch (ValidationException $validationException) {
return $this->cacheConfig();
}
} else {
@ -90,7 +102,7 @@ class Install extends Controller
'database.default.DBPrefix',
'cache.handler',
]);
} catch (\Dotenv\Exception\ValidationException $e) {
} catch (ValidationException $e) {
return view('install/manual_config');
}
}
@ -104,9 +116,9 @@ class Install extends Controller
(new UserModel())->countAll() > 0
) {
// if so, show a 404 page
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
throw PageNotFoundException::forPageNotFound();
}
} catch (\CodeIgniter\Database\Exceptions\DatabaseException $e) {
} catch (DatabaseException $databaseException) {
// Could not connect to the database
// show database config view to fix value
session()->setFlashdata(
@ -128,6 +140,7 @@ class Install extends Controller
/**
* Returns the form to generate the .env config file for the instance.
* @return mixed|void
*/
public function createEnv()
{
@ -135,7 +148,7 @@ class Install extends Controller
try {
$envFile = fopen(ROOTPATH . '.env', 'w');
fclose($envFile);
} catch (\Throwable $e) {
} catch (Throwable $throwable) {
// Could not create the .env file, redirect to a view with manual instructions on how to add it
return view('install/manual_config');
}
@ -252,22 +265,22 @@ class Install extends Controller
/**
* Runs all database migrations required for instance.
*/
public function migrate()
public function migrate(): void
{
$migrations = \Config\Services::migrations();
$migrations = Services::migrations();
!$migrations->setNamespace('Myth\Auth')->latest();
!$migrations->setNamespace('ActivityPub')->latest();
!$migrations->setNamespace('Analytics')->latest();
!$migrations->setNamespace(APP_NAMESPACE)->latest();
$migrations->setNamespace('Myth\Auth')->latest();
$migrations->setNamespace('ActivityPub')->latest();
$migrations->setNamespace('Analytics')->latest();
$migrations->setNamespace(APP_NAMESPACE)->latest();
}
/**
* Runs all database seeds required for instance.
*/
public function seed()
public function seed(): void
{
$seeder = \Config\Database::seeder();
$seeder = Database::seeder();
// Seed database
$seeder->call('AppSeeder');
@ -308,12 +321,12 @@ class Install extends Controller
}
// Save the user
$user = new \App\Entities\User($this->request->getPost());
$user = new User($this->request->getPost());
// Activate user
$user->activate();
$db = \Config\Database::connect();
$db = Database::connect();
$db->transStart();
if (!($userId = $userModel->insert($user, true))) {
@ -345,10 +358,8 @@ class Install extends Controller
* overwrites any existing key and appends new ones
*
* @param array $data key/value config pairs
*
* @return void
*/
public static function writeEnv($configData)
public static function writeEnv($configData): void
{
$envData = file(ROOTPATH . '.env'); // reads an array of lines
@ -360,7 +371,7 @@ class Install extends Controller
$keyVal,
&$replaced
) {
if (strpos($line, $key) === 0) {
if (strpos($line, (string) $key) === 0) {
$replaced = true;
return $keyVal;
}
@ -369,7 +380,7 @@ class Install extends Controller
$envData);
if (!$replaced) {
array_push($envData, $keyVal);
$envData[] = $keyVal;
}
}

View File

@ -8,18 +8,22 @@
namespace App\Controllers;
use ActivityPub\Controllers\NoteController;
use ActivityPub\Entities\Note as ActivityPubNote;
use Analytics\AnalyticsTrait;
use App\Entities\Note as CastopodNote;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
class Note extends \ActivityPub\Controllers\NoteController
class Note extends NoteController
{
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
@ -48,7 +52,7 @@ class Note extends \ActivityPub\Controllers\NoteController
return $this->$method(...$params);
}
public function index()
public function index(): RedirectResponse
{
// Prevent analytics hit when authenticated
if (!can_user_interact()) {
@ -108,7 +112,7 @@ class Note extends \ActivityPub\Controllers\NoteController
$message = $this->request->getPost('message');
$newNote = new \App\Entities\Note([
$newNote = new CastopodNote([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
@ -162,7 +166,7 @@ class Note extends \ActivityPub\Controllers\NoteController
->with('errors', $this->validator->getErrors());
}
$newNote = new \ActivityPub\Entities\Note([
$newNote = new ActivityPubNote([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->note->id,
'message' => $this->request->getPost('message'),

View File

@ -8,6 +8,8 @@
namespace App\Controllers;
use App\Entities\Page as PageEntity;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PageModel;
use App\Models\CreditModel;
use App\Models\PodcastModel;
@ -15,28 +17,28 @@ use App\Models\PodcastModel;
class Page extends BaseController
{
/**
* @var \App\Entities\Page|null
* @var Page|null
*/
protected $page;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->page = (new PageModel())
->where('slug', $params[0])
->first())
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
if (count($params) === 0) {
return $this->$method();
}
return $this->$method();
if (
$this->page = (new PageModel())->where('slug', $params[0])->first()
) {
return $this->$method();
}
throw PageNotFoundException::forPageNotFound();
}
public function index()
{
$cacheName = "page@{$this->page->slug}";
$cacheName = "page-{$this->page->slug}";
if (!($found = cache($cacheName))) {
$data = [
'page' => $this->page,
@ -58,7 +60,7 @@ class Page extends BaseController
$cacheName = "page_credits_{$locale}";
if (!($found = cache($cacheName))) {
$page = new \App\Entities\Page([
$page = new PageEntity([
'title' => lang('Person.credits', [], $locale),
'slug' => 'credits',
'content' => '',
@ -157,10 +159,10 @@ class Page extends BaseController
'role_label' => $credit->role_label,
'is_in' => [
[
'link' => $credit->episode
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .
@ -179,10 +181,10 @@ class Page extends BaseController
$credits[$person_group]['persons'][$person_id]['roles'][
$person_role
]['is_in'][] = [
'link' => $credit->episode
'link' => $credit->episode_id
? $credit->episode->link
: $credit->podcast->link,
'title' => $credit->episode
'title' => $credit->episode_id
? (count($allPodcasts) > 1
? "{$credit->podcast->title}"
: '') .

View File

@ -8,6 +8,8 @@
namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
/*
@ -15,9 +17,9 @@ use CodeIgniter\Controller;
*/
class Platform extends Controller
{
public function index()
public function index(): ResponseInterface
{
$model = new \App\Models\PlatformModel();
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}

View File

@ -18,24 +18,24 @@ class Podcast extends BaseController
use AnalyticsTrait;
/**
* @var \App\Entities\Podcast|null
* @var Podcast
*/
protected $podcast;
public function _remap($method, ...$params)
{
if (count($params) > 0) {
if (
!($this->podcast = (new PodcastModel())->getPodcastByName(
$params[0],
))
) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
unset($params[0]);
if (count($params) === 0) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return $this->$method(...$params);
if (
$this->podcast = (new PodcastModel())->getPodcastByName($params[0])
) {
unset($params[0]);
return $this->$method(...$params);
}
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
public function activity()

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddCategories extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -45,7 +45,7 @@ class AddCategories extends Migration
$this->forge->createTable('categories');
}
public function down()
public function down(): void
{
$this->forge->dropTable('categories');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddLanguages extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'code' => [
@ -32,7 +32,7 @@ class AddLanguages extends Migration
$this->forge->createTable('languages');
}
public function down()
public function down(): void
{
$this->forge->dropTable('languages');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPodcasts extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -204,7 +204,7 @@ class AddPodcasts extends Migration
$this->forge->createTable('podcasts');
}
public function down()
public function down(): void
{
$this->forge->dropTable('podcasts');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddEpisodes extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -197,7 +197,7 @@ class AddEpisodes extends Migration
$this->forge->createTable('episodes');
}
public function down()
public function down(): void
{
$this->forge->dropTable('episodes');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddSoundbites extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -82,7 +82,7 @@ class AddSoundbites extends Migration
$this->forge->createTable('soundbites');
}
public function down()
public function down(): void
{
$this->forge->dropTable('soundbites');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPlatforms extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'slug' => [
@ -43,13 +43,13 @@ class AddPlatforms extends Migration
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()'
'`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()',
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
public function down()
public function down(): void
{
$this->forge->dropTable('platforms');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPodcastsUsers extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
@ -50,7 +50,7 @@ class AddPodcastsUsers extends Migration
$this->forge->createTable('podcasts_users');
}
public function down()
public function down(): void
{
$this->forge->dropTable('podcasts_users');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPages extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -50,7 +50,7 @@ class AddPages extends Migration
$this->forge->createTable('pages');
}
public function down()
public function down(): void
{
$this->forge->dropTable('pages');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPodcastsCategories extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
@ -45,7 +45,7 @@ class AddPodcastsCategories extends Migration
$this->forge->createTable('podcasts_categories');
}
public function down()
public function down(): void
{
$this->forge->dropTable('podcasts_categories');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPersons extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -73,7 +73,7 @@ class AddPersons extends Migration
$this->forge->createTable('persons');
}
public function down()
public function down(): void
{
$this->forge->dropTable('persons');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddPodcastsPersons extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -64,7 +64,7 @@ class AddPodcastsPersons extends Migration
$this->forge->createTable('podcasts_persons');
}
public function down()
public function down(): void
{
$this->forge->dropTable('podcasts_persons');
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Migration;
class AddEpisodesPersons extends Migration
{
public function up()
public function up(): void
{
$this->forge->addField([
'id' => [
@ -76,7 +76,7 @@ class AddEpisodesPersons extends Migration
$this->forge->createTable('episodes_persons');
}
public function down()
public function down(): void
{
$this->forge->dropTable('episodes_persons');
}

View File

@ -14,30 +14,34 @@ use CodeIgniter\Database\Migration;
class AddCreditView extends Migration
{
public function up()
public function up(): void
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personTable = $this->db->prefixTable('persons');
$podcastPersonTable = $this->db->prefixTable('podcasts_persons');
$episodePersonTable = $this->db->prefixTable('episodes_persons');
$personsTable = $this->db->prefixTable('persons');
$podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<EOD
CREATE VIEW `$viewName` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable`
INNER JOIN `$personTable`
ON (`person_id`=`$personTable`.`id`)
CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable`
INNER JOIN `$personTable`
ON (`person_id`=`$personTable`.`id`)
SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
INNER JOIN `{$episodesTable}`
ON (`episode_id`=`{$episodesTable}`.`id`)
WHERE `{$episodesTable}`.published_at <= NOW()
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
EOD;
$this->db->query($createQuery);
}
public function down()
public function down(): void
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `$viewName`");
$this->db->query("DROP VIEW IF EXISTS `{$viewName}`");
}
}

View File

@ -15,19 +15,19 @@ use CodeIgniter\Database\Migration;
class AddEpisodeIdToNotes extends Migration
{
public function up()
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE ${prefix}activitypub_notes
ALTER TABLE {$prefix}activitypub_notes
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
ADD FOREIGN KEY ${prefix}activitypub_notes_episode_id_foreign(episode_id) REFERENCES ${prefix}episodes(id) ON DELETE CASCADE;
ADD FOREIGN KEY {$prefix}activitypub_notes_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE;
SQL;
$this->db->query($createQuery);
}
public function down()
public function down(): void
{
$this->forge->dropForeignKey(
'activitypub_notes',

View File

@ -15,19 +15,19 @@ use CodeIgniter\Database\Migration;
class AddCreatedByToNotes extends Migration
{
public function up()
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE ${prefix}activitypub_notes
ALTER TABLE {$prefix}activitypub_notes
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
ADD FOREIGN KEY ${prefix}activitypub_notes_created_by_foreign(created_by) REFERENCES ${prefix}users(id) ON DELETE CASCADE;
ADD FOREIGN KEY {$prefix}activitypub_notes_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE;
SQL;
$this->db->query($createQuery);
}
public function down()
public function down(): void
{
$this->forge->dropForeignKey(
'activitypub_notes',

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Seeder;
class AppSeeder extends Seeder
{
public function run()
public function run(): void
{
$this->call('AuthSeeder');
$this->call('CategorySeeder');

View File

@ -15,6 +15,9 @@ use CodeIgniter\Database\Seeder;
class AuthSeeder extends Seeder
{
/**
* @var array<string, string>[]
*/
protected $groups = [
[
'name' => 'superadmin',
@ -37,6 +40,8 @@ class AuthSeeder extends Seeder
* ...
* ]
* ```
*
* @var array<string, array<string, string|array>[]>
*/
protected $permissions = [
'users' => [
@ -249,26 +254,16 @@ class AuthSeeder extends Seeder
],
];
static function getGroupIdByName($name, $dataGroups)
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
public function run()
public function run(): void
{
$groupId = 0;
$dataGroups = [];
foreach ($this->groups as $group) {
array_push($dataGroups, [
$dataGroups[] = [
'id' => ++$groupId,
'name' => $group['name'],
'description' => $group['description'],
]);
];
}
// Map permissions to a format the `auth_permissions` table expects
@ -277,21 +272,21 @@ class AuthSeeder extends Seeder
$permissionId = 0;
foreach ($this->permissions as $context => $actions) {
foreach ($actions as $action) {
array_push($dataPermissions, [
$dataPermissions[] = [
'id' => ++$permissionId,
'name' => $context . '-' . $action['name'],
'description' => $action['description'],
]);
];
foreach ($action['has_permission'] as $role) {
// link permission to specified groups
array_push($dataGroupsPermissions, [
$dataGroupsPermissions[] = [
'group_id' => $this->getGroupIdByName(
$role,
$dataGroups,
),
'permission_id' => $permissionId,
]);
];
}
}
}
@ -309,4 +304,16 @@ class AuthSeeder extends Seeder
->ignore(true)
->insertBatch($dataGroupsPermissions);
}
/**
* @param array<string, string|int>[] $dataGroups
*/
static function getGroupIdByName(string $name, array $dataGroups): int
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
}

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Seeder;
class CategorySeeder extends Seeder
{
public function run()
public function run(): void
{
$data = [
[

View File

@ -11,6 +11,8 @@
namespace App\Database\Seeds;
use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
@ -18,7 +20,7 @@ use CodeIgniter\Database\Seeder;
class FakePodcastsAnalyticsSeeder extends Seeder
{
public function run()
public function run(): void
{
$podcast = (new PodcastModel())->first();
@ -27,6 +29,8 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json',
),
true,
512,
JSON_THROW_ON_ERROR,
);
$jsonRSSUserAgents = json_decode(
@ -34,6 +38,8 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'https://raw.githubusercontent.com/opawg/podcast-rss-useragents/master/src/rss-ua.json',
),
true,
512,
JSON_THROW_ON_ERROR,
);
if ($podcast) {
@ -68,7 +74,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
for (
$num_line = 0;
$num_line < rand(1, $proba1);
$num_line++
++$num_line
) {
$proba2 = floor(exp(6 - $age / 20)) + 10;
@ -96,7 +102,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
'.' .
rand(0, 255);
$cityReader = new \GeoIp2\Database\Reader(
$cityReader = new Reader(
WRITEPATH .
'uploads/GeoLite2-City/GeoLite2-City.mmdb',
);
@ -117,7 +123,7 @@ class FakePodcastsAnalyticsSeeder extends Seeder
: $city->subdivisions[0]->isoCode;
$latitude = round($city->location->latitude, 3);
$longitude = round($city->location->longitude, 3);
} catch (\GeoIp2\Exception\AddressNotFoundException $ex) {
} catch (AddressNotFoundException $addressNotFoundException) {
//Bad luck, bad IP, nothing to do.
}

View File

@ -18,6 +18,9 @@ use CodeIgniter\Database\Seeder;
class FakeWebsiteAnalyticsSeeder extends Seeder
{
/**
* @var string[]
*/
protected $keywords = [
'all the smoke podcast',
'apple podcast',
@ -70,6 +73,10 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'wind of change podcast',
'your own backyard podcast',
];
/**
* @var string[]
*/
protected $domains = [
'360.cn ',
'adobe.com ',
@ -123,6 +130,9 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'zoom.us ',
];
/**
* @var string[]
*/
protected $browsers = [
'Android Browser',
'Avast Secure Browser',
@ -168,7 +178,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'WOSBrowser',
];
public function run()
public function run(): void
{
$podcast = (new PodcastModel())->first();
@ -201,7 +211,7 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
for (
$num_line = 0;
$num_line < rand(1, $proba1);
$num_line++
++$num_line
) {
$proba2 = floor(exp(6 - $age / 20)) + 10;

View File

@ -21,7 +21,7 @@ use CodeIgniter\Database\Seeder;
class LanguageSeeder extends Seeder
{
public function run()
public function run(): void
{
$data = [
['code' => 'aa', 'native_name' => 'Afaraf'],
@ -163,7 +163,7 @@ class LanguageSeeder extends Seeder
'native_name' => 'Gàidhlig',
],
['code' => 'gl', 'native_name' => 'Galego'],
['code' => 'gn', 'native_name' => 'Avañe\'ẽ'],
['code' => 'gn', 'native_name' => "Avañe'ẽ"],
['code' => 'gu', 'native_name' => 'ગુજરાતી'],
[
'code' => 'gv',
@ -436,7 +436,7 @@ class LanguageSeeder extends Seeder
],
[
'code' => 'sm',
'native_name' => 'gagana fa\'a Samoa',
'native_name' => "gagana fa'a Samoa",
],
['code' => 'sn', 'native_name' => 'chiShona'],
[

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Seeder;
class PlatformSeeder extends Seeder
{
public function run()
public function run(): void
{
$data = [
[

View File

@ -15,7 +15,7 @@ use CodeIgniter\Database\Seeder;
class TestSeeder extends Seeder
{
public function run()
public function run(): void
{
/** Inserts an active user with the following credentials:
* username: admin
@ -29,6 +29,7 @@ class TestSeeder extends Seeder
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
]);
$this->db
->table('auth_groups_users')
->insert(['group_id' => 1, 'user_id' => 1]);

View File

@ -9,6 +9,7 @@
namespace App\Entities;
use App\Models\PodcastModel;
use RuntimeException;
class Actor extends \ActivityPub\Entities\Actor
{
@ -30,7 +31,7 @@ class Actor extends \ActivityPub\Entities\Actor
public function getPodcast()
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Actor must be created before getting associated podcast.',
);
}

View File

@ -9,15 +9,18 @@
namespace App\Entities;
use App\Models\CategoryModel;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
class Category extends Entity
{
/**
* @var \App\Entity\Category|null
* @var Category|null
*/
protected $parent;
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'parent_id' => 'integer',
@ -26,12 +29,12 @@ class Category extends Entity
'google_category' => 'string',
];
public function getParent()
public function getParent(): ?Category
{
$parentId = $this->attributes['parent_id'];
if (empty($this->parent_id)) {
return null;
}
return $parentId != 0
? (new CategoryModel())->getCategoryById($parentId)
: null;
return (new CategoryModel())->getCategoryById($this->parent_id);
}
}

View File

@ -8,26 +8,26 @@
namespace App\Entities;
use RuntimeException;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use App\Models\EpisodeModel;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
class Credit extends Entity
{
/**
* @var \App\Entities\Person
* @var Person
*/
protected $person;
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
/**
* @var \App\Entities\Episode|null
* @var Episode|null
*/
protected $episode;
@ -41,35 +41,47 @@ class Credit extends Entity
*/
protected $role_label;
public function getPodcast()
/**
* @var array<string, string>
*/
protected $casts = [
'person_group' => 'string',
'person_role' => 'string',
'person_id' => 'integer',
'full_name' => 'integer',
'podcast_id' => 'integer',
'episode_id' => '?integer',
];
public function getPodcast(): Podcast
{
return (new PodcastModel())->getPodcastById(
$this->attributes['podcast_id'],
);
}
public function getEpisode()
public function getEpisode(): ?Episode
{
if (empty($this->episode_id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Credit must have episode_id before getting episode.',
);
}
if (empty($this->episode)) {
$this->episode = (new EpisodeModel())->getPublishedEpisodeById(
$this->episode_id,
$this->podcast_id,
$this->episode_id,
);
}
return $this->episode;
}
public function getPerson()
public function getPerson(): Person
{
if (empty($this->person_id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Credit must have person_id before getting person.',
);
}
@ -83,23 +95,27 @@ class Credit extends Entity
return $this->person;
}
public function getGroupLabel()
public function getGroupLabel(): ?string
{
if (empty($this->person_group)) {
return null;
} else {
return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
}
return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
}
public function getRoleLabel()
public function getRoleLabel(): ?string
{
if (empty($this->person_group) || empty($this->person_role)) {
if (empty($this->person_group)) {
return null;
} else {
return lang(
"PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label",
);
}
if (empty($this->person_role)) {
return null;
}
return lang(
"PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label",
);
}
}

View File

@ -8,20 +8,25 @@
namespace App\Entities;
use App\Libraries\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\EpisodePersonModel;
use App\Models\NoteModel;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\Exceptions\FileNotFoundException;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter;
use RuntimeException;
class Episode extends Entity
{
/**
* @var \App\Entities\Podcast
* @var Podcast
*/
protected $podcast;
@ -31,22 +36,22 @@ class Episode extends Entity
protected $link;
/**
* @var \App\Libraries\Image
* @var Image
*/
protected $image;
/**
* @var \CodeIgniter\Files\File
* @var File
*/
protected $audioFile;
/**
* @var \CodeIgniter\Files\File
* @var File
*/
protected $transcript_file;
/**
* @var \CodeIgniter\Files\File
* @var File
*/
protected $chapters_file;
@ -71,17 +76,17 @@ class Episode extends Entity
protected $audio_file_opengraph_url;
/**
* @var \App\Entities\EpisodePerson[]
* @var EpisodePerson[]
*/
protected $persons;
/**
* @var \App\Entities\Soundbite[]
* @var Soundbite[]
*/
protected $soundbites;
/**
* @var \App\Entities\Note[]
* @var Note[]
*/
protected $notes;
@ -156,15 +161,13 @@ class Episode extends Entity
/**
* Saves an episode image
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
*
* @param UploadedFile|File $image
*/
public function setImage($image)
{
if (
!empty($image) &&
(!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
$image->isValid())
(!($image instanceof UploadedFile) || $image->isValid())
) {
helper('media');
@ -175,7 +178,7 @@ class Episode extends Entity
'podcasts/' . $this->getPodcast()->name,
$this->attributes['slug'],
);
$this->image = new \App\Libraries\Image(
$this->image = new Image(
$this->attributes['image_path'],
$this->attributes['image_mimetype'],
);
@ -185,13 +188,10 @@ class Episode extends Entity
return $this;
}
public function getImage(): \App\Libraries\Image
public function getImage(): Image
{
if ($imagePath = $this->attributes['image_path']) {
return new \App\Libraries\Image(
$imagePath,
$this->attributes['image_mimetype'],
);
return new Image($imagePath, $this->attributes['image_mimetype']);
}
return $this->getPodcast()->image;
}
@ -199,15 +199,14 @@ class Episode extends Entity
/**
* Saves an audio file
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $audioFile
* @param UploadedFile|File $audioFile
*
*/
public function setAudioFile($audioFile = null)
{
if (
!empty($audioFile) &&
(!($audioFile instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
$audioFile->isValid())
(!($audioFile instanceof UploadedFile) || $audioFile->isValid())
) {
helper(['media', 'id3']);
@ -234,16 +233,14 @@ class Episode extends Entity
/**
* Saves an episode transcript file
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $transcriptFile
* @param UploadedFile|File $transcriptFile
*
*/
public function setTranscriptFile($transcriptFile)
{
if (
!empty($transcriptFile) &&
(!(
$transcriptFile instanceof \CodeIgniter\HTTP\Files\UploadedFile
) ||
(!($transcriptFile instanceof UploadedFile) ||
$transcriptFile->isValid())
) {
helper('media');
@ -261,14 +258,14 @@ class Episode extends Entity
/**
* Saves an episode chapters file
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $chaptersFile
* @param UploadedFile|File $chaptersFile
*
*/
public function setChaptersFile($chaptersFile)
{
if (
!empty($chaptersFile) &&
(!($chaptersFile instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
(!($chaptersFile instanceof UploadedFile) ||
$chaptersFile->isValid())
) {
helper('media');
@ -287,7 +284,7 @@ class Episode extends Entity
{
helper('media');
return new \CodeIgniter\Files\File(media_path($this->audio_file_path));
return new File(media_path($this->audio_file_path));
}
public function getTranscriptFile()
@ -295,7 +292,7 @@ class Episode extends Entity
if ($this->attributes['transcript_file_path']) {
helper('media');
return new \CodeIgniter\Files\File(
return new File(
media_path($this->attributes['transcript_file_path']),
);
}
@ -308,7 +305,7 @@ class Episode extends Entity
if ($this->attributes['chapters_file_path']) {
helper('media');
return new \CodeIgniter\Files\File(
return new File(
media_path($this->attributes['chapters_file_path']),
);
}
@ -320,7 +317,7 @@ class Episode extends Entity
{
helper('media');
return media_url($this->audio_file_path);
return media_base_url($this->audio_file_path);
}
public function getAudioFileAnalyticsUrl()
@ -359,7 +356,7 @@ class Episode extends Entity
public function getTranscriptFileUrl()
{
if ($this->attributes['transcript_file_path']) {
return media_url($this->attributes['transcript_file_path']);
return media_base_url($this->attributes['transcript_file_path']);
} else {
return $this->attributes['transcript_file_remote_url'];
}
@ -368,17 +365,14 @@ class Episode extends Entity
/**
* Gets chapters file url from chapters file uri if it exists
* or returns the chapters_file_remote_url which can be null.
*
* @return mixed
* @throws HTTPException
*/
public function getChaptersFileUrl()
public function getChaptersFileUrl(): ?string
{
if ($this->attributes['chapters_file_path']) {
return media_url($this->attributes['chapters_file_path']);
} else {
return $this->attributes['chapters_file_remote_url'];
if ($this->chapters_file_path) {
return media_base_url($this->chapters_file_path);
}
return $this->chapters_file_remote_url;
}
/**
@ -389,7 +383,7 @@ class Episode extends Entity
public function getPersons()
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Episode must be created before getting persons.',
);
}
@ -412,7 +406,7 @@ class Episode extends Entity
public function getSoundbites()
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Episode must be created before getting soundbites.',
);
}
@ -430,7 +424,7 @@ class Episode extends Entity
public function getNotes()
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Episode must be created before getting soundbites.',
);
}
@ -586,23 +580,24 @@ class Episode extends Entity
*/
function getCustomRssString()
{
helper('rss');
if (empty($this->attributes['custom_rss'])) {
if (empty($this->custom_rss)) {
return '';
} else {
$xmlNode = (new \App\Libraries\SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))
->addChild('channel')
->addChild('item');
array_to_rss(
[
'elements' => $this->custom_rss,
],
$xmlNode,
);
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
}
helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))
->addChild('channel')
->addChild('item');
array_to_rss(
[
'elements' => $this->custom_rss,
],
$xmlNode,
);
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
}
/**
@ -613,6 +608,10 @@ class Episode extends Entity
*/
function setCustomRssString($customRssString)
{
if (empty($customRssString)) {
return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
@ -621,6 +620,7 @@ class Episode extends Entity
'</item></channel></rss>',
),
)['elements'][0]['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode(
$customRssArray['elements'],
@ -628,6 +628,8 @@ class Episode extends Entity
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
}
function getPartnerLink($serviceSlug = null)

View File

@ -8,16 +8,19 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
use App\Models\PersonModel;
class EpisodePerson extends Entity
{
/**
* @var \App\Entities\Person
* @var Person
*/
protected $person;
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
@ -30,7 +33,7 @@ class EpisodePerson extends Entity
public function getPerson()
{
return (new PersonModel())->getPersonById(
$this->attributes['person_id']
$this->attributes['person_id'],
);
}
}

View File

@ -8,10 +8,13 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
class Language extends Entity
{
/**
* @var array<string, string>
*/
protected $casts = [
'code' => 'string',
'native_name' => 'string',

View File

@ -9,11 +9,12 @@
namespace App\Entities;
use App\Models\EpisodeModel;
use RuntimeException;
class Note extends \ActivityPub\Entities\Note
{
/**
* @var \App\Entities\Episode|null
* @var Episode|null
*/
protected $episode;
@ -40,7 +41,7 @@ class Note extends \ActivityPub\Entities\Note
public function getEpisode()
{
if (empty($this->episode_id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Note must have an episode_id before getting episode.',
);
}

View File

@ -8,7 +8,7 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
use League\CommonMark\CommonMarkConverter;
class Page extends Entity
@ -23,6 +23,9 @@ class Page extends Entity
*/
protected $content_html;
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'title' => 'string',
@ -32,10 +35,10 @@ class Page extends Entity
public function getLink()
{
return base_url($this->attributes['slug']);
return url_to('page', $this->attributes['slug']);
}
public function getContentHtml()
public function getContentHtml(): string
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',

View File

@ -8,15 +8,21 @@
namespace App\Entities;
use CodeIgniter\Entity;
use App\Libraries\Image;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\Files\File;
use CodeIgniter\Entity\Entity;
class Person extends Entity
{
/**
* @var \App\Libraries\Image
* @var Image
*/
protected $image;
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'full_name' => 'string',
@ -31,12 +37,11 @@ class Person extends Entity
/**
* Saves a picture in `public/media/persons/`
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
*
* @param UploadedFile|File|null $image
*/
public function setImage($image = null)
public function setImage($image = null): self
{
if ($image) {
if ($image !== null) {
helper('media');
$this->attributes['image_mimetype'] = $image->getMimeType();
@ -45,7 +50,7 @@ class Person extends Entity
'persons',
$this->attributes['unique_name'],
);
$this->image = new \App\Libraries\Image(
$this->image = new Image(
$this->attributes['image_path'],
$this->attributes['image_mimetype'],
);
@ -55,9 +60,9 @@ class Person extends Entity
return $this;
}
public function getImage()
public function getImage(): Image
{
return new \App\Libraries\Image(
return new Image(
$this->attributes['image_path'],
$this->attributes['image_mimetype'],
);

View File

@ -8,10 +8,13 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
class Platform extends Entity
{
/**
* @var array<string, string>
*/
protected $casts = [
'slug' => 'string',
'type' => 'string',

View File

@ -8,14 +8,18 @@
namespace App\Entities;
use ActivityPub\Models\ActorModel;
use App\Libraries\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\PlatformModel;
use App\Models\PodcastPersonModel;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
use App\Models\UserModel;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use League\CommonMark\CommonMarkConverter;
use RuntimeException;
class Podcast extends Entity
{
@ -25,32 +29,32 @@ class Podcast extends Entity
protected $link;
/**
* @var \ActivityPub\Entities\Actor
* @var Actor
*/
protected $actor;
/**
* @var \App\Libraries\Image
* @var Image
*/
protected $image;
/**
* @var \App\Entities\Episode[]
* @var Episode[]
*/
protected $episodes;
/**
* @var \App\Entities\PodcastPerson[]
* @var PodcastPerson[]
*/
protected $persons;
/**
* @var \App\Entities\Category
* @var Category
*/
protected $category;
/**
* @var \App\Entities\Category[]
* @var Category[]
*/
protected $other_categories;
@ -60,22 +64,22 @@ class Podcast extends Entity
protected $other_categories_ids;
/**
* @var \App\Entities\User[]
* @var User[]
*/
protected $contributors;
/**
* @var \App\Entities\Platform
* @var Platform
*/
protected $podcastingPlatforms;
/**
* @var \App\Entities\Platform
* @var Platform
*/
protected $socialPlatforms;
/**
* @var \App\Entities\Platform
* @var Platform
*/
protected $fundingPlatforms;
@ -132,12 +136,12 @@ class Podcast extends Entity
/**
* Returns the podcast actor
*
* @return \App\Entities\Actor
* @return Actor
*/
public function getActor()
public function getActor(): Actor
{
if (!$this->attributes['actor_id']) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must have an actor_id before getting actor.',
);
}
@ -152,10 +156,9 @@ class Podcast extends Entity
/**
* Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image
*
* @param UploadedFile|File $image
*/
public function setImage($image = null)
public function setImage($image = null): self
{
if ($image) {
helper('media');
@ -167,7 +170,7 @@ class Podcast extends Entity
'cover',
);
$this->image = new \App\Libraries\Image(
$this->image = new Image(
$this->attributes['image_path'],
$this->attributes['image_mimetype'],
);
@ -177,20 +180,20 @@ class Podcast extends Entity
return $this;
}
public function getImage()
public function getImage(): Image
{
return new \App\Libraries\Image(
return new Image(
$this->attributes['image_path'],
$this->attributes['image_mimetype'],
);
}
public function getLink()
public function getLink(): string
{
return url_to('podcast-activity', $this->attributes['name']);
}
public function getFeedUrl()
public function getFeedUrl(): string
{
return url_to('podcast_feed', $this->attributes['name']);
}
@ -198,12 +201,12 @@ class Podcast extends Entity
/**
* Returns the podcast's episodes
*
* @return \App\Entities\Episode[]
* @return Episode[]
*/
public function getEpisodes()
public function getEpisodes(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting episodes.',
);
}
@ -221,12 +224,12 @@ class Podcast extends Entity
/**
* Returns the podcast's persons
*
* @return \App\Entities\PodcastPerson[]
* @return PodcastPerson[]
*/
public function getPersons()
public function getPersons(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting persons.',
);
}
@ -243,12 +246,12 @@ class Podcast extends Entity
/**
* Returns the podcast category entity
*
* @return \App\Entities\Category
* @return Category
*/
public function getCategory()
public function getCategory(): Category
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting category.',
);
}
@ -265,12 +268,12 @@ class Podcast extends Entity
/**
* Returns all podcast contributors
*
* @return \App\Entities\User[]
* @return User[]
*/
public function getContributors()
public function getContributors(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcasts must be created before getting contributors.',
);
}
@ -284,7 +287,7 @@ class Podcast extends Entity
return $this->contributors;
}
public function setDescriptionMarkdown(string $descriptionMarkdown)
public function setDescriptionMarkdown(string $descriptionMarkdown): self
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
@ -300,8 +303,8 @@ class Podcast extends Entity
}
public function setEpisodeDescriptionFooterMarkdown(
string $episodeDescriptionFooterMarkdown = null
) {
?string $episodeDescriptionFooterMarkdown = null
): self {
if ($episodeDescriptionFooterMarkdown) {
$converter = new CommonMarkConverter([
'html_input' => 'strip',
@ -319,7 +322,7 @@ class Podcast extends Entity
return $this;
}
public function getDescription()
public function getDescription(): string
{
if ($this->description) {
return $this->description;
@ -337,12 +340,12 @@ class Podcast extends Entity
/**
* Returns the podcast's podcasting platform links
*
* @return \App\Entities\Platform[]
* @return Platform[]
*/
public function getPodcastingPlatforms()
public function getPodcastingPlatforms(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting podcasting platform links.',
);
}
@ -357,33 +360,15 @@ class Podcast extends Entity
return $this->podcastingPlatforms;
}
/**
* Returns true if the podcast has podcasting platform links
*/
public function getHasPodcastingPlatforms()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting podcasting platform.',
);
}
foreach ($this->getPodcastingPlatforms() as $podcastingPlatform) {
if ($podcastingPlatform->is_on_embeddable_player) {
return true;
}
}
return false;
}
/**
* Returns the podcast's social platform links
*
* @return \App\Entities\Platform[]
* @return Platform[]
*/
public function getSocialPlatforms()
public function getSocialPlatforms(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting social platform links.',
);
}
@ -398,33 +383,15 @@ class Podcast extends Entity
return $this->socialPlatforms;
}
/**
* Returns true if the podcast has social platform links
*/
public function getHasSocialPlatforms()
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting social platform.',
);
}
foreach ($this->getSocialPlatforms() as $socialPlatform) {
if ($socialPlatform->is_on_embeddable_player) {
return true;
}
}
return false;
}
/**
* Returns the podcast's funding platform links
*
* @return \App\Entities\Platform[]
* @return Platform[]
*/
public function getFundingPlatforms()
public function getFundingPlatforms(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting funding platform links.',
);
}
@ -440,27 +407,12 @@ class Podcast extends Entity
}
/**
* Returns true if the podcast has social platform links
* @return Category[]
*/
public function getHasFundingPlatforms()
public function getOtherCategories(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
'Podcast must be created before getting Funding platform.',
);
}
foreach ($this->getFundingPlatforms() as $fundingPlatform) {
if ($fundingPlatform->is_on_embeddable_player) {
return true;
}
}
return false;
}
public function getOtherCategories()
{
if (empty($this->id)) {
throw new \RuntimeException(
throw new RuntimeException(
'Podcast must be created before getting other categories.',
);
}
@ -474,7 +426,10 @@ class Podcast extends Entity
return $this->other_categories;
}
public function getOtherCategoriesIds()
/**
* @return array<int>
*/
public function getOtherCategoriesIds(): array
{
if (empty($this->other_categories_ids)) {
$this->other_categories_ids = array_column(
@ -488,11 +443,8 @@ class Podcast extends Entity
/**
* Saves the location name and fetches OpenStreetMap info
*
* @param string $locationName
*
*/
public function setLocation($locationName = null)
public function setLocation(?string $locationName = null): self
{
helper('location');
@ -511,6 +463,7 @@ class Podcast extends Entity
$this->attributes['location_geo'] = null;
$this->attributes['location_osmid'] = null;
}
return $this;
}
@ -518,39 +471,39 @@ class Podcast extends Entity
* Get custom rss tag as XML String
*
* @return string
*
*/
function getCustomRssString()
function getCustomRssString(): string
{
helper('rss');
if (empty($this->attributes['custom_rss'])) {
return '';
} else {
$xmlNode = (new \App\Libraries\SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))->addChild('channel');
array_to_rss(
[
'elements' => $this->custom_rss,
],
$xmlNode,
);
return str_replace(
['<channel>', '</channel>'],
'',
$xmlNode->asXML(),
);
}
helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))->addChild('channel');
array_to_rss(
[
'elements' => $this->custom_rss,
],
$xmlNode,
);
return str_replace(['<channel>', '</channel>'], '', $xmlNode->asXML());
}
/**
* Saves custom rss tag into json
*
* @param string $customRssString
*
*/
function setCustomRssString($customRssString)
function setCustomRssString($customRssString): self
{
if (empty($customRssString)) {
return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
@ -559,6 +512,7 @@ class Podcast extends Entity
'</channel></rss>',
),
)['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode(
$customRssArray['elements'],
@ -566,5 +520,7 @@ class Podcast extends Entity
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
}
}

View File

@ -8,16 +8,19 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
use App\Models\PersonModel;
class PodcastPerson extends Entity
{
/**
* @var \App\Entities\Person
* @var Person
*/
protected $person;
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
@ -29,7 +32,7 @@ class PodcastPerson extends Entity
public function getPerson()
{
return (new PersonModel())->getPersonById(
$this->attributes['person_id']
$this->attributes['person_id'],
);
}
}

View File

@ -8,10 +8,13 @@
namespace App\Entities;
use CodeIgniter\Entity;
use CodeIgniter\Entity\Entity;
class Soundbite extends Entity
{
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
@ -23,7 +26,7 @@ class Soundbite extends Entity
'updated_by' => 'integer',
];
public function setUpdatedBy(\App\Entities\User $user)
public function setUpdatedBy(User $user): self
{
$this->attributes['updated_by'] = $user->id;

View File

@ -8,25 +8,29 @@
namespace App\Entities;
use RuntimeException;
use App\Models\PodcastModel;
class User extends \Myth\Auth\Entities\User
{
/**
* Per-user podcasts
* @var \App\Entities\Podcast[]
* @var Podcast[]
*/
protected $podcasts = [];
/**
* The podcast the user is contributing to
* @var \App\Entities\Podcast|null
*
* @var Podcast|null
*/
protected $podcast = null;
protected $podcast;
/**
* Array of field names and the type of value to cast them as
* when they are accessed.
*
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
@ -39,13 +43,13 @@ class User extends \Myth\Auth\Entities\User
/**
* Returns the podcasts the user is contributing to
*
* @return \App\Entities\Podcast[]
* @return Podcast[]
*/
public function getPodcasts()
public function getPodcasts(): array
{
if (empty($this->id)) {
throw new \RuntimeException(
'Users must be created before getting podcasts.'
throw new RuntimeException(
'Users must be created before getting podcasts.',
);
}
@ -58,20 +62,18 @@ class User extends \Myth\Auth\Entities\User
/**
* Returns a podcast the user is contributing to
*
* @return \App\Entities\Podcast
*/
public function getPodcast()
public function getPodcast(): Podcast
{
if (empty($this->podcast_id)) {
throw new \RuntimeException(
'Podcast_id must be set before getting podcast.'
throw new RuntimeException(
'Podcast_id must be set before getting podcast.',
);
}
if (empty($this->podcast)) {
$this->podcast = (new PodcastModel())->getPodcastById(
$this->podcast_id
$this->podcast_id,
);
}

View File

@ -21,10 +21,8 @@ class PermissionFilter implements FilterInterface
* sent back to the client, allowing for error pages,
* redirects, etc.
*
* @param \CodeIgniter\HTTP\RequestInterface $request
* @param array|null $params
*
* @return mixed
* @return void|mixed
*/
public function before(RequestInterface $request, $params = null)
{
@ -59,15 +57,14 @@ class PermissionFilter implements FilterInterface
count($routerParams) > 0
) {
if (
$groupId = (new PodcastModel())->getContributorGroupId(
($groupId = (new PodcastModel())->getContributorGroupId(
$authenticate->id(),
$routerParams[0]
)
$routerParams[0],
)) &&
$authorize->groupHasPermission($permission, $groupId)
) {
if ($authorize->groupHasPermission($permission, $groupId)) {
$result = true;
break;
}
$result = true;
break;
}
} elseif (
$authorize->hasPermission($permission, $authenticate->id())
@ -84,31 +81,25 @@ class PermissionFilter implements FilterInterface
return redirect()
->to($redirectURL)
->with('error', lang('Auth.notEnoughPrivilege'));
} else {
throw new PermissionException(lang('Auth.notEnoughPrivilege'));
}
throw new PermissionException(lang('Auth.notEnoughPrivilege'));
}
}
//--------------------------------------------------------------------
/**
* Allows After filters to inspect and modify the response
* object as needed. This method does not allow any way
* to stop execution of other after filters, short of
* throwing an Exception or Error.
*
* @param \CodeIgniter\HTTP\RequestInterface $request
* @param \CodeIgniter\HTTP\ResponseInterface $response
* @param array|null $arguments
*
* @return void
*/
public function after(
RequestInterface $request,
ResponseInterface $response,
$arguments = null
) {
): void {
}
//--------------------------------------------------------------------

View File

@ -6,16 +6,15 @@
* @link https://castopod.org/
*/
use ActivityPub\Entities\Actor;
use CodeIgniter\Database\Exceptions\DataException;
use Config\Services;
if (!function_exists('set_interact_as_actor')) {
/**
* Sets the actor id of which the user is acting as
*
* @return void
*/
function set_interact_as_actor($actorId)
function set_interact_as_actor($actorId): void
{
$authenticate = Services::authentication();
$authenticate->check();
@ -28,10 +27,8 @@ if (!function_exists('set_interact_as_actor')) {
if (!function_exists('remove_interact_as_actor')) {
/**
* Removes the actor id of which the user is acting as
*
* @return void
*/
function remove_interact_as_actor()
function remove_interact_as_actor(): void
{
$session = session();
$session->remove('interact_as_actor_id');
@ -41,10 +38,8 @@ if (!function_exists('remove_interact_as_actor')) {
if (!function_exists('interact_as_actor_id')) {
/**
* Sets the podcast id of which the user is acting as
*
* @return integer
*/
function interact_as_actor_id()
function interact_as_actor_id(): int
{
$authenticate = Services::authentication();
$authenticate->check();
@ -58,7 +53,7 @@ if (!function_exists('interact_as_actor')) {
/**
* Get the actor the user is currently interacting as
*
* @return \ActivityPub\Entities\Actor|false
* @return Actor|false
*/
function interact_as_actor()
{
@ -78,11 +73,10 @@ if (!function_exists('interact_as_actor')) {
if (!function_exists('can_user_interact')) {
/**
* @return bool
* @throws DataException
*/
function can_user_interact()
function can_user_interact(): bool
{
return interact_as_actor() ? true : false;
return (bool) interact_as_actor();
}
}

View File

@ -8,20 +8,24 @@
use Config\Services;
/**
* Renders the breadcrumb navigation through the Breadcrumb service
*
* @param string $class to be added to the breadcrumb nav
* @return string html breadcrumb
*/
function render_breadcrumb($class = null)
{
$breadcrumb = Services::breadcrumb();
return $breadcrumb->render($class);
if (!function_exists('render_breadcrumb')) {
/**
* Renders the breadcrumb navigation through the Breadcrumb service
*
* @param string $class to be added to the breadcrumb nav
* @return string html breadcrumb
*/
function render_breadcrumb(string $class = null): string
{
$breadcrumb = Services::breadcrumb();
return $breadcrumb->render($class);
}
}
function replace_breadcrumb_params($newParams)
{
$breadcrumb = Services::breadcrumb();
$breadcrumb->replaceParams($newParams);
if (!function_exists('replace_breadcrumb_params')) {
function replace_breadcrumb_params($newParams): void
{
$breadcrumb = Services::breadcrumb();
$breadcrumb->replaceParams($newParams);
}
}

View File

@ -6,24 +6,25 @@
* @link https://castopod.org/
*/
use CodeIgniter\View\Table;
use CodeIgniter\I18n\Time;
if (!function_exists('button')) {
/**
* Button component
*
* Creates a stylized button or button like anchor tag if the URL is defined.
*
* @param string $label The button label
* @param mixed|null $uri URI string or array of URI segments
* @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes
* @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes
*
* @return string
*/
function button(
string $label = '',
$uri = null,
$customOptions = [],
$customAttributes = []
string $uri = '',
array $customOptions = [],
array $customAttributes = []
): string {
$defaultOptions = [
'variant' => 'default',
@ -90,7 +91,7 @@ if (!function_exists('button')) {
$label .= icon($options['iconRight'], 'ml-2');
}
if ($uri) {
if ($uri !== '') {
return anchor(
$uri,
$label,
@ -111,8 +112,8 @@ if (!function_exists('button')) {
);
return <<<HTML
<button class="$buttonClass" $attributes>
$label
<button class="{$buttonClass}" {$attributes}>
{$label}
</button>
HTML;
}
@ -126,19 +127,19 @@ if (!function_exists('icon_button')) {
*
* Abstracts the `button()` helper to create a stylized icon button
*
* @param string $label The button label
* @param mixed|null $uri URI string or array of URI segments
* @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes
* @param string $label The button label
* @param string $uri URI string or array of URI segments
* @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes
*
* @return string
*/
function icon_button(
string $icon,
string $title,
$uri = null,
$customOptions = [],
$customAttributes = []
string $uri = '',
array $customOptions = [],
array $customAttributes = []
): string {
$defaultOptions = [
'isSquared' => true,
@ -197,9 +198,9 @@ if (!function_exists('data_table')) {
*
* @return string
*/
function data_table($columns, $data = [], ...$rest): string
function data_table(array $columns, array $data = [], ...$rest): string
{
$table = new \CodeIgniter\View\Table();
$table = new Table();
$template = [
'table_open' => '<table class="w-full whitespace-no-wrap">',
@ -219,17 +220,17 @@ if (!function_exists('data_table')) {
$tableHeaders = [];
foreach ($columns as $column) {
array_push($tableHeaders, $column['header']);
$tableHeaders[] = $column['header'];
}
$table->setHeading($tableHeaders);
if ($dataCount = count($data)) {
for ($i = 0; $i < $dataCount; $i++) {
if (($dataCount = count($data)) !== 0) {
for ($i = 0; $i < $dataCount; ++$i) {
$row = $data[$i];
$rowData = [];
foreach ($columns as $column) {
array_push($rowData, $column['cell']($row, ...$rest));
$rowData[] = $column['cell']($row, ...$rest);
}
$table->addRow($rowData);
}
@ -251,38 +252,39 @@ if (!function_exists('publication_pill')) {
*
* Shows the stylized publication datetime in regards to current datetime.
*
* @param \CodeIgniter\I18n\Time $publicationDate publication datetime of the episode
* @param Time $publicationDate publication datetime of the episode
* @param boolean $isPublished whether or not the episode has been published
* @param string $customClass css class to add to the component
*
* @return string
*/
function publication_pill(
$publicationDate,
?Time $publicationDate,
$publicationStatus,
$customClass = ''
string $customClass = ''
): string {
if ($publicationDate === null) {
return '';
}
$class =
$publicationStatus === 'published'
? 'text-pine-500 border-pine-500'
: 'text-red-600 border-red-600';
$transParam = [];
if ($publicationDate) {
$transParam = [
'<time pubdate datetime="' .
$publicationDate->format(DateTime::ATOM) .
'" title="' .
$publicationDate .
'">' .
lang('Common.mediumDate', [$publicationDate]) .
'</time>',
];
}
$langOptions = [
'<time pubdate datetime="' .
$publicationDate->format(DateTime::ATOM) .
'" title="' .
$publicationDate .
'">' .
lang('Common.mediumDate', [$publicationDate]) .
'</time>',
];
$label = lang(
'Episode.publication_status.' . $publicationStatus,
$transParam,
$langOptions,
);
return '<span class="px-1 font-semibold border ' .
@ -303,15 +305,13 @@ if (!function_exists('publication_button')) {
*
* Displays the appropriate publication button depending on the publication status.
*
* @param integer $podcastId
* @param integer $episodeId
* @param boolean $publicationStatus the episode's publication status *
* @return string
*/
function publication_button(
$podcastId,
$episodeId,
$publicationStatus
int $podcastId,
int $episodeId,
bool $publicationStatus
): string {
switch ($publicationStatus) {
case 'not_published':
@ -351,17 +351,15 @@ if (!function_exists('episode_numbering')) {
/**
* Returns relevant translated episode numbering.
*
* @param int|null $episodeNumber
* @param int|null $seasonNumber
* @param string $class styling classes
* @param string $is_abbr component will show abbreviated numbering if true
*
* @return string|null
*/
function episode_numbering(
$episodeNumber = null,
$seasonNumber = null,
$class = '',
?int $episodeNumber = null,
?int $seasonNumber = null,
string $class = '',
$isAbbr = false
): string {
if (!$episodeNumber && !$seasonNumber) {
@ -409,36 +407,28 @@ if (!function_exists('episode_numbering')) {
if (!function_exists('location_link')) {
/**
* Returns link to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
*/
function location_link(
$locationName,
$locationGeo,
$locationOsmid,
?string $locationName,
?string $locationGeo,
?string $locationOsmid,
$class = ''
) {
$link = '';
if (!empty($locationName)) {
$link = anchor(
location_url($locationName, $locationGeo, $locationOsmid),
icon('map-pin', 'mr-2') . $locationName,
[
'class' =>
'inline-flex items-baseline hover:underline' .
(empty($class) ? '' : " $class"),
'target' => '_blank',
'rel' => 'noreferrer noopener',
],
);
): string {
if (empty($locationName)) {
return '';
}
return $link;
return anchor(
location_url($locationName, $locationGeo, $locationOsmid),
icon('map-pin', 'mr-2') . $locationName,
[
'class' =>
'inline-flex items-baseline hover:underline' .
(empty($class) ? '' : " {$class}"),
'target' => '_blank',
'rel' => 'noreferrer noopener',
],
);
}
}

View File

@ -1,4 +1,5 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
@ -71,20 +72,15 @@ if (!function_exists('form_switch')) {
*
* Abstracts form_label to stylize it as a switch toggle
*
* @param array $data
* @param string $value
* @param boolean $checked
* @param mixed $extra
*
* @return string
*/
function form_switch(
$label = '',
$data = '',
array $data = [],
string $value = '',
bool $checked = false,
$class = '',
$extra = ''
string $class = '',
array $extra = []
): string {
$data['class'] = 'form-switch';
@ -155,31 +151,26 @@ if (!function_exists('form_multiselect')) {
/**
* Multi-select menu
*
* @param string $name
* @param array $options
* @param array $selected
* @param mixed $extra
*
* @return string
*/
function form_multiselect(
string $name = '',
array $options = [],
array $selected = [],
$customExtra = ''
array $customExtra = []
): string {
$defaultExtra = [
'data-class' => $customExtra['class'],
'data-select-text' => lang('Common.forms.multiSelect.selectText'),
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
'data-no-results-text' => lang(
'Common.forms.multiSelect.noResultsText'
'Common.forms.multiSelect.noResultsText',
),
'data-no-choices-text' => lang(
'Common.forms.multiSelect.noChoicesText'
'Common.forms.multiSelect.noChoicesText',
),
'data-max-item-text' => lang(
'Common.forms.multiSelect.maxItemText'
'Common.forms.multiSelect.maxItemText',
),
];
$extra = stringify_attributes(array_merge($defaultExtra, $customExtra));

View File

@ -6,108 +6,112 @@
* @link https://castopod.org/
*/
use App\Entities\Episode;
use CodeIgniter\Files\File;
use JamesHeinrich\GetID3\GetID3;
use JamesHeinrich\GetID3\WriteTags;
/**
* Gets audio file metadata and ID3 info
*
* @param UploadedFile $file
*
* @return array
*/
function get_file_tags($file)
{
$getID3 = new GetID3();
$FileInfo = $getID3->analyze($file);
if (!function_exists('get_file_tags')) {
/**
* Gets audio file metadata and ID3 info
*
* @param UploadedFile $file
*/
function get_file_tags($file): array
{
$getID3 = new GetID3();
$FileInfo = $getID3->analyze($file);
return [
'filesize' => $FileInfo['filesize'],
'mime_type' => $FileInfo['mime_type'],
'avdataoffset' => $FileInfo['avdataoffset'],
'playtime_seconds' => $FileInfo['playtime_seconds'],
];
}
/**
* Write audio file metadata / ID3 tags
*
* @param App\Entities\Episode $episode
*
* @return UploadedFile
*/
function write_audio_file_tags($episode)
{
helper('media');
$TextEncoding = 'UTF-8';
// Initialize getID3 tag-writing module
$tagwriter = new WriteTags();
$tagwriter->filename = media_path($episode->audio_file_path);
// set various options (optional)
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
$cover = new \CodeIgniter\Files\File($episode->image->id3_path);
$APICdata = file_get_contents($cover->getRealPath());
// TODO: variables used for podcast specific tags
// $podcast_url = $episode->podcast->link;
// $podcast_feed_url = $episode->podcast->feed_url;
// $episode_media_url = $episode->link;
// populate data array
$TagData = [
'title' => [$episode->title],
'artist' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'album' => [$episode->podcast->title],
'year' => [
$episode->published_at ? $episode->published_at->format('Y') : '',
],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [strval($episode->number)],
'copyright_message' => [$episode->podcast->copyright],
'publisher' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'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],
// 'podcast' => [],
// 'podcast_identifier' => [$episode_media_url],
// 'podcast_feed' => [$podcast_feed_url],
// 'podcast_description' => [$podcast->description_markdown],
];
$TagData['attached_picture'][] = [
'picturetypeid' => 2, // Cover. More: module.tag.id3v2.php
'data' => $APICdata,
'description' => 'cover',
'mime' => $cover->getMimeType(),
];
$tagwriter->tag_data = $TagData;
// write tags
if ($tagwriter->WriteTags()) {
echo 'Successfully wrote tags<br>';
if (!empty($tagwriter->warnings)) {
echo 'There were some warnings:<br>' .
implode('<br><br>', $tagwriter->warnings);
}
} else {
echo 'Failed to write tags!<br>' .
implode('<br><br>', $tagwriter->errors);
return [
'filesize' => $FileInfo['filesize'],
'mime_type' => $FileInfo['mime_type'],
'avdataoffset' => $FileInfo['avdataoffset'],
'playtime_seconds' => $FileInfo['playtime_seconds'],
];
}
}
if (!function_exists('write_audio_file_tags')) {
/**
* Write audio file metadata / ID3 tags
*
* @return UploadedFile
*/
function write_audio_file_tags(Episode $episode): void
{
helper('media');
$TextEncoding = 'UTF-8';
// Initialize getID3 tag-writing module
$tagwriter = new WriteTags();
$tagwriter->filename = media_path($episode->audio_file_path);
// set various options (optional)
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
$cover = new File($episode->image->id3_path);
$APICdata = file_get_contents($cover->getRealPath());
// TODO: variables used for podcast specific tags
// $podcast_url = $episode->podcast->link;
// $podcast_feed_url = $episode->podcast->feed_url;
// $episode_media_url = $episode->link;
// populate data array
$TagData = [
'title' => [$episode->title],
'artist' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'album' => [$episode->podcast->title],
'year' => [
$episode->published_at
? $episode->published_at->format('Y')
: '',
],
'genre' => ['Podcast'],
'comment' => [$episode->description],
'track_number' => [strval($episode->number)],
'copyright_message' => [$episode->podcast->copyright],
'publisher' => [
empty($episode->podcast->publisher)
? $episode->podcast->owner_name
: $episode->podcast->publisher,
],
'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],
// 'podcast' => [],
// 'podcast_identifier' => [$episode_media_url],
// 'podcast_feed' => [$podcast_feed_url],
// 'podcast_description' => [$podcast->description_markdown],
];
$TagData['attached_picture'][] = [
'picturetypeid' => 2, // Cover. More: module.tag.id3v2.php
'data' => $APICdata,
'description' => 'cover',
'mime' => $cover->getMimeType(),
];
$tagwriter->tag_data = $TagData;
// write tags
if ($tagwriter->WriteTags()) {
echo 'Successfully wrote tags<br>';
if (!empty($tagwriter->warnings)) {
echo 'There were some warnings:<br>' .
implode('<br><br>', $tagwriter->warnings);
}
} else {
echo 'Failed to write tags!<br>' .
implode('<br><br>', $tagwriter->errors);
}
}
}

View File

@ -6,47 +6,56 @@
* @link https://castopod.org/
*/
/**
* Fetches places from Nominatim OpenStreetMap
*
* @param string $locationName
*
* @return array|null
*/
function fetch_osm_location($locationName)
{
$osmObject = null;
if (!empty($locationName)) {
try {
$client = \Config\Services::curlrequest();
use Config\Services;
$response = $client->request(
'GET',
'https://nominatim.openstreetmap.org/search.php?q=' .
urlencode($locationName) .
'&polygon_geojson=1&format=jsonv2',
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
if (!function_exists('fetch_osm_location')) {
/**
* Fetches places from Nominatim OpenStreetMap
*
* @return array|null
*/
function fetch_osm_location(string $locationName): ?array
{
$osmObject = null;
if (!empty($locationName)) {
try {
$client = Services::curlrequest();
$response = $client->request(
'GET',
'https://nominatim.openstreetmap.org/search.php?q=' .
urlencode($locationName) .
'&polygon_geojson=1&format=jsonv2',
[
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json',
],
],
]
);
$places = json_decode($response->getBody(), true);
$osmObject = [
'geo' =>
empty($places[0]['lat']) || empty($places[0]['lon'])
);
$places = json_decode(
$response->getBody(),
true,
512,
JSON_THROW_ON_ERROR,
);
$osmObject = [
'geo' =>
empty($places[0]['lat']) || empty($places[0]['lon'])
? null
: "geo:{$places[0]['lat']},{$places[0]['lon']}",
'osmid' => empty($places[0]['osm_type'])
? null
: "geo:{$places[0]['lat']},{$places[0]['lon']}",
'osmid' => empty($places[0]['osm_type'])
? null
: strtoupper(substr($places[0]['osm_type'], 0, 1)) .
$places[0]['osm_id'],
];
} catch (\Exception $e) {
//If things go wrong the show must go on
log_message('critical', $e);
: strtoupper(substr($places[0]['osm_type'], 0, 1)) .
$places[0]['osm_id'],
];
} catch (Exception $exception) {
//If things go wrong the show must go on
log_message('critical', $exception);
}
}
return $osmObject;
}
return $osmObject;
}

View File

@ -8,125 +8,120 @@
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\Files\UploadedFile;
use Config\Services;
/**
* Saves a file to the corresponding podcast folder in `public/media`
*
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $filePath
* @param string $folder
* @param string $fileName
*
* @return string The episode's file path in media root
*/
function save_media($filePath, $folder, $mediaName)
{
$fileName = $mediaName . '.' . $filePath->getExtension();
if (!function_exists('save_media')) {
/**
* Saves a file to the corresponding podcast folder in `public/media`
*
* @param File|UploadedFile $filePath
*/
function save_media(
File $filePath,
string $folder,
string $mediaName
): string {
$fileName = $mediaName . '.' . $filePath->getExtension();
$mediaRoot = config('App')->mediaRoot . '/' . $folder;
$mediaRoot = config('App')->mediaRoot . '/' . $folder;
if (!file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true);
touch($mediaRoot . '/index.html');
if (!file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true);
touch($mediaRoot . '/index.html');
}
// move to media folder and overwrite file if already existing
$filePath->move($mediaRoot . '/', $fileName, true);
return $folder . '/' . $fileName;
}
// move to media folder and overwrite file if already existing
$filePath->move($mediaRoot . '/', $fileName, true);
return $folder . '/' . $fileName;
}
/**
* @param string $fileUrl
* @return File
*/
function download_file($fileUrl)
{
$client = \Config\Services::curlrequest();
if (!function_exists('download_file')) {
function download_file(string $fileUrl): File
{
$client = Services::curlrequest();
$response = $client->get($fileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
]);
// redirect to new file location
$newFileUrl = $fileUrl;
while (
in_array(
$response->getStatusCode(),
[
ResponseInterface::HTTP_MOVED_PERMANENTLY,
ResponseInterface::HTTP_FOUND,
ResponseInterface::HTTP_SEE_OTHER,
ResponseInterface::HTTP_NOT_MODIFIED,
ResponseInterface::HTTP_TEMPORARY_REDIRECT,
ResponseInterface::HTTP_PERMANENT_REDIRECT,
],
true,
)
) {
$newFileUrl = (string) trim(
$response->getHeader('location')->getValue(),
);
$response = $client->get($newFileUrl, [
$response = $client->get($fileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
'http_errors' => false,
]);
// redirect to new file location
$newFileUrl = $fileUrl;
while (
in_array(
$response->getStatusCode(),
[
ResponseInterface::HTTP_MOVED_PERMANENTLY,
ResponseInterface::HTTP_FOUND,
ResponseInterface::HTTP_SEE_OTHER,
ResponseInterface::HTTP_NOT_MODIFIED,
ResponseInterface::HTTP_TEMPORARY_REDIRECT,
ResponseInterface::HTTP_PERMANENT_REDIRECT,
],
true,
)
) {
$newFileUrl = trim($response->getHeader('location')->getValue());
$response = $client->get($newFileUrl, [
'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION,
],
'http_errors' => false,
]);
}
$tmpFilename =
time() .
'_' .
bin2hex(random_bytes(10)) .
'.' .
pathinfo(parse_url($newFileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
$tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpFilePath, $response->getBody());
return new File($tmpFilePath);
}
$tmpFilename =
time() .
'_' .
bin2hex(random_bytes(10)) .
'.' .
pathinfo(parse_url($newFileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
$tmpFilePath = WRITEPATH . 'uploads/' . $tmpFilename;
file_put_contents($tmpFilePath, $response->getBody());
return new \CodeIgniter\Files\File($tmpFilePath);
}
if (!function_exists('media_path')) {
/**
* Prefixes the root media path to a given uri
*
* @param string|array $uri URI string or array of URI segments
*/
function media_path($uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
/**
* Prefixes the root media path to a given uri
*
* @param mixed $uri URI string or array of URI segments
* @return string
*/
function media_path($uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
return config('App')->mediaRoot . '/' . $uri;
}
$uri = trim($uri, '/');
return config('App')->mediaRoot . '/' . $uri;
}
/**
* Return the media base URL to use in views
*
* @param mixed $uri URI string or array of URI segments
* @param string $protocol
* @return string
*/
function media_url($uri = '', string $protocol = null): string
{
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
}
if (!function_exists('media_base_url')) {
/**
* Return the media base URL to use in views
*
* @param string|array $uri URI string or array of URI segments
* @param string $protocol
*/
function media_base_url($uri = ''): string
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
function media_base_url($uri = '')
{
// convert segment array to string
if (is_array($uri)) {
$uri = implode('/', $uri);
return rtrim(config('App')->mediaBaseURL, '/') .
'/' .
config('App')->mediaRoot .
'/' .
$uri;
}
$uri = trim($uri, '/');
return rtrim(config('App')->mediaBaseURL, '/') .
'/' .
config('App')->mediaRoot .
'/' .
$uri;
}

View File

@ -6,142 +6,140 @@
* @link https://castopod.org/
*/
/**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`
*
* @param mixed $http_accept_language
*
* @return string|null ISO 639-1 language code or null
*/
function get_browser_language($http_accept_language)
{
$langs = explode(',', $http_accept_language);
if (!empty($langs)) {
return substr($langs[0], 0, 2);
}
if (!function_exists('get_browser_language')) {
/**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`
*
* @return string|null ISO 639-1 language code or null
*/
function get_browser_language(string $httpAcceptLanguage): ?string
{
$langs = explode(',', $httpAcceptLanguage);
if (!empty($langs)) {
return substr($langs[0], 0, 2);
}
return null;
return null;
}
}
/**
* Check if a string starts with some characters
*
* @param string $string
* @param string $query
*
* @return bool
*/
function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
if (!function_exists('startsWith')) {
/**
* Check if a string starts with some characters
*/
function startsWith(string $string, string $query): bool
{
return substr($string, 0, strlen($query)) === $query;
}
}
function slugify($text)
{
if (empty($text)) {
return 'n-a';
if (!function_exists('slugify')) {
function slugify($text)
{
if (empty($text)) {
return 'n-a';
}
// replace non letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$unwanted_array = [
'Š' => 'S',
'š' => 's',
'Đ' => 'Dj',
'đ' => 'dj',
'Ž' => 'Z',
'ž' => 'z',
'Č' => 'C',
'č' => 'c',
'Ć' => 'C',
'ć' => 'c',
'À' => 'A',
'Á' => 'A',
'Â' => 'A',
'Ã' => 'A',
'Ä' => 'A',
'Å' => 'A',
'Æ' => 'AE',
'Ç' => 'C',
'È' => 'E',
'É' => 'E',
'Ê' => 'E',
'Ë' => 'E',
'Ì' => 'I',
'Í' => 'I',
'Î' => 'I',
'Ï' => 'I',
'Ñ' => 'N',
'Ò' => 'O',
'Ó' => 'O',
'Ô' => 'O',
'Õ' => 'O',
'Ö' => 'O',
'Ø' => 'O',
'Œ' => 'OE',
'Ù' => 'U',
'Ú' => 'U',
'Û' => 'U',
'Ü' => 'U',
'Ý' => 'Y',
'Þ' => 'B',
'ß' => 'Ss',
'à' => 'a',
'á' => 'a',
'â' => 'a',
'ã' => 'a',
'ä' => 'a',
'å' => 'a',
'æ' => 'ae',
'ç' => 'c',
'è' => 'e',
'é' => 'e',
'ê' => 'e',
'ë' => 'e',
'ì' => 'i',
'í' => 'i',
'î' => 'i',
'ï' => 'i',
'ð' => 'o',
'ñ' => 'n',
'ò' => 'o',
'ó' => 'o',
'ô' => 'o',
'õ' => 'o',
'ö' => 'o',
'ø' => 'o',
'œ' => 'OE',
'ù' => 'u',
'ú' => 'u',
'û' => 'u',
'ý' => 'y',
'þ' => 'b',
'ÿ' => 'y',
'Ŕ' => 'R',
'ŕ' => 'r',
'/' => '-',
' ' => '-',
];
$text = strtr($text, $unwanted_array);
// transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
// remove unwanted characters
$text = preg_replace('~[^\\-\w]+~', '', $text);
// trim
$text = trim($text, '-');
// remove duplicate -
$text = preg_replace('~-+~', '-', $text);
// lowercase
$text = strtolower($text);
return $text;
}
// replace non letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$unwanted_array = [
'Š' => 'S',
'š' => 's',
'Đ' => 'Dj',
'đ' => 'dj',
'Ž' => 'Z',
'ž' => 'z',
'Č' => 'C',
'č' => 'c',
'Ć' => 'C',
'ć' => 'c',
'À' => 'A',
'Á' => 'A',
'Â' => 'A',
'Ã' => 'A',
'Ä' => 'A',
'Å' => 'A',
'Æ' => 'AE',
'Ç' => 'C',
'È' => 'E',
'É' => 'E',
'Ê' => 'E',
'Ë' => 'E',
'Ì' => 'I',
'Í' => 'I',
'Î' => 'I',
'Ï' => 'I',
'Ñ' => 'N',
'Ò' => 'O',
'Ó' => 'O',
'Ô' => 'O',
'Õ' => 'O',
'Ö' => 'O',
'Ø' => 'O',
'Œ' => 'OE',
'Ù' => 'U',
'Ú' => 'U',
'Û' => 'U',
'Ü' => 'U',
'Ý' => 'Y',
'Þ' => 'B',
'ß' => 'Ss',
'à' => 'a',
'á' => 'a',
'â' => 'a',
'ã' => 'a',
'ä' => 'a',
'å' => 'a',
'æ' => 'ae',
'ç' => 'c',
'è' => 'e',
'é' => 'e',
'ê' => 'e',
'ë' => 'e',
'ì' => 'i',
'í' => 'i',
'î' => 'i',
'ï' => 'i',
'ð' => 'o',
'ñ' => 'n',
'ò' => 'o',
'ó' => 'o',
'ô' => 'o',
'õ' => 'o',
'ö' => 'o',
'ø' => 'o',
'œ' => 'OE',
'ù' => 'u',
'ú' => 'u',
'û' => 'u',
'ý' => 'y',
'ý' => 'y',
'þ' => 'b',
'ÿ' => 'y',
'Ŕ' => 'R',
'ŕ' => 'r',
'/' => '-',
' ' => '-',
];
$text = strtr($text, $unwanted_array);
// transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
// remove unwanted characters
$text = preg_replace('~[^-\w]+~', '', $text);
// trim
$text = trim($text, '-');
// remove duplicate -
$text = preg_replace('~-+~', '-', $text);
// lowercase
$text = strtolower($text);
return $text;
}
//--------------------------------------------------------------------
@ -151,11 +149,8 @@ if (!function_exists('format_duration')) {
* Formats duration in seconds to an hh:mm:ss string
*
* @param int $seconds seconds to format
* @param string $separator
*
* @return string
*/
function format_duration($seconds, $separator = ':')
function format_duration(int $seconds, string $separator = ':'): string
{
return sprintf(
'%02d%s%02d%s%02d',
@ -163,7 +158,7 @@ if (!function_exists('format_duration')) {
$separator,
($seconds / 60) % 60,
$separator,
$seconds % 60
$seconds % 60,
);
}
}

View File

@ -8,26 +8,27 @@
use App\Models\PageModel;
/**
* Returns instance pages as links inside nav tag
*
* @param string $class
* @return string html pages navigation
*/
function render_page_links($class = null)
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
]);
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
if (!function_exists('render_page_links')) {
/**
* Returns instance pages as links inside nav tag
*
* @return string html pages navigation
*/
function render_page_links(string $class = null): string
{
$pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [
'class' => 'px-2 underline hover:no-underline',
]);
}
$links .= anchor(route_to('credits'), lang('Person.credits'), [
'class' => 'px-2 underline hover:no-underline',
]);
foreach ($pages as $page) {
$links .= anchor($page->link, $page->title, [
'class' => 'px-2 underline hover:no-underline',
]);
}
return '<nav class="' . $class . '">' . $links . '</nav>';
return '<nav class="' . $class . '">' . $links . '</nav>';
}
}

View File

@ -6,45 +6,47 @@
* @link https://castopod.org/
*/
/**
* Fetches persons from an episode
*
* @param array $persons
* @param array &$personsArray
*/
function construct_person_array($persons, &$personsArray)
{
foreach ($persons as $person) {
if (array_key_exists($person->person->id, $personsArray)) {
$personsArray[$person->person->id]['roles'] .=
empty($person->person_group) || empty($person->person_role)
? ''
: (empty($personsArray[$person->person->id]['roles'])
? ''
: ', ') .
lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
);
} else {
$personsArray[$person->person->id] = [
'full_name' => $person->person->full_name,
'information_url' => $person->person->information_url,
'thumbnail_url' => $person->person->image->thumbnail_url,
'roles' =>
if (!function_exists('construct_person_array')) {
/**
* Fetches persons from an episode
*
* @param array &$personsArray
*/
function construct_person_array(array $persons, &$personsArray): void
{
foreach ($persons as $person) {
if (array_key_exists($person->person->id, $personsArray)) {
$personsArray[$person->person->id]['roles'] .=
empty($person->person_group) || empty($person->person_role)
? ''
: lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
),
];
: (empty($personsArray[$person->person->id]['roles'])
? ''
: ', ') .
lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
);
} else {
$personsArray[$person->person->id] = [
'full_name' => $person->person->full_name,
'information_url' => $person->person->information_url,
'thumbnail_url' => $person->person->image->thumbnail_url,
'roles' =>
empty($person->person_group) ||
empty($person->person_role)
? ''
: lang(
'PersonsTaxonomy.persons.' .
$person->person_group .
'.roles.' .
$person->person_role .
'.label',
),
];
}
}
}
}

View File

@ -9,517 +9,564 @@
use App\Libraries\SimpleRSSElement;
use CodeIgniter\I18n\Time;
use Config\Mimes;
use App\Entities\Podcast;
use App\Entities\Category;
/**
* Generates the rss feed for a given podcast entity
*
* @param App\Entities\Podcast $podcast
* @param string $service The name of the service that fetches the RSS feed for future reference when the audio file is eventually downloaded
* @return string rss feed as xml
*/
function get_rss_feed($podcast, $serviceSlug = '')
{
$episodes = $podcast->episodes;
if (!function_exists('get_rss_feed')) {
/**
* Generates the rss feed for a given podcast entity
*
* @param string $service The name of the service that fetches the RSS feed for future reference when the audio file is eventually downloaded
* @return string rss feed as xml
*/
function get_rss_feed(Podcast $podcast, $serviceSlug = ''): string
{
$episodes = $podcast->episodes;
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$podcast_namespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$podcast_namespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='$itunes_namespace' xmlns:podcast='$podcast_namespace' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
);
$channel = $rss->addChild('channel');
$atom_link = $channel->addChild(
'atom:link',
null,
'http://www.w3.org/2005/Atom',
);
$atom_link->addAttribute('href', $podcast->feed_url);
$atom_link->addAttribute('rel', 'self');
$atom_link->addAttribute('type', 'application/rss+xml');
if (!empty($podcast->new_feed_url)) {
$channel->addChild(
'new-feed-url',
$podcast->new_feed_url,
$itunes_namespace,
$rss = new SimpleRSSElement(
"<?xml version='1.0' encoding='utf-8'?><rss version='2.0' xmlns:itunes='{$itunes_namespace}' xmlns:podcast='{$podcast_namespace}' xmlns:content='http://purl.org/rss/1.0/modules/content/'></rss>",
);
}
// the last build date corresponds to the creation of the feed.xml cache
$channel->addChild(
'lastBuildDate',
(new Time('now'))->format(DATE_RFC1123),
);
$channel->addChild('generator', 'Castopod Host - https://castopod.org/');
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
$channel = $rss->addChild('channel');
$channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html);
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
$itunes_image->addAttribute('href', $podcast->image->original_url);
$channel->addChild('language', $podcast->language_code);
if (!empty($podcast->location_name)) {
$locationElement = $channel->addChild(
'location',
htmlspecialchars($podcast->location_name),
$podcast_namespace,
);
if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $podcast->location_geo);
}
if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osm', $podcast->location_osmid);
}
}
if (!empty($podcast->payment_pointer)) {
$valueElement = $channel->addChild('value', null, $podcast_namespace);
$valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', '');
$valueElement->addAttribute('suggested', '');
$recipientElement = $valueElement->addChild(
'valueRecipient',
$atom_link = $channel->addChild(
'atom:link',
null,
$podcast_namespace,
'http://www.w3.org/2005/Atom',
);
$recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'ILP');
$recipientElement->addAttribute('address', $podcast->payment_pointer);
$recipientElement->addAttribute('split', 100);
}
$channel
->addChild(
'locked',
$podcast->is_locked ? 'yes' : 'no',
$podcast_namespace,
)
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) {
$atom_link->addAttribute('href', $podcast->feed_url);
$atom_link->addAttribute('rel', 'self');
$atom_link->addAttribute('type', 'application/rss+xml');
if (!empty($podcast->new_feed_url)) {
$channel->addChild(
'new-feed-url',
$podcast->new_feed_url,
$itunes_namespace,
);
}
// the last build date corresponds to the creation of the feed.xml cache
$channel->addChild(
'previousUrl',
$podcast->imported_feed_url,
$podcast_namespace,
'lastBuildDate',
(new Time('now'))->format(DATE_RFC1123),
);
}
foreach ($podcast->podcastingPlatforms as $podcastingPlatform) {
$podcastingPlatformElement = $channel->addChild(
'id',
null,
$podcast_namespace,
$channel->addChild(
'generator',
'Castopod Host - https://castopod.org/',
);
$podcastingPlatformElement->addAttribute(
'platform',
$podcastingPlatform->slug,
);
if (!empty($podcastingPlatform->link_content)) {
$podcastingPlatformElement->addAttribute(
'id',
$podcastingPlatform->link_content,
);
}
if (!empty($podcastingPlatform->link_url)) {
$podcastingPlatformElement->addAttribute(
'url',
htmlspecialchars($podcastingPlatform->link_url),
);
}
}
$channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
foreach ($podcast->socialPlatforms as $socialPlatform) {
$socialPlatformElement = $channel->addChild(
'social',
$socialPlatform->link_content,
$podcast_namespace,
);
$socialPlatformElement->addAttribute('platform', $socialPlatform->slug);
if (!empty($socialPlatform->link_url)) {
$socialPlatformElement->addAttribute(
'url',
htmlspecialchars($socialPlatform->link_url),
);
}
}
$channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html);
foreach ($podcast->fundingPlatforms as $fundingPlatform) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->link_content,
$podcast_namespace,
);
$fundingPlatformElement->addAttribute(
'platform',
$fundingPlatform->slug,
);
if (!empty($socialPlatform->link_url)) {
$fundingPlatformElement->addAttribute(
'url',
htmlspecialchars($fundingPlatform->link_url),
);
}
}
$itunes_image = $channel->addChild('image', null, $itunes_namespace);
foreach ($podcast->persons as $podcastPerson) {
$podcastPersonElement = $channel->addChild(
'person',
htmlspecialchars($podcastPerson->person->full_name),
$podcast_namespace,
);
if (
!empty($podcastPerson->person_role) &&
!empty($podcastPerson->person_group)
) {
$podcastPersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
[],
'en',
),
),
);
}
if (!empty($podcastPerson->person_group)) {
$podcastPersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
[],
'en',
),
),
);
}
$podcastPersonElement->addAttribute(
'img',
$podcastPerson->person->image->large_url,
);
if (!empty($podcastPerson->person->information_url)) {
$podcastPersonElement->addAttribute(
'href',
$podcastPerson->person->information_url,
);
}
}
$itunes_image->addAttribute('href', $podcast->image->original_url);
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
add_category_tag($channel, $other_category);
}
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace,
);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunes_namespace,
);
$channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunes_namespace);
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
$channel->addChild('type', $podcast->type, $itunes_namespace);
$podcast->copyright && $channel->addChild('copyright', $podcast->copyright);
$podcast->is_blocked &&
$channel->addChild('block', 'Yes', $itunes_namespace);
$podcast->is_completed &&
$channel->addChild('complete', 'Yes', $itunes_namespace);
$image = $channel->addChild('image');
$image->addChild('url', $podcast->image->feed_url);
$image->addChild('title', $podcast->title);
$image->addChild('link', $podcast->link);
if (!empty($podcast->custom_rss)) {
array_to_rss(
[
'elements' => $podcast->custom_rss,
],
$channel,
);
}
foreach ($episodes as $episode) {
$item = $channel->addChild('item');
$item->addChild('title', $episode->title);
$enclosure = $item->addChild('enclosure');
$enclosure->addAttribute(
'url',
$episode->audio_file_analytics_url .
(empty($serviceSlug)
? ''
: '?_from=' . urlencode($serviceSlug)),
);
$enclosure->addAttribute('length', $episode->audio_file_size);
$enclosure->addAttribute('type', $episode->audio_file_mimetype);
$item->addChild('guid', $episode->guid);
$item->addChild(
'pubDate',
$episode->published_at->format(DATE_RFC1123),
);
if (!empty($episode->location_name)) {
$locationElement = $item->addChild(
$channel->addChild('language', $podcast->language_code);
if (!empty($podcast->location_name)) {
$locationElement = $channel->addChild(
'location',
htmlspecialchars($episode->location_name),
htmlspecialchars($podcast->location_name),
$podcast_namespace,
);
if (!empty($episode->location_geo)) {
$locationElement->addAttribute('geo', $episode->location_geo);
if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $podcast->location_geo);
}
if (!empty($episode->location_osmid)) {
$locationElement->addAttribute('osm', $episode->location_osmid);
if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osm', $podcast->location_osmid);
}
}
$item->addChildWithCDATA(
'description',
$episode->getDescriptionHtml($serviceSlug),
);
$item->addChild(
'duration',
$episode->audio_file_duration,
$itunes_namespace,
);
$item->addChild('link', $episode->link);
$episode_itunes_image = $item->addChild(
'image',
null,
$itunes_namespace,
);
$episode_itunes_image->addAttribute('href', $episode->image->feed_url);
$episode->parental_advisory &&
$item->addChild(
'explicit',
$episode->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace,
);
$episode->number &&
$item->addChild('episode', $episode->number, $itunes_namespace);
$episode->season_number &&
$item->addChild(
'season',
$episode->season_number,
$itunes_namespace,
);
$item->addChild('episodeType', $episode->type, $itunes_namespace);
if ($episode->transcript_file_url) {
$transcriptElement = $item->addChild(
'transcript',
if (!empty($podcast->payment_pointer)) {
$valueElement = $channel->addChild(
'value',
null,
$podcast_namespace,
);
$transcriptElement->addAttribute(
'url',
$episode->transcript_file_url,
);
$transcriptElement->addAttribute(
'type',
Mimes::guessTypeFromExtension(
pathinfo($episode->transcript_file_url, PATHINFO_EXTENSION),
),
);
$transcriptElement->addAttribute(
'language',
$podcast->language_code,
);
}
if ($episode->chapters_file_url) {
$chaptersElement = $item->addChild(
'chapters',
$valueElement->addAttribute('type', 'webmonetization');
$valueElement->addAttribute('method', '');
$valueElement->addAttribute('suggested', '');
$recipientElement = $valueElement->addChild(
'valueRecipient',
null,
$podcast_namespace,
);
$chaptersElement->addAttribute('url', $episode->chapters_file_url);
$chaptersElement->addAttribute('type', 'application/json+chapters');
$recipientElement->addAttribute('name', $podcast->owner_name);
$recipientElement->addAttribute('type', 'ILP');
$recipientElement->addAttribute(
'address',
$podcast->payment_pointer,
);
$recipientElement->addAttribute('split', 100);
}
foreach ($episode->soundbites as $soundbite) {
$soundbiteElement = $item->addChild(
'soundbite',
empty($soundbite->label) ? null : $soundbite->label,
$channel
->addChild(
'locked',
$podcast->is_locked ? 'yes' : 'no',
$podcast_namespace,
)
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) {
$channel->addChild(
'previousUrl',
$podcast->imported_feed_url,
$podcast_namespace,
);
$soundbiteElement->addAttribute(
'start_time',
$soundbite->start_time,
);
$soundbiteElement->addAttribute('duration', $soundbite->duration);
}
foreach ($episode->persons as $episodePerson) {
$episodePersonElement = $item->addChild(
foreach ($podcast->podcastingPlatforms as $podcastingPlatform) {
$podcastingPlatformElement = $channel->addChild(
'id',
null,
$podcast_namespace,
);
$podcastingPlatformElement->addAttribute(
'platform',
$podcastingPlatform->slug,
);
if (!empty($podcastingPlatform->link_content)) {
$podcastingPlatformElement->addAttribute(
'id',
$podcastingPlatform->link_content,
);
}
if (!empty($podcastingPlatform->link_url)) {
$podcastingPlatformElement->addAttribute(
'url',
htmlspecialchars($podcastingPlatform->link_url),
);
}
}
foreach ($podcast->socialPlatforms as $socialPlatform) {
$socialPlatformElement = $channel->addChild(
'social',
$socialPlatform->link_content,
$podcast_namespace,
);
$socialPlatformElement->addAttribute(
'platform',
$socialPlatform->slug,
);
if (!empty($socialPlatform->link_url)) {
$socialPlatformElement->addAttribute(
'url',
htmlspecialchars($socialPlatform->link_url),
);
}
}
foreach ($podcast->fundingPlatforms as $fundingPlatform) {
$fundingPlatformElement = $channel->addChild(
'funding',
$fundingPlatform->link_content,
$podcast_namespace,
);
$fundingPlatformElement->addAttribute(
'platform',
$fundingPlatform->slug,
);
if (!empty($socialPlatform->link_url)) {
$fundingPlatformElement->addAttribute(
'url',
htmlspecialchars($fundingPlatform->link_url),
);
}
}
foreach ($podcast->persons as $podcastPerson) {
$podcastPersonElement = $channel->addChild(
'person',
htmlspecialchars($episodePerson->person->full_name),
htmlspecialchars($podcastPerson->person->full_name),
$podcast_namespace,
);
if (
!empty($episodePerson->person_role) &&
!empty($episodePerson->person_group)
!empty($podcastPerson->person_role) &&
!empty($podcastPerson->person_group)
) {
$episodePersonElement->addAttribute(
$podcastPersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
[],
'en',
),
),
);
}
if (!empty($episodePerson->person_group)) {
$episodePersonElement->addAttribute(
if (!empty($podcastPerson->person_group)) {
$podcastPersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
"PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
[],
'en',
),
),
);
}
$episodePersonElement->addAttribute(
$podcastPersonElement->addAttribute(
'img',
$episodePerson->person->image->large_url,
$podcastPerson->person->image->large_url,
);
if (!empty($episodePerson->person->information_url)) {
$episodePersonElement->addAttribute(
if (!empty($podcastPerson->person->information_url)) {
$podcastPersonElement->addAttribute(
'href',
$episodePerson->person->information_url,
$podcastPerson->person->information_url,
);
}
}
$episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace);
if (!empty($episode->custom_rss)) {
array_to_rss(
[
'elements' => $episode->custom_rss,
],
$item,
);
// set main category first, then other categories as apple
add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
add_category_tag($channel, $other_category);
}
}
return $rss->asXML();
}
/**
* Adds <itunes:category> and <category> tags to node for a given category
*
* @param \SimpleXMLElement $node
* @param \App\Entities\Category $category
*
* @return void
*/
function add_category_tag($node, $category)
{
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunes_category = $node->addChild('category', null, $itunes_namespace);
$itunes_category->addAttribute(
'text',
$category->parent
? $category->parent->apple_category
: $category->apple_category,
);
if ($category->parent) {
$itunes_category_child = $itunes_category->addChild(
'category',
null,
$channel->addChild(
'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
$itunes_namespace,
);
$itunes_category_child->addAttribute('text', $category->apple_category);
$node->addChild('category', $category->parent->apple_category);
$channel->addChild(
'author',
$podcast->publisher ? $podcast->publisher : $podcast->owner_name,
$itunes_namespace,
);
$channel->addChild('link', $podcast->link);
$owner = $channel->addChild('owner', null, $itunes_namespace);
$owner->addChild('name', $podcast->owner_name, $itunes_namespace);
$owner->addChild('email', $podcast->owner_email, $itunes_namespace);
$channel->addChild('type', $podcast->type, $itunes_namespace);
$podcast->copyright &&
$channel->addChild('copyright', $podcast->copyright);
$podcast->is_blocked &&
$channel->addChild('block', 'Yes', $itunes_namespace);
$podcast->is_completed &&
$channel->addChild('complete', 'Yes', $itunes_namespace);
$image = $channel->addChild('image');
$image->addChild('url', $podcast->image->feed_url);
$image->addChild('title', $podcast->title);
$image->addChild('link', $podcast->link);
if (!empty($podcast->custom_rss)) {
array_to_rss(
[
'elements' => $podcast->custom_rss,
],
$channel,
);
}
foreach ($episodes as $episode) {
$item = $channel->addChild('item');
$item->addChild('title', $episode->title);
$enclosure = $item->addChild('enclosure');
$enclosure->addAttribute(
'url',
$episode->audio_file_analytics_url .
(empty($serviceSlug)
? ''
: '?_from=' . urlencode($serviceSlug)),
);
$enclosure->addAttribute('length', $episode->audio_file_size);
$enclosure->addAttribute('type', $episode->audio_file_mimetype);
$item->addChild('guid', $episode->guid);
$item->addChild(
'pubDate',
$episode->published_at->format(DATE_RFC1123),
);
if (!empty($episode->location_name)) {
$locationElement = $item->addChild(
'location',
htmlspecialchars($episode->location_name),
$podcast_namespace,
);
if (!empty($episode->location_geo)) {
$locationElement->addAttribute(
'geo',
$episode->location_geo,
);
}
if (!empty($episode->location_osmid)) {
$locationElement->addAttribute(
'osm',
$episode->location_osmid,
);
}
}
$item->addChildWithCDATA(
'description',
$episode->getDescriptionHtml($serviceSlug),
);
$item->addChild(
'duration',
$episode->audio_file_duration,
$itunes_namespace,
);
$item->addChild('link', $episode->link);
$episode_itunes_image = $item->addChild(
'image',
null,
$itunes_namespace,
);
$episode_itunes_image->addAttribute(
'href',
$episode->image->feed_url,
);
$episode->parental_advisory &&
$item->addChild(
'explicit',
$episode->parental_advisory === 'explicit'
? 'true'
: 'false',
$itunes_namespace,
);
$episode->number &&
$item->addChild('episode', $episode->number, $itunes_namespace);
$episode->season_number &&
$item->addChild(
'season',
$episode->season_number,
$itunes_namespace,
);
$item->addChild('episodeType', $episode->type, $itunes_namespace);
if ($episode->transcript_file_url) {
$transcriptElement = $item->addChild(
'transcript',
null,
$podcast_namespace,
);
$transcriptElement->addAttribute(
'url',
$episode->transcript_file_url,
);
$transcriptElement->addAttribute(
'type',
Mimes::guessTypeFromExtension(
pathinfo(
$episode->transcript_file_url,
PATHINFO_EXTENSION,
),
),
);
$transcriptElement->addAttribute(
'language',
$podcast->language_code,
);
}
if ($episode->chapters_file_url) {
$chaptersElement = $item->addChild(
'chapters',
null,
$podcast_namespace,
);
$chaptersElement->addAttribute(
'url',
$episode->chapters_file_url,
);
$chaptersElement->addAttribute(
'type',
'application/json+chapters',
);
}
foreach ($episode->soundbites as $soundbite) {
$soundbiteElement = $item->addChild(
'soundbite',
empty($soundbite->label) ? null : $soundbite->label,
$podcast_namespace,
);
$soundbiteElement->addAttribute(
'start_time',
$soundbite->start_time,
);
$soundbiteElement->addAttribute(
'duration',
$soundbite->duration,
);
}
foreach ($episode->persons as $episodePerson) {
$episodePersonElement = $item->addChild(
'person',
htmlspecialchars($episodePerson->person->full_name),
$podcast_namespace,
);
if (
!empty($episodePerson->person_role) &&
!empty($episodePerson->person_group)
) {
$episodePersonElement->addAttribute(
'role',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label",
[],
'en',
),
),
);
}
if (!empty($episodePerson->person_group)) {
$episodePersonElement->addAttribute(
'group',
htmlspecialchars(
lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label",
[],
'en',
),
),
);
}
$episodePersonElement->addAttribute(
'img',
$episodePerson->person->image->large_url,
);
if (!empty($episodePerson->person->information_url)) {
$episodePersonElement->addAttribute(
'href',
$episodePerson->person->information_url,
);
}
}
$episode->is_blocked &&
$item->addChild('block', 'Yes', $itunes_namespace);
if (!empty($episode->custom_rss)) {
array_to_rss(
[
'elements' => $episode->custom_rss,
],
$item,
);
}
}
return $rss->asXML();
}
$node->addChild('category', $category->apple_category);
}
/**
* Converts XML to array
*
* @param \SimpleRSSElement $xmlNode
*
* @return array
*/
function rss_to_array($xmlNode)
{
$nameSpaces = [
'',
'http://www.itunes.com/dtds/podcast-1.0.dtd',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
];
$arrayNode = [];
$arrayNode['name'] = $xmlNode->getName();
$arrayNode['namespace'] = $xmlNode->getNamespaces(false);
if (count($xmlNode->attributes()) > 0) {
if (!function_exists('add_category_tag')) {
/**
* Adds <itunes:category> and <category> tags to node for a given category
*/
function add_category_tag(SimpleXMLElement $node, Category $category): void
{
$itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
$itunes_category = $node->addChild('category', null, $itunes_namespace);
$itunes_category->addAttribute(
'text',
$category->parent !== null
? $category->parent->apple_category
: $category->apple_category,
);
if ($category->parent !== null) {
$itunes_category_child = $itunes_category->addChild(
'category',
null,
$itunes_namespace,
);
$itunes_category_child->addAttribute(
'text',
$category->apple_category,
);
$node->addChild('category', $category->parent->apple_category);
}
$node->addChild('category', $category->apple_category);
}
}
if (!function_exists('rss_to_array')) {
/**
* Converts XML to array
*
* FIXME: should be SimpleRSSElement
* @param SimpleXMLElement $xmlNode
*/
function rss_to_array(SimpleXMLElement $xmlNode): array
{
$nameSpaces = [
'',
'http://www.itunes.com/dtds/podcast-1.0.dtd',
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
];
$arrayNode = [];
$arrayNode['name'] = $xmlNode->getName();
$arrayNode['namespace'] = $xmlNode->getNamespaces(false);
foreach ($xmlNode->attributes() as $key => $value) {
$arrayNode['attributes'][$key] = (string) $value;
}
}
$textcontent = trim((string) $xmlNode);
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
$textcontent = trim((string) $xmlNode);
if (strlen($textcontent) > 0) {
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
}
}
return $arrayNode;
}
return $arrayNode;
}
/**
* Inserts array (converted to XML node) in XML node
*
* @param array $arrayNode
* @param \SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
*
*/
function array_to_rss($arrayNode, &$xmlNode)
{
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
array_key_exists('content', $childArrayNode)
? $childArrayNode['content']
: null,
empty($childArrayNode['namespace'])
? null
: current($childArrayNode['namespace']),
);
if (array_key_exists('attributes', $childArrayNode)) {
foreach (
$childArrayNode['attributes']
as $attributeKey => $attributeValue
) {
$childXmlNode->addAttribute($attributeKey, $attributeValue);
if (!function_exists('array_to_rss')) {
/**
* Inserts array (converted to XML node) in XML node
*
* @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
*/
function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode)
{
if (array_key_exists('elements', $arrayNode)) {
foreach ($arrayNode['elements'] as $childArrayNode) {
$childXmlNode = $xmlNode->addChild(
$childArrayNode['name'],
$childArrayNode['content'] ?? null,
empty($childArrayNode['namespace'])
? null
: current($childArrayNode['namespace']),
);
if (array_key_exists('attributes', $childArrayNode)) {
foreach (
$childArrayNode['attributes']
as $attributeKey => $attributeValue
) {
$childXmlNode->addAttribute(
$attributeKey,
$attributeValue,
);
}
}
array_to_rss($childArrayNode, $childXmlNode);
}
array_to_rss($childArrayNode, $childXmlNode);
}
return $xmlNode;
}
return $xmlNode;
}

View File

@ -6,43 +6,47 @@
* @link https://castopod.org/
*/
/**
* Returns the inline svg icon
*
* @param string $name name of the icon file without the .svg extension
* @param string $class to be added to the svg string
* @return string svg contents
*/
function icon(string $name, string $class = '')
{
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
if ($class !== '') {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
}
if (!function_exists('icon')) {
/**
* Returns the inline svg icon
*
* @param string $name name of the icon file without the .svg extension
* @param string $class to be added to the svg string
* @return string svg contents
*/
function icon(string $name, string $class = ''): string
{
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
if ($class !== '') {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
}
return $svg_contents;
return $svg_contents;
}
}
/**
* Returns the inline svg image
*
* @param string $name name of the image file without the .svg extension
* @param string $class to be added to the svg string
* @return string svg contents
*/
function svg($name, $class = null)
{
$svg_contents = file_get_contents('assets/images/' . $name . '.svg');
if ($class) {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
if (!function_exists('svg')) {
/**
* Returns the inline svg image
*
* @param string $name name of the image file without the .svg extension
* @param string $class to be added to the svg string
* @return string svg contents
*/
function svg(string $name, ?string $class = null): string
{
$svg_contents = file_get_contents('assets/images/' . $name . '.svg');
if ($class) {
$svg_contents = str_replace(
'<svg',
'<svg class="' . $class . '"',
$svg_contents,
);
}
return $svg_contents;
}
return $svg_contents;
}

View File

@ -1,5 +1,11 @@
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
if (!function_exists('host_url')) {
/**
* Return the host URL to use in views
@ -24,10 +30,8 @@ if (!function_exists('host_url')) {
if (!function_exists('current_season_url')) {
/**
* Return the podcast URL with season number to use in views
*
* @return string
*/
function current_season_url()
function current_season_url(): string
{
$season_query_string = '';
if (isset($_GET['season'])) {
@ -42,36 +46,27 @@ if (!function_exists('current_season_url')) {
if (!function_exists('location_url')) {
/**
* Returns URL to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
*/
function location_url($locationName, $locationGeo, $locationOsmid)
{
$uri = '';
function location_url(
string $locationName,
?string $locationGeo = null,
?string $locationOsmid = null
): string {
if (!empty($locationOsmid)) {
$uri =
'https://www.openstreetmap.org/' .
return 'https://www.openstreetmap.org/' .
['N' => 'node', 'W' => 'way', 'R' => 'relation'][
substr($locationOsmid, 0, 1)
] .
'/' .
substr($locationOsmid, 1);
} elseif (!empty($locationGeo)) {
$uri =
'https://www.openstreetmap.org/#map=17/' .
}
if (!empty($locationGeo)) {
return 'https://www.openstreetmap.org/#map=17/' .
str_replace(',', '/', substr($locationGeo, 4));
} elseif (!empty($locationName)) {
$uri =
'https://www.openstreetmap.org/search?query=' .
urlencode($locationName);
}
return $uri;
return 'https://www.openstreetmap.org/search?query=' .
urlencode($locationName);
}
}
//--------------------------------------------------------------------
@ -81,27 +76,29 @@ if (!function_exists('extract_params_from_episode_uri')) {
* Returns podcast name and episode slug from episode string uri
*
* @param URI $episodeUri
* @return string|null
*/
function extract_params_from_episode_uri($episodeUri)
function extract_params_from_episode_uri($episodeUri): ?array
{
preg_match(
'/@(?P<podcastName>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,191})/',
'~@(?P<podcastName>[a-zA-Z0-9\_]{1,32})\/episodes\/(?P<episodeSlug>[a-zA-Z0-9\-]{1,191})~',
$episodeUri->getPath(),
$matches,
);
if (
$matches &&
array_key_exists('podcastName', $matches) &&
array_key_exists('episodeSlug', $matches)
) {
return [
'podcastName' => $matches['podcastName'],
'episodeSlug' => $matches['episodeSlug'],
];
if ($matches === []) {
return null;
}
return null;
if (
!array_key_exists('podcastName', $matches) ||
!array_key_exists('episodeSlug', $matches)
) {
return null;
}
return [
'podcastName' => $matches['podcastName'],
'episodeSlug' => $matches['episodeSlug'],
];
}
}

View File

@ -17,17 +17,17 @@ return [
'submit' => 'Proceed to follow',
],
'favourite' => [
'title' => 'Favourite {actorDisplayName}\'s note',
'title' => "Favourite {actorDisplayName}'s note",
'subtitle' => 'You are going to favourite:',
'submit' => 'Proceed to favourite',
],
'reblog' => [
'title' => 'Share {actorDisplayName}\'s note',
'title' => "Share {actorDisplayName}'s note",
'subtitle' => 'You are going to share:',
'submit' => 'Proceed to share',
],
'reply' => [
'title' => 'Reply to {actorDisplayName}\'s note',
'title' => "Reply to {actorDisplayName}'s note",
'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply',
],

View File

@ -8,7 +8,7 @@
return [
'podcast_contributors' => 'Podcast contributors',
'view' => '{username}\'s contribution to {podcastName}',
'view' => "{username}'s contribution to {podcastName}",
'add' => 'Add contributor',
'add_contributor' => 'Add a contributor for {0}',
'edit_role' => 'Update role for {0}',
@ -28,10 +28,10 @@ return [
'podcast_admin' => 'Podcast admin',
],
'messages' => [
'removeOwnerContributorError' => 'You can\'t remove the podcast owner!',
'removeOwnerContributorError' => "You can't remove the podcast owner!",
'removeContributorSuccess' =>
'You have successfully removed {username} from {podcastTitle}',
'alreadyAddedError' =>
'The contributor you\'re trying to add has already been added!',
"The contributor you're trying to add has already been added!",
],
];

View File

@ -51,7 +51,7 @@ return [
'CF' => 'Central African Republic',
'CG' => 'Congo',
'CH' => 'Switzerland',
'CI' => 'Côte d\'Ivoire',
'CI' => "Côte d'Ivoire",
'CK' => 'Cook Islands',
'CL' => 'Chile',
'CM' => 'Cameroon',
@ -128,12 +128,12 @@ return [
'KI' => 'Kiribati',
'KM' => 'Comoros',
'KN' => 'Saint Kitts and Nevis',
'KP' => 'Korea, Democratic People\'s Republic of',
'KP' => "Korea, Democratic People's Republic of",
'KR' => 'Korea, Republic of',
'KW' => 'Kuwait',
'KY' => 'Cayman Islands',
'KZ' => 'Kazakhstan',
'LA' => 'Lao People\'s Democratic Republic',
'LA' => "Lao People's Democratic Republic",
'LB' => 'Lebanon',
'LC' => 'Saint Lucia',
'LI' => 'Liechtenstein',

View File

@ -127,7 +127,7 @@ return [
],
'unpublish_form' => [
'disclaimer' =>
'Unpublishing the episode will delete all the notes associated with the episode and remove it from the podcast\'s RSS feed.',
"Unpublishing the episode will delete all the notes associated with the episode and remove it from the podcast's RSS feed.",
'understand' => 'I understand, I want to unpublish the episode',
'submit' => 'Unpublish',
],

View File

@ -31,7 +31,7 @@ return [
'db_password' => 'Database password',
'db_prefix' => 'Database prefix',
'db_prefix_hint' =>
'The prefix of the Castopod table names, leave as is if you don\'t know what it means.',
"The prefix of the Castopod table names, leave as is if you don't know what it means.",
'cache_config' => 'Cache configuration',
'cache_config_hint' =>
'Choose your preferred cache handler. Leave it as the default value if you have no clue what it means.',
@ -54,6 +54,6 @@ return [
'databaseConnectError' =>
'Castopod could not connect to your database. Edit your database configuration and try again.',
'writeError' =>
'Couldn\'t create/write the `.env` file. You must create it manually by following the `.env.example` file template in the Castopod package.',
"Couldn't create/write the `.env` file. You must create it manually by following the `.env.example` file template in the Castopod package.",
],
];

View File

@ -10,8 +10,7 @@ return [
'info' => 'My account info',
'changePassword' => 'Change my password',
'messages' => [
'wrongPasswordError' =>
'You\'ve entered the wrong password, try again.',
'wrongPasswordError' => "You've entered the wrong password, try again.",
'passwordChangeSuccess' => 'Password has been successfully changed!',
],
];

Some files were not shown because too many files have changed in this diff Show More