diff --git a/.devcontainer/.htaccess b/.devcontainer/.htaccess new file mode 100644 index 00000000000..b66e8088296 --- /dev/null +++ b/.devcontainer/.htaccess @@ -0,0 +1 @@ +Require all denied diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000000..1d42e95a131 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,38 @@ +FROM alpine:3.24 + +ENV TZ=UTC +SHELL ["/bin/ash", "-eo", "pipefail", "-c"] + +RUN apk add --no-cache \ + tzdata \ + apache2 php-apache2 \ + php php-curl php-gmp php-intl php-mbstring php-xml php-zip \ + php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \ + php-pdo_sqlite php-pdo_mysql php-pdo_pgsql \ + bash composer curl docker-cli-buildx git gpg make nodejs npm shellcheck shfmt sudo + +RUN \ + # Disable unwanted Apache modules and configurations + rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \ + /etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \ + sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \ + /etc/apache2/httpd.conf && \ + sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \ + /etc/apache2/httpd.conf && \ + # Enable required Apache modules + sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|filter|headers|mime|remoteip|setenvif).so$/s/^\s*#//" \ + /etc/apache2/httpd.conf + +RUN adduser --ingroup www-data --disabled-password developer && \ + echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer + +ENV COPY_LOG_TO_SYSLOG=On +ENV COPY_SYSLOG_TO_STDERR=On +ENV CRON_MIN='' +ENV DATA_PATH='' +ENV ENABLE_ACCESS_LOG=1 +ENV FRESHRSS_ENV='development' +ENV LISTEN='0.0.0.0:8080' +ENV TRUSTED_PROXY=0 + +EXPOSE 8080 diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000000..358976f8a61 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,24 @@ +# Dev Container for FreshRSS + +This is a [Development Container](https://containers.dev) to provide a one-click full development environment +with all the needed tools and configurations, to develop and test [FreshRSS](https://github.com/FreshRSS/FreshRSS/). + +It can be used on your local machine (see for instance the [Dev Containers extension for Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)), +or as [GitHub Codespaces](https://github.com/features/codespaces) simply in a Web browser: + +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699) + +## Test instance of FreshRSS + +A test instance of FreshRSS is automatically started as visible from the *Ports* tab: check the *Local Address* column, and click on the *Open in browser* 🌐 icon. +It runs the FreshRSS code that you are currently editing. + +Apache logs can be seen in `/var/log/apache2/access.log` and `/var/log/apache2/error.log`. + +## Software tests + +Running the tests can be done directly from the built-in terminal, e.g.: + +```sh +make test-all +``` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..615d817417c --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,44 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "FreshRSS-dev-Alpine", + "build": { + "dockerfile": "Dockerfile" + }, + "containerEnv": { + "DATA_PATH": "/home/developer/freshrss-data" + }, + "customizations": { + "codespaces": { + "openFiles": [ + ".devcontainer/README.md" + ] + }, + "vscode": { + "extensions": [ + "bmewburn.vscode-intelephense-client", + "DavidAnson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "EditorConfig.EditorConfig", + "foxundermoon.shell-format", + "GitHub.vscode-pull-request-github", + "mrmlnc.vscode-apache", + "ms-azuretools.vscode-docker", + "redhat.vscode-yaml", + "timonwong.shellcheck", + "ValeryanM.vscode-phpsab" + ] + } + }, + "forwardPorts": [ + 8080 + ], + "portsAttributes": { + "8080": { + "label": "FreshRSS Apache", + "onAutoForward": "notify" + } + }, + "remoteUser": "developer", + "postCreateCommand": "sudo .devcontainer/postCreateCommand.sh" +} diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100755 index 00000000000..0fe74051e92 --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +ln -s "$(pwd)" /var/www/FreshRSS + +cp ./Docker/*.Apache.conf /etc/apache2/conf.d/ + +./Docker/entrypoint.sh + +chown -R developer:www-data /home/developer/freshrss-data +chmod -R g+rwX /home/developer/freshrss-data + +httpd -c 'ErrorLog "/var/log/apache2/error.log"' -c 'CustomLog "/var/log/apache2/access.log" combined_proxy' diff --git a/.dockerignore b/.dockerignore index 9f71b21bb9a..599951f8259 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,11 @@ +/.devcontainer/ /.git/ +/.github/ /bin/ /data/ /docs/ +/extensions/node_modules/ +/extensions/vendor/ /node_modules/ +/tests/**/*.sqlite /vendor/ diff --git a/.editorconfig b/.editorconfig index bcb7b4666aa..761ad915b4a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,7 @@ end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true -[*.{css,scss}] +[*.css] indent_style = tab [*.{html,php,phtml}] @@ -15,10 +15,22 @@ indent_style = tab [*.js] indent_style = tab +[*.{json,jsonc}] +indent_style = tab + [*.md] indent_size = 4 indent_style = tab +[*.neon] +indent_style = tab + +[*.sh] +indent_style = tab + +[*.svg] +indent_style = tab + [*.xml] indent_style = tab diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 4af5f2ccfce..00000000000 --- a/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -*.min.js -node_modules/ -p/scripts/vendor/ diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index a528c8f5969..00000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "env": { - "browser": true - }, - "extends": [ - "eslint:recommended", - "standard" - ], - "rules": { - "camelcase": "off", - "comma-dangle": ["warn", "always-multiline"], - "eqeqeq": "off", - "indent": ["warn", "tab", { "SwitchCase": 1 }], - "linebreak-style": ["error", "unix"], - "max-len": ["warn", 165], - "no-tabs": "off", - "semi": ["warn", "always"], - "space-before-function-paren": ["warn", { - "anonymous": "always", - "named": "never", - "asyncArrow": "always" - }], - "yoda": "off" - }, - "root": true -} diff --git a/.gitattributes b/.gitattributes index 9002d5babd7..48cea675016 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,5 @@ *.png -text *.waff -text *.waff2 -text + +/.github/ export-ignore diff --git a/.github/.htaccess b/.github/.htaccess new file mode 100644 index 00000000000..b66e8088296 --- /dev/null +++ b/.github/.htaccess @@ -0,0 +1 @@ +Require all denied diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 387be35430b..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: "[BUG]" -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment information (please complete the following information):** -- Device: [e.g. iPhone6] -- OS: [e.g. Ubuntu 20.04] -- Browser: [e.g. Firefox 86] -- FreshRSS version: [e.g. 1.17.1] -- Database version: [e.g. Mysql 5.7] -- PHP version: [e.g. PHP 7.4] -- Installation type: [e.g. Yunohost] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report_form.yaml b/.github/ISSUE_TEMPLATE/bug_report_form.yaml new file mode 100644 index 00000000000..2bd74439001 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report_form.yaml @@ -0,0 +1,56 @@ +name: Bug Report +description: Create a report to help us improve +type: Bug +title: "[Bug] " +labels: ["Bug (unconfirmed)"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report to help FreshRSS improve! + - type: textarea + id: description + attributes: + label: Describe the bug + description: "A clear and concise description of what the bug is.\n\nIf applicable, add screenshots to help explain your problem." + validations: + required: true + - type: textarea + id: reproduce + attributes: + label: To Reproduce + description: Steps to reproduce the behavior. (Screenshots could help to explain the steps.) + placeholder: "1. Go to '…'\n2. Click on '…'\n3. Scroll down to '…'\n4. See error" + value: "1. Go to '…'\n2. Click on '…'\n3. Scroll down to '…'\n4. See error" + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + validations: + required: false + - type: input + id: freshRSS + attributes: + label: FreshRSS version + description: Which FreshRSS version is installed? + placeholder: e.g. 1.23.1 + validations: + required: true + - type: textarea + id: environment + attributes: + label: System information + description: Please complete the following information + value: "- Database version: [e.g. SQLite, PostgreSQL 17]\n- PHP version: [e.g. PHP 8.2]\n- Installation type: [e.g. Docker, Docker image source, git, Yunohost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 24.04, Win11, MacOS14]\n- Browser: [e.g. Firefox 134]" + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index f9acdb30bec..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[Feature]" -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 00000000000..d6599b7fdfb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,41 @@ +name: Feature Request +description: Suggest an idea for FreshRSS +type: Feature +title: "[Feature] " +labels: ["Feature Request"] +body: + - type: textarea + id: description + attributes: + label: Is your feature request related to a problem? Please describe. + description: "A clear and concise description of what the problem is. Ex. I’m always frustrated when […]" + validations: + required: true + - type: textarea + id: usecase + attributes: + label: Use case + description: "A clear and concise description how you work with FreshRSS. Please support with examples." + validations: + required: false + - type: textarea + id: solution + attributes: + label: Describe the solution you’d like + description: "A clear and concise description of what you want to happen." + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Describe alternatives you’ve considered + description: "A clear and concise description of any alternative solutions or features you’ve considered." + validations: + required: false + - type: textarea + id: context + attributes: + label: Additional context + description: "Add any other context or screenshots about the feature request here." + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feed-report-form.yml b/.github/ISSUE_TEMPLATE/feed-report-form.yml new file mode 100644 index 00000000000..65b88018dfc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feed-report-form.yml @@ -0,0 +1,76 @@ +name: Feed Report +description: "A feed does not work? Type of feed: RSS / Atom, JSON Feed, JSON (dot notation). " +title: "[Feed problem] " +labels: ["Feed problem 🗞️"] +body: + - type: markdown + attributes: + value: | + If you're experiencing issues with a particular feed (RSS, Atom, JSON), please use this form to report the problem. There can be various reasons why a feed may not be working. By sharing as much information as possible, we can better determine the cause and address it. + - type: input + id: feedurl + attributes: + label: Feed URL + description: Share the URL of the feed + placeholder: https://… + validations: + required: true + - type: textarea + id: description + attributes: + label: Describe the problem + description: "A clear and concise description of what the problem is.\n\nIf applicable, add screenshots to help explain the feed problem." + validations: + required: true + - type: textarea + id: log + attributes: + label: Log messages + description: What is written in the log (see administration ./i/?a=logs) + validations: + required: true + - type: textarea + id: w3cfeedvalidation + attributes: + label: W3C Validator + description: "What is the result of the W3C validator service (go to https://validator.w3.org/feed/)?\n\nIf applicable, add the full link and screenshots to help explain the feed problem." + validations: + required: true + - type: input + id: freshRSS + attributes: + label: FreshRSS version + description: Which FreshRSS version is installed? + placeholder: e.g. 1.23.1 + validations: + required: true + - type: textarea + id: environment + attributes: + label: System information + description: Please complete the following information + value: "Database version: [e.g. Mysql 5.7, SQLite]\n- PHP version: [e.g. PHP 8.1]\n- Installation type: [e.g. Docker, Docker image source, git, Yunohost]\n- Web server type: [e.g. Apache, nginx]\n- Device: [e.g. iPhone13]\n- OS: [e.g. Ubuntu 22.04, Win10, MacOS14]\n- Browser: [e.g. Firefox 124]" + validations: + required: true + - type: textarea + id: context + attributes: + label: Additional context + description: Add any other context about the problem here. + validations: + required: false + - type: textarea + id: curlcommands + attributes: + label: Curl commands + description: Have you tried some Curl commands? (See https://github.com/FreshRSS/FreshRSS/discussions/6287 for more information) + validations: + required: false + - type: textarea + id: useragents + attributes: + label: User agents + description: Have you tried some browser user agents in feed settings "Advanced" section? + validations: + required: false + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..380cfdf7868 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,29 @@ +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "monthly" + groups: + eslint: + patterns: + - "*eslint*" + - "globals" + - "neostandard" + stylelint: + patterns: + - "*stylelint*" + - package-ecosystem: "composer" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "composer" + directory: "/lib/" + schedule: + interval: "monthly" diff --git a/.github/instructions/_general.instructions.md b/.github/instructions/_general.instructions.md new file mode 100644 index 00000000000..e0773ba162e --- /dev/null +++ b/.github/instructions/_general.instructions.md @@ -0,0 +1,23 @@ +--- +applyTo: "**" +description: General instructions +--- + +# FreshRSS General instructions + +## KISS principle: Keep It Simple + +* Prioritise simplicity, brevity, and clarity in code and documentation. +* Avoid premature abstractions or over-engineering. +* Reuse and adapt existing code where possible, avoiding duplication. +* Keep changes minimal and focused. +* Favour vanilla solutions: avoid adding third-party dependencies and frameworks. +* Overall, reduce the number of lines and quantity of code (including dependencies). + +## Style and formatting + +* When editing files, obey indentation and white-space as defined in [`.editorconfig`](../../.editorconfig) (some of it can be automatically fixed with `make fix-all`). + +## Spelling + +* Favour British English spelling and conventions in code, comments, and documentation. diff --git a/.github/instructions/css.instructions.md b/.github/instructions/css.instructions.md new file mode 100644 index 00000000000..0370cc5be63 --- /dev/null +++ b/.github/instructions/css.instructions.md @@ -0,0 +1,31 @@ +--- +applyTo: "**/*.css" +description: Editing CSS files +--- + +# CSS files + +* Obey formatting rules defined in [`.stylelintrc.json`](../../.stylelintrc.json) +* Automatic fixes can be done with: + ```sh + npm run stylelint_fix + # or (targeting more than just CSS) + make fix-all + ``` +* Validation can be done with: + ```sh + npm run stylelint + # or (targeting more than just CSS) + make test-all + ``` + +## Right-to-left CSS files + +* Do not edit RTL CSS `*.rtl.css` files manually. +* RTL CSS files are auto-generated from LTR CSS files using the `rtlcss` tool. +* Run the following command to regenerate the RTL files: + ```sh + npm run rtlcss + # or + make rtl + ``` diff --git a/.github/instructions/i18n.instructions.md b/.github/instructions/i18n.instructions.md new file mode 100644 index 00000000000..cc0885352c4 --- /dev/null +++ b/.github/instructions/i18n.instructions.md @@ -0,0 +1,55 @@ +--- +applyTo: "app/i18n/*/*.php" +description: Editing FreshRSS translations +--- + +# FreshRSS translations (i18n) + +* Favour commands over manual edits to add, move, or delete translation keys: + * Use the `cli/manipulate.translation.php` script or the corresponding `make` commands. + +## Commands + +### Add a new translation key + +To add a new translatable string across all languages: + +```sh +./cli/manipulate.translation.php --action add --key 'example.of.key' --value 'Example of text' +# or +make i18n-add-key key='example.of.key' value='Example of text' +``` + +This will: +1. Add the key to English (`app/i18n/en/gen.php`) with the provided value +2. Add the key to all other languages marked as `// TODO` for translators + +You should then: +1. Remove the `// TODO` comment in English `en/` +2. Update to `// IGNORE` comment in US English `en-US/` or adapt the string in case of regional differences + +### Delete a translation key + +To delete a translation key from all languages: + +```sh +./cli/manipulate.translation.php --action delete --key 'old.key.path' +``` + +### Other commands + +See all available commands with: + +```sh +./cli/manipulate.translation.php -a --help +``` + +## Post-fixing + +After manipulating translation, you should call the following to reformat and ensure consistency: + +```sh +composer run-script translations +# or (targeting more than just translations) +make fix-all +``` diff --git a/.github/instructions/javascript.instructions.md b/.github/instructions/javascript.instructions.md new file mode 100644 index 00000000000..d48463232d0 --- /dev/null +++ b/.github/instructions/javascript.instructions.md @@ -0,0 +1,21 @@ +--- +applyTo: "**/*.js" +description: Editing JavaScript files +--- + +# JavaScript files + +* Obey formatting rules defined in [`eslint.config.js`](../../eslint.config.js) +* Automatic fixes can be done with: + ```sh + npm run eslint_fix + # or (targeting more than just JavaScript) + make fix-all + ``` +* Validation can be done with: + ```sh + npm run eslint + # or (targeting more than just JavaScript) + make test-all + ``` +* Check [`package.json`](../../package.json) scripts for details about available individual commands. diff --git a/.github/instructions/markdown.instructions.md b/.github/instructions/markdown.instructions.md new file mode 100644 index 00000000000..a3e265ffb88 --- /dev/null +++ b/.github/instructions/markdown.instructions.md @@ -0,0 +1,19 @@ +--- +applyTo: "**/*.md" +description: Editing Markdown +--- + +# Markdown files + +* Obey formatting rules defined in [`.markdownlint-cli2.jsonc`](../../.markdownlint-cli2.jsonc) +* For fenced code blocks: + * Use language identifiers as much as possible, e.g., `php`, `js`, `css` + * Favour `sh` over `bash` +* Automatic fixes can be done with: + ```sh + npm run markdownlint_fix + ``` +* Validation can be done with: + ```sh + npm run markdownlint + ``` diff --git a/.github/instructions/php.instructions.md b/.github/instructions/php.instructions.md new file mode 100644 index 00000000000..3302c3768e5 --- /dev/null +++ b/.github/instructions/php.instructions.md @@ -0,0 +1,37 @@ +--- +applyTo: "**/*.php" +description: Editing PHP files +--- + +# PHP files + +* See minimum PHP version and available PHP extensions in [`composer.json`](../../composer.json) +* Obey formatting rules defined in [`phpcs.xml`](../../phpcs.xml) +* Automatic fixes can be done with: + ```sh + composer run-script fix + # or (targeting more than just PHP) + make fix-all + ``` +* Validation can be done with: + ```sh + composer test + # or (targeting more than just PHP) + make test-all + ``` +* Check [`composer.json`](../../composer.json) scripts for details about available individual commands. +* For instance, running a single unit test can be done with: + ```sh + composer run-script phpunit -- tests/app/Models/SearchTest.php + ``` + +## Autoloader + +* `spl_autoload_register` is defined in [`lib/lib_rss.php`](../../lib/lib_rss.php) + +Minimal example: + +```php +require dirname(__DIR__) . '/constants.php'; +require LIB_PATH . '/lib_rss.php'; //Includes class autoloader +``` diff --git a/.github/skills/freshrss-git/SKILL.md b/.github/skills/freshrss-git/SKILL.md new file mode 100644 index 00000000000..88e6e83bb3a --- /dev/null +++ b/.github/skills/freshrss-git/SKILL.md @@ -0,0 +1,27 @@ +--- +name: freshrss-git +description: FreshRSS version control, git workflow, branching strategy (edge/latest), diff/compare, PR changes. +allowed-tools: Bash(git:*) Read Grep +--- + +# FreshRSS working with version control (git) + +FreshRSS uses a rolling-release approach with a main branch called `:edge`. +This is the branch to target for most pull requests. + +Releases are tracked in a branch called `:latest`. + +## Check changes made in the current branch + +Also useful to locally check the state of a pull-request (PR). + +```sh +git diff edge...HEAD +``` + +## Before commiting + +```sh +make fix-all +make test-all +``` diff --git a/.github/skills/freshrss-i18n/SKILL.md b/.github/skills/freshrss-i18n/SKILL.md new file mode 100644 index 00000000000..d31b8aef451 --- /dev/null +++ b/.github/skills/freshrss-i18n/SKILL.md @@ -0,0 +1,37 @@ +--- +name: freshrss-i18n +description: Add, move, or format translation strings in FreshRSS. Use when modifying UI text that needs translation (i18n). Handles all supported languages automatically. +allowed-tools: Bash(php:*) Bash(composer:*) Bash(make:*) Read Grep +--- + +# FreshRSS translation management (i18n) + +For instructions and commands to work with translations, make sure to obey [i18n.instructions.md](../../instructions/i18n.instructions.md). + +Translations strings are in `app/i18n/{lang}/` as PHP arrays, used with: + +```php +_t('key.subkey') +``` + +## When to use this skill + +- Adding new user-facing text to the application +- Moving/renaming or deleting existing translation keys +- Adding a new translation file for a new feature area +- Formatting translation files after manual edits + +## Workflow example + +When adding a new UI element: + +1. Identify the appropriate i18n file and section, in proximity of existing strings, avoiding duplication +2. Add a key if an appropriate string does not already exist, using `cli/manipulate.translation.php` or the corresponding `make` commands as defined in the [instructions](../../instructions/i18n.instructions.md). +3. Use in the code: + ```php + + ``` + +## Machine translation + +**Note:** Do not complete translations unless explicitly instructed by the user. Ask for approval and point out that unless the user intends to review the result, translations may be inaccurate. Translate only as a last resort, and only if the user approves. diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 00000000000..4ec9c0caaf4 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,98 @@ +name: Publish Docker images + +on: + push: + branches: + - edge + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-container-image: + name: Build Docker image ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: Debian + file: Docker/Dockerfile + flavor: | + latest=auto + tags: | + type=edge,onlatest=false + type=semver,pattern={{version}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/0.') }} + # type=semver,pattern={{major}}.{{minor}} + - name: Alpine + file: Docker/Dockerfile-Alpine + flavor: | + latest=false + tags: | + type=raw,value=alpine,enable=${{ github.ref == 'refs/heads/latest' || startsWith(github.ref, 'refs/tags/') }} + type=edge,suffix=-alpine,onlatest=false + type=semver,pattern={{version}}-alpine + type=semver,pattern={{major}}-alpine,enable=${{ !startsWith(github.ref, 'refs/tags/0.') }} + # type=semver,pattern={{major}}.{{minor}}-alpine + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0 + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Get FreshRSS version + run: | + FRESHRSS_VERSION=$(sed -n "s/^const FRESHRSS_VERSION = '\(.*\)'.*$/\1/p" constants.php) + echo "$FRESHRSS_VERSION" + echo "FRESHRSS_VERSION=$FRESHRSS_VERSION" >> $GITHUB_ENV + + - name: Add metadata to Docker images + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + flavor: ${{ matrix.flavor }} + images: | + docker.io/freshrss/freshrss + ghcr.io/${{ github.repository }} + tags: ${{ matrix.tags }} + labels: | + org.opencontainers.image.url=https://freshrss.org/ + org.opencontainers.image.version=${{ env.FRESHRSS_VERSION }} + + - name: Login to Docker Hub + if: github.repository_owner == 'FreshRSS' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + if: github.repository_owner == 'FreshRSS' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker images + uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 + with: + context: . # Switching from git context to path context, needed for .dockerignore to be applied. See: https://github.com/docker/build-push-action#git-context + file: ${{ matrix.file }} + platforms: linux/amd64,linux/arm/v7,linux/arm64 + build-args: | + FRESHRSS_VERSION=${{ env.FRESHRSS_VERSION }} + SOURCE_COMMIT=${{ github.sha }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + push: ${{ (github.ref == 'refs/heads/latest' || github.ref == 'refs/heads/edge' || startsWith(github.ref, 'refs/tags/')) && github.repository_owner == 'FreshRSS' }} + provenance: mode=max + sbom: true diff --git a/.github/workflows/dockerhub-description.yml b/.github/workflows/dockerhub-description.yml new file mode 100644 index 00000000000..ced6383d04d --- /dev/null +++ b/.github/workflows/dockerhub-description.yml @@ -0,0 +1,27 @@ +name: Update Docker Hub description + +on: + push: + paths: + - Docker/README.md + branches: + - edge + workflow_dispatch: + +permissions: + contents: read + +jobs: + dockerhub-description: + if: github.repository_owner == 'FreshRSS' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Update repo description + uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: freshrss/freshrss + readme-filepath: Docker/README.md diff --git a/.github/workflows/jekyll-gh-pages.yml b/.github/workflows/jekyll-gh-pages.yml new file mode 100644 index 00000000000..984d35bea18 --- /dev/null +++ b/.github/workflows/jekyll-gh-pages.yml @@ -0,0 +1,59 @@ +# Workflow for building and deploying a Jekyll site to GitHub Pages +name: Deploy Jekyll with GitHub Pages dependencies preinstalled + +on: + # Runs on pushes targeting the default branch + push: + branches: ["edge"] + paths: + - 'docs/**' + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + # Build job + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Setup Ruby + uses: ruby/setup-ruby@95ef2b042f9d7a56d8268cba8559e2842e2ad01b + with: + ruby-version: '3.4.9' + bundler-cache: true # runs 'bundle install' and caches installed gems automatically + cache-version: 1 # Increment this number if you need to re-download cached gems + working-directory: docs + - name: Setup Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + - name: Build with Jekyll + run: | + cd docs + bundle exec jekyll build --destination ../_site + - name: Upload artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml deleted file mode 100644 index 0c6c5bdbd91..00000000000 --- a/.github/workflows/latest.yml +++ /dev/null @@ -1,12 +0,0 @@ -name: Add latest tag to new release -on: - release: - types: [published] - -jobs: - run: - name: Run local action - runs-on: ubuntu-latest - steps: - - name: Run latest-tag - uses: EndBug/latest-tag@6d22a6738f5c33059e3a8c6ca5dcf8eaf8a14599 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index daa91465d74..857c657d3e5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,100 +6,118 @@ on: pull_request: branches: [ edge ] +permissions: + contents: read + jobs: - composer: + tests: # https://github.com/actions/virtual-environments - runs-on: ubuntu-latest + runs-on: ubuntu-26.04 steps: - - uses: actions/checkout@v2 + - name: Git checkout source code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # Composer tests - name: Check PHP syntax - run: find . -name '*.php' -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null + run: composer run-script php-lint - name: Check PHTML syntax - run: find . -name '*.phtml' -print0 | xargs -0 -n1 -P4 php -l 1>/dev/null + run: composer run-script phtml-lint - name: Check translations syntax - run: cli/manipulate.translation.php -a format && git diff --exit-code + run: composer run-script translations && git diff --exit-code - name: Use Composer cache id: composer-cache - uses: actions/cache@v2 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: vendor key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} restore-keys: | ${{ runner.os }}-php- - - run: composer install --prefer-dist --no-progress + - name: Run Composer install + run: composer install --prefer-dist --no-progress if: steps.composer-cache.outputs.cache-hit != 'true' - - name: Run PHP tests - run: composer run-script test + - name: Run PHP unit tests + run: composer run-script phpunit -- --no-progress + - name: PHP_CodeSniffer + run: composer run-script phpcs - npm: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + - name: PHPStan + run: composer run-script phpstan -- --no-progress + + # - name: PHPStan Next + # run: composer run-script phpstan-next -- --no-progress + + # NPM tests - name: Uses Node.js - uses: actions/setup-node@v2 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - # https://nodejs.org/en/about/releases/ - node-version: '14' - cache: 'npm' + # https://nodejs.org/en/about/previous-releases + node-version: lts/* + cache: npm - - run: npm install + - run: npm ci - name: Check JavaScript syntax - run: npm run eslint + run: npm run --silent eslint - name: Check Markdown syntax - run: npm run markdownlint - - - name: Check Right-to-left CSS - run: npm run rtlcss && git diff --exit-code + run: npm run --silent markdownlint - name: Check CSS syntax - run: npm run stylelint + run: npm run --silent stylelint + - name: Check Right-to-left CSS + run: npm run --silent rtlcss && git diff --exit-code - shell: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 + # Shell tests - name: Use shell cache id: shell-cache - uses: actions/cache@v2 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: bin - key: ${{ runner.os }}-bin-shfmt@v3.4.0c-hadolint@v2.7.0 + key: ${{ runner.os }}-bin-shfmt@v3.8.0-hadolint@v2.15.1-typos@v1.49.0 - name: Add ./bin/ to $PATH run: mkdir -p bin/ && echo "${PWD}/bin" >> $GITHUB_PATH - - name: Setup Go - # Multiple Go versions are pre-installed but the default 1.15 is too old - # https://github.com/actions/setup-go - uses: actions/setup-go@v2 - with: - go-version: '1.17' - - name: Install shfmt if: steps.shell-cache.outputs.cache-hit != 'true' - run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.4.0 + run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.8.0 - name: Check shell script syntax - # shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md + # shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2204-Readme.md run: ./tests/shellchecks.sh - name: Install hadolint if: steps.shell-cache.outputs.cache-hit != 'true' - run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.7.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint + run: | + curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.15.1/hadolint-linux-x86_64" && + echo 'c7187db94eeeeca956519a6af171adc31453941a1e777961f6e680f697c8c507 ./bin/hadolint' | sha256sum -c - && + chmod 700 ./bin/hadolint - name: Check Dockerfile syntax run: find . -name 'Dockerfile*' -print0 | xargs -0 -n1 ./bin/hadolint --failure-threshold warning + + - name: Install typos + if: steps.shell-cache.outputs.cache-hit != 'true' + run: | + cd bin ; + wget -q 'https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-x86_64-unknown-linux-musl.tar.gz' && + echo '48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10 typos-v1.49.0-x86_64-unknown-linux-musl.tar.gz' | sha256sum -c - && + tar -xvf *.tar.gz './typos' && + chmod +x typos && + rm *.tar.gz ; + cd .. + + - name: Check spelling + run: bin/typos diff --git a/.gitignore b/.gitignore index 00005eeff6d..b06b69ea7f7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,22 @@ +/.htaccess /bin/ +/constants.local.php +/data.back/ +/extensions/node_modules/ +/extensions/vendor/ /node_modules/ +/phpstan.neon /vendor/ -/constants.local.php + +.vscode/ + +/docs/_site/ +/docs/.sass-cache/ +/docs/.bundle/ +/docs/.jekyll-metadata + +# OS files +.DS_Store # Temp files *~ @@ -17,5 +32,3 @@ *.save *.autosav *.autosave -*.sass-cache -sass.sh diff --git a/.htaccess.dist b/.htaccess.dist new file mode 100644 index 00000000000..f1b518c4a9f --- /dev/null +++ b/.htaccess.dist @@ -0,0 +1,12 @@ +# Copy this file to `.htaccess` for additional root-level protection +# if you cannot set Apache `DocumentRoot` to `./p/` as recommended. + +# Deny files starting with a dot or without extension or with specific extensions + + Require all denied + + +# Deny some sub-folders, which may not be excluded by their own .htaccess + + Require all denied + diff --git a/.jshintignore b/.jshintignore index b8a497857c2..987a6d82e67 100644 --- a/.jshintignore +++ b/.jshintignore @@ -1,3 +1,5 @@ -node_modules -p/scripts/bcrypt.min.js +.git/ +extensions/ +node_modules/ p/scripts/vendor/ +vendor/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc new file mode 100644 index 00000000000..c8036086309 --- /dev/null +++ b/.markdownlint-cli2.jsonc @@ -0,0 +1,36 @@ +{ + "config": { + "default": true, + "blanks-around-fences": false, + "blanks-around-lists": false, + "first-line-heading": false, + "line-length": false, + "no-hard-tabs": false, + "no-inline-html": { + "allowed_elements": ["br", "img", "kbd", "translations", "meta"] + }, + "no-multiple-blanks": { + "maximum": 2 + }, + "no-trailing-spaces": true, + "table-column-style": false, + "ul-indent": false, + "ul-style": { + "style": "consistent" + } + }, + // Markdown files to lint (previously the '**/*.md' CLI argument) + "globs": [ + "**/*.md" + ], + "ignores": [ + "**/node_modules/**", + "extensions/**", + "lib/marienfressinaud/**", + "lib/phpgt/**", + "lib/phpmailer/**", + "lib/simplepie/**", + "p/scripts/vendor/**", + "vendor/**" + ] +} diff --git a/.markdownlint.json b/.markdownlint.json deleted file mode 100644 index c6961a7d6b8..00000000000 --- a/.markdownlint.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "default": true, - "blanks-around-fences": false, - "blanks-around-lists": false, - "first-line-heading": false, - "line-length": false, - "no-hard-tabs": false, - "no-inline-html": { - "allowed_elements": ["br", "kbd"] - }, - "no-multiple-blanks": { - "maximum": 2 - }, - "ul-indent": false, - "ul-style": { - "style": "consistent" - } -} diff --git a/.markdownlintignore b/.markdownlintignore deleted file mode 100644 index c2658d7d1b3..00000000000 --- a/.markdownlintignore +++ /dev/null @@ -1 +0,0 @@ -node_modules/ diff --git a/.stylelintignore b/.stylelintignore index 402ba0702a9..97ef1059ace 100644 --- a/.stylelintignore +++ b/.stylelintignore @@ -1,6 +1,10 @@ -# ignore SASS-generated CSS -p/themes/Ansum/*.css -p/themes/Mapco/*.css -p/themes/Swage/*.css +.git/ +extensions/ +node_modules/ +p/scripts/vendor/ +vendor/ # Ignore RTLCSS auto-generated CSS *.rtl.css + +# actual location is docs/_includes/docs.css +docs/assets/css/docs.css diff --git a/.stylelintrc.json b/.stylelintrc.json index b14138d3ad7..bbb7a81fc26 100644 --- a/.stylelintrc.json +++ b/.stylelintrc.json @@ -1,72 +1,67 @@ { - "extends": "stylelint-config-recommended-scss", - "plugins": [ - "stylelint-order", - "stylelint-scss" - ], - "rules": { - "at-rule-empty-line-before": [ - "always", { - "ignoreAtRules": [ "after-comment", "else" ] - } - ], - "at-rule-name-space-after": [ - "always", { - "ignoreAtRules": [ "after-comment" ] - } - ], - "block-closing-brace-newline-after": [ - "always", { - "ignoreAtRules": [ "if", "else" ] - } - ], - "block-closing-brace-newline-before": "always-multi-line", - "block-opening-brace-newline-after": "always-multi-line", - "block-opening-brace-space-before": "always", - "color-hex-case": "lower", - "color-hex-length": "short", - "color-no-invalid-hex": true, - "declaration-colon-space-after": "always", - "declaration-colon-space-before": "never", - "indentation": "tab", - "no-descending-specificity": null, - "no-eol-whitespace": true, - "property-no-vendor-prefix": true, - "rule-empty-line-before": [ - "always", { - "except": ["after-single-line-comment","first-nested"] - } - ], - "order/properties-order": [ - "margin", - "padding", - "background", - "display", - "float", - "max-width", - "width", - "max-height", - "height", - "color", - "font", - "font-family", - "font-size", - "border", - "border-top", - "border-top-color", - "border-right", - "border-right-color", - "border-bottom", - "border-bottom-color", - "border-left", - "border-left-color", - "border-radius", - "box-shadow" - ], - "scss/at-else-closing-brace-newline-after": "always-last-in-chain", - "scss/at-else-closing-brace-space-after": "always-intermediate", - "scss/at-else-empty-line-before": "never", - "scss/at-if-closing-brace-newline-after": "always-last-in-chain", - "scss/at-if-closing-brace-space-after": "always-intermediate" - } + "extends": "stylelint-config-recommended", + "plugins": [ + "stylelint-order", + "@stylistic/stylelint-plugin" + ], + "rules": { + "at-rule-empty-line-before": [ + "always", { + "ignoreAtRules": [ "after-comment", "else" ] + } + ], + "@stylistic/at-rule-name-space-after": [ + "always", { + "ignoreAtRules": [ "after-comment" ] + } + ], + "@stylistic/block-closing-brace-newline-after": [ + "always", { + "ignoreAtRules": [ "if", "else" ] + } + ], + "@stylistic/block-closing-brace-newline-before": "always-multi-line", + "@stylistic/block-opening-brace-newline-after": "always-multi-line", + "@stylistic/block-opening-brace-space-before": "always", + "@stylistic/color-hex-case": "lower", + "color-hex-length": "short", + "color-no-invalid-hex": true, + "@stylistic/declaration-colon-space-after": "always", + "@stylistic/declaration-colon-space-before": "never", + "@stylistic/indentation": "tab", + "no-descending-specificity": null, + "@stylistic/no-eol-whitespace": true, + "property-no-vendor-prefix": true, + "rule-empty-line-before": [ + "always", { + "except": ["after-single-line-comment","first-nested"] + } + ], + "order/properties-order": [ + "margin", + "padding", + "background", + "display", + "float", + "max-width", + "width", + "max-height", + "height", + "color", + "font", + "font-family", + "font-size", + "border", + "border-top", + "border-top-color", + "border-right", + "border-right-color", + "border-bottom", + "border-bottom-color", + "border-left", + "border-left-color", + "border-radius", + "box-shadow" + ] + } } diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 00000000000..4db8c2287b7 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,61 @@ +[default.extend-identifiers] +ot = "ot" +Ths2 = "Ths2" + +[default.extend-words] +ANDS = "ANDS" +Chanse = "Chanse" +referer = "referer" + +[files] +extend-exclude = [ + "*.fr.md", + "*.map", + "*.min.js", + "*.po", + "*.pot", + "*.rtl.css", + ".git/", + "app/i18n/cs/", + "app/i18n/de/", + "app/i18n/el/", + "app/i18n/es/", + "app/i18n/fa/", + "app/i18n/fi/", + "app/i18n/fr/", + "app/i18n/he/", + "app/i18n/hu/", + "app/i18n/id/", + "app/i18n/it/", + "app/i18n/ja/", + "app/i18n/ko/", + "app/i18n/lt/", + "app/i18n/lv/", + "app/i18n/nl/", + "app/i18n/oc/", + "app/i18n/pl/", + "app/i18n/pt-BR/", + "app/i18n/pt-PT/", + "app/i18n/ru/", + "app/i18n/sk/", + "app/i18n/tr/", + "app/i18n/uk/", + "app/i18n/zh-CN/", + "app/i18n/zh-TW/", + "bin/", + "composer.lock", + "data/", + "docs/CHANGELOG-old.md", + "docs/_config.yml", + "docs/_includes/", + "docs/assets/", + "docs/fr/", + "lib/marienfressinaud/", + "lib/phpgt/", + "lib/phpmailer/", + "lib/simplepie/", + "node_modules/", + "p/scripts/vendor/", + "package-lock.json", + "vendor/", +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..5818626aea8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +--- +description: FreshRSS introduction and conventions for AI agents and human contributors +--- + +# FreshRSS introduction and conventions + +FreshRSS is an open-source news aggregator supporting RSS/ATOM/WebSub and also Web scraping. + +The market segment is mostly self-hosting, although multiple user accounts are supported. +It is supposed to be installed on a modest server (mostly, but not limited to, Linux) and can be accessed through a Web UI and an API. + +It is written in PHP to make it easy to deploy and easy to contribute to. + +More information can be found in [README](README.md) and in the [documentation](docs/en/) published at + +## Instruction structure for AI agents and humans + +* [`.github/instructions/`](.github/instructions/) contain the instructions that primarily match filename patterns. +* [`.github/skills/`](.github/skills/) contain the skills to achieve higher-level goals, potentially combining multiple instructions. +* **`AGENTS.md`** is the entry point description for AI coding agents and contributors, referencing other documents such as instructions and skills. +* Favour standard conventions over vendor-specific ones. +* To reduce duplication, refer to enforceable configuration files instead of excessive free-text repetitions. +* **Note:** Please limit AI-assisted pull requests to **10 open at a time** per contributor. + +## Code structure + +* `app/Controllers/` – Controllers extend `FreshRSS_ActionController`, named as `{name}Controller.php` with class `FreshRSS_{name}_Controller` +* `app/Models/` – Domain models extend `Minz_Model`. + * DAOs may have some database-specific inheritance, e.g.: `*DAO.php` (base or MySQL), `*DAOSQLite.php` (SQLite), `*DAOPGSQL.php` (PostgreSQL) +* `app/views/{controller}/` – View templates are `.phtml` files mixing PHP and HTML +* `app/views/helpers/` – Reusable view elements with `$this->partial('name')` +* `lib/core-extensions/` or `extensions/` – FreshRSS extensions +* `lib/Minz/` – Core framework: routing, sessions, translations, extensions, PDO abstraction +* `lib/lib_rss.php` – Contain `spl_autoload_register` and other global functions +* `p/` – Public Web root. Only the content of this folder should be exposed if possible (`p/` should not be visible in the public URL) + * Only the `p/i/` path should be access controlled. The main entry point is `p/i/index.php` + * `p/api/greader.php` – Primary API for mobile clients + * `p/api/pshb.php` – Receive realtime pushes via WebSub + * `p/themes/` – UI themes + +### Configuration data + +* Constants: [`constants.php`](constants.php) overridden in `constants.local.php` +* System config: [`config.default.php`](config.default.php) overridden in `data/config.php` +* User config: [`config-user.default.php`](config-user.default.php) overridden in `data/users/{username}/config.php` + +> ℹ Access via `FreshRSS_Context::systemConf()` and `FreshRSS_Context::userConf()` + +## Database patterns + +Three database backends supported: SQLite, PostgreSQL, MySQL/MariaDB. +The SQL differences are implemented through inheritance: + +```php +// Base DAO with common queries +class FreshRSS_EntryDAO extends Minz_ModelPdo { } +// Database-specific overrides +class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO { } +class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAO { } +``` + +A factory pattern selects the correct DAO, e.g.: + +```php +FreshRSS_Factory::createEntryDao(); +``` + +**Important**: in database, VARCHAR/TEXT fields are HTML-encoded, except `attributes` fields, which contain JSON, and which sub-strings are not HTML-encoded. + +## Main development commands + +> ℹ Recommended before committing + +```sh +# Runs all tests: PHPUnit, PHPCS, PHPStan, typos +make test-all + +# Auto-fix all trivial issues: whitespace, RTL CSS, translations +make fix-all + +# See a list of commands: +make help +``` + +CI/CD is defined in [`.github/workflows/tests.yml`](.github/workflows/tests.yml) + +A Dev Container is available under [`.devcontainer/`](.devcontainer/). + +## Docker + +> ℹ Check [Docker documentation](Docker/README.md). + +## CLI Tools + +There are scripts in `cli/` for admin tasks, such as: + +```sh +./cli/list-users.php +``` + +> ℹ Check [CLI documentation](cli/README.md). + +When possible, it is best to run as Web server user: + +```sh +sudo -u www-data cli/actualize-user.php --user alice +``` + +or at least re-apply the file permissions after CLI operations: + +```sh +cli/access-permissions.sh +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 071cb8b2c13..92cb07dcfd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,1744 +1,1030 @@ -# FreshRSS changelog +# FreshRSS changelog -## 2021-XX-XX FreshRSS 1.19.0-dev - -* Features - * New setting to automatically mark as read a new article if there is already one with the same title in the same feed [#3303](https://github.com/FreshRSS/FreshRSS/pull/3303) - * New setting to keep only a maximum number of unread articles in a given feed [#3303](https://github.com/FreshRSS/FreshRSS/pull/3303) - * New search operator based on custom labels, or not [#3709](https://github.com/FreshRSS/FreshRSS/pull/3709) - * Search articles with label IDs: `L:12,13,14` or label names: `label:something` or `labels:"my label,my other label,🧪"` - * Search articles with any label: `L:*` or no label: `!L:*` -* Bug fixing - * Fix marking as read a label with SQLite and PostgreSQL [#3711](https://github.com/FreshRSS/FreshRSS/pull/3711) - * Better fallback for feeds without title [#3787](https://github.com/FreshRSS/FreshRSS/pull/3787) -* Security - * Better error handling when a user does not exist (especially for API) [#3751](https://github.com/FreshRSS/FreshRSS/pull/3751) -* Compatibility - * Require PHP 7.0+ (drop support for PHP 5.x) [#3666](https://github.com/FreshRSS/FreshRSS/pull/3666) - * Drop support for Microsoft Internet Explorer (IE11) [#3666](https://github.com/FreshRSS/FreshRSS/pull/3666) - * Fix back-compatibility with Git 2.21- for automatic updates [#3669](https://github.com/FreshRSS/FreshRSS/pull/3669) -* Deployment - * Docker: development image `:oldest` is now based on `alpine:3.5` with PHP 7.0.33 and Apache 2.4.35 [#3666](https://github.com/FreshRSS/FreshRSS/pull/3666) - * Docker: default image updated to Debian 11 Bullseye with PHP 7.4.21 and Apache 2.4.48 [#3782](https://github.com/FreshRSS/FreshRSS/pull/3782) - * Docker: alternative image updated to Alpine 3.14 with PHP 8.0.10 and Apache 2.4.48 [#3715](https://github.com/FreshRSS/FreshRSS/pull/3715) - * Images on Docker Hub are automatically scanned for software vulnerabilities -* UI - * New thumbnail and/or summary options for the normal view [#3805](https://github.com/FreshRSS/FreshRSS/pull/3805) - * Use HTML5 tags with better semantics and structure [#3651](https://github.com/FreshRSS/FreshRSS/pull/3651), [#3676](https://github.com/FreshRSS/FreshRSS/pull/3676), [#3713](https://github.com/FreshRSS/FreshRSS/pull/3713), [#3747](https://github.com/FreshRSS/FreshRSS/pull/3747), [#3830](https://github.com/FreshRSS/FreshRSS/pull/3830), [#3851](https://github.com/FreshRSS/FreshRSS/pull/3851) - * Allow JavaScript in themes [#3739](https://github.com/FreshRSS/FreshRSS/pull/3739) - * Improve layout of settings on small screen [#3818](https://github.com/FreshRSS/FreshRSS/pull/3818), [#3819](https://github.com/FreshRSS/FreshRSS/pull/3819) - * Improve layout of statistics [#3797](https://github.com/FreshRSS/FreshRSS/pull/3797), [#3799](https://github.com/FreshRSS/FreshRSS/pull/3799) - * Updated Flotr2 library [#3800](https://github.com/FreshRSS/FreshRSS/pull/3800) - * Remove jQuery fully [#3847](https://github.com/FreshRSS/FreshRSS/pull/3847) - * Update layout of user queries [#3827](https://github.com/FreshRSS/FreshRSS/pull/3827) - * Improve style of install procedure [#3721](https://github.com/FreshRSS/FreshRSS/pull/3721) - * Add retry button when checking requirements during install [#3771](https://github.com/FreshRSS/FreshRSS/pull/3771) - * Improve notification icon [#3678](https://github.com/FreshRSS/FreshRSS/pull/3678) - * Only show *Back to RSS feeds* when logged-in [#3790](https://github.com/FreshRSS/FreshRSS/pull/3790) - * Add CSS class to back links [#3761](https://github.com/FreshRSS/FreshRSS/pull/3761) - * Misc. minor UI and style improvements [#3792](https://github.com/FreshRSS/FreshRSS/pull/3792), [#3795](https://github.com/FreshRSS/FreshRSS/pull/3795), [#3801](https://github.com/FreshRSS/FreshRSS/pull/3801), [#3802](https://github.com/FreshRSS/FreshRSS/pull/3802), [#3817](https://github.com/FreshRSS/FreshRSS/pull/3817), [#3821](https://github.com/FreshRSS/FreshRSS/pull/3821), [#3824](https://github.com/FreshRSS/FreshRSS/pull/3824), [#3831](https://github.com/FreshRSS/FreshRSS/pull/3831), [#3832](https://github.com/FreshRSS/FreshRSS/pull/3832) -* Themes - * Fix *alternative-dark* theme to avoid bright elements [#3774](https://github.com/FreshRSS/FreshRSS/pull/3774), [#3806](https://github.com/FreshRSS/FreshRSS/pull/3806) - * Improve the contrast of message boxes for the *Origine* theme [#3725](https://github.com/FreshRSS/FreshRSS/pull/3725) - * Uniformize the size of `input`and `select` elements for the *Origine* theme [#3727](https://github.com/FreshRSS/FreshRSS/pull/3727) - * Fix style of banner text for the *Origine* theme [#3731](https://github.com/FreshRSS/FreshRSS/pull/3731) -* i18n - * Fix bug in French and German translations of new/old tags [#3703](https://github.com/FreshRSS/FreshRSS/pull/3703), [#3668](https://github.com/FreshRSS/FreshRSS/pull/3668) - * Lint i18n [#3841](https://github.com/FreshRSS/FreshRSS/pull/3841) - * Add Japanese [#3828](https://github.com/FreshRSS/FreshRSS/pull/3828), [#3834](https://github.com/FreshRSS/FreshRSS/pull/3834) - * Improve Dutch [#3844](https://github.com/FreshRSS/FreshRSS/pull/3844) - * Improve German [#3720](https://github.com/FreshRSS/FreshRSS/pull/3720), [#3846](https://github.com/FreshRSS/FreshRSS/pull/3846) -* Extensions - * Add system configuration for extension [#3626](https://github.com/FreshRSS/FreshRSS/pull/3626) -* Misc. - * Do not show back link on error pages if the user does not have access [#3765](https://github.com/FreshRSS/FreshRSS/pull/3765) - * Added Raindrop.io as sharing option [#3717](https://github.com/FreshRSS/FreshRSS/pull/3717) - * Delete outdated information regarding Firefox feed reader list [#3822](https://github.com/FreshRSS/FreshRSS/pull/3822) - - -## 2021-06-06 FreshRSS 1.18.1 - -* Features - * Support standard `HTTP 410 Gone` by disabling (muting) gone feeds [#3561](https://github.com/FreshRSS/FreshRSS/pull/3561) - * Make advanced feed options such as SSL available to non-admins [#3612](https://github.com/FreshRSS/FreshRSS/pull/3612) -* API - * Supported by [Newsboat 2.24+](https://newsboat.org/) [#3574](https://github.com/FreshRSS/FreshRSS/pull/3574) - * Supported by [RSS Guard](https://github.com/martinrotter/rssguard) [#3627](https://github.com/FreshRSS/FreshRSS/pull/3627) -* UI - * Allow Unicode for shortcuts [#3548](https://github.com/FreshRSS/FreshRSS/pull/3548) -* Bug fixing - * Fix database lock during refresh with MariaDB [#3559](https://github.com/FreshRSS/FreshRSS/pull/3559) - * Fix database creation from CLI [#3544](https://github.com/FreshRSS/FreshRSS/pull/3544) - * Fix: `pdo_sqlite` is optional except for export/import SQLite [#3545](https://github.com/FreshRSS/FreshRSS/pull/3545) - * Fix import of JSON and TT-RSS files, especially with PHP 8 [#3553](https://github.com/FreshRSS/FreshRSS/pull/3553) - * Allow import of more than 999 favourites/labelled articles even with SQLite - * Fix additional SQL limits, especially for SQLite [#3586](https://github.com/FreshRSS/FreshRSS/pull/3586) - * Fix search param encoding in user query [#3541](https://github.com/FreshRSS/FreshRSS/pull/3541) - * Fix undefined variable & dead code when adding feed [#3546](https://github.com/FreshRSS/FreshRSS/pull/3546) - * Fix missing translation in feed configuration [#3554](https://github.com/FreshRSS/FreshRSS/pull/3554) - * Fix double escaping in feed filters [#3563](https://github.com/FreshRSS/FreshRSS/pull/3563) - * Fix bugs in migration system [#3589](https://github.com/FreshRSS/FreshRSS/pull/3589) - * Fix regression preventing showing startup errors [#3590](https://github.com/FreshRSS/FreshRSS/pull/3590) - * Fix form redirection after erroneous user creation [#3656](https://github.com/FreshRSS/FreshRSS/pull/3656) - * Fix JavaScript error during navigation when no article is selected [#3655](https://github.com/FreshRSS/FreshRSS/pull/3655) - * Fix link to add feeds from the empty homepage [#3650](https://github.com/FreshRSS/FreshRSS/pull/3650) - * Fix git update error message [#3645](https://github.com/FreshRSS/FreshRSS/pull/3645) -* SimplePie - * Fix regression about media attachments [#3565](https://github.com/FreshRSS/FreshRSS/pull/3565) - * Fix regression about forcing HTTPS for enclosures [#3568](https://github.com/FreshRSS/FreshRSS/pull/3568) - * Catch ValueError for loadHTML with PHP 8 [simplepie#673](https://github.com/simplepie/simplepie/pull/673) - * Provide access to latest HTTP status code [simplepie#674](https://github.com/simplepie/simplepie/pull/674) - * Fix wrong SimplePie type hint [simplepie#678](https://github.com/simplepie/simplepie/pull/678) - * Merge details from upstream PRs [#3588](https://github.com/FreshRSS/FreshRSS/pull/3588), [#3614](https://github.com/FreshRSS/FreshRSS/pull/3614) -* API - * Compatibility with Web servers providing `ORIG_PATH_INFO` [#3560](https://github.com/FreshRSS/FreshRSS/pull/3560) -* i18n - * Improved Russian [#3579](https://github.com/FreshRSS/FreshRSS/pull/3579) - * Improved Turkish [#3604](https://github.com/FreshRSS/FreshRSS/pull/3604) - * Improved Chinese [#3600](https://github.com/FreshRSS/FreshRSS/pull/3600) -* Code improvements: - * Friendly constant syntax for Intellisense [#3577](https://github.com/FreshRSS/FreshRSS/pull/3577) - * Fix several comments syntaxes [#3615](https://github.com/FreshRSS/FreshRSS/pull/3615) - * Minor uniform stricter HTML [#3616](https://github.com/FreshRSS/FreshRSS/pull/3616) - * Removed unused variable [#3587](https://github.com/FreshRSS/FreshRSS/pull/3587) - * Provide action name in Minz controller exception [#3624](https://github.com/FreshRSS/FreshRSS/pull/3624) - * New convenience method to extract multiline GET parameters from e.g. ` + + + +
+
+ + +
+
+ + diff --git a/app/views/configure/queries.phtml b/app/views/configure/queries.phtml index 2939e068b39..5a5045f6dd8 100644 --- a/app/views/configure/queries.phtml +++ b/app/views/configure/queries.phtml @@ -1,71 +1,251 @@ -partial('aside_configure'); ?> +partial('aside_configure'); +?> +
+

