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: // 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 // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/docker-existing-dockerfile
{ {
"name": "Existing Dockerfile", "name": "Castopod Host dev",
"dockerFile": "./Dockerfile", "dockerComposeFile": ["../docker-compose.yml"],
"service": "app",
"workspaceFolder": "/castopod-host",
"postCreateCommand": "cron && php spark serve --host 0.0.0.0",
"settings": { "settings": {
"terminal.integrated.shell.linux": "/bin/bash", "terminal.integrated.shell.linux": "/bin/bash",
"editor.formatOnSave": true, "editor.formatOnSave": true,
@ -13,18 +16,18 @@
"color-highlight.markerType": "dot-before" "color-highlight.markerType": "dot-before"
}, },
"extensions": [ "extensions": [
"mikestead.dotenv", "mikestead.dotenv",
"bmewburn.vscode-intelephense-client", "bmewburn.vscode-intelephense-client",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"naumovs.color-highlight", "naumovs.color-highlight",
"heybourn.headwind", "heybourn.headwind",
"wayou.vscode-todo-highlight", "wayou.vscode-todo-highlight",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"jamesbirtles.svelte-vscode", "jamesbirtles.svelte-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint", "stylelint.vscode-stylelint",
"wongjn.php-sniffer", "wongjn.php-sniffer",
"eamodio.gitlens" "eamodio.gitlens"
] ]
} }

View File

@ -1,6 +1,7 @@
image: php:7.3-fpm image: php:7.3-fpm
stages: stages:
- quality
- bundle - bundle
- release - release
@ -31,16 +32,30 @@ before_script:
- curl -sL https://deb.nodesource.com/setup_12.x | bash - - curl -sL https://deb.nodesource.com/setup_12.x | bash -
- apt-get update && apt-get install -y nodejs - apt-get update && apt-get install -y nodejs
# Install php and js dependencies # Install all php and js dependencies
- php composer.phar install --no-dev --ignore-platform-reqs - php composer.phar install --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs
- npm install - npm install
# build all UI assets tests:
- npm run build stage: quality
script:
- vendor/bin/phpunit
code-review:
stage: quality
script:
# run rector
- vendor/bin/rector process --dry-run
bundle_app: bundle_app:
stage: bundle stage: bundle
script: 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 # 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/ - 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 jq -y
- apt-get install zip -y - apt-get install zip -y
# make prepare-release.sh executable # make scripts/prepare-release.sh executable
- chmod +x ./prepare-release.sh - chmod +x ./scripts/prepare-release.sh
# IMPORTANT: delete local git tags before release to prevent eventual script failure (ie. tag already exists) # IMPORTANT: delete local git tags before release to prevent eventual script failure (ie. tag already exists)
- git tag | xargs git tag -d - git tag | xargs git tag -d

View File

