Compare commits

..

No commits in common. "develop" and "alpha" have entirely different histories.

2997 changed files with 80308 additions and 200273 deletions

View file

@ -1,575 +0,0 @@
{
"projectName": "castopod",
"projectOwner": "adaures",
"repoType": "gitlab",
"repoHost": "https://code.castopod.org",
"files": ["README.md"],
"imageSize": 100,
"commit": false,
"contributors": [
{
"login": "yassinedoghri",
"name": "Yassine Doghri",
"avatar_url": "https://avatars.githubusercontent.com/u/11021441?v=4",
"profile": "https://yassinedoghri.com",
"contributions": [
"code",
"bug",
"doc",
"review",
"maintenance",
"content",
"design",
"a11y",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"mentoring",
"infra",
"ideas",
"projectManagement",
{
"type": "blog",
"url": "https://blog.castopod.org/author/yassinedoghri/"
}
]
},
{
"login": "benjamin",
"name": "Benjamin Bellamy",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/2/avatar.png",
"profile": "https://code.castopod.org/benjamin",
"contributions": [
"code",
"bug",
"review",
"content",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"infra",
"ideas",
{
"type": "blog",
"url": "https://blog.castopod.org/author/benjamin-bellamy/"
},
"projectManagement",
"talk"
]
},
{
"login": "ola",
"name": "Ola Hneini",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://github.com/ola-hn",
"contributions": [
"code",
"review",
"doc",
"maintenance",
"question",
"ideas"
]
},
{
"login": "rdelaage",
"name": "Romain de Laage",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://mamot.fr/@rdelaage",
"contributions": [
"code",
"infra",
"doc",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas"
]
},
{
"login": "Lyonel",
"name": "Lyonel Bernard",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://twitter.com/lyonelbernard",
"contributions": ["bug", "question", "audio", "ideas"]
},
{
"login": "ctlw83",
"name": "Christopher Lagonick-Weitzel",
"avatar_url": "https://secure.gravatar.com/avatar/7c2a721b52d0763673a600e8f01bd745?s=80&d=identicon",
"profile": "https://www.crypticchameleon.com/",
"contributions": ["bug", "question", "audio", "ideas"]
},
{
"login": "ernestoacostame",
"name": "Ernesto Acosta",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://ernestoacosta.me/",
"contributions": [
"bug",
"audio",
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"question",
"ideas"
]
},
{
"login": "3wen",
"name": "Ewen",
"avatar_url": "https://mastodon.fedi.bzh/system/accounts/avatars/000/000/002/original/6f387690a504ae46.jpg",
"profile": "https://mastodon.fedi.bzh/@ewen",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas",
"code"
]
},
{
"login": "Behel",
"name": "Bastien Luneteau",
"avatar_url": "https://secure.gravatar.com/avatar/ad63ee8ef8e3db8253d21e5012d2724f?s=80&d=identicon",
"profile": "https://code.castopod.org/Behel",
"contributions": ["code", "bug"]
},
{
"login": "cecillie",
"name": "Cécile Ricordeau",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://www.cecillie.fr/",
"contributions": ["design"]
},
{
"login": "PatrykMis",
"name": "Patryk Miś",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/PatrykMis",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "mspanc",
"name": "Marcin Lewandowski",
"avatar_url": "https://secure.gravatar.com/avatar/eed8337939641eac5ad0b570bd6acf96?s=80&d=identicon",
"profile": "https://code.castopod.org/mspanc",
"contributions": ["bug", "ideas"]
},
{
"login": "SJanik",
"name": "Sebastian Janik",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/SJanik",
"contributions": ["code"]
},
{
"login": "patryk",
"name": "Patryk Karczmarczyk",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/patryk",
"contributions": ["code"]
},
{
"login": "ddenis",
"name": "denis d",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/ddenis",
"contributions": ["bug", "ideas"]
},
{
"login": "douglaskastle",
"name": "Douglas Kastle",
"avatar_url": "https://secure.gravatar.com/avatar/b7e652ba4b6bcd440afa069e7f7bc9e6?s=80&d=identicon",
"profile": "https://code.castopod.org/douglaskastle",
"contributions": ["bug", "ideas"]
},
{
"login": "cExplorer",
"name": "cExplorer",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/cExplorer",
"contributions": [
"bug",
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "imacrea",
"name": "ImaCrea",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/imacrea",
"contributions": ["bug", "ideas"]
},
{
"login": "jonas",
"name": "Jonas S",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/jonas",
"contributions": ["code"]
},
{
"login": "yannL",
"name": "LEFEBVRE Yann",
"avatar_url": "https://secure.gravatar.com/avatar/9c46600ce566ec6d526370d8e104b1c8?s=80&d=identicon",
"profile": "https://code.castopod.org/yannL",
"contributions": ["bug"]
},
{
"login": "spaetz",
"name": "Sebastian Späth",
"avatar_url": "https://secure.gravatar.com/avatar/278e1af65e82993efd0ba7bbbacf6435?s=80&d=identicon",
"profile": "https://code.castopod.org/spaetz",
"contributions": ["bug", "ideas"]
},
{
"login": "rocky",
"name": "rocky III",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/rocky",
"contributions": ["bug"]
},
{
"login": "Regenpfeifer",
"name": "Hermann Josef Eckl",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/103/avatar.png",
"profile": "https://code.castopod.org/Regenpfeifer",
"contributions": ["bug"]
},
{
"login": "cyrilledel",
"name": "Delhaye Cyrille",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://code.castopod.org/cyrilledel",
"contributions": ["bug", "ideas"]
},
{
"login": "otetranome",
"name": "João Leandro",
"avatar_url": "https://code.castopod.org/uploads/-/system/user/avatar/113/avatar.png",
"profile": "https://twitter.com/otetranome",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
},
"ideas"
]
},
{
"login": "achouvardas",
"name": "Angelos Chouvardas",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://achouvardas.eu/",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "eivind",
"name": "Eivind",
"avatar_url": "https://mastodon.fjerland.no/system/accounts/avatars/107/769/768/295/192/222/original/e5c985fea6487dcb.jpg",
"profile": "https://mastodon.fjerland.no/@eivind",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "forght",
"name": "forght",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15073833/large/82d1e2e443a6df7edc43a7405dfeeb75_default.png",
"profile": "https://crowdin.com/profile/forght",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "glottis0q",
"name": "glottis0q",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15209934/large/8b17ef6a7399f0b82a8198f87c224195.png",
"profile": "https://crowdin.com/profile/glottis0q",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "BoFFire",
"name": "ButterflyOfFire",
"avatar_url": "https://static.mstdn.fr/static/accounts/avatars/000/065/901/original/5908e93ad5447f15.png",
"profile": "https://mstdn.fr/@ButterflyOfFire",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "lil5",
"name": "Lucian I. Last",
"avatar_url": "https://avatars.githubusercontent.com/u/17646836?v=4",
"profile": "https://github.com/lil5",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "LuuzViir",
"name": "LuuzViir",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13166188/large/d03ab0abc7ce354b210d836955cd3805_default.png",
"profile": "https://crowdin.com/profile/luuzviir",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "cthtc",
"name": "CTHTC",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15211502/large/ed0651060cb8474a9519b5168bd377c1_default.png",
"profile": "https://crowdin.com/profile/cthtc",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "retrograde",
"name": "Russian Retro",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15021651/large/b10c4057f85bf4de49c7fdf01354ecde.jpeg",
"profile": "https://crowdin.com/profile/retrograde",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "mareklach",
"name": "Marek L'ach",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13572324/large/3eeba8d569c247ace33862bf4ef4748f.jpeg",
"profile": "https://crowdin.com/profile/mareklach",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "GunChleoc",
"name": "GunChleoc",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/13043878/large/3223f7b606296a8b1c92c5de39c459a2_default.png",
"profile": "https://crowdin.com/profile/gunchleoc",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "GabiSnow",
"name": "GabiSnow",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15214858/large/5b083bdf9c9e9de67cc6ee72a6c8db18_default.png",
"profile": "https://crowdin.com/profile/gabisnow",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "bendaha",
"name": "bendaha",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15331656/large/cd92450d2c20202299fb3a0075903e20_default.png",
"profile": "https://crowdin.com/profile/bendaha",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "samuelroland",
"name": "Samuel Roland",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/14980053/large/3e154a37d03d6e98ae402ed3f930f4f5.png",
"profile": "https://crowdin.com/profile/samuelroland",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "dimregnier",
"name": "Dimitri Regnier",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://dimitriregnier.net/",
"contributions": ["ideas"]
},
{
"login": "irithys",
"name": "irithys",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15405614/large/3086461c47cce0a0c031925e5f943412.png",
"profile": "https://im.irithys.com/@thy",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "caos30",
"name": "Sergi",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://twitter.com/caos30",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "basen1982",
"name": "Andreas Olsson",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/basen1982",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "leonfrom",
"name": "leonfrom",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/leonfrom",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "agentcobra57",
"name": "agentcobra",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/agentcobra57",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "alephoto85",
"name": "Alessandro",
"avatar_url": "https://crowdin-static.downloads.crowdin.com/avatar/15094649/large/530391f54157af52ae33058ec15b0f99.jpg",
"profile": "https://crowdin.com/profile/alephoto85",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "liimee",
"name": "liimee",
"avatar_url": "https://castopod.org/assets/images/castopod-avatar.jpg",
"profile": "https://crowdin.com/profile/liimee",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "ahmedsabouni",
"name": "Ahmed Sabouni",
"avatar_url": "https://avatars.githubusercontent.com/u/74497842?v=4",
"profile": "https://github.com/ahmedsabouni",
"contributions": [
{
"type": "translation",
"url": "https://translate.castopod.org"
}
]
},
{
"login": "KrzysztofDomanczyk",
"name": "KrzysztofDomanczyk",
"avatar_url": "https://avatars.githubusercontent.com/u/75178474?v=4",
"profile": "https://github.com/KrzysztofDomanczyk",
"contributions": ["code"]
},
{
"login": "Dwev",
"name": "Guy Martin",
"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"
}

View file

@ -1,50 +0,0 @@
####################################################
# Castopod development Docker file
####################################################
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
FROM php:8.5-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install server requirements
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
gnupg \
openssh-client \
# cron for scheduled tasks
cron \
# unzip used by composer
unzip \
# required libraries to install php extensions using
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
libicu-dev \
libpng-dev \
libwebp-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
zlib1g-dev \
libzip-dev \
# ffmpeg for video encoding
ffmpeg \
# intl for Internationalization
&& docker-php-ext-install intl \
&& docker-php-ext-install zip \
# gd for image processing
&& docker-php-ext-configure gd --with-webp --with-jpeg --with-freetype \
&& docker-php-ext-install gd \
&& docker-php-ext-install exif \
&& docker-php-ext-enable exif \
# redis extension for cache
&& pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& docker-php-ext-enable redis \
# mysqli for database access
&& docker-php-ext-install mysqli \
&& docker-php-ext-enable mysqli

View file

@ -1 +0,0 @@
* * * * * /usr/local/bin/php /workspaces/castopod/spark tasks:run >> /dev/null 2>&1

View file

@ -1,70 +1,41 @@
// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/docker-existing-dockerfile // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/docker-existing-dockerfile
{ {
"name": "castopod.local", "name": "Castopod Host dev",
"dockerComposeFile": ["./docker-compose.yml"], "dockerComposeFile": ["../docker-compose.yml", "./docker-compose.yml"],
"service": "app", "service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "workspaceFolder": "/castopod-host",
"postCreateCommand": "composer install && pnpm install && pnpm run build:static && php spark migrate --all && php spark db:seed DevSeeder", "postCreateCommand": "composer install && npm install && npm run build:static",
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder} && crontab .devcontainer/crontab && cron && php spark serve --host 0.0.0.0 --port ${APP_PORT:-8080}", "postStartCommand": "crontab ./crontab && cron && php spark serve --host 0.0.0.0",
"postAttachCommand": "crontab .devcontainer/crontab && service cron reload", "postAttachCommand": "crontab ./crontab && service cron reload",
"shutdownAction": "stopCompose", "shutdownAction": "stopCompose",
"features": { "settings": {
"ghcr.io/devcontainers/features/git:1": {}, "terminal.integrated.defaultProfile.linux": "bash",
"ghcr.io/guiyomh/features/vim:0": {}, "editor.formatOnSave": true,
"ghcr.io/NicoVIII/devcontainer-features/pnpm:2": {} "[php]": {
}, "editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"customizations": { "editor.formatOnSave": false
"vscode": { },
"settings": { "color-highlight.markerType": "dot-before",
"editor.formatOnSave": true, "files.associations": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "*.xml.dist": "xml",
"[php]": { "spark": "php"
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.formatOnSave": false
},
"css.validate": false,
"color-highlight.markerType": "dot-before",
"files.associations": {
"*.xml.dist": "xml",
"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",
"bmewburn.vscode-intelephense-client",
"bradlc.vscode-tailwindcss",
"breezelin.phpstan",
"DavidAnson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"eamodio.gitlens",
"esbenp.prettier-vscode",
"heybourn.headwind",
"jamesbirtles.svelte-vscode",
"kasik96.latte",
"mikestead.dotenv",
"naumovs.color-highlight",
"pflannery.vscode-versionlens",
"runem.lit-plugin",
"streetsidesoftware.code-spell-checker",
"stylelint.vscode-stylelint",
"unifiedjs.vscode-mdx",
"wayou.vscode-todo-highlight",
"yzhang.markdown-all-in-one",
"42Crunch.vscode-openapi"
]
} }
} },
"extensions": [
"mikestead.dotenv",
"bmewburn.vscode-intelephense-client",
"streetsidesoftware.code-spell-checker",
"naumovs.color-highlight",
"heybourn.headwind",
"wayou.vscode-todo-highlight",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"jamesbirtles.svelte-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"eamodio.gitlens",
"breezelin.phpstan",
"kasik96.latte"
]
} }

View file

@ -1,76 +1,10 @@
version: "3"
services: services:
app: app:
build:
context: .
dockerfile: Dockerfile
volumes: volumes:
- ../..:/workspaces:cached # Mounts the project folder to '/workspace'. While this file is in .devcontainer,
- ./uploads.ini:/usr/local/etc/php/conf.d/uploads.ini # mounts are relative to the first file in the list, which is a level up.
environment: - .:/castopod-host:cached
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:${APP_PORT:-8080}/
media_baseURL: http://localhost:${APP_PORT:-8080}/
admin_gateway: cp-admin
auth_gateway: cp-auth
analytics_salt: dev_analytics_salt
database_default_hostname: mariadb
database_default_database: castopod
database_default_username: castopod
database_default_password: castopod
database_default_DBPrefix: cp_
restapi_enabled: 1 #true
email_fromEmail: hello@castopod.local
email_SMTPCrypto: ""
email_SMTPHost: mailpit
email_SMTPUser: castopod
email_SMTPPass: castopod
email_SMTPPort: ${MAILPIT_SMTP_PORT:-1025}
depends_on:
- mariadb
mariadb: # Overrides default command so things don't shut down after the process ends.
image: mariadb:10.2 command: /bin/sh -c "while sleep 1000; do :; done"
volumes:
- ./initdb:/docker-entrypoint-initdb.d
- mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: castopod
MYSQL_USER: castopod
MYSQL_PASSWORD: castopod
phpmyadmin:
image: phpmyadmin/phpmyadmin:latest
environment:
PMA_HOST: mariadb
PMA_PORT: 3306
UPLOAD_LIMIT: 300M
ports:
- 8888:80
volumes:
- phpmyadmin:/sessions
depends_on:
- mariadb
mailpit:
image: axllent/mailpit
restart: always
volumes:
- mailpit:/data
ports:
- ${MAILPIT_WEBUI_PORT:-8025}:8025
- ${MAILPIT_SMTP_PORT:-1025}:1025
environment:
MP_MAX_MESSAGES: 5000
MP_DATA_FILE: /data/mailpit.db
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
volumes:
mariadb:
phpmyadmin:
mailpit:

View file

@ -1,2 +0,0 @@
CREATE DATABASE IF NOT EXISTS `test`;
GRANT ALL ON `test`.* TO 'castopod'@'%';

View file

@ -1,5 +0,0 @@
file_uploads = On
memory_limit = 512M
upload_max_filesize = 500M
post_max_size = 512M
max_execution_time = 300

View file

@ -1,68 +0,0 @@
.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

View file

@ -2,7 +2,7 @@
# Example Environment Configuration file # Example Environment Configuration file
# #
# This file can be used as a starting point for # This file can be used as a starting point for
# your Castopod instance settings. # your Castopod Host instance settings.
# #
# For manual configuration: # For manual configuration:
# - copy this file's contents to a file named `.env` # - copy this file's contents to a file named `.env`
@ -14,10 +14,9 @@
# Instance configuration # Instance configuration
#-------------------------------------------------------------------- #--------------------------------------------------------------------
app.baseURL="https://YOUR_DOMAIN_NAME/" app.baseURL="https://YOUR_DOMAIN_NAME/"
media.baseURL="https://YOUR_MEDIA_DOMAIN_NAME/" app.mediaBaseURL="https://YOUR_MEDIA_DOMAIN_NAME/"
admin.gateway="cp-admin" app.adminGateway="cp-admin"
auth.gateway="cp-auth" app.authGateway="cp-auth"
analytics.salt="RANDOM_STRING_OF_64_CHARACTERS"
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# Database configuration # Database configuration
@ -28,14 +27,6 @@ database.default.username="root"
database.default.password="****" database.default.password="****"
database.default.DBPrefix="cp_" database.default.DBPrefix="cp_"
#--------------------------------------------------------------------
# Email configuration
#--------------------------------------------------------------------
# email.fromEmail="your_email_address"
# email.SMTPHost="your_smtp_host"
# email.SMTPUser="your_smtp_user"
# email.SMTPPass="your_smtp_password"
#-------------------------------------------------------------------- #--------------------------------------------------------------------
# Cache configuration (advanced) # Cache configuration (advanced)
# #
@ -50,21 +41,3 @@ cache.handler="file"
# cache.redis.password=null # cache.redis.password=null
# cache.redis.port=6379 # cache.redis.port=6379
# cache.redis.database=0 # cache.redis.database=0
#--------------------------------------------------------------------
# S3 configuration
#--------------------------------------------------------------------
# media.fileManager="s3"
# media.s3.endpoint="your_s3_host"
# media.s3.key="your_s3_key"
# media.s3.secret="your_s3_secret"
# media.s3.region="your_s3_region"
#--------------------------------------------------------------------
# REST API configuration
#--------------------------------------------------------------------
# restapi.enabled=true
# restapi.basicAuthUsername=castopod
# restapi.basicAuthPassword=password
# restapi.basicAuth=true

18
.eslintrc.json Normal file
View file

@ -0,0 +1,18 @@
{
"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": {}
}

1
.github/FUNDING.yml vendored
View file

@ -1 +0,0 @@
open_collective: castopod

66
.gitignore vendored
View file

@ -60,14 +60,10 @@ writable/logs/*
writable/session/* writable/session/*
!writable/session/index.html !writable/session/index.html
writable/temp/*
!writable/temp/index.html
writable/uploads/* writable/uploads/*
!writable/uploads/index.html !writable/uploads/index.html
writable/debugbar/* writable/debugbar/*
!writable/debugbar/index.html
php_errors.log php_errors.log
@ -86,7 +82,6 @@ tests/coverage*
# Don't save phpunit under version control. # Don't save phpunit under version control.
phpunit phpunit
.phpunit.cache
#------------------------- #-------------------------
# Composer # Composer
@ -107,15 +102,15 @@ _modules/*
.idea/ .idea/
*.iml *.iml
# NetBeans # Netbeans
/nbproject/ nbproject/
/build/ build/
/nbbuild/ nbbuild/
/dist/ dist/
/nbdist/ nbdist/
/nbactions.xml nbactions.xml
/nb-configuration.xml nb-configuration.xml
/.nb-gradle/ .nb-gradle/
# Sublime Text # Sublime Text
*.tmlanguage.cache *.tmlanguage.cache
@ -128,16 +123,14 @@ _modules/*
# Visual Studio Code # Visual Studio Code
.vscode/ .vscode/
.history/
tmp/
/results/ /results/
/phpunit*.xml /phpunit*.xml
/.phpunit.*.cache
# js package manager # npm
yarn.lock yarn.lock
node_modules node_modules
.pnpm-store
# JS # JS
.cache .cache
@ -147,21 +140,14 @@ public/*
!public/media !public/media
!public/.htaccess !public/.htaccess
!public/favicon.ico !public/favicon.ico
!public/icon*
!public/castopod-banner*
!public/castopod-avatar*
!public/index.php !public/index.php
!public/robots.txt !public/robots.txt
!public/.well-known
!public/.well-known/GDPR.yml
public/assets/*
!public/assets/index.html
# public media folder # public media folder
public/media/*
!public/media/index.html
!public/media/podcasts !public/media/podcasts
!public/media/persons !public/media/persons
!public/media/site
public/media/podcasts/* public/media/podcasts/*
!public/media/podcasts/index.html !public/media/podcasts/index.html
@ -169,19 +155,19 @@ public/media/podcasts/*
public/media/persons/* public/media/persons/*
!public/media/persons/index.html !public/media/persons/index.html
public/media/site/*
!public/media/site/index.html
# Generated files # Generated files
modules/Admin/Language/*/PersonsTaxonomy.php app/Language/en/PersonsTaxonomy.php
app/Language/fr/PersonsTaxonomy.php
# Castopod bundle & packages #-------------------------
castopod/ # Docker volumes
castopod-*.zip #-------------------------
castopod-*.tar.gz
# Plugins mariadb
plugins/* phpmyadmin
!plugins/.gitkeep sessions
writable/plugins.json
writable/plugins-lock.json # Castopod Host bundle & packages
castopod-host/
castopod-host-*.zip
castopod-host-*.tar.gz

View file

@ -1,52 +1,30 @@
image: code.castopod.org:5050/adaures/castopod:ci-php8.5 image: code.podlibre.org:5050/podlibre/castopod-host:latest
stages: stages:
- prepare - prepare
- quality - quality
- bundle - bundle
- release - release
- deploy
- build
php-dependencies: php-dependencies:
stage: prepare stage: prepare
script: script:
# Install all php dependencies # Install all php dependencies
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs - composer install --prefer-dist --no-ansi --no-interaction --no-progress --ignore-platform-reqs
cache:
key:
files:
- composer.lock
paths:
- .composer-cache
artifacts: artifacts:
expire_in: 30 mins
paths: paths:
- vendor/ - vendor/
rules: expire_in: 30 mins
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
js-dependencies: js-dependencies:
stage: prepare stage: prepare
script: script:
# Install all js dependencies # Install all npm dependencies
- pnpm install - npm ci
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
artifacts: artifacts:
expire_in: 30 mins
paths: paths:
- node_modules/ - node_modules/
rules: expire_in: 30 mins
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
lint-commit-msg: lint-commit-msg:
stage: quality stage: quality
@ -56,10 +34,6 @@ lint-commit-msg:
- ./scripts/lint-commit-msg.sh - ./scripts/lint-commit-msg.sh
dependencies: dependencies:
- js-dependencies - js-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- if: $CI_COMMIT_BRANCH =~ /^(develop|main|alpha|beta|next)$/
lint-php: lint-php:
stage: quality stage: quality
@ -72,46 +46,25 @@ lint-php:
- vendor/bin/rector process --dry-run --ansi - vendor/bin/rector process --dry-run --ansi
dependencies: dependencies:
- php-dependencies - php-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
lint-js: lint-js:
stage: quality stage: quality
script: script:
- pnpm run format - npm run prettier
- pnpm run typecheck - npm run typecheck
- pnpm run lint - npm run lint
- pnpm run lint:css - npm run lint:css
dependencies: dependencies:
- js-dependencies - js-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
tests: tests:
stage: quality stage: quality
services:
- mariadb:10.11
variables:
MYSQL_ROOT_PASSWORD: "R00Tp4ssW0RD"
MYSQL_DATABASE: "test"
MYSQL_USER: "castopod"
MYSQL_PASSWORD: "castopod"
script: script:
- echo "SHOW DATABASES;" | mariadb --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mariadb "$MYSQL_DATABASE" --skip_ssl
# run phpunit without code coverage # run phpunit without code coverage
# TODO: add code coverage # TODO: add code coverage
- vendor/bin/phpunit --no-coverage - vendor/bin/phpunit --no-coverage
dependencies: dependencies:
- php-dependencies - php-dependencies
rules:
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/
when: never
- when: on_success
bundle: bundle:
stage: bundle stage: bundle
@ -123,21 +76,19 @@ bundle:
# make scripts/bundle.sh executable # make scripts/bundle.sh executable
- chmod +x ./scripts/bundle.sh - chmod +x ./scripts/bundle.sh
# bundle castopod with commit ref as version # bundle castopod-host with commit ref as version
- ./scripts/bundle.sh ${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA} - ./scripts/bundle.sh ${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}
dependencies: dependencies:
- php-dependencies - php-dependencies
- js-dependencies - js-dependencies
artifacts: artifacts:
name: "castopod-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}" name: "castopod-host-${CI_COMMIT_REF_SLUG}_${CI_COMMIT_SHORT_SHA}"
paths: paths:
- castopod - castopod-host
rules: except:
- if: $CI_PROJECT_NAMESPACE != "adaures" - main
when: never - beta
- if: $CI_COMMIT_BRANCH =~ /^(main|alpha|beta|next)$/ || $CI_COMMIT_TAG - alpha
when: never
- when: on_success
release: release:
stage: release stage: release
@ -154,45 +105,11 @@ release:
- chmod +x ./scripts/package.sh - chmod +x ./scripts/package.sh
# run semantic-release script (configured in `.releaserc.json` file) # run semantic-release script (configured in `.releaserc.json` file)
- pnpm run release - npm run release
dependencies: dependencies:
- php-dependencies - php-dependencies
- js-dependencies - js-dependencies
artifacts: only:
paths: - main
- castopod - alpha
rules: - beta
- 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
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
rules:
- if: $CI_PROJECT_NAMESPACE != "adaures"
when: never
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_COMMIT_MESSAGE =~ /^chore\(release\):/ && $CI_COMMIT_TAG

View file

@ -6,7 +6,7 @@
1. [First step] 1. [First step]
2. [Second step] 2. [Second step]
3. [and so on] 3. [and so on...]
### Expected behavior ### Expected behavior
@ -27,7 +27,7 @@ logs, and code as it's very hard to read otherwise.
- OS: [e.g. Ubuntu server] - OS: [e.g. Ubuntu server]
- Browser: [e.g. chrome, safari] - Browser: [e.g. chrome, safari]
- Web server: [eg. Apache] - Web server: [eg. Apache]
- [any other relevant context] - [any other relevant context...]
### Possible fixes ### Possible fixes

View file

@ -1,17 +0,0 @@
### Before submitting an issue
1. **Use the issue search** &mdash; check if the issue has already been
reported.
2. **Check if the issue has been fixed** &mdash; try to reproduce it using the
latest release.
3. **Isolate the problem** &mdash; ideally create a
[reduced test case](https://css-tricks.com/reduced-test-cases/) and a live
example.
4. **Select an issue template** &mdash; choose a template from `bug` or
`feature-request` and fill out the info you deem necessary. The more context
we get, the easier it is to implement the feature or fix the bug you report.
Check out the [CONTRIBUTING manual](../../CONTRIBUTING.md) for more info.

View file

@ -1,7 +1,7 @@
### Is your feature request related to a problem? Please describe ### Is your feature request related to a problem? Please describe
A clear and concise description of what the problem is. Ex. I'm always A clear and concise description of what the problem is. Ex. I'm always
frustrated when [] frustrated when [...]
### Describe the solution you'd like ### Describe the solution you'd like

View file

@ -1 +1,4 @@
pnpm exec commitlint --verbose --edit "$1" #!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --verbose --edit "$1"

View file

@ -1,8 +1,11 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# CaptainHook 5.10.0 # CaptainHook 5.10.0
INTERACTIVE="--no-interaction" INTERACTIVE="--no-interaction"
vendor/bin/captainhook $INTERACTIVE --configuration=captainhook.json --bootstrap=vendor/autoload.php hook:pre-commit "$@" <&0 vendor/bin/captainhook $INTERACTIVE --configuration=captainhook.json --bootstrap=vendor/autoload.php hook:pre-commit "$@" <&0
pnpm run typecheck npm run typecheck
pnpm exec lint-staged npx lint-staged

View file

@ -1,3 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# CaptainHook 5.10.0 # CaptainHook 5.10.0
INTERACTIVE="--no-interaction" INTERACTIVE="--no-interaction"

View file

@ -2,7 +2,7 @@
"trailingComma": "es5", "trailingComma": "es5",
"overrides": [ "overrides": [
{ {
"files": ["*.md", "*.mdx"], "files": "*.md",
"options": { "options": {
"proseWrap": "always" "proseWrap": "always"
} }

View file

@ -1,86 +1,17 @@
{ {
"branches": [ "branches": [
"main", "main",
{ { "name": "alpha", "prerelease": true },
"name": "alpha", { "name": "beta", "prerelease": true }
"prerelease": true
},
{
"name": "beta",
"prerelease": true
},
{
"name": "next",
"prerelease": true
}
], ],
"plugins": [ "plugins": [
[ "@semantic-release/commit-analyzer",
"@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator",
{
"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/changelog",
[ [
"@semantic-release/exec", "@semantic-release/exec",
{ {
"prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh ${nextRelease.version} && pnpm exec prettier --write CHANGELOG.md" "prepareCmd": "./scripts/bundle.sh ${nextRelease.version} && ./scripts/package.sh ${nextRelease.version} && npx prettier --write CHANGELOG.md"
} }
], ],
"@semantic-release/npm", "@semantic-release/npm",
@ -93,22 +24,21 @@
"package.json", "package.json",
"package-lock.json", "package-lock.json",
"CHANGELOG.md" "CHANGELOG.md"
], ]
"message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}"
} }
], ],
[ [
"@semantic-release/gitlab", "@semantic-release/gitlab",
{ {
"gitlabUrl": "https://code.castopod.org/", "gitlabUrl": "https://code.podlibre.org/",
"assets": [ "assets": [
{ {
"path": "castopod-*.zip", "path": "castopod-host-*.zip",
"label": "Castopod Package (zip)" "label": "Castopod Host Package (zip)"
}, },
{ {
"path": "castopod-*.tar.gz", "path": "castopod-host-*.tar.gz",
"label": "Castopod Package (tar.gz)" "label": "Castopod Host Package (tar.gz)"
} }
] ]
} }

View file

@ -1,17 +1,14 @@
# rsync filter rules to copy required files for Castopod's bundle # rsync filter rules to copy required files for Castopod Host's bundle
+ resources/icons/*** - app/Views/_assets/
+ resources/
+ app/*** + app/***
+ modules/***
+ plugins/***
+ public/*** + public/***
+ themes/***
+ vendor/*** + vendor/***
+ writable/*** + writable/***
+ .env.example + .env.example
+ DEPENDENCIES.md
+ LICENSE.md + LICENSE.md
+ README.md + README.md
+ spark + INSTALL.md
+ php-icons.php + UPDATE.md
- ** - **

View file

@ -1,5 +1,5 @@
{ {
"extends": "stylelint-config-standard", "extends": "stylelint-config-recommended",
"rules": { "rules": {
"at-rule-no-unknown": [ "at-rule-no-unknown": [
true, true,
@ -10,24 +10,10 @@
"responsive", "responsive",
"variants", "variants",
"screen", "screen",
"layer", "layer"
"config"
] ]
} }
], ],
"at-rule-no-deprecated": [ "no-descending-specificity": null
true,
{
"ignoreAtRules": ["apply"]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme"]
}
],
"no-descending-specificity": null,
"selector-class-pattern": null
} }
} }

View file

View file

@ -1,4 +1,4 @@
# Authors # Authors
- [Benjamin Bellamy](https://code.castopod.org/benjamin) <ben@castopod.org> - [Benjamin Bellamy](https://code.podlibre.org/benjamin) <ben@podlibre.org>
- [Yassine Doghri](https://code.castopod.org/yassine) <yassine@castopod.org> - [Yassine Doghri](https://code.podlibre.org/yassine) <yassine@podlibre.org>

View file

@ -1,3887 +1,868 @@
## [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) # [1.0.0-alpha.80](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.79...v1.0.0-alpha.80) (2021-12-29)
### 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 ### 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
- **premium:** set itunes:block on premium feeds to prevent indexing
([88851b0](https://code.castopod.org/adaures/castopod/commit/88851b022663d575a816f0e2f33f0353767dd52d))
- **rss:** generate podcast guid if empty
([a5aef2a](https://code.castopod.org/adaures/castopod/commit/a5aef2a63e464632f3941649d455672835989e6c)),
closes [#450](https://code.castopod.org/adaures/castopod/issues/450)
### Features
- add trailer tags to rss if trailer episodes are present
([80fdd9c](https://code.castopod.org/adaures/castopod/commit/80fdd9cfb4a95feac6ed0000435a013fc83e6892))
- add transcript display to episode page
([4d141fc](https://code.castopod.org/adaures/castopod/commit/4d141fceae56fa9e666b42c32a830ff9c68989db)),
closes [#411](https://code.castopod.org/adaures/castopod/issues/411)
- **platforms:** add telegram to socials
([004f804](https://code.castopod.org/adaures/castopod/commit/004f804045cd8e884361bb4318109fbdd7afc9a8))
- **platforms:** add truefans.fm and episodes.fm
([d046ecc](https://code.castopod.org/adaures/castopod/commit/d046ecc52f6ccd41d09f6de48e00d2c61d25d7f0)),
closes [#458](https://code.castopod.org/adaures/castopod/issues/458)
[#459](https://code.castopod.org/adaures/castopod/issues/459)
## [1.10.5](https://code.castopod.org/adaures/castopod/compare/v1.10.4...v1.10.5) (3/12/2024)
### Bug Fixes
- **file-uploads:** validate chapters json content + remove permit_empty rule to
uploaded files
([6289c42](https://code.castopod.org/adaures/castopod/commit/6289c42b1189f074c7e4e4cd9fbfd73bf26625c9)),
closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
## [1.10.4](https://code.castopod.org/adaures/castopod/compare/v1.10.3...v1.10.4) (2/26/2024)
### Bug Fixes
- display chapters in episode preview page
([797516a](https://code.castopod.org/adaures/castopod/commit/797516a2ec7d88704412a5cca50421e8eef38eec)),
closes [#445](https://code.castopod.org/adaures/castopod/issues/445)
## [1.10.3](https://code.castopod.org/adaures/castopod/compare/v1.10.2...v1.10.3) (2/21/2024)
### Bug Fixes
- **chapters:** use episode cover when chapter img is an empty string
([a343de4](https://code.castopod.org/adaures/castopod/commit/a343de4cf6ba38561b8fe675fa9c38d9f0ecfec7)),
closes [#444](https://code.castopod.org/adaures/castopod/issues/444)
- **import:** set episodes as premium if podcast is set as premium by default
([dfd66be](https://code.castopod.org/adaures/castopod/commit/dfd66beebfcca1670b0a9d389e8e3f8d2d08d2f2))
## [1.10.2](https://code.castopod.org/adaures/castopod/compare/v1.10.1...v1.10.2) (2/20/2024)
### Bug Fixes
- **podcast-import:** move closing parenthasis when checking for owner name and
email existence
([cec7815](https://code.castopod.org/adaures/castopod/commit/cec78155f94a222edcf7964c0a2f3a3e0f46a98d))
## [1.10.1](https://code.castopod.org/adaures/castopod/compare/v1.10.0...v1.10.1) (2/20/2024)
### Bug Fixes
- **fediverse:** use config name to get Fediverse config properties instead of
hardcoded class string
([5fd0980](https://code.castopod.org/adaures/castopod/commit/5fd0980ff7101d45051a2daa3f635694f85609d7))
# [1.10.0](https://code.castopod.org/adaures/castopod/compare/v1.9.0...v1.10.0) (2/19/2024)
### Bug Fixes
- **op3:** move op3 prefix to enclosure url instead of audio proxy
([d580369](https://code.castopod.org/adaures/castopod/commit/d5803692357952d82d54efd8d3aa71de3a1c9571))
- **podcast-import:** rollback transaction before exception is thrown
([419bb04](https://code.castopod.org/adaures/castopod/commit/419bb04716088586b87b2c8f24a954ca8cfd6c76)),
closes [#429](https://code.castopod.org/adaures/castopod/issues/429)
[#319](https://code.castopod.org/adaures/castopod/issues/319)
[#443](https://code.castopod.org/adaures/castopod/issues/443)
[#438](https://code.castopod.org/adaures/castopod/issues/438)
### Features
- add podcast:season and podcast:episode tags to rss feed
([98c6658](https://code.castopod.org/adaures/castopod/commit/98c6658840eedd55bd6d8042f8a69c342b87cd71))
- add support for podcasting 2.0 "medium" tag with podcast, music and audiobook
([630e788](https://code.castopod.org/adaures/castopod/commit/630e788f0e1ddfe5de229bd415a8e15361efa746)),
closes [#439](https://code.castopod.org/adaures/castopod/issues/439)
- display chapters in episode's public page
([87cc437](https://code.castopod.org/adaures/castopod/commit/87cc437e1ead5486ed46ca37e2055aaf5c9445c1)),
closes [#423](https://code.castopod.org/adaures/castopod/issues/423)
- support VTT transcript file format in addition to SRT
([7071b4b](https://code.castopod.org/adaures/castopod/commit/7071b4b6f48cb9a2f766064f3a5c23f92b293718)),
closes [#433](https://code.castopod.org/adaures/castopod/issues/433)
# [1.9.0](https://code.castopod.org/adaures/castopod/compare/v1.8.2...v1.9.0) (1/31/2024)
### Bug Fixes
- **i18n:** escape language strings in form fields to prevent them from
disappearing
([3cb5ffd](https://code.castopod.org/adaures/castopod/commit/3cb5ffd25b9604a83cd12935e641dab7c88fba47)),
closes [#412](https://code.castopod.org/adaures/castopod/issues/412)
- **podcast-about:** update stats query to discard scheduled episodes from
episodes number
([67c037c](https://code.castopod.org/adaures/castopod/commit/67c037c9eb1e15c6945eaf74ec0ff30b33f4b704))
- **premium-subs:** clear subscription list cache after insert
([2accb0f](https://code.castopod.org/adaures/castopod/commit/2accb0f7652330b29c3adb85a2e1b0d5d83f1389)),
closes [#430](https://code.castopod.org/adaures/castopod/issues/430)
- **s3:** remove proxy, set objects acl to public-read, and serve files using
their public urls
([6a77a9d](https://code.castopod.org/adaures/castopod/commit/6a77a9d2f29c849775a3d1bcbd819f73f21d9aa6))
### Features
- add actor domain to handle in follow page
([de099ac](https://code.castopod.org/adaures/castopod/commit/de099ac64300b8edb86e387fde89c0a3e9472f46))
- **admin:** add podcast's OP3 analytics dashboard link
([5f3752b](https://code.castopod.org/adaures/castopod/commit/5f3752b4430f6f2d5f9e5f6a7a003bc4d2f9d487))
## [1.8.2](https://code.castopod.org/adaures/castopod/compare/v1.8.1...v1.8.2) (1/17/2024)
### Bug Fixes
- **transcript:** add condition when concatenating sub text to prevent second
line duplication
([6cbfec0](https://code.castopod.org/adaures/castopod/commit/6cbfec0d7d9bf85c8014d379026648857ea13373))
## [1.8.1](https://code.castopod.org/adaures/castopod/compare/v1.8.0...v1.8.1) (1/16/2024)
### Bug Fixes
- **models:** set updatedField as empty string when not used
([164f4d3](https://code.castopod.org/adaures/castopod/commit/164f4d3be74ec8d371fb40d7fe730f7b2940ca05))
# [1.8.0](https://code.castopod.org/adaures/castopod/compare/v1.7.4...v1.8.0) (1/15/2024)
### Bug Fixes
- **episode-form:** add required validation rules for title and slug
([30a3473](https://code.castopod.org/adaures/castopod/commit/30a34738635bf4f4a4c6b2a7174f7e439f0dfc6e)),
closes [#420](https://code.castopod.org/adaures/castopod/issues/420)
- **import:** check for empty string when generating podcast guid for feeds not
including one
([ac5336f](https://code.castopod.org/adaures/castopod/commit/ac5336fbc5fb8038de541dd06938a8beb2e8d733))
- **install:** add created superadmin to most powerful group in instance, ie.
superadmin
([2ed511f](https://code.castopod.org/adaures/castopod/commit/2ed511f8a0005dc06eda5afd6b1d13beee1eb9dd))
- **persons:** delete person avatar when deleting a person
([c1ec98c](https://code.castopod.org/adaures/castopod/commit/c1ec98c95656844712011ff30b84c397b78da311)),
closes [#419](https://code.castopod.org/adaures/castopod/issues/419)
- **platforms:** add matrix.org as a social platform
([9178c3f](https://code.castopod.org/adaures/castopod/commit/9178c3f3afa16e104d25ae159728e90a3bbd57c3)),
closes [#421](https://code.castopod.org/adaures/castopod/issues/421)
### Features
- **admin:** add tooltip for not authorized routes
([f7f9baf](https://code.castopod.org/adaures/castopod/commit/f7f9bafc3e56621fab2569d9d76baafe0a2e940d))
- **admin:** emphasize unprivileged items in sidebar with "prohibited" icon
([0bd7dde](https://code.castopod.org/adaures/castopod/commit/0bd7ddea58adf502121b83e5c09317e20912fb4e))
- allow hiding owner's email in public RSS feed
([222e02a](https://code.castopod.org/adaures/castopod/commit/222e02a2af9ecb8b8768a63d3054f4c3ef54e991))
- **persons:** order persons by full_name ASC for easier list scanning
([68a599f](https://code.castopod.org/adaures/castopod/commit/68a599fee08c71763b9336e14b1c0d9e28c4449b)),
closes [#418](https://code.castopod.org/adaures/castopod/issues/418)
## [1.7.4](https://code.castopod.org/adaures/castopod/compare/v1.7.3...v1.7.4) (1/3/2024)
### Bug Fixes
- **media:** add missing HEAD route for static assets served with S3
([b61a32c](https://code.castopod.org/adaures/castopod/commit/b61a32c8a9b10e129666804d533487430ce7432c))
## [1.7.3](https://code.castopod.org/adaures/castopod/compare/v1.7.2...v1.7.3) (12/21/2023)
### Bug Fixes
- **analytics:** upgrade opawg's user-agents-php to user-agents-v2-php
([8cd7886](https://code.castopod.org/adaures/castopod/commit/8cd78866762e26aa63c224dace6c247e0e9dc068))
- **platforms:** add Threads and YouTube Music
([9264a2d](https://code.castopod.org/adaures/castopod/commit/9264a2d74cc95278c9d84c99ef914fdbcaf8a97f))
## [1.7.2](https://code.castopod.org/adaures/castopod/compare/v1.7.1...v1.7.2) (12/12/2023)
### Bug Fixes
- **episode-form:** render episode number optional when episode type is trailer
or bonus
([694328f](https://code.castopod.org/adaures/castopod/commit/694328f10865b2fcd6436122de46866dae81f945))
## [1.7.1](https://code.castopod.org/adaures/castopod/compare/v1.7.0...v1.7.1) (12/1/2023)
### Bug Fixes
- **housekeeping:** add where clause to check episode_id is not null on reset
comments count
([119742c](https://code.castopod.org/adaures/castopod/commit/119742cdbb2c2f7f847692fb76f6ff1dbb2e25b6))
# [1.7.0](https://code.castopod.org/adaures/castopod/compare/v1.6.5...v1.7.0) (11/29/2023)
### Bug Fixes
- **admin-ux:** hide navigation submenus in details panel for easier scanning
([b047a3c](https://code.castopod.org/adaures/castopod/commit/b047a3c6707114d04c276758f2e543eef90d72f5))
- **admin:** remove episode title truncation + display description in two lines
in episode list
([f4ffa30](https://code.castopod.org/adaures/castopod/commit/f4ffa30ec4341f43e22b1f983781ad04c956aa25)),
closes [#386](https://code.castopod.org/adaures/castopod/issues/386)
- **auth:** display error messages from validator
([5a834c0](https://code.castopod.org/adaures/castopod/commit/5a834c0f8957fc016e73325a3c3ff05e524d0755))
- **housekeeping:** remove unnecessary $tablePrefix variable when resetting post
count
([97d793f](https://code.castopod.org/adaures/castopod/commit/97d793f55e7eb3b049980e5081950baa2bb1b881)),
closes [#383](https://code.castopod.org/adaures/castopod/issues/383)
- **import:** handle bad values for location attributes
([642981f](https://code.castopod.org/adaures/castopod/commit/642981fd358ccf118d3d7a957fb6be7933c016ac))
- **import:** use cocur/slugify library to handle non latin text
([4ca7f9c](https://code.castopod.org/adaures/castopod/commit/4ca7f9ccae1e352bf26a3b6db4de73bac7b84382))
- move monetization outside of podcast form + add broadcast section to podcast
menu
([dff8516](https://code.castopod.org/adaures/castopod/commit/dff85168b32a6df77425ef51865588ebcd8b8ba9))
- **nodeinfo2:** import database config + use dynamic table prefix for active
local actors query
([6a7ef01](https://code.castopod.org/adaures/castopod/commit/6a7ef0109a6e52144ca687b979ffe56fba66165b))
- **persons:** set roles field as optional + set `Cast > Host` as default value
([02132dc](https://code.castopod.org/adaures/castopod/commit/02132dc46640807e2bc4cfc406c911fa097f36fe)),
closes [#347](https://code.castopod.org/adaures/castopod/issues/347)
- **platforms:** make platforms' websites and submit urls more prominent
([61cf8fa](https://code.castopod.org/adaures/castopod/commit/61cf8fa3e2435ee2a9bdd8e711b8d69d4ca4ec4c))
- **podcast-form:** move fediverse section below author section
([1861d67](https://code.castopod.org/adaures/castopod/commit/1861d67971e2cc0c20ace091f037f6436437a50d))
- reorder podcast form fields + extract sync feeds to its own form
([2d52fa1](https://code.castopod.org/adaures/castopod/commit/2d52fa1046faf1b8d81304e35fc24a7874315e6e))
### Features
- **admin:** add rss feed link to podcast side navigation
([18e2633](https://code.castopod.org/adaures/castopod/commit/18e2633a49dbbeb57a685f129a2ab158397de61e))
- **icons:** update new Deezer logo
([f2d5b27](https://code.castopod.org/adaures/castopod/commit/f2d5b272ac385a978d7e173121faafe03d7a7200))
- **install:** init database and create superadmin using CLI
([02d4ba6](https://code.castopod.org/adaures/castopod/commit/02d4ba69ac007ebd1eccab428a98b54051aaf70c)),
closes [#380](https://code.castopod.org/adaures/castopod/issues/380)
- **ux:** add episode description to episode cards
([5f8d413](https://code.castopod.org/adaures/castopod/commit/5f8d413b84b236077a75934da9409f37d34cb4a5))
## [1.6.5](https://code.castopod.org/adaures/castopod/compare/v1.6.4...v1.6.5) (2023-09-26)
### Bug Fixes
- **fediverse:** use NoteObject including episode link in content (hotfix)
([ffa530e](https://code.castopod.org/adaures/castopod/commit/ffa530e187ff6488648a7cf749ca0173765a5d87))
## [1.6.4](https://code.castopod.org/adaures/castopod/compare/v1.6.3...v1.6.4) (2023-09-17)
### Bug Fixes
- **fediverse:** do not cache remote action form + fix typo on post routes for
passing post uuid
([4ecb42f](https://code.castopod.org/adaures/castopod/commit/4ecb42f7c82eb8d41d27c7b9705b3278ea04ab79))
- **fediverse:** update post controller namespace in routes
([3189f12](https://code.castopod.org/adaures/castopod/commit/3189f122067dc47d6de93c3185aca66d7df95e1a))
## [1.6.3](https://code.castopod.org/adaures/castopod/compare/v1.6.2...v1.6.3) (2023-09-14)
### Bug Fixes
- **fediverse:** add `index` to post controller-method to access post's jsonld
contents
([35142d8](https://code.castopod.org/adaures/castopod/commit/35142d8e565e828a977ba2b4de77c1b47a633beb))
## [1.6.2](https://code.castopod.org/adaures/castopod/compare/v1.6.1...v1.6.2) (2023-09-11)
### Bug Fixes
- **migrations:** remove if exists modifier for drop index
([82013c9](https://code.castopod.org/adaures/castopod/commit/82013c9cde901c54fdb3a833890aa693e8542627)),
closes [#382](https://code.castopod.org/adaures/castopod/issues/382)
## [1.6.1](https://code.castopod.org/adaures/castopod/compare/v1.6.0...v1.6.1) (2023-09-09)
### Bug Fixes
- **admin:** redirect root fediverse route to fediverse-blocked-actors
([ba5324e](https://code.castopod.org/adaures/castopod/commit/ba5324ea1942a3939f186e974d29fb393c54b253))
- **analytics:** show full referrer domain in web pages visits reports
([6be38e9](https://code.castopod.org/adaures/castopod/commit/6be38e9fda3d1436d81686e1a3a5e5b173e390a0)),
closes [#367](https://code.castopod.org/adaures/castopod/issues/367)
- **auth:** overwrite Shield's PermissionFilter
([c6e8000](https://code.castopod.org/adaures/castopod/commit/c6e8000bab54f4a32068578f750f4cf9d91bad89))
- **auth:** update shield from v1.0.0-beta.3 to v1.0.0-beta.6
([23842df](https://code.castopod.org/adaures/castopod/commit/23842df03ae28e416390e2436442b8e7c8340333))
- **platforms:** add missing tiktok to social platforms seed
([8dfdaf3](https://code.castopod.org/adaures/castopod/commit/8dfdaf321566050e9c53683e70864871eb55d618))
- remove fediverse prefix to prevent migration error + load routes during
podcast import
([7ff1dbe](https://code.castopod.org/adaures/castopod/commit/7ff1dbe9030768074b2fe7c7f570bfb9e7336f62))
- **routes:** overwrite RouteCollection to include all routes + update js and
php dependencies
([b4f1b91](https://code.castopod.org/adaures/castopod/commit/b4f1b916bfec53f071e8d0d900081c6d74486e53))
- update Router to include latest CI changes with alternate-content logic
([ae57601](https://code.castopod.org/adaures/castopod/commit/ae57601c838a7aa9469bae8038ac1c30d8c9a51e))
- use podcast-activity named route instead of not existing actor route
([3c35718](https://code.castopod.org/adaures/castopod/commit/3c357183ca51545787fcfc801b4a5829d9cd8ad6))
# [1.6.0](https://code.castopod.org/adaures/castopod/compare/v1.5.2...v1.6.0) (2023-08-28)
### Bug Fixes
- **home:** update where clause when getting all podcasts to prevent draft
podcasts from showing up
([7a1eea5](https://code.castopod.org/adaures/castopod/commit/7a1eea58d3cbc1982baaec21d87a36e218e1910a))
- **media:** copy and delete temp file when saving instead of moving it for FS
FileManager
([9346e78](https://code.castopod.org/adaures/castopod/commit/9346e787bd2a2c815533092279f96ae1fe0d9aae)),
closes [#338](https://code.castopod.org/adaures/castopod/issues/338)
- **media:** get path using media_path_absolute when saving media file
([754e7a6](https://code.castopod.org/adaures/castopod/commit/754e7a6b4b2c12cf50c1c8b166732dc3255f36fb))
- **media:** init file properties in setAttributes' Model method + set defaults
to pathinfo data
([0775add](https://code.castopod.org/adaures/castopod/commit/0775add67860b94a35b68c01b133ec8ec969f539))
- **premium-podcasts:** show premium flag only when podcast has published
premium episodes
([d10c4fd](https://code.castopod.org/adaures/castopod/commit/d10c4fd7538e6af8a5b0eb232a06522fe8c4bf8e))
- **s3:** add a flag to serve media files by redirecting to a presigned url
instead of default proxy
([11aa358](https://code.castopod.org/adaures/castopod/commit/11aa3586a04c166404954600235634cee77219df))
### Features
- **episode:** add preview link in admin to view and share episode before
publication
([7d21b35](https://code.castopod.org/adaures/castopod/commit/7d21b3509ec5d1aa65420efa038f44bcd235e64f))
## [1.5.2](https://code.castopod.org/adaures/castopod/compare/v1.5.1...v1.5.2) (2023-07-31)
### Bug Fixes
- **credits:** remove undefined $podcast variable from page layout
([73a5b68](https://code.castopod.org/adaures/castopod/commit/73a5b680875cc520fd15c529c01d44df728f9be2)),
closes [#359](https://code.castopod.org/adaures/castopod/issues/359)
- **platforms:** change twitter to X + add buymeacoffee and kofi as funding
([d69b4e4](https://code.castopod.org/adaures/castopod/commit/d69b4e4857fcb1ac1c05ac59c78d130788f00400)),
closes [#353](https://code.castopod.org/adaures/castopod/issues/353)
[#361](https://code.castopod.org/adaures/castopod/issues/361)
## [1.5.1](https://code.castopod.org/adaures/castopod/compare/v1.5.0...v1.5.1) (2023-07-29)
### Bug Fixes
- **admin-ui:** remove button labels on smaller screens in podcast view
([9cc5ffd](https://code.castopod.org/adaures/castopod/commit/9cc5ffd1439fdc86f46a03f4319cae32db95f84e))
- **rss:** set srt transcripts' mimetype to application/x-subrip with
rel="captions" attribute
([16a3fdb](https://code.castopod.org/adaures/castopod/commit/16a3fdb56e3f07185e75d106216f29519ccb25f7)),
closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
- **rss:** update podcast extension namespace
([6833dd0](https://code.castopod.org/adaures/castopod/commit/6833dd05ab51bc530d34fd4174ad732f623226c0)),
closes [#360](https://code.castopod.org/adaures/castopod/issues/360)
# [1.5.0](https://code.castopod.org/adaures/castopod/compare/v1.4.7...v1.5.0) (2023-07-27)
### Bug Fixes
- **admin-ui:** truncate header title + remove sticky podcast banner card on
mobile
([63c20da](https://code.castopod.org/adaures/castopod/commit/63c20da5ffd500265f06fa38f2b2c963e14602af))
### Features
- add podcast links page including social, podcasting and funding links
([8ae2929](https://code.castopod.org/adaures/castopod/commit/8ae292933af15fa99856582ac24e985bfef37d5b))
## [1.4.7](https://code.castopod.org/adaures/castopod/compare/v1.4.6...v1.4.7) (2023-07-19)
### Bug Fixes
- **s3:** allow CORS for served static files
([9b955c9](https://code.castopod.org/adaures/castopod/commit/9b955c9ce25a06a9102b67ebe77375dc45d28f0f))
## [1.4.6](https://code.castopod.org/adaures/castopod/compare/v1.4.5...v1.4.6) (2023-07-11)
### Bug Fixes
- **fediverse:** expand object before sending accept follow request
([082cdc9](https://code.castopod.org/adaures/castopod/commit/082cdc9ee79d004c2ed748e3b8046e9141bf0242)),
closes [#350](https://code.castopod.org/adaures/castopod/issues/350)
- **podcast-import:** remove error log when no import in queue, exit with
success instead
([5e719f3](https://code.castopod.org/adaures/castopod/commit/5e719f3e9eb6cf48c3fd8ac97181638b24d03fc9))
## [1.4.5](https://code.castopod.org/adaures/castopod/compare/v1.4.4...v1.4.5) (2023-07-04)
### Bug Fixes
- **s3:** handle range requests to serve media files
([41a5932](https://code.castopod.org/adaures/castopod/commit/41a59322332c835808a32987aaf8ec6cafbf5fca))
## [1.4.4](https://code.castopod.org/adaures/castopod/compare/v1.4.3...v1.4.4) (2023-07-02)
### Bug Fixes
- **audio-clipper:** init segment position on firstUpdate + improve UX by adding
ghost handle
([aa68386](https://code.castopod.org/adaures/castopod/commit/aa683866671d14c0b9a11b09c74eb132673e5547)),
closes [#351](https://code.castopod.org/adaures/castopod/issues/351)
- set resized images to 72dpi for compatibility with Apple Podcasts
([0b327cb](https://code.castopod.org/adaures/castopod/commit/0b327cb4d9c92d0ae227a0f08ede3b29390df172)),
closes [#282](https://code.castopod.org/adaures/castopod/issues/282)
## [1.4.3](https://code.castopod.org/adaures/castopod/compare/v1.4.2...v1.4.3) (2023-06-29)
### Bug Fixes
- **video-clipper:** add -t option to ffmpeg command to stop generation after
duration
([60814b8](https://code.castopod.org/adaures/castopod/commit/60814b8d202419c2bdbf6abb7c2bde447537b7e9)),
closes [#341](https://code.castopod.org/adaures/castopod/issues/341)
## [1.4.2](https://code.castopod.org/adaures/castopod/compare/v1.4.1...v1.4.2) (2023-06-27)
### Bug Fixes
- **fediverse:** check that actor's images mimetype is present or guess it
otherwise
([06c4f15](https://code.castopod.org/adaures/castopod/commit/06c4f15477a568407a3d3c1e5e489bc0241bc1e9)),
closes [#348](https://code.castopod.org/adaures/castopod/issues/348)
- **podcast-import:** show cancel or retry action depending on task status
([e42258d](https://code.castopod.org/adaures/castopod/commit/e42258de1f331aac0cbb380b80cd8fc7f9d7dc18))
## [1.4.1](https://code.castopod.org/adaures/castopod/compare/v1.4.0...v1.4.1) (2023-06-22)
### Bug Fixes
- **podcast-import:** set default values for person group and role if not found
in taxonomy
([aa46dca](https://code.castopod.org/adaures/castopod/commit/aa46dca4e399bf2e544d62dcb4a9a0328e4e6c41))
# [1.4.0](https://code.castopod.org/adaures/castopod/compare/v1.3.5...v1.4.0) (2023-06-21)
### Bug Fixes
- **charts:** set duration charts label to HHhMM for listening time analytics
([3fc1d8e](https://code.castopod.org/adaures/castopod/commit/3fc1d8e18dc8119251c72dcaa7e5121246c2b194))
- **embed:** set height of player iframe from config
([4665741](https://code.castopod.org/adaures/castopod/commit/4665741425532f253a46a42ba05602047798dba2))
- **s3:** serve files without cache if dummy cache handler + add http referer
header to redirect
([30db9f0](https://code.castopod.org/adaures/castopod/commit/30db9f0667bf7f7a5f186ea667a524d1e3b502db))
- **s3:** use presigned request uri to serve static files
([cb92dc7](https://code.castopod.org/adaures/castopod/commit/cb92dc73f17543d32d1cdc24db72403a5c561a74))
- **webmanifest:** import misc helper to get site_icon_url
([548a11d](https://code.castopod.org/adaures/castopod/commit/548a11d501749fa61ef894fd8818abae5668554f))
### Features
- **import:** run podcast imports' processes asynchronously using tasks
([d8e1d40](https://code.castopod.org/adaures/castopod/commit/d8e1d4031d86de9a3889b74ae2a6d9c90af8a1da))
- **rest-api:** add endpoints for episodes and full text search for podcasts and
episodes
([85505d4](https://code.castopod.org/adaures/castopod/commit/85505d4b3181c96bc91619e3ab9b0601f8e1c120)),
closes [#296](https://code.castopod.org/adaures/castopod/issues/296)
## [1.3.5](https://code.castopod.org/adaures/castopod/compare/v1.3.4...v1.3.5) (2023-05-09)
### Bug Fixes
- replace essence with embera to create preview cards
([c682f03](https://code.castopod.org/adaures/castopod/commit/c682f03a67c6c0ebbcc6ff45d9a037f6f9823bde))
## [1.3.4](https://code.castopod.org/adaures/castopod/compare/v1.3.3...v1.3.4) (2023-05-05)
### Bug Fixes
- **import-update:** insert episodes incrementally into database
([108fdf8](https://code.castopod.org/adaures/castopod/commit/108fdf84b8dd458fc71a06a77d14069287ab8e42))
## [1.3.3](https://code.castopod.org/adaures/castopod/compare/v1.3.2...v1.3.3) (2023-04-17)
### Bug Fixes
- unnescape podcast title special characters in "find us on" section
([f727276](https://code.castopod.org/adaures/castopod/commit/f727276f820a8ef2c47947f40a37a4a157b509ef)),
closes [#323](https://code.castopod.org/adaures/castopod/issues/323)
- **websub:** add missing misc helper import
([855aacc](https://code.castopod.org/adaures/castopod/commit/855aacce0bf3841a876cd593e668e116149080aa))
## [1.3.2](https://code.castopod.org/adaures/castopod/compare/v1.3.1...v1.3.2) (2023-04-14)
### Bug Fixes
- remove path key when getting default avatar path
([c5a1359](https://code.castopod.org/adaures/castopod/commit/c5a1359218d61c0f78006f2bd5785e317f32bade))
- **s3:** serve files using media base url to allow for CDN setup
([502f53c](https://code.castopod.org/adaures/castopod/commit/502f53c9701da3b8da2caef1eb54df25b7d2d86a))
## [1.3.1](https://code.castopod.org/adaures/castopod/compare/v1.3.0...v1.3.1) (2023-04-13)
### Bug Fixes
- **s3:** add proxy to serve images from s3 to client
([a76724a](https://code.castopod.org/adaures/castopod/commit/a76724a8cfee700f6874f86b35616d61facc664e)),
closes [#321](https://code.castopod.org/adaures/castopod/issues/321)
# [1.3.0](https://code.castopod.org/adaures/castopod/compare/v1.2.4...v1.3.0) (2023-04-03)
### Bug Fixes
- delete files using file_manager when deleting episode and podcast
([41d8efe](https://code.castopod.org/adaures/castopod/commit/41d8efe6e71566eba44bfdfd00d1708ac4338366))
### Features
- **media:** set media storage directory as configurable
([7e1a470](https://code.castopod.org/adaures/castopod/commit/7e1a470ba42172eb4c3864ab3652e9f8b55d1ba8))
## [1.2.4](https://code.castopod.org/adaures/castopod/compare/v1.2.3...v1.2.4) (2023-03-23)
### Bug Fixes
- allow images to have .jpeg extension consistently
([ae5e12b](https://code.castopod.org/adaures/castopod/commit/ae5e12be3b15fe50cb2311abcbbc19ac23b592f6))
- **s3:** delete persons image sizes from bucket + add keyPrefix to config
([208c271](https://code.castopod.org/adaures/castopod/commit/208c2715f900371987c3b75a749fe937a3db1991))
- **s3:** do not create bucket if not exists, check if healthy instead
([da7076f](https://code.castopod.org/adaures/castopod/commit/da7076fc2d49d07708d5adaa99733487b7f52e20))
### Reverts
- **homepage:** remove redirect to install if database is not setup
([d4954e0](https://code.castopod.org/adaures/castopod/commit/d4954e026d5e0d48c5f15ed69d1ce71abb34d1a1))
## [1.2.3](https://code.castopod.org/adaures/castopod/compare/v1.2.2...v1.2.3) (2023-03-18)
### Bug Fixes
- **notifications:** set mark-all-as-read parameter to be podcast_id instead of
actor_id
([2748f23](https://code.castopod.org/adaures/castopod/commit/2748f2313797e50d8a2a7b87df09c0bc6e64360a))
## [1.2.2](https://code.castopod.org/adaures/castopod/compare/v1.2.1...v1.2.2) (2023-03-18)
### Bug Fixes
- **migration:** change old media file_key to file_path
([a414142](https://code.castopod.org/adaures/castopod/commit/a4141421aa1d6e89742b390b042382f729f965a9)),
closes [#314](https://code.castopod.org/adaures/castopod/issues/314)
## [1.2.1](https://code.castopod.org/adaures/castopod/compare/v1.2.0...v1.2.1) (2023-03-17)
### Bug Fixes
- change app.mediaBaseURL to media.baseURL in install, docker entrypoints and
docs
([b3c6e05](https://code.castopod.org/adaures/castopod/commit/b3c6e05e6fcd8a518eeedeefde28b61f879ba71d))
# [1.2.0](https://code.castopod.org/adaures/castopod/compare/v1.1.2...v1.2.0) (2023-03-17)
### Bug Fixes
- **analytics:** check the x_forwarded_for client header
([1111177](https://code.castopod.org/adaures/castopod/commit/1111177eb7fea4eba6d119b17acdf3bf416492ef))
- **auth:** update podcast editors' permissions
([a9b6308](https://code.castopod.org/adaures/castopod/commit/a9b630884bc318499ea7f03862d5752dd5f178e1))
- **contributors:** add dash to prevent deleting permissions from other podcast
([5d2a2d4](https://code.castopod.org/adaures/castopod/commit/5d2a2d49c489cd98f9c9ecbca35fd5d21a9cadfb)),
closes [#310](https://code.castopod.org/adaures/castopod/issues/310)
- display bandwidth limit on dashboard when set in .env
([a2a87ab](https://code.castopod.org/adaures/castopod/commit/a2a87abf7caea3c87bcf2d0988610cc07782de9e))
- **docker:** update nginx configuration
([8884598](https://code.castopod.org/adaures/castopod/commit/8884598a56d0e2550776ef4cee5e53558c20e009))
- **platforms:** update 'submit_url' for Antennapod
([9fc49a7](https://code.castopod.org/adaures/castopod/commit/9fc49a7430406f50e68318c5fd7c577ae1ebd9df))
### Features
- add downloads count to episode list
([b63c1dc](https://code.castopod.org/adaures/castopod/commit/b63c1dc9b1ed41626b99ba852a9a00ed417059ba))
- add health route to check if db, cache and file manager are ok
([1dde11f](https://code.castopod.org/adaures/castopod/commit/1dde11f8e42b66684a956068f5347e9289f4918b))
- **media:** add s3 to manage media files
([d93fc98](https://code.castopod.org/adaures/castopod/commit/d93fc98469ffe93913b65e539dec396891708c70))
### Reverts
- **install:** reset condition to look for instance owner before continuing
install
([fc009f3](https://code.castopod.org/adaures/castopod/commit/fc009f3d0058028bbbb6418603cf820c0f7cea80))
## [1.1.2](https://code.castopod.org/adaures/castopod/compare/v1.1.1...v1.1.2) (2022-12-14)
### Bug Fixes
- **analytics:** set EpisodeAudioController to init user session data
([77ccb30](https://code.castopod.org/adaures/castopod/commit/77ccb306009eb093147c56789535e754f3d85570))
## [1.1.1](https://code.castopod.org/adaures/castopod/compare/v1.1.0...v1.1.1) (2022-12-13)
### Bug Fixes
- **op3:** remove scheme when wraping audio URI
([0ad22e4](https://code.castopod.org/adaures/castopod/commit/0ad22e49bc488e96df5a41495f5b242559b64a45))
- **rss:** add file extension to enclosure url
([964cbba](https://code.castopod.org/adaures/castopod/commit/964cbba54f16556408bf8280c544a52e6be5c9fc))
# [1.1.0](https://code.castopod.org/adaures/castopod/compare/v1.0.5...v1.1.0) (2022-12-09)
### Bug Fixes
- **notifications:** remove cache inconsistencies when marking notification as
read
([46d7054](https://code.castopod.org/adaures/castopod/commit/46d70541d313c836ab0c078ba6121fe5fe956e62))
- **notifications:** retrieve activity from database instead of getting cache
([7fbbd08](https://code.castopod.org/adaures/castopod/commit/7fbbd08da6a37d08608900ad318e72815fe4b0c4))
- **podcast:soundbite:** rename start time attribute to follow spec
([689831c](https://code.castopod.org/adaures/castopod/commit/689831c26c756d454de432900d23bc09a37f890b))
### Features
- **analytics:** add OP3 analytics service option + update episode audio url
([16527ed](https://code.castopod.org/adaures/castopod/commit/16527ed529265f2925e205856c684e34175a8933))
## [1.0.5](https://code.castopod.org/adaures/castopod/compare/v1.0.4...v1.0.5) (2022-11-25)
### Bug Fixes
- **router:** revert to CI4 v4.2.7 to include all routes
([c13cfa0](https://code.castopod.org/adaures/castopod/commit/c13cfa0ea0679751521ca4157b953043ecc7974a))
## [1.0.4](https://code.castopod.org/adaures/castopod/compare/v1.0.3...v1.0.4) (2022-11-21)
### Bug Fixes
- update actorUsername regex to get url_to actor
([1d6b177](https://code.castopod.org/adaures/castopod/commit/1d6b177a55111ede01fba1c08499036d474533bc))
## [1.0.3](https://code.castopod.org/adaures/castopod/compare/v1.0.2...v1.0.3) (2022-11-17)
### Bug Fixes
- **dashboard-ui:** fill the blank gaps between cards on smaller screen sizes
([00836cc](https://code.castopod.org/adaures/castopod/commit/00836cc368c75ae2e23fa5dc4a53a5bb6eb2ce24))
## [1.0.2](https://code.castopod.org/adaures/castopod/compare/v1.0.1...v1.0.2) (2022-11-04)
### Bug Fixes
- **auth:** disallow registration by default
([379b9be](https://code.castopod.org/adaures/castopod/commit/379b9be2b99574fe4af4009b01128dba2c75f037))
- **contributors:** add prefix to podcast group to delete contributor
([9f785db](https://code.castopod.org/adaures/castopod/commit/9f785db7ba674638a6f456aa3626f3f8100911f1))
- extract podcast ids from user groups using a regex
([e26215a](https://code.castopod.org/adaures/castopod/commit/e26215a11fc23aa0ad5ccff8ee97d6c6e8a09c1a))
- **notifications:** add manage-notifications permission to podcast
([ed7c247](https://code.castopod.org/adaures/castopod/commit/ed7c247bcbbb450e5ff96418930d3b37ce912cc4))
- **platforms:** convert special characters to htmlentities to validate url
([82310a2](https://code.castopod.org/adaures/castopod/commit/82310a2e0b426e84501090bdd9c0cf592d1c0d53))
## [1.0.1](https://code.castopod.org/adaures/castopod/compare/v1.0.0...v1.0.1) (2022-11-01)
### Bug Fixes
- **platforms:** trim platform url before validation and storage
([259fe5f](https://code.castopod.org/adaures/castopod/commit/259fe5f697a833e268cde88e959bc19bd662edf6))
# 1.0.0 (2022-10-20)
### Bug Fixes
- **a11y:** replace active tab color to contrast with background on podcast and
episode pages
([f3785e1](https://code.castopod.org/adaures/castopod/commit/f3785e140147d085a2fb6a62ded87cdfe360f442))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **activitypub:** add conditions for possibly missing actor properties + add
user-agent to requests
([8fbf948](https://code.castopod.org/adaures/castopod/commit/8fbf948fbba22ffd33966a1b2ccd42e8f7c1f8a2))
- **activitypub:** add target actor id to like / announce activities to send
directly to note's actor
([962dd30](https://code.castopod.org/adaures/castopod/commit/962dd305f5d3f6eadc68f400e0e8f953827fe20d))
- **activitypub:** add target_actor_id for create activity to broadcast post
reply
([0128a21](https://code.castopod.org/adaures/castopod/commit/0128a21ec55dcc0a2fbf4081dadb4c4737735ba1))
- **activitypub:** allow cors on get requests for routes exposing acitivitypub
objects
([2f24809](https://code.castopod.org/adaures/castopod/commit/2f2480998f9abb34f02ab186c65d462a74b4e640))
- **activitypub:** set created_by to null for reblog if no user + update episode
oembed data
([209dfbd](https://code.castopod.org/adaures/castopod/commit/209dfbd134e1a2cc02e7c24c158d786fa4dda61d))
- add admin-audio-player to vite config to have admin player show up
([93cb9b2](https://code.castopod.org/adaures/castopod/commit/93cb9b24701c09b92820204a67c1fc1b3c044708))
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent - add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error ext_in error
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)), ([339bef8](https://code.podlibre.org/podlibre/castopod-host/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145) closes [#145](https://code.podlibre.org/podlibre/castopod-host/issues/145)
- add category_label component to include parent category in about podcast page
([74e7d68](https://code.castopod.org/adaures/castopod/commit/74e7d68ac834885c4b89ee6e7d60db2157165799)) # [1.0.0-alpha.79](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.78...v1.0.0-alpha.79) (2021-12-20)
- add explicit int conversion when formatting episode duration
([1253096](https://code.castopod.org/adaures/castopod/commit/1253096197a0d30692bdafa7152f250cd9a71acf)) ### Bug Fixes
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
- add href to castopod website on login page
([cc54257](https://code.castopod.org/adaures/castopod/commit/cc5425735184ad738aa0f38540f18e8971f8f56e))
- add missing explicit badge for podcasts and episodes
([cdf9f9d](https://code.castopod.org/adaures/castopod/commit/cdf9f9d53f2597f19455cb65c51da4677bb99327))
- add open graph size for podcast images to replace the inadequate large format
([33aae1f](https://code.castopod.org/adaures/castopod/commit/33aae1f7934e4962116e94e477dbf48e24971f5f))
- add public/media folder to castopod bundle
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
- add translation key for audio-clipper trim labels
([db191ac](https://code.castopod.org/adaures/castopod/commit/db191ac31bd16bad2a72afdb8b25c685adf86a6e))
- add underline and semibold font weight for prose links to have them stand out
([d4d8671](https://code.castopod.org/adaures/castopod/commit/d4d867121c50bded4176a53d7154cf1bb347e306))
- add where condition to get episode count without deleted episodes
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
- **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** set initial value for duration and bandwidth
([ee50539](https://code.castopod.org/adaures/castopod/commit/ee5053959154b1a2e5fbe4b43162968425206a26))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
- **audio-clipper:** add mouse position offset when stretching clip to prevent
content from jumping
([602654b](https://code.castopod.org/adaures/castopod/commit/602654b99b33ee8c29da080058a0aaea976cd484))
- **audio-clipper:** show audio playing progress + put waveform behind audio
clipper
([01a09dc](https://code.castopod.org/adaures/castopod/commit/01a09dc447b81c5412ceb45d6706a867939fd4dd))
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
- **bundle:** include modules and themes when copying files with rsync
([cd5bb88](https://code.castopod.org/adaures/castopod/commit/cd5bb8835c6e259408a8c13a2196a347e161da83))
- **bundle:** update vite input files path + add `set -e` in bash scripts to
fail if command fails
([0ee53c7](https://code.castopod.org/adaures/castopod/commit/0ee53c71ffadb8a6ddb1febd9f912bc99f5f7a0b))
- **cache:** add locale for podcast and episode pages + clear some persisting
cache in models
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61)
- **cache:** delete posts and comments pages cache when updating platform links
([f7c3e5b](https://code.castopod.org/adaures/castopod/commit/f7c3e5bf4ad43389bf8d58d2c4aaf16b81cbce00)),
closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
- **cache:** return a non cached view when connected
([e2e7358](https://code.castopod.org/adaures/castopod/commit/e2e735815d805a48eed2ea3288d060d0ddb253a3))
- **cache:** suffix cache names with authenticated for credits, map and pages
([418a70b](https://code.castopod.org/adaures/castopod/commit/418a70b2a670d8ba0ab6c15fa5faa41f6be55e53))
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- change image size requirement hints
([ea20206](https://code.castopod.org/adaures/castopod/commit/ea20206ee674eb54dd3ea188d2a2e2d41425df65))
- change message upon cancellation of episode publication
([9859c74](https://code.castopod.org/adaures/castopod/commit/9859c7434c2a3478ce035f7a4de20f594d63f5b0))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
- clear cache when deleting podcast banner
([99bb40b](https://code.castopod.org/adaures/castopod/commit/99bb40b8bc17b8ee2cd8468a82e46ea280c92cb6))
- comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- **comments:** add comment view partials for public pages
([fcecbe1](https://code.castopod.org/adaures/castopod/commit/fcecbe1c68b0d28d19454fba65caf3ab769fbc75))
- correct chart data
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
- define podcast_id and platform_slug as foreign keys in podcasts_plaforms table
([6e9451a](https://code.castopod.org/adaures/castopod/commit/6e9451a1103b43750fa70ad576de36af25ca29cb))
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
- **email:** set the correct url in the activation and forgot emails
([10fc6f1](https://code.castopod.org/adaures/castopod/commit/10fc6f17c6838a58348f32ccfd0cf05f9d3e172c)),
closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
- **embed:** open embedded player's links in new tab
([4aa73d7](https://code.castopod.org/adaures/castopod/commit/4aa73d71e3b8c0a6c3f75f4d1d45c4d693aba64c))
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
- **episode-unpublish:** set consistent posts_counts' increments/decrements for
actors and episodes
([8acdafd](https://code.castopod.org/adaures/castopod/commit/8acdafd26044e50a4d6ee451bf24ad66003c5bb3)),
closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
- escape characters for `min` in format_duration_symbol
([3b6722a](https://code.castopod.org/adaures/castopod/commit/3b6722a42b9e4330e5235d4ceed41c777159f4dc))
- escape generated feed tag values and remove new lines from public pages meta
description
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46)
- expire default query cache upon scheduled episode publication
([b72e7c8](https://code.castopod.org/adaures/castopod/commit/b72e7c8691c887e41107baea0a4d50a39eaf8c8b)),
closes [#81](https://code.castopod.org/adaures/castopod/issues/81)
- explicitly cast seconds to int in iso8601_duration helper function
([779653f](https://code.castopod.org/adaures/castopod/commit/779653f75b140942f731cbb238bc0667cc461307))
- **fediverse:** set default castopod avatar url when actor avatar is not
present
([460f52f](https://code.castopod.org/adaures/castopod/commit/460f52f70e493d619c28632db6c698e88f0ebb5f))
- **fediverse:** set model instances as non shared to prevent overlapping
([91128fa](https://code.castopod.org/adaures/castopod/commit/91128fad7a68e1f4e5acacba90b6899288699e61))
- fix layout bugs in admin and update translation files
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
- **get_browser_language:** return defaultLocale if browser doesn't send user
preferred language
([9cc2996](https://code.castopod.org/adaures/castopod/commit/9cc299626181048b85b629bbe7f5806a1f5d21ff))
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
- **home:** remove hardcoded prefix in getAllPodcasts query
([92d5cc5](https://code.castopod.org/adaures/castopod/commit/92d5cc50a3e533875cd894dccc417918102d4b7f))
- **housekeeping:** replace the use of GLOB_BRACE with looping over file
extensions
([42d92d0](https://code.castopod.org/adaures/castopod/commit/42d92d0c8dfe0c567c28f5bfdda129890fa4c2ec)),
closes [#154](https://code.castopod.org/adaures/castopod/issues/154)
- **housekeeping:** set default sizes value + ignore illegal IFD size error to
proceed with script
([f21ca57](https://code.castopod.org/adaures/castopod/commit/f21ca57603cfa503699b7e09a155e18d876d65fe))
- **housekeeping:** use EpisodeModel's builder to reset comments count
([65e9c0b](https://code.castopod.org/adaures/castopod/commit/65e9c0b05ea4992884149cb4a4b071bf31a20a1a))
- **htaccess:** add ? after index.php in RewriteRule
([d9d139e](https://code.castopod.org/adaures/castopod/commit/d9d139eefa03c28d1a064b3b32c9036193497e57)),
closes [#152](https://code.castopod.org/adaures/castopod/issues/152)
- **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be
sent in any order
([b7f285e](https://code.castopod.org/adaures/castopod/commit/b7f285e4e24247fedb94f030356fa6f291f525cc))
- **images:** set default mimetype if none is specified when getting size info
([6e4acc6](https://code.castopod.org/adaures/castopod/commit/6e4acc64ad256178cee7905402b48bafcd49f84c))
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
- **import:** add validation for handle field to prevent
Router.invalidParameterType error
([5bf7200](https://code.castopod.org/adaures/castopod/commit/5bf7200fb390f2447b29f24b495f24483cf7b205)),
closes [#119](https://code.castopod.org/adaures/castopod/issues/119)
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
- **import:** save media files during podcast import + set missing media fields
([a9989d8](https://code.castopod.org/adaures/castopod/commit/a9989d841a634f8cf6c04df25f40bb1e7d4fcdcc))
- **import:** set default episode type if not set
([d7250ab](https://code.castopod.org/adaures/castopod/commit/d7250ab03f9b032830c575ad58b51c8d60b7a49a))
- **import:** set episode and season numbers to null when not present in item - **import:** set episode and season numbers to null when not present in item
tag tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d)) ([3211398](https://code.podlibre.org/podlibre/castopod-host/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c)) # [1.0.0-alpha.78](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.77...v1.0.0-alpha.78) (2021-12-15)
- include missing variables on public ui's episode page and remote_actions
([193b373](https://code.castopod.org/adaures/castopod/commit/193b373bc94a5270acae99b637aa84b6cb2dedfe)) ### Bug Fixes
- **input-component:** unset required attribute to prevent rendering it when
false - **import:** add extension when downloading file without + truncate slug if too
([db9ac13](https://code.castopod.org/adaures/castopod/commit/db9ac13860bce58235a5da275910bea605a00626)) long
- **install:** add password validation when creating super admin ([c5f18bb](https://code.podlibre.org/podlibre/castopod-host/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
([5a2ca0c](https://code.castopod.org/adaures/castopod/commit/5a2ca0cc4ae85cc15960201c86f131cb822f714f))
- **install:** redirect manually to install wizard on first visit # [1.0.0-alpha.77](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.76...v1.0.0-alpha.77) (2021-11-23)
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **install:** redirect to host_url install route on instanceConfig validation ### Bug Fixes
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a)) - **cors:** add preflight option routes for episode, podcast and status objects
- **install:** redirect to input baseUrl after instance config ([a281abf](https://code.podlibre.org/podlibre/castopod-host/commit/a281abfda475388a07943c169dab460cc2d4f944))
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
- **install:** set message block on forms to show error messages
([3a0a20d](https://code.castopod.org/adaures/castopod/commit/3a0a20d59cdae7f166325efb750eaa6e9800ba6e)),
closes [#157](https://code.castopod.org/adaures/castopod/issues/157)
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
- **json-ld:** add missing properties to PodcastSeries object
([e97266c](https://code.castopod.org/adaures/castopod/commit/e97266c5d4883a10f68b3685ecc0d1942f54d658))
- keep subtitle line breaks when parsing srt file to json
([cfb3da6](https://code.castopod.org/adaures/castopod/commit/cfb3da6592f2de23cb1a7ac420f19fc77fa338aa))
- **layouts:** replace holy-grail layout with tailwind config + widen public
podcast layout
([be5a287](https://code.castopod.org/adaures/castopod/commit/be5a28787fdb180b64d9bf570120eff7072ab9aa))
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
- **markdown-editor:** remove unnecessary buttons for podcast and episode
editors + add extensions
([9c4f60e](https://code.castopod.org/adaures/castopod/commit/9c4f60e00bcbd4f784f12d2a6fed357ad402ee2e))
- **md-editor:** build new markdown editor with lit +
github/markdown-toolbar-element
([9ec1cb9](https://code.castopod.org/adaures/castopod/commit/9ec1cb93da6f41124c48b8cf14ee6942e865bede)),
closes [#93](https://code.castopod.org/adaures/castopod/issues/93)
[#94](https://code.castopod.org/adaures/castopod/issues/94)
[#120](https://code.castopod.org/adaures/castopod/issues/120)
- **migrations:** ignore invalid utf8 chars for media files metadata + update
transcript parser
([45e8f99](https://code.castopod.org/adaures/castopod/commit/45e8f99e753cc02ec105e6f4d7fe026a205724f8))
- minor corrections
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- move html escaping on credits page
([fbffdbd](https://code.castopod.org/adaures/castopod/commit/fbffdbde78544c83138ee6234c62d43056f407b6))
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
- **notifications:** add trigger after activities update + update insert trigger
([e5d16e8](https://code.castopod.org/adaures/castopod/commit/e5d16e87119021fa5a43470d67ddfe5128e57f74))
- **notifications:** notify actors after activities insert / update using model
callback methods
([e08555a](https://code.castopod.org/adaures/castopod/commit/e08555a4e9a6c15eeba18273c63403f82eddae35))
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
- overwrite common lang function to escape returned string
([4c490c1](https://code.castopod.org/adaures/castopod/commit/4c490c15bb6642ad0b2aaddf08d8af25de99b4b0)),
closes [#196](https://code.castopod.org/adaures/castopod/issues/196)
[#198](https://code.castopod.org/adaures/castopod/issues/198)
- overwrite getActorById to return app's Actor entity
([f2bc2f7](https://code.castopod.org/adaures/castopod/commit/f2bc2f7e01aa166faa627df6fe4d5ed4887c16e5))
- **package.json:** update destination of postcss generation scripts
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- **pages:** add locale to page cache
([8f999ce](https://code.castopod.org/adaures/castopod/commit/8f999ce2f7ee1416c30cf58c84f67b3d11b3f142))
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
- pass timezone to relative time component to show the localized time in the UI
([b9db936](https://code.castopod.org/adaures/castopod/commit/b9db936461d4cb914958bb3256bb910bbd7ba815))
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
- **persons:** set person picture as optional for better ux
([7fdea63](https://code.castopod.org/adaures/castopod/commit/7fdea63de7e572810082c84fff3013af580df58b)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
- **player-styling:** revert vite to 2.8 to reference the player css
([e07d3af](https://code.castopod.org/adaures/castopod/commit/e07d3afea9af85b8361227e000fb64b502781668))
- **podcast-activity:** check if transcript and chapters are set before
including them in audio
([5855a25](https://code.castopod.org/adaures/castopod/commit/5855a250936f91641efef77650890a18d8e9917f))
- **podcast-import:** move guid attribute declaration for Episode entity to - **podcast-import:** move guid attribute declaration for Episode entity to
include slug data include slug data
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328)) ([5d02ae3](https://code.podlibre.org/podlibre/castopod-host/commit/5d02ae39908a9d743627135b372bf981134c4328))
- **podcast:** use markdown description value for editor + set prose class to
about description # [1.0.0-alpha.76](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.75...v1.0.0-alpha.76) (2021-10-26)
([f304d97](https://code.castopod.org/adaures/castopod/commit/f304d97b14e0ef383509cb3bba50beb55bf701ba)),
closes [#156](https://code.castopod.org/adaures/castopod/issues/156) ### Bug Fixes
- prefill description footer input when creating a new episode
([9ea5ca3](https://code.castopod.org/adaures/castopod/commit/9ea5ca31697c70d176294f8aea37bd57d471fcf7))
- **premium-podcasts:** display unlock button in embed when premium episode
([ca109ba](https://code.castopod.org/adaures/castopod/commit/ca109ba3a8a08e661fd2484454b1983c3418f15d))
- **premium-podcasts:** remove cache in unlock form + redirect to podcast if
podcast is not premium
([242352c](https://code.castopod.org/adaures/castopod/commit/242352c4d9cd936de14e8e8a5d78ebf1287b1f95))
- **premium-podcasts:** return different cached page when podcast is unlocked
([b1303c5](https://code.castopod.org/adaures/castopod/commit/b1303c525517498b0edfb9885ff36e08c72628b5))
- **pwa:** add scope to webmanifests to allow installing an app per podcast
([74c683e](https://code.castopod.org/adaures/castopod/commit/74c683eb44398a84443ec17903c3e002bb5ea9b9))
- **pwa:** set app display as standalone in the webmanifests
([7aa37d2](https://code.castopod.org/adaures/castopod/commit/7aa37d24ac13a1ee160c01a56b43621d7efcfbbc))
- re-order graph values
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- redirect to non cached views when authenticated in public views
([482b47b](https://code.castopod.org/adaures/castopod/commit/482b47ba6bdab7f27fc5704a559567228e07cd14))
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- remove cache from remote follow form to display error messages
([90e4443](https://code.castopod.org/adaures/castopod/commit/90e44437bdf37d8024ef609b2f7336dbdfc3b974))
- remove defer from js script declaration as it is a module
([18ae557](https://code.castopod.org/adaures/castopod/commit/18ae557e97f1cef775cd1e75fb1fedee7f1c0cc9))
- remove fixed size from podcast sidebar + rearrange account info + space out
import radio inputs
([776eec6](https://code.castopod.org/adaures/castopod/commit/776eec6f0d533d6c92ebec16f7a9dbfcde1f41f4))
- remove heavy image cover data from audio file metadata
([f74403b](https://code.castopod.org/adaures/castopod/commit/f74403bd7a5089b760603abe36264e7615be0e78))
- remove required for other_categories field and add podcast_id to latest
podcasts query
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- remove required property to persons picture
([c546be3](https://code.castopod.org/adaures/castopod/commit/c546be385b243014243ae93356006cd126d2f00d)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- remove value escaping for form inputs and textareas
([bc6dea2](https://code.castopod.org/adaures/castopod/commit/bc6dea2f8ad1cf0aee0eaa93151332fbac7fb771))
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
- rename issue_templates labels
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
- rename podcast name to podcast handle to clarify field usage
([9dd4c77](https://code.castopod.org/adaures/castopod/commit/9dd4c7741eb1b7cb5fc214ff674697f3aa986df0)),
closes [#126](https://code.castopod.org/adaures/castopod/issues/126)
- reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace deletedField with published_at for episodes
([14d7d07](https://code.castopod.org/adaures/castopod/commit/14d7d078225cdc8980759273a5dc4163d9f84b06))
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
- replace hardcoded style links with vite service + set default value for remote - replace hardcoded style links with vite service + set default value for remote
transcript url transcript url
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)), ([3f2e056](https://code.podlibre.org/podlibre/castopod-host/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149) closes [#149](https://code.podlibre.org/podlibre/castopod-host/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150) [#150](https://code.podlibre.org/podlibre/castopod-host/issues/150)
- replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191)) # [1.0.0-alpha.75](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.74...v1.0.0-alpha.75) (2021-10-05)
- restore default podcast icon on public website
([342778b](https://code.castopod.org/adaures/castopod/commit/342778bac3c684328d72633961df1a2ebdc1330e)) ### Bug Fixes
- revert to beta.1's codeigniter4 version
([e831411](https://code.castopod.org/adaures/castopod/commit/e83141127080ccde44987195db46ba97fd6cc2ca))
- rewrite regenerate image function to use saveSizes method from Image entity
([3889912](https://code.castopod.org/adaures/castopod/commit/38899124ec27e94a8c798bc2db528f9f785eec20))
- **router:** check if Accept header is set before getting value
([10a2ae0](https://code.castopod.org/adaures/castopod/commit/10a2ae02484672d6a0fbc6e7b943519c5ec16cb6)),
closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
- **router:** trim URI slash to match same routes for URIs with and without
trailing slash
([9e9375f](https://code.castopod.org/adaures/castopod/commit/9e9375f9a2cd6102f827b36ec521f4c86a557c00))
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
- **rss:** cast number type values to string in rss_helper - **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)), ([7180ae9](https://code.podlibre.org/podlibre/castopod-host/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148) closes [#148](https://code.podlibre.org/podlibre/castopod-host/issues/148)
- **rss:** do not escape podcast and episode titles in the xml
([0dd3b7e](https://code.castopod.org/adaures/castopod/commit/0dd3b7e0bf00d5a9eb80c93cba1efcada59ec3c1)), # [1.0.0-alpha.74](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2021-09-28)
closes [#138](https://code.castopod.org/adaures/castopod/issues/138)
[#71](https://code.castopod.org/adaures/castopod/issues/71) ### Features
- **rss:** remove escaping for publisher and owner name
([6fc6347](https://code.castopod.org/adaures/castopod/commit/6fc6347846c126618cb7ff50164181650308d0c0)) - **platforms:** add missing newpodcastapps.com's platforms
- **rss:** round episode durations and soundbites ([92dd370](https://code.podlibre.org/podlibre/castopod-host/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
([c9fb987](https://code.castopod.org/adaures/castopod/commit/c9fb987fcfbe17069ec68fdbc823777079ce574b)),
closes [#214](https://code.castopod.org/adaures/castopod/issues/214) # [1.0.0-alpha.73](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.72...v1.0.0-alpha.73) (2021-09-22)
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)), ### Bug Fixes
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
- **rss:** use originalPath instead of originalMediaPath in Image library - **map:** update episode markers query to discard unpublished episodes
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920)) ([b3caac4](https://code.podlibre.org/podlibre/castopod-host/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47)) # [1.0.0-alpha.72](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.71...v1.0.0-alpha.72) (2021-09-20)
- **search-episodes:** add fallback sql query using LIKE for search query with
less than 4 characters ### Bug Fixes
([e66bf44](https://code.castopod.org/adaures/castopod/commit/e66bf44341175bc5a10fbf7dfa00b351e76136c2)),
closes [#236](https://code.castopod.org/adaures/castopod/issues/236) - rename field status to task_status to get scheduled activities
- **security:** add csrf filter + prevent xss attacks by escaping user input ([4ff82a5](https://code.podlibre.org/podlibre/castopod-host/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
([cd2e1e1](https://code.castopod.org/adaures/castopod/commit/cd2e1e1dc37c53d32d00971c451c4800b8fd6107))
- set cache expiration to next note publish to show note on publication date # [1.0.0-alpha.71](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.70...v1.0.0-alpha.71) (2021-09-17)
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value ### Features
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set episode duration translation to hardcoded english - **map:** display geolocated episodes on a map page
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)), ([4357cc2](https://code.podlibre.org/podlibre/castopod-host/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
- set episode guid upon episode creation # [1.0.0-alpha.70](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.69...v1.0.0-alpha.70) (2021-08-31)
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48) ### Bug Fixes
- set episode numbers during import + remove all custom form_helpers + minor ui
issues - **partner:** set correct image URL
([99a3b8d](https://code.castopod.org/adaures/castopod/commit/99a3b8d33e00482da50dd62bdaa9215a351a56e4)) ([61554be](https://code.podlibre.org/podlibre/castopod-host/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
- set interact_as_actor for user upon password reset
([ad8f5f5](https://code.castopod.org/adaures/castopod/commit/ad8f5f5a0fac7b0b9cc10a0b86200f014aca7553)), # [1.0.0-alpha.69](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.68...v1.0.0-alpha.69) (2021-08-23)
closes [#178](https://code.castopod.org/adaures/castopod/issues/178)
- set localized slug_field key as string in french language ### Bug Fixes
([17fb29b](https://code.castopod.org/adaures/castopod/commit/17fb29b20993b7deee4e252e0e3a4a2459ee0d98))
- set location to null when getting empty string - **import:** cast description's SimpleXMLElement to string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6)) ([02d17be](https://code.podlibre.org/podlibre/castopod-host/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
- set storage limit as disk_total_space instead of free space
([7512e2e](https://code.castopod.org/adaures/castopod/commit/7512e2ed1ff5656cd63a4fc2524296dbb8b4164a)) # [1.0.0-alpha.68](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.67...v1.0.0-alpha.68) (2021-08-19)
- **settings:** add .jpg extension to site-icon file input to display all jpeg
images ### Bug Fixes
([f611a16](https://code.castopod.org/adaures/castopod/commit/f611a16cd0c1a389e1c5a287eaec9d2a927a4bb6))
- **socialinteract:** move social interact uri into uri attribute + update - **analytics:** redirect to mp3 file even when referer was not set
social data upon import ([9fc388d](https://code.podlibre.org/podlibre/castopod-host/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
([12b2200](https://code.castopod.org/adaures/castopod/commit/12b22008a237185cb736fc29352fab22421dad16))
- sort episodes by published_at with unpublished episodes at the begining # [1.0.0-alpha.67](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.66...v1.0.0-alpha.67) (2021-07-24)
([1686f84](https://code.castopod.org/adaures/castopod/commit/1686f840d16f2bd3d71d7f222a59b8e6a838fd6e)),
closes [#249](https://code.castopod.org/adaures/castopod/issues/249) ### Features
- sort episodic podcasts by season
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74)) - allow cross origin requests on episode comments
- **themes:** update themes stylesheet route and remove css extension ([e12f95a](https://code.podlibre.org/podlibre/castopod-host/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
([e4e7e00](https://code.castopod.org/adaures/castopod/commit/e4e7e0005e931967dd6162588f1c5913dbf4603e))
- **types:** update fake seeders types + fix bugs # [1.0.0-alpha.66](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.65...v1.0.0-alpha.66) (2021-07-24)
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- **ui:** remove empty tooltip when hovering on sponsor button ### Features
([40aa661](https://code.castopod.org/adaures/castopod/commit/40aa661289e1d1517fffcea5d257183bc9c458e4))
- unpublish episode before deleting it + add validation step before deletion - **rss:** add podcast:comments tag to link to episode comments
([f75bd76](https://code.castopod.org/adaures/castopod/commit/f75bd76458eeb01a2d37912695e33f77d03b7a69)), ([32e8c7c](https://code.podlibre.org/podlibre/castopod-host/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
closes [#112](https://code.castopod.org/adaures/castopod/issues/112)
[#55](https://code.castopod.org/adaures/castopod/issues/55) # [1.0.0-alpha.65](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2021-07-22)
- update .htaccess for shared hosting config
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e)) ### Bug Fixes
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- update conditions when checking for empty max_episodes and season_number - update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45)) ([fbad0b5](https://code.podlibre.org/podlibre/castopod-host/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
- update form_textarea to prevent escaping value
([78548b5](https://code.castopod.org/adaures/castopod/commit/78548b5cd75ea7d6688d1945ff5449ea4f6bec68)) # [1.0.0-alpha.64](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2021-07-12)
- update iso-369 language table seeder
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- update ivoox podcasting icon
([f2b69a4](https://code.castopod.org/adaures/castopod/commit/f2b69a47339c887f57883ec612f3d200e512ac1c))
- update MarkdownEditor component + restyle Button and other components
([b05d177](https://code.castopod.org/adaures/castopod/commit/b05d177f1b7f44fef043ac5eb41f07133a2cf52d))
- update purgecss content path for php helper files
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
- update translations for settings' tasks to include what they should be used
for
([06b1a8b](https://code.castopod.org/adaures/castopod/commit/06b1a8b29b6ce5d81c5570d250bdac4e0c9ee5ca))
- use slash instead of backslash to call layout
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
- use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries
([4e22a0d](https://code.castopod.org/adaures/castopod/commit/4e22a0d5e4b60941d41071f059aac80cbaf38fbf))
- **users:** remove required roles input when editing user + prevent owner's
roles from being edited
([1c8af75](https://code.castopod.org/adaures/castopod/commit/1c8af7550ba27d8c8473ae96acd21ad7731fd863)),
closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
- **ux:** have podcast dashboard card link to podcast dashboard if only one
podcast in instance
([7dabee5](https://code.castopod.org/adaures/castopod/commit/7dabee58a187abe92358d962da506a836e29cda3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- validate slug length when submitting episode form + clean permalink edit
prefix
([b07ac09](https://code.castopod.org/adaures/castopod/commit/b07ac093b2cae646f9a897bc9dfeeaef6eda6561))
- **video-clips:** check if created video exists before recreating it and
failing
([dff1208](https://code.castopod.org/adaures/castopod/commit/dff12087251b2b89e195604202094b5ddd9a0936))
- **video-clips:** clear video clip cache after process has finished
([3ae6232](https://code.castopod.org/adaures/castopod/commit/3ae62325856f6ff331a5d9ed901b9fa097ca7055))
- **video-clips:** create unique temporary files for resources to be deleted
after generation
([7f7c878](https://code.castopod.org/adaures/castopod/commit/7f7c878cb6ecf7b4a967b2af87da82bc6593081e))
- **video-clips:** set audio codec to aac, fixing audio issue on twitter
([3c22c68](https://code.castopod.org/adaures/castopod/commit/3c22c68ee81f77bd7fcf7e2739ee6af016407843))
- **video-clips:** set longer podcast and episode lengths for squared format
([c030113](https://code.castopod.org/adaures/castopod/commit/c0301134c2048dc29eb2b995e4d5c22c49444100))
- **video-clips:** tweak portrait parameters to have subtitles display without
overflowing
([2385b1a](https://code.castopod.org/adaures/castopod/commit/2385b1a2926d1344569836e18cb30adb4c604664))
- **video-clips:** update condition to check if ffmpeg is installed
([b57f0b6](https://code.castopod.org/adaures/castopod/commit/b57f0b6eb65dccf22cb4d55f93d18ca36857d7fc)),
closes [#163](https://code.castopod.org/adaures/castopod/issues/163)
- **xml-editor:** escape xml editor's content + restyle form sections to prevent
overflowing
([588590b](https://code.castopod.org/adaures/castopod/commit/588590bd2c0346e2465ff8f1930580d76a3bf068))
- **xml-editor:** prettify xml even without root node
([ca55c24](https://code.castopod.org/adaures/castopod/commit/ca55c248d0562a8529071c1f10be12f40ef50dda))
### Features ### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments - **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013)) ([9e1e5d2](https://code.podlibre.org/podlibre/castopod-host/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
- add about page in admin with instance info + database update button
([d0836f3](https://code.castopod.org/adaures/castopod/commit/d0836f3ee360a836f815c59ea755f288501dc517)) # [1.0.0-alpha.63](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2021-07-12)
- add alternate rss feed link tag to podcast page head
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)), ### Features
closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
- add analytics and unknown useragents
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add audio-clipper toolbar + add video-clip-previewer
([0255753](https://code.castopod.org/adaures/castopod/commit/02557539e6eb48fc23ee2ee3b0c75aee3310965b))
- add audio-clipper webcomponent (wip)
([21d4251](https://code.castopod.org/adaures/castopod/commit/21d4251b9bcd5acb0f8a1761bc4edc34a3dbc228))
- add autofocus to input field "Email or username" on login page
([19caed4](https://code.castopod.org/adaures/castopod/commit/19caed4bce0daab9ccf6ab9645f44b60eb87de88))
- add basic stats on podcast about page
([1670558](https://code.castopod.org/adaures/castopod/commit/1670558473dba47219d470ff21d6224db6ab42ba))
- add breadcrumb in admin area
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
- add codemirror to display xml editor for custom rss field
([f15f262](https://code.castopod.org/adaures/castopod/commit/f15f26240cd5311fa9d07779f364b6639a501dec))
- add cumulative listening time charts
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
- add default icons to Alert component
([0d98001](https://code.castopod.org/adaures/castopod/commit/0d9800123b135e4fa1a2acd14a5e039c12174333))
- add DropdownMenu component + remove global audio player in admin
([abb7fba](https://code.castopod.org/adaures/castopod/commit/abb7fbac276d77b7d31a0aeba75d464f3ba3ad46))
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- add french translation
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add heading component + update ecs rules to fix views
([23bdc6f](https://code.castopod.org/adaures/castopod/commit/23bdc6f8e36b7e8dfbe32755a54dea59ad913432))
- add housekeeping task to run after migrations
([89dee41](https://code.castopod.org/adaures/castopod/commit/89dee41d583e57251ea9315402a757f03571d7ad))
- add install wizard form to bootstrap database and create the first superadmin
user
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
- add instructions on production error page to ease Castopod debugging process
([9eab54e](https://code.castopod.org/adaures/castopod/commit/9eab54e0853ccb8300d9f9b743cd84aefbf06549)),
closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
- add ISO 3166 country codes
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add js audio player on podcast, admin and embeddable player pages + fix admon
episodes ux
([0e14eb4](https://code.castopod.org/adaures/castopod/commit/0e14eb4d3f526b0fd256a6144f3fbfc3fe52a357)),
closes [#131](https://code.castopod.org/adaures/castopod/issues/131)
- add label to sponsor button on podcast page
([c29c018](https://code.castopod.org/adaures/castopod/commit/c29c018c7a543fc9398b5d7d11f086123e2b33f2)),
closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
- add legalNoticeURL to app config for setting an external url to legal notice
([711843a](https://code.castopod.org/adaures/castopod/commit/711843a0c81e1e2ec7a015431786df4ef32d5092))
- add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
- add map analytics, add episodes analytics, clean analytics page layout,
translate countries
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add media entity and link documents, images and audio files to it
([6ecf286](https://code.castopod.org/adaures/castopod/commit/6ecf2866cfcde31a0840f15c3340808ce14b44cf))
- add notifications inbox for actors
([999999e](https://code.castopod.org/adaures/castopod/commit/999999e3efab7b1aad7568e4fd114dc7bac04f38)),
closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
- add Noto Sans Mono font to use for durations + button to access new video clip
form in list
([7609bb6](https://code.castopod.org/adaures/castopod/commit/7609bb60330539aa91bfdafbb35c2d585624218a))
- add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
- add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
- add permanent delete feature for podcasts 🎉
([dbb4030](https://code.castopod.org/adaures/castopod/commit/dbb4030da49f9ea1f61759fb7c66d71fc29ea4a1)),
closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
- add platform models
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add podcast banner field for each podcast + refactor images configuration
([4a8147b](https://code.castopod.org/adaures/castopod/commit/4a8147bfbbd98d9badfc57a0f2a18bdd5812e802))
- add premium podcasts to manage subscriptions for premium episodes
([3234500](https://code.castopod.org/adaures/castopod/commit/3234500e2d967438ad140f65da801a543f43775d)),
closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
- add publish feature for podcasts and set draft by default
([3d363f2](https://code.castopod.org/adaures/castopod/commit/3d363f2efe99836ac05c305a2fa683e342f06561)),
closes [#128](https://code.castopod.org/adaures/castopod/issues/128)
[#220](https://code.castopod.org/adaures/castopod/issues/220)
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
- add replied to post or comment to reply element
([d0f9c60](https://code.castopod.org/adaures/castopod/commit/d0f9c6018f1af527099f3e26b5d824710fa11caf))
- add schema.org json-ld objects to podcasts, episodes, posts and comments pages
([902f959](https://code.castopod.org/adaures/castopod/commit/902f959b30a10839684f093eb86edebc5d826a0b))
- add task to housekeeping setting for resetting all instance counts
([9303e51](https://code.castopod.org/adaures/castopod/commit/9303e51bc50d730a8026f58984e83b840360ee88))
- add unique listeners analytics
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add update rss feed feature for podcasts to import their latest episodes
([5eb9dc1](https://code.castopod.org/adaures/castopod/commit/5eb9dc168eb9af04767829b76242c9120f55d46d)),
closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
- add user permissions and basic groups to handle authorizations
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18)
- add WebSub module for pushing feed updates to open hubs
([10d3f73](https://code.castopod.org/adaures/castopod/commit/10d3f73786ba141e27a822b2585c4a244ee92c14))
- **admin:** add instance wide dashboard with storage and bandwidth usage
([b1a6c02](https://code.castopod.org/adaures/castopod/commit/b1a6c02e56fdc01a7ff69fa7e7dd8ea71380b7ba)),
closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
- **admin:** add search form in podcast episodes list
([6be5d12](https://code.castopod.org/adaures/castopod/commit/6be5d12877342a7c56e25ea8dd15a975c6ce45ac)),
closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
- **admin:** make header stick on scroll and show title + action buttons using
css only
([d60498c](https://code.castopod.org/adaures/castopod/commit/d60498c1beb970a14eeb3bbe02d1b1d8116624b0))
- **admin:** update admin layout for better ux + update brand pine colors
([d86142e](https://code.castopod.org/adaures/castopod/commit/d86142ebe7cd7582835f180b79fbeaaaba703528))
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
- **analytics-gdpr:** update cached personal data to expire at midnight
([0188b67](https://code.castopod.org/adaures/castopod/commit/0188b67354a756f0c926edd7b46623ab5b20c12b))
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
- **analytics:** add charts and data export
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
- **analytics:** add current date and secret salt to analytics hash for improved
privacy
([6f2e7c0](https://code.castopod.org/adaures/castopod/commit/6f2e7c009c24830d4f08633bfbde3b75f40bf215))
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
- **api:** add rest api with podcasts read endpoints
([e64001d](https://code.castopod.org/adaures/castopod/commit/e64001d00604bcf587ec5e9a631282f212df450d)),
closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
- apply colour theme to embed player
([9548337](https://code.castopod.org/adaures/castopod/commit/9548337a7c49879e8b58c2dfece46e3cfc9517eb)),
closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
- **auth:** add auth.enable2FA config to enable two-factor authentication
([7213ed2](https://code.castopod.org/adaures/castopod/commit/7213ed290c977ce8723f6d92addadc03913576ee))
- build hashed static files to renew browser cache - build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)), ([37c54d2](https://code.podlibre.org/podlibre/castopod-host/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107) closes [#107](https://code.podlibre.org/podlibre/castopod-host/issues/107)
- **cache:** add podcast and episode pages to cache + clear them after insert or
update # [1.0.0-alpha.62](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.61...v1.0.0-alpha.62) (2021-07-02)
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds ### Bug Fixes
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **clips:** setup clip entities and model + save video clip to have it - **episode:** replace guid's empty string value to null
generated in the background ([441052a](https://code.podlibre.org/podlibre/castopod-host/commit/441052af8d99e6e317edefd1e58ad71799357088))
([2f6fdf9](https://code.castopod.org/adaures/castopod/commit/2f6fdf9091d52ca49709fc82621ba1c6dd0e817d))
- **comments:** add comments to episodes + update naming of status to post # [1.0.0-alpha.61](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2021-06-23)
([bb4752c](https://code.castopod.org/adaures/castopod/commit/bb4752c35e086664f5fd75fdc0d56546a1e356f6))
- **comments:** add like / undo like to comment + add comment page ### Bug Fixes
([0c187ef](https://code.castopod.org/adaures/castopod/commit/0c187ef7a9278a60bcc6e5ee4d69d948b51e5c54))
- **components:** add custom view renderer with ComponentRenderer adapted from - **release:** add missing version number to castopod-host package
bonfire2 ([8f3e9d9](https://code.podlibre.org/podlibre/castopod-host/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
([a95de8b](https://code.castopod.org/adaures/castopod/commit/a95de8bab010f6b01c598da72191abe97e473687)) - **ux:** allow for empty message upon episode publication and warn user on
- create optimized & resized images upon upload submit
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)), ([33d01b8](https://code.podlibre.org/podlibre/castopod-host/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6) closes [#129](https://code.podlibre.org/podlibre/castopod-host/issues/129)
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭ # [1.0.0-alpha.60](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2021-06-21)
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
- **datetime-picker:** set material_green theme to flatpickr ### Features
([3ce6541](https://code.castopod.org/adaures/castopod/commit/3ce6541003260677e722a916ad6bc83ef47c4371))
- **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
- display legal disclaimer and warning on podcast import page
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
- edit + delete podcast and episode
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
- enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9)
- enhance ui using javascript in admin area
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- **episode-unpublish:** remove episode comments upon unpublish
([78acd7f](https://code.castopod.org/adaures/castopod/commit/78acd7f5c057c82507d801c424040296dbaba586))
- **episode:** add form to allow editing episode's publication date to a past
date
([d783d16](https://code.castopod.org/adaures/castopod/commit/d783d16eb73d3f896a3dea39a766b4e963e53abf)),
closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
- **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
- **episodes:** replace soft delete with permanent delete
([eb9ff52](https://code.castopod.org/adaures/castopod/commit/eb9ff522c25af8ceb2ed08614b581757ee791d42))
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
- **fonts:** replace Montserrat with Inter for better readablity
([bfa11d0](https://code.castopod.org/adaures/castopod/commit/bfa11d007d04b8ac714c8cf3b8050a6aaf177a26))
- **GDPR:** add GDPR.yml file to public/.well-known/
([86bccc3](https://code.castopod.org/adaures/castopod/commit/86bccc3d5cc9562b89196f1766ac91cdc8ad786d))
- **gdpr:** add purpose for granting access to premium content
([47d6d81](https://code.castopod.org/adaures/castopod/commit/47d6d81b798ec3ed467e0f4339c98c8a6b80cecd))
- **home:** sort podcasts by recent activity + add dropdown menu to choose
between sorting options
([7b89da6](https://code.castopod.org/adaures/castopod/commit/7b89da6106c150708782d39ed2742fe416c41e89)),
closes [#164](https://code.castopod.org/adaures/castopod/issues/164)
- **housekeeping:** add clear_cache option to flush redis or files cache
([99bfac0](https://code.castopod.org/adaures/castopod/commit/99bfac0b428a4bc6fe8bfd10a355dfd93f42ba5c))
- **i18n:** add 7 new languages + update german translations
([d021abb](https://code.castopod.org/adaures/castopod/commit/d021abb52f5525d93810e25df2b453c918d7bc8b))
- **i18n:** add german language as supported locale + create Language files from
english source
([c220b31](https://code.castopod.org/adaures/castopod/commit/c220b310ed59cad188af044b1fed0c39efc7da5b))
- **i18n:** add Norwegian Nynorsk to supported locales
([ced61fc](https://code.castopod.org/adaures/castopod/commit/ced61fc2364f954c1f6e0208b572faf5741498a8))
- **i18n:** add Polish translation
([2d83b44](https://code.castopod.org/adaures/castopod/commit/2d83b44add9e4e00766a1f326377ed892f48ad73))
- **i18n:** add Spanish to supported locales
([e340b54](https://code.castopod.org/adaures/castopod/commit/e340b54a84d7dcdf9ba910fe7ff39c453fac0968))
- **i18n:** add support for German and Brazilian Portuguese languages
([c9b9fe4](https://code.castopod.org/adaures/castopod/commit/c9b9fe4ee893de9a1df7f8269c39d08a90d205d6))
- **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca)
locales
([48d1443](https://code.castopod.org/adaures/castopod/commit/48d14434727c3310a391160c7af02c56b7e20425))
- **icons:** add default icons for podcasting, social and funding platforms +
remove complex icons
([5bcdfeb](https://code.castopod.org/adaures/castopod/commit/5bcdfebe6489b5d6b90f3c828b014ec4e9a7e7e1)),
closes [#166](https://code.castopod.org/adaures/castopod/issues/166)
[#167](https://code.castopod.org/adaures/castopod/issues/167)
[#170](https://code.castopod.org/adaures/castopod/issues/170)
- **icons:** add podnews icon to podcasting platforms
([5f42355](https://code.castopod.org/adaures/castopod/commit/5f423557c2b78fd7c38c5e0caab6c6c80d21e36e)),
closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
- import podcast from an rss feed url
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
- integrate stylized form components and update podcast edit page
([6536729](https://code.castopod.org/adaures/castopod/commit/653672954606a23796e8a7bda3c34fd6b92f84e0))
- make displayed publication time as relative time using @github/time-elements
([230e139](https://code.castopod.org/adaures/castopod/commit/230e139e43324b9ebef06ca8f6e13b3d9a7bdc70))
- make episode description more visible on episode pages
([90533be](https://code.castopod.org/adaures/castopod/commit/90533be0298249e5527870c01329fce5f94ec2dc)),
closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
- **media:** clean media api + create an entity per media type
([fafaa7e](https://code.castopod.org/adaures/castopod/commit/fafaa7e689b17f09a2b056081fa1f4fc53bf716b))
- **media:** save audio, images, transcripts and chapters to media for episode
and persons
([58e2a00](https://code.castopod.org/adaures/castopod/commit/58e2a00a87fa7d5b188e13cc521d94f0cfddba50))
- **meta-tags:** add activitypub alternate links to podcast, episode, comment
and post pages
([bd61752](https://code.castopod.org/adaures/castopod/commit/bd61752be2f574323b05d1d0aee0df55adf9a74e))
- minor corrections to some tables
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
- **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about
the castopod instance
([88fddc8](https://code.castopod.org/adaures/castopod/commit/88fddc81d730978f2a4d8a671936b54041e3fe45))
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
- **podcast-form:** add new_feed_url field to set an url when changing domain or
host
([e7eec48](https://code.castopod.org/adaures/castopod/commit/e7eec48e7bc06a9aa907db01ed3e5b536e7dd8be))
- **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **podcast:** create a podcast using form
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- **podcasting 2.0:** update podcast:social tag to adhere to latest spec
([a597cf4](https://code.castopod.org/adaures/castopod/commit/a597cf4ecfa6807a3413177d99c816056a7e7c45))
- prefill season and episode numbers + set episode number as mandatory for
serial podcasts
([07d740b](https://code.castopod.org/adaures/castopod/commit/07d740b79f9283e389e723954f680f909ce5de4a)),
closes [#134](https://code.castopod.org/adaures/castopod/issues/134)
[#136](https://code.castopod.org/adaures/castopod/issues/136)
- **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13)
- **pwa:** add service-worker + webmanifest for each podcasts to have them
install on devices
([fee2c1c](https://code.castopod.org/adaures/castopod/commit/fee2c1c0d0d03c4ff0a6a207b0a5e0c22bb7b13a))
- redesign public podcast and episode pages + remove any information clutter for
better ux
([9321400](https://code.castopod.org/adaures/castopod/commit/932140077c671f0486a2cd08ceb6126c7ecde87f))
- replace form helper functions with components in admin template
([e64548b](https://code.castopod.org/adaures/castopod/commit/e64548b982ba47ff35f2272e2e30dd85eeba950b))
- replace slug field with interactive permalink component
([578022b](https://code.castopod.org/adaures/castopod/commit/578022b8c5163ffaf8db5870ed5ec9d5d9536477))
- restyle episode and person cards + add focus style to interactive elements for
a11y
([a505a1d](https://code.castopod.org/adaures/castopod/commit/a505a1de56e8e3056379bd60d0595f432e294728))
- **rss:** add ˂podcast:guid˃ tag for channel - **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c)) ([1fab10e](https://code.podlibre.org/podlibre/castopod-host/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)), # [1.0.0-alpha.59](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.58...v1.0.0-alpha.59) (2021-06-15)
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75) ### Bug Fixes
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80) - check that additional files are valid when creating episode
- **rss:** add podcast:comments tag to link to episode comments ([eac5bc8](https://code.podlibre.org/podlibre/castopod-host/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
- **rss:** add podcast:location tag # [1.0.0-alpha.58](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.57...v1.0.0-alpha.58) (2021-06-11)
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
- **rss:** add rss feed route without the `.xml` extension ### Bug Fixes
([94c0b7c](https://code.castopod.org/adaures/castopod/commit/94c0b7c15920dae9ade5cdc79c7996dbfe82ba05)),
closes [#247](https://code.castopod.org/adaures/castopod/issues/247) - cast actor_id to pass as int to set_interact_as_actor() function
- **rss:** add soundbites according to the podcastindex specs ([56a8e5d](https://code.podlibre.org/podlibre/castopod-host/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)), - **analytics:** set duration field to precise decimal as episode's audio file
closes [#83](https://code.castopod.org/adaures/castopod/issues/83) duration
- **rss:** add transcript and chapters support ([d772685](https://code.podlibre.org/podlibre/castopod-host/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)), - **analytics:** update migrations to set decimal precision for latitude and
closes [#72](https://code.castopod.org/adaures/castopod/issues/72) longitude
[#82](https://code.castopod.org/adaures/castopod/issues/82) ([714d6b5](https://code.podlibre.org/podlibre/castopod-host/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- **rss:** generate rss feed from podcast entity - check for database connection and podcasts table existence before redirecting
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666)) to install
- **rss:** update monetization tag so that it meets PodcastIndex requirements ([eb74e81](https://code.podlibre.org/podlibre/castopod-host/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e)) - save transcript and chapters files to podcasts folder
- **select:** enhance select input with choices.js ([63f49c7](https://code.podlibre.org/podlibre/castopod-host/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
([910d457](https://code.castopod.org/adaures/castopod/commit/910d457cf843e0fc334b3505a4727d51633395ac)) - set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.podlibre.org/podlibre/castopod-host/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.podlibre.org/podlibre/castopod-host/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set location to null when getting empty string
([71b1b5f](https://code.podlibre.org/podlibre/castopod-host/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- update condition in home controller to redirect to install page
([33f1b91](https://code.podlibre.org/podlibre/castopod-host/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.podlibre.org/podlibre/castopod-host/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.podlibre.org/podlibre/castopod-host/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.podlibre.org/podlibre/castopod-host/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.podlibre.org/podlibre/castopod-host/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.podlibre.org/podlibre/castopod-host/commit/76a4bf344160df679db29e236e7df7822970fb60))
- update broken contributor dropdown fields
([e5b7515](https://code.podlibre.org/podlibre/castopod-host/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.podlibre.org/podlibre/castopod-host/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- update condition in AnalyticsTrait
([fbc0967](https://code.podlibre.org/podlibre/castopod-host/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
### Performance Improvements
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.podlibre.org/podlibre/castopod-host/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.podlibre.org/podlibre/castopod-host/commit/433745f194c73407999b207090478563283876a5))
# [1.0.0-alpha.57](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.56...v1.0.0-alpha.57) (2021-05-12)
### Bug Fixes
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.podlibre.org/podlibre/castopod-host/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
# [1.0.0-alpha.56](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.55...v1.0.0-alpha.56) (2021-05-12)
### Bug Fixes
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.podlibre.org/podlibre/castopod-host/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
# [1.0.0-alpha.55](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.54...v1.0.0-alpha.55) (2021-05-03)
### Features
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.podlibre.org/podlibre/castopod-host/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
# [1.0.0-alpha.54](https://code.podlibre.org/podlibre/castopod-host/compare/v1.0.0-alpha.53...v1.0.0-alpha.54) (2021-05-03)
### Features
- set app parameter forceGlobalSecureRequests = true forcing requests to go - set app parameter forceGlobalSecureRequests = true forcing requests to go
through https through https
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117)) ([d9dff1b](https://code.podlibre.org/podlibre/castopod-host/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- set podcast / episode description in the pages description meta tag
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
- **settings:** add general config for instance (site name, description and
icon)
([5c56f3e](https://code.castopod.org/adaures/castopod/commit/5c56f3e6f00a61af2ccf50811c155c325f2b10fa))
- **settings:** add theme settings to set an accent color for all public pages
([5c529a8](https://code.castopod.org/adaures/castopod/commit/5c529a83aa6d6147d94e5aee996e6b0ab02f0ce4))
- simplify podcast page's layout for better ux
([2c0efc6](https://code.castopod.org/adaures/castopod/commit/2c0efc6563604dd067be88cfc9ddd88a01745e64))
- **soundbites:** add soundbite list and creation forms with audio-clipper
component
([de19317](https://code.castopod.org/adaures/castopod/commit/de19317138a2106deb825c1eed7dda036ed7dac3))
- style file inputs using tailwind's file class
([8208ab6](https://code.castopod.org/adaures/castopod/commit/8208ab6785aae8c49f78eb9ac8cd53d77ec8e5e5))
- **themes:** add ViewThemes library to set views in root themes folder
([7a27676](https://code.castopod.org/adaures/castopod/commit/7a276764e6a1ee3619d9d3488f6163215db75338))
- **themes:** set different default banner per theme
([11c916f](https://code.castopod.org/adaures/castopod/commit/11c916fe433eb749ac32230c48e256057564cbb0))
- **themes:** set generic css variables for colors to enable instance themes
([a746a78](https://code.castopod.org/adaures/castopod/commit/a746a781b4bfc78209cf8302c6d7bb3cb452e446))
- toggle podcast sidebar on smaller screens
([f0205ec](https://code.castopod.org/adaures/castopod/commit/f0205ec274414e881cba40d6776126f05eaee583))
- **transcript:** parse srt subtitles into json file + add max file size info
below audio file input
([0098761](https://code.castopod.org/adaures/castopod/commit/00987610a068c8d6cdd4421ea16585fa037eb61a))
- **ui:** create ViewComponents library to enable building class and view files
components
([94872f2](https://code.castopod.org/adaures/castopod/commit/94872f2338e6025c2f3770be256160838dae9003))
- update analytics so to meet IABv2 requirements
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
- update pine colors + create charts components
([a50abc1](https://code.castopod.org/adaures/castopod/commit/a50abc138d4997b564e3065b37504cda5ce62da6))
- **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
- **ux:** remove admin dashboard and redirect directly to podcast list - **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd)) ([27c48b8](https://code.podlibre.org/podlibre/castopod-host/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- **video-clip:** add video-clip page with video preview + logs - add cache to ActivityPub sql queries + cache activity and note pages
([42538dd](https://code.castopod.org/adaures/castopod/commit/42538dd7577be0ffe59b4fdfadbd76cc89e5ef30)) ([2d297f4](https://code.podlibre.org/podlibre/castopod-host/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
- **video-clip:** generate video clips in the bg using a cron job + add video
clip page + tidy up UI
([db0e427](https://code.castopod.org/adaures/castopod/commit/db0e4272bd6d307c562e1f961d2747cb62de0f35))
- **video-clips:** add dimensions for portrait and squared formats
([3af404d](https://code.castopod.org/adaures/castopod/commit/3af404da3dd1901c78cc7e1778fc225f6716207d))
- **video-clips:** add new themes + add castopod logo as a watermark
([1d1490b](https://code.castopod.org/adaures/castopod/commit/1d1490b06a1f5ecb10b3b98a72efc55d09c10944))
- **video-clips:** add route for scheduled video clips + list video clips with
status
([2065ebb](https://code.castopod.org/adaures/castopod/commit/2065ebbee5e3d0f890ac90b55ca984f1d62a184c))
- **video-clips:** allow episodeNumbering text to stand in the indent of
episodeTitle paragraph
([71a063d](https://code.castopod.org/adaures/castopod/commit/71a063dac311cb21639801fbae6af7c5106c2699))
- **video-clips:** generate a 16:9 video using ffmpeg
([35aa7ea](https://code.castopod.org/adaures/castopod/commit/35aa7ea5d9a339b3e6f745137282268d69fe2231))
- **video-clips:** generate subtitles clip using transcript json to have
subtitles accross video
([3ce07e4](https://code.castopod.org/adaures/castopod/commit/3ce07e455d171e29be30d8ad45055510eb8d363c))
- **video-clips:** replace hardcoded colors with config's theme colors
([e462abf](https://code.castopod.org/adaures/castopod/commit/e462abf6d660e41d2170c52caf45704008de58e9))
- **vite:** add vite config to decouple it from CI_ENVIRONMENT
([8721719](https://code.castopod.org/adaures/castopod/commit/8721719cd7cf32e94823541eafaba1e9309355a8))
- write id3v2 tags to episode's audio file
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Performance Improvements ### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method - **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54)) ([54b84f9](https://code.podlibre.org/podlibre/castopod-host/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
- defer javascript + lazy load images for faster page loads
([f0685e4](https://code.castopod.org/adaures/castopod/commit/f0685e44799dfb494592ff97841c0ae035381db8))
- **docker:** add redis caching service for development - **docker:** add redis caching service for development
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca)) ([05ace8c](https://code.podlibre.org/podlibre/castopod-host/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
# [1.0.0-alpha.53](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.52...v1.0.0-alpha.53) (2021-04-16)
### Bug Fixes
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.podlibre.org/podlibre/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.podlibre.org/podlibre/castopod/issues/114)
# [1.0.0-alpha.52](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.51...v1.0.0-alpha.52) (2021-04-16)
### Bug Fixes
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.podlibre.org/podlibre/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.podlibre.org/podlibre/castopod/issues/111)
# [1.0.0-alpha.51](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.50...v1.0.0-alpha.51) (2021-04-15)
### Bug Fixes
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.podlibre.org/podlibre/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.podlibre.org/podlibre/castopod/issues/104)
# [1.0.0-alpha.50](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.49...v1.0.0-alpha.50) (2021-04-14)
### Bug Fixes
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.podlibre.org/podlibre/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
# [1.0.0-alpha.49](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.48...v1.0.0-alpha.49) (2021-04-12)
### Bug Fixes
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.podlibre.org/podlibre/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
# [1.0.0-alpha.48](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.47...v1.0.0-alpha.48) (2021-04-10)
### Bug Fixes
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.podlibre.org/podlibre/castopod/issues/103)
([35b5be0](https://code.podlibre.org/podlibre/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
# [1.0.0-alpha.47](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.46...v1.0.0-alpha.47) (2021-04-10)
### Bug Fixes
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.podlibre.org/podlibre/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
# [1.0.0-alpha.46](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.45...v1.0.0-alpha.46) (2021-04-09)
### Bug Fixes
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.podlibre.org/podlibre/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.podlibre.org/podlibre/castopod/issues/100)
# [1.0.0-alpha.45](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.44...v1.0.0-alpha.45) (2021-04-08)
### Bug Fixes
- add head request to analytics_hit route
([f0a2f0b](https://code.podlibre.org/podlibre/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
# [1.0.0-alpha.44](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.43...v1.0.0-alpha.44) (2021-04-08)
### Bug Fixes
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.podlibre.org/podlibre/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.podlibre.org/podlibre/castopod/issues/96)
# [1.0.0-alpha.43](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.42...v1.0.0-alpha.43) (2021-04-08)
### Bug Fixes
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.podlibre.org/podlibre/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.podlibre.org/podlibre/castopod/issues/5)
[#86](https://code.podlibre.org/podlibre/castopod/issues/86)
# [1.0.0-alpha.42](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.41...v1.0.0-alpha.42) (2021-04-02)
### Features
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.podlibre.org/podlibre/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.podlibre.org/podlibre/castopod/issues/69)
[#65](https://code.podlibre.org/podlibre/castopod/issues/65)
[#85](https://code.podlibre.org/podlibre/castopod/issues/85)
[#51](https://code.podlibre.org/podlibre/castopod/issues/51)
[#91](https://code.podlibre.org/podlibre/castopod/issues/91)
[#92](https://code.podlibre.org/podlibre/castopod/issues/92)
[#88](https://code.podlibre.org/podlibre/castopod/issues/88)
# [1.0.0-alpha.41](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.40...v1.0.0-alpha.41) (2021-03-30)
### Features
- **partner:** add link and image in episode description
([ad07bb9](https://code.podlibre.org/podlibre/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
# [1.0.0-alpha.40](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.39...v1.0.0-alpha.40) (2021-03-19)
### Features
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.podlibre.org/podlibre/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
# [1.0.0-alpha.39](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.38...v1.0.0-alpha.39) (2021-03-01)
### Bug Fixes
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.podlibre.org/podlibre/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
# [1.0.0-alpha.38](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.37...v1.0.0-alpha.38) (2021-02-27)
### Features
- **embeddable-player:** add embeddable player widget
([141788f](https://code.podlibre.org/podlibre/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
# [1.0.0-alpha.37](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.36...v1.0.0-alpha.37) (2021-02-17)
### Bug Fixes
- **import:** remove query string from files url
([109c4aa](https://code.podlibre.org/podlibre/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
# [1.0.0-alpha.36](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.35...v1.0.0-alpha.36) (2021-02-16)
### Features
- **platforms:** add pod.link
([3d7a232](https://code.podlibre.org/podlibre/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
# [1.0.0-alpha.35](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.34...v1.0.0-alpha.35) (2021-02-12)
### Bug Fixes
- **admin:** save block and lock switches
([b66c0af](https://code.podlibre.org/podlibre/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
# [1.0.0-alpha.34](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.33...v1.0.0-alpha.34) (2021-02-11)
### Bug Fixes
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.podlibre.org/podlibre/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
# [1.0.0-alpha.33](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.32...v1.0.0-alpha.33) (2021-02-10)
### Features
- **platforms:** add helloasso
([16cb993](https://code.podlibre.org/podlibre/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
# [1.0.0-alpha.32](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.31...v1.0.0-alpha.32) (2021-02-10)
### Features
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.podlibre.org/podlibre/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
# [1.0.0-alpha.31](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.30...v1.0.0-alpha.31) (2020-12-23)
### Features
- **rss:** add podcast:location tag
([c0a2282](https://code.podlibre.org/podlibre/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
# [1.0.0-alpha.30](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.29...v1.0.0-alpha.30) (2020-12-21)
### Features
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.podlibre.org/podlibre/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
# [1.0.0-alpha.29](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.28...v1.0.0-alpha.29) (2020-12-10)
### Bug Fixes
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.podlibre.org/podlibre/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.podlibre.org/podlibre/castopod/issues/70)
### Reverts ### Reverts
- **install:** redirect to install in homepage if no database was set
([73f094d](https://code.castopod.org/adaures/castopod/commit/73f094daf26a8cf75e39ebff1eeb7f9039276312))
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
- **soundbites:** remove soundbite table from episode's public page - **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835)) ([5dc0f19](https://code.podlibre.org/podlibre/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
- use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a)) # [1.0.0-alpha.28](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.27...v1.0.0-alpha.28) (2020-12-07)
### Features
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.podlibre.org/podlibre/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.podlibre.org/podlibre/castopod/issues/83)
# [1.0.0-alpha.27](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.26...v1.0.0-alpha.27) (2020-12-07)
### Features
- **platforms:** add AntennaPod
([53e9cfd](https://code.podlibre.org/podlibre/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
# [1.0.0-alpha.26](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.25...v1.0.0-alpha.26) (2020-11-30)
### Bug Fixes
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.podlibre.org/podlibre/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
# [1.0.0-alpha.25](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.24...v1.0.0-alpha.25) (2020-11-30)
### Features
- **platforms:** add podfriend
([9fdc8d3](https://code.podlibre.org/podlibre/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
# [1.0.0-alpha.24](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.23...v1.0.0-alpha.24) (2020-11-26)
### Features
- **monetization:** add Web Monetization support
([96a6026](https://code.podlibre.org/podlibre/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
# [1.0.0-alpha.23](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.22...v1.0.0-alpha.23) (2020-11-24)
### Bug Fixes
- define podcastNamespaceLink value
([0d744d2](https://code.podlibre.org/podlibre/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
# [1.0.0-alpha.22](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.21...v1.0.0-alpha.22) (2020-11-24)
### Features
- **rss:** add transcript and chapters support
([e769d83](https://code.podlibre.org/podlibre/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.podlibre.org/podlibre/castopod/issues/72)
[#82](https://code.podlibre.org/podlibre/castopod/issues/82)
# [1.0.0-alpha.21](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.20...v1.0.0-alpha.21) (2020-11-24)
### Features
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.podlibre.org/podlibre/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
# [1.0.0-alpha.20](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.19...v1.0.0-alpha.20) (2020-11-24)
### Bug Fixes
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.podlibre.org/podlibre/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
### Features
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.podlibre.org/podlibre/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.podlibre.org/podlibre/castopod/issues/73)
[#75](https://code.podlibre.org/podlibre/castopod/issues/75)
[#76](https://code.podlibre.org/podlibre/castopod/issues/76)
[#80](https://code.podlibre.org/podlibre/castopod/issues/80)
# [1.0.0-alpha.19](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.18...v1.0.0-alpha.19) (2020-11-13)
### Bug Fixes
- handle HEAD requests on podcast_feed route
([74b2640](https://code.podlibre.org/podlibre/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.podlibre.org/podlibre/castopod/issues/79)
# [1.0.0-alpha.18](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.17...v1.0.0-alpha.18) (2020-11-09)
### Features
- **platforms:** add Podcast Index
([ad52b1c](https://code.podlibre.org/podlibre/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
# [1.0.0-alpha.17](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.16...v1.0.0-alpha.17) (2020-11-05)
### Bug Fixes
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.podlibre.org/podlibre/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
# [1.0.0-alpha.16](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.15...v1.0.0-alpha.16) (2020-11-04)
### Features
- add Open Graph and Twitter meta tags
([af970b8](https://code.podlibre.org/podlibre/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.podlibre.org/podlibre/castopod/issues/41)
# [1.0.0-alpha.15](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.14...v1.0.0-alpha.15) (2020-11-03)
### Features
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.podlibre.org/podlibre/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
# [1.0.0-alpha.14](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.13...v1.0.0-alpha.14) (2020-11-02)
### Features
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.podlibre.org/podlibre/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
# [1.0.0-alpha.13](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2020-10-29)
### Bug Fixes
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.podlibre.org/podlibre/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
### Features
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.podlibre.org/podlibre/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.podlibre.org/podlibre/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
# [1.0.0-alpha.12](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.11...v1.0.0-alpha.12) (2020-10-26)
### Bug Fixes
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.podlibre.org/podlibre/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
# [1.0.0-alpha.11](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.10...v1.0.0-alpha.11) (2020-10-26)
### Features
- add CDN url
([972bcbf](https://code.podlibre.org/podlibre/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.podlibre.org/podlibre/castopod/issues/37)
# [1.0.0-alpha.10](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2020-10-26)
### Bug Fixes
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.podlibre.org/podlibre/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
# [1.0.0-alpha.9](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2020-10-26)
### Features
- display castopod version in admin footer
([9f2574e](https://code.podlibre.org/podlibre/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.podlibre.org/podlibre/castopod/issues/68)
# [1.0.0-alpha.8](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2020-10-22)
### Features
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.podlibre.org/podlibre/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.podlibre.org/podlibre/castopod/issues/47)
# [1.0.0-alpha.7](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2020-10-21)
### Features
- **analytics:** add service name from rss user-agent
([7202b98](https://code.podlibre.org/podlibre/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
### BREAKING CHANGES ### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts - **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated procedure were updated
# [1.0.0-beta.24](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.23...v1.0.0-beta.24) (2022-10-14) # [1.0.0-alpha.6](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2020-10-20)
### Bug Fixes
- **router:** trim URI slash to match same routes for URIs with and without
trailing slash
([9e9375f](https://code.castopod.org/adaures/castopod/commit/9e9375f9a2cd6102f827b36ec521f4c86a557c00))
### Features
- **episode:** add form to allow editing episode's publication date to a past
date
([d783d16](https://code.castopod.org/adaures/castopod/commit/d783d16eb73d3f896a3dea39a766b4e963e53abf)),
closes [#97](https://code.castopod.org/adaures/castopod/issues/97)
- **rss:** add rss feed route without the `.xml` extension
([94c0b7c](https://code.castopod.org/adaures/castopod/commit/94c0b7c15920dae9ade5cdc79c7996dbfe82ba05)),
closes [#247](https://code.castopod.org/adaures/castopod/issues/247)
# [1.0.0-beta.23](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.22...v1.0.0-beta.23) (2022-09-29)
### Bug Fixes
- **premium-podcasts:** display unlock button in embed when premium episode
([ca109ba](https://code.castopod.org/adaures/castopod/commit/ca109ba3a8a08e661fd2484454b1983c3418f15d))
- **premium-podcasts:** remove cache in unlock form + redirect to podcast if
podcast is not premium
([242352c](https://code.castopod.org/adaures/castopod/commit/242352c4d9cd936de14e8e8a5d78ebf1287b1f95))
- **premium-podcasts:** return different cached page when podcast is unlocked
([b1303c5](https://code.castopod.org/adaures/castopod/commit/b1303c525517498b0edfb9885ff36e08c72628b5))
### Features
- add instructions on production error page to ease Castopod debugging process
([9eab54e](https://code.castopod.org/adaures/castopod/commit/9eab54e0853ccb8300d9f9b743cd84aefbf06549)),
closes [#224](https://code.castopod.org/adaures/castopod/issues/224)
- add premium podcasts to manage subscriptions for premium episodes
([3234500](https://code.castopod.org/adaures/castopod/commit/3234500e2d967438ad140f65da801a543f43775d)),
closes [#193](https://code.castopod.org/adaures/castopod/issues/193)
- **gdpr:** add purpose for granting access to premium content
([47d6d81](https://code.castopod.org/adaures/castopod/commit/47d6d81b798ec3ed467e0f4339c98c8a6b80cecd))
# [1.0.0-beta.22](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.21...v1.0.0-beta.22) (2022-09-23)
### Bug Fixes
- **fediverse:** set default castopod avatar url when actor avatar is not
present
([460f52f](https://code.castopod.org/adaures/castopod/commit/460f52f70e493d619c28632db6c698e88f0ebb5f))
- **import:** set default episode type if not set
([d7250ab](https://code.castopod.org/adaures/castopod/commit/d7250ab03f9b032830c575ad58b51c8d60b7a49a))
- **input-component:** unset required attribute to prevent rendering it when
false
([db9ac13](https://code.castopod.org/adaures/castopod/commit/db9ac13860bce58235a5da275910bea605a00626))
- **notifications:** notify actors after activities insert / update using model
callback methods
([e08555a](https://code.castopod.org/adaures/castopod/commit/e08555a4e9a6c15eeba18273c63403f82eddae35))
- overwrite getActorById to return app's Actor entity
([f2bc2f7](https://code.castopod.org/adaures/castopod/commit/f2bc2f7e01aa166faa627df6fe4d5ed4887c16e5))
- remove heavy image cover data from audio file metadata
([f74403b](https://code.castopod.org/adaures/castopod/commit/f74403bd7a5089b760603abe36264e7615be0e78))
- set storage limit as disk_total_space instead of free space
([7512e2e](https://code.castopod.org/adaures/castopod/commit/7512e2ed1ff5656cd63a4fc2524296dbb8b4164a))
- **ui:** remove empty tooltip when hovering on sponsor button
([40aa661](https://code.castopod.org/adaures/castopod/commit/40aa661289e1d1517fffcea5d257183bc9c458e4))
- **users:** remove required roles input when editing user + prevent owner's
roles from being edited
([1c8af75](https://code.castopod.org/adaures/castopod/commit/1c8af7550ba27d8c8473ae96acd21ad7731fd863)),
closes [#239](https://code.castopod.org/adaures/castopod/issues/239)
- **ux:** have podcast dashboard card link to podcast dashboard if only one
podcast in instance
([7dabee5](https://code.castopod.org/adaures/castopod/commit/7dabee58a187abe92358d962da506a836e29cda3))
# [1.0.0-beta.21](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.20...v1.0.0-beta.21) (2022-09-06)
### Bug Fixes
- **email:** set the correct url in the activation and forgot emails
([10fc6f1](https://code.castopod.org/adaures/castopod/commit/10fc6f17c6838a58348f32ccfd0cf05f9d3e172c)),
closes [#204](https://code.castopod.org/adaures/castopod/issues/204)
- **notifications:** add trigger after activities update + update insert trigger
([e5d16e8](https://code.castopod.org/adaures/castopod/commit/e5d16e87119021fa5a43470d67ddfe5128e57f74))
### Features
- **i18n:** add support for Simplified Chinese (zh-Hans) and Catalan (ca)
locales
([48d1443](https://code.castopod.org/adaures/castopod/commit/48d14434727c3310a391160c7af02c56b7e20425))
# [1.0.0-beta.20](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2022-08-12)
### Bug Fixes
- add underline and semibold font weight for prose links to have them stand out
([d4d8671](https://code.castopod.org/adaures/castopod/commit/d4d867121c50bded4176a53d7154cf1bb347e306))
- **router:** check if Accept header is set before getting value
([10a2ae0](https://code.castopod.org/adaures/castopod/commit/10a2ae02484672d6a0fbc6e7b943519c5ec16cb6)),
closes [#228](https://code.castopod.org/adaures/castopod/issues/228)
- **search-episodes:** add fallback sql query using LIKE for search query with
less than 4 characters
([e66bf44](https://code.castopod.org/adaures/castopod/commit/e66bf44341175bc5a10fbf7dfa00b351e76136c2)),
closes [#236](https://code.castopod.org/adaures/castopod/issues/236)
- set interact_as_actor for user upon password reset
([ad8f5f5](https://code.castopod.org/adaures/castopod/commit/ad8f5f5a0fac7b0b9cc10a0b86200f014aca7553)),
closes [#178](https://code.castopod.org/adaures/castopod/issues/178)
### Features
- add label to sponsor button on podcast page
([c29c018](https://code.castopod.org/adaures/castopod/commit/c29c018c7a543fc9398b5d7d11f086123e2b33f2)),
closes [#162](https://code.castopod.org/adaures/castopod/issues/162)
- add notifications inbox for actors
([999999e](https://code.castopod.org/adaures/castopod/commit/999999e3efab7b1aad7568e4fd114dc7bac04f38)),
closes [#215](https://code.castopod.org/adaures/castopod/issues/215)
# [1.0.0-beta.19](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2022-07-21)
### Bug Fixes
- **episode-unpublish:** set consistent posts_counts' increments/decrements for
actors and episodes
([8acdafd](https://code.castopod.org/adaures/castopod/commit/8acdafd26044e50a4d6ee451bf24ad66003c5bb3)),
closes [#233](https://code.castopod.org/adaures/castopod/issues/233)
- **get_browser_language:** return defaultLocale if browser doesn't send user
preferred language
([9cc2996](https://code.castopod.org/adaures/castopod/commit/9cc299626181048b85b629bbe7f5806a1f5d21ff))
### Features
- **episode-unpublish:** remove episode comments upon unpublish
([78acd7f](https://code.castopod.org/adaures/castopod/commit/78acd7f5c057c82507d801c424040296dbaba586))
# [1.0.0-beta.18](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2022-07-07)
### Bug Fixes
- **player-styling:** revert vite to 2.8 to reference the player css
([e07d3af](https://code.castopod.org/adaures/castopod/commit/e07d3afea9af85b8361227e000fb64b502781668))
### Features
- add legalNoticeURL to app config for setting an external url to legal notice
([711843a](https://code.castopod.org/adaures/castopod/commit/711843a0c81e1e2ec7a015431786df4ef32d5092))
# [1.0.0-beta.17](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2022-07-06)
### Bug Fixes
- explicitly cast seconds to int in iso8601_duration helper function
([779653f](https://code.castopod.org/adaures/castopod/commit/779653f75b140942f731cbb238bc0667cc461307))
- **housekeeping:** use EpisodeModel's builder to reset comments count
([65e9c0b](https://code.castopod.org/adaures/castopod/commit/65e9c0b05ea4992884149cb4a4b071bf31a20a1a))
- **rss:** round episode durations and soundbites
([c9fb987](https://code.castopod.org/adaures/castopod/commit/c9fb987fcfbe17069ec68fdbc823777079ce574b)),
closes [#214](https://code.castopod.org/adaures/castopod/issues/214)
- **xml-editor:** prettify xml even without root node
([ca55c24](https://code.castopod.org/adaures/castopod/commit/ca55c248d0562a8529071c1f10be12f40ef50dda))
### Features
- add publish feature for podcasts and set draft by default
([3d363f2](https://code.castopod.org/adaures/castopod/commit/3d363f2efe99836ac05c305a2fa683e342f06561)),
closes [#128](https://code.castopod.org/adaures/castopod/issues/128)
[#220](https://code.castopod.org/adaures/castopod/issues/220)
- **admin:** add instance wide dashboard with storage and bandwidth usage
([b1a6c02](https://code.castopod.org/adaures/castopod/commit/b1a6c02e56fdc01a7ff69fa7e7dd8ea71380b7ba)),
closes [#216](https://code.castopod.org/adaures/castopod/issues/216)
- **datetime-picker:** set material_green theme to flatpickr
([3ce6541](https://code.castopod.org/adaures/castopod/commit/3ce6541003260677e722a916ad6bc83ef47c4371))
# [1.0.0-beta.16](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2022-06-24)
### Bug Fixes
- change image size requirement hints
([ea20206](https://code.castopod.org/adaures/castopod/commit/ea20206ee674eb54dd3ea188d2a2e2d41425df65))
### Features
- add update rss feed feature for podcasts to import their latest episodes
([5eb9dc1](https://code.castopod.org/adaures/castopod/commit/5eb9dc168eb9af04767829b76242c9120f55d46d)),
closes [#183](https://code.castopod.org/adaures/castopod/issues/183)
- **admin:** add search form in podcast episodes list
([6be5d12](https://code.castopod.org/adaures/castopod/commit/6be5d12877342a7c56e25ea8dd15a975c6ce45ac)),
closes [#26](https://code.castopod.org/adaures/castopod/issues/26)
- **api:** add rest api with podcasts read endpoints
([e64001d](https://code.castopod.org/adaures/castopod/commit/e64001d00604bcf587ec5e9a631282f212df450d)),
closes [#210](https://code.castopod.org/adaures/castopod/issues/210)
# [1.0.0-beta.15](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2022-06-14)
### Bug Fixes
- replace deletedField with published_at for episodes
([14d7d07](https://code.castopod.org/adaures/castopod/commit/14d7d078225cdc8980759273a5dc4163d9f84b06))
### Features
- add default icons to Alert component
([0d98001](https://code.castopod.org/adaures/castopod/commit/0d9800123b135e4fa1a2acd14a5e039c12174333))
- add permanent delete feature for podcasts 🎉
([dbb4030](https://code.castopod.org/adaures/castopod/commit/dbb4030da49f9ea1f61759fb7c66d71fc29ea4a1)),
closes [#89](https://code.castopod.org/adaures/castopod/issues/89)
- apply colour theme to embed player
([9548337](https://code.castopod.org/adaures/castopod/commit/9548337a7c49879e8b58c2dfece46e3cfc9517eb)),
closes [#201](https://code.castopod.org/adaures/castopod/issues/201)
- **episodes:** replace soft delete with permanent delete
([eb9ff52](https://code.castopod.org/adaures/castopod/commit/eb9ff522c25af8ceb2ed08614b581757ee791d42))
# [1.0.0-beta.14](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2022-04-23)
### Bug Fixes
- **home:** remove hardcoded prefix in getAllPodcasts query
([92d5cc5](https://code.castopod.org/adaures/castopod/commit/92d5cc50a3e533875cd894dccc417918102d4b7f))
- overwrite common lang function to escape returned string
([4c490c1](https://code.castopod.org/adaures/castopod/commit/4c490c15bb6642ad0b2aaddf08d8af25de99b4b0)),
closes [#196](https://code.castopod.org/adaures/castopod/issues/196)
[#198](https://code.castopod.org/adaures/castopod/issues/198)
### Features
- **i18n:** add Spanish to supported locales
([e340b54](https://code.castopod.org/adaures/castopod/commit/e340b54a84d7dcdf9ba910fe7ff39c453fac0968))
# [1.0.0-beta.13](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2022-04-14)
### Bug Fixes
- **rss:** remove escaping for publisher and owner name
([e2046e4](https://code.castopod.org/adaures/castopod/commit/e2046e4b116ecddb5e6d68487f666b95fd7f493c))
- use UTC_TIMESTAMP() to get current utc date instead of NOW() in sql queries
([853a6ba](https://code.castopod.org/adaures/castopod/commit/853a6ba9155b6687604304d59f03d0efb75a9f96))
### Features
- **i18n:** add Norwegian Nynorsk to supported locales
([744340d](https://code.castopod.org/adaures/castopod/commit/744340df615bee38a54c4abbbb7f03d51b61a39d))
# [1.0.0-beta.12](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.11...v1.0.0-beta.12) (2022-04-05)
### Bug Fixes
- update form_textarea to prevent escaping value
([78548b5](https://code.castopod.org/adaures/castopod/commit/78548b5cd75ea7d6688d1945ff5449ea4f6bec68))
### Features
- **i18n:** add support for German and Brazilian Portuguese languages
([19da003](https://code.castopod.org/adaures/castopod/commit/19da003fd396bff20b89ad330b787e9cdbe8d919))
# [1.0.0-beta.11](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.10...v1.0.0-beta.11) (2022-04-01)
### Bug Fixes
- change message upon cancellation of episode publication
([9859c74](https://code.castopod.org/adaures/castopod/commit/9859c7434c2a3478ce035f7a4de20f594d63f5b0))
- prefill description footer input when creating a new episode
([9ea5ca3](https://code.castopod.org/adaures/castopod/commit/9ea5ca31697c70d176294f8aea37bd57d471fcf7))
- remove value escaping for form inputs and textareas
([bc6dea2](https://code.castopod.org/adaures/castopod/commit/bc6dea2f8ad1cf0aee0eaa93151332fbac7fb771))
- restore default podcast icon on public website
([342778b](https://code.castopod.org/adaures/castopod/commit/342778bac3c684328d72633961df1a2ebdc1330e))
- **socialinteract:** move social interact uri into uri attribute + update
social data upon import
([12b2200](https://code.castopod.org/adaures/castopod/commit/12b22008a237185cb736fc29352fab22421dad16))
### Features
- **analytics-gdpr:** update cached personal data to expire at midnight
([0188b67](https://code.castopod.org/adaures/castopod/commit/0188b67354a756f0c926edd7b46623ab5b20c12b))
- **analytics:** add current date and secret salt to analytics hash for improved
privacy
([6f2e7c0](https://code.castopod.org/adaures/castopod/commit/6f2e7c009c24830d4f08633bfbde3b75f40bf215))
- **i18n:** add 7 new languages + update german translations
([d021abb](https://code.castopod.org/adaures/castopod/commit/d021abb52f5525d93810e25df2b453c918d7bc8b))
- **i18n:** add german language as supported locale + create Language files from
english source
([c220b31](https://code.castopod.org/adaures/castopod/commit/c220b310ed59cad188af044b1fed0c39efc7da5b))
- **icons:** add podnews icon to podcasting platforms
([5f42355](https://code.castopod.org/adaures/castopod/commit/5f423557c2b78fd7c38c5e0caab6c6c80d21e36e)),
closes [#190](https://code.castopod.org/adaures/castopod/issues/190)
# [1.0.0-beta.10](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.9...v1.0.0-beta.10) (2022-03-15)
### Bug Fixes
- add explicit int conversion when formatting episode duration
([1253096](https://code.castopod.org/adaures/castopod/commit/1253096197a0d30692bdafa7152f250cd9a71acf))
- add href to castopod website on login page
([cc54257](https://code.castopod.org/adaures/castopod/commit/cc5425735184ad738aa0f38540f18e8971f8f56e))
- move html escaping on credits page
([fbffdbd](https://code.castopod.org/adaures/castopod/commit/fbffdbde78544c83138ee6234c62d43056f407b6))
- remove cache from remote follow form to display error messages
([90e4443](https://code.castopod.org/adaures/castopod/commit/90e44437bdf37d8024ef609b2f7336dbdfc3b974))
### Features
- add autofocus to input field "Email or username" on login page
([19caed4](https://code.castopod.org/adaures/castopod/commit/19caed4bce0daab9ccf6ab9645f44b60eb87de88))
- add WebSub module for pushing feed updates to open hubs
([10d3f73](https://code.castopod.org/adaures/castopod/commit/10d3f73786ba141e27a822b2585c4a244ee92c14))
- **GDPR:** add GDPR.yml file to public/.well-known/
([86bccc3](https://code.castopod.org/adaures/castopod/commit/86bccc3d5cc9562b89196f1766ac91cdc8ad786d))
# [1.0.0-beta.9](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.8...v1.0.0-beta.9) (2022-03-04)
### Bug Fixes
- **cache:** delete posts and comments pages cache when updating platform links
([f7c3e5b](https://code.castopod.org/adaures/castopod/commit/f7c3e5bf4ad43389bf8d58d2c4aaf16b81cbce00)),
closes [#169](https://code.castopod.org/adaures/castopod/issues/169)
- escape characters for `min` in format_duration_symbol
([3b6722a](https://code.castopod.org/adaures/castopod/commit/3b6722a42b9e4330e5235d4ceed41c777159f4dc))
- **security:** add csrf filter + prevent xss attacks by escaping user input
([cd2e1e1](https://code.castopod.org/adaures/castopod/commit/cd2e1e1dc37c53d32d00971c451c4800b8fd6107))
- update ivoox podcasting icon
([f2b69a4](https://code.castopod.org/adaures/castopod/commit/f2b69a47339c887f57883ec612f3d200e512ac1c))
- **video-clips:** update condition to check if ffmpeg is installed
([b57f0b6](https://code.castopod.org/adaures/castopod/commit/b57f0b6eb65dccf22cb4d55f93d18ca36857d7fc)),
closes [#163](https://code.castopod.org/adaures/castopod/issues/163)
### Features
- **i18n:** add Polish translation
([2d83b44](https://code.castopod.org/adaures/castopod/commit/2d83b44add9e4e00766a1f326377ed892f48ad73))
- **icons:** add default icons for podcasting, social and funding platforms +
remove complex icons
([5bcdfeb](https://code.castopod.org/adaures/castopod/commit/5bcdfebe6489b5d6b90f3c828b014ec4e9a7e7e1)),
closes [#166](https://code.castopod.org/adaures/castopod/issues/166)
[#167](https://code.castopod.org/adaures/castopod/issues/167)
[#170](https://code.castopod.org/adaures/castopod/issues/170)
- make episode description more visible on episode pages
([90533be](https://code.castopod.org/adaures/castopod/commit/90533be0298249e5527870c01329fce5f94ec2dc)),
closes [#171](https://code.castopod.org/adaures/castopod/issues/171)
- **podcasting 2.0:** update podcast:social tag to adhere to latest spec
([a597cf4](https://code.castopod.org/adaures/castopod/commit/a597cf4ecfa6807a3413177d99c816056a7e7c45))
# [1.0.0-beta.8](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.7...v1.0.0-beta.8) (2022-02-10)
### Features
- **podcast-form:** add new_feed_url field to set an url when changing domain or
host
([e7eec48](https://code.castopod.org/adaures/castopod/commit/e7eec48e7bc06a9aa907db01ed3e5b536e7dd8be))
# [1.0.0-beta.7](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.6...v1.0.0-beta.7) (2022-02-05)
### Bug Fixes
- **activitypub:** allow cors on get requests for routes exposing acitivitypub
objects
([2f24809](https://code.castopod.org/adaures/castopod/commit/2f2480998f9abb34f02ab186c65d462a74b4e640))
- **fediverse:** set model instances as non shared to prevent overlapping
([91128fa](https://code.castopod.org/adaures/castopod/commit/91128fad7a68e1f4e5acacba90b6899288699e61))
- **htaccess:** add ? after index.php in RewriteRule
([d9d139e](https://code.castopod.org/adaures/castopod/commit/d9d139eefa03c28d1a064b3b32c9036193497e57)),
closes [#152](https://code.castopod.org/adaures/castopod/issues/152)
### Features
- **home:** sort podcasts by recent activity + add dropdown menu to choose
between sorting options
([7b89da6](https://code.castopod.org/adaures/castopod/commit/7b89da6106c150708782d39ed2742fe416c41e89)),
closes [#164](https://code.castopod.org/adaures/castopod/issues/164)
# [1.0.0-beta.6](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.5...v1.0.0-beta.6) (2022-02-03)
### Bug Fixes
- **activitypub:** add conditions for possibly missing actor properties + add
user-agent to requests
([8fbf948](https://code.castopod.org/adaures/castopod/commit/8fbf948fbba22ffd33966a1b2ccd42e8f7c1f8a2))
- **activitypub:** add target actor id to like / announce activities to send
directly to note's actor
([962dd30](https://code.castopod.org/adaures/castopod/commit/962dd305f5d3f6eadc68f400e0e8f953827fe20d))
- **activitypub:** add target_actor_id for create activity to broadcast post
reply
([0128a21](https://code.castopod.org/adaures/castopod/commit/0128a21ec55dcc0a2fbf4081dadb4c4737735ba1))
- **http-signature:** update SIGNATURE_PATTERN allowing signature keys to be
sent in any order
([b7f285e](https://code.castopod.org/adaures/castopod/commit/b7f285e4e24247fedb94f030356fa6f291f525cc))
- **install:** set message block on forms to show error messages
([3a0a20d](https://code.castopod.org/adaures/castopod/commit/3a0a20d59cdae7f166325efb750eaa6e9800ba6e)),
closes [#157](https://code.castopod.org/adaures/castopod/issues/157)
- **markdown-editor:** remove unnecessary buttons for podcast and episode
editors + add extensions
([9c4f60e](https://code.castopod.org/adaures/castopod/commit/9c4f60e00bcbd4f784f12d2a6fed357ad402ee2e))
- **podcast-activity:** check if transcript and chapters are set before
including them in audio
([5855a25](https://code.castopod.org/adaures/castopod/commit/5855a250936f91641efef77650890a18d8e9917f))
- **podcast:** use markdown description value for editor + set prose class to
about description
([f304d97](https://code.castopod.org/adaures/castopod/commit/f304d97b14e0ef383509cb3bba50beb55bf701ba)),
closes [#156](https://code.castopod.org/adaures/castopod/issues/156)
# [1.0.0-beta.5](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-01-31)
### Bug Fixes
- **analytics:** set initial value for duration and bandwidth
([ee50539](https://code.castopod.org/adaures/castopod/commit/ee5053959154b1a2e5fbe4b43162968425206a26))
# [1.0.0-beta.4](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2022-01-29)
### Bug Fixes
- **housekeeping:** replace the use of GLOB_BRACE with looping over file
extensions
([42d92d0](https://code.castopod.org/adaures/castopod/commit/42d92d0c8dfe0c567c28f5bfdda129890fa4c2ec)),
closes [#154](https://code.castopod.org/adaures/castopod/issues/154)
- **housekeeping:** set default sizes value + ignore illegal IFD size error to
proceed with script
([f21ca57](https://code.castopod.org/adaures/castopod/commit/f21ca57603cfa503699b7e09a155e18d876d65fe))
### Features
- **housekeeping:** add clear_cache option to flush redis or files cache
([99bfac0](https://code.castopod.org/adaures/castopod/commit/99bfac0b428a4bc6fe8bfd10a355dfd93f42ba5c))
# [1.0.0-beta.3](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2022-01-28)
### Bug Fixes
- revert to beta.1's codeigniter4 version
([e831411](https://code.castopod.org/adaures/castopod/commit/e83141127080ccde44987195db46ba97fd6cc2ca))
# [1.0.0-beta.2](https://code.castopod.org/adaures/castopod/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2022-01-28)
### Bug Fixes
- **migrations:** ignore invalid utf8 chars for media files metadata + update
transcript parser
([45e8f99](https://code.castopod.org/adaures/castopod/commit/45e8f99e753cc02ec105e6f4d7fe026a205724f8))
- **video-clips:** set audio codec to aac, fixing audio issue on twitter
([3c22c68](https://code.castopod.org/adaures/castopod/commit/3c22c68ee81f77bd7fcf7e2739ee6af016407843))
- **video-clips:** set longer podcast and episode lengths for squared format
([c030113](https://code.castopod.org/adaures/castopod/commit/c0301134c2048dc29eb2b995e4d5c22c49444100))
# 1.0.0-beta.1 (2022-01-23)
### Bug Fixes
- **a11y:** replace active tab color to contrast with background on podcast and
episode pages
([f3785e1](https://code.castopod.org/adaures/castopod/commit/f3785e140147d085a2fb6a62ded87cdfe360f442))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **activitypub:** set created_by to null for reblog if no user + update episode
oembed data
([209dfbd](https://code.castopod.org/adaures/castopod/commit/209dfbd134e1a2cc02e7c24c158d786fa4dda61d))
- add admin-audio-player to vite config to have admin player show up
([93cb9b2](https://code.castopod.org/adaures/castopod/commit/93cb9b24701c09b92820204a67c1fc1b3c044708))
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
- add category_label component to include parent category in about podcast page
([74e7d68](https://code.castopod.org/adaures/castopod/commit/74e7d68ac834885c4b89ee6e7d60db2157165799))
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
- add missing explicit badge for podcasts and episodes
([cdf9f9d](https://code.castopod.org/adaures/castopod/commit/cdf9f9d53f2597f19455cb65c51da4677bb99327))
- add open graph size for podcast images to replace the inadequate large format
([33aae1f](https://code.castopod.org/adaures/castopod/commit/33aae1f7934e4962116e94e477dbf48e24971f5f))
- add public/media folder to castopod bundle
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52)
- add translation key for audio-clipper trim labels
([db191ac](https://code.castopod.org/adaures/castopod/commit/db191ac31bd16bad2a72afdb8b25c685adf86a6e))
- add where condition to get episode count without deleted episodes
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67)
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
- **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
- **audio-clipper:** add mouse position offset when stretching clip to prevent
content from jumping
([602654b](https://code.castopod.org/adaures/castopod/commit/602654b99b33ee8c29da080058a0aaea976cd484))
- **audio-clipper:** show audio playing progress + put waveform behind audio
clipper
([01a09dc](https://code.castopod.org/adaures/castopod/commit/01a09dc447b81c5412ceb45d6706a867939fd4dd))
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
- **bundle:** include modules and themes when copying files with rsync
([cd5bb88](https://code.castopod.org/adaures/castopod/commit/cd5bb8835c6e259408a8c13a2196a347e161da83))
- **bundle:** update vite input files path + add `set -e` in bash scripts to
fail if command fails
([0ee53c7](https://code.castopod.org/adaures/castopod/commit/0ee53c71ffadb8a6ddb1febd9f912bc99f5f7a0b))
- **cache:** add locale for podcast and episode pages + clear some persisting
cache in models
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61)
- **cache:** return a non cached view when connected
([e2e7358](https://code.castopod.org/adaures/castopod/commit/e2e735815d805a48eed2ea3288d060d0ddb253a3))
- **cache:** suffix cache names with authenticated for credits, map and pages
([418a70b](https://code.castopod.org/adaures/castopod/commit/418a70b2a670d8ba0ab6c15fa5faa41f6be55e53))
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
- clear cache when deleting podcast banner
([99bb40b](https://code.castopod.org/adaures/castopod/commit/99bb40b8bc17b8ee2cd8468a82e46ea280c92cb6))
- comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- **comments:** add comment view partials for public pages
([fcecbe1](https://code.castopod.org/adaures/castopod/commit/fcecbe1c68b0d28d19454fba65caf3ab769fbc75))
- correct chart data
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23)
- define podcast_id and platform_slug as foreign keys in podcasts_plaforms table
([6e9451a](https://code.castopod.org/adaures/castopod/commit/6e9451a1103b43750fa70ad576de36af25ca29cb))
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
- **embed:** open embedded player's links in new tab
([4aa73d7](https://code.castopod.org/adaures/castopod/commit/4aa73d71e3b8c0a6c3f75f4d1d45c4d693aba64c))
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
- escape generated feed tag values and remove new lines from public pages meta
description
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46)
- expire default query cache upon scheduled episode publication
([b72e7c8](https://code.castopod.org/adaures/castopod/commit/b72e7c8691c887e41107baea0a4d50a39eaf8c8b)),
closes [#81](https://code.castopod.org/adaures/castopod/issues/81)
- fix layout bugs in admin and update translation files
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40)
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
- **images:** set default mimetype if none is specified when getting size info
([6e4acc6](https://code.castopod.org/adaures/castopod/commit/6e4acc64ad256178cee7905402b48bafcd49f84c))
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
- **import:** add validation for handle field to prevent
Router.invalidParameterType error
([5bf7200](https://code.castopod.org/adaures/castopod/commit/5bf7200fb390f2447b29f24b495f24483cf7b205)),
closes [#119](https://code.castopod.org/adaures/castopod/issues/119)
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
- **import:** save media files during podcast import + set missing media fields
([a9989d8](https://code.castopod.org/adaures/castopod/commit/a9989d841a634f8cf6c04df25f40bb1e7d4fcdcc))
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
- include missing variables on public ui's episode page and remote_actions
([193b373](https://code.castopod.org/adaures/castopod/commit/193b373bc94a5270acae99b637aa84b6cb2dedfe))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
- **install:** redirect to input baseUrl after instance config
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53)
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
- **json-ld:** add missing properties to PodcastSeries object
([e97266c](https://code.castopod.org/adaures/castopod/commit/e97266c5d4883a10f68b3685ecc0d1942f54d658))
- keep subtitle line breaks when parsing srt file to json
([cfb3da6](https://code.castopod.org/adaures/castopod/commit/cfb3da6592f2de23cb1a7ac420f19fc77fa338aa))
- **layouts:** replace holy-grail layout with tailwind config + widen public
podcast layout
([be5a287](https://code.castopod.org/adaures/castopod/commit/be5a28787fdb180b64d9bf570120eff7072ab9aa))
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
- **md-editor:** build new markdown editor with lit +
github/markdown-toolbar-element
([9ec1cb9](https://code.castopod.org/adaures/castopod/commit/9ec1cb93da6f41124c48b8cf14ee6942e865bede)),
closes [#93](https://code.castopod.org/adaures/castopod/issues/93)
[#94](https://code.castopod.org/adaures/castopod/issues/94)
[#120](https://code.castopod.org/adaures/castopod/issues/120)
- minor corrections
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
- **package.json:** update destination of postcss generation scripts
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- **pages:** add locale to page cache
([8f999ce](https://code.castopod.org/adaures/castopod/commit/8f999ce2f7ee1416c30cf58c84f67b3d11b3f142))
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
- pass timezone to relative time component to show the localized time in the UI
([b9db936](https://code.castopod.org/adaures/castopod/commit/b9db936461d4cb914958bb3256bb910bbd7ba815))
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
- **persons:** set person picture as optional for better ux
([7fdea63](https://code.castopod.org/adaures/castopod/commit/7fdea63de7e572810082c84fff3013af580df58b)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39)
- **podcast-import:** move guid attribute declaration for Episode entity to
include slug data
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328))
- **pwa:** add scope to webmanifests to allow installing an app per podcast
([74c683e](https://code.castopod.org/adaures/castopod/commit/74c683eb44398a84443ec17903c3e002bb5ea9b9))
- **pwa:** set app display as standalone in the webmanifests
([7aa37d2](https://code.castopod.org/adaures/castopod/commit/7aa37d24ac13a1ee160c01a56b43621d7efcfbbc))
- re-order graph values
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- redirect to non cached views when authenticated in public views
([482b47b](https://code.castopod.org/adaures/castopod/commit/482b47ba6bdab7f27fc5704a559567228e07cd14))
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- remove defer from js script declaration as it is a module
([18ae557](https://code.castopod.org/adaures/castopod/commit/18ae557e97f1cef775cd1e75fb1fedee7f1c0cc9))
- remove fixed size from podcast sidebar + rearrange account info + space out
import radio inputs
([776eec6](https://code.castopod.org/adaures/castopod/commit/776eec6f0d533d6c92ebec16f7a9dbfcde1f41f4))
- remove required for other_categories field and add podcast_id to latest
podcasts query
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- remove required property to persons picture
([c546be3](https://code.castopod.org/adaures/castopod/commit/c546be385b243014243ae93356006cd126d2f00d)),
closes [#125](https://code.castopod.org/adaures/castopod/issues/125)
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
- rename issue_templates labels
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60)
- rename podcast name to podcast handle to clarify field usage
([9dd4c77](https://code.castopod.org/adaures/castopod/commit/9dd4c7741eb1b7cb5fc214ff674697f3aa986df0)),
closes [#126](https://code.castopod.org/adaures/castopod/issues/126)
- reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
- replace hardcoded style links with vite service + set default value for remote
transcript url
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150)
- replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
- rewrite regenerate image function to use saveSizes method from Image entity
([3889912](https://code.castopod.org/adaures/castopod/commit/38899124ec27e94a8c798bc2db528f9f785eec20))
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
- **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
- **rss:** do not escape podcast and episode titles in the xml
([0dd3b7e](https://code.castopod.org/adaures/castopod/commit/0dd3b7e0bf00d5a9eb80c93cba1efcada59ec3c1)),
closes [#138](https://code.castopod.org/adaures/castopod/issues/138)
[#71](https://code.castopod.org/adaures/castopod/issues/71)
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set episode duration translation to hardcoded english
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.castopod.org/adaures/castopod/issues/64)
- set episode guid upon episode creation
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48)
- set episode numbers during import + remove all custom form_helpers + minor ui
issues
([99a3b8d](https://code.castopod.org/adaures/castopod/commit/99a3b8d33e00482da50dd62bdaa9215a351a56e4))
- set localized slug_field key as string in french language
([17fb29b](https://code.castopod.org/adaures/castopod/commit/17fb29b20993b7deee4e252e0e3a4a2459ee0d98))
- set location to null when getting empty string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- **settings:** add .jpg extension to site-icon file input to display all jpeg
images
([f611a16](https://code.castopod.org/adaures/castopod/commit/f611a16cd0c1a389e1c5a287eaec9d2a927a4bb6))
- sort episodic podcasts by season
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
- **themes:** update themes stylesheet route and remove css extension
([e4e7e00](https://code.castopod.org/adaures/castopod/commit/e4e7e0005e931967dd6162588f1c5913dbf4603e))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- unpublish episode before deleting it + add validation step before deletion
([f75bd76](https://code.castopod.org/adaures/castopod/commit/f75bd76458eeb01a2d37912695e33f77d03b7a69)),
closes [#112](https://code.castopod.org/adaures/castopod/issues/112)
[#55](https://code.castopod.org/adaures/castopod/issues/55)
- update .htaccess for shared hosting config
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
- update iso-369 language table seeder
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- update MarkdownEditor component + restyle Button and other components
([b05d177](https://code.castopod.org/adaures/castopod/commit/b05d177f1b7f44fef043ac5eb41f07133a2cf52d))
- update purgecss content path for php helper files
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59)
- update translations for settings' tasks to include what they should be used
for
([06b1a8b](https://code.castopod.org/adaures/castopod/commit/06b1a8b29b6ce5d81c5570d250bdac4e0c9ee5ca))
- use slash instead of backslash to call layout
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- **video-clips:** check if created video exists before recreating it and
failing
([dff1208](https://code.castopod.org/adaures/castopod/commit/dff12087251b2b89e195604202094b5ddd9a0936))
- **video-clips:** clear video clip cache after process has finished
([3ae6232](https://code.castopod.org/adaures/castopod/commit/3ae62325856f6ff331a5d9ed901b9fa097ca7055))
- **video-clips:** create unique temporary files for resources to be deleted
after generation
([7f7c878](https://code.castopod.org/adaures/castopod/commit/7f7c878cb6ecf7b4a967b2af87da82bc6593081e))
- **video-clips:** tweak portrait parameters to have subtitles display without
overflowing
([2385b1a](https://code.castopod.org/adaures/castopod/commit/2385b1a2926d1344569836e18cb30adb4c604664))
- **xml-editor:** escape xml editor's content + restyle form sections to prevent
overflowing
([588590b](https://code.castopod.org/adaures/castopod/commit/588590bd2c0346e2465ff8f1930580d76a3bf068))
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
- add alternate rss feed link tag to podcast page head
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.castopod.org/adaures/castopod/issues/35)
- add analytics and unknown useragents
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add audio-clipper toolbar + add video-clip-previewer
([0255753](https://code.castopod.org/adaures/castopod/commit/02557539e6eb48fc23ee2ee3b0c75aee3310965b))
- add audio-clipper webcomponent (wip)
([21d4251](https://code.castopod.org/adaures/castopod/commit/21d4251b9bcd5acb0f8a1761bc4edc34a3dbc228))
- add basic stats on podcast about page
([1670558](https://code.castopod.org/adaures/castopod/commit/1670558473dba47219d470ff21d6224db6ab42ba))
- add breadcrumb in admin area
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17)
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
- add codemirror to display xml editor for custom rss field
([f15f262](https://code.castopod.org/adaures/castopod/commit/f15f26240cd5311fa9d07779f364b6639a501dec))
- add cumulative listening time charts
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
- add DropdownMenu component + remove global audio player in admin
([abb7fba](https://code.castopod.org/adaures/castopod/commit/abb7fbac276d77b7d31a0aeba75d464f3ba3ad46))
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- add french translation
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add heading component + update ecs rules to fix views
([23bdc6f](https://code.castopod.org/adaures/castopod/commit/23bdc6f8e36b7e8dfbe32755a54dea59ad913432))
- add housekeeping task to run after migrations
([89dee41](https://code.castopod.org/adaures/castopod/commit/89dee41d583e57251ea9315402a757f03571d7ad))
- add install wizard form to bootstrap database and create the first superadmin
user
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2)
- add ISO 3166 country codes
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add js audio player on podcast, admin and embeddable player pages + fix admon
episodes ux
([0e14eb4](https://code.castopod.org/adaures/castopod/commit/0e14eb4d3f526b0fd256a6144f3fbfc3fe52a357)),
closes [#131](https://code.castopod.org/adaures/castopod/issues/131)
- add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
- add map analytics, add episodes analytics, clean analytics page layout,
translate countries
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add media entity and link documents, images and audio files to it
([6ecf286](https://code.castopod.org/adaures/castopod/commit/6ecf2866cfcde31a0840f15c3340808ce14b44cf))
- add Noto Sans Mono font to use for durations + button to access new video clip
form in list
([7609bb6](https://code.castopod.org/adaures/castopod/commit/7609bb60330539aa91bfdafbb35c2d585624218a))
- add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
- add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24)
- add platform models
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add podcast banner field for each podcast + refactor images configuration
([4a8147b](https://code.castopod.org/adaures/castopod/commit/4a8147bfbbd98d9badfc57a0f2a18bdd5812e802))
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
- add replied to post or comment to reply element
([d0f9c60](https://code.castopod.org/adaures/castopod/commit/d0f9c6018f1af527099f3e26b5d824710fa11caf))
- add schema.org json-ld objects to podcasts, episodes, posts and comments pages
([902f959](https://code.castopod.org/adaures/castopod/commit/902f959b30a10839684f093eb86edebc5d826a0b))
- add task to housekeeping setting for resetting all instance counts
([9303e51](https://code.castopod.org/adaures/castopod/commit/9303e51bc50d730a8026f58984e83b840360ee88))
- add unique listeners analytics
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add user permissions and basic groups to handle authorizations
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18)
- **admin:** make header stick on scroll and show title + action buttons using
css only
([d60498c](https://code.castopod.org/adaures/castopod/commit/d60498c1beb970a14eeb3bbe02d1b1d8116624b0))
- **admin:** update admin layout for better ux + update brand pine colors
([d86142e](https://code.castopod.org/adaures/castopod/commit/d86142ebe7cd7582835f180b79fbeaaaba703528))
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
- **analytics:** add charts and data export
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
- build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
- **cache:** add podcast and episode pages to cache + clear them after insert or
update
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **clips:** setup clip entities and model + save video clip to have it
generated in the background
([2f6fdf9](https://code.castopod.org/adaures/castopod/commit/2f6fdf9091d52ca49709fc82621ba1c6dd0e817d))
- **comments:** add comments to episodes + update naming of status to post
([bb4752c](https://code.castopod.org/adaures/castopod/commit/bb4752c35e086664f5fd75fdc0d56546a1e356f6))
- **comments:** add like / undo like to comment + add comment page
([0c187ef](https://code.castopod.org/adaures/castopod/commit/0c187ef7a9278a60bcc6e5ee4d69d948b51e5c54))
- **components:** add custom view renderer with ComponentRenderer adapted from
bonfire2
([a95de8b](https://code.castopod.org/adaures/castopod/commit/a95de8bab010f6b01c598da72191abe97e473687))
- create optimized & resized images upon upload
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6)
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
- **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
- display legal disclaimer and warning on podcast import page
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34)
- edit + delete podcast and episode
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
- enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9)
- enhance ui using javascript in admin area
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
- **fonts:** replace Montserrat with Inter for better readablity
([bfa11d0](https://code.castopod.org/adaures/castopod/commit/bfa11d007d04b8ac714c8cf3b8050a6aaf177a26))
- import podcast from an rss feed url
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21)
- integrate stylized form components and update podcast edit page
([6536729](https://code.castopod.org/adaures/castopod/commit/653672954606a23796e8a7bda3c34fd6b92f84e0))
- make displayed publication time as relative time using @github/time-elements
([230e139](https://code.castopod.org/adaures/castopod/commit/230e139e43324b9ebef06ca8f6e13b3d9a7bdc70))
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
- **media:** clean media api + create an entity per media type
([fafaa7e](https://code.castopod.org/adaures/castopod/commit/fafaa7e689b17f09a2b056081fa1f4fc53bf716b))
- **media:** save audio, images, transcripts and chapters to media for episode
and persons
([58e2a00](https://code.castopod.org/adaures/castopod/commit/58e2a00a87fa7d5b188e13cc521d94f0cfddba50))
- **meta-tags:** add activitypub alternate links to podcast, episode, comment
and post pages
([bd61752](https://code.castopod.org/adaures/castopod/commit/bd61752be2f574323b05d1d0aee0df55adf9a74e))
- minor corrections to some tables
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
- **nodeinfo2:** add .well-known route for nodeinfo2 containing metadata about
the castopod instance
([88fddc8](https://code.castopod.org/adaures/castopod/commit/88fddc81d730978f2a4d8a671936b54041e3fe45))
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
- **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **podcast:** create a podcast using form
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- prefill season and episode numbers + set episode number as mandatory for
serial podcasts
([07d740b](https://code.castopod.org/adaures/castopod/commit/07d740b79f9283e389e723954f680f909ce5de4a)),
closes [#134](https://code.castopod.org/adaures/castopod/issues/134)
[#136](https://code.castopod.org/adaures/castopod/issues/136)
- **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13)
- **pwa:** add service-worker + webmanifest for each podcasts to have them
install on devices
([fee2c1c](https://code.castopod.org/adaures/castopod/commit/fee2c1c0d0d03c4ff0a6a207b0a5e0c22bb7b13a))
- redesign public podcast and episode pages + remove any information clutter for
better ux
([9321400](https://code.castopod.org/adaures/castopod/commit/932140077c671f0486a2cd08ceb6126c7ecde87f))
- replace form helper functions with components in admin template
([e64548b](https://code.castopod.org/adaures/castopod/commit/e64548b982ba47ff35f2272e2e30dd85eeba950b))
- replace slug field with interactive permalink component
([578022b](https://code.castopod.org/adaures/castopod/commit/578022b8c5163ffaf8db5870ed5ec9d5d9536477))
- restyle episode and person cards + add focus style to interactive elements for
a11y
([a505a1d](https://code.castopod.org/adaures/castopod/commit/a505a1de56e8e3056379bd60d0595f432e294728))
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75)
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80)
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
- **rss:** add podcast:location tag
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
- **rss:** add transcript and chapters support
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.castopod.org/adaures/castopod/issues/72)
[#82](https://code.castopod.org/adaures/castopod/issues/82)
- **rss:** generate rss feed from podcast entity
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
- **select:** enhance select input with choices.js
([910d457](https://code.castopod.org/adaures/castopod/commit/910d457cf843e0fc334b3505a4727d51633395ac))
- set app parameter forceGlobalSecureRequests = true forcing requests to go
through https
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- set podcast / episode description in the pages description meta tag
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44)
- **settings:** add general config for instance (site name, description and
icon)
([5c56f3e](https://code.castopod.org/adaures/castopod/commit/5c56f3e6f00a61af2ccf50811c155c325f2b10fa))
- **settings:** add theme settings to set an accent color for all public pages
([5c529a8](https://code.castopod.org/adaures/castopod/commit/5c529a83aa6d6147d94e5aee996e6b0ab02f0ce4))
- simplify podcast page's layout for better ux
([2c0efc6](https://code.castopod.org/adaures/castopod/commit/2c0efc6563604dd067be88cfc9ddd88a01745e64))
- **soundbites:** add soundbite list and creation forms with audio-clipper
component
([de19317](https://code.castopod.org/adaures/castopod/commit/de19317138a2106deb825c1eed7dda036ed7dac3))
- style file inputs using tailwind's file class
([8208ab6](https://code.castopod.org/adaures/castopod/commit/8208ab6785aae8c49f78eb9ac8cd53d77ec8e5e5))
- **themes:** add ViewThemes library to set views in root themes folder
([7a27676](https://code.castopod.org/adaures/castopod/commit/7a276764e6a1ee3619d9d3488f6163215db75338))
- **themes:** set different default banner per theme
([11c916f](https://code.castopod.org/adaures/castopod/commit/11c916fe433eb749ac32230c48e256057564cbb0))
- **themes:** set generic css variables for colors to enable instance themes
([a746a78](https://code.castopod.org/adaures/castopod/commit/a746a781b4bfc78209cf8302c6d7bb3cb452e446))
- toggle podcast sidebar on smaller screens
([f0205ec](https://code.castopod.org/adaures/castopod/commit/f0205ec274414e881cba40d6776126f05eaee583))
- **transcript:** parse srt subtitles into json file + add max file size info
below audio file input
([0098761](https://code.castopod.org/adaures/castopod/commit/00987610a068c8d6cdd4421ea16585fa037eb61a))
- **ui:** create ViewComponents library to enable building class and view files
components
([94872f2](https://code.castopod.org/adaures/castopod/commit/94872f2338e6025c2f3770be256160838dae9003))
- update analytics so to meet IABv2 requirements
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10)
- update pine colors + create charts components
([a50abc1](https://code.castopod.org/adaures/castopod/commit/a50abc138d4997b564e3065b37504cda5ce62da6))
- **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11)
- **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- **video-clip:** add video-clip page with video preview + logs
([42538dd](https://code.castopod.org/adaures/castopod/commit/42538dd7577be0ffe59b4fdfadbd76cc89e5ef30))
- **video-clip:** generate video clips in the bg using a cron job + add video
clip page + tidy up UI
([db0e427](https://code.castopod.org/adaures/castopod/commit/db0e4272bd6d307c562e1f961d2747cb62de0f35))
- **video-clips:** add dimensions for portrait and squared formats
([3af404d](https://code.castopod.org/adaures/castopod/commit/3af404da3dd1901c78cc7e1778fc225f6716207d))
- **video-clips:** add new themes + add castopod logo as a watermark
([1d1490b](https://code.castopod.org/adaures/castopod/commit/1d1490b06a1f5ecb10b3b98a72efc55d09c10944))
- **video-clips:** add route for scheduled video clips + list video clips with
status
([2065ebb](https://code.castopod.org/adaures/castopod/commit/2065ebbee5e3d0f890ac90b55ca984f1d62a184c))
- **video-clips:** allow episodeNumbering text to stand in the indent of
episodeTitle paragraph
([71a063d](https://code.castopod.org/adaures/castopod/commit/71a063dac311cb21639801fbae6af7c5106c2699))
- **video-clips:** generate a 16:9 video using ffmpeg
([35aa7ea](https://code.castopod.org/adaures/castopod/commit/35aa7ea5d9a339b3e6f745137282268d69fe2231))
- **video-clips:** generate subtitles clip using transcript json to have
subtitles accross video
([3ce07e4](https://code.castopod.org/adaures/castopod/commit/3ce07e455d171e29be30d8ad45055510eb8d363c))
- **video-clips:** replace hardcoded colors with config's theme colors
([e462abf](https://code.castopod.org/adaures/castopod/commit/e462abf6d660e41d2170c52caf45704008de58e9))
- **vite:** add vite config to decouple it from CI_ENVIRONMENT
([8721719](https://code.castopod.org/adaures/castopod/commit/8721719cd7cf32e94823541eafaba1e9309355a8))
- write id3v2 tags to episode's audio file
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
- defer javascript + lazy load images for faster page loads
([f0685e4](https://code.castopod.org/adaures/castopod/commit/f0685e44799dfb494592ff97841c0ae035381db8))
- **docker:** add redis caching service for development
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
- **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
- use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))
### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated
# [1.0.0-alpha.80](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.79...v1.0.0-alpha.80) (2021-12-29)
### Bug Fixes
- add application/octet-stream mimetype to mp3 and m4a extensions to prevent
ext_in error
([339bef8](https://code.castopod.org/adaures/castopod/commit/339bef878e54983d86e91e6ff7a931a843d321b3)),
closes [#145](https://code.castopod.org/adaures/castopod/issues/145)
# [1.0.0-alpha.79](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.78...v1.0.0-alpha.79) (2021-12-20)
### Bug Fixes
- **import:** set episode and season numbers to null when not present in item
tag
([3211398](https://code.castopod.org/adaures/castopod/commit/3211398c78b1b28b76a46427ee07874bbf84a85d))
# [1.0.0-alpha.78](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.77...v1.0.0-alpha.78) (2021-12-15)
### Bug Fixes
- **import:** add extension when downloading file without + truncate slug if too
long
([c5f18bb](https://code.castopod.org/adaures/castopod/commit/c5f18bb6dc08a758ff735454bbe9cfa45a68c09b))
# [1.0.0-alpha.77](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.76...v1.0.0-alpha.77) (2021-11-23)
### Bug Fixes
- **cors:** add preflight option routes for episode, podcast and status objects
([a281abf](https://code.castopod.org/adaures/castopod/commit/a281abfda475388a07943c169dab460cc2d4f944))
- **podcast-import:** move guid attribute declaration for Episode entity to
include slug data
([5d02ae3](https://code.castopod.org/adaures/castopod/commit/5d02ae39908a9d743627135b372bf981134c4328))
# [1.0.0-alpha.76](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.75...v1.0.0-alpha.76) (2021-10-26)
### Bug Fixes
- replace hardcoded style links with vite service + set default value for remote
transcript url
([3f2e056](https://code.castopod.org/adaures/castopod/commit/3f2e05608e43d47bbb518a9acfaf56ec3eefafb4)),
closes [#149](https://code.castopod.org/adaures/castopod/issues/149)
[#150](https://code.castopod.org/adaures/castopod/issues/150)
# [1.0.0-alpha.75](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.74...v1.0.0-alpha.75) (2021-10-05)
### Bug Fixes
- **rss:** cast number type values to string in rss_helper
([7180ae9](https://code.castopod.org/adaures/castopod/commit/7180ae9ec700930b69c04ed91f8eceea16ad77ce)),
closes [#148](https://code.castopod.org/adaures/castopod/issues/148)
# [1.0.0-alpha.74](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.73...v1.0.0-alpha.74) (2021-09-28)
### Features
- **platforms:** add missing newpodcastapps.com's platforms
([92dd370](https://code.castopod.org/adaures/castopod/commit/92dd370e2f9a464edd26cddcde96d0e16f91548d))
# [1.0.0-alpha.73](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.72...v1.0.0-alpha.73) (2021-09-22)
### Bug Fixes
- **map:** update episode markers query to discard unpublished episodes
([b3caac4](https://code.castopod.org/adaures/castopod/commit/b3caac45b12a23e4289d00133d2ad7915d084c44))
# [1.0.0-alpha.72](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.71...v1.0.0-alpha.72) (2021-09-20)
### Bug Fixes
- rename field status to task_status to get scheduled activities
([4ff82a5](https://code.castopod.org/adaures/castopod/commit/4ff82a5f0a38dbbc9e272fca7df70ea5a190e334))
# [1.0.0-alpha.71](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.70...v1.0.0-alpha.71) (2021-09-17)
### Features
- **map:** display geolocated episodes on a map page
([4357cc2](https://code.castopod.org/adaures/castopod/commit/4357cc25ccc585ce398035c1c25d566b6a9df775))
# [1.0.0-alpha.70](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.69...v1.0.0-alpha.70) (2021-08-31)
### Bug Fixes
- **partner:** set correct image URL
([61554be](https://code.castopod.org/adaures/castopod/commit/61554be12a64d59ab99fab810b1b05632b408f3a))
# [1.0.0-alpha.69](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.68...v1.0.0-alpha.69) (2021-08-23)
### Bug Fixes
- **import:** cast description's SimpleXMLElement to string
([02d17be](https://code.castopod.org/adaures/castopod/commit/02d17be4ffe229fc6657207d31eba0543b5f1a4c))
# [1.0.0-alpha.68](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.67...v1.0.0-alpha.68) (2021-08-19)
### Bug Fixes
- **analytics:** redirect to mp3 file even when referer was not set
([9fc388d](https://code.castopod.org/adaures/castopod/commit/9fc388d154f29c335dedcd624abe8c1751762c07))
# [1.0.0-alpha.67](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.66...v1.0.0-alpha.67) (2021-07-24)
### Features
- allow cross origin requests on episode comments
([e12f95a](https://code.castopod.org/adaures/castopod/commit/e12f95aca13c6d54489a9cfd99d4cd2490fe83ab))
# [1.0.0-alpha.66](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.65...v1.0.0-alpha.66) (2021-07-24)
### Features
- **rss:** add podcast:comments tag to link to episode comments
([32e8c7c](https://code.castopod.org/adaures/castopod/commit/32e8c7c16a61ffe08e2f3bfbdeda556811a0358c))
# [1.0.0-alpha.65](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.64...v1.0.0-alpha.65) (2021-07-22)
### Bug Fixes
- update conditions when checking for empty max_episodes and season_number
([fbad0b5](https://code.castopod.org/adaures/castopod/commit/fbad0b59f68c65eba2fdcd5a8d3b312b622e9a45))
# [1.0.0-alpha.64](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.63...v1.0.0-alpha.64) (2021-07-12)
### Features
- **activitypub:** add Podcast actor and PodcastEpisode object with comments
([9e1e5d2](https://code.castopod.org/adaures/castopod/commit/9e1e5d2e862d6a3345d11ca7f96b955c76bfa013))
# [1.0.0-alpha.63](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.62...v1.0.0-alpha.63) (2021-07-12)
### Features
- build hashed static files to renew browser cache
([37c54d2](https://code.castopod.org/adaures/castopod/commit/37c54d247749bdf8f528babd4a78f24d48051063)),
closes [#107](https://code.castopod.org/adaures/castopod/issues/107)
# [1.0.0-alpha.62](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.61...v1.0.0-alpha.62) (2021-07-02)
### Bug Fixes
- **episode:** replace guid's empty string value to null
([441052a](https://code.castopod.org/adaures/castopod/commit/441052af8d99e6e317edefd1e58ad71799357088))
# [1.0.0-alpha.61](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.60...v1.0.0-alpha.61) (2021-06-23)
### Bug Fixes
- **release:** add missing version number to castopod-host package
([8f3e9d9](https://code.castopod.org/adaures/castopod/commit/8f3e9d90c14545d3f84d4469b26a53db4554b4dc))
- **ux:** allow for empty message upon episode publication and warn user on
submit
([33d01b8](https://code.castopod.org/adaures/castopod/commit/33d01b8d4fd6ebf24e9f011aa705c456c846956c)),
closes [#129](https://code.castopod.org/adaures/castopod/issues/129)
# [1.0.0-alpha.60](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.59...v1.0.0-alpha.60) (2021-06-21)
### Features
- **rss:** add ˂podcast:guid˃ tag for channel
([1fab10e](https://code.castopod.org/adaures/castopod/commit/1fab10eb0d63bb7c3edf34ffe691e2aec2c2e43c))
# [1.0.0-alpha.59](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.58...v1.0.0-alpha.59) (2021-06-15)
### Bug Fixes
- check that additional files are valid when creating episode
([eac5bc8](https://code.castopod.org/adaures/castopod/commit/eac5bc876de125e1fe08d1b89f767a04fc0fbfb6))
# [1.0.0-alpha.58](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.57...v1.0.0-alpha.58) (2021-06-11)
### Bug Fixes
- cast actor_id to pass as int to set_interact_as_actor() function
([56a8e5d](https://code.castopod.org/adaures/castopod/commit/56a8e5d7dd615322aeb007e730801c65d0b02e5c))
- **analytics:** set duration field to precise decimal as episode's audio file
duration
([d772685](https://code.castopod.org/adaures/castopod/commit/d77268540569b2be9d91d5e09aefb3ff5ac2b071))
- **analytics:** update migrations to set decimal precision for latitude and
longitude
([714d6b5](https://code.castopod.org/adaures/castopod/commit/714d6b5d4950e52cf1c3170bb59954f98ffd48bd))
- check for database connection and podcasts table existence before redirecting
to install
([eb74e81](https://code.castopod.org/adaures/castopod/commit/eb74e81c3d93581e310b391cd029e62a0d690a8a))
- save transcript and chapters files to podcasts folder
([63f49c7](https://code.castopod.org/adaures/castopod/commit/63f49c719f672b615c5a8893d3868dffcd332e47))
- set cache expiration to next note publish to show note on publication date
([0a66de3](https://code.castopod.org/adaures/castopod/commit/0a66de3e6c17d4ac94ee8e13bd00ceaf64b1303e))
- set episode description footer to null when empty value
([3a7d97d](https://code.castopod.org/adaures/castopod/commit/3a7d97d660046d80698611311ff3708110d2af82))
- set location to null when getting empty string
([71b1b5f](https://code.castopod.org/adaures/castopod/commit/71b1b5f775af475b1dc78328330e277f565e41b6))
- update condition in home controller to redirect to install page
([33f1b91](https://code.castopod.org/adaures/castopod/commit/33f1b91d55dd0652c979d50fc85879dbf88a4a42))
- **activity-pub:** cache issues when navigating to activity stream urls
([7bcbfb3](https://code.castopod.org/adaures/castopod/commit/7bcbfb32f7cca08d111be46c7f1640e372d4a4b0))
- **activity-pub:** get database records using new model instances
([92536dd](https://code.castopod.org/adaures/castopod/commit/92536ddb3812214a9c5682b92e547e5c1998a5d7))
- **category:** remove uncategorized option to enforce users in choosing a
category
([8c64f25](https://code.castopod.org/adaures/castopod/commit/8c64f25a0e72fec03d25544797d32623b2276fce))
- **install:** redirect manually to install wizard on first visit
([2ceaaca](https://code.castopod.org/adaures/castopod/commit/2ceaaca44f1b82fc64d961e2fb4f4aaeade7e736))
- **types:** update fake seeders types + fix bugs
([76a4bf3](https://code.castopod.org/adaures/castopod/commit/76a4bf344160df679db29e236e7df7822970fb60))
- update broken contributor dropdown fields
([e5b7515](https://code.castopod.org/adaures/castopod/commit/e5b75150234bd7f19e01def93425d3bda7379dd3))
- **ux:** redirect user to install page on database error in home page
([9017e30](https://code.castopod.org/adaures/castopod/commit/9017e30bf41bed8c2be65091bbc5fb1e63aef87a))
- update condition in AnalyticsTrait
([fbc0967](https://code.castopod.org/adaures/castopod/commit/fbc0967caa81630d514ddb1b93b0834ebb4d913b))
### Performance Improvements
- **cache:** use deleteMatching method to prevent forgetting cached elements in
models
([76afc0c](https://code.castopod.org/adaures/castopod/commit/76afc0cfa2feb087697bae4bc138e4956873dd62))
### Reverts
- set deprecated config options back in App config
([433745f](https://code.castopod.org/adaures/castopod/commit/433745f194c73407999b207090478563283876a5))
# [1.0.0-alpha.57](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.56...v1.0.0-alpha.57) (2021-05-12)
### Bug Fixes
- **follow:** add missing helpers to Actor controller
([ee53a73](https://code.castopod.org/adaures/castopod/commit/ee53a732dc12ebbf5706e14969749a12cfd9d559))
# [1.0.0-alpha.56](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.55...v1.0.0-alpha.56) (2021-05-12)
### Bug Fixes
- **rss:** use originalPath instead of originalMediaPath in Image library
([b4012b7](https://code.castopod.org/adaures/castopod/commit/b4012b7d2ed6b34b69ad767570dd33f0dc7db920))
# [1.0.0-alpha.55](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.54...v1.0.0-alpha.55) (2021-05-03)
### Features
- add remote_url alternative for transcript and chapters files
([3143c9a](https://code.castopod.org/adaures/castopod/commit/3143c9ad36e4cf1364205cf2be39c0c96f80fdd2))
# [1.0.0-alpha.54](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.53...v1.0.0-alpha.54) (2021-05-03)
### Features
- set app parameter forceGlobalSecureRequests = true forcing requests to go
through https
([d9dff1b](https://code.castopod.org/adaures/castopod/commit/d9dff1b8bf89c8b526ad6cb89f98a1f160d49117))
- **ux:** remove admin dashboard and redirect directly to podcast list
([27c48b8](https://code.castopod.org/adaures/castopod/commit/27c48b8fa930b33e5e15f0c8685e468e857ca9cd))
- add cache to ActivityPub sql queries + cache activity and note pages
([2d297f4](https://code.castopod.org/adaures/castopod/commit/2d297f45b3d7ef6e8711875a0b9b908e878115fa))
### Performance Improvements
- **cache:** update CI4 to use cache's deleteMatching method
([54b84f9](https://code.castopod.org/adaures/castopod/commit/54b84f96843af13f579fea49102c8c2ef81b0a54))
- **docker:** add redis caching service for development
([05ace8c](https://code.castopod.org/adaures/castopod/commit/05ace8cff2ef02d19abd40097ac5546dca6a54ca))
# [1.0.0-alpha.53](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.52...v1.0.0-alpha.53) (2021-04-16)
### Bug Fixes
- check that note has a preview_card_id before displaying it
([acb8b3a](https://code.castopod.org/adaures/castopod/commit/acb8b3a40172ccb184ffe544760601d756692e6c)),
closes [#114](https://code.castopod.org/adaures/castopod/issues/114)
# [1.0.0-alpha.52](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.51...v1.0.0-alpha.52) (2021-04-16)
### Bug Fixes
- **avatar:** use default avatar when no avatar url has been set
([9d23c7e](https://code.castopod.org/adaures/castopod/commit/9d23c7e7e142c6cf1a1418e37e41d711064593c4)),
closes [#111](https://code.castopod.org/adaures/castopod/issues/111)
# [1.0.0-alpha.51](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.50...v1.0.0-alpha.51) (2021-04-15)
### Bug Fixes
- **interact-as:** set actor_id instead of podcast id upon login event
([5dfade7](https://code.castopod.org/adaures/castopod/commit/5dfade7cf37f339c56d2e577c679b88a1b1d9336)),
closes [#104](https://code.castopod.org/adaures/castopod/issues/104)
# [1.0.0-alpha.50](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.49...v1.0.0-alpha.50) (2021-04-14)
### Bug Fixes
- **persons:** prevent overflow of persons list by adding horizontal scroll
([9e8995d](https://code.castopod.org/adaures/castopod/commit/9e8995dc6e039032cc65f87895cf770f99e8b244))
# [1.0.0-alpha.49](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.48...v1.0.0-alpha.49) (2021-04-12)
### Bug Fixes
- **multiselect:** add missing class names in choices options for purge to work
properly
([719538d](https://code.castopod.org/adaures/castopod/commit/719538d0ccb28af3c3c5e1a4b6468d4b772fe819))
# [1.0.0-alpha.48](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.47...v1.0.0-alpha.48) (2021-04-10)
### Bug Fixes
- **import-with-escaped-characters:** remove \CodeIgniter\HTTP\URI in
download_file, closes
[#103](https://code.castopod.org/adaures/castopod/issues/103)
([35b5be0](https://code.castopod.org/adaures/castopod/commit/35b5be095ff54d27acec1610a846ec0cdbdf1d65))
# [1.0.0-alpha.47](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.46...v1.0.0-alpha.47) (2021-04-10)
### Bug Fixes
- **episodeCount:** add missing brackets to French language file
([c1b4112](https://code.castopod.org/adaures/castopod/commit/c1b411265ad9b06e95a8b097ecf73445b88dcb45))
# [1.0.0-alpha.46](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.45...v1.0.0-alpha.46) (2021-04-09)
### Bug Fixes
- **episodes-page:** handle defaultQuery being null when no podcast episodes
([15183b7](https://code.castopod.org/adaures/castopod/commit/15183b7eab57dac007bcdfa8c3651239de1ae05a)),
closes [#100](https://code.castopod.org/adaures/castopod/issues/100)
# [1.0.0-alpha.45](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.44...v1.0.0-alpha.45) (2021-04-08)
### Bug Fixes
- add head request to analytics_hit route
([f0a2f0b](https://code.castopod.org/adaures/castopod/commit/f0a2f0bea491ca91976b351bb79837e95c9d094b))
# [1.0.0-alpha.44](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.43...v1.0.0-alpha.44) (2021-04-08)
### Bug Fixes
- **rss:** set ❬itunes:author❭ tag to owner_name if publisher not specified
([2271c14](https://code.castopod.org/adaures/castopod/commit/2271c1445b1ded12bc53b5d23b5e59d12b17c71a)),
closes [#96](https://code.castopod.org/adaures/castopod/issues/96)
# [1.0.0-alpha.43](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.42...v1.0.0-alpha.43) (2021-04-08)
### Bug Fixes
- **episode-form:** show warning to set `memory_limit`, `upload_max_filesize` &
`post_max_size`
([3b3c218](https://code.castopod.org/adaures/castopod/commit/3b3c218b9c868e9f12c54d7670e69d84c9ee79c0)),
closes [#5](https://code.castopod.org/adaures/castopod/issues/5)
[#86](https://code.castopod.org/adaures/castopod/issues/86)
# [1.0.0-alpha.42](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.41...v1.0.0-alpha.42) (2021-04-02)
### Features
- **fediverse:** implement activitypub protocols + update user interface
([2f525c0](https://code.castopod.org/adaures/castopod/commit/2f525c0f6e44d320bff16e22c223481923ba683e)),
closes [#69](https://code.castopod.org/adaures/castopod/issues/69)
[#65](https://code.castopod.org/adaures/castopod/issues/65)
[#85](https://code.castopod.org/adaures/castopod/issues/85)
[#51](https://code.castopod.org/adaures/castopod/issues/51)
[#91](https://code.castopod.org/adaures/castopod/issues/91)
[#92](https://code.castopod.org/adaures/castopod/issues/92)
[#88](https://code.castopod.org/adaures/castopod/issues/88)
# [1.0.0-alpha.41](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.40...v1.0.0-alpha.41) (2021-03-30)
### Features
- **partner:** add link and image in episode description
([ad07bb9](https://code.castopod.org/adaures/castopod/commit/ad07bb9330dc9493813368e969e1f3a3def44614))
# [1.0.0-alpha.40](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.39...v1.0.0-alpha.40) (2021-03-19)
### Features
- **custom-rss:** add custom xml tag injection in rss feed for ❬channel❭ and
❬item❭
([6ecdaad](https://code.castopod.org/adaures/castopod/commit/6ecdaad911d06b7f7a2b7d24710968c7eb9118f6))
# [1.0.0-alpha.39](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.38...v1.0.0-alpha.39) (2021-03-01)
### Bug Fixes
- **embeddable-player:** enable any ancestor when X-Frame-Options is set on
server
([44a4962](https://code.castopod.org/adaures/castopod/commit/44a4962e0b7e3ed87e9914b4e7792a0d52330ff8))
# [1.0.0-alpha.38](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.37...v1.0.0-alpha.38) (2021-02-27)
### Features
- **embeddable-player:** add embeddable player widget
([141788f](https://code.castopod.org/adaures/castopod/commit/141788fa089f9dedc8956c64ca515a4a4625f904))
# [1.0.0-alpha.37](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.36...v1.0.0-alpha.37) (2021-02-17)
### Bug Fixes
- **import:** remove query string from files url
([109c4aa](https://code.castopod.org/adaures/castopod/commit/109c4aa1afb72dd8b99c0302d74a7fef5a38638e))
# [1.0.0-alpha.36](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.35...v1.0.0-alpha.36) (2021-02-16)
### Features
- **platforms:** add pod.link
([3d7a232](https://code.castopod.org/adaures/castopod/commit/3d7a2320ddd116e4a311605421126aff57243219))
# [1.0.0-alpha.35](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.34...v1.0.0-alpha.35) (2021-02-12)
### Bug Fixes
- **admin:** save block and lock switches
([b66c0af](https://code.castopod.org/adaures/castopod/commit/b66c0afc8fab2e338402a9a4f8105e5f5459e208))
# [1.0.0-alpha.34](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.33...v1.0.0-alpha.34) (2021-02-11)
### Bug Fixes
- **rss-import:** add Castopod user-agent, handle redirects for downloaded
files, add Content namespace
([214243b](https://code.castopod.org/adaures/castopod/commit/214243b3fec4937e45ef1ceaba1149004cdf3b44))
# [1.0.0-alpha.33](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.32...v1.0.0-alpha.33) (2021-02-10)
### Features
- **platforms:** add helloasso
([16cb993](https://code.castopod.org/adaures/castopod/commit/16cb993ee6e28987a840fc27a9c2c73794c67697))
# [1.0.0-alpha.32](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.31...v1.0.0-alpha.32) (2021-02-10)
### Features
- **person:** add podcastindex.org namespace person tag
([8acd011](https://code.castopod.org/adaures/castopod/commit/8acd011f13e99492ef4b44b327685bb006fe5f8f))
# [1.0.0-alpha.31](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.30...v1.0.0-alpha.31) (2020-12-23)
### Features
- **rss:** add podcast:location tag
([c0a2282](https://code.castopod.org/adaures/castopod/commit/c0a22829bd87d48535a86e60c6cd7280e44683a2))
# [1.0.0-alpha.30](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.29...v1.0.0-alpha.30) (2020-12-21)
### Features
- **rss:** update monetization tag so that it meets PodcastIndex requirements
([4c7ecbe](https://code.castopod.org/adaures/castopod/commit/4c7ecbee83950e5f9f2482cedaab18a1ac9bfc9e))
# [1.0.0-alpha.29](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.28...v1.0.0-alpha.29) (2020-12-10)
### Bug Fixes
- **episodes:** add publication status + set publication date to null when none
has been set
([d882981](https://code.castopod.org/adaures/castopod/commit/d882981b3a86c81921ce6b07d4cf61fc13983689)),
closes [#70](https://code.castopod.org/adaures/castopod/issues/70)
### Reverts
- **soundbites:** remove soundbite table from episode's public page
([5dc0f19](https://code.castopod.org/adaures/castopod/commit/5dc0f19656de0d764f627d6ae78a9e306c901835))
# [1.0.0-alpha.28](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.27...v1.0.0-alpha.28) (2020-12-07)
### Features
- **rss:** add soundbites according to the podcastindex specs
([6b34617](https://code.castopod.org/adaures/castopod/commit/6b34617d07c70522cb941e96d91d9987493413eb)),
closes [#83](https://code.castopod.org/adaures/castopod/issues/83)
# [1.0.0-alpha.27](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.26...v1.0.0-alpha.27) (2020-12-07)
### Features
- **platforms:** add AntennaPod
([53e9cfd](https://code.castopod.org/adaures/castopod/commit/53e9cfd61c794b1539e9d4691d3c4e73c4b7aaa7))
# [1.0.0-alpha.26](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.25...v1.0.0-alpha.26) (2020-11-30)
### Bug Fixes
- **analytics:** update service management so that it works with new OPAWG slug
values
([7fe9d42](https://code.castopod.org/adaures/castopod/commit/7fe9d42500ade2c6fa3ff4365b4affc475af0e51))
# [1.0.0-alpha.25](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.24...v1.0.0-alpha.25) (2020-11-30)
### Features
- **platforms:** add podfriend
([9fdc8d3](https://code.castopod.org/adaures/castopod/commit/9fdc8d32930234c7ffd2be6892be57febcef1086))
# [1.0.0-alpha.24](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.23...v1.0.0-alpha.24) (2020-11-26)
### Features
- **monetization:** add Web Monetization support
([96a6026](https://code.castopod.org/adaures/castopod/commit/96a6026f1db452085360f5fe248de82a2ec06468))
# [1.0.0-alpha.23](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.22...v1.0.0-alpha.23) (2020-11-24)
### Bug Fixes
- define podcastNamespaceLink value
([0d744d2](https://code.castopod.org/adaures/castopod/commit/0d744d212df0d070ceea185068eaf2746e1ccd48))
# [1.0.0-alpha.22](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.21...v1.0.0-alpha.22) (2020-11-24)
### Features
- **rss:** add transcript and chapters support
([e769d83](https://code.castopod.org/adaures/castopod/commit/e769d83a932c169e52a630a17cd4dd8ac5cebaf6)),
closes [#72](https://code.castopod.org/adaures/castopod/issues/72)
[#82](https://code.castopod.org/adaures/castopod/issues/82)
# [1.0.0-alpha.21](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.20...v1.0.0-alpha.21) (2020-11-24)
### Features
- **platforms:** add Fediverse and some funding platforms, add link on logo
([afc3d50](https://code.castopod.org/adaures/castopod/commit/afc3d50289bb4173e0697d109ffe72f6814b93d1))
# [1.0.0-alpha.20](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.19...v1.0.0-alpha.20) (2020-11-24)
### Bug Fixes
- **import:** use <image><url> tag when no <itunes:image> is present
([20e607a](https://code.castopod.org/adaures/castopod/commit/20e607afb755bc75056041738fa7cbf6723d754c))
### Features
- **rss:** add podcast-namespace tags for platforms + previousUrl tag
([dbba8dc](https://code.castopod.org/adaures/castopod/commit/dbba8dc58133967c778514268cbfed8098ed1dbc)),
closes [#73](https://code.castopod.org/adaures/castopod/issues/73)
[#75](https://code.castopod.org/adaures/castopod/issues/75)
[#76](https://code.castopod.org/adaures/castopod/issues/76)
[#80](https://code.castopod.org/adaures/castopod/issues/80)
# [1.0.0-alpha.19](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.18...v1.0.0-alpha.19) (2020-11-13)
### Bug Fixes
- handle HEAD requests on podcast_feed route
([74b2640](https://code.castopod.org/adaures/castopod/commit/74b2640f2a25c4cd6fd8835fc492c2a6893d4950)),
closes [#79](https://code.castopod.org/adaures/castopod/issues/79)
# [1.0.0-alpha.18](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.17...v1.0.0-alpha.18) (2020-11-09)
### Features
- **platforms:** add Podcast Index
([ad52b1c](https://code.castopod.org/adaures/castopod/commit/ad52b1cc2b7d0bc844970214d205961a7196b4a9))
# [1.0.0-alpha.17](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.16...v1.0.0-alpha.17) (2020-11-05)
### Bug Fixes
- **open-graph:** replace non existant episode description to podcast
description in podcast page
([b02584e](https://code.castopod.org/adaures/castopod/commit/b02584ee609af1ad1b5680cc28208d113eb0410b))
# [1.0.0-alpha.16](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.15...v1.0.0-alpha.16) (2020-11-04)
### Features
- add Open Graph and Twitter meta tags
([af970b8](https://code.castopod.org/adaures/castopod/commit/af970b8bac949e4c63047e04aca1b7403a4e8deb)),
closes [#41](https://code.castopod.org/adaures/castopod/issues/41)
# [1.0.0-alpha.15](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.14...v1.0.0-alpha.15) (2020-11-03)
### Features
- **analytics:** add 'other' group to pie charts in order to display more
accurate data
([73acef9](https://code.castopod.org/adaures/castopod/commit/73acef933ff3485987afc5157de022910876fc12))
# [1.0.0-alpha.14](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.13...v1.0.0-alpha.14) (2020-11-02)
### Features
- **analytics:** add weekday and hour bar charts
([8ab3132](https://code.castopod.org/adaures/castopod/commit/8ab313296bb4a254ab05e90b17d896039839b784))
# [1.0.0-alpha.13](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.12...v1.0.0-alpha.13) (2020-10-29)
### Bug Fixes
- **episodes-table:** set descriptions to be not null
([6774ec1](https://code.castopod.org/adaures/castopod/commit/6774ec10fa78527be6b7548ca1dc34ad0ada090c))
### Features
- add episode_numbering() component helper to display episode and season numbers
([3f4a6bd](https://code.castopod.org/adaures/castopod/commit/3f4a6bd0b9f870f16107a41b102b6bf734868198))
- **episodes:** replace all audio file URL parameters with base64 encoded data
([e1f65cd](https://code.castopod.org/adaures/castopod/commit/e1f65cd3b53353a30d4ab6eb5312393cf04a1676))
# [1.0.0-alpha.12](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.11...v1.0.0-alpha.12) (2020-10-26)
### Bug Fixes
- replace getWebEnclosureUrl with getEnclosureWebUrl
([8122cea](https://code.castopod.org/adaures/castopod/commit/8122ceaf8a70050f14b3078f28b024e7d7cdb9ac))
# [1.0.0-alpha.11](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.10...v1.0.0-alpha.11) (2020-10-26)
### Features
- add CDN url
([972bcbf](https://code.castopod.org/adaures/castopod/commit/972bcbf65ee119b8641ca3c4e5c0e8cf9ca8dd4f)),
closes [#37](https://code.castopod.org/adaures/castopod/issues/37)
# [1.0.0-alpha.10](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.9...v1.0.0-alpha.10) (2020-10-26)
### Bug Fixes
- **install:** redirect to host_url install route on instanceConfig validation
error
([99250b1](https://code.castopod.org/adaures/castopod/commit/99250b1868657c249a447399c7ebc69e00d43d1a))
# [1.0.0-alpha.9](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.8...v1.0.0-alpha.9) (2020-10-26)
### Features
- display castopod version in admin footer
([9f2574e](https://code.castopod.org/adaures/castopod/commit/9f2574e6fbb61dac4e1a4252dff30017685da5f0)),
closes [#68](https://code.castopod.org/adaures/castopod/issues/68)
# [1.0.0-alpha.8](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.7...v1.0.0-alpha.8) (2020-10-22)
### Features
- **episodes:** schedule episode with future publication_date by using cache
expiration time
([4f1e773](https://code.castopod.org/adaures/castopod/commit/4f1e773c0f9e4c2597f6c1b0a4773dfb34b2f203)),
closes [#47](https://code.castopod.org/adaures/castopod/issues/47)
# [1.0.0-alpha.7](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.6...v1.0.0-alpha.7) (2020-10-21)
### Features
- **analytics:** add service name from rss user-agent
([7202b98](https://code.castopod.org/adaures/castopod/commit/7202b9867bd59aafa8c338a4230fb5e5c55b24c6))
### BREAKING CHANGES
- **analytics:** analytics_podcasts_by_player table and analytics_podcasts
procedure were updated
# [1.0.0-alpha.6](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.5...v1.0.0-alpha.6) (2020-10-20)
### Bug Fixes ### Bug Fixes
- **cache:** add locale for podcast and episode pages + clear some persisting - **cache:** add locale for podcast and episode pages + clear some persisting
cache in models cache in models
([9cec8a8](https://code.castopod.org/adaures/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)), ([9cec8a8](https://code.podlibre.org/podlibre/castopod/commit/9cec8a81ccbb7239402fe6633dbc31979272302a)),
closes [#42](https://code.castopod.org/adaures/castopod/issues/42) closes [#42](https://code.podlibre.org/podlibre/castopod/issues/42)
[#61](https://code.castopod.org/adaures/castopod/issues/61) [#61](https://code.podlibre.org/podlibre/castopod/issues/61)
# [1.0.0-alpha.5](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2020-10-20) # [1.0.0-alpha.5](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.4...v1.0.0-alpha.5) (2020-10-20)
### Features ### Features
- add lock podcast according to the Podcastindex podcast-namespace to prevent - add lock podcast according to the Podcastindex podcast-namespace to prevent
unauthozized import unauthozized import
([72b3012](https://code.castopod.org/adaures/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b)) ([72b3012](https://code.podlibre.org/podlibre/castopod/commit/72b301272e0b70ded3e2b237391909e3f152ad0b))
# [1.0.0-alpha.4](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2020-10-20) # [1.0.0-alpha.4](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.3...v1.0.0-alpha.4) (2020-10-20)
### Features ### Features
- **analytics:** add charts and data export - **analytics:** add charts and data export
([78625c4](https://code.castopod.org/adaures/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef)) ([78625c4](https://code.podlibre.org/podlibre/castopod/commit/78625c471b4f03a09bd42f72b82217e1f2d01cef))
# [1.0.0-alpha.3](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2020-10-19) # [1.0.0-alpha.3](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.2...v1.0.0-alpha.3) (2020-10-19)
### Bug Fixes ### Bug Fixes
- **analytics:** remove charts empty values + remove useless language cache - **analytics:** remove charts empty values + remove useless language cache
([1678794](https://code.castopod.org/adaures/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc)) ([1678794](https://code.podlibre.org/podlibre/castopod/commit/16787941539ba4014281a366789ea896a9cd2afc))
# [1.0.0-alpha.2](https://code.castopod.org/adaures/castopod/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2020-10-19) # [1.0.0-alpha.2](https://code.podlibre.org/podlibre/castopod/compare/v1.0.0-alpha.1...v1.0.0-alpha.2) (2020-10-19)
### Features ### Features
- add cumulative listening time charts - add cumulative listening time charts
([588b4d2](https://code.castopod.org/adaures/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716)) ([588b4d2](https://code.podlibre.org/podlibre/castopod/commit/588b4d28da00bc12d02126e23181690f54d81716))
# 1.0.0-alpha.1 (2020-10-16) # 1.0.0-alpha.1 (2020-10-16)
### Bug Fixes ### Bug Fixes
- add public/media folder to castopod bundle - add public/media folder to castopod bundle
([8053d35](https://code.castopod.org/adaures/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)), ([8053d35](https://code.podlibre.org/podlibre/castopod/commit/8053d3521b481872711dabaaf265d08b9bfbaa87)),
closes [#52](https://code.castopod.org/adaures/castopod/issues/52) closes [#52](https://code.podlibre.org/podlibre/castopod/issues/52)
- add where condition to get episode count without deleted episodes - add where condition to get episode count without deleted episodes
([7661734](https://code.castopod.org/adaures/castopod/commit/7661734ed296654630f3668132671117519145dd)), ([7661734](https://code.podlibre.org/podlibre/castopod/commit/7661734ed296654630f3668132671117519145dd)),
closes [#67](https://code.castopod.org/adaures/castopod/issues/67) closes [#67](https://code.podlibre.org/podlibre/castopod/issues/67)
- comment all cache clean after page update to prevent analytics cache deletion - comment all cache clean after page update to prevent analytics cache deletion
([e6197a4](https://code.castopod.org/adaures/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7)) ([e6197a4](https://code.podlibre.org/podlibre/castopod/commit/e6197a4972a3cce3d67dd7972bb54f8720b8e5b7))
- correct chart data - correct chart data
([4d3e9c8](https://code.castopod.org/adaures/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a)) ([4d3e9c8](https://code.podlibre.org/podlibre/castopod/commit/4d3e9c8c02cdc882e9fe1c29625695b6f83c820a))
- correct percona compatibility issue - correct percona compatibility issue
([e53f819](https://code.castopod.org/adaures/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef)) ([e53f819](https://code.podlibre.org/podlibre/castopod/commit/e53f819264b2d6902996f11ffcbb7c99295a90ef))
- correct php-fpm issues - correct php-fpm issues
([1ef55d7](https://code.castopod.org/adaures/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0)) ([1ef55d7](https://code.podlibre.org/podlibre/castopod/commit/1ef55d7315bb44abe05f02ec8a84b6b6a557a9a0))
- correct referrer bug - correct referrer bug
([ed69b2f](https://code.castopod.org/adaures/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637)) ([ed69b2f](https://code.podlibre.org/podlibre/castopod/commit/ed69b2f5004ed1cd18bac824c08a0df01f5d2637))
- correction for servers with low int precision - correction for servers with low int precision
([31b7828](https://code.castopod.org/adaures/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571)) ([31b7828](https://code.podlibre.org/podlibre/castopod/commit/31b7828e77519ef43e9bcfcbdf6c21712f97a571))
- declare typed properties in PHPDoc for php<7.4 - declare typed properties in PHPDoc for php<7.4
([14dd44d](https://code.castopod.org/adaures/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)), ([14dd44d](https://code.podlibre.org/podlibre/castopod/commit/14dd44d03d6db0d9ae4198db8e65c92a0e45cb31)),
closes [#23](https://code.castopod.org/adaures/castopod/issues/23) closes [#23](https://code.podlibre.org/podlibre/castopod/issues/23)
- escape generated feed tag values and remove new lines from public pages meta - escape generated feed tag values and remove new lines from public pages meta
description description
([6238a43](https://code.castopod.org/adaures/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)), ([6238a43](https://code.podlibre.org/podlibre/castopod/commit/6238a43863210afe8988ad7cf251e6bfc6c8557c)),
closes [#57](https://code.castopod.org/adaures/castopod/issues/57) closes [#57](https://code.podlibre.org/podlibre/castopod/issues/57)
[#46](https://code.castopod.org/adaures/castopod/issues/46) [#46](https://code.podlibre.org/podlibre/castopod/issues/46)
- fix layout bugs in admin and update translation files - fix layout bugs in admin and update translation files
([a834171](https://code.castopod.org/adaures/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)), ([a834171](https://code.podlibre.org/podlibre/castopod/commit/a83417180cf61cdfadc5509b0aaa2fdb66592be3)),
closes [#40](https://code.castopod.org/adaures/castopod/issues/40) closes [#40](https://code.podlibre.org/podlibre/castopod/issues/40)
- minor corrections - minor corrections
([13be386](https://code.castopod.org/adaures/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171)) ([13be386](https://code.podlibre.org/podlibre/castopod/commit/13be386842e94d9def1f7de4720931d8f6935171))
- move analytics to helper - move analytics to helper
([d311917](https://code.castopod.org/adaures/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603)) ([d311917](https://code.podlibre.org/podlibre/castopod/commit/d31191732e41aa106234b5ebe6e54ee02f0ce603))
- re-order graph values - re-order graph values
([35f633b](https://code.castopod.org/adaures/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f)) ([35f633b](https://code.podlibre.org/podlibre/castopod/commit/35f633b4c71c087d1ddc9bba9e9bbe18de09204f))
- remove required for other_categories field and add podcast_id to latest - remove required for other_categories field and add podcast_id to latest
podcasts query podcasts query
([5417be0](https://code.castopod.org/adaures/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243)) ([5417be0](https://code.podlibre.org/podlibre/castopod/commit/5417be0049288489a19c7b575aa77bd1e2bc0243))
- rename issue_templates labels - rename issue_templates labels
([9f00305](https://code.castopod.org/adaures/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6)) ([9f00305](https://code.podlibre.org/podlibre/castopod/commit/9f00305844e5a168e89d727fe29892b4ad5e48d6))
- rename MyAccount controller file - rename MyAccount controller file
([e109df3](https://code.castopod.org/adaures/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)), ([e109df3](https://code.podlibre.org/podlibre/castopod/commit/e109df3004a3a98d72de39532e062fff9917f50f)),
closes [#60](https://code.castopod.org/adaures/castopod/issues/60) closes [#60](https://code.podlibre.org/podlibre/castopod/issues/60)
- reorder fields as composite primary keys for analytics tables - reorder fields as composite primary keys for analytics tables
([9660aa9](https://code.castopod.org/adaures/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63)) ([9660aa9](https://code.podlibre.org/podlibre/castopod/commit/9660aa97c8ffd4fe61f3a388d52b9ac5dd8e1d63))
- replace website key for webpages in breadcrumb translate file - replace website key for webpages in breadcrumb translate file
([50e32ff](https://code.castopod.org/adaures/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191)) ([50e32ff](https://code.podlibre.org/podlibre/castopod/commit/50e32ff75636c1d4c5d945a267e884cb26ad7191))
- set episode duration translation to hardcoded english - set episode duration translation to hardcoded english
([c39efc9](https://code.castopod.org/adaures/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)), ([c39efc9](https://code.podlibre.org/podlibre/castopod/commit/c39efc9489180662edcebd142d4476c0617ea97f)),
closes [#64](https://code.castopod.org/adaures/castopod/issues/64) closes [#64](https://code.podlibre.org/podlibre/castopod/issues/64)
- set episode guid upon episode creation - set episode guid upon episode creation
([ad8b153](https://code.castopod.org/adaures/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)), ([ad8b153](https://code.podlibre.org/podlibre/castopod/commit/ad8b153f2a3b1a3b1751bf63785c4950e1516e6b)),
closes [#48](https://code.castopod.org/adaures/castopod/issues/48) closes [#48](https://code.podlibre.org/podlibre/castopod/issues/48)
- update purgecss content path for php helper files - update purgecss content path for php helper files
([eb70bb4](https://code.castopod.org/adaures/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)), ([eb70bb4](https://code.podlibre.org/podlibre/castopod/commit/eb70bb4f7078ff347aeb8f5dcc7896311d289466)),
closes [#59](https://code.castopod.org/adaures/castopod/issues/59) closes [#59](https://code.podlibre.org/podlibre/castopod/issues/59)
- **install:** redirect to input baseUrl after instance config - **install:** redirect to input baseUrl after instance config
([2426af7](https://code.castopod.org/adaures/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)), ([2426af7](https://code.podlibre.org/podlibre/castopod/commit/2426af7de8c9d426aaf534ff17b67f71c2e9f374)),
closes [#53](https://code.castopod.org/adaures/castopod/issues/53) closes [#53](https://code.podlibre.org/podlibre/castopod/issues/53)
- **platforms:** display platform link only when visible is toggled on - **platforms:** display platform link only when visible is toggled on
([6e503c8](https://code.castopod.org/adaures/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)), ([6e503c8](https://code.podlibre.org/podlibre/castopod/commit/6e503c8d6182987e48892370623183f871bbd1c1)),
closes [#39](https://code.castopod.org/adaures/castopod/issues/39) closes [#39](https://code.podlibre.org/podlibre/castopod/issues/39)
- sort episodic podcasts by season - sort episodic podcasts by season
([d7b6794](https://code.castopod.org/adaures/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74)) ([d7b6794](https://code.podlibre.org/podlibre/castopod/commit/d7b6794f68f9a01fd606a407c6eb4c12d15dee74))
- update .htaccess for shared hosting config - update .htaccess for shared hosting config
([2379826](https://code.castopod.org/adaures/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e)) ([2379826](https://code.podlibre.org/podlibre/castopod/commit/2379826352e2f4b5060910bf9f29268610102f2e))
- update iso-369 language table seeder - update iso-369 language table seeder
([0c90db4](https://code.castopod.org/adaures/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6)) ([0c90db4](https://code.podlibre.org/podlibre/castopod/commit/0c90db44c40de5af5b0b32b54489bda9424d9ef6))
- **package.json:** update destination of postcss generation scripts - **package.json:** update destination of postcss generation scripts
([21413f8](https://code.castopod.org/adaures/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964)) ([21413f8](https://code.podlibre.org/podlibre/castopod/commit/21413f8af3b8a0ac01d8c6f15bcd7a63e524e964))
- use slash instead of backslash to call layout - use slash instead of backslash to call layout
([a80adb2](https://code.castopod.org/adaures/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb)) ([a80adb2](https://code.podlibre.org/podlibre/castopod/commit/a80adb22958fc0a38374cbce2d950a0042e699eb))
### Features ### Features
- add alternate rss feed link tag to podcast page head - add alternate rss feed link tag to podcast page head
([a973c09](https://code.castopod.org/adaures/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)), ([a973c09](https://code.podlibre.org/podlibre/castopod/commit/a973c097d54a3d0186c4079b9d4d3e81aae38505)),
closes [#35](https://code.castopod.org/adaures/castopod/issues/35) closes [#35](https://code.podlibre.org/podlibre/castopod/issues/35)
- add analytics and unknown useragents - add analytics and unknown useragents
([ec92e65](https://code.castopod.org/adaures/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb)) ([ec92e65](https://code.podlibre.org/podlibre/castopod/commit/ec92e65aa42e09b1df04600b52a0c679dfc494bb))
- add breadcrumb in admin area - add breadcrumb in admin area
([7fb1de2](https://code.castopod.org/adaures/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)), ([7fb1de2](https://code.podlibre.org/podlibre/castopod/commit/7fb1de2cf3c97c4cd7afe3bd71bbe66041786ecd)),
closes [#17](https://code.castopod.org/adaures/castopod/issues/17) closes [#17](https://code.podlibre.org/podlibre/castopod/issues/17)
- add french translation - add french translation
([196920d](https://code.castopod.org/adaures/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091)) ([196920d](https://code.podlibre.org/podlibre/castopod/commit/196920d62f1810b4c35f800d17d7f93627319091))
- add install wizard form to bootstrap database and create the first superadmin - add install wizard form to bootstrap database and create the first superadmin
user user
([cba871c](https://code.castopod.org/adaures/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)), ([cba871c](https://code.podlibre.org/podlibre/castopod/commit/cba871c5df9f7120c44d9952456ebbd0d220669e)),
closes [#2](https://code.castopod.org/adaures/castopod/issues/2) closes [#2](https://code.podlibre.org/podlibre/castopod/issues/2)
- add ISO 3166 country codes - add ISO 3166 country codes
([97cd94b](https://code.castopod.org/adaures/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4)) ([97cd94b](https://code.podlibre.org/podlibre/castopod/commit/97cd94b47494b66faf43fbbe0748872da80020a4))
- add map analytics, add episodes analytics, clean analytics page layout, - add map analytics, add episodes analytics, clean analytics page layout,
translate countries translate countries
([07eae83](https://code.castopod.org/adaures/castopod/commit/07eae83a00d860e149359fae67d549488403d88b)) ([07eae83](https://code.podlibre.org/podlibre/castopod/commit/07eae83a00d860e149359fae67d549488403d88b))
- add npm for js dependencies + move src/ files to root folder - add npm for js dependencies + move src/ files to root folder
([cbb83a6](https://code.castopod.org/adaures/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48)) ([cbb83a6](https://code.podlibre.org/podlibre/castopod/commit/cbb83a6f308ac9357e9fb0cca5edae9d3fee5b48))
- add pages table to store custom instance pages (eg. legal-notice, cookie - add pages table to store custom instance pages (eg. legal-notice, cookie
policy, etc.) policy, etc.)
([9c224a8](https://code.castopod.org/adaures/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)), ([9c224a8](https://code.podlibre.org/podlibre/castopod/commit/9c224a8ac6dd95f3c6c087a300fc8bac48e8090f)),
closes [#24](https://code.castopod.org/adaures/castopod/issues/24) closes [#24](https://code.podlibre.org/podlibre/castopod/issues/24)
- add platform models - add platform models
([a333d29](https://code.castopod.org/adaures/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb)) ([a333d29](https://code.podlibre.org/podlibre/castopod/commit/a333d291966229a909c0851fd8b890ed97c48ceb))
- add platforms form in podcast settings - add platforms form in podcast settings
([043f49c](https://code.castopod.org/adaures/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9)) ([043f49c](https://code.podlibre.org/podlibre/castopod/commit/043f49c784bc007ca0fa756ca4ed2d3b08843ad9))
- add platforms tables - add platforms tables
([ce59344](https://code.castopod.org/adaures/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722)) ([ce59344](https://code.podlibre.org/podlibre/castopod/commit/ce5934419a516c9926dd3fd0ace3c11a95b60722))
- add unique listeners analytics - add unique listeners analytics
([3a49258](https://code.castopod.org/adaures/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9)) ([3a49258](https://code.podlibre.org/podlibre/castopod/commit/3a4925816f3268230640525ad7af507aab8eecb9))
- add user permissions and basic groups to handle authorizations - add user permissions and basic groups to handle authorizations
([d58e518](https://code.castopod.org/adaures/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)), ([d58e518](https://code.podlibre.org/podlibre/castopod/commit/d58e51874a4722921b75b0049117015c2380406e)),
closes [#3](https://code.castopod.org/adaures/castopod/issues/3) closes [#3](https://code.podlibre.org/podlibre/castopod/issues/3)
[#18](https://code.castopod.org/adaures/castopod/issues/18) [#18](https://code.podlibre.org/podlibre/castopod/issues/18)
- create optimized & resized images upon upload - create optimized & resized images upon upload
([02e4441](https://code.castopod.org/adaures/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)), ([02e4441](https://code.podlibre.org/podlibre/castopod/commit/02e4441f98f27e9534e5b9b63279153d14632ccd)),
closes [#6](https://code.castopod.org/adaures/castopod/issues/6) closes [#6](https://code.podlibre.org/podlibre/castopod/issues/6)
- display legal disclaimer and warning on podcast import page - display legal disclaimer and warning on podcast import page
([2f07992](https://code.castopod.org/adaures/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)), ([2f07992](https://code.podlibre.org/podlibre/castopod/commit/2f07992e5508b34b91f194eebfac80c51e80e90a)),
closes [#34](https://code.castopod.org/adaures/castopod/issues/34) closes [#34](https://code.podlibre.org/podlibre/castopod/issues/34)
- edit + delete podcast and episode - edit + delete podcast and episode
([ac5f0c7](https://code.castopod.org/adaures/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5)) ([ac5f0c7](https://code.podlibre.org/podlibre/castopod/commit/ac5f0c732806e955c01e05b7867801bc938c6bd5))
- enhance admin ui with responsive design and ux improvements - enhance admin ui with responsive design and ux improvements
([2d44b45](https://code.castopod.org/adaures/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)), ([2d44b45](https://code.podlibre.org/podlibre/castopod/commit/2d44b457a02205d2e7da258d7029b8bc5da39533)),
closes [#31](https://code.castopod.org/adaures/castopod/issues/31) closes [#31](https://code.podlibre.org/podlibre/castopod/issues/31)
[#9](https://code.castopod.org/adaures/castopod/issues/9) [#9](https://code.podlibre.org/podlibre/castopod/issues/9)
- enhance ui using javascript in admin area - enhance ui using javascript in admin area
([c0e66d5](https://code.castopod.org/adaures/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77)) ([c0e66d5](https://code.podlibre.org/podlibre/castopod/commit/c0e66d5f7012026e145d106f4d6bd3ba792a1b77))
- import podcast from an rss feed url - import podcast from an rss feed url
([9a5d5a1](https://code.castopod.org/adaures/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)), ([9a5d5a1](https://code.podlibre.org/podlibre/castopod/commit/9a5d5a15b4945eb319da9e999c4ca60a0a4f6d2d)),
closes [#21](https://code.castopod.org/adaures/castopod/issues/21) closes [#21](https://code.podlibre.org/podlibre/castopod/issues/21)
- set podcast / episode description in the pages description meta tag - set podcast / episode description in the pages description meta tag
([1c4a504](https://code.castopod.org/adaures/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)), ([1c4a504](https://code.podlibre.org/podlibre/castopod/commit/1c4a50442bea2d3449efce9c5ff1c80743152f55)),
closes [#44](https://code.castopod.org/adaures/castopod/issues/44) closes [#44](https://code.podlibre.org/podlibre/castopod/issues/44)
- update analytics so to meet IABv2 requirements - update analytics so to meet IABv2 requirements
([03e23a2](https://code.castopod.org/adaures/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)), ([03e23a2](https://code.podlibre.org/podlibre/castopod/commit/03e23a28bf9b1b73fba55352c36a8cd6cc8ae729)),
closes [#10](https://code.castopod.org/adaures/castopod/issues/10) closes [#10](https://code.podlibre.org/podlibre/castopod/issues/10)
- **cache:** add podcast and episode pages to cache + clear them after insert or - **cache:** add podcast and episode pages to cache + clear them after insert or
update update
([da0f047](https://code.castopod.org/adaures/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9)) ([da0f047](https://code.podlibre.org/podlibre/castopod/commit/da0f0472819007e02e5da37399f2377772c618b9))
- **categories:** create model, entity, migrations and seeds - **categories:** create model, entity, migrations and seeds
([f73b042](https://code.castopod.org/adaures/castopod/commit/f73b042cc091be82abdbbca8992080875d526972)) ([f73b042](https://code.podlibre.org/podlibre/castopod/commit/f73b042cc091be82abdbbca8992080875d526972))
- **devcontainer:** add devcontainer settings for dev environment - **devcontainer:** add devcontainer settings for dev environment
([69e7266](https://code.castopod.org/adaures/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc)) ([69e7266](https://code.podlibre.org/podlibre/castopod/commit/69e72667365247b63430dee88194e8f0d7c28edc))
- **episodes:** add create form and view pages for episode - **episodes:** add create form and view pages for episode
([f3b2c8b](https://code.castopod.org/adaures/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)), ([f3b2c8b](https://code.podlibre.org/podlibre/castopod/commit/f3b2c8b84f3d93bef734e34dbe8ed729535e45e9)),
closes [#1](https://code.castopod.org/adaures/castopod/issues/1) closes [#1](https://code.podlibre.org/podlibre/castopod/issues/1)
- **episodes:** add migrations, model and entity for episodes table - **episodes:** add migrations, model and entity for episodes table
([0444821](https://code.castopod.org/adaures/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b)) ([0444821](https://code.podlibre.org/podlibre/castopod/commit/044482174ede555ce19a2d8c6f48771cc8e7d27b))
- **podcast:** create a podcast using form - **podcast:** create a podcast using form
([1202ba3](https://code.castopod.org/adaures/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34)) ([1202ba3](https://code.podlibre.org/podlibre/castopod/commit/1202ba3545f521097c60a6a2af95e70527cd1d34))
- **podcast-form:** update routes and redirect to podcast page - **podcast-form:** update routes and redirect to podcast page
([12ce905](https://code.castopod.org/adaures/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8)) ([12ce905](https://code.podlibre.org/podlibre/castopod/commit/12ce905799002dc9c07e6de092342d30ba9fd7d8))
- **public-ui:** adapt public podcast and episode pages to wireframes - **public-ui:** adapt public podcast and episode pages to wireframes
([40a0535](https://code.castopod.org/adaures/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)), ([40a0535](https://code.podlibre.org/podlibre/castopod/commit/40a0535fc1bc12a24994b651f5e00b35995cbdda)),
closes [#30](https://code.castopod.org/adaures/castopod/issues/30) closes [#30](https://code.podlibre.org/podlibre/castopod/issues/30)
[#13](https://code.castopod.org/adaures/castopod/issues/13) [#13](https://code.podlibre.org/podlibre/castopod/issues/13)
- **rss:** generate rss feed from podcast entity - **rss:** generate rss feed from podcast entity
([c815ecd](https://code.castopod.org/adaures/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666)) ([c815ecd](https://code.podlibre.org/podlibre/castopod/commit/c815ecd6640931fee0895f80908a3ddfac482666))
- **users:** add myth-auth to handle users crud + add admin gateway only - **users:** add myth-auth to handle users crud + add admin gateway only
accessible by login accessible by login
([c63a077](https://code.castopod.org/adaures/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)), ([c63a077](https://code.podlibre.org/podlibre/castopod/commit/c63a077618c61b4cde7f25ffc650a4b0e1495f44)),
closes [#11](https://code.castopod.org/adaures/castopod/issues/11) closes [#11](https://code.podlibre.org/podlibre/castopod/issues/11)
- minor corrections to some tables - minor corrections to some tables
([3bf9420](https://code.castopod.org/adaures/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086)) ([3bf9420](https://code.podlibre.org/podlibre/castopod/commit/3bf9420b5956a501b3b24405d243a71a928d6086))
- write id3v2 tags to episode's audio file - write id3v2 tags to episode's audio file
([4651d01](https://code.castopod.org/adaures/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d)) ([4651d01](https://code.podlibre.org/podlibre/castopod/commit/4651d01a84ff3ea8433a8ae26cfd750a1ec9e88d))
### Reverts ### Reverts
- use basic input file for episodes audio files instead of button for better UX - use basic input file for episodes audio files instead of button for better UX
([d5f22fb](https://code.castopod.org/adaures/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a)) ([d5f22fb](https://code.podlibre.org/podlibre/castopod/commit/d5f22fbb38c43d9b37df401eff655958a57cb40a))

View file

@ -1,162 +1,128 @@
# Contributor Covenant 3.0 Code of Conduct # Contributor Covenant Code of Conduct
## Our Pledge ## Our Pledge
We pledge to make our community welcoming, safe, and equitable for all. 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 are committed to fostering an environment that respects and promotes the We pledge to act and interact in ways that contribute to an open, welcoming,
dignity, rights, and contributions of all individuals, regardless of diverse, inclusive, and healthy community.
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.
## Encouraged Behaviors ## Our Standards
While acknowledging differences in social norms, we all strive to meet our Examples of behavior that contributes to a positive environment for our
community's expectations for positive behavior. We also understand that our community include:
words and actions may be interpreted differently than we intend based on
culture, background, or native language.
With these considerations in mind, we agree to behave mindfully toward each - Demonstrating empathy and kindness toward other people
other and act in ways that center our shared values, including: - 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
1. Respecting the **purpose of our community**, our activities, and our ways of Examples of unacceptable behavior include:
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**.
## Restricted Behaviors - 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
We agree to restrict the following behaviors in our community. Instances, ## Enforcement Responsibilities
threats, and promotion of these behaviors are violations of this Code of
Conduct.
1. **Harassment.** Violating explicitly expressed boundaries or engaging in Community leaders are responsible for clarifying and enforcing our standards of
unnecessary personal attention after any clear request to stop. acceptable behavior and will take appropriate and fair corrective action in
2. **Character attacks.** Making insulting, demeaning, or pejorative comments response to any behavior that they deem inappropriate, threatening, offensive,
directed at a community member or group of people. or harmful.
3. **Stereotyping or discrimination.** Characterizing anyones 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.
### Other Restrictions Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
1. **Misleading identity.** Impersonating someone else for any reason, or not aligned to this Code of Conduct, and will communicate reasons for moderation
pretending to be someone else to evade enforcement actions. decisions when appropriate.
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 ## Scope
This Code of Conduct applies within all community spaces, and also applies when This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public or other an individual is officially representing the community in public spaces.
spaces. Examples of representing our community include using an official email Examples of representing our community include using an official e-mail address,
address, posting via an official social media account, or acting as an appointed posting via an official social media account, or acting as an appointed
representative at an online or offline event. 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@podlibre.org](mailto:abuse@podlibre.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 ## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 3.0, This Code of Conduct is adapted from the [Contributor Covenant][homepage],
permanently available at version 2.0, available at
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Contributor Covenant is stewarded by the Organization for Ethical Source and Community Impact Guidelines were inspired by
licensed under CC BY-SA 4.0. To view a copy of this license, visit [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/)
For answers to common questions about Contributor Covenant, see the FAQ at [homepage]: https://www.contributor-covenant.org
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
Translations are provided at For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). https://www.contributor-covenant.org/faq. Translations are available at
Additional enforcement and community guideline resources can be found at https://www.contributor-covenant.org/translations.
[https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources).
The enforcement ladder was inspired by the work of
[Mozillas code of conduct team](https://github.com/mozilla/inclusion).

View file

@ -1,437 +0,0 @@
# Setup your development environment
## Introduction
Castopod is a web app based on the `php` framework
[CodeIgniter 4](https://codeigniter.com).
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.
> You don't need any prior knowledge of Docker to follow the next steps.
> However, if you wish to use your own environment, feel free to do so!
## Setup instructions
### 1. Pre-requisites
0. Install [Docker](https://docs.docker.com/get-docker).
1. Clone the Castopod repository by running:
```bash
git clone https://code.castopod.org/adaures/castopod.git
```
2. Create a `.env` file with the minimum required config to connect the app to
the database and use redis as a cache handler:
```ini
CI_ENVIRONMENT="development"
# If set to development, you must run `pnpm run dev` to start the static assets server
vite.environment="development"
# By default, this is set to true in the app config.
# For development, this must be set to false as it is
# on a local environment
app.forceGlobalSecureRequests=false
app.baseURL="http://localhost:8080/"
admin.gateway="cp-admin"
auth.gateway="cp-auth"
database.default.hostname="mariadb"
database.default.database="castopod"
database.default.username="castopod"
database.default.password="castopod"
database.default.DBPrefix="dev_"
analytics.salt="DEV_ANALYTICS_SALT"
cache.handler="redis"
cache.redis.host="redis"
# You may not want to use redis as your cache handler
# Comment/remove the two lines above and uncomment
# the next line for file caching.
# -----------------------
#cache.handler="file"
######################################
# Media config
######################################
media.baseURL="http://localhost:8080/"
# S3
# Uncomment to store s3 objects using adobe/s3mock service
# -----------------------
#media.fileManager="s3"
#media.s3.bucket="castopod"
#media.s3.endpoint="http://172.31.0.6:9090/"
#media.s3.pathStyleEndpoint=true
```
> [!NOTE]
> You can tweak your environment by setting more environment variables in
> your custom `.env` file. See the `env` for examples or the
> [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
`Settings` > `Resources` > `File Sharing`
### 2. (recommended) Develop inside the app container with VSCode
If you're working in VSCode, you can take advantage of the `.devcontainer/`
folder. It defines a development environment (dev container) with preinstalled
requirements and VSCode extensions so you don't have to worry about them. All
required services will be loaded automagically! 🪄
1. Install the VSCode extension
[Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. `Ctrl/Cmd + Shift + P` > `Open in container`
> 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.
During development, you will have to start [Vite](https://vitejs.dev)'s dev
server for compiling the typescript code and styles:
```bash
# run Vite dev server
pnpm run dev
```
If there is any issue with the PHP server not running, you can restart them
using the following commands:
```bash
# run Castopod server
php spark serve - 0.0.0.0
```
3. You're all set! 🎉
You're now **inside the dev container**, you may use the VSCode console
(`Terminal` > `New Terminal`) to run any command:
```bash
# PHP is installed
php -v
# Composer is installed
composer -V
# pnpm is installed
pnpm -v
# git is installed
git version
```
For more info, see
[Developing inside a Container](https://code.visualstudio.com/docs/devcontainers/containers)
### 3. Start hacking
You're all set! Start working your magic by updating the project's files! Help
yourself to the
[CodeIgniter4 User Guide](https://codeigniter.com/user_guide/index.html) for
more insights.
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**
### 2-alt. Develop outside the app container
You do not wish to use the VSCode devcontainer? No problem!
1. Start the Docker containers manually:
Go to the project's root folder and run:
```bash
# starts all services declared in docker-compose.yml file
# -d option starts the containers in the background
docker-compose up -d
# See all running processes (you should see 3 processes running)
docker-compose ps
# Alternatively, you can check all docker processes
docker ps -a
```
> The `docker-compose up -d` command will boot 5 containers in the
> background:
>
> - `castopod_app`: a php based container with Castopod requirements
> installed
> - `castopod_redis`: a [redis](https://redis.io/) database to handle queries
> and pages caching
> - `castopod_mariadb`: a [mariadb](https://mariadb.org/) server for
> persistent data
> - `castopod_phpmyadmin`: a phpmyadmin server to visualize the mariadb
> database.
> - `castopod_s3`: a mock s3 server to work on the s3 fileManager
2. Run any command inside the containers by prefixing them with
`docker-compose run --rm app`:
```bash
# use PHP
docker-compose run --rm app php -v
# use Composer
docker-compose run --rm app composer -V
# use pnpm
docker-compose run --rm app pnpm -v
# use git
docker-compose run --rm app git version
```
---
## Going Further
### Install Castopod's dependencies
1. Install php dependencies with [Composer](https://getcomposer.org/)
```bash
composer install
```
> [!NOTE]
> The php dependencies aren't included in the repository. Composer will check
> the `composer.json` and `composer.lock` files to download the packages with
> the right versions. The dependencies will live under the `vendor/` folder.
> For more info, check out the
> [Composer documentation](https://getcomposer.org/doc/).
2. Install JavaScript dependencies with [pnpm](https://pnpm.io/)
```bash
pnpm install
```
> [!NOTE]
> 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
> [PNPM documentation](https://pnpm.io/motivation).
3. Generate static assets:
```bash
# build all static assets at once
pnpm run build:static
# build specific assets
pnpm run build:icons
pnpm run build:svg
```
> [!NOTE]
> The static assets generated live under the `public/assets` folder, it
> includes JavaScript, styles, images, fonts, icons and svg files.
### Initialize and populate database
> [!TIP]
> You may skip this section if you go through the install wizard (go to
> `/cp-install`).
1. Build the database with the migrate command:
```bash
# loads the database schema during first migration
php spark migrate -all
```
You may need to undo the migration (rollback):
```bash
# rolls back database schema (deletes all tables and their content)
php spark migrate:rollback
```
2. Populate the database with the required data:
```bash
# Populates all required data
php spark db:seed DevSeeder
```
You may choose to add data separately:
```bash
# Populates all categories
php spark db:seed CategorySeeder
# Populates all Languages
php spark db:seed LanguageSeeder
# Adds a superadmin with [admin@castopod.local / castopod] credentials
php spark db:seed DevSuperadminSeeder
```
3. (optional) Populate the database with test data:
- Populate with fake podcast analytics:
```bash
php spark db:seed FakePodcastsAnalyticsSeeder
```
- Populate with fake website analytics:
```bash
php spark db:seed FakeWebsiteAnalyticsSeeder
```
### Useful docker / docker-compose commands
- Monitor the app container:
```bash
docker-compose logs --tail 50 --follow --timestamps app
```
- Interact with the Redis server using included redis-cli command:
```bash
docker exec -it castopod_redis redis-cli
```
- Monitor the Redis container:
```bash
docker-compose logs --tail 50 --follow --timestamps redis
```
- Monitor the mariadb container:
```bash
docker-compose logs --tail 50 --follow --timestamps mariadb
```
- Monitor the phpmyadmin container:
```bash
docker-compose logs --tail 50 --follow --timestamps phpmyadmin
```
- Restart docker containers:
```bash
docker-compose restart
```
- Destroy all containers, opposite of `up` command:
```bash
docker-compose down
```
- Rebuild app container:
```bash
docker-compose build app
```
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
This happens when running `pnpm install`.
👉 By default, docker might not have access to enough RAM. Allocate more memory
and run `pnpm install` again.
### (Linux) Files created inside container are attributed to root locally
You may use Linux user namespaces to fix this on your machine:
> [!NOTE]
> Replace "username" with your local username
1. Go to `/etc/docker/daemon.json` and add:
```json
{
"userns-remap": "username"
}
```
2. Configure the subordinate uid/guid:
```bash
# in /etc/subuid
username:1000:1
username:100000:65536
```
```bash
# in /etc/subgid
username:1000:1
username:100000:65536
```
3. Restart Docker:
```bash
sudo systemctl restart docker
```
4. That's it! Now, the root user in the container will be mapped to the user on
your local machine, no more permission issues! 🎉
You can check
[this great article](https://www.jujens.eu/posts/en/2017/Jul/02/docker-userns-remap/)
to know more about how it works.

View file

@ -1,16 +1,8 @@
# Contributing to Castopod # Contributing to Castopod Host
Love Castopod and want to help? Thanks so much, there's something to do for Love Castopod Host and want to help? Thanks so much, there's something to do for
everybody! everybody!
> [!NOTE]
> Castopod follows the [all contributors](https://allcontributors.org/)
> specification in an effort to **recognize any kind of contribution**, not just
> code!
> If you've made a contribution and do not appear in the
> [contributors](../index.md#contributors-✨) list, please
> [let us know](../index.md#contact) so we can correct our mistake! 🙂
Please take a moment to review this document in order to make the contribution Please take a moment to review this document in order to make the contribution
process easy and effective for everyone involved. process easy and effective for everyone involved.
@ -19,34 +11,17 @@ developers managing and developing this open source project. In return, they
should reciprocate that respect in addressing your issue or assessing patches should reciprocate that respect in addressing your issue or assessing patches
and features. and features.
## Translating Castopod ⚠️ Note that **any** contribution made on a repository other than
[the original repository](https://code.podlibre.org/podlibre/castopod-host) will
We use [Crowdin](https://translate.castopod.org/) to manage translation files not be accepted.
for [Castopod](https://code.castopod.org/), the
[documentation](https://docs.castopod.org/) and the
[landing](https://castopod.org/) websites.
Whether you'd like to correct a translation error, validate new translations or
include your language to Castopod, head into the
[crowdin project](https://translate.castopod.org/) to get started.
> [!NOTE]
> To prevent degrading user experience, new languages are included to Castopod
> when they reach a certain threshold (~90%).
## Using the issue tracker ## Using the issue tracker
The [issue tracker](https://code.castopod.org/adaures/castopod/-/issues) is the The [issue tracker](https://code.podlibre.org/podlibre/castopod-host/-/issues)
preferred channel for [bug reports](#bug-reports), is the preferred channel for [bug reports](#bug-reports),
[features requests](#feature-requests) and [features requests](#feature-requests) and
[submitting pull requests](#pull-requests). [submitting pull requests](#pull-requests).
## ⚠️ Security issues and vulnerabilities
If you encounter any security issue or vulnerability in the Castopod source,
please contact us directly by email at
[security@castopod.org](mailto:security@castopod.org)
## Bug reports ## Bug reports
A bug is a _demonstrable problem_ that is caused by the code in the repository. A bug is a _demonstrable problem_ that is caused by the code in the repository.
@ -70,9 +45,8 @@ your environment? What steps will reproduce the issue? What browser(s) and OS
experience the problem? What would you expect to be the outcome? All these experience the problem? What would you expect to be the outcome? All these
details will help people to fix any potential bugs. 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)
> [Issue templates](https://docs.gitlab.com/ee/user/project/description_templates.html#using-the-templates) have > have been created for this project. You may use them to help you follow those
> been created for this project. You may use them to help you follow those
> guidelines. > guidelines.
## Feature requests ## Feature requests
@ -98,33 +72,33 @@ 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 Adhering to the following process is the best way to get your work included in
the project: the project:
1. [Fork](https://docs.gitlab.com/ee/user/project/repository/forking_workflow.html) 1. [Fork](https://docs.gitlab.com/ee/gitlab-basics/fork-project.html) the
the project, clone your fork, and configure the remotes: project, clone your fork, and configure the remotes:
```bash ```bash
# Clone your fork of the repo into the current directory # Clone your fork of the repo into the current directory
git clone https://code.castopod.org/<your-username>/castopod.git git clone https://code.podlibre.org/<your-username>/castopod-host.git
# Navigate to the newly cloned directory # Navigate to the newly cloned directory
cd castopod cd castopod-host
# Assign the original repo to a remote called "upstream" # Assign the original repo to a remote called "upstream"
git remote add upstream https://code.castopod.org/adaures/castopod.git git remote add upstream https://code.podlibre.org/podlibre/castopod-host.git
``` ```
2. If you cloned a while ago, get the latest changes from upstream: 2. If you cloned a while ago, get the latest changes from upstream:
```bash ```bash
git checkout main git checkout main
git pull upstream main git pull upstream main
``` ```
3. Create a new topic branch (off the `main` branch) to contain your feature, 3. Create a new topic branch (off the `main` branch) to contain your feature,
change, or fix: change, or fix:
```bash ```bash
git checkout -b <topic-branch-name> git checkout -b <topic-branch-name>
``` ```
4. Commit your changes in logical chunks. Please adhere to these 4. Commit your changes in logical chunks. Please adhere to these
[git commit message guidelines](https://conventionalcommits.org/) or your [git commit message guidelines](https://conventionalcommits.org/) or your
@ -134,23 +108,22 @@ the project:
5. Locally merge (or rebase) the upstream dev branch into your topic branch: 5. Locally merge (or rebase) the upstream dev branch into your topic branch:
```bash ```bash
git pull [--rebase] upstream main git pull [--rebase] upstream main
``` ```
6. Push your topic branch up to your fork: 6. Push your topic branch up to your fork:
```bash ```bash
git push origin <topic-branch-name> git push origin <topic-branch-name>
``` ```
7. [Open a Pull Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html#new-merge-request-from-a-fork) 7. [Open a Pull Request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html#new-merge-request-from-a-fork)
with a clear title and description. with a clear title and description.
> [!IMPORTANT] **IMPORTANT**: By submitting a patch, you agree to allow the project owners to
> By submitting a patch, you agree to allow the project owners to license your license your work under the terms of the
> work under the terms of the [GNU AGPLv3](https://code.podlibre.org/podlibre/castopod-host/-/blob/main/LICENSE).
> [GNU AGPLv3](https://code.castopod.org/adaures/castopod/-/blob/develop/LICENSE.md).
## Collaborating guidelines ## Collaborating guidelines

View file

@ -1,26 +1,66 @@
# Castopod dependencies # Castopod Host dependencies
Castopod uses the following components: Castopod Host uses the following components:
## PHP Dependencies PHP Dependencies:
PHP dependencies can be found in the [composer.json](./composer.json) file. - [CodeIgniter 4](https://codeigniter.com)
([MIT License](https://codeigniter.com/user_guide/license.html))
- [WhichBrowser/Parser-PHP](https://github.com/WhichBrowser/Parser-PHP)
([MIT License](https://github.com/WhichBrowser/Parser-PHP/blob/master/LICENSE))
- [GeoIP2 PHP API](https://github.com/maxmind/GeoIP2-php)
([Apache License 2.0](https://github.com/maxmind/GeoIP2-php/blob/master/LICENSE))
- [getID3](https://github.com/JamesHeinrich/getID3)
([GNU General Public License v3](https://github.com/JamesHeinrich/getID3/blob/2.0/licenses/license.gpl-30.txt))
- [myth-auth](https://github.com/lonnieezell/myth-auth)
([MIT license](https://github.com/lonnieezell/myth-auth/blob/develop/LICENSE.md))
- [commonmark](https://commonmark.thephpleague.com/)
([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE))
- [phpdotenv](https://github.com/vlucas/phpdotenv)
([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE))
- [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown)
([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE))
- [opawg/user-agents-php](https://github.com/opawg/user-agents-php)
([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE))
- [podlibre/ipcat](https://github.com/podlibre/ipcat)
([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE))
- [podlibre/podcast-namespace](https://code.podlibre.org/podlibre/podcastnamespace)
([MIT License](https://code.podlibre.org/podlibre/podcastnamespace/-/blob/master/LICENSE))
- [phpseclib](https://phpseclib.com/)
([MIT License](https://github.com/phpseclib/phpseclib/blob/master/LICENSE))
- [codeigniter4-uuid](https://github.com/michalsn/codeigniter4-uuid)
([MIT License](https://github.com/michalsn/codeigniter4-uuid/blob/develop/LICENSE))
- [essence](https://github.com/essence/essence)
([The FreeBSD License](https://github.com/essence/essence/blob/master/LICENSE.txt))
## Javascript dependencies Javascript dependencies:
Javascript dependencies can be found in the [package.json](./package.json) file. - [rollup](https://rollupjs.org/)
([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md))
- [tailwindcss](https://tailwindcss.com/)
([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE))
- [ProseMirror](https://prosemirror.net/)
([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE))
- [amCharts 4](https://github.com/amcharts/amcharts4)
([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE))
- [Choices.js](https://joshuajohnson.co.uk/Choices/)
([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE))
- [flatpickr](https://flatpickr.js.org/)
([MIT License](https://github.com/flatpickr/flatpickr/blob/master/LICENSE.md))
- [popperjs](https://popper.js.org/)
([MIT License](https://github.com/popperjs/popper-core/blob/master/LICENSE.md))
## Other dependencies Other:
- [Kumbh Sans](https://fonts.google.com/specimen/Kumbh+Sans) - [Kumbh Sans](https://fonts.google.com/specimen/Kumbh+Sans)
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)) ([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
- [Inter](https://fonts.google.com/specimen/Inter) - [Montserrat](https://fonts.google.com/specimen/Montserrat)
([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL)) ([Open Font License](https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL))
- [RemixIcon](https://remixicon.com/) - [RemixIcon](https://remixicon.com/)
([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License)) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License))
- [OPAWG/User agent list](https://github.com/opawg/user-agents-v2) - [OPAWG/User agent list](https://github.com/opawg/user-agents)
([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([by Open Podcast Analytics Working Group](https://github.com/opawg))
([MIT license](https://github.com/opawg/user-agents-v2/blob/master/LICENSE)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE))
- [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents) - [OPAWG/podcast-rss-useragents](https://github.com/opawg/podcast-rss-useragents)
([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([by Open Podcast Analytics Working Group](https://github.com/opawg))
([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE)) ([MIT license](https://github.com/opawg/podcast-rss-useragents/blob/master/LICENSE))

62
Dockerfile Normal file
View file

@ -0,0 +1,62 @@
####################################################
# Castopod Host development Docker file
####################################################
# ⚠️ NOT optimized for production
# should be used only for development purposes
#---------------------------------------------------
FROM php:8.0-fpm
LABEL maintainer="Yassine Doghri <yassine@doghri.fr>"
COPY . /castopod-host
WORKDIR /castopod-host
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# Install server requirements
RUN apt-get update \
# gnupg to sign commits with gpg
&& apt-get install --yes --no-install-recommends gnupg \
# npm through the nodejs package
&& curl -fsSL https://deb.nodesource.com/setup_14.x | bash - \
&& apt-get update \
&& apt-get install --yes --no-install-recommends nodejs \
# update npm
&& npm install --global npm@7 \
&& apt-get update \
&& apt-get install --yes --no-install-recommends \
git \
openssh-client \
vim \
# cron for scheduled tasks
cron \
# unzip used by composer
unzip \
# required libraries to install php extensions using
# https://github.com/mlocati/docker-php-extension-installer (included in php's docker image)
libicu-dev \
libpng-dev \
libjpeg-dev \
zlib1g-dev \
libzip-dev \
# intl for Internationalization
&& docker-php-ext-install intl \
&& docker-php-ext-install zip \
# gd for image processing
&& docker-php-ext-configure gd --with-jpeg \
&& docker-php-ext-install gd \
# redis extension for cache
&& pecl install -o -f redis \
&& rm -rf /tmp/pear \
&& 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 \

View file

@ -1,83 +0,0 @@
# This file lists processing purposes and the personal data gathered by
# Castopod.
# It is intended for hosting providers who want to provide a service
# based on Castopod, helping them to comply with GDPR requirements. Note
# that the services powered by Castopod may collect more data, HTTP logs
# in particular. As a hosting provider, you must inform your users of their
# rights and how their data are used and protected.
purpose:
Deduplicate number of audio file downloads made by the same listener
for analytics purposes
lawfulness: legitimate interest
data: (User IP address + Browser User Agent)
required: yes
visibility: none
description:
In order to produce analytics data comparable to the podcasting
ecosystem standards, the User IP address (REMOTE_ADDR) with the
browser User Agent (HTTP_USER_AGENT) are stored when an audio file
is downloaded.
mitigation:
The data (User IP address + Browser User Agent) is never stored in plain
format.
The data is concatenated with a cryptographic salt, the current date,
and the podcast or episode IDs.
The data is hashed (using sha1) after being concatenated and before
being stored.
The data is stored in a cache database (eg. Redis).
The data expires every day at midnight (server time).
purpose: Connect users to their accounts
lawfulness: legitimate interest
data: username
required: yes
visibility: authenticated users
description:
The username is used to identify users during the login process.
The username is only required for users accessing the admin area.
mitigation:
The username does not have to be a real or known identity.
data: user e-mail address
required: yes
visibility: administrators
description:
The e-mail address is used for administrative purposes, to identify users
during the login process and in case of forgotten password.
data: password
required: yes
visibility: private
description:
The password is used to check the identity of users during the login
process.
mitigation:
Only hashes (using the Argon2 key derivation function) of the passwords
are stored in the database (but they transit over the network).
purpose: Claim ownership of a podcast
lawfulness: legitimate interest
data: Podcast e-mail address
required: yes
visibility: public
description:
The podcast e-mail address is used to claim podcast ownership on other
platforms (such as Apple Podcasts).
mitigation:
The e-mail can be generic.
purpose: Grant access to premium content
lawfulness: legitimate interest
data: Subscriber's email address
required: yes
visibility: administrators
description:
The subscriber's e-mail address is used to provide credentials for
listening to premium content.
mitigation:
The e-mail can be generic.

123
INSTALL.md Normal file
View file

@ -0,0 +1,123 @@
# How to install Castopod Host <!-- omit in toc -->
_Castopod Host_ was thought-out to be easy to install. Whether using dedicated
or shared hosting, you can install it on most PHP-MySQL compatible web servers.
## Table of contents <!-- omit in toc -->
- [Install instructions](#install-instructions)
- [0. Pre-requisites](#0-pre-requisites)
- [(recommended) Install Wizard](#recommended-install-wizard)
- [(alternative) Manual configuration](#alternative-manual-configuration)
- [Web Server Requirements](#web-server-requirements)
- [PHP v8.0 or higher](#php-v80-or-higher)
- [MySQL compatible database](#mysql-compatible-database)
- [Privileges](#privileges)
- [(Optional) Other recommendations](#optional-other-recommendations)
- [Security concerns](#security-concerns)
## Install instructions
### 0. Pre-requisites
0. Get a Web Server with requirements installed
1. Create a MySQL database for Castopod Host with a user having access and
modification privileges (for more info, see
[Web Server Requirements](#web-server-requirements)).
2. Activate HTTPS on your domain with an _SSL certificate_.
3. Download and unzip the latest
[Castopod Host Package](https://code.podlibre.org/podlibre/castopod-host/-/releases)
onto the web server if you havent already.
- ⚠️ Set the web server document root to the `public/` sub-folder.
4. Add a cron task on your web server to run every minute (replace the paths
accordingly):
```php
* * * * * /path/to/php /path/to/castopod-host/public/index.php scheduled-activities
```
> ⚠️ Social features will not work properly if you do not set the task. It is
> used to broadcast social activities to the fediverse.
### (recommended) Install Wizard
1. Run the Castopod Host install script by going to the install wizard page
(`https://your_domain_name.com/cp-install`) in your favorite web browser.
2. Follow the instructions on your screen.
3. Start podcasting!
> **Note:**
>
> The install script writes a `.env` file in the package root. If you cannot go
> through the install wizard, you can
> [create and update the `.env` file manually](#alternative-manual-configuration).
### (alternative) Manual configuration
1. Rename the `.env.example` file to `.env` and update the default values with
your own.
2. Upload the `.env` file to the Castopod Host Package root on your server.
3. Go to `/cp-install` to finish the install process.
4. Start podcasting!
## Web Server Requirements
### PHP v8.0 or higher
PHP version 8.0 or higher is required, with the following extensions installed:
- [intl](https://php.net/manual/en/intl.requirements.php)
- [libcurl](https://php.net/manual/en/curl.requirements.php)
- [mbstring](https://php.net/manual/en/mbstring.installation.php)
- [gd](https://www.php.net/manual/en/image.installation.php)
Additionally, make sure that the following extensions are enabled in your PHP:
- json (enabled by default - don't turn it off)
- xml (enabled by default - don't turn it off)
- [mysqlnd](https://php.net/manual/en/mysqlnd.install.php)
### MySQL compatible database
> We recommend using [MariaDB](https://mariadb.org).
You will need the server hostname, database name, username and password to
complete the installation process. If you do not have these, please contact your
server administrator.
> NB. Castopod Host only works with supported MySQL compatible databases. It
> will break with MySQL v5.6 for example as its end of life was on February
> 5, 2021.
#### Privileges
User must have at least these privileges on the database for Castopod Host to
work: `ALTER`, `DELETE`, `EXECUTE`, `INDEX`, `INSERT`, `SELECT`, `UPDATE`.
### (Optional) Other recommendations
- Redis for better cache performances.
- CDN for static files caching and better performances.
- e-mail gateway for lost passwords.
## Security concerns
Castopod Host is built on top of Codeigniter, a PHP framework that encourages
[good security practices](https://codeigniter.com/user_guide/concepts/security.html).
To maximize your instance safety and prevent any malicious attack, we recommend
you update all your Castopod Host files permissions after installation (to avoid
any permission error):
- `writable/` folder must be **readable** and **writable**.
- `public/media/` folder must be **readable** and **writable**.
- any other file must be set to **readonly**.
For instance, if you are using Apache or NGINX with Ubuntu you may do the
following:
```bash
sudo chown -R root:root /path/to/castopod-host
sudo chown -R www-data:www-data /path/to/castopod-host/writable
sudo chown -R www-data:www-data /path/to/castopod-host/public/media
```

208
README.md
View file

@ -1,191 +1,75 @@
<div align="center"> <h1 style="text-align: center">
<h1> <img src="https://podlibre.org/static/images/Castopod-Title.svg" alt="Castopod Host" />
<a href="https://castopod.org/"> </h1>
<img src="./docs/src/assets/castopod-logo-inline.svg" alt="Castopod" height="64px" />
</a> > ⚠️ **Castopod Host is in alpha version**. It is still under heavy development
</h1> > and may not be 100% stable as new features are being worked on.
_Castopod Host_ is a free and open-source podcast hosting solution made for
podcasters who want engage and interact with their audience.
Create, upload, publish, interact with your followers and get comprehensive
audience measurements that respect your listeners privacy.
Whether you choose to install it on your own server or have it hosted by a
professional, all your data and analytics belong to you and you only!
<div style="text-align: center">
<img src="https://podlibre.org/static/images/Castopod-Mascot-Server.svg" alt="Castopod Mascot" />
</div> </div>
<div align="center"> You may find Castopod Host's source code on the
[original repository](https://code.podlibre.org/podlibre/castopod-host) or,
alternatively, on the
[github repository (mirror)](https://github.com/podlibre/castopod-host).
[![release-badge]][release]&nbsp;[![license-badge]][license]&nbsp;[![crowdin-badge]][crowdin]&nbsp;[![contributions-badge]][contributions]&nbsp;[![semantic-release-badge]][semantic-release]&nbsp;[![discord-badge]][discord]&nbsp;[![stars-badge]][stars] ## Install / Update
</div> To install or update Castopod Host on your PHP/MySQL server:
Castopod is a free and open-source podcast hosting solution made for podcasters - Download
who want engage and interact with their audience. [Castopod Host's latest Package (zip or tar.gz)](https://code.podlibre.org/podlibre/castopod-host/-/releases):
## Getting started - Follow one of the procedures on:
Castopod comes pre-packaged with all the required static assets and - [“How to **install** Castopod Host”](./INSTALL.md)
dependencies, you may download and install it by checking out the - or [“How to **update** Castopod Host”](./UPDATE.md)
[getting started page](https://castopod.org/getting-started/)!
## Security issues and vulnerabilities ## Documentation
If you encounter any security issue or vulnerability in the Castopod source, You can check Castopod Host's documentation for
please contact us directly by email at [setting up a development environment](./docs/setup-development.md).
[security@castopod.org](mailto:security@castopod.org)
## Contributing ## Contributing
Contributions are always welcome! Love Castopod Host and would like to help? Check out the
[contribution guidelines](./CONTRIBUTING.md) for this project, everything should
be there!
See the [contribution guidelines](./CONTRIBUTING.md) for ways to get started. ⚠️ Note that **any** contribution made on a repository other than
[the original repository](https://code.podlibre.org/podlibre/castopod-host) will
not be accepted.
> [!Important] ## Support
> **Any** contribution made on a repository other than
> [the original repository](https://code.castopod.org/adaures/castopod) will not
> be accepted.
## Contributors ✨
Thanks goes to these wonderful people
([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://yassinedoghri.com"><img src="https://avatars.githubusercontent.com/u/11021441?v=4?s=100" width="100px;" alt="Yassine Doghri"/><br /><sub><b>Yassine Doghri</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=yassinedoghri" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=yassinedoghri" title="Reviewed Pull Requests">👀</a> <a href="#maintenance-yassinedoghri" title="Maintenance">🚧</a> <a href="#content-yassinedoghri" title="Content">🖋</a> <a href="#design-yassinedoghri" title="Design">🎨</a> <a href="#a11y-yassinedoghri" title="Accessibility">️️️️♿️</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-yassinedoghri" title="Answering Questions">💬</a> <a href="#mentoring-yassinedoghri" title="Mentoring">🧑‍🏫</a> <a href="#infra-yassinedoghri" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-yassinedoghri" title="Ideas, Planning, & Feedback">🤔</a> <a href="#projectManagement-yassinedoghri" title="Project Management">📆</a> <a href="https://blog.castopod.org/author/yassinedoghri/" title="Blogposts">📝</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/benjamin"><img src="https://code.castopod.org/uploads/-/system/user/avatar/2/avatar.png?s=100" width="100px;" alt="Benjamin Bellamy"/><br /><sub><b>Benjamin Bellamy</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=benjamin" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=benjamin" title="Reviewed Pull Requests">👀</a> <a href="#content-benjamin" title="Content">🖋</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-benjamin" title="Answering Questions">💬</a> <a href="#infra-benjamin" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#ideas-benjamin" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://blog.castopod.org/author/benjamin-bellamy/" title="Blogposts">📝</a> <a href="#projectManagement-benjamin" title="Project Management">📆</a> <a href="#talk-benjamin" title="Talks">📢</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ola-hn"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ola Hneini"/><br /><sub><b>Ola Hneini</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/merge_requests?scope=all&state=all&approver_usernames[]=ola" title="Reviewed Pull Requests">👀</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#maintenance-ola" title="Maintenance">🚧</a> <a href="#question-ola" title="Answering Questions">💬</a> <a href="#ideas-ola" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mamot.fr/@rdelaage"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Romain de Laage"/><br /><sub><b>Romain de Laage</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="#infra-rdelaage" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-rdelaage" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/lyonelbernard"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Lyonel Bernard"/><br /><sub><b>Lyonel Bernard</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Lyonel" title="Bug reports">🐛</a> <a href="#question-Lyonel" title="Answering Questions">💬</a> <a href="#audio-Lyonel" title="Audio">🔊</a> <a href="#ideas-Lyonel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.crypticchameleon.com/"><img src="https://secure.gravatar.com/avatar/7c2a721b52d0763673a600e8f01bd745?s=80&d=identicon?s=100" width="100px;" alt="Christopher Lagonick-Weitzel"/><br /><sub><b>Christopher Lagonick-Weitzel</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ctlw83" title="Bug reports">🐛</a> <a href="#question-ctlw83" title="Answering Questions">💬</a> <a href="#audio-ctlw83" title="Audio">🔊</a> <a href="#ideas-ctlw83" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ernestoacosta.me/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Ernesto Acosta"/><br /><sub><b>Ernesto Acosta</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ernestoacostame" title="Bug reports">🐛</a> <a href="#audio-ernestoacostame" title="Audio">🔊</a> <a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#question-ernestoacostame" title="Answering Questions">💬</a> <a href="#ideas-ernestoacostame" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fedi.bzh/@ewen"><img src="https://mastodon.fedi.bzh/system/accounts/avatars/000/000/002/original/6f387690a504ae46.jpg?s=100" width="100px;" alt="Ewen"/><br /><sub><b>Ewen</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-3wen" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Behel"><img src="https://secure.gravatar.com/avatar/ad63ee8ef8e3db8253d21e5012d2724f?s=80&d=identicon?s=100" width="100px;" alt="Bastien Luneteau"/><br /><sub><b>Bastien Luneteau</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a> <a href="https://code.castopod.org/adaures/castopod/issues?author_username=Behel" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.cecillie.fr/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Cécile Ricordeau"/><br /><sub><b>Cécile Ricordeau</b></sub></a><br /><a href="#design-cecillie" title="Design">🎨</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/PatrykMis"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Patryk Miś"/><br /><sub><b>Patryk Miś</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/mspanc"><img src="https://secure.gravatar.com/avatar/eed8337939641eac5ad0b570bd6acf96?s=80&d=identicon?s=100" width="100px;" alt="Marcin Lewandowski"/><br /><sub><b>Marcin Lewandowski</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=mspanc" title="Bug reports">🐛</a> <a href="#ideas-mspanc" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/SJanik"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sebastian Janik"/><br /><sub><b>Sebastian Janik</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/patryk"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Patryk Karczmarczyk"/><br /><sub><b>Patryk Karczmarczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/ddenis"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="denis d"/><br /><sub><b>denis d</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=ddenis" title="Bug reports">🐛</a> <a href="#ideas-ddenis" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/douglaskastle"><img src="https://secure.gravatar.com/avatar/b7e652ba4b6bcd440afa069e7f7bc9e6?s=80&d=identicon?s=100" width="100px;" alt="Douglas Kastle"/><br /><sub><b>Douglas Kastle</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=douglaskastle" title="Bug reports">🐛</a> <a href="#ideas-douglaskastle" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/cExplorer"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="cExplorer"/><br /><sub><b>cExplorer</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=cExplorer" title="Bug reports">🐛</a> <a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/imacrea"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="ImaCrea"/><br /><sub><b>ImaCrea</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=imacrea" title="Bug reports">🐛</a> <a href="#ideas-imacrea" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/jonas"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Jonas S"/><br /><sub><b>Jonas S</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/yannL"><img src="https://secure.gravatar.com/avatar/9c46600ce566ec6d526370d8e104b1c8?s=80&d=identicon?s=100" width="100px;" alt="LEFEBVRE Yann"/><br /><sub><b>LEFEBVRE Yann</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=yannL" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/spaetz"><img src="https://secure.gravatar.com/avatar/278e1af65e82993efd0ba7bbbacf6435?s=80&d=identicon?s=100" width="100px;" alt="Sebastian Späth"/><br /><sub><b>Sebastian Späth</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=spaetz" title="Bug reports">🐛</a> <a href="#ideas-spaetz" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/rocky"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="rocky III"/><br /><sub><b>rocky III</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=rocky" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/Regenpfeifer"><img src="https://code.castopod.org/uploads/-/system/user/avatar/103/avatar.png?s=100" width="100px;" alt="Hermann Josef Eckl"/><br /><sub><b>Hermann Josef Eckl</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Regenpfeifer" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://code.castopod.org/cyrilledel"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Delhaye Cyrille"/><br /><sub><b>Delhaye Cyrille</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=cyrilledel" title="Bug reports">🐛</a> <a href="#ideas-cyrilledel" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/otetranome"><img src="https://code.castopod.org/uploads/-/system/user/avatar/113/avatar.png?s=100" width="100px;" alt="João Leandro"/><br /><sub><b>João Leandro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a> <a href="#ideas-otetranome" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://achouvardas.eu/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Angelos Chouvardas"/><br /><sub><b>Angelos Chouvardas</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mastodon.fjerland.no/@eivind"><img src="https://mastodon.fjerland.no/system/accounts/avatars/107/769/768/295/192/222/original/e5c985fea6487dcb.jpg?s=100" width="100px;" alt="Eivind"/><br /><sub><b>Eivind</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/forght"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15073833/large/82d1e2e443a6df7edc43a7405dfeeb75_default.png?s=100" width="100px;" alt="forght"/><br /><sub><b>forght</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/glottis0q"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15209934/large/8b17ef6a7399f0b82a8198f87c224195.png?s=100" width="100px;" alt="glottis0q"/><br /><sub><b>glottis0q</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mstdn.fr/@ButterflyOfFire"><img src="https://static.mstdn.fr/static/accounts/avatars/000/065/901/original/5908e93ad5447f15.png?s=100" width="100px;" alt="ButterflyOfFire"/><br /><sub><b>ButterflyOfFire</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lil5"><img src="https://avatars.githubusercontent.com/u/17646836?v=4?s=100" width="100px;" alt="Lucian I. Last"/><br /><sub><b>Lucian I. Last</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/luuzviir"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13166188/large/d03ab0abc7ce354b210d836955cd3805_default.png?s=100" width="100px;" alt="LuuzViir"/><br /><sub><b>LuuzViir</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/cthtc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15211502/large/ed0651060cb8474a9519b5168bd377c1_default.png?s=100" width="100px;" alt="CTHTC"/><br /><sub><b>CTHTC</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/retrograde"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15021651/large/b10c4057f85bf4de49c7fdf01354ecde.jpeg?s=100" width="100px;" alt="Russian Retro"/><br /><sub><b>Russian Retro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/mareklach"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13572324/large/3eeba8d569c247ace33862bf4ef4748f.jpeg?s=100" width="100px;" alt="Marek L'ach"/><br /><sub><b>Marek L'ach</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gunchleoc"><img src="https://crowdin-static.downloads.crowdin.com/avatar/13043878/large/3223f7b606296a8b1c92c5de39c459a2_default.png?s=100" width="100px;" alt="GunChleoc"/><br /><sub><b>GunChleoc</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/gabisnow"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15214858/large/5b083bdf9c9e9de67cc6ee72a6c8db18_default.png?s=100" width="100px;" alt="GabiSnow"/><br /><sub><b>GabiSnow</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/bendaha"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15331656/large/cd92450d2c20202299fb3a0075903e20_default.png?s=100" width="100px;" alt="bendaha"/><br /><sub><b>bendaha</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/samuelroland"><img src="https://crowdin-static.downloads.crowdin.com/avatar/14980053/large/3e154a37d03d6e98ae402ed3f930f4f5.png?s=100" width="100px;" alt="Samuel Roland"/><br /><sub><b>Samuel Roland</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://dimitriregnier.net/"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Dimitri Regnier"/><br /><sub><b>Dimitri Regnier</b></sub></a><br /><a href="#ideas-dimregnier" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://im.irithys.com/@thy"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15405614/large/3086461c47cce0a0c031925e5f943412.png?s=100" width="100px;" alt="irithys"/><br /><sub><b>irithys</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://twitter.com/caos30"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Sergi"/><br /><sub><b>Sergi</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/basen1982"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="Andreas Olsson"/><br /><sub><b>Andreas Olsson</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/leonfrom"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="leonfrom"/><br /><sub><b>leonfrom</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/agentcobra57"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="agentcobra"/><br /><sub><b>agentcobra</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/alephoto85"><img src="https://crowdin-static.downloads.crowdin.com/avatar/15094649/large/530391f54157af52ae33058ec15b0f99.jpg?s=100" width="100px;" alt="Alessandro"/><br /><sub><b>Alessandro</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://crowdin.com/profile/liimee"><img src="https://castopod.org/assets/images/castopod-avatar.jpg?s=100" width="100px;" alt="liimee"/><br /><sub><b>liimee</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahmedsabouni"><img src="https://avatars.githubusercontent.com/u/74497842?v=4?s=100" width="100px;" alt="Ahmed Sabouni"/><br /><sub><b>Ahmed Sabouni</b></sub></a><br /><a href="https://translate.castopod.org" title="Translation">🌍</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KrzysztofDomanczyk"><img src="https://avatars.githubusercontent.com/u/75178474?v=4?s=100" width="100px;" alt="KrzysztofDomanczyk"/><br /><sub><b>KrzysztofDomanczyk</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Dwev"><img src="https://avatars.githubusercontent.com/u/46626050?v=4?s=100" width="100px;" alt="Guy Martin"/><br /><sub><b>Guy Martin</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/issues?author_username=Dwev" title="Bug reports">🐛</a> <a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/prcutler"><img src="https://avatars.githubusercontent.com/u/67276?v=4?s=100" width="100px;" alt="Paul Cutler"/><br /><sub><b>Paul Cutler</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Documentation">📖</a> <a href="#question-prcutler" title="Answering Questions">💬</a> <a href="#ideas-prcutler" title="Ideas, Planning, & Feedback">🤔</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nateritter"><img src="https://avatars.githubusercontent.com/u/198798?v=4?s=100" width="100px;" alt="Nate Ritter"/><br /><sub><b>Nate Ritter</b></sub></a><br /><a href="https://code.castopod.org/adaures/castopod/commits/master" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the
[all-contributors](https://github.com/all-contributors/all-contributors)
specification. Contributions of any kind welcome!
## Contact
You may reach us for help or ask any question you have on: You may reach us for help or ask any question you have on:
- [Discord](https://castopod.org/discord) (for direct interaction with - [Discord](https://castopod.org/discord) (for direct interaction with
developers and the community) developers and the community)
- [Issue tracker](https://code.castopod.org/adaures/castopod/-/issues) (for
feature requests & bug reports)
Alternatively, you can follow us on social media platforms to get news about Alternatively, you can follow us on social media platforms to get news about
Castopod: Castopod:
- [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance) - [podlibre.social](https://podlibre.social/@Castopod) (Mastodon instance)
- [Bluesky](https://bsky.app/profile/castopod.org) - [Twitter](https://twitter.com/castopod)
- [LinkedIn](https://linkedin.com/company/castopod)
- [Facebook](https://www.facebook.com/castopod) - [Facebook](https://www.facebook.com/castopod)
## Sponsors ## Sponsors
The ongoing development of Castopod is made possible with the support of its [Castopod](https://nlnet.nl/project/Castopod/) was funded through the
backers. If you'd like to help, please consider [NGI0 Discovery](https://nlnet.nl/discovery/) Fund under grant agreement
[sponsoring Castopod's development](https://opencollective.com/castopod/contribute). Nº 825322.
<table> The fund was established by NLnet with financial support from the European
<tbody> Commission's [Next Generation Internet](https://www.ngi.eu/) programme, under
<tr> the aegis of DG Communications Networks, Content and Technology.
<td align="center">
<a href="https://docs.castopod.org/images/sponsors/adaures.svg" target="_blank" rel="noopener noreferrer"><img height="48" src="./docs/src/assets/images/sponsors/adaures.svg" alt="Ad Aures" /></a>
</td>
<td align="center">
<a href="https://nlnet.nl/project/Castopod/" target="_blank" rel="noopener noreferrer"><img src="./docs/src/assets/images/sponsors/nlnet.svg" alt="NLnet Logo" height="48" /></a>
</td>
</tr>
</tbody>
</table>
## License
[GNU Affero General Public License v3.0](https://choosealicense.com/licenses/agpl-3.0/)
Copyright © 2020-present, [Ad Aures](https://adaures.com/).
[release]: https://code.castopod.org/adaures/castopod/-/releases
[release-badge]:
https://img.shields.io/gitlab/v/release/2?color=brightgreen&gitlab_url=https%3A%2F%2Fcode.castopod.org%2F&include_prereleases&label=release
[license]: https://code.castopod.org/adaures/castopod/-/blob/beta/LICENSE.md
[license-badge]:
https://img.shields.io/github/license/ad-aures/castopod?color=blue
[contributions]: https://code.castopod.org/adaures/castopod/-/issues
[contributions-badge]:
https://img.shields.io/badge/contributions-welcome-brightgreen.svg
[semantic-release]: https://github.com/semantic-release/semantic-release
[semantic-release-badge]:
https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
[discord]: https://castopod.org/discord
[discord-badge]: https://img.shields.io/badge/chat-on%20discord-7389D8
[stars]: https://github.com/ad-aures/castopod/stargazers
[stars-badge]:
https://img.shields.io/github/stars/ad-aures/castopod?style=social
[crowdin]: https://translate.castopod.org/project/castopod
[crowdin-badge]: https://badges.crowdin.net/castopod/localized.svg

95
UPDATE.md Normal file
View file

@ -0,0 +1,95 @@
# How to update Castopod Host <!-- omit in toc -->
After installing _Castopod Host_, you may want to update your instance to the
latest version in order to enjoy the latest features ✨, bug fixes 🐛 and
performance improvements ⚡.
## Table of contents <!-- omit in toc -->
- [Manual update instructions](#manual-update-instructions)
- [Automatic update instructions](#automatic-update-instructions)
- [Frequently asked questions (FAQ)](#frequently-asked-questions-faq)
- [Where can I find my _Castopod Host_ version?](#where-can-i-find-my-castopod-host-version)
- [I haven't updated my instance in a long time… What should I do?](#i-havent-updated-my-instance-in-a-long-time-what-should-i-do)
- [Should I make a backup before updating?](#should-i-make-a-backup-before-updating)
## Manual update instructions
1. Go to the
[releases page](https://code.podlibre.org/podlibre/castopod-host/-/releases)
and see if your instance is up to date with the latest _Castopod Host_
version
- cf.
[Where can I find my _Castopod Host_ version?](#where-can-i-find-my-castopod-host-version)
2. Download the latest release package named `Castopod Host Package`, you may
choose between the `zip` or `tar.gz` archives
- ⚠️ Make sure you download the Castopod Host Package and **NOT** the Source
Code
3. On your server:
- Remove all files except `.env` and `public/media`
- Copy the new files from the downloaded package into your server
- Note: you may need to reset files permissions as during the install
process. Check
[Security Concerns section in INSTALL.md](./INSTALL.md#security-concerns).
4. Alpha releases may come with additional update instructions (see
[releases page](https://code.podlibre.org/podlibre/castopod-host/-/releases)).
They are usually database migration scripts in `.sql` format to update your
database schema.
- 👉 Make sure you run the scripts on your phpmyadmin panel or using command
line to update the database along with the package files!
- cf.
[I haven't updated my instance in a long time… What should I do?](#i-havent-updated-my-instance-in-a-long-time-what-should-i-do)
5. If you are using redis, clear your cache.
6. ✨ Enjoy your fresh instance, you're all done!
## Automatic update instructions
> Coming soon... 👀
## Frequently asked questions (FAQ)
### Where can I find my _Castopod Host_ version?
Go to your _Castopod Host_ admin panel, the version is displayed on the bottom
right corner.
Alternatively, you can find the version in the `app > Config > Constants.php`
file.
### I haven't updated my instance in a long time… What should I do?
No problem! Just get the latest release as described above. Only, when going
through the release instructions (4), perform them sequentially, from the oldest
to the newest.
> You may want to backup your instance depending on how long you haven't updated
> _Castopod Host_.
For example, if you're on `v1.0.0-alpha.42` and would like to upgrade to
`v1.0.0-alpha.58`:
0. (recommended) Make a backup of your files and database.
1. Download the latest release, overwrite your files whilst keeping `.env` and
`public/media`.
2. Go through each release update instructions sequentially (from oldest to
newest) starting with `v1.0.0-alpha.43`, `v1.0.0-alpha.44`,
`v1.0.0-alpha.45`, …, `v1.0.0-alpha.58`.
3. ✨ Enjoy your fresh instance, you're all done!
### Should I make a backup before updating?
We advise you do, so you don't lose everything if anything goes wrong!
More generally, we advise you make regular backups of your Castopod Host files
and database to prevent you from losing it all…

View file

@ -1,2 +1,6 @@
<IfModule authz_core_module> Require all denied </IfModule> <IfModule authz_core_module>
<IfModule !authz_core_module> Deny from all </IfModule> Require all denied
</IfModule>
<IfModule !authz_core_module>
Deny from all
</IfModule>

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\FlatAuthorization as MythAuthFlatAuthorization;
class FlatAuthorization extends MythAuthFlatAuthorization
{
/**
* The group model to use. Usually the class noted below (or an extension thereof) but can be any compatible
* CodeIgniter Model.
*
* @var PermissionModel
*/
protected $permissionModel;
/**
* Checks a group to see if they have the specified permission.
*/
public function groupHasPermission(int | string $permission, int $groupId): bool
{
// Get the Permission ID
$permissionId = $this->getPermissionID($permission);
if (! is_numeric($permissionId)) {
return false;
}
return $this->permissionModel->doesGroupHavePermission($groupId, $permissionId);
}
/**
* Makes user part of given groups.
*
* @param array<string, string> $groups Either collection of ID or names
*/
public function setUserGroups(int $userId, array $groups = []): bool
{
// remove user from all groups before resetting it in new groups
$this->groupModel->removeUserFromAllGroups($userId);
if ($groups === []) {
return true;
}
foreach ($groups as $group) {
$this->addUserToGroup($userId, $group);
}
return true;
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\GroupModel as MythAuthGroupModel;
class GroupModel extends MythAuthGroupModel
{
/**
* @return mixed[]
*/
public function getContributorRoles(): array
{
return $this->select('auth_groups.*')
->like('name', 'podcast_', 'after')
->findAll();
}
/**
* @return mixed[]
*/
public function getUserRoles(): array
{
return $this->select('auth_groups.*')
->notLike('name', 'podcast_', 'after')
->findAll();
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Authorization;
use Myth\Auth\Authorization\PermissionModel as MythAuthPermissionModel;
class PermissionModel extends MythAuthPermissionModel
{
/**
* Checks to see if a user, or one of their groups, has a specific permission.
*/
public function doesGroupHavePermission(int $groupId, int $permissionId): bool
{
// Check group permissions and take advantage of caching
$groupPerms = $this->getPermissionsForGroup($groupId);
return count($groupPerms) &&
array_key_exists($permissionId, $groupPerms);
}
/**
* Gets all permissions for a group in a way that can be easily used to check against:
*
* [ id => name, id => name ]
*
* @return array<int, string>
*/
public function getPermissionsForGroup(int $groupId): array
{
$cacheName = "group{$groupId}_permissions";
if (! ($found = cache($cacheName))) {
$groupPermissions = $this->db
->table('auth_groups_permissions')
->select('id, auth_permissions.name')
->join('auth_permissions', 'auth_permissions.id = permission_id', 'inner')
->where('group_id', $groupId)
->get()
->getResultObject();
$found = [];
foreach ($groupPermissions as $row) {
$found[$row->id] = strtolower($row->name);
}
cache()
->save($cacheName, $found, 300);
}
return $found;
}
}

View file

@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Commands;
use App\Models\EpisodeModel;
use CodeIgniter\CLI\BaseCommand;
class EpisodesComputeDownloads extends BaseCommand
{
/**
* The Command's Group
*
* @var string
*/
protected $group = 'Episodes';
/**
* The Command's Name
*
* @var string
*/
protected $name = 'episodes:compute-downloads';
/**
* The Command's Description
*
* @var string
*/
protected $description = "Calculates all episodes downloads and stores results in episodes' downloads_count field.";
/**
* Actually execute a command.
*/
public function run(array $params): void
{
$episodesModel = new EpisodeModel();
$query = $episodesModel->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();
}
}

View file

@ -2,49 +2,13 @@
declare(strict_types=1); declare(strict_types=1);
use ViewThemes\Theme;
/** /**
* The goal of this file is to allow developers a location where they can overwrite core procedural functions and * The goal of this file is to allow developers a location where they can overwrite core procedural functions and
* replace them with their own. This file is loaded during the bootstrap process and is called during the framework's * replace them with their own. This file is loaded during the bootstrap process and is called during the frameworks
* execution. * execution.
* *
* This can be looked at as a `master helper` file that is loaded early on, and may also contain additional functions * This can be looked at as a `master helper` file that is loaded early on, and may also contain additional functions
* that you'd like to use throughout your entire application * that you'd like to use throughout your entire application
* *
* @see: https://codeigniter.com/user_guide/extending/common.html * @link: https://codeigniter4.github.io/CodeIgniter4/
*/ */
if (! function_exists('view')) {
/**
* Grabs the current RendererInterface-compatible class and tells it to render the specified view. Simply provides a
* convenience method that can be used in Controllers, libraries, and routed closures.
*
* NOTE: Does not provide any escaping of the data, so that must all be handled manually by the developer.
*
* @param array<string, mixed> $data
* @param array<string, mixed> $options Unused - reserved for third-party extensions.
*/
function view(string $name, array $data = [], array $options = []): string
{
if (array_key_exists('theme', $options)) {
Theme::setTheme($options['theme']);
}
$path = Theme::path();
/** @var CodeIgniter\View\View $renderer */
$renderer = single_service('renderer', $path);
$saveData = config('View')
->saveData;
if (array_key_exists('saveData', $options)) {
$saveData = (bool) $options['saveData'];
unset($options['saveData']);
}
return $renderer->setData($data, 'raw')
->render($name, $options, $saveData);
}
}

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Config;
use ActivityPub\Config\ActivityPub as ActivityPubBase;
use App\Libraries\NoteObject;
class ActivityPub extends ActivityPubBase
{
/**
* --------------------------------------------------------------------
* ActivityPub Objects
* --------------------------------------------------------------------
*/
public string $noteObject = NoteObject::class;
/**
* --------------------------------------------------------------------
* Default avatar and cover images
* --------------------------------------------------------------------
*/
public string $defaultAvatarImagePath = 'assets/images/castopod-avatar-default.jpg';
public string $defaultAvatarImageMimetype = 'image/jpeg';
public string $defaultCoverImagePath = 'assets/images/castopod-cover-default.jpg';
public string $defaultCoverImageMimetype = 'image/jpeg';
}

43
app/Config/Analytics.php Normal file
View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Config;
use Analytics\Config\Analytics as AnalyticsBase;
class Analytics extends AnalyticsBase
{
/**
* --------------------------------------------------------------------
* Route filters options
* --------------------------------------------------------------------
*/
public array $routeFilters = [
'analytics-full-data' => 'permission:podcasts-view,podcast-view',
'analytics-data' => 'permission:podcasts-view,podcast-view',
'analytics-filtered-data' => 'permission:podcasts-view,podcast-view',
];
public function __construct()
{
parent::__construct();
// set the analytics gateway behind the admin gateway.
// Only logged in users should be able to view analytics
$this->gateway = config('App')
->adminGateway . '/analytics';
}
/**
* get the full audio file url
*
* @param string|string[] $audioFilePath
*/
public function getAudioFileUrl($audioFilePath): string
{
helper('media');
return media_base_url($audioFilePath);
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use Override; use CodeIgniter\Session\Handlers\FileHandler;
class App extends BaseConfig class App extends BaseConfig
{ {
@ -14,34 +14,38 @@ class App extends BaseConfig
* Base Site URL * Base Site URL
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* URL to your CodeIgniter root. Typically, this will be your base URL, * URL to your CodeIgniter root. Typically this will be your base URL,
* WITH a trailing slash: * WITH a trailing slash:
* *
* E.g., http://example.com/ * http://example.com/
*
* If this is not set then CodeIgniter will try guess the protocol, domain
* and path to your installation. However, you should always configure this
* explicitly and never rely on auto-guessing, especially in production
* environments.
*/ */
public string $baseURL = 'http://localhost:8080/'; public string $baseURL = 'http://localhost:8080/';
/** /**
* Allowed Hostnames in the Site URL other than the hostname in the baseURL. * --------------------------------------------------------------------------
* If you want to accept multiple Hostnames, set this. * Media Base URL
* --------------------------------------------------------------------------
* *
* E.g., * URL to your media root. Typically this will be your base URL,
* When your site URL ($baseURL) is 'http://example.com/', and your site * WITH a trailing slash:
* also accepts 'http://media.example.com/' and 'http://accounts.example.com/':
* ['media.example.com', 'accounts.example.com']
* *
* @var list<string> * http://cdn.example.com/
*/ */
public array $allowedHostnames = []; public string $mediaBaseURL = 'http://localhost:8080/';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Index File * Index File
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Typically, this will be your `index.php` file, unless you've renamed it to * Typically this will be your index.php file, unless you've renamed it to
* something else. If you have configured your web server to remove this file * something else. If you are using mod_rewrite to remove the page set this
* from your site URIs, set this variable to an empty string. * variable so that it is blank.
*/ */
public string $indexPage = ''; public string $indexPage = '';
@ -50,42 +54,18 @@ class App extends BaseConfig
* URI PROTOCOL * URI PROTOCOL
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* This item determines which server global should be used to retrieve the * This item determines which getServer global should be used to retrieve the
* URI string. The default setting of 'REQUEST_URI' works for most servers. * URI string. The default setting of 'REQUEST_URI' works for most servers.
* If your links do not seem to work, try one of the other delicious flavors: * If your links do not seem to work, try one of the other delicious flavors:
* *
* 'REQUEST_URI': Uses $_SERVER['REQUEST_URI'] * 'REQUEST_URI' Uses $_SERVER['REQUEST_URI']
* 'QUERY_STRING': Uses $_SERVER['QUERY_STRING'] * 'QUERY_STRING' Uses $_SERVER['QUERY_STRING']
* 'PATH_INFO': Uses $_SERVER['PATH_INFO'] * 'PATH_INFO' Uses $_SERVER['PATH_INFO']
* *
* WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded! * WARNING: If you set this to 'PATH_INFO', URIs will always be URL-decoded!
*/ */
public string $uriProtocol = 'REQUEST_URI'; public string $uriProtocol = 'REQUEST_URI';
/*
*--------------------------------------------------------------------------
* Allowed URL Characters
*--------------------------------------------------------------------------
*
* This lets you specify which characters are permitted within your URLs.
* When someone tries to submit a URL with disallowed characters they will
* get a warning message.
*
* As a security measure you are STRONGLY encouraged to restrict URLs to
* as few characters as possible.
*
* By default, only these are allowed: `a-z 0-9~%.:_-`
*
* Set an empty string to allow all characters -- but only if you are insane.
*
* The configured value is actually a regular expression character group
* and it will be used as: '/\A[<permittedURIChars>]+\z/iu'
*
* DO NOT CHANGE THIS UNLESS YOU FULLY UNDERSTAND THE REPERCUSSIONS!!
*
*/
public string $permittedURIChars = 'a-z 0-9~%.:_\-@';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Default Locale * Default Locale
@ -119,23 +99,9 @@ class App extends BaseConfig
* by the application in descending order of priority. If no match is * by the application in descending order of priority. If no match is
* found, the first locale will be used. * found, the first locale will be used.
* *
* IncomingRequest::setLocale() also uses this list. * @var string[]
*
* @var list<string>
*/ */
public array $supportedLocales = [ public array $supportedLocales = ['en', 'fr'];
'en',
'fr',
'pl',
'de',
'pt-br',
'nn-no',
'es',
'zh-hans',
'ca',
'br',
'sr-latn',
];
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
@ -144,9 +110,6 @@ class App extends BaseConfig
* *
* The default timezone that will be used in your application to display * The default timezone that will be used in your application to display
* dates with the date helper, and can be retrieved through app_timezone() * dates with the date helper, and can be retrieved through app_timezone()
*
* @see https://www.php.net/manual/en/timezones.php for list of timezones
* supported by PHP.
*/ */
public string $appTimezone = 'UTC'; public string $appTimezone = 'UTC';
@ -170,10 +133,170 @@ class App extends BaseConfig
* If true, this will force every request made to this application to be * If true, this will force every request made to this application to be
* made via a secure connection (HTTPS). If the incoming request is not * made via a secure connection (HTTPS). If the incoming request is not
* secure, the user will be redirected to a secure version of the page * secure, the user will be redirected to a secure version of the page
* and the HTTP Strict Transport Security (HSTS) header will be set. * and the HTTP Strict Transport Security header will be set.
*/ */
public bool $forceGlobalSecureRequests = true; public bool $forceGlobalSecureRequests = true;
/**
* --------------------------------------------------------------------------
* Session Driver
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*/
public string $sessionDriver = FileHandler::class;
/**
* --------------------------------------------------------------------------
* Session Cookie Name
* --------------------------------------------------------------------------
*
* The session cookie name, must contain only [0-9a-z_-] characters
*/
public string $sessionCookieName = 'ci_session';
/**
* --------------------------------------------------------------------------
* Session Expiration
* --------------------------------------------------------------------------
*
* The number of SECONDS you want the session to last.
* Setting to 0 (zero) means expire when the browser is closed.
*/
public int $sessionExpiration = 7200;
/**
* --------------------------------------------------------------------------
* Session Save Path
* --------------------------------------------------------------------------
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*/
public string $sessionSavePath = WRITEPATH . 'session';
/**
* --------------------------------------------------------------------------
* Session Match IP
* --------------------------------------------------------------------------
*
* Whether to match the user's IP address when reading the session data.
*
* WARNING: If you're using the database driver, don't forget to update
* your session table's PRIMARY KEY when changing this setting.
*/
public bool $sessionMatchIP = false;
/**
* --------------------------------------------------------------------------
* Session Time to Update
* --------------------------------------------------------------------------
*
* How many seconds between CI regenerating the session ID.
*/
public int $sessionTimeToUpdate = 300;
/**
* --------------------------------------------------------------------------
* Session Regenerate Destroy
* --------------------------------------------------------------------------
*
* Whether to destroy session data associated with the old session ID
* when auto-regenerating the session ID. When set to FALSE, the data
* will be later deleted by the garbage collector.
*/
public bool $sessionRegenerateDestroy = false;
/**
* --------------------------------------------------------------------------
* Cookie Prefix
* --------------------------------------------------------------------------
*
* Set a cookie name prefix if you need to avoid collisions.
*
* @deprecated use Config\Cookie::$prefix property instead.
*/
public string $cookiePrefix = '';
/**
* --------------------------------------------------------------------------
* Cookie Domain
* --------------------------------------------------------------------------
*
* Set to `.your-domain.com` for site-wide cookies.
*
* @deprecated use Config\Cookie::$domain property instead.
*/
public string $cookieDomain = '';
/**
* --------------------------------------------------------------------------
* Cookie Path
* --------------------------------------------------------------------------
*
* Typically will be a forward slash.
*
* @deprecated use Config\Cookie::$path property instead.
*/
public string $cookiePath = '/';
/**
* --------------------------------------------------------------------------
* Cookie Secure
* --------------------------------------------------------------------------
*
* Cookie will only be set if a secure HTTPS connection exists.
*
* @deprecated use Config\Cookie::$secure property instead.
*/
public bool $cookieSecure = false;
/**
* --------------------------------------------------------------------------
* Cookie HttpOnly
* --------------------------------------------------------------------------
*
* Cookie will only be accessible via HTTP(S) (no JavaScript).
*
* @deprecated use Config\Cookie::$httponly property instead.
*/
public bool $cookieHTTPOnly = true;
/**
* --------------------------------------------------------------------------
* Cookie SameSite
* --------------------------------------------------------------------------
*
* Configure cookie SameSite setting. Allowed values are:
* - None
* - Lax
* - Strict
* - ''
*
* Alternatively, you can use the constant names:
* - `Cookie::SAMESITE_NONE`
* - `Cookie::SAMESITE_LAX`
* - `Cookie::SAMESITE_STRICT`
*
* Defaults to `Lax` for compatibility with modern browsers. Setting `''`
* (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$cookieSecure` must also be set.
*
* @deprecated use Config\Cookie::$samesite property instead.
*/
public string $cookieSameSite = 'Lax';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Reverse Proxy IPs * Reverse Proxy IPs
@ -181,21 +304,103 @@ class App extends BaseConfig
* *
* If your server is behind a reverse proxy, you must whitelist the proxy * If your server is behind a reverse proxy, you must whitelist the proxy
* IP addresses from which CodeIgniter should trust headers such as * IP addresses from which CodeIgniter should trust headers such as
* X-Forwarded-For or Client-IP in order to properly identify * HTTP_X_FORWARDED_FOR and HTTP_CLIENT_IP in order to properly identify
* the visitor's IP address. * the visitor's IP address.
* *
* You need to set a proxy IP address or IP address with subnets and * You can use both an array or a comma-separated list of proxy addresses,
* the HTTP header for the client IP address. * as well as specifying whole subnets. Here are a few examples:
* *
* Here are some examples: * Comma-separated: '10.0.1.200,192.168.5.0/24'
* [ * Array: ['10.0.1.200', '192.168.5.0/24']
* '10.0.1.200' => 'X-Forwarded-For',
* '192.168.5.0/24' => 'X-Real-IP',
* ]
* *
* @var array<string, string>|string * @var string|string[]
*/ */
public $proxyIPs = []; public string | array $proxyIPs = '';
/**
* --------------------------------------------------------------------------
* CSRF Token Name
* --------------------------------------------------------------------------
*
* The token name.
*
* @deprecated Use `Config\Security` $tokenName property instead of using this property.
*/
public string $CSRFTokenName = 'csrf_test_name';
/**
* --------------------------------------------------------------------------
* CSRF Header Name
* --------------------------------------------------------------------------
*
* The header name.
*
* @deprecated Use `Config\Security` $headerName property instead of using this property.
*/
public string $CSRFHeaderName = 'X-CSRF-TOKEN';
/**
* --------------------------------------------------------------------------
* CSRF Cookie Name
* --------------------------------------------------------------------------
*
* The cookie name.
*
* @deprecated Use `Config\Security` $cookieName property instead of using this property.
*/
public string $CSRFCookieName = 'csrf_cookie_name';
/**
* --------------------------------------------------------------------------
* CSRF Expire
* --------------------------------------------------------------------------
*
* The number in seconds the token should expire.
*
* @deprecated Use `Config\Security` $expire property instead of using this property.
*/
public int $CSRFExpire = 7200;
/**
* --------------------------------------------------------------------------
* CSRF Regenerate
* --------------------------------------------------------------------------
*
* Regenerate token on every submission?
*
* @deprecated Use `Config\Security` $regenerate property instead of using this property.
*/
public bool $CSRFRegenerate = true;
/**
* --------------------------------------------------------------------------
* CSRF Redirect
* --------------------------------------------------------------------------
*
* Redirect to previous page with error on failure?
*
* @deprecated Use `Config\Security` $redirect property instead of using this property.
*/
public bool $CSRFRedirect = true;
/**
* --------------------------------------------------------------------------
* CSRF SameSite
* --------------------------------------------------------------------------
*
* Setting for CSRF SameSite cookie token. Allowed values are:
* - None
* - Lax
* - Strict
* - ''
*
* Defaults to `Lax` as recommended in this link:
*
* @see https://portswigger.net/web-security/csrf/samesite-cookies
*
* @deprecated Use `Config\Security` $samesite property instead of using this property.
*/
public string $CSRFSameSite = 'Lax';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
@ -217,70 +422,33 @@ class App extends BaseConfig
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Instance / Site Config * Media root folder
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Defines the root folder for media files storage
*/ */
public string $siteName = 'Castopod'; public string $mediaRoot = 'media';
public string $siteTitleSeparator = ' | ';
public string $siteDescription = 'Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.';
/** /**
* @var array<int|string, string> * --------------------------------------------------------------------------
* Admin gateway
* --------------------------------------------------------------------------
* Defines a base route for all admin pages
*/ */
public array $siteIcon = [ public string $adminGateway = 'cp-admin';
'ico' => '/favicon.ico',
'64' => '/icon-64.png',
'180' => '/icon-180.png',
'192' => '/icon-192.png',
'512' => '/icon-512.png',
];
public string $theme = 'pine';
/** /**
* Storage limit in Gigabytes * --------------------------------------------------------------------------
* Auth gateway
* --------------------------------------------------------------------------
* Defines a base route for all authentication related pages
*/ */
public ?int $storageLimit = null; public string $authGateway = 'cp-auth';
/** /**
* Bandwidth limit (per month) in Gigabytes * --------------------------------------------------------------------------
* Install gateway
* --------------------------------------------------------------------------
* Defines a base route for instance installation
*/ */
public ?int $bandwidthLimit = null; public string $installGateway = 'cp-install';
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);
}
} }

58
app/Config/Auth.php Normal file
View file

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

View file

@ -27,39 +27,30 @@ class Autoload extends AutoloadConfig
* their location on the file system. These are used by the autoloader * their location on the file system. These are used by the autoloader
* to locate files the first time they have been instantiated. * to locate files the first time they have been instantiated.
* *
* The 'Config' (APPPATH . 'Config') and 'CodeIgniter' (SYSTEMPATH) are * The '/app' and '/system' directories are already mapped for you.
* already mapped for you. * you may change the name of the 'App' namespace if you wish,
*
* You may change the name of the 'App' namespace if you wish,
* but this should be done prior to creating any namespaced classes, * but this should be done prior to creating any namespaced classes,
* else you will need to modify all of those classes for this to work. * else you will need to modify all of those classes for this to work.
* *
* @var array<string, list<string>|string> * Prototype:
*
* $psr4 = [
* 'CodeIgniter' => SYSTEMPATH,
* 'App' => APPPATH
* ];
*
* @var array<string, string>
*/ */
public $psr4 = [ public $psr4 = [
APP_NAMESPACE => APPPATH, APP_NAMESPACE => APPPATH,
'Modules' => ROOTPATH . 'modules/', 'Config' => APPPATH . 'Config',
'Modules\Admin' => ROOTPATH . 'modules/Admin/', 'ActivityPub' => APPPATH . 'Libraries/ActivityPub',
'Modules\Analytics' => ROOTPATH . 'modules/Analytics/', 'Analytics' => APPPATH . 'Libraries/Analytics',
'Modules\Api\Rest\V1' => ROOTPATH . 'modules/Api/Rest/V1',
'Modules\Auth' => ROOTPATH . 'modules/Auth/',
'Modules\Fediverse' => ROOTPATH . 'modules/Fediverse/',
'Modules\Install' => ROOTPATH . 'modules/Install/',
'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/',
'Modules\WebSub' => ROOTPATH . 'modules/WebSub/',
'Themes' => ROOTPATH . 'themes',
'ViewComponents' => APPPATH . 'Libraries/ViewComponents/',
'ViewThemes' => APPPATH . 'Libraries/ViewThemes/',
]; ];
/** /**
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* Class Map
* ------------------------------------------------------------------- * -------------------------------------------------------------------
* The class map provides a map of class names and their exact * The class map provides a map of class names and their exact
* location on the drive. Classes loaded in this manner will have * location on the drive. Classes loaded in this manner will have
@ -86,25 +77,12 @@ class Autoload extends AutoloadConfig
* or for loading functions. * or for loading functions.
* *
* Prototype: * Prototype:
* * ```
* $files = [ * $files = [
* '/path/to/my/file.php', * '/path/to/my/file.php',
* ]; * ];
* * ```
* @var list<string> * @var array<int, string>
*/ */
public $files = []; public $files = [];
/**
* -------------------------------------------------------------------
* Helpers
* -------------------------------------------------------------------
* Prototype:
* $helpers = [
* 'form',
* ];
*
* @var list<string>
*/
public $helpers = ['auth', 'setting', 'plugins'];
} }

View file

@ -9,10 +9,8 @@ declare(strict_types=1);
* In development, we want to show as many errors as possible to help * In development, we want to show as many errors as possible to help
* make sure they don't make it to production. And save us hours of * make sure they don't make it to production. And save us hours of
* painful debugging. * painful debugging.
*
* If you set 'display_errors' to '1', CI4's detailed error report will show.
*/ */
error_reporting(E_ALL); error_reporting(-1);
ini_set('display_errors', '1'); ini_set('display_errors', '1');
/** /**

View file

@ -8,13 +8,9 @@ declare(strict_types=1);
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Don't show ANY in production environments. Instead, let the system catch * Don't show ANY in production environments. Instead, let the system catch
* it and display a generic error message. * it and display a generic error message.
*
* If you set 'display_errors' to '1', CI4's detailed error report will show.
*/ */
error_reporting(E_ALL & ~E_DEPRECATED);
// If you want to suppress more types of errors.
// error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
ini_set('display_errors', '0'); ini_set('display_errors', '0');
error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT & ~E_USER_NOTICE & ~E_USER_DEPRECATED);
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------

View file

@ -2,12 +2,6 @@
declare(strict_types=1); declare(strict_types=1);
/*
* The environment testing is reserved for PHPUnit testing. It has special
* conditions built into the framework at various places to assist with that.
* You cant use it for your development.
*/
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* ERROR DISPLAY * ERROR DISPLAY
@ -16,7 +10,7 @@ declare(strict_types=1);
* make sure they don't make it to production. And save us hours of * make sure they don't make it to production. And save us hours of
* painful debugging. * painful debugging.
*/ */
error_reporting(E_ALL); error_reporting(-1);
ini_set('display_errors', '1'); ini_set('display_errors', '1');
/** /**

View file

@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class CURLRequest extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* CURLRequest Share Connection Options
* --------------------------------------------------------------------------
*
* Share connection options between requests.
*
* @var list<int>
*
* @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
* --------------------------------------------------------------------------
*
* Whether share options between requests or not.
*
* If true, all the options won't be reset between requests.
* It may cause an error request with unnecessary headers.
*/
public bool $shareOptions = false;
}

View file

@ -4,8 +4,6 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Handlers\ApcuHandler;
use CodeIgniter\Cache\Handlers\DummyHandler; use CodeIgniter\Cache\Handlers\DummyHandler;
use CodeIgniter\Cache\Handlers\FileHandler; use CodeIgniter\Cache\Handlers\FileHandler;
use CodeIgniter\Cache\Handlers\MemcachedHandler; use CodeIgniter\Cache\Handlers\MemcachedHandler;
@ -37,6 +35,37 @@ class Cache extends BaseConfig
*/ */
public string $backupHandler = 'dummy'; 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/';
/**
* --------------------------------------------------------------------------
* Cache Include Query String
* --------------------------------------------------------------------------
*
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* array('q') = Enabled, but only take into account the specified list
* of query parameters.
*
* @var boolean|string[]
*/
public bool | array $cacheQueryString = false;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Key Prefix * Key Prefix
@ -60,51 +89,36 @@ class Cache extends BaseConfig
*/ */
public int $ttl = 60; public int $ttl = 60;
/**
* --------------------------------------------------------------------------
* Reserved Characters
* --------------------------------------------------------------------------
*
* A string of reserved characters that will not be allowed in keys or tags.
* Strings that violate this restriction will cause handlers to throw.
* Default: {}()/\@:
*
* Note: The default set is required for PSR-6 compliance.
*/
public string $reservedCharacters = '{}()/\@:';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* File settings * File settings
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
*
* Your file storage preferences can be specified below, if you are using * Your file storage preferences can be specified below, if you are using
* the File driver. * the File driver.
* *
* @var array{storePath?: string, mode?: int} * @var array<string, string|int|null>
*/ */
public array $file = [ public array $file = [
'storePath' => WRITEPATH . 'cache/', 'storePath' => WRITEPATH . 'cache/',
'mode' => 0640, 'mode' => 0640,
]; ];
/** /**
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
* Memcached settings * Memcached settings
* ------------------------------------------------------------------------- * -------------------------------------------------------------------------
*
* Your Memcached servers can be specified below, if you are using * Your Memcached servers can be specified below, if you are using
* the Memcached drivers. * the Memcached drivers.
* *
* @see https://codeigniter.com/user_guide/libraries/caching.html#memcached * @see https://codeigniter.com/user_guide/libraries/caching.html#memcached
* *
* @var array{host?: string, port?: int, weight?: int, raw?: bool} * @var array<string, string|int|boolean>
*/ */
public array $memcached = [ public array $memcached = [
'host' => '127.0.0.1', 'host' => '127.0.0.1',
'port' => 11211, 'port' => 11211,
'weight' => 1, 'weight' => 1,
'raw' => false, 'raw' => false,
]; ];
/** /**
@ -114,24 +128,14 @@ class Cache extends BaseConfig
* Your Redis server can be specified below, if you are using * Your Redis server can be specified below, if you are using
* the Redis or Predis drivers. * the Redis or Predis drivers.
* *
* @var array{ * @var array<string, string|int|null>
* host?: string,
* password?: string|null,
* port?: int,
* timeout?: int,
* async?: bool,
* persistent?: bool,
* database?: int
* }
*/ */
public array $redis = [ public array $redis = [
'host' => '127.0.0.1', 'host' => '127.0.0.1',
'password' => null, 'password' => null,
'port' => 6379, 'port' => 6379,
'timeout' => 0, 'timeout' => 0,
'async' => false, // specific to Predis and ignored by the native Redis extension 'database' => 0,
'persistent' => false,
'database' => 0,
]; ];
/** /**
@ -142,58 +146,14 @@ class Cache extends BaseConfig
* This is an array of cache engine alias' and class names. Only engines * This is an array of cache engine alias' and class names. Only engines
* that are listed here are allowed to be used. * that are listed here are allowed to be used.
* *
* @var array<string, class-string<CacheInterface>> * @var array<string, string>
*/ */
public array $validHandlers = [ public array $validHandlers = [
'apcu' => ApcuHandler::class, 'dummy' => DummyHandler::class,
'dummy' => DummyHandler::class, 'file' => FileHandler::class,
'file' => FileHandler::class,
'memcached' => MemcachedHandler::class, 'memcached' => MemcachedHandler::class,
'predis' => PredisHandler::class, 'predis' => PredisHandler::class,
'redis' => RedisHandler::class, 'redis' => RedisHandler::class,
'wincache' => WincacheHandler::class, 'wincache' => WincacheHandler::class,
]; ];
/**
* --------------------------------------------------------------------------
* Web Page Caching: Cache Include Query String
* --------------------------------------------------------------------------
*
* Whether to take the URL query string into consideration when generating
* output cache files. Valid options are:
*
* false = Disabled
* true = Enabled, take all query parameters into account.
* Please be aware that this may result in numerous cache
* files generated for the same page over and over again.
* ['q'] = Enabled, but only take into account the specified list
* of query parameters.
*
* @var bool|list<string>
*/
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<int>
*/
public array $cacheStatusCodes = [];
} }

View file

@ -1,150 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Colors extends BaseConfig
{
/**
* @var array<string, array<string, mixed>>
*/
public array $themes = [
/* Castopod's brand color */
'pine' => [
'accent-base' => [174, 100, 29],
'accent-hover' => [172, 100, 17],
'accent-muted' => [131, 100, 12],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [172, 100, 17],
'heading-background' => [111, 64, 94],
'background-elevated' => [0, 0, 100],
'background-base' => [173, 44, 96],
'background-navigation' => [172, 100, 17],
'background-header' => [172, 100, 17],
'background-highlight' => [111, 64, 94],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [111, 42, 86],
'border-contrast' => [0, 0, 0],
'border-navigation' => [131, 100, 12],
'text-base' => [158, 8, 3],
'text-muted' => [172, 8, 38],
],
/* Red / Rose color */
'crimson' => [
'accent-base' => [350, 87, 61],
'accent-hover' => [348, 75, 40],
'accent-muted' => [348, 73, 32],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [348, 73, 32],
'heading-background' => [344, 79, 96],
'background-elevated' => [0, 0, 100],
'background-base' => [350, 44, 96],
'background-header' => [348, 75, 40],
'background-highlight' => [344, 79, 96],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [348, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [340, 8, 3],
'text-muted' => [345, 8, 38],
],
/* Blue color */
'lake' => [
'accent-base' => [194, 100, 44],
'accent-hover' => [194, 100, 22],
'accent-muted' => [195, 100, 11],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [194, 100, 22],
'heading-background' => [195, 100, 92],
'background-elevated' => [0, 0, 100],
'background-base' => [196, 44, 96],
'background-header' => [194, 100, 22],
'background-highlight' => [195, 100, 92],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [195, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [194, 8, 3],
'text-muted' => [195, 8, 38],
],
/* Orange color */
'amber' => [
'accent-base' => [17, 100, 57],
'accent-hover' => [17, 100, 35],
'accent-muted' => [17, 100, 24],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [17, 100, 35],
'heading-background' => [17, 100, 89],
'background-elevated' => [0, 0, 100],
'background-base' => [15, 44, 96],
'background-header' => [17, 100, 35],
'background-highlight' => [17, 100, 89],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [17, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [15, 8, 3],
'text-muted' => [17, 8, 38],
],
/* Violet color */
'jacaranda' => [
'accent-base' => [254, 72, 52],
'accent-hover' => [254, 73, 30],
'accent-muted' => [254, 71, 19],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [254, 73, 30],
'heading-background' => [254, 73, 84],
'background-elevated' => [0, 0, 100],
'background-base' => [253, 44, 96],
'background-header' => [254, 73, 30],
'background-highlight' => [254, 88, 91],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [254, 42, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [253, 8, 3],
'text-muted' => [254, 8, 38],
],
/* Black color */
'onyx' => [
'accent-base' => [240, 17, 2],
'accent-hover' => [240, 17, 17],
'accent-muted' => [240, 17, 17],
'accent-contrast' => [0, 0, 100],
'heading-foreground' => [240, 17, 17],
'heading-background' => [240, 17, 94],
'background-elevated' => [0, 0, 100],
'background-base' => [240, 17, 96],
'background-header' => [240, 12, 17],
'background-highlight' => [240, 17, 94],
'background-backdrop' => [0, 0, 50],
'border-subtle' => [240, 17, 86],
'border-contrast' => [0, 0, 0],
'text-base' => [240, 8, 3],
'text-muted' => [240, 8, 38],
],
];
}

View file

@ -11,7 +11,7 @@ declare(strict_types=1);
| |
| NOTE: this constant is updated upon release with Continuous Integration. | NOTE: this constant is updated upon release with Continuous Integration.
*/ */
defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3'); defined('CP_VERSION') || define('CP_VERSION', '1.0.0-alpha.80');
/* /*
| -------------------------------------------------------------------- | --------------------------------------------------------------------
@ -24,23 +24,10 @@ defined('CP_VERSION') || define('CP_VERSION', '2.0.0-next.3');
| classes should use. | classes should use.
| |
| NOTE: changing this will require manually modifying the | 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'); 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 | Composer Path
@ -65,9 +52,9 @@ defined('MINUTE') || define('MINUTE', 60);
defined('HOUR') || define('HOUR', 3600); defined('HOUR') || define('HOUR', 3600);
defined('DAY') || define('DAY', 86400); defined('DAY') || define('DAY', 86400);
defined('WEEK') || define('WEEK', 604800); defined('WEEK') || define('WEEK', 604800);
defined('MONTH') || define('MONTH', 2_592_000); defined('MONTH') || define('MONTH', 2592000);
defined('YEAR') || define('YEAR', 31_536_000); defined('YEAR') || define('YEAR', 31536000);
defined('DECADE') || define('DECADE', 315_360_000); defined('DECADE') || define('DECADE', 315360000);
/* /*
| -------------------------------------------------------------------------- | --------------------------------------------------------------------------

View file

@ -26,77 +26,37 @@ class ContentSecurityPolicy extends BaseConfig
*/ */
public ?string $reportURI = null; 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 * 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. * numbers of old URLs that need to be rewritten.
*/ */
public bool $upgradeInsecureRequests = false; 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>|string|null * @var string|string[]|null
*/ */
public string | array | null $defaultSrc = null; public string | array | null $defaultSrc = null;
/** /**
* Lists allowed scripts' URLs. * Lists allowed scripts' URLs.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $scriptSrc = 'self'; public string | array $scriptSrc = 'self';
/**
* Specifies valid sources for JavaScript <script> elements.
*
* @var list<string>|string
*/
public array|string $scriptSrcElem = 'self';
/**
* Specifies valid sources for JavaScript inline event
* handlers and JavaScript URLs.
*
* @var list<string>|string
*/
public array|string $scriptSrcAttr = 'self';
/** /**
* Lists allowed stylesheets' URLs. * Lists allowed stylesheets' URLs.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $styleSrc = 'self'; public string | array $styleSrc = 'self';
/**
* Specifies valid sources for stylesheets <link> elements.
*
* @var list<string>|string
*/
public array|string $styleSrcElem = 'self';
/**
* Specifies valid sources for stylesheets inline
* style attributes and `<style>` elements.
*
* @var list<string>|string
*/
public array|string $styleSrcAttr = 'self';
/** /**
* Defines the origins from which images can be loaded. * Defines the origins from which images can be loaded.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $imageSrc = 'self'; public string | array $imageSrc = 'self';
@ -105,35 +65,35 @@ class ContentSecurityPolicy extends BaseConfig
* *
* Will default to self if not overridden * Will default to self if not overridden
* *
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $baseURI = null; public string | array | null $baseURI = null;
/** /**
* Lists the URLs for workers and embedded frame contents * Lists the URLs for workers and embedded frame contents
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $childSrc = 'self'; public string | array $childSrc = 'self';
/** /**
* Limits the origins that you can connect to (via XHR, WebSockets, and EventSource). * Limits the origins that you can connect to (via XHR, WebSockets, and EventSource).
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $connectSrc = 'self'; public string | array $connectSrc = 'self';
/** /**
* Specifies the origins that can serve web fonts. * Specifies the origins that can serve web fonts.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $fontSrc; public string | array $fontSrc;
/** /**
* Lists valid endpoints for submission from `<form>` tags. * Lists valid endpoints for submission from `<form>` tags.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $formAction = 'self'; public string | array $formAction = 'self';
@ -142,67 +102,47 @@ class ContentSecurityPolicy extends BaseConfig
* `<embed>`, and `<applet>` tags. This directive can't be used in `<meta>` tags and applies only to non-HTML * `<embed>`, and `<applet>` tags. This directive can't be used in `<meta>` tags and applies only to non-HTML
* resources. * resources.
* *
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $frameAncestors = null; public string | array | null $frameAncestors = null;
/** /**
* The frame-src directive restricts the URLs which may be loaded into nested browsing contexts. * The frame-src directive restricts the URLs which may be loaded into nested browsing contexts.
* *
* @var list<string>|string|null * @var string[]|string|null
*/ */
public string | array | null $frameSrc = null; public string | array | null $frameSrc = null;
/** /**
* Restricts the origins allowed to deliver video and audio. * Restricts the origins allowed to deliver video and audio.
* *
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $mediaSrc = null; public string | array | null $mediaSrc = null;
/** /**
* Allows control over Flash and other plugins. * Allows control over Flash and other plugins.
* *
* @var list<string>|string * @var string|string[]
*/ */
public string | array $objectSrc = 'self'; public string | array $objectSrc = 'self';
/** /**
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $manifestSrc = null; public string | array | null $manifestSrc = null;
/**
* @var list<string>|string
*/
public array|string $workerSrc = [];
/** /**
* Limits the kinds of plugins a page may invoke. * Limits the kinds of plugins a page may invoke.
* *
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $pluginTypes = null; public string | array | null $pluginTypes = null;
/** /**
* List of actions allowed. * List of actions allowed.
* *
* @var list<string>|string|null * @var string|string[]|null
*/ */
public string | array | null $sandbox = null; public string | array | null $sandbox = null;
/**
* Nonce placeholder for style tags.
*/
public string $styleNonceTag = '{csp-style-nonce}';
/**
* Nonce placeholder for script tags.
*/
public string $scriptNonceTag = '{csp-script-nonce}';
/**
* Replace nonce tag automatically?
*/
public bool $autoNonce = true;
} }

View file

@ -84,8 +84,6 @@ class Cookie extends BaseConfig
* Defaults to `Lax` for compatibility with modern browsers. Setting `''` * Defaults to `Lax` for compatibility with modern browsers. Setting `''`
* (empty string) means default SameSite attribute set by browsers (`Lax`) * (empty string) means default SameSite attribute set by browsers (`Lax`)
* will be set on cookies. If set to `None`, `$secure` must also be set. * will be set on cookies. If set to `None`, `$secure` must also be set.
*
* @var ''|'Lax'|'None'|'Strict'
*/ */
public string $samesite = 'Lax'; public string $samesite = 'Lax';

View file

@ -1,107 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Cross-Origin Resource Sharing (CORS) Configuration
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
*/
class Cors extends BaseConfig
{
/**
* The default CORS configuration.
*
* @var array{
* allowedOrigins: list<string>,
* allowedOriginsPatterns: list<string>,
* supportsCredentials: bool,
* allowedHeaders: list<string>,
* exposedHeaders: list<string>,
* allowedMethods: list<string>,
* maxAge: int,
* }
*/
public array $default = [
/**
* Origins for the `Access-Control-Allow-Origin` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
*
* E.g.:
* - ['http://localhost:8080']
* - ['https://www.example.com']
*/
'allowedOrigins' => [],
/**
* Origin regex patterns for the `Access-Control-Allow-Origin` header.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
*
* NOTE: A pattern specified here is part of a regular expression. It will
* be actually `#\A<pattern>\z#`.
*
* E.g.:
* - ['https://\w+\.example\.com']
*/
'allowedOriginsPatterns' => [],
/**
* Weather to send the `Access-Control-Allow-Credentials` header.
*
* The Access-Control-Allow-Credentials response header tells browsers whether
* the server allows cross-origin HTTP requests to include credentials.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
*/
'supportsCredentials' => false,
/**
* Set headers to allow.
*
* The Access-Control-Allow-Headers response header is used in response to
* a preflight request which includes the Access-Control-Request-Headers to
* indicate which HTTP headers can be used during the actual request.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
*/
'allowedHeaders' => [],
/**
* Set headers to expose.
*
* The Access-Control-Expose-Headers response header allows a server to
* indicate which response headers should be made available to scripts running
* in the browser, in response to a cross-origin request.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
*/
'exposedHeaders' => [],
/**
* Set methods to allow.
*
* The Access-Control-Allow-Methods response header specifies one or more
* methods allowed when accessing a resource in response to a preflight
* request.
*
* E.g.:
* - ['GET', 'POST', 'PUT', 'DELETE']
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
*/
'allowedMethods' => [],
/**
* Set how many seconds the results of a preflight request can be cached.
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
*/
'maxAge' => 7200,
];
}

View file

@ -24,67 +24,53 @@ class Database extends Config
/** /**
* The default database connection. * The default database connection.
* *
* @var array<string, mixed> * @var array<string, string|bool|int|array>
*/ */
public array $default = [ public array $default = [
'DSN' => '', 'DSN' => '',
'hostname' => 'localhost', 'hostname' => 'localhost',
'username' => '', 'username' => '',
'password' => '', 'password' => '',
'database' => '', 'database' => '',
'DBDriver' => 'MySQLi', 'DBDriver' => 'MySQLi',
'DBPrefix' => 'cp_', 'DBPrefix' => 'cp_',
'pConnect' => false, 'pConnect' => false,
'DBDebug' => true, 'DBDebug' => ENVIRONMENT !== 'production',
'charset' => 'utf8mb4', 'charset' => 'utf8mb4',
'DBCollat' => 'utf8mb4_unicode_ci', 'DBCollat' => 'utf8mb4_unicode_ci',
'swapPre' => '', 'swapPre' => '',
'encrypt' => false, 'encrypt' => false,
'compress' => false, 'compress' => false,
'strictOn' => false, 'strictOn' => false,
'failover' => [], 'failover' => [],
'port' => 3306, 'port' => 3306,
'numberNative' => false,
'foundRows' => false,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
]; ];
/** /**
* This database connection is used when running PHPUnit database tests. * This database connection is used when running PHPUnit database tests.
* *
* @var array<string, mixed> * @noRector StringClassNameToClassConstantRector
*
* @var array<string, string|bool|int|array>
*/ */
public array $tests = [ public array $tests = [
'DSN' => '', 'DSN' => '',
'hostname' => '127.0.0.1', 'hostname' => '127.0.0.1',
'username' => '', 'username' => '',
'password' => '', 'password' => '',
'database' => ':memory:', 'database' => ':memory:',
'DBDriver' => 'SQLite3', 'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_', 'DBPrefix' => 'db_',
// Needed to ensure we're working correctly with prefixes live. DO NOT REMOVE FOR CI DEVS 'pConnect' => false,
'pConnect' => false, 'DBDebug' => ENVIRONMENT !== 'production',
'DBDebug' => true, 'charset' => 'utf8',
'charset' => 'utf8', 'DBCollat' => 'utf8_general_ci',
'DBCollat' => '', 'swapPre' => '',
'swapPre' => '', 'encrypt' => false,
'encrypt' => false, 'compress' => false,
'compress' => false, 'strictOn' => false,
'strictOn' => false, 'failover' => [],
'failover' => [], 'port' => 3306,
'port' => 3306,
'foreignKeys' => true,
'busyTimeout' => 1000,
'synchronous' => null,
'dateFormat' => [
'date' => 'Y-m-d',
'datetime' => 'Y-m-d H:i:s',
'time' => 'H:i:s',
],
]; ];
//-------------------------------------------------------------------- //--------------------------------------------------------------------

View file

@ -12,34 +12,42 @@ class DocTypes
* @var array<string, string> * @var array<string, string>
*/ */
public array $list = [ public array $list = [
'xhtml11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">', 'xhtml11' =>
'xhtml1-strict' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
'xhtml1-trans' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">', 'xhtml1-strict' =>
'xhtml1-frame' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
'xhtml-basic11' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">', 'xhtml1-trans' =>
'html5' => '<!DOCTYPE html>', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
'html4-strict' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">', 'xhtml1-frame' =>
'html4-trans' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
'html4-frame' => '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">', 'xhtml-basic11' =>
'mathml1' => '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">', '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
'mathml2' => '<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">', 'html5' => '<!DOCTYPE html>',
'svg10' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">', 'html4-strict' =>
'svg11' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">',
'svg11-basic' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">', 'html4-trans' =>
'svg11-tiny' => '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">',
'xhtml-math-svg-xh' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">', 'html4-frame' =>
'xhtml-math-svg-sh' => '<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">', '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">',
'xhtml-rdfa-1' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">', 'mathml1' =>
'xhtml-rdfa-2' => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">', '<!DOCTYPE math SYSTEM "http://www.w3.org/Math/DTD/mathml1/mathml.dtd">',
'mathml2' =>
'<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 2.0//EN" "http://www.w3.org/Math/DTD/mathml2/mathml2.dtd">',
'svg10' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">',
'svg11' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
'svg11-basic' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Basic//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd">',
'svg11-tiny' =>
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">',
'xhtml-math-svg-xh' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-math-svg-sh' =>
'<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD XHTML 1.1 plus MathML 2.0 plus SVG 1.1//EN" "http://www.w3.org/2002/04/xhtml-math-svg/xhtml-math-svg.dtd">',
'xhtml-rdfa-1' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.0//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-1.dtd">',
'xhtml-rdfa-2' =>
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML+RDFa 1.1//EN" "http://www.w3.org/MarkUp/DTD/xhtml-rdfa-2.dtd">',
]; ];
/**
* Whether to remove the solidus (`/`) character for void HTML elements (e.g. `<input>`)
* for HTML5 compatibility.
*
* Set to:
* `true` - to be HTML5 compatible
* `false` - to be XHTML compatible
*/
public bool $html5 = true;
} }

View file

@ -8,21 +8,21 @@ use CodeIgniter\Config\BaseConfig;
class Email extends BaseConfig class Email extends BaseConfig
{ {
public string $fromEmail = ''; public string $fromEmail;
public string $fromName = 'Castopod'; public string $fromName;
public string $recipients = ''; public string $recipients;
/** /**
* The "user agent" * The "user agent"
*/ */
public string $userAgent = 'Castopod/' . CP_VERSION; public string $userAgent = 'CodeIgniter';
/** /**
* The mail sending protocol: mail, sendmail, smtp * The mail sending protocol: mail, sendmail, smtp
*/ */
public string $protocol = 'smtp'; public string $protocol = 'mail';
/** /**
* The server path to Sendmail. * The server path to Sendmail.
@ -30,24 +30,19 @@ class Email extends BaseConfig
public string $mailPath = '/usr/sbin/sendmail'; public string $mailPath = '/usr/sbin/sendmail';
/** /**
* SMTP Server Hostname * SMTP Server Address
*/ */
public string $SMTPHost = ''; public string $SMTPHost;
/**
* Which SMTP authentication method to use: login, plain
*/
public string $SMTPAuthMethod = 'login';
/** /**
* SMTP Username * SMTP Username
*/ */
public string $SMTPUser = ''; public string $SMTPUser;
/** /**
* SMTP Password * SMTP Password
*/ */
public string $SMTPPass = ''; public string $SMTPPass;
/** /**
* SMTP Port * SMTP Port
@ -65,11 +60,7 @@ class Email extends BaseConfig
public bool $SMTPKeepAlive = false; public bool $SMTPKeepAlive = false;
/** /**
* SMTP Encryption. * SMTP Encryption. Either tls or ssl
*
* @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command
* to the server. 'ssl' means implicit SSL. Connection on port
* 465 should set this to ''.
*/ */
public string $SMTPCrypto = 'tls'; public string $SMTPCrypto = 'tls';
@ -86,7 +77,7 @@ class Email extends BaseConfig
/** /**
* Type of mail, either 'text' or 'html' * Type of mail, either 'text' or 'html'
*/ */
public string $mailType = 'html'; public string $mailType = 'text';
/** /**
* Character set (utf-8, iso-8859-1, etc.) * Character set (utf-8, iso-8859-1, etc.)
@ -127,11 +118,4 @@ class Email extends BaseConfig
* Enable notify message from server * Enable notify message from server
*/ */
public bool $DSN = false; public bool $DSN = false;
public function __construct()
{
parent::__construct();
$this->userAgent = 'Castopod/' . CP_VERSION . '; +' . base_url('', 'https');
}
} }

View file

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
class Embed extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Embeddable player config
* --------------------------------------------------------------------------
*/
public int $width = 485;
public int $height = 112;
}

View file

@ -25,23 +25,6 @@ class Encryption extends BaseConfig
*/ */
public string $key = ''; public string $key = '';
/**
* --------------------------------------------------------------------------
* Previous Encryption Keys
* --------------------------------------------------------------------------
*
* When rotating encryption keys, add old keys here to maintain ability
* to decrypt data encrypted with previous keys. Encryption always uses
* the current $key. Decryption tries current key first, then falls back
* to previous keys if decryption fails.
*
* In .env file, use comma-separated string:
* encryption.previousKeys = hex2bin:9be8c64fcea509867...,hex2bin:3f5a1d8e9c2b7a4f6...
*
* @var list<string>|string
*/
public array|string $previousKeys = '';
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Encryption Driver to Use * Encryption Driver to Use
@ -75,37 +58,4 @@ class Encryption extends BaseConfig
* HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'. * HMAC digest to use, e.g. 'SHA512' or 'SHA256'. Default value is 'SHA512'.
*/ */
public string $digest = 'SHA512'; public string $digest = 'SHA512';
/**
* Whether the cipher-text should be raw. If set to false, then it will be base64 encoded.
* This setting is only used by OpenSSLHandler.
*
* Set to false for CI3 Encryption compatibility.
*/
public bool $rawData = true;
/**
* Encryption key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'encryption' for CI3 Encryption compatibility.
*/
public string $encryptKeyInfo = '';
/**
* Authentication key info.
* This setting is only used by OpenSSLHandler.
*
* Set to 'authentication' for CI3 Encryption compatibility.
*/
public string $authKeyInfo = '';
/**
* Cipher to use.
* This setting is only used by OpenSSLHandler.
*
* Set to 'AES-128-CBC' to decrypt encrypted data that encrypted
* by CI3 Encryption default configuration.
*/
public string $cipher = 'AES-256-CTR';
} }

View file

@ -5,12 +5,10 @@ declare(strict_types=1);
namespace Config; namespace Config;
use App\Entities\Actor; use App\Entities\Actor;
use App\Entities\Post; use App\Entities\Status;
use App\Models\EpisodeModel; use App\Entities\User;
use CodeIgniter\Debug\Toolbar\Collectors\Database;
use CodeIgniter\Events\Events; use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\FrameworkException; use CodeIgniter\Exceptions\FrameworkException;
use CodeIgniter\HotReloader\HotReloader;
/* /*
* -------------------------------------------------------------------- * --------------------------------------------------------------------
@ -29,7 +27,8 @@ use CodeIgniter\HotReloader\HotReloader;
* Events::on('create', [$myInstance, 'myMethod']); * Events::on('create', [$myInstance, 'myMethod']);
*/ */
Events::on('pre_system', static function (): void { Events::on('pre_system', function () {
// @phpstan-ignore-next-line
if (ENVIRONMENT !== 'testing') { if (ENVIRONMENT !== 'testing') {
if (ini_get('zlib.output_compression')) { if (ini_get('zlib.output_compression')) {
throw FrameworkException::forEnabledZlibOutputCompression(); throw FrameworkException::forEnabledZlibOutputCompression();
@ -39,7 +38,9 @@ Events::on('pre_system', static function (): void {
ob_end_flush(); ob_end_flush();
} }
ob_start(static fn ($buffer) => $buffer); ob_start(function ($buffer) {
return $buffer;
});
} }
/* /*
@ -47,32 +48,42 @@ Events::on('pre_system', static function (): void {
* Debug Toolbar Listeners. * Debug Toolbar Listeners.
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* If you delete, they will no longer be collected. * If you delete, they will no longer be collected.
*
* @phpstan-ignore-next-line
*/ */
if (CI_DEBUG && ! is_cli()) { if (CI_DEBUG && ! is_cli()) {
Events::on('DBQuery', Database::class . '::collect'); Events::on('DBQuery', 'CodeIgniter\Debug\Toolbar\Collectors\Database::collect');
service('toolbar') Services::toolbar()->respond();
->respond();
// Hot Reload route - for framework use on the hot reloader.
if (ENVIRONMENT === 'development') {
service('routes')->get('__hot-reload', static function (): void {
new HotReloader()
->run();
});
}
} }
}); });
Events::on('login', function (User $user): void {
helper('auth');
// set interact_as_actor_id value
$userPodcasts = $user->podcasts;
if ($userPodcasts = $user->podcasts) {
set_interact_as_actor($userPodcasts[0]->actor_id);
}
});
Events::on('logout', function (User $user): void {
helper('auth');
// remove user's interact_as_actor session
remove_interact_as_actor();
});
/* /*
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* Fediverse events * ActivityPub events
* -------------------------------------------------------------------- * --------------------------------------------------------------------
*/ */
/** /**
* @param Actor $actor * @param Actor $actor
* @param Actor $targetActor * @param Actor $targetActor
*/ */
Events::on('on_follow', static function ($actor, $targetActor): void { Events::on('on_follow', function ($actor, $targetActor): void {
if ($actor->is_podcast) { if ($actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$actor->podcast->id}*"); ->deleteMatching("podcast#{$actor->podcast->id}*");
@ -92,7 +103,7 @@ Events::on('on_follow', static function ($actor, $targetActor): void {
* @param Actor $actor * @param Actor $actor
* @param Actor $targetActor * @param Actor $targetActor
*/ */
Events::on('on_undo_follow', static function ($actor, $targetActor): void { Events::on('on_undo_follow', function ($actor, $targetActor): void {
if ($actor->is_podcast) { if ($actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$actor->podcast->id}*"); ->deleteMatching("podcast#{$actor->podcast->id}*");
@ -109,53 +120,82 @@ Events::on('on_undo_follow', static function ($actor, $targetActor): void {
}); });
/** /**
* @param Post $post * @param Status $status
*/ */
Events::on('on_post_add', static function ($post): void { Events::on('on_status_add', function ($status): void {
model(EpisodeModel::class, false)->builder() if ($status->in_reply_to_id !== null) {
->where('id', $post->episode_id) $status = $status->reply_to_status;
->increment('posts_count'); }
if ($post->actor->is_podcast) {
if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
// Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs // Removing all of the podcast pages is a bit overkill, but works to avoid caching bugs
// same for other events below // same for other events below
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
}); });
/** /**
* @param Post $post * @param Status $status
*/ */
Events::on('on_post_remove', static function ($post): void { Events::on('on_status_remove', function ($status): void {
if ($episodeId = $post->episode_id) { if ($status->in_reply_to_id !== null) {
model(EpisodeModel::class, false)->builder() Events::trigger('on_status_remove', $status->reply_to_status);
}
if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId) ->where('id', $episodeId)
->decrement('posts_count'); ->decrement('statuses_total', 1 + $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total', $status->reblogs_count);
model('EpisodeModel')
->where('id', $episodeId)
->decrement('favourites_total', $status->favourites_count);
} }
if ($post->actor->is_podcast) { if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
}); });
/** /**
* @param Actor $actor * @param Actor $actor
* @param Post $post * @param Status $status
*/ */
Events::on('on_post_reblog', static function ($actor, $post): void { Events::on('on_status_reblog', function ($actor, $status): void {
if ($post->actor->is_podcast) { if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->increment('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->increment('statuses_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -165,106 +205,111 @@ Events::on('on_post_reblog', static function ($actor, $post): void {
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}"); if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
} }
}); });
/** /**
* @param Post $reblogPost * @param Status $reblogStatus
*/ */
Events::on('on_post_undo_reblog', static function ($reblogPost): void { Events::on('on_status_undo_reblog', function ($reblogStatus): void {
$post = $reblogPost->reblog_of_post; $status = $reblogStatus->reblog_of_status;
if ($post->actor->is_podcast) { if ($episodeId = $status->episode_id) {
model('EpisodeModel')
->where('id', $episodeId)
->decrement('reblogs_total');
model('EpisodeModel')
->where('id', $episodeId)
->decrement('statuses_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
cache() cache()
->deleteMatching("page_post#{$reblogPost->id}*"); ->deleteMatching("page_status#{$reblogStatus->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}"); if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}");
} }
if ($reblogPost->actor->is_podcast) { if ($reblogStatus->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$reblogPost->actor->podcast->id}*"); ->deleteMatching("podcast#{$reblogStatus->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$reblogPost->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$reblogStatus->actor->podcast->id}*");
} }
}); });
/** /**
* @param Post $reply * @param Status $reply
*/ */
Events::on('on_post_reply', static function ($reply): void { Events::on('on_status_reply', function ($reply): void {
$post = $reply->reply_to_post; $status = $reply->reply_to_status;
if ($post->in_reply_to_id === null) {
model(EpisodeModel::class, false)->builder()
->where('id', $post->episode_id)
->increment('comments_count');
}
if ($post->actor->is_podcast) { if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast-{$post->actor->podcast->handle}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
}); });
/** /**
* @param Post $reply * @param Status $reply
*/ */
Events::on('on_reply_remove', static function ($reply): void { Events::on('on_reply_remove', function ($reply): void {
$post = $reply->reply_to_post; $status = $reply->reply_to_status;
if ($post->in_reply_to_id === null) {
model(EpisodeModel::class, false)->builder()
->where('id', $post->episode_id)
->decrement('comments_count');
}
if ($post->actor->is_podcast) { if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast-{$post->actor->podcast->handle}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
cache() cache()
->deleteMatching("page_post#{$reply->id}*"); ->deleteMatching("page_status#{$reply->id}*");
}); });
/** /**
* @param Actor $actor * @param Actor $actor
* @param Post $post * @param Status $status
*/ */
Events::on('on_post_favourite', static function ($actor, $post): void { Events::on('on_status_favourite', function ($actor, $status): void {
if ($post->actor->is_podcast) { if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->increment('favourites_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*"); if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -276,20 +321,27 @@ Events::on('on_post_favourite', static function ($actor, $post): void {
/** /**
* @param Actor $actor * @param Actor $actor
* @param Post $post * @param Status $status
*/ */
Events::on('on_post_undo_favourite', static function ($actor, $post): void { Events::on('on_status_undo_favourite', function ($actor, $status): void {
if ($post->actor->is_podcast) { if ($status->episode_id) {
model('EpisodeModel')
->where('id', $status->episode_id)
->decrement('favourites_total');
}
if ($status->actor->is_podcast) {
cache() cache()
->deleteMatching("podcast#{$post->actor->podcast->id}*"); ->deleteMatching("podcast#{$status->actor->podcast->id}*");
cache() cache()
->deleteMatching("page_podcast#{$post->actor->podcast->id}*"); ->deleteMatching("page_podcast#{$status->actor->podcast->id}*");
} }
cache() cache()
->deleteMatching("page_post#{$post->id}*"); ->deleteMatching("page_status#{$status->id}*");
if ($post->in_reply_to_id !== null) {
cache()->deleteMatching("page_post#{$post->in_reply_to_id}*"); if ($status->in_reply_to_id !== null) {
cache()->deleteMatching("page_status#{$status->in_reply_to_id}*");
} }
if ($actor->is_podcast) { if ($actor->is_podcast) {
@ -299,34 +351,34 @@ Events::on('on_post_undo_favourite', static function ($actor, $post): void {
} }
}); });
Events::on('on_block_actor', static function (int $actorId): void { Events::on('on_block_actor', function (int $actorId): void {
cache()->deleteMatching('page_podcast*'); cache()->deleteMatching('page_podcast*');
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_post*'); ->deleteMatching('page_status*');
}); });
Events::on('on_unblock_actor', static function (int $actorId): void { Events::on('on_unblock_actor', function (int $actorId): void {
cache()->deleteMatching('page_podcast*'); cache()->deleteMatching('page_podcast*');
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_post*'); ->deleteMatching('page_status*');
}); });
Events::on('on_block_domain', static function (string $domainName): void { Events::on('on_block_domain', function (string $domainName): void {
cache()->deleteMatching('page_podcast*'); cache()->deleteMatching('page_podcast*');
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_post*'); ->deleteMatching('page_status*');
}); });
Events::on('on_unblock_domain', static function (string $domainName): void { Events::on('on_unblock_domain', function (string $domainName): void {
cache()->deleteMatching('page_podcast*'); cache()->deleteMatching('page_podcast*');
cache() cache()
->deleteMatching('podcast*'); ->deleteMatching('podcast*');
cache() cache()
->deleteMatching('page_post*'); ->deleteMatching('page_status*');
}); });

View file

@ -5,10 +5,6 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\ExceptionHandler;
use CodeIgniter\Debug\ExceptionHandlerInterface;
use Psr\Log\LogLevel;
use Throwable;
/** /**
* Setup how the exception handler works. * Setup how the exception handler works.
@ -33,7 +29,7 @@ class Exceptions extends BaseConfig
* Any status codes here will NOT be logged if logging is turned on. * Any status codes here will NOT be logged if logging is turned on.
* By default, only 404 (Page Not Found) exceptions are ignored. * By default, only 404 (Page Not Found) exceptions are ignored.
* *
* @var list<int> * @var int[]
*/ */
public array $ignoreCodes = [404]; public array $ignoreCodes = [404];
@ -56,53 +52,7 @@ class Exceptions extends BaseConfig
* In order to specify 2 levels, use "/" to separate. * In order to specify 2 levels, use "/" to separate.
* ex. ['server', 'setup/password', 'secret_token'] * ex. ['server', 'setup/password', 'secret_token']
* *
* @var list<string> * @var string[]
*/ */
public array $sensitiveDataInTrace = []; public array $sensitiveDataInTrace = [];
/**
* --------------------------------------------------------------------------
* WHETHER TO THROW AN EXCEPTION ON DEPRECATED ERRORS
* --------------------------------------------------------------------------
* If set to `true`, DEPRECATED errors are only logged and no exceptions are
* thrown. This option also works for user deprecations.
*/
public bool $logDeprecations = true;
/**
* --------------------------------------------------------------------------
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
* --------------------------------------------------------------------------
* If `$logDeprecations` is set to `true`, this sets the log level
* to which the deprecation will be logged. This should be one of the log
* levels recognized by PSR-3.
*
* The related `Config\Logger::$threshold` should be adjusted, if needed,
* to capture logging the deprecations.
*/
public string $deprecationLogLevel = LogLevel::WARNING;
/*
* DEFINE THE HANDLERS USED
* --------------------------------------------------------------------------
* Given the HTTP status code, returns exception handler that
* should be used to deal with this error. By default, it will run CodeIgniter's
* default handler and display the error information in the expected format
* for CLI, HTTP, or AJAX requests, as determined by is_cli() and the expected
* response format.
*
* Custom handlers can be returned if you want to handle one or more specific
* error codes yourself like:
*
* if (in_array($statusCode, [400, 404, 500])) {
* return new \App\Libraries\MyExceptionHandler();
* }
* if ($exception instanceOf PageNotFoundException) {
* return new \App\Libraries\MyExceptionHandler();
* }
*/
public function handler(int $statusCode, Throwable $exception): ExceptionHandlerInterface
{
return new ExceptionHandler($this);
}
} }

View file

@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
/**
* Enable/disable backward compatibility breaking features.
*/
class Feature extends BaseConfig
{
/**
* Use improved new auto routing instead of the legacy version.
*/
public bool $autoRoutesImproved = true;
/**
* Use filter execution order in 4.4 or before.
*/
public bool $oldFilterOrder = false;
/**
* The behavior of `limit(0)` in Query Builder.
*
* If true, `limit(0)` returns all records. (the behavior of 4.4.x or before in version 4.x.)
* If false, `limit(0)` returns no records. (the behavior of 3.1.9 or later in version 3.x.)
*/
public bool $limitZeroAsAll = true;
/**
* Use strict location negotiation.
*
* By default, the locale is selected based on a loose comparison of the language code (ISO 639-1)
* Enabling strict comparison will also consider the region code (ISO 3166-1 alpha-2).
*/
public bool $strictLocaleNegotiation = false;
}

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2022 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Config;
use App\Libraries\NoteObject;
use Exception;
use Modules\Fediverse\Config\Fediverse as FediverseBaseConfig;
class Fediverse extends FediverseBaseConfig
{
/**
* --------------------------------------------------------------------
* ActivityPub Objects
* --------------------------------------------------------------------
*/
public string $noteObject = NoteObject::class;
public string $defaultAvatarImagePath = 'castopod-avatar_thumbnail.webp';
public string $defaultAvatarImageMimetype = 'image/webp';
public function __construct()
{
parent::__construct();
try {
$appTheme = service('settings')
->get('App.theme');
$defaultBanner = config('Images')
->podcastBannerDefaultPaths[$appTheme] ?? config('Images')->podcastBannerDefaultPaths['default'];
} catch (Exception) {
$defaultBanner = config('Images')
->podcastBannerDefaultPaths['default'];
}
['dirname' => $dirname, 'extension' => $extension, 'filename' => $filename] = pathinfo(
$defaultBanner['path'],
);
$defaultBannerPath = $filename;
if ($dirname !== '.') {
$defaultBannerPathList = [$dirname, $filename];
$defaultBannerPath = implode('/', $defaultBannerPathList);
}
helper('media');
$this->defaultCoverImagePath = $defaultBannerPath . '_federation.' . $extension;
$this->defaultCoverImageMimetype = $defaultBanner['mimetype'];
}
}

View file

@ -4,101 +4,54 @@ declare(strict_types=1);
namespace Config; namespace Config;
use App\Filters\AllowCorsFilter; use ActivityPub\Filters\ActivityPubFilter;
use App\Filters\PermissionFilter;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Filters\CSRF; use CodeIgniter\Filters\CSRF;
use CodeIgniter\Filters\DebugToolbar; use CodeIgniter\Filters\DebugToolbar;
use CodeIgniter\Filters\ForceHTTPS;
use CodeIgniter\Filters\Honeypot; use CodeIgniter\Filters\Honeypot;
use CodeIgniter\Filters\InvalidChars; use Myth\Auth\Filters\LoginFilter;
use CodeIgniter\Filters\PageCache; use Myth\Auth\Filters\RoleFilter;
use CodeIgniter\Filters\PerformanceMetrics;
use CodeIgniter\Filters\SecureHeaders;
use Modules\Auth\Filters\PermissionFilter;
class Filters extends BaseConfig class Filters extends BaseConfig
{ {
/** /**
* Configures aliases for Filter classes to make reading things nicer and simpler. * Configures aliases for Filter classes to make reading things nicer and simpler.
* *
* @var array<string, class-string|list<class-string>> * @var array<string, string>
*
* [filter_name => classname]
* or [filter_name => [classname1, classname2, ...]]
*/ */
public array $aliases = [ public array $aliases = [
'csrf' => CSRF::class, 'csrf' => CSRF::class,
'toolbar' => DebugToolbar::class, 'toolbar' => DebugToolbar::class,
'honeypot' => Honeypot::class, 'honeypot' => Honeypot::class,
'invalidchars' => InvalidChars::class, 'login' => LoginFilter::class,
'secureheaders' => SecureHeaders::class, 'role' => RoleFilter::class,
'allow-cors' => AllowCorsFilter::class, 'permission' => PermissionFilter::class,
'cors' => Cors::class, 'activity-pub' => ActivityPubFilter::class,
'forcehttps' => ForceHTTPS::class,
'pagecache' => PageCache::class,
'performance' => PerformanceMetrics::class,
];
/**
* List of special required filters.
*
* The filters listed here are special. They are applied before and after
* other kinds of filters, and always applied even if a route does not exist.
*
* Filters set by default provide framework functionality. If removed,
* those functions will no longer work.
*
* @see https://codeigniter.com/user_guide/incoming/filters.html#provided-filters
*
* @var array{before: list<string>, after: list<string>}
*/
public array $required = [
'before' => [
'forcehttps', // Force Global Secure Requests
'pagecache', // Web Page Caching
],
'after' => [
'pagecache', // Web Page Caching
'performance', // Performance Metrics
'toolbar', // Debug Toolbar
],
]; ];
/** /**
* List of filter aliases that are always applied before and after every request. * List of filter aliases that are always applied before and after every request.
* *
* @var array{ * @var array<string, string[]>
* before: array<string, array{except: list<string>|string}>|list<string>,
* after: array<string, array{except: list<string>|string}>|list<string>
* }
*/ */
public array $globals = [ public array $globals = [
'before' => [ 'before' => [
// 'honeypot', // 'honeypot',
'csrf' => [ // 'csrf',
'except' => [
'@[a-zA-Z0-9\_]{1,32}/inbox',
'api/rest/v1/episodes',
'api/rest/v1/episodes/[0-9]+/publish',
],
],
// 'invalidchars',
], ],
'after' => [ 'after' => [
'toolbar',
// 'honeypot', // 'honeypot',
// 'secureheaders',
], ],
]; ];
/** /**
* List of filter aliases that works on a particular HTTP method (GET, POST, etc.). * List of filter aliases that works on a particular HTTP method (GET, POST, etc.).
* *
* Example: 'POST' => ['foo', 'bar'] * Example: 'post' => ['csrf', 'throttle']
* *
* If you use this, you should disable auto-routing because auto-routing permits any HTTP method to access a * @var array<string, string[]>
* controller. Accessing the controller with a method you dont expect could bypass the filter.
*
* @var array<string, list<string>>
*/ */
public array $methods = []; public array $methods = [];
@ -107,7 +60,7 @@ class Filters extends BaseConfig
* *
* Example: 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']] * Example: 'isLoggedIn' => ['before' => ['account/*', 'profiles/*']]
* *
* @var array<string, array<string, list<string>>> * @var array<string, array<string, string[]>>
*/ */
public array $filters = []; public array $filters = [];
@ -116,14 +69,9 @@ class Filters extends BaseConfig
parent::__construct(); parent::__construct();
$this->filters = [ $this->filters = [
'session' => [ 'login' => [
'before' => [config('Admin')->gateway . '*', config('Analytics')->gateway . '*'], 'before' => [config('App')->adminGateway . '*'],
],
'podcast-unlock' => [
'before' => ['*@*/episodes/*'],
], ],
]; ];
$this->aliases['permission'] = PermissionFilter::class;
} }
} }

View file

@ -6,9 +6,6 @@ namespace Config;
use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters; use CodeIgniter\Config\ForeignCharacters as BaseForeignCharacters;
/**
* @immutable
*/
class ForeignCharacters extends BaseForeignCharacters class ForeignCharacters extends BaseForeignCharacters
{ {
} }

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Format\FormatterInterface;
use CodeIgniter\Format\JSONFormatter; use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter; use CodeIgniter\Format\XMLFormatter;
@ -23,7 +24,7 @@ class Format extends BaseConfig
* These formats are only checked when the data passed to the respond() * These formats are only checked when the data passed to the respond()
* method is an array. * method is an array.
* *
* @var list<string> * @var string[]
*/ */
public array $supportedResponseFormats = [ public array $supportedResponseFormats = [
'application/json', 'application/json',
@ -44,8 +45,8 @@ class Format extends BaseConfig
*/ */
public array $formatters = [ public array $formatters = [
'application/json' => JSONFormatter::class, 'application/json' => JSONFormatter::class,
'application/xml' => XMLFormatter::class, 'application/xml' => XMLFormatter::class,
'text/xml' => XMLFormatter::class, 'text/xml' => XMLFormatter::class,
]; ];
/** /**
@ -60,16 +61,19 @@ class Format extends BaseConfig
*/ */
public array $formatterOptions = [ public array $formatterOptions = [
'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES, 'application/json' => JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
'application/xml' => 0, 'application/xml' => 0,
'text/xml' => 0, 'text/xml' => 0,
]; ];
//--------------------------------------------------------------------
/** /**
* -------------------------------------------------------------------------- * A Factory method to return the appropriate formatter for the given mime type.
* Maximum depth for JSON encoding.
* --------------------------------------------------------------------------
* *
* This value determines how deep the JSON encoder will traverse nested structures. * @deprecated This is an alias of `\CodeIgniter\Format\Format::getFormatter`. Use that instead.
*/ */
public int $jsonEncodeDepth = 512; public function getFormatter(string $mime): FormatterInterface
{
return Services::format()->getFormatter($mime);
}
} }

View file

@ -25,22 +25,22 @@ class Generators extends BaseConfig
* *
* YOU HAVE BEEN WARNED! * YOU HAVE BEEN WARNED!
* *
* @var array<string, string|array<string,string>> * @var array<string, string>
*/ */
public array $views = [ public array $views = [
'make:cell' => [ 'make:command' =>
'class' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', 'make:controller' =>
], 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', 'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', 'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', 'make:migration' =>
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php', 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php', 'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php', 'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php', 'make:validation' =>
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php', 'CodeIgniter\Commands\Generators\Views\validation.tpl.php',
'make:validation' => 'CodeIgniter\Commands\Generators\Views\validation.tpl.php', 'session:migration' =>
'session:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php', 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
]; ];
} }

View file

@ -30,15 +30,6 @@ class Honeypot extends BaseConfig
/** /**
* Honeypot container * Honeypot container
*
* If you enabled CSP, you can remove `style="display:none"`.
*/ */
public string $container = '<div style="display:none">{template}</div>'; public string $container = '<div style="display:none">{template}</div>';
/**
* The id attribute for Honeypot container tag
*
* Used when CSP is enabled.
*/
public string $containerId = 'hpc';
} }

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
class Hostnames
{
// List of known two-part TLDs for subdomain extraction
public const TWO_PART_TLDS = [
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'sch.uk', 'ltd.uk', 'plc.uk',
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
'co.jp', 'ac.jp', 'go.jp', 'or.jp', 'ne.jp', 'gr.jp',
'co.nz', 'org.nz', 'govt.nz', 'ac.nz', 'net.nz', 'geek.nz', 'maori.nz', 'school.nz',
'co.in', 'net.in', 'org.in', 'ind.in', 'ac.in', 'gov.in', 'res.in',
'com.cn', 'net.cn', 'org.cn', 'gov.cn', 'edu.cn',
'com.sg', 'net.sg', 'org.sg', 'gov.sg', 'edu.sg', 'per.sg',
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
'co.kr', 'or.kr', 'go.kr', 'ac.kr', 'ne.kr', 'pe.kr',
'co.th', 'or.th', 'go.th', 'ac.th', 'net.th', 'in.th',
'com.my', 'net.my', 'org.my', 'edu.my', 'gov.my', 'mil.my', 'name.my',
'com.mx', 'org.mx', 'net.mx', 'edu.mx', 'gob.mx',
'com.br', 'net.br', 'org.br', 'gov.br', 'edu.br', 'art.br', 'eng.br',
'co.il', 'org.il', 'ac.il', 'gov.il', 'net.il', 'muni.il',
'co.id', 'or.id', 'ac.id', 'go.id', 'net.id', 'web.id', 'my.id',
'com.hk', 'edu.hk', 'gov.hk', 'idv.hk', 'net.hk', 'org.hk',
'com.tw', 'net.tw', 'org.tw', 'edu.tw', 'gov.tw', 'idv.tw',
'com.sa', 'net.sa', 'org.sa', 'gov.sa', 'edu.sa', 'sch.sa', 'med.sa',
'co.ae', 'net.ae', 'org.ae', 'gov.ae', 'ac.ae', 'sch.ae',
'com.tr', 'net.tr', 'org.tr', 'gov.tr', 'edu.tr', 'av.tr', 'gen.tr',
'co.ke', 'or.ke', 'go.ke', 'ac.ke', 'sc.ke', 'me.ke', 'mobi.ke', 'info.ke',
'com.ng', 'org.ng', 'gov.ng', 'edu.ng', 'net.ng', 'sch.ng', 'name.ng',
'com.pk', 'net.pk', 'org.pk', 'gov.pk', 'edu.pk', 'fam.pk',
'com.eg', 'edu.eg', 'gov.eg', 'org.eg', 'net.eg',
'com.cy', 'net.cy', 'org.cy', 'gov.cy', 'ac.cy',
'com.lk', 'org.lk', 'edu.lk', 'gov.lk', 'net.lk', 'int.lk',
'com.bd', 'net.bd', 'org.bd', 'ac.bd', 'gov.bd', 'mil.bd',
'com.ar', 'net.ar', 'org.ar', 'gov.ar', 'edu.ar', 'mil.ar',
'gob.cl', 'com.pl', 'net.pl', 'org.pl', 'gov.pl', 'edu.pl',
'co.ir', 'ac.ir', 'org.ir', 'id.ir', 'gov.ir', 'sch.ir', 'net.ir',
];
}

View file

@ -17,8 +17,6 @@ class Images extends BaseConfig
/** /**
* The path to the image library. Required for ImageMagick, GraphicsMagick, or NetPBM. * The path to the image library. Required for ImageMagick, GraphicsMagick, or NetPBM.
*
* @deprecated 4.7.0 No longer used.
*/ */
public string $libraryPath = '/usr/local/bin/convert'; public string $libraryPath = '/usr/local/bin/convert';
@ -28,181 +26,48 @@ class Images extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $handlers = [ public array $handlers = [
'gd' => GDHandler::class, 'gd' => GDHandler::class,
'imagick' => ImageMagickHandler::class, 'imagick' => ImageMagickHandler::class,
]; ];
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Uploaded images sizes (in px) | Uploaded images resizing sizes (in px)
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| The sizes listed below determine the resizing of images when uploaded. | The sizes listed below determine the resizing of images when uploaded.
| All uploaded images are of 1:1 ratio (width and height are the same).
*/ */
/** public int $thumbnailSize = 150;
* Podcast cover image sizes
* public int $mediumSize = 320;
* Uploaded podcast covers are of 1:1 ratio (width and height are the same).
* public int $largeSize = 1024;
* Size of images linked in the rss feed (should be between 1400 and 3000). Size for ID3 tag cover art (should be
* between 300 and 800)
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, array<string, int|string>>
*/
public array $podcastCoverSizes = [
'tiny' => [
'width' => 40,
'height' => 40,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'large' => [
'width' => 1024,
'height' => 1024,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'feed' => [
'width' => 1400,
'height' => 1400,
],
'id3' => [
'width' => 500,
'height' => 500,
],
'og' => [
'width' => 1200,
'height' => 1200,
],
'federation' => [
'width' => 400,
'height' => 400,
],
'webmanifest192' => [
'width' => 192,
'height' => 192,
'mimetype' => 'image/png',
'extension' => 'png',
],
'webmanifest512' => [
'width' => 512,
'height' => 512,
'mimetype' => 'image/png',
'extension' => 'png',
],
];
/** /**
* Podcast header cover image * Size of images linked in the rss feed (should be between 1400 and 3000)
*
* Uploaded podcast header covers are of 3:1 ratio
*
* @var array<string, array<string, int|string>>
*/ */
public array $podcastBannerSizes = [ public int $feedSize = 1400;
'small' => [
'width' => 320,
'height' => 128,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 960,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'federation' => [
'width' => 1500,
'height' => 500,
],
];
public string $avatarDefaultPath = 'assets/images/castopod-avatar.jpg';
public string $avatarDefaultMimeType = 'image/jpg';
/** /**
* @var array<string, array<string, string>> * Size for ID3 tag cover art (should be between 300 and 800)
*/ */
public array $podcastBannerDefaultPaths = [ public int $id3Size = 500;
'default' => [
'path' => 'assets/images/castopod-banner-pine.jpg',
'mimetype' => 'image/jpeg',
],
'pine' => [
'path' => 'assets/images/castopod-banner-pine.jpg',
'mimetype' => 'image/jpeg',
],
'crimson' => [
'path' => 'assets/images/castopod-banner-crimson.jpg',
'mimetype' => 'image/jpeg',
],
'amber' => [
'path' => 'assets/images/castopod-banner-amber.jpg',
'mimetype' => 'image/jpeg',
],
'lake' => [
'path' => 'assets/images/castopod-banner-lake.jpg',
'mimetype' => 'image/jpeg',
],
'jacaranda' => [
'path' => 'assets/images/castopod-banner-jacaranda.jpg',
'mimetype' => 'image/jpeg',
],
'onyx' => [
'path' => 'assets/images/castopod-banner-onyx.jpg',
'mimetype' => 'image/jpeg',
],
];
public string $podcastBannerDefaultMimeType = 'image/jpeg'; /*
|--------------------------------------------------------------------------
| Uploaded images naming extensions
|--------------------------------------------------------------------------
| The properties listed below set the name extensions for the resized images
*/
/** public string $thumbnailSuffix = '_thumbnail';
* Person image
* public string $mediumSuffix = '_medium';
* Uploaded person images are of 1:1 ratio (width and height are the same).
* public string $largeSuffix = '_large';
* Array values are as follows: 'name' => [width, height]
* public string $feedSuffix = '_feed';
* @var array<string, array<string, int|string>>
*/ public string $id3Suffix = '_id3';
public array $personAvatarSizes = [
'federation' => [
'width' => 400,
'height' => 400,
],
'tiny' => [
'width' => 40,
'height' => 40,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'thumbnail' => [
'width' => 150,
'height' => 150,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
'medium' => [
'width' => 320,
'height' => 320,
'mimetype' => 'image/webp',
'extension' => 'webp',
],
];
} }

View file

@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Config; namespace Config;
use Kint\Parser\ConstructablePluginInterface; use CodeIgniter\Config\BaseConfig;
use Kint\Renderer\Rich\TabPluginInterface; use Kint\Renderer\Renderer;
use Kint\Renderer\Rich\ValuePluginInterface;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Kint
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options * We use Kint's `RichRenderer` and `CLIRenderer`. This area contains options
@ -17,7 +17,7 @@ use Kint\Renderer\Rich\ValuePluginInterface;
* *
* @see https://kint-php.github.io/kint/ for details on these settings. * @see https://kint-php.github.io/kint/ for details on these settings.
*/ */
class Kint class Kint extends BaseConfig
{ {
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -26,9 +26,9 @@ class Kint
*/ */
/** /**
* @var list<class-string<ConstructablePluginInterface>|ConstructablePluginInterface>|null * @var string[]
*/ */
public ?array $plugins = []; public array $plugins = [];
public int $maxDepth = 6; public int $maxDepth = 6;
@ -46,15 +46,17 @@ class Kint
public bool $richFolder = false; public bool $richFolder = false;
/** public int $richSort = Renderer::SORT_FULL;
* @var array<string, class-string<ValuePluginInterface>>|null
*/
public ?array $richObjectPlugins = [];
/** /**
* @var array<string, class-string<TabPluginInterface>>|null * @var string[]
*/ */
public ?array $richTabPlugins = []; public array $richObjectPlugins = [];
/**
* @var string[]
*/
public array $richTabPlugins = [];
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------

View file

@ -6,7 +6,6 @@ namespace Config;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Log\Handlers\FileHandler; use CodeIgniter\Log\Handlers\FileHandler;
use CodeIgniter\Log\Handlers\HandlerInterface;
class Logger extends BaseConfig class Logger extends BaseConfig
{ {
@ -39,9 +38,9 @@ class Logger extends BaseConfig
* For a live site you'll usually enable Critical or higher (3) to be logged otherwise * For a live site you'll usually enable Critical or higher (3) to be logged otherwise
* your log files will fill up very fast. * your log files will fill up very fast.
* *
* @var int|list<int> * @var int|int[]
*/ */
public int | array $threshold = (ENVIRONMENT === 'production') ? 4 : 9; public int | array $threshold = 4;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
@ -61,7 +60,7 @@ class Logger extends BaseConfig
* The logging system supports multiple actions to be taken when something * The logging system supports multiple actions to be taken when something
* is logged. This is done by allowing for multiple Handlers, special classes * is logged. This is done by allowing for multiple Handlers, special classes
* designed to write the log to their chosen destinations, whether that is * designed to write the log to their chosen destinations, whether that is
* a file on the server, a cloud-based service, or even taking actions such * a file on the getServer, a cloud-based service, or even taking actions such
* as emailing the dev team. * as emailing the dev team.
* *
* Each handler is defined by the class name used for that handler, and it * Each handler is defined by the class name used for that handler, and it
@ -76,7 +75,7 @@ class Logger extends BaseConfig
* Handlers are executed in the order defined in this array, starting with * Handlers are executed in the order defined in this array, starting with
* the handler on top and continuing down. * the handler on top and continuing down.
* *
* @var array<class-string<HandlerInterface>, array<string, int|list<string>|string>> * @var array<string, mixed>
*/ */
public array $handlers = [ public array $handlers = [
/* /*
@ -115,32 +114,5 @@ class Logger extends BaseConfig
*/ */
'path' => '', 'path' => '',
], ],
/*
* The ChromeLoggerHandler requires the use of the Chrome web browser
* and the ChromeLogger extension. Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ChromeLoggerHandler' => [
// /*
// * The log levels that this handler will handle.
// */
// 'handles' => ['critical', 'alert', 'emergency', 'debug',
// 'error', 'info', 'notice', 'warning'],
// ],
/*
* The ErrorlogHandler writes the logs to PHP's native `error_log()` function.
* Uncomment this block to use it.
*/
// 'CodeIgniter\Log\Handlers\ErrorlogHandler' => [
// /* The log levels this handler can handle. */
// 'handles' => ['critical', 'alert', 'emergency', 'debug', 'error', 'info', 'notice', 'warning'],
//
// /*
// * The message type where the error should go. Can be 0 or 4, or use the
// * class constants: `ErrorlogHandler::TYPE_OS` (0) or `ErrorlogHandler::TYPE_SAPI` (4)
// */
// 'messageType' => 0,
// ],
]; ];
} }

View file

@ -27,7 +27,9 @@ class Migrations extends BaseConfig
* *
* This is the name of the table that will store the current migrations state. * This is the name of the table that will store the current migrations state.
* When migrations runs it will store in a database table which migration * When migrations runs it will store in a database table which migration
* files have already been run. * level the system is at. It then compares the migration level in this
* table to the $config['migration_version'] if they are not the same it
* will migrate up. This must be set.
*/ */
public string $table = 'migrations'; public string $table = 'migrations';
@ -46,19 +48,4 @@ class Migrations extends BaseConfig
* - Y_m_d_His_ * - Y_m_d_His_
*/ */
public string $timestampFormat = 'Y-m-d-His_'; public string $timestampFormat = 'Y-m-d-His_';
/**
* --------------------------------------------------------------------------
* Enable/Disable Migration Lock
* --------------------------------------------------------------------------
*
* Locking is disabled by default.
*
* When enabled, it will prevent multiple migration processes
* from running at the same time by using a lock mechanism.
*
* This is useful in production environments to avoid conflicts
* or race conditions during concurrent deployments.
*/
public bool $lock = false;
} }

View file

@ -5,6 +5,8 @@ declare(strict_types=1);
namespace Config; namespace Config;
/** /**
* Mimes
*
* This file contains an array of mime types. It is used by the Upload class to help identify allowed file types. * This file contains an array of mime types. It is used by the Upload class to help identify allowed file types.
* *
* When more than one variation for an extension exist (like jpg, jpeg, etc) the most common one should be first in the * When more than one variation for an extension exist (like jpg, jpeg, etc) the most common one should be first in the
@ -13,12 +15,13 @@ namespace Config;
* When working with mime types, please make sure you have the ´fileinfo´ extension enabled to reliably detect the * When working with mime types, please make sure you have the ´fileinfo´ extension enabled to reliably detect the
* media types. * media types.
*/ */
class Mimes class Mimes
{ {
/** /**
* Map of extensions to mime types. * Map of extensions to mime types.
* *
* @var array<string, list<string>|string> * @var array<string, string|string[]>
*/ */
public static $mimes = [ public static $mimes = [
'hqx' => [ 'hqx' => [
@ -50,24 +53,21 @@ class Mimes
'dms' => 'application/octet-stream', 'dms' => 'application/octet-stream',
'lha' => 'application/octet-stream', 'lha' => 'application/octet-stream',
'lzh' => 'application/octet-stream', 'lzh' => 'application/octet-stream',
'exe' => ['application/octet-stream', 'exe' => ['application/octet-stream', 'application/x-msdownload'],
'application/vnd.microsoft.portable-executable',
'application/x-dosexec',
'application/x-msdownload'],
'class' => 'application/octet-stream', 'class' => 'application/octet-stream',
'psd' => ['application/x-photoshop', 'image/vnd.adobe.photoshop'], 'psd' => ['application/x-photoshop', 'image/vnd.adobe.photoshop'],
'so' => 'application/octet-stream', 'so' => 'application/octet-stream',
'sea' => 'application/octet-stream', 'sea' => 'application/octet-stream',
'dll' => 'application/octet-stream', 'dll' => 'application/octet-stream',
'oda' => 'application/oda', 'oda' => 'application/oda',
'pdf' => ['application/pdf', 'application/force-download', 'application/x-download'], 'pdf' => ['application/pdf', 'application/force-download', 'application/x-download'],
'ai' => ['application/pdf', 'application/postscript'], 'ai' => ['application/pdf', 'application/postscript'],
'eps' => 'application/postscript', 'eps' => 'application/postscript',
'ps' => 'application/postscript', 'ps' => 'application/postscript',
'smi' => 'application/smil', 'smi' => 'application/smil',
'smil' => 'application/smil', 'smil' => 'application/smil',
'mif' => 'application/vnd.mif', 'mif' => 'application/vnd.mif',
'xls' => [ 'xls' => [
'application/vnd.ms-excel', 'application/vnd.ms-excel',
'application/msexcel', 'application/msexcel',
'application/x-msexcel', 'application/x-msexcel',
@ -87,17 +87,21 @@ class Mimes
'application/vnd.ms-office', 'application/vnd.ms-office',
'application/msword', 'application/msword',
], ],
'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'], 'pptx' => [
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'application/x-zip',
'application/zip',
],
'wbxml' => 'application/wbxml', 'wbxml' => 'application/wbxml',
'wmlc' => 'application/wmlc', 'wmlc' => 'application/wmlc',
'dcr' => 'application/x-director', 'dcr' => 'application/x-director',
'dir' => 'application/x-director', 'dir' => 'application/x-director',
'dxr' => 'application/x-director', 'dxr' => 'application/x-director',
'dvi' => 'application/x-dvi', 'dvi' => 'application/x-dvi',
'gtar' => 'application/x-gtar', 'gtar' => 'application/x-gtar',
'gz' => 'application/x-gzip', 'gz' => 'application/x-gzip',
'gzip' => 'application/x-gzip', 'gzip' => 'application/x-gzip',
'php' => [ 'php' => [
'application/x-php', 'application/x-php',
'application/x-httpd-php', 'application/x-httpd-php',
'application/php', 'application/php',
@ -105,41 +109,41 @@ class Mimes
'text/x-php', 'text/x-php',
'application/x-httpd-php-source', 'application/x-httpd-php-source',
], ],
'php4' => 'application/x-httpd-php', 'php4' => 'application/x-httpd-php',
'php3' => 'application/x-httpd-php', 'php3' => 'application/x-httpd-php',
'phtml' => 'application/x-httpd-php', 'phtml' => 'application/x-httpd-php',
'phps' => 'application/x-httpd-php-source', 'phps' => 'application/x-httpd-php-source',
'js' => ['application/x-javascript', 'text/plain'], 'js' => ['application/x-javascript', 'text/plain'],
'swf' => 'application/x-shockwave-flash', 'swf' => 'application/x-shockwave-flash',
'sit' => 'application/x-stuffit', 'sit' => 'application/x-stuffit',
'tar' => 'application/x-tar', 'tar' => 'application/x-tar',
'tgz' => ['application/x-tar', 'application/x-gzip-compressed'], 'tgz' => ['application/x-tar', 'application/x-gzip-compressed'],
'z' => 'application/x-compress', 'z' => 'application/x-compress',
'xhtml' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml',
'xht' => 'application/xhtml+xml', 'xht' => 'application/xhtml+xml',
'zip' => [ 'zip' => [
'application/x-zip', 'application/x-zip',
'application/zip', 'application/zip',
'application/x-zip-compressed', 'application/x-zip-compressed',
'application/s-compressed', 'application/s-compressed',
'multipart/x-zip', 'multipart/x-zip',
], ],
'rar' => ['application/vnd.rar', 'application/x-rar', 'application/rar', 'application/x-rar-compressed'], 'rar' => ['application/vnd.rar', 'application/x-rar', 'application/rar', 'application/x-rar-compressed'],
'mid' => 'audio/midi', 'mid' => 'audio/midi',
'midi' => 'audio/midi', 'midi' => 'audio/midi',
'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3', 'application/octet-stream'], 'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3', 'application/octet-stream'],
'mpga' => 'audio/mpeg', 'mpga' => 'audio/mpeg',
'mp2' => 'audio/mpeg', 'mp2' => 'audio/mpeg',
'aif' => ['audio/x-aiff', 'audio/aiff'], 'aif' => ['audio/x-aiff', 'audio/aiff'],
'aiff' => ['audio/x-aiff', 'audio/aiff'], 'aiff' => ['audio/x-aiff', 'audio/aiff'],
'aifc' => 'audio/x-aiff', 'aifc' => 'audio/x-aiff',
'ram' => 'audio/x-pn-realaudio', 'ram' => 'audio/x-pn-realaudio',
'rm' => 'audio/x-pn-realaudio', 'rm' => 'audio/x-pn-realaudio',
'rpm' => 'audio/x-pn-realaudio-plugin', 'rpm' => 'audio/x-pn-realaudio-plugin',
'ra' => 'audio/x-realaudio', 'ra' => 'audio/x-realaudio',
'rv' => 'video/vnd.rn-realvideo', 'rv' => 'video/vnd.rn-realvideo',
'wav' => ['audio/x-wav', 'audio/wave', 'audio/wav'], 'wav' => ['audio/x-wav', 'audio/wave', 'audio/wav'],
'bmp' => [ 'bmp' => [
'image/bmp', 'image/bmp',
'image/x-bmp', 'image/x-bmp',
'image/x-bitmap', 'image/x-bitmap',
@ -152,48 +156,47 @@ class Mimes
'application/x-bmp', 'application/x-bmp',
'application/x-win-bitmap', 'application/x-win-bitmap',
], ],
'gif' => 'image/gif', 'gif' => 'image/gif',
'jpg' => ['image/jpeg', 'image/pjpeg'], 'jpg' => ['image/jpeg', 'image/pjpeg'],
'jpeg' => ['image/jpeg', 'image/pjpeg'], 'jpeg' => ['image/jpeg', 'image/pjpeg'],
'jpe' => ['image/jpeg', 'image/pjpeg'], 'jpe' => ['image/jpeg', 'image/pjpeg'],
'jp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'jp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'j2k' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'j2k' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpf' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'jpf' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpg2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'jpg2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpx' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'jpx' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'jpm' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'jpm' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'mj2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'], 'mjp2' => ['image/jp2', 'video/mj2', 'image/jpx', 'image/jpm'],
'png' => ['image/png', 'image/x-png'], 'png' => ['image/png', 'image/x-png'],
'webp' => 'image/webp', 'tif' => 'image/tiff',
'tif' => 'image/tiff', 'tiff' => 'image/tiff',
'tiff' => 'image/tiff', 'css' => ['text/css', 'text/plain'],
'css' => ['text/css', 'text/plain'], 'html' => ['text/html', 'text/plain'],
'html' => ['text/html', 'text/plain'], 'htm' => ['text/html', 'text/plain'],
'htm' => ['text/html', 'text/plain'],
'shtml' => ['text/html', 'text/plain'], 'shtml' => ['text/html', 'text/plain'],
'txt' => 'text/plain', 'txt' => 'text/plain',
'text' => 'text/plain', 'text' => 'text/plain',
'log' => ['text/plain', 'text/x-log'], 'log' => ['text/plain', 'text/x-log'],
'rtx' => 'text/richtext', 'rtx' => 'text/richtext',
'rtf' => 'text/rtf', 'rtf' => 'text/rtf',
'xml' => ['application/xml', 'text/xml', 'text/plain'], 'xml' => ['application/xml', 'text/xml', 'text/plain'],
'xsl' => ['application/xml', 'text/xsl', 'text/xml'], 'xsl' => ['application/xml', 'text/xsl', 'text/xml'],
'mpeg' => 'video/mpeg', 'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg', 'mpg' => 'video/mpeg',
'mpe' => 'video/mpeg', 'mpe' => 'video/mpeg',
'qt' => 'video/quicktime', 'qt' => 'video/quicktime',
'mov' => 'video/quicktime', 'mov' => 'video/quicktime',
'avi' => ['video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'], 'avi' => ['video/x-msvideo', 'video/msvideo', 'video/avi', 'application/x-troff-msvideo'],
'movie' => 'video/x-sgi-movie', 'movie' => 'video/x-sgi-movie',
'doc' => ['application/msword', 'application/vnd.ms-office'], 'doc' => ['application/msword', 'application/vnd.ms-office'],
'docx' => [ 'docx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip', 'application/zip',
'application/msword', 'application/msword',
'application/x-zip', 'application/x-zip',
], ],
'dot' => ['application/msword', 'application/vnd.ms-office'], 'dot' => ['application/msword', 'application/vnd.ms-office'],
'dotx' => [ 'dotx' => [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/zip', 'application/zip',
@ -209,49 +212,49 @@ class Mimes
'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12',
'word' => ['application/msword', 'application/octet-stream'], 'word' => ['application/msword', 'application/octet-stream'],
'xl' => 'application/excel', 'xl' => 'application/excel',
'eml' => 'message/rfc822', 'eml' => 'message/rfc822',
'json' => ['application/json', 'text/json', 'text/plain'], 'json' => ['application/json', 'text/json'],
'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'], 'pem' => ['application/x-x509-user-cert', 'application/x-pem-file', 'application/octet-stream'],
'p10' => ['application/x-pkcs10', 'application/pkcs10'], 'p10' => ['application/x-pkcs10', 'application/pkcs10'],
'p12' => 'application/x-pkcs12', 'p12' => 'application/x-pkcs12',
'p7a' => 'application/x-pkcs7-signature', 'p7a' => 'application/x-pkcs7-signature',
'p7c' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'], 'p7c' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7m' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'], 'p7m' => ['application/pkcs7-mime', 'application/x-pkcs7-mime'],
'p7r' => 'application/x-pkcs7-certreqresp', 'p7r' => 'application/x-pkcs7-certreqresp',
'p7s' => 'application/pkcs7-signature', 'p7s' => 'application/pkcs7-signature',
'crt' => ['application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'], 'crt' => ['application/x-x509-ca-cert', 'application/x-x509-user-cert', 'application/pkix-cert'],
'crl' => ['application/pkix-crl', 'application/pkcs-crl'], 'crl' => ['application/pkix-crl', 'application/pkcs-crl'],
'der' => 'application/x-x509-ca-cert', 'der' => 'application/x-x509-ca-cert',
'kdb' => 'application/octet-stream', 'kdb' => 'application/octet-stream',
'pgp' => 'application/pgp', 'pgp' => 'application/pgp',
'gpg' => 'application/gpg-keys', 'gpg' => 'application/gpg-keys',
'sst' => 'application/octet-stream', 'sst' => 'application/octet-stream',
'csr' => 'application/octet-stream', 'csr' => 'application/octet-stream',
'rsa' => 'application/x-pkcs7', 'rsa' => 'application/x-pkcs7',
'cer' => ['application/pkix-cert', 'application/x-x509-ca-cert'], 'cer' => ['application/pkix-cert', 'application/x-x509-ca-cert'],
'3g2' => 'video/3gpp2', '3g2' => 'video/3gpp2',
'3gp' => ['video/3gp', 'video/3gpp'], '3gp' => ['video/3gp', 'video/3gpp'],
'mp4' => 'video/mp4', 'mp4' => 'video/mp4',
'm4a' => ['audio/m4a', 'audio/x-m4a', 'application/octet-stream'], 'm4a' => ['audio/m4a', 'audio/x-m4a', 'application/octet-stream'],
'f4v' => ['video/mp4', 'video/x-f4v'], 'f4v' => ['video/mp4', 'video/x-f4v'],
'flv' => 'video/x-flv', 'flv' => 'video/x-flv',
'webm' => 'video/webm', 'webm' => 'video/webm',
'aac' => 'audio/x-acc', 'aac' => 'audio/x-acc',
'm4u' => 'application/vnd.mpegurl', 'm4u' => 'application/vnd.mpegurl',
'm3u' => 'text/plain', 'm3u' => 'text/plain',
'xspf' => 'application/xspf+xml', 'xspf' => 'application/xspf+xml',
'vlc' => 'application/videolan', 'vlc' => 'application/videolan',
'wmv' => ['video/x-ms-wmv', 'video/x-ms-asf'], 'wmv' => ['video/x-ms-wmv', 'video/x-ms-asf'],
'au' => 'audio/x-au', 'au' => 'audio/x-au',
'ac3' => 'audio/ac3', 'ac3' => 'audio/ac3',
'flac' => 'audio/x-flac', 'flac' => 'audio/x-flac',
'ogg' => ['audio/ogg', 'video/ogg', 'application/ogg'], 'ogg' => ['audio/ogg', 'video/ogg', 'application/ogg'],
'kmz' => ['application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'], 'kmz' => ['application/vnd.google-earth.kmz', 'application/zip', 'application/x-zip'],
'kml' => ['application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'], 'kml' => ['application/vnd.google-earth.kml+xml', 'application/xml', 'text/xml'],
'ics' => 'text/calendar', 'ics' => 'text/calendar',
'ical' => 'text/calendar', 'ical' => 'text/calendar',
'zsh' => 'text/x-scriptzsh', 'zsh' => 'text/x-scriptzsh',
'7zip' => [ '7zip' => [
'application/x-compressed', 'application/x-compressed',
'application/x-zip-compressed', 'application/x-zip-compressed',
@ -276,11 +279,10 @@ class Mimes
], ],
'svg' => ['image/svg+xml', 'image/svg', 'application/xml', 'text/xml'], 'svg' => ['image/svg+xml', 'image/svg', 'application/xml', 'text/xml'],
'vcf' => 'text/x-vcard', 'vcf' => 'text/x-vcard',
'srt' => ['application/x-subrip', 'text/srt', 'text/plain', 'application/octet-stream'], 'srt' => ['text/srt', 'text/plain', 'application/octet-stream'],
'vtt' => ['text/vtt', 'text/plain'], 'vtt' => ['text/vtt', 'text/plain'],
'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'], 'ico' => ['image/x-icon', 'image/x-ico', 'image/vnd.microsoft.icon'],
'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle', 'model/stl', 'stl' => ['application/sla', 'application/vnd.ms-pki.stl', 'application/x-navistyle'],
'application/octet-stream', ],
]; ];
/** /**
@ -304,28 +306,35 @@ class Mimes
/** /**
* Attempts to determine the best file extension for a given mime type. * Attempts to determine the best file extension for a given mime type.
* *
* @param string|null $proposedExtension - default extension (in case there is more than one with the same mime type) * @param string $proposedExtension - default extension (in case there is more than one with the same mime type)
* @return string|null The extension determined, or null if unable to match. * @return string|null The extension determined, or null if unable to match.
*/ */
public static function guessExtensionFromType(string $type, ?string $proposedExtension = null): ?string public static function guessExtensionFromType(string $type, string $proposedExtension = ''): ?string
{ {
$type = trim(strtolower($type), '. '); $type = trim(strtolower($type), '. ');
$proposedExtension = trim(strtolower($proposedExtension ?? '')); $proposedExtension = trim(strtolower($proposedExtension));
if ( if ($proposedExtension !== '') {
$proposedExtension !== '' if (array_key_exists($proposedExtension, static::$mimes) && in_array(
&& array_key_exists($proposedExtension, static::$mimes) $type,
&& in_array($type, (array) static::$mimes[$proposedExtension], true) is_string(static::$mimes[$proposedExtension]) ? [
) { static::$mimes[$proposedExtension],
// The detected mime type matches with the proposed extension. ] : static::$mimes[$proposedExtension],
return $proposedExtension; true
)) {
// The detected mime type matches with the proposed extension.
return $proposedExtension;
}
// An extension was proposed, but the media type does not match the mime type list.
return null;
} }
// Reverse check the mime type list if no extension was proposed. // Reverse check the mime type list if no extension was proposed.
// This search is order sensitive! // This search is order sensitive!
foreach (static::$mimes as $ext => $types) { foreach (static::$mimes as $ext => $types) {
if (in_array($type, (array) $types, true)) { if ((is_string($types) && $types === $type) || (is_array($types) && in_array($type, $types, true))) {
return $ext; return $ext;
} }
} }

View file

@ -6,12 +6,6 @@ namespace Config;
use CodeIgniter\Modules\Modules as BaseModules; use CodeIgniter\Modules\Modules as BaseModules;
/**
* Modules Configuration.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/
class Modules extends BaseModules class Modules extends BaseModules
{ {
/** /**
@ -39,29 +33,6 @@ class Modules extends BaseModules
*/ */
public $discoverInComposer = true; public $discoverInComposer = true;
/**
* The Composer package list for Auto-Discovery
* This setting is optional.
*
* E.g.:
* [
* 'only' => [
* // List up all packages to auto-discover
* 'codeigniter4/shield',
* ],
* ]
* or
* [
* 'exclude' => [
* // List up packages to exclude.
* 'pestphp/pest',
* ],
* ]
*
* @var array{only?: list<string>, exclude?: list<string>}
*/
public $composerPackages = [];
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Auto-Discovery Rules * Auto-Discovery Rules
@ -72,7 +43,7 @@ class Modules extends BaseModules
* *
* If it is not listed, only the base application elements will be used. * If it is not listed, only the base application elements will be used.
* *
* @var list<string> * @var string[]
*/ */
public $aliases = ['events', 'filters', 'registrars', 'routes', 'services']; public $aliases = ['events', 'filters', 'registrars', 'routes', 'services'];
} }

View file

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
/**
* Optimization Configuration.
*
* NOTE: This class does not extend BaseConfig for performance reasons.
* So you cannot replace the property values with Environment Variables.
*
* WARNING: Do not use these options when running the app in the Worker Mode.
*/
class Optimize
{
/**
* --------------------------------------------------------------------------
* Config Caching
* --------------------------------------------------------------------------
*
* @see https://codeigniter.com/user_guide/concepts/factories.html#config-caching
*/
public bool $configCacheEnabled = false;
/**
* --------------------------------------------------------------------------
* Config Caching
* --------------------------------------------------------------------------
*
* @see https://codeigniter.com/user_guide/concepts/autoloader.html#file-locator-caching
*/
public bool $locatorCacheEnabled = false;
}

View file

@ -21,11 +21,13 @@ class Pager extends BaseConfig
* and the desired group as $pagerGroup; * and the desired group as $pagerGroup;
* *
* @var array<string, string> * @var array<string, string>
*
* @noRector Rector\Php55\Rector\String_\StringClassNameToClassConstantRector
*/ */
public array $templates = [ public $templates = [
'default_full' => 'App\Views\pager\default_full', 'default_full' => 'App\Views\pager\default_full',
'default_simple' => 'CodeIgniter\Pager\Views\default_simple', 'default_simple' => 'CodeIgniter\Pager\Views\default_simple',
'default_head' => 'CodeIgniter\Pager\Views\default_head', 'default_head' => 'CodeIgniter\Pager\Views\default_head',
]; ];
/** /**

View file

@ -5,16 +5,16 @@ declare(strict_types=1);
namespace Config; namespace Config;
/** /**
* Paths
*
* Holds the paths that are used by the system to locate the main directories, app, system, etc. * Holds the paths that are used by the system to locate the main directories, app, system, etc.
* *
* Modifying these allows you to restructure your application, share a system folder between multiple applications, and * Modifying these allows you to restructure your application, share a system folder between multiple applications, and
* more. * more.
* *
* All paths are relative to the project's root folder. * All paths are relative to the project's root folder.
*
* NOTE: This class is required prior to Autoloader instantiation,
* and does not extend BaseConfig.
*/ */
class Paths class Paths
{ {
/** /**
@ -26,7 +26,7 @@ class Paths
* the path if the folder is not in the same directory as this file. * the path if the folder is not in the same directory as this file.
*/ */
public string $systemDirectory = public string $systemDirectory =
__DIR__ . '/../../vendor/codeigniter4/framework/system'; __DIR__ . '/../../vendor/codeigniter4/codeigniter4/system';
/** /**
* --------------------------------------------------------------- * ---------------------------------------------------------------
@ -35,8 +35,8 @@ class Paths
* *
* If you want this front controller to use a different "app" * If you want this front controller to use a different "app"
* folder than the default one you can set its name here. The folder * folder than the default one you can set its name here. The folder
* can also be renamed or relocated anywhere on your server. If * can also be renamed or relocated anywhere on your getServer. If
* you do, use a full server path. * you do, use a full getServer path.
* *
* @see http://codeigniter.com/user_guide/general/managing_apps.html * @see http://codeigniter.com/user_guide/general/managing_apps.html
*/ */
@ -72,7 +72,7 @@ class Paths
* This variable must contain the name of the directory that * This variable must contain the name of the directory that
* contains the view files used by your application. By * contains the view files used by your application. By
* default this is in `app/Views`. This value * default this is in `app/Views`. This value
* is used when no value is provided to `service('renderer')`. * is used when no value is provided to `Services::renderer()`.
*/ */
public string $viewDirectory = __DIR__ . '/../Views'; public string $viewDirectory = __DIR__ . '/../Views';
} }

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\Publisher as BasePublisher;
/**
* Publisher Configuration
*
* Defines basic security restrictions for the Publisher class to prevent abuse by injecting malicious files into a
* project.
*/
class Publisher extends BasePublisher
{
/**
* A list of allowed destinations with a (pseudo-)regex of allowed files for each destination. Attempts to publish
* to directories not in this list will result in a PublisherException. Files that do no fit the pattern will cause
* copy/merge to fail.
*
* @var array<string, string>
*/
public $restrictions = [
ROOTPATH => '*',
FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i',
];
}

View file

@ -2,19 +2,41 @@
declare(strict_types=1); declare(strict_types=1);
use CodeIgniter\Router\RouteCollection; namespace Config;
// Create a new instance of our RouteCollection class.
$routes = Services::routes();
// Load the system's routing file first, so that the app and ENVIRONMENT
// can override as needed.
if (file_exists(SYSTEMPATH . 'Config/Routes.php')) {
require SYSTEMPATH . 'Config/Routes.php';
}
/**
* --------------------------------------------------------------------
* Router Setup
* --------------------------------------------------------------------
*/
$routes->setDefaultNamespace('App\Controllers');
$routes->setDefaultController('Home');
$routes->setDefaultMethod('index');
$routes->setTranslateURIDashes(false);
$routes->set404Override();
$routes->setAutoRoute(false);
/** /**
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* Placeholder definitions * Placeholder definitions
* -------------------------------------------------------------------- * --------------------------------------------------------------------
*/ */
/** @var RouteCollection $routes */
$routes->addPlaceholder('podcastHandle', '[a-zA-Z0-9\_]{1,32}'); $routes->addPlaceholder('podcastName', '[a-zA-Z0-9\_]{1,32}');
$routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,128}'); $routes->addPlaceholder('slug', '[a-zA-Z0-9\-]{1,191}');
$routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}'); $routes->addPlaceholder('base64', '[A-Za-z0-9\.\_]+\-{0,2}');
$routes->addPlaceholder('postAction', '\bfavourite|\breblog|\breply'); $routes->addPlaceholder('platformType', '\bpodcasting|\bsocial|\bfunding');
$routes->addPlaceholder('embedTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent'); $routes->addPlaceholder('statusAction', '\bfavourite|\breblog|\breply');
$routes->addPlaceholder('embeddablePlayerTheme', '\blight|\bdark|\blight-transparent|\bdark-transparent');
$routes->addPlaceholder( $routes->addPlaceholder(
'uuid', 'uuid',
'[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}', '[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}',
@ -26,111 +48,700 @@ $routes->addPlaceholder(
* -------------------------------------------------------------------- * --------------------------------------------------------------------
*/ */
$routes->get('manifest.webmanifest', 'WebmanifestController', [
'as' => 'webmanifest',
]);
$routes->get('themes/colors', 'ColorsController', [
'as' => 'themes-colors-css',
]);
// health check
$routes->get('/health', 'HomeController::health', [
'as' => 'health',
]);
// We get a performance increase by specifying the default // We get a performance increase by specifying the default
// route since we don't have to scan directories. // route since we don't have to scan directories.
$routes->get('/', 'HomeController', [ $routes->get('/', 'HomeController::index', [
'as' => 'home', 'as' => 'home',
]); ]);
// Install Wizard route
$routes->group(config('App')->installGateway, function ($routes): void {
$routes->get('/', 'InstallController', [
'as' => 'install',
]);
$routes->post('instance-config', 'InstallController::attemptInstanceConfig', [
'as' => 'instance-config',
]);
$routes->post('database-config', 'InstallController::attemptDatabaseConfig', [
'as' => 'database-config',
]);
$routes->post('cache-config', 'InstallController::attemptCacheConfig', [
'as' => 'cache-config',
]);
$routes->post(
'create-superadmin',
'InstallController::attemptCreateSuperAdmin',
[
'as' => 'create-superadmin',
],
);
});
$routes->get('.well-known/platforms', 'Platform'); $routes->get('.well-known/platforms', 'Platform');
service('auth') // Admin area
->routes($routes); $routes->group(
config('App')
->adminGateway,
[
'namespace' => 'App\Controllers\Admin',
],
function ($routes): void {
$routes->get('/', 'HomeController', [
'as' => 'admin',
]);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PersonController', [
'as' => 'person-list',
'filter' => 'permission:person-list',
]);
$routes->get('new', 'PersonController::create', [
'as' => 'person-create',
'filter' => 'permission:person-create',
]);
$routes->post('new', 'PersonController::attemptCreate', [
'filter' => 'permission:person-create',
]);
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PersonController::view/$1', [
'as' => 'person-view',
'filter' => 'permission:person-view',
]);
$routes->get('edit', 'PersonController::edit/$1', [
'as' => 'person-edit',
'filter' => 'permission:person-edit',
]);
$routes->post('edit', 'PersonController::attemptEdit/$1', [
'filter' => 'permission:person-edit',
]);
$routes->add('delete', 'PersonController::delete/$1', [
'as' => 'person-delete',
'filter' => 'permission:person-delete',
]);
});
});
// Podcasts
$routes->group('podcasts', function ($routes): void {
$routes->get('/', 'PodcastController::list', [
'as' => 'podcast-list',
]);
$routes->get('new', 'PodcastController::create', [
'as' => 'podcast-create',
'filter' => 'permission:podcasts-create',
]);
$routes->post('new', 'PodcastController::attemptCreate', [
'filter' => 'permission:podcasts-create',
]);
$routes->get('import', 'PodcastImportController', [
'as' => 'podcast-import',
'filter' => 'permission:podcasts-import',
]);
$routes->post('import', 'PodcastImportController::attemptImport', [
'filter' => 'permission:podcasts-import',
]);
// Podcast
// Use ids in admin area to help permission and group lookups
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PodcastController::view/$1', [
'as' => 'podcast-view',
'filter' => 'permission:podcasts-view,podcast-view',
]);
$routes->get('edit', 'PodcastController::edit/$1', [
'as' => 'podcast-edit',
'filter' => 'permission:podcast-edit',
]);
$routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit',
]);
$routes->get('delete', 'PodcastController::delete/$1', [
'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete',
]);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'PodcastPersonController/$1', [
'as' => 'podcast-person-manage',
'filter' => 'permission:podcast-edit',
]);
$routes->post(
'/',
'PodcastPersonController::attemptAdd/$1',
[
'filter' => 'permission:podcast-edit',
],
);
$routes->get(
'(:num)/remove',
'PodcastPersonController::remove/$1/$2',
[
'as' => 'podcast-person-remove',
'filter' => 'permission:podcast-edit',
],
);
});
$routes->group('analytics', function ($routes): void {
$routes->get('/', 'PodcastController::viewAnalytics/$1', [
'as' => 'podcast-analytics',
'filter' => 'permission:podcasts-view,podcast-view',
]);
$routes->get(
'webpages',
'PodcastController::viewAnalyticsWebpages/$1',
[
'as' => 'podcast-analytics-webpages',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'locations',
'PodcastController::viewAnalyticsLocations/$1',
[
'as' => 'podcast-analytics-locations',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'unique-listeners',
'PodcastController::viewAnalyticsUniqueListeners/$1',
[
'as' => 'podcast-analytics-unique-listeners',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'listening-time',
'PodcastController::viewAnalyticsListeningTime/$1',
[
'as' => 'podcast-analytics-listening-time',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'time-periods',
'PodcastController::viewAnalyticsTimePeriods/$1',
[
'as' => 'podcast-analytics-time-periods',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
$routes->get(
'players',
'PodcastController::viewAnalyticsPlayers/$1',
[
'as' => 'podcast-analytics-players',
'filter' => 'permission:podcasts-view,podcast-view',
],
);
});
// Podcast episodes
$routes->group('episodes', function ($routes): void {
$routes->get('/', 'EpisodeController::list/$1', [
'as' => 'episode-list',
'filter' =>
'permission:episodes-list,podcast_episodes-list',
]);
$routes->get('new', 'EpisodeController::create/$1', [
'as' => 'episode-create',
'filter' => 'permission:podcast_episodes-create',
]);
$routes->post(
'new',
'EpisodeController::attemptCreate/$1',
[
'filter' => 'permission:podcast_episodes-create',
],
);
// Episode
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'EpisodeController::view/$1/$2', [
'as' => 'episode-view',
'filter' =>
'permission:episodes-view,podcast_episodes-view',
]);
$routes->get('edit', 'EpisodeController::edit/$1/$2', [
'as' => 'episode-edit',
'filter' => 'permission:podcast_episodes-edit',
]);
$routes->post(
'edit',
'EpisodeController::attemptEdit/$1/$2',
[
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'publish',
'EpisodeController::publish/$1/$2',
[
'as' => 'episode-publish',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish',
'EpisodeController::attemptPublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-edit',
'EpisodeController::publishEdit/$1/$2',
[
'as' => 'episode-publish_edit',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish-edit',
'EpisodeController::attemptPublishEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-cancel',
'EpisodeController::publishCancel/$1/$2',
[
'as' => 'episode-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'unpublish',
'EpisodeController::unpublish/$1/$2',
[
'as' => 'episode-unpublish',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'unpublish',
'EpisodeController::attemptUnpublish/$1/$2',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'delete',
'EpisodeController::delete/$1/$2',
[
'as' => 'episode-delete',
'filter' =>
'permission:podcast_episodes-delete',
],
);
$routes->get(
'transcript-delete',
'EpisodeController::transcriptDelete/$1/$2',
[
'as' => 'transcript-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'chapters-delete',
'EpisodeController::chaptersDelete/$1/$2',
[
'as' => 'chapters-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'soundbites',
'EpisodeController::soundbitesEdit/$1/$2',
[
'as' => 'soundbites-edit',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->post(
'soundbites',
'EpisodeController::soundbitesAttemptEdit/$1/$2',
[
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'soundbites/(:num)/delete',
'EpisodeController::soundbiteDelete/$1/$2/$3',
[
'as' => 'soundbite-delete',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->get(
'embeddable-player',
'EpisodeController::embeddablePlayer/$1/$2',
[
'as' => 'embeddable-player-add',
'filter' => 'permission:podcast_episodes-edit',
],
);
$routes->group('persons', function ($routes): void {
$routes->get('/', 'EpisodePersonController/$1/$2', [
'as' => 'episode-person-manage',
'filter' => 'permission:podcast_episodes-edit',
]);
$routes->post(
'/',
'EpisodePersonController::attemptAdd/$1/$2',
[
'filter' =>
'permission:podcast_episodes-edit',
],
);
$routes->get(
'(:num)/remove',
'EpisodePersonController::remove/$1/$2/$3',
[
'as' => 'episode-person-remove',
'filter' =>
'permission:podcast_episodes-edit',
],
);
});
});
});
// Podcast contributors
$routes->group('contributors', function ($routes): void {
$routes->get('/', 'ContributorController::list/$1', [
'as' => 'contributor-list',
'filter' =>
'permission:podcasts-view,podcast-manage_contributors',
]);
$routes->get('add', 'ContributorController::add/$1', [
'as' => 'contributor-add',
'filter' => 'permission:podcast-manage_contributors',
]);
$routes->post(
'add',
'ContributorController::attemptAdd/$1',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
// Contributor
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'ContributorController::view/$1/$2', [
'as' => 'contributor-view',
'filter' =>
'permission:podcast-manage_contributors',
]);
$routes->get(
'edit',
'ContributorController::edit/$1/$2',
[
'as' => 'contributor-edit',
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->post(
'edit',
'ContributorController::attemptEdit/$1/$2',
[
'filter' =>
'permission:podcast-manage_contributors',
],
);
$routes->get(
'remove',
'ContributorController::remove/$1/$2',
[
'as' => 'contributor-remove',
'filter' =>
'permission:podcast-manage_contributors',
],
);
});
});
$routes->group('platforms', function ($routes): void {
$routes->get(
'/',
'PodcastPlatformController::platforms/$1/podcasting',
[
'as' => 'platforms-podcasting',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'social',
'PodcastPlatformController::platforms/$1/social',
[
'as' => 'platforms-social',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'funding',
'PodcastPlatformController::platforms/$1/funding',
[
'as' => 'platforms-funding',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->post(
'save/(:platformType)',
'PodcastPlatformController::attemptPlatformsUpdate/$1/$2',
[
'as' => 'platforms-save',
'filter' => 'permission:podcast-manage_platforms',
],
);
$routes->get(
'(:slug)/podcast-platform-remove',
'PodcastPlatformController::removePodcastPlatform/$1/$2',
[
'as' => 'podcast-platform-remove',
'filter' => 'permission:podcast-manage_platforms',
],
);
});
});
});
// Instance wide Fediverse config
$routes->group('fediverse', function ($routes): void {
$routes->get('/', 'FediverseController::dashboard', [
'as' => 'fediverse-dashboard',
]);
$routes->get(
'blocked-actors',
'FediverseController::blockedActors',
[
'as' => 'fediverse-blocked-actors',
'filter' => 'permission:fediverse-block_actors',
],
);
$routes->get(
'blocked-domains',
'FediverseController::blockedDomains',
[
'as' => 'fediverse-blocked-domains',
'filter' => 'permission:fediverse-block_domains',
],
);
});
// Pages
$routes->group('pages', function ($routes): void {
$routes->get('/', 'PageController::list', [
'as' => 'page-list',
]);
$routes->get('new', 'PageController::create', [
'as' => 'page-create',
'filter' => 'permission:pages-manage',
]);
$routes->post('new', 'PageController::attemptCreate', [
'filter' => 'permission:pages-manage',
]);
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'PageController::view/$1', [
'as' => 'page-view',
]);
$routes->get('edit', 'PageController::edit/$1', [
'as' => 'page-edit',
'filter' => 'permission:pages-manage',
]);
$routes->post('edit', 'PageController::attemptEdit/$1', [
'filter' => 'permission:pages-manage',
]);
$routes->get('delete', 'PageController::delete/$1', [
'as' => 'page-delete',
'filter' => 'permission:pages-manage',
]);
});
});
// Users
$routes->group('users', function ($routes): void {
$routes->get('/', 'UserController::list', [
'as' => 'user-list',
'filter' => 'permission:users-list',
]);
$routes->get('new', 'UserController::create', [
'as' => 'user-create',
'filter' => 'permission:users-create',
]);
$routes->post('new', 'UserController::attemptCreate', [
'filter' => 'permission:users-create',
]);
// User
$routes->group('(:num)', function ($routes): void {
$routes->get('/', 'UserController::view/$1', [
'as' => 'user-view',
'filter' => 'permission:users-view',
]);
$routes->get('edit', 'UserController::edit/$1', [
'as' => 'user-edit',
'filter' => 'permission:users-manage_authorizations',
]);
$routes->post('edit', 'UserController::attemptEdit/$1', [
'filter' => 'permission:users-manage_authorizations',
]);
$routes->get('ban', 'UserController::ban/$1', [
'as' => 'user-ban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get('unban', 'UserController::unBan/$1', [
'as' => 'user-unban',
'filter' => 'permission:users-manage_bans',
]);
$routes->get(
'force-pass-reset',
'UserController::forcePassReset/$1',
[
'as' => 'user-force_pass_reset',
'filter' => 'permission:users-force_pass_reset',
],
);
$routes->get('delete', 'UserController::delete/$1', [
'as' => 'user-delete',
'filter' => 'permission:users-delete',
]);
});
});
// My account
$routes->group('my-account', function ($routes): void {
$routes->get('/', 'MyAccountController', [
'as' => 'my-account',
]);
$routes->get(
'change-password',
'MyAccountController::changePassword/$1',
[
'as' => 'change-password',
],
);
$routes->post('change-password', 'MyAccountController::attemptChange/$1');
});
},
);
/**
* Overwriting Myth:auth routes file
*/
$routes->group(config('App')->authGateway, function ($routes): void {
// Login/out
$routes->get('login', 'AuthController::login', [
'as' => 'login',
]);
$routes->post('login', 'AuthController::attemptLogin');
$routes->get('logout', 'AuthController::logout', [
'as' => 'logout',
]);
// Registration
$routes->get('register', 'AuthController::register', [
'as' => 'register',
]);
$routes->post('register', 'AuthController::attemptRegister');
// Activation
$routes->get('activate-account', 'AuthController::activateAccount', [
'as' => 'activate-account',
]);
$routes->get(
'resend-activate-account',
'AuthController::resendActivateAccount',
[
'as' => 'resend-activate-account',
],
);
// Forgot/Resets
$routes->get('forgot', 'AuthController::forgotPassword', [
'as' => 'forgot',
]);
$routes->post('forgot', 'AuthController::attemptForgot');
$routes->get('reset-password', 'AuthController::resetPassword', [
'as' => 'reset-password',
]);
$routes->post('reset-password', 'AuthController::attemptReset');
});
// Podcast's Public routes // Podcast's Public routes
$routes->group('@(:podcastHandle)', static function ($routes): void { $routes->group('@(:podcastName)', function ($routes): void {
// override default Fediverse Library's actor route $routes->get('/', 'PodcastController::activity/$1', [
'as' => 'podcast-activity',
]);
// override default ActivityPub Library's actor route
$routes->options('/', 'ActivityPubController::preflight'); $routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'PodcastController::activity/$1', [ $routes->get('/', 'PodcastController::activity/$1', [
'as' => 'podcast-activity', 'as' => 'actor',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'ActorController::index/$1', 'controller-method' => 'ActorController/$1',
], ],
'application/podcast-activity+json' => [ 'application/podcast-activity+json' => [
'namespace' => 'App\Controllers', 'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::podcastActor/$1', 'controller-method' => 'PodcastController::podcastActor/$1',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'ActorController::index/$1', 'controller-method' => 'ActorController/$1',
], ],
], ],
'filter' => 'allow-cors',
]);
$routes->get('manifest.webmanifest', 'WebmanifestController::podcastManifest/$1', [
'as' => 'podcast-webmanifest',
]);
$routes->get('links', 'PodcastController::links/$1', [
'as' => 'podcast-links',
]);
$routes->get('about', 'PodcastController::about/$1', [
'as' => 'podcast-about',
]); ]);
$routes->options('episodes', 'ActivityPubController::preflight'); $routes->options('episodes', 'ActivityPubController::preflight');
$routes->get('episodes', 'PodcastController::episodes/$1', [ $routes->get('episodes', 'PodcastController::episodes/$1', [
'as' => 'podcast-episodes', 'as' => 'podcast-episodes',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1', 'controller-method' => 'PodcastController::episodeCollection/$1',
], ],
'application/podcast-activity+json' => [ 'application/podcast-activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1', 'controller-method' => 'PodcastController::episodeCollection/$1',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'App\Controllers',
'controller-method' => 'PodcastController::episodeCollection/$1', 'controller-method' => 'PodcastController::episodeCollection/$1',
], ],
], ],
'filter' => 'allow-cors',
]); ]);
$routes->group('episodes/(:slug)', static function ($routes): void { $routes->group('episodes/(:slug)', function ($routes): void {
$routes->options('/', 'ActivityPubController::preflight'); $routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'EpisodeController::index/$1/$2', [ $routes->get('/', 'EpisodeController/$1/$2', [
'as' => 'episode', 'as' => 'episode',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2', 'controller-method' => 'EpisodeController::episodeObject/$1/$2',
], ],
'application/podcast-activity+json' => [ 'application/podcast-activity+json' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2', 'controller-method' => 'EpisodeController::episodeObject/$1/$2',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'App\Controllers',
'controller-method' => 'EpisodeController::episodeObject/$1/$2', 'controller-method' => 'EpisodeController::episodeObject/$1/$2',
], ],
], ],
'filter' => 'allow-cors',
]);
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
'as' => 'episode-activity',
]);
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
'as' => 'episode-chapters',
]);
$routes->get('transcript', 'EpisodeController::transcript/$1/$2', [
'as' => 'episode-transcript',
]); ]);
$routes->options('comments', 'ActivityPubController::preflight'); $routes->options('comments', 'ActivityPubController::preflight');
$routes->get('comments', 'EpisodeController::comments/$1/$2', [ $routes->get('comments', 'EpisodeController::comments/$1/$2', [
'as' => 'episode-comments', 'as' => 'episode-comments',
'application/activity+json' => [ 'application/activity+json' => [
'controller-method' => 'EpisodeController::comments/$1/$2', 'controller-method' => 'EpisodeController::comments/$1/$2',
], ],
@ -140,27 +751,6 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'controller-method' => 'EpisodeController::comments/$1/$2', 'controller-method' => 'EpisodeController::comments/$1/$2',
], ],
'filter' => 'allow-cors',
]);
$routes->options('comments/(:uuid)', 'ActivityPubController::preflight');
$routes->get('comments/(:uuid)', 'EpisodeCommentController::view/$1/$2/$3', [
'as' => 'episode-comment',
'application/activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/podcast-activity+json' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'controller-method' => 'EpisodeController::commentObject/$1/$2',
],
'filter' => 'allow-cors',
]);
$routes->get('comments/(:uuid)/replies', 'EpisodeCommentController::replies/$1/$2/$3', [
'as' => 'episode-comment-replies',
]);
$routes->post('comments/(:uuid)/like', 'EpisodeCommentController::likeAction/$1/$2/$3', [
'as' => 'episode-comment-attempt-like',
]); ]);
$routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [ $routes->get('oembed.json', 'EpisodeController::oembedJSON/$1/$2', [
'as' => 'episode-oembed-json', 'as' => 'episode-oembed-json',
@ -168,139 +758,144 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
$routes->get('oembed.xml', 'EpisodeController::oembedXML/$1/$2', [ $routes->get('oembed.xml', 'EpisodeController::oembedXML/$1/$2', [
'as' => 'episode-oembed-xml', 'as' => 'episode-oembed-xml',
]); ]);
$routes->group('embed', static function ($routes): void { $routes->group('embeddable-player', function ($routes): void {
$routes->get('/', 'EpisodeController::embed/$1/$2', [ $routes->get('/', 'EpisodeController::embeddablePlayer/$1/$2', [
'as' => 'embed', 'as' => 'embeddable-player',
]); ]);
$routes->get('(:embedTheme)', 'EpisodeController::embed/$1/$2/$3', [ $routes->get(
'as' => 'embed-theme', '(:embeddablePlayerTheme)',
],); 'EpisodeController::embeddablePlayer/$1/$2/$3',
[
'as' => 'embeddable-player-theme',
],
);
}); });
}); });
$routes->head('feed.xml', 'FeedController::index/$1', [
'as' => 'podcast-rss-feed', $routes->head('feed.xml', 'FeedController/$1', [
'as' => 'podcast_feed',
]); ]);
$routes->get('feed.xml', 'FeedController::index/$1', [ $routes->get('feed.xml', 'FeedController/$1', [
'as' => 'podcast-rss-feed', 'as' => 'podcast_feed',
]); ]);
$routes->head('feed', 'FeedController::index/$1');
$routes->get('feed', 'FeedController::index/$1');
}); });
// audio routes
$routes->head('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioController::index/$1/$2', [
'as' => 'episode-audio',
], );
$routes->get('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioController::index/$1/$2', [
'as' => 'episode-audio',
], );
// episode preview link
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
'as' => 'episode-preview',
]);
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
'as' => 'episode-preview-activity',
]);
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
'as' => 'episode-preview-chapters',
]);
$routes->get('/p/(:uuid)/transcript', 'EpisodePreviewController::transcript/$1', [
'as' => 'episode-preview-transcript',
]);
// Other pages // Other pages
$routes->get('/credits', 'CreditsController', [ $routes->get('/credits', 'CreditsController', [
'as' => 'credits', 'as' => 'credits',
]); ]);
$routes->get('/map', 'MapController', [ $routes->get('/map', 'MapMarkerController', [
'as' => 'map', 'as' => 'map',
]); ]);
$routes->get('/episodes-markers', 'MapController::getEpisodesMarkers', [ $routes->get('/episodes-markers', 'MapMarkerController::getEpisodesMarkers', [
'as' => 'episodes-markers', 'as' => 'episodes-markers',
]); ]);
$routes->get('/pages/(:slug)', 'PageController::index/$1', [ $routes->get('/pages/(:slug)', 'PageController/$1', [
'as' => 'page', 'as' => 'page',
]); ]);
// interacting as an actor
$routes->post('interact-as-actor', 'AuthController::attemptInteractAsActor', [
'as' => 'interact-as-actor',
]);
/** /**
* Overwriting Fediverse routes file * Overwriting ActivityPub routes file
*/ */
$routes->group('@(:podcastHandle)', static function ($routes): void { $routes->group('@(:podcastName)', function ($routes): void {
$routes->post('posts/new', 'PostController::createAction/$1', [ $routes->post('statuses/new', 'StatusController::attemptCreate/$1', [
'as' => 'post-attempt-create', 'as' => 'status-attempt-create',
'filter' => 'permission:podcast$1.manage-publications', 'filter' => 'permission:podcast-manage_publications',
]); ]);
// Post // Status
$routes->group('posts/(:uuid)', static function ($routes): void { $routes->group('statuses/(:uuid)', function ($routes): void {
$routes->options('/', 'ActivityPubController::preflight'); $routes->options('/', 'ActivityPubController::preflight');
$routes->get('/', 'PostController::view/$1/$2', [ $routes->get('/', 'StatusController::view/$1/$2', [
'as' => 'post', 'as' => 'status',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'PostController::index/$2', 'controller-method' => 'StatusController/$2',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'PostController::index/$2', 'controller-method' => 'StatusController/$2',
], ],
], ],
'filter' => 'allow-cors',
]); ]);
$routes->options('replies', 'ActivityPubController::preflight'); $routes->options('replies', 'ActivityPubController::preflight');
$routes->get('replies', 'PostController::index/$1/$2', [ $routes->get('replies', 'StatusController/$1/$2', [
'as' => 'post-replies', 'as' => 'status-replies',
'alternate-content' => [ 'alternate-content' => [
'application/activity+json' => [ 'application/activity+json' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'PostController::replies/$2', 'controller-method' => 'StatusController::replies/$2',
], ],
'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [ 'application/ld+json; profile="https://www.w3.org/ns/activitystreams' => [
'namespace' => 'Modules\Fediverse\Controllers', 'namespace' => 'ActivityPub\Controllers',
'controller-method' => 'PostController::replies/$2', 'controller-method' => 'StatusController::replies/$2',
], ],
], ],
'filter' => 'allow-cors',
]); ]);
// Actions // Actions
$routes->post('action', 'PostController::action/$1/$2', [ $routes->post('action', 'StatusController::attemptAction/$1/$2', [
'as' => 'post-attempt-action', 'as' => 'status-attempt-action',
'filter' => 'permission:podcast$1.interact-as', 'filter' => 'permission:podcast-interact_as',
]); ]);
$routes->post( $routes->post(
'block-actor', 'block-actor',
'PostController::blockActorAction/$1/$2', 'StatusController::attemptBlockActor/$1/$2',
[ [
'as' => 'post-attempt-block-actor', 'as' => 'status-attempt-block-actor',
'filter' => 'permission:fediverse.manage-blocks', 'filter' => 'permission:fediverse-block_actors',
], ],
); );
$routes->post( $routes->post(
'block-domain', 'block-domain',
'PostController::blockDomainAction/$1/$2', 'StatusController::attemptBlockDomain/$1/$2',
[ [
'as' => 'post-attempt-block-domain', 'as' => 'status-attempt-block-domain',
'filter' => 'permission:fediverse.manage-blocks', 'filter' => 'permission:fediverse-block_domains',
], ],
); );
$routes->post('delete', 'PostController::deleteAction/$1/$2', [ $routes->post('delete', 'StatusController::attemptDelete/$1/$2', [
'as' => 'post-attempt-delete', 'as' => 'status-attempt-delete',
'filter' => 'permission:podcast$1.manage-publications', 'filter' => 'permission:podcast-manage_publications',
]); ]);
$routes->get( $routes->get(
'remote/(:postAction)', 'remote/(:statusAction)',
'PostController::remoteActionAction/$1/$2/$3', 'StatusController::remoteAction/$1/$2/$3',
[ [
'as' => 'post-remote-action', 'as' => 'status-remote-action',
], ],
); );
}); });
$routes->get('follow', 'ActorController::followView/$1', [
$routes->get('follow', 'ActorController::follow/$1', [
'as' => 'follow', 'as' => 'follow',
]); ]);
$routes->get('outbox', 'ActorController::outbox/$1', [ $routes->get('outbox', 'ActorController::outbox/$1', [
'as' => 'outbox', 'as' => 'outbox',
'filter' => 'fediverse:verify-activitystream', 'filter' => 'activity-pub:verify-activitystream',
]); ]);
}); });
/*
* --------------------------------------------------------------------
* Additional Routing
* --------------------------------------------------------------------
*
* There will often be times that you need additional routing and you
* need it to be able to override any defaults in this file. Environment
* based routes is one such time. require() additional route files here
* to make that happen.
*
* You will have access to the $routes object within that file without
* needing to reload it.
*/
if (file_exists(APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php')) {
require APPPATH . 'Config/' . ENVIRONMENT . '/Routes.php';
}

View file

@ -1,151 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\Routing as BaseRouting;
/**
* Routing configuration
*/
class Routing extends BaseRouting
{
/**
* For Defined Routes.
* An array of files that contain route definitions.
* Route files are read in order, with the first match
* found taking precedence.
*
* Default: APPPATH . 'Config/Routes.php'
*
* @var list<string>
*/
public array $routeFiles = [
APPPATH . 'Config/Routes.php',
ROOTPATH . 'modules/Admin/Config/Routes.php',
ROOTPATH . 'modules/Analytics/Config/Routes.php',
ROOTPATH . 'modules/Api/Rest/V1/Config/Routes.php',
ROOTPATH . 'modules/Auth/Config/Routes.php',
ROOTPATH . 'modules/Fediverse/Config/Routes.php',
ROOTPATH . 'modules/Install/Config/Routes.php',
ROOTPATH . 'modules/Platforms/Config/Routes.php',
ROOTPATH . 'modules/PodcastImport/Config/Routes.php',
ROOTPATH . 'modules/PremiumPodcasts/Config/Routes.php',
];
/**
* For Defined Routes and Auto Routing.
* The default namespace to use for Controllers when no other
* namespace has been specified.
*
* Default: 'App\Controllers'
*/
public string $defaultNamespace = 'App\Controllers';
/**
* For Auto Routing.
* The default controller to use when no other controller has been
* specified.
*
* Default: 'Home'
*/
public string $defaultController = 'HomeController';
/**
* For Defined Routes and Auto Routing.
* The default method to call on the controller when no other
* method has been set in the route.
*
* Default: 'index'
*/
public string $defaultMethod = 'index';
/**
* For Auto Routing.
* Whether to translate dashes in URIs for controller/method to underscores.
* Primarily useful when using the auto-routing.
*
* Default: false
*/
public bool $translateURIDashes = false;
/**
* Sets the class/method that should be called if routing doesn't
* find a match. It can be the controller/method name like: Users::index
*
* This setting is passed to the Router class and handled there.
*
* If you want to use a closure, you will have to set it in the
* routes file by calling:
*
* $routes->set404Override(function() {
* // Do something here
* });
*
* Example:
* public $override404 = 'App\Errors::show404';
*/
public ?string $override404 = null;
/**
* If TRUE, the system will attempt to match the URI against
* Controllers by matching each segment against folders/files
* in APPPATH/Controllers, when a match wasn't found against
* defined routes.
*
* If FALSE, will stop searching and do NO automatic routing.
*/
public bool $autoRoute = false;
/**
* If TRUE, the system will look for attributes on controller
* class and methods that can run before and after the
* controller/method.
*
* If FALSE, will ignore any attributes.
*/
public bool $useControllerAttributes = true;
/**
* For Defined Routes.
* If TRUE, will enable the use of the 'prioritize' option
* when defining routes.
*
* Default: false
*/
public bool $prioritize = false;
/**
* For Defined Routes.
* If TRUE, matched multiple URI segments will be passed as one parameter.
*
* Default: false
*/
public bool $multipleSegmentsOneParam = false;
/**
* For Auto Routing (Improved).
* Map of URI segments and namespaces.
*
* The key is the first URI segment. The value is the controller namespace.
* E.g.,
* [
* 'blog' => 'Acme\Blog\Controllers',
* ]
*
* @var array<string, string> [ uri_segment => namespace ]
*/
public array $moduleRoutes = [];
/**
* For Auto Routing (Improved).
* Whether to translate dashes in URIs for controller/method to CamelCase.
* E.g., blog-controller -> BlogController
*
* If you enable this, $translateURIDashes is ignored.
*
* Default: false
*/
public bool $translateUriToCamelCase = true;
}

View file

@ -8,32 +8,12 @@ use CodeIgniter\Config\BaseConfig;
class Security extends BaseConfig class Security extends BaseConfig
{ {
/**
* --------------------------------------------------------------------------
* CSRF Protection Method
* --------------------------------------------------------------------------
*
* Protection Method for Cross Site Request Forgery protection.
*
* @var 'cookie'|'session'
*/
public string $csrfProtection = 'session';
/**
* --------------------------------------------------------------------------
* CSRF Token Randomization
* --------------------------------------------------------------------------
*
* Randomize the CSRF Token for added security.
*/
public bool $tokenRandomize = true;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* CSRF Token Name * CSRF Token Name
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Token name for Cross Site Request Forgery protection. * Token name for Cross Site Request Forgery protection cookie.
*/ */
public string $tokenName = 'csrf_test_name'; public string $tokenName = 'csrf_test_name';
@ -42,7 +22,7 @@ class Security extends BaseConfig
* CSRF Header Name * CSRF Header Name
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Header name for Cross Site Request Forgery protection. * Token name for Cross Site Request Forgery protection cookie.
*/ */
public string $headerName = 'X-CSRF-TOKEN'; public string $headerName = 'X-CSRF-TOKEN';
@ -51,7 +31,7 @@ class Security extends BaseConfig
* CSRF Cookie Name * CSRF Cookie Name
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Cookie name for Cross Site Request Forgery protection. * Cookie name for Cross Site Request Forgery protection cookie.
*/ */
public string $cookieName = 'csrf_cookie_name'; public string $cookieName = 'csrf_cookie_name';
@ -71,7 +51,7 @@ class Security extends BaseConfig
* CSRF Regenerate * CSRF Regenerate
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* Regenerate CSRF Token on every submission. * Regenerate CSRF Token on every request.
*/ */
public bool $regenerate = true; public bool $regenerate = true;
@ -80,7 +60,7 @@ class Security extends BaseConfig
* CSRF Redirect * CSRF Redirect
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* *
* @see https://codeigniter4.github.io/userguide/libraries/security.html#redirection-on-failure * Redirect to previous page with error on failure.
*/ */
public bool $redirect = (ENVIRONMENT === 'production'); public bool $redirect = true;
} }

View file

@ -4,16 +4,20 @@ declare(strict_types=1);
namespace Config; namespace Config;
use App\Authorization\FlatAuthorization;
use App\Authorization\GroupModel;
use App\Authorization\PermissionModel;
use App\Libraries\Breadcrumb; use App\Libraries\Breadcrumb;
use App\Libraries\HtmlHead;
use App\Libraries\Negotiate; use App\Libraries\Negotiate;
use App\Libraries\Router; use App\Libraries\Router;
use App\Libraries\Vite;
use App\Models\UserModel;
use CodeIgniter\Config\BaseService; use CodeIgniter\Config\BaseService;
use CodeIgniter\HTTP\Negotiate as CodeIgniterHTTPNegotiate;
use CodeIgniter\HTTP\Request; use CodeIgniter\HTTP\Request;
use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\Model;
use CodeIgniter\Router\RouteCollectionInterface; use CodeIgniter\Router\RouteCollectionInterface;
use CodeIgniter\Router\Router as CodeIgniterRouter; use Myth\Auth\Models\LoginModel;
/** /**
* Services Configuration file. * Services Configuration file.
@ -30,18 +34,20 @@ class Services extends BaseService
/** /**
* The Router class uses a RouteCollection's array of routes, and determines the correct Controller and Method to * The Router class uses a RouteCollection's array of routes, and determines the correct Controller and Method to
* execute. * execute.
*
* @noRector PHPStan\Reflection\MissingMethodFromReflectionException
*/ */
public static function router( public static function router(
?RouteCollectionInterface $routes = null, ?RouteCollectionInterface $routes = null,
?Request $request = null, ?Request $request = null,
bool $getShared = true, bool $getShared = true
): CodeIgniterRouter { ): Router {
if ($getShared) { if ($getShared) {
return static::getSharedInstance('router', $routes, $request); return static::getSharedInstance('router', $routes, $request);
} }
$routes ??= static::routes(); $routes = $routes ?? static::routes();
$request ??= static::request(); $request = $request ?? static::request();
return new Router($routes, $request); return new Router($routes, $request);
} }
@ -49,20 +55,82 @@ class Services extends BaseService
/** /**
* The Negotiate class provides the content negotiation features for working the request to determine correct * The Negotiate class provides the content negotiation features for working the request to determine correct
* language, encoding, charset, and more. * language, encoding, charset, and more.
*
* @noRector PHPStan\Reflection\MissingMethodFromReflectionException
*/ */
public static function negotiator( public static function negotiator(?RequestInterface $request = null, bool $getShared = true): Negotiate
?RequestInterface $request = null, {
bool $getShared = true,
): CodeIgniterHTTPNegotiate {
if ($getShared) { if ($getShared) {
return static::getSharedInstance('negotiator', $request); return static::getSharedInstance('negotiator', $request);
} }
$request ??= static::request(); $request = $request ?? static::request();
return new Negotiate($request); return new Negotiate($request);
} }
/**
* @return mixed
*/
public static function authentication(
string $lib = 'local',
Model $userModel = null,
Model $loginModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance('authentication', $lib, $userModel, $loginModel);
}
// config() checks first in app/Config
$config = config('Auth');
$class = $config->authenticationLibs[$lib];
$instance = new $class($config);
if ($userModel === null) {
$userModel = new UserModel();
}
if ($loginModel === null) {
$loginModel = new LoginModel();
}
return $instance->setUserModel($userModel)
->setLoginModel($loginModel);
}
/**
* @return mixed
*/
public static function authorization(
Model $groupModel = null,
Model $permissionModel = null,
Model $userModel = null,
bool $getShared = true
) {
if ($getShared) {
return self::getSharedInstance('authorization', $groupModel, $permissionModel, $userModel);
}
if ($groupModel === null) {
$groupModel = new GroupModel();
}
if ($permissionModel === null) {
$permissionModel = new PermissionModel();
}
$instance = new FlatAuthorization($groupModel, $permissionModel);
if ($userModel === null) {
$userModel = new UserModel();
}
return $instance->setUserModel($userModel);
}
public static function breadcrumb(bool $getShared = true): Breadcrumb public static function breadcrumb(bool $getShared = true): Breadcrumb
{ {
if ($getShared) { if ($getShared) {
@ -72,12 +140,12 @@ class Services extends BaseService
return new Breadcrumb(); return new Breadcrumb();
} }
public static function html_head(bool $getShared = true): HtmlHead public static function vite(bool $getShared = true): Vite
{ {
if ($getShared) { if ($getShared) {
return self::getSharedInstance('html_head'); return self::getSharedInstance('vite');
} }
return new HtmlHead(); return new Vite();
} }
} }

View file

@ -1,130 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Session\Handlers\BaseHandler;
use CodeIgniter\Session\Handlers\FileHandler;
class Session extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Session Driver
* --------------------------------------------------------------------------
*
* The session storage driver to use:
* - `CodeIgniter\Session\Handlers\ArrayHandler` (for testing)
* - `CodeIgniter\Session\Handlers\FileHandler`
* - `CodeIgniter\Session\Handlers\DatabaseHandler`
* - `CodeIgniter\Session\Handlers\MemcachedHandler`
* - `CodeIgniter\Session\Handlers\RedisHandler`
*
* @var class-string<BaseHandler>
*/
public string $driver = FileHandler::class;
/**
* --------------------------------------------------------------------------
* Session Cookie Name
* --------------------------------------------------------------------------
*
* The session cookie name, must contain only [0-9a-z_-] characters
*/
public string $cookieName = 'ci_session';
/**
* --------------------------------------------------------------------------
* Session Expiration
* --------------------------------------------------------------------------
*
* The number of SECONDS you want the session to last.
* Setting to 0 (zero) means expire when the browser is closed.
*/
public int $expiration = 7200;
/**
* --------------------------------------------------------------------------
* Session Save Path
* --------------------------------------------------------------------------
*
* The location to save sessions to and is driver dependent.
*
* For the 'files' driver, it's a path to a writable directory.
* WARNING: Only absolute paths are supported!
*
* For the 'database' driver, it's a table name.
* Please read up the manual for the format with other session drivers.
*
* IMPORTANT: You are REQUIRED to set a valid save path!
*/
public string $savePath = WRITEPATH . 'session';
/**
* --------------------------------------------------------------------------
* Session Match IP
* --------------------------------------------------------------------------
*
* Whether to match the user's IP address when reading the session data.
*
* WARNING: If you're using the database driver, don't forget to update
* your session table's PRIMARY KEY when changing this setting.
*/
public bool $matchIP = false;
/**
* --------------------------------------------------------------------------
* Session Time to Update
* --------------------------------------------------------------------------
*
* How many seconds between CI regenerating the session ID.
*/
public int $timeToUpdate = 300;
/**
* --------------------------------------------------------------------------
* Session Regenerate Destroy
* --------------------------------------------------------------------------
*
* Whether to destroy session data associated with the old session ID
* when auto-regenerating the session ID. When set to FALSE, the data
* will be later deleted by the garbage collector.
*/
public bool $regenerateDestroy = false;
/**
* --------------------------------------------------------------------------
* Session Database Group
* --------------------------------------------------------------------------
*
* DB Group for the database session.
*/
public ?string $DBGroup = null;
/**
* --------------------------------------------------------------------------
* Lock Retry Interval (microseconds)
* --------------------------------------------------------------------------
*
* This is used for RedisHandler.
*
* Time (microseconds) to wait if lock cannot be acquired.
* The default is 100,000 microseconds (= 0.1 seconds).
*/
public int $lockRetryInterval = 100_000;
/**
* --------------------------------------------------------------------------
* Lock Max Retries
* --------------------------------------------------------------------------
*
* This is used for RedisHandler.
*
* Maximum number of lock acquisition attempts.
* The default is 300 times. That is lock timeout is about 30 (0.1 * 300)
* seconds.
*/
public int $lockMaxRetries = 300;
}

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Tasks\Scheduler;
class Tasks extends BaseConfig
{
/**
* --------------------------------------------------------------------------
* Should performance metrics be logged
* --------------------------------------------------------------------------
*
* If true, will log the time it takes for each task to run.
* Requires the settings table to have been created previously.
*/
public bool $logPerformance = false;
/**
* --------------------------------------------------------------------------
* Maximum performance logs
* --------------------------------------------------------------------------
*
* The maximum number of logs that should be saved per Task.
* Lower numbers reduced the amount of database required to
* store the logs.
*/
public int $maxLogsPerTask = 10;
/**
* Register any tasks within this method for the application.
* Called by the TaskRunner.
*/
public function init(Scheduler $schedule): void
{
$schedule->command('fediverse:broadcast')
->everyMinute()
->named('fediverse-broadcast');
$schedule->command('websub:publish')
->everyMinute()
->named('websub-publish');
$schedule->command('video-clips:generate')
->everyMinute()
->named('video-clips-generate');
$schedule->command('podcast:import')
->everyMinute()
->named('podcast-import');
$schedule->command('episodes:compute-downloads')
->everyHour()
->named('episodes:compute-downloads');
}
}

View file

@ -33,7 +33,7 @@ class Toolbar extends BaseConfig
* List of toolbar collectors that will be called when Debug Toolbar * List of toolbar collectors that will be called when Debug Toolbar
* fires up and collects data from. * fires up and collects data from.
* *
* @var list<class-string> * @var string[]
*/ */
public array $collectors = [ public array $collectors = [
Timers::class, Timers::class,
@ -46,16 +46,6 @@ class Toolbar extends BaseConfig
Events::class, Events::class,
]; ];
/**
* --------------------------------------------------------------------------
* Collect Var Data
* --------------------------------------------------------------------------
*
* If set to false var data from the views will not be collected. Useful to
* avoid high memory usage when there are lots of data passed to the view.
*/
public bool $collectVarData = true;
/** /**
* -------------------------------------------------------------------------- * --------------------------------------------------------------------------
* Max History * Max History
@ -90,56 +80,4 @@ class Toolbar extends BaseConfig
* `$maxQueries` defines the maximum amount of queries that will be stored. * `$maxQueries` defines the maximum amount of queries that will be stored.
*/ */
public int $maxQueries = 100; public int $maxQueries = 100;
/**
* --------------------------------------------------------------------------
* Watched Directories
* --------------------------------------------------------------------------
*
* Contains an array of directories that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
* We restrict the values to keep performance as high as possible.
*
* NOTE: The ROOTPATH will be prepended to all values.
*
* @var list<string>
*/
public array $watchedDirectories = ['app', 'modules', 'themes'];
/**
* --------------------------------------------------------------------------
* Watched File Extensions
* --------------------------------------------------------------------------
*
* Contains an array of file extensions that will be watched for changes and
* used to determine if the hot-reload feature should reload the page or not.
*
* @var list<string>
*/
public array $watchedExtensions = ['php', 'css', 'js', 'html', 'svg', 'json', 'env'];
/**
* --------------------------------------------------------------------------
* Ignored HTTP Headers
* --------------------------------------------------------------------------
*
* CodeIgniter Debug Toolbar normally injects HTML and JavaScript into every
* HTML response. This is correct for full page loads, but it breaks requests
* that expect only a clean HTML fragment.
*
* Libraries like HTMX, Unpoly, and Hotwire (Turbo) update parts of the page or
* manage navigation on the client side. Injecting the Debug Toolbar into their
* responses can cause invalid HTML, duplicated scripts, or JavaScript errors
* (such as infinite loops or "Maximum call stack size exceeded").
*
* Any request containing one of the following headers is treated as a
* client-managed or partial request, and the Debug Toolbar injection is skipped.
*
* @var array<string, string|null>
*/
public array $disableOnHeaders = [
'X-Requested-With' => 'xmlhttprequest', // AJAX requests
'HX-Request' => 'true', // HTMX requests
'X-Up-Version' => null, // Unpoly partial requests
];
} }

View file

@ -27,47 +27,47 @@ class UserAgents extends BaseConfig
*/ */
public array $platforms = [ public array $platforms = [
'windows nt 10.0' => 'Windows 10', 'windows nt 10.0' => 'Windows 10',
'windows nt 6.3' => 'Windows 8.1', 'windows nt 6.3' => 'Windows 8.1',
'windows nt 6.2' => 'Windows 8', 'windows nt 6.2' => 'Windows 8',
'windows nt 6.1' => 'Windows 7', 'windows nt 6.1' => 'Windows 7',
'windows nt 6.0' => 'Windows Vista', 'windows nt 6.0' => 'Windows Vista',
'windows nt 5.2' => 'Windows 2003', 'windows nt 5.2' => 'Windows 2003',
'windows nt 5.1' => 'Windows XP', 'windows nt 5.1' => 'Windows XP',
'windows nt 5.0' => 'Windows 2000', 'windows nt 5.0' => 'Windows 2000',
'windows nt 4.0' => 'Windows NT 4.0', 'windows nt 4.0' => 'Windows NT 4.0',
'winnt4.0' => 'Windows NT 4.0', 'winnt4.0' => 'Windows NT 4.0',
'winnt 4.0' => 'Windows NT', 'winnt 4.0' => 'Windows NT',
'winnt' => 'Windows NT', 'winnt' => 'Windows NT',
'windows 98' => 'Windows 98', 'windows 98' => 'Windows 98',
'win98' => 'Windows 98', 'win98' => 'Windows 98',
'windows 95' => 'Windows 95', 'windows 95' => 'Windows 95',
'win95' => 'Windows 95', 'win95' => 'Windows 95',
'windows phone' => 'Windows Phone', 'windows phone' => 'Windows Phone',
'windows' => 'Unknown Windows OS', 'windows' => 'Unknown Windows OS',
'android' => 'Android', 'android' => 'Android',
'blackberry' => 'BlackBerry', 'blackberry' => 'BlackBerry',
'iphone' => 'iOS', 'iphone' => 'iOS',
'ipad' => 'iOS', 'ipad' => 'iOS',
'ipod' => 'iOS', 'ipod' => 'iOS',
'os x' => 'Mac OS X', 'os x' => 'Mac OS X',
'ppc mac' => 'Power PC Mac', 'ppc mac' => 'Power PC Mac',
'freebsd' => 'FreeBSD', 'freebsd' => 'FreeBSD',
'ppc' => 'Macintosh', 'ppc' => 'Macintosh',
'linux' => 'Linux', 'linux' => 'Linux',
'debian' => 'Debian', 'debian' => 'Debian',
'sunos' => 'Sun Solaris', 'sunos' => 'Sun Solaris',
'beos' => 'BeOS', 'beos' => 'BeOS',
'apachebench' => 'ApacheBench', 'apachebench' => 'ApacheBench',
'aix' => 'AIX', 'aix' => 'AIX',
'irix' => 'Irix', 'irix' => 'Irix',
'osf' => 'DEC OSF', 'osf' => 'DEC OSF',
'hp-ux' => 'HP-UX', 'hp-ux' => 'HP-UX',
'netbsd' => 'NetBSD', 'netbsd' => 'NetBSD',
'bsdi' => 'BSDi', 'bsdi' => 'BSDi',
'openbsd' => 'OpenBSD', 'openbsd' => 'OpenBSD',
'gnu' => 'GNU/Linux', 'gnu' => 'GNU/Linux',
'unix' => 'Unknown Unix OS', 'unix' => 'Unknown Unix OS',
'symbian' => 'Symbian OS', 'symbian' => 'Symbian OS',
]; ];
/** /**
@ -81,37 +81,37 @@ class UserAgents extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $browsers = [ public array $browsers = [
'OPR' => 'Opera', 'OPR' => 'Opera',
'Flock' => 'Flock', 'Flock' => 'Flock',
'Edge' => 'Spartan', 'Edge' => 'Spartan',
'Edg' => 'Edge', 'Edg' => 'Edge',
'Chrome' => 'Chrome', 'Chrome' => 'Chrome',
// Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string // Opera 10+ always reports Opera/9.80 and appends Version/<real version> to the user agent string
'Opera.*?Version' => 'Opera', 'Opera.*?Version' => 'Opera',
'Opera' => 'Opera', 'Opera' => 'Opera',
'MSIE' => 'Internet Explorer', 'MSIE' => 'Internet Explorer',
'Internet Explorer' => 'Internet Explorer', 'Internet Explorer' => 'Internet Explorer',
'Trident.* rv' => 'Internet Explorer', 'Trident.* rv' => 'Internet Explorer',
'Shiira' => 'Shiira', 'Shiira' => 'Shiira',
'Firefox' => 'Firefox', 'Firefox' => 'Firefox',
'Chimera' => 'Chimera', 'Chimera' => 'Chimera',
'Phoenix' => 'Phoenix', 'Phoenix' => 'Phoenix',
'Firebird' => 'Firebird', 'Firebird' => 'Firebird',
'Camino' => 'Camino', 'Camino' => 'Camino',
'Netscape' => 'Netscape', 'Netscape' => 'Netscape',
'OmniWeb' => 'OmniWeb', 'OmniWeb' => 'OmniWeb',
'Safari' => 'Safari', 'Safari' => 'Safari',
'Mozilla' => 'Mozilla', 'Mozilla' => 'Mozilla',
'Konqueror' => 'Konqueror', 'Konqueror' => 'Konqueror',
'icab' => 'iCab', 'icab' => 'iCab',
'Lynx' => 'Lynx', 'Lynx' => 'Lynx',
'Links' => 'Links', 'Links' => 'Links',
'hotjava' => 'HotJava', 'hotjava' => 'HotJava',
'amaya' => 'Amaya', 'amaya' => 'Amaya',
'IBrowse' => 'IBrowse', 'IBrowse' => 'IBrowse',
'Maxthon' => 'Maxthon', 'Maxthon' => 'Maxthon',
'Ubuntu' => 'Ubuntu Web Browser', 'Ubuntu' => 'Ubuntu Web Browser',
'Vivaldi' => 'Vivaldi', 'Vivaldi' => 'Vivaldi',
]; ];
/** /**
@ -139,86 +139,86 @@ class UserAgents extends BaseConfig
// 'motorola' => 'Motorola' // 'motorola' => 'Motorola'
// Phones and Manufacturers // Phones and Manufacturers
'motorola' => 'Motorola', 'motorola' => 'Motorola',
'nokia' => 'Nokia', 'nokia' => 'Nokia',
'palm' => 'Palm', 'palm' => 'Palm',
'iphone' => 'Apple iPhone', 'iphone' => 'Apple iPhone',
'ipad' => 'iPad', 'ipad' => 'iPad',
'ipod' => 'Apple iPod Touch', 'ipod' => 'Apple iPod Touch',
'sony' => 'Sony Ericsson', 'sony' => 'Sony Ericsson',
'ericsson' => 'Sony Ericsson', 'ericsson' => 'Sony Ericsson',
'blackberry' => 'BlackBerry', 'blackberry' => 'BlackBerry',
'cocoon' => 'O2 Cocoon', 'cocoon' => 'O2 Cocoon',
'blazer' => 'Treo', 'blazer' => 'Treo',
'lg' => 'LG', 'lg' => 'LG',
'amoi' => 'Amoi', 'amoi' => 'Amoi',
'xda' => 'XDA', 'xda' => 'XDA',
'mda' => 'MDA', 'mda' => 'MDA',
'vario' => 'Vario', 'vario' => 'Vario',
'htc' => 'HTC', 'htc' => 'HTC',
'samsung' => 'Samsung', 'samsung' => 'Samsung',
'sharp' => 'Sharp', 'sharp' => 'Sharp',
'sie-' => 'Siemens', 'sie-' => 'Siemens',
'alcatel' => 'Alcatel', 'alcatel' => 'Alcatel',
'benq' => 'BenQ', 'benq' => 'BenQ',
'ipaq' => 'HP iPaq', 'ipaq' => 'HP iPaq',
'mot-' => 'Motorola', 'mot-' => 'Motorola',
'playstation portable' => 'PlayStation Portable', 'playstation portable' => 'PlayStation Portable',
'playstation 3' => 'PlayStation 3', 'playstation 3' => 'PlayStation 3',
'playstation vita' => 'PlayStation Vita', 'playstation vita' => 'PlayStation Vita',
'hiptop' => 'Danger Hiptop', 'hiptop' => 'Danger Hiptop',
'nec-' => 'NEC', 'nec-' => 'NEC',
'panasonic' => 'Panasonic', 'panasonic' => 'Panasonic',
'philips' => 'Philips', 'philips' => 'Philips',
'sagem' => 'Sagem', 'sagem' => 'Sagem',
'sanyo' => 'Sanyo', 'sanyo' => 'Sanyo',
'spv' => 'SPV', 'spv' => 'SPV',
'zte' => 'ZTE', 'zte' => 'ZTE',
'sendo' => 'Sendo', 'sendo' => 'Sendo',
'nintendo dsi' => 'Nintendo DSi', 'nintendo dsi' => 'Nintendo DSi',
'nintendo ds' => 'Nintendo DS', 'nintendo ds' => 'Nintendo DS',
'nintendo 3ds' => 'Nintendo 3DS', 'nintendo 3ds' => 'Nintendo 3DS',
'wii' => 'Nintendo Wii', 'wii' => 'Nintendo Wii',
'open web' => 'Open Web', 'open web' => 'Open Web',
'openweb' => 'OpenWeb', 'openweb' => 'OpenWeb',
// Operating Systems // Operating Systems
'android' => 'Android', 'android' => 'Android',
'symbian' => 'Symbian', 'symbian' => 'Symbian',
'SymbianOS' => 'SymbianOS', 'SymbianOS' => 'SymbianOS',
'elaine' => 'Palm', 'elaine' => 'Palm',
'series60' => 'Symbian S60', 'series60' => 'Symbian S60',
'windows ce' => 'Windows CE', 'windows ce' => 'Windows CE',
// Browsers // Browsers
'obigo' => 'Obigo', 'obigo' => 'Obigo',
'netfront' => 'Netfront Browser', 'netfront' => 'Netfront Browser',
'openwave' => 'Openwave Browser', 'openwave' => 'Openwave Browser',
'mobilexplorer' => 'Mobile Explorer', 'mobilexplorer' => 'Mobile Explorer',
'operamini' => 'Opera Mini', 'operamini' => 'Opera Mini',
'opera mini' => 'Opera Mini', 'opera mini' => 'Opera Mini',
'opera mobi' => 'Opera Mobile', 'opera mobi' => 'Opera Mobile',
'fennec' => 'Firefox Mobile', 'fennec' => 'Firefox Mobile',
// Other // Other
'digital paths' => 'Digital Paths', 'digital paths' => 'Digital Paths',
'avantgo' => 'AvantGo', 'avantgo' => 'AvantGo',
'xiino' => 'Xiino', 'xiino' => 'Xiino',
'novarra' => 'Novarra Transcoder', 'novarra' => 'Novarra Transcoder',
'vodafone' => 'Vodafone', 'vodafone' => 'Vodafone',
'docomo' => 'NTT DoCoMo', 'docomo' => 'NTT DoCoMo',
'o2' => 'O2', 'o2' => 'O2',
// Fallback // Fallback
'mobile' => 'Generic Mobile', 'mobile' => 'Generic Mobile',
'wireless' => 'Generic Mobile', 'wireless' => 'Generic Mobile',
'j2me' => 'Generic Mobile', 'j2me' => 'Generic Mobile',
'midp' => 'Generic Mobile', 'midp' => 'Generic Mobile',
'cldc' => 'Generic Mobile', 'cldc' => 'Generic Mobile',
'up.link' => 'Generic Mobile', 'up.link' => 'Generic Mobile',
'up.browser' => 'Generic Mobile', 'up.browser' => 'Generic Mobile',
'smartphone' => 'Generic Mobile', 'smartphone' => 'Generic Mobile',
'cellphone' => 'Generic Mobile', 'cellphone' => 'Generic Mobile',
]; ];
/** /**
@ -231,34 +231,24 @@ class UserAgents extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $robots = [ public array $robots = [
'googlebot' => 'Googlebot', 'googlebot' => 'Googlebot',
'google-pagerenderer' => 'Google Page Renderer', 'msnbot' => 'MSNBot',
'google-read-aloud' => 'Google Read Aloud', 'baiduspider' => 'Baiduspider',
'google-safety' => 'Google Safety Bot', 'bingbot' => 'Bing',
'msnbot' => 'MSNBot', 'slurp' => 'Inktomi Slurp',
'baiduspider' => 'Baiduspider', 'yahoo' => 'Yahoo',
'bingbot' => 'Bing', 'ask jeeves' => 'Ask Jeeves',
'bingpreview' => 'BingPreview', 'fastcrawler' => 'FastCrawler',
'slurp' => 'Inktomi Slurp', 'infoseek' => 'InfoSeek Robot 1.0',
'yahoo' => 'Yahoo', 'lycos' => 'Lycos',
'ask jeeves' => 'Ask Jeeves', 'yandex' => 'YandexBot',
'fastcrawler' => 'FastCrawler',
'infoseek' => 'InfoSeek Robot 1.0',
'lycos' => 'Lycos',
'yandex' => 'YandexBot',
'mediapartners-google' => 'MediaPartners Google', 'mediapartners-google' => 'MediaPartners Google',
'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
'adsbot-google' => 'AdsBot Google', 'adsbot-google' => 'AdsBot Google',
'feedfetcher-google' => 'Feedfetcher Google', 'feedfetcher-google' => 'Feedfetcher Google',
'curious george' => 'Curious George', 'curious george' => 'Curious George',
'ia_archiver' => 'Alexa Crawler', 'ia_archiver' => 'Alexa Crawler',
'MJ12bot' => 'Majestic-12', 'MJ12bot' => 'Majestic-12',
'Uptimebot' => 'Uptimebot', 'Uptimebot' => 'Uptimebot',
'duckduckbot' => 'DuckDuckBot',
'sogou' => 'Sogou Spider',
'exabot' => 'Exabot',
'bot' => 'Generic Bot',
'crawler' => 'Generic Crawler',
'spider' => 'Generic Spider',
]; ];
} }

View file

@ -5,27 +5,28 @@ declare(strict_types=1);
namespace Config; namespace Config;
use App\Validation\FileRules as AppFileRules; use App\Validation\FileRules as AppFileRules;
use App\Validation\OtherRules; use App\Validation\Rules as AppRules;
use CodeIgniter\Config\BaseConfig; use CodeIgniter\Validation\CreditCardRules;
use CodeIgniter\Validation\StrictRules\CreditCardRules; use CodeIgniter\Validation\FileRules;
use CodeIgniter\Validation\StrictRules\FileRules; use CodeIgniter\Validation\FormatRules;
use CodeIgniter\Validation\StrictRules\FormatRules; use CodeIgniter\Validation\Rules;
use CodeIgniter\Validation\StrictRules\Rules; use Myth\Auth\Authentication\Passwords\ValidationRules as PasswordRules;
class Validation extends BaseConfig class Validation
{ {
/** /**
* Stores the classes that contain the rules that are available. * Stores the classes that contain the rules that are available.
* *
* @var list<string> * @var string[]
*/ */
public array $ruleSets = [ public array $ruleSets = [
Rules::class, Rules::class,
FormatRules::class, FormatRules::class,
FileRules::class, FileRules::class,
CreditCardRules::class, CreditCardRules::class,
AppRules::class,
AppFileRules::class, AppFileRules::class,
OtherRules::class, PasswordRules::class,
]; ];
/** /**
@ -34,7 +35,7 @@ class Validation extends BaseConfig
* @var array<string, string> * @var array<string, string>
*/ */
public array $templates = [ public array $templates = [
'list' => 'CodeIgniter\Validation\Views\list', 'list' => 'CodeIgniter\Validation\Views\list',
'single' => 'CodeIgniter\Validation\Views\single', 'single' => 'CodeIgniter\Validation\Views\single',
]; ];
} }

View file

@ -5,13 +5,7 @@ declare(strict_types=1);
namespace Config; namespace Config;
use CodeIgniter\Config\View as BaseView; use CodeIgniter\Config\View as BaseView;
use CodeIgniter\View\ViewDecoratorInterface;
use ViewComponents\Decorator;
/**
* @phpstan-type parser_callable (callable(mixed): mixed)
* @phpstan-type parser_callable_string (callable(mixed): mixed)&string
*/
class View extends BaseView class View extends BaseView
{ {
/** /**
@ -31,8 +25,7 @@ class View extends BaseView
* *
* Examples: { title|esc(js) } { created_on|date(Y-m-d)|esc(attr) } * Examples: { title|esc(js) } { created_on|date(Y-m-d)|esc(attr) }
* *
* @var array<string, string> * @var string[]
* @phpstan-var array<string, parser_callable_string>
*/ */
public $filters = []; public $filters = [];
@ -40,35 +33,7 @@ class View extends BaseView
* Parser Plugins provide a way to extend the functionality provided by the core Parser by creating aliases that * Parser Plugins provide a way to extend the functionality provided by the core Parser by creating aliases that
* will be replaced with any callable. Can be single or tag pair. * will be replaced with any callable. Can be single or tag pair.
* *
* @var array<string, callable|list<string>|string> * @var string[]
* @phpstan-var array<string, list<parser_callable_string>|parser_callable_string|parser_callable>
*/ */
public $plugins = []; public $plugins = [];
/**
* View Decorators are class methods that will be run in sequence to have a chance to alter the generated output
* just prior to caching the results.
*
* All classes must implement CodeIgniter\View\ViewDecoratorInterface
*
* @var list<class-string<ViewDecoratorInterface>>
*/
public array $decorators = [Decorator::class];
/**
* Subdirectory within app/Views for namespaced view overrides.
*
* Namespaced views will be searched in:
*
* app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...}
*
* This allows application-level overrides for package or module views
* without modifying vendor source files.
*
* Examples:
* 'overrides' -> app/Views/overrides/Example/Blog/post/card.php
* 'vendor' -> app/Views/vendor/Example/Blog/post/card.php
* '' -> app/Views/Example/Blog/post/card.php (direct mapping)
*/
public string $appOverridesFolder = 'overrides';
} }

View file

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
use ViewComponents\Config\ViewComponents as ViewComponentsConfig;
class ViewComponents extends ViewComponentsConfig
{
/**
* @var string[]
*/
public array $lookupPaths = [
ROOTPATH . 'themes/cp_app/',
ROOTPATH . 'themes/cp_admin/',
ROOTPATH . 'themes/cp_auth/',
ROOTPATH . 'themes/cp_install/',
];
}

View file

@ -1,42 +0,0 @@
<?php
// app/Config/Vite.php
declare(strict_types=1);
namespace Config;
use CodeIgniterVite\Config\Vite as ViteConfig;
class Vite extends ViteConfig
{
public function __construct()
{
parent::__construct();
$adminGateway = config('Admin')
->gateway;
$installGateway = config('Install')
->gateway;
$this->routesAssets = [
[
'routes' => ['*'],
'exclude' => [$adminGateway . '*', $installGateway . '*'],
'assets' => ['styles/site.css', 'js/app.ts', 'js/podcast.ts', 'js/audio-player.ts'],
],
[
'routes' => ['/map'],
'assets' => ['js/map.ts'],
],
[
'routes' => ['/' . $adminGateway . '*'],
'assets' => ['styles/admin.css', 'js/admin.ts', 'js/admin-audio-player.ts'],
],
[
'routes' => [$installGateway . '*'],
'assets' => ['styles/install.css'],
],
];
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
namespace Config;
/**
* This configuration controls how CodeIgniter behaves when running
* in worker mode (with FrankenPHP).
*/
class WorkerMode
{
/**
* Persistent Services
*
* List of service names that should persist across requests.
* These services will NOT be reset between requests.
*
* Services not in this list will be reset for each request to prevent
* state leakage.
*
* Recommended persistent services:
* - `autoloader`: PSR-4 autoloading configuration
* - `locator`: File locator
* - `exceptions`: Exception handler
* - `commands`: CLI commands registry
* - `codeigniter`: Main application instance
* - `superglobals`: Superglobals wrapper
* - `routes`: Router configuration
* - `cache`: Cache instance
*
* @var list<string>
*/
public array $persistentServices = [
'autoloader',
'locator',
'exceptions',
'commands',
'codeigniter',
'superglobals',
'routes',
'cache',
];
/**
* Force Garbage Collection
*
* Whether to force garbage collection after each request.
* Helps prevent memory leaks at a small performance cost.
*/
public bool $forceGarbageCollection = true;
}

View file

@ -3,15 +3,28 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Ad Aures * @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use Modules\Fediverse\Controllers\ActivityPubController as FediverseActivityPubController; use CodeIgniter\Controller;
use CodeIgniter\HTTP\Response;
class ActivityPubController extends FediverseActivityPubController class ActivityPubController extends Controller
{ {
/**
* @noRector ReturnTypeDeclarationRector
*/
public function preflight(): Response
{
return $this->response->setHeader('Access-Control-Allow-Origin', '*') // for allowing any domain, insecure
->setHeader('Access-Control-Allow-Headers', '*') // for allowing any headers, insecure
->setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS') // allows GET and OPTIONS methods only
->setHeader('Access-Control-Max-Age', '86400')
->setHeader('Cache-Control', 'public, max-age=86400')
->setStatusCode(200);
}
} }

View file

@ -3,37 +3,46 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use Modules\Analytics\AnalyticsTrait; use ActivityPub\Controllers\ActorController as ActivityPubActorController;
use Modules\Fediverse\Controllers\ActorController as FediverseActorController; use Analytics\AnalyticsTrait;
class ActorController extends FediverseActorController class ActorController extends ActivityPubActorController
{ {
use AnalyticsTrait; use AnalyticsTrait;
/** /**
* @var list<string> * @var string[]
*/ */
protected $helpers = ['svg', 'components', 'misc', 'seo']; protected $helpers = ['auth', 'svg', 'components', 'misc'];
public function followView(): string public function follow(): string
{ {
// @phpstan-ignore-next-line // Prevent analytics hit when authenticated
$this->registerPodcastWebpageHit($this->actor->podcast->id); if (! can_user_interact()) {
// @phpstan-ignore-next-line
$this->registerPodcastWebpageHit($this->actor->podcast->id);
}
helper(['form', 'components', 'svg']); $cacheName = "page_podcast-{$this->actor->username}_follow";
// @phpstan-ignore-next-line if (! ($cachedView = cache($cacheName))) {
set_follow_metatags($this->actor); helper(['form', 'components', 'svg']);
$data = [ $data = [
'actor' => $this->actor, 'actor' => $this->actor,
]; ];
return view('podcast/follow', $data); return view('podcast/follow', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
} }
} }

View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace App\Controllers\Admin;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Psr\Log\LoggerInterface;
/**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. Extend this class in any new controllers: class Home extends BaseController
*
* For security be sure to declare any new methods as protected or private.
*/
class BaseController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend BaseController.
*
* @var string[]
*/
protected $helpers = ['auth', 'breadcrumb', 'svg', 'components', 'misc'];
/**
* Constructor.
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
}
}

View file

@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Authorization\GroupModel;
use App\Entities\Podcast;
use App\Entities\User;
use App\Models\PodcastModel;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Exception;
class ContributorController extends BaseController
{
protected Podcast $podcast;
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound();
}
if (($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) <= 1) {
return $this->{$method}();
}
if (($this->user = (new UserModel())->getPodcastContributor((int) $params[1], (int) $params[0])) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/contributor/list', $data);
}
public function view(): string
{
$data = [
'contributor' => (new UserModel())->getPodcastContributor($this->user->id, $this->podcast->id),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('admin/contributor/view', $data);
}
public function add(): string
{
helper('form');
$users = (new UserModel())->findAll();
$userOptions = array_reduce(
$users,
function ($result, $user) {
$result[$user->id] = $user->username;
return $result;
},
[],
);
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'userOptions' => $userOptions,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/contributor/add', $data);
}
public function attemptAdd(): RedirectResponse
{
try {
(new PodcastModel())->addPodcastContributor(
(int) $this->request->getPost('user'),
$this->podcast->id,
(int) $this->request->getPost('role'),
);
} catch (Exception) {
return redirect()
->back()
->withInput()
->with('errors', [lang('Contributor.messages.alreadyAddedError')]);
}
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getContributorRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->id] = lang('Contributor.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'podcast' => $this->podcast,
'user' => $this->user,
'contributorGroupId' => (new PodcastModel())->getContributorGroupId(
$this->user->id,
$this->podcast->id,
),
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->user->username,
]);
return view('admin/contributor/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
(new PodcastModel())->updatePodcastContributor(
$this->user->id,
$this->podcast->id,
(int) $this->request->getPost('role'),
);
return redirect()->route('contributor-list', [$this->podcast->id]);
}
public function remove(): RedirectResponse
{
if ($this->podcast->created_by === $this->user->id) {
return redirect()
->back()
->with('errors', [lang('Contributor.messages.removeOwnerContributorError')]);
}
$podcastModel = new PodcastModel();
if (
! $podcastModel->removePodcastContributor($this->user->id, $this->podcast->id)
) {
return redirect()
->back()
->with('errors', $podcastModel->errors());
}
return redirect()
->back()
->with(
'message',
lang('Contributor.messages.removeContributorSuccess', [
'username' => $this->user->username,
'podcastTitle' => $this->podcast->title,
]),
);
}
}

View file

@ -0,0 +1,785 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Status;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
class EpisodeController extends BaseController
{
protected Podcast $podcast;
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (
($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (count($params) > 1) {
if (
! ($episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
}
return $this->{$method}(...$params);
}
public function list(): string
{
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->orderBy('created_at', 'desc');
$data = [
'podcast' => $this->podcast,
'episodes' => $episodes->paginate(10),
'pager' => $episodes->pager,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/episode/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/view', $data);
}
public function create(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/episode/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'audio_file' => 'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'transcript_file' =>
'ext_in[transcript,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters,json]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$image = null;
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$image = new Image($imageFile);
}
$newEpisode = new Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'guid' => null,
'audio_file' => $this->request->getFile('audio_file'),
'description_markdown' => $this->request->getPost('description'),
'image' => $image,
'location' => $this->request->getPost('location_name') === '' ? null : new Location($this->request->getPost(
'location_name'
)),
'transcript' => $this->request->getFile('transcript'),
'chapters' => $this->request->getFile('chapters'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'number' => $this->request->getPost('episode_number')
? $this->request->getPost('episode_number')
: null,
'season_number' => $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null,
'type' => $this->request->getPost('type'),
'is_blocked' => $this->request->getPost('block') === 'yes',
'custom_rss_string' => $this->request->getPost('custom_rss'),
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => null,
]);
$transcriptChoice = $this->request->getPost('transcript-choice');
if (
$transcriptChoice === 'upload-file'
&& ($transcriptFile = $this->request->getFile('transcript_file'))
&& $transcriptFile->isValid()
) {
$newEpisode->transcript_file = $transcriptFile;
} elseif ($transcriptChoice === 'remote-url') {
$newEpisode->transcript_file_remote_url = $this->request->getPost(
'transcript_file_remote_url'
) === '' ? null : $this->request->getPost('transcript_file_remote_url');
}
$chaptersChoice = $this->request->getPost('chapters-choice');
if (
$chaptersChoice === 'upload-file'
&& ($chaptersFile = $this->request->getFile('chapters_file'))
&& $chaptersFile->isValid()
) {
$newEpisode->chapters_file = $chaptersFile;
} elseif ($chaptersChoice === 'remote-url') {
$newEpisode->chapters_file_remote_url = $this->request->getPost(
'chapters_file_remote_url'
) === '' ? null : $this->request->getPost('chapters_file_remote_url');
}
$episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
return redirect()->route('episode-view', [$this->podcast->id, $newEpisodeId]);
}
public function edit(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'audio_file' =>
'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]|permit_empty',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'transcript_file' =>
'ext_in[transcript_file,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters_file,json]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->episode->title = $this->request->getPost('title');
$this->episode->slug = $this->request->getPost('slug');
$this->episode->description_markdown = $this->request->getPost('description');
$this->episode->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
);
$this->episode->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->episode->number = $this->request->getPost('episode_number')
? $this->request->getPost('episode_number')
: null;
$this->episode->season_number = $this->request->getPost('season_number')
? $this->request->getPost('season_number')
: null;
$this->episode->type = $this->request->getPost('type');
$this->episode->is_blocked = $this->request->getPost('block') === 'yes';
$this->episode->custom_rss_string = $this->request->getPost('custom_rss');
$this->episode->updated_by = (int) user_id();
$audioFile = $this->request->getFile('audio_file');
if ($audioFile !== null && $audioFile->isValid()) {
$this->episode->audio_file = $audioFile;
}
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->episode->image = new Image($imageFile);
}
$transcriptChoice = $this->request->getPost('transcript-choice');
if ($transcriptChoice === 'upload-file') {
$transcriptFile = $this->request->getFile('transcript_file');
if ($transcriptFile !== null && $transcriptFile->isValid()) {
$this->episode->transcript_file = $transcriptFile;
$this->episode->transcript_file_remote_url = null;
}
} elseif ($transcriptChoice === 'remote-url') {
if (
($transcriptFileRemoteUrl = $this->request->getPost('transcript_file_remote_url')) &&
(($transcriptFile = $this->episode->transcript_file) !== null)
) {
unlink((string) $transcriptFile);
$this->episode->transcript_file_path = null;
}
$this->episode->transcript_file_remote_url = $transcriptFileRemoteUrl === '' ? null : $transcriptFileRemoteUrl;
}
$chaptersChoice = $this->request->getPost('chapters-choice');
if ($chaptersChoice === 'upload-file') {
$chaptersFile = $this->request->getFile('chapters_file');
if ($chaptersFile !== null && $chaptersFile->isValid()) {
$this->episode->chapters_file = $chaptersFile;
$this->episode->chapters_file_remote_url = null;
}
} elseif ($chaptersChoice === 'remote-url') {
if (
($chaptersFileRemoteUrl = $this->request->getPost('chapters_file_remote_url')) &&
(($chaptersFile = $this->episode->chapters_file) !== null)
) {
unlink((string) $chaptersFile);
$this->episode->chapters_file_path = null;
}
$this->episode->chapters_file_remote_url = $chaptersFileRemoteUrl === '' ? null : $chaptersFileRemoteUrl;
}
$db = db_connect();
$db->transStart();
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
// update podcast's episode_description_footer_markdown if changed
$this->podcast->episode_description_footer_markdown = $this->request->getPost(
'description_footer'
) === '' ? null : $this->request->getPost('description_footer');
if ($this->podcast->hasChanged('episode_description_footer_markdown')) {
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function transcriptDelete(): RedirectResponse
{
unlink((string) $this->episode->transcript_file);
$this->episode->transcript_file_path = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->back();
}
public function chaptersDelete(): RedirectResponse
{
unlink((string) $this->episode->chapters_file);
$this->episode->chapters_file_path = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
return redirect()->back();
}
public function publish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'not_published') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/publish', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_error')
);
}
public function attemptPublish(): RedirectResponse
{
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$newStatus = new Status([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
'message' => $this->request->getPost('message'),
'created_by' => user_id(),
]);
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone('UTC');
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
}
} else {
$this->episode->published_at = Time::now();
}
$newStatus->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->addStatus($newStatus)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function publishEdit(): string | RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'status' => (new StatusModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/publish_edit', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_edit_error')
);
}
public function attemptPublishEdit(): RedirectResponse
{
$rules = [
'status_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone('UTC');
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
}
} else {
$this->episode->published_at = Time::now();
}
$status = (new StatusModel())->getStatusById($this->request->getPost('status_id'));
if ($status !== null) {
$status->message = $this->request->getPost('message');
$status->published_at = $this->episode->published_at;
$statusModel = new StatusModel();
if (! $statusModel->editStatus($status)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
}
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function publishCancel(): RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
$db = db_connect();
$db->transStart();
$statusModel = new StatusModel();
$status = $statusModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => $this->episode->id,
])
->first();
$statusModel->removeStatus($status);
$this->episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.publish_cancel_error')
);
}
public function unpublish(): string | RedirectResponse
{
if ($this->episode->publication_status === 'published') {
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/unpublish', $data);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'error',
lang('Episode.unpublish_error')
);
}
public function attemptUnpublish(): RedirectResponse
{
$rules = [
'understand' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$allStatusesLinkedToEpisode = (new StatusModel())
->where([
'episode_id' => $this->episode->id,
])
->findAll();
foreach ($allStatusesLinkedToEpisode as $status) {
(new StatusModel())->removeStatus($status);
}
// set episode published_at to null to unpublish
$this->episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($this->episode->id, $this->episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function delete(): RedirectResponse
{
(new EpisodeModel())->delete($this->episode->id);
return redirect()->route('episode-list', [$this->podcast->id]);
}
public function soundbitesEdit(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/soundbites', $data);
}
public function soundbitesAttemptEdit(): RedirectResponse
{
$soundbites = $this->request->getPost('soundbites');
$rules = [
'soundbites.0.start_time' =>
'permit_empty|required_with[soundbites.0.duration]|decimal|greater_than_equal_to[0]',
'soundbites.0.duration' =>
'permit_empty|required_with[soundbites.0.start_time]|decimal|greater_than_equal_to[0]',
];
foreach (array_keys($soundbites) as $soundbite_id) {
$rules += [
"soundbites.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
"soundbites.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]',
];
}
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
foreach ($soundbites as $soundbite_id => $soundbite) {
if ((int) $soundbite['start_time'] < (int) $soundbite['duration']) {
$data = [
'podcast_id' => $this->podcast->id,
'episode_id' => $this->episode->id,
'start_time' => (float) $soundbite['start_time'],
'duration' => (float) $soundbite['duration'],
'label' => $soundbite['label'],
'updated_by' => user_id(),
];
if ($soundbite_id === 0) {
$data += [
'created_by' => user_id(),
];
} else {
$data += [
'id' => $soundbite_id,
];
}
$soundbiteModel = new SoundbiteModel();
if (! $soundbiteModel->save($data)) {
return redirect()
->back()
->withInput()
->with('errors', $soundbiteModel->errors());
}
}
}
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
}
public function soundbiteDelete(string $soundbiteId): RedirectResponse
{
(new SoundbiteModel())->deleteSoundbite($this->podcast->id, $this->episode->id, (int) $soundbiteId);
return redirect()->route('soundbites-edit', [$this->podcast->id, $this->episode->id]);
}
public function embeddablePlayer(): string
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'themes' => EpisodeModel::$themes,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/embeddable_player', $data);
}
}

View file

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class EpisodePersonController extends BaseController
{
protected Podcast $podcast;
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) &&
($this->episode = (new EpisodeModel())
->where([
'id' => $params[1],
'podcast_id' => $params[0],
])
->first())
) {
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper('form');
$data = [
'episode' => $this->episode,
'podcast' => $this->podcast,
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
1 => $this->episode->title,
]);
return view('admin/episode/persons', $data);
}
public function attemptAdd(): RedirectResponse
{
$rules = [
'persons' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new PersonModel())->addEpisodePersons(
$this->podcast->id,
$this->episode->id,
$this->request->getPost('persons'),
$this->request->getPost('roles') ?? [],
);
return redirect()->back();
}
public function remove(string $personId): RedirectResponse
{
(new PersonModel())->removePersonFromEpisode($this->podcast->id, $this->episode->id, (int) $personId);
return redirect()->back();
}
}

View file

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
class FediverseController extends BaseController
{
public function dashboard(): string
{
return view('admin/fediverse/dashboard');
}
public function blockedActors(): string
{
helper(['form']);
$blockedActors = model('ActorModel')
->getBlockedActors();
return view('admin/fediverse/blocked_actors', [
'blockedActors' => $blockedActors,
]);
}
public function blockedDomains(): string
{
helper(['form']);
$blockedDomains = model('BlockedDomainModel')
->getBlockedDomains();
return view('admin/fediverse/blocked_domains', [
'blockedDomains' => $blockedDomains,
]);
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use CodeIgniter\HTTP\RedirectResponse;
class HomeController extends BaseController
{
public function index(): RedirectResponse
{
session()->keepFlashdata('message');
return redirect()->route('podcast-list');
}
}

View file

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Models\UserModel;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class MyAccountController extends BaseController
{
public function index(): string
{
return view('admin/my_account/view');
}
public function changePassword(): string
{
helper('form');
return view('admin/my_account/change_password');
}
public function attemptChange(): RedirectResponse
{
$auth = Services::authentication();
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'password' => 'required',
'new_password' => 'required|strong_password|differs[password]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
$credentials = [
'email' => user()
->email,
'password' => $this->request->getPost('password'),
];
if (! $auth->validate($credentials)) {
return redirect()
->back()
->withInput()
->with('error', lang('MyAccount.messages.wrongPasswordError'));
}
user()
->password = $this->request->getPost('new_password');
if (! $userModel->update(user_id(), user())) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->back()
->with('message', lang('MyAccount.messages.passwordChangeSuccess'));
}
}

View file

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Page;
use App\Models\PageModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PageController extends BaseController
{
protected ?Page $page;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if ($this->page = (new PageModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'pages' => (new PageModel())->findAll(),
];
return view('admin/page/list', $data);
}
public function view(): string
{
return view('admin/page/view', [
'page' => $this->page,
]);
}
public function create(): string
{
helper('form');
return view('admin/page/create');
}
public function attemptCreate(): RedirectResponse
{
$page = new Page([
'title' => $this->request->getPost('title'),
'slug' => $this->request->getPost('slug'),
'content_markdown' => $this->request->getPost('content'),
]);
$pageModel = new PageModel();
if (! $pageModel->insert($page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()
->route('page-list')
->with('message', lang('Page.messages.createSuccess', [
'pageTitle' => $page->title,
]));
}
public function edit(): string
{
helper('form');
replace_breadcrumb_params([
0 => $this->page->title,
]);
return view('admin/page/edit', [
'page' => $this->page,
]);
}
public function attemptEdit(): RedirectResponse
{
$this->page->title = $this->request->getPost('title');
$this->page->slug = $this->request->getPost('slug');
$this->page->content_markdown = $this->request->getPost('content');
$pageModel = new PageModel();
if (! $pageModel->update($this->page->id, $this->page)) {
return redirect()
->back()
->withInput()
->with('errors', $pageModel->errors());
}
return redirect()->route('page-list');
}
public function delete(): RedirectResponse
{
(new PageModel())->delete($this->page->id);
return redirect()->route('page-list');
}
}

View file

@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Image;
use App\Entities\Person;
use App\Models\PersonModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PersonController extends BaseController
{
protected ?Person $person;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($this->person = (new PersonModel())->getPersonById((int) $params[0])) !== null
) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
$data = [
'persons' => (new PersonModel())->findAll(),
];
return view('admin/person/list', $data);
}
public function view(): string
{
$data = [
'person' => $this->person,
];
replace_breadcrumb_params([
0 => $this->person->full_name,
]);
return view('admin/person/view', $data);
}
public function create(): string
{
helper(['form']);
return view('admin/person/create');
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$person = new Person([
'full_name' => $this->request->getPost('full_name'),
'unique_name' => $this->request->getPost('unique_name'),
'information_url' => $this->request->getPost('information_url'),
'image' => new Image($this->request->getFile('image')),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
$personModel = new PersonModel();
if (! $personModel->insert($person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-list');
}
public function edit(): string
{
helper('form');
$data = [
'person' => $this->person,
];
replace_breadcrumb_params([
0 => $this->person->full_name,
]);
return view('admin/person/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$this->person->full_name = $this->request->getPost('full_name');
$this->person->unique_name = $this->request->getPost('unique_name');
$this->person->information_url = $this->request->getPost('information_url');
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->person->image = new Image($imageFile);
}
$this->person->updated_by = user_id();
$personModel = new PersonModel();
if (! $personModel->update($this->person->id, $this->person)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
return redirect()->route('person-view', [$this->person->id]);
}
public function delete(): RedirectResponse
{
(new PersonModel())->delete($this->person->id);
return redirect()->route('person-list');
}
}

View file

@ -0,0 +1,378 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class PodcastController extends BaseController
{
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
$this->podcast = $podcast;
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
if (! has_permission('podcasts-list')) {
$data = [
'podcasts' => (new PodcastModel())->getUserPodcasts((int) user_id()),
];
} else {
$data = [
'podcasts' => (new PodcastModel())->findAll(),
];
}
return view('admin/podcast/list', $data);
}
public function view(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/view', $data);
}
public function viewAnalytics(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/index', $data);
}
public function viewAnalyticsWebpages(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/webpages', $data);
}
public function viewAnalyticsLocations(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/locations', $data);
}
public function viewAnalyticsUniqueListeners(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/unique_listeners', $data);
}
public function viewAnalyticsListeningTime(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/listening_time', $data);
}
public function viewAnalyticsTimePeriods(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/time_periods', $data);
}
public function viewAnalyticsPlayers(): string
{
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/analytics/players', $data);
}
public function create(): string
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
];
return view('admin/podcast/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
if (
($partnerId = $this->request->getPost('partner_id')) === '' ||
($partnerLinkUrl = $this->request->getPost('partner_link_url')) === '' ||
($partnerImageUrl = $this->request->getPost('partner_image_url')) === '') {
$partnerId = null;
$partnerLinkUrl = null;
$partnerImageUrl = null;
}
$podcast = new Podcast([
'guid' => podcast_uuid(url_to('podcast_feed', $this->request->getPost('name'))),
'title' => $this->request->getPost('title'),
'name' => $this->request->getPost('name'),
'description_markdown' => $this->request->getPost('description'),
'image' => new Image($this->request->getFile('image')),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null,
'owner_name' => $this->request->getPost('owner_name'),
'owner_email' => $this->request->getPost('owner_email'),
'publisher' => $this->request->getPost('publisher'),
'type' => $this->request->getPost('type'),
'copyright' => $this->request->getPost('copyright'),
'location' => $this->request->getPost('location_name') === '' ? null : new Location($this->request->getPost(
'location_name'
)),
'payment_pointer' => $this->request->getPost(
'payment_pointer'
) === '' ? null : $this->request->getPost('payment_pointer'),
'custom_rss_string' => $this->request->getPost('custom_rss'),
'partner_id' => $partnerId,
'partner_link_url' => $partnerLinkUrl,
'partner_image_url' => $partnerImageUrl,
'is_blocked' => $this->request->getPost('block') === 'yes',
'is_completed' => $this->request->getPost('complete') === 'yes',
'is_locked' => $this->request->getPost('lock') === 'yes',
'created_by' => user_id(),
'updated_by' => user_id(),
]);
$podcastModel = new PodcastModel();
$db = db_connect();
$db->transStart();
if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
(int) $newPodcastId,
$this->request->getPost('other_categories') ?? [],
);
// set interact as the newly created podcast actor
$createdPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($createdPodcast->actor_id);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
public function edit(): string
{
helper('form');
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'podcast' => $this->podcast,
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
if (
($partnerId = $this->request->getPost('partner_id')) === '' ||
($partnerLinkUrl = $this->request->getPost('partner_link_url')) === '' ||
($partnerImageUrl = $this->request->getPost('partner_image_url')) === '') {
$partnerId = null;
$partnerLinkUrl = null;
$partnerImageUrl = null;
}
$this->podcast->title = $this->request->getPost('title');
$this->podcast->description_markdown = $this->request->getPost('description');
$image = $this->request->getFile('image');
if ($image !== null && $image->isValid()) {
$this->podcast->image = new Image($image);
}
$this->podcast->language_code = $this->request->getPost('language');
$this->podcast->category_id = $this->request->getPost('category');
$this->podcast->parental_advisory =
$this->request->getPost('parental_advisory') !== 'undefined'
? $this->request->getPost('parental_advisory')
: null;
$this->podcast->publisher = $this->request->getPost('publisher');
$this->podcast->owner_name = $this->request->getPost('owner_name');
$this->podcast->owner_email = $this->request->getPost('owner_email');
$this->podcast->type = $this->request->getPost('type');
$this->podcast->copyright = $this->request->getPost('copyright');
$this->podcast->location = $this->request->getPost('location_name') === '' ? null : new Location(
$this->request->getPost('location_name')
);
$this->podcast->payment_pointer = $this->request->getPost(
'payment_pointer'
) === '' ? null : $this->request->getPost('payment_pointer');
$this->podcast->custom_rss_string = $this->request->getPost('custom_rss');
$this->podcast->partner_id = $partnerId;
$this->podcast->partner_link_url = $partnerLinkUrl;
$this->podcast->partner_image_url = $partnerImageUrl;
$this->podcast->is_blocked = $this->request->getPost('block') === 'yes';
$this->podcast->is_completed =
$this->request->getPost('complete') === 'yes';
$this->podcast->is_locked = $this->request->getPost('lock') === 'yes';
$this->podcast->updated_by = (int) user_id();
$db = db_connect();
$db->transStart();
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
// set Podcast categories
(new CategoryModel())->setPodcastCategories(
$this->podcast->id,
$this->request->getPost('other_categories') ?? [],
);
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function latestEpisodes(int $limit, int $podcast_id): string
{
$episodes = (new EpisodeModel())
->where('podcast_id', $podcast_id)
->orderBy('created_at', 'desc')
->findAll($limit);
return view('admin/podcast/latest_episodes', [
'episodes' => $episodes,
]);
}
public function delete(): RedirectResponse
{
(new PodcastModel())->delete($this->podcast->id);
return redirect()->route('podcast-list');
}
}

View file

@ -0,0 +1,453 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Episode;
use App\Entities\Image;
use App\Entities\Location;
use App\Entities\Person;
use App\Entities\Podcast;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
use ErrorException;
use League\HTMLToMarkdown\HtmlConverter;
use Podlibre\PodcastNamespace\ReversedTaxonomy;
class PodcastImportController extends BaseController
{
protected ?Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper(['form', 'misc']);
$languageOptions = (new LanguageModel())->getLanguageOptions();
$categoryOptions = (new CategoryModel())->getCategoryOptions();
$data = [
'languageOptions' => $languageOptions,
'categoryOptions' => $categoryOptions,
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
];
return view('admin/podcast/import', $data);
}
public function attemptImport(): RedirectResponse
{
helper(['media', 'misc']);
$rules = [
'imported_feed_url' => 'required|validate_url',
'season_number' => 'is_natural_no_zero|permit_empty',
'max_episodes' => 'is_natural_no_zero|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
try {
ini_set('user_agent', 'Castopod/' . CP_VERSION);
$feed = simplexml_load_file($this->request->getPost('imported_feed_url'));
} catch (ErrorException $errorException) {
return redirect()
->back()
->withInput()
->with('errors', [
$errorException->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$nsItunes = $feed->channel[0]->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
$nsPodcast = $feed->channel[0]->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
);
$nsContent = $feed->channel[0]->children('http://purl.org/rss/1.0/modules/content/');
if ((string) $nsPodcast->locked === 'yes') {
return redirect()
->back()
->withInput()
->with('errors', [lang('PodcastImport.lock_import')]);
}
$converter = new HtmlConverter();
$channelDescriptionHtml = (string) $feed->channel[0]->description;
try {
if (
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$imageFile = download_file((string) $nsItunes->image->attributes()['href']);
} else {
$imageFile = download_file((string) $feed->channel[0]->image->url);
}
$location = null;
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
$location = new Location(
(string) $nsPodcast->location,
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
);
}
if (property_exists($nsPodcast, 'guid') && $nsPodcast->guid !== null) {
$guid = (string) $nsPodcast->guid;
} else {
$guid = podcast_uuid(url_to('podcast_feed', $this->request->getPost('name')));
}
$podcast = new Podcast([
'guid' => $guid,
'name' => $this->request->getPost('name'),
'imported_feed_url' => $this->request->getPost('imported_feed_url'),
'new_feed_url' => url_to('podcast_feed', $this->request->getPost('name')),
'title' => (string) $feed->channel[0]->title,
'description_markdown' => $converter->convert($channelDescriptionHtml),
'description_html' => $channelDescriptionHtml,
'image' => new Image($imageFile),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
? 'explicit'
: (in_array((string) $nsItunes->explicit, ['no', 'false'], true)
? 'clean'
: null))
: null,
'owner_name' => (string) $nsItunes->owner->name,
'owner_email' => (string) $nsItunes->owner->email,
'publisher' => (string) $nsItunes->author,
'type' => property_exists(
$nsItunes,
'type'
) && $nsItunes->type !== null ? (string) $nsItunes->type : 'episodic',
'copyright' => (string) $feed->channel[0]->copyright,
'is_blocked' =>
property_exists($nsItunes, 'block') && $nsItunes->block !== null && (string) $nsItunes->block === 'yes',
'is_completed' =>
property_exists(
$nsItunes,
'complete'
) && $nsItunes->complete !== null && (string) $nsItunes->complete === 'yes',
'location' => $location,
'created_by' => user_id(),
'updated_by' => user_id(),
]);
} catch (ErrorException $ex) {
return redirect()
->back()
->withInput()
->with('errors', [
$ex->getMessage() .
': <a href="' .
$this->request->getPost('imported_feed_url') .
'" rel="noreferrer noopener" target="_blank">' .
$this->request->getPost('imported_feed_url') .
' ⎋</a>',
]);
}
$podcastModel = new PodcastModel();
$db = db_connect();
$db->transStart();
if (! ($newPodcastId = $podcastModel->insert($podcast, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$authorize = Services::authorization();
$podcastAdminGroup = $authorize->group('podcast_admin');
$podcastModel->addPodcastContributor(user_id(), $newPodcastId, (int) $podcastAdminGroup->id);
$podcastsPlatformsData = [];
$platformTypes = [
[
'name' => 'podcasting',
'elements' => $nsPodcast->id,
],
[
'name' => 'social',
'elements' => $nsPodcast->social,
],
[
'name' => 'funding',
'elements' => $nsPodcast->funding,
],
];
$platformModel = new PlatformModel();
foreach ($platformTypes as $platformType) {
foreach ($platformType['elements'] as $platform) {
$platformLabel = $platform->attributes()['platform'];
$platformSlug = slugify((string) $platformLabel);
if ($platformModel->getPlatform($platformSlug) !== null) {
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $newPodcastId,
'link_url' => $platform->attributes()['url'],
'link_content' => $platform->attributes()['id'],
'is_visible' => false,
];
}
}
}
if (count($podcastsPlatformsData) > 1) {
$platformModel->createPodcastPlatforms($newPodcastId, $podcastsPlatformsData);
}
foreach ($nsPodcast->person as $podcastPerson) {
$fullName = (string) $podcastPerson;
$personModel = new PersonModel();
$newPersonId = null;
if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id;
} else {
$newPodcastPerson = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $podcastPerson->attributes()['href'],
'image' => new Image(download_file((string) $podcastPerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
if (! $newPersonId = $personModel->insert($newPodcastPerson)) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
// TODO: these checks should be in the taxonomy as default values
$podcastPersonGroup = $podcastPerson->attributes()['group'] ?? 'Cast';
$podcastPersonRole = $podcastPerson->attributes()['role'] ?? 'Host';
$personGroup = ReversedTaxonomy::$taxonomy[(string) $podcastPersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $podcastPersonRole]['slug'];
$podcastPersonModel = new PersonModel();
if (! $podcastPersonModel->addPodcastPerson(
$newPodcastId,
$newPersonId,
$personGroupSlug,
$personRoleSlug
)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastPersonModel->errors());
}
}
$numberItems = $feed->channel[0]->item->count();
$lastItem =
$this->request->getPost('max_episodes') !== '' &&
$this->request->getPost('max_episodes') < $numberItems
? (int) $this->request->getPost('max_episodes')
: $numberItems;
$slugs = [];
for ($itemNumber = 1; $itemNumber <= $lastItem; ++$itemNumber) {
$item = $feed->channel[0]->item[$numberItems - $itemNumber];
$nsItunes = $item->children('http://www.itunes.com/dtds/podcast-1.0.dtd');
$nsPodcast = $item->children(
'https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md',
);
$nsContent = $item->children('http://purl.org/rss/1.0/modules/content/');
$textToSlugify = $this->request->getPost('slug_field') === 'title'
? (string) $item->title
: basename((string) $item->link);
$slug = slugify($textToSlugify, 185);
if (in_array($slug, $slugs, true)) {
$slugNumber = 2;
while (in_array($slug . '-' . $slugNumber, $slugs, true)) {
++$slugNumber;
}
$slug = $slug . '-' . $slugNumber;
}
$slugs[] = $slug;
$itemDescriptionHtml = match ($this->request->getPost('description_field')) {
'content' => (string) $nsContent->encoded,
'summary' => (string) $nsItunes->summary,
'subtitle_summary' => $nsItunes->subtitle . '<br/>' . $nsItunes->summary,
default => (string) $item->description,
};
if (
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$episodeImage = new Image(download_file((string) $nsItunes->image->attributes()['href']));
} else {
$episodeImage = null;
}
$location = null;
if (property_exists($nsPodcast, 'location') && $nsPodcast->location !== null) {
$location = new Location(
(string) $nsPodcast->location,
$nsPodcast->location->attributes()['geo'] === null ? null : (string) $nsPodcast->location->attributes()['geo'],
$nsPodcast->location->attributes()['osm'] === null ? null : (string) $nsPodcast->location->attributes()['osm'],
);
}
$newEpisode = new Episode([
'podcast_id' => $newPodcastId,
'title' => $item->title,
'slug' => $slug,
'guid' => $item->guid ?? null,
'audio_file' => download_file(
(string) $item->enclosure->attributes()['url'],
(string) $item->enclosure->attributes()['type']
),
'description_markdown' => $converter->convert($itemDescriptionHtml),
'description_html' => $itemDescriptionHtml,
'image' => $episodeImage,
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
? 'explicit'
: (in_array((string) $nsItunes->explicit, ['no', 'false'], true)
? 'clean'
: null))
: null,
'number' =>
$this->request->getPost('force_renumber') === 'yes'
? $itemNumber
: ((string) $nsItunes->episode === '' ? null : (int) $nsItunes->episode),
'season_number' =>
$this->request->getPost('season_number') === ''
? ((string) $nsItunes->season === '' ? null : (int) $nsItunes->season)
: (int) $this->request->getPost('season_number'),
'type' => property_exists($nsItunes, 'episodeType') && $nsItunes->episodeType !== null
? (string) $nsItunes->episodeType
: 'full',
'is_blocked' => property_exists(
$nsItunes,
'block'
) && $nsItunes->block !== null && (string) $nsItunes->block === 'yes',
'location' => $location,
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => strtotime((string) $item->pubDate),
]);
$episodeModel = new EpisodeModel();
if (! ($newEpisodeId = $episodeModel->insert($newEpisode, true))) {
// FIXME: What shall we do?
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
foreach ($nsPodcast->person as $episodePerson) {
$fullName = (string) $episodePerson;
$personModel = new PersonModel();
$newPersonId = null;
if (($newPerson = $personModel->getPerson($fullName)) !== null) {
$newPersonId = $newPerson->id;
} else {
$newPerson = new Person([
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $episodePerson->attributes()['href'],
'image' => new Image(download_file((string) $episodePerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
if (! ($newPersonId = $personModel->insert($newPerson))) {
return redirect()
->back()
->withInput()
->with('errors', $personModel->errors());
}
}
// TODO: these checks should be in the taxonomy as default values
$episodePersonGroup = $episodePerson->attributes()['group'] ?? 'Cast';
$episodePersonRole = $episodePerson->attributes()['role'] ?? 'Host';
$personGroup = ReversedTaxonomy::$taxonomy[(string) $episodePersonGroup];
$personGroupSlug = $personGroup['slug'];
$personRoleSlug = $personGroup['roles'][(string) $episodePersonRole]['slug'];
$episodePersonModel = new PersonModel();
if (! $episodePersonModel->addEpisodePerson(
$newPodcastId,
$newEpisodeId,
$newPersonId,
$personGroupSlug,
$personRoleSlug
)) {
return redirect()
->back()
->withInput()
->with('errors', $episodePersonModel->errors());
}
}
}
// set interact as the newly imported podcast actor
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($importedPodcast->actor_id);
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);
}
}

View file

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Models\PersonModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
class PodcastPersonController extends BaseController
{
protected Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
'podcastPersons' => (new PersonModel())->getPodcastPersons($this->podcast->id),
'personOptions' => (new PersonModel())->getPersonOptions(),
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/persons', $data);
}
public function attemptAdd(): RedirectResponse
{
$rules = [
'persons' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
(new PersonModel())->addPodcastPersons(
$this->podcast->id,
$this->request->getPost('persons'),
$this->request->getPost('roles') ?? [],
);
return redirect()->back();
}
public function remove(string $personId): RedirectResponse
{
(new PersonModel())->removePersonFromPodcast($this->podcast->id, (int) $personId);
return redirect()->back();
}
}

View file

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Entities\Podcast;
use App\Models\PlatformModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class PodcastPlatformController extends BaseController
{
protected ?Podcast $podcast;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if (
($this->podcast = (new PodcastModel())->getPodcastById((int) $params[0])) !== null
) {
unset($params[0]);
return $this->{$method}(...$params);
}
throw PageNotFoundException::forPageNotFound();
}
public function index(): string
{
return view('admin/podcast/platforms/dashboard');
}
public function platforms(string $platformType): string
{
helper('form');
$data = [
'podcast' => $this->podcast,
'platformType' => $platformType,
'platforms' => (new PlatformModel())->getPlatformsWithLinks($this->podcast->id, $platformType),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('admin/podcast/platforms', $data);
}
public function attemptPlatformsUpdate(string $platformType): RedirectResponse
{
$platformModel = new PlatformModel();
$validation = Services::validation();
$podcastsPlatformsData = [];
foreach (
$this->request->getPost('platforms')
as $platformSlug => $podcastPlatform
) {
$podcastPlatformUrl = $podcastPlatform['url'];
if ($podcastPlatformUrl === null) {
continue;
}
if (! $validation->check($podcastPlatformUrl, 'validate_url')) {
continue;
}
$podcastsPlatformsData[] = [
'platform_slug' => $platformSlug,
'podcast_id' => $this->podcast->id,
'link_url' => $podcastPlatformUrl,
'link_content' => $podcastPlatform['content'],
'is_visible' =>
array_key_exists('visible', $podcastPlatform) &&
$podcastPlatform['visible'] === 'yes',
'is_on_embeddable_player' =>
array_key_exists(
'on_embeddable_player',
$podcastPlatform,
) && $podcastPlatform['on_embeddable_player'] === 'yes',
];
}
$platformModel->savePodcastPlatforms($this->podcast->id, $platformType, $podcastsPlatformsData);
return redirect()
->back()
->with('message', lang('Platforms.messages.updateSuccess'));
}
public function removePodcastPlatform(string $platformSlug): RedirectResponse
{
(new PlatformModel())->removePodcastPlatform($this->podcast->id, $platformSlug);
return redirect()
->back()
->with('message', lang('Platforms.messages.removeLinkSuccess'));
}
}

View file

@ -0,0 +1,247 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers\Admin;
use App\Authorization\GroupModel;
use App\Entities\User;
use App\Models\UserModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use Config\Services;
class UserController extends BaseController
{
protected ?User $user;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) === 0) {
return $this->{$method}();
}
if ($this->user = (new UserModel())->find($params[0])) {
return $this->{$method}();
}
throw PageNotFoundException::forPageNotFound();
}
public function list(): string
{
$data = [
'users' => (new UserModel())->findAll(),
];
return view('admin/user/list', $data);
}
public function view(): string
{
$data = [
'user' => $this->user,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('admin/user/view', $data);
}
public function create(): string
{
helper('form');
$data = [
'roles' => (new GroupModel())->getUserRoles(),
];
return view('admin/user/create', $data);
}
public function attemptCreate(): RedirectResponse
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules([
'only' => ['username'],
]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
],
);
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new User($this->request->getPost());
// Activate user
$user->activate();
// Force user to reset his password on first connection
$user->forcePasswordReset();
if (! $userModel->insert($user)) {
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.createSuccess', [
'username' => $user->username,
]));
}
public function edit(): string
{
helper('form');
$roles = (new GroupModel())->getUserRoles();
$roleOptions = array_reduce(
$roles,
function ($result, $role) {
$result[$role->name] = lang('User.roles.' . $role->name);
return $result;
},
[],
);
$data = [
'user' => $this->user,
'roleOptions' => $roleOptions,
];
replace_breadcrumb_params([
0 => $this->user->username,
]);
return view('admin/user/edit', $data);
}
public function attemptEdit(): RedirectResponse
{
$authorize = Services::authorization();
$roles = $this->request->getPost('roles');
$authorize->setUserGroups($this->user->id, $roles ?? []);
// Success!
return redirect()
->route('user-list')
->with('message', lang('User.messages.rolesEditSuccess', [
'username' => $this->user->username,
]));
}
public function forcePassReset(): RedirectResponse
{
$userModel = new UserModel();
$this->user->forcePasswordReset();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
// Success!
return redirect()
->route('user-list')
->with(
'message',
lang('User.messages.forcePassResetSuccess', [
'username' => $this->user->username,
]),
);
}
public function ban(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.banSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
$userModel = new UserModel();
// TODO: add ban reason?
$this->user->ban('');
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.banSuccess', [
'username' => $this->user->username,
]));
}
public function unBan(): RedirectResponse
{
$userModel = new UserModel();
$this->user->unBan();
if (! $userModel->update($this->user->id, $this->user)) {
return redirect()
->back()
->with('errors', $userModel->errors());
}
return redirect()
->route('user-list')
->with('message', lang('User.messages.unbanSuccess', [
'username' => $this->user->username,
]));
}
public function delete(): RedirectResponse
{
$authorize = Services::authorization();
if ($authorize->inGroup('superadmin', $this->user->id)) {
return redirect()
->back()
->with('errors', [
lang('User.messages.deleteSuperAdminError', [
'username' => $this->user->username,
]),
]);
}
(new UserModel())->delete($this->user->id);
return redirect()
->back()
->with('message', lang('User.messages.deleteSuccess', [
'username' => $this->user->username,
]));
}
}

View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\User;
use CodeIgniter\HTTP\RedirectResponse;
use Myth\Auth\Controllers\AuthController as MythAuthController;
class AuthController extends MythAuthController
{
/**
* An array of helpers to be automatically loaded upon class instantiation.
*
* @var string[]
*/
protected $helpers = ['components'];
/**
* Attempt to register a new user.
*/
public function attemptRegister(): RedirectResponse
{
// Check if registration is allowed
if (! $this->config->allowRegistration) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.registerDisabled'));
}
$users = model('UserModel');
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = [
'username' =>
'required|alpha_numeric_space|min_length[3]|is_unique[users.username]',
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
// Save the user
$allowedPostFields = array_merge(['password'], $this->config->validFields, $this->config->personalFields);
$user = new User($this->request->getPost($allowedPostFields));
$this->config->requireActivation === null
? $user->activate()
: $user->generateActivateHash();
// Ensure default group gets assigned if set
if ($this->config->defaultUserGroup !== null) {
$users = $users->withGroup($this->config->defaultUserGroup);
}
if (! $users->save($user)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
if ($this->config->requireActivation !== null) {
$activator = service('activator');
$sent = $activator->send($user);
if (! $sent) {
return redirect()
->back()
->withInput()
->with('error', $activator->error() ?? lang('Auth.unknownError'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.activationSuccess'));
}
// Success!
return redirect()
->route('login')
->with('message', lang('Auth.registerSuccess'));
}
/**
* Verifies the code with the email and saves the new password, if they all pass validation.
*/
public function attemptReset(): RedirectResponse
{
if ($this->config->activeResetter === null) {
return redirect()
->route('login')
->with('error', lang('Auth.forgotDisabled'));
}
$users = model('UserModel');
// First things first - log the reset attempt.
$users->logResetAttempt(
$this->request->getPost('email'),
$this->request->getPost('token'),
$this->request->getIPAddress(),
(string) $this->request->getUserAgent(),
);
$rules = [
'token' => 'required',
'email' => 'required|valid_email',
'password' => 'required|strong_password',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $users->errors());
}
$user = $users
->where('email', $this->request->getPost('email'))
->where('reset_hash', $this->request->getPost('token'))
->first();
if ($user === null) {
return redirect()
->back()
->with('error', lang('Auth.forgotNoUser'));
}
// Reset token still valid?
if (
$user->reset_expires !== null &&
time() > $user->reset_expires->getTimestamp()
) {
return redirect()
->back()
->withInput()
->with('error', lang('Auth.resetTokenExpired'));
}
// Success! Save the new password, and cleanup the reset hash.
$user->password = $this->request->getPost('password');
$user->reset_hash = null;
$user->reset_at = date('Y-m-d H:i:s');
$user->reset_expires = null;
$user->force_pass_reset = false;
$users->save($user);
return redirect()
->route('login')
->with('message', lang('Auth.resetSuccess'));
}
public function attemptInteractAsActor(): RedirectResponse
{
$rules = [
'actor_id' => 'required|numeric',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', service('validation')->getErrors());
}
helper('auth');
set_interact_as_actor((int) $this->request->getPost('actor_id'));
return redirect()->back();
}
}

View file

@ -7,51 +7,35 @@ namespace App\Controllers;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\HTTP\RequestInterface; use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Override;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use ViewThemes\Theme;
/** /**
* Class BaseController
*
* BaseController provides a convenient place for loading components and performing functions that are needed by all * BaseController provides a convenient place for loading components and performing functions that are needed by all
* your controllers. * your controllers. Extend this class in any new controllers: class Home extends BaseController
* *
* Extend this class in any new controllers: * For security be sure to declare any new methods as protected or private.
* ```
* class Home extends BaseController
* ```
*
* For security, be sure to declare any new methods as protected or private.
*/ */
abstract class BaseController extends Controller class BaseController extends Controller
{ {
/** /**
* An array of helpers to be loaded automatically upon * An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* class instantiation. These helpers will be available * other controllers that extend BaseController.
* to all other controllers that extend BaseController.
* *
* @var list<string> * @var string[]
*/ */
protected $helpers = []; protected $helpers = ['auth', 'svg', 'components', 'misc'];
/** /**
* Be sure to declare properties for any property fetch you initialized. * Constructor.
* The creation of dynamic property is deprecated in PHP 8.2.
*/ */
// protected $session;
#[Override]
public function initController( public function initController(
RequestInterface $request, RequestInterface $request,
ResponseInterface $response, ResponseInterface $response,
LoggerInterface $logger, LoggerInterface $logger
): void { ): void {
// Load here all helpers you want to be available in your controllers that extend BaseController.
// Caution: Do not put the this below the parent::initController() call below.
$this->helpers = [...$this->helpers, 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
// Do Not Edit This Line // Do Not Edit This Line
parent::initController($request, $response, $logger); parent::initController($request, $response, $logger);
Theme::setTheme('app');
} }
} }

View file

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
class ColorsController extends Controller
{
public function index(): ResponseInterface
{
$cacheName = 'colors.css';
if (
! ($colorsCssBody = cache($cacheName))
) {
$colorThemes = config('Colors')
->themes;
$colorsCssBody = '';
foreach ($colorThemes as $name => $color) {
$colorsCssBody .= ".theme-{$name} {";
foreach ($color as $variable => $value) {
$colorsCssBody .= "--color-{$variable}: {$value[0]} {$value[1]}% {$value[2]}%;";
}
$colorsCssBody .= '}';
}
cache()
->save($cacheName, $colorsCssBody, DECADE);
}
return $this->response->setHeader('Content-Type', 'text/css')
->setHeader('charset', 'UTF-8')
->setBody($colorsCssBody);
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -20,23 +20,17 @@ class CreditsController extends BaseController
{ {
$locale = service('request') $locale = service('request')
->getLocale(); ->getLocale();
$allPodcasts = (new PodcastModel())->findAll();
$cacheName = implode( $cacheName = "page_credits_{$locale}";
'_',
array_filter(['page', 'credits', $locale, auth()->loggedIn() ? 'authenticated' : null]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$page = new Page([ $page = new Page([
'title' => lang('Person.credits', [], $locale), 'title' => lang('Person.credits', [], $locale),
'slug' => 'credits', 'slug' => 'credits',
'content_markdown' => '', 'content_markdown' => '',
]); ]);
$allPodcasts = new PodcastModel() $allCredits = (new CreditModel())->findAll();
->findAll();
$allCredits = new CreditModel()
->findAll();
// Unlike the carpenter, we make a tree from a table: // Unlike the carpenter, we make a tree from a table:
$personGroup = null; $personGroup = null;
@ -50,24 +44,27 @@ class CreditsController extends BaseController
$personRole = $credit->person_role; $personRole = $credit->person_role;
$credits[$personGroup] = [ $credits[$personGroup] = [
'group_label' => $credit->group_label, 'group_label' => $credit->group_label,
'persons' => [ 'persons' => [
$personId => [ $personId => [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'), 'thumbnail_url' =>
'information_url' => $credit->person->information_url, $credit->person->image->thumbnail_url,
'roles' => [ 'information_url' =>
$credit->person->information_url,
'roles' => [
$personRole => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' ' ? "{$credit->podcast->title} "
: '') . : '') .
esc($credit->episode->title) . $credit->episode
->title .
episode_numbering( episode_numbering(
$credit->episode $credit->episode
->number, ->number,
@ -76,7 +73,7 @@ class CreditsController extends BaseController
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: esc($credit->podcast->title), : $credit->podcast->title,
], ],
], ],
], ],
@ -88,22 +85,23 @@ class CreditsController extends BaseController
$personId = $credit->person_id; $personId = $credit->person_id;
$personRole = $credit->person_role; $personRole = $credit->person_role;
$credits[$personGroup]['persons'][$personId] = [ $credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name, 'full_name' => $credit->person->full_name,
'thumbnail_url' => get_avatar_url($credit->person, 'thumbnail'), 'thumbnail_url' =>
$credit->person->image->thumbnail_url,
'information_url' => $credit->person->information_url, 'information_url' => $credit->person->information_url,
'roles' => [ 'roles' => [
$personRole => [ $personRole => [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' ' ? "{$credit->podcast->title} "
: '') . : '') .
esc($credit->episode->title) . $credit->episode->title .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode $credit->episode
@ -111,7 +109,7 @@ class CreditsController extends BaseController
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: esc($credit->podcast->title), : $credit->podcast->title,
], ],
], ],
], ],
@ -123,23 +121,23 @@ class CreditsController extends BaseController
$personRole $personRole
] = [ ] = [
'role_label' => $credit->role_label, 'role_label' => $credit->role_label,
'is_in' => [ 'is_in' => [
[ [
'link' => $credit->episode_id 'link' => $credit->episode_id
? $credit->episode->link ? $credit->episode->link
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' ' ? "{$credit->podcast->title} "
: '') . : '') .
esc($credit->episode->title) . $credit->episode->title .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode->season_number, $credit->episode->season_number,
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: esc($credit->podcast->title), : $credit->podcast->title,
], ],
], ],
]; ];
@ -152,27 +150,26 @@ class CreditsController extends BaseController
: $credit->podcast->link, : $credit->podcast->link,
'title' => $credit->episode_id 'title' => $credit->episode_id
? (count($allPodcasts) > 1 ? (count($allPodcasts) > 1
? esc($credit->podcast->title) . ' ' ? "{$credit->podcast->title} "
: '') . : '') .
esc($credit->episode->title) . $credit->episode->title .
episode_numbering( episode_numbering(
$credit->episode->number, $credit->episode->number,
$credit->episode->season_number, $credit->episode->season_number,
'text-xs ml-2', 'text-xs ml-2',
true, true,
) )
: esc($credit->podcast->title), : $credit->podcast->title,
]; ];
} }
} }
set_page_metatags($page);
$data = [ $data = [
'page' => $page, 'page' => $page,
'credits' => $credits, 'credits' => $credits,
]; ];
$found = view('pages/credits', $data); $found = view('credits', $data);
cache() cache()
->save($cacheName, $found, DECADE); ->save($cacheName, $found, DECADE);

View file

@ -1,174 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\HTTP\URI;
use Modules\Analytics\Config\Analytics;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Override;
use Psr\Log\LoggerInterface;
class EpisodeAudioController extends Controller
{
/**
* An array of helpers to be loaded automatically upon class instantiation. These helpers will be available to all
* other controllers that extend Analytics.
*
* @var list<string>
*/
protected $helpers = ['analytics'];
protected Podcast $podcast;
protected Episode $episode;
protected Analytics $analyticsConfig;
#[Override]
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger,
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
set_user_session_deny_list_ip();
set_user_session_location();
set_user_session_player();
$this->analyticsConfig = config('Analytics');
}
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 2) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
if (
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): RedirectResponse | ResponseInterface
{
// check if episode is premium?
$subscription = null;
// check if podcast is already unlocked before any token validation
if ($this->episode->is_premium && ! ($subscription = service('premium_podcasts')->subscription(
$this->episode->podcast->handle,
)) instanceof Subscription) {
// look for token as GET parameter
if (($token = $this->request->getGet('token')) === null) {
return $this->response->setStatusCode(401)
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Episode is premium, you must provide a token to unlock it.',
],
]);
}
// check if there's a valid subscription for the provided token
if (! ($subscription = new SubscriptionModel()->validateSubscription(
$this->episode->podcast->handle,
$token,
)) instanceof Subscription) {
return $this->response->setStatusCode(401, 'Invalid token!')
->setJSON([
'errors' => [
'status' => 401,
'title' => 'Unauthorized',
'detail' => 'Invalid token!',
],
]);
}
}
$session = service('session');
$serviceName = '';
if ($this->request->getGet('_from')) {
$serviceName = $this->request->getGet('_from');
} elseif ($session->get('embed_domain') !== null) {
$serviceName = $session->get('embed_domain');
} elseif ($session->get('referer') !== null && $session->get('referer') !== '- Direct -') {
$serviceName = parse_url((string) $session->get('referer'), PHP_URL_HOST);
}
$audioFileSize = $this->episode->audio->file_size;
$audioFileHeaderSize = $this->episode->audio->header_size;
$audioDuration = $this->episode->audio->duration;
// bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics
// - if audio is less than or equal to 60s, then take the audio file_size
// - if audio is more than 60s, then take the audio file_header_size + 60s
$bytesThreshold = $audioDuration <= 60
? $audioFileSize
: $audioFileHeaderSize +
(int) floor((($audioFileSize - $audioFileHeaderSize) / $audioDuration) * 60);
podcast_hit(
$this->episode->podcast_id,
$this->episode->id,
$bytesThreshold,
$audioFileSize,
$audioDuration,
$this->episode->published_at->getTimestamp(),
$serviceName,
$subscription instanceof Subscription ? $subscription->id : null,
);
$audioFileURI = new URI(service('file_manager')->getUrl($this->episode->audio->file_key));
$queryParams = [];
foreach ($this->request->getGet() as $key => $value) {
// do not include token in query params
if ($key !== 'token') {
$queryParams[$key] = $value;
}
}
$audioFileURI->setQueryArray($queryParams);
return redirect()->to((string) $audioFileURI);
}
}

View file

@ -1,184 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Entities\EpisodeComment;
use App\Entities\Podcast;
use App\Libraries\CommentObject;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Entities\Actor;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class EpisodeCommentController extends BaseController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
protected Episode $episode;
protected EpisodeComment $comment;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 3) {
throw PageNotFoundException::forPageNotFound();
}
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $podcast->actor;
if (
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode
) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if (
! ($comment = new EpisodeCommentModel()->getCommentById($params[2])) instanceof EpisodeComment
) {
throw PageNotFoundException::forPageNotFound();
}
$this->comment = $comment;
unset($params[2]);
unset($params[1]);
unset($params[0]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"episode#{$this->episode->id}",
"comment#{$this->comment->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_comment_metatags($this->comment);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'episode' => $this->episode,
'comment' => $this->comment,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('episode/comment', $data);
}
return view('episode/comment', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function commentObject(): ResponseInterface
{
$commentObject = new CommentObject($this->comment);
return $this->response
->setContentType('application/json')
->setBody($commentObject->toJSON());
}
public function replies(): ResponseInterface
{
/**
* get comment replies
*/
$commentReplies = model(EpisodeCommentModel::class, false)
->where('in_reply_to_id', service('uuid')->fromString($this->comment->id)->getBytes())
->orderBy('created_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page');
if ($pageNumber < 1) {
$commentReplies->paginate(12);
$pager = $commentReplies->pager;
$collection = new OrderedCollectionObject(null, $pager);
} else {
$paginatedReplies = $commentReplies->paginate(12, 'default', $pageNumber);
$pager = $commentReplies->pager;
$orderedItems = [];
foreach ($paginatedReplies as $reply) {
$replyObject = new CommentObject($reply);
$orderedItems[] = $replyObject;
}
$collection = new OrderedCollectionPage($pager, $orderedItems);
}
return $this->response
->setContentType('application/activity+json')
->setBody($collection->toJSON());
}
public function likeAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
public function replyAction(): RedirectResponse
{
if (! ($interactAsActor = interact_as_actor()) instanceof Actor) {
return redirect()->back();
}
model('LikeModel')
->toggleLike($interactAsActor, $this->comment);
return redirect()->back();
}
}

View file

@ -3,13 +3,16 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Episode; use App\Entities\Episode;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\NoteObject; use App\Libraries\NoteObject;
@ -18,12 +21,9 @@ use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\Response;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Config\Embed; use Config\Services;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
use Modules\Media\FileManagers\FileManagerInterface;
use SimpleXMLElement; use SimpleXMLElement;
class EpisodeController extends BaseController class EpisodeController extends BaseController
@ -41,7 +41,7 @@ class EpisodeController extends BaseController
} }
if ( if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast ($podcast = (new PodcastModel())->getPodcastByName($params[0])) === null
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
@ -49,7 +49,7 @@ class EpisodeController extends BaseController
$this->podcast = $podcast; $this->podcast = $podcast;
if ( if (
! ($episode = new EpisodeModel()->getEpisodeBySlug($params[0], $params[1])) instanceof Episode ($episode = (new EpisodeModel())->getEpisodeBySlug($params[0], $params[1])) === null
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
@ -64,41 +64,36 @@ class EpisodeController extends BaseController
public function index(): string public function index(): string
{ {
$this->registerPodcastWebpageHit($this->episode->podcast_id); // Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->episode->podcast_id);
}
$cacheName = implode( $locale = service('request')
'_', ->getLocale();
array_filter([ $cacheName =
'page', "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_{$locale}" .
"podcast#{$this->podcast->id}", (can_user_interact() ? '_authenticated' : '');
"episode#{$this->episode->id}",
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episode' => $this->episode, 'episode' => $this->episode,
]; ];
$secondsToNextUnpublishedEpisode = new EpisodeModel() $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
->getSecondsToNextUnpublishedEpisode($this->podcast->id); $this->podcast->id,
);
if (auth()->loggedIn()) { if (can_user_interact()) {
helper('form'); helper('form');
return view('podcast/episode_authenticated', $data);
return view('episode/comments', $data);
} }
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/comments', $data, [ return view('podcast/episode', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE, 'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -106,208 +101,44 @@ class EpisodeController extends BaseController
return $cachedView; return $cachedView;
} }
public function activity(): string public function embeddablePlayer(string $theme = 'light-transparent'): string
{ {
$this->registerPodcastWebpageHit($this->episode->podcast_id); header('Content-Security-Policy: frame-ancestors https://* http://*');
$cacheName = implode( // Prevent analytics hit when authenticated
'_', if (! can_user_interact()) {
array_filter([ $this->registerPodcastWebpageHit($this->episode->podcast_id);
'page', }
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}", $session = Services::session();
'activity', $session->start();
service('request') if (isset($_SERVER['HTTP_REFERER'])) {
->getLocale(), $session->set('embeddable_player_domain', parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST));
is_unlocked($this->podcast->handle) ? 'unlocked' : null, }
auth()
->loggedIn() ? 'authenticated' : null, $locale = service('request')
]), ->getLocale();
);
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode); $theme = EpisodeModel::$themes[$theme];
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episode' => $this->episode, 'episode' => $this->episode,
'theme' => $theme,
]; ];
$secondsToNextUnpublishedEpisode = new EpisodeModel() $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
->getSecondsToNextUnpublishedEpisode($this->podcast->id); $this->podcast->id,
);
if (auth()->loggedIn()) {
helper('form');
return view('episode/activity', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update // The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/activity', $data, [ return view('embeddable_player', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE, 'cache' => $secondsToNextUnpublishedEpisode
'cache_name' => $cacheName, ? $secondsToNextUnpublishedEpisode
]); : DECADE,
}
return $cachedView;
}
public function chapters(): string
{
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'chapters',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get chapters from json file
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
if (auth()->loggedIn()) {
helper('form');
return view('episode/chapters', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/chapters', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function transcript(): string
{
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'transcript',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_episode_metatags($this->episode);
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
];
// get transcript from json file
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key,
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
if (auth()->loggedIn()) {
helper('form');
return view('episode/transcript', $data);
}
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('episode/transcript', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function embed(string $theme = 'light-transparent'): string
{
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
$this->registerPodcastWebpageHit($this->episode->podcast_id);
$session = service('session');
if (service('superglobals')->server('HTTP_REFERER') !== null) {
$session->set('embed_domain', parse_url(service('superglobals')->server('HTTP_REFERER'), PHP_URL_HOST));
}
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
"episode#{$this->episode->id}",
'embed',
$theme,
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$themeData = EpisodeModel::$themes[$theme];
$data = [
'podcast' => $this->podcast,
'episode' => $this->episode,
'theme' => $theme,
'themeData' => $themeData,
];
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
// The page cache is set to a decade so it is deleted manually upon podcast update
return view('embed', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -318,25 +149,24 @@ class EpisodeController extends BaseController
public function oembedJSON(): ResponseInterface public function oembedJSON(): ResponseInterface
{ {
return $this->response->setJSON([ return $this->response->setJSON([
'type' => 'rich', 'type' => 'rich',
'version' => '1.0', 'version' => '1.0',
'title' => $this->episode->title, 'title' => $this->episode->title,
'provider_name' => $this->podcast->title, 'provider_name' => $this->podcast->title,
'provider_url' => $this->podcast->link, 'provider_url' => $this->podcast->link,
'author_name' => $this->podcast->title, 'author_name' => $this->podcast->title,
'author_url' => $this->podcast->link, 'author_url' => $this->podcast->link,
'html' => '<iframe src="' . 'html' =>
$this->episode->embed_url . '<iframe src="' .
'" width="100%" height="' . config('Embed')->height . '" frameborder="0" scrolling="no"></iframe>', $this->episode->embeddable_player_url .
'width' => config('Embed') '" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
->width, 'width' => 600,
'height' => config('Embed') 'height' => 200,
->height, 'thumbnail_url' => $this->episode->image->large_url,
'thumbnail_url' => $this->episode->cover->og_url,
'thumbnail_width' => config('Images') 'thumbnail_width' => config('Images')
->podcastCoverSizes['og']['width'], ->largeSize,
'thumbnail_height' => config('Images') 'thumbnail_height' => config('Images')
->podcastCoverSizes['og']['height'], ->largeSize,
]); ]);
} }
@ -351,27 +181,27 @@ class EpisodeController extends BaseController
$oembed->addChild('provider_url', $this->podcast->link); $oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title); $oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link); $oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->cover->og_url); $oembed->addChild('thumbnail', $this->episode->image->large_url);
$oembed->addChild('thumbnail_width', (string) config('Images')->podcastCoverSizes['og']['width']); $oembed->addChild('thumbnail_width', config('Images')->largeSize);
$oembed->addChild('thumbnail_height', (string) config('Images')->podcastCoverSizes['og']['height']); $oembed->addChild('thumbnail_height', config('Images')->largeSize);
$oembed->addChild( $oembed->addChild(
'html', 'html',
htmlspecialchars( htmlentities(
'<iframe src="' . '<iframe src="' .
$this->episode->embed_url . $this->episode->embeddable_player_url .
'" width="100%" height="' . config( '" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
Embed::class,
)->height . '" frameborder="0" scrolling="no"></iframe>',
), ),
); );
$oembed->addChild('width', (string) config('Embed')->width); $oembed->addChild('width', '600');
$oembed->addChild('height', (string) config('Embed')->height); $oembed->addChild('height', '200');
// @phpstan-ignore-next-line return $this->response->setXML((string) $oembed);
return $this->response->setXML($oembed);
} }
public function episodeObject(): ResponseInterface /**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeObject(): Response
{ {
$podcastObject = new PodcastEpisode($this->episode); $podcastObject = new PodcastEpisode($this->episode);
@ -380,16 +210,21 @@ class EpisodeController extends BaseController
->setBody($podcastObject->toJSON()); ->setBody($podcastObject->toJSON());
} }
public function comments(): ResponseInterface /**
* @noRector ReturnTypeDeclarationRector
*/
public function comments(): Response
{ {
/** /**
* get comments: aggregated replies from posts referring to the episode * get comments: aggregated replies from posts referring to the episode
*/ */
$episodeComments = model('PostModel') $episodeComments = model('StatusModel')
->whereIn('in_reply_to_id', fn (BaseBuilder $builder): BaseBuilder => $builder->select('id') ->whereIn('in_reply_to_id', function (BaseBuilder $builder): BaseBuilder {
->from('fediverse_posts') return $builder->select('id')
->where('episode_id', $this->episode->id)) ->from('activitypub_statuses')
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('episode_id', $this->episode->id);
})
->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'ASC'); ->orderBy('published_at', 'ASC');
$pageNumber = (int) $this->request->getGet('page'); $pageNumber = (int) $this->request->getGet('page');
@ -403,8 +238,10 @@ class EpisodeController extends BaseController
$pager = $episodeComments->pager; $pager = $episodeComments->pager;
$orderedItems = []; $orderedItems = [];
foreach ($paginatedComments as $comment) { if ($paginatedComments !== null) {
$orderedItems[] = new NoteObject($comment)->toArray(); foreach ($paginatedComments as $comment) {
$orderedItems[] = (new NoteObject($comment))->toArray();
}
} }
// @phpstan-ignore-next-line // @phpstan-ignore-next-line

View file

@ -1,113 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2023 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Episode;
use App\Models\EpisodeModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use Modules\Media\FileManagers\FileManagerInterface;
class EpisodePreviewController extends BaseController
{
protected Episode $episode;
public function _remap(string $method, string ...$params): mixed
{
if (count($params) < 1) {
throw PageNotFoundException::forPageNotFound();
}
// find episode by previewUUID
$episode = new EpisodeModel()
->getEpisodeByPreviewId($params[0]);
if (! $episode instanceof Episode) {
throw PageNotFoundException::forPageNotFound();
}
$this->episode = $episode;
if ($episode->publication_status === 'published') {
// redirect to episode page
return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
}
unset($params[0]);
return $this->{$method}(...$params);
}
public function index(): string
{
helper('form');
return view('episode/preview-comments', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function activity(): string
{
helper('form');
return view('episode/preview-activity', [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
]);
}
public function chapters(): string
{
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if (isset($this->episode->chapters->file_key)) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
$chapters = json_decode($episodeChaptersJsonString, true);
$data['chapters'] = $chapters;
}
helper('form');
return view('episode/preview-chapters', $data);
}
public function transcript(): string
{
// get transcript from json file
$data = [
'podcast' => $this->episode->podcast,
'episode' => $this->episode,
];
if ($this->episode->transcript !== null) {
$data['transcript'] = $this->episode->transcript;
if ($this->episode->transcript->json_key !== null) {
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager');
$transcriptJsonString = (string) $fileManager->getFileContents(
$this->episode->transcript->json_key,
);
$data['captions'] = json_decode($transcriptJsonString, true);
}
}
helper('form');
return view('episode/preview-transcript', $data);
}
}

View file

@ -3,59 +3,36 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2022 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Controller; use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\IncomingRequest;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\ResponseInterface;
use Exception; use Exception;
use Modules\PremiumPodcasts\Entities\Subscription; use Opawg\UserAgentsPhp\UserAgentsRSS;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use Opawg\UserAgentsV2Php\UserAgentsRSS;
class FeedController extends Controller class FeedController extends Controller
{ {
/** public function index(string $podcastName): ResponseInterface
* Instance of the main Request object.
*
* @var IncomingRequest
*/
protected $request;
public function index(string $podcastHandle): ResponseInterface
{ {
$podcast = new PodcastModel() helper('rss');
->where('handle', $podcastHandle)
$podcast = (new PodcastModel())->where('name', $podcastName)
->first(); ->first();
if (! $podcast instanceof Podcast) { if (! $podcast) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
// 301 redirect to new feed?
$redirectToNewFeed = service('settings')
->get('Podcast.redirect_to_new_feed', 'podcast:' . $podcast->id);
if ($redirectToNewFeed && $podcast->new_feed_url !== null && filter_var(
$podcast->new_feed_url,
FILTER_VALIDATE_URL,
) && $podcast->new_feed_url !== current_url()) {
return redirect()->to($podcast->new_feed_url, 301);
}
helper(['rss', 'premium_podcasts', 'misc']);
$service = null; $service = null;
try { try {
$service = UserAgentsRSS::find(service('superglobals')->server('HTTP_USER_AGENT')); $service = UserAgentsRSS::find($_SERVER['HTTP_USER_AGENT']);
} catch (Exception $exception) { } catch (Exception $exception) {
// If things go wrong the show must go on and the user must be able to download the file // If things go wrong the show must go on and the user must be able to download the file
log_message('critical', $exception->getMessage()); log_message('critical', $exception->getMessage());
@ -66,32 +43,25 @@ class FeedController extends Controller
$serviceSlug = $service['slug']; $serviceSlug = $service['slug'];
} }
$subscription = null; $cacheName =
$token = $this->request->getGet('token'); "podcast#{$podcast->id}_feed" . ($service ? "_{$serviceSlug}" : '');
if ($token) {
$subscription = new SubscriptionModel()
->validateSubscription($podcastHandle, $token);
}
$cacheName = implode(
'_',
array_filter([
"podcast#{$podcast->id}",
'feed',
$service ? $serviceSlug : null,
$subscription instanceof Subscription ? "subscription#{$subscription->id}" : null,
]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = get_rss_feed($podcast, $serviceSlug, $subscription, $token); $found = get_rss_feed($podcast, $serviceSlug);
// The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update // The page cache is set to expire after next episode publication or a decade by default so it is deleted manually upon podcast update
$secondsToNextUnpublishedEpisode = new EpisodeModel() $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
->getSecondsToNextUnpublishedEpisode($podcast->id); $podcast->id,
);
cache() cache()
->save($cacheName, $found, $secondsToNextUnpublishedEpisode ?: DECADE); ->save(
$cacheName,
$found,
$secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
);
} }
return $this->response->setXML($found); return $this->response->setXML($found);

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -11,74 +11,33 @@ declare(strict_types=1);
namespace App\Controllers; namespace App\Controllers;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface; use Config\Services;
use Modules\Media\FileManagers\FileManagerInterface;
class HomeController extends BaseController class HomeController extends BaseController
{ {
public function index(): RedirectResponse | string public function index(): RedirectResponse | string
{ {
$sortOptions = ['activity', 'created_desc', 'created_asc']; $db = db_connect();
$sortBy = in_array($this->request->getGet('sort'), $sortOptions, true) ? $this->request->getGet( if ($db->getDatabase() === '' || ! $db->tableExists('podcasts')) {
'sort', // Database connection has not been set or could not find the podcasts table
) : 'activity'; // Redirecting to install page because it is likely that Castopod Host has not been installed yet.
// NB: as base_url wouldn't have been defined here, redirect to install wizard manually
$route = Services::routes()->reverseRoute('install');
return redirect()->to(rtrim(host_url(), '/') . $route);
}
$allPodcasts = new PodcastModel() $allPodcasts = (new PodcastModel())->findAll();
->getAllPodcasts($sortBy);
// check if there's only one podcast to redirect user to it // check if there's only one podcast to redirect user to it
if (count($allPodcasts) === 1) { if (count($allPodcasts) === 1) {
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]); return redirect()->route('podcast-activity', [$allPodcasts[0]->name]);
} }
set_home_metatags();
// default behavior: list all podcasts on home page // default behavior: list all podcasts on home page
$data = [ $data = [
'podcasts' => $allPodcasts, 'podcasts' => $allPodcasts,
'sortBy' => $sortBy,
]; ];
return view('home', $data); return view('home', $data);
} }
public function health(): ResponseInterface
{
$errors = [];
try {
db_connect();
} catch (DatabaseException) {
$errors[] = 'Unable to connect to the database.';
}
// --- Can Castopod connect to the cache handler
if (config('Cache')->handler !== 'dummy' && cache()->getCacheInfo() === null) {
$errors[] = 'Unable connect to the cache handler.';
}
// --- Can Castopod write to storage?
/** @var FileManagerInterface $fileManager */
$fileManager = service('file_manager', false);
if (! $fileManager->isHealthy()) {
$errors[] = 'Problem with file manager.';
}
if ($errors !== []) {
return $this->response->setStatusCode(503)
->setJSON([
'code' => 503,
'errors' => $errors,
]);
}
return $this->response->setStatusCode(200)
->setJSON([
'code' => 200,
'message' => '✨ All good!',
]);
}
} }

View file

@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\User;
use App\Models\UserModel;
use CodeIgniter\Controller;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Database;
use Config\Services;
use Dotenv\Dotenv;
use Dotenv\Exception\ValidationException;
use Psr\Log\LoggerInterface;
use Throwable;
class InstallController extends Controller
{
/**
* @var string[]
*/
protected $helpers = ['form', 'components', 'svg'];
/**
* Constructor.
*/
public function initController(
RequestInterface $request,
ResponseInterface $response,
LoggerInterface $logger
): void {
// Do Not Edit This Line
parent::initController($request, $response, $logger);
}
/**
* Every operation goes through this method to handle the install logic.
*
* If all required actions have already been performed, the install route will show a 404 page.
*/
public function index(): string
{
if (! file_exists(ROOTPATH . '.env')) {
// create empty .env file
try {
$envFile = fopen(ROOTPATH . '.env', 'w');
fclose($envFile);
} catch (Throwable) {
// Could not create the .env file, redirect to a view with instructions on how to add it manually
return view('install/manual_config');
}
}
// Check if .env has all required fields
$dotenv = Dotenv::createUnsafeImmutable(ROOTPATH);
$dotenv->load();
// Check if the created .env file is writable to continue install process
if (is_really_writable(ROOTPATH . '.env')) {
try {
$dotenv->required(['app.baseURL', 'app.adminGateway', 'app.authGateway']);
} catch (ValidationException) {
// form to input instance configuration
return $this->instanceConfig();
}
try {
$dotenv->required([
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
]);
} catch (ValidationException) {
return $this->databaseConfig();
}
try {
$dotenv->required('cache.handler');
} catch (ValidationException) {
return $this->cacheConfig();
}
} else {
try {
$dotenv->required([
'app.baseURL',
'app.adminGateway',
'app.authGateway',
'database.default.hostname',
'database.default.database',
'database.default.username',
'database.default.password',
'database.default.DBPrefix',
'cache.handler',
]);
} catch (ValidationException) {
return view('install/manual_config');
}
}
try {
$db = db_connect();
// Check if superadmin has been created, meaning migrations and seeds have passed
if (
$db->tableExists('users') &&
(new UserModel())->countAll() > 0
) {
// if so, show a 404 page
throw PageNotFoundException::forPageNotFound();
}
} catch (DatabaseException) {
// Could not connect to the database
// show database config view to fix value
session()
->setFlashdata('error', lang('Install.messages.databaseConnectError'));
return view('install/database_config');
}
// migrate if no user has been created
$this->migrate();
// Check if all seeds have succeeded
$this->seed();
return $this->createSuperAdmin();
}
public function instanceConfig(): string
{
return view('install/instance_config');
}
public function attemptInstanceConfig(): RedirectResponse
{
$rules = [
'hostname' => 'required|validate_url',
'media_base_url' => 'permit_empty|validate_url',
'admin_gateway' => 'required',
'auth_gateway' => 'required|differs[admin_gateway]',
];
if (! $this->validate($rules)) {
return redirect()
->to((host_url() === null ? config('App') ->baseURL : host_url()) . config('App')->installGateway)
->withInput()
->with('errors', $this->validator->getErrors());
}
$baseUrl = $this->request->getPost('hostname');
$mediaBaseUrl = $this->request->getPost('media_base_url');
self::writeEnv([
'app.baseURL' => $baseUrl,
'app.mediaBaseURL' =>
$mediaBaseUrl === '' ? $baseUrl : $mediaBaseUrl,
'app.adminGateway' => $this->request->getPost('admin_gateway'),
'app.authGateway' => $this->request->getPost('auth_gateway'),
]);
helper('text');
// redirect to full install url with new baseUrl input
return redirect()->to(reduce_double_slashes($baseUrl . '/' . config('App')->installGateway));
}
public function databaseConfig(): string
{
return view('install/database_config');
}
public function attemptDatabaseConfig(): RedirectResponse
{
$rules = [
'db_hostname' => 'required',
'db_name' => 'required',
'db_username' => 'required',
'db_password' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'database.default.hostname' => $this->request->getPost('db_hostname'),
'database.default.database' => $this->request->getPost('db_name'),
'database.default.username' => $this->request->getPost('db_username'),
'database.default.password' => $this->request->getPost('db_password'),
'database.default.DBPrefix' => $this->request->getPost('db_prefix'),
]);
return redirect()->back();
}
public function cacheConfig(): string
{
return view('install/cache_config');
}
public function attemptCacheConfig(): RedirectResponse
{
$rules = [
'cache_handler' => 'required',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
self::writeEnv([
'cache.handler' => $this->request->getPost('cache_handler'),
]);
return redirect()->back();
}
/**
* Runs all database migrations required for instance.
*/
public function migrate(): void
{
$migrations = Services::migrations();
$migrations->setNamespace('Myth\Auth')
->latest();
$migrations->setNamespace('ActivityPub')
->latest();
$migrations->setNamespace('Analytics')
->latest();
$migrations->setNamespace(APP_NAMESPACE)
->latest();
}
/**
* Runs all database seeds required for instance.
*/
public function seed(): void
{
$seeder = Database::seeder();
// Seed database
$seeder->call('AppSeeder');
}
/**
* Returns the form to create a the first superadmin user for the instance.
*/
public function createSuperAdmin(): string
{
return view('install/create_superadmin');
}
/**
* Creates the first superadmin user or redirects back to form if any error.
*
* After creation, user is redirected to login page to input its credentials.
*/
public function attemptCreateSuperAdmin(): RedirectResponse
{
$userModel = new UserModel();
// Validate here first, since some things,
// like the password, can only be validated properly here.
$rules = array_merge(
$userModel->getValidationRules([
'only' => ['username'],
]),
[
'email' => 'required|valid_email|is_unique[users.email]',
'password' => 'required|strong_password',
],
);
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
// Save the user
$user = new User($this->request->getPost());
// Activate user
$user->activate();
$db = db_connect();
$db->transStart();
if (! ($userId = $userModel->insert($user, true))) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $userModel->errors());
}
// add newly created user to superadmin group
$authorization = Services::authorization();
$authorization->addUserToGroup($userId, 'superadmin');
$db->transComplete();
// Success!
// set redirect_url session as admin area to go to after login
session()
->set('redirect_url', route_to('admin'));
return redirect()
->route('login')
->with('message', lang('Install.messages.createSuperAdminSuccess'));
}
/**
* writes config values in .env file overwrites any existing key and appends new ones
*
* @param array<string, string> $configData key/value config pairs
*/
public static function writeEnv(array $configData): void
{
$envData = file(ROOTPATH . '.env'); // reads an array of lines
foreach ($configData as $key => $value) {
$replaced = false;
$keyVal = $key . '="' . $value . '"' . PHP_EOL;
$envData = array_map(
function ($line) use ($key, $keyVal, &$replaced) {
if (str_starts_with($line, $key)) {
$replaced = true;
return $keyVal;
}
return $line;
},
$envData
);
if (! $replaced) {
$envData[] = $keyVal;
}
}
file_put_contents(ROOTPATH . '.env', implode('', $envData));
}
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface;
class MapController extends BaseController
{
public function index(): string
{
$cacheName = implode(
'_',
array_filter([
'page',
'map',
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) {
return view('pages/map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
public function getEpisodesMarkers(): ResponseInterface
{
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = new EpisodeModel()
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->where('location_geo is not')
->findAll();
$found = [];
foreach ($episodes as $episode) {
$found[] = [
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'location_name' => esc($episode->location->name),
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'cover_url' => $episode->cover->thumbnail_url,
'podcast_title' => esc($episode->podcast->title),
'episode_title' => esc($episode->title),
];
}
// The page cache is set to a decade so it is deleted manually upon episode update
cache()
->save($cacheName, $found, DECADE);
}
return $this->response->setJSON($found);
}
}

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\EpisodeModel;
use CodeIgniter\HTTP\ResponseInterface;
class MapMarkerController extends BaseController
{
public function index(): string
{
$locale = service('request')
->getLocale();
$cacheName = "page_map_{$locale}";
if (! ($found = cache($cacheName))) {
$found = view('map', [], [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $found;
}
public function getEpisodesMarkers(): ResponseInterface
{
$cacheName = 'episodes_markers';
if (! ($found = cache($cacheName))) {
$episodes = (new EpisodeModel())
->where('`published_at` <= NOW()', null, false)
->where('location_geo is not', null)
->findAll();
$found = [];
foreach ($episodes as $episode) {
$found[] = [
'latitude' => $episode->location->latitude,
'longitude' => $episode->location->longitude,
'location_name' => $episode->location->name,
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'image_path' => $episode->image->thumbnail_url,
'podcast_title' => $episode->podcast->title,
'episode_title' => $episode->title,
];
}
// The page cache is set to a decade so it is deleted manually upon episode update
cache()
->save($cacheName, $found, DECADE);
}
return $this->response->setJSON($found);
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -20,13 +20,13 @@ class PageController extends BaseController
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
if ($params === []) { if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
$page = new PageModel() if (
->where('slug', $params[0])->first(); ($page = (new PageModel())->where('slug', $params[0])->first()) === null
if (! $page instanceof Page) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
@ -37,25 +37,13 @@ class PageController extends BaseController
public function index(): string public function index(): string
{ {
$cacheName = implode( $cacheName = "page-{$this->page->slug}";
'_',
array_filter([
'page',
$this->page->slug,
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
set_page_metatags($this->page);
$data = [ $data = [
'page' => $this->page, 'page' => $this->page,
]; ];
$found = view('pages/page', $data); $found = view('page', $data);
// The page cache is set to a decade so it is deleted manually upon page update // The page cache is set to a decade so it is deleted manually upon page update
cache() cache()

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Models\PlatformModel;
use CodeIgniter\Controller;
use CodeIgniter\HTTP\ResponseInterface;
/*
* Provide public access to all platforms so that they can be exported
*/
class PlatformController extends Controller
{
public function index(): ResponseInterface
{
$model = new PlatformModel();
return $this->response->setJSON($model->getPlatforms());
}
}

View file

@ -3,24 +3,25 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Controllers; namespace App\Controllers;
use ActivityPub\Objects\OrderedCollectionObject;
use ActivityPub\Objects\OrderedCollectionPage;
use Analytics\AnalyticsTrait;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Libraries\PodcastActor; use App\Libraries\PodcastActor;
use App\Libraries\PodcastEpisode; use App\Libraries\PodcastEpisode;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface; use CodeIgniter\HTTP\RedirectResponse;
use Modules\Analytics\AnalyticsTrait; use CodeIgniter\HTTP\Response;
use Modules\Fediverse\Objects\OrderedCollectionObject;
use Modules\Fediverse\Objects\OrderedCollectionPage;
class PodcastController extends BaseController class PodcastController extends BaseController
{ {
@ -30,12 +31,12 @@ class PodcastController extends BaseController
public function _remap(string $method, string ...$params): mixed public function _remap(string $method, string ...$params): mixed
{ {
if ($params === []) { if (count($params) === 0) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
if ( if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast ($podcast = (new PodcastModel())->getPodcastByName($params[0])) === null
) { ) {
throw PageNotFoundException::forPageNotFound(); throw PageNotFoundException::forPageNotFound();
} }
@ -47,7 +48,7 @@ class PodcastController extends BaseController
return $this->{$method}(...$params); return $this->{$method}(...$params);
} }
public function podcastActor(): ResponseInterface public function podcastActor(): RedirectResponse
{ {
$podcastActor = new PodcastActor($this->podcast); $podcastActor = new PodcastActor($this->podcast);
@ -58,7 +59,10 @@ class PodcastController extends BaseController
public function activity(): string public function activity(): string
{ {
$this->registerPodcastWebpageHit($this->podcast->id); // Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode( $cacheName = implode(
'_', '_',
@ -68,79 +72,30 @@ class PodcastController extends BaseController
'activity', 'activity',
service('request') service('request')
->getLocale(), ->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null, can_user_interact() ? '_authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
if (! ($cachedView = cache($cacheName))) { if (! ($cachedView = cache($cacheName))) {
set_podcast_metatags($this->podcast, 'activity');
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'posts' => new PostModel() 'statuses' => (new StatusModel())->getActorPublishedStatuses($this->podcast->actor_id),
->getActorPublishedPosts($this->podcast->actor_id),
]; ];
// if user is logged in then send to the authenticated activity view // if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) { if (can_user_interact()) {
helper('form'); helper('form');
return view('podcast/activity_authenticated', $data);
return view('podcast/activity', $data);
} }
$secondsToNextUnpublishedEpisode = new EpisodeModel() $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
->getSecondsToNextUnpublishedEpisode($this->podcast->id); $this->podcast->id,
);
return view('podcast/activity', $data, [ return view('podcast/activity', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE, 'cache' => $secondsToNextUnpublishedEpisode
'cache_name' => $cacheName, ? $secondsToNextUnpublishedEpisode
]); : DECADE,
}
return $cachedView;
}
public function about(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"podcast#{$this->podcast->id}",
'about',
service('request')
->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$stats = new EpisodeModel()
->getPodcastStats($this->podcast->id);
set_podcast_metatags($this->podcast, 'about');
$data = [
'podcast' => $this->podcast,
'stats' => $stats,
];
// // if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('podcast/about', $data);
}
$secondsToNextUnpublishedEpisode = new EpisodeModel()
->getSecondsToNextUnpublishedEpisode($this->podcast->id);
return view('podcast/about', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -150,14 +105,16 @@ class PodcastController extends BaseController
public function episodes(): string public function episodes(): string
{ {
$this->registerPodcastWebpageHit($this->podcast->id); // Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$yearQuery = $this->request->getGet('year'); $yearQuery = $this->request->getGet('year');
$seasonQuery = $this->request->getGet('season'); $seasonQuery = $this->request->getGet('season');
if (! $yearQuery && ! $seasonQuery) { if (! $yearQuery && ! $seasonQuery) {
$defaultQuery = new PodcastModel() $defaultQuery = (new PodcastModel())->getDefaultQuery($this->podcast->id);
->getDefaultQuery($this->podcast->id);
if ($defaultQuery) { if ($defaultQuery) {
if ($defaultQuery['type'] === 'season') { if ($defaultQuery['type'] === 'season') {
$seasonQuery = $defaultQuery['data']['season_number']; $seasonQuery = $defaultQuery['data']['season_number'];
@ -177,9 +134,7 @@ class PodcastController extends BaseController
$seasonQuery ? 'season' . $seasonQuery : null, $seasonQuery ? 'season' . $seasonQuery : null,
service('request') service('request')
->getLocale(), ->getLocale(),
is_unlocked($this->podcast->handle) ? 'unlocked' : null, can_user_interact() ? '_authenticated' : null,
auth()
->loggedIn() ? 'authenticated' : null,
]), ]),
); );
@ -195,17 +150,18 @@ class PodcastController extends BaseController
$isActive = $yearQuery === $year['year']; $isActive = $yearQuery === $year['year'];
if ($isActive) { if ($isActive) {
$activeQuery = [ $activeQuery = [
'type' => 'year', 'type' => 'year',
'value' => $year['year'], 'value' => $year['year'],
'label' => $year['year'], 'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'], 'number_of_episodes' => $year['number_of_episodes'],
]; ];
} }
$episodesNavigation[] = [ $episodesNavigation[] = [
'label' => $year['year'], 'label' => $year['year'],
'number_of_episodes' => $year['number_of_episodes'], 'number_of_episodes' => $year['number_of_episodes'],
'route' => route_to('podcast-episodes', $this->podcast->handle) . 'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'?year=' . '?year=' .
$year['year'], $year['year'],
'is_active' => $isActive, 'is_active' => $isActive,
@ -216,7 +172,7 @@ class PodcastController extends BaseController
$isActive = $seasonQuery === $season['season_number']; $isActive = $seasonQuery === $season['season_number'];
if ($isActive) { if ($isActive) {
$activeQuery = [ $activeQuery = [
'type' => 'season', 'type' => 'season',
'value' => $season['season_number'], 'value' => $season['season_number'],
'label' => lang('Podcast.season', [ 'label' => lang('Podcast.season', [
'seasonNumber' => $season['season_number'], 'seasonNumber' => $season['season_number'],
@ -230,30 +186,38 @@ class PodcastController extends BaseController
'seasonNumber' => $season['season_number'], 'seasonNumber' => $season['season_number'],
]), ]),
'number_of_episodes' => $season['number_of_episodes'], 'number_of_episodes' => $season['number_of_episodes'],
'route' => route_to('podcast-episodes', $this->podcast->handle) . 'route' =>
route_to('podcast-episodes', $this->podcast->name) .
'?season=' . '?season=' .
$season['season_number'], $season['season_number'],
'is_active' => $isActive, 'is_active' => $isActive,
]; ];
} }
set_podcast_metatags($this->podcast, 'episodes');
$data = [ $data = [
'podcast' => $this->podcast, 'podcast' => $this->podcast,
'episodesNav' => $episodesNavigation, 'episodesNav' => $episodesNavigation,
'activeQuery' => $activeQuery, 'activeQuery' => $activeQuery,
'episodes' => new EpisodeModel() 'episodes' => (new EpisodeModel())->getPodcastEpisodes(
->getPodcastEpisodes($this->podcast->id, $this->podcast->type, $yearQuery, $seasonQuery), $this->podcast->id,
$this->podcast->type,
$yearQuery,
$seasonQuery,
),
]; ];
if (auth()->loggedIn()) { $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
return view('podcast/episodes', $data); $this->podcast->id,
} );
$secondsToNextUnpublishedEpisode = new EpisodeModel() // if user is logged in then send to the authenticated episodes view
->getSecondsToNextUnpublishedEpisode($this->podcast->id); if (can_user_interact()) {
return view('podcast/episodes_authenticated', $data);
}
return view('podcast/episodes', $data, [ return view('podcast/episodes', $data, [
'cache' => $secondsToNextUnpublishedEpisode ?: DECADE, 'cache' => $secondsToNextUnpublishedEpisode
? $secondsToNextUnpublishedEpisode
: DECADE,
'cache_name' => $cacheName, 'cache_name' => $cacheName,
]); ]);
} }
@ -261,16 +225,19 @@ class PodcastController extends BaseController
return $cachedView; return $cachedView;
} }
public function episodeCollection(): ResponseInterface /**
* @noRector ReturnTypeDeclarationRector
*/
public function episodeCollection(): Response
{ {
if ($this->podcast->type === 'serial') { if ($this->podcast->type === 'serial') {
// podcast is serial // podcast is serial
$episodes = model('EpisodeModel') $episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`published_at` <= NOW()', null, false)
->orderBy('season_number DESC, number ASC'); ->orderBy('season_number DESC, number ASC');
} else { } else {
$episodes = model('EpisodeModel') $episodes = model('EpisodeModel')
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`published_at` <= NOW()', null, false)
->orderBy('published_at', 'DESC'); ->orderBy('published_at', 'DESC');
} }
@ -285,8 +252,10 @@ class PodcastController extends BaseController
$pager = $episodes->pager; $pager = $episodes->pager;
$orderedItems = []; $orderedItems = [];
foreach ($paginatedEpisodes as $episode) { if ($paginatedEpisodes !== null) {
$orderedItems[] = new PodcastEpisode($episode)->toArray(); foreach ($paginatedEpisodes as $episode) {
$orderedItems[] = (new PodcastEpisode($episode))->toArray();
}
} }
// @phpstan-ignore-next-line // @phpstan-ignore-next-line
@ -297,12 +266,4 @@ class PodcastController extends BaseController
->setContentType('application/activity+json') ->setContentType('application/activity+json')
->setBody($collection->toJSON()); ->setBody($collection->toJSON());
} }
public function links(): string
{
set_podcast_metatags($this->podcast, 'links');
return view('podcast/links', [
'podcast' => $this->podcast,
]);
}
} }

View file

@ -1,275 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Actor;
use App\Entities\Podcast;
use App\Entities\Post as CastopodPost;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
use Modules\Analytics\AnalyticsTrait;
use Modules\Fediverse\Controllers\PostController as FediversePostController;
use Override;
class PostController extends FediversePostController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
/**
* @var CastopodPost
*/
protected $post;
/**
* @var list<string>
*/
protected $helpers = ['auth', 'fediverse', 'svg', 'components', 'misc', 'seo', 'premium_podcasts'];
#[Override]
public function _remap(string $method, string ...$params): mixed
{
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($params[0])) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (count($params) <= 1) {
unset($params[0]);
return $this->{$method}(...$params);
}
if (
! ($post = new PostModel()->getPostById($params[1])) instanceof CastopodPost
) {
throw PageNotFoundException::forPageNotFound();
}
$this->post = $post;
// show 404 if post is private
if ($this->post->is_private && ! can_user_interact()) {
throw PageNotFoundException::forPageNotFound();
}
unset($params[0]);
unset($params[1]);
return $this->{$method}(...$params);
}
public function view(): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
$cacheName = implode(
'_',
array_filter([
'page',
"post#{$this->post->id}",
service('request')
->getLocale(),
auth()
->loggedIn() ? 'authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
set_post_metatags($this->post);
$data = [
'post' => $this->post,
'podcast' => $this->podcast,
];
// if user is logged in then send to the authenticated activity view
if (auth()->loggedIn()) {
helper('form');
return view('post/post', $data);
}
return view('post/post', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
#[Override]
public function createAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url_strict|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$message = $validData['message'];
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $validData['episode_url'];
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = new EpisodeModel()->getEpisodeBySlug($params['podcastHandle'], $params['episodeSlug']))
) {
$newPost->episode_id = $episode->id;
}
$newPost->message = $message;
$postModel = new PostModel();
if (
! $postModel
->addPost($newPost, ! (bool) $newPost->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Post has been successfully created
return redirect()->back();
}
#[Override]
public function replyAction(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$newPost = new CastopodPost([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->post->id,
'message' => $validData['message'],
'is_private' => $this->post->is_private,
'published_at' => Time::now(),
'created_by' => user_id(),
]);
if ($this->post->episode_id !== null) {
$newPost->episode_id = $this->post->episode_id;
}
$postModel = new PostModel();
if (! $postModel->addReply($newPost)) {
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
// Reply post without preview card has been successfully created
return redirect()->back();
}
#[Override]
public function favouriteAction(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->post);
return redirect()->back();
}
#[Override]
public function reblogAction(): RedirectResponse
{
new PostModel()
->toggleReblog(interact_as_actor(), $this->post);
return redirect()->back();
}
public function action(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$validData = $this->validator->getValidated();
$action = $validData['action'];
return match ($action) {
'favourite' => $this->favouriteAction(),
'reblog' => $this->reblogAction(),
'reply' => $this->replyAction(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteActionView(string $action): string
{
$this->registerPodcastWebpageHit($this->podcast->id);
set_remote_actions_metatags($this->post, $action);
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'post' => $this->post,
'action' => $action,
];
helper('form');
// NO VIEW CACHING: form has a CSRF token which should change on each request
return view('post/remote_action', $data);
}
}

View file

@ -0,0 +1,254 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use ActivityPub\Controllers\StatusController as ActivityPubStatusController;
use ActivityPub\Entities\Status as ActivityPubStatus;
use Analytics\AnalyticsTrait;
use App\Entities\Actor;
use App\Entities\Podcast;
use App\Entities\Status as CastopodStatus;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use App\Models\StatusModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\URI;
use CodeIgniter\I18n\Time;
class StatusController extends ActivityPubStatusController
{
use AnalyticsTrait;
protected Podcast $podcast;
protected Actor $actor;
/**
* @var string[]
*/
protected $helpers = ['auth', 'activitypub', 'svg', 'components', 'misc'];
public function _remap(string $method, string ...$params): mixed
{
if (
($podcast = (new PodcastModel())->getPodcastByName($params[0],)) === null
) {
throw PageNotFoundException::forPageNotFound();
}
$this->podcast = $podcast;
$this->actor = $this->podcast->actor;
if (
count($params) > 1 &&
($status = (new StatusModel())->getStatusById($params[1])) !== null
) {
$this->status = $status;
unset($params[0]);
unset($params[1]);
}
return $this->{$method}(...$params);
}
public function view(): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter([
'page',
"status#{$this->status->id}",
service('request')
->getLocale(),
can_user_interact() ? '_authenticated' : null,
]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'status' => $this->status,
];
// if user is logged in then send to the authenticated activity view
if (can_user_interact()) {
helper('form');
return view('podcast/status_authenticated', $data);
}
return view('podcast/status', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return $cachedView;
}
public function attemptCreate(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
'episode_url' => 'valid_url|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$message = $this->request->getPost('message');
$newStatus = new CastopodStatus([
'actor_id' => interact_as_actor_id(),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
// get episode if episodeUrl has been set
$episodeUri = $this->request->getPost('episode_url');
if (
$episodeUri &&
($params = extract_params_from_episode_uri(new URI($episodeUri))) &&
($episode = (new EpisodeModel())->getEpisodeBySlug($params['podcastName'], $params['episodeSlug']))
) {
$newStatus->episode_id = $episode->id;
}
$newStatus->message = $message;
$statusModel = new StatusModel();
if (
! $statusModel
->addStatus($newStatus, ! (bool) $newStatus->episode_id, true)
) {
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
// Status has been successfully created
return redirect()->back();
}
public function attemptReply(): RedirectResponse
{
$rules = [
'message' => 'required|max_length[500]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$newStatus = new ActivityPubStatus([
'actor_id' => interact_as_actor_id(),
'in_reply_to_id' => $this->status->id,
'message' => $this->request->getPost('message'),
'published_at' => Time::now(),
'created_by' => user_id(),
]);
$statusModel = new StatusModel();
if (! $statusModel->addReply($newStatus)) {
return redirect()
->back()
->withInput()
->with('errors', $statusModel->errors());
}
// Reply status without preview card has been successfully created
return redirect()->back();
}
public function attemptFavourite(): RedirectResponse
{
model('FavouriteModel')->toggleFavourite(interact_as_actor(), $this->status);
return redirect()->back();
}
public function attemptReblog(): RedirectResponse
{
(new StatusModel())->toggleReblog(interact_as_actor(), $this->status);
return redirect()->back();
}
public function attemptAction(): RedirectResponse
{
$rules = [
'action' => 'required|in_list[favourite,reblog,reply]',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$action = $this->request->getPost('action');
return match ($action) {
'favourite' => $this->attemptFavourite(),
'reblog' => $this->attemptReblog(),
'reply' => $this->attemptReply(),
default => redirect()
->back()
->withInput()
->with('errors', 'error'),
};
}
public function remoteAction(string $action): string
{
// Prevent analytics hit when authenticated
if (! can_user_interact()) {
$this->registerPodcastWebpageHit($this->podcast->id);
}
$cacheName = implode(
'_',
array_filter(['page', "status#{$this->status->id}", "remote_{$action}", service('request') ->getLocale()]),
);
if (! ($cachedView = cache($cacheName))) {
$data = [
'podcast' => $this->podcast,
'actor' => $this->actor,
'status' => $this->status,
'action' => $action,
];
helper('form');
return view('podcast/status_remote_action', $data, [
'cache' => DECADE,
'cache_name' => $cacheName,
]);
}
return (string) $cachedView;
}
}

View file

@ -1,117 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Controllers;
use App\Entities\Podcast;
use App\Models\PodcastModel;
use CodeIgniter\Controller;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\ResponseInterface;
class WebmanifestController extends Controller
{
/**
* @var array<string, array<string, string>>
*/
final public const array THEME_COLORS = [
'pine' => [
'theme' => '#009486',
'background' => '#F0F9F8',
],
'lake' => [
'theme' => '#00ACE0',
'background' => '#F0F7F9',
],
'jacaranda' => [
'theme' => '#562CDD',
'background' => '#F2F0F9',
],
'crimson' => [
'theme' => '#F24562',
'background' => '#F9F0F2',
],
'amber' => [
'theme' => '#FF6224',
'background' => '#F9F3F0',
],
'onyx' => [
'theme' => '#040406',
'background' => '#F3F3F7',
],
];
public function index(): ResponseInterface
{
helper('misc');
$webmanifest = [
'name' => esc(service('settings') ->get('App.siteName')),
'description' => esc(service('settings') ->get('App.siteDescription')),
'lang' => service('request')
->getLocale(),
'start_url' => base_url(),
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => get_site_icon_url('192'),
'type' => 'image/png',
'sizes' => '192x192',
],
[
'src' => get_site_icon_url('512'),
'type' => 'image/png',
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
public function podcastManifest(string $podcastHandle): ResponseInterface
{
if (
! ($podcast = new PodcastModel()->getPodcastByHandle($podcastHandle)) instanceof Podcast
) {
throw PageNotFoundException::forPageNotFound();
}
$webmanifest = [
'name' => esc($podcast->title),
'short_name' => $podcast->at_handle,
'description' => $podcast->description,
'lang' => $podcast->language_code,
'start_url' => $podcast->link,
'scope' => '/' . $podcast->at_handle,
'display' => 'standalone',
'orientation' => 'portrait',
'theme_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['theme'],
'background_color' => self::THEME_COLORS[service('settings')->get('App.theme')]['background'],
'icons' => [
[
'src' => $podcast->cover->webmanifest192_url,
'type' => $podcast->cover->webmanifest192_mimetype,
'sizes' => '192x192',
],
[
'src' => $podcast->cover->webmanifest512_url,
'type' => $podcast->cover->webmanifest512_mimetype,
'sizes' => '512x512',
],
],
];
return $this->response->setJSON($webmanifest);
}
}

View file

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* Class AddAddPodcastsPlatforms Creates podcasts_platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace Analytics\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'link_content' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embeddable_player' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->createTable('podcasts_platforms');
}
public function down(): void
{
$this->forge->dropTable('podcasts_platforms');
}
}

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* Class AddCategories Creates categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCategories extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'code' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('code');
$this->forge->addForeignKey('parent_id', 'categories', 'id');
$this->forge->createTable('categories');
}
public function down(): void
{
$this->forge->dropTable('categories');
}
}

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* Class AddLanguages Creates languages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddLanguages extends Migration
{
public function up(): void
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
$this->forge->addPrimaryKey('code');
$this->forge->createTable('languages');
}
public function down(): void
{
$this->forge->dropTable('languages');
}
}

View file

@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcasts Creates podcasts table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcasts extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'guid' => [
'type' => 'CHAR',
'constraint' => 36,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'name' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('name');
$this->forge->addUniqueKey('guid');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey('actor_id', 'activitypub_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('podcasts');
}
public function down(): void
{
$this->forge->dropTable('podcasts');
}
}

View file

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodes Creates episodes table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodes extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
],
'audio_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_duration' => [
// exact value for duration with max 99999,999 ~ 27.7 hours
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
'comment' => 'Playtime in seconds',
],
'audio_file_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'audio_file_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'File size in bytes',
],
'audio_file_header_size' => [
'type' => 'INT',
'unsigned' => true,
'comment' => 'Header size in bytes',
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
],
'transcript_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'transcript_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'chapters_file_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
'chapters_file_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
'default' => null,
],
'number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'favourites_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'reblogs_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'statuses_total' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
}
public function down(): void
{
$this->forge->dropTable('episodes');
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* Class AddSoundbites Creates soundbites table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddSoundbites extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'start_time' => [
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
],
'duration' => [
// soundbite duration cannot be higher than 9999,999 seconds ~ 2.77 hours
'type' => 'DECIMAL(7,3)',
'unsigned' => true,
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('soundbites');
}
public function down(): void
{
$this->forge->dropTable('soundbites');
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* Class AddPlatforms Creates platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPlatforms extends Migration
{
public function up(): void
{
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
'default' => null,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT NOW()');
$this->forge->addField('`updated_at` timestamp NOT NULL DEFAULT NOW() ON UPDATE NOW()');
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
public function down(): void
{
$this->forge->dropTable('platforms');
}
}

View file

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastUsers Creates podcast_users table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsUsers extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'user_id' => [
'type' => 'INT',
'unsigned' => true,
],
'group_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['user_id', 'podcast_id']);
$this->forge->addForeignKey('user_id', 'users', 'id', '', 'CASCADE');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('group_id', 'auth_groups', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_users');
}
public function down(): void
{
$this->forge->dropTable('podcasts_users');
}
}

View file

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* Class AddPages Creates pages table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPages extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 191,
'unique' => true,
],
'content_markdown' => [
'type' => 'TEXT',
],
'content_html' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
'deleted_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
public function down(): void
{
$this->forge->dropTable('pages');
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsCategories Creates podcasts_categories table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsCategories extends Migration
{
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_categories');
}
public function down(): void
{
$this->forge->dropTable('podcasts_categories');
}
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/**
* Class Persons Creates persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'full_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the full name or alias of the person.',
],
'unique_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the slug name or alias of the person.',
'unique' => true,
],
'information_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' =>
'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'image_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
}
public function down(): void
{
$this->forge->dropTable('persons');
}
}

View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsPersons Creates podcasts_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddPodcastsPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['podcast_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_persons');
}
public function down(): void
{
$this->forge->dropTable('podcasts_persons');
}
}

View file

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodesPersons Creates episodes_persons table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodesPersons extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'episode_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('episodes_persons');
}
public function down(): void
{
$this->forge->dropTable('episodes_persons');
}
}

View file

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/**
* Class AddCreditView Creates Credit View in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreditView extends Migration
{
public function up(): void
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personsTable = $this->db->prefixTable('persons');
$podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<CODE_SAMPLE
CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
INNER JOIN `{$episodesTable}`
ON (`episode_id`=`{$episodesTable}`.`id`)
WHERE `{$episodesTable}`.published_at <= NOW()
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `{$viewName}`");
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeIdToStatuses Adds episode_id field to activitypub_statuses table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodeIdToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `episode_id` INT UNSIGNED NULL AFTER `replies_count`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_episode_id_foreign(episode_id) REFERENCES {$prefix}episodes(id) ON DELETE CASCADE;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_episode_id_foreign');
$this->forge->dropColumn('activitypub_statuses', 'episode_id');
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToStatuses Adds created_by field to activitypub_statuses table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCreatedByToStatuses extends Migration
{
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<CODE_SAMPLE
ALTER TABLE {$prefix}activitypub_statuses
ADD COLUMN `created_by` INT UNSIGNED AFTER `episode_id`,
ADD FOREIGN KEY {$prefix}activitypub_statuses_created_by_foreign(created_by) REFERENCES {$prefix}users(id) ON DELETE CASCADE;
CODE_SAMPLE;
$this->db->query($createQuery);
}
public function down(): void
{
$this->forge->dropForeignKey('activitypub_statuses', 'activitypub_statuses_created_by_foreign');
$this->forge->dropColumn('activitypub_statuses', 'created_by');
}
}

View file

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCategories Creates categories table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddCategories extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
],
'parent_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'code' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'apple_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'google_category' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey('code');
$this->forge->addForeignKey('parent_id', 'categories', 'id');
$this->forge->createTable('categories');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('categories');
}
}

View file

@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddLanguages Creates languages table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddLanguages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'code' => [
'type' => 'VARCHAR',
'comment' => 'ISO 639-1 language code',
'constraint' => 2,
],
'native_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
]);
$this->forge->addPrimaryKey('code');
$this->forge->createTable('languages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('languages');
}
}

View file

@ -1,216 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcasts Creates podcasts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcasts extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'guid' => [
'type' => 'CHAR',
'constraint' => 36,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'handle' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
],
'banner_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
],
'owner_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'owner_email' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'publisher' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['episodic', 'serial'],
'default' => 'episodic',
],
'copyright' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_completed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_locked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 1,
],
'imported_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The RSS feed URL if this podcast was imported, NULL otherwise.',
'null' => true,
],
'new_feed_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The RSS new feed URL if this podcast is moving out, NULL otherwise.',
'null' => true,
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'is_premium_by_default' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
// TODO: remove name in favor of username from actor
$this->forge->addUniqueKey('handle');
$this->forge->addUniqueKey('guid');
$this->forge->addUniqueKey('actor_id');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('cover_id', 'media', 'id');
$this->forge->addForeignKey('banner_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('category_id', 'categories', 'id');
$this->forge->addForeignKey('language_code', 'languages', 'code');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('podcasts');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts');
}
}

View file

@ -1,182 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodes Creates episodes table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'guid' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'audio_id' => [
'type' => 'INT',
'unsigned' => true,
],
'description_markdown' => [
'type' => 'TEXT',
],
'description_html' => [
'type' => 'TEXT',
],
'cover_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'transcript_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'transcript_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'chapters_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'chapters_remote_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'parental_advisory' => [
'type' => 'ENUM',
'constraint' => ['clean', 'explicit'],
'null' => true,
],
'number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'season_number' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['trailer', 'full', 'bonus'],
'default' => 'full',
],
'is_blocked' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'location_name' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'location_geo' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'location_osm' => [
'type' => 'VARCHAR',
'constraint' => 12,
'null' => true,
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'posts_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'comments_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
],
'is_premium' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('audio_id', 'media', 'id');
$this->forge->addForeignKey('cover_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('transcript_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('chapters_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('episodes');
// Add Full-Text Search index on title and description_markdown
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
ADD FULLTEXT title (title, description_markdown);
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episodes');
}
}

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPlatforms Creates platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()',
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('platforms');
}
}

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddAddPodcastsPlatforms Creates podcasts_platforms table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsPlatforms extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug', 'CASCADE');
$this->forge->createTable('podcasts_platforms');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_platforms');
}
}

View file

@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeComments creates episode_comments table in database
*
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodeComments extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'BINARY',
'constraint' => 16,
],
'uri' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'in_reply_to_id' => [
'type' => 'BINARY',
'constraint' => 16,
'null' => true,
],
'message' => [
'type' => 'VARCHAR',
'constraint' => 5000,
],
'message_html' => [
'type' => 'VARCHAR',
'constraint' => 6000,
],
'likes_count' => [
'type' => 'INT',
'unsigned' => true,
],
'replies_count' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->createTable('episode_comments');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episode_comments');
}
}

View file

@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddLikes Creates likes table in database
*
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddLikes extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'actor_id' => [
'type' => 'INT',
'unsigned' => true,
],
'comment_id' => [
'type' => 'BINARY',
'constraint' => 16,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT current_timestamp()');
$this->forge->addPrimaryKey(['actor_id', 'comment_id']);
$this->forge->addForeignKey('actor_id', 'fediverse_actors', 'id', '', 'CASCADE');
$this->forge->addForeignKey('comment_id', 'episode_comments', 'id', '', 'CASCADE');
$this->forge->createTable('likes');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('likes');
}
}

View file

@ -1,59 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPages Creates pages table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPages extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 128,
'unique' => true,
],
'content_markdown' => [
'type' => 'TEXT',
],
'content_html' => [
'type' => 'TEXT',
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->createTable('pages');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('pages');
}
}

View file

@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsCategories Creates podcasts_categories table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsCategories extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'category_id' => [
'type' => 'INT',
'unsigned' => true,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'category_id']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('category_id', 'categories', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_categories');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_categories');
}
}

View file

@ -1,105 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddClips extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'start_time' => [
'type' => 'DECIMAL(8,3)',
'unsigned' => true,
],
'duration' => [
// clip duration cannot be higher than 9999,999 seconds ~ 2.77 hours
'type' => 'DECIMAL(7,3)',
'unsigned' => true,
],
'title' => [
'type' => 'VARCHAR',
'constraint' => 128,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['audio', 'video'],
],
'media_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'metadata' => [
'type' => 'JSON',
'null' => true,
],
'status' => [
'type' => 'ENUM',
'constraint' => ['queued', 'pending', 'running', 'passed', 'failed'],
],
'logs' => [
'type' => 'TEXT',
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'job_started_at' => [
'type' => 'DATETIME',
'null' => true,
],
'job_ended_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('media_id', 'media', 'id', '', 'CASCADE');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('clips');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('clips');
}
}

View file

@ -1,78 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class Persons Creates persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'full_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the full name or alias of the person.',
],
'unique_name' => [
'type' => 'VARCHAR',
'constraint' => 192,
'comment' => 'This is the slug name or alias of the person.',
'unique' => true,
],
'information_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'comment' => 'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'avatar_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
],
'created_by' => [
'type' => 'INT',
'unsigned' => true,
],
'updated_by' => [
'type' => 'INT',
'unsigned' => true,
],
'created_at' => [
'type' => 'DATETIME',
],
'updated_at' => [
'type' => 'DATETIME',
],
]);
$this->forge->addKey('id', true);
$this->forge->addForeignKey('avatar_id', 'media', 'id', '', 'SET NULL');
$this->forge->addForeignKey('created_by', 'users', 'id');
$this->forge->addForeignKey('updated_by', 'users', 'id');
$this->forge->createTable('persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('persons');
}
}

View file

@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsPersons Creates podcasts_persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey(['podcast_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('podcasts_persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('podcasts_persons');
}
}

View file

@ -1,62 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodesPersons Creates episodes_persons table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodesPersons extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_id' => [
'type' => 'INT',
'unsigned' => true,
],
'person_group' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'person_role' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addUniqueKey(['podcast_id', 'episode_id', 'person_id', 'person_group', 'person_role']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
$this->forge->addForeignKey('person_id', 'persons', 'id', '', 'CASCADE');
$this->forge->createTable('episodes_persons');
}
#[Override]
public function down(): void
{
$this->forge->dropTable('episodes_persons');
}
}

View file

@ -1,49 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddCreditsView extends BaseMigration
{
#[Override]
public function up(): void
{
// Creates View for credit UNION query
$viewName = $this->db->prefixTable('credits');
$personsTable = $this->db->prefixTable('persons');
$podcastPersonsTable = $this->db->prefixTable('podcasts_persons');
$episodePersonsTable = $this->db->prefixTable('episodes_persons');
$episodesTable = $this->db->prefixTable('episodes');
$createQuery = <<<SQL
CREATE VIEW `{$viewName}` AS
SELECT `person_group`, `person_id`, `full_name`, `person_role`, `podcast_id`, NULL AS `episode_id` FROM `{$podcastPersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
UNION
SELECT `person_group`, `person_id`, `full_name`, `person_role`, {$episodePersonsTable}.`podcast_id`, `episode_id` FROM `{$episodePersonsTable}`
INNER JOIN `{$personsTable}`
ON (`person_id`=`{$personsTable}`.`id`)
INNER JOIN `{$episodesTable}`
ON (`episode_id`=`{$episodesTable}`.`id`)
WHERE `{$episodesTable}`.published_at <= UTC_TIMESTAMP()
ORDER BY `person_group`, `full_name`, `person_role`, `podcast_id`, `episode_id`;
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$viewName = $this->db->prefixTable('credits');
$this->db->query("DROP VIEW IF EXISTS `{$viewName}`");
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddEpisodeIdToPosts Adds episode_id field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddEpisodeIdToPosts extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$this->forge->addColumn('fediverse_posts', [
'episode_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'replies_count',
],
]);
$this->forge->addForeignKey(
'episode_id',
'episodes',
'id',
'',
'CASCADE',
$prefix . 'fediverse_posts_episode_id_foreign',
);
$this->forge->processIndexes('fediverse_posts');
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropForeignKey('fediverse_posts', $prefix . 'fediverse_posts_episode_id_foreign');
$this->forge->dropColumn('fediverse_posts', 'episode_id');
}
}

View file

@ -1,52 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToPosts Adds created_by field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddCreatedByToPosts extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$this->forge->addColumn('fediverse_posts', [
'created_by' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'after' => 'episode_id',
],
]);
$this->forge->addForeignKey(
'created_by',
'users',
'id',
'',
'CASCADE',
$prefix . 'fediverse_posts_created_by_foreign',
);
$this->forge->processIndexes('fediverse_posts');
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$this->forge->dropForeignKey('fediverse_posts', $prefix . 'fediverse_posts_created_by_foreign');
$this->forge->dropColumn('fediverse_posts', 'created_by');
}
}

View file

@ -1,56 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use Override;
class AddFullTextSearchIndexes extends BaseMigration
{
#[Override]
public function up(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes DROP INDEX title;
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
ADD FULLTEXT episodes_search (title, description_markdown, slug, location_name);
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}podcasts
ADD FULLTEXT podcasts_search (title, description_markdown, handle, location_name);
SQL;
$this->db->query($createQuery);
}
#[Override]
public function down(): void
{
$prefix = $this->db->getPrefix();
$createQuery = <<<SQL
ALTER TABLE {$prefix}episodes
DROP INDEX episodes_search;
SQL;
$this->db->query($createQuery);
$createQuery = <<<SQL
ALTER TABLE {$prefix}podcasts
DROP INDEX podcasts_search;
SQL;
$this->db->query($createQuery);
}
}

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use Override;
class AddEpisodePreviewId extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'preview_id' => [
'type' => 'BINARY',
'constraint' => 16,
'after' => 'podcast_id',
],
];
$this->forge->addColumn('episodes', $fields);
// set preview_id as unique key
$prefix = $this->db->getPrefix();
$uniquePreviewId = <<<CODE_SAMPLE
ALTER TABLE `{$prefix}episodes`
ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`);
CODE_SAMPLE;
$this->db->query($uniquePreviewId);
}
#[Override]
public function down(): void
{
$fields = ['preview_id'];
$this->forge->dropColumn('episodes', $fields);
}
}

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsOwnerEmailRemovedFromFeed adds is_owner_email_removed_from_feed field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsOwnerEmailRemovedFromFeed extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'is_owner_email_removed_from_feed' => [
'type' => 'BOOLEAN',
'null' => false,
'default' => 0,
'after' => 'owner_email',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$fields = ['is_owner_email_removed_from_feed'];
$this->forge->dropColumn('podcasts', $fields);
}
}

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsMediumField extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'medium' => [
'type' => "ENUM('podcast','music','audiobook')",
'null' => false,
'default' => 'podcast',
'after' => 'type',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$fields = ['medium'];
$this->forge->dropColumn('podcasts', $fields);
}
}

View file

@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsVerifyTxtField adds 1 field to podcast table in database to support podcast:txt tag
*
* @copyright 2024 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class AddPodcastsVerifyTxtField extends BaseMigration
{
#[Override]
public function up(): void
{
$fields = [
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
];
$this->forge->addColumn('podcasts', $fields);
}
#[Override]
public function down(): void
{
$this->forge->dropColumn('podcasts', 'verify_txt');
}
}

View file

@ -1,157 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
class RefactorPlatforms extends Migration
{
#[Override]
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'INT',
'unsigned' => true,
'auto_increment' => true,
],
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
'after' => 'podcast_id',
],
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey('id');
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE', 'platforms_podcast_id_foreign');
$this->forge->addUniqueKey(['podcast_id', 'type', 'slug']);
$this->forge->createTable('platforms_temp');
$platformsData = $this->db->table('podcasts_platforms')
->select('podcasts_platforms.*, type')
->join('platforms', 'platforms.slug = podcasts_platforms.platform_slug')
->get()
->getResultArray();
$data = [];
foreach ($platformsData as $platformData) {
$data[] = [
'podcast_id' => $platformData['podcast_id'],
'type' => $platformData['type'],
'slug' => $platformData['platform_slug'],
'link_url' => $platformData['link_url'],
'account_id' => $platformData['account_id'],
'is_visible' => $platformData['is_visible'],
];
}
if ($data !== []) {
$this->db->table('platforms_temp')
->insertBatch($data);
}
$this->forge->dropTable('platforms');
$this->forge->dropTable('podcasts_platforms');
$this->forge->renameTable('platforms_temp', 'platforms');
}
#[Override]
public function down(): void
{
// delete platforms
$this->forge->dropTable('platforms');
// recreate platforms and podcasts_platforms tables
$this->forge->addField([
'slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'type' => [
'type' => 'ENUM',
'constraint' => ['podcasting', 'social', 'funding'],
],
'label' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'home_url' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
'submit_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
]);
$this->forge->addField('`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP()');
$this->forge->addField(
'`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP()',
);
$this->forge->addPrimaryKey('slug');
$this->forge->createTable('platforms');
$this->forge->addField([
'podcast_id' => [
'type' => 'INT',
'unsigned' => true,
],
'platform_slug' => [
'type' => 'VARCHAR',
'constraint' => 32,
],
'link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
],
'account_id' => [
'type' => 'VARCHAR',
'constraint' => 128,
'null' => true,
],
'is_visible' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
'is_on_embed' => [
'type' => 'TINYINT',
'constraint' => 1,
'default' => 0,
],
]);
$this->forge->addPrimaryKey(['podcast_id', 'platform_slug']);
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
$this->forge->addForeignKey('platform_slug', 'platforms', 'slug', 'CASCADE');
$this->forge->createTable('podcasts_platforms');
}
}

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
use Override;
/**
* CodeIgniter 4.5.1 introduces new DataCaster class that breaks deserialization of import queue tasks.
* This just removes them altogether.
*/
class ClearImportQueue extends Migration
{
#[Override]
public function up(): void
{
service('settings')->forget('Import.queue');
}
#[Override]
public function down(): void
{
// nothing
}
}

View file

@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddEpisodeDownloadsCount extends Migration
{
public function up(): void
{
$fields = [
'downloads_count' => [
'type' => 'INT',
'unsigned' => true,
'default' => 0,
'after' => 'is_published_on_hubs',
],
];
$this->forge->addColumn('episodes', $fields);
}
public function down(): void
{
$this->forge->dropColumn('episodes', 'downloads_count');
}
}

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedPodcastsFields extends BaseMigration
{
#[Override]
public function up(): void
{
// TODO: migrate data
$this->forge->dropColumn(
'podcasts',
'episode_description_footer_markdown,episode_description_footer_html,is_owner_email_removed_from_feed,medium,payment_pointer,verify_txt,custom_rss,partner_id,partner_link_url,partner_image_url',
);
}
#[Override]
public function down(): void
{
$fields = [
'episode_description_footer_markdown' => [
'type' => 'TEXT',
'null' => true,
],
'episode_description_footer_html' => [
'type' => 'TEXT',
'null' => true,
],
'is_owner_email_removed_from_feed' => [
'type' => 'BOOLEAN',
'null' => false,
'default' => 0,
'after' => 'owner_email',
],
'medium' => [
'type' => "ENUM('podcast','music','audiobook')",
'null' => false,
'default' => 'podcast',
'after' => 'type',
],
'payment_pointer' => [
'type' => 'VARCHAR',
'constraint' => 128,
'comment' => 'Wallet address for Web Monetization payments',
'null' => true,
],
'verify_txt' => [
'type' => 'TEXT',
'null' => true,
'after' => 'location_osm',
],
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
'partner_id' => [
'type' => 'VARCHAR',
'constraint' => 32,
'null' => true,
],
'partner_link_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
'partner_image_url' => [
'type' => 'VARCHAR',
'constraint' => 512,
'null' => true,
],
];
$this->forge->addColumn('podcasts', $fields);
}
}

View file

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddPodcastsMediumField adds medium field to podcast table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use Override;
class DropDeprecatedEpisodesFields extends BaseMigration
{
#[Override]
public function up(): void
{
$this->forge->dropColumn('episodes', 'custom_rss');
}
#[Override]
public function down(): void
{
$fields = [
'custom_rss' => [
'type' => 'JSON',
'null' => true,
],
];
$this->forge->addColumn('episodes', $fields);
}
}

View file

@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AddCreatedByToPosts Adds created_by field to posts table in database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Migrations;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Migration;
use Override;
class BaseMigration extends Migration
{
/**
* Database Connection instance
*
* @var BaseConnection
*/
protected $db;
#[Override]
public function up(): void
{
}
#[Override]
public function down(): void
{
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/** /**
* Class AppSeeder Calls all required seeders for castopod to work properly * Class AppSeeder Calls all required seeders for castopod to work properly
* *
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -13,14 +13,14 @@ declare(strict_types=1);
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Override;
class AppSeeder extends Seeder class AppSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$this->call('AuthSeeder');
$this->call('CategorySeeder'); $this->call('CategorySeeder');
$this->call('LanguageSeeder'); $this->call('LanguageSeeder');
$this->call('PlatformSeeder');
} }
} }

View file

@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
/**
* Class PermissionSeeder Inserts permissions
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class AuthSeeder extends Seeder
{
/**
* @var array<string, string>[]
*/
protected array $groups = [
[
'name' => 'superadmin',
'description' =>
'Somebody who has access to all the castopod instance features',
],
[
'name' => 'podcast_admin',
'description' =>
'Somebody who has access to all the features within a given podcast',
],
];
/**
* Build permissions array as a list of:
*
* ``` context => [ [action, description], [action, description], ... ] ```
*
* @var array<string, array<string, string|array>[]>
*/
protected array $permissions = [
'users' => [
[
'name' => 'create',
'description' => 'Create a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all users',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any user info',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_authorizations',
'description' => 'Add or remove roles/permissions to a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'manage_bans',
'description' => 'Ban / unban a user',
'has_permission' => ['superadmin'],
],
[
'name' => 'force_pass_reset',
'description' =>
'Force a user to update his password upon next login',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete user without removing him from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of a user from the database',
'has_permission' => ['superadmin'],
],
],
'pages' => [
[
'name' => 'manage',
'description' => 'List / create / edit / delete pages',
'has_permission' => ['superadmin'],
],
],
'podcasts' => [
[
'name' => 'create',
'description' => 'Add a new podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'import',
'description' => 'Import a new podcast from an external feed',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all podcasts and their episodes',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any podcast and their contributors list',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete a podcast without removing it from database',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete_permanently',
'description' => 'Delete any podcast from the database',
'has_permission' => ['superadmin'],
],
],
'episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of any podcast',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any episode of any podcast',
'has_permission' => ['superadmin'],
],
],
'podcast' => [
[
'name' => 'view',
'description' => 'View a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_contributors',
'description' =>
'Add / remove contributors to a podcast and edit their roles',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_platforms',
'description' => 'Set / remove platform links of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'manage_publications',
'description' =>
'Publish / unpublish episodes & statuses of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'interact_as',
'description' =>
'Interact as the podcast to favourite / share or reply to statuses.',
'has_permission' => ['podcast_admin'],
],
],
'podcast_episodes' => [
[
'name' => 'list',
'description' => 'List all episodes of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'view',
'description' => 'View any episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'create',
'description' => 'Add new episodes for a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'edit',
'description' => 'Edit an episode of a podcast',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete',
'description' =>
'Delete an episode of a podcast without removing it from the database',
'has_permission' => ['podcast_admin'],
],
[
'name' => 'delete_permanently',
'description' =>
'Delete all occurrences of an episode of a podcast from the database',
'has_permission' => ['podcast_admin'],
],
],
'person' => [
[
'name' => 'create',
'description' => 'Add a new person',
'has_permission' => ['superadmin'],
],
[
'name' => 'list',
'description' => 'List all persons',
'has_permission' => ['superadmin'],
],
[
'name' => 'view',
'description' => 'View any person',
'has_permission' => ['superadmin'],
],
[
'name' => 'edit',
'description' => 'Edit a person',
'has_permission' => ['superadmin'],
],
[
'name' => 'delete',
'description' =>
'Delete permanently any person from the database',
'has_permission' => ['superadmin'],
],
],
'fediverse' => [
[
'name' => 'block_actors',
'description' =>
'Block an activitypub actors from interacting with the instance.',
'has_permission' => ['superadmin'],
],
[
'name' => 'block_domains',
'description' =>
'Block an activitypub domains from interacting with the instance.',
'has_permission' => ['superadmin'],
],
],
];
public function run(): void
{
$groupId = 0;
$dataGroups = [];
foreach ($this->groups as $group) {
$dataGroups[] = [
'id' => ++$groupId,
'name' => $group['name'],
'description' => $group['description'],
];
}
// Map permissions to a format the `auth_permissions` table expects
$dataPermissions = [];
$dataGroupsPermissions = [];
$permissionId = 0;
foreach ($this->permissions as $context => $actions) {
foreach ($actions as $action) {
$dataPermissions[] = [
'id' => ++$permissionId,
'name' => $context . '-' . $action['name'],
'description' => $action['description'],
];
foreach ($action['has_permission'] as $role) {
// link permission to specified groups
$dataGroupsPermissions[] = [
'group_id' => $this->getGroupIdByName($role, $dataGroups),
'permission_id' => $permissionId,
];
}
}
}
$this->db
->table('auth_permissions')
->ignore(true)
->insertBatch($dataPermissions);
$this->db
->table('auth_groups')
->ignore(true)
->insertBatch($dataGroups);
$this->db
->table('auth_groups_permissions')
->ignore(true)
->insertBatch($dataGroupsPermissions);
}
/**
* @param array<string, string|int>[] $dataGroups
*/
public static function getGroupIdByName(string $name, array $dataGroups): ?int
{
foreach ($dataGroups as $group) {
if ($group['name'] === $name) {
return $group['id'];
}
}
return null;
}
}

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/** /**
* Class CategorySeeder Inserts values in categories table in database * Class CategorySeeder Inserts values in categories table in database
* *
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -13,791 +13,787 @@ declare(strict_types=1);
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Override;
class CategorySeeder extends Seeder class CategorySeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$data = [ $data = [
[ [
'id' => 1, 'id' => 1,
'parent_id' => null, 'parent_id' => null,
'code' => 'arts', 'code' => 'arts',
'apple_category' => 'Arts', 'apple_category' => 'Arts',
'google_category' => 'Arts', 'google_category' => 'Arts',
], ],
[ [
'id' => 2, 'id' => 2,
'parent_id' => null, 'parent_id' => null,
'code' => 'business', 'code' => 'business',
'apple_category' => 'Business', 'apple_category' => 'Business',
'google_category' => 'Business', 'google_category' => 'Business',
], ],
[ [
'id' => 3, 'id' => 3,
'parent_id' => null, 'parent_id' => null,
'code' => 'comedy', 'code' => 'comedy',
'apple_category' => 'Comedy', 'apple_category' => 'Comedy',
'google_category' => 'Comedy', 'google_category' => 'Comedy',
], ],
[ [
'id' => 4, 'id' => 4,
'parent_id' => null, 'parent_id' => null,
'code' => 'education', 'code' => 'education',
'apple_category' => 'Education', 'apple_category' => 'Education',
'google_category' => 'Education', 'google_category' => 'Education',
], ],
[ [
'id' => 5, 'id' => 5,
'parent_id' => null, 'parent_id' => null,
'code' => 'fiction', 'code' => 'fiction',
'apple_category' => 'Fiction', 'apple_category' => 'Fiction',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 6, 'id' => 6,
'parent_id' => null, 'parent_id' => null,
'code' => 'government', 'code' => 'government',
'apple_category' => 'Government', 'apple_category' => 'Government',
'google_category' => 'Government & Organizations', 'google_category' => 'Government & Organizations',
], ],
[ [
'id' => 7, 'id' => 7,
'parent_id' => null, 'parent_id' => null,
'code' => 'health_and_fitness', 'code' => 'health_and_fitness',
'apple_category' => 'Health & Fitness', 'apple_category' => 'Health & Fitness',
'google_category' => 'Health', 'google_category' => 'Health',
], ],
[ [
'id' => 8, 'id' => 8,
'parent_id' => null, 'parent_id' => null,
'code' => 'history', 'code' => 'history',
'apple_category' => 'History', 'apple_category' => 'History',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 9, 'id' => 9,
'parent_id' => null, 'parent_id' => null,
'code' => 'kids_and_family', 'code' => 'kids_and_family',
'apple_category' => 'Kids & Family', 'apple_category' => 'Kids & Family',
'google_category' => 'Kids & Family', 'google_category' => 'Kids & Family',
], ],
[ [
'id' => 10, 'id' => 10,
'parent_id' => null, 'parent_id' => null,
'code' => 'leisure', 'code' => 'leisure',
'apple_category' => 'Leisure', 'apple_category' => 'Leisure',
'google_category' => 'Games & Hobbies', 'google_category' => 'Games & Hobbies',
], ],
[ [
'id' => 11, 'id' => 11,
'parent_id' => null, 'parent_id' => null,
'code' => 'music', 'code' => 'music',
'apple_category' => 'Music', 'apple_category' => 'Music',
'google_category' => 'Music', 'google_category' => 'Music',
], ],
[ [
'id' => 12, 'id' => 12,
'parent_id' => null, 'parent_id' => null,
'code' => 'news', 'code' => 'news',
'apple_category' => 'News', 'apple_category' => 'News',
'google_category' => 'News & Politics', 'google_category' => 'News & Politics',
], ],
[ [
'id' => 13, 'id' => 13,
'parent_id' => null, 'parent_id' => null,
'code' => 'religion_and_spirituality', 'code' => 'religion_and_spirituality',
'apple_category' => 'Religion & Spirituality', 'apple_category' => 'Religion & Spirituality',
'google_category' => 'Religion & Spirituality', 'google_category' => 'Religion & Spirituality',
], ],
[ [
'id' => 14, 'id' => 14,
'parent_id' => null, 'parent_id' => null,
'code' => 'science', 'code' => 'science',
'apple_category' => 'Science', 'apple_category' => 'Science',
'google_category' => 'Science & Medicine', 'google_category' => 'Science & Medicine',
], ],
[ [
'id' => 15, 'id' => 15,
'parent_id' => null, 'parent_id' => null,
'code' => 'society_and_culture', 'code' => 'society_and_culture',
'apple_category' => 'Society & Culture', 'apple_category' => 'Society & Culture',
'google_category' => 'Society & Culture', 'google_category' => 'Society & Culture',
], ],
[ [
'id' => 16, 'id' => 16,
'parent_id' => null, 'parent_id' => null,
'code' => 'sports', 'code' => 'sports',
'apple_category' => 'Sports', 'apple_category' => 'Sports',
'google_category' => 'Sports & Recreation', 'google_category' => 'Sports & Recreation',
], ],
[ [
'id' => 17, 'id' => 17,
'parent_id' => null, 'parent_id' => null,
'code' => 'technology', 'code' => 'technology',
'apple_category' => 'Technology', 'apple_category' => 'Technology',
'google_category' => 'Technology', 'google_category' => 'Technology',
], ],
[ [
'id' => 18, 'id' => 18,
'parent_id' => null, 'parent_id' => null,
'code' => 'true_crime', 'code' => 'true_crime',
'apple_category' => 'True Crime', 'apple_category' => 'True Crime',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 19, 'id' => 19,
'parent_id' => null, 'parent_id' => null,
'code' => 'tv_and_film', 'code' => 'tv_and_film',
'apple_category' => 'TV & Film', 'apple_category' => 'TV & Film',
'google_category' => 'TV & Film', 'google_category' => 'TV & Film',
], ],
[ [
'id' => 20, 'id' => 20,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'books', 'code' => 'books',
'apple_category' => 'Books', 'apple_category' => 'Books',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 21, 'id' => 21,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'design', 'code' => 'design',
'apple_category' => 'Design', 'apple_category' => 'Design',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 22, 'id' => 22,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'fashion_and_beauty', 'code' => 'fashion_and_beauty',
'apple_category' => 'Fashion & Beauty', 'apple_category' => 'Fashion & Beauty',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 23, 'id' => 23,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'food', 'code' => 'food',
'apple_category' => 'Food', 'apple_category' => 'Food',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 24, 'id' => 24,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'performing_arts', 'code' => 'performing_arts',
'apple_category' => 'Performing Arts', 'apple_category' => 'Performing Arts',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 25, 'id' => 25,
'parent_id' => 1, 'parent_id' => 1,
'code' => 'visual_arts', 'code' => 'visual_arts',
'apple_category' => 'Visual Arts', 'apple_category' => 'Visual Arts',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 26, 'id' => 26,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'careers', 'code' => 'careers',
'apple_category' => 'Careers', 'apple_category' => 'Careers',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 27, 'id' => 27,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'entrepreneurship', 'code' => 'entrepreneurship',
'apple_category' => 'Entrepreneurship', 'apple_category' => 'Entrepreneurship',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 28, 'id' => 28,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'investing', 'code' => 'investing',
'apple_category' => 'Investing', 'apple_category' => 'Investing',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 29, 'id' => 29,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'management', 'code' => 'management',
'apple_category' => 'Management', 'apple_category' => 'Management',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 30, 'id' => 30,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'marketing', 'code' => 'marketing',
'apple_category' => 'Marketing', 'apple_category' => 'Marketing',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 31, 'id' => 31,
'parent_id' => 2, 'parent_id' => 2,
'code' => 'non_profit', 'code' => 'non_profit',
'apple_category' => 'Non-Profit', 'apple_category' => 'Non-Profit',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 32, 'id' => 32,
'parent_id' => 3, 'parent_id' => 3,
'code' => 'comedy_interviews', 'code' => 'comedy_interviews',
'apple_category' => 'Comedy Interviews', 'apple_category' => 'Comedy Interviews',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 33, 'id' => 33,
'parent_id' => 3, 'parent_id' => 3,
'code' => 'improv', 'code' => 'improv',
'apple_category' => 'Improv', 'apple_category' => 'Improv',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 34, 'id' => 34,
'parent_id' => 3, 'parent_id' => 3,
'code' => 'stand_up', 'code' => 'stand_up',
'apple_category' => 'Stand-Up', 'apple_category' => 'Stand-Up',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 35, 'id' => 35,
'parent_id' => 4, 'parent_id' => 4,
'code' => 'courses', 'code' => 'courses',
'apple_category' => 'Courses', 'apple_category' => 'Courses',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 36, 'id' => 36,
'parent_id' => 4, 'parent_id' => 4,
'code' => 'how_to', 'code' => 'how_to',
'apple_category' => 'How To', 'apple_category' => 'How To',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 37, 'id' => 37,
'parent_id' => 4, 'parent_id' => 4,
'code' => 'language_learning', 'code' => 'language_learning',
'apple_category' => 'Language Learning', 'apple_category' => 'Language Learning',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 38, 'id' => 38,
'parent_id' => 4, 'parent_id' => 4,
'code' => 'self_improvement', 'code' => 'self_improvement',
'apple_category' => 'Self-Improvement', 'apple_category' => 'Self-Improvement',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 39, 'id' => 39,
'parent_id' => 5, 'parent_id' => 5,
'code' => 'comedy_fiction', 'code' => 'comedy_fiction',
'apple_category' => 'Comedy Fiction', 'apple_category' => 'Comedy Fiction',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 40, 'id' => 40,
'parent_id' => 5, 'parent_id' => 5,
'code' => 'drama', 'code' => 'drama',
'apple_category' => 'Drama', 'apple_category' => 'Drama',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 41, 'id' => 41,
'parent_id' => 5, 'parent_id' => 5,
'code' => 'science_fiction', 'code' => 'science_fiction',
'apple_category' => 'Science Fiction', 'apple_category' => 'Science Fiction',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 42, 'id' => 42,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'alternative_health', 'code' => 'alternative_health',
'apple_category' => 'Alternative Health', 'apple_category' => 'Alternative Health',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 43, 'id' => 43,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'fitness', 'code' => 'fitness',
'apple_category' => 'Fitness', 'apple_category' => 'Fitness',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 44, 'id' => 44,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'medicine', 'code' => 'medicine',
'apple_category' => 'Medicine', 'apple_category' => 'Medicine',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 45, 'id' => 45,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'mental_health', 'code' => 'mental_health',
'apple_category' => 'Mental Health', 'apple_category' => 'Mental Health',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 46, 'id' => 46,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'nutrition', 'code' => 'nutrition',
'apple_category' => 'Nutrition', 'apple_category' => 'Nutrition',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 47, 'id' => 47,
'parent_id' => 7, 'parent_id' => 7,
'code' => 'sexuality', 'code' => 'sexuality',
'apple_category' => 'Sexuality', 'apple_category' => 'Sexuality',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 48, 'id' => 48,
'parent_id' => 9, 'parent_id' => 9,
'code' => 'education_for_kids', 'code' => 'education_for_kids',
'apple_category' => 'Education for Kids', 'apple_category' => 'Education for Kids',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 49, 'id' => 49,
'parent_id' => 9, 'parent_id' => 9,
'code' => 'parenting', 'code' => 'parenting',
'apple_category' => 'Parenting', 'apple_category' => 'Parenting',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 50, 'id' => 50,
'parent_id' => 9, 'parent_id' => 9,
'code' => 'pets_and_animals', 'code' => 'pets_and_animals',
'apple_category' => 'Pets & Animals', 'apple_category' => 'Pets & Animals',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 51, 'id' => 51,
'parent_id' => 9, 'parent_id' => 9,
'code' => 'stories_for_kids', 'code' => 'stories_for_kids',
'apple_category' => 'Stories for Kids', 'apple_category' => 'Stories for Kids',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 52, 'id' => 52,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'animation_and_manga', 'code' => 'animation_and_manga',
'apple_category' => 'Animation & Manga', 'apple_category' => 'Animation & Manga',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 53, 'id' => 53,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'automotive', 'code' => 'automotive',
'apple_category' => 'Automotive', 'apple_category' => 'Automotive',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 54, 'id' => 54,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'aviation', 'code' => 'aviation',
'apple_category' => 'Aviation', 'apple_category' => 'Aviation',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 55, 'id' => 55,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'crafts', 'code' => 'crafts',
'apple_category' => 'Crafts', 'apple_category' => 'Crafts',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 56, 'id' => 56,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'games', 'code' => 'games',
'apple_category' => 'Games', 'apple_category' => 'Games',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 57, 'id' => 57,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'hobbies', 'code' => 'hobbies',
'apple_category' => 'Hobbies', 'apple_category' => 'Hobbies',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 58, 'id' => 58,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'home_and_garden', 'code' => 'home_and_garden',
'apple_category' => 'Home & Garden', 'apple_category' => 'Home & Garden',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 59, 'id' => 59,
'parent_id' => 10, 'parent_id' => 10,
'code' => 'video_games', 'code' => 'video_games',
'apple_category' => 'Video Games', 'apple_category' => 'Video Games',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 60, 'id' => 60,
'parent_id' => 11, 'parent_id' => 11,
'code' => 'music_commentary', 'code' => 'music_commentary',
'apple_category' => 'Music Commentary', 'apple_category' => 'Music Commentary',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 61, 'id' => 61,
'parent_id' => 11, 'parent_id' => 11,
'code' => 'music_history', 'code' => 'music_history',
'apple_category' => 'Music History', 'apple_category' => 'Music History',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 62, 'id' => 62,
'parent_id' => 11, 'parent_id' => 11,
'code' => 'music_interviews', 'code' => 'music_interviews',
'apple_category' => 'Music Interviews', 'apple_category' => 'Music Interviews',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 63, 'id' => 63,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'business_news', 'code' => 'business_news',
'apple_category' => 'Business News', 'apple_category' => 'Business News',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 64, 'id' => 64,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'daily_news', 'code' => 'daily_news',
'apple_category' => 'Daily News', 'apple_category' => 'Daily News',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 65, 'id' => 65,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'entertainment_news', 'code' => 'entertainment_news',
'apple_category' => 'Entertainment News', 'apple_category' => 'Entertainment News',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 66, 'id' => 66,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'news_commentary', 'code' => 'news_commentary',
'apple_category' => 'News Commentary', 'apple_category' => 'News Commentary',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 67, 'id' => 67,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'politics', 'code' => 'politics',
'apple_category' => 'Politics', 'apple_category' => 'Politics',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 68, 'id' => 68,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'sports_news', 'code' => 'sports_news',
'apple_category' => 'Sports News', 'apple_category' => 'Sports News',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 69, 'id' => 69,
'parent_id' => 12, 'parent_id' => 12,
'code' => 'tech_news', 'code' => 'tech_news',
'apple_category' => 'Tech News', 'apple_category' => 'Tech News',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 70, 'id' => 70,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'buddhism', 'code' => 'buddhism',
'apple_category' => 'Buddhism', 'apple_category' => 'Buddhism',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 71, 'id' => 71,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'christianity', 'code' => 'christianity',
'apple_category' => 'Christianity', 'apple_category' => 'Christianity',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 72, 'id' => 72,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'hinduism', 'code' => 'hinduism',
'apple_category' => 'Hinduism', 'apple_category' => 'Hinduism',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 73, 'id' => 73,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'islam', 'code' => 'islam',
'apple_category' => 'Islam', 'apple_category' => 'Islam',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 74, 'id' => 74,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'judaism', 'code' => 'judaism',
'apple_category' => 'Judaism', 'apple_category' => 'Judaism',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 75, 'id' => 75,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'religion', 'code' => 'religion',
'apple_category' => 'Religion', 'apple_category' => 'Religion',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 76, 'id' => 76,
'parent_id' => 13, 'parent_id' => 13,
'code' => 'spirituality', 'code' => 'spirituality',
'apple_category' => 'Spirituality', 'apple_category' => 'Spirituality',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 77, 'id' => 77,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'astronomy', 'code' => 'astronomy',
'apple_category' => 'Astronomy', 'apple_category' => 'Astronomy',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 78, 'id' => 78,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'chemistry', 'code' => 'chemistry',
'apple_category' => 'Chemistry', 'apple_category' => 'Chemistry',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 79, 'id' => 79,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'earth_sciences', 'code' => 'earth_sciences',
'apple_category' => 'Earth Sciences', 'apple_category' => 'Earth Sciences',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 80, 'id' => 80,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'life_sciences', 'code' => 'life_sciences',
'apple_category' => 'Life Sciences', 'apple_category' => 'Life Sciences',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 81, 'id' => 81,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'mathematics', 'code' => 'mathematics',
'apple_category' => 'Mathematics', 'apple_category' => 'Mathematics',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 82, 'id' => 82,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'natural_sciences', 'code' => 'natural_sciences',
'apple_category' => 'Natural Sciences', 'apple_category' => 'Natural Sciences',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 83, 'id' => 83,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'nature', 'code' => 'nature',
'apple_category' => 'Nature', 'apple_category' => 'Nature',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 84, 'id' => 84,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'physics', 'code' => 'physics',
'apple_category' => 'Physics', 'apple_category' => 'Physics',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 85, 'id' => 85,
'parent_id' => 14, 'parent_id' => 14,
'code' => 'social_sciences', 'code' => 'social_sciences',
'apple_category' => 'Social Sciences', 'apple_category' => 'Social Sciences',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 86, 'id' => 86,
'parent_id' => 15, 'parent_id' => 15,
'code' => 'documentary', 'code' => 'documentary',
'apple_category' => 'Documentary', 'apple_category' => 'Documentary',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 87, 'id' => 87,
'parent_id' => 15, 'parent_id' => 15,
'code' => 'personal_journals', 'code' => 'personal_journals',
'apple_category' => 'Personal Journals', 'apple_category' => 'Personal Journals',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 88, 'id' => 88,
'parent_id' => 15, 'parent_id' => 15,
'code' => 'philosophy', 'code' => 'philosophy',
'apple_category' => 'Philosophy', 'apple_category' => 'Philosophy',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 89, 'id' => 89,
'parent_id' => 15, 'parent_id' => 15,
'code' => 'places_and_travel', 'code' => 'places_and_travel',
'apple_category' => 'Places & Travel', 'apple_category' => 'Places & Travel',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 90, 'id' => 90,
'parent_id' => 15, 'parent_id' => 15,
'code' => 'relationships', 'code' => 'relationships',
'apple_category' => 'Relationships', 'apple_category' => 'Relationships',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 91, 'id' => 91,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'baseball', 'code' => 'baseball',
'apple_category' => 'Baseball', 'apple_category' => 'Baseball',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 92, 'id' => 92,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'basketball', 'code' => 'basketball',
'apple_category' => 'Basketball', 'apple_category' => 'Basketball',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 93, 'id' => 93,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'cricket', 'code' => 'cricket',
'apple_category' => 'Cricket', 'apple_category' => 'Cricket',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 94, 'id' => 94,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'fantasy_sports', 'code' => 'fantasy_sports',
'apple_category' => 'Fantasy Sports', 'apple_category' => 'Fantasy Sports',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 95, 'id' => 95,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'football', 'code' => 'football',
'apple_category' => 'Football', 'apple_category' => 'Football',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 96, 'id' => 96,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'golf', 'code' => 'golf',
'apple_category' => 'Golf', 'apple_category' => 'Golf',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 97, 'id' => 97,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'hockey', 'code' => 'hockey',
'apple_category' => 'Hockey', 'apple_category' => 'Hockey',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 98, 'id' => 98,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'rugby', 'code' => 'rugby',
'apple_category' => 'Rugby', 'apple_category' => 'Rugby',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 99, 'id' => 99,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'running', 'code' => 'running',
'apple_category' => 'Running', 'apple_category' => 'Running',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 100, 'id' => 100,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'soccer', 'code' => 'soccer',
'apple_category' => 'Soccer', 'apple_category' => 'Soccer',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 101, 'id' => 101,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'swimming', 'code' => 'swimming',
'apple_category' => 'Swimming', 'apple_category' => 'Swimming',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 102, 'id' => 102,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'tennis', 'code' => 'tennis',
'apple_category' => 'Tennis', 'apple_category' => 'Tennis',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 103, 'id' => 103,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'volleyball', 'code' => 'volleyball',
'apple_category' => 'Volleyball', 'apple_category' => 'Volleyball',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 104, 'id' => 104,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'wilderness', 'code' => 'wilderness',
'apple_category' => 'Wilderness', 'apple_category' => 'Wilderness',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 105, 'id' => 105,
'parent_id' => 16, 'parent_id' => 16,
'code' => 'wrestling', 'code' => 'wrestling',
'apple_category' => 'Wrestling', 'apple_category' => 'Wrestling',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 106, 'id' => 106,
'parent_id' => 19, 'parent_id' => 19,
'code' => 'after_shows', 'code' => 'after_shows',
'apple_category' => 'After Shows', 'apple_category' => 'After Shows',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 107, 'id' => 107,
'parent_id' => 19, 'parent_id' => 19,
'code' => 'film_history', 'code' => 'film_history',
'apple_category' => 'Film History', 'apple_category' => 'Film History',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 108, 'id' => 108,
'parent_id' => 19, 'parent_id' => 19,
'code' => 'film_interviews', 'code' => 'film_interviews',
'apple_category' => 'Film Interviews', 'apple_category' => 'Film Interviews',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 109, 'id' => 109,
'parent_id' => 19, 'parent_id' => 19,
'code' => 'film_reviews', 'code' => 'film_reviews',
'apple_category' => 'Film Reviews', 'apple_category' => 'Film Reviews',
'google_category' => '', 'google_category' => '',
], ],
[ [
'id' => 110, 'id' => 110,
'parent_id' => 19, 'parent_id' => 19,
'code' => 'tv_reviews', 'code' => 'tv_reviews',
'apple_category' => 'TV Reviews', 'apple_category' => 'TV Reviews',
'google_category' => '', 'google_category' => '',
], ],
]; ];
foreach ($data as $categoryLine) { $this->db
$this->db ->table('categories')
->table('categories') ->ignore(true)
->ignore(true) ->insertBatch($data);
->insert($categoryLine);
}
} }
} }

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class AppSeeder Calls all required seeders for castopod to work properly
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use Override;
class DevSeeder extends Seeder
{
#[Override]
public function run(): void
{
$this->call('CategorySeeder');
$this->call('LanguageSeeder');
$this->call('DevSuperadminSeeder');
}
}

View file

@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
/**
* Class TestSeeder Inserts a superadmin user in the database
*
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Override;
class DevSuperadminSeeder extends Seeder
{
#[Override]
public function run(): void
{
if (new UserModel()->where('is_owner', true)->first() instanceof User) {
return;
}
/**
* Inserts an owner with the following credentials: admin: `admin@example.com` password: `castopod`
*/
// Get the User Provider (UserModel by default)
$users = auth()
->getProvider();
$user = new User([
'username' => 'admin',
'email' => 'admin@castopod.local',
'password' => 'castopod',
'is_owner' => true,
]);
$users->save($user);
// To get the complete user object with ID, we need to get from the database
$user = $users->findById($users->getInsertID());
$user->addGroup(setting('AuthGroups.mostPowerfulGroup'));
}
}

View file

@ -5,26 +5,22 @@ declare(strict_types=1);
/** /**
* Class FakePodcastsAnalyticsSeeder Inserts Fake Analytics in the database * Class FakePodcastsAnalyticsSeeder Inserts Fake Analytics in the database
* *
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Database\Seeds; namespace App\Database\Seeds;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Exception;
use GeoIp2\Database\Reader; use GeoIp2\Database\Reader;
use GeoIp2\Exception\AddressNotFoundException; use GeoIp2\Exception\AddressNotFoundException;
use Override;
class FakePodcastsAnalyticsSeeder extends Seeder class FakePodcastsAnalyticsSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$jsonUserAgents = json_decode( $jsonUserAgents = json_decode(
@ -43,165 +39,163 @@ class FakePodcastsAnalyticsSeeder extends Seeder
JSON_THROW_ON_ERROR, JSON_THROW_ON_ERROR,
); );
$podcast = new PodcastModel() $podcast = (new PodcastModel())->first();
->first();
if (! $podcast instanceof Podcast) { if ($podcast !== null) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n"); $firstEpisode = (new EpisodeModel())
} ->selectMin('published_at')
->first();
$firstEpisode = new EpisodeModel() for (
->selectMin('published_at') $date = strtotime((string) $firstEpisode->published_at);
->first(); $date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$analyticsPodcasts = [];
$analyticsPodcastsByHour = [];
$analyticsPodcastsByCountry = [];
$analyticsPodcastsByEpisode = [];
$analyticsPodcastsByPlayer = [];
$analyticsPodcastsByRegion = [];
if (! $firstEpisode instanceof Episode) { $episodes = (new EpisodeModel())
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate an episode first."); ->where('podcast_id', $podcast->id)
} ->where('`published_at` <= NOW()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = floor(exp(3 - $age / 40)) + 1;
for ( for (
$date = strtotime((string) $firstEpisode->published_at); $lineNumber = 0;
$date < strtotime('now'); $lineNumber < rand(1, (int) $probability1);
$date = strtotime(date('Y-m-d', $date) . ' +1 day') ++$lineNumber
) { ) {
$analyticsPodcasts = []; $probability2 = floor(exp(6 - $age / 20)) + 10;
$analyticsPodcastsByHour = [];
$analyticsPodcastsByCountry = [];
$analyticsPodcastsByEpisode = [];
$analyticsPodcastsByPlayer = [];
$analyticsPodcastsByRegion = [];
$episodes = new EpisodeModel() $player =
->where('podcast_id', $podcast->id) $jsonUserAgents[
->where('`published_at` <= UTC_TIMESTAMP()', null, false) rand(1, count($jsonUserAgents) - 1)
->findAll(); ];
foreach ($episodes as $episode) { $service =
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400); $jsonRSSUserAgents[
$probability1 = floor(exp(3 - $age / 40)) + 1; rand(1, count($jsonRSSUserAgents) - 1)
]['slug'];
$app = isset($player['app']) ? $player['app'] : '';
$device = isset($player['device'])
? $player['device']
: '';
$os = isset($player['os']) ? $player['os'] : '';
$isBot = isset($player['bot']) ? $player['bot'] : 0;
for ( $fakeIp =
$lineNumber = 0; rand(0, 255) .
$lineNumber < random_int(1, (int) $probability1); '.' .
++$lineNumber rand(0, 255) .
) { '.' .
$probability2 = floor(exp(6 - $age / 20)) + 10; rand(0, 255) .
'.' .
rand(0, 255);
$player = $cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
$jsonUserAgents[
random_int(1, count($jsonUserAgents) - 1) $countryCode = 'N/A';
$regionCode = 'N/A';
$latitude = null;
$longitude = null;
try {
$city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode === null
? 'N/A'
: $city->country->isoCode;
$regionCode = $city->subdivisions === []
? 'N/A'
: $city->subdivisions[0]->isoCode;
$latitude = round((float) $city->location->latitude, 3);
$longitude = round((float) $city->location->longitude, 3);
} catch (AddressNotFoundException) {
//Bad luck, bad IP, nothing to do.
}
$hits = rand(0, (int) $probability2);
$analyticsPodcasts[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'duration' => rand(60, 3600),
'bandwidth' => rand(1000000, 10000000),
'hits' => $hits,
'unique_listeners' => $hits,
];
$analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'hour' => rand(0, 23),
'hits' => $hits,
];
$analyticsPodcastsByCountry[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'hits' => $hits,
];
$analyticsPodcastsByEpisode[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'episode_id' => $episode->id,
'age' => $age,
'hits' => $hits,
];
$analyticsPodcastsByPlayer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'service' => $service,
'app' => $app,
'device' => $device,
'os' => $os,
'is_bot' => $isBot,
'hits' => $hits,
];
$analyticsPodcastsByRegion[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'region_code' => $regionCode,
'latitude' => $latitude,
'longitude' => $longitude,
'hits' => $hits,
]; ];
$service =
$jsonRSSUserAgents[
random_int(1, count($jsonRSSUserAgents) - 1)
]['slug'];
$app = $player['app'] ?? '';
$device = $player['device'] ?? '';
$os = $player['os'] ?? '';
$isBot = $player['bot'] ?? 0;
$fakeIp =
random_int(0, 255) .
'.' .
random_int(0, 255) .
'.' .
random_int(0, 255) .
'.' .
random_int(0, 255);
$cityReader = new Reader(WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb');
$countryCode = 'N/A';
$regionCode = 'N/A';
$latitude = null;
$longitude = null;
try {
$city = $cityReader->city($fakeIp);
$countryCode = $city->country->isoCode ?? 'N/A';
$regionCode = $city->subdivisions === []
? 'N/A'
: $city->subdivisions[0]->isoCode;
$latitude = round((float) $city->location->latitude, 3);
$longitude = round((float) $city->location->longitude, 3);
} catch (AddressNotFoundException) {
//Bad luck, bad IP, nothing to do.
} }
$hits = random_int(0, (int) $probability2);
$analyticsPodcasts[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'duration' => random_int(60, 3600),
'bandwidth' => random_int(1000000, 10000000),
'hits' => $hits,
'unique_listeners' => $hits,
];
$analyticsPodcastsByHour[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'hour' => random_int(0, 23),
'hits' => $hits,
];
$analyticsPodcastsByCountry[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'hits' => $hits,
];
$analyticsPodcastsByEpisode[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'episode_id' => $episode->id,
'age' => $age,
'hits' => $hits,
];
$analyticsPodcastsByPlayer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'service' => $service,
'app' => $app,
'device' => $device,
'os' => $os,
'is_bot' => $isBot,
'hits' => $hits,
];
$analyticsPodcastsByRegion[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'country_code' => $countryCode,
'region_code' => $regionCode,
'latitude' => $latitude,
'longitude' => $longitude,
'hits' => $hits,
];
} }
$this->db
->table('analytics_podcasts')
->ignore(true)
->insertBatch($analyticsPodcasts);
$this->db
->table('analytics_podcasts_by_hour')
->ignore(true)
->insertBatch($analyticsPodcastsByHour);
$this->db
->table('analytics_podcasts_by_country')
->ignore(true)
->insertBatch($analyticsPodcastsByCountry);
$this->db
->table('analytics_podcasts_by_episode')
->ignore(true)
->insertBatch($analyticsPodcastsByEpisode);
$this->db
->table('analytics_podcasts_by_player')
->ignore(true)
->insertBatch($analyticsPodcastsByPlayer);
$this->db
->table('analytics_podcasts_by_region')
->ignore(true)
->insertBatch($analyticsPodcastsByRegion);
} }
} else {
$this->db echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
->table('analytics_podcasts')
->ignore(true)
->insertBatch($analyticsPodcasts);
$this->db
->table('analytics_podcasts_by_hour')
->ignore(true)
->insertBatch($analyticsPodcastsByHour);
$this->db
->table('analytics_podcasts_by_country')
->ignore(true)
->insertBatch($analyticsPodcastsByCountry);
$this->db
->table('analytics_podcasts_by_episode')
->ignore(true)
->insertBatch($analyticsPodcastsByEpisode);
$this->db
->table('analytics_podcasts_by_player')
->ignore(true)
->insertBatch($analyticsPodcastsByPlayer);
$this->db
->table('analytics_podcasts_by_region')
->ignore(true)
->insertBatch($analyticsPodcastsByRegion);
} }
} }
} }

View file

@ -5,20 +5,17 @@ declare(strict_types=1);
/** /**
* Class FakeWebsiteAnalyticsSeeder Inserts Fake Analytics in the database * Class FakeWebsiteAnalyticsSeeder Inserts Fake Analytics in the database
* *
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Database\Seeds; namespace App\Database\Seeds;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Exception;
use Override;
class FakeWebsiteAnalyticsSeeder extends Seeder class FakeWebsiteAnalyticsSeeder extends Seeder
{ {
@ -182,96 +179,90 @@ class FakeWebsiteAnalyticsSeeder extends Seeder
'WOSBrowser', 'WOSBrowser',
]; ];
#[Override]
public function run(): void public function run(): void
{ {
$podcast = new PodcastModel() $podcast = (new PodcastModel())->first();
->first();
if (! $podcast instanceof Podcast) { if ($podcast) {
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n"); $firstEpisode = (new EpisodeModel())
} ->selectMin('published_at')
->first();
$firstEpisode = new EpisodeModel() for (
->selectMin('published_at') $date = strtotime((string) $firstEpisode->published_at);
->first(); $date < strtotime('now');
$date = strtotime(date('Y-m-d', $date) . ' +1 day')
) {
$websiteByBrowser = [];
$websiteByEntryPage = [];
$websiteByReferer = [];
if (! $firstEpisode instanceof Episode) { $episodes = (new EpisodeModel())
throw new Exception("COULD NOT POPULATE DATABASE:\n\tCreate an episode first."); ->where('podcast_id', $podcast->id)
} ->where('`published_at` <= NOW()', null, false)
->findAll();
foreach ($episodes as $episode) {
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400);
$probability1 = (int) floor(exp(3 - $age / 40)) + 1;
for ( for (
$date = strtotime((string) $firstEpisode->published_at); $lineNumber = 0;
$date < strtotime('now'); $lineNumber < rand(1, $probability1);
$date = strtotime(date('Y-m-d', $date) . ' +1 day') ++$lineNumber
) { ) {
$websiteByBrowser = []; $probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$websiteByEntryPage = [];
$websiteByReferer = [];
$episodes = new EpisodeModel() $domain =
->where('podcast_id', $podcast->id) $this->domains[rand(0, count($this->domains) - 1)];
->where('`published_at` <= UTC_TIMESTAMP()', null, false) $keyword =
->findAll(); $this->keywords[
foreach ($episodes as $episode) { rand(0, count($this->keywords) - 1)
$age = floor(($date - strtotime((string) $episode->published_at)) / 86400); ];
$probability1 = (int) floor(exp(3 - $age / 40)) + 1; $browser =
$this->browsers[
rand(0, count($this->browsers) - 1)
];
for ( $hits = rand(0, $probability2);
$lineNumber = 0;
$lineNumber < random_int(1, $probability1);
++$lineNumber
) {
$probability2 = (int) floor(exp(6 - $age / 20)) + 10;
$domain = $websiteByBrowser[] = [
$this->domains[random_int(0, count($this->domains) - 1)]; 'podcast_id' => $podcast->id,
$keyword = 'date' => date('Y-m-d', $date),
$this->keywords[ 'browser' => $browser,
random_int(0, count($this->keywords) - 1) 'hits' => $hits,
]; ];
$browser = $websiteByEntryPage[] = [
$this->browsers[ 'podcast_id' => $podcast->id,
random_int(0, count($this->browsers) - 1) 'date' => date('Y-m-d', $date),
'entry_page_url' => $episode->link,
'hits' => $hits,
]; ];
$websiteByReferer[] = [
$hits = random_int(0, $probability2); 'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
$websiteByBrowser[] = [ 'referer_url' =>
'podcast_id' => $podcast->id, 'http://' . $domain . '/?q=' . $keyword,
'date' => date('Y-m-d', $date), 'domain' => $domain,
'browser' => $browser, 'keywords' => $keyword,
'hits' => $hits, 'hits' => $hits,
]; ];
$websiteByEntryPage[] = [ }
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'entry_page_url' => $episode->link,
'hits' => $hits,
];
$websiteByReferer[] = [
'podcast_id' => $podcast->id,
'date' => date('Y-m-d', $date),
'referer_url' => 'http://' . $domain . '/?q=' . $keyword,
'domain' => $domain,
'keywords' => $keyword,
'hits' => $hits,
];
} }
$this->db
->table('analytics_website_by_browser')
->ignore(true)
->insertBatch($websiteByBrowser);
$this->db
->table('analytics_website_by_entry_page')
->ignore(true)
->insertBatch($websiteByEntryPage);
$this->db
->table('analytics_website_by_referer')
->ignore(true)
->insertBatch($websiteByReferer);
} }
} else {
$this->db echo "COULD NOT POPULATE DATABASE:\n\tCreate a podcast with episodes first.\n";
->table('analytics_website_by_browser')
->ignore(true)
->insertBatch($websiteByBrowser);
$this->db
->table('analytics_website_by_entry_page')
->ignore(true)
->insertBatch($websiteByEntryPage);
$this->db
->table('analytics_website_by_referer')
->ignore(true)
->insertBatch($websiteByReferer);
} }
} }
} }

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
/** /**
* Class LanguageSeeder Inserts values in languages table in database * Class LanguageSeeder Inserts values in languages table in database
* *
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -18,757 +18,754 @@ declare(strict_types=1);
namespace App\Database\Seeds; namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder; use CodeIgniter\Database\Seeder;
use Override;
class LanguageSeeder extends Seeder class LanguageSeeder extends Seeder
{ {
#[Override]
public function run(): void public function run(): void
{ {
$data = [ $data = [
[ [
'code' => 'aa', 'code' => 'aa',
'native_name' => 'Afaraf', 'native_name' => 'Afaraf',
], ],
[ [
'code' => 'ab', 'code' => 'ab',
'native_name' => 'аҧсуа бызшәа, аҧсшәа', 'native_name' => 'аҧсуа бызшәа, аҧсшәа',
], ],
[ [
'code' => 'ae', 'code' => 'ae',
'native_name' => 'Avesta', 'native_name' => 'avesta',
], ],
[ [
'code' => 'af', 'code' => 'af',
'native_name' => 'Afrikaans', 'native_name' => 'Afrikaans',
], ],
[ [
'code' => 'ak', 'code' => 'ak',
'native_name' => 'Akan', 'native_name' => 'Akan',
], ],
[ [
'code' => 'am', 'code' => 'am',
'native_name' => 'አማርኛ', 'native_name' => 'አማርኛ',
], ],
[ [
'code' => 'an', 'code' => 'an',
'native_name' => 'Aragonés', 'native_name' => 'aragonés',
], ],
[ [
'code' => 'ar', 'code' => 'ar',
'native_name' => 'العربية', 'native_name' => 'العربية',
], ],
[ [
'code' => 'as', 'code' => 'as',
'native_name' => 'অসমীয়া', 'native_name' => 'অসমীয়া',
], ],
[ [
'code' => 'av', 'code' => 'av',
'native_name' => 'авар мацӀ, магӀарул мацӀ', 'native_name' => 'авар мацӀ, магӀарул мацӀ',
], ],
[ [
'code' => 'ay', 'code' => 'ay',
'native_name' => 'Aymar aru', 'native_name' => 'aymar aru',
], ],
[ [
'code' => 'az', 'code' => 'az',
'native_name' => 'azərbaycan dili', 'native_name' => 'azərbaycan dili',
], ],
[ [
'code' => 'ba', 'code' => 'ba',
'native_name' => 'башҡорт теле', 'native_name' => 'башҡорт теле',
], ],
[ [
'code' => 'be', 'code' => 'be',
'native_name' => 'беларуская мова', 'native_name' => 'беларуская мова',
], ],
[ [
'code' => 'bg', 'code' => 'bg',
'native_name' => 'български език', 'native_name' => 'български език',
], ],
[ [
'code' => 'bh', 'code' => 'bh',
'native_name' => 'भोजपुरी', 'native_name' => 'भोजपुरी',
], ],
[ [
'code' => 'bi', 'code' => 'bi',
'native_name' => 'Bislama', 'native_name' => 'Bislama',
], ],
[ [
'code' => 'bm', 'code' => 'bm',
'native_name' => 'Bamanankan', 'native_name' => 'bamanankan',
], ],
[ [
'code' => 'bn', 'code' => 'bn',
'native_name' => 'বাংলা', 'native_name' => 'বাংলা',
], ],
[ [
'code' => 'bo', 'code' => 'bo',
'native_name' => 'བོད་ཡིག', 'native_name' => 'བོད་ཡིག',
], ],
[ [
'code' => 'br', 'code' => 'br',
'native_name' => 'Brezhoneg', 'native_name' => 'brezhoneg',
], ],
[ [
'code' => 'bs', 'code' => 'bs',
'native_name' => 'Bosanski jezik', 'native_name' => 'bosanski jezik',
], ],
[ [
'code' => 'ca', 'code' => 'ca',
'native_name' => 'Català, valencià', 'native_name' => 'català, valencià',
], ],
[ [
'code' => 'ce', 'code' => 'ce',
'native_name' => 'нохчийн мотт', 'native_name' => 'нохчийн мотт',
], ],
[ [
'code' => 'ch', 'code' => 'ch',
'native_name' => 'Chamoru', 'native_name' => 'Chamoru',
], ],
[ [
'code' => 'co', 'code' => 'co',
'native_name' => 'Corsu, lingua corsa', 'native_name' => 'corsu, lingua corsa',
], ],
[ [
'code' => 'cr', 'code' => 'cr',
'native_name' => 'ᓀᐦᐃᔭᐍᐏᐣ', 'native_name' => 'ᓀᐦᐃᔭᐍᐏᐣ',
], ],
[ [
'code' => 'cs', 'code' => 'cs',
'native_name' => 'čeština, český jazyk', 'native_name' => 'čeština, český jazyk',
], ],
[ [
'code' => 'cu', 'code' => 'cu',
'native_name' => 'ѩзыкъ словѣньскъ', 'native_name' => 'ѩзыкъ словѣньскъ',
], ],
[ [
'code' => 'cv', 'code' => 'cv',
'native_name' => 'чӑваш чӗлхи', 'native_name' => 'чӑваш чӗлхи',
], ],
[ [
'code' => 'cy', 'code' => 'cy',
'native_name' => 'Cymraeg', 'native_name' => 'Cymraeg',
], ],
[ [
'code' => 'da', 'code' => 'da',
'native_name' => 'Dansk', 'native_name' => 'dansk',
], ],
[ [
'code' => 'de', 'code' => 'de',
'native_name' => 'Deutsch', 'native_name' => 'Deutsch',
], ],
[ [
'code' => 'dv', 'code' => 'dv',
'native_name' => 'ދިވެހި', 'native_name' => 'ދިވެހި',
], ],
[ [
'code' => 'dz', 'code' => 'dz',
'native_name' => 'རྫོང་ཁ', 'native_name' => 'རྫོང་ཁ',
], ],
[ [
'code' => 'ee', 'code' => 'ee',
'native_name' => 'Eʋegbe', 'native_name' => 'Eʋegbe',
], ],
[ [
'code' => 'el', 'code' => 'el',
'native_name' => 'ελληνικά', 'native_name' => 'ελληνικά',
], ],
[ [
'code' => 'en', 'code' => 'en',
'native_name' => 'English', 'native_name' => 'English',
], ],
[ [
'code' => 'eo', 'code' => 'eo',
'native_name' => 'Esperanto', 'native_name' => 'Esperanto',
], ],
[ [
'code' => 'es', 'code' => 'es',
'native_name' => 'Español', 'native_name' => 'Español',
], ],
[ [
'code' => 'et', 'code' => 'et',
'native_name' => 'eesti, eesti keel', 'native_name' => 'eesti, eesti keel',
], ],
[ [
'code' => 'eu', 'code' => 'eu',
'native_name' => 'Euskara, euskera', 'native_name' => 'euskara, euskera',
], ],
[ [
'code' => 'fa', 'code' => 'fa',
'native_name' => 'فارسی', 'native_name' => 'فارسی',
], ],
[ [
'code' => 'ff', 'code' => 'ff',
'native_name' => 'Fulfulde, Pulaar, Pular', 'native_name' => 'Fulfulde, Pulaar, Pular',
], ],
[ [
'code' => 'fi', 'code' => 'fi',
'native_name' => 'Suomi, suomen kieli', 'native_name' => 'suomi, suomen kieli',
], ],
[ [
'code' => 'fj', 'code' => 'fj',
'native_name' => 'Vosa Vakaviti', 'native_name' => 'vosa Vakaviti',
], ],
[ [
'code' => 'fo', 'code' => 'fo',
'native_name' => 'Føroyskt', 'native_name' => 'føroyskt',
], ],
[ [
'code' => 'fr', 'code' => 'fr',
'native_name' => 'Français, langue française', 'native_name' => 'français, langue française',
], ],
[ [
'code' => 'fy', 'code' => 'fy',
'native_name' => 'Frysk', 'native_name' => 'Frysk',
], ],
[ [
'code' => 'ga', 'code' => 'ga',
'native_name' => 'Gaeilge', 'native_name' => 'Gaeilge',
], ],
[ [
'code' => 'gd', 'code' => 'gd',
'native_name' => 'Gàidhlig', 'native_name' => 'Gàidhlig',
], ],
[ [
'code' => 'gl', 'code' => 'gl',
'native_name' => 'Galego', 'native_name' => 'Galego',
], ],
[ [
'code' => 'gn', 'code' => 'gn',
'native_name' => "Avañe'ẽ", 'native_name' => "Avañe'ẽ",
], ],
[ [
'code' => 'gu', 'code' => 'gu',
'native_name' => 'ગુજરાતી', 'native_name' => 'ગુજરાતી',
], ],
[ [
'code' => 'gv', 'code' => 'gv',
'native_name' => 'Gaelg, Gailck', 'native_name' => 'Gaelg, Gailck',
], ],
[ [
'code' => 'ha', 'code' => 'ha',
'native_name' => '(Hausa) هَوُسَ', 'native_name' => '(Hausa) هَوُسَ',
], ],
[ [
'code' => 'he', 'code' => 'he',
'native_name' => 'עברית', 'native_name' => 'עברית',
], ],
[ [
'code' => 'hi', 'code' => 'hi',
'native_name' => 'हिन्दी, हिंदी', 'native_name' => 'हिन्दी, हिंदी',
], ],
[ [
'code' => 'ho', 'code' => 'ho',
'native_name' => 'Hiri Motu', 'native_name' => 'Hiri Motu',
], ],
[ [
'code' => 'hr', 'code' => 'hr',
'native_name' => 'Hrvatski jezik', 'native_name' => 'hrvatski jezik',
], ],
[ [
'code' => 'ht', 'code' => 'ht',
'native_name' => 'Kreyòl ayisyen', 'native_name' => 'Kreyòl ayisyen',
], ],
[ [
'code' => 'hu', 'code' => 'hu',
'native_name' => 'Magyar', 'native_name' => 'magyar',
], ],
[ [
'code' => 'hy', 'code' => 'hy',
'native_name' => 'Հայերեն', 'native_name' => 'Հայերեն',
], ],
[ [
'code' => 'hz', 'code' => 'hz',
'native_name' => 'Otjiherero', 'native_name' => 'Otjiherero',
], ],
[ [
'code' => 'ia', 'code' => 'ia',
'native_name' => 'Interlingua', 'native_name' => 'Interlingua',
], ],
[ [
'code' => 'id', 'code' => 'id',
'native_name' => 'Bahasa Indonesia', 'native_name' => 'Bahasa Indonesia',
], ],
[ [
'code' => 'ie', 'code' => 'ie',
'native_name' => 'Interlingue, formerly Occidental', 'native_name' =>
'(originally:) Occidental, (after WWII:) Interlingue',
], ],
[ [
'code' => 'ig', 'code' => 'ig',
'native_name' => 'Asụsụ Igbo', 'native_name' => 'Asụsụ Igbo',
], ],
[ [
'code' => 'ii', 'code' => 'ii',
'native_name' => 'ꆈꌠ꒿ Nuosuhxop', 'native_name' => 'ꆈꌠ꒿ Nuosuhxop',
], ],
[ [
'code' => 'ik', 'code' => 'ik',
'native_name' => 'Iñupiaq, Iñupiatun', 'native_name' => 'Iñupiaq, Iñupiatun',
], ],
[ [
'code' => 'io', 'code' => 'io',
'native_name' => 'Ido', 'native_name' => 'Ido',
], ],
[ [
'code' => 'is', 'code' => 'is',
'native_name' => 'Íslenska', 'native_name' => 'Íslenska',
], ],
[ [
'code' => 'it', 'code' => 'it',
'native_name' => 'Italiano', 'native_name' => 'Italiano',
], ],
[ [
'code' => 'iu', 'code' => 'iu',
'native_name' => 'ᐃᓄᒃᑎᑐᑦ', 'native_name' => 'ᐃᓄᒃᑎᑐᑦ',
], ],
[ [
'code' => 'ja', 'code' => 'ja',
'native_name' => '日本語 (にほんご)', 'native_name' => '日本語 (にほんご)',
], ],
[ [
'code' => 'jv', 'code' => 'jv',
'native_name' => 'ꦧꦱꦗꦮ, Basa Jawa', 'native_name' => 'ꦧꦱꦗꦮ, Basa Jawa',
], ],
[ [
'code' => 'ka', 'code' => 'ka',
'native_name' => 'ქართული', 'native_name' => 'ქართული',
], ],
[ [
'code' => 'kg', 'code' => 'kg',
'native_name' => 'Kikongo', 'native_name' => 'Kikongo',
], ],
[ [
'code' => 'ki', 'code' => 'ki',
'native_name' => 'Gĩkũyũ', 'native_name' => 'Gĩkũyũ',
], ],
[ [
'code' => 'kj', 'code' => 'kj',
'native_name' => 'Kuanyama', 'native_name' => 'Kuanyama',
], ],
[ [
'code' => 'kk', 'code' => 'kk',
'native_name' => 'қазақ тілі', 'native_name' => 'қазақ тілі',
], ],
[ [
'code' => 'kl', 'code' => 'kl',
'native_name' => 'Kalaallisut, kalaallit oqaasii', 'native_name' => 'kalaallisut, kalaallit oqaasii',
], ],
[ [
'code' => 'km', 'code' => 'km',
'native_name' => 'ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ', 'native_name' => 'ខ្មែរ, ខេមរភាសា, ភាសាខ្មែរ',
], ],
[ [
'code' => 'kn', 'code' => 'kn',
'native_name' => 'ಕನ್ನಡ', 'native_name' => 'ಕನ್ನಡ',
], ],
[ [
'code' => 'ko', 'code' => 'ko',
'native_name' => '한국어', 'native_name' => '한국어',
], ],
[ [
'code' => 'kr', 'code' => 'kr',
'native_name' => 'Kanuri', 'native_name' => 'Kanuri',
], ],
[ [
'code' => 'ks', 'code' => 'ks',
'native_name' => 'कश्मीरी, كشميري‎', 'native_name' => 'कश्मीरी, كشميري‎',
], ],
[ [
'code' => 'ku', 'code' => 'ku',
'native_name' => 'Kurdî, کوردی‎', 'native_name' => 'Kurdî, کوردی‎',
], ],
[ [
'code' => 'kv', 'code' => 'kv',
'native_name' => 'коми кыв', 'native_name' => 'коми кыв',
], ],
[ [
'code' => 'kw', 'code' => 'kw',
'native_name' => 'Kernewek', 'native_name' => 'Kernewek',
], ],
[ [
'code' => 'ky', 'code' => 'ky',
'native_name' => 'Кыргызча, Кыргыз тили', 'native_name' => 'Кыргызча, Кыргыз тили',
], ],
[ [
'code' => 'la', 'code' => 'la',
'native_name' => 'Latine, lingua latina', 'native_name' => 'latine, lingua latina',
], ],
[ [
'code' => 'lb', 'code' => 'lb',
'native_name' => 'Lëtzebuergesch', 'native_name' => 'Lëtzebuergesch',
], ],
[ [
'code' => 'lg', 'code' => 'lg',
'native_name' => 'Luganda', 'native_name' => 'Luganda',
], ],
[ [
'code' => 'li', 'code' => 'li',
'native_name' => 'Limburgs', 'native_name' => 'Limburgs',
], ],
[ [
'code' => 'ln', 'code' => 'ln',
'native_name' => 'Lingála', 'native_name' => 'Lingála',
], ],
[ [
'code' => 'lo', 'code' => 'lo',
'native_name' => 'ພາສາລາວ', 'native_name' => 'ພາສາລາວ',
], ],
[ [
'code' => 'lt', 'code' => 'lt',
'native_name' => 'Lietuvių kalba', 'native_name' => 'lietuvių kalba',
], ],
[ [
'code' => 'lu', 'code' => 'lu',
'native_name' => 'Kiluba', 'native_name' => 'Kiluba',
], ],
[ [
'code' => 'lv', 'code' => 'lv',
'native_name' => 'Latviešu valoda', 'native_name' => 'latviešu valoda',
], ],
[ [
'code' => 'mg', 'code' => 'mg',
'native_name' => 'Fiteny malagasy', 'native_name' => 'fiteny malagasy',
], ],
[ [
'code' => 'mh', 'code' => 'mh',
'native_name' => 'Kajin M̧ajeļ', 'native_name' => 'Kajin M̧ajeļ',
], ],
[ [
'code' => 'mi', 'code' => 'mi',
'native_name' => 'Te reo Māori', 'native_name' => 'te reo Māori',
], ],
[ [
'code' => 'mk', 'code' => 'mk',
'native_name' => 'македонски јазик', 'native_name' => 'македонски јазик',
], ],
[ [
'code' => 'ml', 'code' => 'ml',
'native_name' => 'മലയാളം', 'native_name' => 'മലയാളം',
], ],
[ [
'code' => 'mn', 'code' => 'mn',
'native_name' => 'Монгол хэл', 'native_name' => 'Монгол хэл',
], ],
[ [
'code' => 'mr', 'code' => 'mr',
'native_name' => 'मराठी', 'native_name' => 'मराठी',
], ],
[ [
'code' => 'ms', 'code' => 'ms',
'native_name' => 'Bahasa Melayu, بهاس ملايو‎', 'native_name' => 'Bahasa Melayu, بهاس ملايو‎',
], ],
[ [
'code' => 'mt', 'code' => 'mt',
'native_name' => 'Malti', 'native_name' => 'Malti',
], ],
[ [
'code' => 'my', 'code' => 'my',
'native_name' => 'ဗမာစာ', 'native_name' => 'ဗမာစာ',
], ],
[ [
'code' => 'na', 'code' => 'na',
'native_name' => 'Dorerin Naoero', 'native_name' => 'Dorerin Naoero',
], ],
[ [
'code' => 'nb', 'code' => 'nb',
'native_name' => 'Norsk Bokmål', 'native_name' => 'Norsk Bokmål',
], ],
[ [
'code' => 'nd', 'code' => 'nd',
'native_name' => 'isiNdebele', 'native_name' => 'isiNdebele',
], ],
[ [
'code' => 'ne', 'code' => 'ne',
'native_name' => 'नेपाली', 'native_name' => 'नेपाली',
], ],
[ [
'code' => 'ng', 'code' => 'ng',
'native_name' => 'Owambo', 'native_name' => 'Owambo',
], ],
[ [
'code' => 'nl', 'code' => 'nl',
'native_name' => 'Nederlands, Vlaams', 'native_name' => 'Nederlands, Vlaams',
], ],
[ [
'code' => 'nn', 'code' => 'nn',
'native_name' => 'Norsk Nynorsk', 'native_name' => 'Norsk Nynorsk',
], ],
[ [
'code' => 'no', 'code' => 'no',
'native_name' => 'Norsk', 'native_name' => 'Norsk',
], ],
[ [
'code' => 'nr', 'code' => 'nr',
'native_name' => 'isiNdebele', 'native_name' => 'isiNdebele',
], ],
[ [
'code' => 'nv', 'code' => 'nv',
'native_name' => 'Diné bizaad', 'native_name' => 'Diné bizaad',
], ],
[ [
'code' => 'ny', 'code' => 'ny',
'native_name' => 'Chicheŵa, chinyanja', 'native_name' => 'chiCheŵa, chinyanja',
], ],
[ [
'code' => 'oc', 'code' => 'oc',
'native_name' => 'Occitan, lenga dòc', 'native_name' => 'occitan, lenga dòc',
], ],
[ [
'code' => 'oj', 'code' => 'oj',
'native_name' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ', 'native_name' => 'ᐊᓂᔑᓈᐯᒧᐎᓐ',
], ],
[ [
'code' => 'om', 'code' => 'om',
'native_name' => 'Afaan Oromoo', 'native_name' => 'Afaan Oromoo',
], ],
[ [
'code' => 'or', 'code' => 'or',
'native_name' => 'ଓଡ଼ିଆ', 'native_name' => 'ଓଡ଼ିଆ',
], ],
[ [
'code' => 'os', 'code' => 'os',
'native_name' => 'ирон æвзаг', 'native_name' => 'ирон æвзаг',
], ],
[ [
'code' => 'pa', 'code' => 'pa',
'native_name' => 'ਪੰਜਾਬੀ, پنجابی‎', 'native_name' => 'ਪੰਜਾਬੀ, پنجابی‎',
], ],
[ [
'code' => 'pi', 'code' => 'pi',
'native_name' => 'पालि, पाळि', 'native_name' => 'पालि, पाळि',
], ],
[ [
'code' => 'pl', 'code' => 'pl',
'native_name' => 'język polski, polszczyzna', 'native_name' => 'język polski, polszczyzna',
], ],
[ [
'code' => 'ps', 'code' => 'ps',
'native_name' => 'پښتو', 'native_name' => 'پښتو',
], ],
[ [
'code' => 'pt', 'code' => 'pt',
'native_name' => 'Português', 'native_name' => 'Português',
], ],
[ [
'code' => 'qu', 'code' => 'qu',
'native_name' => 'Runa Simi, Kichwa', 'native_name' => 'Runa Simi, Kichwa',
], ],
[ [
'code' => 'rm', 'code' => 'rm',
'native_name' => 'Rumantsch Grischun', 'native_name' => 'Rumantsch Grischun',
], ],
[ [
'code' => 'rn', 'code' => 'rn',
'native_name' => 'Ikirundi', 'native_name' => 'Ikirundi',
], ],
[ [
'code' => 'ro', 'code' => 'ro',
'native_name' => 'Română', 'native_name' => 'Română',
], ],
[ [
'code' => 'ru', 'code' => 'ru',
'native_name' => 'Pусский', 'native_name' => 'русский',
], ],
[ [
'code' => 'rw', 'code' => 'rw',
'native_name' => 'Ikinyarwanda', 'native_name' => 'Ikinyarwanda',
], ],
[ [
'code' => 'sa', 'code' => 'sa',
'native_name' => 'संस्कृतम्', 'native_name' => 'संस्कृतम्',
], ],
[ [
'code' => 'sc', 'code' => 'sc',
'native_name' => 'Sardu', 'native_name' => 'sardu',
], ],
[ [
'code' => 'sd', 'code' => 'sd',
'native_name' => 'सिन्धी, سنڌي، سندھی‎', 'native_name' => 'सिन्धी, سنڌي، سندھی‎',
], ],
[ [
'code' => 'se', 'code' => 'se',
'native_name' => 'Davvisámegiella', 'native_name' => 'Davvisámegiella',
], ],
[ [
'code' => 'sg', 'code' => 'sg',
'native_name' => 'Yângâ tî sängö', 'native_name' => 'yângâ tî sängö',
], ],
[ [
'code' => 'si', 'code' => 'si',
'native_name' => 'සිංහල', 'native_name' => 'සිංහල',
], ],
[ [
'code' => 'sk', 'code' => 'sk',
'native_name' => 'Slovenčina, Slovenský Jazyk', 'native_name' => 'Slovenčina, Slovenský Jazyk',
], ],
[ [
'code' => 'sl', 'code' => 'sl',
'native_name' => 'Slovenski Jezik, Slovenščina', 'native_name' => 'Slovenski Jezik, Slovenščina',
], ],
[ [
'code' => 'sm', 'code' => 'sm',
'native_name' => "Gagana fa'a Samoa", 'native_name' => "gagana fa'a Samoa",
], ],
[ [
'code' => 'sn', 'code' => 'sn',
'native_name' => 'chiShona', 'native_name' => 'chiShona',
], ],
[ [
'code' => 'so', 'code' => 'so',
'native_name' => 'Soomaaliga, af Soomaali', 'native_name' => 'Soomaaliga, af Soomaali',
], ],
[ [
'code' => 'sq', 'code' => 'sq',
'native_name' => 'Shqip', 'native_name' => 'Shqip',
], ],
[ [
'code' => 'sr', 'code' => 'sr',
'native_name' => 'српски језик', 'native_name' => 'српски језик',
], ],
[ [
'code' => 'ss', 'code' => 'ss',
'native_name' => 'SiSwati', 'native_name' => 'SiSwati',
], ],
[ [
'code' => 'st', 'code' => 'st',
'native_name' => 'Sesotho', 'native_name' => 'Sesotho',
], ],
[ [
'code' => 'su', 'code' => 'su',
'native_name' => 'Basa Sunda', 'native_name' => 'Basa Sunda',
], ],
[ [
'code' => 'sv', 'code' => 'sv',
'native_name' => 'Svenska', 'native_name' => 'Svenska',
], ],
[ [
'code' => 'sw', 'code' => 'sw',
'native_name' => 'Kiswahili', 'native_name' => 'Kiswahili',
], ],
[ [
'code' => 'ta', 'code' => 'ta',
'native_name' => 'தமிழ்', 'native_name' => 'தமிழ்',
], ],
[ [
'code' => 'te', 'code' => 'te',
'native_name' => 'తెలుగు', 'native_name' => 'తెలుగు',
], ],
[ [
'code' => 'tg', 'code' => 'tg',
'native_name' => 'тоҷикӣ, toçikī, تاجیکی‎', 'native_name' => 'тоҷикӣ, toçikī, تاجیکی‎',
], ],
[ [
'code' => 'th', 'code' => 'th',
'native_name' => 'ไทย', 'native_name' => 'ไทย',
], ],
[ [
'code' => 'ti', 'code' => 'ti',
'native_name' => 'ትግርኛ', 'native_name' => 'ትግርኛ',
], ],
[ [
'code' => 'tk', 'code' => 'tk',
'native_name' => 'Türkmen, Түркмен', 'native_name' => 'Türkmen, Түркмен',
], ],
[ [
'code' => 'tl', 'code' => 'tl',
'native_name' => 'Wikang Tagalog', 'native_name' => 'Wikang Tagalog',
], ],
[ [
'code' => 'tn', 'code' => 'tn',
'native_name' => 'Setswana', 'native_name' => 'Setswana',
], ],
[ [
'code' => 'to', 'code' => 'to',
'native_name' => 'Faka Tonga', 'native_name' => 'Faka Tonga',
], ],
[ [
'code' => 'tr', 'code' => 'tr',
'native_name' => 'Türkçe', 'native_name' => 'Türkçe',
], ],
[ [
'code' => 'ts', 'code' => 'ts',
'native_name' => 'Xitsonga', 'native_name' => 'Xitsonga',
], ],
[ [
'code' => 'tt', 'code' => 'tt',
'native_name' => 'татар теле, tatar tele', 'native_name' => 'татар теле, tatar tele',
], ],
[ [
'code' => 'tw', 'code' => 'tw',
'native_name' => 'Twi', 'native_name' => 'Twi',
], ],
[ [
'code' => 'ty', 'code' => 'ty',
'native_name' => 'Reo Tahiti', 'native_name' => 'Reo Tahiti',
], ],
[ [
'code' => 'ug', 'code' => 'ug',
'native_name' => 'ئۇيغۇرچە‎, Uyghurche', 'native_name' => 'ئۇيغۇرچە‎, Uyghurche',
], ],
[ [
'code' => 'uk', 'code' => 'uk',
'native_name' => 'Українська', 'native_name' => 'Українська',
], ],
[ [
'code' => 'ur', 'code' => 'ur',
'native_name' => 'اردو', 'native_name' => 'اردو',
], ],
[ [
'code' => 'uz', 'code' => 'uz',
'native_name' => 'Oʻzbek, Ўзбек, أۇزبېك‎', 'native_name' => 'Oʻzbek, Ўзбек, أۇزبېك‎',
], ],
[ [
'code' => 've', 'code' => 've',
'native_name' => 'Tshivenḓa', 'native_name' => 'Tshivenḓa',
], ],
[ [
'code' => 'vi', 'code' => 'vi',
'native_name' => 'Tiếng Việt', 'native_name' => 'Tiếng Việt',
], ],
[ [
'code' => 'vo', 'code' => 'vo',
'native_name' => 'Volapük', 'native_name' => 'Volapük',
], ],
[ [
'code' => 'wa', 'code' => 'wa',
'native_name' => 'Walon', 'native_name' => 'Walon',
], ],
[ [
'code' => 'wo', 'code' => 'wo',
'native_name' => 'Wollof', 'native_name' => 'Wollof',
], ],
[ [
'code' => 'xh', 'code' => 'xh',
'native_name' => 'isiXhosa', 'native_name' => 'isiXhosa',
], ],
[ [
'code' => 'yi', 'code' => 'yi',
'native_name' => 'ייִדיש', 'native_name' => 'ייִדיש',
], ],
[ [
'code' => 'yo', 'code' => 'yo',
'native_name' => 'Yorùbá', 'native_name' => 'Yorùbá',
], ],
[ [
'code' => 'za', 'code' => 'za',
'native_name' => 'Saɯ cueŋƅ, Saw cuengh', 'native_name' => 'Saɯ cueŋƅ, Saw cuengh',
], ],
[ [
'code' => 'zh', 'code' => 'zh',
'native_name' => '中文 (Zhōngwén), 汉语, 漢語', 'native_name' => '中文 (Zhōngwén), 汉语, 漢語',
], ],
[ [
'code' => 'zu', 'code' => 'zu',
'native_name' => 'isiZulu', 'native_name' => 'isiZulu',
], ],
]; ];
foreach ($data as $languageLine) { $this->db
$this->db ->table('languages')
->table('languages') ->ignore(true)
->ignore(true) ->insertBatch($data);
->insert($languageLine);
}
} }
} }

View file

@ -0,0 +1,591 @@
<?php
declare(strict_types=1);
/**
* Class PlatformsSeeder Inserts values in platforms table in database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class PlatformSeeder extends Seeder
{
public function run(): void
{
$data = [
[
'slug' => 'amazon',
'type' => 'podcasting',
'label' => 'Amazon Music and Audible',
'home_url' => 'https://music.amazon.com/podcasts',
'submit_url' => 'http://amazon.com/podcasters',
],
[
'slug' => 'antennapod',
'type' => 'podcasting',
'label' => 'AntennaPod',
'home_url' => 'https://antennapod.org/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'apple',
'type' => 'podcasting',
'label' => 'Apple Podcasts',
'home_url' => 'https://www.apple.com/itunes/podcasts/',
'submit_url' =>
'https://podcastsconnect.apple.com/my-podcasts/new-feed',
],
[
'slug' => 'blubrry',
'type' => 'podcasting',
'label' => 'Blubrry',
'home_url' => 'https://www.blubrry.com/',
'submit_url' => 'https://www.blubrry.com/addpodcast.php',
],
[
'slug' => 'breaker',
'type' => 'podcasting',
'label' => 'Breaker',
'home_url' => 'https://www.breaker.audio/',
'submit_url' => 'https://podcasters.breaker.audio/',
],
[
'slug' => 'castbox',
'type' => 'podcasting',
'label' => 'Castbox',
'home_url' => 'https://castbox.fm/',
'submit_url' =>
'https://helpcenter.castbox.fm/portal/kb/articles/submit-my-podcast',
],
[
'slug' => 'castopod',
'type' => 'podcasting',
'label' => 'Castopod',
'home_url' => 'https://castopod.org/',
'submit_url' => 'https://castopod.org/instances',
],
[
'slug' => 'castro',
'type' => 'podcasting',
'label' => 'Castro',
'home_url' => 'http://castro.fm/',
'submit_url' =>
'https://castro.fm/support/link-to-your-podcast-in-castro',
],
[
'slug' => 'chartable',
'type' => 'podcasting',
'label' => 'Chartable',
'home_url' => 'https://chartable.com/',
'submit_url' => 'https://chartable.com/podcasts/submit',
],
[
'slug' => 'deezer',
'type' => 'podcasting',
'label' => 'Deezer',
'home_url' => 'https://www.deezer.com/',
'submit_url' => 'https://podcasters.deezer.com/submission',
],
[
'slug' => 'fyyd',
'type' => 'podcasting',
'label' => 'fyyd',
'home_url' => 'https://fyyd.de/',
'submit_url' => 'https://fyyd.de/add-feed',
],
[
'slug' => 'google',
'type' => 'podcasting',
'label' => 'Google Podcasts',
'home_url' => 'https://podcasts.google.com/about',
'submit_url' =>
'https://search.google.com/search-console/about',
],
[
'slug' => 'ivoox',
'type' => 'podcasting',
'label' => 'Ivoox',
'home_url' => 'https://www.ivoox.com/',
'submit_url' => 'http://www.ivoox.com/upload-podcast_u.html',
],
[
'slug' => 'listennotes',
'type' => 'podcasting',
'label' => 'ListenNotes',
'home_url' => 'https://www.listennotes.com/',
'submit_url' => 'https://www.listennotes.com/submit/',
],
[
'slug' => 'overcast',
'type' => 'podcasting',
'label' => 'Overcast',
'home_url' => 'https://overcast.fm/',
'submit_url' => 'https://overcast.fm/podcasterinfo',
],
[
'slug' => 'playerfm',
'type' => 'podcasting',
'label' => 'Player.Fm',
'home_url' => 'https://player.fm/',
'submit_url' => 'https://player.fm/importer/feed',
],
[
'slug' => 'pocketcasts',
'type' => 'podcasting',
'label' => 'Pocketcasts',
'home_url' => 'https://www.pocketcasts.com/',
'submit_url' => 'https://www.pocketcasts.com/submit/',
],
[
'slug' => 'podbean',
'type' => 'podcasting',
'label' => 'Podbean',
'home_url' => 'https://www.podbean.com/',
'submit_url' => 'https://www.podbean.com/site/submitPodcast',
],
[
'slug' => 'podcastaddict',
'type' => 'podcasting',
'label' => 'Podcast Addict',
'home_url' => 'https://podcastaddict.com/',
'submit_url' => 'https://podcastaddict.com/submit',
],
[
'slug' => 'podcastindex',
'type' => 'podcasting',
'label' => 'Podcast Index',
'home_url' => 'https://podcastindex.org/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'podchaser',
'type' => 'podcasting',
'label' => 'Podchaser',
'home_url' => 'https://www.podchaser.com/',
'submit_url' => 'https://www.podchaser.com/creators/edit',
],
[
'slug' => 'podcloud',
'type' => 'podcasting',
'label' => 'podCloud',
'home_url' => 'https://podcloud.fr/',
'submit_url' => 'https://podcloud.fr/studio/podcasts/new',
],
[
'slug' => 'podinstall',
'type' => 'podcasting',
'label' => 'Podinstall',
'home_url' => 'https://www.podinstall.com/',
'submit_url' => 'https://www.podinstall.com/claim.html',
],
[
'slug' => 'podlink',
'type' => 'podcasting',
'label' => 'pod.link',
'home_url' => 'https://pod.link/',
'submit_url' => 'https://pod.link',
],
[
'slug' => 'podtail',
'type' => 'podcasting',
'label' => 'Podtail',
'home_url' => 'https://podtail.com/',
'submit_url' => 'https://podtail.com/about/faq/',
],
[
'slug' => 'podfriend',
'type' => 'podcasting',
'label' => 'Podfriend',
'home_url' => 'https://www.podfriend.com/',
'submit_url' => 'https://api.podcastindex.org/signup',
],
[
'slug' => 'podverse',
'type' => 'podcasting',
'label' => 'Podverse',
'home_url' => 'https://podverse.fm/',
'submit_url' =>
'https://docs.google.com/forms/d/e/1FAIpQLSdewKP-YrE8zGjDPrkmoJEwCxPl_gizEkmzAlTYsiWAuAk1Ng/viewform',
],
[
'slug' => 'radiopublic',
'type' => 'podcasting',
'label' => 'RadioPublic',
'home_url' => 'https://radiopublic.com/',
'submit_url' => 'https://podcasters.radiopublic.com/signup',
],
[
'slug' => 'spotify',
'type' => 'podcasting',
'label' => 'Spotify',
'home_url' => 'https://www.spotify.com/',
'submit_url' => 'https://podcasters.spotify.com/submit',
],
[
'slug' => 'spreaker',
'type' => 'podcasting',
'label' => 'Spreaker',
'home_url' => 'https://www.spreaker.com/',
'submit_url' => 'https://www.spreaker.com/cms/shows/rss-import',
],
[
'slug' => 'stitcher',
'type' => 'podcasting',
'label' => 'Stitcher',
'home_url' => 'https://www.stitcher.com/',
'submit_url' => 'https://partners.stitcher.com/join',
],
[
'slug' => 'tunein',
'type' => 'podcasting',
'label' => 'TuneIn',
'home_url' => 'https://tunein.com/',
'submit_url' =>
'https://help.tunein.com/contact/add-podcast-S19TR3Sdf',
],
[
'slug' => 'anytime',
'type' => 'podcasting',
'label' => 'Anytime Podcast Player',
'home_url' => 'https://anytimeplayer.app/',
'submit_url' => '',
],
[
'slug' => 'breez',
'type' => 'podcasting',
'label' => 'Breez',
'home_url' => 'https://breez.technology/',
'submit_url' => '',
],
[
'slug' => 'castamatic',
'type' => 'podcasting',
'label' => 'Castamatic',
'home_url' => 'https://castamatic.com/',
'submit_url' => '',
],
[
'slug' => 'castcoverage',
'type' => 'podcasting',
'label' => 'CastCoverage',
'home_url' => 'http://castcoverage.com/',
'submit_url' => '',
],
[
'slug' => 'curiocaster',
'type' => 'podcasting',
'label' => 'CurioCaster',
'home_url' => 'https://curiocaster.com/',
'submit_url' => '',
],
[
'slug' => 'escapepod',
'type' => 'podcasting',
'label' => 'Escapepod',
'home_url' => 'http://y20k.org/escapepod/',
'submit_url' => '',
],
[
'slug' => 'fountain',
'type' => 'podcasting',
'label' => 'Fountain',
'home_url' => 'https://www.fountain.fm/',
'submit_url' => '',
],
[
'slug' => 'gpodder',
'type' => 'podcasting',
'label' => 'gPodder',
'home_url' => 'https://gpodder.org/',
'submit_url' => '',
],
[
'slug' => 'hypercatcher',
'type' => 'podcasting',
'label' => 'HyperCatcher',
'home_url' => 'https://hypercatcher.com/',
'submit_url' => '',
],
[
'slug' => 'ivyfm',
'type' => 'podcasting',
'label' => 'Ivy Podcast Discovery',
'home_url' => 'https://ivy.fm/',
'submit_url' => '',
],
[
'slug' => 'jumplink',
'type' => 'podcasting',
'label' => 'JumpLink',
'home_url' => 'https://jump.link/',
'submit_url' => 'https://jump.link/a/accounts/signup/',
],
[
'slug' => 'kasts',
'type' => 'podcasting',
'label' => 'Kasts',
'home_url' => 'https://apps.kde.org/kasts/',
'submit_url' => '',
],
[
'slug' => 'playapod',
'type' => 'podcasting',
'label' => 'Playapod',
'home_url' => 'https://playapod.com/',
'submit_url' => '',
],
[
'slug' => 'plink',
'type' => 'podcasting',
'label' => 'Plink',
'home_url' => 'https://plinkhq.com/',
'submit_url' => '',
],
[
'slug' => 'podcastchapters',
'type' => 'podcasting',
'label' => 'Podcast Chapters',
'home_url' => 'https://chaptersapp.com/',
'submit_url' => '',
],
[
'slug' => 'podcastguru',
'type' => 'podcasting',
'label' => 'Podcast Guru',
'home_url' => 'https://podcastguru.io/',
'submit_url' => 'https://podcastguru.io/promote-your-podcast/',
],
[
'slug' => 'podlp',
'type' => 'podcasting',
'label' => 'PodLP',
'home_url' => 'https://podlp.com/',
'submit_url' => 'https://podlp.com/submit.html',
],
[
'slug' => 'podnews',
'type' => 'podcasting',
'label' => 'podnews',
'home_url' => 'https://podnews.net/podcast/subscribe-pages',
'submit_url' => '',
],
[
'slug' => 'podstation',
'type' => 'podcasting',
'label' => 'podStation',
'home_url' => 'https://podstation.github.io/',
'submit_url' => '',
],
[
'slug' => 'sphinxchat',
'type' => 'podcasting',
'label' => 'Sphinx',
'home_url' => 'https://sphinx.chat/',
'submit_url' => '',
],
[
'slug' => 'tsacdop',
'type' => 'podcasting',
'label' => 'Tsacdop',
'home_url' => 'https://www.tsacdop.app/',
'submit_url' => '',
],
[
'slug' => 'zion',
'type' => 'podcasting',
'label' => 'Zion',
'home_url' => 'https://getzion.com/',
'submit_url' => 'https://shop.n2n2.chat/',
],
[
'slug' => 'paypal',
'type' => 'funding',
'label' => 'Paypal',
'home_url' => 'https://www.paypal.com/',
'submit_url' => 'https://www.paypal.com/paypalme/my/grab',
],
[
'slug' => 'gofundme',
'type' => 'funding',
'label' => 'GoFundMe',
'home_url' => 'https://www.gofundme.com/',
'submit_url' => 'https://www.gofundme.com/sign-up',
],
[
'slug' => 'helloasso',
'type' => 'funding',
'label' => 'helloasso',
'home_url' => 'https://www.helloasso.com/',
'submit_url' => 'https://auth.helloasso.com/inscription',
],
[
'slug' => 'indiegogo',
'type' => 'funding',
'label' => 'Indiegogo',
'home_url' => 'https://www.indiegogo.com/',
'submit_url' => 'https://www.indiegogo.com/start-a-campaign#/',
],
[
'slug' => 'kickstarter',
'type' => 'funding',
'label' => 'Kickstarter',
'home_url' => 'https://www.kickstarter.com/',
'submit_url' => 'https://www.kickstarter.com/learn',
],
[
'slug' => 'kisskissbankbank',
'type' => 'funding',
'label' => 'KissKissBankBank',
'home_url' => 'https://www.kisskissbankbank.com/',
'submit_url' =>
'https://www.kisskissbankbank.com/en/financer-mon-projet',
],
[
'slug' => 'liberapay',
'type' => 'funding',
'label' => 'Liberapay',
'home_url' => 'https://liberapay.com/',
'submit_url' => 'https://liberapay.com/sign-up',
],
[
'slug' => 'patreon',
'type' => 'funding',
'label' => 'Patreon',
'home_url' => 'https://www.patreon.com/',
'submit_url' => 'https://www.patreon.com/create',
],
[
'slug' => 'tipeee',
'type' => 'funding',
'label' => 'Tipeee',
'home_url' => 'https://tipeee.com/',
'submit_url' => 'https://tipeee.com/register/',
],
[
'slug' => 'ulule',
'type' => 'funding',
'label' => 'Ulule',
'home_url' => 'https://www.ulule.com/',
'submit_url' => 'https://www.ulule.com/projects/create/#/',
],
[
'slug' => 'discord',
'type' => 'social',
'label' => 'Discord',
'home_url' => 'https://discord.com/',
'submit_url' => 'https://discord.com/register',
],
[
'slug' => 'facebook',
'type' => 'social',
'label' => 'Facebook',
'home_url' => 'https://www.facebook.com/',
'submit_url' =>
'https://www.facebook.com/pages/creation/?ref_type=comet_home',
],
[
'slug' => 'funkwhale',
'type' => 'social',
'label' => 'Funkwhale',
'home_url' => 'https://funkwhale.audio/',
'submit_url' => 'https://network.funkwhale.audio/dashboards/',
],
[
'slug' => 'instagram',
'type' => 'social',
'label' => 'Instagram',
'home_url' => 'https://www.instagram.com/',
'submit_url' =>
'https://www.instagram.com/accounts/emailsignup/',
],
[
'slug' => 'linkedin',
'type' => 'social',
'label' => 'LinkedIn',
'home_url' => 'https://www.linkedin.com/',
'submit_url' => 'https://www.linkedin.com/company/setup/new/',
],
[
'slug' => 'mastodon',
'type' => 'social',
'label' => 'Mastodon',
'home_url' => 'https://joinmastodon.org/',
'submit_url' => 'https://joinmastodon.org/communities',
],
[
'slug' => 'mobilizon',
'type' => 'social',
'label' => 'Mobilizon',
'home_url' => 'https://joinmobilizon.org/',
'submit_url' => 'https://instances.joinmobilizon.org/instances',
],
[
'slug' => 'peertube',
'type' => 'social',
'label' => 'PeerTube',
'home_url' => 'https://joinpeertube.org/',
'submit_url' => 'https://joinpeertube.org/instances',
],
[
'slug' => 'pixelfed',
'type' => 'social',
'label' => 'Pixelfed',
'home_url' => 'https://pixelfed.org/',
'submit_url' => 'https://beta.joinpixelfed.org/',
],
[
'slug' => 'plume',
'type' => 'social',
'label' => 'Plume',
'home_url' => 'https://joinplu.me/',
'submit_url' => 'https://joinplu.me/#instances',
],
[
'slug' => 'slack',
'type' => 'social',
'label' => 'Slack',
'home_url' => 'https://slack.com/',
'submit_url' => 'https://slack.com/get-started#/create',
],
[
'slug' => 'twitch',
'type' => 'social',
'label' => 'Twitch',
'home_url' => 'https://www.twitch.tv/',
'submit_url' => 'https://www.twitch.tv/signup',
],
[
'slug' => 'twitter',
'type' => 'social',
'label' => 'Twitter',
'home_url' => 'https://twitter.com/',
'submit_url' => 'https://twitter.com/i/flow/signup',
],
[
'slug' => 'writefreely',
'type' => 'social',
'label' => 'WriteFreely',
'home_url' => 'https://writefreely.org/',
'submit_url' => 'https://writefreely.org/instances',
],
[
'slug' => 'youtube',
'type' => 'social',
'label' => 'Youtube',
'home_url' => 'https://www.youtube.com/',
'submit_url' => 'https://creatoracademy.youtube.com/page/home',
],
];
$this->db
->table('platforms')
->ignore(true)
->insertBatch($data);
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* Class TestSeeder Inserts a superadmin user in the database
*
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class TestSeeder extends Seeder
{
public function run(): void
{
/**
* Inserts an active user with the following credentials: username: admin password: AGUehL3P
*/
$this->db->table('users')
->insert([
'id' => 1,
'username' => 'admin',
'email' => 'admin@example.com',
'password_hash' =>
'$2y$10$TXJEHX/djW8jtzgpDVf7dOOCGo5rv1uqtAYWdwwwkttQcDkAeB2.6',
'active' => 1,
]);
$this->db
->table('auth_groups_users')
->insert([
'group_id' => 1,
'user_id' => 1,
]);
}
}

View file

@ -3,22 +3,22 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace App\Entities;
use ActivityPub\Entities\Actor as ActivityPubActor;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use Modules\Fediverse\Entities\Actor as FediverseActor; use RuntimeException;
use Override;
/** /**
* @property Podcast|null $podcast * @property Podcast|null $podcast
* @property boolean $is_podcast * @property boolean $is_podcast
*/ */
class Actor extends FediverseActor class Actor extends ActivityPubActor
{ {
protected ?Podcast $podcast = null; protected ?Podcast $podcast = null;
@ -26,36 +26,19 @@ class Actor extends FediverseActor
public function getIsPodcast(): bool public function getIsPodcast(): bool
{ {
return $this->getPodcast() instanceof Podcast; return $this->getPodcast() !== null;
} }
public function getPodcast(): ?Podcast public function getPodcast(): ?Podcast
{ {
if (! $this->podcast instanceof Podcast) { if ($this->id === null) {
$this->podcast = new PodcastModel() throw new RuntimeException('Podcast id must be set before getting associated podcast.');
->getPodcastByActorId($this->id); }
if ($this->podcast === null) {
$this->podcast = (new PodcastModel())->getPodcastByActorId($this->id);
} }
return $this->podcast; return $this->podcast;
} }
#[Override]
public function getAvatarImageUrl(): string
{
if ($this->podcast instanceof Podcast) {
return $this->podcast->cover->thumbnail_url;
}
return parent::getAvatarImageUrl();
}
#[Override]
public function getAvatarImageMimetype(): string
{
if ($this->podcast instanceof Podcast) {
return $this->podcast->cover->thumbnail_mimetype;
}
return parent::getAvatarImageMimetype();
}
} }

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -15,7 +15,7 @@ use CodeIgniter\Entity\Entity;
/** /**
* @property int $id * @property int $id
* @property ?int $parent_id * @property int $parent_id
* @property Category|null $parent * @property Category|null $parent
* @property string $code * @property string $code
* @property string $apple_category * @property string $apple_category
@ -29,20 +29,22 @@ class Category extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'parent_id' => '?integer', 'parent_id' => '?integer',
'code' => 'string', 'code' => 'string',
'apple_category' => 'string', 'apple_category' => 'string',
'google_category' => 'string', 'google_category' => 'string',
]; ];
/**
* @noRector ReturnTypeDeclarationRector
*/
public function getParent(): ?self public function getParent(): ?self
{ {
if ($this->parent_id === null) { if ($this->parent_id === null) {
return null; return null;
} }
return new CategoryModel() return (new CategoryModel())->getCategoryById($this->parent_id);
->getCategoryById($this->parent_id);
} }
} }

View file

@ -1,156 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Clip;
use App\Entities\Episode;
use App\Entities\Podcast;
use App\Models\EpisodeModel;
use App\Models\PodcastModel;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
/**
* @property int $id
* @property int $podcast_id
* @property Podcast $podcast
* @property int $episode_id
* @property Episode $episode
* @property string $title
* @property double $start_time
* @property ?double $end_time
* @property double $duration
* @property string $type
* @property int|null $media_id
* @property Video|Audio|null $media
* @property array<mixed>|null $metadata
* @property string $status
* @property string $logs
* @property User $user
* @property int $created_by
* @property int $updated_by
* @property Time|null $job_started_at
* @property Time|null $job_ended_at
*/
class BaseClip extends Entity
{
/**
* @var Video|Audio|null
*/
protected $media;
protected ?int $job_duration = null;
protected ?float $end_time = null;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['created_at', 'updated_at', 'job_started_at', 'job_ended_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'integer',
'podcast_id' => 'integer',
'episode_id' => 'integer',
'title' => 'string',
'start_time' => 'double',
'duration' => 'double',
'type' => 'string',
'media_id' => '?integer',
'metadata' => '?json-array',
'status' => 'string',
'logs' => 'string',
'created_by' => 'integer',
'updated_by' => 'integer',
];
public function getJobDuration(): ?int
{
if ($this->job_duration === null && $this->job_started_at && $this->job_ended_at) {
$this->job_duration = ($this->job_started_at->difference($this->job_ended_at))
->getSeconds();
}
return $this->job_duration;
}
public function getEndTime(): float
{
if ($this->end_time === null) {
$this->end_time = $this->start_time + $this->duration;
}
return $this->end_time;
}
public function getPodcast(): ?Podcast
{
return new PodcastModel()
->getPodcastById($this->podcast_id);
}
public function getEpisode(): ?Episode
{
return new EpisodeModel()
->getEpisodeById($this->episode_id);
}
public function getUser(): ?User
{
/** @var ?User */
return new UserModel()
->find($this->created_by);
}
public function setMedia(File $file, string $fileKey): static
{
if ($this->media_id !== null) {
$this->getMedia()
->setFile($file);
$this->getMedia()
->updated_by = $this->attributes['updated_by'];
new MediaModel('audio')
->updateMedia($this->getMedia());
} else {
$media = new Audio([
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$media->setFile($file);
$this->attributes['media_id'] = new MediaModel()->saveMedia($media);
}
return $this;
}
public function getMedia(): Audio | Video | null
{
if ($this->media_id !== null && $this->media === null) {
$this->media = new MediaModel($this->type)
->getMediaById($this->media_id);
}
return $this->media;
}
}

View file

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Clip;
class Soundbite extends BaseClip
{
protected string $type = 'audio';
}

View file

@ -1,88 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities\Clip;
use CodeIgniter\Files\File;
use Modules\Media\Entities\Video;
use Modules\Media\Models\MediaModel;
use Override;
/**
* @property array{name:string,preview:string} $theme
* @property string $format
*/
class VideoClip extends BaseClip
{
protected string $type = 'video';
/**
* @param array<string, mixed>|null $data
*/
public function __construct(?array $data = null)
{
parent::__construct($data);
if ($this->metadata !== null && $this->metadata !== []) {
$this->theme = $this->metadata['theme'];
$this->format = $this->metadata['format'];
}
}
/**
* @param array{name:string,preview:string} $theme
*/
public function setTheme(array $theme): self
{
// TODO: change?
$this->attributes['metadata'] = json_decode($this->attributes['metadata'] ?? '[]', true);
$this->attributes['theme'] = $theme;
$this->attributes['metadata']['theme'] = $theme;
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
return $this;
}
public function setFormat(string $format): self
{
$this->attributes['metadata'] = json_decode((string) $this->attributes['metadata'], true);
$this->attributes['format'] = $format;
$this->attributes['metadata']['format'] = $format;
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
return $this;
}
#[Override]
public function setMedia(File $file, string $fileKey): static
{
if ($this->attributes['media_id'] !== null) {
// media is already set, do nothing
return $this;
}
$video = new Video([
'file_key' => $fileKey,
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['created_by'],
'updated_by' => $this->attributes['created_by'],
]);
$video->setFile($file);
$this->attributes['media_id'] = new MediaModel('video')->saveMedia($video);
return $this;
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -45,19 +45,22 @@ class Credit extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'podcast_id' => 'integer', 'podcast_id' => 'integer',
'episode_id' => '?integer', 'episode_id' => '?integer',
'person_id' => 'integer', 'person_id' => 'integer',
'full_name' => 'string', 'full_name' => 'string',
'person_group' => 'string', 'person_group' => 'string',
'person_role' => 'string', 'person_role' => 'string',
]; ];
public function getPerson(): ?Person public function getPerson(): ?Person
{ {
if (! $this->person instanceof Person) { if ($this->person_id === null) {
$this->person = new PersonModel() throw new RuntimeException('Credit must have person_id before getting person.');
->getPersonById($this->person_id); }
if ($this->person === null) {
$this->person = (new PersonModel())->getPersonById($this->person_id);
} }
return $this->person; return $this->person;
@ -65,9 +68,12 @@ class Credit extends Entity
public function getPodcast(): ?Podcast public function getPodcast(): ?Podcast
{ {
if (! $this->podcast instanceof Podcast) { if ($this->podcast_id === null) {
$this->podcast = new PodcastModel() throw new RuntimeException('Credit must have podcast_id before getting podcast.');
->getPodcastById($this->podcast_id); }
if ($this->podcast === null) {
$this->podcast = (new PodcastModel())->getPodcastById($this->podcast_id);
} }
return $this->podcast; return $this->podcast;
@ -79,9 +85,8 @@ class Credit extends Entity
throw new RuntimeException('Credit must have episode_id before getting episode.'); throw new RuntimeException('Credit must have episode_id before getting episode.');
} }
if (! $this->episode instanceof Episode) { if ($this->episode === null) {
$this->episode = new EpisodeModel() $this->episode = (new EpisodeModel())->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
->getPublishedEpisodeById($this->podcast_id, $this->episode_id);
} }
return $this->episode; return $this->episode;
@ -89,11 +94,10 @@ class Credit extends Entity
public function getGroupLabel(): string public function getGroupLabel(): string
{ {
if ($this->person_group === '') { if ($this->person_group === null) {
return ''; return '';
} }
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.label"); return lang("PersonsTaxonomy.persons.{$this->person_group}.label");
} }
@ -107,7 +111,6 @@ class Credit extends Entity
return ''; return '';
} }
/** @var string */
return lang("PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label"); return lang("PersonsTaxonomy.persons.{$this->person_group}.roles.{$this->person_role}.label");
} }
} }

View file

@ -3,113 +3,108 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace App\Entities;
use App\Entities\Clip\Soundbite; use App\Libraries\SimpleRSSElement;
use App\Models\ClipModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel; use App\Models\SoundbiteModel;
use App\Models\StatusModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File; use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use Exception; use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment; use RuntimeException;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Media\Entities\Audio;
use Modules\Media\Entities\Chapters;
use Modules\Media\Entities\Image;
use Modules\Media\Entities\Transcript;
use Modules\Media\Models\MediaModel;
use Override;
/** /**
* @property int $id * @property int $id
* @property int $podcast_id * @property int $podcast_id
* @property Podcast $podcast * @property Podcast $podcast
* @property ?string $preview_id
* @property string $preview_link
* @property string $link * @property string $link
* @property string $guid * @property string $guid
* @property string $slug * @property string $slug
* @property string $title * @property string $title
* @property int $audio_id * @property File $audio_file
* @property ?Audio $audio * @property string $audio_file_url
* @property string $audio_url * @property string $audio_file_analytics_url
* @property string $audio_web_url * @property string $audio_file_web_url
* @property string $audio_opengraph_url * @property string $audio_file_opengraph_url
* @property string $audio_file_path
* @property double $audio_file_duration
* @property string $audio_file_mimetype
* @property int $audio_file_size
* @property int $audio_file_header_size
* @property string|null $description Holds text only description, striped of any markdown or html special characters * @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown * @property string $description_markdown
* @property string $description_html * @property string $description_html
* @property ?int $cover_id * @property Image $image
* @property ?Image $cover * @property string|null $image_path
* @property int|null $transcript_id * @property string|null $image_mimetype
* @property Transcript|null $transcript * @property File|null $transcript_file
* @property string|null $transcript_remote_url * @property string|null $transcript_file_url
* @property int|null $chapters_id * @property string|null $transcript_file_path
* @property Chapters|null $chapters * @property string|null $transcript_file_remote_url
* @property string|null $chapters_remote_url * @property File|null $chapters_file
* @property string|null $chapters_file_url
* @property string|null $chapters_file_path
* @property string|null $chapters_file_remote_url
* @property string|null $parental_advisory * @property string|null $parental_advisory
* @property ?int $number * @property int $number
* @property ?int $season_number * @property int $season_number
* @property string $type * @property string $type
* @property bool $is_blocked * @property bool $is_blocked
* @property Location|null $location * @property Location|null $location
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm * @property string|null $location_osm
* @property bool $is_published_on_hubs * @property array|null $custom_rss
* @property int $downloads_count * @property string $custom_rss_string
* @property int $posts_count * @property int $favourites_total
* @property int $comments_count * @property int $reblogs_total
* @property EpisodeComment[]|null $comments * @property int $statuses_total
* @property bool $is_premium
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string $publication_status * @property string $publication_status;
* @property Time|null $published_at * @property Time|null $published_at;
* @property Time $created_at * @property Time $created_at;
* @property Time $updated_at * @property Time $updated_at;
* @property Time|null $deleted_at;
* *
* @property Person[] $persons * @property Person[] $persons;
* @property Soundbite[] $soundbites * @property Soundbite[] $soundbites;
* @property string $embed_url * @property string $embeddable_player_url;
*/ */
class Episode extends Entity class Episode extends Entity
{ {
public string $link = '';
public string $audio_url = '';
public string $audio_web_url = '';
public string $audio_opengraph_url = '';
protected Podcast $podcast; protected Podcast $podcast;
protected ?Audio $audio = null; protected string $link;
protected string $embed_url = ''; protected File $audio_file;
protected ?Image $cover = null; protected string $audio_file_url;
protected string $audio_file_analytics_url;
protected string $audio_file_web_url;
protected string $audio_file_opengraph_url;
protected string $embeddable_player_url;
protected Image $image;
protected ?string $description = null; protected ?string $description = null;
protected ?Transcript $transcript = null; protected File $transcript_file;
protected ?Chapters $chapters = null; protected File $chapters_file;
/** /**
* @var Person[]|null * @var Person[]|null
@ -122,272 +117,237 @@ class Episode extends Entity
protected ?array $soundbites = null; protected ?array $soundbites = null;
/** /**
* @var Post[]|null * @var Status[]|null
*/ */
protected ?array $posts = null; protected ?array $statuses = null;
/** /**
* @var EpisodeComment[]|null * @var Status[]|null
*/ */
protected ?array $comments = null; protected ?array $comments = null;
protected ?Location $location = null; protected ?Location $location = null;
protected string $custom_rss_string;
protected ?string $publication_status = null; protected ?string $publication_status = null;
/** /**
* @var array<int, string> * @var string[]
* @phpstan-var list<string>
*/ */
protected $dates = ['published_at', 'created_at', 'updated_at']; protected $dates = ['published_at', 'created_at', 'updated_at', 'deleted_at'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'podcast_id' => 'integer', 'podcast_id' => 'integer',
'preview_id' => '?string', 'guid' => 'string',
'guid' => 'string', 'slug' => 'string',
'slug' => 'string', 'title' => 'string',
'title' => 'string', 'audio_file_path' => 'string',
'audio_id' => 'integer', 'audio_file_duration' => 'double',
'description_markdown' => 'string', 'audio_file_mimetype' => 'string',
'description_html' => 'string', 'audio_file_size' => 'integer',
'cover_id' => '?integer', 'audio_file_header_size' => 'integer',
'transcript_id' => '?integer', 'description_markdown' => 'string',
'transcript_remote_url' => '?string', 'description_html' => 'string',
'chapters_id' => '?integer', 'image_path' => '?string',
'chapters_remote_url' => '?string', 'image_mimetype' => '?string',
'parental_advisory' => '?string', 'transcript_file_path' => '?string',
'number' => '?integer', 'transcript_file_remote_url' => '?string',
'season_number' => '?integer', 'chapters_file_path' => '?string',
'type' => 'string', 'chapters_file_remote_url' => '?string',
'is_blocked' => 'boolean', 'parental_advisory' => '?string',
'location_name' => '?string', 'number' => '?integer',
'location_geo' => '?string', 'season_number' => '?integer',
'location_osm' => '?string', 'type' => 'string',
'is_published_on_hubs' => 'boolean', 'is_blocked' => 'boolean',
'downloads_count' => 'integer', 'location_name' => '?string',
'posts_count' => 'integer', 'location_geo' => '?string',
'comments_count' => 'integer', 'location_osm' => '?string',
'is_premium' => 'boolean', 'custom_rss' => '?json-array',
'created_by' => 'integer', 'favourites_total' => 'integer',
'updated_by' => 'integer', 'reblogs_total' => 'integer',
'statuses_total' => 'integer',
'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
/** /**
* @param array<string, mixed> $data * Saves an episode image
*/ */
#[Override] public function setImage(?Image $image = null): static
public function injectRawData(array $data): static
{ {
parent::injectRawData($data); if ($image === null) {
return $this;
}
$this->link = url_to('episode', esc($this->getPodcast()->handle, 'url'), esc($this->attributes['slug'], 'url')); // Save image
$image->saveImage('podcasts/' . $this->getPodcast()->name, $this->attributes['slug']);
$this->audio_url = url_to( $this->attributes['image_mimetype'] = $image->mimetype;
'episode-audio', $this->attributes['image_path'] = $image->path;
$this->getPodcast()
->handle, return $this;
$this->slug, }
$this->getAudio()
->file_extension, public function getImage(): Image
{
if ($imagePath = $this->attributes['image_path']) {
return new Image(null, $imagePath, $this->attributes['image_mimetype']);
}
return $this->getPodcast()
->image;
}
/**
* Saves an audio file
*/
public function setAudioFile(UploadedFile | File $audioFile): static
{
helper(['media', 'id3']);
$audioMetadata = get_file_tags($audioFile);
$this->attributes['audio_file_path'] = save_media(
$audioFile,
'podcasts/' . $this->getPodcast()->name,
$this->attributes['slug'],
);
$this->attributes['audio_file_duration'] =
$audioMetadata['playtime_seconds'];
$this->attributes['audio_file_mimetype'] = $audioMetadata['mime_type'];
$this->attributes['audio_file_size'] = $audioMetadata['filesize'];
$this->attributes['audio_file_header_size'] =
$audioMetadata['avdataoffset'];
return $this;
}
/**
* Saves an episode transcript file
*/
public function setTranscriptFile(UploadedFile | File $transcriptFile): static
{
helper('media');
$this->attributes['transcript_file_path'] = save_media(
$transcriptFile,
'podcasts/' . $this->getPodcast()
->name,
$this->attributes['slug'] . '-transcript',
); );
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
$this->audio_web_url = $this->audio_url . '?_from=-+Website+-';
return $this; return $this;
} }
public function setCover(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) {
$this->getCover()
->setFile($file);
$this->getCover()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$cover->setFile($file);
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
}
return $this;
}
public function getCover(): Image
{
if ($this->cover instanceof Image) {
return $this->cover;
}
if ($this->cover_id === null) {
$this->cover = $this->getPodcast()
->getCover();
return $this->cover;
}
$this->cover = new MediaModel('image')
->getMediaById($this->cover_id);
return $this->cover;
}
public function setAudio(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if ($this->audio_id !== 0) {
$this->getAudio()
->setFile($file);
$this->getAudio()
->updated_by = $this->attributes['updated_by'];
new MediaModel('audio')
->updateMedia($this->getAudio());
} else {
$audio = new Audio([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $file->getRandomName(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$audio->setFile($file);
$this->attributes['audio_id'] = new MediaModel()->saveMedia($audio);
}
return $this;
}
public function getAudio(): Audio
{
if (! $this->audio instanceof Audio) {
$this->audio = new MediaModel('audio')
->getMediaById($this->audio_id);
}
return $this->audio;
}
public function setTranscript(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if ($this->getTranscript() instanceof Transcript) {
$this->getTranscript()
->setFile($file);
$this->getTranscript()
->updated_by = $this->attributes['updated_by'];
new MediaModel('transcript')
->updateMedia($this->getTranscript());
} else {
$transcript = new Transcript([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$transcript->setFile($file);
$this->attributes['transcript_id'] = new MediaModel('transcript')->saveMedia($transcript);
}
return $this;
}
public function getTranscript(): ?Transcript
{
if ($this->transcript_id !== null && ! $this->transcript instanceof Transcript) {
$this->transcript = new MediaModel('transcript')
->getMediaById($this->transcript_id);
}
return $this->transcript;
}
public function setChapters(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if ($this->getChapters() instanceof Chapters) {
$this->getChapters()
->setFile($file);
$this->getChapters()
->updated_by = $this->attributes['updated_by'];
new MediaModel('chapters')
->updateMedia($this->getChapters());
} else {
$chapters = new Chapters([
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
'language_code' => $this->getPodcast()
->language_code,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$chapters->setFile($file);
$this->attributes['chapters_id'] = new MediaModel('chapters')->saveMedia($chapters);
}
return $this;
}
public function getChapters(): ?Chapters
{
if ($this->chapters_id !== null && ! $this->chapters instanceof Chapters) {
$this->chapters = new MediaModel('chapters')
->getMediaById($this->chapters_id);
}
return $this->chapters;
}
/** /**
* Gets transcript url from transcript file uri if it exists or returns the transcript_remote_url which can be null. * Saves an episode chapters file
*/ */
public function getTranscriptUrl(): ?string public function setChaptersFile(UploadedFile | File $chaptersFile): static
{ {
if ($this->transcript instanceof Transcript) { helper('media');
return $this->transcript->file_url;
$this->attributes['chapters_file_path'] = save_media(
$chaptersFile,
'podcasts/' . $this->getPodcast()
->name,
$this->attributes['slug'] . '-chapters',
);
return $this;
}
public function getAudioFile(): File
{
helper('media');
return new File(media_path($this->audio_file_path));
}
public function getTranscriptFile(): ?File
{
if ($this->attributes['transcript_file_path']) {
helper('media');
return new File(media_path($this->attributes['transcript_file_path']));
} }
return $this->transcript_remote_url; return null;
}
public function getChaptersFile(): ?File
{
if ($this->attributes['chapters_file_path']) {
helper('media');
return new File(media_path($this->attributes['chapters_file_path']));
}
return null;
}
public function getAudioFileUrl(): string
{
helper('media');
return media_base_url($this->audio_file_path);
}
public function getAudioFileAnalyticsUrl(): string
{
helper('analytics');
// remove 'podcasts/' from audio file path
$strippedAudioFilePath = substr($this->audio_file_path, 9);
return generate_episode_analytics_url(
$this->podcast_id,
$this->id,
$strippedAudioFilePath,
$this->audio_file_duration,
$this->audio_file_size,
$this->audio_file_header_size,
$this->published_at,
);
}
public function getAudioFileWebUrl(): string
{
return $this->getAudioFileAnalyticsUrl() . '?_from=-+Website+-';
}
public function getAudioFileOpengraphUrl(): string
{
return $this->getAudioFileAnalyticsUrl() . '?_from=-+Open+Graph+-';
} }
/** /**
* Gets chapters file url from chapters file uri if it exists or returns the chapters_remote_url which can be null. * Gets transcript url from transcript file uri if it exists or returns the transcript_file_remote_url which can be
* null.
*/
public function getTranscriptFileUrl(): ?string
{
if ($this->attributes['transcript_file_path']) {
return media_base_url($this->attributes['transcript_file_path']);
}
return $this->attributes['transcript_file_remote_url'];
}
/**
* Gets chapters file url from chapters file uri if it exists or returns the chapters_file_remote_url which can be
* null.
*/ */
public function getChaptersFileUrl(): ?string public function getChaptersFileUrl(): ?string
{ {
if ($this->chapters instanceof Chapters) { if ($this->chapters_file_path) {
return $this->chapters->file_url; return media_base_url($this->chapters_file_path);
} }
return $this->chapters_remote_url; return $this->chapters_file_remote_url;
} }
/** /**
@ -397,101 +357,145 @@ class Episode extends Entity
*/ */
public function getPersons(): array public function getPersons(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting persons.');
}
if ($this->persons === null) { if ($this->persons === null) {
$this->persons = new PersonModel() $this->persons = (new PersonModel())->getEpisodePersons($this->podcast_id, $this->id);
->getEpisodePersons($this->podcast_id, $this->id);
} }
return $this->persons; return $this->persons;
} }
/** /**
* Returns the episodes clips * Returns the episodes soundbites
* *
* @return Soundbite[] * @return Soundbite[]
*/ */
public function getSoundbites(): array public function getSoundbites(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting soundbites.');
}
if ($this->soundbites === null) { if ($this->soundbites === null) {
$this->soundbites = new ClipModel() $this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites($this->getPodcast() ->id, $this->id);
->getEpisodeSoundbites($this->getPodcast()->id, $this->id);
} }
return $this->soundbites; return $this->soundbites;
} }
/** /**
* @return Post[] * @return Status[]
*/ */
public function getPosts(): array public function getStatuses(): array
{ {
if ($this->posts === null) { if ($this->id === null) {
$this->posts = new PostModel() throw new RuntimeException('Episode must be created before getting statuses.');
->getEpisodePosts($this->id);
} }
return $this->posts; if ($this->statuses === null) {
$this->statuses = (new StatusModel())->getEpisodeStatuses($this->id);
}
return $this->statuses;
} }
/** /**
* @return EpisodeComment[] * @return Status[]
*/ */
public function getComments(): array public function getComments(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Episode must be created before getting comments.');
}
if ($this->comments === null) { if ($this->comments === null) {
$this->comments = new EpisodeCommentModel() $this->comments = (new StatusModel())->getEpisodeComments($this->id);
->getEpisodeComments($this->id);
} }
return $this->comments; return $this->comments;
} }
public function getEmbedUrl(?string $theme = null): string public function getLink(): string
{ {
return $theme return url_to('episode', $this->getPodcast()->name, $this->attributes['slug']);
? url_to('embed-theme', esc($this->getPodcast()->handle), esc($this->attributes['slug']), $theme) }
: url_to('embed', esc($this->getPodcast()->handle), esc($this->attributes['slug']));
public function getEmbeddablePlayerUrl(string $theme = null): string
{
return base_url(
$theme
? route_to(
'embeddable-player-theme',
$this->getPodcast()
->name,
$this->attributes['slug'],
$theme,
)
: route_to('embeddable-player', $this->getPodcast() ->name, $this->attributes['slug']),
);
} }
public function setGuid(?string $guid = null): static public function setGuid(?string $guid = null): static
{ {
$this->attributes['guid'] = $guid ?? $this->link; $this->attributes['guid'] = $guid === null ? $this->getLink() : $guid;
return $this; return $this;
} }
public function getPodcast(): ?Podcast public function getPodcast(): ?Podcast
{ {
return new PodcastModel() return (new PodcastModel())->getPodcastById($this->podcast_id);
->getPodcastById($this->podcast_id);
} }
public function setDescriptionMarkdown(string $descriptionMarkdown): static public function setDescriptionMarkdown(string $descriptionMarkdown): static
{ {
$config = [ $converter = new CommonMarkConverter([
'html_input' => 'escape', 'html_input' => 'strip',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]; ]);
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['description_markdown'] = $descriptionMarkdown; $this->attributes['description_markdown'] = $descriptionMarkdown;
$this->attributes['description_html'] = $converter->convert($descriptionMarkdown); $this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
return $this; return $this;
} }
public function getDescriptionHtml(?string $serviceSlug = null): string
{
$descriptionHtml = '';
if (
$this->getPodcast()
->partner_id !== null &&
$this->getPodcast()
->partner_link_url !== null &&
$this->getPodcast()
->partner_image_url !== null
) {
$descriptionHtml .= "<div><a href=\"{$this->getPartnerLink(
$serviceSlug,
)}\" rel=\"sponsored noopener noreferrer\" target=\"_blank\"><img src=\"{$this->getPartnerImageUrl(
$serviceSlug,
)}\" alt=\"Partner image\" /></a></div>";
}
$descriptionHtml .= $this->attributes['description_html'];
if ($this->getPodcast()->episode_description_footer_html) {
$descriptionHtml .= "<footer>{$this->getPodcast()
->episode_description_footer_html}</footer>";
}
return $descriptionHtml;
}
public function getDescription(): string public function getDescription(): string
{ {
if ($this->description === null) { if ($this->description === null) {
$this->description = trim( $this->description = trim(
(string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])), preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
); );
} }
@ -501,10 +505,8 @@ class Episode extends Entity
public function getPublicationStatus(): string public function getPublicationStatus(): string
{ {
if ($this->publication_status === null) { if ($this->publication_status === null) {
if (! $this->published_at instanceof Time) { if ($this->published_at === null) {
$this->publication_status = 'not_published'; $this->publication_status = 'not_published';
} elseif ($this->getPodcast()->publication_status !== 'published') {
$this->publication_status = 'with_podcast';
} elseif ($this->published_at->isBefore(Time::now())) { } elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published'; $this->publication_status = 'published';
} else { } else {
@ -520,7 +522,7 @@ class Episode extends Entity
*/ */
public function setLocation(?Location $location = null): static public function setLocation(?Location $location = null): static
{ {
if (! $location instanceof Location) { if ($location === null) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null; $this->attributes['location_osm'] = null;
@ -548,33 +550,88 @@ class Episode extends Entity
return null; return null;
} }
if (! $this->location instanceof Location) { if ($this->location === null) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm); $this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
} }
return $this->location; return $this->location;
} }
public function getPreviewLink(): string /**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{ {
if ($this->preview_id === null) { if ($this->custom_rss === null) {
// generate preview id return '';
if (! $previewUUID = new EpisodeModel()->setEpisodePreviewId($this->id)) {
throw new Exception('Could not set episode preview id');
}
$this->preview_id = $previewUUID;
} }
return url_to('episode-preview', (string) $this->preview_id); helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))
->addChild('channel')
->addChild('item');
array_to_rss([
'elements' => $this->custom_rss,
], $xmlNode);
return str_replace(['<item>', '</item>'], '', $xmlNode->asXML());
} }
/** /**
* Returns the episode's clip count * Saves custom rss tag into json
*/ */
public function getClipCount(): int|string public function setCustomRssString(?string $customRssString = null): static
{ {
return new ClipModel() if ($customRssString === null) {
->getClipCount($this->podcast_id, $this->id); return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel><item>' .
$customRssString .
'</item></channel></rss>',
),
)['elements'][0]['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
}
public function getPartnerLink(?string $serviceSlug = null): string
{
$partnerLink =
rtrim($this->getPodcast()->partner_link_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']);
if ($serviceSlug !== null) {
$partnerLink .= '&_from=' . $serviceSlug;
}
return $partnerLink;
}
public function getPartnerImageUrl(string $serviceSlug = null): string
{
return rtrim($this->getPodcast()->partner_image_url, '/') .
'?pid=' .
$this->getPodcast()
->partner_id .
'&guid=' .
urlencode($this->attributes['guid']) .
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
} }
} }

View file

@ -1,142 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use App\Models\ActorModel;
use App\Models\EpisodeCommentModel;
use App\Models\EpisodeModel;
use CodeIgniter\I18n\Time;
use Michalsn\Uuid\UuidEntity;
use RuntimeException;
/**
* @property string $id
* @property string $uri
* @property int $episode_id
* @property Episode|null $episode
* @property int $actor_id
* @property Actor|null $actor
* @property ?string $in_reply_to_id
* @property EpisodeComment|null $reply_to_comment
* @property string $message
* @property string $message_html
* @property int $likes_count
* @property int $replies_count
* @property Time $created_at
* @property int $created_by
*
* @property EpisodeComment[] $replies
*/
class EpisodeComment extends UuidEntity
{
protected ?Episode $episode = null;
protected ?Actor $actor = null;
protected ?EpisodeComment $reply_to_comment = null;
/**
* @var EpisodeComment[]|null
*/
protected ?array $replies = null;
protected bool $has_replies = false;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['created_at'];
/**
* @var array<string, string>
*/
protected $casts = [
'id' => 'string',
'uri' => 'string',
'episode_id' => 'integer',
'actor_id' => 'integer',
'in_reply_to_id' => '?string',
'message' => 'string',
'message_html' => 'string',
'likes_count' => 'integer',
'replies_count' => 'integer',
'created_by' => 'integer',
'is_from_post' => 'boolean',
];
public function getEpisode(): ?Episode
{
if (! $this->episode instanceof Episode) {
$this->episode = new EpisodeModel()
->getEpisodeById($this->episode_id);
}
return $this->episode;
}
/**
* Returns the comment's actor
*/
public function getActor(): ?Actor
{
if (! $this->actor instanceof Actor) {
$this->actor = model(ActorModel::class, false)
->getActorById($this->actor_id);
}
return $this->actor;
}
/**
* @return EpisodeComment[]
*/
public function getReplies(): array
{
if ($this->replies === null) {
$this->replies = new EpisodeCommentModel()
->getCommentReplies($this->id);
}
return $this->replies;
}
public function getHasReplies(): bool
{
return $this->getReplies() !== [];
}
public function getReplyToComment(): ?self
{
if ($this->in_reply_to_id === null) {
throw new RuntimeException('Comment is not a reply.');
}
if (! $this->reply_to_comment instanceof self) {
$this->reply_to_comment = model(EpisodeCommentModel::class, false)
->getCommentById($this->in_reply_to_id);
}
return $this->reply_to_comment;
}
public function setMessage(string $message): static
{
helper('fediverse');
$messageWithoutTags = strip_tags($message);
$this->attributes['message'] = $messageWithoutTags;
$this->attributes['message_html'] = str_replace("\n", '<br />', linkify($messageWithoutTags));
return $this;
}
}

232
app/Entities/Image.php Normal file
View file

@ -0,0 +1,232 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use Config\Images;
use Config\Services;
use RuntimeException;
/**
* @property File|null $file
* @property string $dirname
* @property string $filename
* @property string $extension
* @property string $mimetype
* @property string $path
* @property string $url
* @property string $thumbnail_path
* @property string $thumbnail_url
* @property string $medium_path
* @property string $medium_url
* @property string $large_path
* @property string $large_url
* @property string $feed_path
* @property string $feed_url
* @property string $id3_path
* @property string $id3_url
*/
class Image extends Entity
{
protected Images $config;
protected ?File $file = null;
protected string $dirname;
protected string $filename;
protected string $extension;
public function __construct(?File $file, string $path = '', string $mimetype = '')
{
if ($file === null && $path === '') {
throw new RuntimeException('File or path must be set to create an Image.');
}
$this->config = config('Images');
$dirname = '';
$filename = '';
$extension = '';
if ($file !== null) {
$dirname = $file->getPath();
$filename = $file->getBasename();
$extension = $file->getExtension();
$mimetype = $file->getMimeType();
}
if ($path !== '') {
[
'filename' => $filename,
'dirname' => $dirname,
'extension' => $extension,
] = pathinfo($path);
}
$this->file = $file;
$this->dirname = $dirname;
$this->filename = $filename;
$this->extension = $extension;
$this->mimetype = $mimetype;
}
public function getFile(): File
{
if ($this->file === null) {
$this->file = new File($this->path);
}
return $this->file;
}
public function getPath(): string
{
return $this->dirname . '/' . $this->filename . '.' . $this->extension;
}
public function getUrl(): string
{
helper('media');
return media_base_url($this->path);
}
public function getThumbnailPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->thumbnailSuffix .
'.' .
$this->extension;
}
public function getThumbnailUrl(): string
{
helper('media');
return media_base_url($this->thumbnail_path);
}
public function getMediumPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->mediumSuffix .
'.' .
$this->extension;
}
public function getMediumUrl(): string
{
helper('media');
return media_base_url($this->medium_path);
}
public function getLargePath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->largeSuffix .
'.' .
$this->extension;
}
public function getLargeUrl(): string
{
helper('media');
return media_base_url($this->large_path);
}
public function getFeedPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->feedSuffix .
'.' .
$this->extension;
}
public function getFeedUrl(): string
{
helper('media');
return media_base_url($this->feed_path);
}
public function getId3Path(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->id3Suffix .
'.' .
$this->extension;
}
public function getId3Url(): string
{
helper('media');
return media_base_url($this->id3_path);
}
public function saveImage(string $dirname, string $filename): void
{
helper('media');
$this->dirname = $dirname;
$this->filename = $filename;
save_media($this->file, $this->dirname, $this->filename);
$imageService = Services::image();
$thumbnailSize = $this->config->thumbnailSize;
$mediumSize = $this->config->mediumSize;
$largeSize = $this->config->largeSize;
$feedSize = $this->config->feedSize;
$id3Size = $this->config->id3Size;
$imageService
->withFile(media_path($this->path))
->resize($thumbnailSize, $thumbnailSize)
->save(media_path($this->thumbnail_path));
$imageService
->withFile(media_path($this->path))
->resize($mediumSize, $mediumSize)
->save(media_path($this->medium_path));
$imageService
->withFile(media_path($this->path))
->resize($largeSize, $largeSize)
->save(media_path($this->large_path));
$imageService
->withFile(media_path($this->path))
->resize($feedSize, $feedSize)
->save(media_path($this->feed_path));
$imageService
->withFile(media_path($this->path))
->resize($id3Size, $id3Size)
->save(media_path($this->id3_path));
}
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -22,7 +22,7 @@ class Language extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'code' => 'string', 'code' => 'string',
'native_name' => 'string', 'native_name' => 'string',
]; ];
} }

View file

@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
/**
* @copyright 2021 Ad Aures
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use Michalsn\Uuid\UuidEntity;
/**
* @property int $actor_id
* @property string $comment_id
*/
class Like extends UuidEntity
{
/**
* @var string[]
*/
protected $uuids = ['comment_id'];
/**
* @var array<string, string>
*/
protected $casts = [
'actor_id' => 'integer',
'comment_id' => 'string',
];
}

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Ad Aures * @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -11,6 +11,7 @@ declare(strict_types=1);
namespace App\Entities; namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use Config\Services;
/** /**
* @property string $url * @property string $url
@ -22,9 +23,15 @@ use CodeIgniter\Entity\Entity;
*/ */
class Location extends Entity class Location extends Entity
{ {
private const string OSM_URL = 'https://www.openstreetmap.org/'; /**
* @var string
*/
private const OSM_URL = 'https://www.openstreetmap.org/';
private const string NOMINATIM_URL = 'https://nominatim.openstreetmap.org/'; /**
* @var string
*/
private const NOMINATIM_URL = 'https://nominatim.openstreetmap.org/';
public function __construct( public function __construct(
protected string $name, protected string $name,
@ -35,18 +42,14 @@ class Location extends Entity
$longitude = null; $longitude = null;
if ($geo !== null) { if ($geo !== null) {
$geoArray = explode(',', substr($geo, 4)); $geoArray = explode(',', substr($geo, 4));
$latitude = floatval($geoArray[0]);
if (count($geoArray) === 2) { $longitude = floatval($geoArray[1]);
$latitude = (float) $geoArray[0];
$longitude = (float) $geoArray[1];
}
} }
parent::__construct([ parent::__construct([
'name' => $name, 'name' => $name,
'geo' => $geo, 'geo' => $geo,
'osm' => $osm, 'osm' => $osm,
'latitude' => $latitude, 'latitude' => $latitude,
'longitude' => $longitude, 'longitude' => $longitude,
]); ]);
} }
@ -78,7 +81,7 @@ class Location extends Entity
*/ */
public function fetchOsmLocation(): static public function fetchOsmLocation(): static
{ {
$client = service('curlrequest'); $client = Services::curlrequest();
$response = $client->request( $response = $client->request(
'GET', 'GET',
@ -89,12 +92,12 @@ class Location extends Entity
[ [
'headers' => [ 'headers' => [
'User-Agent' => 'Castopod/' . CP_VERSION, 'User-Agent' => 'Castopod/' . CP_VERSION,
'Accept' => 'application/json', 'Accept' => 'application/json',
], ],
], ],
); );
$places = json_decode((string) $response->getBody(), false, 512, JSON_THROW_ON_ERROR); $places = json_decode($response->getBody(), false, 512, JSON_THROW_ON_ERROR);
if ($places === []) { if ($places === []) {
return $this; return $this;
@ -102,16 +105,16 @@ class Location extends Entity
if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists( if (property_exists($places[0], 'lat') && $places[0]->lat !== null && (property_exists(
$places[0], $places[0],
'lon', 'lon'
) && $places[0]->lon !== null)) { ) && $places[0]->lon !== null)) {
$this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}"; $this->attributes['geo'] = "geo:{$places[0]->lat},{$places[0]->lon}";
} }
if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists( if (property_exists($places[0], 'osm_type') && $places[0]->osm_type !== null && (property_exists(
$places[0], $places[0],
'osm_id', 'osm_id'
) && $places[0]->osm_id !== null)) { ) && $places[0]->osm_id !== null)) {
$this->attributes['osm'] = strtoupper(substr((string) $places[0]->osm_type, 0, 1)) . $places[0]->osm_id; $this->attributes['osm'] = strtoupper(substr($places[0]->osm_type, 0, 1)) . $places[0]->osm_id;
} }
return $this; return $this;

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -12,12 +12,7 @@ namespace App\Entities;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use League\CommonMark\Environment\Environment; use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
/** /**
* @property int $id * @property int $id
@ -40,11 +35,11 @@ class Page extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'title' => 'string', 'title' => 'string',
'slug' => 'string', 'slug' => 'string',
'content_markdown' => 'string', 'content_markdown' => 'string',
'content_html' => 'string', 'content_html' => 'string',
]; ];
public function getLink(): string public function getLink(): string
@ -54,20 +49,13 @@ class Page extends Entity
public function setContentMarkdown(string $contentMarkdown): static public function setContentMarkdown(string $contentMarkdown): static
{ {
$config = [ $converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]; ]);
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['content_markdown'] = $contentMarkdown; $this->attributes['content_markdown'] = $contentMarkdown;
$this->attributes['content_html'] = $converter->convert($contentMarkdown); $this->attributes['content_html'] = $converter->convertToHtml($contentMarkdown);
return $this; return $this;
} }

View file

@ -3,7 +3,7 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2021 Ad Aures * @copyright 2021 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
@ -12,10 +12,6 @@ namespace App\Entities;
use App\Models\PersonModel; use App\Models\PersonModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use RuntimeException; use RuntimeException;
/** /**
@ -23,15 +19,20 @@ use RuntimeException;
* @property string $full_name * @property string $full_name
* @property string $unique_name * @property string $unique_name
* @property string|null $information_url * @property string|null $information_url
* @property ?int $avatar_id * @property Image $image
* @property ?Image $avatar * @property string $image_path
* @property string $image_mimetype
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property object[]|null $roles * @property object[]|null $roles
*/ */
class Person extends Entity class Person extends Entity
{ {
protected ?Image $avatar = null; protected Image $image;
protected ?int $podcast_id = null;
protected ?int $episode_id = null;
/** /**
* @var object[]|null * @var object[]|null
@ -42,61 +43,37 @@ class Person extends Entity
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'full_name' => 'string', 'full_name' => 'string',
'unique_name' => 'string', 'unique_name' => 'string',
'information_url' => '?string', 'information_url' => '?string',
'avatar_id' => '?int', 'image_path' => 'string',
'podcast_id' => '?integer', 'image_mimetype' => 'string',
'episode_id' => '?integer', 'podcast_id' => '?integer',
'created_by' => 'integer', 'episode_id' => '?integer',
'updated_by' => 'integer', 'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
/** /**
* Saves the person avatar in `public/media/persons/` * Saves a picture in `public/media/persons/`
*/ */
public function setAvatar(UploadedFile | File|null $file = null): static public function setImage(Image $image): static
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { helper('media');
return $this;
}
if (array_key_exists('avatar_id', $this->attributes) && $this->attributes['avatar_id'] !== null) { // Save image
$this->getAvatar() $image->saveImage('persons', $this->attributes['unique_name']);
->setFile($file);
$this->getAvatar()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getAvatar());
} else {
$avatar = new Image([
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
'sizes' => config('Images')
->personAvatarSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$avatar->setFile($file);
$this->attributes['avatar_id'] = new MediaModel('image')->saveMedia($avatar); $this->attributes['image_mimetype'] = $image->mimetype;
} $this->attributes['image_path'] = $image->path;
return $this; return $this;
} }
public function getAvatar(): ?Image public function getImage(): Image
{ {
if ($this->avatar_id === null) { return new Image(null, $this->attributes['image_path'], $this->attributes['image_mimetype']);
return null;
}
if (! $this->avatar instanceof Image) {
$this->avatar = new MediaModel('image')
->getMediaById($this->avatar_id);
}
return $this->avatar;
} }
/** /**
@ -109,12 +86,11 @@ class Person extends Entity
} }
if ($this->roles === null) { if ($this->roles === null) {
$this->roles = new PersonModel() $this->roles = (new PersonModel())->getPersonRoles(
->getPersonRoles( $this->id,
$this->id, (int) $this->attributes['podcast_id'],
(int) $this->attributes['podcast_id'], array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null
array_key_exists('episode_id', $this->attributes) ? (int) $this->attributes['episode_id'] : null, );
);
} }
return $this->roles; return $this->roles;

42
app/Entities/Platform.php Normal file
View file

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity\Entity;
/**
* @property string $slug
* @property string $type
* @property string $label
* @property string $home_url
* @property string|null $submit_url
* @property string|null $link_url
* @property string|null $link_content
* @property bool|null $is_visible
* @property bool|null $is_on_embeddable_player
*/
class Platform extends Entity
{
/**
* @var array<string, string>
*/
protected $casts = [
'slug' => 'string',
'type' => 'string',
'label' => 'string',
'home_url' => 'string',
'submit_url' => '?string',
'link_url' => '?string',
'link_content' => '?string',
'is_visible' => '?boolean',
'is_on_embeddable_player' => '?boolean',
];
}

View file

@ -3,36 +3,22 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* @copyright 2020 Ad Aures * @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
namespace App\Entities; namespace App\Entities;
use App\Models\ActorModel; use App\Libraries\SimpleRSSElement;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\PersonModel; use App\Models\PersonModel;
use App\Models\PlatformModel;
use App\Models\UserModel;
use CodeIgniter\Entity\Entity; use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\I18n\Time; use CodeIgniter\I18n\Time;
use CodeIgniter\Shield\Entities\User; use League\CommonMark\CommonMarkConverter;
use Exception;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Autolink\AutolinkExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\MarkdownConverter;
use Modules\Auth\Models\UserModel;
use Modules\Media\Entities\Image;
use Modules\Media\Models\MediaModel;
use Modules\Platforms\Entities\Platform;
use Modules\Platforms\Models\PlatformModel;
use Modules\PremiumPodcasts\Entities\Subscription;
use Modules\PremiumPodcasts\Models\SubscriptionModel;
use RuntimeException; use RuntimeException;
/** /**
@ -40,18 +26,16 @@ use RuntimeException;
* @property string $guid * @property string $guid
* @property int $actor_id * @property int $actor_id
* @property Actor|null $actor * @property Actor|null $actor
* @property string $handle * @property string $name
* @property string $at_handle
* @property string $link * @property string $link
* @property string $feed_url * @property string $feed_url
* @property string $title * @property string $title
* @property string|null $description Holds text only description, striped of any markdown or html special characters * @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown * @property string $description_markdown
* @property string $description_html * @property string $description_html
* @property int $cover_id * @property Image $image
* @property ?Image $cover * @property string $image_path
* @property int|null $banner_id * @property string $image_mimetype
* @property ?Image $banner
* @property string $language_code * @property string $language_code
* @property int $category_id * @property int $category_id
* @property Category|null $category * @property Category|null $category
@ -63,6 +47,8 @@ use RuntimeException;
* @property string $owner_email * @property string $owner_email
* @property string $type * @property string $type
* @property string|null $copyright * @property string|null $copyright
* @property string|null $episode_description_footer_markdown
* @property string|null $episode_description_footer_html
* @property bool $is_blocked * @property bool $is_blocked
* @property bool $is_completed * @property bool $is_completed
* @property bool $is_locked * @property bool $is_locked
@ -72,20 +58,21 @@ use RuntimeException;
* @property string|null $location_name * @property string|null $location_name
* @property string|null $location_geo * @property string|null $location_geo
* @property string|null $location_osm * @property string|null $location_osm
* @property bool $is_published_on_hubs * @property string|null $payment_pointer
* @property array|null $custom_rss
* @property string $custom_rss_string
* @property string|null $partner_id
* @property string|null $partner_link_url
* @property string|null $partner_image_url
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string $publication_status * @property Time $created_at;
* @property bool $is_premium_by_default * @property Time $updated_at;
* @property bool $is_premium * @property Time|null $deleted_at;
* @property Time|null $published_at
* @property Time $created_at
* @property Time $updated_at
* *
* @property Episode[] $episodes * @property Episode[] $episodes
* @property Person[] $persons * @property Person[] $persons
* @property User[] $contributors * @property User[] $contributors
* @property Subscription[] $subscriptions
* @property Platform[] $podcasting_platforms * @property Platform[] $podcasting_platforms
* @property Platform[] $social_platforms * @property Platform[] $social_platforms
* @property Platform[] $funding_platforms * @property Platform[] $funding_platforms
@ -94,13 +81,9 @@ class Podcast extends Entity
{ {
protected string $link; protected string $link;
protected string $at_handle;
protected ?Actor $actor = null; protected ?Actor $actor = null;
protected ?Image $cover = null; protected Image $image;
protected ?Image $banner = null;
protected ?string $description = null; protected ?string $description = null;
@ -112,9 +95,9 @@ class Podcast extends Entity
protected ?array $other_categories = null; protected ?array $other_categories = null;
/** /**
* @var int[] * @var string[]|null
*/ */
protected array $other_categories_ids = []; protected ?array $other_categories_ids = null;
/** /**
* @var Episode[]|null * @var Episode[]|null
@ -131,11 +114,6 @@ class Podcast extends Entity
*/ */
protected ?array $contributors = null; protected ?array $contributors = null;
/**
* @var Subscription[]|null
*/
protected ?array $subscriptions = null;
/** /**
* @var Platform[]|null * @var Platform[]|null
*/ */
@ -153,164 +131,89 @@ class Podcast extends Entity
protected ?Location $location = null; protected ?Location $location = null;
protected ?string $publication_status = null; protected string $custom_rss_string;
/**
* @var array<int, string>
* @phpstan-var list<string>
*/
protected $dates = ['published_at', 'created_at', 'updated_at'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'guid' => 'string', 'guid' => 'string',
'actor_id' => 'integer', 'actor_id' => 'integer',
'handle' => 'string', 'name' => 'string',
'title' => 'string', 'title' => 'string',
'description_markdown' => 'string', 'description_markdown' => 'string',
'description_html' => 'string', 'description_html' => 'string',
'cover_id' => 'int', 'image_path' => 'string',
'banner_id' => '?int', 'image_mimetype' => 'string',
'language_code' => 'string', 'language_code' => 'string',
'category_id' => 'integer', 'category_id' => 'integer',
'parental_advisory' => '?string', 'parental_advisory' => '?string',
'publisher' => '?string', 'publisher' => '?string',
'owner_name' => 'string', 'owner_name' => 'string',
'owner_email' => 'string', 'owner_email' => 'string',
'type' => 'string', 'type' => 'string',
'copyright' => '?string', 'copyright' => '?string',
'is_blocked' => 'boolean', 'episode_description_footer_markdown' => '?string',
'is_completed' => 'boolean', 'episode_description_footer_html' => '?string',
'is_locked' => 'boolean', 'is_blocked' => 'boolean',
'is_premium_by_default' => 'boolean', 'is_completed' => 'boolean',
'imported_feed_url' => '?string', 'is_locked' => 'boolean',
'new_feed_url' => '?string', 'imported_feed_url' => '?string',
'location_name' => '?string', 'new_feed_url' => '?string',
'location_geo' => '?string', 'location_name' => '?string',
'location_osm' => '?string', 'location_geo' => '?string',
'is_published_on_hubs' => 'boolean', 'location_osm' => '?string',
'created_by' => 'integer', 'payment_pointer' => '?string',
'updated_by' => 'integer', 'custom_rss' => '?json-array',
'partner_id' => '?string',
'partner_link_url' => '?string',
'partner_image_url' => '?string',
'created_by' => 'integer',
'updated_by' => 'integer',
]; ];
public function getAtHandle(): string public function getActor(): Actor
{
return '@' . $this->handle;
}
public function getActor(): ?Actor
{ {
if ($this->actor_id === 0) { if ($this->actor_id === 0) {
throw new RuntimeException('Podcast must have an actor_id before getting actor.'); throw new RuntimeException('Podcast must have an actor_id before getting actor.');
} }
if (! $this->actor instanceof Actor) { if ($this->actor === null) {
$this->actor = model(ActorModel::class, false) $this->actor = model('ActorModel')
->getActorById($this->actor_id); ->getActorById($this->actor_id);
} }
return $this->actor; return $this->actor;
} }
public function setCover(UploadedFile | File|null $file = null): self /**
* Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
*/
public function setImage(Image $image): static
{ {
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) { // Save image
return $this; $image->saveImage('podcasts/' . $this->attributes['name'], 'cover');
}
if (array_key_exists('cover_id', $this->attributes) && $this->attributes['cover_id'] !== null) { $this->attributes['image_mimetype'] = $image->mimetype;
$this->getCover() $this->attributes['image_path'] = $image->path;
->setFile($file);
$this->getCover()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getCover());
} else {
$cover = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
'sizes' => config('Images')
->podcastCoverSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$cover->setFile($file);
$this->attributes['cover_id'] = new MediaModel('image')->saveMedia($cover);
}
return $this; return $this;
} }
public function getCover(): Image public function getImage(): Image
{ {
if (! $this->cover instanceof Image) { return new Image(null, $this->image_path, $this->image_mimetype);
$cover = new MediaModel('image')
->getMediaById($this->cover_id);
if (! $cover instanceof Image) {
throw new Exception('Could not retrieve podcast cover.');
}
$this->cover = $cover;
}
return $this->cover;
}
public function setBanner(UploadedFile | File|null $file = null): self
{
if (! $file instanceof File || ($file instanceof UploadedFile && ! $file->isValid())) {
return $this;
}
if (array_key_exists('banner_id', $this->attributes) && $this->attributes['banner_id'] !== null) {
$this->getBanner()
->setFile($file);
$this->getBanner()
->updated_by = $this->attributes['updated_by'];
new MediaModel('image')
->updateMedia($this->getBanner());
} else {
$banner = new Image([
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
'sizes' => config('Images')
->podcastBannerSizes,
'uploaded_by' => $this->attributes['updated_by'],
'updated_by' => $this->attributes['updated_by'],
]);
$banner->setFile($file);
$this->attributes['banner_id'] = new MediaModel('image')->saveMedia($banner);
}
return $this;
}
public function getBanner(): ?Image
{
if ($this->banner_id === null) {
return null;
}
if (! $this->banner instanceof Image) {
$this->banner = new MediaModel('image')
->getMediaById($this->banner_id);
}
return $this->banner;
} }
public function getLink(): string public function getLink(): string
{ {
return url_to('podcast-activity', $this->attributes['handle']); return url_to('podcast-activity', $this->attributes['name']);
} }
public function getFeedUrl(): string public function getFeedUrl(): string
{ {
return url_to('podcast-rss-feed', $this->attributes['handle']); return url_to('podcast_feed', $this->attributes['name']);
} }
/** /**
@ -320,23 +223,17 @@ class Podcast extends Entity
*/ */
public function getEpisodes(): array public function getEpisodes(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting episodes.');
}
if ($this->episodes === null) { if ($this->episodes === null) {
$this->episodes = new EpisodeModel() $this->episodes = (new EpisodeModel())->getPodcastEpisodes($this->id, $this->type);
->getPodcastEpisodes($this->id, $this->type);
} }
return $this->episodes; return $this->episodes;
} }
/**
* Returns the podcast's episodes count
*/
public function getEpisodesCount(): int|string
{
return new EpisodeModel()
->getPodcastEpisodesCount($this->id);
}
/** /**
* Returns the podcast's persons * Returns the podcast's persons
* *
@ -344,9 +241,12 @@ class Podcast extends Entity
*/ */
public function getPersons(): array public function getPersons(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting persons.');
}
if ($this->persons === null) { if ($this->persons === null) {
$this->persons = new PersonModel() $this->persons = (new PersonModel())->getPodcastPersons($this->id);
->getPodcastPersons($this->id);
} }
return $this->persons; return $this->persons;
@ -357,29 +257,17 @@ class Podcast extends Entity
*/ */
public function getCategory(): ?Category public function getCategory(): ?Category
{ {
if (! $this->category instanceof Category) { if ($this->id === null) {
$this->category = new CategoryModel() throw new RuntimeException('Podcast must be created before getting category.');
->getCategoryById($this->category_id); }
if ($this->category === null) {
$this->category = (new CategoryModel())->getCategoryById($this->category_id);
} }
return $this->category; return $this->category;
} }
/**
* Returns all podcast subscriptions
*
* @return Subscription[]
*/
public function getSubscriptions(): array
{
if ($this->subscriptions === null) {
$this->subscriptions = new SubscriptionModel()
->getPodcastSubscriptions($this->id);
}
return $this->subscriptions;
}
/** /**
* Returns all podcast contributors * Returns all podcast contributors
* *
@ -387,9 +275,12 @@ class Podcast extends Entity
*/ */
public function getContributors(): array public function getContributors(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcasts must be created before getting contributors.');
}
if ($this->contributors === null) { if ($this->contributors === null) {
$this->contributors = new UserModel() $this->contributors = (new UserModel())->getPodcastContributors($this->id);
->getPodcastContributors($this->id);
} }
return $this->contributors; return $this->contributors;
@ -397,21 +288,41 @@ class Podcast extends Entity
public function setDescriptionMarkdown(string $descriptionMarkdown): static public function setDescriptionMarkdown(string $descriptionMarkdown): static
{ {
$config = [ $converter = new CommonMarkConverter([
'html_input' => 'escape', 'html_input' => 'strip',
'allow_unsafe_links' => false, 'allow_unsafe_links' => false,
]; ]);
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new AutolinkExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new DisallowedRawHtmlExtension());
$converter = new MarkdownConverter($environment);
$this->attributes['description_markdown'] = $descriptionMarkdown; $this->attributes['description_markdown'] = $descriptionMarkdown;
$this->attributes['description_html'] = $converter->convert($descriptionMarkdown); $this->attributes['description_html'] = $converter->convertToHtml($descriptionMarkdown);
return $this;
}
public function setEpisodeDescriptionFooterMarkdown(?string $episodeDescriptionFooterMarkdown = null): static
{
if ($episodeDescriptionFooterMarkdown === null || $episodeDescriptionFooterMarkdown === '') {
$this->attributes[
'episode_description_footer_markdown'
] = null;
$this->attributes[
'episode_description_footer_html'
] = null;
return $this;
}
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
$this->attributes[
'episode_description_footer_markdown'
] = $episodeDescriptionFooterMarkdown;
$this->attributes[
'episode_description_footer_html'
] = $converter->convertToHtml($episodeDescriptionFooterMarkdown);
return $this; return $this;
} }
@ -420,28 +331,13 @@ class Podcast extends Entity
{ {
if ($this->description === null) { if ($this->description === null) {
$this->description = trim( $this->description = trim(
(string) preg_replace('~\s+~', ' ', strip_tags((string) $this->attributes['description_html'])), (string) preg_replace('~\s+~', ' ', strip_tags($this->attributes['description_html'])),
); );
} }
return $this->description; return $this->description;
} }
public function getPublicationStatus(): string
{
if ($this->publication_status === null) {
if (! $this->published_at instanceof Time) {
$this->publication_status = 'not_published';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
$this->publication_status = 'scheduled';
}
}
return $this->publication_status;
}
/** /**
* Returns the podcast's podcasting platform links * Returns the podcast's podcasting platform links
* *
@ -449,9 +345,12 @@ class Podcast extends Entity
*/ */
public function getPodcastingPlatforms(): array public function getPodcastingPlatforms(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting podcasting platform links.');
}
if ($this->podcasting_platforms === null) { if ($this->podcasting_platforms === null) {
$this->podcasting_platforms = new PlatformModel() $this->podcasting_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'podcasting');
->getPlatforms($this->id, 'podcasting');
} }
return $this->podcasting_platforms; return $this->podcasting_platforms;
@ -464,9 +363,12 @@ class Podcast extends Entity
*/ */
public function getSocialPlatforms(): array public function getSocialPlatforms(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting social platform links.');
}
if ($this->social_platforms === null) { if ($this->social_platforms === null) {
$this->social_platforms = new PlatformModel() $this->social_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'social');
->getPlatforms($this->id, 'social');
} }
return $this->social_platforms; return $this->social_platforms;
@ -479,9 +381,12 @@ class Podcast extends Entity
*/ */
public function getFundingPlatforms(): array public function getFundingPlatforms(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting funding platform links.');
}
if ($this->funding_platforms === null) { if ($this->funding_platforms === null) {
$this->funding_platforms = new PlatformModel() $this->funding_platforms = (new PlatformModel())->getPodcastPlatforms($this->id, 'funding');
->getPlatforms($this->id, 'funding');
} }
return $this->funding_platforms; return $this->funding_platforms;
@ -492,9 +397,12 @@ class Podcast extends Entity
*/ */
public function getOtherCategories(): array public function getOtherCategories(): array
{ {
if ($this->id === null) {
throw new RuntimeException('Podcast must be created before getting other categories.');
}
if ($this->other_categories === null) { if ($this->other_categories === null) {
$this->other_categories = new CategoryModel() $this->other_categories = (new CategoryModel())->getPodcastCategories($this->id);
->getPodcastCategories($this->id);
} }
return $this->other_categories; return $this->other_categories;
@ -505,7 +413,7 @@ class Podcast extends Entity
*/ */
public function getOtherCategoriesIds(): array public function getOtherCategoriesIds(): array
{ {
if ($this->other_categories_ids === []) { if ($this->other_categories_ids === null) {
$this->other_categories_ids = array_column($this->getOtherCategories(), 'id'); $this->other_categories_ids = array_column($this->getOtherCategories(), 'id');
} }
@ -517,7 +425,7 @@ class Podcast extends Entity
*/ */
public function setLocation(?Location $location = null): static public function setLocation(?Location $location = null): static
{ {
if (! $location instanceof Location) { if ($location === null) {
$this->attributes['location_name'] = null; $this->attributes['location_name'] = null;
$this->attributes['location_geo'] = null; $this->attributes['location_geo'] = null;
$this->attributes['location_osm'] = null; $this->attributes['location_osm'] = null;
@ -545,17 +453,58 @@ class Podcast extends Entity
return null; return null;
} }
if (! $this->location instanceof Location) { if ($this->location === null) {
$this->location = new Location($this->location_name, $this->location_geo, $this->location_osm); $this->location = new Location($this->location_name, $this->location_geo, $this->location_osm);
} }
return $this->location; return $this->location;
} }
public function getIsPremium(): bool /**
* Get custom rss tag as XML String
*/
public function getCustomRssString(): string
{ {
// podcast is premium if at least one of its episodes is set as premium if ($this->attributes['custom_rss'] === null) {
return new EpisodeModel() return '';
->doesPodcastHavePremiumEpisodes($this->id); }
helper('rss');
$xmlNode = (new SimpleRSSElement(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"/>',
))->addChild('channel');
array_to_rss([
'elements' => $this->custom_rss,
], $xmlNode);
return str_replace(['<channel>', '</channel>'], '', $xmlNode->asXML());
}
/**
* Saves custom rss tag into json
*/
public function setCustomRssString(string $customRssString): static
{
if ($customRssString === '') {
return $this;
}
helper('rss');
$customRssArray = rss_to_array(
simplexml_load_string(
'<?xml version="1.0" encoding="utf-8"?><rss xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:podcast="https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md" xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0"><channel>' .
$customRssString .
'</channel></rss>',
),
)['elements'][0];
if (array_key_exists('elements', $customRssArray)) {
$this->attributes['custom_rss'] = json_encode($customRssArray['elements']);
} else {
$this->attributes['custom_rss'] = null;
}
return $this;
} }
} }

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