diff --git a/app/Config/App.php b/app/Config/App.php index ae52af66..fe5c59a8 100644 --- a/app/Config/App.php +++ b/app/Config/App.php @@ -293,7 +293,7 @@ class App extends BaseConfig * (empty string) means default SameSite attribute set by browsers (`Lax`) * will be set on cookies. If set to `None`, `$cookieSecure` must also be set. * - * @deprecated use Config\Cookie::$samesite property instead. + * @deprecated `Config\Cookie` $samesite property is used. */ public string $cookieSameSite = 'Lax'; diff --git a/app/Config/Constants.php b/app/Config/Constants.php index e685c5d2..462eea3c 100644 --- a/app/Config/Constants.php +++ b/app/Config/Constants.php @@ -52,9 +52,9 @@ defined('MINUTE') || define('MINUTE', 60); defined('HOUR') || define('HOUR', 3600); defined('DAY') || define('DAY', 86400); defined('WEEK') || define('WEEK', 604800); -defined('MONTH') || define('MONTH', 2592000); -defined('YEAR') || define('YEAR', 31536000); -defined('DECADE') || define('DECADE', 315360000); +defined('MONTH') || define('MONTH', 2_592_000); +defined('YEAR') || define('YEAR', 31_536_000); +defined('DECADE') || define('DECADE', 315_360_000); /* | -------------------------------------------------------------------------- @@ -91,3 +91,18 @@ defined('EXIT_USER_INPUT') || define('EXIT_USER_INPUT', 7); // invalid user inpu defined('EXIT_DATABASE') || define('EXIT_DATABASE', 8); // database error defined('EXIT__AUTO_MIN') || define('EXIT__AUTO_MIN', 9); // lowest automatically-assigned error code defined('EXIT__AUTO_MAX') || define('EXIT__AUTO_MAX', 125); // highest automatically-assigned error code + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_LOW instead. + */ +define('EVENT_PRIORITY_LOW', 200); + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_NORMAL instead. + */ +define('EVENT_PRIORITY_NORMAL', 100); + +/** + * @deprecated Use \CodeIgniter\Events\Events::PRIORITY_HIGH instead. + */ +define('EVENT_PRIORITY_HIGH', 10); diff --git a/app/Config/ContentSecurityPolicy.php b/app/Config/ContentSecurityPolicy.php index 44670867..301f7731 100644 --- a/app/Config/ContentSecurityPolicy.php +++ b/app/Config/ContentSecurityPolicy.php @@ -145,4 +145,19 @@ class ContentSecurityPolicy extends BaseConfig * @var string|string[]|null */ public string | array | null $sandbox = null; + + /** + * Nonce tag for style + */ + public string $styleNonceTag = '{csp-style-nonce}'; + + /** + * Nonce tag for script + */ + public string $scriptNonceTag = '{csp-script-nonce}'; + + /** + * Replace nonce tag automatically + */ + public bool $autoNonce = true; } diff --git a/app/Config/Database.php b/app/Config/Database.php index e0813199..7042ccf8 100644 --- a/app/Config/Database.php +++ b/app/Config/Database.php @@ -61,8 +61,9 @@ class Database extends Config 'database' => ':memory:', 'DBDriver' => 'SQLite3', 'DBPrefix' => 'db_', + // Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS 'pConnect' => false, - 'DBDebug' => ENVIRONMENT !== 'production', + 'DBDebug' => (ENVIRONMENT !== 'production'), 'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci', 'swapPre' => '', @@ -71,6 +72,7 @@ class Database extends Config 'strictOn' => false, 'failover' => [], 'port' => 3306, + 'foreignKeys' => true, ]; //-------------------------------------------------------------------- diff --git a/app/Config/Events.php b/app/Config/Events.php index 9fd5f311..f8567024 100644 --- a/app/Config/Events.php +++ b/app/Config/Events.php @@ -39,9 +39,7 @@ Events::on('pre_system', function () { ob_end_flush(); } - ob_start(function ($buffer) { - return $buffer; - }); + ob_start(static fn ($buffer) => $buffer); } /* @@ -132,11 +130,11 @@ Events::on('on_post_add', function ($post): void { if ($post->episode_id !== null) { if ($isReply) { - model(EpisodeModel::class, false) + model(EpisodeModel::class, false)->builder() ->where('id', $post->episode_id) ->increment('comments_count'); } else { - model(EpisodeModel::class, false) + model(EpisodeModel::class, false)->builder() ->where('id', $post->episode_id) ->increment('posts_count'); } @@ -161,7 +159,7 @@ Events::on('on_post_remove', function ($post): void { } if ($episodeId = $post->episode_id) { - model(EpisodeModel::class, false) + model(EpisodeModel::class, false)->builder() ->where('id', $episodeId) ->decrement('posts_count'); } diff --git a/app/Config/Feature.php b/app/Config/Feature.php index 25659ddd..1f381b3c 100644 --- a/app/Config/Feature.php +++ b/app/Config/Feature.php @@ -12,7 +12,7 @@ use CodeIgniter\Config\BaseConfig; class Feature extends BaseConfig { /** - * Enable multiple filters for a route or not + * Enable multiple filters for a route or not. * * If you enable this: * - CodeIgniter\CodeIgniter::handleRequest() uses: @@ -24,4 +24,9 @@ class Feature extends BaseConfig * - CodeIgniter\Router\RouteCollection::getFiltersForRoute(), instead of getFilterForRoute() */ public bool $multipleFilters = false; + + /** + * Use improved new auto routing instead of the default legacy version. + */ + public bool $autoRoutesImproved = false; } diff --git a/app/Config/Filters.php b/app/Config/Filters.php index fdc14d79..f7088509 100644 --- a/app/Config/Filters.php +++ b/app/Config/Filters.php @@ -59,7 +59,10 @@ class Filters extends BaseConfig /** * List of filter aliases that works on a particular HTTP method (GET, POST, etc.). * - * Example: 'post' => ['csrf', 'throttle'] + * Example: 'post' => ['foo', 'bar'] + * + * If you use this, you should disable auto-routing because auto-routing permits any HTTP method to access a + * controller. Accessing the controller with a method you don’t expect could bypass the filter. * * @var array */ diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php index bf94916e..b47fc9a5 100644 --- a/app/Config/Mimes.php +++ b/app/Config/Mimes.php @@ -169,6 +169,7 @@ class Mimes 'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'png' => ['image/png', 'image/x-png'], + 'webp' => 'image/webp', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'css' => ['text/css', 'text/plain'], diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php index 39018572..61e752fa 100644 --- a/app/Config/Publisher.php +++ b/app/Config/Publisher.php @@ -14,4 +14,15 @@ use CodeIgniter\Config\Publisher as BasePublisher; */ class Publisher extends BasePublisher { + /** + * A list of allowed destinations with a (pseudo-)regex of allowed files for each destination. Attempts to publish + * to directories not in this list will result in a PublisherException. Files that do no fit the pattern will cause + * copy/merge to fail. + * + * @var array + */ + public $restrictions = [ + ROOTPATH => '*', + FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + ]; } diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 1432ceda..0bb3fb1d 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -9,7 +9,7 @@ $routes = Services::routes(); // Load the system's routing file first, so that the app and ENVIRONMENT // can override as needed. -if (file_exists(SYSTEMPATH . 'Config/Routes.php')) { +if (is_file(SYSTEMPATH . 'Config/Routes.php')) { require SYSTEMPATH . 'Config/Routes.php'; } @@ -23,6 +23,11 @@ $routes->setDefaultController('Home'); $routes->setDefaultMethod('index'); $routes->setTranslateURIDashes(false); $routes->set404Override(); + +// The Auto Routing (Legacy) is very dangerous. It is easy to create vulnerable apps +// where controller filters or CSRF protection are bypassed. +// If you don't want to define all routes, please use the Auto Routing (Improved). +// Set `$autoRoutesImproved` to true in `app/Config/Feature.php` and set the following to true. $routes->setAutoRoute(false); /** @@ -303,6 +308,6 @@ $routes->group('@(:podcastHandle)', function ($routes): void { * You will have access to the $routes object within that file without * needing to reload it. */ -if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) { +if (is_file(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) { require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php'; } diff --git a/app/Config/Security.php b/app/Config/Security.php index 92c105d8..0b986a41 100644 --- a/app/Config/Security.php +++ b/app/Config/Security.php @@ -83,4 +83,23 @@ class Security extends BaseConfig * Redirect to previous page with error on failure. */ public bool $redirect = true; + + /** + * -------------------------------------------------------------------------- + * CSRF SameSite + * -------------------------------------------------------------------------- + * + * Setting for CSRF SameSite cookie token. + * + * Allowed values are: None - Lax - Strict - ''. + * + * Defaults to `Lax` as recommended in this link: + * + * @see https://portswigger.net/web-security/csrf/samesite-cookies + * + * @var string + * + * @deprecated `Config\Cookie` $samesite property is used. + */ + public $samesite = 'Lax'; } diff --git a/app/Config/Validation.php b/app/Config/Validation.php index bcabb7f4..ae8d9e69 100644 --- a/app/Config/Validation.php +++ b/app/Config/Validation.php @@ -6,13 +6,14 @@ namespace Config; use App\Validation\FileRules as AppFileRules; use App\Validation\Rules as AppRules; +use CodeIgniter\Config\BaseConfig; use CodeIgniter\Validation\CreditCardRules; use CodeIgniter\Validation\FileRules; use CodeIgniter\Validation\FormatRules; use CodeIgniter\Validation\Rules; use Myth\Auth\Authentication\Passwords\ValidationRules as PasswordRules; -class Validation +class Validation extends BaseConfig { /** * Stores the classes that contain the rules that are available. diff --git a/app/Config/View.php b/app/Config/View.php index 0650d129..212f28c9 100644 --- a/app/Config/View.php +++ b/app/Config/View.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Config; use CodeIgniter\Config\View as BaseView; +use CodeIgniter\View\ViewDecoratorInterface; class View extends BaseView { @@ -36,4 +37,14 @@ class View extends BaseView * @var string[] */ public $plugins = []; + + /** + * View Decorators are class methods that will be run in sequence to have a chance to alter the generated output + * just prior to caching the results. + * + * All classes must implement CodeIgniter\View\ViewDecoratorInterface + * + * @var class-string[] + */ + public array $decorators = []; } diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 312f1ec2..fb3d273c 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -18,7 +18,7 @@ use ViewThemes\Theme; * * For security be sure to declare any new methods as protected or private. */ -class BaseController extends Controller +abstract class BaseController extends Controller { /** * Constructor. diff --git a/app/Database/Migrations/2020-05-29-120000_add_media.php b/app/Database/Migrations/2020-05-29-120000_add_media.php index af6c4aab..177827c5 100644 --- a/app/Database/Migrations/2020-05-29-120000_add_media.php +++ b/app/Database/Migrations/2020-05-29-120000_add_media.php @@ -67,10 +67,6 @@ class AddMedia extends Migration 'updated_at' => [ 'type' => 'DATETIME', ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], ]); $this->forge->addKey('id', true); diff --git a/app/Database/Migrations/2020-08-17-150000_add_pages.php b/app/Database/Migrations/2020-08-17-150000_add_pages.php index 35eb249c..c544cb7e 100644 --- a/app/Database/Migrations/2020-08-17-150000_add_pages.php +++ b/app/Database/Migrations/2020-08-17-150000_add_pages.php @@ -45,10 +45,6 @@ class AddPages extends Migration 'updated_at' => [ 'type' => 'DATETIME', ], - 'deleted_at' => [ - 'type' => 'DATETIME', - 'null' => true, - ], ]); $this->forge->addPrimaryKey('id'); $this->forge->createTable('pages'); diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 32223685..6474642e 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -136,7 +136,7 @@ class Episode extends Entity /** * @var string[] */ - protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at']; + protected $dates = ['published_at', 'created_at', 'updated_at']; /** * @var array diff --git a/app/Entities/EpisodeComment.php b/app/Entities/EpisodeComment.php index 43769dda..f2dda34c 100644 --- a/app/Entities/EpisodeComment.php +++ b/app/Entities/EpisodeComment.php @@ -96,12 +96,13 @@ class EpisodeComment extends UuidEntity throw new RuntimeException('Comment must have an actor_id before getting actor.'); } - if ($this->actor === null) { + if (! $this->actor instanceof Actor) { // @phpstan-ignore-next-line $this->actor = model(ActorModel::class, false) ->getActorById($this->actor_id); } + // @phpstan-ignore-next-line return $this->actor; } @@ -135,7 +136,7 @@ class EpisodeComment extends UuidEntity throw new RuntimeException('Comment is not a reply.'); } - if ($this->reply_to_comment === null) { + if (! $this->reply_to_comment instanceof self) { $this->reply_to_comment = model(EpisodeCommentModel::class, false) ->getCommentById($this->in_reply_to_id); } diff --git a/app/Entities/Media/BaseMedia.php b/app/Entities/Media/BaseMedia.php index 2acf221d..3c0c88ee 100644 --- a/app/Entities/Media/BaseMedia.php +++ b/app/Entities/Media/BaseMedia.php @@ -38,7 +38,7 @@ class BaseMedia extends Entity /** * @var string[] */ - protected $dates = ['uploaded_at', 'updated_at', 'deleted_at']; + protected $dates = ['uploaded_at', 'updated_at']; /** * @var array @@ -112,6 +112,6 @@ class BaseMedia extends Entity public function delete(): bool|BaseResult { $mediaModel = new MediaModel(); - return $mediaModel->delete($this->id, true); + return $mediaModel->delete($this->id); } } diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index 84e99cda..9e58b461 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -197,12 +197,13 @@ class Podcast extends Entity throw new RuntimeException('Podcast must have an actor_id before getting actor.'); } - if ($this->actor === null) { + if (! $this->actor instanceof Actor) { // @phpstan-ignore-next-line $this->actor = model(ActorModel::class, false) ->getActorById($this->actor_id); } + // @phpstan-ignore-next-line return $this->actor; } diff --git a/app/Models/ClipModel.php b/app/Models/ClipModel.php index 79cab11c..a47e172e 100644 --- a/app/Models/ClipModel.php +++ b/app/Models/ClipModel.php @@ -131,7 +131,7 @@ class ClipModel extends Model public function getRunningVideoClipsCount(): int { - $result = $this + $result = $this->builder() ->select('COUNT(*) as `running_count`') ->where([ 'type' => 'video', @@ -146,6 +146,7 @@ class ClipModel extends Model public function doesVideoClipExist(VideoClip $videoClip): int | false { $result = $this->select('id') + ->builder() ->where([ 'podcast_id' => $videoClip->podcast_id, 'episode_id' => $videoClip->episode_id, diff --git a/app/Models/EpisodeCommentModel.php b/app/Models/EpisodeCommentModel.php index 47e4fd8d..83909982 100644 --- a/app/Models/EpisodeCommentModel.php +++ b/app/Models/EpisodeCommentModel.php @@ -13,6 +13,7 @@ namespace App\Models; use App\Entities\EpisodeComment; use App\Libraries\CommentObject; use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\BaseResult; use Michalsn\Uuid\UuidModel; use Modules\Fediverse\Activities\CreateActivity; use Modules\Fediverse\Models\ActivityModel; @@ -82,11 +83,11 @@ class EpisodeCommentModel extends UuidModel } if ($comment->in_reply_to_id === null) { - (new EpisodeModel()) + (new EpisodeModel())->builder() ->where('id', $comment->episode_id) ->increment('comments_count'); } else { - (new self()) + (new self())->builder() ->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes()) ->increment('replies_count'); } @@ -146,17 +147,19 @@ class EpisodeCommentModel extends UuidModel public function getEpisodeComments(int $episodeId): array { // TODO: merge with replies from posts linked to episode linked - $episodeComments = $this->select('*, 0 as is_from_post') + $episodeCommentsBuilder = $this->builder(); + $episodeComments = $episodeCommentsBuilder->select('*, 0 as is_from_post') ->where([ 'episode_id' => $episodeId, 'in_reply_to_id' => null, ]) ->getCompiledSelect(); - $episodePostsReplies = $this->db->table(config('Fediverse')->tablesPrefix . 'posts') - ->select( - 'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post' - ) + $postModel = new PostModel(); + $episodePostsRepliesBuilder = $postModel->builder(); + $episodePostsReplies = $episodePostsRepliesBuilder->select( + 'id, uri, episode_id, actor_id, in_reply_to_id, message, message_html, favourites_count as likes_count, replies_count, published_at as created_at, created_by, 1 as is_from_post' + ) ->whereIn('in_reply_to_id', function (BaseBuilder $builder) use (&$episodeId): BaseBuilder { return $builder->select('id') ->from(config('Fediverse')->tablesPrefix . 'posts') @@ -168,6 +171,7 @@ class EpisodeCommentModel extends UuidModel ->where('`created_at` <= UTC_TIMESTAMP()', null, false) ->getCompiledSelect(); + /** @var BaseResult $allEpisodeComments */ $allEpisodeComments = $this->db->query( $episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC' ); @@ -211,7 +215,8 @@ class EpisodeCommentModel extends UuidModel public function resetRepliesCount(): int | false { - $commentsRepliesCount = $this->select('episode_comments.id, COUNT(*) as `replies_count`') + $commentsRepliesCount = $this->builder() + ->select('episode_comments.id, COUNT(*) as `replies_count`') ->join('episode_comments as c2', 'episode_comments.id = c2.in_reply_to_id') ->groupBy('episode_comments.id') ->get() diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 3b20c07b..d5a3555c 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace App\Models; use App\Entities\Episode; +use CodeIgniter\Database\BaseResult; use CodeIgniter\I18n\Time; use CodeIgniter\Model; @@ -264,7 +265,8 @@ class EpisodeModel extends Model */ public function getSecondsToNextUnpublishedEpisode(int $podcastId): int | false { - $result = $this->select('TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), `published_at`) as timestamp_diff') + $result = $this->builder() + ->select('TIMESTAMPDIFF(SECOND, UTC_TIMESTAMP(), `published_at`) as timestamp_diff') ->where([ 'podcast_id' => $podcastId, ]) @@ -280,10 +282,11 @@ class EpisodeModel extends Model public function getCurrentSeasonNumber(int $podcastId): ?int { - $result = $this->select('MAX(season_number) as current_season_number') + $result = $this->builder() + ->select('MAX(season_number) as current_season_number') ->where([ 'podcast_id' => $podcastId, - $this->deletedField => null, + 'published_at IS NOT' => null, ]) ->get() ->getResultArray(); @@ -293,11 +296,12 @@ class EpisodeModel extends Model public function getNextEpisodeNumber(int $podcastId, ?int $seasonNumber): int { - $result = $this->select('MAX(number) as next_episode_number') + $result = $this->builder() + ->select('MAX(number) as next_episode_number') ->where([ 'podcast_id' => $podcastId, 'season_number' => $seasonNumber, - $this->deletedField => null, + 'published_at IS NOT' => null, ])->get() ->getResultArray(); @@ -309,13 +313,13 @@ class EpisodeModel extends Model */ public function getPodcastStats(int $podcastId): array { - $result = $this->select( - 'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at' - ) + $result = $this->builder() + ->select( + 'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at' + ) ->where([ 'podcast_id' => $podcastId, 'published_at IS NOT' => null, - $this->deletedField => null, ])->get() ->getResultArray(); @@ -333,13 +337,16 @@ class EpisodeModel extends Model public function resetCommentsCount(): int | false { - $episodeCommentsCount = $this->select('episodes.id, COUNT(*) as `comments_count`') + $episodeCommentsBuilder = $this->builder(); + $episodeCommentsCount = $episodeCommentsBuilder->select('episodes.id, COUNT(*) as `comments_count`') ->join('episode_comments', 'episodes.id = episode_comments.episode_id') ->where('in_reply_to_id', null) ->groupBy('episodes.id') ->getCompiledSelect(); - $episodePostsRepliesCount = $this + $postModel = new PostModel(); + $episodePostsRepliesBuilder = $postModel->builder(); + $episodePostsRepliesCount = $episodePostsRepliesBuilder ->select('episodes.id, COUNT(*) as `comments_count`') ->join( config('Fediverse') @@ -350,6 +357,7 @@ class EpisodeModel extends Model ->groupBy('episodes.id') ->getCompiledSelect(); + /** @var BaseResult $query */ $query = $this->db->query( 'SELECT `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `id`' ); @@ -365,7 +373,8 @@ class EpisodeModel extends Model public function resetPostsCount(): int | false { - $episodePostsCount = $this->select('episodes.id, COUNT(*) as `posts_count`') + $episodePostsCount = $this->builder() + ->select('episodes.id, COUNT(*) as `posts_count`') ->join( config('Fediverse') ->tablesPrefix . 'posts', diff --git a/app/Models/LikeModel.php b/app/Models/LikeModel.php index 8462d4c3..f4eebe36 100644 --- a/app/Models/LikeModel.php +++ b/app/Models/LikeModel.php @@ -56,7 +56,7 @@ class LikeModel extends UuidModel 'comment_id' => $comment->id, ]); - (new EpisodeCommentModel()) + (new EpisodeCommentModel())->builder() ->where('id', service('uuid')->fromString($comment->id)->getBytes()) ->increment('likes_count'); @@ -91,7 +91,7 @@ class LikeModel extends UuidModel { $this->db->transStart(); - (new EpisodeCommentModel()) + (new EpisodeCommentModel())->builder() ->where('id', service('uuid') ->fromString($comment->id) ->getBytes()) ->decrement('likes_count'); diff --git a/app/Models/MediaModel.php b/app/Models/MediaModel.php index 0357eb67..dc0f1f5b 100644 --- a/app/Models/MediaModel.php +++ b/app/Models/MediaModel.php @@ -36,7 +36,7 @@ class MediaModel extends Model /** * @var bool */ - protected $useSoftDeletes = true; + protected $useSoftDeletes = false; /** * @var bool @@ -121,6 +121,7 @@ class MediaModel extends Model 'id' => $mediaId, ]); + /** @var object $result */ $result = $builder->first(); $mediaClass = $this->returnType; $found = new $mediaClass($result->toArray(false, true)); @@ -176,7 +177,7 @@ class MediaModel extends Model { $media->deleteFile(); - return $this->delete($media->id, true); + return $this->delete($media->id); } /** diff --git a/app/Models/PageModel.php b/app/Models/PageModel.php index 664196f2..62772b59 100644 --- a/app/Models/PageModel.php +++ b/app/Models/PageModel.php @@ -38,7 +38,7 @@ class PageModel extends Model /** * @var bool */ - protected $useSoftDeletes = true; + protected $useSoftDeletes = false; /** * @var bool diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php index a01bb233..00d065a0 100644 --- a/app/Models/PersonModel.php +++ b/app/Models/PersonModel.php @@ -110,7 +110,7 @@ class PersonModel extends Model $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}_roles"; if (! ($found = cache($cacheName))) { - $found = $this + $found = $this->builder() ->select('episodes_persons.person_group as group, episodes_persons.person_role as role') ->join('episodes_persons', 'persons.id = episodes_persons.person_id') ->where('persons.id', $personId) @@ -122,7 +122,7 @@ class PersonModel extends Model $cacheName = "podcast#{$podcastId}_person#{$personId}_roles"; if (! ($found = cache($cacheName))) { - $found = $this + $found = $this->builder() ->select('podcasts_persons.person_group as group, podcasts_persons.person_role as role') ->join('podcasts_persons', 'persons.id = podcasts_persons.person_id') ->where('persons.id', $personId) @@ -210,15 +210,15 @@ class PersonModel extends Model { $cacheName = "podcast#{$podcastId}_episode#{$episodeId}_persons"; if (! ($found = cache($cacheName))) { - $found = $this + $this->builder() ->select( 'persons.*, episodes_persons.podcast_id as podcast_id, episodes_persons.episode_id as episode_id' ) ->distinct() ->join('episodes_persons', 'persons.id = episodes_persons.person_id') ->where('episodes_persons.episode_id', $episodeId) - ->orderby('persons.full_name') - ->findAll(); + ->orderby('persons.full_name'); + $found = $this->findAll(); cache() ->save($cacheName, $found, DECADE); @@ -234,13 +234,13 @@ class PersonModel extends Model { $cacheName = "podcast#{$podcastId}_persons"; if (! ($found = cache($cacheName))) { - $found = $this + $this->builder() ->select('persons.*, podcasts_persons.podcast_id as podcast_id') ->distinct() ->join('podcasts_persons', 'persons.id=podcasts_persons.person_id') ->where('podcasts_persons.podcast_id', $podcastId) - ->orderby('persons.full_name') - ->findAll(); + ->orderby('persons.full_name'); + $found = $this->findAll(); cache() ->save($cacheName, $found, DECADE); diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index b28c9e2d..e9f618e3 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -175,9 +175,10 @@ class PodcastModel extends Model $fediverseTablePrefix = $prefix . config('Fediverse') ->tablesPrefix; - $this->select( - 'podcasts.*, MAX(' . $fediverseTablePrefix . 'posts.published_at' . ') as max_published_at' - ) + $this->builder() + ->select( + 'podcasts.*, MAX(' . $fediverseTablePrefix . 'posts.published_at' . ') as max_published_at' + ) ->join( $fediverseTablePrefix . 'posts', $fediverseTablePrefix . 'posts.actor_id = podcasts.actor_id', @@ -302,11 +303,12 @@ class PodcastModel extends Model if (! ($found = cache($cacheName))) { $episodeModel = new EpisodeModel(); $found = $episodeModel + ->builder() ->select('YEAR(published_at) as year, count(*) as number_of_episodes') ->where([ 'podcast_id' => $podcastId, 'season_number' => null, - $episodeModel->deletedField => null, + 'published_at IS NOT' => null, ]) ->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->groupBy('year') @@ -338,11 +340,12 @@ class PodcastModel extends Model if (! ($found = cache($cacheName))) { $episodeModel = new EpisodeModel(); $found = $episodeModel + ->builder() ->select('season_number, count(*) as number_of_episodes') ->where([ 'podcast_id' => $podcastId, 'season_number is not' => null, - $episodeModel->deletedField => null, + 'published_at IS NOT' => null, ]) ->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->groupBy('season_number') diff --git a/app/Views/errors/html/error_exception.php b/app/Views/errors/html/error_exception.php index 9e9e9a9a..01a72cf2 100644 --- a/app/Views/errors/html/error_exception.php +++ b/app/Views/errors/html/error_exception.php @@ -32,7 +32,7 @@ $error_id = uniqid('error', true); ?>
-

