\n\n",
"contributors": [
{
"login": "yassinedoghri",
"name": "Yassine Doghri",
- "avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/3/avatar.png",
- "profile": "https://github.com/yassinedoghri",
+ "avatar_url": "https://avatars.githubusercontent.com/u/11021441?v=4",
+ "profile": "https://yassinedoghri.com",
"contributions": [
"code",
"bug",
@@ -472,18 +470,6 @@
}
]
},
- {
- "login": "ghose",
- "name": "ghose (XoseM)",
- "avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/12617257/large/a201650da44fed28890b0e0d8477a663.jpg",
- "profile": "https://crowdin.com/profile/xosem",
- "contributions": [
- {
- "type": "translation",
- "url": "https://translate.castopod.org"
- }
- ]
- },
{
"login": "basen1982",
"name": "Andreas Olsson",
@@ -545,7 +531,7 @@
]
},
{
- "login": "ahmed.sabouni11",
+ "login": "ahmedsabouni",
"name": "Ahmed Sabouni",
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
"profile": "https://github.com/ahmedsabouni",
@@ -564,11 +550,25 @@
"contributions": ["code"]
},
{
- "login": "NeoluxConsulting",
+ "login": "Dwev",
"name": "Guy Martin",
- "avatar_url": "https://secure.gravatar.com/avatar/6e745565356330c1e29a85d52bffdaa1?s=80&d=identicon",
- "profile": "https://code.castopod.org/NeoluxConsulting",
+ "avatar_url": "https://avatars.githubusercontent.com/u/46626050?v=4",
+ "profile": "https://github.com/Dwev",
"contributions": ["bug", "code"]
+ },
+ {
+ "login": "prcutler",
+ "name": "Paul Cutler",
+ "avatar_url": "https://avatars.githubusercontent.com/u/67276?v=4",
+ "profile": "https://github.com/prcutler",
+ "contributions": ["doc", "question", "ideas"]
+ },
+ {
+ "login": "nateritter",
+ "name": "Nate Ritter",
+ "avatar_url": "https://avatars.githubusercontent.com/u/198798?v=4",
+ "profile": "https://github.com/nateritter",
+ "contributions": ["code"]
}
],
"commitConvention": "none"
diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index ea6f4a4a..cca42fb0 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -4,7 +4,7 @@
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
-FROM php:8.2-fpm
+FROM php:8.5-fpm
LABEL maintainer="Yassine Doghri "
@@ -12,7 +12,7 @@ LABEL maintainer="Yassine Doghri "
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install server requirements
-RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
+RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get update \
&& apt-get install --yes --no-install-recommends nodejs \
# gnupg to sign commits with gpg
@@ -47,11 +47,4 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& docker-php-ext-enable redis \
# mysqli for database access
&& docker-php-ext-install mysqli \
- && docker-php-ext-enable mysqli \
- # configure php
- && echo "file_uploads = On\n" \
- "memory_limit = 512M\n" \
- "upload_max_filesize = 500M\n" \
- "post_max_size = 512M\n" \
- "max_execution_time = 300\n" \
- > /usr/local/etc/php/conf.d/uploads.ini
+ && docker-php-ext-enable mysqli
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 3673a17b..f1c12034 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,7 +6,7 @@
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
"postCreateCommand": "composer install && pnpm install && pnpm run build:static && php spark migrate --all && php spark db:seed DevSeeder",
- "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0",
+ "postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0 --port ${APP_PORT:-8080}",
"postAttachCommand": "crontab .devcontainer/crontab && service cron reload",
"shutdownAction": "stopCompose",
"features": {
@@ -30,7 +30,17 @@
"spark": "php",
"env": "dotenv",
".rsync-filter": "diff"
- }
+ },
+ "json.schemas": [
+ {
+ "fileMatch": [
+ "plugins/**/manifest.json",
+ "tests/modules/Plugins/mocks/manifests/*.json",
+ "tests/modules/Plugins/mocks/plugins/**/manifest.json"
+ ],
+ "url": "/workspaces/castopod/modules/Plugins/Manifest/manifest.schema.json"
+ }
+ ]
},
"extensions": [
"astro-build.astro-vscode",
@@ -52,7 +62,8 @@
"stylelint.vscode-stylelint",
"unifiedjs.vscode-mdx",
"wayou.vscode-todo-highlight",
- "yzhang.markdown-all-in-one"
+ "yzhang.markdown-all-in-one",
+ "42Crunch.vscode-openapi"
]
}
}
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 5aa15231..665b78e9 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -1,20 +1,19 @@
-version: "3"
-
services:
app:
build:
context: .
dockerfile: Dockerfile
- ports:
- - 8080:8080
volumes:
- ../..:/workspaces:cached
+ - ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini
environment:
+ APP_PORT: ${APP_PORT:-8080} # used in devcontainer.json file
+ VITE_PORT: ${VITE_PORT:-5173} # used in ../vite.config.js file
CI_ENVIRONMENT: development
vite_environment: development
app_forceGlobalSecureRequests: 0 #false
- app_baseURL: http://localhost:8080/
- media_baseURL: http://localhost:8080/
+ app_baseURL: http://localhost:${APP_PORT:-8080}/
+ media_baseURL: http://localhost:${APP_PORT:-8080}/
admin_gateway: cp-admin
auth_gateway: cp-auth
analytics_salt: dev_analytics_salt
@@ -29,16 +28,10 @@ services:
email_SMTPHost: mailpit
email_SMTPUser: castopod
email_SMTPPass: castopod
- email_SMTPPort: 1025
+ email_SMTPPort: ${MAILPIT_SMTP_PORT:-1025}
depends_on:
- - redis
- mariadb
- redis:
- image: redis:alpine
- volumes:
- - redis:/data
-
mariadb:
image: mariadb:10.2
volumes:
@@ -69,8 +62,8 @@ services:
volumes:
- mailpit:/data
ports:
- - 8025:8025
- - 1025:1025
+ - ${MAILPIT_WEBUI_PORT:-8025}:8025
+ - ${MAILPIT_SMTP_PORT:-1025}:1025
environment:
MP_MAX_MESSAGES: 5000
MP_DATA_FILE: /data/mailpit.db
@@ -78,7 +71,6 @@ services:
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
- redis:
mariadb:
phpmyadmin:
mailpit:
diff --git a/.devcontainer/uploads.ini b/.devcontainer/uploads.ini
new file mode 100644
index 00000000..23b3c1cd
--- /dev/null
+++ b/.devcontainer/uploads.ini
@@ -0,0 +1,5 @@
+file_uploads = On
+memory_limit = 512M
+upload_max_filesize = 500M
+post_max_size = 512M
+max_execution_time = 300
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..1b773dc8
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,68 @@
+.env
+
+.git/
+node_modules/
+vendor/
+build/
+docs/
+scripts/
+tests/
+
+#-------------------------
+# Temporary Files
+#-------------------------
+writable/cache/*
+!writable/cache/index.html
+
+writable/logs/*
+!writable/logs/index.html
+
+writable/session/*
+!writable/session/index.html
+
+writable/temp/*
+!writable/temp/index.html
+
+writable/uploads/*
+!writable/uploads/index.html
+
+writable/debugbar/*
+!writable/debugbar/index.html
+
+# public folder
+public/*
+!public/media
+!public/.htaccess
+!public/favicon.ico
+!public/icon*
+!public/castopod-banner*
+!public/castopod-avatar*
+!public/index.php
+!public/robots.txt
+!public/.well-known
+!public/.well-known/GDPR.yml
+
+public/assets/*
+!public/assets/index.html
+
+# public media folder
+!public/media/podcasts
+!public/media/persons
+!public/media/site
+
+public/media/podcasts/*
+!public/media/podcasts/index.html
+
+public/media/persons/*
+!public/media/persons/index.html
+
+public/media/site/*
+!public/media/site/index.html
+
+# Generated files
+modules/Admin/Language/*/PersonsTaxonomy.php
+
+# Castopod bundle & packages
+castopod/
+castopod-*.zip
+castopod-*.tar.gz
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index cff4e85e..00000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "env": {
- "browser": true,
- "es2020": true
- },
- "parser": "@typescript-eslint/parser",
- "plugins": ["@typescript-eslint"],
- "extends": [
- "eslint:recommended",
- "plugin:@typescript-eslint/recommended",
- "plugin:prettier/recommended"
- ],
- "parserOptions": {
- "ecmaVersion": 11,
- "sourceType": "module"
- },
- "rules": {}
-}
diff --git a/.gitignore b/.gitignore
index 539bf182..b7ea048c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,7 +67,7 @@ writable/uploads/*
!writable/uploads/index.html
writable/debugbar/*
-!writable/debugbar/.gitkeep
+!writable/debugbar/index.html
php_errors.log
@@ -107,15 +107,15 @@ _modules/*
.idea/
*.iml
-# Netbeans
-nbproject/
-build/
-nbbuild/
-dist/
-nbdist/
-nbactions.xml
-nb-configuration.xml
-.nb-gradle/
+# NetBeans
+/nbproject/
+/build/
+/nbbuild/
+/dist/
+/nbdist/
+/nbactions.xml
+/nb-configuration.xml
+/.nb-gradle/
# Sublime Text
*.tmlanguage.cache
@@ -128,6 +128,7 @@ nb-configuration.xml
# Visual Studio Code
.vscode/
+.history/
tmp/
/results/
@@ -174,16 +175,13 @@ public/media/site/*
# Generated files
modules/Admin/Language/*/PersonsTaxonomy.php
-#-------------------------
-# Docker volumes
-#-------------------------
-
-mariadb
-phpmyadmin
-sessions
-data
-
# Castopod bundle & packages
castopod/
castopod-*.zip
castopod-*.tar.gz
+
+# Plugins
+plugins/*
+!plugins/.gitkeep
+writable/plugins.json
+writable/plugins-lock.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ee3acd7e..32076043 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: code.castopod.org:5050/adaures/castopod:ci
+image: code.castopod.org:5050/adaures/castopod:ci-php8.5
stages:
- prepare
@@ -23,6 +23,10 @@ php-dependencies:
expire_in: 30 mins
paths:
- vendor/
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
js-dependencies:
stage: prepare
@@ -39,6 +43,10 @@ js-dependencies:
expire_in: 30 mins
paths:
- node_modules/
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
lint-commit-msg:
stage: quality
@@ -48,11 +56,10 @@ lint-commit-msg:
- ./scripts/lint-commit-msg.sh
dependencies:
- js-dependencies
- only:
- - develop
- - main
- - beta
- - alpha
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - if: $CI_COMMIT_BRANCH =~ /^(develop|main|alpha|beta|next)$/
lint-php:
stage: quality
@@ -65,34 +72,46 @@ lint-php:
- vendor/bin/rector process --dry-run --ansi
dependencies:
- php-dependencies
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
lint-js:
stage: quality
script:
- - pnpm run prettier
+ - pnpm run format
- pnpm run typecheck
- pnpm run lint
- pnpm run lint:css
dependencies:
- js-dependencies
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
tests:
stage: quality
services:
- - mariadb:10.2
+ - mariadb:10.11
variables:
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
MYSQL_DATABASE: "test"
MYSQL_USER: "castopod"
MYSQL_PASSWORD: "castopod"
script:
- - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE"
+ - echo "SHOW DATABASES;" | mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip_ssl
# run phpunit without code coverage
# TODO: add code coverage
- vendor/bin/phpunit --no-coverage
dependencies:
- php-dependencies
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
bundle:
stage: bundle
@@ -113,13 +132,12 @@ bundle:
name: "castopod-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
paths:
- castopod
- only:
- variables:
- - $CI_PROJECT_NAMESPACE == "adaures"
- except:
- - main
- - beta
- - alpha
+ rules:
+ - if: $CI_PROJECT_NAMESPACE != "adaures"
+ when: never
+ - if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/ || $CI_COMMIT_TAG
+ when: never
+ - when: on_success
release:
stage: release
@@ -143,38 +161,38 @@ release:
artifacts:
paths:
- castopod
- - CP_VERSION.env
- only:
- - main
- - beta
- - alpha
+ rules:
+ - if: $CI_PROJECT_NAMESPACE != "adaures"
+ when: never
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/
website:
stage: deploy
trigger: adaures/castopod.org
- only:
- - main
- - beta
- - alpha
+ rules:
+ - if: $CI_PROJECT_NAMESPACE != "adaures"
+ when: never
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG
documentation:
stage: deploy
trigger:
include: docs/.gitlab-ci.yml
strategy: depend
+ rules:
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
+ when: never
+ - when: on_success
docker:
stage: build
trigger:
include: docker/production/.gitlab-ci.yml
strategy: depend
- variables:
- PARENT_PIPELINE_ID: $CI_PIPELINE_ID
- only:
- refs:
- - develop
- - main
- - beta
- - alpha
- variables:
- - $CI_PROJECT_NAMESPACE == "adaures"
+ rules:
+ - if: $CI_PROJECT_NAMESPACE != "adaures"
+ when: never
+ - if: $CI_COMMIT_BRANCH == "develop"
+ - if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG
diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md
index 7fec749a..c6041a61 100644
--- a/.gitlab/issue_templates/bug.md
+++ b/.gitlab/issue_templates/bug.md
@@ -6,7 +6,7 @@
1. [First step]
2. [Second step]
-3. [and so on...]
+3. [and so on…]
### Expected behavior
@@ -27,7 +27,7 @@ logs, and code as it's very hard to read otherwise.
- OS: [e.g. Ubuntu server]
- Browser: [e.g. chrome, safari]
- Web server: [eg. Apache]
-- [any other relevant context...]
+- [any other relevant context…]
### Possible fixes
diff --git a/.gitlab/issue_templates/feature-request.md b/.gitlab/issue_templates/feature-request.md
index 644a8fd4..0886d392 100644
--- a/.gitlab/issue_templates/feature-request.md
+++ b/.gitlab/issue_templates/feature-request.md
@@ -1,7 +1,7 @@
### Is your feature request related to a problem? Please describe
A clear and concise description of what the problem is. Ex. I'm always
-frustrated when [...]
+frustrated when […]
### Describe the solution you'd like
diff --git a/.prettierrc.json b/.prettierrc.json
index d567a64c..cae76d94 100644
--- a/.prettierrc.json
+++ b/.prettierrc.json
@@ -2,7 +2,7 @@
"trailingComma": "es5",
"overrides": [
{
- "files": "*.md",
+ "files": ["*.md", "*.mdx"],
"options": {
"proseWrap": "always"
}
diff --git a/.releaserc.json b/.releaserc.json
index af98b785..7a8b6584 100644
--- a/.releaserc.json
+++ b/.releaserc.json
@@ -8,11 +8,74 @@
{
"name": "beta",
"prerelease": true
+ },
+ {
+ "name": "next",
+ "prerelease": true
}
],
"plugins": [
- "@semantic-release/commit-analyzer",
- "@semantic-release/release-notes-generator",
+ [
+ "@semantic-release/commit-analyzer",
+ {
+ "preset": "conventionalcommits",
+ "releaseRules": [
+ {
+ "type": "docs",
+ "scope": "README",
+ "release": "patch"
+ },
+ {
+ "type": "refactor",
+ "scope": "core-*",
+ "release": "minor"
+ },
+ {
+ "type": "refactor",
+ "release": "patch"
+ }
+ ],
+ "parserOpts": {
+ "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
+ }
+ }
+ ],
+ [
+ "@semantic-release/release-notes-generator",
+ {
+ "preset": "conventionalcommits",
+ "parserOpts": {
+ "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES", "BREAKING"]
+ },
+ "presetConfig": {
+ "types": [
+ {
+ "type": "feat",
+ "section": "Features"
+ },
+ {
+ "type": "fix",
+ "section": "Bug Fixes"
+ },
+ {
+ "type": "chore",
+ "section": "Internal",
+ "hidden": false
+ },
+ {
+ "type": "refactor",
+ "section": "Internal",
+ "hidden": false
+ },
+ {
+ "type": "perf",
+ "section": "Internal",
+ "hidden": false
+ }
+ ]
+ }
+ }
+ ],
"@semantic-release/changelog",
[
"@semantic-release/exec",
@@ -30,7 +93,8 @@
"package.json",
"package-lock.json",
"CHANGELOG.md"
- ]
+ ],
+ "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
}
],
[
diff --git a/.rsync-filter b/.rsync-filter
index 576403e0..0fcb79ec 100644
--- a/.rsync-filter
+++ b/.rsync-filter
@@ -1,9 +1,10 @@
# rsync filter rules to copy required files for Castopod's bundle
-+ app/Resources/icons/***
-- app/Resources/**
++ resources/icons/***
++ resources/
+ app/***
+ modules/***
++ plugins/***
+ public/***
+ themes/***
+ vendor/***
diff --git a/.stylelintrc.json b/.stylelintrc.json
index b9a5630c..8ea025c1 100644
--- a/.stylelintrc.json
+++ b/.stylelintrc.json
@@ -10,10 +10,17 @@
"responsive",
"variants",
"screen",
- "layer"
+ "layer",
+ "config"
]
}
],
+ "at-rule-no-deprecated": [
+ true,
+ {
+ "ignoreAtRules": ["apply"]
+ }
+ ],
"function-no-unknown": [
true,
{
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c05874dd..29f4a170 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,185 @@
+## [2.0.0-next.3](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.2...v2.0.0-next.3) (2024-12-30)
+
+### Features
+
+- **api:** add Episode create and publish endpoints
+ ([a90cdfd](https://code.castopod.org/adaures/castopod/commit/a90cdfdcdbde7a8fb520c6815d7b757947aea055))
+- **image:** add image size's width and height
+ ([f50098e](https://code.castopod.org/adaures/castopod/commit/f50098ec8926c8ae40718f5f128b6de7fe721b46))
+- **plugins:** add defaultValue for all field types
+ ([d3a98db](https://code.castopod.org/adaures/castopod/commit/d3a98db6d0112b5f59daddd2708c09dd2e595332))
+- **plugins:** add group field type + multiple option to render field arrays
+ ([11ccd0e](https://code.castopod.org/adaures/castopod/commit/11ccd0ebe71d476d8c0dbfe28edcf01f7f362b83))
+- **plugins:** add html field type + CodeEditor component + rework html head
+ generation
+ ([8cf9c6d](https://code.castopod.org/adaures/castopod/commit/8cf9c6dc833aedcccbc4cdb309b111f84d97d629))
+- **rss:** add option for 301 redirect to new feed url
+ ([8402cc2](https://code.castopod.org/adaures/castopod/commit/8402cc29d2d0c61b014a7e03e5ccce7d3c11782a))
+
+### Bug Fixes
+
+- add downloads_count to episodes table, computed every hour
+ ([f981937](https://code.castopod.org/adaures/castopod/commit/f9819376455c371eb5bd3c84ad938698335a3d67))
+- allow passing json to app.proxyIPs config to set it
+ ([cbf739e](https://code.castopod.org/adaures/castopod/commit/cbf739e95cc0ad6e83a21353b8f4678e68d74f63))
+- **api:** cast integers when creating episode
+ ([775b302](https://code.castopod.org/adaures/castopod/commit/775b302f7c886e30e133c8a8c68764301b6c663b))
+- **docker-image:** clear cache to account for new assets and data structure
+ changes
+ ([63c763f](https://code.castopod.org/adaures/castopod/commit/63c763f941195b3758c4b91acd8c350a5e7bb9c2)),
+ closes [#510](https://code.castopod.org/adaures/castopod/issues/510)
+- edit remap functions to get episode in episode admin controllers
+ ([9f74cca](https://code.castopod.org/adaures/castopod/commit/9f74cca342fedd896977efd2e89d0143959f3c4f))
+- **episode:** do not change slug when editing episode title
+ ([a83afb0](https://code.castopod.org/adaures/castopod/commit/a83afb0004511db80337806577fbc36f8d777116)),
+ closes [#513](https://code.castopod.org/adaures/castopod/issues/513)
+- **fediverse:** add "processing" and "failed" statuses to better manage
+ broadcast load
+ ([1d7583d](https://code.castopod.org/adaures/castopod/commit/1d7583d738219574ae3d45d294dc94e7e406472b)),
+ closes [#511](https://code.castopod.org/adaures/castopod/issues/511)
+- **icons:** set correct names for lock and lock-unlock icons in premium banner
+ ([37ee6d3](https://code.castopod.org/adaures/castopod/commit/37ee6d35b4bb66ce23dc271fb846200d1be0e7f6))
+- **plugins:** clear cache after activating or deactivating plugin
+ ([08c7df2](https://code.castopod.org/adaures/castopod/commit/08c7df2a5d5be340490c78deeef823167eb1b2fc))
+- **plugins:** delete relevant cache when submitting settings
+ ([00bd4c0](https://code.castopod.org/adaures/castopod/commit/00bd4c02ee23b181d74e7731626bfec3b1ff4916))
+- **podcast-model:** always query podcast from database when clearing cache
+ ([d30c49c](https://code.castopod.org/adaures/castopod/commit/d30c49cdff380c15db4f1851631a255a5baffcbe))
+- **premium-podcasts:** update query to validate subscription
+ ([2b1bbf3](https://code.castopod.org/adaures/castopod/commit/2b1bbf34303ead927f433b5c7d5d888ca3799954))
+- **preview:** delete episode preview cache after editing episode
+ ([732d429](https://code.castopod.org/adaures/castopod/commit/732d42923d0d7a66ff1ebd5841458e4205060560)),
+ closes [#514](https://code.castopod.org/adaures/castopod/issues/514)
+- **release:** add conventional-changelog-conventionalcommits for CHANGELOG
+ generation
+ ([6934c8a](https://code.castopod.org/adaures/castopod/commit/6934c8aa8f0b7f9eea7c3f6f4089c56b2391d9a6))
+- **rss:** add subscription id to cache name to prevent premium feeds from
+ overlapping
+ ([74f9325](https://code.castopod.org/adaures/castopod/commit/74f9325946d03a0d4efce57045e41cc9454ff97c))
+- set user as www-data when running cron jobs in docker's supervisord config
+ ([65d74f1](https://code.castopod.org/adaures/castopod/commit/65d74f14e612be3757c9304518eee112705f5ff9))
+- typo in EpisodeController remap function to get episode
+ ([f288a75](https://code.castopod.org/adaures/castopod/commit/f288a750f580ab19b04a170cc76bf8769084e19d))
+- update select and multi-select options to value/label arrays
+ ([63f93f5](https://code.castopod.org/adaures/castopod/commit/63f93f585bec4a11022cc8c75deb34968cba2348))
+
+### Internal
+
+- **plugins:** create Field objects per field type in settings forms + handle
+ rendering in class
+ ([34be5bc](https://code.castopod.org/adaures/castopod/commit/34be5bccabb7531afdcc6ebaf1dd39e4dfbe0677))
+- remove fields from podcast and episode entities to be replaced with plugins
+ ([b869acb](https://code.castopod.org/adaures/castopod/commit/b869acb3a988a3616d883a41c25d9c8409bd5518))
+- rename controller methods for views and actions to be more consistent
+ ([85704bf](https://code.castopod.org/adaures/castopod/commit/85704bfbe03fe5e38ff5e76a0e1cf0e5f1275f57))
+- update CodeIgniter to v4.5.6
+ ([f295e9a](https://code.castopod.org/adaures/castopod/commit/f295e9aa4ca3129df24a22779f7c19bba7fac370))
+- update codigniter-icons to v1.0.1
+ ([fa6967e](https://code.castopod.org/adaures/castopod/commit/fa6967e65cef1705b19cbb205132c4c751507d53))
+- update js dependencies to latest
+ ([70c9797](https://code.castopod.org/adaures/castopod/commit/70c97971fcf5bbeee826578057ae0e3afbbbd8a8))
+
+# [2.0.0-next.2](https://code.castopod.org/adaures/castopod/compare/v2.0.0-next.1...v2.0.0-next.2) (2024-07-08)
+
+### Bug Fixes
+
+- **audio-player:** set player icons to default instead of missing Castopod's
+ ([0ba0a25](https://code.castopod.org/adaures/castopod/commit/0ba0a25b11bd67aeeb47a8179b72152dfd4a36da))
+- broken icon call in frontend default pages template
+ ([3228362](https://code.castopod.org/adaures/castopod/commit/322836254e86be7878e21438177ee8f73f03a2fa))
+- **manifest:** set repository url as required in docstring typings
+ ([a8c81b3](https://code.castopod.org/adaures/castopod/commit/a8c81b3fa19a28dbd608027c231dcac31eafb38f))
+- set correct icons parameters in map and funding links views
+ ([5d35524](https://code.castopod.org/adaures/castopod/commit/5d355248753be24e3cf324144ff076f2fc23be88)),
+ closes [#500](https://code.castopod.org/adaures/castopod/issues/500)
+
+### Features
+
+- **plugins:** add `minCastopodVersion` to denote incompatibility with previous
+ Castopod versions
+ ([fc9ea75](https://code.castopod.org/adaures/castopod/commit/fc9ea7597e454e5c7c7af043d29af7bbe119e342))
+- **plugins:** load and display LICENSE.md file if found in plugin's directory
+ ([fee7905](https://code.castopod.org/adaures/castopod/commit/fee7905935a9adf963b4485b437fe4d972c14b5f))
+
+# [2.0.0-next.1](https://code.castopod.org/adaures/castopod/compare/v1.11.0...v2.0.0-next.1) (6/19/2024)
+
+### Bug Fixes
+
+- add missing php-icons config file to bundle
+ ([56612f0](https://code.castopod.org/adaures/castopod/commit/56612f0c762aa2d98e3c8c77fba88ffdf6f46a44))
+- **docs:** add base to og image using env variable
+ ([fe67659](https://code.castopod.org/adaures/castopod/commit/fe676590f23a33bdbe8905d234760923c029e350))
+- **import:** rewrite download_file helper to output curl response directly to
+ file
+ ([eb7ad2f](https://code.castopod.org/adaures/castopod/commit/eb7ad2f7e1c0137f222f47e47062887de42c4824))
+- include app/Resources/icons folder to bundle
+ ([3fd5efc](https://code.castopod.org/adaures/castopod/commit/3fd5efc7956977acc19e53182f25b12813964a7d))
+- **platforms:** add platforms service + reduce memory consumption when
+ rendering platform cards
+ ([fe73e9f](https://code.castopod.org/adaures/castopod/commit/fe73e9fae9ea5d5ce946680aec194308bb2e620c))
+- set owner email visibility when editing podcast
+ ([fc4f982](https://code.castopod.org/adaures/castopod/commit/fc4f9825568cd4384c5b3cfe972accd146548807)),
+ closes [#473](https://code.castopod.org/adaures/castopod/issues/473)
+
+### Build System
+
+- release next major version as prerelease
+ ([8275226](https://code.castopod.org/adaures/castopod/commit/827522643e9f8a5ea9be05b4847dc637f0f43a13))
+
+### Features
+
+- add Plugins module with base files for plugins architecture
+ ([7253e13](https://code.castopod.org/adaures/castopod/commit/7253e13ac2118f6f165f54ea0cbcd63d51ab9205))
+- **plugins:** abstract settings form for general, podcast and episode types
+ ([b62b483](https://code.castopod.org/adaures/castopod/commit/b62b483ad9ff114a22a9ee52e1a1a2c9fa444d42))
+- **plugins:** activate / deactivate plugin using settings table
+ ([27d2a1b](https://code.castopod.org/adaures/castopod/commit/27d2a1b0ffba9454dd54cbb4251a2d179b09762a))
+- **plugins:** add aside with plugin metadata next to plugin's readme
+ ([dfb7888](https://code.castopod.org/adaures/castopod/commit/dfb7888aeb689b4066abc37084e08cd7f1d0f15d))
+- **plugins:** add before channel/item hooks to allow podcast/episode data edit
+ when generating rss
+ ([80d2c48](https://code.castopod.org/adaures/castopod/commit/80d2c48ee265cb32ed0d710c488292fcbc120044))
+- **plugins:** add json schema definition for plugin manifest
+ ([b5eddf3](https://code.castopod.org/adaures/castopod/commit/b5eddf351f6f6fa1c299fbac31cbd056ef232330))
+- **plugins:** add methods to easily retrieve general, podcast and episode
+ settings in hooks methods
+ ([3a900bb](https://code.castopod.org/adaures/castopod/commit/3a900bbab68b819cedf8943540d2ee0aeb6e8539))
+- **plugins:** add new field types + validate & cast user data before storing
+ settings
+ ([6f833fc](https://code.castopod.org/adaures/castopod/commit/6f833fc76a3aa6c6b87c27ad18a2fb90e537e21e))
+- **plugins:** add options to manifest for building forms and storing plugin
+ settings
+ ([3d8aedf](https://code.castopod.org/adaures/castopod/commit/3d8aedf9c34e6927b6d3b11445d5f0e669b347d7))
+- **plugins:** add settings page for podcast and episode if defined in the
+ plugin's manifest
+ ([89ac92f](https://code.castopod.org/adaures/castopod/commit/89ac92fb412a04231ce52fd6480c9ab893b19ef5))
+- **plugins:** add siteHead hook to add custom meta tags to public pages
+ ([e80a33b](https://code.castopod.org/adaures/castopod/commit/e80a33bf2ad4fe1b47037add7470a6c2770f4036))
+- **plugins:** display errors when plugin is invalid instead of crashing
+ ([8ec7909](https://code.castopod.org/adaures/castopod/commit/8ec79097bbdbcbce622518ef61c068f20e0ef74e))
+- **plugins:** handle empty states and long strings in UI
+ ([45ac2a4](https://code.castopod.org/adaures/castopod/commit/45ac2a4be96532b9456e6af1d26ba4ada3649303))
+- **plugins:** load and validate plugin manifest.json
+ ([1510e36](https://code.castopod.org/adaures/castopod/commit/1510e36c0acd2b254622ec230acd1d2461ee9bf3))
+- **plugins:** load plugins using file locator service
+ ([587938d](https://code.castopod.org/adaures/castopod/commit/587938d2bf307b823af143586b9ec9e9b44e8dc1))
+- **plugins:** load README.md file to view plugin's instructions in UI
+ ([e6bfdfc](https://code.castopod.org/adaures/castopod/commit/e6bfdfc3902705285701c13c8067fe0f538425c6))
+- **plugins:** register plugins using Plugin.php file instead of namespace +
+ simplify i18n structure
+ ([2035c39](https://code.castopod.org/adaures/castopod/commit/2035c39fd138a1fd408516bd1972ab6a02544c10))
+- **plugins:** uninstall plugins via CLI and admin UI
+ ([9a80de4](https://code.castopod.org/adaures/castopod/commit/9a80de40686bbf4288da21cc2a6dde8036580e47))
+- set owner email to hidden by default in podcast create form
+ ([7a6d9df](https://code.castopod.org/adaures/castopod/commit/7a6d9df6db8a6184b8250ced0475f3e741dde7f4))
+- support podcast:txt tag with verify use case
+ ([57e459e](https://code.castopod.org/adaures/castopod/commit/57e459e187ed048430f4137172e22396cd02bf81)),
+ closes [#468](https://code.castopod.org/adaures/castopod/issues/468)
+
+### BREAKING CHANGES
+
+- next major release including plugins architecture
+
# [1.11.0](https://code.castopod.org/adaures/castopod/compare/v1.10.5...v1.11.0) (4/17/2024)
### Bug Fixes
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 57da0daf..b1291ae9 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,128 +1,162 @@
-# Contributor Covenant Code of Conduct
+# Contributor Covenant 3.0 Code of Conduct
## Our Pledge
-We as members, contributors, and leaders pledge to make participation in our
-community a harassment-free experience for everyone, regardless of age, body
-size, visible or invisible disability, ethnicity, sex characteristics, gender
-identity and expression, level of experience, education, socio-economic status,
-nationality, personal appearance, race, religion, or sexual identity and
-orientation.
+We pledge to make our community welcoming, safe, and equitable for all.
-We pledge to act and interact in ways that contribute to an open, welcoming,
-diverse, inclusive, and healthy community.
+We are committed to fostering an environment that respects and promotes the
+dignity, rights, and contributions of all individuals, regardless of
+characteristics including race, ethnicity, caste, color, age, physical
+characteristics, neurodiversity, disability, sex or gender, gender identity or
+expression, sexual orientation, language, philosophy or religion, national or
+social origin, socio-economic position, level of education, or other status. The
+same privileges of participation are extended to everyone who participates in
+good faith and in accordance with this Covenant.
-## Our Standards
+## Encouraged Behaviors
-Examples of behavior that contributes to a positive environment for our
-community include:
+While acknowledging differences in social norms, we all strive to meet our
+community's expectations for positive behavior. We also understand that our
+words and actions may be interpreted differently than we intend based on
+culture, background, or native language.
-- Demonstrating empathy and kindness toward other people
-- Being respectful of differing opinions, viewpoints, and experiences
-- Giving and gracefully accepting constructive feedback
-- Accepting responsibility and apologizing to those affected by our mistakes,
- and learning from the experience
-- Focusing on what is best not just for us as individuals, but for the overall
- community
+With these considerations in mind, we agree to behave mindfully toward each
+other and act in ways that center our shared values, including:
-Examples of unacceptable behavior include:
+1. Respecting the **purpose of our community**, our activities, and our ways of
+ gathering.
+2. Engaging **kindly and honestly** with others.
+3. Respecting **different viewpoints** and experiences.
+4. **Taking responsibility** for our actions and contributions.
+5. Gracefully giving and accepting **constructive feedback**.
+6. Committing to **repairing harm** when it occurs.
+7. Behaving in other ways that promote and sustain the **well-being of our
+ community**.
-- The use of sexualized language or imagery, and sexual attention or advances of
- any kind
-- Trolling, insulting or derogatory comments, and personal or political attacks
-- Public or private harassment
-- Publishing others' private information, such as a physical or email address,
- without their explicit permission
-- Other conduct which could reasonably be considered inappropriate in a
- professional setting
+## Restricted Behaviors
-## Enforcement Responsibilities
+We agree to restrict the following behaviors in our community. Instances,
+threats, and promotion of these behaviors are violations of this Code of
+Conduct.
-Community leaders are responsible for clarifying and enforcing our standards of
-acceptable behavior and will take appropriate and fair corrective action in
-response to any behavior that they deem inappropriate, threatening, offensive,
-or harmful.
+1. **Harassment.** Violating explicitly expressed boundaries or engaging in
+ unnecessary personal attention after any clear request to stop.
+2. **Character attacks.** Making insulting, demeaning, or pejorative comments
+ directed at a community member or group of people.
+3. **Stereotyping or discrimination.** Characterizing anyone’s personality or
+ behavior on the basis of immutable identities or traits.
+4. **Sexualization.** Behaving in a way that would generally be considered
+ inappropriately intimate in the context or purpose of the community.
+5. **Violating confidentiality**. Sharing or acting on someone's personal or
+ private information without their permission.
+6. **Endangerment.** Causing, encouraging, or threatening violence or other harm
+ toward any person or group.
+7. Behaving in other ways that **threaten the well-being** of our community.
-Community leaders have the right and responsibility to remove, edit, or reject
-comments, commits, code, wiki edits, issues, and other contributions that are
-not aligned to this Code of Conduct, and will communicate reasons for moderation
-decisions when appropriate.
+### Other Restrictions
+
+1. **Misleading identity.** Impersonating someone else for any reason, or
+ pretending to be someone else to evade enforcement actions.
+2. **Failing to credit sources.** Not properly crediting the sources of content
+ you contribute.
+3. **Promotional materials**. Sharing marketing or other commercial content in a
+ way that is outside the norms of the community.
+4. **Irresponsible communication.** Failing to responsibly present content which
+ includes, links or describes any other restricted behaviors.
+
+## Reporting an Issue
+
+Tensions can occur between community members even when they are trying their
+best to collaborate. Not every conflict represents a code of conduct violation,
+and this Code of Conduct reinforces encouraged behaviors and norms that can help
+avoid conflicts and minimize harm.
+
+When an incident does occur, it is important to report it promptly. To report a
+possible violation, email us at [abuse@castopod.org](mailto:abuse@castopod.org).
+
+Community Moderators take reports of violations seriously and will make every
+effort to respond in a timely manner. They will investigate all reports of code
+of conduct violations, reviewing messages, logs, and recordings, or interviewing
+witnesses and other participants. Community Moderators will keep investigation
+and enforcement actions as transparent as possible while prioritizing safety and
+confidentiality. In order to honor these values, enforcement actions are carried
+out in private with the involved parties, but communicating to the whole
+community may be part of a mutually agreed upon resolution.
+
+## Addressing and Repairing Harm
+
+If an investigation by the Community Moderators finds that this Code of Conduct
+has been violated, the following enforcement ladder may be used to determine how
+best to repair harm, based on the incident's impact on the individuals involved
+and the community as a whole. Depending on the severity of a violation, lower
+rungs on the ladder may be skipped.
+
+1. Warning
+ 1. Event: A violation involving a single incident or series of incidents.
+ 2. Consequence: A private, written warning from the Community Moderators.
+ 3. Repair: Examples of repair include a private written apology,
+ acknowledgement of responsibility, and seeking clarification on
+ expectations.
+2. Temporarily Limited Activities
+ 1. Event: A repeated incidence of a violation that previously resulted in a
+ warning, or the first incidence of a more serious violation.
+ 2. Consequence: A private, written warning with a time-limited cooldown
+ period designed to underscore the seriousness of the situation and give
+ the community members involved time to process the incident. The cooldown
+ period may be limited to particular communication channels or interactions
+ with particular community members.
+ 3. Repair: Examples of repair may include making an apology, using the
+ cooldown period to reflect on actions and impact, and being thoughtful
+ about re-entering community spaces after the period is over.
+3. Temporary Suspension
+ 1. Event: A pattern of repeated violation which the Community Moderators have
+ tried to address with warnings, or a single serious violation.
+ 2. Consequence: A private written warning with conditions for return from
+ suspension. In general, temporary suspensions give the person being
+ suspended time to reflect upon their behavior and possible corrective
+ actions.
+ 3. Repair: Examples of repair include respecting the spirit of the
+ suspension, meeting the specified conditions for return, and being
+ thoughtful about how to reintegrate with the community when the suspension
+ is lifted.
+4. Permanent Ban
+ 1. Event: A pattern of repeated code of conduct violations that other steps
+ on the ladder have failed to resolve, or a violation so serious that the
+ Community Moderators determine there is no way to keep the community safe
+ with this person as a member.
+ 2. Consequence: Access to all community spaces, tools, and communication
+ channels is removed. In general, permanent bans should be rarely used,
+ should have strong reasoning behind them, and should only be resorted to
+ if working through other remedies has failed to change the behavior.
+ 3. Repair: There is no possible repair in cases of this severity.
+
+This enforcement ladder is intended as a guideline. It does not limit the
+ability of Community Managers to use their discretion and judgment, in keeping
+with the best interests of our community.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
-an individual is officially representing the community in public spaces.
-Examples of representing our community include using an official e-mail address,
-posting via an official social media account, or acting as an appointed
+an individual is officially representing the community in public or other
+spaces. Examples of representing our community include using an official email
+address, posting via an official social media account, or acting as an appointed
representative at an online or offline event.
-## Enforcement
-
-Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported to the community leaders responsible for enforcement at
-[abuse@castopod.org](mailto:abuse@castopod.org). All complaints will be reviewed
-and investigated promptly and fairly.
-
-All community leaders are obligated to respect the privacy and security of the
-reporter of any incident.
-
-## Enforcement Guidelines
-
-Community leaders will follow these Community Impact Guidelines in determining
-the consequences for any action they deem in violation of this Code of Conduct:
-
-### 1. Correction
-
-**Community Impact**: Use of inappropriate language or other behavior deemed
-unprofessional or unwelcome in the community.
-
-**Consequence**: A private, written warning from community leaders, providing
-clarity around the nature of the violation and an explanation of why the
-behavior was inappropriate. A public apology may be requested.
-
-### 2. Warning
-
-**Community Impact**: A violation through a single incident or series of
-actions.
-
-**Consequence**: A warning with consequences for continued behavior. No
-interaction with the people involved, including unsolicited interaction with
-those enforcing the Code of Conduct, for a specified period of time. This
-includes avoiding interactions in community spaces as well as external channels
-like social media. Violating these terms may lead to a temporary or permanent
-ban.
-
-### 3. Temporary Ban
-
-**Community Impact**: A serious violation of community standards, including
-sustained inappropriate behavior.
-
-**Consequence**: A temporary ban from any sort of interaction or public
-communication with the community for a specified period of time. No public or
-private interaction with the people involved, including unsolicited interaction
-with those enforcing the Code of Conduct, is allowed during this period.
-Violating these terms may lead to a permanent ban.
-
-### 4. Permanent Ban
-
-**Community Impact**: Demonstrating a pattern of violation of community
-standards, including sustained inappropriate behavior, harassment of an
-individual, or aggression toward or disparagement of classes of individuals.
-
-**Consequence**: A permanent ban from any sort of public interaction within the
-community.
-
## Attribution
-This Code of Conduct is adapted from the [Contributor Covenant][homepage],
-version 2.0, available at
-https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+This Code of Conduct is adapted from the Contributor Covenant, version 3.0,
+permanently available at
+[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
-Community Impact Guidelines were inspired by
-[Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
+Contributor Covenant is stewarded by the Organization for Ethical Source and
+licensed under CC BY-SA 4.0. To view a copy of this license, visit
+[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
-[homepage]: https://www.contributor-covenant.org
-
-For answers to common questions about this code of conduct, see the FAQ at
-https://www.contributor-covenant.org/faq. Translations are available at
-https://www.contributor-covenant.org/translations.
+For answers to common questions about Contributor Covenant, see the FAQ at
+[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
+Translations are provided at
+[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations).
+Additional enforcement and community guideline resources can be found at
+[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources).
+The enforcement ladder was inspired by the work of
+[Mozilla’s code of conduct team](https://github.com/mozilla/inclusion).
diff --git a/CONTRIBUTING-DEV.md b/CONTRIBUTING-DEV.md
index df75bf50..907d3ce1 100644
--- a/CONTRIBUTING-DEV.md
+++ b/CONTRIBUTING-DEV.md
@@ -5,7 +5,7 @@
Castopod is a web app based on the `php` framework
[CodeIgniter 4](https://codeigniter.com).
-We use [Docker](https://www.docker.com/) quickly setup a dev environment. A
+We use [Docker](https://www.docker.com/) to quickly setup a dev environment. A
`docker-compose.yml` and `Dockerfile` are included in the project's root folder
to help you kickstart your contribution.
@@ -16,9 +16,9 @@ to help you kickstart your contribution.
### 1. Pre-requisites
-0. Install [docker](https://docs.docker.com/get-docker).
+0. Install [Docker](https://docs.docker.com/get-docker).
-1. Clone Castopod project by running:
+1. Clone the Castopod repository by running:
```bash
git clone https://code.castopod.org/adaures/castopod.git
@@ -79,7 +79,7 @@ to help you kickstart your contribution.
> [CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html)
> for more info.
-3. (for docker desktop) Add the repository you've cloned to docker desktop's
+3. (for Docker desktop) Add the repository you've cloned to Docker desktop's
`Settings` > `Resources` > `File Sharing`
### 2. (recommended) Develop inside the app container with VSCode
@@ -96,7 +96,7 @@ required services will be loaded automagically! 🪄
> The VSCode window will reload inside the dev container. Expect several
> minutes during first load as it is building all necessary services.
- **Note**: The dev container will start by running Castopod's php server.
+ **Note**: The dev container will start by running Castopod's PHP server.
During development, you will have to start [Vite](https://vitejs.dev)'s dev
server for compiling the typescript code and styles:
@@ -105,7 +105,7 @@ required services will be loaded automagically! 🪄
pnpm run dev
```
- If there is any issue with the php server not running, you can restart them
+ If there is any issue with the PHP server not running, you can restart them
using the following commands:
```bash
@@ -146,12 +146,10 @@ To see your changes, go to:
- `http://localhost:8080/` for the Castopod website
- `http://localhost:8080/cp-admin` for the Castopod admin:
-
- email: **admin@castopod.local**
- password: **castopod**
- `http://localhost:8888/` for the phpmyadmin interface:
-
- username: **castopod**
- password: **castopod**
@@ -159,9 +157,9 @@ To see your changes, go to:
You do not wish to use the VSCode devcontainer? No problem!
-1. Start docker containers manually:
+1. Start the Docker containers manually:
- Go to project's root folder and run:
+ Go to the project's root folder and run:
```bash
# starts all services declared in docker-compose.yml file
@@ -225,14 +223,14 @@ You do not wish to use the VSCode devcontainer? No problem!
> For more info, check out the
> [Composer documentation](https://getcomposer.org/doc/).
-2. Install javascript dependencies with [pnpm](https://pnpm.io/)
+2. Install JavaScript dependencies with [pnpm](https://pnpm.io/)
```bash
pnpm install
```
> [!NOTE]
- > The javascript dependencies aren't included in the repository. Pnpm will
+ > The JavaScript dependencies aren't included in the repository. Pnpm will
> check the `package.json` and `pnpm-lock.yaml` files to download the
> packages with the right versions. The dependencies will live under the
> `node_module` folder. For more info, check out the
@@ -251,7 +249,7 @@ You do not wish to use the VSCode devcontainer? No problem!
> [!NOTE]
> The static assets generated live under the `public/assets` folder, it
- > includes javascript, styles, images, fonts, icons and svg files.
+ > includes JavaScript, styles, images, fonts, icons and svg files.
### Initialize and populate database
@@ -293,8 +291,7 @@ You do not wish to use the VSCode devcontainer? No problem!
php spark db:seed DevSuperadminSeeder
```
-3. (optionnal) Populate the database with test data:
-
+3. (optional) Populate the database with test data:
- Populate with fake podcast analytics:
```bash
@@ -315,13 +312,13 @@ You do not wish to use the VSCode devcontainer? No problem!
docker-compose logs --tail 50 --follow --timestamps app
```
-- Interact with redis server using included redis-cli command:
+- Interact with the Redis server using included redis-cli command:
```bash
docker exec -it castopod_redis redis-cli
```
-- Monitor the redis container:
+- Monitor the Redis container:
```bash
docker-compose logs --tail 50 --follow --timestamps redis
@@ -357,10 +354,37 @@ docker-compose down
docker-compose build app
```
-Check [docker](https://docs.docker.com/engine/reference/commandline/docker/) and
+Check [Docker](https://docs.docker.com/engine/reference/commandline/docker/) and
[docker-compose](https://docs.docker.com/compose/reference/) documentations for
more insights.
+### Updating Documentation
+
+Castopod's documentation is written in Markdown and uses the Astro Starlight
+framework. To update Castopod's documentation, including the Getting Started
+guide and User Guide:
+
+1. Change directories to the `docs` directory and install the dependencies:
+
+ ```bash
+ cd docs/
+ pnpm i
+ ```
+
+2. Start the documentation development server:
+
+ ```bash
+ pnpm run dev --host
+ ```
+
+3. The documentation development server runs on port 4321. In your browser visit
+ `http://localhost:4321/docs`. If the page displays a 404 Not Found error,
+ click on the Castopod logo in the upper left hand corner of the page and the
+ documentation should load.
+
+4. Edit the Markdown files with your documentation updates. The Astro Starlight
+ development server will automatically update each time you save a change.
+
## Known issues
### Allocation failed - JavaScript heap out of memory
@@ -399,7 +423,7 @@ You may use Linux user namespaces to fix this on your machine:
username:100000:65536
```
-3. Restart docker:
+3. Restart Docker:
```bash
sudo systemctl restart docker
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 6557e02b..a3468537 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -71,8 +71,8 @@ experience the problem? What would you expect to be the outcome? All these
details will help people to fix any potential bugs.
> [!NOTE]
-> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates)
-> have been created for this project. You may use them to help you follow those
+> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates) have
+> been created for this project. You may use them to help you follow those
> guidelines.
## Feature requests
@@ -98,8 +98,8 @@ accurate comments, etc.) and any other requirements (such as test coverage).
Adhering to the following process is the best way to get your work included in
the project:
-1. [Fork](https://docs.gitlab.com/ee/gitlab-basics/fork-project.html) the
- project, clone your fork, and configure the remotes:
+1. [Fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html)
+ the project, clone your fork, and configure the remotes:
```bash
# Clone your fork of the repo into the current directory
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index b9c083c2..97175ffd 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -18,9 +18,9 @@ Javascript dependencies can be found in the [package.json](./package.json) file.
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
- [RemixIcon](https://remixicon.com/)
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
-- [OPAWG/User agent list](https://github.com/opawg/user-agents)
+- [OPAWG/User agent list](https://github.com/opawg/user-agents-v2)
([by Open Podcast Analytics Working Group](https://github.com/opawg))
- ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
+ ([MIT license](https://github.com/opawg/user-agents-v2/blob/master/LICENSE))
- [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents)
([by Open Podcast Analytics Working Group](https://github.com/opawg))
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))
diff --git a/README.md b/README.md
index 49143e74..6460720c 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
-
@@ -143,7 +141,7 @@ Alternatively, you can follow us on social media platforms to get news about
Castopod:
- [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance)
-- [Twitter](https://twitter.com/castopod)
+- [Bluesky](https://bsky.app/profile/castopod.org)
- [LinkedIn](https://linkedin.com/company/castopod)
- [Facebook](https://www.facebook.com/castopod)
@@ -157,10 +155,10 @@ backers. If you'd like to help, please consider
-
+
-
+
diff --git a/app/Commands/EpisodesComputeDownloads.php b/app/Commands/EpisodesComputeDownloads.php
new file mode 100644
index 00000000..1bdc7b2d
--- /dev/null
+++ b/app/Commands/EpisodesComputeDownloads.php
@@ -0,0 +1,50 @@
+builder()
+ ->select('episodes.id as id, IFNULL(SUM(ape.hits),0) as downloads_count')
+ ->join('analytics_podcasts_by_episode ape', 'episodes.id=ape.episode_id', 'left')
+ ->groupBy('episodes.id');
+
+ $episodeModel2 = new EpisodeModel();
+ $episodeModel2->builder()
+ ->setQueryAsData($query)
+ ->onConstraint('id')
+ ->updateBatch();
+ }
+}
diff --git a/app/Common.php b/app/Common.php
index 6f720768..89981c0d 100644
--- a/app/Common.php
+++ b/app/Common.php
@@ -37,7 +37,7 @@ if (! function_exists('view')) {
$renderer = single_service('renderer', $path);
$saveData = config('View')
-->saveData;
+ ->saveData;
if (array_key_exists('saveData', $options)) {
$saveData = (bool) $options['saveData'];
diff --git a/app/Config/App.php b/app/Config/App.php
index 9dd39d53..404ff5a0 100644
--- a/app/Config/App.php
+++ b/app/Config/App.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
+use Override;
class App extends BaseConfig
{
@@ -192,9 +193,9 @@ class App extends BaseConfig
* '192.168.5.0/24' => 'X-Real-IP',
* ]
*
- * @var array
+ * @var array|string
*/
- public array $proxyIPs = [];
+ public $proxyIPs = [];
/**
* --------------------------------------------------------------------------
@@ -249,4 +250,37 @@ class App extends BaseConfig
public ?int $bandwidthLimit = null;
public ?string $legalNoticeURL = null;
+
+ /**
+ * AuthToken Config Constructor
+ */
+ public function __construct()
+ {
+ parent::__construct();
+
+ if (is_string($this->proxyIPs)) {
+ $array = json_decode($this->proxyIPs, true);
+ if (is_array($array)) {
+ $this->proxyIPs = $array;
+ }
+ }
+ }
+
+ /**
+ * Override parent initEnvValue() to allow for direct setting to array properties values from ENV
+ *
+ * In order to set array properties via ENV vars we need to set the property to a string value first.
+ *
+ * @param mixed $property
+ */
+ #[Override]
+ protected function initEnvValue(&$property, string $name, string $prefix, string $shortPrefix): void
+ {
+ // if attempting to set property from ENV, first set to empty string
+ if ($name === 'proxyIPs' && $this->getEnvValue($name, $prefix, $shortPrefix) !== null) {
+ $property = '';
+ }
+
+ parent::initEnvValue($property, $name, $prefix, $shortPrefix);
+ }
}
diff --git a/app/Config/Autoload.php b/app/Config/Autoload.php
index 45072e31..3fd8e175 100644
--- a/app/Config/Autoload.php
+++ b/app/Config/Autoload.php
@@ -16,8 +16,6 @@ use CodeIgniter\Config\AutoloadConfig;
*
* NOTE: If you use an identical key in $psr4 or $classmap, then
* the values in this file will overwrite the framework's values.
- *
- * @immutable
*/
class Autoload extends AutoloadConfig
{
@@ -50,6 +48,7 @@ class Autoload extends AutoloadConfig
'Modules\Media' => ROOTPATH . 'modules/Media/',
'Modules\MediaClipper' => ROOTPATH . 'modules/MediaClipper/',
'Modules\Platforms' => ROOTPATH . 'modules/Platforms/',
+ 'Modules\Plugins' => ROOTPATH . 'modules/Plugins/',
'Modules\PodcastImport' => ROOTPATH . 'modules/PodcastImport/',
'Modules\PremiumPodcasts' => ROOTPATH . 'modules/PremiumPodcasts/',
'Modules\Update' => ROOTPATH . 'modules/Update/',
@@ -57,7 +56,6 @@ class Autoload extends AutoloadConfig
'Themes' => ROOTPATH . 'themes',
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
- 'Vite' => APPPATH . 'Libraries/Vite/',
];
/**
@@ -95,7 +93,7 @@ class Autoload extends AutoloadConfig
*
* @var list
*/
- public $files = [APPPATH . 'Libraries/ViewComponents/Helpers/view_components_helper.php'];
+ public $files = [];
/**
* -------------------------------------------------------------------
@@ -108,5 +106,5 @@ class Autoload extends AutoloadConfig
*
* @var list
*/
- public $helpers = ['auth', 'setting', 'icons'];
+ public $helpers = ['auth', 'setting', 'plugins'];
}
diff --git a/app/Config/CURLRequest.php b/app/Config/CURLRequest.php
index 040800df..4dbb7afa 100644
--- a/app/Config/CURLRequest.php
+++ b/app/Config/CURLRequest.php
@@ -8,6 +8,19 @@ use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
+ /**
+ * --------------------------------------------------------------------------
+ * CURLRequest Share Connection Options
+ * --------------------------------------------------------------------------
+ *
+ * Share connection options between requests.
+ *
+ * @var list
+ *
+ * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
+ */
+ public array $shareConnectionOptions = [CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_DNS];
+
/**
* --------------------------------------------------------------------------
* CURLRequest Share Options
diff --git a/app/Config/Cache.php b/app/Config/Cache.php
index 20bd2f27..bbf812f9 100644
--- a/app/Config/Cache.php
+++ b/app/Config/Cache.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Config;
use CodeIgniter\Cache\CacheInterface;
+use CodeIgniter\Cache\Handlers\ApcuHandler;
use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler;
@@ -36,18 +37,6 @@ class Cache extends BaseConfig
*/
public string $backupHandler = 'dummy';
- /**
- * --------------------------------------------------------------------------
- * Cache Directory Path
- * --------------------------------------------------------------------------
- *
- * The path to where cache files should be stored, if using a file-based
- * system.
- *
- * @deprecated Use the driver-specific variant under $file
- */
- public string $storePath = WRITEPATH . 'cache/';
-
/**
* --------------------------------------------------------------------------
* Key Prefix
@@ -88,10 +77,11 @@ class Cache extends BaseConfig
* --------------------------------------------------------------------------
* File settings
* --------------------------------------------------------------------------
+ *
* Your file storage preferences can be specified below, if you are using
* the File driver.
*
- * @var array
+ * @var array{storePath?: string, mode?: int}
*/
public array $file = [
'storePath' => WRITEPATH . 'cache/',
@@ -102,12 +92,13 @@ class Cache extends BaseConfig
* -------------------------------------------------------------------------
* Memcached settings
* -------------------------------------------------------------------------
+ *
* Your Memcached servers can be specified below, if you are using
* the Memcached drivers.
*
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
*
- * @var array
+ * @var array{host?: string, port?: int, weight?: int, raw?: bool}
*/
public array $memcached = [
'host' => '127.0.0.1',
@@ -123,14 +114,24 @@ class Cache extends BaseConfig
* Your Redis server can be specified below, if you are using
* the Redis or Predis drivers.
*
- * @var array
+ * @var array{
+ * host?: string,
+ * password?: string|null,
+ * port?: int,
+ * timeout?: int,
+ * async?: bool,
+ * persistent?: bool,
+ * database?: int
+ * }
*/
public array $redis = [
- 'host' => '127.0.0.1',
- 'password' => null,
- 'port' => 6379,
- 'timeout' => 0,
- 'database' => 0,
+ 'host' => '127.0.0.1',
+ 'password' => null,
+ 'port' => 6379,
+ 'timeout' => 0,
+ 'async' => false, // specific to Predis and ignored by the native Redis extension
+ 'persistent' => false,
+ 'database' => 0,
];
/**
@@ -144,6 +145,7 @@ class Cache extends BaseConfig
* @var array>
*/
public array $validHandlers = [
+ 'apcu' => ApcuHandler::class,
'dummy' => DummyHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class,
@@ -170,4 +172,28 @@ class Cache extends BaseConfig
* @var bool|list
*/
public $cacheQueryString = false;
+
+ /**
+ * --------------------------------------------------------------------------
+ * Web Page Caching: Cache Status Codes
+ * --------------------------------------------------------------------------
+ *
+ * HTTP status codes that are allowed to be cached. Only responses with
+ * these status codes will be cached by the PageCache filter.
+ *
+ * Default: [] - Cache all status codes (backward compatible)
+ *
+ * Recommended: [200] - Only cache successful responses
+ *
+ * You can also use status codes like:
+ * [200, 404, 410] - Cache successful responses and specific error codes
+ * [200, 201, 202, 203, 204] - All 2xx successful responses
+ *
+ * WARNING: Using [] may cache temporary error pages (404, 500, etc).
+ * Consider restricting to [200] for production applications to avoid
+ * caching errors that should be temporary.
+ *
+ * @var list
+ */
+ public array $cacheStatusCodes = [];
}
diff --git a/app/Config/Constants.php b/app/Config/Constants.php
index 90eac0e4..e23c0e0d 100644
--- a/app/Config/Constants.php
+++ b/app/Config/Constants.php
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
-defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
+defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3');
/*
| --------------------------------------------------------------------
@@ -24,10 +24,23 @@ defined('CP_VERSION') || define('CP_VERSION', '1.11.0');
| classes should use.
|
| NOTE: changing this will require manually modifying the
- | existing namespaces of App\* namespaced-classes.
+ | existing namespaces of App* namespaced-classes.
*/
defined('APP_NAMESPACE') || define('APP_NAMESPACE', 'App');
+/*
+ | --------------------------------------------------------------------
+ | Plugins Path
+ | --------------------------------------------------------------------
+ |
+ | This defines the folder in which plugins will live.
+ */
+defined('PLUGINS_PATH') ||
+ define('PLUGINS_PATH', ROOTPATH . 'plugins' . DIRECTORY_SEPARATOR);
+
+defined('PLUGINS_KEY_PATTERN') ||
+ define('PLUGINS_KEY_PATTERN', '[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*');
+
/*
| --------------------------------------------------------------------------
| Composer Path
@@ -91,18 +104,3 @@ 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 6c08b13c..99fa0b0a 100644
--- a/app/Config/ContentSecurityPolicy.php
+++ b/app/Config/ContentSecurityPolicy.php
@@ -26,14 +26,24 @@ class ContentSecurityPolicy extends BaseConfig
*/
public ?string $reportURI = null;
+ /**
+ * Specifies a reporting endpoint to which violation reports ought to be sent.
+ */
+ public ?string $reportTo = null;
+
/**
* Instructs user agents to rewrite URL schemes, changing HTTP to HTTPS. This directive is for websites with large
* numbers of old URLs that need to be rewritten.
*/
public bool $upgradeInsecureRequests = false;
+ // -------------------------------------------------------------------------
+ // CSP DIRECTIVES SETTINGS
+ // NOTE: once you set a policy to 'none', it cannot be further restricted
+ // -------------------------------------------------------------------------
+
/**
- * Will default to self if not overridden
+ * Will default to `'self'` if not overridden
*
* @var list|string|null
*/
@@ -46,6 +56,21 @@ class ContentSecurityPolicy extends BaseConfig
*/
public string | array $scriptSrc = 'self';
+ /**
+ * Specifies valid sources for JavaScript
+ HTML);
+
+ if ($this->title) {
+ $this->tag('title', esc($this->title));
+ }
+
+ if (url_is(route_to('admin') . '*') || url_is(base_url(config('Auth')->gateway) . '*')) {
+ // restricted admin and auth areas, do not index
+ $this->meta('robots', 'noindex');
+ } else {
+ // public website, set siteHead hook only there
+ service('plugins')
+ ->siteHead($this);
+ }
+
+ $head = '';
+ foreach ($this->tags as $tag) {
+ if ($tag['value'] === null) {
+ $head .= <<stringify_attributes($tag['attributes'])}/>
+ HTML;
+ } else {
+ $head .= <<stringify_attributes($tag['attributes'])}>{$tag['value']}{$tag['name']}>
+ HTML;
+ }
+ }
+
+ $head .= $this->rawContent . '';
+
+ // reset head for next render
+ $this->title = null;
+ $this->tags = [];
+ $this->rawContent = '';
+
+ return $head;
+ }
+
+ public function title(string $title): self
+ {
+ $this->title = $title;
+ return $this->meta('title', $title)
+ ->og('title', $title)
+ ->twitter('title', $title);
+ }
+
+ public function description(string $desc): self
+ {
+ return $this->meta('description', $desc)
+ ->og('description', $desc)
+ ->twitter('description', $desc);
+ }
+
+ public function image(string $url, string $card = 'summary_large_image'): self
+ {
+ return $this->og('image', $url)
+ ->twitter('card', $card)
+ ->twitter('image', $url);
+ }
+
+ public function canonical(string $url): self
+ {
+ return $this->tag('link', null, [
+ 'rel' => 'canonical',
+ 'href' => $url,
+ ]);
+ }
+
+ public function twitter(string $name, string $value): self
+ {
+ $this->meta("twitter:{$name}", $value);
+ return $this;
+ }
+
+ /**
+ * @param array $attributes
+ */
+ public function tag(string $name, ?string $value = null, array $attributes = []): self
+ {
+ $this->tags[] = [
+ 'name' => $name,
+ 'value' => $value,
+ 'attributes' => $attributes,
+ ];
+
+ return $this;
+ }
+
+ public function meta(string $name, string $content): self
+ {
+ $this->tag('meta', null, [
+ 'name' => $name,
+ 'content' => $content,
+ ]);
+
+ return $this;
+ }
+
+ public function og(string $name, string $content): self
+ {
+ $this->meta('og:' . $name, $content);
+
+ return $this;
+ }
+
+ public function appendRawContent(string $content): self
+ {
+ $this->rawContent .= $content;
+
+ return $this;
+ }
+
+ /**
+ * @param array $attributes
+ */
+ private function stringify_attributes(array $attributes): string
+ {
+ return stringify_attributes($attributes);
+ }
+}
diff --git a/app/Libraries/Router.php b/app/Libraries/Router.php
index 1f3abc55..5ce2c227 100644
--- a/app/Libraries/Router.php
+++ b/app/Libraries/Router.php
@@ -15,10 +15,11 @@ declare(strict_types=1);
namespace App\Libraries;
use CodeIgniter\Exceptions\PageNotFoundException;
-use CodeIgniter\Router\Exceptions\RedirectException;
+use CodeIgniter\HTTP\Exceptions\RedirectException;
use CodeIgniter\Router\Exceptions\RouterException;
use CodeIgniter\Router\Router as CodeIgniterRouter;
-use Config\Services;
+use Config\Routing;
+use Override;
class Router extends CodeIgniterRouter
{
@@ -30,6 +31,7 @@ class Router extends CodeIgniterRouter
*
* @return boolean Whether the route was matched or not.
*/
+ #[Override]
protected function checkRoutes(string $uri): bool
{
$routes = $this->collection->getRoutes($this->collection->getHTTPVerb());
@@ -43,7 +45,7 @@ class Router extends CodeIgniterRouter
// Loop through the route array looking for wildcards
foreach ($routes as $routeKey => $handler) {
- $routeKey = $routeKey === '/' ? $routeKey : ltrim($routeKey, '/ ');
+ $routeKey = $routeKey === '/' ? $routeKey : ltrim((string) $routeKey, '/ ');
$matchedKey = $routeKey;
@@ -67,7 +69,7 @@ class Router extends CodeIgniterRouter
throw new RedirectException(
preg_replace('#^' . $routeKey . '$#u', (string) $redirectTo, $uri),
- $this->collection->getRedirectCode($routeKey)
+ $this->collection->getRedirectCode($routeKey),
);
}
@@ -77,7 +79,7 @@ class Router extends CodeIgniterRouter
preg_match(
'#^' . str_replace('{locale}', '(?[^/]+)', $matchedKey) . '$#u',
$uri,
- $matched
+ $matched,
);
if ($this->collection->shouldUseSupportedLocalesOnly()
@@ -115,8 +117,8 @@ class Router extends CodeIgniterRouter
array_key_exists('alternate-content', $this->matchedRouteOptions) &&
is_array($this->matchedRouteOptions['alternate-content'])
) {
- $request = Services::request();
- $negotiate = Services::negotiator();
+ $request = service('request');
+ $negotiate = service('negotiator');
// Accept header is mandatory
if ($request->header('Accept') === null) {
@@ -180,24 +182,50 @@ class Router extends CodeIgniterRouter
return true;
}
- [$controller] = explode('::', (string) $handler);
+ if (str_contains((string) $handler, '::')) {
+ [$controller, $methodAndParams] = explode('::', (string) $handler);
+ } else {
+ $controller = $handler;
+ $methodAndParams = '';
+ }
// Checks `/` in controller name
- if (str_contains($controller, '/')) {
+ if (str_contains((string) $controller, '/')) {
throw RouterException::forInvalidControllerName($handler);
}
if (str_contains((string) $handler, '$') && str_contains($routeKey, '(')) {
// Checks dynamic controller
- if (str_contains($controller, '$')) {
+ if (str_contains((string) $controller, '$')) {
throw RouterException::forDynamicController($handler);
}
- // Using back-references
- $handler = preg_replace('#^' . $routeKey . '$#u', (string) $handler, $uri);
+ if (config(Routing::class)->multipleSegmentsOneParam === false) {
+ // Using back-references
+ $segments = explode(
+ '/',
+ (string) preg_replace('#\A' . $routeKey . '\z#u', (string) $handler, $uri),
+ );
+ } else {
+ if (str_contains($methodAndParams, '/')) {
+ [$method, $handlerParams] = explode('/', $methodAndParams, 2);
+ $params = explode('/', $handlerParams);
+ $handlerSegments = array_merge([$controller . '::' . $method], $params);
+ } else {
+ $handlerSegments = [$handler];
+ }
+
+ $segments = [];
+
+ foreach ($handlerSegments as $segment) {
+ $segments[] = $this->replaceBackReferences($segment, $matches);
+ }
+ }
+ } else {
+ $segments = explode('/', (string) $handler);
}
- $this->setRequest(explode('/', (string) $handler));
+ $this->setRequest($segments);
$this->setMatchedRoute($matchedKey, $handler);
diff --git a/app/Libraries/SimpleRSSElement.php b/app/Libraries/RssFeed.php
similarity index 55%
rename from app/Libraries/SimpleRSSElement.php
rename to app/Libraries/RssFeed.php
index 1dd2e431..631c80f1 100644
--- a/app/Libraries/SimpleRSSElement.php
+++ b/app/Libraries/RssFeed.php
@@ -11,10 +11,34 @@ declare(strict_types=1);
namespace App\Libraries;
use DOMDocument;
+use Override;
use SimpleXMLElement;
-class SimpleRSSElement extends SimpleXMLElement
+class RssFeed extends SimpleXMLElement
{
+ public const ATOM_NS = 'atom';
+
+ public const ATOM_NAMESPACE = 'http://www.w3.org/2005/Atom';
+
+ public const ITUNES_NS = 'itunes';
+
+ public const ITUNES_NAMESPACE = 'http://www.itunes.com/dtds/podcast-1.0.dtd';
+
+ public const PODCAST_NS = 'podcast';
+
+ public const PODCAST_NAMESPACE = 'https://podcastindex.org/namespace/1.0';
+
+ public function __construct(string $contents = '')
+ {
+ parent::__construct(sprintf(
+ "%s",
+ $this::ATOM_NAMESPACE,
+ $this::ITUNES_NAMESPACE,
+ $this::PODCAST_NAMESPACE,
+ $contents,
+ ));
+ }
+
/**
* Adds a child with $value inside CDATA
*
@@ -47,6 +71,7 @@ class SimpleRSSElement extends SimpleXMLElement
*
* @return static The addChild method returns a SimpleXMLElement object representing the child added to the XML node.
*/
+ #[Override]
public function addChild($name, $value = null, $namespace = null, $escape = true): static
{
$newChild = parent::addChild($name, null, $namespace);
@@ -57,12 +82,41 @@ class SimpleRSSElement extends SimpleXMLElement
return $newChild;
}
- if (is_array($value)) {
- return $newChild;
- }
-
$node->appendChild($no->createTextNode($value));
return $newChild;
}
+
+ /**
+ * Add RssFeed code into a RssFeed
+ *
+ * adapted from: https://stackoverflow.com/a/23527002
+ *
+ * @param self|array $nodes
+ */
+ public function appendNodes(self|array $nodes): void
+ {
+ if (! is_array($nodes)) {
+ $nodes = [$nodes];
+ }
+
+ foreach ($nodes as $element) {
+ $namespaces = $element->getNamespaces();
+ $namespace = array_first($namespaces) ?? null;
+
+ if (trim((string) $element) === '') {
+ $simpleRSS = $this->addChild($element->getName(), null, $namespace);
+ } else {
+ $simpleRSS = $this->addChild($element->getName(), (string) $element, $namespace);
+ }
+
+ foreach ($element->children() as $child) {
+ $simpleRSS->appendNodes($child);
+ }
+
+ foreach ($element->attributes() as $name => $value) {
+ $simpleRSS->addAttribute($name, (string) $value);
+ }
+ }
+ }
}
diff --git a/app/Libraries/ViewComponents/Component.php b/app/Libraries/ViewComponents/Component.php
index b86e4a85..eaf9744a 100644
--- a/app/Libraries/ViewComponents/Component.php
+++ b/app/Libraries/ViewComponents/Component.php
@@ -4,26 +4,32 @@ declare(strict_types=1);
namespace ViewComponents;
-class Component implements ComponentInterface
-{
- protected string $slot = '';
+use Override;
- protected string $class = '';
+abstract class Component implements ComponentInterface
+{
+ /**
+ * @var list
+ */
+ protected array $props = [];
+
+ /**
+ * @var array
+ */
+ protected array $casts = [];
+
+ protected ?string $slot = null;
/**
* @var array
*/
- protected array $attributes = [
- 'class' => '',
- ];
+ protected array $attributes = [];
/**
* @param array $attributes
*/
public function __construct(array $attributes)
{
- helper('viewcomponents');
-
// overwrite default attributes if set
$this->attributes = [...$this->attributes, ...$attributes];
@@ -42,11 +48,42 @@ class Component implements ComponentInterface
if (is_callable([$this, $method])) {
$this->{$method}($value);
} else {
+ if (array_key_exists($name, $this->casts)) {
+ $value = match ($this->casts[$name]) {
+ 'boolean' => $value === 'true',
+ 'number' => (int) $value,
+ 'array' => json_decode(htmlspecialchars_decode($value), true),
+ default => $value,
+ };
+ }
+
$this->{$name} = $value;
}
+
+ // remove from attributes
+ if (in_array($name, $this->props, true)) {
+ unset($this->attributes[$name]);
+ }
+ }
+
+ unset($this->attributes['slot']);
+ }
+
+ public function mergeClass(string $class): void
+ {
+ if (! array_key_exists('class', $this->attributes)) {
+ $this->attributes['class'] = $class;
+ } else {
+ $this->attributes['class'] .= ' ' . $class;
}
}
+ public function getStringifiedAttributes(): string
+ {
+ return stringify_attributes($this->attributes);
+ }
+
+ #[Override]
public function render(): string
{
return static::class . ': RENDER METHOD NOT IMPLEMENTED';
diff --git a/app/Libraries/ViewComponents/ComponentRenderer.php b/app/Libraries/ViewComponents/ComponentRenderer.php
index f9e916c0..e9a68172 100644
--- a/app/Libraries/ViewComponents/ComponentRenderer.php
+++ b/app/Libraries/ViewComponents/ComponentRenderer.php
@@ -43,38 +43,38 @@ class ComponentRenderer
private function renderSelfClosingTags(string $output): string
{
// Pattern borrowed and adapted from Laravel's ComponentTagCompiler
- // Should match any Component tags
+ // Should match any Component tags
$pattern = "/
<
- \s*
- (?[A-Z][A-Za-z0-9\.]*?)
- \s*
+ \\s*
+ x[-\\:](?[\\w\\-\\:\\.]*)
+ \\s*
(?
(?:
- \s+
+ \\s+
(?:
(?:
- \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\}
+ \\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\}
)
|
(?:
- [\w\-:.@]+
+ [\\w\\-:.@]+
(
=
(?:
\\\"[^\\\"]*\\\"
|
- \'[^\']*\'
+ \\'[^\\']*\\'
|
- [^\'\\\"=<>]+
+ [^\\'\\\"=<>]+
)
)?
)
)
)*
- \s*
+ \\s*
)
- \/>
+ \\/>
/x";
/*
@@ -96,8 +96,9 @@ class ComponentRenderer
private function renderPairedTags(string $output): string
{
- $pattern = '/<\s*(?[A-Z][A-Za-z0-9\.]*?)(?(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?.*)<\/\s*\1\s*>/uUsm';
- ini_set('pcre.backtrack_limit', '-1');
+ // ini_set('pcre.backtrack_limit', '-1');
+ $pattern = '/<\s*x[-\:](?[\w\-\:\.]*?)(?(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?.*)<\/\s*x-\1\s*>/uiUsm';
+
/*
$matches[0] = full tags matched and all of its content
$matches[name] = pascal cased tag name
@@ -167,8 +168,6 @@ class ComponentRenderer
(
\"[^\"]+\"
|
- \'[^\']+\'
- |
\\\'[^\\\']+\\\'
|
[^\s>]+
diff --git a/app/Libraries/ViewComponents/Config/Services.php b/app/Libraries/ViewComponents/Config/Services.php
index bb9c1d21..b7a1c74d 100644
--- a/app/Libraries/ViewComponents/Config/Services.php
+++ b/app/Libraries/ViewComponents/Config/Services.php
@@ -22,6 +22,7 @@ class Services extends BaseService
public static function components(bool $getShared = true): ComponentRenderer
{
if ($getShared) {
+ /** @phpstan-ignore return.type */
return self::getSharedInstance('components');
}
diff --git a/app/Libraries/ViewComponents/Decorator.php b/app/Libraries/ViewComponents/Decorator.php
index 4701052f..d8e7bfb6 100644
--- a/app/Libraries/ViewComponents/Decorator.php
+++ b/app/Libraries/ViewComponents/Decorator.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ViewComponents;
use CodeIgniter\View\ViewDecoratorInterface;
+use Override;
/**
* Enables rendering of View Components into the views.
@@ -15,6 +16,7 @@ class Decorator implements ViewDecoratorInterface
{
private static ?ComponentRenderer $components = null;
+ #[Override]
public static function decorate(string $html): string
{
$components = self::factory();
diff --git a/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php b/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php
deleted file mode 100644
index 9c7caf7d..00000000
--- a/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php
+++ /dev/null
@@ -1,33 +0,0 @@
- $val) {
- $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . $val . '"';
- }
-
- return rtrim($atts, ',');
- }
-}
diff --git a/app/Libraries/ViewThemes/Theme.php b/app/Libraries/ViewThemes/Theme.php
index 51d69ffe..b1b9b008 100644
--- a/app/Libraries/ViewThemes/Theme.php
+++ b/app/Libraries/ViewThemes/Theme.php
@@ -46,7 +46,7 @@ class Theme
/**
* Returns the path to the specified theme folder. If no theme is provided, will use the current theme.
*/
- public static function path(string $theme = null): string
+ public static function path(?string $theme = null): string
{
if ($theme === null) {
$theme = static::current();
diff --git a/app/Libraries/Vite/Config/Services.php b/app/Libraries/Vite/Config/Services.php
deleted file mode 100644
index cd5adfde..00000000
--- a/app/Libraries/Vite/Config/Services.php
+++ /dev/null
@@ -1,30 +0,0 @@
-|null
- */
- protected ?array $manifestData = null;
-
- /**
- * @var array|null
- */
- protected ?array $manifestCSSData = null;
-
- public function asset(string $path, string $type): string
- {
- if (config('Vite')->environment !== 'production') {
- return $this->loadDev($path, $type);
- }
-
- return $this->loadProd($path, $type);
- }
-
- private function loadDev(string $path, string $type): string
- {
- return $this->getHtmlTag(config('Vite') ->baseUrl . config('Vite')->assetsRoot . "/{$path}", $type);
- }
-
- private function loadProd(string $path, string $type): string
- {
- if ($this->manifestData === null) {
- $cacheName = 'vite-manifest';
- if (! ($cachedManifest = cache($cacheName))) {
- $manifestPath = config('Vite')
- ->assetsRoot . '/' . config('Vite')
- ->manifestFile;
- try {
- if (($manifestContents = file_get_contents($manifestPath)) !== false) {
- $cachedManifest = json_decode($manifestContents, true);
- cache()
- ->save($cacheName, $cachedManifest, DECADE);
- }
- } catch (ErrorException) {
- // ERROR when retrieving the manifest file
- die("Could not load manifest: {$manifestPath} file not found!");
- }
- }
-
- $this->manifestData = $cachedManifest;
- }
-
- $html = '';
- if (array_key_exists($path, $this->manifestData)) {
- $manifestElement = $this->manifestData[$path];
-
- // import css dependencies if any
- if (array_key_exists('css', $manifestElement)) {
- foreach ($manifestElement['css'] as $cssFile) {
- $html .= $this->getHtmlTag('/' . config('Vite')->assetsRoot . '/' . $cssFile, 'css');
- }
- }
-
- // import dependencies first for faster js loading
- if (array_key_exists('imports', $manifestElement)) {
- foreach ($manifestElement['imports'] as $importPath) {
- if (array_key_exists($importPath, $this->manifestData)) {
- // import css dependencies if any
- if (array_key_exists('css', $this->manifestData[$importPath])) {
- foreach ($this->manifestData[$importPath]['css'] as $cssFile) {
- $html .= $this->getHtmlTag(
- '/' . config('Vite')->assetsRoot . '/' . $cssFile,
- 'css'
- );
- }
- }
-
- $html .= $this->getHtmlTag(
- '/' . config('Vite')->assetsRoot . '/' . $this->manifestData[$importPath]['file'],
- 'js'
- );
- }
- }
- }
-
- $html .= $this->getHtmlTag('/' . config('Vite')->assetsRoot . '/' . $manifestElement['file'], $type);
- }
-
- return $html;
- }
-
- private function getHtmlTag(string $assetUrl, string $type): string
- {
- return match ($type) {
- 'css' => <<
- HTML
- ,
- 'js' => <<
- HTML
- ,
- default => '',
- };
- }
-}
diff --git a/app/Models/ActorModel.php b/app/Models/ActorModel.php
index b34993d9..59369100 100644
--- a/app/Models/ActorModel.php
+++ b/app/Models/ActorModel.php
@@ -12,14 +12,16 @@ namespace App\Models;
use App\Entities\Actor;
use Modules\Fediverse\Models\ActorModel as FediverseActorModel;
+use Override;
class ActorModel extends FediverseActorModel
{
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Actor::class;
+ #[Override]
public function getActorById(int $id): ?Actor
{
return $this->find($id);
diff --git a/app/Models/CategoryModel.php b/app/Models/CategoryModel.php
index 8847d2c1..2c67efeb 100644
--- a/app/Models/CategoryModel.php
+++ b/app/Models/CategoryModel.php
@@ -31,7 +31,7 @@ class CategoryModel extends Model
protected $allowedFields = ['parent_id', 'code', 'apple_category', 'google_category'];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Category::class;
@@ -65,12 +65,17 @@ class CategoryModel extends Model
$options = array_reduce(
$categories,
static function (array $result, Category $category): array {
- $result[$category->id] = '';
+ $label = '';
if ($category->parent instanceof Category) {
- $result[$category->id] = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
+ $label = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
}
- $result[$category->id] .= lang('Podcast.category_options.' . $category->code);
+ $label .= lang('Podcast.category_options.' . $category->code);
+
+ $result[] = [
+ 'value' => $category->id,
+ 'label' => $label,
+ ];
return $result;
},
[],
diff --git a/app/Models/ClipModel.php b/app/Models/ClipModel.php
index 16acbcfc..cc5209c9 100644
--- a/app/Models/ClipModel.php
+++ b/app/Models/ClipModel.php
@@ -67,8 +67,8 @@ class ClipModel extends Model
public function __construct(
protected string $type = 'audio',
- ConnectionInterface &$db = null,
- ValidationInterface $validation = null
+ ?ConnectionInterface &$db = null,
+ ?ValidationInterface $validation = null,
) {
switch ($type) {
case 'audio':
@@ -122,7 +122,6 @@ class ClipModel extends Model
$found[$key] = new VideoClip($videoClip->toArray());
}
- // @phpstan-ignore-next-line
return $found;
}
@@ -162,15 +161,11 @@ class ClipModel extends Model
return (int) $result[0]['id'];
}
- public function deleteVideoClip(int $podcastId, int $episodeId, int $clipId): BaseResult | bool
+ public function deleteVideoClip(int $clipId): BaseResult | bool
{
$this->clearVideoClipCache($clipId);
- return $this->delete([
- 'podcast_id' => $podcastId,
- 'episode_id' => $episodeId,
- 'id' => $clipId,
- ]);
+ return $this->delete($clipId);
}
public function getClipCount(int $podcastId, int $episodeId): int
@@ -240,11 +235,7 @@ class ClipModel extends Model
{
$this->clearSoundbiteCache($podcastId, $episodeId, $clipId);
- return $this->delete([
- 'podcast_id' => $podcastId,
- 'episode_id' => $episodeId,
- 'id' => $clipId,
- ]);
+ return $this->delete($clipId);
}
public function clearSoundbiteCache(int $podcastId, int $episodeId, int $clipId): void
diff --git a/app/Models/CreditModel.php b/app/Models/CreditModel.php
index 021b81d6..23342d4c 100644
--- a/app/Models/CreditModel.php
+++ b/app/Models/CreditModel.php
@@ -21,7 +21,7 @@ class CreditModel extends Model
protected $table = 'credits';
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Credit::class;
}
diff --git a/app/Models/EpisodeCommentModel.php b/app/Models/EpisodeCommentModel.php
index c999830b..60582586 100644
--- a/app/Models/EpisodeCommentModel.php
+++ b/app/Models/EpisodeCommentModel.php
@@ -25,7 +25,7 @@ use Modules\Fediverse\Objects\TombstoneObject;
class EpisodeCommentModel extends UuidModel
{
/**
- * @var string
+ * @var class-string
*/
protected $returnType = EpisodeComment::class;
@@ -86,11 +86,13 @@ class EpisodeCommentModel extends UuidModel
}
if ($comment->in_reply_to_id === null) {
- (new EpisodeModel())->builder()
+ new EpisodeModel()
+ ->builder()
->where('id', $comment->episode_id)
->increment('comments_count');
} else {
- (new self())->builder()
+ new self()
+ ->builder()
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
->increment('replies_count');
}
@@ -102,7 +104,7 @@ class EpisodeCommentModel extends UuidModel
'episode-comment',
esc($comment->actor->username),
$comment->episode->slug,
- $comment->id
+ $comment->id,
);
$createActivity = new CreateActivity();
@@ -180,7 +182,8 @@ class EpisodeCommentModel extends UuidModel
->where('id', $comment->episode_id)
->decrement('comments_count');
} else {
- (new self())->builder()
+ new self()
+ ->builder()
->where('id', service('uuid')->fromString($comment->in_reply_to_id)->getBytes())
->decrement('replies_count');
}
@@ -201,7 +204,7 @@ class EpisodeCommentModel extends UuidModel
{
// TODO: merge with replies from posts linked to episode linked
$episodeCommentsBuilder = $this->builder();
- $episodeComments = $episodeCommentsBuilder->select('*, 0 as is_from_post')
+ $episodeComments = $episodeCommentsBuilder->select('*, 0 as is_private, 0 as is_from_post')
->where([
'episode_id' => $episodeId,
'in_reply_to_id' => null,
@@ -211,7 +214,7 @@ class EpisodeCommentModel extends UuidModel
$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'
+ '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, is_private, 1 as is_from_post',
)
->whereIn('in_reply_to_id', static function (BaseBuilder $builder) use (&$episodeId): BaseBuilder {
return $builder->select('id')
@@ -221,19 +224,23 @@ class EpisodeCommentModel extends UuidModel
'in_reply_to_id' => null,
]);
})
- ->where('`created_at` <= UTC_TIMESTAMP()', null, false)
- ->getCompiledSelect();
+ ->where('`created_at` <= UTC_TIMESTAMP()', null, false);
+
+ // do not get private replies if public
+ if (! can_user_interact()) {
+ $episodePostsRepliesBuilder->where('is_private', false);
+ }
+
+ $episodePostsReplies = $episodePostsRepliesBuilder->getCompiledSelect();
/** @var BaseResult $allEpisodeComments */
$allEpisodeComments = $this->db->query(
- $episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
+ $episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC',
);
- // FIXME:?
- // @phpstan-ignore-next-line
return $this->convertUuidFieldsToStrings(
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
- $this->tempReturnType
+ $this->tempReturnType,
);
}
diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php
index 0a644cdc..c1ec3a02 100644
--- a/app/Models/EpisodeModel.php
+++ b/app/Models/EpisodeModel.php
@@ -87,8 +87,8 @@ class EpisodeModel extends UuidModel
'location_name',
'location_geo',
'location_osm',
- 'custom_rss',
'is_published_on_hubs',
+ 'downloads_count',
'posts_count',
'comments_count',
'is_premium',
@@ -98,7 +98,7 @@ class EpisodeModel extends UuidModel
];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Episode::class;
@@ -197,7 +197,7 @@ class EpisodeModel extends UuidModel
public function getEpisodeByPreviewId(string $previewId): ?Episode
{
- $cacheName = "podcast_episode#preview-{$previewId}";
+ $cacheName = "podcast_episode-preview#{$previewId}";
if (! ($found = cache($cacheName))) {
$builder = $this->where([
'preview_id' => $this->uuid->fromString($previewId)
@@ -235,8 +235,8 @@ class EpisodeModel extends UuidModel
public function getPodcastEpisodes(
int $podcastId,
string $podcastType,
- string $year = null,
- string $season = null
+ ?string $year = null,
+ ?string $season = null,
): array {
$cacheName = implode(
'_',
@@ -347,7 +347,7 @@ class EpisodeModel extends UuidModel
{
$result = $this->builder()
->select(
- 'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at'
+ 'COUNT(DISTINCT season_number) as number_of_seasons, COUNT(*) as number_of_episodes, MIN(published_at) as first_published_at',
)
->where('podcast_id', $podcastId)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
@@ -368,29 +368,32 @@ class EpisodeModel extends UuidModel
public function resetCommentsCount(): int | false
{
- $episodeCommentsCount = (new EpisodeCommentModel())->builder()
+ $episodeCommentsCount = new EpisodeCommentModel()
+ ->builder()
->select('episode_id, COUNT(*) as `comments_count`')
- ->where('in_reply_to_id', null)
+ ->where('in_reply_to_id')
->groupBy('episode_id')
->getCompiledSelect();
- $episodePostsRepliesCount = (new PostModel())->builder()
+ $episodePostsRepliesCount = new PostModel()
+ ->builder()
->select('fediverse_posts.episode_id as episode_id, COUNT(*) as `comments_count`')
->join('fediverse_posts as fp', 'fediverse_posts.id = fp.in_reply_to_id')
- ->where('fediverse_posts.in_reply_to_id', null)
- ->where('fediverse_posts.episode_id IS NOT', null)
+ ->where('fediverse_posts.in_reply_to_id')
+ ->where('fediverse_posts.episode_id IS NOT')
->groupBy('fediverse_posts.episode_id')
->getCompiledSelect();
/** @var BaseResult $query */
$query = $this->db->query(
- 'SELECT `episode_id` as `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `episode_id`'
+ 'SELECT `episode_id` as `id`, SUM(`comments_count`) as `comments_count` FROM (' . $episodeCommentsCount . ' UNION ALL ' . $episodePostsRepliesCount . ') x GROUP BY `episode_id`',
);
$countsPerEpisodeId = $query->getResultArray();
if ($countsPerEpisodeId !== []) {
- return (new self())->updateBatch($countsPerEpisodeId, 'id');
+ return new self()
+ ->updateBatch($countsPerEpisodeId, 'id');
}
return 0;
@@ -401,7 +404,7 @@ class EpisodeModel extends UuidModel
$episodePostsCount = $this->builder()
->select('episodes.id, COUNT(*) as `posts_count`')
->join('fediverse_posts', 'episodes.id = fediverse_posts.episode_id')
- ->where('in_reply_to_id', null)
+ ->where('in_reply_to_id')
->groupBy('episodes.id')
->get()
->getResultArray();
@@ -429,7 +432,8 @@ class EpisodeModel extends UuidModel
}
/** @var ?Episode $episode */
- $episode = (new self())->find($episodeId);
+ $episode = new self()
+ ->find($episodeId);
if (! $episode instanceof Episode) {
return $data;
@@ -441,7 +445,7 @@ class EpisodeModel extends UuidModel
cache()
->deleteMatching("podcast-{$episode->podcast->handle}*");
cache()
- ->delete("podcast_episode#{$episode->id}");
+ ->deleteMatching('podcast_episode*');
cache()
->deleteMatching("page_podcast#{$episode->podcast_id}*");
cache()
@@ -480,7 +484,7 @@ class EpisodeModel extends UuidModel
')
->select("{$podcastTable}.created_at AS podcast_created_at")
->select(
- "{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown"
+ "{$podcastTable}.title as podcast_title, {$podcastTable}.handle as podcast_handle, {$podcastTable}.description_markdown as podcast_description_markdown",
)
->join($podcastTable, "{$podcastTable} on {$podcastTable}.id = {$episodeTable}.podcast_id")
->where('
@@ -489,7 +493,7 @@ class EpisodeModel extends UuidModel
. 'OR' .
$podcastModel->getFullTextMatchClauseForPodcasts($podcastTable, $query)
. ')
- ');
+ ', );
return $this->builder;
}
@@ -523,7 +527,8 @@ class EpisodeModel extends UuidModel
}
/** @var ?Episode $episode */
- $episode = (new self())->find($episodeId);
+ $episode = new self()
+ ->find($episodeId);
if (! $episode instanceof Episode) {
return $data;
diff --git a/app/Models/LanguageModel.php b/app/Models/LanguageModel.php
index bec367f1..b68a9197 100644
--- a/app/Models/LanguageModel.php
+++ b/app/Models/LanguageModel.php
@@ -31,7 +31,7 @@ class LanguageModel extends Model
protected $allowedFields = ['code', 'native_name'];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Language::class;
@@ -56,7 +56,10 @@ class LanguageModel extends Model
$options = array_reduce(
$languages,
static function (array $result, Language $language): array {
- $result[$language->code] = $language->native_name;
+ $result[] = [
+ 'value' => $language->code,
+ 'label' => $language->native_name,
+ ];
return $result;
},
[],
diff --git a/app/Models/LikeModel.php b/app/Models/LikeModel.php
index 05a69881..f3f382de 100644
--- a/app/Models/LikeModel.php
+++ b/app/Models/LikeModel.php
@@ -36,7 +36,7 @@ class LikeModel extends UuidModel
protected $allowedFields = ['actor_id', 'comment_id'];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Like::class;
@@ -56,7 +56,8 @@ class LikeModel extends UuidModel
'comment_id' => $comment->id,
]);
- (new EpisodeCommentModel())->builder()
+ new EpisodeCommentModel()
+ ->builder()
->where('id', service('uuid')->fromString($comment->id)->getBytes())
->increment('likes_count');
@@ -91,7 +92,8 @@ class LikeModel extends UuidModel
{
$this->db->transStart();
- (new EpisodeCommentModel())->builder()
+ new EpisodeCommentModel()
+ ->builder()
->where('id', service('uuid') ->fromString($comment->id) ->getBytes())
->decrement('likes_count');
diff --git a/app/Models/PageModel.php b/app/Models/PageModel.php
index ce2e4fcc..f9a9eb5a 100644
--- a/app/Models/PageModel.php
+++ b/app/Models/PageModel.php
@@ -31,7 +31,7 @@ class PageModel extends Model
protected $allowedFields = ['id', 'title', 'slug', 'content_markdown', 'content_html'];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Page::class;
diff --git a/app/Models/PersonModel.php b/app/Models/PersonModel.php
index c997c8e8..705ad50a 100644
--- a/app/Models/PersonModel.php
+++ b/app/Models/PersonModel.php
@@ -39,7 +39,7 @@ class PersonModel extends Model
];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Person::class;
@@ -145,8 +145,11 @@ class PersonModel extends Model
$this->select('`id`, `full_name`')
->orderBy('`full_name`', 'ASC')
->findAll(),
- static function (array $result, $person): array {
- $result[$person->id] = $person->full_name;
+ static function (array $result, Person $person): array {
+ $result[] = [
+ 'value' => $person->id,
+ 'label' => $person->full_name,
+ ];
return $result;
},
[],
@@ -174,9 +177,10 @@ class PersonModel extends Model
if (! ($options = cache($cacheName))) {
foreach ($personsTaxonomy as $group_key => $group) {
foreach ($group['roles'] as $role_key => $role) {
- $options[
- "{$group_key},{$role_key}"
- ] = "{$group['label']} › {$role['label']}";
+ $options[] = [
+ 'value' => sprintf('%s,%s', $group_key, $role_key),
+ 'label' => sprintf('%s › %s', $group['label'], $role['label']),
+ ];
}
}
@@ -211,7 +215,7 @@ class PersonModel extends Model
if (! ($found = cache($cacheName))) {
$this->builder()
->select(
- 'persons.*, episodes_persons.podcast_id as podcast_id, episodes_persons.episode_id as episode_id'
+ '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')
@@ -253,7 +257,7 @@ class PersonModel extends Model
int $episodeId,
int $personId,
string $groupSlug,
- string $roleSlug
+ string $roleSlug,
): bool {
return $this->db->table('episodes_persons')
->insert([
@@ -293,9 +297,10 @@ class PersonModel extends Model
cache()
->delete("podcast#{$podcastId}_persons");
- (new PodcastModel())->clearCache([
- 'id' => $podcastId,
- ]);
+ new PodcastModel()
+ ->clearCache([
+ 'id' => $podcastId,
+ ]);
$data = [];
foreach ($personIds as $personId) {
@@ -335,9 +340,10 @@ class PersonModel extends Model
cache()->deleteMatching("podcast#{$podcastId}_person#{$personId}*");
cache()
->delete("podcast#{$podcastId}_persons");
- (new PodcastModel())->clearCache([
- 'id' => $podcastId,
- ]);
+ new PodcastModel()
+ ->clearCache([
+ 'id' => $podcastId,
+ ]);
return $this->db->table('podcasts_persons')
->delete([
@@ -359,9 +365,10 @@ class PersonModel extends Model
if ($personIds !== []) {
cache()
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
- (new EpisodeModel())->clearCache([
- 'id' => $episodeId,
- ]);
+ new EpisodeModel()
+ ->clearCache([
+ 'id' => $episodeId,
+ ]);
$data = [];
foreach ($personIds as $personId) {
@@ -400,9 +407,10 @@ class PersonModel extends Model
cache()->deleteMatching("podcast#{$podcastId}_episode#{$episodeId}_person#{$personId}*");
cache()
->delete("podcast#{$podcastId}_episode#{$episodeId}_persons");
- (new EpisodeModel())->clearCache([
- 'id' => $episodeId,
- ]);
+ new EpisodeModel()
+ ->clearCache([
+ 'id' => $episodeId,
+ ]);
return $this->db->table('episodes_persons')
->delete([
diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php
index 79f24e02..c64516f1 100644
--- a/app/Models/PodcastModel.php
+++ b/app/Models/PodcastModel.php
@@ -38,8 +38,6 @@ class PodcastModel extends Model
'handle',
'description_markdown',
'description_html',
- 'episode_description_footer_markdown',
- 'episode_description_footer_html',
'cover_id',
'banner_id',
'language_code',
@@ -47,10 +45,8 @@ class PodcastModel extends Model
'parental_advisory',
'owner_name',
'owner_email',
- 'is_owner_email_removed_from_feed',
'publisher',
'type',
- 'medium',
'copyright',
'imported_feed_url',
'new_feed_url',
@@ -60,13 +56,7 @@ class PodcastModel extends Model
'location_name',
'location_geo',
'location_osm',
- 'verify_txt',
- 'payment_pointer',
- 'custom_rss',
'is_published_on_hubs',
- 'partner_id',
- 'partner_link_url',
- 'partner_image_url',
'is_premium_by_default',
'published_at',
'created_by',
@@ -74,7 +64,7 @@ class PodcastModel extends Model
];
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Podcast::class;
@@ -173,7 +163,7 @@ class PodcastModel extends Model
/**
* @return Podcast[]
*/
- public function getAllPodcasts(string $orderBy = null): array
+ public function getAllPodcasts(?string $orderBy = null): array
{
$prefix = $this->db->getPrefix();
@@ -185,8 +175,8 @@ class PodcastModel extends Model
->where(
'`' . $prefix . 'fediverse_posts`.`published_at` <= UTC_TIMESTAMP()',
null,
- false
- )->orWhere('fediverse_posts.published_at', null)
+ false,
+ )->orWhere('fediverse_posts.published_at')
->groupEnd()
->groupBy('podcasts.actor_id')
->orderBy('max_published_at', 'DESC');
@@ -319,7 +309,8 @@ class PodcastModel extends Model
];
}
- $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode($podcastId);
+ $secondsToNextUnpublishedEpisode = new EpisodeModel()
+ ->getSecondsToNextUnpublishedEpisode($podcastId);
cache()
->save($cacheName, $defaultQuery, $secondsToNextUnpublishedEpisode ?: DECADE);
@@ -335,7 +326,8 @@ class PodcastModel extends Model
*/
public function clearCache(array $data): array
{
- $podcast = (new self())->getPodcastById((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
+ $podcast = new self()
+ ->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
// delete cache for users' podcasts
cache()
@@ -399,21 +391,22 @@ class PodcastModel extends Model
$domain =
$url->getHost() . ($url->getPort() ? ':' . $url->getPort() : '');
- $actorId = (new ActorModel())->insert(
- [
- 'uri' => url_to('podcast-activity', $username),
- 'username' => $username,
- 'domain' => $domain,
- 'private_key' => $privatekey,
- 'public_key' => $publickey,
- 'display_name' => $data['data']['title'],
- 'summary' => $data['data']['description_html'],
- 'inbox_url' => url_to('inbox', $username),
- 'outbox_url' => url_to('outbox', $username),
- 'followers_url' => url_to('followers', $username),
- ],
- true,
- );
+ $actorId = new ActorModel()
+ ->insert(
+ [
+ 'uri' => url_to('podcast-activity', $username),
+ 'username' => $username,
+ 'domain' => $domain,
+ 'private_key' => $privatekey,
+ 'public_key' => $publickey,
+ 'display_name' => $data['data']['title'],
+ 'summary' => $data['data']['description_html'],
+ 'inbox_url' => url_to('inbox', $username),
+ 'outbox_url' => url_to('outbox', $username),
+ 'followers_url' => url_to('followers', $username),
+ ],
+ true,
+ );
$data['data']['actor_id'] = $actorId;
@@ -427,10 +420,12 @@ class PodcastModel extends Model
*/
protected function setActorAvatar(array $data): array
{
- $podcast = (new self())->getPodcastById((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
+ $podcast = new self()
+ ->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
if ($podcast instanceof Podcast) {
- $podcastActor = (new ActorModel())->find($podcast->actor_id);
+ $podcastActor = new ActorModel()
+ ->find($podcast->actor_id);
if (! $podcastActor instanceof Actor) {
return $data;
@@ -439,7 +434,8 @@ class PodcastModel extends Model
$podcastActor->avatar_image_url = $podcast->cover->federation_url;
$podcastActor->avatar_image_mimetype = $podcast->cover->federation_mimetype;
- (new ActorModel())->update($podcast->actor_id, $podcastActor);
+ new ActorModel()
+ ->update($podcast->actor_id, $podcastActor);
}
return $data;
@@ -452,7 +448,8 @@ class PodcastModel extends Model
*/
protected function updatePodcastActor(array $data): array
{
- $podcast = (new self())->getPodcastById((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
+ $podcast = new self()
+ ->find((int) (is_array($data['id']) ? $data['id'][0] : $data['id']));
if ($podcast instanceof Podcast) {
$actorModel = new ActorModel();
@@ -488,7 +485,7 @@ class PodcastModel extends Model
{
if (! array_key_exists(
'guid',
- $data['data']
+ $data['data'],
) || $data['data']['guid'] === null || $data['data']['guid'] === '') {
$uuid = service('uuid');
$feedUrl = url_to('podcast-rss-feed', $data['data']['handle']);
diff --git a/app/Models/PostModel.php b/app/Models/PostModel.php
index 25a834a7..06b0b00a 100644
--- a/app/Models/PostModel.php
+++ b/app/Models/PostModel.php
@@ -16,7 +16,7 @@ use Modules\Fediverse\Models\PostModel as FediversePostModel;
class PostModel extends FediversePostModel
{
/**
- * @var string
+ * @var class-string
*/
protected $returnType = Post::class;
@@ -32,6 +32,7 @@ class PostModel extends FediversePostModel
'episode_id',
'message',
'message_html',
+ 'is_private',
'favourites_count',
'reblogs_count',
'replies_count',
@@ -49,7 +50,7 @@ class PostModel extends FediversePostModel
return $this->where([
'episode_id' => $episodeId,
])
- ->where('in_reply_to_id', null)
+ ->where('in_reply_to_id')
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->orderBy('published_at', 'DESC')
->findAll();
diff --git a/app/Resources/icons/funding/_index.php b/app/Resources/icons/funding/_index.php
deleted file mode 100644
index e4e34444..00000000
--- a/app/Resources/icons/funding/_index.php
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
diff --git a/app/Resources/images/castopod-logo.svg b/app/Resources/images/castopod-logo.svg
deleted file mode 100644
index 039deb74..00000000
--- a/app/Resources/images/castopod-logo.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/app/Resources/images/castopod-mascot_confused.svg b/app/Resources/images/castopod-mascot_confused.svg
deleted file mode 100644
index ab32c445..00000000
--- a/app/Resources/images/castopod-mascot_confused.svg
+++ /dev/null
@@ -1,40 +0,0 @@
-
diff --git a/app/Resources/js/admin.ts b/app/Resources/js/admin.ts
deleted file mode 100644
index c4fe7dea..00000000
--- a/app/Resources/js/admin.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import "@github/markdown-toolbar-element";
-import "@github/relative-time-element";
-import "./modules/audio-clipper";
-import ClientTimezone from "./modules/ClientTimezone";
-import Clipboard from "./modules/Clipboard";
-import DateTimePicker from "./modules/DateTimePicker";
-import Dropdown from "./modules/Dropdown";
-import HotKeys from "./modules/HotKeys";
-import "./modules/markdown-preview";
-import "./modules/markdown-write-preview";
-import MultiSelect from "./modules/MultiSelect";
-import "./modules/permalink-edit";
-import "./modules/play-soundbite";
-import PublishMessageWarning from "./modules/PublishMessageWarning";
-import Select from "./modules/Select";
-import SidebarToggler from "./modules/SidebarToggler";
-import Slugify from "./modules/Slugify";
-import ThemePicker from "./modules/ThemePicker";
-import Time from "./modules/Time";
-import Tooltip from "./modules/Tooltip";
-import ValidateFileSize from "./modules/ValidateFileSize";
-import "./modules/video-clip-previewer";
-import VideoClipBuilder from "./modules/VideoClipBuilder";
-import "./modules/xml-editor";
-
-Dropdown();
-Tooltip();
-Select();
-MultiSelect();
-Slugify();
-SidebarToggler();
-ClientTimezone();
-DateTimePicker();
-Time();
-Clipboard();
-ThemePicker();
-PublishMessageWarning();
-HotKeys();
-ValidateFileSize();
-VideoClipBuilder();
diff --git a/app/Resources/js/app.ts b/app/Resources/js/app.ts
deleted file mode 100644
index 7b944f47..00000000
--- a/app/Resources/js/app.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import Dropdown from "./modules/Dropdown";
-import Tooltip from "./modules/Tooltip";
-
-Dropdown();
-Tooltip();
diff --git a/app/Resources/js/charts.ts b/app/Resources/js/charts.ts
deleted file mode 100644
index 97b2c4d9..00000000
--- a/app/Resources/js/charts.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import DrawCharts from "./modules/Charts";
-
-DrawCharts();
diff --git a/app/Resources/js/install.ts b/app/Resources/js/install.ts
deleted file mode 100644
index e3bb9d53..00000000
--- a/app/Resources/js/install.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Tooltip from "./modules/Tooltip";
-
-Tooltip();
diff --git a/app/Resources/js/map.ts b/app/Resources/js/map.ts
deleted file mode 100644
index 195a97d0..00000000
--- a/app/Resources/js/map.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import DrawEpisodesMaps from "./modules/EpisodesMap";
-
-DrawEpisodesMaps();
diff --git a/app/Resources/js/modules/xml-editor.ts b/app/Resources/js/modules/xml-editor.ts
deleted file mode 100644
index 74ed5c2f..00000000
--- a/app/Resources/js/modules/xml-editor.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-import { indentWithTab } from "@codemirror/commands";
-import { xml } from "@codemirror/lang-xml";
-import {
- defaultHighlightStyle,
- syntaxHighlighting,
-} from "@codemirror/language";
-import { Compartment, EditorState } from "@codemirror/state";
-import { keymap } from "@codemirror/view";
-import { basicSetup, EditorView } from "codemirror";
-import { css, html, LitElement, TemplateResult } from "lit";
-import { customElement, queryAssignedNodes, state } from "lit/decorators.js";
-import prettifyXML from "xml-formatter";
-
-const language = new Compartment();
-
-@customElement("xml-editor")
-export class XMLEditor extends LitElement {
- @queryAssignedNodes({ slot: "textarea" })
- _textarea!: NodeListOf;
-
- @state()
- editorState!: EditorState;
-
- @state()
- editorView!: EditorView;
-
- firstUpdated(): void {
- const minHeightEditor = EditorView.theme({
- ".cm-content, .cm-gutter": {
- minHeight: this._textarea[0].clientHeight + "px",
- },
- });
-
- let editorContents = "";
- if (this._textarea[0].value) {
- try {
- editorContents = prettifyXML(this._textarea[0].value, {
- indentation: " ",
- });
- } catch (e) {
- // xml doesn't have a root node
- editorContents = prettifyXML(
- "" + this._textarea[0].value + "",
- {
- indentation: " ",
- }
- );
- // remove root, unnecessary lines and indents
- editorContents = editorContents
- .replace(/^/, "")
- .replace(/<\/root>$/, "")
- .replace(/^\s*[\r\n]/gm, "")
- .replace(/[\r\n] {2}/gm, "\r\n")
- .trim();
- }
- }
-
- this.editorState = EditorState.create({
- doc: editorContents,
- extensions: [
- basicSetup,
- keymap.of([indentWithTab]),
- language.of(xml()),
- minHeightEditor,
- syntaxHighlighting(defaultHighlightStyle),
- ],
- });
-
- this.editorView = new EditorView({
- state: this.editorState,
- root: this.shadowRoot as ShadowRoot,
- parent: this.shadowRoot as ShadowRoot,
- });
-
- this._textarea[0].hidden = true;
- if (this._textarea[0].form) {
- this._textarea[0].form.addEventListener("submit", () => {
- this._textarea[0].value = this.editorView.state.doc.toString();
- });
- }
- }
-
- disconnectedCallback(): void {
- if (this._textarea[0].form) {
- this._textarea[0].form.removeEventListener("submit", () => {
- this._textarea[0].value = this.editorView.state.doc.toString();
- });
- }
- }
-
- static styles = css`
- .cm-editor {
- border-radius: 0.5rem;
- overflow: hidden;
- border: 3px solid hsl(var(--color-border-contrast));
- background-color: hsl(var(--color-background-elevated));
- }
- .cm-editor.cm-focused {
- outline: 2px solid transparent;
- box-shadow:
- 0 0 0 2px hsl(var(--color-background-elevated)),
- 0 0 0 calc(4px) hsl(var(--color-accent-base));
- }
- .cm-gutters {
- background-color: hsl(var(--color-background-elevated)) !important;
- }
-
- .cm-activeLine {
- background-color: hsl(var(--color-background-highlight)) !important;
- }
-
- .cm-activeLineGutter {
- background-color: hsl(var(--color-background-highlight)) !important;
- }
-
- .ͼ4 .cm-line {
- caret-color: hsl(var(--color-text-base)) !important;
- }
-
- .ͼ1 .cm-cursor {
- border: none;
- }
- `;
-
- render(): TemplateResult<1> {
- return html``;
- }
-}
diff --git a/app/Resources/js/podcast.ts b/app/Resources/js/podcast.ts
deleted file mode 100644
index 1d53a99f..00000000
--- a/app/Resources/js/podcast.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import "@github/relative-time-element";
-import SidebarToggler from "./modules/SidebarToggler";
-import Time from "./modules/Time";
-import Toggler from "./modules/Toggler";
-import Tooltip from "./modules/Tooltip";
-
-Time();
-Toggler();
-Tooltip();
-SidebarToggler();
diff --git a/app/Resources/styles/index.css b/app/Resources/styles/index.css
deleted file mode 100644
index b894e7a3..00000000
--- a/app/Resources/styles/index.css
+++ /dev/null
@@ -1,15 +0,0 @@
-@import url("./tailwind.css");
-@import url("./custom.css");
-@import url("./fonts.css");
-@import url("./colors.css");
-@import url("./breadcrumb.css");
-@import url("./dropdown.css");
-@import url("./choices.css");
-@import url("./radioBtn.css");
-@import url("./colorRadioBtn.css");
-@import url("./switch.css");
-@import url("./radioToggler.css");
-@import url("./formInputTabs.css");
-@import url("./stickyHeader.css");
-@import url("./readMore.css");
-@import url("./seeMore.css");
diff --git a/app/Resources/styles/radioBtn.css b/app/Resources/styles/radioBtn.css
deleted file mode 100644
index 72834e3e..00000000
--- a/app/Resources/styles/radioBtn.css
+++ /dev/null
@@ -1,23 +0,0 @@
-@layer components {
- .form-radio-btn {
- @apply absolute mt-3 ml-3 border-contrast border-3 text-accent-base;
-
- &:focus {
- @apply ring-accent;
- }
-
- &:checked {
- @apply ring-2 ring-contrast;
-
- & + label {
- @apply text-accent-contrast bg-accent-base;
- }
- }
-
- & + label {
- @apply inline-flex items-center py-2 pl-8 pr-2 text-sm font-semibold rounded-lg cursor-pointer border-contrast bg-elevated border-3;
-
- color: hsl(var(--color-text-muted));
- }
- }
-}
diff --git a/app/Validation/FileRules.php b/app/Validation/FileRules.php
index 579ec3ca..2a149d46 100644
--- a/app/Validation/FileRules.php
+++ b/app/Validation/FileRules.php
@@ -11,13 +11,15 @@ declare(strict_types=1);
namespace App\Validation;
use CodeIgniter\Validation\FileRules as ValidationFileRules;
+use Override;
class FileRules extends ValidationFileRules
{
/**
* Checks an uploaded file to verify that the dimensions are within a specified allowable dimension.
*/
- public function min_dims(string $blank = null, string $params = ''): bool
+ #[Override]
+ public function min_dims(?string $blank = null, string $params = ''): bool
{
// Grab the file name off the top of the $params
// after we split it.
@@ -59,7 +61,7 @@ class FileRules extends ValidationFileRules
/**
* Checks an uploaded image to verify that the ratio corresponds to the params
*/
- public function is_image_ratio(string $blank = null, string $params = ''): bool
+ public function is_image_ratio(?string $blank = null, string $params = ''): bool
{
// Grab the file name off the top of the $params
// after we split it.
@@ -99,7 +101,7 @@ class FileRules extends ValidationFileRules
/**
* Checks that an uploaded json file's content is valid
*/
- public function is_json(string $blank = null, string $params = ''): bool
+ public function is_json(?string $blank = null, string $params = ''): bool
{
// Grab the file name off the top of the $params
// after we split it.
diff --git a/app/Validation/OtherRules.php b/app/Validation/OtherRules.php
new file mode 100644
index 00000000..74782809
--- /dev/null
+++ b/app/Validation/OtherRules.php
@@ -0,0 +1,29 @@
+ 'alert',
+ ];
/**
* @var 'default'|'success'|'danger'|'warning'
*/
protected string $variant = 'default';
+ #[Override]
public function render(): string
{
- $variants = [
+ $variantData = match ($this->variant) {
'success' => [
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
- 'glyph' => 'check-fill', // @icon('check-fill')
+ 'glyph' => 'check-fill', // @icon("check-fill")
],
'danger' => [
'class' => 'text-red-900 bg-red-100 border-red-300',
- 'glyph' => 'close-fill', // @icon('close-fill')
+ 'glyph' => 'close-fill', // @icon("close-fill")
],
'warning' => [
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
- 'glyph' => 'alert-fill', // @icon('alert-fill')
+ 'glyph' => 'alert-fill', // @icon("alert-fill")
],
- 'default' => [
+ default => [
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
- 'glyph' => 'error-warning-fill', // @icon('error-warning-fill')
+ 'glyph' => 'error-warning-fill', // @icon("error-warning-fill")
],
- ];
+ };
- if (! array_key_exists($this->variant, $variants)) {
- $this->variant = 'default';
- }
-
- $glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [
+ $glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [
'class' => 'flex-shrink-0 mr-2 text-lg',
]);
- $title = $this->title === null ? '' : '
diff --git a/docs/src/content/docs/ca/getting-started/update.mdx b/docs/src/content/docs/ca/getting-started/update.mdx
index d41ec18a..48a866e2 100644
--- a/docs/src/content/docs/ca/getting-started/update.mdx
+++ b/docs/src/content/docs/ca/getting-started/update.mdx
@@ -12,26 +12,22 @@ d'errors 🐛 i millores de rendiment ⚡.
0. ⚠️ Before any update, we highly recommend you backup your Castopod files and
database.
-
- cf.
[Should I make a backup before updating?](#should-i-make-a-backup-before-updating)
1. Go to the
[releases page](https://code.castopod.org/adaures/castopod/-/releases) and
see if your instance is up to date with the latest Castopod version
-
- cf.
[Where can I find my Castopod version?](#where-can-i-find-my-castopod-version)
2. Download the latest release package named `Castopod Package`, you may choose
between the `zip` or `tar.gz` archives
-
- ⚠️ Make sure you download the Castopod Package and **NOT** the Source Code
- Note that you can also download the latest package from
[castopod.org](https://castopod.org/)
3. On your server:
-
- Remove all files except `.env` and `public/media`
- Copy the new files from the downloaded package into your server
@@ -51,6 +47,7 @@ d'errors 🐛 i millores de rendiment ⚡.
5. Clear your cache from your `Castopod Admin` > `Settings` > `general` >
`Housekeeping`
+
6. ✨ Enjoy your fresh instance, you're all done!