+

+
+ -
- +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
- - -

+ api_enabled) { ?> +
+ +
+
+ +
+
+ +
+

+

+
+
+ +
+ +
+ +

+
+
+ - queries as $key => $query) { ?> -
-
-
- getName() ?> - - - - - - +
+ +
+ +
+ +

- hasParameters()) { ?> -
-
-
-
+
+
+ +
+ + + +
- isDeprecated()) { ?> -
-
-
-
+
+
+ +
+
- -
    - hasSearch()) { ?> -
  • getSearch()->getRawInput(), ENT_NOQUOTES, 'UTF-8')) ?>
  • - +
+
+ +
+ +
+
- getState()) { ?> -
  • getState()) ?>
  • - +
    +
    + + +
    +
    +
    + - getOrder()) { ?> -
  • getOrder())) ?>
  • - - getGet()) { ?> -
  • getGetType(), $query->getGetName()) ?>
  • - - - -
    + _t('conf.query.state_all'), + 1 => _t('conf.query.state_read'), + 2 => _t('conf.query.state_unread'), + 3 => _t('conf.query.state_all'), + 4 => _t('conf.query.state_favorite'), + 5 => _t('conf.query.state_read_favorite'), + 6 => _t('conf.query.state_unread_favorite'), + 7 => _t('conf.query.state_favorite'), + 8 => _t('conf.query.state_not_favorite'), + 9 => _t('conf.query.state_read_not_favorite'), + 10 => _t('conf.query.state_unread_not_favorite'), + 11 => _t('conf.query.state_not_favorite'), + 12 => _t('conf.query.state_all'), + 13 => _t('conf.query.state_read'), + 14 => _t('conf.query.state_unread'), + 15 => _t('conf.query.state_all'), + ]; + ?> +
    + + queries) < 1) { ?> +
    +

    +

    - + queries as $key => $query) { ?> +
    +
    +
    +

    getName() ?>

    + + + + + + + + + +
    + hasParameters()) { ?> +
    +
    +
    +
    +
    + isDeprecated()) { ?> +
    +
    +
    +
    +
    + +
      + hasSearch()) { ?> +
    • getSearch()->toString(expandUserQueries: false), ENT_NOQUOTES, 'UTF-8')) ?>
    • + + + getState()) { ?> +
    • getState()] ?? '' ?>
    • + + + getOrder() !== '') { ?> +
    • getOrder()) { + 'ASC' => _t('conf.query.order_asc'), + 'DESC' => _t('conf.query.order_desc'), + default => '', + } ?>
    • + + + getGet() !== '') { ?> +
    • getGetType()) { + 'A' => _t('conf.query.get_A'), + 'Z' => _t('conf.query.get_Z'), + 'all' => _t('conf.query.get_all'), + 'all_labels' => _t('conf.query.get_all_labels'), + 'category' => _t('conf.query.get_category', $query->getGetName()), + 'favorite' => _t('conf.query.get_favorite'), + 'feed' => _t('conf.query.get_feed', $query->getGetName()), + 'important' => _t('conf.query.get_important'), + 'label' => _t('conf.query.get_label', $query->getGetName()), + default => '', + } ?>
    • + + +
    +
    +
    +
    -
    +
    -displaySlider ? ' class="active"' : ''; ?> -> +query != null) ? ' active' : ''; ?> + + -
    > -query)) { - $this->renderHelper('configure/query'); - } -?> -
    diff --git a/app/views/configure/query.phtml b/app/views/configure/query.phtml index 6f38d9efbf1..51051b98a36 100644 --- a/app/views/configure/query.phtml +++ b/app/views/configure/query.phtml @@ -1,5 +1,10 @@ query) { +if (!Minz_Request::paramBoolean('ajax')) { + $this->partial('aside_configure'); +} +if ($this->query !== null) { $this->renderHelper('configure/query'); } diff --git a/app/views/configure/reading.phtml b/app/views/configure/reading.phtml index e19a14067c0..6d054e8f269 100644 --- a/app/views/configure/reading.phtml +++ b/app/views/configure/reading.phtml @@ -1,252 +1,409 @@ -partial('aside_configure'); ?> - -
    - - +partial('aside_configure'); +?> +

    -
    + - -
    - -
    - -

    +
    + +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +

    +
    -
    -
    - -
    - +
    +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    -
    - + sort; + $userSortOrder = FreshRSS_Context::userConf()->sort_order; + $userSortCombined = $userSort === 'rand' ? 'rand' : $userSort . '_' . strtolower($userSortOrder ?? 'desc'); + ?> +
    + +
    + +

    +
    -
    -
    -
    - + secondary_sort; + $userSecondarySortOrder = FreshRSS_Context::userConf()->secondary_sort_order; + $userSecondarySortCombined = $userSecondarySort === 'rand' ? 'rand' : $userSecondarySort . '_' . strtolower($userSecondarySortOrder ?? 'desc'); + ?> +
    + +
    + +

    +
    -
    + -
    -
    - +
    + +
    + +
    + +
    -
    -
    -
    - +
    +
    + +
    -
    -
    -
    - +
    +
    + +
    -
    -
    -
    - +
    +
    + +

    +
    -
    + -
    -
    - +
    + +
    +
    + +
    -
    -
    -
    - +
    +
    + +
    -
    -
    -
    - +
    +
    + +
    -
    + -
    -
    - +
    + +
    +
    + +
    -
    -
    - -
    - - - +
    +
    + +
    +
    + +
    + +
    + +
    +
    - +
    + +
    + +
    +
    - +
    + +
    + +
    +
    - +
    + +
    + +
    -
    -
    - -
    - +
    + +
    + +
    -
    + + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +

    +

    + +

    +
    +
    + +
    + +
    + +

    +

    + +

    +
    +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    + +
    +
    +
    @@ -256,4 +413,4 @@
    -
    +
    diff --git a/app/views/configure/shortcut.phtml b/app/views/configure/shortcut.phtml index ff94fcf565a..37847e39a33 100644 --- a/app/views/configure/shortcut.phtml +++ b/app/views/configure/shortcut.phtml @@ -1,10 +1,9 @@ -partial('aside_configure'); ?> - -
    - - +partial('aside_configure'); +?> +

    @@ -13,51 +12,42 @@ - shortcuts; ?> + htmlspecialchars($string, ENT_COMPAT, 'UTF-8'), FreshRSS_Context::userConf()->shortcuts); + ?> - +

    , ', $nonStandard)) ?>

    -
    + - +
    - +
    - +
    - -
    -
    - -
    - -
    - +
    @@ -70,40 +60,35 @@
    - +
    - +
    - +
    - +
    - +
    @@ -112,16 +97,14 @@
    - +
    - +
    @@ -133,8 +116,7 @@

    - +
    @@ -143,41 +125,43 @@
    - +
    - +
    - +

    +
    + +
    + +
    +
    +
    - +
    - +
    @@ -185,44 +169,53 @@
    +
    + +
    + +
    +
    + +
    + +
    + +
    +
    +
    - +
    - +
    - +

    - +
    - +
    - +
    @@ -235,4 +228,4 @@
    - + diff --git a/app/views/configure/system.phtml b/app/views/configure/system.phtml index 251ffa03ad6..5b6c4dd5ce5 100644 --- a/app/views/configure/system.phtml +++ b/app/views/configure/system.phtml @@ -1,87 +1,158 @@ -partial('aside_configure'); ?> - -
    - - +partial('aside_configure'); +?> +

    -
    +
    - + +
    +
    + +
    + +
    + +

    +

    +
    +
    + +
    + +
    + pubsubhubbub_enabled && Minz_Request::serverIsPublic(FreshRSS_Context::systemConf()->base_url) ? 'checked="checked"' : '' ?> /> +

    +

    - +
    - +
    - -

    +
    +
    - 1 ? _t('admin.user.numbers', $number) : _t('admin.user.number', $number)); - ?> +
    +
    - + +

    + internal_host_allowlist; + $internal_host_allowlist_fromenv = false; + } + ?> +
    - +
    - + + +

    + +
    +

    +
    - + +
    + +
    +
    + +
    +
    - + + + (= )(= )
    -
    - -

    + 1 ? _t('admin.user.numbers', $number) : _t('admin.user.number', $number)); ?> +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + + + + +

    + +
    +
    + +
    +
    +
    @@ -92,4 +163,6 @@
    - + + + diff --git a/app/views/entry/bookmark.phtml b/app/views/entry/bookmark.phtml old mode 100755 new mode 100644 index d85706669c1..a124df67303 --- a/app/views/entry/bookmark.phtml +++ b/app/views/entry/bookmark.phtml @@ -1,16 +1,19 @@ Minz_Request::controllerName(), 'a' => Minz_Request::actionName(), - 'params' => Minz_Request::fetchGET(), -); + 'params' => array_filter($_GET, 'is_string', ARRAY_FILTER_USE_KEY), +]; -$url['params']['is_favorite'] = Minz_Request::param('is_favorite', true) ? '0' : '1'; +$url['params']['is_favorite'] = (Minz_Request::paramTernary('is_favorite') ?? true) ? '0' : '1'; FreshRSS::loadStylesAndScripts(); -echo json_encode(array( - 'url' => str_ireplace('&', '&', Minz_Url::display($url)), - 'icon' => _i($url['params']['is_favorite'] === '1' ? 'non-starred' : 'starred') - )); +echo json_encode([ + 'url' => str_ireplace('&', '&', Minz_Url::display($url)), + 'icon' => _i($url['params']['is_favorite'] === '1' ? 'non-starred' : 'starred') +]); diff --git a/app/views/entry/read.phtml b/app/views/entry/read.phtml old mode 100755 new mode 100644 index 44193da9cfb..071782895cd --- a/app/views/entry/read.phtml +++ b/app/views/entry/read.phtml @@ -1,7 +1,9 @@ $this->tags, - )); +echo json_encode([ + 'tags' => $this->tagsForEntries, + ]); diff --git a/app/views/error/index.phtml b/app/views/error/index.phtml index 0d14d7fc78c..50ad16eba0a 100644 --- a/app/views/error/index.phtml +++ b/app/views/error/index.phtml @@ -1,13 +1,19 @@ -
    + +

    code ?>

    - errorMessage, ENT_NOQUOTES, 'UTF-8') ?>
    + errorMessage, ENT_NOQUOTES, 'UTF-8') ?> +

    +

    - +

    -
    + diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml index 8933eeae5b6..c722b3a1eb2 100644 --- a/app/views/extension/configure.phtml +++ b/app/views/extension/configure.phtml @@ -1,3 +1,4 @@ renderHelper('extension/configure'); diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index cd5ccec48e5..68eb820887b 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -1,30 +1,37 @@ -partial('aside_configure'); ?> - -
    - - +partial('aside_configure'); +?> +

    -

    - extension_list['system'])) { + extension_list['system'])) { ?> +

    +
      + extension_list['system'] as $ext) { - $this->ext_details = $ext; - $this->renderHelper('extension/details'); - } - ?> + $this->ext_details = $ext; ?> +
    • + renderHelper('extension/details'); ?> +
    • + +
    extension_list['user'])) { ?>

    +
      extension_list['user'] as $ext) { - $this->ext_details = $ext; - $this->renderHelper('extension/details'); - } - } + $this->ext_details = $ext; ?> +
    • + renderHelper('extension/details'); ?> +
    • + +
    + extension_list['system']) && empty($this->extension_list['user'])) { ?> @@ -34,13 +41,14 @@ available_extensions)) { ?>

    -
    +
    + available_extensions as $ext) { ?> @@ -49,31 +57,41 @@ +
    - extensions_installed[$ext['name']])) { ?> - extensions_installed[$ext['name']], $ext['version']) >= 0) { ?> + extensions_installed[$ext['entrypoint']])) { ?> + extensions_installed[$ext['entrypoint']]), strval($ext['version'])) >= 0) { ?> - extensions_installed[$ext['name']] != $ext['version']) { ?> + extensions_installed[$ext['entrypoint']]) !== strval($ext['version'])) { ?>
    + +

    -
    +
    -extension) ? ' class="active"' : ''; ?> -> -
    > -extension)) { - $this->renderHelper('extension/configure'); - } -?> -
    +extension) ? ' active' : ''; ?> +extension) ? _url('extension', 'index') : ''; ?> + + + + + diff --git a/app/views/extension/serve.phtml b/app/views/extension/serve.phtml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/views/feed/add.phtml b/app/views/feed/add.phtml index a585fcedcfe..39a7631feb2 100644 --- a/app/views/feed/add.phtml +++ b/app/views/feed/add.phtml @@ -1,5 +1,9 @@ -feed) { ?> -
    +feed !== null) { +?> +

    load_ok) { ?> @@ -8,78 +12,82 @@ - - load_ok) { ?> -
    - -
    - +
    + + load_ok) { ?> +
    + +
    + +
    -
    - feed->description(); if ($desc != '') { ?> -
    - -
    - + feed->description(); if ($desc != '') { ?> +
    + +
    + +
    -
    - + -
    - -
    - feed->website() ?> - +
    + +
    +
    + + +
    +
    -
    - + -
    - -
    -
    - - +
    + +
    +
    + + +
    +
    +
    -
    -
    -
    - -
    - - - +
    + +
    + +
    -
    + - - feed->httpAuth(false); ?> -
    - -
    - -

    +
    + + feed->httpAuth(false); ?> +
    + +
    + +

    +
    - -
    -
    - - +
    + +
    +
    + + +
    -
    +
    @@ -88,5 +96,5 @@
    -
    +
    diff --git a/app/views/feed/contentSelectorPreview.phtml b/app/views/feed/contentSelectorPreview.phtml index 3bb27859ab5..0fc22c8ed60 100644 --- a/app/views/feed/contentSelectorPreview.phtml +++ b/app/views/feed/contentSelectorPreview.phtml @@ -1,8 +1,21 @@ - +darkMode !== 'no') { + $class .= ' darkMode_' . FreshRSS_Context::userConf()->darkMode; + } +?> - + xml:lang="language ?>"> - + @@ -27,7 +40,7 @@
    -
    +
    htmlContent ?>
    diff --git a/app/views/helpers/category/update.phtml b/app/views/helpers/category/update.phtml index 085b49aa66a..da7cde63d44 100644 --- a/app/views/helpers/category/update.phtml +++ b/app/views/helpers/category/update.phtml @@ -1,182 +1,349 @@ +category === null) { + throw new FreshRSS_Context_Exception('Category not initialised!'); + } +?>
    -

    category->name() ?>

    +

    + category->name() ?> + category->kind() === FreshRSS_Category::KIND_DYNAMIC_OPML ? _i('opml-dyn') : '' ?> +

    -
    + displaySlider; + if (!$ajax) { + $from = 'update'; + $slider = []; + } elseif ($from === '') { + $from = 'index'; + } + ?> + + +
    + +
    + +
    + category->id() === FreshRSS_CategoryDAO::DEFAULTCATEGORYID ? 'readonly="readonly"' : '' + ?> /> +
    +
    +
    + +
    + +

    +
    +
    - -
    - -
    - category->id() == FreshRSS_CategoryDAO::DEFAULTCATEGORYID ? 'disabled="disabled"' : ''; - ?> /> -
    -
    -
    - -
    - -

    -
    -
    - -
    -
    - - - -
    -
    - - - category->attributes('archiving'); - if (empty($archiving)) { - $archiving = [ 'default' => true ]; - } else { - $archiving['default'] = false; - } - $volatile = [ - 'enable_keep_period' => false, - 'keep_period_count' => '3', - 'keep_period_unit' => 'P1M', - ]; - if (!empty($archiving['keep_period'])) { - if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { - $volatile['enable_keep_period'] = true; - $volatile['keep_period_count'] = $matches['count']; - $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); - } - } - //Defaults - if (!isset($archiving['keep_max'])) { - $archiving['keep_max'] = false; - } - if (!isset($archiving['keep_favourites'])) { - $archiving['keep_favourites'] = true; - } - if (!isset($archiving['keep_labels'])) { - $archiving['keep_labels'] = true; - } - if (!isset($archiving['keep_unreads'])) { - $archiving['keep_unreads'] = false; - } - if (!isset($archiving['keep_min'])) { - $archiving['keep_min'] = 50; - } - ?> - -

    - -

    - -
    - -
    - -
    -
    - - - - - - -
    -
    - - -
    -
    +
    +
    + +
    + +
    + category->attributeBoolean('show_unread_count'); ?> + +
    +
    + +
    +
    + + +
    +
    + category->isDefault()): ?> -

    - default_category->name()) ?> -

    - -
    -
    - - category->isDefault()): ?> - - -
    -
    +
    + +
    + +
    +
    + + + +
    +

    +

    +
    +
    +
    +
    + + + +
    +
    +
    + + +
    + + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +

    +

    + +

    +
    +
    + +
    +
    + + +
    +
    +
    + +
    + + category->attributeArray('archiving'); + /** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */ + if (empty($archiving)) { + $archiving = [ 'default' => true ]; + } else { + $archiving['default'] = false; + } + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + if (!empty($archiving['keep_period']) && is_string($archiving['keep_period'])) { + if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { + $volatile['enable_keep_period'] = true; + $volatile['keep_period_count'] = $matches['count']; + $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); + } + } + //Defaults + if (!isset($archiving['keep_max']) || !is_int($archiving['keep_max'])) { + $archiving['keep_max'] = 0; + } + if (!isset($archiving['keep_min']) || !is_int($archiving['keep_min'])) { + $archiving['keep_min'] = 50; + } + if (!isset($archiving['keep_favourites']) || !is_bool($archiving['keep_favourites'])) { + $archiving['keep_favourites'] = true; + } + if (!isset($archiving['keep_labels']) || !is_bool($archiving['keep_labels'])) { + $archiving['keep_labels'] = true; + } + if (!isset($archiving['keep_unreads']) || !is_bool($archiving['keep_unreads'])) { + $archiving['keep_unreads'] = false; + } + ?> + +

    +
    + +

    + +
    + +
    + +
    +
    + + + + + + +
    +
    + + +
    +
    +
    + +
    + category->isDefault()): ?> +

    + +

    + +
    +
    + + + + + category->isDefault()): ?> + + +
    +
    +
    + +
    + +
    + +
    + + + +

    +
    +
    +
    diff --git a/app/views/helpers/configure/query.phtml b/app/views/helpers/configure/query.phtml index a9bf4478ddd..448f743674e 100644 --- a/app/views/helpers/configure/query.phtml +++ b/app/views/helpers/configure/query.phtml @@ -1,95 +1,177 @@ +query === null) { + throw new FreshRSS_Context_Exception('Query not initialised!'); + } + + $ajax = Minz_Request::paramBoolean('ajax') || $this->displaySlider; +?>

    query->getName() ?>

    -
    -
    +
    - + +
    - -
    - +
    - +
    - +
    - - - - +
    -
    - -
    - + + api_enabled) { ?> +
    + +
    +
    + + query->sharedUrlRss() !== ''): ?> + + + +
    +
    + + query->sharedUrlOpml() !== ''): ?> +
      +
    • +
    + +
    +

    +

    +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +

    +
    -
    -
    - -
    - + + +
    + +
    + +
    + +

    +
    +
    +
    + +
    + + + + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    -
    -
    -
    - - +
    +
    + + +
    -
    +
    diff --git a/app/views/helpers/export/articles.phtml b/app/views/helpers/export/articles.phtml index 0bbfb86ecb5..f7d2022c903 100644 --- a/app/views/helpers/export/articles.phtml +++ b/app/views/helpers/export/articles.phtml @@ -1,66 +1,37 @@ 'user/' . str_replace('/', '', $username) . '/state/org.freshrss/' . $this->type, 'title' => $this->list_title, 'author' => $username, - 'items' => array(), -); + 'items' => [], +]; -echo rtrim(json_encode($articles, $options), " ]}\n\r\t"), "\n"; +echo rtrim(json_encode($articles, $options) ?: '', " ]}\n\r\t"), "\n"; $first = true; if (empty($this->entryIdsTagNames)) { - $this->entryIdsTagNames = array(); + $this->entryIdsTagNames = []; } -foreach ($this->entriesRaw as $entryRaw) { - if ($entryRaw == null) { - continue; +foreach ($this->entries as $entry) { + if (!$this->internal_rendering) { + /** @var FreshRSS_Entry|null $entry */ + $entry = Minz_ExtensionManager::callHook(Minz_HookType::EntryBeforeDisplay, $entry); } - $entry = FreshRSS_EntryDAO::daoToEntry($entryRaw); - if (!isset($this->feed)) { - $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $entry->feed()); - if ($feed === null) { - $feed = $entry->feed(true); - } - } else { - $feed = $this->feed; + if ($entry === null) { + continue; } - $article = array( - 'id' => $entry->guid(), - 'timestampUsec' => '' . $entry->id(), - 'categories' => array_values($entry->tags()), - 'title' => $entry->title(), - 'author' => $entry->authors(true), - 'published' => $entry->date(true), - 'updated' => $entry->date(true), - 'alternate' => array(array( - 'href' => htmlspecialchars_decode($entry->link(), ENT_QUOTES), - 'type' => 'text/html', - )), - 'content' => array( - 'content' => $entry->content(), - ), - 'origin' => array( - 'streamId' => $feed == null ? '' : $feed->id(), - 'title' => $feed == null ? '' : $feed->name(), - 'htmlUrl' => $feed == null ? '' : $feed->website(), - 'feedUrl' => $feed == null ? '' : $feed->url(), - ) - ); - $article['categories'][] = $entry->isRead() ? 'user/-/state/com.google/read' : 'user/-/state/com.google/unread'; - if ($entry->isFavorite()) { - $article['categories'][] = 'user/-/state/com.google/starred'; - } - $tagNames = isset($this->entryIdsTagNames['e_' . $entry->id()]) ? $this->entryIdsTagNames['e_' . $entry->id()] : array(); - foreach ($tagNames as $tagName) { - $article['categories'][] = 'user/-/label/' . $tagName; - } + $feed = $this->feed ?? FreshRSS_Category::findFeed($this->categories, $entry->feedId()); + $entry->_feed($feed); + + $article = $entry->toGReader('freshrss', $this->entryIdsTagNames['e_' . $entry->id()] ?? []); $line = json_encode($article, $options); if ($line != '') { diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml index edb4d4eda3b..27e2bb4383e 100644 --- a/app/views/helpers/export/opml.phtml +++ b/app/views/helpers/export/opml.phtml @@ -1,28 +1,194 @@ array( - 'title' => FreshRSS_Context::$system_conf->title, - 'dateCreated' => date('D, d M Y H:i:s') - ), - 'body' => array() -); - -foreach ($this->categories as $key => $cat) { - $opml_array['body'][$key] = array( - 'text' => $cat['name'], - '@outlines' => array() - ); - - foreach ($cat['feeds'] as $feed) { - $opml_array['body'][$key]['@outlines'][] = array( +/** + * @param array $feeds + * @return list> + */ +function feedsToOutlines(array $feeds, bool $excludeMutedFeeds = false, bool $includeSensitiveCurlParams = false): array { + $outlines = []; + foreach ($feeds as $feed) { + if ($feed->mute() && $excludeMutedFeeds) { + continue; + } + + $outline = [ 'text' => htmlspecialchars_decode($feed->name(), ENT_QUOTES), - 'type' => 'rss', + 'type' => FreshRSS_Export_Service::TYPE_RSS_ATOM, 'xmlUrl' => htmlspecialchars_decode($feed->url(), ENT_QUOTES), 'htmlUrl' => htmlspecialchars_decode($feed->website(), ENT_QUOTES), 'description' => htmlspecialchars_decode($feed->description(), ENT_QUOTES), - ); + ]; + + switch ($feed->kind()) { + case FreshRSS_Feed::KIND_HTML_XPATH: + $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH; + break; + case FreshRSS_Feed::KIND_XML_XPATH: + $outline['type'] = FreshRSS_Export_Service::TYPE_XML_XPATH; + break; + case FreshRSS_Feed::KIND_JSON_DOTNOTATION: + $outline['type'] = FreshRSS_Export_Service::TYPE_JSON_DOTNOTATION; + break; + case FreshRSS_Feed::KIND_JSONFEED: + $outline['type'] = FreshRSS_Export_Service::TYPE_JSONFEED; + break; + case FreshRSS_Feed::KIND_HTML_XPATH_JSON_DOTNOTATION: + $outline['type'] = FreshRSS_Export_Service::TYPE_HTML_XPATH_JSON_DOTNOTATION; + break; + } + + $outline['frss:priority'] = match ($feed->priority()) { + FreshRSS_Feed::PRIORITY_IMPORTANT => FreshRSS_Export_Service::PRIORITY_IMPORTANT, + FreshRSS_Feed::PRIORITY_MAIN_STREAM => null, // Default + FreshRSS_Feed::PRIORITY_CATEGORY => FreshRSS_Export_Service::PRIORITY_CATEGORY, + FreshRSS_Feed::PRIORITY_FEED => FreshRSS_Export_Service::PRIORITY_FEED, + FreshRSS_Feed::PRIORITY_HIDDEN => FreshRSS_Export_Service::PRIORITY_HIDDEN, + default => null, + }; + + if ($feed->attributeString('unicityCriteria') != '' && $feed->attributeString('unicityCriteria') !== 'id') { + $outline['frss:unicityCriteria'] = $feed->attributeString('unicityCriteria'); + } + + if ($feed->attributeBoolean('unicityCriteriaForced')) { + $outline['frss:unicityCriteriaForced'] = 'true'; + } + + // Refresh interval (TTL), signed: a negative value indicates a muted feed + if ($feed->ttl(raw: true) !== FreshRSS_Feed::TTL_DEFAULT) { + $outline['frss:ttl'] = (string)$feed->ttl(raw: true); + } + + if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) { + /** @var array */ + $xPathSettings = $feed->attributeArray('xpath') ?? []; + $outline['frss:xPathItem'] = $xPathSettings['item'] ?? null; + $outline['frss:xPathItemTitle'] = $xPathSettings['itemTitle'] ?? null; + $outline['frss:xPathItemContent'] = $xPathSettings['itemContent'] ?? null; + $outline['frss:xPathItemUri'] = $xPathSettings['itemUri'] ?? null; + $outline['frss:xPathItemAuthor'] = $xPathSettings['itemAuthor'] ?? null; + $outline['frss:xPathItemTimestamp'] = $xPathSettings['itemTimestamp'] ?? null; + $outline['frss:xPathItemTimeFormat'] = $xPathSettings['itemTimeFormat'] ?? null; + $outline['frss:xPathItemThumbnail'] = $xPathSettings['itemThumbnail'] ?? null; + $outline['frss:xPathItemCategories'] = $xPathSettings['itemCategories'] ?? null; + $outline['frss:xPathItemUid'] = $xPathSettings['itemUid'] ?? null; + } elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTNOTATION || $feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH_JSON_DOTNOTATION) { + /** @var array */ + $jsonSettings = $feed->attributeArray('json_dotnotation') ?? []; + $outline['frss:jsonItem'] = $jsonSettings['item'] ?? null; + $outline['frss:jsonItemTitle'] = $jsonSettings['itemTitle'] ?? null; + $outline['frss:jsonItemContent'] = $jsonSettings['itemContent'] ?? null; + $outline['frss:jsonItemUri'] = $jsonSettings['itemUri'] ?? null; + $outline['frss:jsonItemAuthor'] = $jsonSettings['itemAuthor'] ?? null; + $outline['frss:jsonItemTimestamp'] = $jsonSettings['itemTimestamp'] ?? null; + $outline['frss:jsonItemTimeFormat'] = $jsonSettings['itemTimeFormat'] ?? null; + $outline['frss:jsonItemThumbnail'] = $jsonSettings['itemThumbnail'] ?? null; + $outline['frss:jsonItemCategories'] = $jsonSettings['itemCategories'] ?? null; + $outline['frss:jsonItemUid'] = $jsonSettings['itemUid'] ?? null; + if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH_JSON_DOTNOTATION) { + $outline['frss:xPathToJson'] = $feed->attributeString('xPathToJson'); + } + } + + if (!empty($feed->filtersAction('read'))) { + $filters = ''; + foreach ($feed->filtersAction('read') as $filterRead) { + $filters .= $filterRead->toString(expandUserQueries: false) . "\n"; + } + $filters = trim($filters); + $outline['frss:filtersActionRead'] = $filters; + } + + if ($feed->pathEntries() != '') { + $outline['frss:cssFullContent'] = htmlspecialchars_decode($feed->pathEntries(), ENT_QUOTES); + } + + if (!empty($feed->attributeArray('path_entries_conditions'))) { + $conditions = ''; + foreach ($feed->attributeArray('path_entries_conditions') as $condition) { + if (is_string($condition)) { + $conditions .= $condition . "\n"; + } + } + $conditions = trim($conditions); + $outline['frss:cssFullContentConditions'] = $conditions; + } + + if ($feed->attributeString('path_entries_filter') != '') { + $outline['frss:cssContentFilter'] = $feed->attributeString('path_entries_filter'); + } + + $curl_params = FreshRSS_http_Util::sanitizeCurlParams($feed->attributeArray('curl_params') ?? []); + // NB: Only include sensitive cURL parameters for private exports, not for public shares + if ($includeSensitiveCurlParams && !empty($curl_params)) { + $outline['frss:CURLOPT_COOKIE'] = $curl_params[CURLOPT_COOKIE] ?? null; + $outline['frss:CURLOPT_COOKIEFILE'] = $curl_params[CURLOPT_COOKIEFILE] ?? null; + $outline['frss:CURLOPT_FOLLOWLOCATION'] = $curl_params[CURLOPT_FOLLOWLOCATION] ?? null; + $outline['frss:CURLOPT_MAXREDIRS'] = $curl_params[CURLOPT_MAXREDIRS] ?? null; + $outline['frss:CURLOPT_POST'] = $curl_params[CURLOPT_POST] ?? null; + $outline['frss:CURLOPT_POSTFIELDS'] = $curl_params[CURLOPT_POSTFIELDS] ?? null; + $outline['frss:CURLOPT_PROXY'] = $curl_params[CURLOPT_PROXY] ?? null; + $outline['frss:CURLOPT_PROXYTYPE'] = $curl_params[CURLOPT_PROXYTYPE] ?? null; + if ($outline['frss:CURLOPT_PROXYTYPE'] === 3) { // Legacy for NONE + $outline['frss:CURLOPT_PROXYTYPE'] = -1; + } + $outline['frss:CURLOPT_USERAGENT'] = $curl_params[CURLOPT_USERAGENT] ?? null; + + if (!empty($curl_params[CURLOPT_HTTPHEADER]) && is_array($curl_params[CURLOPT_HTTPHEADER])) { + $headers = ''; + foreach ($curl_params[CURLOPT_HTTPHEADER] as $header) { + if (is_string($header)) { + $headers .= $header . "\n"; + } + } + $headers = trim($headers); + $outline['frss:CURLOPT_HTTPHEADER'] = $headers; + } + } + + // Remove null or invalid attributes + $outline = array_filter($outline, static function ($value) { return (is_string($value) || is_int($value) || is_bool($value)) && $value !== ''; }); + + $outlines[] = $outline; + } + + return $outlines; +} + +/** @var FreshRSS_View $this */ + +$opml_array = [ + 'namespaces' => [ + 'frss' => FreshRSS_Export_Service::FRSS_NAMESPACE, + ], + 'head' => [ + 'title' => FreshRSS_Context::systemConf()->title, + 'dateCreated' => new DateTime(), + ], + 'body' => [], +]; + +if (!empty($this->categories)) { + foreach ($this->categories as $cat) { + $outline = [ + 'text' => htmlspecialchars_decode($cat->name(), ENT_QUOTES), + '@outlines' => feedsToOutlines($cat->feeds(), $this->excludeMutedFeeds, $this->includeSensitiveCurlParams), + ]; + + if ($cat->kind() === FreshRSS_Category::KIND_DYNAMIC_OPML) { + $outline['frss:opmlUrl'] = $cat->attributeString('opml_url'); + } + + $opml_array['body'][] = $outline; } } -echo libopml_render($opml_array); +if (!empty($this->feeds)) { + $opml_array['body'] = array_merge($opml_array['body'], feedsToOutlines($this->feeds, $this->excludeMutedFeeds, $this->includeSensitiveCurlParams)); +} + +$libopml = new \marienfressinaud\LibOpml\LibOpml(strict: false); +$opml = $libopml->render($opml_array); +/** @var string $opml */ +echo $opml; diff --git a/app/views/helpers/extension/configure.phtml b/app/views/helpers/extension/configure.phtml index 4f8c521ae46..d0ed528bbb0 100644 --- a/app/views/helpers/extension/configure.phtml +++ b/app/views/helpers/extension/configure.phtml @@ -1,4 +1,14 @@ -
    +partial('aside_configure'); + } + if ($this->extension === null) { + throw new FreshRSS_Context_Exception('Extension not initialised!'); + } +?> +

    extension->getName() ?> (extension->getVersion() ?>) — extension->isEnabled() ? _t('admin.extensions.enabled') : _t('admin.extensions.disabled') ?> @@ -6,6 +16,17 @@

    extension->getDescription() ?> — extension->getAuthor() ?>

    + +
    + +
    +
    + +
    +
    +
    + +

    extension->getConfigureView(); diff --git a/app/views/helpers/extension/details.phtml b/app/views/helpers/extension/details.phtml index ed9674e3d53..d1f77302043 100644 --- a/app/views/helpers/extension/details.phtml +++ b/app/views/helpers/extension/details.phtml @@ -1,21 +1,28 @@ -
      -
    • - ext_details->getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> - ext_details->getName()); ?> -
      - - ext_details->isEnabled()) { ?> - - - - - - - -
      - - - -
    • -
    • ext_details->getName() ?>
    • -
    +ext_details === null) { + throw new FreshRSS_Context_Exception('Extension not initialised!'); + } + + $name_encoded = urlencode($this->ext_details->getName()); + $ext_enabled = $this->ext_details->isEnabled(); + if ($ext_enabled) { + $button_class = ' active'; + $name_class = ''; + $action = 'disable'; + $title = _t('gen.action.disable'); + } else { + $button_class = ''; + $name_class = ' disabled'; + $action = 'enable'; + $title = _t('gen.action.enable'); + } + if ($this->ext_details->getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) {?> + + + ext_details->getName() ?> + + + ext_details->getName() ?> + diff --git a/app/views/helpers/feed/update.phtml b/app/views/helpers/feed/update.phtml index 50a9ac9ccba..2e4fc1ac264 100644 --- a/app/views/helpers/feed/update.phtml +++ b/app/views/helpers/feed/update.phtml @@ -1,4 +1,11 @@ -
    +feed === null) { + return; + } +?> +

    feed->name() ?>

    @@ -7,487 +14,990 @@
    -

    feed->description()) ?>

    + feed->inError()): ?> +

    + +
    + feed->lastError() > 1) { ?> + feed->lastError()), timeago($this->feed->lastError())) ?>
    + + +

    + + feed->lastUpdate()), timeago($this->feed->lastUpdate())) ?> + feed->newestArticleReceivedDate() > 0): ?>
    + feed->newestArticleReceivedDate()), timeago($this->feed->newestArticleReceivedDate())) ?> + + feed->newestArticlePublicationDate() > 0): ?>
    + feed->newestArticlePublicationDate()), timeago($this->feed->newestArticlePublicationDate())) ?> + +

    feed->nbEntries(); ?> - - feed->inError()) { ?> -

    - -

    - - -
    + +

    + + + displaySlider; + if (!$ajax) { + $from = 'feed'; + $slider = []; + } elseif ($from === '') { + $from = $this->cfrom ?: 'index'; + } + if ($from === '') { + $url = _url('subscription', 'feed', 'id', $this->feed->id(), ...$slider); + } else { + $get = Minz_Request::paramString('get'); + if ($get === '') { + $url = _url('subscription', 'feed', 'id', $this->feed->id(), 'from', $from, 'error', Minz_Request::paramBoolean('error') ? 1 : 0, ...$slider); + } else { + $url = _url('subscription', 'feed', 'id', $this->feed->id(), 'from', $from, 'get', $get, ...$slider); + } + } + ?> + - -
    - -
    - +
    + +
    + feed->favicon(); + $originalIconUrl = ''; + if ($this->feed->customFavicon()) { + $this->feed->_attribute('customFavicon', false); + $this->feed->resetFaviconHash(); + } + $originalIconUrl = $this->feed->favicon(); + $this->feed->_attribute('customFavicon', $currentIconUrl !== $originalIconUrl); + $this->feed->resetFaviconHash(); + $ext_url = Minz_ExtensionManager::callHook(Minz_HookType::CustomFaviconBtnUrl, $this->feed); + ?> +
    +
    + ✇ +
    + + + + + + +
    +

    feed->customFaviconExt() ?? '', ENT_NOQUOTES, 'UTF-8')) ?>

    +

    +
    -
    -
    - -
    - +
    + +
    + +
    -
    -
    - -
    -
    - - +
    + +
    +
    -
    -
    - -
    -
    - - +
    + +
    +
    + + +
    +
    +
    +
    + +
    +
    + + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    - + feed->attributeBoolean('show_unread_count'); + $sucLocked = $this->feed->priority() >= FreshRSS_Feed::PRIORITY_IMPORTANT && + FreshRSS_Context::userConf()->show_unread_count !== 'none'; + ?> +
    + +
    + + + + +
    -
    -
    - -
    - + +
    + +
    + +
    -
    -
    - -
    - + + feed->defaultSort(); + $feedDefaultOrder = $this->feed->defaultOrder(); + $feedDefaultSortOrder = $feedDefaultSort !== null + ? ($feedDefaultSort === 'rand' ? 'rand' : $feedDefaultSort . '_' . strtolower($feedDefaultOrder ?? 'desc')) + : ''; + ?> +
    + +
    + +
    -
    -
    - -
    - - + ?> +
    + + +

    +
    -
    - feed->pubSubHubbubEnabled()) { ?>
    +
    -
    - -
    -
    - - - + feed->pubSubHubbubEnabled()) { ?> +
    +
    + +
    +
    + + +
    +
    + + + feed->id()); + } else { + $url = _url('feed', 'delete', 'id', $this->feed->id(), 'from', $from); + } + ?> + +
    -
    + - - feed->httpAuth(false); ?> -
    - -
    - -

    +
    + + feed->httpAuth(false); ?> +
    + +
    + +

    +
    -
    -
    - -
    -
    - - +
    + +
    +
    + + +
    -
    -
    -
    - - +
    +
    + + +
    -
    + - -
    - -
    - +
    -
    -
    - -
    - +
    + +
    + + +
    -
    -
    - -
    - +
    -
    -
    - -
    - + +
    -
    -
    - -
    - -

    +
    + +
    + +

    +

    + +

    +
    -
    -
    -
    - - +
    + +
    + +
    -
    - +
    +
    + + +
    +
    + -
    -
    - - feed->attributes('archiving'); - if (empty($archiving)) { - $archiving = [ 'default' => true ]; - } else { - $archiving['default'] = false; - } - $volatile = [ - 'enable_keep_period' => false, - 'keep_period_count' => '3', - 'keep_period_unit' => 'P1M', - ]; - if (!empty($archiving['keep_period'])) { - if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { - $volatile['enable_keep_period'] = true; - $volatile['keep_period_count'] = $matches['count']; - $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); + feed->attributeArray('archiving'); + /** @var array{default?:bool,keep_period?:string,keep_max?:int,keep_min?:int,keep_favourites?:bool,keep_labels?:bool,keep_unreads?:bool}|null $archiving */ + if (empty($archiving)) { + $archiving = [ 'default' => true ]; + } else { + $archiving['default'] = false; } - } - //Defaults - if (!isset($archiving['keep_max'])) { - $archiving['keep_max'] = false; - } - if (!isset($archiving['keep_min'])) { - $archiving['keep_min'] = 50; - } - if (!isset($archiving['keep_favourites'])) { - $archiving['keep_favourites'] = true; - } - if (!isset($archiving['keep_labels'])) { - $archiving['keep_labels'] = true; - } - if (!isset($archiving['keep_unreads'])) { - $archiving['keep_unreads'] = false; - } - ?> - -

    - -

    + $volatile = [ + 'enable_keep_period' => false, + 'keep_period_count' => '3', + 'keep_period_unit' => 'P1M', + ]; + if (!empty($archiving['keep_period']) && is_string($archiving['keep_period'])) { + if (preg_match('/^PT?(?P\d+)[YMWDH]$/', $archiving['keep_period'], $matches)) { + $volatile['enable_keep_period'] = true; + $volatile['keep_period_count'] = $matches['count']; + $volatile['keep_period_unit'] = str_replace($matches['count'], '1', $archiving['keep_period']); + } + } + //Defaults + if (!isset($archiving['keep_max']) || !is_int($archiving['keep_max'])) { + $archiving['keep_max'] = 0; + } + if (!isset($archiving['keep_min']) || !is_int($archiving['keep_min'])) { + $archiving['keep_min'] = 50; + } + if (!isset($archiving['keep_favourites']) || !is_bool($archiving['keep_favourites'])) { + $archiving['keep_favourites'] = true; + } + if (!isset($archiving['keep_labels']) || !is_bool($archiving['keep_labels'])) { + $archiving['keep_labels'] = true; + } + if (!isset($archiving['keep_unreads']) || !is_bool($archiving['keep_unreads'])) { + $archiving['keep_unreads'] = false; + } + ?> -
    - -
    - +

    + +

    + +
    +
    +
    + +
    -
    - -