at line

+

at line

@@ -66,9 +66,9 @@ $error_id = uniqid('error', true); ?> @@ -201,7 +201,7 @@ $error_id = uniqid('error', true); ?> HTTP Method - getMethod(true)) ?> + getMethod())) ?> IP Address @@ -316,7 +316,7 @@ $error_id = uniqid('error', true); ?> - +
Response StatusgetStatusCode() . ' - ' . $response->getReason()) ?>getStatusCode() . ' - ' . $response->getReasonPhrase()) ?>
@@ -352,7 +352,7 @@ $error_id = uniqid('error', true); ?>
    -
  1. +
diff --git a/builds b/builds index 0b10a150..cc2ca085 100644 --- a/builds +++ b/builds @@ -39,7 +39,7 @@ if (is_file($file)) { if ($dev) { $array['minimum-stability'] = 'dev'; $array['prefer-stable'] = true; - $array['repositories'] = $array['repositories'] ?? []; + $array['repositories'] ??= []; $found = false; diff --git a/composer.json b/composer.json index 14cbca1c..a3257bb5 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "license": "AGPL-3.0-or-later", "require": { "php": "^8.0", - "codeigniter4/framework": "^4", + "codeigniter4/framework": "^4.2.0", "james-heinrich/getid3": "^2.0.x-dev", "whichbrowser/parser": "^v2.1.1", "geoip2/geoip2": "^v2.12.2", @@ -35,10 +35,6 @@ "symplify/coding-standard": "^10.1" }, "autoload": { - "psr-4": { - "App\\": "app", - "Config\\": "app/Config" - }, "exclude-from-classmap": [ "**/Database/Migrations/**" ] diff --git a/composer.lock b/composer.lock index aa1b0a37..86d66339 100644 --- a/composer.lock +++ b/composer.lock @@ -173,16 +173,16 @@ }, { "name": "codeigniter4/framework", - "version": "v4.1.9", + "version": "v4.2.0", "source": { "type": "git", "url": "https://github.com/codeigniter4/framework.git", - "reference": "4ec623a6b8269dd09f570ab514e5864276bb7f56" + "reference": "29f0e9eb2442eba41f4e9832b6695c7584e096e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/framework/zipball/4ec623a6b8269dd09f570ab514e5864276bb7f56", - "reference": "4ec623a6b8269dd09f570ab514e5864276bb7f56", + "url": "https://api.github.com/repos/codeigniter4/framework/zipball/29f0e9eb2442eba41f4e9832b6695c7584e096e0", + "reference": "29f0e9eb2442eba41f4e9832b6695c7584e096e0", "shasum": "" }, "require": { @@ -190,15 +190,15 @@ "ext-intl": "*", "ext-json": "*", "ext-mbstring": "*", - "kint-php/kint": "^4.0", + "kint-php/kint": "^4.1.1", "laminas/laminas-escaper": "^2.9", - "php": "^7.3 || ^8.0", + "php": "^7.4 || ^8.0", "psr/log": "^1.1" }, "require-dev": { "codeigniter/coding-standard": "^1.1", "fakerphp/faker": "^1.9", - "friendsofphp/php-cs-fixer": "^3.1", + "friendsofphp/php-cs-fixer": "3.6.*", "mikey179/vfsstream": "^1.6", "nexusphp/cs-config": "^3.3", "phpunit/phpunit": "^9.1", @@ -223,7 +223,7 @@ "slack": "https://codeigniterchat.slack.com", "source": "https://github.com/codeigniter4/CodeIgniter4" }, - "time": "2022-02-26T00:51:52+00:00" + "time": "2022-06-03T14:04:14+00:00" }, { "name": "codeigniter4/settings", diff --git a/env b/env index 84a59a84..67faaee5 100644 --- a/env +++ b/env @@ -21,12 +21,14 @@ #-------------------------------------------------------------------- # app.baseURL = '' +# If you have trouble with `.`, you could also use `_`. +# app_baseURL = '' # app.forceGlobalSecureRequests = false # app.sessionDriver = 'CodeIgniter\Session\Handlers\FileHandler' # app.sessionCookieName = 'ci_session' # app.sessionExpiration = 7200 -# app.sessionSavePath = NULL +# app.sessionSavePath = null # app.sessionMatchIP = false # app.sessionTimeToUpdate = 300 # app.sessionRegenerateDestroy = false @@ -60,7 +62,7 @@ # contentsecuritypolicy.scriptSrc = 'self' # contentsecuritypolicy.styleSrc = 'self' # contentsecuritypolicy.imageSrc = 'self' -# contentsecuritypolicy.base_uri = null +# contentsecuritypolicy.baseURI = null # contentsecuritypolicy.childSrc = null # contentsecuritypolicy.connectSrc = 'self' # contentsecuritypolicy.fontSrc = null @@ -73,6 +75,9 @@ # contentsecuritypolicy.reportURI = null # contentsecuritypolicy.sandbox = false # contentsecuritypolicy.upgradeInsecureRequests = false +# contentsecuritypolicy.styleNonceTag = '{csp-style-nonce}' +# contentsecuritypolicy.scriptNonceTag = '{csp-script-nonce}' +# contentsecuritypolicy.autoNonce = true #-------------------------------------------------------------------- # COOKIE diff --git a/modules/Admin/Controllers/BaseController.php b/modules/Admin/Controllers/BaseController.php index a38b8e84..667460a1 100644 --- a/modules/Admin/Controllers/BaseController.php +++ b/modules/Admin/Controllers/BaseController.php @@ -19,7 +19,7 @@ use ViewThemes\Theme; * For security be sure to declare any new methods as protected or private. */ -class BaseController extends Controller +abstract class BaseController extends Controller { /** * Constructor. diff --git a/modules/Admin/Controllers/PodcastController.php b/modules/Admin/Controllers/PodcastController.php index aef52ed5..8a18b7b3 100644 --- a/modules/Admin/Controllers/PodcastController.php +++ b/modules/Admin/Controllers/PodcastController.php @@ -513,11 +513,15 @@ class PodcastController extends BaseController 'type' => 'cover', 'file' => $this->podcast->cover, ], + ]; + + if ($this->podcast->banner_id !== null) { + $podcastMediaList[] = [ 'type' => 'banner', 'file' => $this->podcast->banner, - ], - ]; + ]; + } foreach ($podcastMediaList as $podcastMedia) { if ($podcastMedia['file'] !== null && ! $podcastMedia['file']->delete()) { @@ -566,6 +570,15 @@ class PodcastController extends BaseController } } + if ($this->podcast->actor_id === interact_as_actor_id()) { + //set interact to the most recently created podcast actor + $mostRecentPodcast = (new PodcastModel())->orderBy('created_at', 'desc') + ->first(); + if ($mostRecentPodcast !== null) { + set_interact_as_actor($mostRecentPodcast->actor_id); + } + } + $db->transComplete(); //delete podcast media files and folder diff --git a/phpstan.neon b/phpstan.neon index c32c499c..48893dbd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -19,8 +19,5 @@ parameters: - themes/* ignoreErrors: - '#Cannot access property [\$a-z_]+ on ((array\|)?object)#' - - '#^Call to an undefined method CodeIgniter\\Database\\BaseBuilder#' - '#^Call to an undefined method CodeIgniter\\Database\\ConnectionInterface#' - - '#^Call to an undefined method CodeIgniter\\Model#' - '#^Access to an undefined property App\\Entities\\Media\\Image#' - - '#^Access to an undefined property CodeIgniter\\Database\\BaseBuilder#' \ No newline at end of file diff --git a/public/index.php b/public/index.php index 4d04f8a5..cc758a8b 100644 --- a/public/index.php +++ b/public/index.php @@ -2,11 +2,16 @@ declare(strict_types=1); +use CodeIgniter\Config\DotEnv; use Config\Paths; +use Config\Services; // Path to the front controller (this file) define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR); +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + /* *--------------------------------------------------------------- * BOOTSTRAP THE APPLICATION @@ -16,20 +21,34 @@ define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR); * and fires up an environment-specific bootstrapping. */ -// Ensure the current directory is pointing to the front controller's directory -chdir(__DIR__); - // Load our paths config file // This is the line that might need to be changed, depending on your folder structure. -$pathsConfig = FCPATH . '../app/Config/Paths.php'; -// ^^^ Change this if you move your application folder -require realpath($pathsConfig) ?: $pathsConfig; +require FCPATH . '../app/Config/Paths.php'; +// ^^^ Change this line if you move your application folder $paths = new Paths(); // Location of the framework bootstrap file. -$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; -$app = require realpath($bootstrap) ?: $bootstrap; +require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; + +// Load environment settings from .env files into $_SERVER and $_ENV +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new DotEnv(ROOTPATH))->load(); + +/* + * --------------------------------------------------------------- + * GRAB OUR CODEIGNITER INSTANCE + * --------------------------------------------------------------- + * + * The CodeIgniter class contains the core functionality to make + * the application run, and does all of the dirty work to get + * the pieces all working together. + */ + +$app = Services::codeigniter(); +$app->initialize(); +$context = is_cli() ? 'php-cli' : 'web'; +$app->setContext($context); /* *--------------------------------------------------------------- @@ -38,4 +57,5 @@ $app = require realpath($bootstrap) ?: $bootstrap; * Now that everything is setup, it's time to actually fire * up the engines and make this app do its thang. */ + $app->run(); diff --git a/spark b/spark index f62aeddb..225422aa 100644 --- a/spark +++ b/spark @@ -1,6 +1,15 @@ #!/usr/bin/env php + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + /* * -------------------------------------------------------------------- * CodeIgniter command-line tools @@ -12,8 +21,28 @@ * this class mainly acts as a passthru to the framework itself. */ +// Refuse to run when called from php-cgi +if (strpos(PHP_SAPI, 'cgi') === 0) { + exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); +} + +// We want errors to be shown when using it from the CLI. +error_reporting(-1); +ini_set('display_errors', '1'); + +/** + * @var bool + * + * @deprecated No longer in use. `CodeIgniter` has `$context` property. + */ define('SPARKED', true); +// Path to the front controller +define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); + +// Ensure the current directory is pointing to the front controller's directory +chdir(FCPATH); + /* *--------------------------------------------------------------- * BOOTSTRAP THE APPLICATION @@ -23,47 +52,39 @@ define('SPARKED', true); * and fires up an environment-specific bootstrapping. */ -// Refuse to run when called from php-cgi -if (strpos(PHP_SAPI, 'cgi') === 0) -{ - die("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); -} - -// Path to the front controller -define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); - // Load our paths config file -$pathsConfig = 'app/Config/Paths.php'; +// This is the line that might need to be changed, depending on your folder structure. +require FCPATH . '../app/Config/Paths.php'; // ^^^ Change this line if you move your application folder -require realpath($pathsConfig) ?: $pathsConfig; $paths = new Config\Paths(); -// Ensure the current directory is pointing to the front controller's directory -chdir(FCPATH); +// Location of the framework bootstrap file. +require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; -$bootstrap = rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php'; -$app = require realpath($bootstrap) ?: $bootstrap; +// Load environment settings from .env files into $_SERVER and $_ENV +require_once SYSTEMPATH . 'Config/DotEnv.php'; +(new CodeIgniter\Config\DotEnv(ROOTPATH))->load(); + +// Grab our CodeIgniter +$app = Config\Services::codeigniter(); +$app->initialize(); +$app->setContext('spark'); // Grab our Console $console = new CodeIgniter\CLI\Console($app); -// We want errors to be shown when using it from the CLI. -error_reporting(-1); -ini_set('display_errors', '1'); - // Show basic information before we do anything else. -if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) -{ - unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore - $suppress = true; +if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) { + unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore + $suppress = true; } $console->showHeader($suppress); // fire off the command in the main framework. $response = $console->run(); -if ($response->getStatusCode() >= 300) -{ - exit($response->getStatusCode()); + +if ($response->getStatusCode() >= 300) { + exit($response->getStatusCode()); } diff --git a/tests/README.md b/tests/README.md index 3452c2d1..99812ccd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -8,12 +8,12 @@ test your application. Those details can be found in the documentation. ## Resources - [CodeIgniter 4 User Guide on Testing](https://codeigniter4.github.io/userguide/testing/index.html) -- [PHPUnit docs](https://phpunit.readthedocs.io/en/8.5/index.html) +- [PHPUnit docs](https://phpunit.de/documentation.html) ## Requirements It is recommended to use the latest version of PHPUnit. At the time of this -writing we are running version 8.5.13. Support for this has been built into the +writing we are running version 9.x. Support for this has been built into the **composer.json** file that ships with CodeIgniter and can easily be installed via [Composer](https://getcomposer.org/) if you don't already have it installed globally. @@ -34,7 +34,8 @@ A number of the tests use a running database. In order to set up the database edit the details for the `tests` group in **app/Config/Database.php** or **phpunit.xml**. Make sure that you provide a database engine that is currently running on your machine. More details on a test database setup are in the -_Docs>>Testing>>Testing Your Database_ section of the documentation. +[Testing Your Database](https://codeigniter4.github.io/userguide/testing/database.html) +section of the documentation. If you want to run the tests without using live database you can exclude @DatabaseLive group. Or make a copy of **phpunit.dist.xml** - call it @@ -48,6 +49,10 @@ the main directory. > ./phpunit +If you are using Windows, use the following command. + + > vendor\bin\phpunit + You can limit tests to those within a single test directory by specifying the directory name after phpunit. diff --git a/themes/cp_app/episode/_layout.php b/themes/cp_app/episode/_layout.php index fd6b02a7..075a28ad 100644 --- a/themes/cp_app/episode/_layout.php +++ b/themes/cp_app/episode/_layout.php @@ -44,13 +44,13 @@