@ -11,7 +11,7 @@
[ [
"@semantic-release/exec", "@semantic-release/exec",
{ {
"prepareCmd": "./prepare-release.sh ${nextRelease.version}" "prepareCmd": "./scripts/prepare-release.sh ${nextRelease.version}"
} }
], ],
"@semantic-release/npm", "@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 FROM php:7.3-fpm
LABEL maintainer="Yassine Doghri<yassine@podlibre.org>"
COPY . /castopod-host COPY . /castopod-host
WORKDIR /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 ### Install CodeIgniter's server requirements
#-- https://github.com/codeigniter4/appstarter#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 class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
{ {
//--------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------
/** /**
* Checks a group to see if they have the specified permission. * Checks a group to see if they have the specified permission.
* *
* @param int|string $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 // Get the Permission ID
$permissionId = $this->getPermissionID($permission); $permissionId = $this->getPermissionID($permission);
@ -36,36 +18,23 @@ class FlatAuthorization extends \Myth\Auth\Authorization\FlatAuthorization
return false; return false;
} }
if ( return (bool) $this->permissionModel->doesGroupHavePermission(
$this->permissionModel->doesGroupHavePermission( $groupId,
$groupId, $permissionId,
(int) $permissionId );
)
) {
return true;
}
return false;
} }
/** /**
* Makes user part of given groups. * Makes user part of given groups.
* *
* @param $userId * @param array $groups Either collection of ID or names
* @param array|null $groups // Either collection of ID or names
*
* @return bool
*/ */
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 // remove user from all groups before resetting it in new groups
$this->groupModel->removeUserFromAllGroups($userId); $this->groupModel->removeUserFromAllGroups($userId);
if (empty($groups)) { if ($groups = []) {
return true; return true;
} }

View File

@ -4,14 +4,20 @@ namespace App\Authorization;
class GroupModel extends \Myth\Auth\Authorization\GroupModel class GroupModel extends \Myth\Auth\Authorization\GroupModel
{ {
public function getContributorRoles() /**
* @return mixed[]
*/
public function getContributorRoles(): array
{ {
return $this->select('auth_groups.*') return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after') ->like('name', 'podcast_', 'after')
->findAll(); ->findAll();
} }
public function getUserRoles() /**
* @return mixed[]
*/
public function getUserRoles(): array
{ {
return $this->select('auth_groups.*') return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after') ->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, * Checks to see if a user, or one of their groups,
* has a specific permission. * has a specific permission.
*
* @param $userId
* @param $permissionId
*
* @return bool
*/ */
public function doesGroupHavePermission( public function doesGroupHavePermission(
int $groupId, int $groupId,
@ -33,9 +28,7 @@ class PermissionModel extends \Myth\Auth\Authorization\PermissionModel
* id => name * id => name
* ] * ]
* *
* @param int $groupId * @return array<int, string>
*
* @return array
*/ */
public function getPermissionsForGroup(int $groupId): array public function getPermissionsForGroup(int $groupId): array
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -62,6 +62,7 @@ class Database extends Config
'username' => '', 'username' => '',
'password' => '', 'password' => '',
'database' => ':memory:', 'database' => ':memory:',
/** @noRector StringClassNameToClassConstantRector */
'DBDriver' => 'SQLite3', 'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS 'DBPrefix' => 'db_', // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS
'pConnect' => false, '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'); helper('auth');
// set interact_as_actor_id value // 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'); helper('auth');
// remove user's interact_as_actor session // remove user's interact_as_actor session
@ -75,7 +75,7 @@ Events::on('logout', function ($user) {
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* Update episode metadata counts * Update episode metadata counts
*/ */
Events::on('on_note_add', function ($note) { Events::on('on_note_add', function ($note): void {
if ($note->episode_id) { if ($note->episode_id) {
model('EpisodeModel') model('EpisodeModel')
->where('id', $note->episode_id) ->where('id', $note->episode_id)
@ -87,7 +87,7 @@ Events::on('on_note_add', function ($note) {
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*"); 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) { if ($note->episode_id) {
model('EpisodeModel') model('EpisodeModel')
->where('id', $note->episode_id) ->where('id', $note->episode_id)
@ -106,7 +106,7 @@ Events::on('on_note_remove', function ($note) {
cache()->deleteMatching("page_note#{$note->id}*"); 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) { if ($episodeId = $note->episode_id) {
model('EpisodeModel') model('EpisodeModel')
->where('id', $episodeId) ->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; $note = $reblogNote->reblog_of_note;
if ($episodeId = $note->episode_id) { if ($episodeId = $note->episode_id) {
model('EpisodeModel') 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; $note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*"); cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->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; $note = $reply->reply_to_note;
cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*"); cache()->deleteMatching("page_podcast#{$note->actor->podcast->id}*");
cache()->deleteMatching("page_note#{$note->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) { if ($note->episode_id) {
model('EpisodeModel') model('EpisodeModel')
->where('id', $note->episode_id) ->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) { if ($note->episode_id) {
model('EpisodeModel') model('EpisodeModel')
->where('id', $note->episode_id) ->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_podcast*');
cache()->deleteMatching('page_note*'); 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_podcast*');
cache()->deleteMatching('page_note*'); 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_podcast*');
cache()->deleteMatching('page_note*'); 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_podcast*');
cache()->deleteMatching('page_note*'); cache()->deleteMatching('page_note*');
}); });

View File

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

View File

@ -2,6 +2,8 @@
namespace Config; namespace Config;
use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\FormatterInterface; use CodeIgniter\Format\FormatterInterface;
@ -40,9 +42,9 @@ class Format extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public $formatters = [ public $formatters = [
'application/json' => 'CodeIgniter\Format\JSONFormatter', 'application/json' => JSONFormatter::class,
'application/xml' => 'CodeIgniter\Format\XMLFormatter', 'application/xml' => XMLFormatter::class,
'text/xml' => 'CodeIgniter\Format\XMLFormatter', '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. * 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. * @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); 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; public $maxDepth = 6;
/**
* @var bool
*/
public $displayCalledFrom = true; public $displayCalledFrom = true;
/**
* @var bool
*/
public $expanded = false; public $expanded = false;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| RichRenderer Settings | RichRenderer Settings
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
/**
* @var string
*/
public $richTheme = 'aante-light.css'; public $richTheme = 'aante-light.css';
/**
* @var bool
*/
public $richFolder = false; public $richFolder = false;
/**
* @var int
*/
public $richSort = Renderer::SORT_FULL; public $richSort = Renderer::SORT_FULL;
public $richObjectPlugins = null; public $richObjectPlugins;
public $richTabPlugins = null; public $richTabPlugins;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| CLI Settings | CLI Settings
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
*/ */
/**
* @var bool
*/
public $cliColors = true; public $cliColors = true;
/**
* @var bool
*/
public $cliForceUTF8 = false; public $cliForceUTF8 = false;
/**
* @var bool
*/
public $cliDetectWidth = true; public $cliDetectWidth = true;
/**
* @var int
*/
public $cliMinWidth = 40; public $cliMinWidth = 40;
} }

View File

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

View File

@ -22,7 +22,7 @@ class Mimes
/** /**
* Map of extensions to mime types. * Map of extensions to mime types.
* *
* @var array * @var array<string, string>
*/ */
public static $mimes = [ public static $mimes = [
'hqx' => [ 'hqx' => [
@ -321,11 +321,9 @@ class Mimes
/** /**
* Attempts to determine the best mime type for the given file extension. * 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. * @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), '. '); $extension = trim(strtolower($extension), '. ');
@ -341,15 +339,13 @@ class Mimes
/** /**
* Attempts to determine the best file extension for a given mime type. * 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) * @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. * @return string|null The extension determined, or null if unable to match.
*/ */
public static function guessExtensionFromType( public static function guessExtensionFromType(
string $type, string $type,
string $proposedExtension = null string $proposedExtension = null
) { ): ?string {
$type = trim(strtolower($type), '. '); $type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension)); $proposedExtension = trim(strtolower($proposedExtension));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,9 @@
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Entities\Episode;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\EpisodePersonModel; use App\Models\EpisodePersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
@ -16,42 +19,39 @@ use App\Models\PersonModel;
class EpisodePerson extends BaseController class EpisodePerson extends BaseController
{ {
/** /**
* @var \App\Entities\Podcast * @var Podcast
*/ */
protected $podcast; protected $podcast;
/** /**
* @var \App\Entities\Episode * @var Episode
*/ */
protected $episode; protected $episode;
public function _remap($method, ...$params) public function _remap($method, ...$params)
{ {
if (count($params) > 1) { if (count($params) <= 2) {
if ( throw PageNotFoundException::forPageNotFound();
!($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();
} }
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() public function index()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@
namespace App\Controllers\Admin; namespace App\Controllers\Admin;
use App\Entities\Podcast;
use CodeIgniter\Exceptions\PageNotFoundException;
use App\Models\PlatformModel; use App\Models\PlatformModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use Config\Services; use Config\Services;
@ -15,20 +17,22 @@ use Config\Services;
class PodcastPlatform extends BaseController class PodcastPlatform extends BaseController
{ {
/** /**
* @var \App\Entities\Podcast|null * @var Podcast|null
*/ */
protected $podcast; protected $podcast;
public function _remap($method, ...$params) public function _remap($method, ...$params)
{ {
if ( if (count($params) === 0) {
!($this->podcast = (new PodcastModel())->getPodcastById($params[0])) return $this->$method();
) {
throw \CodeIgniter\Exceptions\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() public function index()
@ -45,7 +49,7 @@ class PodcastPlatform extends BaseController
'platformType' => $platformType, 'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks( 'platforms' => (new PlatformModel())->getPlatformsWithLinks(
$this->podcast->id, $this->podcast->id,
$platformType $platformType,
), ),
]; ];
@ -65,36 +69,35 @@ class PodcastPlatform extends BaseController
as $platformSlug => $podcastPlatform as $platformSlug => $podcastPlatform
) { ) {
$podcastPlatformUrl = $podcastPlatform['url']; $podcastPlatformUrl = $podcastPlatform['url'];
if (empty($podcastPlatformUrl)) {
if ( continue;
!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 (!$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( $platformModel->savePodcastPlatforms(
$this->podcast->id, $this->podcast->id,
$platformType, $platformType,
$podcastsPlatformsData $podcastsPlatformsData,
); );
return redirect() return redirect()
@ -106,7 +109,7 @@ class PodcastPlatform extends BaseController
{ {
(new PlatformModel())->removePodcastPlatform( (new PlatformModel())->removePodcastPlatform(
$this->podcast->id, $this->podcast->id,
$platformSlug $platformSlug,
); );
return redirect() return redirect()

View File

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

View File

@ -8,9 +8,11 @@
namespace App\Controllers; namespace App\Controllers;
use Myth\Auth\Controllers\AuthController;
use App\Entities\User; 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 * 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, * Verifies the code with the email and saves the new password,
* if they all pass validation. * if they all pass validation.
*
* @return mixed
*/ */
public function attemptReset() public function attemptReset(): RedirectResponse
{ {
if ($this->config->activeResetter === false) { if ($this->config->activeResetter === false) {
return redirect() return redirect()

View File

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

View File

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

View File

@ -8,32 +8,34 @@
namespace App\Controllers; namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Exceptions\PageNotFoundException;
use Opawg\UserAgentsPhp\UserAgentsRSS;
use Exception;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Controller; use CodeIgniter\Controller;
class Feed extends Controller class Feed extends Controller
{ {
public function index($podcastName) public function index($podcastName): ResponseInterface
{ {
helper('rss'); helper('rss');
$podcast = (new PodcastModel())->where('name', $podcastName)->first(); $podcast = (new PodcastModel())->where('name', $podcastName)->first();
if (!$podcast) { if (!$podcast) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
$serviceSlug = ''; $serviceSlug = '';
try { try {
$service = \Opawg\UserAgentsPhp\UserAgentsRSS::find( $service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']);
$_SERVER['HTTP_USER_AGENT'],
);
if ($service) { if ($service) {
$serviceSlug = $service['slug']; $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 // 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 = $cacheName =

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,8 @@
namespace App\Controllers; namespace App\Controllers;
use CodeIgniter\HTTP\ResponseInterface;
use App\Models\PlatformModel;
use CodeIgniter\Controller; use CodeIgniter\Controller;
/* /*
@ -15,9 +17,9 @@ use CodeIgniter\Controller;
*/ */
class Platform extends 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()); return $this->response->setJSON($model->getPlatforms());
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,30 +14,34 @@ use CodeIgniter\Database\Migration;
class AddCreditView extends Migration class AddCreditView extends Migration
{ {
public function up() public function up(): void
{ {
// Creates View for credit UNION query // Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits'); $viewName = $this->db->prefixTable('credits');
$personTable = $this->db->prefixTable('persons'); $personsTable = $this->db->prefixTable('persons');
$podcastPersonTable = $this->db->prefixTable('podcasts_persons'); $podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonTable = $this->db->prefixTable('episodes_persons'); $episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<EOD $createQuery = <<<EOD
CREATE VIEW `$viewName` AS CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `$podcastPersonTable` SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `$personTable` INNER JOIN `{$personsTable}`
ON (`person_id`=`$personTable`.`id`) ON (`person_id`=`{$personsTable}`.`id`)
UNION UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, `episode_id` FROM `$episodePersonTable` SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `$personTable` INNER JOIN `{$personsTable}`
ON (`person_id`=`$personTable`.`id`) 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`; ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
EOD; EOD;
$this->db->query($createQuery); $this->db->query($createQuery);
} }
public function down() public function down(): void
{ {
$viewName = $this->db->prefixTable('credits'); $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 class AddEpisodeIdToNotes extends Migration
{ {
public function up() public function up(): void
{ {
$prefix = $this->db->getPrefix(); $prefix = $this->db->getPrefix();
$createQuery = <<<SQL $createQuery = <<<SQL
ALTER TABLE ${prefix}activitypub_notes ALTER TABLE {$prefix}activitypub_notes
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`, 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; SQL;
$this->db->query($createQuery); $this->db->query($createQuery);
} }
public function down() public function down(): void
{ {
$this->forge->dropForeignKey( $this->forge->dropForeignKey(
'activitypub_notes', 'activitypub_notes',

View File

@ -15,19 +15,19 @@ use CodeIgniter\Database\Migration;
class AddCreatedByToNotes extends Migration class AddCreatedByToNotes extends Migration
{ {
public function up() public function up(): void
{ {
$prefix = $this->db->getPrefix(); $prefix = $this->db->getPrefix();
$createQuery = <<<SQL $createQuery = <<<SQL
ALTER TABLE ${prefix}activitypub_notes ALTER TABLE {$prefix}activitypub_notes
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`, 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; SQL;
$this->db->query($createQuery); $this->db->query($createQuery);
} }
public function down() public function down(): void
{ {
$this->forge->dropForeignKey( $this->forge->dropForeignKey(
'activitypub_notes', 'activitypub_notes',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,26 +8,26 @@
namespace App\Entities; namespace App\Entities;
use RuntimeException;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Entity;
class Credit extends Entity class Credit extends Entity
{ {
/** /**
* @var \App\Entities\Person * @var Person
*/ */
protected $person; protected $person;
/** /**
* @var \App\Entities\Podcast * @var Podcast
*/ */
protected $podcast; protected $podcast;
/** /**
* @var \App\Entities\Episode|null * @var Episode|null
*/ */
protected $episode; protected $episode;
@ -41,35 +41,47 @@ class Credit extends Entity
*/ */
protected $role_label; 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( return (new PodcastModel())->getPodcastById(
$this->attributes['podcast_id'], $this->attributes['podcast_id'],
); );
} }
public function getEpisode() public function getEpisode(): ?Episode
{ {
if (empty($this->episode_id)) { if (empty($this->episode_id)) {
throw new \RuntimeException( throw new RuntimeException(
'Credit must have episode_id before getting episode.', 'Credit must have episode_id before getting episode.',
); );
} }
if (empty($this->episode)) { if (empty($this->episode)) {
$this->episode = (new EpisodeModel())->getPublishedEpisodeById( $this->episode = (new EpisodeModel())->getPublishedEpisodeById(
$this->episode_id,
$this->podcast_id, $this->podcast_id,
$this->episode_id,
); );
} }
return $this->episode; return $this->episode;
} }
public function getPerson() public function getPerson(): Person
{ {
if (empty($this->person_id)) { if (empty($this->person_id)) {
throw new \RuntimeException( throw new RuntimeException(
'Credit must have person_id before getting person.', 'Credit must have person_id before getting person.',
); );
} }
@ -83,23 +95,27 @@ class Credit extends Entity
return $this->person; return $this->person;
} }
public function getGroupLabel() public function getGroupLabel(): ?string
{ {
if (empty($this->person_group)) { if (empty($this->person_group)) {
return null; 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; 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; namespace App\Entities;
use App\Libraries\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\SoundbiteModel; use App\Models\SoundbiteModel;
use App\Models\EpisodePersonModel; use App\Models\EpisodePersonModel;
use App\Models\NoteModel; use App\Models\NoteModel;
use CodeIgniter\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\Exceptions\FileNotFoundException; use CodeIgniter\Files\Exceptions\FileNotFoundException;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Exceptions\HTTPException; use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use RuntimeException;
class Episode extends Entity class Episode extends Entity
{ {
/** /**
* @var \App\Entities\Podcast * @var Podcast
*/ */
protected $podcast; protected $podcast;
@ -31,22 +36,22 @@ class Episode extends Entity
protected $link; protected $link;
/** /**
* @var \App\Libraries\Image * @var Image
*/ */
protected $image; protected $image;
/** /**
* @var \CodeIgniter\Files\File * @var File
*/ */
protected $audioFile; protected $audioFile;
/** /**
* @var \CodeIgniter\Files\File * @var File
*/ */
protected $transcript_file; protected $transcript_file;
/** /**
* @var \CodeIgniter\Files\File * @var File
*/ */
protected $chapters_file; protected $chapters_file;
@ -71,17 +76,17 @@ class Episode extends Entity
protected $audio_file_opengraph_url; protected $audio_file_opengraph_url;
/** /**
* @var \App\Entities\EpisodePerson[] * @var EpisodePerson[]
*/ */
protected $persons; protected $persons;
/** /**
* @var \App\Entities\Soundbite[] * @var Soundbite[]
*/ */
protected $soundbites; protected $soundbites;
/** /**
* @var \App\Entities\Note[] * @var Note[]
*/ */
protected $notes; protected $notes;
@ -156,15 +161,13 @@ class Episode extends Entity
/** /**
* Saves an episode image * Saves an episode image
* *
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $image * @param UploadedFile|File $image
*
*/ */
public function setImage($image) public function setImage($image)
{ {
if ( if (
!empty($image) && !empty($image) &&
(!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) || (!($image instanceof UploadedFile) || $image->isValid())
$image->isValid())
) { ) {
helper('media'); helper('media');
@ -175,7 +178,7 @@ class Episode extends Entity
'podcasts/' . $this->getPodcast()->name, 'podcasts/' . $this->getPodcast()->name,
$this->attributes['slug'], $this->attributes['slug'],
); );
$this->image = new \App\Libraries\Image( $this->image = new Image(
$this->attributes['image_path'], $this->attributes['image_path'],
$this->attributes['image_mimetype'], $this->attributes['image_mimetype'],
); );
@ -185,13 +188,10 @@ class Episode extends Entity
return $this; return $this;
} }
public function getImage(): \App\Libraries\Image public function getImage(): Image
{ {
if ($imagePath = $this->attributes['image_path']) { if ($imagePath = $this->attributes['image_path']) {
return new \App\Libraries\Image( return new Image($imagePath, $this->attributes['image_mimetype']);
$imagePath,
$this->attributes['image_mimetype'],
);
} }
return $this->getPodcast()->image; return $this->getPodcast()->image;
} }
@ -199,15 +199,14 @@ class Episode extends Entity
/** /**
* Saves an audio file * Saves an audio file
* *
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $audioFile * @param UploadedFile|File $audioFile
* *
*/ */
public function setAudioFile($audioFile = null) public function setAudioFile($audioFile = null)
{ {
if ( if (
!empty($audioFile) && !empty($audioFile) &&
(!($audioFile instanceof \CodeIgniter\HTTP\Files\UploadedFile) || (!($audioFile instanceof UploadedFile) || $audioFile->isValid())
$audioFile->isValid())
) { ) {
helper(['media', 'id3']); helper(['media', 'id3']);
@ -234,16 +233,14 @@ class Episode extends Entity
/** /**
* Saves an episode transcript file * Saves an episode transcript file
* *
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $transcriptFile * @param UploadedFile|File $transcriptFile
* *
*/ */
public function setTranscriptFile($transcriptFile) public function setTranscriptFile($transcriptFile)
{ {
if ( if (
!empty($transcriptFile) && !empty($transcriptFile) &&
(!( (!($transcriptFile instanceof UploadedFile) ||
$transcriptFile instanceof \CodeIgniter\HTTP\Files\UploadedFile
) ||
$transcriptFile->isValid()) $transcriptFile->isValid())
) { ) {
helper('media'); helper('media');
@ -261,14 +258,14 @@ class Episode extends Entity
/** /**
* Saves an episode chapters file * Saves an episode chapters file
* *
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $chaptersFile * @param UploadedFile|File $chaptersFile
* *
*/ */
public function setChaptersFile($chaptersFile) public function setChaptersFile($chaptersFile)
{ {
if ( if (
!empty($chaptersFile) && !empty($chaptersFile) &&
(!($chaptersFile instanceof \CodeIgniter\HTTP\Files\UploadedFile) || (!($chaptersFile instanceof UploadedFile) ||
$chaptersFile->isValid()) $chaptersFile->isValid())
) { ) {
helper('media'); helper('media');
@ -287,7 +284,7 @@ class Episode extends Entity
{ {
helper('media'); 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() public function getTranscriptFile()
@ -295,7 +292,7 @@ class Episode extends Entity
if ($this->attributes['transcript_file_path']) { if ($this->attributes['transcript_file_path']) {
helper('media'); helper('media');
return new \CodeIgniter\Files\File( return new File(
media_path($this->attributes['transcript_file_path']), media_path($this->attributes['transcript_file_path']),
); );
} }
@ -308,7 +305,7 @@ class Episode extends Entity
if ($this->attributes['chapters_file_path']) { if ($this->attributes['chapters_file_path']) {
helper('media'); helper('media');
return new \CodeIgniter\Files\File( return new File(
media_path($this->attributes['chapters_file_path']), media_path($this->attributes['chapters_file_path']),
); );
} }
@ -320,7 +317,7 @@ class Episode extends Entity
{ {
helper('media'); helper('media');
return media_url($this->audio_file_path); return media_base_url($this->audio_file_path);
} }
public function getAudioFileAnalyticsUrl() public function getAudioFileAnalyticsUrl()
@ -359,7 +356,7 @@ class Episode extends Entity
public function getTranscriptFileUrl() public function getTranscriptFileUrl()
{ {
if ($this->attributes['transcript_file_path']) { if ($this->attributes['transcript_file_path']) {
return media_url($this->attributes['transcript_file_path']); return media_base_url($this->attributes['transcript_file_path']);
} else { } else {
return $this->attributes['transcript_file_remote_url']; 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 * Gets chapters file url from chapters file uri if it exists
* or returns the chapters_file_remote_url which can be null. * 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']) { if ($this->chapters_file_path) {
return media_url($this->attributes['chapters_file_path']); return media_base_url($this->chapters_file_path);
} else {
return $this->attributes['chapters_file_remote_url'];
} }
return $this->chapters_file_remote_url;
} }
/** /**
@ -389,7 +383,7 @@ class Episode extends Entity
public function getPersons() public function getPersons()
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Episode must be created before getting persons.', 'Episode must be created before getting persons.',
); );
} }
@ -412,7 +406,7 @@ class Episode extends Entity
public function getSoundbites() public function getSoundbites()
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Episode must be created before getting soundbites.', 'Episode must be created before getting soundbites.',
); );
} }
@ -430,7 +424,7 @@ class Episode extends Entity
public function getNotes() public function getNotes()
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Episode must be created before getting soundbites.', 'Episode must be created before getting soundbites.',
); );
} }
@ -586,23 +580,24 @@ class Episode extends Entity
*/ */
function getCustomRssString() function getCustomRssString()
{ {
helper('rss'); if (empty($this->custom_rss)) {
if (empty($this->attributes['custom_rss'])) {
return ''; 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) function setCustomRssString($customRssString)
{ {
if (empty($customRssString)) {
return $this;
}
helper('rss'); helper('rss');
$customRssArray = rss_to_array( $customRssArray = rss_to_array(
simplexml_load_string( simplexml_load_string(
@ -621,6 +620,7 @@ class Episode extends Entity
'</item></channel></rss>', '</item></channel></rss>',
), ),
)['elements'][0]['elements'][0]; )['elements'][0]['elements'][0];
if (array_key_exists('elements', $customRssArray)) { if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode( $this->attributes['custom_rss'] = json_encode(
$customRssArray['elements'], $customRssArray['elements'],
@ -628,6 +628,8 @@ class Episode extends Entity
} else { } else {
$this->attributes['custom_rss'] = null; $this->attributes['custom_rss'] = null;
} }
return $this;
} }
function getPartnerLink($serviceSlug = null) function getPartnerLink($serviceSlug = null)

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
namespace App\Entities; namespace App\Entities;
use CodeIgniter\Entity; use CodeIgniter\Entity\Entity;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
class Page extends Entity class Page extends Entity
@ -23,6 +23,9 @@ class Page extends Entity
*/ */
protected $content_html; protected $content_html;
/**
* @var array<string, string>
*/
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'title' => 'string', 'title' => 'string',
@ -32,10 +35,10 @@ class Page extends Entity
public function getLink() 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([ $converter = new CommonMarkConverter([
'html_input' => 'strip', 'html_input' => 'strip',

View File

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

View File

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

View File

@ -8,14 +8,18 @@
namespace App\Entities; namespace App\Entities;
use ActivityPub\Models\ActorModel; use App\Libraries\Image;
use App\Libraries\SimpleRSSElement;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PlatformModel; use App\Models\PlatformModel;
use App\Models\PodcastPersonModel; use App\Models\PodcastPersonModel;
use CodeIgniter\Entity; use CodeIgniter\Entity\Entity;
use App\Models\UserModel; use App\Models\UserModel;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use League\CommonMark\CommonMarkConverter; use League\CommonMark\CommonMarkConverter;
use RuntimeException;
class Podcast extends Entity class Podcast extends Entity
{ {
@ -25,32 +29,32 @@ class Podcast extends Entity
protected $link; protected $link;
/** /**
* @var \ActivityPub\Entities\Actor * @var Actor
*/ */
protected $actor; protected $actor;
/** /**
* @var \App\Libraries\Image * @var Image
*/ */
protected $image; protected $image;
/** /**
* @var \App\Entities\Episode[] * @var Episode[]
*/ */
protected $episodes; protected $episodes;
/** /**
* @var \App\Entities\PodcastPerson[] * @var PodcastPerson[]
*/ */
protected $persons; protected $persons;
/** /**
* @var \App\Entities\Category * @var Category
*/ */
protected $category; protected $category;
/** /**
* @var \App\Entities\Category[] * @var Category[]
*/ */
protected $other_categories; protected $other_categories;
@ -60,22 +64,22 @@ class Podcast extends Entity
protected $other_categories_ids; protected $other_categories_ids;
/** /**
* @var \App\Entities\User[] * @var User[]
*/ */
protected $contributors; protected $contributors;
/** /**
* @var \App\Entities\Platform * @var Platform
*/ */
protected $podcastingPlatforms; protected $podcastingPlatforms;
/** /**
* @var \App\Entities\Platform * @var Platform
*/ */
protected $socialPlatforms; protected $socialPlatforms;
/** /**
* @var \App\Entities\Platform * @var Platform
*/ */
protected $fundingPlatforms; protected $fundingPlatforms;
@ -132,12 +136,12 @@ class Podcast extends Entity
/** /**
* Returns the podcast actor * Returns the podcast actor
* *
* @return \App\Entities\Actor * @return Actor
*/ */
public function getActor() public function getActor(): Actor
{ {
if (!$this->attributes['actor_id']) { if (!$this->attributes['actor_id']) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must have an actor_id before getting actor.', '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/` * 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) { if ($image) {
helper('media'); helper('media');
@ -167,7 +170,7 @@ class Podcast extends Entity
'cover', 'cover',
); );
$this->image = new \App\Libraries\Image( $this->image = new Image(
$this->attributes['image_path'], $this->attributes['image_path'],
$this->attributes['image_mimetype'], $this->attributes['image_mimetype'],
); );
@ -177,20 +180,20 @@ class Podcast extends Entity
return $this; return $this;
} }
public function getImage() public function getImage(): Image
{ {
return new \App\Libraries\Image( return new Image(
$this->attributes['image_path'], $this->attributes['image_path'],
$this->attributes['image_mimetype'], $this->attributes['image_mimetype'],
); );
} }
public function getLink() public function getLink(): string
{ {
return url_to('podcast-activity', $this->attributes['name']); return url_to('podcast-activity', $this->attributes['name']);
} }
public function getFeedUrl() public function getFeedUrl(): string
{ {
return url_to('podcast_feed', $this->attributes['name']); return url_to('podcast_feed', $this->attributes['name']);
} }
@ -198,12 +201,12 @@ class Podcast extends Entity
/** /**
* Returns the podcast's episodes * Returns the podcast's episodes
* *
* @return \App\Entities\Episode[] * @return Episode[]
*/ */
public function getEpisodes() public function getEpisodes(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting episodes.', 'Podcast must be created before getting episodes.',
); );
} }
@ -221,12 +224,12 @@ class Podcast extends Entity
/** /**
* Returns the podcast's persons * Returns the podcast's persons
* *
* @return \App\Entities\PodcastPerson[] * @return PodcastPerson[]
*/ */
public function getPersons() public function getPersons(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting persons.', 'Podcast must be created before getting persons.',
); );
} }
@ -243,12 +246,12 @@ class Podcast extends Entity
/** /**
* Returns the podcast category entity * Returns the podcast category entity
* *
* @return \App\Entities\Category * @return Category
*/ */
public function getCategory() public function getCategory(): Category
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting category.', 'Podcast must be created before getting category.',
); );
} }
@ -265,12 +268,12 @@ class Podcast extends Entity
/** /**
* Returns all podcast contributors * Returns all podcast contributors
* *
* @return \App\Entities\User[] * @return User[]
*/ */
public function getContributors() public function getContributors(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcasts must be created before getting contributors.', 'Podcasts must be created before getting contributors.',
); );
} }
@ -284,7 +287,7 @@ class Podcast extends Entity
return $this->contributors; return $this->contributors;
} }
public function setDescriptionMarkdown(string $descriptionMarkdown) public function setDescriptionMarkdown(string $descriptionMarkdown): self
{ {
$converter = new CommonMarkConverter([ $converter = new CommonMarkConverter([
'html_input' => 'strip', 'html_input' => 'strip',
@ -300,8 +303,8 @@ class Podcast extends Entity
} }
public function setEpisodeDescriptionFooterMarkdown( public function setEpisodeDescriptionFooterMarkdown(
string $episodeDescriptionFooterMarkdown = null ?string $episodeDescriptionFooterMarkdown = null
) { ): self {
if ($episodeDescriptionFooterMarkdown) { if ($episodeDescriptionFooterMarkdown) {
$converter = new CommonMarkConverter([ $converter = new CommonMarkConverter([
'html_input' => 'strip', 'html_input' => 'strip',
@ -319,7 +322,7 @@ class Podcast extends Entity
return $this; return $this;
} }
public function getDescription() public function getDescription(): string
{ {
if ($this->description) { if ($this->description) {
return $this->description; return $this->description;
@ -337,12 +340,12 @@ class Podcast extends Entity
/** /**
* Returns the podcast's podcasting platform links * Returns the podcast's podcasting platform links
* *
* @return \App\Entities\Platform[] * @return Platform[]
*/ */
public function getPodcastingPlatforms() public function getPodcastingPlatforms(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting podcasting platform links.', 'Podcast must be created before getting podcasting platform links.',
); );
} }
@ -357,33 +360,15 @@ class Podcast extends Entity
return $this->podcastingPlatforms; 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 * Returns the podcast's social platform links
* *
* @return \App\Entities\Platform[] * @return Platform[]
*/ */
public function getSocialPlatforms() public function getSocialPlatforms(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting social platform links.', 'Podcast must be created before getting social platform links.',
); );
} }
@ -398,33 +383,15 @@ class Podcast extends Entity
return $this->socialPlatforms; 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 * Returns the podcast's funding platform links
* *
* @return \App\Entities\Platform[] * @return Platform[]
*/ */
public function getFundingPlatforms() public function getFundingPlatforms(): array
{ {
if (empty($this->id)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast must be created before getting funding platform links.', '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)) { if (empty($this->id)) {
throw new \RuntimeException( 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(
'Podcast must be created before getting other categories.', 'Podcast must be created before getting other categories.',
); );
} }
@ -474,7 +426,10 @@ class Podcast extends Entity
return $this->other_categories; return $this->other_categories;
} }
public function getOtherCategoriesIds() /**
* @return array<int>
*/
public function getOtherCategoriesIds(): array
{ {
if (empty($this->other_categories_ids)) { if (empty($this->other_categories_ids)) {
$this->other_categories_ids = array_column( $this->other_categories_ids = array_column(
@ -488,11 +443,8 @@ class Podcast extends Entity
/** /**
* Saves the location name and fetches OpenStreetMap info * 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'); helper('location');
@ -511,6 +463,7 @@ class Podcast extends Entity
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osmid'] = null; $this->attributes['location_osmid'] = null;
} }
return $this; return $this;
} }
@ -518,39 +471,39 @@ class Podcast extends Entity
* Get custom rss tag as XML String * Get custom rss tag as XML String
* *
* @return string * @return string
*
*/ */
function getCustomRssString() function getCustomRssString(): string
{ {
helper('rss');
if (empty($this->attributes['custom_rss'])) { if (empty($this->attributes['custom_rss'])) {
return ''; 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 * Saves custom rss tag into json
* *
* @param string $customRssString * @param string $customRssString
*
*/ */
function setCustomRssString($customRssString) function setCustomRssString($customRssString): self
{ {
if (empty($customRssString)) {
return $this;
}
helper('rss'); helper('rss');
$customRssArray = rss_to_array( $customRssArray = rss_to_array(
simplexml_load_string( simplexml_load_string(
@ -559,6 +512,7 @@ class Podcast extends Entity
'</channel></rss>', '</channel></rss>',
), ),
)['elements'][0]; )['elements'][0];
if (array_key_exists('elements', $customRssArray)) { if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode( $this->attributes['custom_rss'] = json_encode(
$customRssArray['elements'], $customRssArray['elements'],
@ -566,5 +520,7 @@ class Podcast extends Entity
} else { } else {
$this->attributes['custom_rss'] = null; $this->attributes['custom_rss'] = null;
} }
return $this;
} }
} }

View File

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

View File

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

View File

@ -8,25 +8,29 @@
namespace App\Entities; namespace App\Entities;
use RuntimeException;
use App\Models\PodcastModel; use App\Models\PodcastModel;
class User extends \Myth\Auth\Entities\User class User extends \Myth\Auth\Entities\User
{ {
/** /**
* Per-user podcasts * Per-user podcasts
* @var \App\Entities\Podcast[] * @var Podcast[]
*/ */
protected $podcasts = []; protected $podcasts = [];
/** /**
* The podcast the user is contributing to * 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 * Array of field names and the type of value to cast them as
* when they are accessed. * when they are accessed.
*
* @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
@ -39,13 +43,13 @@ class User extends \Myth\Auth\Entities\User
/** /**
* Returns the podcasts the user is contributing to * 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)) { if (empty($this->id)) {
throw new \RuntimeException( throw new RuntimeException(
'Users must be created before getting podcasts.' '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 * Returns a podcast the user is contributing to
*
* @return \App\Entities\Podcast
*/ */
public function getPodcast() public function getPodcast(): Podcast
{ {
if (empty($this->podcast_id)) { if (empty($this->podcast_id)) {
throw new \RuntimeException( throw new RuntimeException(
'Podcast_id must be set before getting podcast.' 'Podcast_id must be set before getting podcast.',
); );
} }
if (empty($this->podcast)) { if (empty($this->podcast)) {
$this->podcast = (new PodcastModel())->getPodcastById( $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, * sent back to the client, allowing for error pages,
* redirects, etc. * redirects, etc.
* *
* @param \CodeIgniter\HTTP\RequestInterface $request
* @param array|null $params * @param array|null $params
* * @return void|mixed
* @return mixed
*/ */
public function before(RequestInterface $request, $params = null) public function before(RequestInterface $request, $params = null)
{ {
@ -59,15 +57,14 @@ class PermissionFilter implements FilterInterface
count($routerParams) > 0 count($routerParams) > 0
) { ) {
if ( if (
$groupId = (new PodcastModel())->getContributorGroupId( ($groupId = (new PodcastModel())->getContributorGroupId(
$authenticate->id(), $authenticate->id(),
$routerParams[0] $routerParams[0],
) )) &&
$authorize->groupHasPermission($permission, $groupId)
) { ) {
if ($authorize->groupHasPermission($permission, $groupId)) { $result = true;
$result = true; break;
break;
}
} }
} elseif ( } elseif (
$authorize->hasPermission($permission, $authenticate->id()) $authorize->hasPermission($permission, $authenticate->id())
@ -84,31 +81,25 @@ class PermissionFilter implements FilterInterface
return redirect() return redirect()
->to($redirectURL) ->to($redirectURL)
->with('error', lang('Auth.notEnoughPrivilege')); ->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 * Allows After filters to inspect and modify the response
* object as needed. This method does not allow any way * object as needed. This method does not allow any way
* to stop execution of other after filters, short of * to stop execution of other after filters, short of
* throwing an Exception or Error. * throwing an Exception or Error.
* *
* @param \CodeIgniter\HTTP\RequestInterface $request
* @param \CodeIgniter\HTTP\ResponseInterface $response
* @param array|null $arguments * @param array|null $arguments
*
* @return void
*/ */
public function after( public function after(
RequestInterface $request, RequestInterface $request,
ResponseInterface $response, ResponseInterface $response,
$arguments = null $arguments = null
) { ): void {
} }
//-------------------------------------------------------------------- //--------------------------------------------------------------------

View File

@ -6,16 +6,15 @@
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use ActivityPub\Entities\Actor;
use CodeIgniter\Database\Exceptions\DataException; use CodeIgniter\Database\Exceptions\DataException;
use Config\Services; use Config\Services;
if (!function_exists('set_interact_as_actor')) { if (!function_exists('set_interact_as_actor')) {
/** /**
* Sets the actor id of which the user is acting as * 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 = Services::authentication();
$authenticate->check(); $authenticate->check();
@ -28,10 +27,8 @@ if (!function_exists('set_interact_as_actor')) {
if (!function_exists('remove_interact_as_actor')) { if (!function_exists('remove_interact_as_actor')) {
/** /**
* Removes the actor id of which the user is acting as * 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 = session();
$session->remove('interact_as_actor_id'); $session->remove('interact_as_actor_id');
@ -41,10 +38,8 @@ if (!function_exists('remove_interact_as_actor')) {
if (!function_exists('interact_as_actor_id')) { if (!function_exists('interact_as_actor_id')) {
/** /**
* Sets the podcast id of which the user is acting as * 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 = Services::authentication();
$authenticate->check(); $authenticate->check();
@ -58,7 +53,7 @@ if (!function_exists('interact_as_actor')) {
/** /**
* Get the actor the user is currently interacting as * Get the actor the user is currently interacting as
* *
* @return \ActivityPub\Entities\Actor|false * @return Actor|false
*/ */
function interact_as_actor() function interact_as_actor()
{ {
@ -78,11 +73,10 @@ if (!function_exists('interact_as_actor')) {
if (!function_exists('can_user_interact')) { if (!function_exists('can_user_interact')) {
/** /**
* @return bool
* @throws DataException * @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; use Config\Services;
/** if (!function_exists('render_breadcrumb')) {
* Renders the breadcrumb navigation through the Breadcrumb service /**
* * Renders the breadcrumb navigation through the Breadcrumb service
* @param string $class to be added to the breadcrumb nav *
* @return string html breadcrumb * @param string $class to be added to the breadcrumb nav
*/ * @return string html breadcrumb
function render_breadcrumb($class = null) */
{ function render_breadcrumb(string $class = null): string
$breadcrumb = Services::breadcrumb(); {
return $breadcrumb->render($class); $breadcrumb = Services::breadcrumb();
return $breadcrumb->render($class);
}
} }
function replace_breadcrumb_params($newParams) if (!function_exists('replace_breadcrumb_params')) {
{ function replace_breadcrumb_params($newParams): void
$breadcrumb = Services::breadcrumb(); {
$breadcrumb->replaceParams($newParams); $breadcrumb = Services::breadcrumb();
$breadcrumb->replaceParams($newParams);
}
} }

View File

@ -6,24 +6,25 @@
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use CodeIgniter\View\Table;
use CodeIgniter\I18n\Time;
if (!function_exists('button')) { if (!function_exists('button')) {
/** /**
* Button component * Button component
* *
* Creates a stylized button or button like anchor tag if the URL is defined. * Creates a stylized button or button like anchor tag if the URL is defined.
* *
* @param string $label The button label * @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param mixed|null $uri URI string or array of URI segments * @param array $customAttributes Additional attributes
* @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes
* *
* @return string * @return string
*/ */
function button( function button(
string $label = '', string $label = '',
$uri = null, string $uri = '',
$customOptions = [], array $customOptions = [],
$customAttributes = [] array $customAttributes = []
): string { ): string {
$defaultOptions = [ $defaultOptions = [
'variant' => 'default', 'variant' => 'default',
@ -90,7 +91,7 @@ if (!function_exists('button')) {
$label .= icon($options['iconRight'], 'ml-2'); $label .= icon($options['iconRight'], 'ml-2');
} }
if ($uri) { if ($uri !== '') {
return anchor( return anchor(
$uri, $uri,
$label, $label,
@ -111,8 +112,8 @@ if (!function_exists('button')) {
); );
return <<<HTML return <<<HTML
<button class="$buttonClass" $attributes> <button class="{$buttonClass}" {$attributes}>
$label {$label}
</button> </button>
HTML; HTML;
} }
@ -126,19 +127,19 @@ if (!function_exists('icon_button')) {
* *
* Abstracts the `button()` helper to create a stylized icon button * Abstracts the `button()` helper to create a stylized icon button
* *
* @param string $label The button label * @param string $label The button label
* @param mixed|null $uri URI string or array of URI segments * @param string $uri URI string or array of URI segments
* @param array $customOptions button options: variant, size, iconLeft, iconRight * @param array $customOptions button options: variant, size, iconLeft, iconRight
* @param array $customAttributes Additional attributes * @param array $customAttributes Additional attributes
* *
* @return string * @return string
*/ */
function icon_button( function icon_button(
string $icon, string $icon,
string $title, string $title,
$uri = null, string $uri = '',
$customOptions = [], array $customOptions = [],
$customAttributes = [] array $customAttributes = []
): string { ): string {
$defaultOptions = [ $defaultOptions = [
'isSquared' => true, 'isSquared' => true,
@ -197,9 +198,9 @@ if (!function_exists('data_table')) {
* *
* @return string * @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 = [ $template = [
'table_open' => '<table class="w-full whitespace-no-wrap">', 'table_open' => '<table class="w-full whitespace-no-wrap">',
@ -219,17 +220,17 @@ if (!function_exists('data_table')) {
$tableHeaders = []; $tableHeaders = [];
foreach ($columns as $column) { foreach ($columns as $column) {
array_push($tableHeaders, $column['header']); $tableHeaders[] = $column['header'];
} }
$table->setHeading($tableHeaders); $table->setHeading($tableHeaders);
if ($dataCount = count($data)) { if (($dataCount = count($data)) !== 0) {
for ($i = 0; $i < $dataCount; $i++) { for ($i = 0; $i < $dataCount; ++$i) {
$row = $data[$i]; $row = $data[$i];
$rowData = []; $rowData = [];
foreach ($columns as $column) { foreach ($columns as $column) {
array_push($rowData, $column['cell']($row, ...$rest)); $rowData[] = $column['cell']($row, ...$rest);
} }
$table->addRow($rowData); $table->addRow($rowData);
} }
@ -251,38 +252,39 @@ if (!function_exists('publication_pill')) {
* *
* Shows the stylized publication datetime in regards to current datetime. * 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 boolean $isPublished whether or not the episode has been published
* @param string $customClass css class to add to the component * @param string $customClass css class to add to the component
* *
* @return string * @return string
*/ */
function publication_pill( function publication_pill(
$publicationDate, ?Time $publicationDate,
$publicationStatus, $publicationStatus,
$customClass = '' string $customClass = ''
): string { ): string {
if ($publicationDate === null) {
return '';
}
$class = $class =
$publicationStatus === 'published' $publicationStatus === 'published'
? 'text-pine-500 border-pine-500' ? 'text-pine-500 border-pine-500'
: 'text-red-600 border-red-600'; : 'text-red-600 border-red-600';
$transParam = []; $langOptions = [
if ($publicationDate) { '<time pubdate datetime="' .
$transParam = [ $publicationDate->format(DateTime::ATOM) .
'<time pubdate datetime="' . '" title="' .
$publicationDate->format(DateTime::ATOM) . $publicationDate .
'" title="' . '">' .
$publicationDate . lang('Common.mediumDate', [$publicationDate]) .
'">' . '</time>',
lang('Common.mediumDate', [$publicationDate]) . ];
'</time>',
];
}
$label = lang( $label = lang(
'Episode.publication_status.' . $publicationStatus, 'Episode.publication_status.' . $publicationStatus,
$transParam, $langOptions,
); );
return '<span class="px-1 font-semibold border ' . 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. * Displays the appropriate publication button depending on the publication status.
* *
* @param integer $podcastId
* @param integer $episodeId
* @param boolean $publicationStatus the episode's publication status * * @param boolean $publicationStatus the episode's publication status *
* @return string * @return string
*/ */
function publication_button( function publication_button(
$podcastId, int $podcastId,
$episodeId, int $episodeId,
$publicationStatus bool $publicationStatus
): string { ): string {
switch ($publicationStatus) { switch ($publicationStatus) {
case 'not_published': case 'not_published':
@ -351,17 +351,15 @@ if (!function_exists('episode_numbering')) {
/** /**
* Returns relevant translated episode numbering. * Returns relevant translated episode numbering.
* *
* @param int|null $episodeNumber
* @param int|null $seasonNumber
* @param string $class styling classes * @param string $class styling classes
* @param string $is_abbr component will show abbreviated numbering if true * @param string $is_abbr component will show abbreviated numbering if true
* *
* @return string|null * @return string|null
*/ */
function episode_numbering( function episode_numbering(
$episodeNumber = null, ?int $episodeNumber = null,
$seasonNumber = null, ?int $seasonNumber = null,
$class = '', string $class = '',
$isAbbr = false $isAbbr = false
): string { ): string {
if (!$episodeNumber && !$seasonNumber) { if (!$episodeNumber && !$seasonNumber) {
@ -409,36 +407,28 @@ if (!function_exists('episode_numbering')) {
if (!function_exists('location_link')) { if (!function_exists('location_link')) {
/** /**
* Returns link to display from location info * Returns link to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
*/ */
function location_link( function location_link(
$locationName, ?string $locationName,
$locationGeo, ?string $locationGeo,
$locationOsmid, ?string $locationOsmid,
$class = '' $class = ''
) { ): string {
$link = ''; if (empty($locationName)) {
return '';
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',
],
);
} }
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 <?php
/** /**
* @copyright 2020 Podlibre * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @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 * Abstracts form_label to stylize it as a switch toggle
* *
* @param array $data
* @param string $value
* @param boolean $checked
* @param mixed $extra
*
* @return string * @return string
*/ */
function form_switch( function form_switch(
$label = '', $label = '',
$data = '', array $data = [],
string $value = '', string $value = '',
bool $checked = false, bool $checked = false,
$class = '', string $class = '',
$extra = '' array $extra = []
): string { ): string {
$data['class'] = 'form-switch'; $data['class'] = 'form-switch';
@ -155,31 +151,26 @@ if (!function_exists('form_multiselect')) {
/** /**
* Multi-select menu * Multi-select menu
* *
* @param string $name
* @param array $options
* @param array $selected
* @param mixed $extra
*
* @return string * @return string
*/ */
function form_multiselect( function form_multiselect(
string $name = '', string $name = '',
array $options = [], array $options = [],
array $selected = [], array $selected = [],
$customExtra = '' array $customExtra = []
): string { ): string {
$defaultExtra = [ $defaultExtra = [
'data-class' => $customExtra['class'], 'data-class' => $customExtra['class'],
'data-select-text' => lang('Common.forms.multiSelect.selectText'), 'data-select-text' => lang('Common.forms.multiSelect.selectText'),
'data-loading-text' => lang('Common.forms.multiSelect.loadingText'), 'data-loading-text' => lang('Common.forms.multiSelect.loadingText'),
'data-no-results-text' => lang( 'data-no-results-text' => lang(
'Common.forms.multiSelect.noResultsText' 'Common.forms.multiSelect.noResultsText',
), ),
'data-no-choices-text' => lang( 'data-no-choices-text' => lang(
'Common.forms.multiSelect.noChoicesText' 'Common.forms.multiSelect.noChoicesText',
), ),
'data-max-item-text' => lang( 'data-max-item-text' => lang(
'Common.forms.multiSelect.maxItemText' 'Common.forms.multiSelect.maxItemText',
), ),
]; ];
$extra = stringify_attributes(array_merge($defaultExtra, $customExtra)); $extra = stringify_attributes(array_merge($defaultExtra, $customExtra));

View File

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

View File

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

View File

@ -8,125 +8,120 @@
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\Files\UploadedFile;
use Config\Services;
/** if (!function_exists('save_media')) {
* Saves a file to the corresponding podcast folder in `public/media` /**
* * Saves a file to the corresponding podcast folder in `public/media`
* @param \CodeIgniter\HTTP\Files\UploadedFile|\CodeIgniter\Files\File $filePath *
* @param string $folder * @param File|UploadedFile $filePath
* @param string $fileName */
* function save_media(
* @return string The episode's file path in media root File $filePath,
*/ string $folder,
function save_media($filePath, $folder, $mediaName) string $mediaName
{ ): string {
$fileName = $mediaName . '.' . $filePath->getExtension(); $fileName = $mediaName . '.' . $filePath->getExtension();
$mediaRoot = config('App')->mediaRoot . '/' . $folder; $mediaRoot = config('App')->mediaRoot . '/' . $folder;
if (!file_exists($mediaRoot)) { if (!file_exists($mediaRoot)) {
mkdir($mediaRoot, 0777, true); mkdir($mediaRoot, 0777, true);
touch($mediaRoot . '/index.html'); 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;
} }
/** if (!function_exists('download_file')) {
* @param string $fileUrl function download_file(string $fileUrl): File
* @return File {
*/ $client = Services::curlrequest();
function download_file($fileUrl)
{
$client = \Config\Services::curlrequest();
$response = $client->get($fileUrl, [ $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, [
'headers' => [ 'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION, '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, '/');
/** return config('App')->mediaRoot . '/' . $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);
} }
$uri = trim($uri, '/');
return config('App')->mediaRoot . '/' . $uri;
} }
/** if (!function_exists('media_base_url')) {
* Return the media base URL to use in views /**
* * Return the media base URL to use in views
* @param mixed $uri URI string or array of URI segments *
* @param string $protocol * @param string|array $uri URI string or array of URI segments
* @return string * @param string $protocol
*/ */
function media_url($uri = '', string $protocol = null): string function media_base_url($uri = ''): string
{ {
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol); // convert segment array to string
} if (is_array($uri)) {
$uri = implode('/', $uri);
}
$uri = trim($uri, '/');
function media_base_url($uri = '') return rtrim(config('App')->mediaBaseURL, '/') .
{ '/' .
// convert segment array to string config('App')->mediaRoot .
if (is_array($uri)) { '/' .
$uri = implode('/', $uri); $uri;
} }
$uri = trim($uri, '/');
return rtrim(config('App')->mediaBaseURL, '/') .
'/' .
config('App')->mediaRoot .
'/' .
$uri;
} }

View File

@ -6,142 +6,140 @@
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
/** if (!function_exists('get_browser_language')) {
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE` /**
* * 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
* @return string|null ISO 639-1 language code or null */
*/ function get_browser_language(string $httpAcceptLanguage): ?string
function get_browser_language($http_accept_language) {
{ $langs = explode(',', $httpAcceptLanguage);
$langs = explode(',', $http_accept_language); if (!empty($langs)) {
if (!empty($langs)) { return substr($langs[0], 0, 2);
return substr($langs[0], 0, 2); }
}
return null; return null;
}
} }
/** if (!function_exists('startsWith')) {
* Check if a string starts with some characters /**
* * Check if a string starts with some characters
* @param string $string */
* @param string $query function startsWith(string $string, string $query): bool
* {
* @return bool return substr($string, 0, strlen($query)) === $query;
*/ }
function startsWith($string, $query)
{
return substr($string, 0, strlen($query)) === $query;
} }
function slugify($text) if (!function_exists('slugify')) {
{ function slugify($text)
if (empty($text)) { {
return 'n-a'; 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 * Formats duration in seconds to an hh:mm:ss string
* *
* @param int $seconds seconds to format * @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( return sprintf(
'%02d%s%02d%s%02d', '%02d%s%02d%s%02d',
@ -163,7 +158,7 @@ if (!function_exists('format_duration')) {
$separator, $separator,
($seconds / 60) % 60, ($seconds / 60) % 60,
$separator, $separator,
$seconds % 60 $seconds % 60,
); );
} }
} }

View File

@ -8,26 +8,27 @@
use App\Models\PageModel; use App\Models\PageModel;
/** if (!function_exists('render_page_links')) {
* Returns instance pages as links inside nav tag /**
* * Returns instance pages as links inside nav tag
* @param string $class *
* @return string html pages navigation * @return string html pages navigation
*/ */
function render_page_links($class = null) function render_page_links(string $class = null): string
{ {
$pages = (new PageModel())->findAll(); $pages = (new PageModel())->findAll();
$links = anchor(route_to('home'), lang('Common.home'), [ $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', '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/ * @link https://castopod.org/
*/ */
/** if (!function_exists('construct_person_array')) {
* Fetches persons from an episode /**
* * Fetches persons from an episode
* @param array $persons *
* @param array &$personsArray * @param array &$personsArray
*/ */
function construct_person_array($persons, &$personsArray) function construct_person_array(array $persons, &$personsArray): void
{ {
foreach ($persons as $person) { foreach ($persons as $person) {
if (array_key_exists($person->person->id, $personsArray)) { if (array_key_exists($person->person->id, $personsArray)) {
$personsArray[$person->person->id]['roles'] .= $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' =>
empty($person->person_group) || empty($person->person_role) empty($person->person_group) || empty($person->person_role)
? '' ? ''
: lang( : (empty($personsArray[$person->person->id]['roles'])
'PersonsTaxonomy.persons.' . ? ''
$person->person_group . : ', ') .
'.roles.' . lang(
$person->person_role . 'PersonsTaxonomy.persons.' .
'.label', $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 App\Libraries\SimpleRSSElement;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Config\Mimes; use Config\Mimes;
use App\Entities\Podcast;
use App\Entities\Category;
/** if (!function_exists('get_rss_feed')) {
* Generates the rss feed for a given podcast entity /**
* * 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 * @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 * @return string rss feed as xml
*/ */
function get_rss_feed($podcast, $serviceSlug = '') function get_rss_feed(Podcast $podcast, $serviceSlug = ''): string
{ {
$episodes = $podcast->episodes; $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 = $podcast_namespace =
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md'; 'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md';
$rss = new SimpleRSSElement( $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>", "<?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,
); );
}
// the last build date corresponds to the creation of the feed.xml cache $channel = $rss->addChild('channel');
$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->addChild('title', $podcast->title); $atom_link = $channel->addChild(
$channel->addChildWithCDATA('description', $podcast->description_html); 'atom:link',
$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',
null, null,
$podcast_namespace, 'http://www.w3.org/2005/Atom',
); );
$recipientElement->addAttribute('name', $podcast->owner_name); $atom_link->addAttribute('href', $podcast->feed_url);
$recipientElement->addAttribute('type', 'ILP'); $atom_link->addAttribute('rel', 'self');
$recipientElement->addAttribute('address', $podcast->payment_pointer); $atom_link->addAttribute('type', 'application/rss+xml');
$recipientElement->addAttribute('split', 100);
} if (!empty($podcast->new_feed_url)) {
$channel $channel->addChild(
->addChild( 'new-feed-url',
'locked', $podcast->new_feed_url,
$podcast->is_locked ? 'yes' : 'no', $itunes_namespace,
$podcast_namespace, );
) }
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) { // the last build date corresponds to the creation of the feed.xml cache
$channel->addChild( $channel->addChild(
'previousUrl', 'lastBuildDate',
$podcast->imported_feed_url, (new Time('now'))->format(DATE_RFC1123),
$podcast_namespace,
); );
} $channel->addChild(
'generator',
foreach ($podcast->podcastingPlatforms as $podcastingPlatform) { 'Castopod Host - https://castopod.org/',
$podcastingPlatformElement = $channel->addChild(
'id',
null,
$podcast_namespace,
); );
$podcastingPlatformElement->addAttribute( $channel->addChild('docs', 'https://cyber.harvard.edu/rss/rss.html');
'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) { $channel->addChild('title', $podcast->title);
$socialPlatformElement = $channel->addChild( $channel->addChildWithCDATA('description', $podcast->description_html);
'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) { $itunes_image = $channel->addChild('image', null, $itunes_namespace);
$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) { $itunes_image->addAttribute('href', $podcast->image->original_url);
$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,
);
}
}
// set main category first, then other categories as apple $channel->addChild('language', $podcast->language_code);
add_category_tag($channel, $podcast->category); if (!empty($podcast->location_name)) {
foreach ($podcast->other_categories as $other_category) { $locationElement = $channel->addChild(
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(
'location', 'location',
htmlspecialchars($episode->location_name), htmlspecialchars($podcast->location_name),
$podcast_namespace, $podcast_namespace,
); );
if (!empty($episode->location_geo)) { if (!empty($podcast->location_geo)) {
$locationElement->addAttribute('geo', $episode->location_geo); $locationElement->addAttribute('geo', $podcast->location_geo);
} }
if (!empty($episode->location_osmid)) { if (!empty($podcast->location_osmid)) {
$locationElement->addAttribute('osm', $episode->location_osmid); $locationElement->addAttribute('osm', $podcast->location_osmid);
} }
} }
$item->addChildWithCDATA( if (!empty($podcast->payment_pointer)) {
'description', $valueElement = $channel->addChild(
$episode->getDescriptionHtml($serviceSlug), 'value',
);
$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, null,
$podcast_namespace, $podcast_namespace,
); );
$transcriptElement->addAttribute( $valueElement->addAttribute('type', 'webmonetization');
'url', $valueElement->addAttribute('method', '');
$episode->transcript_file_url, $valueElement->addAttribute('suggested', '');
); $recipientElement = $valueElement->addChild(
$transcriptElement->addAttribute( 'valueRecipient',
'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, null,
$podcast_namespace, $podcast_namespace,
); );
$chaptersElement->addAttribute('url', $episode->chapters_file_url); $recipientElement->addAttribute('name', $podcast->owner_name);
$chaptersElement->addAttribute('type', 'application/json+chapters'); $recipientElement->addAttribute('type', 'ILP');
$recipientElement->addAttribute(
'address',
$podcast->payment_pointer,
);
$recipientElement->addAttribute('split', 100);
} }
$channel
foreach ($episode->soundbites as $soundbite) { ->addChild(
$soundbiteElement = $item->addChild( 'locked',
'soundbite', $podcast->is_locked ? 'yes' : 'no',
empty($soundbite->label) ? null : $soundbite->label, $podcast_namespace,
)
->addAttribute('owner', $podcast->owner_email);
if (!empty($podcast->imported_feed_url)) {
$channel->addChild(
'previousUrl',
$podcast->imported_feed_url,
$podcast_namespace, $podcast_namespace,
); );
$soundbiteElement->addAttribute(
'start_time',
$soundbite->start_time,
);
$soundbiteElement->addAttribute('duration', $soundbite->duration);
} }
foreach ($episode->persons as $episodePerson) { foreach ($podcast->podcastingPlatforms as $podcastingPlatform) {
$episodePersonElement = $item->addChild( $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', 'person',
htmlspecialchars($episodePerson->person->full_name), htmlspecialchars($podcastPerson->person->full_name),
$podcast_namespace, $podcast_namespace,
); );
if ( if (
!empty($episodePerson->person_role) && !empty($podcastPerson->person_role) &&
!empty($episodePerson->person_group) !empty($podcastPerson->person_group)
) { ) {
$episodePersonElement->addAttribute( $podcastPersonElement->addAttribute(
'role', 'role',
htmlspecialchars( htmlspecialchars(
lang( lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.roles.{$episodePerson->person_role}.label", "PersonsTaxonomy.persons.{$podcastPerson->person_group}.roles.{$podcastPerson->person_role}.label",
[], [],
'en', 'en',
), ),
), ),
); );
} }
if (!empty($episodePerson->person_group)) { if (!empty($podcastPerson->person_group)) {
$episodePersonElement->addAttribute( $podcastPersonElement->addAttribute(
'group', 'group',
htmlspecialchars( htmlspecialchars(
lang( lang(
"PersonsTaxonomy.persons.{$episodePerson->person_group}.label", "PersonsTaxonomy.persons.{$podcastPerson->person_group}.label",
[], [],
'en', 'en',
), ),
), ),
); );
} }
$episodePersonElement->addAttribute( $podcastPersonElement->addAttribute(
'img', 'img',
$episodePerson->person->image->large_url, $podcastPerson->person->image->large_url,
); );
if (!empty($episodePerson->person->information_url)) { if (!empty($podcastPerson->person->information_url)) {
$episodePersonElement->addAttribute( $podcastPersonElement->addAttribute(
'href', 'href',
$episodePerson->person->information_url, $podcastPerson->person->information_url,
); );
} }
} }
$episode->is_blocked && // set main category first, then other categories as apple
$item->addChild('block', 'Yes', $itunes_namespace); add_category_tag($channel, $podcast->category);
foreach ($podcast->other_categories as $other_category) {
if (!empty($episode->custom_rss)) { add_category_tag($channel, $other_category);
array_to_rss(
[
'elements' => $episode->custom_rss,
],
$item,
);
} }
}
return $rss->asXML(); $channel->addChild(
} 'explicit',
$podcast->parental_advisory === 'explicit' ? 'true' : 'false',
/**
* 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,
$itunes_namespace, $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);
} }
/** if (!function_exists('add_category_tag')) {
* Converts XML to array /**
* * Adds <itunes:category> and <category> tags to node for a given category
* @param \SimpleRSSElement $xmlNode */
* function add_category_tag(SimpleXMLElement $node, Category $category): void
* @return array {
*/ $itunes_namespace = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
function rss_to_array($xmlNode)
{ $itunes_category = $node->addChild('category', null, $itunes_namespace);
$nameSpaces = [ $itunes_category->addAttribute(
'', 'text',
'http://www.itunes.com/dtds/podcast-1.0.dtd', $category->parent !== null
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md', ? $category->parent->apple_category
]; : $category->apple_category,
$arrayNode = []; );
$arrayNode['name'] = $xmlNode->getName();
$arrayNode['namespace'] = $xmlNode->getNamespaces(false); if ($category->parent !== null) {
if (count($xmlNode->attributes()) > 0) { $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) { foreach ($xmlNode->attributes() as $key => $value) {
$arrayNode['attributes'][$key] = (string) $value; $arrayNode['attributes'][$key] = (string) $value;
} }
} $textcontent = trim((string) $xmlNode);
$textcontent = trim((string) $xmlNode); if (strlen($textcontent) > 0) {
if (strlen($textcontent) > 0) { $arrayNode['content'] = $textcontent;
$arrayNode['content'] = $textcontent;
}
foreach ($nameSpaces as $currentNameSpace) {
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
} }
foreach ($nameSpaces as $currentNameSpace) {
foreach ($xmlNode->children($currentNameSpace) as $childXmlNode) {
$arrayNode['elements'][] = rss_to_array($childXmlNode);
}
}
return $arrayNode;
} }
return $arrayNode;
} }
/** if (!function_exists('array_to_rss')) {
* Inserts array (converted to XML node) in XML node /**
* * 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 * @param SimpleRSSElement $xmlNode The XML parent node where this arrayNode should be attached
* */
*/ function array_to_rss(array $arrayNode, SimpleRSSElement &$xmlNode)
function array_to_rss($arrayNode, &$xmlNode) {
{ if (array_key_exists('elements', $arrayNode)) {
if (array_key_exists('elements', $arrayNode)) { foreach ($arrayNode['elements'] as $childArrayNode) {
foreach ($arrayNode['elements'] as $childArrayNode) { $childXmlNode = $xmlNode->addChild(
$childXmlNode = $xmlNode->addChild( $childArrayNode['name'],
$childArrayNode['name'], $childArrayNode['content'] ?? null,
array_key_exists('content', $childArrayNode) empty($childArrayNode['namespace'])
? $childArrayNode['content'] ? null
: null, : current($childArrayNode['namespace']),
empty($childArrayNode['namespace']) );
? null if (array_key_exists('attributes', $childArrayNode)) {
: current($childArrayNode['namespace']), foreach (
); $childArrayNode['attributes']
if (array_key_exists('attributes', $childArrayNode)) { as $attributeKey => $attributeValue
foreach ( ) {
$childArrayNode['attributes'] $childXmlNode->addAttribute(
as $attributeKey => $attributeValue $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/ * @link https://castopod.org/
*/ */
/** if (!function_exists('icon')) {
* Returns the inline svg 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 * @param string $name name of the icon file without the .svg extension
* @return string svg contents * @param string $class to be added to the svg string
*/ * @return string svg contents
function icon(string $name, string $class = '') */
{ function icon(string $name, string $class = ''): string
$svg_contents = file_get_contents('assets/icons/' . $name . '.svg'); {
if ($class !== '') { $svg_contents = file_get_contents('assets/icons/' . $name . '.svg');
$svg_contents = str_replace( if ($class !== '') {
'<svg', $svg_contents = str_replace(
'<svg class="' . $class . '"', '<svg',
$svg_contents, '<svg class="' . $class . '"',
); $svg_contents,
} );
}
return $svg_contents; return $svg_contents;
}
} }
/** if (!function_exists('svg')) {
* Returns the inline svg image /**
* * 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 * @param string $name name of the image file without the .svg extension
* @return string svg contents * @param string $class to be added to the svg string
*/ * @return string svg contents
function svg($name, $class = null) */
{ function svg(string $name, ?string $class = null): string
$svg_contents = file_get_contents('assets/images/' . $name . '.svg'); {
if ($class) { $svg_contents = file_get_contents('assets/images/' . $name . '.svg');
$svg_contents = str_replace( if ($class) {
'<svg', $svg_contents = str_replace(
'<svg class="' . $class . '"', '<svg',
$svg_contents, '<svg class="' . $class . '"',
); $svg_contents,
);
}
return $svg_contents;
} }
return $svg_contents;
} }

View File

@ -1,5 +1,11 @@
<?php <?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')) { if (!function_exists('host_url')) {
/** /**
* Return the host URL to use in views * Return the host URL to use in views
@ -24,10 +30,8 @@ if (!function_exists('host_url')) {
if (!function_exists('current_season_url')) { if (!function_exists('current_season_url')) {
/** /**
* Return the podcast URL with season number to use in views * 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 = ''; $season_query_string = '';
if (isset($_GET['season'])) { if (isset($_GET['season'])) {
@ -42,36 +46,27 @@ if (!function_exists('current_season_url')) {
if (!function_exists('location_url')) { if (!function_exists('location_url')) {
/** /**
* Returns URL to display from location info * Returns URL to display from location info
*
* @param string $locationName
* @param string $locationGeo
* @param string $locationOsmid
*
* @return string
*/ */
function location_url($locationName, $locationGeo, $locationOsmid) function location_url(
{ string $locationName,
$uri = ''; ?string $locationGeo = null,
?string $locationOsmid = null
): string {
if (!empty($locationOsmid)) { if (!empty($locationOsmid)) {
$uri = return 'https://www.openstreetmap.org/' .
'https://www.openstreetmap.org/' .
['N' => 'node', 'W' => 'way', 'R' => 'relation'][ ['N' => 'node', 'W' => 'way', 'R' => 'relation'][
substr($locationOsmid, 0, 1) substr($locationOsmid, 0, 1)
] . ] .
'/' . '/' .
substr($locationOsmid, 1); substr($locationOsmid, 1);
} elseif (!empty($locationGeo)) { }
$uri = if (!empty($locationGeo)) {
'https://www.openstreetmap.org/#map=17/' . return 'https://www.openstreetmap.org/#map=17/' .
str_replace(',', '/', substr($locationGeo, 4)); 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 * Returns podcast name and episode slug from episode string uri
* *
* @param URI $episodeUri * @param URI $episodeUri
* @return string|null
*/ */
function extract_params_from_episode_uri($episodeUri) function extract_params_from_episode_uri($episodeUri): ?array
{ {
preg_match( 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(), $episodeUri->getPath(),
$matches, $matches,
); );
if ( if ($matches === []) {
$matches && return null;
array_key_exists('podcastName', $matches) &&
array_key_exists('episodeSlug', $matches)
) {
return [
'podcastName' => $matches['podcastName'],
'episodeSlug' => $matches['episodeSlug'],
];
} }
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', 'submit' => 'Proceed to follow',
], ],
'favourite' => [ 'favourite' => [
'title' => 'Favourite {actorDisplayName}\'s note', 'title' => "Favourite {actorDisplayName}'s note",
'subtitle' => 'You are going to favourite:', 'subtitle' => 'You are going to favourite:',
'submit' => 'Proceed to favourite', 'submit' => 'Proceed to favourite',
], ],
'reblog' => [ 'reblog' => [
'title' => 'Share {actorDisplayName}\'s note', 'title' => "Share {actorDisplayName}'s note",
'subtitle' => 'You are going to share:', 'subtitle' => 'You are going to share:',
'submit' => 'Proceed to share', 'submit' => 'Proceed to share',
], ],
'reply' => [ 'reply' => [
'title' => 'Reply to {actorDisplayName}\'s note', 'title' => "Reply to {actorDisplayName}'s note",
'subtitle' => 'You are going to reply to:', 'subtitle' => 'You are going to reply to:',
'submit' => 'Proceed to reply', 'submit' => 'Proceed to reply',
], ],

View File

@ -8,7 +8,7 @@
return [ return [
'podcast_contributors' => 'Podcast contributors', 'podcast_contributors' => 'Podcast contributors',
'view' => '{username}\'s contribution to {podcastName}', 'view' => "{username}'s contribution to {podcastName}",
'add' => 'Add contributor', 'add' => 'Add contributor',
'add_contributor' => 'Add a contributor for {0}', 'add_contributor' => 'Add a contributor for {0}',
'edit_role' => 'Update role for {0}', 'edit_role' => 'Update role for {0}',
@ -28,10 +28,10 @@ return [
'podcast_admin' => 'Podcast admin', 'podcast_admin' => 'Podcast admin',
], ],
'messages' => [ 'messages' => [
'removeOwnerContributorError' => 'You can\'t remove the podcast owner!', 'removeOwnerContributorError' => "You can't remove the podcast owner!",
'removeContributorSuccess' => 'removeContributorSuccess' =>
'You have successfully removed {username} from {podcastTitle}', 'You have successfully removed {username} from {podcastTitle}',
'alreadyAddedError' => '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', 'CF' => 'Central African Republic',
'CG' => 'Congo', 'CG' => 'Congo',
'CH' => 'Switzerland', 'CH' => 'Switzerland',
'CI' => 'Côte d\'Ivoire', 'CI' => "Côte d'Ivoire",
'CK' => 'Cook Islands', 'CK' => 'Cook Islands',
'CL' => 'Chile', 'CL' => 'Chile',
'CM' => 'Cameroon', 'CM' => 'Cameroon',
@ -128,12 +128,12 @@ return [
'KI' => 'Kiribati', 'KI' => 'Kiribati',
'KM' => 'Comoros', 'KM' => 'Comoros',
'KN' => 'Saint Kitts and Nevis', 'KN' => 'Saint Kitts and Nevis',
'KP' => 'Korea, Democratic People\'s Republic of', 'KP' => "Korea, Democratic People's Republic of",
'KR' => 'Korea, Republic of', 'KR' => 'Korea, Republic of',
'KW' => 'Kuwait', 'KW' => 'Kuwait',
'KY' => 'Cayman Islands', 'KY' => 'Cayman Islands',
'KZ' => 'Kazakhstan', 'KZ' => 'Kazakhstan',
'LA' => 'Lao People\'s Democratic Republic', 'LA' => "Lao People's Democratic Republic",
'LB' => 'Lebanon', 'LB' => 'Lebanon',
'LC' => 'Saint Lucia', 'LC' => 'Saint Lucia',
'LI' => 'Liechtenstein', 'LI' => 'Liechtenstein',

View File

@ -127,7 +127,7 @@ return [
], ],
'unpublish_form' => [ 'unpublish_form' => [
'disclaimer' => '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', 'understand' => 'I understand, I want to unpublish the episode',
'submit' => 'Unpublish', 'submit' => 'Unpublish',
], ],

View File

@ -31,7 +31,7 @@ return [
'db_password' => 'Database password', 'db_password' => 'Database password',
'db_prefix' => 'Database prefix', 'db_prefix' => 'Database prefix',
'db_prefix_hint' => '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' => 'Cache configuration',
'cache_config_hint' => 'cache_config_hint' =>
'Choose your preferred cache handler. Leave it as the default value if you have no clue what it means.', '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' => 'databaseConnectError' =>
'Castopod could not connect to your database. Edit your database configuration and try again.', 'Castopod could not connect to your database. Edit your database configuration and try again.',
'writeError' => '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', 'info' => 'My account info',
'changePassword' => 'Change my password', 'changePassword' => 'Change my password',
'messages' => [ 'messages' => [
'wrongPasswordError' => 'wrongPasswordError' => "You've entered the wrong password, try again.",
'You\'ve entered the wrong password, try again.',
'passwordChangeSuccess' => 'Password has been successfully changed!', 'passwordChangeSuccess' => 'Password has been successfully changed!',
], ],
]; ];

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