fix: replace deletedField with published_at for episodes

- remove delete_at field + soft delete for media and pages
- update CodeIgniter4 to 4.2.0 + update all starter files
- explicitly use builder() when creating queries from model
This commit is contained in:
Yassine Doghri 2022-06-13 16:30:34 +00:00
parent dbb4030da4
commit 14d7d07822
42 changed files with 297 additions and 141 deletions

View File

@ -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';

View File

@ -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);

View File

@ -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;
}

View File

@ -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,
];
//--------------------------------------------------------------------

View File

@ -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');
}

View File

@ -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;
}

View File

@ -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 dont expect could bypass the filter.
*
* @var array<string, string[]>
*/

View File

@ -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'],

View File

@ -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<string,string>
*/
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',
];
}

View File

@ -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';
}

View File

@ -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';
}

View File

@ -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.

View File

@ -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<ViewDecoratorInterface>[]
*/
public array $decorators = [];
}

View File

@ -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.

View File

@ -67,10 +67,6 @@ class AddMedia extends Migration
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);

View File

@ -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');

View File

@ -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<string, string>

View File

@ -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);
}

View File

@ -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<string, string>
@ -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);
}
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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()

View File

@ -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',

View File

@ -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');

View File

@ -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);
}
/**

View File

@ -38,7 +38,7 @@ class PageModel extends Model
/**
* @var bool
*/
protected $useSoftDeletes = true;
protected $useSoftDeletes = false;
/**
* @var bool

View File

@ -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);

View File

@ -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')

View File

@ -32,7 +32,7 @@ $error_id = uniqid('error', true); ?>
<!-- Source -->
<div class="container">
<p><b><?= esc(static::cleanPath($file, $line)) ?></b> at line <b><?= esc($line) ?></b></p>
<p><b><?= esc(clean_path($file, $line)) ?></b> at line <b><?= esc($line) ?></b></p>
<?php if (is_file($file)) : ?>
<div class="source">
@ -66,9 +66,9 @@ $error_id = uniqid('error', true); ?>
<?php if (isset($row['file']) && is_file($row['file'])) :?>
<?php
if (isset($row['function']) && in_array($row['function'], ['include', 'include_once', 'require', 'require_once'], true)) {
echo esc($row['function'] . ' ' . static::cleanPath($row['file']));
echo esc($row['function'] . ' ' . clean_path($row['file']));
} else {
echo esc(static::cleanPath($row['file']) . ' : ' . $row['line']);
echo esc(clean_path($row['file']) . ' : ' . $row['line']);
}
?>
<?php else : ?>
@ -201,7 +201,7 @@ $error_id = uniqid('error', true); ?>
</tr>
<tr>
<td>HTTP Method</td>
<td><?= esc($request->getMethod(true)) ?></td>
<td><?= esc(strtoupper($request->getMethod())) ?></td>
</tr>
<tr>
<td>IP Address</td>
@ -316,7 +316,7 @@ $error_id = uniqid('error', true); ?>
<table>
<tr>
<td style="width: 15em">Response Status</td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReason()) ?></td>
<td><?= esc($response->getStatusCode() . ' - ' . $response->getReasonPhrase()) ?></td>
</tr>
</table>
@ -352,7 +352,7 @@ $error_id = uniqid('error', true); ?>
<ol>
<?php foreach ($files as $file) :?>
<li><?= esc(static::cleanPath($file)) ?></li>
<li><?= esc(clean_path($file)) ?></li>
<?php endforeach ?>
</ol>
</div>

2
builds
View File

@ -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;

View File

@ -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/**"
]

16
composer.lock generated
View File

@ -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",

9
env
View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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#'

View File

@ -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();

73
spark
View File

@ -1,6 +1,15 @@
#!/usr/bin/env php
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* 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());
}

View File

@ -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.

View File

@ -44,13 +44,13 @@
<?php endif; ?>
<nav class="flex items-center justify-between h-10 col-start-2 text-white bg-header">
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center flex-1 h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
'podcast' => esc($podcast->title),
]) ?>">
<?= icon('arrow-left', 'text-lg') ?>
<div class="flex items-center flex-1 min-w-0 gap-x-2">
<?= icon('arrow-left', 'text-lg flex-shrink-0') ?>
<div class="flex items-center min-w-0 gap-x-2">
<img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->cover->tiny_url ?>" alt="<?= esc($episode->podcast->title) ?>" loading="lazy" />
<div class="flex flex-col flex-1 overflow-hidden">
<div class="flex flex-col overflow-hidden">
<span class="text-sm font-semibold leading-none truncate"><?= esc($episode->podcast->title) ?></span>
<span class="text-xs"><?= lang('Podcast.followers', [
'numberOfFollowers' => $podcast->actor->followers_count,

View File

@ -1,6 +1,6 @@
<footer>
<?php if (can_user_interact()): ?>
<form action="<?= route_to('episode-comment-attempt-like', esc(interact_as_actor()->username), esc($comment->episode->slug), $comment->id) ?>" method="POST" class="flex items-center gap-x-4">
<form action="<?= route_to('episode-comment-attempt-like', esc($comment->episode->podcast->handle), esc($comment->episode->slug), $comment->id) ?>" method="POST" class="flex items-center gap-x-4">
<?= csrf_field() ?>
<button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang(
'Comment.likes',

View File

@ -1,6 +1,6 @@
<footer>
<?php if (can_user_interact()): ?>
<form action="<?= route_to('episode-comment-attempt-like', esc(interact_as_actor()->username), esc($reply->episode->slug), $reply->id) ?>" method="POST" class="flex items-center gap-x-4">
<form action="<?= route_to('episode-comment-attempt-like', esc($reply->episode->podcast->handle), esc($reply->episode->slug), $reply->id) ?>" method="POST" class="flex items-center gap-x-4">
<?= csrf_field() ?>
<button type="submit" name="action" class="inline-flex items-center hover:underline group" title="<?= lang(