Merge branch 'edge' into mention-ttrss-exporter

This commit is contained in:
maTh 2024-03-05 22:12:58 +01:00 committed by GitHub
commit 26e6698393
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
535 changed files with 12825 additions and 11331 deletions

View File

@ -13,6 +13,8 @@ or as [GitHub Codespaces](https://github.com/features/codespaces) simply in a We
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.:

View File

@ -9,4 +9,4 @@ cp ./Docker/*.Apache.conf /etc/apache2/conf.d/
chown -R developer:www-data /home/developer/freshrss-data
chmod -R g+rwX /home/developer/freshrss-data
httpd
httpd -c 'ErrorLog "/var/log/apache2/error.log"' -c 'CustomLog "/var/log/apache2/access.log" combined_proxy'

View File

@ -1,6 +1,10 @@
/.devcontainer/
/.git/
/.github/
/bin/
/data/
/docs/
/extensions/node_modules/
/extensions/vendor/
/node_modules/
/vendor/

View File

@ -1,5 +1,5 @@
*.min.js
.git/
*.min.js
extensions/
node_modules/
p/scripts/vendor/

93
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,93 @@
name: Publish Docker images
on:
push:
branches:
- edge
release:
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@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Checkout
uses: actions/checkout@v4
- 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@v5
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@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker images
uses: docker/build-push-action@v5
with:
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' }}

View File

@ -0,0 +1,24 @@
name: Update Docker Hub description
on:
push:
paths:
- Docker/README.md
branches:
- edge
workflow_dispatch:
jobs:
dockerhub-description:
if: github.repository_owner == 'FreshRSS'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update repo description
uses: peter-evans/dockerhub-description@dc67fad7001ef9e8e3c124cb7a64e16d0a63d864
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: freshrss/freshrss
readme-filepath: Docker/README.md

51
.github/workflows/jekyll-gh-pages.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# 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"]
# 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@v4
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Build with Jekyll
uses: actions/jekyll-build-pages@v1
with:
source: ./docs/
destination: ./_site
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
# 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@v2

View File

@ -14,7 +14,7 @@ jobs:
steps:
- name: Git checkout source code
uses: actions/checkout@v3
uses: actions/checkout@v4
# Composer tests
@ -55,10 +55,10 @@ jobs:
# NPM tests
- name: Uses Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
# https://nodejs.org/en/about/releases/
node-version: '18'
# https://nodejs.org/en/about/previous-releases
node-version: '20'
cache: 'npm'
- run: npm ci
@ -82,14 +82,14 @@ jobs:
uses: actions/cache@v3
with:
path: bin
key: ${{ runner.os }}-bin-shfmt@v3.6.0-hadolint@v2.12.0-typos@v1.13.6
key: ${{ runner.os }}-bin-shfmt@v3.7.0-hadolint@v2.12.0-typos@v1.17.0
- name: Add ./bin/ to $PATH
run: mkdir -p bin/ && echo "${PWD}/bin" >> $GITHUB_PATH
- name: Install shfmt
if: steps.shell-cache.outputs.cache-hit != 'true'
run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0
run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.7.0
- name: Check shell script syntax
# shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2204-Readme.md
@ -106,7 +106,7 @@ jobs:
if: steps.shell-cache.outputs.cache-hit != 'true'
run: |
cd bin ;
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' &&
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.17.0/typos-v1.17.0-x86_64-unknown-linux-musl.tar.gz' &&
tar -xvf *.tar.gz './typos' &&
chmod +x typos &&
rm *.tar.gz ;

2
.gitignore vendored
View File

@ -1,4 +1,6 @@
/bin/
/extensions/node_modules/
/extensions/vendor/
/node_modules/
/vendor/
/data.back/

View File

@ -1,4 +1,5 @@
.git/
extensions/
node_modules/
p/scripts/bcrypt.min.js
p/scripts/vendor/

View File

@ -1,4 +1,5 @@
.git/
extensions/
lib/marienfressinaud/
lib/phpgt/
lib/phpmailer/

View File

@ -2,7 +2,8 @@
"extends": "stylelint-config-recommended-scss",
"plugins": [
"stylelint-order",
"stylelint-scss"
"stylelint-scss",
"stylelint-stylistic"
],
"rules": {
"at-rule-empty-line-before": [
@ -10,27 +11,27 @@
"ignoreAtRules": [ "after-comment", "else" ]
}
],
"at-rule-name-space-after": [
"stylistic/at-rule-name-space-after": [
"always", {
"ignoreAtRules": [ "after-comment" ]
}
],
"block-closing-brace-newline-after": [
"stylistic/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",
"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,
"declaration-colon-space-after": "always",
"declaration-colon-space-before": "never",
"indentation": "tab",
"stylistic/declaration-colon-space-after": "always",
"stylistic/declaration-colon-space-before": "never",
"stylistic/indentation": "tab",
"no-descending-specificity": null,
"no-eol-whitespace": true,
"stylistic/no-eol-whitespace": true,
"property-no-vendor-prefix": true,
"rule-empty-line-before": [
"always", {

View File

@ -16,10 +16,12 @@ extend-exclude = [
".git/",
"app/i18n/cz/",
"app/i18n/de/",
"app/i18n/el/",
"app/i18n/es/",
"app/i18n/fa/",
"app/i18n/fr/",
"app/i18n/he/",
"app/i18n/hu/",
"app/i18n/id/",
"app/i18n/it/",
"app/i18n/ja/",

View File

@ -1,6 +1,259 @@
# FreshRSS changelog
## 2023-XX-XX FreshRSS 1.21.1-dev
## 2024-XX-XX FreshRSS 1.23.2-dev
## 2023-12-30 FreshRSS 1.23.1
* Bug fixing
* Fix crash regression with the option *Max number of tags shown* [#5978](https://github.com/FreshRSS/FreshRSS/pull/5978)
* Fix crash regression when enabling extensions defined by old FreshRSS installations [#5979](https://github.com/FreshRSS/FreshRSS/pull/5979)
* Fix crash regression during export when using MySQL [#5988](https://github.com/FreshRSS/FreshRSS/pull/5988)
* More robust assignment of categories to feeds [#5986](https://github.com/FreshRSS/FreshRSS/pull/5986)
* Fix `base_url` being cleared when saving settings [#5992](https://github.com/FreshRSS/FreshRSS/pull/5992)
* Fix unwanted button in UI of update page [#5999](https://github.com/FreshRSS/FreshRSS/pull/5999)
* Deployment
* Exclude more folders with `.dockerignore` [#5996](https://github.com/FreshRSS/FreshRSS/pull/5996)
* i18n
* Improve simplified Chinese [#5977](https://github.com/FreshRSS/FreshRSS/pull/5977)
* Improve Hungarian [#6000](https://github.com/FreshRSS/FreshRSS/pull/6000)
## 2023-12-24 FreshRSS 1.23.0
* Features
* New *Important feeds* group in the main view, with corresponding new priority level for feeds [#5782](https://github.com/FreshRSS/FreshRSS/pull/5782)
* Entries from important feeds are not marked as read during *scroll*, during *focus*, nor during *Mark all as read*
* Add filter actions (auto mark as read) at category level and at global levels [#5942](https://github.com/FreshRSS/FreshRSS/pull/5942)
* Improve reliability of *Max number of articles to keep unread* [#5905](https://github.com/FreshRSS/FreshRSS/pull/5905)
* New option to mark entries as read when focused from keyboard shortcut [5812](https://github.com/FreshRSS/FreshRSS/pull/5812)
* New display option to hide *My labels* in article footers [#5884](https://github.com/FreshRSS/FreshRSS/pull/5884)
* Add support for more thumbnail types in feeds enclosures [#5806](https://github.com/FreshRSS/FreshRSS/pull/5806)
* Support for favicons with non-absolute paths [#5839](https://github.com/FreshRSS/FreshRSS/pull/5839)
* Increase SQL (`VARCHAR`) text fields length to maximum possible [#5788](https://github.com/FreshRSS/FreshRSS/pull/5788)
* Increase SQL date fields to 64-bit to be ready for year 2038+ [#5570](https://github.com/FreshRSS/FreshRSS/pull/5570)
* Compatibility
* Require PHP 7.4+, and implement *typed properties* [#5720](https://github.com/FreshRSS/FreshRSS/pull/5720)
* Soft require Apache 2.4+ (but repair minimal compatibility with Apache 2.2) [#5791](https://github.com/FreshRSS/FreshRSS/pull/5791), [#5804](https://github.com/FreshRSS/FreshRSS/pull/5804)
* Bug fixing
* Fix regression in Docker `CRON_MIN` if any environment variable contains a single quote [#5795](https://github.com/FreshRSS/FreshRSS/pull/5795)
* Improve filtering of cron environment variables [#5898](https://github.com/FreshRSS/FreshRSS/pull/5898)
* Fix the `TRUSTED_PROXY` environment variable used in combination with *trusted sources* [#5853](https://github.com/FreshRSS/FreshRSS/pull/5853)
* Fix regression in marking as read if an identical title already exists [#5937](https://github.com/FreshRSS/FreshRSS/pull/5937)
* Fix JavaScript regression in label dropdown [#5785](https://github.com/FreshRSS/FreshRSS/pull/5785)
* Fix regression when renaming a label [#5842](https://github.com/FreshRSS/FreshRSS/pull/5842)
* Fix API for adding feed with a title [#5868](https://github.com/FreshRSS/FreshRSS/pull/5868)
* Fix regression in UI of update page [#5802](https://github.com/FreshRSS/FreshRSS/pull/5802)
* Fix XPath encoding [#5912](https://github.com/FreshRSS/FreshRSS/pull/5912)
* Fix notifications, in particular during login [#5959](https://github.com/FreshRSS/FreshRSS/pull/5959)
* Deployment
* Use GitHub Actions to build Docker images, offering architectures `amd64`, `arm32v7`, `arm64v8` with automatic detection [#5808](https://github.com/FreshRSS/FreshRSS/pull/5808)
* Docker alternative image updated to Alpine 3.19 with PHP 8.2.13 and Apache 2.4.58 [#5383](https://github.com/FreshRSS/FreshRSS/pull/5383)
* Extensions
* Upgrade extensions code to PHP 7.4+ [#5901](https://github.com/FreshRSS/FreshRSS/pull/5901), [#5957](https://github.com/FreshRSS/FreshRSS/pull/5957)
* Breaking change: upgraded extensions require FreshRSS 1.23.0+ [Extensions#181](https://github.com/FreshRSS/Extensions/pull/181)
* Pass FreshRSS version to JavaScript client side for extensions [#5902](https://github.com/FreshRSS/FreshRSS/pull/5902)
* Add GitHub Actions and PHPStan for automatic testing of the Extensions repository [Extensions#185](https://github.com/FreshRSS/Extensions/pull/185)
* API
* Improve handling of new lines in enclosure descriptions (e.g., YouTube video descriptions) [#5859](https://github.com/FreshRSS/FreshRSS/pull/5859)
* Security
* Avoid printing exceptions in favicons [#5867](https://github.com/FreshRSS/FreshRSS/pull/5867)
* Remove unneeded execution permissions on some files [#5831](https://github.com/FreshRSS/FreshRSS/pull/5831)
* UI
* Ensure that enough articles are loaded on window resize [#5815](https://github.com/FreshRSS/FreshRSS/pull/5815)
* Improve *Nord* theme [#5885](https://github.com/FreshRSS/FreshRSS/pull/5885)
* Do not show message *Add some feeds* [#5827](https://github.com/FreshRSS/FreshRSS/pull/5827)
* Various UI and style improvements [#5886](https://github.com/FreshRSS/FreshRSS/pull/5886)
* i18n
* Fix font priority for languages using Han characters [#5930](https://github.com/FreshRSS/FreshRSS/pull/5930)
* Improve Dutch [#5796](https://github.com/FreshRSS/FreshRSS/pull/5796)
* Improve Hungarian [#5918](https://github.com/FreshRSS/FreshRSS/pull/5918)
* Misc.
* Increase PHPStan from Level 7 to [Level 8](https://phpstan.org/user-guide/rule-levels) [#5946](https://github.com/FreshRSS/FreshRSS/pull/5946)
* Compatibility PHP 8.2+ for running automated tests [#5826](https://github.com/FreshRSS/FreshRSS/pull/5826)
* Use PHP [`declare(strict_types=1);`](https://php.net/language.types.declarations#language.types.declarations.strict) [#5830](https://github.com/FreshRSS/FreshRSS/pull/5830)
* Better stack trace for SQL errors [#5916](https://github.com/FreshRSS/FreshRSS/pull/5916)
* Code improvements [#5511](https://github.com/FreshRSS/FreshRSS/pull/5511), [#5945](https://github.com/FreshRSS/FreshRSS/pull/5945)
* Update dev dependencies [#5787](https://github.com/FreshRSS/FreshRSS/pull/5787)
## 2023-10-30 FreshRSS 1.22.1
* Bug fixing
* Fix regression in i18n English fallback for extensions [#5752](https://github.com/FreshRSS/FreshRSS/pull/5752)
* Fix identification of thumbnails [#5750](https://github.com/FreshRSS/FreshRSS/pull/5750)
* OpenID Connect compatibility with colon `:` in `OIDC_SCOPES` [#5753](https://github.com/FreshRSS/FreshRSS/pull/5753), [#5764](https://github.com/FreshRSS/FreshRSS/pull/5764)
* Avoid a warning on non-numeric `TRUSTED_PROXY` environment variable [#5733](https://github.com/FreshRSS/FreshRSS/pull/5733)
* Better identification of proxied client IP with `RemoteIPInternalProxy` in Apache [#5740](https://github.com/FreshRSS/FreshRSS/pull/5740)
* Deployment
* Export all environment variables to cron (to allow custom environment variables such as for Kubernetes) [#5772](https://github.com/FreshRSS/FreshRSS/pull/5772)
* Docker: Upgraded Alpine dev image `freshrss/freshrss:newest` to PHP 8.3 and Apache 2.4.58 [#5764](https://github.com/FreshRSS/FreshRSS/pull/5764)
* Compatibility
* Test compatibility with PHP 8.3 [#5764](https://github.com/FreshRSS/FreshRSS/pull/5764)
* UI
* Improve *Origine* theme (dark mode) [#5745](https://github.com/FreshRSS/FreshRSS/pull/5745)
* Improve *Nord* theme [#5754](https://github.com/FreshRSS/FreshRSS/pull/5754)
* Various UI and style improvements [#5737](https://github.com/FreshRSS/FreshRSS/pull/5737), [#5765](https://github.com/FreshRSS/FreshRSS/pull/5765),
[#5773](https://github.com/FreshRSS/FreshRSS/pull/5773), [#5774](https://github.com/FreshRSS/FreshRSS/pull/5774)
* i18n
* Better i18n string for feed submenu for mark as read [#5762](https://github.com/FreshRSS/FreshRSS/pull/5762)
* Improve Dutch [#5759](https://github.com/FreshRSS/FreshRSS/pull/5759)
* Misc.
* Move to GitHub Actions for our GitHub Pages [#5681](https://github.com/FreshRSS/FreshRSS/pull/5681)
* Update dev dependencies and use `stylelint-stylistic` [#5766](https://github.com/FreshRSS/FreshRSS/pull/5766)
## 2023-10-23 FreshRSS 1.22.0
* Features
* Add support for OpenID Connect (only in our default Debian-based Docker image for `x86_64`, not Alpine) through [`libapache2-mod-auth-openidc`](https://github.com/OpenIDC/mod_auth_openidc)
[#5351](https://github.com/FreshRSS/FreshRSS/pull/5351), [#5463](https://github.com/FreshRSS/FreshRSS/pull/5463), [#5481](https://github.com/FreshRSS/FreshRSS/pull/5481),
[#5523](https://github.com/FreshRSS/FreshRSS/pull/5523), [#5646](https://github.com/FreshRSS/FreshRSS/pull/5646)
* Allow sharing in anonymous mode [#5261](https://github.com/FreshRSS/FreshRSS/pull/5261)
* Support Unix socket for MySQL / MariaDB [#5166](https://github.com/FreshRSS/FreshRSS/pull/5166)
* Use proxy settings also for fetching favicons [#5421](https://github.com/FreshRSS/FreshRSS/pull/5421)
* Add mutual exclusion semaphore for better scaling of actualize script [#5235](https://github.com/FreshRSS/FreshRSS/pull/5235)
* Better reporting of XPath failures [#5317](https://github.com/FreshRSS/FreshRSS/pull/5317)
* Add sharing with Buffer.com [#5286](https://github.com/FreshRSS/FreshRSS/pull/5286)
* Add sharing with Omnivore [#5477](https://github.com/FreshRSS/FreshRSS/pull/5477)
* Improve sharing with Linkding [#5433](https://github.com/FreshRSS/FreshRSS/pull/5433)
* Do not automatically update feeds after import, to better support multiple imports [#5629](https://github.com/FreshRSS/FreshRSS/pull/5629)
* Compatibility for servers disabling `set_time_limit()` [#5675](https://github.com/FreshRSS/FreshRSS/pull/5675)
* New configuration constant `CLEANCACHE_HOURS` [#5144](https://github.com/FreshRSS/FreshRSS/pull/5144)
* Bug fixing
* Fix cache refresh [#5562](https://github.com/FreshRSS/FreshRSS/pull/5562)
* Fix and improvement of hash of articles using *load full content* [#5576](https://github.com/FreshRSS/FreshRSS/pull/5576)
* Fix case of falsy GUIDs [#5412](https://github.com/FreshRSS/FreshRSS/pull/5412)
* Fix and improve JSON export/import [#5332](https://github.com/FreshRSS/FreshRSS/pull/5332), [#5626](https://github.com/FreshRSS/FreshRSS/pull/5626)
* Fix enclosures in RSS output [#5540](https://github.com/FreshRSS/FreshRSS/pull/5540)
* Fix parenthesis escaping bug in searches [#5633](https://github.com/FreshRSS/FreshRSS/pull/5633)
* Fix regression in Fever API enclosures [#5214](https://github.com/FreshRSS/FreshRSS/pull/5214)
* Fix regression in Fever API mark-all-as-read [#5185](https://github.com/FreshRSS/FreshRSS/pull/5185)
* Fix regression in OPML export of single feeds [#5238](https://github.com/FreshRSS/FreshRSS/pull/5238)
* Fix warning during OPML export with empty attributes [#5559](https://github.com/FreshRSS/FreshRSS/pull/5559)
* Fix extensions in *actualize script* [#5243](https://github.com/FreshRSS/FreshRSS/pull/5243)
* Fix link to configuration (system or user) for extensions [#5394](https://github.com/FreshRSS/FreshRSS/pull/5394)
* Fix *mark as read upon gone* option in some conditions [#5315](https://github.com/FreshRSS/FreshRSS/pull/5315),
[#5382](https://github.com/FreshRSS/FreshRSS/pull/5382), [#5404](https://github.com/FreshRSS/FreshRSS/pull/5404)
* Fix *mark selection as unread* [#5367](https://github.com/FreshRSS/FreshRSS/pull/5367)
* Fix warning in articles repartition statistics [#5228](https://github.com/FreshRSS/FreshRSS/pull/5228)
* Fix count entries with some databases [#5368](https://github.com/FreshRSS/FreshRSS/pull/5368)
* Fix MariaDB database size calculation [#5655](https://github.com/FreshRSS/FreshRSS/pull/5655)
* Fix feed position attribute [#5203](https://github.com/FreshRSS/FreshRSS/pull/5203)
* Fix warning when tagging entries [#5221](https://github.com/FreshRSS/FreshRSS/pull/5221)
* Fix labels in anonymous mode [#5650](https://github.com/FreshRSS/FreshRSS/pull/5650)
* Fix bug not allowing strings for tags in XPath [#5653](https://github.com/FreshRSS/FreshRSS/pull/5653)
* Fix get and order when saving user query [#5515](https://github.com/FreshRSS/FreshRSS/pull/5515)
* Fix search using user queries [#5669](https://github.com/FreshRSS/FreshRSS/pull/5669)
* Fix regression of access to logs even when auto-update is disabled [#5577](https://github.com/FreshRSS/FreshRSS/pull/5577)
* Fix access to Apache logs from Dev Container [#5660](https://github.com/FreshRSS/FreshRSS/pull/5660)
* Fix malformed HTTP header in case of internal fatal error [#5699](https://github.com/FreshRSS/FreshRSS/pull/5699)
* Fix rare exception for HTML notifications [#5690](https://github.com/FreshRSS/FreshRSS/pull/5690)
* UI
* New option to display website name and/or favicon of articles [#4969](https://github.com/FreshRSS/FreshRSS/pull/4969)
* Support `<meta name="theme-color" .../>` [#5105](https://github.com/FreshRSS/FreshRSS/pull/5105)
* Config user settings in slider [#5094](https://github.com/FreshRSS/FreshRSS/pull/5094)
* Improve theme selector [#5281](https://github.com/FreshRSS/FreshRSS/pull/5281), [#5688](https://github.com/FreshRSS/FreshRSS/pull/5688)
* Improve *share to clipboard* with animation and icon [#5295](https://github.com/FreshRSS/FreshRSS/pull/5295)
* Allow *share to clipboard* even for localhost and without HTTPS [#5606](https://github.com/FreshRSS/FreshRSS/pull/5606)
* Feedback when tag with same name as category already exists [#5181](https://github.com/FreshRSS/FreshRSS/pull/5181)
* Show *base URL* in configuration [#5656](https://github.com/FreshRSS/FreshRSS/pull/5656), [#5657](https://github.com/FreshRSS/FreshRSS/pull/5657)
* Show *Terms of Service* in config menu [#5215](https://github.com/FreshRSS/FreshRSS/pull/5215)
* Show *Terms of Service* in footer [#5222](https://github.com/FreshRSS/FreshRSS/pull/5222)
* Improve *about* page [#5192](https://github.com/FreshRSS/FreshRSS/pull/5192)
* Improve *update* page [#5420](https://github.com/FreshRSS/FreshRSS/pull/5420), [#5636](https://github.com/FreshRSS/FreshRSS/pull/5636),
[#5647](https://github.com/FreshRSS/FreshRSS/pull/5647)
* Improve Step 1 of install process [#5350](https://github.com/FreshRSS/FreshRSS/pull/5350)
* Improve *Global view* on mobile [#5297](https://github.com/FreshRSS/FreshRSS/pull/5297)
* Reduce network overhead for Global view [#5496](https://github.com/FreshRSS/FreshRSS/pull/5496)
* Fix *Global view*: Stick the article to the top when opened [#5153](https://github.com/FreshRSS/FreshRSS/pull/5153)
* Fix configuration views that are using a slider [#5469](https://github.com/FreshRSS/FreshRSS/pull/5469)
* Fix highlight next/prev article while using shortcuts [#5211](https://github.com/FreshRSS/FreshRSS/pull/5211)
* Fix regression in statistics column name *% of total* [#5232](https://github.com/FreshRSS/FreshRSS/pull/5232)
* Fix macOS feed title meta-click behaviour [#5492](https://github.com/FreshRSS/FreshRSS/pull/5492)
* Improve themes
* *Origine* (dark mode) [#5229](https://github.com/FreshRSS/FreshRSS/pull/5229),
[#5288](https://github.com/FreshRSS/FreshRSS/pull/5288), [#5437](https://github.com/FreshRSS/FreshRSS/pull/5437)
* *Alternative Dark* [#5206](https://github.com/FreshRSS/FreshRSS/pull/5206)
* *Ansum* / *Mapco* [#5453](https://github.com/FreshRSS/FreshRSS/pull/5453)
* *Dark* [#5280](https://github.com/FreshRSS/FreshRSS/pull/5280), [#5439](https://github.com/FreshRSS/FreshRSS/pull/5439)
* *Flat* (un-deprecated) [#5316](https://github.com/FreshRSS/FreshRSS/pull/5316)
* *Nord* [#5689](https://github.com/FreshRSS/FreshRSS/pull/5689), [#5719](https://github.com/FreshRSS/FreshRSS/pull/5719)
* Delete previously deprecated themes: *BlueLagoon*, *Screwdriver* [#5374](https://github.com/FreshRSS/FreshRSS/pull/5374),
[#5694](https://github.com/FreshRSS/FreshRSS/pull/5694)
* Various UI and style improvements [#5147](https://github.com/FreshRSS/FreshRSS/pull/5147), [#5216](https://github.com/FreshRSS/FreshRSS/pull/5216),
[#5303](https://github.com/FreshRSS/FreshRSS/pull/5303), [#5304](https://github.com/FreshRSS/FreshRSS/pull/5304), [#5397](https://github.com/FreshRSS/FreshRSS/pull/5397),
[#5398](https://github.com/FreshRSS/FreshRSS/pull/5398), [#5400](https://github.com/FreshRSS/FreshRSS/pull/5400), [#5603](https://github.com/FreshRSS/FreshRSS/pull/5603),
[#5695](https://github.com/FreshRSS/FreshRSS/pull/5695)
* Security
* Rework trusted proxies (especially with Docker) [#5549](https://github.com/FreshRSS/FreshRSS/pull/5549)
* Automatic trusted sources during install [#5358](https://github.com/FreshRSS/FreshRSS/pull/5358)
* Show remote IP address in case of HTTP Basic Auth error [#5314](https://github.com/FreshRSS/FreshRSS/pull/5314)
* Deployment
* Docker listen on all interfaces by default, including IPv6 [#5180](https://github.com/FreshRSS/FreshRSS/pull/5180)
* Docker default image updated to Debian 12 Bookworm with PHP 8.2.7 and Apache 2.4.57 [#5461](https://github.com/FreshRSS/FreshRSS/pull/5461)
* Docker alternative image updated to Alpine 3.18 with PHP 8.1.23 and Apache 2.4.58 [#5383](https://github.com/FreshRSS/FreshRSS/pull/5383)
* Docker quiet Apache `a2enmod` [#5464](https://github.com/FreshRSS/FreshRSS/pull/5464)
* Docker: Add `DATA_PATH` to cron env [#5531](https://github.com/FreshRSS/FreshRSS/pull/5531)
* i18n
* Fix i18n for automatic dark mode configuration [#5168](https://github.com/FreshRSS/FreshRSS/pull/5168)
* Clarify that maximum number to keep is per feed [#5458](https://github.com/FreshRSS/FreshRSS/pull/5458)
* Add Hungarian [#5589](https://github.com/FreshRSS/FreshRSS/pull/5589), [#5593](https://github.com/FreshRSS/FreshRSS/pull/5593)
* Add Latvian [#5254](https://github.com/FreshRSS/FreshRSS/pull/5254)
* Add Persian [#5571](https://github.com/FreshRSS/FreshRSS/pull/5571)
* Remove unneeded quotes in feed warning [#5480](https://github.com/FreshRSS/FreshRSS/pull/5480)
* Improve German [#5171](https://github.com/FreshRSS/FreshRSS/pull/5171), [#5468](https://github.com/FreshRSS/FreshRSS/pull/5468),
[#5640](https://github.com/FreshRSS/FreshRSS/pull/5640)
* Improve Spanish [#5408](https://github.com/FreshRSS/FreshRSS/pull/5408), [#5436](https://github.com/FreshRSS/FreshRSS/pull/5436),
[#5609](https://github.com/FreshRSS/FreshRSS/pull/5609)
* Extensions
* Fix fallback to English for extensions [#5426](https://github.com/FreshRSS/FreshRSS/pull/5426)
* Allow deep-link to extension configuration [#5449](https://github.com/FreshRSS/FreshRSS/pull/5449)
* New extension hook `entry_auto_read` [#5505](https://github.com/FreshRSS/FreshRSS/pull/5505), [#5561](https://github.com/FreshRSS/FreshRSS/pull/5561)
* Simplify extension method [#5234](https://github.com/FreshRSS/FreshRSS/pull/5234)
* Remove obsolete core extensions *Google Group* and *Tumblr* [#5457](https://github.com/FreshRSS/FreshRSS/pull/5457)
* SimplePie
* Fix `error_reporting` for PHP 8.1+ [#5199](https://github.com/FreshRSS/FreshRSS/pull/5199)
* Misc.
* Reduce database locks [#5576](https://github.com/FreshRSS/FreshRSS/pull/5576), [#5625](https://github.com/FreshRSS/FreshRSS/pull/5625),
[#5648](https://github.com/FreshRSS/FreshRSS/pull/5648), [#5649](https://github.com/FreshRSS/FreshRSS/pull/5649)
* Improve MySQL / MariaDB performance for updating cached SQL values [#5648](https://github.com/FreshRSS/FreshRSS/pull/5648)
* Increase time limit import OPML [#5231](https://github.com/FreshRSS/FreshRSS/pull/5231)
* Save SQL attributes as native Unicode [#5371](https://github.com/FreshRSS/FreshRSS/pull/5371)
* Remove old SQL auto-updates [#5625](https://github.com/FreshRSS/FreshRSS/pull/5625), [#5649](https://github.com/FreshRSS/FreshRSS/pull/5649)
* Improve Dev Container (update to Alpine 3.18, use `DATA_PATH` environment variable) [#5423](https://github.com/FreshRSS/FreshRSS/pull/5423)
* Update `lib_opml` [#5188](https://github.com/FreshRSS/FreshRSS/pull/5188)
* Update `lib/http-conditional` [#5277](https://github.com/FreshRSS/FreshRSS/pull/5277)
* Update *PHPMailer* [#5389](https://github.com/FreshRSS/FreshRSS/pull/5389)
* Use typed access to request parameters [#5267](https://github.com/FreshRSS/FreshRSS/pull/5267)
* Typed view model classes [#5380](https://github.com/FreshRSS/FreshRSS/pull/5380)
* Remove `ConfigurationSetter` [#5251](https://github.com/FreshRSS/FreshRSS/pull/5251), [#5580](https://github.com/FreshRSS/FreshRSS/pull/5580)
* Ignore `./data.back/` in `.gitignore` [#5359](https://github.com/FreshRSS/FreshRSS/pull/5359)
* Composer dev command compatibility with macOS [#5379](https://github.com/FreshRSS/FreshRSS/pull/5379)
* Code improvements [#5089](https://github.com/FreshRSS/FreshRSS/pull/5089),
[#5212](https://github.com/FreshRSS/FreshRSS/pull/5212), [#5213](https://github.com/FreshRSS/FreshRSS/pull/5213), [#5362](https://github.com/FreshRSS/FreshRSS/pull/5362),
[#5470](https://github.com/FreshRSS/FreshRSS/pull/5470), [#5501](https://github.com/FreshRSS/FreshRSS/pull/5501), [#5504](https://github.com/FreshRSS/FreshRSS/pull/5504),
[#5667](https://github.com/FreshRSS/FreshRSS/pull/5667)
* Increase PHPStan from Level 5 to [level 7](https://phpstan.org/user-guide/rule-levels) [#4112](https://github.com/FreshRSS/FreshRSS/issues/4112),
[#5064](https://github.com/FreshRSS/FreshRSS/pull/5064), [#5087](https://github.com/FreshRSS/FreshRSS/pull/5087), [#5090](https://github.com/FreshRSS/FreshRSS/pull/5090),
[#5106](https://github.com/FreshRSS/FreshRSS/pull/5106), [#5108](https://github.com/FreshRSS/FreshRSS/pull/5108), [#5230](https://github.com/FreshRSS/FreshRSS/pull/5230),
[#5239](https://github.com/FreshRSS/FreshRSS/pull/5239), [#5258](https://github.com/FreshRSS/FreshRSS/pull/5258), [#5263](https://github.com/FreshRSS/FreshRSS/pull/5263),
[#5264](https://github.com/FreshRSS/FreshRSS/pull/5264), [#5269](https://github.com/FreshRSS/FreshRSS/pull/5269), [#5272](https://github.com/FreshRSS/FreshRSS/pull/5272),
[#5275](https://github.com/FreshRSS/FreshRSS/pull/5275), [#5279](https://github.com/FreshRSS/FreshRSS/pull/5279), [#5282](https://github.com/FreshRSS/FreshRSS/pull/5282),
[#5283](https://github.com/FreshRSS/FreshRSS/pull/5283), [#5289](https://github.com/FreshRSS/FreshRSS/pull/5289), [#5290](https://github.com/FreshRSS/FreshRSS/pull/5290),
[#5291](https://github.com/FreshRSS/FreshRSS/pull/5291), [#5292](https://github.com/FreshRSS/FreshRSS/pull/5292), [#5299](https://github.com/FreshRSS/FreshRSS/pull/5299),
[#5305](https://github.com/FreshRSS/FreshRSS/pull/5305), [#5307](https://github.com/FreshRSS/FreshRSS/pull/5307), [#5309](https://github.com/FreshRSS/FreshRSS/pull/5309),
[#5313](https://github.com/FreshRSS/FreshRSS/pull/5313), [#5318](https://github.com/FreshRSS/FreshRSS/pull/5318), [#5319](https://github.com/FreshRSS/FreshRSS/pull/5319),
[#5327](https://github.com/FreshRSS/FreshRSS/pull/5327), [#5328](https://github.com/FreshRSS/FreshRSS/pull/5328), [#5352](https://github.com/FreshRSS/FreshRSS/pull/5352),
[#5353](https://github.com/FreshRSS/FreshRSS/pull/5353), [#5354](https://github.com/FreshRSS/FreshRSS/pull/5354), [#5361](https://github.com/FreshRSS/FreshRSS/pull/5361),
[#5366](https://github.com/FreshRSS/FreshRSS/pull/5366), [#5370](https://github.com/FreshRSS/FreshRSS/pull/5370), [#5373](https://github.com/FreshRSS/FreshRSS/pull/5373),
[#5376](https://github.com/FreshRSS/FreshRSS/pull/5376), [#5384](https://github.com/FreshRSS/FreshRSS/pull/5384), [#5388](https://github.com/FreshRSS/FreshRSS/pull/5388),
[#5393](https://github.com/FreshRSS/FreshRSS/pull/5393), [#5400](https://github.com/FreshRSS/FreshRSS/pull/5400), [#5406](https://github.com/FreshRSS/FreshRSS/pull/5406),
[#5429](https://github.com/FreshRSS/FreshRSS/pull/5429), [#5431](https://github.com/FreshRSS/FreshRSS/pull/5431), [#5434](https://github.com/FreshRSS/FreshRSS/pull/5434),
[#5578](https://github.com/FreshRSS/FreshRSS/pull/5578)
* Update dev dependencies [#5336](https://github.com/FreshRSS/FreshRSS/pull/5336), [#5339](https://github.com/FreshRSS/FreshRSS/pull/5339),
[#5478](https://github.com/FreshRSS/FreshRSS/pull/5478), [#5513](https://github.com/FreshRSS/FreshRSS/pull/5513), [#5541](https://github.com/FreshRSS/FreshRSS/pull/5541),
[#5691](https://github.com/FreshRSS/FreshRSS/pull/5691), [#5693](https://github.com/FreshRSS/FreshRSS/pull/5693)
## 2023-03-04 FreshRSS 1.21.0
@ -1458,7 +1711,7 @@
* Features
* New option `disable_update` (also from CLI) to hide the system to update to new FreshRSS versions [#1436](https://github.com/FreshRSS/FreshRSS/pull/1436)
* Share with nown [#1420](https://github.com/FreshRSS/FreshRSS/pull/1420)
* Share with Known [#1420](https://github.com/FreshRSS/FreshRSS/pull/1420)
* Share with GNU social [#1422](https://github.com/FreshRSS/FreshRSS/issues/1422)
* UI
* New theme *Origine-compact* [#1388](https://github.com/FreshRSS/FreshRSS/pull/1388)

View File

@ -10,6 +10,7 @@ People are sorted by name so please keep this order.
* [312k](https://github.com/312k): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:312k)
* [4xfu](https://github.com/4xfu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:4xfu)
* [Aaron Schif](https://github.com/aaronschif): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:aaronschif)
* [Adam Stephens](https://github.com/adamcstephens): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:adamcstephens)
* [Adrien Dorsaz](https://github.com/Trim): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Trim), [Web](https://adorsaz.ch/)
* [Aidi Stan](https://github.com/aidistan): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:aidistan), [Web](https://aidistan.site/)
@ -20,18 +21,21 @@ People are sorted by name so please keep this order.
* [Amaury Carrade](https://github.com/AmauryCarrade): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:AmauryCarrade), [Web](https://amaury.carrade.eu/)
* [AmirHossein Marjani](https://github.com/Marjani): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Marjani)
* [Amrul Izwan](https://github.com/amrulizwan): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:amrulizwan)
* [András Marczinkó](https://github.com/andris155): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:andris155)
* [Andrew Barrow](https://github.com/acbgbca): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:acbgbca)
* [Andrew Hunter](https://github.com/rexbron): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rexbron)
* [Andrew Rabert](https://github.com/nvllsvm): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nvllsvm), [Web](https://nullsum.net)
* [andris155](https://github.com/andris155): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:andris155)
* [Anton Smirnov](https://github.com/arokettu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:arokettu), [Web](https://sandfox.me/)
* [ArthurHoaro](https://github.com/ArthurHoaro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ArthurHoaro)
* [Artur Weigandt](https://github.com/Art4): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Art4), [Web](https://ruhr.social/@Art4)
* [ASMfreaK](https://github.com/ASMfreaK): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ASMfreaK)
* [Axel Leroy](https://github.com/axeleroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:axeleroy), [Web](https://axel.leroy.sh/)
* [azlux](https://github.com/azlux): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:azlux), [Web](https://azlux.fr/)
* [Balázs Keresztury](https://github.com/belidzs/): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:belidzs), [Web](https://keresztury.com/)
* [Bartosz Taudul](https://github.com/wolfpld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wolfpld), [Web](https://wolf.nereid.pl/)
* [Ben Passmore](https://github.com/passbe): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:passbe), [Web](https://passbe.com/)
* [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
* [Benjamin Reich](https://github.com/b-reich): [contributions](https://github.com/FreshRSS/FreshRSS/commits/edge?author=b-reich), [Web](https://benjaminreich.de/)
* [bluewhale235](https://github.com/BuleWhale): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:BuleWhale)
* [bpatath](https://github.com/bpatath): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bpatath)
* [Brewal Bouvet](https://github.com/Jucgshu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jucgshu), [Web](https://dizolo.eu/)
@ -62,18 +66,23 @@ People are sorted by name so please keep this order.
* [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ealdraed)
* [Ed Sandor](https://github.com/ewsandor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ewsandor), [Web](https://ewsandor.com)
* [Edgardo Ramírez](https://github.com/SoldierCorp): [contributors](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SoldierCorp)
* [EdJoPaTo](https://github.com/EdJoPaTo): [contributions](https://github.com/FreshRSS/FreshRSS/commits/edge?author=EdJoPaTo)
* [equinoxmatt](https://github.com/equinoxmatt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:equinoxmatt)
* [Exerra](https://github.com/Exerra): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Exerra), [Web](https://exerra.xyz)
* [fabianski7](https://github.com/fabianski7): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:fabianski7)
* [Fabio Lovato](https://github.com/loviuz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:loviuz)
* [Fake4d](https://github.com/Fake4d): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Fake4d)
* [Felix2yu 石渠清心](https://github.com/Felix2yu): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Felix2yu), [Web](https://yufei.im/)
* [FireFingers21](https://github.com/firefingers21): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:firefingers21)
* [flo0627](https://github.com/flo0627): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:flo0627)
* [François-Xavier Payet](https://github.com/foux): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:foux)
* [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Frenzie), [Web](http://fransdejonge.com/)
* [FromTheMoon85](https://github.com/FromTheMoon85): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:FromTheMoon85)
* [Gaurav Thakur](https://github.com/notfoss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:notfoss), [Web](https://blog.notfoss.com/)
* [Gianni Scolaro](https://github.com/giannidsp): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:giannidsp)
* [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
* [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
* [Guilherme Gall](https://github.com/gmgall): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gmgall), [Web](https://gmgall.net/)
* [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
* [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
* [Guillaume Pugnet](https://github.com/GuillaumePugnet): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:GuillaumePugnet)
@ -86,6 +95,7 @@ People are sorted by name so please keep this order.
* [ibiruai](https://github.com/ibiruai): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ibiruai)
* [id-konstantin-stepanov](https://github.com/id-konstantin-stepanov): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:id-konstantin-stepanov)
* [Ilias Vrachnis](https://github.com/vrachnis): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:vrachnis)
* [jaden](https://github.com/jaden): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jaden)
* [Jake Mannens](https://github.com/jakem72360): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jakem72360)
* [Jamie Slome](https://github.com/JamieSlome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:JamieSlome), [Web](https://418sec.com/)
* [Jan Lukas Gernert](https://github.com/jangernert): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jangernert)
@ -93,13 +103,16 @@ People are sorted by name so please keep this order.
* [Jaussoin Timothée](https://github.com/edhelas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:edhelas), [Web](http://edhelas.movim.eu/)
* [Jeremy](https://github.com/Germs2004): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Germs2004)
* [jlefler](https://github.com/jlefler): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jlefler)
* [Joe Stump](https://github.com/joestump): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:joestump), [Web](http://stu.mp)
* [Joel Garcia](https://github.com/joelchrono12): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:joelchrono12), [Web](https://joelchrono12.xyz)
* [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:cez81)
* [Joris Kinable](https://github.com/jkinable): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jkinable)
* [Jules Bertholet](https://github.com/Jules-Bertholet): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Jules-Bertholet)
* [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:j8r), [Web](https://blog.jrei.ch/)
* [Julien-Pierre Avérous](https://github.com/javerous): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:javerous), [Web](https://www.sourcemac.com/)
* [Justin Tracey](https://github.com/jtracey): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jtracey), [Web](https://unsuspicious.click)
* [Kaibin Yang](https://github.com/SkyYkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SkyYkb), [Web](https://kaibinyang.com/)
* [Kasimir Cash](https://github.com/KasimirCash): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:KasimirCash)
* [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kevinpapst), [Web](http://www.kevinpapst.de/)
* [Kiblyn11](https://github.com/Kiblyn11): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Kiblyn11)
* [kinoushe](https://github.com/kinoushe): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kinoushe)
@ -108,6 +121,7 @@ People are sorted by name so please keep this order.
* [Konstantinos Megas](https://github.com/nextdoorpanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nextdoorpanda)
* [Kristian Salonen](https://github.com/krisu5): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:krisu5)
* [Leepic](https://github.com/Leepic): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Leepic)
* [LLeana](https://github.com/LleanaRuv): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:LleanaRuv)
* [loft17](https://github.com/loft17): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:loft17)
* [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ldidry), [Web](https://www.fiat-tux.fr/)
* [Luc Sanchez](https://github.com/ColonelMoutarde): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ColonelMoutarde), [Web](https://www.luc-sanchez.fr/)
@ -121,6 +135,7 @@ People are sorted by name so please keep this order.
* [Marcus Rohrmoser](https://github.com/mro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mro), [Web](http://mro.name/~me)
* [Marek Pavelka](https://github.com/marapavelka): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marapavelka), [Web](https://marekpavelka.cz)
* [Marien Fressinaud](https://github.com/marienfressinaud): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:marienfressinaud), [Web](https://marienfressinaud.fr/)
* [Mark Monteiro](https://github.com/mark-monteiro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mark-monteiro), [Web](https://markmonteiro.info/)
* [Martin](https://github.com/C0rn3j): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:C0rn3j), [Web](https://rys.pw/)
* [math-GH](https://github.com/math-GH): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:math-GH)
* [Matt Sephton](https://github.com/gingerbeardman): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gingerbeardman)
@ -133,8 +148,12 @@ People are sorted by name so please keep this order.
* [Mike Vanbuskirk](https://github.com/codevbus): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:codevbus) [Web](http://mikevanbuskirk.io/)
* [miles](https://github.com/miles170): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miles170)
* [mincerafter42](https://github.com/mincerafter42): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mincerafter42), [Web](https://mincerafter42.github.io)
* [Mossroy](https://github.com/mossroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mossroy)
* [Mossroy](https://github.com/mossroy): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mossroy), [Web](https://blog.mossroy.fr/)
* [MSZ](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mszkb)
* [Mubarak Harran Alketbi](https://github.com/MHketbi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:MHketbi)
* [Myuki](https://github.com/Myuki): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Myuki)
* [NaeiKinDus](https://github.com/NaeiKinDus): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:NaeiKinDus)
* [Nainor](https://github.com/Nainor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Nainor)
* [nanhualyq](https://github.com/nanhualyq): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:nanhualyq)
* [Natalie Stroud](https://github.com/natastro): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:natastro)
@ -152,6 +171,7 @@ People are sorted by name so please keep this order.
* [Olivier Brencklé](https://github.com/obrenckle): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:obrenckle)
* [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:blankoworld), [Web](https://olivier.dossmann.net)
* [ORelio](https://github.com/ORelio): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ORelio), [Web](https://microzoom.fr/)
* [otaconix](https://github.com/otaconix): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:otaconix)
* [Pablo Caro](https://github.com/pcaro90): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pcaro90), [Web](https://pcaro.es/)
* [PAHXO](https://github.com/PAHXO): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:PAHXO)
* [papaschloss](https://github.com/papaschloss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:papaschloss)
@ -180,17 +200,22 @@ People are sorted by name so please keep this order.
* [Rebecca Scott](https://github.com/becdetat): [contirbutions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:becdetat), [Web](https://becdetat.com)
* [Rezad](https://github.com/rezad1393): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rezad1393)
* [Robert Kaussow](https://github.com/xoxys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xoxys), [Web](https://geeklabor.de/)
* [robertdahlem](https://github.com/robertdahlem): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:robertdahlem)
* [rocka](https://github.com/rocka): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rocka)
* [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:romibi)
* [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
* [Rufubi](https://github.com/Rufubi): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Rufubi)
* [ryoku-cha](https://github.com/ryoku-cha): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:ryoku-cha)
* [Sadetdin EYILI](https://github.com/sad270): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sad270)
* [Sam Cohen](https://github.com/samc1213): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:samc1213)
* [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
* [Sebastian K](https://github.com/skrollme): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:skrollme)
* [shn7798](https://github.com/shn7798): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:shn7798), [Web](http://www.code2talk.com/)
* [Simone "roughnecks" Canaletti](https://github.com/roughnecks): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:roughnecks), [Web](https://woodpeckersnest.space/)
* [sirideain](https://github.com/sirideain): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:sirideain)
* [skrlet13](https://github.com/skrlet13): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:skrlet13), [Web](https://www.skrlet13.cl/)
* [Sp3r4z](https://github.com/Sp3r4z): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Sp3r4z)
* [Steve Jones](https://github.com/squaregoldfish): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:squaregoldfish)
* [Strubbl](https://github.com/Strubbl): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Strubbl)
* [Stunkymonkey](https://github.com/Stunkymonkey): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Stunkymonkey)
* [stysebae](https://github.com/stysebae): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:stysebae)
@ -212,9 +237,13 @@ People are sorted by name so please keep this order.
* [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
* [upskaling](https://github.com/upskaling): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:upskaling)
* [Virgil Chen](https://github.com/VirgilChen97): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:VirgilChen97)
* [VYSE V.E.O](https://github.com/V-E-O): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:V-E-O)
* [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Wanabo)
* [witchcraze](https://github.com/witchcraze): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:witchcraze)
* [wtoscer](https://github.com/wtoscer): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wtoscer)
* [xnaas](https://github.com/xnaas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xnaas), [Web](https://xnaas.info/)
* [XtremeOwnage](https://github.com/XtremeOwnage): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:XtremeOwnageDotCom), [Web](https://static.xtremeownage.com/)
* [Yamakuni](https://github.com/Yamakuni): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Yamakuni), [Web](https://ofanch.me/)
* [yzqzss|一座桥在水上](https://github.com/yzqzss): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:yzqzss), [Web](https://blog.othing.xyz/)
* [Zhaofeng Li](https://github.com/zhaofengli): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhaofengli), [Web](https://zhaofeng.li/)
* [Zhiyuan Zheng](https://github.com/zhzy0077): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhzy0077)

View File

@ -20,7 +20,6 @@ COPY . /var/www/FreshRSS
COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
ARG FRESHRSS_VERSION
ARG SOURCE_BRANCH
ARG SOURCE_COMMIT
LABEL \
@ -28,7 +27,7 @@ LABEL \
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
org.opencontainers.image.title="FreshRSS" \
org.opencontainers.image.url="https://freshrss.org/" \
@ -37,6 +36,7 @@ LABEL \
RUN a2dismod -q -f alias autoindex negotiation status && \
a2dismod -q auth_openidc && \
phpdismod calendar exif ffi ftp gettext mysqli posix readline shmop sockets sysvmsg sysvsem sysvshm xsl && \
a2enmod -q deflate expires headers mime remoteip setenvif && \
a2disconf -q '*' && \
a2dissite -q '*' && \

View File

@ -1,4 +1,4 @@
FROM alpine:3.18
FROM alpine:3.19
ENV TZ UTC
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
@ -17,7 +17,6 @@ COPY . /var/www/FreshRSS
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
ARG FRESHRSS_VERSION
ARG SOURCE_BRANCH
ARG SOURCE_COMMIT
LABEL \
@ -25,7 +24,7 @@ LABEL \
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
org.opencontainers.image.title="FreshRSS" \
org.opencontainers.image.url="https://freshrss.org/" \

View File

@ -5,11 +5,11 @@ SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
RUN echo 'http://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories && \
apk add --no-cache \
tzdata \
apache2 php82-apache2 \
apache2 php83-apache2 \
apache-mod-auth-openidc \
php82 php82-curl php82-gmp php82-intl php82-mbstring php82-xml php82-zip \
php82-ctype php82-dom php82-fileinfo php82-iconv php82-json php82-opcache php82-openssl php82-phar php82-session php82-simplexml php82-xmlreader php82-xmlwriter php82-xml php82-tokenizer php82-zlib \
php82-pdo_sqlite php82-pdo_mysql php82-pdo_pgsql
php83 php83-curl php83-gmp php83-intl php83-mbstring php83-xml php83-zip \
php83-ctype php83-dom php83-fileinfo php83-iconv php83-json php83-opcache php83-openssl php83-phar php83-session php83-simplexml php83-xmlreader php83-xmlwriter php83-xml php83-tokenizer php83-zlib \
php83-pdo_sqlite php83-pdo_mysql php83-pdo_pgsql
RUN mkdir -p /var/www/FreshRSS /run/apache2/
WORKDIR /var/www/FreshRSS
@ -18,7 +18,6 @@ COPY . /var/www/FreshRSS
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
ARG FRESHRSS_VERSION
ARG SOURCE_BRANCH
ARG SOURCE_COMMIT
LABEL \
@ -26,7 +25,7 @@ LABEL \
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
org.opencontainers.image.title="FreshRSS" \
org.opencontainers.image.url="https://freshrss.org/" \
@ -41,8 +40,9 @@ RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
/etc/apache2/httpd.conf && \
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
/etc/apache2/httpd.conf && \
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php82 /usr/bin/php; else true; fi && \
echo 'memory_limit = 256M' > /etc/php82/conf.d/10_memory.ini && \
mv /etc/apache2/conf.d/mod-auth-openidc.conf /etc/apache2/conf.d/mod-auth-openidc.conf.bak && \
if [ ! -f /usr/bin/php ]; then ln -s /usr/bin/php83 /usr/bin/php; else true; fi && \
echo 'memory_limit = 256M' > /etc/php83/conf.d/10_memory.ini && \
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
touch /var/www/FreshRSS/Docker/env.txt && \

View File

@ -1,4 +1,4 @@
FROM alpine:3.8
FROM alpine:3.13
ENV TZ UTC
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
@ -7,7 +7,7 @@ RUN apk add --no-cache \
tzdata \
apache2 php7-apache2 \
php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
php7-ctype php7-dom php7-iconv php7-json php7-opcache php7-openssl php7-phar php7-session php7-xmlreader php7-xml php7-zlib \
php7-ctype php7-dom php7-fileinfo php7-iconv php7-json php7-opcache php7-openssl php7-phar php7-session php7-simplexml php7-xmlreader php7-xmlwriter php7-xml php7-tokenizer php7-zlib \
php7-pdo_sqlite php7-pdo_mysql php7-pdo_pgsql
RUN mkdir -p /var/www/FreshRSS /run/apache2/
@ -17,7 +17,6 @@ COPY . /var/www/FreshRSS
COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
ARG FRESHRSS_VERSION
ARG SOURCE_BRANCH
ARG SOURCE_COMMIT
LABEL \
@ -25,7 +24,7 @@ LABEL \
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
org.opencontainers.image.revision="${SOURCE_COMMIT}" \
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
org.opencontainers.image.title="FreshRSS" \
org.opencontainers.image.url="https://freshrss.org/" \

View File

@ -1,81 +0,0 @@
# Only relevant for Docker Hub or QEMU multi-architecture builds.
# Prefer the normal `Dockerfile` if you are building manually on the targeted architecture.
FROM arm32v7/debian:12-slim
# Requires ./hooks/*
COPY ./Docker/qemu-arm-* /usr/bin/
ENV TZ UTC
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && \
apt-get install --no-install-recommends -y \
ca-certificates cron \
apache2 libapache2-mod-php \
libapache2-mod-auth-openidc \
php-curl php-gmp php-intl php-mbstring php-xml php-zip \
php-sqlite3 php-mysql php-pgsql && \
rm -rf /var/lib/apt/lists/*
RUN mkdir -p /var/www/FreshRSS/ /run/apache2/
WORKDIR /var/www/FreshRSS
COPY . /var/www/FreshRSS
COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
ARG FRESHRSS_VERSION
ARG SOURCE_BRANCH
ARG SOURCE_COMMIT
LABEL \
org.opencontainers.image.authors="Alkarex" \
org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
org.opencontainers.image.licenses="AGPL-3.0" \
org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
org.opencontainers.image.title="FreshRSS" \
org.opencontainers.image.url="https://freshrss.org/" \
org.opencontainers.image.vendor="FreshRSS" \
org.opencontainers.image.version="$FRESHRSS_VERSION"
RUN a2dismod -q -f alias autoindex negotiation status && \
a2dismod -q auth_openidc && \
a2enmod -q deflate expires headers mime remoteip setenvif && \
a2disconf -q '*' && \
a2dissite -q '*' && \
a2ensite -q 'FreshRSS*'
RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
# Disable built-in updates when using Docker, as the full image is supposed to be updated instead.
sed -r -i "\\#disable_update#s#^.*#\t'disable_update' => true,#" ./config.default.php && \
touch /var/www/FreshRSS/Docker/env.txt && \
echo "17,47 * * * * . /var/www/FreshRSS/Docker/env.txt; \
su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
2>> /proc/1/fd/2 > /tmp/FreshRSS.log" > /etc/crontab.freshrss.default
# Seems needed for arm32v7/ubuntu on Docker Hub
RUN update-ca-certificates -f
# Useful with the `--squash` build option
RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
ENV COPY_LOG_TO_SYSLOG On
ENV COPY_SYSLOG_TO_STDERR On
ENV CRON_MIN ''
ENV DATA_PATH ''
ENV FRESHRSS_ENV ''
ENV LISTEN ''
ENV OIDC_ENABLED ''
ENV TRUSTED_PROXY ''
ENTRYPOINT ["./Docker/entrypoint.sh"]
EXPOSE 80
# hadolint ignore=DL3025
CMD ([ -z "$CRON_MIN" ] || cron) && \
. /etc/apache2/envvars && \
exec apache2 -D FOREGROUND $([ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ] && echo '-D OIDC_ENABLED')

View File

@ -11,7 +11,7 @@ ErrorLog /dev/stderr
# Can be disabled by setting the TRUSTED_PROXY environment variable to 0:
RemoteIPHeader X-Forwarded-For
# Can be overridden by the TRUSTED_PROXY environment variable:
RemoteIPTrustedProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
RemoteIPInternalProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
</IfModule>
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy

View File

@ -1,9 +1,17 @@
![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/freshrss/freshrss.svg)
![Docker Pulls](https://img.shields.io/docker/pulls/freshrss/freshrss.svg)
[![Liberapay donations](https://img.shields.io/liberapay/receives/FreshRSS.svg?logo=liberapay)](https://liberapay.com/FreshRSS/donate)
# Deploy FreshRSS with Docker
Our official images are available on [Docker Hub](https://hub.docker.com/r/freshrss/freshrss/).
FreshRSS is a self-hosted RSS feed aggregator.
* Official website: [`freshrss.org`](https://freshrss.org/)
* Official Docker images: [`hub.docker.com/r/freshrss/freshrss`](https://hub.docker.com/r/freshrss/freshrss/)
* Repository: [`github.com/FreshRSS/FreshRSS`](https://github.com/FreshRSS/FreshRSS/)
* Documentation: [`freshrss.github.io/FreshRSS`](https://freshrss.github.io/FreshRSS/)
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
![FreshRSS logo](https://github.com/FreshRSS/FreshRSS/raw/edge/docs/img/FreshRSS-logo.png)
## Install Docker
@ -66,13 +74,15 @@ The [tags](https://hub.docker.com/r/freshrss/freshrss/tags) correspond to FreshR
* `:latest` (default) is the [latest stable release](https://github.com/FreshRSS/FreshRSS/releases/latest)
* `:edge` is the rolling release, same than our [git `edge` branch](https://github.com/FreshRSS/FreshRSS/tree/edge)
* `:x.y.z` are [specific FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases)
* `:arm` or `:*-arm` are the ARM `arm32v7` versions (e.g., for Raspberry Pi).
* `:x.y.z` tags correspond to [specific FreshRSS releases](https://github.com/FreshRSS/FreshRSS/releases), allowing you to target a precise version for deployment
* `:x` tags track the latest release within a major version series. For instance, `:1` will update to include any `1.x` releases, but will exclude versions beyond `2.x`
* `*-alpine` use Linux Alpine as base-image instead of Debian
* Our Docker images are designed with multi-architecture support, accommodating a variety of Linux platforms including `linux/arm/v7`, `linux/arm64`, and `linux/amd64`.
* For other platforms, see the [custom build section](#build-custom-docker-image)
### Linux: Debian vs. Alpine
Our default image is based on [Debian](https://www.debian.org/). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `:alpine` or `*-alpine` tag suffix).
Our default image is based on [Debian](https://www.debian.org/). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `*-alpine` tag suffix).
In [our tests](https://github.com/FreshRSS/FreshRSS/pull/2205) (2019), Alpine was slower,
while Alpine is smaller on disk (and much faster to build),
and with newer packages in general (Apache, PHP).
@ -108,7 +118,7 @@ docker rm freshrss_old
## Build custom Docker image
Building your own Docker image is especially relevant for platforms not available on our Docker Hub,
which is currently limited to `x64` (Intel, AMD) and `arm32v7`.
which is currently limited to `x64` (Intel, AMD), `arm32v7`, `arm64`.
> If you try to run an image for the wrong platform, you might get an error message like *exec format error*.
@ -329,7 +339,7 @@ services:
LISTEN: 0.0.0.0:80
# Optional parameter, remove for automatic settings, set to 0 to disable,
# or (if you use a proxy) to a space-separated list of trusted IP ranges
# compatible with https://httpd.apache.org/docs/current/mod/mod_remoteip.html#remoteiptrustedproxy
# compatible with https://httpd.apache.org/docs/current/mod/mod_remoteip.html#remoteipinternalproxy
# This impacts which IP address is logged (X-Forwarded-For or REMOTE_ADDR).
# This also impacts external authentication methods;
# see https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html
@ -395,27 +405,6 @@ docker-compose down --remove-orphans --volumes
> You can combine it with `-f docker-compose-db.yml` to spin a PostgreSQL database.
### Docker Compose and ARM64
If youre working or want to host on an ARM64 system (such as Apple Silicon (M1/M2)) youll need to use the `arm` tag in your `docker-compose.yml` file:
```yaml
image: freshrss/freshrss:arm
```
If you then get this error message when running `docker compose up`:
> The requested images platform (linux/arm/v7) does not match the detected host platform (linux/arm64/v8) and no specific platform was requested
… you will also need to specify the platform in the `service` part:
```yaml
services:
freshrss:
image: freshrss/freshrss:arm
platform: linux/arm/v7
container_name: freshrss
```
## Run in production
For production, it is a good idea to use a reverse proxy on your host server, providing HTTPS.
@ -430,6 +419,26 @@ SERVER_DNS=freshrss.example.net
### Use [Træfik](https://traefik.io/traefik/) reverse proxy
#### Option 1: server FreshRSS as a sub-domain
Use [`Host()` rule](https://doc.traefik.io/traefik/routing/routers/#rule), like:
```yml
- traefik.http.routers.freshrss.rule=Host(`freshrss.example.net`)
```
#### Option 2: serve FreshRSS as a sub-path
Use [`PathPrefix()` rules](https://doc.traefik.io/traefik/routing/routers/#rule) and [`StripPrefix` middleware](https://doc.traefik.io/traefik/middlewares/http/stripprefix/#stripprefix), like:
```yml
- traefik.http.middlewares.freshrssM3.stripprefix.prefixes=/freshrss
- traefik.http.routers.freshrss.middlewares=freshrssM3
- traefik.http.routers.freshrss.rule=PathPrefix(`/freshrss`)
```
#### Full example
Here is the recommended configuration using automatic [Lets Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS.
See [`docker-compose-proxy.yml`](./freshrss/docker-compose-proxy.yml)

View File

@ -12,27 +12,29 @@ if [ -n "$LISTEN" ]; then
fi
if [ -n "$TRUSTED_PROXY" ]; then
if [ "$TRUSTED_PROXY" -eq 0 ]; then
# Disable RemoteIPHeader and RemoteIPTrustedProxy
if [ "$TRUSTED_PROXY" = "0" ]; then
# Disable RemoteIPHeader and RemoteIPInternalProxy
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "/^\s*RemoteIP.*$/s/^/#/" {} \;
else
# Custom list for RemoteIPTrustedProxy
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "\\#^\s*RemoteIPTrustedProxy#s#^.*#\tRemoteIPTrustedProxy $TRUSTED_PROXY#" {} \;
# Custom list for RemoteIPInternalProxy
find /etc/apache2/ -type f -name FreshRSS.Apache.conf -exec sed -r -i "\\#^\s*RemoteIPInternalProxy#s#^.*#\tRemoteIPInternalProxy $TRUSTED_PROXY#" {} \;
fi
fi
if [ -n "$OIDC_ENABLED" ] && [ "$OIDC_ENABLED" -ne 0 ]; then
a2enmod -q auth_openidc
# Debian
(which a2enmod >/dev/null && a2enmod -q auth_openidc) ||
# Alpine
(mv /etc/apache2/conf.d/mod-auth-openidc.conf.bak /etc/apache2/conf.d/mod-auth-openidc.conf && echo 'Enabling module auth_openidc.')
if [ -n "$OIDC_SCOPES" ]; then
# Compatibility with : as separator instead of space
OIDC_SCOPES=$(echo "$OIDC_SCOPES" | tr ':' ' ')
export OIDC_SCOPES
fi
fi
if [ -n "$CRON_MIN" ]; then
(
echo "export TZ=$TZ"
echo "export COPY_LOG_TO_SYSLOG=$COPY_LOG_TO_SYSLOG"
echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
echo "export FRESHRSS_ENV=$FRESHRSS_ENV"
echo "export DATA_PATH=$DATA_PATH"
) >/var/www/FreshRSS/Docker/env.txt
awk -v RS='\0' '!/^(FRESHRSS_INSTALL|FRESHRSS_USER|HOME|PATH|PWD|SHLVL|TERM|_)=/ {gsub("\047", "\047\\\047\047"); print "export \047" $0 "\047"}' /proc/self/environ >/var/www/FreshRSS/Docker/env.txt
sed </etc/crontab.freshrss.default \
-r "s#^[^ ]+ #$CRON_MIN #" | crontab -
fi

View File

@ -6,7 +6,7 @@ volumes:
services:
freshrss-db:
image: postgres:15
image: postgres:16
container_name: freshrss-db
hostname: freshrss-db
restart: unless-stopped

View File

@ -7,7 +7,7 @@ volumes:
services:
traefik:
image: traefik:2.10
image: traefik:2.11
container_name: traefik
restart: unless-stopped
logging:
@ -16,8 +16,6 @@ services:
ports:
- 80:80
- 443:443
networks:
- network
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- traefik-tmp:/tmp
@ -53,7 +51,12 @@ services:
- traefik.http.middlewares.freshrssM2.headers.referrerPolicy=no-referrer-when-downgrade
- traefik.http.middlewares.freshrssM2.headers.stsSeconds=31536000
- traefik.http.routers.freshrss.entryPoints=https
- traefik.http.routers.freshrss.middlewares=freshrssM1,freshrssM2
- traefik.http.routers.freshrss.rule=Host(`${SERVER_DNS}`)
- traefik.http.routers.freshrss.tls.certResolver=letsEncrypt
- traefik.http.routers.freshrss.tls=true
## Option 1: server FreshRSS as sub-domain
- traefik.http.routers.freshrss.middlewares=freshrssM1,freshrssM2
- traefik.http.routers.freshrss.rule=Host(`${SERVER_DNS}`)
## Option 2: serve FreshRSS as sub-path
# - traefik.http.middlewares.freshrssM3.stripprefix.prefixes=/freshrss
# - traefik.http.routers.freshrss.middlewares=freshrssM1,freshrssM2,freshrssM3
# - traefik.http.routers.freshrss.rule=PathPrefix(`/freshrss`)

View File

@ -1,21 +0,0 @@
#!/bin/bash
cd ..
FRESHRSS_VERSION=$(grep "'FRESHRSS_VERSION'" constants.php | cut -d "'" -f4)
echo "$FRESHRSS_VERSION"
if [[ $DOCKERFILE_PATH == *-ARM ]]; then
#TODO: Add --squash --platform arm options when Docker Hub daemon supports them
docker build \
--build-arg FRESHRSS_VERSION="$FRESHRSS_VERSION" \
--build-arg SOURCE_BRANCH="$SOURCE_BRANCH" \
--build-arg SOURCE_COMMIT="$SOURCE_COMMIT" \
-f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" .
else
#TODO: Add --squash option when Docker Hub daemon supports it
docker build \
--build-arg FRESHRSS_VERSION="$FRESHRSS_VERSION" \
--build-arg SOURCE_BRANCH="$SOURCE_BRANCH" \
--build-arg SOURCE_COMMIT="$SOURCE_COMMIT" \
-f "$DOCKERFILE_PATH" -t "$IMAGE_NAME" .
fi

View File

@ -1,4 +0,0 @@
#!/bin/bash
mv ../README.md ../README.en.md
mv README.md ../

View File

@ -1,11 +0,0 @@
#!/bin/bash
if [[ $DOCKERFILE_PATH == *-ARM ]]; then
# https://github.com/balena-io/qemu
# Download a local copy of QEMU on Docker Hub build machine
curl -LSs 'https://github.com/balena-io/qemu/releases/download/v3.0.0%2Bresin/qemu-3.0.0+resin-arm.tar.gz' | tar -xzv --strip-components=1 --wildcards '*/qemu-*'
# https://github.com/multiarch/qemu-user-static
# Register qemu-*-static for all supported processors except the current one, but also remove all registered binfmt_misc before
docker run --rm --privileged multiarch/qemu-user-static:register --reset
fi

View File

@ -21,8 +21,6 @@ endif
ifeq ($(findstring alpine,$(TAG)),alpine)
DOCKERFILE=Dockerfile-Alpine
else ifeq ($(findstring arm,$(TAG)),arm)
DOCKERFILE=Dockerfile-QEMU-ARM
else
DOCKERFILE=Dockerfile
endif
@ -73,7 +71,7 @@ lint-fix: vendor/bin/phpcbf ## Fix the errors detected by the linter
bin/composer:
mkdir -p bin/
wget 'https://raw.githubusercontent.com/composer/getcomposer.org/b5dbe5ebdec95ce71b3128b359bd5a85cb0a722d/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
wget 'https://raw.githubusercontent.com/composer/getcomposer.org/8af47a6fd4910073ea7580378d6252c708f83a06/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
vendor/bin/phpunit: bin/composer
bin/composer install --prefer-dist --no-progress
@ -90,7 +88,7 @@ vendor/bin/phpcbf: bin/composer
bin/typos:
mkdir -p bin/
cd bin ; \
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' && \
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.17.0/typos-v1.17.0-x86_64-unknown-linux-musl.tar.gz' && \
tar -xvf *.tar.gz './typos' && \
chmod +x typos && \
rm *.tar.gz ; \

View File

@ -12,17 +12,21 @@ Il se veut léger et facile à prendre en main tout en étant un outil puissant
Il permet de gérer plusieurs utilisateurs, dispose dun mode de lecture anonyme, et supporte les étiquettes personnalisées.
Il y a une API pour les clients (mobiles), ainsi quune [interface en ligne de commande](cli/README.md).
Grâce au standard [WebSub](https://www.w3.org/TR/websub/) (anciennement [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, telles [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
Grâce au standard [WebSub](https://freshrss.github.io/FreshRSS/fr/users/08_PubSubHubbub.html),
FreshRSS est capable de recevoir des notifications push instantanées depuis les sources compatibles, [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
FreshRSS supporte nativement le [moissonnage du Web (Web Scraping)](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html) basique,
basé sur [XPath](https://www.w3.org/TR/xpath-10/), pour les sites Web sans flux RSS / Atom.
Supporte aussi les documents JSON.
FreshRSS permet de [repartager des sélections darticles par HTML, RSS, et OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
Plusieurs [méthodes de connexion](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) sont supportées : formulaire Web (avec un mode anonyme), Authentification HTTP (compatible avec proxy), OpenID Connect.
Enfin, FreshRSS permet lajout d[extensions](#extensions) pour encore plus de personnalisation.
* Site officiel : <https://freshrss.org>
* Démo : <http://demo.freshrss.org/>
* Démo : <https://demo.freshrss.org>
* Licence : [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.fr.html)
![Logo de FreshRSS](docs/img/FreshRSS-logo.png)
@ -57,11 +61,12 @@ FreshRSS nest fourni avec aucune garantie.
* Fonctionne aussi sur mobile (sauf certaines fonctionnalités)
* Serveur modeste, par exemple sous Linux ou Windows
* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
* Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
* PHP 7.2+
* Requis : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), et [PDO_MySQL](https://www.php.net/pdo-mysql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_PGSQL](https://www.php.net/pdo-pgsql)
* Recommandés : [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion dencodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
* MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+
* Serveur Web Apache2.4+ (recommandé), ou nginx, lighttpd (non testé sur les autres)
* PHP 7.4+
* Extensions requises : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
* Extensions recommandées : [PDO_SQLite](https://www.php.net/pdo-sqlite) (pour lexport/import), [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion dencodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
* Extension pour base de données : [PDO_PGSQL](https://www.php.net/pdo-pgsql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_MySQL](https://www.php.net/pdo-mysql)
* PostgreSQL 9.5+ ou SQLite ou MySQL 5.5.3+ ou MariaDB 5.5+
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
@ -117,8 +122,8 @@ sudo git checkout latest
# Mettre les droits daccès pour le serveur Web
sudo cli/access-permissions.sh
# Si vous souhaitez permettre les mises à jour par linterface Web
sudo chmod -R g+w .
# Si vous souhaitez permettre les mises à jour par linterface Web (un peu moins sûr)
sudo chown www-data:www-data -R .
# Publier FreshRSS dans votre répertoire HTML public
sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
@ -227,18 +232,19 @@ et [lAPI Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm
| [FocusReader](https://play.google.com/store/apps/details?id=allen.town.focus.reader) | Android | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | | ✔️ | | ✓ | ✔️ |
| [Readrops](https://github.com/readrops/Readrops) | Android | [✔️](https://github.com/readrops/Readrops) | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | | | | | ✔️ |
| [Fluent Reader Lite](https://hyliu.me/fluent-reader-lite/) | Android, iOS| [✔️](https://github.com/yang991178/fluent-reader-lite) | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | | | ✓ | | | |
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | En développement | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | | | |
| [Read You](https://github.com/Ashinch/ReadYou/) | Android | [✔️](https://github.com/Ashinch/ReadYou/) | [En développement](https://github.com/Ashinch/ReadYou/discussions/542) | GReader, Fever | | ⭐⭐ | | ✔️ | ✔️ | | | ✔️ |
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | En développement | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | | | |
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, macOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | Fever | ✔️ | ⭐ | | ✔️ | ✓ | | | |
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | ✔️ | ✔️ | |
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | GReader, Fever | | ⭐⭐ | | ✔️ | ✔️ | ✔️ | | |
| [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, macOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | | ⭐ | | ✔️ | ✔️ | | ✔️ | |
| [Vienna RSS](http://www.vienna-rss.com/) | macOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
| [Readkit](https://apps.apple.com/app/readkit/id588726889) | iOS, macOS | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | ✓ | 💲 |
| [Readkit](https://apps.apple.com/app/readkit-read-later-rss/id1615798039) | iOS, macOS | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | ✓ | 💲 |
| [Reeder](https://www.reederapp.com/) | iOS, macOS | | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | | ✔️ |
| [lire](https://lireapp.com/) | iOS, macOS | | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
| [Unread](https://apps.apple.com/app/unread-2/id1363637349) | iOS | | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ✔️ | | | |
| [Fiery Feeds](https://apps.apple.com/app/fiery-feeds-rss-reader/id1158763303) | iOS | | ✔️✔️ | Fever | ❔ | ❔ | ❔ | ❔ | ❔ | | | |
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, macOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | En développement | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | | ❔ | ✔️ |
| [Netnewswire](https://ranchero.com/netnewswire/) | iOS, macOS | [✔️](https://github.com/Ranchero-Software/NetNewsWire) | En développement | GReader | ✔️ | ❔ | ❔ | ❔ | ✔️ | | ❔ | ✔️ |
# Bibliothèques incluses

View File

@ -12,17 +12,21 @@ It is lightweight, easy to work with, powerful, and customizable.
It is a multi-user application with an anonymous reading mode. It supports custom tags.
There is an API for (mobile) clients, and a [Command-Line Interface](cli/README.md).
Thanks to the [WebSub](https://www.w3.org/TR/websub/) standard (formerly [PubSubHubbub](https://github.com/pubsubhubbub/PubSubHubbub)),
FreshRSS is able to receive instant push notifications from compatible sources, such as [Mastodon](https://joinmastodon.org), [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, FeedBurner, etc.
Thanks to the [WebSub](https://freshrss.github.io/FreshRSS/en/users/WebSub.html) standard,
FreshRSS is able to receive instant push notifications from compatible sources, such as [Friendica](https://friendi.ca), [WordPress](https://wordpress.org/plugins/pubsubhubbub/), Blogger, Medium, etc.
FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
FreshRSS natively supports basic [Web scraping](https://freshrss.github.io/FreshRSS/en/users/11_website_scraping.html),
based on [XPath](https://www.w3.org/TR/xpath-10/), for Web sites not providing any RSS / Atom feed.
Also supports JSON documents.
FreshRSS offers the ability to [reshare selections of articles by HTML, RSS, and OPML](https://freshrss.github.io/FreshRSS/en/users/user_queries.html).
Different [login methods](https://freshrss.github.io/FreshRSS/en/admins/09_AccessControl.html) are supported: Web form (including an anonymous option), HTTP Authentication (compatible with proxy delegation), OpenID Connect.
Finally, FreshRSS supports [extensions](#extensions) for further tuning.
* Official website: <https://freshrss.org>
* Demo: <https://demo.freshrss.org/>
* Demo: <https://demo.freshrss.org>
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
![FreshRSS logo](docs/img/FreshRSS-logo.png)
@ -57,11 +61,12 @@ FreshRSS comes with absolutely no warranty.
* Works on mobile (except a few features)
* Light server running Linux or Windows
* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
* A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
* PHP 7.2+
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), and [PDO_MySQL](https://www.php.net/pdo-mysql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_PGSQL](https://www.php.net/pdo-pgsql)
* Recommended extensions: [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
* MySQL 5.5.3+ or MariaDB equivalent, or SQLite 3.7.4+, or PostgreSQL 9.5+
* A web server: Apache2.4+ (recommended), nginx, lighttpd (not tested on others)
* PHP 7.4+
* Required extensions: [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype)
* Recommended extensions: [PDO_SQLite](https://www.php.net/pdo-sqlite) (for export/import), [GMP](https://www.php.net/gmp) (for API access on 32-bit platforms), [IDN](https://www.php.net/intl.idn) (for Internationalized Domain Names), [mbstring](https://www.php.net/mbstring) (for Unicode strings), [iconv](https://www.php.net/iconv) (for charset conversion), [ZIP](https://www.php.net/zip) (for import/export), [zlib](https://www.php.net/zlib) (for compressed feeds)
* Extension for database: [PDO_PGSQL](https://www.php.net/pdo-pgsql) or [PDO_SQLite](https://www.php.net/pdo-sqlite) or [PDO_MySQL](https://www.php.net/pdo-mysql)
* PostgreSQL 9.5+ or SQLite or MySQL 5.5.3+ or MariaDB 5.5+
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
@ -124,13 +129,14 @@ and [Fever API](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html)
| [Readrops](https://github.com/readrops/Readrops) | Android | [✔️](https://github.com/readrops/Readrops) | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | | | | | ✔️ |
| [Fluent Reader Lite](https://hyliu.me/fluent-reader-lite/) | Android, iOS| [✔️](https://github.com/yang991178/fluent-reader-lite) | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | | | ✓ | | | |
| [FocusReader](https://play.google.com/store/apps/details?id=allen.town.focus.reader) | Android | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | | ✔️ | | ✓ | ✔️ |
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | Work in progress | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | | | |
| [Read You](https://github.com/Ashinch/ReadYou/) | Android | [✔️](https://github.com/Ashinch/ReadYou/) | [Work in progress](https://github.com/Ashinch/ReadYou/discussions/542) | GReader, Fever | | ⭐⭐ | | ✔️ | ✔️ | | | ✔️ |
| [ChristopheHenry](https://gitlab.com/christophehenry/freshrss-android) | Android | [✔️](https://gitlab.com/christophehenry/freshrss-android) | Work in progress | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | | | |
| [Fluent Reader](https://hyliu.me/fluent-reader/) | Windows, Linux, macOS| [✔️](https://github.com/yang991178/fluent-reader) | ✔️✔️ | GReader, Fever | ✔️ | ⭐ | | ✔️ | ✓ | | | |
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | ✔️ | ✔️ | |
| [RSS Guard](https://github.com/martinrotter/rssguard) | Windows, GNU/Linux, macOS, OS/2 | [✔️](https://github.com/martinrotter/rssguard) | ✔️✔️ | GReader | ✔️ | ⭐⭐ | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| [NewsFlash](https://gitlab.com/news-flash/news_flash_gtk) | GNU/Linux | [✔️](https://gitlab.com/news-flash/news_flash_gtk) | ✔️✔️ | GReader, Fever | | ⭐⭐ | | ✔️ | ✔️ | ✔️ | | |
| [Newsboat 2.24+](https://newsboat.org/) | GNU/Linux, macOS, FreeBSD | [✔️](https://github.com/newsboat/newsboat/) | ✔️✔️ | GReader | | ⭐ | | ✔️ | ✔️ | | ✔️ | |
| [Vienna RSS](http://www.vienna-rss.com/) | macOS | [✔️](https://github.com/ViennaRSS/vienna-rss) | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
| [Readkit](https://apps.apple.com/app/readkit/id588726889) | iOS, macOS | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | ✓ | 💲 |
| [Readkit](https://apps.apple.com/app/readkit-read-later-rss/id1615798039) | iOS, macOS | | ✔️✔️ | GReader | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | ✓ | 💲 |
| [Reeder](https://www.reederapp.com/)* | iOS, macOS | | ✔️✔️ | GReader, Fever | ✔️ | ⭐⭐⭐ | | ✔️ | ✔️ | | | ✔️ |
| [lire](https://lireapp.com/) | iOS, macOS | | ✔️✔️ | GReader | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ | ❔ |
| [Unread](https://apps.apple.com/app/unread-2/id1363637349) | iOS | | ✔️✔️ | Fever | ✔️ | ❔ | ❔ | ❔ | ✔️ | | | |

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This controller manage API-related features.
@ -12,18 +13,20 @@ class FreshRSS_api_Controller extends FreshRSS_ActionController {
*/
public static function updatePassword(string $apiPasswordPlain) {
$username = Minz_User::name();
$userConfig = FreshRSS_Context::$user_conf;
if ($username == null) {
return _t('feedback.api.password.failed');
}
$apiPasswordHash = FreshRSS_password_Util::hash($apiPasswordPlain);
$userConfig->apiPasswordHash = $apiPasswordHash;
FreshRSS_Context::userConf()->apiPasswordHash = $apiPasswordHash;
$feverKey = FreshRSS_fever_Util::updateKey($username, $apiPasswordPlain);
if (!$feverKey) {
return _t('feedback.api.password.failed');
}
$userConfig->feverKey = $feverKey;
if ($userConfig->save()) {
FreshRSS_Context::userConf()->feverKey = $feverKey;
if (FreshRSS_Context::userConf()->save()) {
return false;
} else {
return _t('feedback.api.password.failed');

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This controller handles action about authentication.
@ -27,23 +28,26 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
$anon = Minz_Request::paramBoolean('anon_access');
$anon_refresh = Minz_Request::paramBoolean('anon_refresh');
$auth_type = Minz_Request::paramString('auth_type') ?: 'none';
$auth_type = Minz_Request::paramString('auth_type') ?: 'form';
$unsafe_autologin = Minz_Request::paramBoolean('unsafe_autologin');
$api_enabled = Minz_Request::paramBoolean('api_enabled');
if ($anon !== FreshRSS_Context::$system_conf->allow_anonymous ||
$auth_type !== FreshRSS_Context::$system_conf->auth_type ||
$anon_refresh !== FreshRSS_Context::$system_conf->allow_anonymous_refresh ||
$unsafe_autologin !== FreshRSS_Context::$system_conf->unsafe_autologin_enabled ||
$api_enabled !== FreshRSS_Context::$system_conf->api_enabled) {
if ($anon !== FreshRSS_Context::systemConf()->allow_anonymous ||
$auth_type !== FreshRSS_Context::systemConf()->auth_type ||
$anon_refresh !== FreshRSS_Context::systemConf()->allow_anonymous_refresh ||
$unsafe_autologin !== FreshRSS_Context::systemConf()->unsafe_autologin_enabled ||
$api_enabled !== FreshRSS_Context::systemConf()->api_enabled) {
// TODO: test values from form
FreshRSS_Context::$system_conf->auth_type = $auth_type;
FreshRSS_Context::$system_conf->allow_anonymous = $anon;
FreshRSS_Context::$system_conf->allow_anonymous_refresh = $anon_refresh;
FreshRSS_Context::$system_conf->unsafe_autologin_enabled = $unsafe_autologin;
FreshRSS_Context::$system_conf->api_enabled = $api_enabled;
if (in_array($auth_type, ['form', 'http_auth', 'none'], true)) {
FreshRSS_Context::systemConf()->auth_type = $auth_type;
} else {
FreshRSS_Context::systemConf()->auth_type = 'form';
}
FreshRSS_Context::systemConf()->allow_anonymous = $anon;
FreshRSS_Context::systemConf()->allow_anonymous_refresh = $anon_refresh;
FreshRSS_Context::systemConf()->unsafe_autologin_enabled = $unsafe_autologin;
FreshRSS_Context::systemConf()->api_enabled = $api_enabled;
$ok &= FreshRSS_Context::$system_conf->save();
$ok &= FreshRSS_Context::systemConf()->save();
}
invalidateHttpCache();
@ -61,14 +65,13 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
*
* It forwards to the correct login page (form) or main page if
* the user is already connected.
* @throws Minz_ConfigurationParamException
*/
public function loginAction(): void {
if (FreshRSS_Auth::hasAccess() && Minz_Request::paramString('u') === '') {
Minz_Request::forward(['c' => 'index', 'a' => 'index'], true);
}
$auth_type = FreshRSS_Context::$system_conf->auth_type;
$auth_type = FreshRSS_Context::systemConf()->auth_type;
FreshRSS_Context::initUser(Minz_User::INTERNAL_USER, false);
switch ($auth_type) {
case 'form':
@ -113,53 +116,62 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
FreshRSS_View::prependTitle(_t('gen.auth.login') . ' · ');
FreshRSS_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$this->view->cookie_days = (int)round($limits['cookie_duration'] / 86400, 1);
$isPOST = Minz_Request::isPost() && !Minz_Session::param('POST_to_GET');
$isPOST = Minz_Request::isPost() && !Minz_Session::paramBoolean('POST_to_GET');
Minz_Session::_param('POST_to_GET');
if ($isPOST) {
$nonce = Minz_Session::param('nonce', '');
$nonce = Minz_Session::paramString('nonce');
$username = Minz_Request::paramString('username');
$challenge = Minz_Request::paramString('challenge');
if ($nonce === '') {
Minz_Log::warning("Invalid session during login for user={$username}, nonce={$nonce}");
header('HTTP/1.1 403 Forbidden');
Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect
Minz_Request::setBadNotification(_t('install.session.nok'));
Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false);
return;
}
usleep(random_int(100, 10000)); //Primitive mitigation of timing attacks, in μs
FreshRSS_Context::initUser($username);
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
// Initialise the default user to be able to display the error page
FreshRSS_Context::initUser(FreshRSS_Context::$system_conf->default_user);
FreshRSS_Context::initUser(FreshRSS_Context::systemConf()->default_user);
Minz_Error::error(403, _t('feedback.auth.login.invalid'), false);
return;
}
if (!FreshRSS_Context::$user_conf->enabled || FreshRSS_Context::$user_conf->passwordHash == '') {
if (!FreshRSS_Context::userConf()->enabled || FreshRSS_Context::userConf()->passwordHash == '') {
usleep(random_int(100, 5000)); //Primitive mitigation of timing attacks, in μs
Minz_Error::error(403, _t('feedback.auth.login.invalid'), false);
return;
}
$ok = FreshRSS_FormAuth::checkCredentials(
$username, FreshRSS_Context::$user_conf->passwordHash, $nonce, $challenge
$username, FreshRSS_Context::userConf()->passwordHash, $nonce, $challenge
);
if ($ok) {
// Set session parameter to give access to the user.
Minz_Session::_params([
Minz_User::CURRENT_USER => $username,
'passwordHash' => FreshRSS_Context::$user_conf->passwordHash,
'passwordHash' => FreshRSS_Context::userConf()->passwordHash,
'csrf' => false,
]);
FreshRSS_Auth::giveAccess();
// Set cookie parameter if needed.
if (Minz_Request::paramBoolean('keep_logged_in')) {
FreshRSS_FormAuth::makeCookie($username, FreshRSS_Context::$user_conf->passwordHash);
FreshRSS_FormAuth::makeCookie($username, FreshRSS_Context::userConf()->passwordHash);
} else {
FreshRSS_FormAuth::deleteCookie();
}
Minz_Translate::init(FreshRSS_Context::$user_conf->language);
Minz_Translate::init(FreshRSS_Context::userConf()->language);
// All is good, go back to the original request or the index.
$url = Minz_Url::unserialize(Minz_Request::paramString('original_request'));
@ -169,13 +181,12 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
Minz_Request::good(_t('feedback.auth.login.success'), $url);
} else {
Minz_Log::warning("Password mismatch for user={$username}, nonce={$nonce}, c={$challenge}");
header('HTTP/1.1 403 Forbidden');
Minz_Session::_param('POST_to_GET', true); //Prevent infinite internal redirect
Minz_Request::setBadNotification(_t('feedback.auth.login.invalid'));
Minz_Request::forward(['c' => 'auth', 'a' => 'login'], false);
}
} elseif (FreshRSS_Context::$system_conf->unsafe_autologin_enabled) {
} elseif (FreshRSS_Context::systemConf()->unsafe_autologin_enabled) {
$username = Minz_Request::paramString('u');
$password = Minz_Request::paramString('p');
Minz_Request::_param('p');
@ -187,11 +198,11 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
FreshRSS_FormAuth::deleteCookie();
FreshRSS_Context::initUser($username);
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
return;
}
$s = FreshRSS_Context::$user_conf->passwordHash;
$s = FreshRSS_Context::userConf()->passwordHash;
$ok = password_verify($password, $s);
unset($password);
if ($ok) {
@ -202,7 +213,7 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
]);
FreshRSS_Auth::giveAccess();
Minz_Translate::init(FreshRSS_Context::$user_conf->language);
Minz_Translate::init(FreshRSS_Context::userConf()->language);
Minz_Request::good(_t('feedback.auth.login.success'), ['c' => 'index', 'a' => 'index']);
} else {
@ -241,8 +252,19 @@ class FreshRSS_auth_Controller extends FreshRSS_ActionController {
}
$this->view->show_tos_checkbox = file_exists(TOS_FILENAME);
$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
$this->view->preferred_language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::$system_conf->language);
$this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation;
$this->view->preferred_language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
FreshRSS_View::prependTitle(_t('gen.auth.registration.title') . ' · ');
}
public static function getLogoutUrl(): string {
if (($_SERVER['AUTH_TYPE'] ?? '') === 'openid-connect') {
$url_string = urlencode(Minz_Request::guessBaseUrl());
return './oidc/?logout=' . $url_string . '/';
# The trailing slash is necessary so that we dont redirect to http://.
# https://bz.apache.org/bugzilla/show_bug.cgi?id=61355#c13
} else {
return _url('auth', 'logout') ?: '';
}
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle actions relative to categories.
@ -32,7 +33,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
$url_redirect = ['c' => 'subscription', 'a' => 'add'];
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$this->view->categories = $catDAO->listCategories(false) ?: [];
if (count($this->view->categories) >= $limits['max_categories']) {
@ -60,10 +61,10 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
$opml_url = checkUrl(Minz_Request::paramString('opml_url'));
if ($opml_url != '') {
$cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
$cat->_attributes('opml_url', $opml_url);
$cat->_attribute('opml_url', $opml_url);
} else {
$cat->_kind(FreshRSS_Category::KIND_NORMAL);
$cat->_attributes('opml_url', null);
$cat->_attribute('opml_url', null);
}
if ($catDAO->addCategoryObject($cat)) {
@ -79,45 +80,80 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
/**
* This action updates the given category.
* @todo Check whether this function is used at all
* @see FreshRSS_subscription_Controller::categoryAction() (consider merging)
*
* Request parameters are:
* - id
* - name
*/
public function updateAction(): void {
$catDAO = FreshRSS_Factory::createCategoryDao();
$url_redirect = ['c' => 'subscription', 'a' => 'index'];
if (Minz_Request::paramBoolean('ajax')) {
$this->view->_layout(null);
}
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$id = Minz_Request::paramInt('id');
$category = $categoryDAO->searchById($id);
if ($id === 0 || null === $category) {
Minz_Error::error(404);
return;
}
$this->view->category = $category;
FreshRSS_View::prependTitle($category->name() . ' · ' . _t('sub.title') . ' · ');
if (Minz_Request::isPost()) {
invalidateHttpCache();
$category->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
$id = Minz_Request::paramInt('id');
$name = Minz_Request::paramString('name');
if (strlen($name) <= 0) {
Minz_Request::bad(_t('feedback.sub.category.no_name'), $url_redirect);
if (Minz_Request::paramBoolean('use_default_purge_options')) {
$category->_attribute('archiving', null);
} else {
if (!Minz_Request::paramBoolean('enable_keep_max')) {
$keepMax = false;
} elseif (($keepMax = Minz_Request::paramInt('keep_max')) !== 0) {
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
}
if (Minz_Request::paramBoolean('enable_keep_period')) {
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
if (is_numeric(Minz_Request::paramString('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::paramString('keep_period_unit'))) {
$keepPeriod = str_replace('1', Minz_Request::paramString('keep_period_count'), Minz_Request::paramString('keep_period_unit'));
}
} else {
$keepPeriod = false;
}
$category->_attribute('archiving', [
'keep_period' => $keepPeriod,
'keep_max' => $keepMax,
'keep_min' => Minz_Request::paramInt('keep_min'),
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
]);
}
$cat = $catDAO->searchById($id);
if ($cat === null) {
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
$position = Minz_Request::paramInt('position') ?: null;
$category->_attribute('position', $position);
$opml_url = checkUrl(Minz_Request::paramString('opml_url'));
if ($opml_url != '') {
$category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
$category->_attribute('opml_url', $opml_url);
} else {
$category->_kind(FreshRSS_Category::KIND_NORMAL);
$category->_attribute('opml_url', null);
}
$values = [
'name' => $cat->name(),
'kind' => $cat->kind(),
'attributes' => $cat->attributes(),
'kind' => $category->kind(),
'name' => Minz_Request::paramString('name'),
'attributes' => $category->attributes(),
];
if ($catDAO->updateCategory($id, $values)) {
invalidateHttpCache();
$url_redirect = ['c' => 'subscription', 'params' => ['id' => $id, 'type' => 'category']];
if (false !== $categoryDAO->updateCategory($id, $values)) {
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
}
Minz_Request::forward($url_redirect, true);
}
/**
@ -154,9 +190,10 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
}
// Remove related queries.
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'c_' . $id, FreshRSS_Context::$user_conf->queries);
FreshRSS_Context::$user_conf->save();
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */
$queries = remove_query_by_get('c_' . $id, FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.sub.category.deleted'), $url_redirect);
}
@ -194,10 +231,11 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
// Remove related queries
foreach ($feeds as $feed) {
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'f_' . $feed->id(), FreshRSS_Context::$user_conf->queries);
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> */
$queries = remove_query_by_get('f_' . $feed->id(), FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
}
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.sub.category.emptied'), $url_redirect);
} else {
@ -222,11 +260,13 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
$id = Minz_Request::paramInt('id');
if ($id === 0) {
Minz_Request::bad(_t('feedback.sub.category.no_id'), $url_redirect);
return;
}
$category = $catDAO->searchById($id);
if ($category === null) {
Minz_Request::bad(_t('feedback.sub.category.not_exist'), $url_redirect);
return;
}
invalidateHttpCache();
@ -252,7 +292,7 @@ class FreshRSS_category_Controller extends FreshRSS_ActionController {
$successes = 0;
$errors = 0;
$catDAO = FreshRSS_Factory::createCategoryDao();
$categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::$user_conf->dynamic_opml_ttl_default ?? 86400);
$categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::userConf()->dynamic_opml_ttl_default ?? 86400);
foreach ($categories as $category) {
if ($category->refreshDynamicOpml()) {
$successes++;

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle every configuration options.
@ -34,7 +35,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* - display of read action in footer
* - display of favorite action in footer
* - display of sharing action in footer
* - display of tags in footer
* - display of article tags in footer
* - display of my Labels in footer
* - display of date in footer
* - display of open action in footer
* - html5 notification timeout (default: 0)
@ -42,31 +44,32 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
*/
public function displayAction(): void {
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->language = Minz_Request::paramString('language') ?: 'en';
FreshRSS_Context::$user_conf->timezone = Minz_Request::paramString('timezone');
FreshRSS_Context::$user_conf->theme = Minz_Request::paramString('theme') ?: FreshRSS_Themes::$defaultTheme;
FreshRSS_Context::$user_conf->darkMode = Minz_Request::paramString('darkMode') ?: 'no';
FreshRSS_Context::$user_conf->content_width = Minz_Request::paramString('content_width') ?: 'thin';
FreshRSS_Context::$user_conf->topline_read = Minz_Request::paramBoolean('topline_read');
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::paramBoolean('topline_favorite');
FreshRSS_Context::$user_conf->topline_date = Minz_Request::paramBoolean('topline_date');
FreshRSS_Context::$user_conf->topline_link = Minz_Request::paramBoolean('topline_link');
FreshRSS_Context::$user_conf->topline_website = Minz_Request::paramString('topline_website');
FreshRSS_Context::$user_conf->topline_thumbnail = Minz_Request::paramString('topline_thumbnail');
FreshRSS_Context::$user_conf->topline_summary = Minz_Request::paramBoolean('topline_summary');
FreshRSS_Context::$user_conf->topline_display_authors = Minz_Request::paramBoolean('topline_display_authors');
FreshRSS_Context::$user_conf->bottomline_read = Minz_Request::paramBoolean('bottomline_read');
FreshRSS_Context::$user_conf->bottomline_favorite = Minz_Request::paramBoolean('bottomline_favorite');
FreshRSS_Context::$user_conf->bottomline_sharing = Minz_Request::paramBoolean('bottomline_sharing');
FreshRSS_Context::$user_conf->bottomline_tags = Minz_Request::paramBoolean('bottomline_tags');
FreshRSS_Context::$user_conf->bottomline_date = Minz_Request::paramBoolean('bottomline_date');
FreshRSS_Context::$user_conf->bottomline_link = Minz_Request::paramBoolean('bottomline_link');
FreshRSS_Context::$user_conf->show_nav_buttons = Minz_Request::paramBoolean('show_nav_buttons');
FreshRSS_Context::$user_conf->html5_notif_timeout = Minz_Request::paramInt('html5_notif_timeout');
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->language = Minz_Request::paramString('language') ?: 'en';
FreshRSS_Context::userConf()->timezone = Minz_Request::paramString('timezone');
FreshRSS_Context::userConf()->theme = Minz_Request::paramString('theme') ?: FreshRSS_Themes::$defaultTheme;
FreshRSS_Context::userConf()->darkMode = Minz_Request::paramString('darkMode') ?: 'no';
FreshRSS_Context::userConf()->content_width = Minz_Request::paramString('content_width') ?: 'thin';
FreshRSS_Context::userConf()->topline_read = Minz_Request::paramBoolean('topline_read');
FreshRSS_Context::userConf()->topline_favorite = Minz_Request::paramBoolean('topline_favorite');
FreshRSS_Context::userConf()->topline_date = Minz_Request::paramBoolean('topline_date');
FreshRSS_Context::userConf()->topline_link = Minz_Request::paramBoolean('topline_link');
FreshRSS_Context::userConf()->topline_website = Minz_Request::paramString('topline_website');
FreshRSS_Context::userConf()->topline_thumbnail = Minz_Request::paramString('topline_thumbnail');
FreshRSS_Context::userConf()->topline_summary = Minz_Request::paramBoolean('topline_summary');
FreshRSS_Context::userConf()->topline_display_authors = Minz_Request::paramBoolean('topline_display_authors');
FreshRSS_Context::userConf()->bottomline_read = Minz_Request::paramBoolean('bottomline_read');
FreshRSS_Context::userConf()->bottomline_favorite = Minz_Request::paramBoolean('bottomline_favorite');
FreshRSS_Context::userConf()->bottomline_sharing = Minz_Request::paramBoolean('bottomline_sharing');
FreshRSS_Context::userConf()->bottomline_tags = Minz_Request::paramBoolean('bottomline_tags');
FreshRSS_Context::userConf()->bottomline_myLabels = Minz_Request::paramBoolean('bottomline_myLabels');
FreshRSS_Context::userConf()->bottomline_date = Minz_Request::paramBoolean('bottomline_date');
FreshRSS_Context::userConf()->bottomline_link = Minz_Request::paramBoolean('bottomline_link');
FreshRSS_Context::userConf()->show_nav_buttons = Minz_Request::paramBoolean('show_nav_buttons');
FreshRSS_Context::userConf()->html5_notif_timeout = Minz_Request::paramInt('html5_notif_timeout');
FreshRSS_Context::userConf()->save();
Minz_Session::_param('language', FreshRSS_Context::$user_conf->language);
Minz_Translate::reset(FreshRSS_Context::$user_conf->language);
Minz_Session::_param('language', FreshRSS_Context::userConf()->language);
Minz_Translate::reset(FreshRSS_Context::userConf()->language);
invalidateHttpCache();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'display' ]);
@ -103,41 +106,48 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* - opened on site
* - scrolled
* - received
* - focus
* Default values are false unless specified.
*/
public function readingAction(): void {
if (Minz_Request::isPost()) {
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::paramInt('posts_per_page') ?: 10;
FreshRSS_Context::$user_conf->view_mode = Minz_Request::paramString('view_mode', true) ?: 'normal';
FreshRSS_Context::$user_conf->default_view = Minz_Request::paramString('default_view') ?: 'adaptive';
FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::paramBoolean('show_fav_unread');
FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::paramBoolean('auto_load_more');
FreshRSS_Context::$user_conf->display_posts = Minz_Request::paramBoolean('display_posts');
FreshRSS_Context::$user_conf->display_categories = Minz_Request::paramString('display_categories') ?: 'active';
FreshRSS_Context::$user_conf->show_tags = Minz_Request::paramString('show_tags') ?: '0';
FreshRSS_Context::$user_conf->show_tags_max = Minz_Request::paramInt('show_tags_max');
FreshRSS_Context::$user_conf->show_author_date = Minz_Request::paramString('show_author_date') ?: '0';
FreshRSS_Context::$user_conf->show_feed_name = Minz_Request::paramString('show_feed_name') ?: 't';
FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::paramBoolean('hide_read_feeds');
FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::paramBoolean('onread_jump_next');
FreshRSS_Context::$user_conf->lazyload = Minz_Request::paramBoolean('lazyload');
FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::paramBoolean('sides_close_article');
FreshRSS_Context::$user_conf->sticky_post = Minz_Request::paramBoolean('sticky_post');
FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
FreshRSS_Context::$user_conf->sort_order = Minz_Request::paramString('sort_order') ?: 'DESC';
FreshRSS_Context::$user_conf->mark_when = [
FreshRSS_Context::userConf()->posts_per_page = Minz_Request::paramInt('posts_per_page') ?: 10;
FreshRSS_Context::userConf()->view_mode = Minz_Request::paramString('view_mode', true) ?: 'normal';
FreshRSS_Context::userConf()->default_view = Minz_Request::paramString('default_view') ?: 'adaptive';
FreshRSS_Context::userConf()->show_fav_unread = Minz_Request::paramBoolean('show_fav_unread');
FreshRSS_Context::userConf()->auto_load_more = Minz_Request::paramBoolean('auto_load_more');
FreshRSS_Context::userConf()->display_posts = Minz_Request::paramBoolean('display_posts');
FreshRSS_Context::userConf()->display_categories = Minz_Request::paramString('display_categories') ?: 'active';
FreshRSS_Context::userConf()->show_tags = Minz_Request::paramString('show_tags') ?: '0';
FreshRSS_Context::userConf()->show_tags_max = Minz_Request::paramInt('show_tags_max');
FreshRSS_Context::userConf()->show_author_date = Minz_Request::paramString('show_author_date') ?: '0';
FreshRSS_Context::userConf()->show_feed_name = Minz_Request::paramString('show_feed_name') ?: 't';
FreshRSS_Context::userConf()->hide_read_feeds = Minz_Request::paramBoolean('hide_read_feeds');
FreshRSS_Context::userConf()->onread_jump_next = Minz_Request::paramBoolean('onread_jump_next');
FreshRSS_Context::userConf()->lazyload = Minz_Request::paramBoolean('lazyload');
FreshRSS_Context::userConf()->sides_close_article = Minz_Request::paramBoolean('sides_close_article');
FreshRSS_Context::userConf()->sticky_post = Minz_Request::paramBoolean('sticky_post');
FreshRSS_Context::userConf()->reading_confirm = Minz_Request::paramBoolean('reading_confirm');
FreshRSS_Context::userConf()->auto_remove_article = Minz_Request::paramBoolean('auto_remove_article');
FreshRSS_Context::userConf()->mark_updated_article_unread = Minz_Request::paramBoolean('mark_updated_article_unread');
if (in_array(Minz_Request::paramString('sort_order'), ['ASC', 'DESC'], true)) {
FreshRSS_Context::userConf()->sort_order = Minz_Request::paramString('sort_order');
} else {
FreshRSS_Context::userConf()->sort_order = 'DESC';
}
FreshRSS_Context::userConf()->mark_when = [
'article' => Minz_Request::paramBoolean('mark_open_article'),
'gone' => Minz_Request::paramBoolean('read_upon_gone'),
'max_n_unread' => Minz_Request::paramBoolean('enable_keep_max_n_unread') ? Minz_Request::paramInt('keep_max_n_unread') : false,
'reception' => Minz_Request::paramBoolean('mark_upon_reception'),
'same_title_in_feed' =>
Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') && Minz_Request::paramBoolean('read_when_same_title_in_feed'),
'same_title_in_feed' => Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') ?
Minz_Request::paramInt('read_when_same_title_in_feed') : false,
'scroll' => Minz_Request::paramBoolean('mark_scroll'),
'site' => Minz_Request::paramBoolean('mark_open_site'),
'focus' => Minz_Request::paramBoolean('mark_focus'),
];
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'reading' ]);
@ -162,8 +172,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (Minz_Request::isPost()) {
$params = $_POST;
FreshRSS_Context::$user_conf->sharing = $params['share'];
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->sharing = $params['share'];
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'integration' ]);
@ -193,8 +203,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
$default = Minz_Configuration::load(FRESHRSS_PATH . '/config-user.default.php');
$shortcuts = $default['shortcuts'];
}
FreshRSS_Context::$user_conf->shortcuts = array_map('trim', $shortcuts);
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->shortcuts = array_map('trim', $shortcuts);
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
Minz_Request::good(_t('feedback.conf.shortcuts_updated'), ['c' => 'configure', 'a' => 'shortcut']);
@ -231,8 +241,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
$keepPeriod = false;
}
FreshRSS_Context::$user_conf->ttl_default = Minz_Request::paramInt('ttl_default') ?: FreshRSS_Feed::TTL_DEFAULT;
FreshRSS_Context::$user_conf->archiving = [
FreshRSS_Context::userConf()->ttl_default = Minz_Request::paramInt('ttl_default') ?: FreshRSS_Feed::TTL_DEFAULT;
FreshRSS_Context::userConf()->archiving = [
'keep_period' => $keepPeriod,
'keep_max' => $keepMax,
'keep_min' => Minz_Request::paramInt('keep_min_default'),
@ -240,9 +250,9 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
];
FreshRSS_Context::$user_conf->keep_history_default = null; //Legacy < FreshRSS 1.15
FreshRSS_Context::$user_conf->old_entries = null; //Legacy < FreshRSS 1.15
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->keep_history_default = null; //Legacy < FreshRSS 1.15
FreshRSS_Context::userConf()->old_entries = null; //Legacy < FreshRSS 1.15
FreshRSS_Context::userConf()->save();
invalidateHttpCache();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'archiving' ]);
@ -253,15 +263,17 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
'keep_period_count' => '3',
'keep_period_unit' => 'P1M',
];
$keepPeriod = FreshRSS_Context::$user_conf->archiving['keep_period'];
if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
$volatile = [
'enable_keep_period' => true,
'keep_period_count' => $matches['count'],
'keep_period_unit' => str_replace($matches['count'], '1', $keepPeriod),
];
if (!empty(FreshRSS_Context::userConf()->archiving['keep_period'])) {
$keepPeriod = FreshRSS_Context::userConf()->archiving['keep_period'];
if (preg_match('/^PT?(?P<count>\d+)[YMWDH]$/', $keepPeriod, $matches)) {
$volatile = [
'enable_keep_period' => true,
'keep_period_count' => $matches['count'],
'keep_period_unit' => str_replace($matches['count'], '1', $keepPeriod),
];
}
}
FreshRSS_Context::$user_conf->volatile = $volatile;
FreshRSS_Context::userConf()->volatile = $volatile;
$entryDAO = FreshRSS_Factory::createEntryDao();
$this->view->nb_total = $entryDAO->count();
@ -289,12 +301,8 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
public function queriesAction(): void {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/draggable.js?' . @filemtime(PUBLIC_PATH . '/scripts/draggable.js')));
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
if (Minz_Request::isPost()) {
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $params */
/** @var array<int,array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $params */
$params = Minz_Request::paramArray('queries');
$queries = [];
@ -306,22 +314,22 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($query['search'])) {
$query['search'] = urldecode($query['search']);
}
$queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
} else {
$this->view->queries = [];
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao);
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
$this->view->queries[intval($key)] = new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
}
}
$this->view->categories = $category_dao->listCategories(false) ?: [];
$this->view->feeds = $feed_dao->listFeeds();
$this->view->tags = $tag_dao->listTags() ?: [];
$this->view->categories = FreshRSS_Context::categories();
$this->view->feeds = FreshRSS_Context::feeds();
$this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::paramTernary('id') !== null) {
$id = Minz_Request::paramInt('id');
@ -346,26 +354,26 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
}
$id = Minz_Request::paramInt('id');
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::$user_conf->queries[$id])) {
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::userConf()->queries[$id])) {
Minz_Error::error(404);
return;
}
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
$query = new FreshRSS_UserQuery(FreshRSS_Context::$user_conf->queries[$id], $feed_dao, $category_dao, $tag_dao);
$query = new FreshRSS_UserQuery(FreshRSS_Context::userConf()->queries[$id], FreshRSS_Context::categories(), FreshRSS_Context::labels());
$this->view->query = $query;
$this->view->queryId = $id;
$this->view->categories = $category_dao->listCategories(false) ?: [];
$this->view->feeds = $feed_dao->listFeeds();
$this->view->tags = $tag_dao->listTags() ?: [];
$this->view->categories = FreshRSS_Context::categories();
$this->view->feeds = FreshRSS_Context::feeds();
$this->view->tags = FreshRSS_Context::labels();
if (Minz_Request::isPost()) {
/** @var array<string,string|array<string,string>> $params */
$params = array_filter(Minz_Request::paramArray('query'));
$queryParams = [];
$name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
if ('' === $name) {
$name = _t('conf.query.number', $id + 1);
}
$queryParams['name'] = $name;
if (!empty($params['get']) && is_string($params['get'])) {
$queryParams['get'] = htmlspecialchars_decode($params['get'], ENT_QUOTES);
}
@ -378,17 +386,23 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
if (!empty($params['state']) && is_array($params['state'])) {
$queryParams['state'] = (int)(array_sum($params['state']));
}
$name = Minz_Request::paramString('name') ?: _t('conf.query.number', $id + 1);
if ('' === $name) {
$name = _t('conf.query.number', $id + 1);
if (empty($params['token']) || !is_string($params['token'])) {
$queryParams['token'] = FreshRSS_UserQuery::generateToken($name);
} else {
$queryParams['token'] = $params['token'];
}
if (!empty($params['shareRss']) && ctype_digit($params['shareRss'])) {
$queryParams['shareRss'] = (bool)$params['shareRss'];
}
if (!empty($params['shareOpml']) && ctype_digit($params['shareOpml'])) {
$queryParams['shareOpml'] = (bool)$params['shareOpml'];
}
$queryParams['name'] = $name;
$queryParams['url'] = Minz_Url::display(['params' => $queryParams]);
$queries = FreshRSS_Context::$user_conf->queries;
$queries[$id] = (new FreshRSS_UserQuery($queryParams, $feed_dao, $category_dao, $tag_dao))->toArray();
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
$queries = FreshRSS_Context::userConf()->queries;
$queries[$id] = (new FreshRSS_UserQuery($queryParams, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries', 'params' => ['id' => (string)$id] ]);
}
@ -401,15 +415,15 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
*/
public function deleteQueryAction(): void {
$id = Minz_Request::paramInt('id');
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::$user_conf->queries[$id])) {
if (Minz_Request::paramTernary('id') === null || empty(FreshRSS_Context::userConf()->queries[$id])) {
Minz_Error::error(404);
return;
}
$queries = FreshRSS_Context::$user_conf->queries;
$queries = FreshRSS_Context::userConf()->queries;
unset($queries[$id]);
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.conf.updated'), [ 'c' => 'configure', 'a' => 'queries' ]);
}
@ -422,21 +436,18 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
* lean data.
*/
public function bookmarkQueryAction(): void {
$category_dao = FreshRSS_Factory::createCategoryDao();
$feed_dao = FreshRSS_Factory::createFeedDao();
$tag_dao = FreshRSS_Factory::createTagDao();
$queries = [];
foreach (FreshRSS_Context::$user_conf->queries as $key => $query) {
$queries[$key] = (new FreshRSS_UserQuery($query, $feed_dao, $category_dao, $tag_dao))->toArray();
foreach (FreshRSS_Context::userConf()->queries as $key => $query) {
$queries[$key] = (new FreshRSS_UserQuery($query, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
}
$params = $_GET;
unset($params['rid']);
$params['url'] = Minz_Url::display(['params' => $params]);
$params['name'] = _t('conf.query.number', count($queries) + 1);
$queries[] = (new FreshRSS_UserQuery($params, $feed_dao, $category_dao, $tag_dao))->toArray();
$queries[] = (new FreshRSS_UserQuery($params, FreshRSS_Context::categories(), FreshRSS_Context::labels()))->toArray();
FreshRSS_Context::$user_conf->queries = $queries;
FreshRSS_Context::$user_conf->save();
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
Minz_Request::good(_t('feedback.conf.query_created', $params['name']), [ 'c' => 'configure', 'a' => 'queries' ]);
}
@ -465,16 +476,16 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
}
if (Minz_Request::isPost()) {
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$limits['max_registrations'] = Minz_Request::paramInt('max-registrations') ?: 1;
$limits['max_feeds'] = Minz_Request::paramInt('max-feeds') ?: 16384;
$limits['max_categories'] = Minz_Request::paramInt('max-categories') ?: 16384;
$limits['cookie_duration'] = Minz_Request::paramInt('cookie-duration') ?: FreshRSS_Auth::DEFAULT_COOKIE_DURATION;
FreshRSS_Context::$system_conf->limits = $limits;
FreshRSS_Context::$system_conf->title = Minz_Request::paramString('instance-name') ?: 'FreshRSS';
FreshRSS_Context::$system_conf->auto_update_url = Minz_Request::paramString('auto-update-url');
FreshRSS_Context::$system_conf->force_email_validation = Minz_Request::paramBoolean('force-email-validation');
FreshRSS_Context::$system_conf->save();
FreshRSS_Context::systemConf()->limits = $limits;
FreshRSS_Context::systemConf()->title = Minz_Request::paramString('instance-name') ?: 'FreshRSS';
FreshRSS_Context::systemConf()->auto_update_url = Minz_Request::paramString('auto-update-url');
FreshRSS_Context::systemConf()->force_email_validation = Minz_Request::paramBoolean('force-email-validation');
FreshRSS_Context::systemConf()->save();
invalidateHttpCache();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle every entry actions.
@ -7,9 +8,8 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
/**
* JavaScript request or not.
* @var bool
*/
private $ajax = false;
private bool $ajax = false;
/**
* This action is called before every other action in that class. It is
@ -72,7 +72,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
if (!$get) {
// No get? Mark all entries as read (from $id_max)
$entryDAO->markReadEntries($id_max, false, 0, null, 0, $is_read);
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT, null, 0, $is_read);
} else {
$type_get = $get[0];
$get = (int)substr($get, 2);
@ -84,10 +84,16 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
$entryDAO->markReadFeed($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
break;
case 's':
$entryDAO->markReadEntries($id_max, true, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
$entryDAO->markReadEntries($id_max, true, null, FreshRSS_Feed::PRIORITY_IMPORTANT,
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
break;
case 'a':
$entryDAO->markReadEntries($id_max, false, 0, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_MAIN_STREAM, FreshRSS_Feed::PRIORITY_IMPORTANT,
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
break;
case 'i':
$entryDAO->markReadEntries($id_max, false, FreshRSS_Feed::PRIORITY_IMPORTANT, null,
FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
break;
case 't':
$entryDAO->markReadTag($get, $id_max, FreshRSS_Context::$search, FreshRSS_Context::$state, $is_read);
@ -169,7 +175,9 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
Minz_Request::forward($url_redirect, true);
}
@set_time_limit(300);
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->optimize();
@ -188,7 +196,9 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
* @todo should be in feedController
*/
public function purgeAction(): void {
@set_time_limit(300);
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$feeds = $feedDAO->listFeeds();
@ -199,7 +209,7 @@ class FreshRSS_entry_Controller extends FreshRSS_ActionController {
$feedDAO->beginTransaction();
foreach ($feeds as $feed) {
$nb_total += $feed->cleanOldEntries();
$nb_total += ($feed->cleanOldEntries() ?: 0);
}
$feedDAO->updateCachedValues();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle error page.
@ -14,8 +15,9 @@ class FreshRSS_error_Controller extends FreshRSS_ActionController {
* - error_logs (default: array())
*/
public function indexAction(): void {
$code_int = Minz_Session::param('error_code', 404);
$error_logs = Minz_Session::param('error_logs', []);
$code_int = Minz_Session::paramInt('error_code') ?: 404;
/** @var array<string> */
$error_logs = Minz_Session::paramArray('error_logs');
Minz_Session::_params([
'error_code' => false,
'error_logs' => false,

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* The controller to manage extensions.
@ -38,7 +39,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
/**
* fetch extension list from GitHub
* @return array<string,array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}>
* @return array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}>
*/
protected function getAvailableExtensionList(): array {
$extensionListUrl = 'https://raw.githubusercontent.com/FreshRSS/Extensions/master/extensions.json';
@ -53,7 +54,7 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
// fetch the list as an array
/** @var array<string,mixed> $list*/
$list = json_decode($json, true);
if (empty($list)) {
if (!is_array($list) || empty($list['extensions']) || !is_array($list['extensions'])) {
Minz_Log::warning('Failed to convert extension file list');
return [];
}
@ -61,9 +62,21 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
// By now, all the needed data is kept in the main extension file.
// In the future we could fetch detail information from the extensions metadata.json, but I tend to stick with
// the current implementation for now, unless it becomes too much effort maintain the extension list manually
/** @var array<string,array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> $extensions*/
$extensions = $list['extensions'];
$extensions = [];
foreach ($list['extensions'] as $extension) {
if (isset($extension['version']) && is_numeric($extension['version'])) {
$extension['version'] = (string)$extension['version'];
}
foreach (['author', 'description', 'directory', 'entrypoint', 'method', 'name', 'type', 'url', 'version'] as $key) {
if (empty($extension[$key]) || !is_string($extension[$key])) {
continue 2;
}
}
if (!in_array($extension['type'], ['system', 'user'], true)) {
continue;
}
$extensions[] = $extension;
}
return $extensions;
}
@ -135,9 +148,9 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
$conf = null;
if ($type === 'system') {
$conf = FreshRSS_Context::$system_conf;
$conf = FreshRSS_Context::systemConf();
} elseif ($type === 'user') {
$conf = FreshRSS_Context::$user_conf;
$conf = FreshRSS_Context::userConf();
}
$res = $ext->install();
@ -197,9 +210,9 @@ class FreshRSS_extension_Controller extends FreshRSS_ActionController {
$conf = null;
if ($type === 'system') {
$conf = FreshRSS_Context::$system_conf;
$conf = FreshRSS_Context::systemConf();
} elseif ($type === 'user') {
$conf = FreshRSS_Context::$user_conf;
$conf = FreshRSS_Context::userConf();
}
$res = $ext->uninstall();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle every feed actions.
@ -14,11 +15,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
// Token is useful in the case that anonymous refresh is forbidden
// and CRON task cannot be used with php command so the user can
// set a CRON task to refresh his feeds by using token inside url
$token = FreshRSS_Context::$user_conf->token;
$token = FreshRSS_Context::userConf()->token;
$token_param = Minz_Request::paramString('token');
$token_is_ok = ($token != '' && $token == $token_param);
$action = Minz_Request::actionName();
$allow_anonymous_refresh = FreshRSS_Context::$system_conf->allow_anonymous_refresh;
$allow_anonymous_refresh = FreshRSS_Context::systemConf()->allow_anonymous_refresh;
if ($action !== 'actualize' ||
!($allow_anonymous_refresh || $token_is_ok)) {
Minz_Error::error(403);
@ -29,20 +30,23 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
/**
* @param array<string,mixed> $attributes
* @throws FreshRSS_AlreadySubscribed_Exception
* @throws FreshRSS_FeedNotAdded_Exception
* @throws FreshRSS_BadUrl_Exception
* @throws FreshRSS_Feed_Exception
* @throws FreshRSS_FeedNotAdded_Exception
* @throws Minz_FileNotExistException
*/
public static function addFeed(string $url, string $title = '', int $cat_id = 0, string $new_cat_name = '',
string $http_auth = '', array $attributes = [], int $kind = FreshRSS_Feed::KIND_RSS): FreshRSS_Feed {
FreshRSS_UserDAO::touch();
@set_time_limit(300);
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
$catDAO = FreshRSS_Factory::createCategoryDao();
$url = trim($url);
/** @var string|null $url */
/** @var string|null $urlHooked */
$urlHooked = Minz_ExtensionManager::callHook('check_url_before_add', $url);
if ($urlHooked === null) {
throw new FreshRSS_FeedNotAdded_Exception($url);
@ -61,7 +65,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($cat === null) {
$catDAO->checkDefault();
}
$cat_id = $cat === null ? FreshRSS_CategoryDAO::DEFAULTCATEGORYID : $cat->id();
$feed = new FreshRSS_Feed($url); //Throws FreshRSS_BadUrl_Exception
$title = trim($title);
@ -69,9 +72,13 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$feed->_name($title);
}
$feed->_kind($kind);
$feed->_attributes('', $attributes);
$feed->_attributes($attributes);
$feed->_httpAuth($http_auth);
$feed->_categoryId($cat_id);
if ($cat === null) {
$feed->_categoryId(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
} else {
$feed->_category($cat);
}
switch ($kind) {
case FreshRSS_Feed::KIND_RSS:
case FreshRSS_Feed::KIND_RSS_FORCED:
@ -102,7 +109,10 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$feed->_id($id);
// Ok, feed has been added in database. Now we have to refresh entries.
self::actualizeFeed($id, $url, false, null);
[, , $nb_new_articles] = self::actualizeFeeds($id, $url);
if ($nb_new_articles > 0) {
self::commitNewEntries();
}
return $feed;
}
@ -145,7 +155,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
'params' => [],
];
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$this->view->feeds = $feedDAO->listFeeds();
if (count($this->view->feeds) >= $limits['max_feeds']) {
Minz_Request::bad(_t('feedback.sub.feed.over_max', $limits['max_feeds']), $url_redirect);
@ -169,6 +179,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$useragent = Minz_Request::paramString('curl_params_useragent');
$proxy_address = Minz_Request::paramString('curl_params');
$proxy_type = Minz_Request::paramString('proxy_type');
$request_method = Minz_Request::paramString('curl_method');
$request_fields = Minz_Request::paramString('curl_fields', true);
$opts = [];
if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
@ -189,6 +202,15 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($useragent !== '') {
$opts[CURLOPT_USERAGENT] = $useragent;
}
if ($request_method === 'POST') {
$opts[CURLOPT_POST] = true;
if ($request_fields !== '') {
$opts[CURLOPT_POSTFIELDS] = $request_fields;
if (json_decode($request_fields, true) !== null) {
$opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json'];
}
}
}
$attributes = [
'curl_params' => empty($opts) ? null : $opts,
@ -236,6 +258,44 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if (!empty($xPathSettings)) {
$attributes['xpath'] = $xPathSettings;
}
} elseif ($feed_kind === FreshRSS_Feed::KIND_JSON_DOTPATH) {
$jsonSettings = [];
if (Minz_Request::paramString('jsonFeedTitle') !== '') {
$jsonSettings['feedTitle'] = Minz_Request::paramString('jsonFeedTitle', true);
}
if (Minz_Request::paramString('jsonItem') !== '') {
$jsonSettings['item'] = Minz_Request::paramString('jsonItem', true);
}
if (Minz_Request::paramString('jsonItemTitle') !== '') {
$jsonSettings['itemTitle'] = Minz_Request::paramString('jsonItemTitle', true);
}
if (Minz_Request::paramString('jsonItemContent') !== '') {
$jsonSettings['itemContent'] = Minz_Request::paramString('jsonItemContent', true);
}
if (Minz_Request::paramString('jsonItemUri') !== '') {
$jsonSettings['itemUri'] = Minz_Request::paramString('jsonItemUri', true);
}
if (Minz_Request::paramString('jsonItemAuthor') !== '') {
$jsonSettings['itemAuthor'] = Minz_Request::paramString('jsonItemAuthor', true);
}
if (Minz_Request::paramString('jsonItemTimestamp') !== '') {
$jsonSettings['itemTimestamp'] = Minz_Request::paramString('jsonItemTimestamp', true);
}
if (Minz_Request::paramString('jsonItemTimeFormat') !== '') {
$jsonSettings['itemTimeFormat'] = Minz_Request::paramString('jsonItemTimeFormat', true);
}
if (Minz_Request::paramString('jsonItemThumbnail') !== '') {
$jsonSettings['itemThumbnail'] = Minz_Request::paramString('jsonItemThumbnail', true);
}
if (Minz_Request::paramString('jsonItemCategories') !== '') {
$jsonSettings['itemCategories'] = Minz_Request::paramString('jsonItemCategories', true);
}
if (Minz_Request::paramString('jsonItemUid') !== '') {
$jsonSettings['itemUid'] = Minz_Request::paramString('jsonItemUid', true);
}
if (!empty($jsonSettings)) {
$attributes['json_dotpath'] = $jsonSettings;
}
}
try {
@ -324,40 +384,59 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
/**
* @return array{0:int,1:FreshRSS_Feed|false,2:int}
* @return array{0:int,1:FreshRSS_Feed|null,2:int} Number of updated feeds, first feed or null, number of new articles
* @throws FreshRSS_BadUrl_Exception
*/
public static function actualizeFeed(int $feed_id, string $feed_url, bool $force, ?SimplePie $simplePiePush = null,
bool $noCommit = false, int $maxFeeds = 10): array {
@set_time_limit(300);
public static function actualizeFeeds(?int $feed_id = null, ?string $feed_url = null, ?int $maxFeeds = null, ?SimplePie $simplePiePush = null): array {
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
if (!is_int($feed_id) || $feed_id <= 0) {
$feed_id = null;
}
if (!is_string($feed_url) || trim($feed_url) === '') {
$feed_url = null;
}
if (!is_int($maxFeeds) || $maxFeeds <= 0) {
$maxFeeds = PHP_INT_MAX;
}
$feedDAO = FreshRSS_Factory::createFeedDao();
$entryDAO = FreshRSS_Factory::createEntryDao();
// Create a list of feeds to actualize.
// If feed_id is set and valid, corresponding feed is added to the list but
// alone in order to automatize further process.
$feeds = [];
if ($feed_id > 0 || $feed_url) {
$feed = $feed_id > 0 ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
if ($feed) {
if ($feed_id !== null || $feed_url !== null) {
$feed = $feed_id !== null ? $feedDAO->searchById($feed_id) : $feedDAO->searchByUrl($feed_url);
if ($feed !== null && $feed->id() > 0) {
$feeds[] = $feed;
$feed_id = $feed->id();
}
} else {
$feeds = $feedDAO->listFeedsOrderUpdate(-1);
}
// Set maxFeeds to a minimum of 10
if ($maxFeeds < 10) {
$maxFeeds = 10;
// Hydrate category for each feed to avoid that each feed has to make an SQL request
$categories = [];
$catDAO = FreshRSS_Factory::createCategoryDao();
foreach ($catDAO->listCategories(false, false) as $category) {
$categories[$category->id()] = $category;
}
foreach ($feeds as $feed) {
$category = $categories[$feed->categoryId()] ?? null;
if ($category !== null) {
$feed->_category($category);
}
}
}
// WebSub (PubSubHubbub) support
$pubsubhubbubEnabledGeneral = FreshRSS_Context::$system_conf->pubsubhubbub_enabled;
$pubsubhubbubEnabledGeneral = FreshRSS_Context::systemConf()->pubsubhubbub_enabled;
$pshbMinAge = time() - (3600 * 24); //TODO: Make a configuration.
$updated_feeds = 0;
$nb_new_articles = 0;
foreach ($feeds as $feed) {
/** @var FreshRSS_Feed|null $feed */
$feed = Minz_ExtensionManager::callHook('feed_before_actualize', $feed);
@ -368,7 +447,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$url = $feed->url(); //For detection of HTTP 301
$pubSubHubbubEnabled = $pubsubhubbubEnabledGeneral && $feed->pubSubHubbubEnabled();
if ($simplePiePush === null && $feed_id === 0 && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
if ($simplePiePush === null && $feed_id === null && $pubSubHubbubEnabled && ($feed->lastUpdate() > $pshbMinAge)) {
//$text = 'Skip pull of feed using PubSubHubbub: ' . $url;
//Minz_Log::debug($text);
//Minz_Log::debug($text, PSHB_LOG);
@ -381,14 +460,14 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$mtime = $feed->cacheModifiedTime() ?: 0;
$ttl = $feed->ttl();
if ($ttl === FreshRSS_Feed::TTL_DEFAULT) {
$ttl = FreshRSS_Context::$user_conf->ttl_default;
$ttl = FreshRSS_Context::userConf()->ttl_default;
}
if ($simplePiePush === null && $feed_id === 0 && (time() <= $feed->lastUpdate() + $ttl)) {
if ($simplePiePush === null && $feed_id === null && (time() <= $feed->lastUpdate() + $ttl)) {
//Too early to refresh from source, but check whether the feed was updated by another user
= 10; // negligible offset errors in seconds
if ($mtime <= 0 ||
$feed->lastUpdate() + >= $mtime ||
time() + >= $mtime + FreshRSS_Context::$system_conf->limits['cache_duration']) { // is cache still valid?
time() + >= $mtime + FreshRSS_Context::systemConf()->limits['cache_duration']) { // is cache still valid?
continue; //Nothing newer from other users
}
Minz_Log::debug('Feed ' . $feed->url(false) . ' was updated at ' . date('c', $feed->lastUpdate()) .
@ -417,6 +496,16 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('XML+XPath parsing failed for [' . $feed->url(false) . ']');
}
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH) {
$simplePie = $feed->loadJson();
if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('JSON dotpath parsing failed for [' . $feed->url(false) . ']');
}
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSONFEED) {
$simplePie = $feed->loadJson();
if ($simplePie === null) {
throw new FreshRSS_Feed_Exception('JSON Feed parsing failed for [' . $feed->url(false) . ']');
}
} else {
$simplePie = $feed->load(false, $feedIsNew);
}
@ -447,19 +536,23 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
$needFeedCacheRefresh = false;
$nbMarkedUnread = 0;
if (count($newGuids) > 0) {
$titlesAsRead = [];
$readWhenSameTitleInFeed = $feed->attributes('read_when_same_title_in_feed');
if ($readWhenSameTitleInFeed == false) {
$readWhenSameTitleInFeed = FreshRSS_Context::$user_conf->mark_when['same_title_in_feed'];
if ($feed->attributeBoolean('read_when_same_title_in_feed') === null) {
$readWhenSameTitleInFeed = (int)FreshRSS_Context::userConf()->mark_when['same_title_in_feed'];
} elseif ($feed->attributeBoolean('read_when_same_title_in_feed') === false) {
$readWhenSameTitleInFeed = 0;
} else {
$readWhenSameTitleInFeed = $feed->attributeInt('read_when_same_title_in_feed') ?? 0;
}
if ($readWhenSameTitleInFeed > 0) {
/** @var array<string,bool> $titlesAsRead*/
$titlesAsRead = array_flip($feedDAO->listTitles($feed->id(), (int)$readWhenSameTitleInFeed));
$titlesAsRead = array_flip($feedDAO->listTitles($feed->id(), $readWhenSameTitleInFeed));
} else {
$titlesAsRead = [];
}
$mark_updated_article_unread = $feed->attributes('mark_updated_article_unread') ?? FreshRSS_Context::$user_conf->mark_updated_article_unread;
$mark_updated_article_unread = $feed->attributeBoolean('mark_updated_article_unread') ?? FreshRSS_Context::userConf()->mark_updated_article_unread;
// For this feed, check existing GUIDs already in database.
$existingHashForGuids = $entryDAO->listHashForFeedGuids($feed->id(), $newGuids) ?: [];
@ -494,8 +587,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
if (!$entry->isRead()) {
$needFeedCacheRefresh = true;
$feed->incPendingUnread(); //Maybe
$needFeedCacheRefresh = true; //Maybe
$nbMarkedUnread++;
}
// If the entry has changed, there is a good chance for the full content to have changed as well.
@ -509,10 +602,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
} else {
$id = uTimeString();
$entry->_id($id);
$entry->applyFilterActions($titlesAsRead);
if ($readWhenSameTitleInFeed > 0) {
$titlesAsRead[$entry->title()] = true;
}
$entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry);
if (!($entry instanceof FreshRSS_Entry)) {
@ -520,6 +609,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
continue;
}
$entry->applyFilterActions($titlesAsRead);
if ($readWhenSameTitleInFeed > 0) {
$titlesAsRead[$entry->title()] = true;
}
if ($pubSubHubbubEnabled && !$simplePiePush) { //We use push, but have discovered an article by pull!
$text = 'An article was discovered by pull although we use PubSubHubbub!: Feed ' .
SimplePie_Misc::url_remove_credentials($url) .
@ -535,9 +629,6 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
$entryDAO->addEntry($entry->toArray(), true);
if (!$entry->isRead()) {
$feed->incPendingUnread();
}
$nb_new_articles++;
}
}
@ -563,7 +654,11 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
}
$feedDAO->updateLastUpdate($feed->id(), false, $mtime);
$needFeedCacheRefresh |= ($feed->keepMaxUnread() != false);
if ($feed->keepMaxUnread() !== null && ($feed->nbNotRead() + $nbMarkedUnread > $feed->keepMaxUnread())) {
Minz_Log::debug('Existing unread entries (' . ($feed->nbNotRead() + $nbMarkedUnread) . ') exceeding max number of ' .
$feed->keepMaxUnread() . ' for [' . $feed->url(false) . ']');
$needFeedCacheRefresh |= ($feed->markAsReadMaxUnread() != false);
}
if ($simplePiePush === null) {
// Do not call for WebSub events, as we do not know the list of articles still on the upstream feed.
$needFeedCacheRefresh |= ($feed->markAsReadUponGone($feedIsEmpty, $mtime) != false);
@ -639,62 +734,128 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
unset($feed);
gc_collect_cycles();
// No more than $maxFeeds feeds unless $force is true to avoid overloading
// the server.
if ($updated_feeds >= $maxFeeds && !$force) {
if ($updated_feeds >= $maxFeeds) {
break;
}
}
if (!$noCommit && ($nb_new_articles > 0 || $updated_feeds > 0)) {
return [$updated_feeds, reset($feeds) ?: null, $nb_new_articles];
}
/**
* @return int|false The number of articles marked as read, of false if error
*/
private static function keepMaxUnreads() {
$affected = 0;
$entryDAO = FreshRSS_Factory::createEntryDao();
$newUnreadEntriesPerFeed = $entryDAO->newUnreadEntriesPerFeed();
$feedDAO = FreshRSS_Factory::createFeedDao();
$feeds = $feedDAO->listFeedsOrderUpdate(-1);
foreach ($feeds as $feed) {
if (!empty($newUnreadEntriesPerFeed[$feed->id()]) && $feed->keepMaxUnread() !== null &&
($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()] > $feed->keepMaxUnread())) {
Minz_Log::debug('New unread entries (' . ($feed->nbNotRead() + $newUnreadEntriesPerFeed[$feed->id()]) . ') exceeding max number of ' .
$feed->keepMaxUnread() . ' for [' . $feed->url(false) . ']');
$n = $feed->markAsReadMaxUnread();
if ($n === false) {
$affected = false;
break;
} else {
$affected += $n;
}
}
}
if ($feedDAO->updateCachedValues() === false) {
$affected = false;
}
return $affected;
}
/**
* Auto-add labels to new articles.
* @param int $nbNewEntries The number of top recent entries to process.
* @return int|false The number of new labels added, or false in case of error.
*/
private static function applyLabelActions(int $nbNewEntries) {
$tagDAO = FreshRSS_Factory::createTagDao();
$labels = FreshRSS_Context::labels();
$labels = array_filter($labels, static function (FreshRSS_Tag $label) {
return !empty($label->filtersAction('label'));
});
if (count($labels) <= 0) {
return 0;
}
$entryDAO = FreshRSS_Factory::createEntryDao();
/** @var array<array{id_tag:int,id_entry:string}> $applyLabels */
$applyLabels = [];
foreach (FreshRSS_Entry::fromTraversable($entryDAO->selectAll($nbNewEntries)) as $entry) {
foreach ($labels as $label) {
$label->applyFilterActions($entry, $applyLabel);
if ($applyLabel) {
$applyLabels[] = [
'id_tag' => $label->id(),
'id_entry' => $entry->id(),
];
}
}
}
return $tagDAO->tagEntries($applyLabels);
}
public static function commitNewEntries(): void {
$entryDAO = FreshRSS_Factory::createEntryDao();
['all' => $nbNewEntries, 'unread' => $nbNewUnreadEntries] = $entryDAO->countNewEntries();
if ($nbNewEntries > 0) {
if (!$entryDAO->inTransaction()) {
$entryDAO->beginTransaction();
}
$entryDAO->commitNewEntries();
$feedDAO->updateCachedValues();
if ($entryDAO->commitNewEntries()) {
self::applyLabelActions($nbNewEntries);
if ($nbNewUnreadEntries > 0) {
self::keepMaxUnreads();
}
}
if ($entryDAO->inTransaction()) {
$entryDAO->commit();
}
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();
}
return [$updated_feeds, reset($feeds), $nb_new_articles];
}
/**
* This action actualizes entries from one or several feeds.
*
* Parameters are:
* - id (default: false): Feed ID
* - url (default: false): Feed URL
* - force (default: false)
* - id (default: null): Feed ID, or set to -1 to commit new articles to the main database
* - url (default: null): Feed URL (instead of feed ID)
* - maxFeeds (default: 10): Max number of feeds to refresh
* - noCommit (default: 0): Set to 1 to prevent committing the new articles to the main database
* If id and url are not specified, all the feeds are actualized. But if force is
* false, process stops at 10 feeds to avoid time execution problem.
* If id and url are not specified, all the feeds are actualized, within the limits of maxFeeds.
*/
public function actualizeAction(): int {
Minz_Session::_param('actualize_feeds', false);
$id = Minz_Request::paramInt('id');
$url = Minz_Request::paramString('url');
$force = Minz_Request::paramBoolean('force');
$maxFeeds = Minz_Request::paramInt('maxFeeds');
$maxFeeds = Minz_Request::paramInt('maxFeeds') ?: 10;
$noCommit = ($_POST['noCommit'] ?? 0) == 1;
$feed = null;
if ($id == -1 && !$noCommit) { //Special request only to commit & refresh DB cache
if ($id === -1 && !$noCommit) { //Special request only to commit & refresh DB cache
$updated_feeds = 0;
$entryDAO = FreshRSS_Factory::createEntryDao();
$feedDAO = FreshRSS_Factory::createFeedDao();
$entryDAO->beginTransaction();
$entryDAO->commitNewEntries();
$feedDAO->updateCachedValues();
$entryDAO->commit();
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();
$feed = null;
self::commitNewEntries();
} else {
FreshRSS_category_Controller::refreshDynamicOpmls();
[$updated_feeds, $feed] = self::actualizeFeed($id, $url, $force, null, $noCommit, $maxFeeds);
if ($id === 0 && $url === '') {
// Case of a batch refresh (e.g. cron)
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
FreshRSS_feed_Controller::commitNewEntries();
FreshRSS_category_Controller::refreshDynamicOpmls();
}
[$updated_feeds, $feed, $nbNewArticles] = self::actualizeFeeds($id, $url, $maxFeeds);
if (!$noCommit && $nbNewArticles > 0) {
FreshRSS_feed_Controller::commitNewEntries();
}
}
if (Minz_Request::paramBoolean('ajax')) {
@ -706,22 +867,19 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
$this->view->_layout(null);
} elseif ($feed instanceof FreshRSS_Feed) {
// Redirect to the main page with correct notification.
if ($updated_feeds === 1) {
Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), [
'params' => ['get' => 'f_' . $feed->id()]
]);
} elseif ($updated_feeds > 1) {
Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), []);
} else {
Minz_Request::good(_t('feedback.sub.feed.no_refresh'), []);
}
Minz_Request::good(_t('feedback.sub.feed.actualized', $feed->name()), [
'params' => ['get' => 'f_' . $id]
]);
} elseif ($updated_feeds >= 1) {
Minz_Request::good(_t('feedback.sub.feed.n_actualized', $updated_feeds), []);
} else {
Minz_Request::good(_t('feedback.sub.feed.no_refresh'), []);
}
return $updated_feeds;
}
/**
* @throws Minz_ConfigurationNamespaceException
* @throws JsonException
* @throws Minz_PDOConnectionException
*/
public static function renameFeed(int $feed_id, string $feed_name): bool {
@ -793,10 +951,10 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
// TODO: Delete old favicon
// Remove related queries
FreshRSS_Context::$user_conf->queries = remove_query_by_get(
'f_' . $feed_id, FreshRSS_Context::$user_conf->queries);
FreshRSS_Context::$user_conf->save();
/** @var array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries */
$queries = remove_query_by_get('f_' . $feed_id, FreshRSS_Context::userConf()->queries);
FreshRSS_Context::userConf()->queries = $queries;
FreshRSS_Context::userConf()->save();
return true;
}
return false;
@ -875,7 +1033,9 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
* @throws FreshRSS_BadUrl_Exception
*/
public function reloadAction(): void {
@set_time_limit(300);
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
//Get Feed ID.
$feed_id = Minz_Request::paramInt('id');
@ -892,14 +1052,17 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
//Re-fetch articles as if the feed was new.
$feedDAO->updateFeed($feed->id(), [ 'lastUpdate' => 0 ]);
self::actualizeFeed($feed_id, '', false);
[, , $nb_new_articles] = self::actualizeFeeds($feed_id);
if ($nb_new_articles > 0) {
FreshRSS_feed_Controller::commitNewEntries();
}
//Extract all feed entries from database, load complete content and store them back in database.
$entries = $entryDAO->listWhere('f', $feed_id, FreshRSS_Entry::STATE_ALL, 'DESC', $limit);
//We need another DB connection in parallel for unbuffered streaming
Minz_ModelPdo::$usesSharedPdo = false;
if (FreshRSS_Context::$system_conf->db['type'] === 'mysql') {
if (FreshRSS_Context::systemConf()->db['type'] === 'mysql') {
// Second parallel connection for unbuffered streaming: MySQL
$entryDAO2 = FreshRSS_Factory::createEntryDao();
} else {

View File

@ -1,15 +1,14 @@
<?php
declare(strict_types=1);
/**
* Controller to handle every import and export actions.
*/
class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
/** @var FreshRSS_EntryDAO */
private $entryDAO;
private FreshRSS_EntryDAO $entryDAO;
/** @var FreshRSS_FeedDAO */
private $feedDAO;
private FreshRSS_FeedDAO $feedDAO;
/**
* This action is called before every other action in that class. It is
@ -182,7 +181,9 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
Minz_Request::bad(_t('feedback.import_export.file_cannot_be_uploaded'), [ 'c' => 'importExport', 'a' => 'index' ]);
}
@set_time_limit(300);
if (function_exists('set_time_limit')) {
@set_time_limit(300);
}
$error = false;
try {
@ -201,7 +202,6 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
}
// And finally, we get import status and redirect to the home page
Minz_Session::_param('actualize_feeds', true);
$content_notif = $error === true ? _t('feedback.import_export.feeds_imported_with_errors') : _t('feedback.import_export.feeds_imported');
Minz_Request::good($content_notif);
}
@ -289,7 +289,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
*/
private function importJson(string $article_file, bool $starred = false): bool {
$article_object = json_decode($article_file, true);
if ($article_object == null) {
if (!is_array($article_object)) {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS error trying to import a non-JSON file' . "\n");
} else {
@ -299,14 +299,14 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
}
$items = $article_object['items'] ?? $article_object;
$mark_as_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
$mark_as_read = FreshRSS_Context::userConf()->mark_when['reception'] ? 1 : 0;
$error = false;
$article_to_feed = [];
$nb_feeds = count($this->feedDAO->listFeeds());
$newFeedGuids = [];
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
// First, we check feeds of articles are in DB (and add them if needed).
foreach ($items as &$item) {
@ -364,7 +364,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
}
$tagDAO = FreshRSS_Factory::createTagDao();
$labels = $tagDAO->listTags() ?: [];
$labels = FreshRSS_Context::labels();
$knownLabels = [];
foreach ($labels as $label) {
$knownLabels[$label->name()]['id'] = $label->id();
@ -601,7 +601,7 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
return;
}
$username = Minz_User::name();
$username = Minz_User::name() ?? '_';
$export_service = new FreshRSS_Export_Service($username);
$export_opml = Minz_Request::paramBoolean('export_opml');

View File

@ -1,15 +1,20 @@
<?php
declare(strict_types=1);
/**
* This class handles main actions of FreshRSS.
*/
class FreshRSS_index_Controller extends FreshRSS_ActionController {
public function firstAction(): void {
$this->view->html_url = Minz_Url::display(['c' => 'index', 'a' => 'index'], 'html', 'root');
}
/**
* This action only redirect on the default view mode (normal or global)
*/
public function indexAction(): void {
$preferred_output = FreshRSS_Context::$user_conf->view_mode;
$preferred_output = FreshRSS_Context::userConf()->view_mode;
Minz_Request::forward([
'c' => 'index',
'a' => $preferred_output,
@ -20,7 +25,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
* This action displays the normal view of FreshRSS.
*/
public function normalAction(): void {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
return;
@ -35,7 +40,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
FreshRSS_Context::updateUsingRequest();
FreshRSS_Context::updateUsingRequest(true);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@ -47,7 +52,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
'media-src' => '*',
]);
$this->view->categories = FreshRSS_Context::$categories;
$this->view->categories = FreshRSS_Context::categories();
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$title = FreshRSS_Context::$name;
@ -59,15 +64,10 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
FreshRSS_Context::$id_max = time() . '000000';
$this->view->callbackBeforeFeeds = static function (FreshRSS_View $view) {
try {
$tagDAO = FreshRSS_Factory::createTagDao();
$view->tags = $tagDAO->listTags(true) ?: [];
$view->nbUnreadTags = 0;
foreach ($view->tags as $tag) {
$view->nbUnreadTags += $tag->nbUnread();
}
} catch (Exception $e) {
Minz_Log::notice($e->getMessage());
$view->tags = FreshRSS_Context::labels(true);
$view->nbUnreadTags = 0;
foreach ($view->tags as $tag) {
$view->nbUnreadTags += $tag->nbUnread();
}
};
@ -106,7 +106,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
* This action displays the global view of FreshRSS.
*/
public function globalAction(): void {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
if (!FreshRSS_Auth::hasAccess() && !$allow_anonymous) {
Minz_Request::forward(['c' => 'auth', 'a' => 'login']);
return;
@ -116,12 +116,12 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/global_view.js?' . @filemtime(PUBLIC_PATH . '/scripts/global_view.js')));
try {
FreshRSS_Context::updateUsingRequest();
FreshRSS_Context::updateUsingRequest(true);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
$this->view->categories = FreshRSS_Context::$categories;
$this->view->categories = FreshRSS_Context::categories();
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$title = _t('index.feed.title_global');
@ -140,10 +140,11 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
/**
* This action displays the RSS feed of FreshRSS.
* @deprecated See user query RSS sharing instead
*/
public function rssAction(): void {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
$token = FreshRSS_Context::$user_conf->token;
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
$token = FreshRSS_Context::userConf()->token;
$token_param = Minz_Request::paramString('token');
$token_is_ok = ($token != '' && $token === $token_param);
@ -155,7 +156,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
FreshRSS_Context::updateUsingRequest();
FreshRSS_Context::updateUsingRequest(false);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@ -167,16 +168,22 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
Minz_Error::error(404);
}
// No layout for RSS output.
$this->view->rss_url = PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']);
$this->view->html_url = Minz_Url::display('', 'html', true);
$this->view->rss_title = FreshRSS_Context::$name . ' | ' . FreshRSS_View::title();
$this->view->rss_url = htmlspecialchars(
PUBLIC_TO_INDEX_PATH . '/' . (empty($_SERVER['QUERY_STRING']) ? '' : '?' . $_SERVER['QUERY_STRING']), ENT_COMPAT, 'UTF-8');
// No layout for RSS output.
$this->view->_layout(null);
header('Content-Type: application/rss+xml; charset=utf-8');
}
/**
* @deprecated See user query OPML sharing instead
*/
public function opmlAction(): void {
$allow_anonymous = FreshRSS_Context::$system_conf->allow_anonymous;
$token = FreshRSS_Context::$user_conf->token;
$allow_anonymous = FreshRSS_Context::systemConf()->allow_anonymous;
$token = FreshRSS_Context::userConf()->token;
$token_param = Minz_Request::paramString('token');
$token_is_ok = ($token != '' && $token === $token_param);
@ -186,7 +193,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
}
try {
FreshRSS_Context::updateUsingRequest();
FreshRSS_Context::updateUsingRequest(false);
} catch (FreshRSS_Context_Exception $e) {
Minz_Error::error(404);
}
@ -195,25 +202,23 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
$type = (string)$get[0];
$id = (int)$get[1];
$catDAO = FreshRSS_Factory::createCategoryDao();
$categories = $catDAO->listCategories(true, true);
$this->view->excludeMutedFeeds = true;
switch ($type) {
case 'a':
$this->view->categories = $categories;
$this->view->categories = FreshRSS_Context::categories();
break;
case 'c':
$cat = $categories[$id] ?? null;
$cat = FreshRSS_Context::categories()[$id] ?? null;
if ($cat == null) {
Minz_Error::error(404);
return;
}
$this->view->categories = [ $cat ];
$this->view->categories = [ $cat->id() => $cat ];
break;
case 'f':
// We most likely already have the feed object in cache
$feed = FreshRSS_CategoryDAO::findFeed($categories, $id);
$feed = FreshRSS_Category::findFeed(FreshRSS_Context::categories(), $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@ -222,7 +227,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
return;
}
}
$this->view->feeds = [ $feed ];
$this->view->feeds = [ $feed->id() => $feed ];
break;
case 's':
case 't':
@ -240,6 +245,7 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
/**
* This method returns a list of entries based on the Context object.
* @return Traversable<FreshRSS_Entry>
* @throws FreshRSS_EntriesGetter_Exception
*/
public static function listEntriesByContext(): Traversable {
$entryDAO = FreshRSS_Factory::createEntryDao();
@ -253,17 +259,14 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
$id = 0;
}
$limit = FreshRSS_Context::$number;
$date_min = 0;
if (FreshRSS_Context::$sinceHours) {
if (FreshRSS_Context::$sinceHours > 0) {
$date_min = time() - (FreshRSS_Context::$sinceHours * 3600);
$limit = FreshRSS_Context::$user_conf->max_posts_per_rss;
}
foreach ($entryDAO->listWhere(
$type, $id, FreshRSS_Context::$state, FreshRSS_Context::$order,
$limit, FreshRSS_Context::$first_id,
FreshRSS_Context::$number, FreshRSS_Context::$offset, FreshRSS_Context::$first_id,
FreshRSS_Context::$search, $date_min)
as $entry) {
yield $entry;

View File

@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
/**
* @var FreshRSS_ViewJavascript
* @phpstan-ignore-next-line
*/
protected $view;
@ -20,11 +20,15 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
header('Content-Type: application/json; charset=UTF-8');
Minz_Session::_param('actualize_feeds', false);
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->minorDbMaintenance();
Minz_ExtensionManager::callHookVoid('freshrss_user_maintenance');
$catDAO = FreshRSS_Factory::createCategoryDao();
$this->view->categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::$user_conf->dynamic_opml_ttl_default);
$this->view->categories = $catDAO->listCategoriesOrderUpdate(FreshRSS_Context::userConf()->dynamic_opml_ttl_default);
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::userConf()->ttl_default);
}
public function nbUnreadsPerFeedAction(): void {
@ -48,10 +52,11 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
header('Pragma: no-cache');
$user = $_GET['user'] ?? '';
if (FreshRSS_Context::initUser($user)) {
FreshRSS_Context::initUser($user);
if (FreshRSS_Context::hasUserConf()) {
try {
$salt = FreshRSS_Context::$system_conf->salt;
$s = FreshRSS_Context::$user_conf->passwordHash;
$salt = FreshRSS_Context::systemConf()->salt;
$s = FreshRSS_Context::userConf()->passwordHash;
if (strlen($s) >= 60) {
//CRYPT_BLOWFISH Salt: "$2a$", a two digit cost parameter, "$", and 22 characters from the alphabet "./0-9A-Za-z".
$this->view->salt1 = substr($s, 0, 29);
@ -63,7 +68,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
Minz_Log::warning('Nonce failure: ' . $me->getMessage());
}
} else {
Minz_Log::notice('Nonce failure due to invalid username!');
Minz_Log::notice('Nonce failure due to invalid username! ' . $user);
}
//Failure: Return random data.
$this->view->salt1 = sprintf('$2a$%02d$', FreshRSS_password_Util::BCRYPT_COST);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle application statistics.
@ -7,7 +8,6 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
/**
* @var FreshRSS_ViewStats
* @phpstan-ignore-next-line
*/
protected $view;
@ -34,7 +34,6 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
$catDAO = FreshRSS_Factory::createCategoryDao();
$catDAO->checkDefault();
$this->view->categories = $catDAO->listSortedCategories(false) ?: [];
$this->view->default_category = $catDAO->getDefault();
FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
}
@ -194,7 +193,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
if ($id !== 0) {
$this->view->displaySlider = true;
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feed = $feedDAO->searchById($id);
$this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
}
}
@ -222,8 +221,8 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
$id = null;
}
$this->view->categories = $categoryDAO->listCategories() ?: [];
$this->view->feed = $id === null ? null : $feedDAO->searchById($id);
$this->view->categories = $categoryDAO->listCategories(true) ?: [];
$this->view->feed = $id === null ? FreshRSS_Feed::default() : ($feedDAO->searchById($id) ?? FreshRSS_Feed::default());
$this->view->days = $statsDAO->getDays();
$this->view->months = $statsDAO->getMonths();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle subscription actions.
@ -17,7 +18,6 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$catDAO = FreshRSS_Factory::createCategoryDao();
$catDAO->checkDefault();
$this->view->categories = $catDAO->listSortedCategories(false, true) ?: [];
$this->view->default_category = $catDAO->getDefault();
$signalError = false;
foreach ($this->view->categories as $cat) {
@ -59,7 +59,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
break;
default:
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->view->feed = $feedDAO->searchById($id);
$this->view->feed = $feedDAO->searchById($id) ?? FreshRSS_Feed::default();
break;
}
}
@ -118,13 +118,13 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$feed->_ttl(Minz_Request::paramInt('ttl') ?: FreshRSS_Feed::TTL_DEFAULT);
$feed->_mute(Minz_Request::paramBoolean('mute'));
$feed->_attributes('read_upon_gone', Minz_Request::paramTernary('read_upon_gone'));
$feed->_attributes('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
$feed->_attributes('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
$feed->_attributes('clear_cache', Minz_Request::paramTernary('clear_cache'));
$feed->_attribute('read_upon_gone', Minz_Request::paramTernary('read_upon_gone'));
$feed->_attribute('mark_updated_article_unread', Minz_Request::paramTernary('mark_updated_article_unread'));
$feed->_attribute('read_upon_reception', Minz_Request::paramTernary('read_upon_reception'));
$feed->_attribute('clear_cache', Minz_Request::paramTernary('clear_cache'));
$keep_max_n_unread = Minz_Request::paramInt('keep_max_n_unread');
$feed->_attributes('keep_max_n_unread', $keep_max_n_unread > 0 ? $keep_max_n_unread : null);
$keep_max_n_unread = Minz_Request::paramTernary('keep_max_n_unread') === true ? Minz_Request::paramInt('keep_max_n_unread') : null;
$feed->_attribute('keep_max_n_unread', $keep_max_n_unread >= 0 ? $keep_max_n_unread : null);
$read_when_same_title_in_feed = Minz_Request::paramString('read_when_same_title_in_feed');
if ($read_when_same_title_in_feed === '') {
@ -135,7 +135,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$read_when_same_title_in_feed = false;
}
}
$feed->_attributes('read_when_same_title_in_feed', $read_when_same_title_in_feed);
$feed->_attribute('read_when_same_title_in_feed', $read_when_same_title_in_feed);
$cookie = Minz_Request::paramString('curl_params_cookie');
$cookie_file = Minz_Request::paramBoolean('curl_params_cookiefile');
@ -143,6 +143,8 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
$useragent = Minz_Request::paramString('curl_params_useragent');
$proxy_address = Minz_Request::paramString('curl_params');
$proxy_type = Minz_Request::paramString('proxy_type');
$request_method = Minz_Request::paramString('curl_method');
$request_fields = Minz_Request::paramString('curl_fields', true);
$opts = [];
if ($proxy_type !== '') {
$opts[CURLOPT_PROXY] = $proxy_address;
@ -163,16 +165,27 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
if ($useragent !== '') {
$opts[CURLOPT_USERAGENT] = $useragent;
}
$feed->_attributes('curl_params', empty($opts) ? null : $opts);
$feed->_attributes('content_action', Minz_Request::paramString('content_action', true) ?: 'replace');
if ($request_method === 'POST') {
$opts[CURLOPT_POST] = true;
if ($request_fields !== '') {
$opts[CURLOPT_POSTFIELDS] = $request_fields;
if (json_decode($request_fields, true) !== null) {
$opts[CURLOPT_HTTPHEADER] = ['Content-Type: application/json'];
}
}
}
$feed->_attributes('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
$feed->_attribute('curl_params', empty($opts) ? null : $opts);
$feed->_attribute('content_action', Minz_Request::paramString('content_action', true) ?: 'replace');
$feed->_attribute('ssl_verify', Minz_Request::paramTernary('ssl_verify'));
$timeout = Minz_Request::paramInt('timeout');
$feed->_attributes('timeout', $timeout > 0 ? $timeout : null);
$feed->_attribute('timeout', $timeout > 0 ? $timeout : null);
if (Minz_Request::paramBoolean('use_default_purge_options')) {
$feed->_attributes('archiving', null);
$feed->_attribute('archiving', null);
} else {
if (Minz_Request::paramBoolean('enable_keep_max')) {
$keepMax = Minz_Request::paramInt('keep_max') ?: FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
@ -187,7 +200,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
} else {
$keepPeriod = false;
}
$feed->_attributes('archiving', [
$feed->_attribute('archiving', [
'keep_period' => $keepPeriod,
'keep_max' => $keepMax,
'keep_min' => Minz_Request::paramInt('keep_min'),
@ -197,7 +210,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
]);
}
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::paramString('filteractions_read')) ?: []);
$feed->_filtersAction('read', Minz_Request::paramTextToArray('filteractions_read'));
$feed->_kind(Minz_Request::paramInt('feed_kind') ?: FreshRSS_Feed::KIND_RSS);
if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
@ -223,10 +236,48 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
if (Minz_Request::paramString('xPathItemUid') != '')
$xPathSettings['itemUid'] = Minz_Request::paramString('xPathItemUid', true);
if (!empty($xPathSettings))
$feed->_attributes('xpath', $xPathSettings);
$feed->_attribute('xpath', $xPathSettings);
} elseif ($feed->kind() === FreshRSS_Feed::KIND_JSON_DOTPATH) {
$jsonSettings = [];
if (Minz_Request::paramString('jsonFeedTitle') !== '') {
$jsonSettings['feedTitle'] = Minz_Request::paramString('jsonFeedTitle', true);
}
if (Minz_Request::paramString('jsonItem') !== '') {
$jsonSettings['item'] = Minz_Request::paramString('jsonItem', true);
}
if (Minz_Request::paramString('jsonItemTitle') !== '') {
$jsonSettings['itemTitle'] = Minz_Request::paramString('jsonItemTitle', true);
}
if (Minz_Request::paramString('jsonItemContent') !== '') {
$jsonSettings['itemContent'] = Minz_Request::paramString('jsonItemContent', true);
}
if (Minz_Request::paramString('jsonItemUri') !== '') {
$jsonSettings['itemUri'] = Minz_Request::paramString('jsonItemUri', true);
}
if (Minz_Request::paramString('jsonItemAuthor') !== '') {
$jsonSettings['itemAuthor'] = Minz_Request::paramString('jsonItemAuthor', true);
}
if (Minz_Request::paramString('jsonItemTimestamp') !== '') {
$jsonSettings['itemTimestamp'] = Minz_Request::paramString('jsonItemTimestamp', true);
}
if (Minz_Request::paramString('jsonItemTimeFormat') !== '') {
$jsonSettings['itemTimeFormat'] = Minz_Request::paramString('jsonItemTimeFormat', true);
}
if (Minz_Request::paramString('jsonItemThumbnail') !== '') {
$jsonSettings['itemThumbnail'] = Minz_Request::paramString('jsonItemThumbnail', true);
}
if (Minz_Request::paramString('jsonItemCategories') !== '') {
$jsonSettings['itemCategories'] = Minz_Request::paramString('jsonItemCategories', true);
}
if (Minz_Request::paramString('jsonItemUid') !== '') {
$jsonSettings['itemUid'] = Minz_Request::paramString('jsonItemUid', true);
}
if (!empty($jsonSettings)) {
$feed->_attribute('json_dotpath', $jsonSettings);
}
}
$feed->_attributes('path_entries_filter', Minz_Request::paramString('path_entries_filter', true));
$feed->_attribute('path_entries_filter', Minz_Request::paramString('path_entries_filter', true));
$values = [
'name' => Minz_Request::paramString('name'),
@ -279,79 +330,6 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
}
}
public function categoryAction(): void {
if (Minz_Request::paramBoolean('ajax')) {
$this->view->_layout(null);
}
$categoryDAO = FreshRSS_Factory::createCategoryDao();
$id = Minz_Request::paramInt('id');
$category = $categoryDAO->searchById($id);
if ($id === 0 || null === $category) {
Minz_Error::error(404);
return;
}
$this->view->category = $category;
FreshRSS_View::prependTitle($category->name() . ' · ' . _t('sub.title') . ' · ');
if (Minz_Request::isPost()) {
if (Minz_Request::paramBoolean('use_default_purge_options')) {
$category->_attributes('archiving', null);
} else {
if (!Minz_Request::paramBoolean('enable_keep_max')) {
$keepMax = false;
} elseif (($keepMax = Minz_Request::paramInt('keep_max')) !== 0) {
$keepMax = FreshRSS_Feed::ARCHIVING_RETENTION_COUNT_LIMIT;
}
if (Minz_Request::paramBoolean('enable_keep_period')) {
$keepPeriod = FreshRSS_Feed::ARCHIVING_RETENTION_PERIOD;
if (is_numeric(Minz_Request::paramString('keep_period_count')) && preg_match('/^PT?1[YMWDH]$/', Minz_Request::paramString('keep_period_unit'))) {
$keepPeriod = str_replace('1', Minz_Request::paramString('keep_period_count'), Minz_Request::paramString('keep_period_unit'));
}
} else {
$keepPeriod = false;
}
$category->_attributes('archiving', [
'keep_period' => $keepPeriod,
'keep_max' => $keepMax,
'keep_min' => Minz_Request::paramInt('keep_min'),
'keep_favourites' => Minz_Request::paramBoolean('keep_favourites'),
'keep_labels' => Minz_Request::paramBoolean('keep_labels'),
'keep_unreads' => Minz_Request::paramBoolean('keep_unreads'),
]);
}
$position = Minz_Request::paramInt('position') ?: null;
$category->_attributes('position', $position);
$opml_url = checkUrl(Minz_Request::paramString('opml_url'));
if ($opml_url != '') {
$category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
$category->_attributes('opml_url', $opml_url);
} else {
$category->_kind(FreshRSS_Category::KIND_NORMAL);
$category->_attributes('opml_url', null);
}
$values = [
'kind' => $category->kind(),
'name' => Minz_Request::paramString('name'),
'attributes' => $category->attributes(),
];
invalidateHttpCache();
$url_redirect = ['c' => 'subscription', 'params' => ['id' => $id, 'type' => 'category']];
if (false !== $categoryDAO->updateCategory($id, $values)) {
Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
}
}
}
/**
* This action displays the bookmarklet page.
*/

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle every tag actions.
@ -7,9 +8,8 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
/**
* JavaScript request or not.
* @var bool|mixed
*/
private $ajax = false;
private bool $ajax = false;
/**
* This action is called before every other action in that class. It is
@ -17,14 +17,10 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
* underlying framework.
*/
public function firstAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
// If ajax request, we do not print layout
$this->ajax = Minz_Request::paramBoolean('ajax');
if ($this->ajax) {
$this->view->_layout(null);
Minz_Request::_param('ajax');
}
}
@ -32,11 +28,14 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
* This action adds (checked=true) or removes (checked=false) a tag to an entry.
*/
public function tagEntryAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
if (Minz_Request::isPost()) {
$id_tag = Minz_Request::paramInt('id_tag');
$name_tag = Minz_Request::paramString('name_tag');
$id_entry = Minz_Request::paramString('id_entry');
$checked = Minz_Request::paramTernary('checked');
$checked = Minz_Request::paramBoolean('checked');
if ($id_entry != '') {
$tagDAO = FreshRSS_Factory::createTagDao();
if ($id_tag == 0 && $name_tag !== '' && $checked) {
@ -64,6 +63,9 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
}
public function deleteAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
if (Minz_Request::isPost()) {
$id_tag = Minz_Request::paramInt('id_tag');
if ($id_tag !== 0) {
@ -81,7 +83,55 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
}
}
/**
* This action updates the given tag.
*/
public function updateAction(): void {
if (Minz_Request::paramBoolean('ajax')) {
$this->view->_layout(null);
}
$tagDAO = FreshRSS_Factory::createTagDao();
$id = Minz_Request::paramInt('id');
$tag = $tagDAO->searchById($id);
if ($id === 0 || $tag === null) {
Minz_Error::error(404);
return;
}
$this->view->tag = $tag;
FreshRSS_View::prependTitle($tag->name() . ' · ' . _t('sub.title') . ' · ');
if (Minz_Request::isPost()) {
invalidateHttpCache();
$ok = true;
if ($tag->name() !== Minz_Request::paramString('name')) {
$ok = $tagDAO->updateTagName($tag->id(), Minz_Request::paramString('name')) !== false;
}
if ($ok) {
$tag->_filtersAction('label', Minz_Request::paramTextToArray('filteractions_label'));
$ok = $tagDAO->updateTagAttributes($tag->id(), $tag->attributes()) !== false;
}
invalidateHttpCache();
$url_redirect = ['c' => 'tag', 'a' => 'update', 'params' => ['id' => $id]];
if ($ok) {
Minz_Request::good(_t('feedback.tag.updated'), $url_redirect);
} else {
Minz_Request::bad(_t('feedback.tag.error'), $url_redirect);
}
}
}
public function getTagsForEntryAction(): void {
if (!FreshRSS_Auth::hasAccess() && !FreshRSS_Context::systemConf()->allow_anonymous) {
Minz_Error::error(403);
}
$this->view->_layout(null);
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: private, no-cache, no-store, must-revalidate');
@ -91,6 +141,9 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
}
public function addAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
if (!Minz_Request::isPost()) {
Minz_Error::error(405);
}
@ -107,9 +160,12 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
/**
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException|JsonException
* @throws Minz_PDOConnectionException
*/
public function renameAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
if (!Minz_Request::isPost()) {
Minz_Error::error(405);
}
@ -124,7 +180,7 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
$tagDAO = FreshRSS_Factory::createTagDao();
$sourceTag = $tagDAO->searchById($sourceId);
$sourceName = $sourceTag === null ? null : $sourceTag->name();
$sourceName = $sourceTag === null ? '' : $sourceTag->name();
$targetTag = $tagDAO->searchByName($targetName);
if ($targetTag === null) {
// There is no existing tag with the same target name
@ -139,7 +195,10 @@ class FreshRSS_tag_Controller extends FreshRSS_ActionController {
}
public function indexAction(): void {
if (!FreshRSS_Auth::hasAccess()) {
Minz_Error::error(403);
}
$tagDAO = FreshRSS_Factory::createTagDao();
$this->view->tags = $tagDAO->listTags() ?: [];
$this->view->tags = $tagDAO->listTags(true) ?: [];
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_update_Controller extends FreshRSS_ActionController {
@ -10,18 +11,19 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
/**
* Automatic change to the new name of edge branch since FreshRSS 1.18.0.
* @throws Minz_Exception
*/
public static function migrateToGitEdge(): bool {
$errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
throw new Exception($errorMessage);
throw new Minz_Exception($errorMessage);
}
//Note `git branch --show-current` requires git 2.22+
exec('git symbolic-ref --short HEAD', $output, $return);
if ($return != 0) {
throw new Exception($errorMessage);
throw new Minz_Exception($errorMessage);
}
$line = implode('', $output);
if ($line !== 'master' && $line !== 'dev') {
@ -32,13 +34,13 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
unset($output);
exec('git checkout edge --guess -f', $output, $return);
if ($return != 0) {
throw new Exception($errorMessage);
throw new Minz_Exception($errorMessage);
}
unset($output);
exec('git reset --hard FETCH_HEAD', $output, $return);
if ($return != 0) {
throw new Exception($errorMessage);
throw new Minz_Exception($errorMessage);
}
return true;
@ -63,6 +65,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
chdir(FRESHRSS_PATH);
$output = [];
try {
/** @throws ValueError */
exec('git fetch --prune', $output, $return);
if ($return == 0) {
$output = [];
@ -71,7 +74,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
$line = implode('; ', $output);
Minz_Log::warning('git fetch warning: ' . $line);
}
} catch (Exception $e) {
} catch (Throwable $e) {
Minz_Log::warning('git fetch error: ' . $e->getMessage());
}
chdir($cwd);
@ -100,11 +103,9 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
$output = [];
self::migrateToGitEdge();
} catch (Exception $e) {
} catch (Throwable $e) {
Minz_Log::warning('Git error: ' . $e->getMessage());
if (empty($output)) {
$output = $e->getMessage();
}
$output = $e->getMessage();
$return = 1;
}
chdir($cwd);
@ -193,7 +194,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
return;
}
} else {
$auto_update_url = FreshRSS_Context::$system_conf->auto_update_url . '/?v=' . FRESHRSS_VERSION;
$auto_update_url = FreshRSS_Context::systemConf()->auto_update_url . '/?v=' . FRESHRSS_VERSION;
Minz_Log::debug('HTTP GET ' . $auto_update_url);
$curlResource = curl_init($auto_update_url);
@ -256,7 +257,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
}
public function applyAction(): void {
if (FreshRSS_Context::$system_conf->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
if (FreshRSS_Context::systemConf()->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
Minz_Request::forward(['c' => 'update'], true);
}
@ -269,7 +270,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
$res = do_post_update();
}
Minz_ExtensionManager::callHook('post_update');
Minz_ExtensionManager::callHookVoid('post_update');
if ($res === true) {
@unlink(UPDATE_FILENAME);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Controller to handle user actions.
@ -8,7 +9,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
* The username is also used as folder name, file name, and part of SQL table name.
* '_' is a reserved internal username.
*/
public const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@-]{1,38}|[0-9a-zA-Z])';
public const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@\-]{1,38}|[0-9a-zA-Z])';
public static function checkUsername(string $username): bool {
return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;
@ -28,8 +29,8 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
if ($email !== null && $userConfig->mail_login !== $email) {
$userConfig->mail_login = $email;
if (FreshRSS_Context::$system_conf->force_email_validation) {
$salt = FreshRSS_Context::$system_conf->salt;
if (FreshRSS_Context::systemConf()->force_email_validation) {
$salt = FreshRSS_Context::systemConf()->salt;
$userConfig->email_validation_token = sha1($salt . uniqid('' . mt_rand(), true));
$mailer = new FreshRSS_User_Mailer();
$mailer->send_email_need_validation($user, $userConfig);
@ -87,7 +88,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
Minz_Error::error(403);
}
$email_not_verified = FreshRSS_Context::$user_conf->email_validation_token != '';
$email_not_verified = FreshRSS_Context::userConf()->email_validation_token != '';
$this->view->disable_aside = false;
if ($email_not_verified) {
$this->view->_layout('simple');
@ -98,17 +99,15 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
FreshRSS_View::appendScript(Minz_Url::display('/scripts/bcrypt.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/bcrypt.min.js')));
if (Minz_Request::isPost()) {
$system_conf = FreshRSS_Context::$system_conf;
$user_config = FreshRSS_Context::$user_conf;
$old_email = $user_config->mail_login;
if (Minz_Request::isPost() && Minz_User::name() != null) {
$old_email = FreshRSS_Context::userConf()->mail_login;
$email = Minz_Request::paramString('email');
$passwordPlain = Minz_Request::paramString('newPasswordPlain', true);
Minz_Request::_param('newPasswordPlain'); //Discard plain-text password ASAP
$_POST['newPasswordPlain'] = '';
if ($system_conf->force_email_validation && empty($email)) {
if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
Minz_Request::bad(
_t('user.email.feedback.required'),
['c' => 'user', 'a' => 'profile']
@ -131,10 +130,10 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
]
);
Minz_Session::_param('passwordHash', FreshRSS_Context::$user_conf->passwordHash);
Minz_Session::_param('passwordHash', FreshRSS_Context::userConf()->passwordHash);
if ($ok) {
if ($system_conf->force_email_validation && $email !== $old_email) {
if (FreshRSS_Context::systemConf()->force_email_validation && $email !== $old_email) {
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'validateEmail']);
} elseif ($passwordPlain == '') {
Minz_Request::good(_t('feedback.profile.updated'), ['c' => 'user', 'a' => 'profile']);
@ -201,7 +200,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
}
$this->view->show_email_field = FreshRSS_Context::$system_conf->force_email_validation;
$this->view->show_email_field = FreshRSS_Context::systemConf()->force_email_validation;
$this->view->current_user = Minz_Request::paramString('u');
foreach (listUsers() as $user) {
@ -209,7 +208,11 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
}
/** @param array<string,mixed> $userConfigOverride */
/**
* @param array<string,mixed> $userConfigOverride
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
public static function createUser(string $new_user_name, ?string $email, string $passwordPlain,
array $userConfigOverride = [], bool $insertDefaultFeeds = true): bool {
$userConfig = [];
@ -285,8 +288,6 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
if (Minz_Request::isPost()) {
$system_conf = FreshRSS_Context::$system_conf;
$new_user_name = Minz_Request::paramString('new_user_name');
$email = Minz_Request::paramString('new_user_email');
$passwordPlain = Minz_Request::paramString('new_user_passwordPlain', true);
@ -319,7 +320,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$tos_enabled = file_exists(TOS_FILENAME);
$accept_tos = Minz_Request::paramBoolean('accept_tos');
if ($system_conf->force_email_validation && empty($email)) {
if (FreshRSS_Context::systemConf()->force_email_validation && empty($email)) {
Minz_Request::bad(
_t('user.email.feedback.required'),
$badRedirectUrl
@ -341,7 +342,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
$ok = self::createUser($new_user_name, $email, $passwordPlain, [
'language' => Minz_Request::paramString('new_user_language') ?: FreshRSS_Context::$user_conf->language,
'language' => Minz_Request::paramString('new_user_language') ?: FreshRSS_Context::userConf()->language,
'timezone' => Minz_Request::paramString('new_user_timezone'),
'is_admin' => Minz_Request::paramBoolean('new_user_is_admin'),
'enabled' => true,
@ -356,12 +357,16 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
// get started immediately.
if ($ok && !FreshRSS_Auth::hasAccess('admin')) {
$user_conf = get_user_configuration($new_user_name);
Minz_Session::_params([
Minz_User::CURRENT_USER => $new_user_name,
'passwordHash' => $user_conf->passwordHash,
'csrf' => false,
]);
FreshRSS_Auth::giveAccess();
if ($user_conf !== null) {
Minz_Session::_params([
Minz_User::CURRENT_USER => $new_user_name,
'passwordHash' => $user_conf->passwordHash,
'csrf' => false,
]);
FreshRSS_Auth::giveAccess();
} else {
$ok = false;
}
}
if ($ok) {
@ -378,7 +383,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
public static function deleteUser(string $username): bool {
$ok = self::checkUsername($username);
if ($ok) {
$default_user = FreshRSS_Context::$system_conf->default_user;
$default_user = FreshRSS_Context::systemConf()->default_user;
$ok &= (strcasecmp($username, $default_user) !== 0); //It is forbidden to delete the default user
}
$user_data = join_path(DATA_PATH, 'users', $username);
@ -414,7 +419,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
* It returns 403 if user isnt logged in and `username` param isnt passed.
*/
public function validateEmailAction(): void {
if (!FreshRSS_Context::$system_conf->force_email_validation) {
if (!FreshRSS_Context::systemConf()->force_email_validation) {
Minz_Error::error(404);
}
@ -427,7 +432,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
if ($username !== '') {
$user_config = get_user_configuration($username);
} elseif (FreshRSS_Auth::hasAccess()) {
$user_config = FreshRSS_Context::$user_conf;
$user_config = FreshRSS_Context::userConf();
} else {
Minz_Error::error(403);
return;
@ -488,9 +493,8 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
$username = Minz_User::name();
$user_config = FreshRSS_Context::$user_conf;
if ($user_config->email_validation_token === '') {
if (FreshRSS_Context::userConf()->email_validation_token === '') {
Minz_Request::forward([
'c' => 'index',
'a' => 'index',
@ -498,7 +502,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
}
$mailer = new FreshRSS_User_Mailer();
$ok = $mailer->send_email_need_validation($username, $user_config);
$ok = $username != null && $mailer->send_email_need_validation($username, FreshRSS_Context::userConf());
$redirect_url = ['c' => 'user', 'a' => 'validateEmail'];
if ($ok) {
@ -536,11 +540,11 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$ok = true;
if ($self_deletion) {
// We check the password if its a self-destruction
$nonce = Minz_Session::param('nonce', '');
$nonce = Minz_Session::paramString('nonce');
$challenge = Minz_Request::paramString('challenge');
$ok &= FreshRSS_FormAuth::checkCredentials(
$username, FreshRSS_Context::$user_conf->passwordHash,
$username, FreshRSS_Context::userConf()->passwordHash,
$nonce, $challenge
);
}
@ -595,6 +599,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
if (null === $userConfig = get_user_configuration($username)) {
Minz_Error::error(500);
return;
}
$userConfig->_param($field, $value);
@ -638,6 +643,9 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
$databaseDAO = FreshRSS_Factory::createDatabaseDAO($username);
$userConfiguration = get_user_configuration($username);
if ($userConfiguration === null) {
throw new Exception('Error loading user configuration!');
}
return [
'feed_count' => $feedDAO->count(),
@ -648,7 +656,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
'enabled' => $userConfiguration->enabled,
'is_admin' => $userConfiguration->is_admin,
'last_user_activity' => date('c', FreshRSS_UserDAO::mtime($username)) ?: '',
'is_default' => FreshRSS_Context::$system_conf->default_user === $username,
'is_default' => FreshRSS_Context::systemConf()->default_user === $username,
];
}
}

View File

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
class FreshRSS_AlreadySubscribed_Exception extends Exception {
class FreshRSS_AlreadySubscribed_Exception extends Minz_Exception {
/** @var string */
private $feedName = '';
private string $feedName = '';
public function __construct(string $url, string $feedName) {
parent::__construct('Already subscribed! ' . $url, 2135);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {

View File

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/**
* An exception raised when a context is invalid
*/
class FreshRSS_Context_Exception extends Exception {
class FreshRSS_Context_Exception extends Minz_Exception {
}

View File

@ -1,5 +0,0 @@
<?php
class FreshRSS_DAO_Exception extends Exception {
}

View File

@ -1,5 +1,6 @@
<?php
declare(strict_types=1);
class FreshRSS_EntriesGetter_Exception extends Exception {
class FreshRSS_EntriesGetter_Exception extends Minz_Exception {
}

View File

@ -1,5 +1,6 @@
<?php
declare(strict_types=1);
class FreshRSS_Feed_Exception extends Exception {
class FreshRSS_Feed_Exception extends Minz_Exception {
}

View File

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
class FreshRSS_FeedNotAdded_Exception extends Exception {
class FreshRSS_FeedNotAdded_Exception extends Minz_Exception {
/** @var string */
private $url = '';
private string $url = '';
public function __construct(string $url) {
parent::__construct('Feed not added! ' . $url, 2147);

View File

@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
class FreshRSS_Zip_Exception extends Exception {
class FreshRSS_Zip_Exception extends Minz_Exception {
/** @var int */
private $zipErrorCode = 0;
private int $zipErrorCode = 0;
public function __construct(int $zipErrorCode) {
parent::__construct('ZIP error!', 2141);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_ZipMissing_Exception extends Exception {
class FreshRSS_ZipMissing_Exception extends Minz_Exception {
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS extends Minz_FrontController {
/**
@ -15,7 +16,6 @@ class FreshRSS extends Minz_FrontController {
* - Init i18n (need context)
* - Init sharing system (need user conf and i18n)
* - Init generic styles and scripts (need user conf)
* - Init notifications
* - Enable user extensions (need all the other initializations)
*/
public function init(): void {
@ -23,16 +23,14 @@ class FreshRSS extends Minz_FrontController {
Minz_Session::init('FreshRSS');
}
Minz_ActionController::$defaultViewType = FreshRSS_View::class;
FreshRSS_Context::initSystem();
if (FreshRSS_Context::$system_conf == null) {
if (!FreshRSS_Context::hasSystemConf()) {
$message = 'Error during context system init!';
Minz_Error::error(500, $message, false);
die($message);
}
if (FreshRSS_Context::$system_conf->logo_html != '') {
if (FreshRSS_Context::systemConf()->logo_html != '') {
// Relax Content Security Policy to allow external images if a custom logo HTML is used
Minz_ActionController::_defaultCsp([
'default-src' => "'self'",
@ -46,10 +44,10 @@ class FreshRSS extends Minz_FrontController {
// Auth has to be initialized before using currentUser session parameter
// because its this part which create this parameter.
self::initAuth();
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
FreshRSS_Context::initUser();
}
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
$message = 'Error during context user init!';
Minz_Error::error(500, $message, false);
die($message);
@ -57,30 +55,29 @@ class FreshRSS extends Minz_FrontController {
// Complete initialization of the other FreshRSS / Minz components.
self::initI18n();
self::loadNotifications();
// Enable extensions for the current (logged) user.
if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) {
$ext_list = FreshRSS_Context::$user_conf->extensions_enabled;
if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::systemConf()->allow_anonymous) {
$ext_list = FreshRSS_Context::userConf()->extensions_enabled;
Minz_ExtensionManager::enableByList($ext_list, 'user');
}
if (FreshRSS_Context::$system_conf->force_email_validation && !FreshRSS_Auth::hasAccess('admin')) {
if (FreshRSS_Context::systemConf()->force_email_validation && !FreshRSS_Auth::hasAccess('admin')) {
self::checkEmailValidated();
}
Minz_ExtensionManager::callHook('freshrss_init');
Minz_ExtensionManager::callHookVoid('freshrss_init');
}
private static function initAuth(): void {
FreshRSS_Auth::init();
if (Minz_Request::isPost()) {
if (FreshRSS_Context::$system_conf == null || !(FreshRSS_Auth::isCsrfOk() ||
if (!FreshRSS_Context::hasSystemConf() || !(FreshRSS_Auth::isCsrfOk() ||
(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' && !FreshRSS_Auth::hasAccess('admin')) ||
(Minz_Request::controllerName() === 'feed' && Minz_Request::actionName() === 'actualize'
&& FreshRSS_Context::$system_conf->allow_anonymous_refresh) ||
&& FreshRSS_Context::systemConf()->allow_anonymous_refresh) ||
(Minz_Request::controllerName() === 'javascript' && Minz_Request::actionName() === 'actualize'
&& FreshRSS_Context::$system_conf->allow_anonymous)
&& FreshRSS_Context::systemConf()->allow_anonymous)
)) {
// Token-based protection against XSRF attacks, except for the login or self-create user forms
self::initI18n();
@ -90,14 +87,14 @@ class FreshRSS extends Minz_FrontController {
}
private static function initI18n(): void {
$userLanguage = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->language : null;
$systemLanguage = isset(FreshRSS_Context::$system_conf) ? FreshRSS_Context::$system_conf->language : null;
$userLanguage = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf()->language : null;
$systemLanguage = FreshRSS_Context::hasSystemConf() ? FreshRSS_Context::systemConf()->language : null;
$language = Minz_Translate::getLanguage($userLanguage, Minz_Request::getPreferredLanguages(), $systemLanguage);
Minz_Session::_param('language', $language);
Minz_Translate::init($language);
$timezone = isset(FreshRSS_Context::$user_conf) ? FreshRSS_Context::$user_conf->timezone : '';
$timezone = FreshRSS_Context::hasUserConf() ? FreshRSS_Context::userConf()->timezone : '';
if ($timezone == '') {
$timezone = FreshRSS_Context::defaultTimeZone();
}
@ -110,10 +107,10 @@ class FreshRSS extends Minz_FrontController {
}
public static function loadStylesAndScripts(): void {
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
return;
}
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
$theme = FreshRSS_Themes::load(FreshRSS_Context::userConf()->theme);
if ($theme) {
foreach(array_reverse($theme['files']) as $file) {
switch (substr($file, -3)) {
@ -144,19 +141,12 @@ class FreshRSS extends Minz_FrontController {
}
}
//Use prepend to insert before extensions. Added in reverse order.
if (Minz_Request::controllerName() !== 'index') {
if (!in_array(Minz_Request::controllerName(), ['index', ''], true)) {
FreshRSS_View::prependScript(Minz_Url::display('/scripts/extra.js?' . @filemtime(PUBLIC_PATH . '/scripts/extra.js')));
}
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
}
private static function loadNotifications(): void {
$notif = Minz_Request::getNotification();
if (!empty($notif)) {
FreshRSS_View::_param('notification', $notif);
}
}
public static function preLayout(): void {
header("X-Content-Type-Options: nosniff");
@ -166,7 +156,7 @@ class FreshRSS extends Minz_FrontController {
private static function checkEmailValidated(): void {
$email_not_verified = FreshRSS_Auth::hasAccess() &&
FreshRSS_Context::$user_conf !== null && FreshRSS_Context::$user_conf->email_validation_token !== '';
FreshRSS_Context::hasUserConf() && FreshRSS_Context::userConf()->email_validation_token !== '';
$action_is_allowed = (
Minz_Request::is('user', 'validateEmail') ||
Minz_Request::is('user', 'sendValidationEmail') ||

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Manage the emails sent to the users.
@ -7,17 +8,20 @@ class FreshRSS_User_Mailer extends Minz_Mailer {
/**
* @var FreshRSS_View
* @phpstan-ignore-next-line
*/
protected $view;
public function __construct() {
parent::__construct(FreshRSS_View::class);
}
public function send_email_need_validation(string $username, FreshRSS_UserConfiguration $user_config): bool {
Minz_Translate::reset($user_config->language);
$this->view->_path('user_mailer/email_need_validation.txt.php');
$this->view->username = $username;
$this->view->site_title = FreshRSS_Context::$system_conf->title;
$this->view->site_title = FreshRSS_Context::systemConf()->title;
$this->view->validation_url = Minz_Url::display(
[
'c' => 'user',
@ -31,7 +35,7 @@ class FreshRSS_User_Mailer extends Minz_Mailer {
true
);
$subject_prefix = '[' . FreshRSS_Context::$system_conf->title . ']';
$subject_prefix = '[' . FreshRSS_Context::systemConf()->title . ']';
return $this->mail(
$user_config->mail_login,
$subject_prefix . ' ' ._t('user.mailer.email_need_validation.title')

View File

@ -1,10 +1,14 @@
<?php
declare(strict_types=1);
class FreshRSS_ActionController extends Minz_ActionController {
abstract class FreshRSS_ActionController extends Minz_ActionController {
/**
* @var FreshRSS_View
* @phpstan-ignore-next-line
*/
protected $view;
public function __construct(string $viewType = '') {
parent::__construct($viewType === '' ? FreshRSS_View::class : $viewType);
}
}

View File

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
/**
* Logic to work with (JSON) attributes (for entries, feeds, categories, tags...).
*/
trait FreshRSS_AttributesTrait {
/**
* @var array<string,mixed>
*/
private array $attributes = [];
/** @return array<string,mixed> */
public function attributes(): array {
return $this->attributes;
}
/**
* @param non-empty-string $key
* @return array<int|string,mixed>|null
*/
public function attributeArray(string $key): ?array {
$a = $this->attributes[$key] ?? null;
return is_array($a) ? $a : null;
}
/** @param non-empty-string $key */
public function attributeBoolean(string $key): ?bool {
$a = $this->attributes[$key] ?? null;
return is_bool($a) ? $a : null;
}
/** @param non-empty-string $key */
public function attributeInt(string $key): ?int {
$a = $this->attributes[$key] ?? null;
return is_int($a) ? $a : null;
}
/** @param non-empty-string $key */
public function attributeString(string $key): ?string {
$a = $this->attributes[$key] ?? null;
return is_string($a) ? $a : null;
}
/** @param string|array<string,mixed> $values Values, not HTML-encoded */
public function _attributes($values): void {
if (is_string($values)) {
$values = json_decode($values, true);
}
if (is_array($values)) {
$this->attributes = $values;
}
}
/**
* @param non-empty-string $key
* @param array<string,mixed>|mixed|null $value Value, not HTML-encoded
*/
public function _attribute(string $key, $value = null): void {
if ($value === null) {
unset($this->attributes[$key]);
} else {
$this->attributes[$key] = $value;
}
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This class handles all authentication process.
@ -9,8 +10,7 @@ class FreshRSS_Auth {
*/
public const DEFAULT_COOKIE_DURATION = 7776000;
/** @var bool */
private static $login_ok = false;
private static bool $login_ok = false;
/**
* This method initializes authentication system.
@ -21,10 +21,10 @@ class FreshRSS_Auth {
self::removeAccess();
}
self::$login_ok = Minz_Session::param('loginOk', false);
self::$login_ok = Minz_Session::paramBoolean('loginOk');
$current_user = Minz_User::name();
if ($current_user === null) {
$current_user = FreshRSS_Context::$system_conf->default_user;
$current_user = FreshRSS_Context::systemConf()->default_user;
Minz_Session::_params([
Minz_User::CURRENT_USER => $current_user,
'csrf' => false,
@ -51,7 +51,7 @@ class FreshRSS_Auth {
* @return bool true if user can be connected, false otherwise.
*/
private static function accessControl(): bool {
$auth_type = FreshRSS_Context::$system_conf->auth_type;
$auth_type = FreshRSS_Context::systemConf()->auth_type;
switch ($auth_type) {
case 'form':
$credentials = FreshRSS_FormAuth::getCredentialsFromCookie();
@ -71,13 +71,13 @@ class FreshRSS_Auth {
return false;
}
$login_ok = FreshRSS_UserDAO::exists($current_user);
if (!$login_ok && FreshRSS_Context::$system_conf->http_auth_auto_register) {
if (!$login_ok && FreshRSS_Context::systemConf()->http_auth_auto_register) {
$email = null;
if (FreshRSS_Context::$system_conf->http_auth_auto_register_email_field !== '' &&
isset($_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field])) {
$email = (string)$_SERVER[FreshRSS_Context::$system_conf->http_auth_auto_register_email_field];
if (FreshRSS_Context::systemConf()->http_auth_auto_register_email_field !== '' &&
isset($_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field])) {
$email = (string)$_SERVER[FreshRSS_Context::systemConf()->http_auth_auto_register_email_field];
}
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::$system_conf->language);
$language = Minz_Translate::getLanguage(null, Minz_Request::getPreferredLanguages(), FreshRSS_Context::systemConf()->language);
Minz_Translate::init($language);
$login_ok = FreshRSS_user_Controller::createUser($current_user, $email, '', [
'language' => $language,
@ -103,17 +103,17 @@ class FreshRSS_Auth {
*/
public static function giveAccess(): bool {
FreshRSS_Context::initUser();
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
self::$login_ok = false;
return false;
}
switch (FreshRSS_Context::$system_conf->auth_type) {
switch (FreshRSS_Context::systemConf()->auth_type) {
case 'form':
self::$login_ok = Minz_Session::param('passwordHash') === FreshRSS_Context::$user_conf->passwordHash;
self::$login_ok = Minz_Session::paramString('passwordHash') === FreshRSS_Context::userConf()->passwordHash;
break;
case 'http_auth':
$current_user = Minz_User::name();
$current_user = Minz_User::name() ?? '';
self::$login_ok = strcasecmp($current_user, httpAuthUser()) === 0;
break;
case 'none':
@ -138,12 +138,12 @@ class FreshRSS_Auth {
* @return bool true if user has corresponding access, false else.
*/
public static function hasAccess(string $scope = 'general'): bool {
if (FreshRSS_Context::$user_conf == null) {
if (!FreshRSS_Context::hasUserConf()) {
return false;
}
$currentUser = Minz_User::name();
$isAdmin = FreshRSS_Context::$user_conf->is_admin;
$default_user = FreshRSS_Context::$system_conf->default_user;
$isAdmin = FreshRSS_Context::userConf()->is_admin;
$default_user = FreshRSS_Context::systemConf()->default_user;
$ok = self::$login_ok;
switch ($scope) {
case 'general':
@ -180,11 +180,11 @@ class FreshRSS_Auth {
}
}
if ($username == '') {
$username = FreshRSS_Context::$system_conf->default_user;
$username = FreshRSS_Context::systemConf()->default_user;
}
Minz_User::change($username);
switch (FreshRSS_Context::$system_conf->auth_type) {
switch (FreshRSS_Context::systemConf()->auth_type) {
case 'form':
Minz_Session::_param('passwordHash');
FreshRSS_FormAuth::deleteCookie();
@ -202,20 +202,20 @@ class FreshRSS_Auth {
* Return if authentication is enabled on this instance of FRSS.
*/
public static function accessNeedsLogin(): bool {
return FreshRSS_Context::$system_conf->auth_type !== 'none';
return FreshRSS_Context::systemConf()->auth_type !== 'none';
}
/**
* Return if authentication requires a PHP action.
*/
public static function accessNeedsAction(): bool {
return FreshRSS_Context::$system_conf->auth_type === 'form';
return FreshRSS_Context::systemConf()->auth_type === 'form';
}
public static function csrfToken(): string {
$csrf = Minz_Session::param('csrf');
$csrf = Minz_Session::paramString('csrf');
if ($csrf == '') {
$salt = FreshRSS_Context::$system_conf->salt;
$salt = FreshRSS_Context::systemConf()->salt;
$csrf = sha1($salt . uniqid('' . random_int(0, mt_getrandmax()), true));
Minz_Session::_param('csrf', $csrf);
}
@ -223,7 +223,7 @@ class FreshRSS_Auth {
}
public static function isCsrfOk(?string $token = null): bool {
$csrf = Minz_Session::param('csrf');
$csrf = Minz_Session::paramString('csrf');
if ($token === null) {
$token = $_POST['_csrf'] ?? '';
}

View File

@ -1,37 +1,42 @@
<?php
declare(strict_types=1);
/**
* Contains Boolean search from the search form.
*/
class FreshRSS_BooleanSearch {
/** @var string */
private $raw_input = '';
private string $raw_input = '';
/** @var array<FreshRSS_BooleanSearch|FreshRSS_Search> */
private $searches = [];
private array $searches = [];
/**
* @phpstan-var 'AND'|'OR'|'AND NOT'
* @var string
*/
private $operator;
private string $operator;
/** @param 'AND'|'OR'|'AND NOT' $operator */
public function __construct(string $input, int $level = 0, string $operator = 'AND') {
public function __construct(string $input, int $level = 0, string $operator = 'AND', bool $allowUserQueries = true) {
$this->operator = $operator;
$input = trim($input);
if ($input == '') {
if ($input === '') {
return;
}
$this->raw_input = $input;
if ($level === 0) {
$input = preg_replace('/:&quot;(.*?)&quot;/', ':"\1"', $input);
if (!is_string($input)) {
return;
}
$input = preg_replace('/(?<=[\s!-]|^)&quot;(.*?)&quot;/', '"\1"', $input);
if (!is_string($input)) {
return;
}
$input = $this->parseUserQueryNames($input);
$input = $this->parseUserQueryIds($input);
$input = $this->parseUserQueryNames($input, $allowUserQueries);
$input = $this->parseUserQueryIds($input, $allowUserQueries);
$input = trim($input);
}
$this->raw_input = $input;
// Either parse everything as a series of BooleanSearchs combined by implicit AND
// or parse everything as a series of Searchs combined by explicit OR
@ -41,7 +46,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by name and expand them in the input string.
*/
private function parseUserQueryNames(string $input): string {
private function parseUserQueryNames(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bsearch:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matchesFound)) {
$all_matches[] = $matchesFound;
@ -54,8 +59,8 @@ class FreshRSS_BooleanSearch {
if (!empty($all_matches)) {
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::$user_conf->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query);
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[$query->getName()] = $query;
}
@ -69,7 +74,11 @@ class FreshRSS_BooleanSearch {
$name = trim($matches['search'][$i]);
if (!empty($queries[$name])) {
$fromS[] = $matches[0][$i];
$toS[] = '(' . trim($queries[$name]->getSearch()) . ')';
if ($allowUserQueries) {
$toS[] = '(' . trim($queries[$name]->getSearch()->getRawInput()) . ')';
} else {
$toS[] = '';
}
}
}
}
@ -82,7 +91,7 @@ class FreshRSS_BooleanSearch {
/**
* Parse the user queries (saved searches) by ID and expand them in the input string.
*/
private function parseUserQueryIds(string $input): string {
private function parseUserQueryIds(string $input, bool $allowUserQueries = true): string {
$all_matches = [];
if (preg_match_all('/\bS:(?P<search>\d+)/', $input, $matchesFound)) {
@ -92,8 +101,8 @@ class FreshRSS_BooleanSearch {
if (!empty($all_matches)) {
/** @var array<string,FreshRSS_UserQuery> */
$queries = [];
foreach (FreshRSS_Context::$user_conf->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query);
foreach (FreshRSS_Context::userConf()->queries as $raw_query) {
$query = new FreshRSS_UserQuery($raw_query, FreshRSS_Context::categories(), FreshRSS_Context::labels());
$queries[] = $query;
}
@ -108,7 +117,11 @@ class FreshRSS_BooleanSearch {
$id = (int)(trim($matches['search'][$i])) - 1;
if (!empty($queries[$id])) {
$fromS[] = $matches[0][$i];
$toS[] = '(' . trim($queries[$id]->getSearch()) . ')';
if ($allowUserQueries) {
$toS[] = '(' . trim($queries[$id]->getSearch()->getRawInput()) . ')';
} else {
$toS[] = '';
}
}
}
}

View File

@ -1,6 +1,8 @@
<?php
declare(strict_types=1);
class FreshRSS_Category extends Minz_Model {
use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait;
/**
* Normal
@ -12,40 +14,33 @@ class FreshRSS_Category extends Minz_Model {
*/
public const KIND_DYNAMIC_OPML = 2;
/** @var int */
private $id = 0;
/** @var int */
private $kind = 0;
/** @var string */
private $name;
/** @var int */
private $nbFeeds = -1;
/** @var int */
private $nbNotRead = -1;
private int $id = 0;
private int $kind = 0;
private string $name;
private int $nbFeeds = -1;
private int $nbNotRead = -1;
/** @var array<FreshRSS_Feed>|null */
private $feeds;
private ?array $feeds = null;
/** @var bool|int */
private $hasFeedsWithError = false;
/** @var array<string,mixed> */
private $attributes = [];
/** @var int */
private $lastUpdate = 0;
/** @var bool */
private $error = false;
private int $lastUpdate = 0;
private bool $error = false;
/**
* @param array<FreshRSS_Feed>|null $feeds
*/
public function __construct(string $name = '', ?array $feeds = null) {
public function __construct(string $name = '', int $id = 0, ?array $feeds = null) {
$this->_id($id);
$this->_name($name);
if ($feeds !== null) {
$this->_feeds($feeds);
$this->nbFeeds = 0;
$this->nbNotRead = 0;
foreach ($feeds as $feed) {
$feed->_category($this);
$this->nbFeeds++;
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= $feed->inError();
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
}
}
}
@ -100,7 +95,7 @@ class FreshRSS_Category extends Minz_Model {
}
/**
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
@ -113,12 +108,10 @@ class FreshRSS_Category extends Minz_Model {
foreach ($this->feeds as $feed) {
$this->nbFeeds++;
$this->nbNotRead += $feed->nbNotRead();
$this->hasFeedsWithError |= $feed->inError();
$this->hasFeedsWithError |= ($feed->inError() && !$feed->mute());
}
$this->sortFeeds();
}
return $this->feeds ?? [];
}
@ -126,22 +119,10 @@ class FreshRSS_Category extends Minz_Model {
return (bool)($this->hasFeedsWithError);
}
/**
* @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>)
* @return array<string,mixed>|mixed|null
*/
public function attributes(string $key = '') {
if ($key === '') {
return $this->attributes;
} else {
return $this->attributes[$key] ?? null;
}
}
public function _id(int $id): void {
$this->id = $id;
if ($id === FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
$this->_name(_t('gen.short.default_category'));
$this->name = _t('gen.short.default_category');
}
}
@ -150,7 +131,9 @@ class FreshRSS_Category extends Minz_Model {
}
public function _name(string $value): void {
$this->name = mb_strcut(trim($value), 0, 255, 'UTF-8');
if ($this->id !== FreshRSS_CategoryDAO::DEFAULTCATEGORYID) {
$this->name = mb_strcut(trim($value), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
}
}
/** @param array<FreshRSS_Feed>|FreshRSS_Feed $values */
@ -158,7 +141,6 @@ class FreshRSS_Category extends Minz_Model {
if (!is_array($values)) {
$values = [$values];
}
$this->feeds = $values;
$this->sortFeeds();
}
@ -170,27 +152,11 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
$this->feeds = [];
}
$feed->_category($this);
$this->feeds[] = $feed;
$this->sortFeeds();
}
/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
public function _attributes(string $key, $value): void {
if ('' === $key) {
if (is_string($value)) {
$value = json_decode($value, true);
}
if (is_array($value)) {
$this->attributes = $value;
}
} elseif (null === $value) {
unset($this->attributes[$key]);
} else {
$this->attributes[$key] = $value;
}
}
/**
* @param array<string> $attributes
* @throws FreshRSS_Context_Exception
@ -202,8 +168,8 @@ class FreshRSS_Category extends Minz_Model {
}
public function refreshDynamicOpml(): bool {
$url = $this->attributes('opml_url');
if ($url == '') {
$url = $this->attributeString('opml_url');
if ($url == null) {
return false;
}
$ok = true;
@ -243,7 +209,7 @@ class FreshRSS_Category extends Minz_Model {
foreach ($dryRunCategory->feeds() as $dryRunFeed) {
if (empty($existingFeeds[$dryRunFeed->url()])) {
// The feed does not exist in the current category, so add that feed
$dryRunFeed->_categoryId($this->id());
$dryRunFeed->_category($this);
$ok &= ($feedDAO->addFeedObject($dryRunFeed) !== false);
} else {
$existingFeed = $existingFeeds[$dryRunFeed->url()];
@ -273,8 +239,54 @@ class FreshRSS_Category extends Minz_Model {
if ($this->feeds === null) {
return;
}
usort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
uasort($this->feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
}
/**
* Access cached feed
* @param array<FreshRSS_Category> $categories
*/
public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
$feed->_category($category); // Should already be done; just to be safe
return $feed;
}
}
}
return null;
}
/**
* Access cached feeds
* @param array<FreshRSS_Category> $categories
* @return array<int,FreshRSS_Feed>
*/
public static function findFeeds(array $categories): array {
$result = [];
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
$result[$feed->id()] = $feed;
}
}
return $result;
}
/**
* @param array<FreshRSS_Category> $categories
*/
public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->priority() >= $minPriority) {
$n += $feed->nbNotRead();
}
}
}
return $n;
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_CategoryDAO extends Minz_ModelPdo {
@ -29,8 +30,8 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
} elseif ('attributes' === $name) { //v1.15.0
$ok = $this->pdo->exec('ALTER TABLE `_category` ADD COLUMN attributes TEXT') !== false;
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}> $feeds */
/** @var array<array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'keep_history':?int,'ttl':int,'attributes':string}> $feeds */
$feeds = $this->fetchAssoc('SELECT * FROM `_feed`') ?? [];
$stm = $this->pdo->prepare('UPDATE `_feed` SET attributes = :attributes WHERE id = :id');
@ -82,11 +83,11 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
return false;
}
/** @param array<string> $errorInfo */
/** @param array<string|int> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
foreach (['kind', 'lastUpdate', 'error', 'attributes'] as $column) {
if (stripos($errorLines[0], $column) !== false) {
return $this->addColumn($column);
@ -100,7 +101,6 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo {
/**
* @param array{'name':string,'id'?:int,'kind'?:int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string|array<string,mixed>} $valuesTmp
* @return int|false
* @throws JsonException
*/
public function addCategory(array $valuesTmp) {
// TRIM() to provide a type hint as text
@ -152,9 +152,8 @@ SQL;
}
/**
* @param array{'name':string,'kind':int,'attributes'?:string|array<string,mixed>} $valuesTmp
* @param array{'name':string,'kind':int,'attributes'?:array<string,mixed>|mixed|null} $valuesTmp
* @return int|false
* @throws JsonException
*/
public function updateCategory(int $id, array $valuesTmp) {
// No tag of the same name
@ -229,6 +228,7 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'name':string,'kind':int,'lastUpdate':int,'error':int,'attributes'?:array<string,mixed>} $row */
yield $row;
}
} else {
@ -244,26 +244,26 @@ SQL;
public function searchById(int $id): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, ['id' => $id]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
$cat = self::daoToCategory($res);
return $cat[0] ?? null;
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$categories = self::daoToCategories($res);
return reset($categories) ?: null;
}
public function searchByName(string $name): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE name=:name';
$res = $this->fetchAssoc($sql, ['name' => $name]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
$cat = self::daoToCategory($res);
return $cat[0] ?? null;
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate':int,'error':int|bool,'attributes':string}> $res */
$categories = self::daoToCategories($res);
return reset($categories) ?: null;
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listSortedCategories(bool $prePopulateFeeds = true, bool $details = false): array {
$categories = $this->listCategories($prePopulateFeeds, $details);
uasort($categories, static function (FreshRSS_Category $a, FreshRSS_Category $b) {
$aPosition = $a->attributes('position');
$bPosition = $b->attributes('position');
$aPosition = $a->attributeInt('position');
$bPosition = $b->attributeInt('position');
if ($aPosition === $bPosition) {
return ($a->name() < $b->name()) ? -1 : 1;
} elseif (null === $aPosition) {
@ -277,23 +277,23 @@ SQL;
return $categories;
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listCategories(bool $prePopulateFeeds = true, bool $details = false): array {
if ($prePopulateFeeds) {
$sql = 'SELECT c.id AS c_id, c.name AS c_name, c.kind AS c_kind, c.`lastUpdate` AS c_last_update, c.error AS c_error, c.attributes AS c_attributes, '
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.kind, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
. ($details ? 'f.* ' : 'f.id, f.name, f.url, f.kind, f.website, f.priority, f.error, f.attributes, f.`cache_nbEntries`, f.`cache_nbUnreads`, f.ttl ')
. 'FROM `_category` c '
. 'LEFT OUTER JOIN `_feed` f ON f.category=c.id '
. 'WHERE f.priority >= :priority_normal '
. 'WHERE f.priority >= :priority '
. 'GROUP BY f.id, c_id '
. 'ORDER BY c.name, f.name';
$stm = $this->pdo->prepare($sql);
$values = [ ':priority_normal' => FreshRSS_Feed::PRIORITY_NORMAL ];
$values = [ ':priority' => FreshRSS_Feed::PRIORITY_CATEGORY ];
if ($stm !== false && $stm->execute($values)) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
/** @var array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
return self::daoToCategoryPrepopulated($res);
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'category'?:int,'website'?:string,'priority'?:int,'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $res */
return self::daoToCategoriesPrepopulated($res);
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -305,11 +305,11 @@ SQL;
} else {
$res = $this->fetchAssoc('SELECT * FROM `_category` ORDER BY name');
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
return $res == null ? [] : self::daoToCategory($res);
return empty($res) ? [] : self::daoToCategories($res);
}
}
/** @return array<FreshRSS_Category> */
/** @return array<int,FreshRSS_Category> */
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0): array {
$sql = 'SELECT * FROM `_category` WHERE kind = :kind AND `lastUpdate` < :lu ORDER BY `lastUpdate`'
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
@ -318,7 +318,7 @@ SQL;
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
$stm->bindValue(':lu', time() - $defaultCacheDuration, PDO::PARAM_INT) &&
$stm->execute()) {
return self::daoToCategory($stm->fetchAll(PDO::FETCH_ASSOC));
return self::daoToCategories($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $stm ? $stm->errorInfo() : $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -331,11 +331,11 @@ SQL;
public function getDefault(): ?FreshRSS_Category {
$sql = 'SELECT * FROM `_category` WHERE id=:id';
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]);
$res = $this->fetchAssoc($sql, [':id' => self::DEFAULTCATEGORYID]) ?? [];
/** @var array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $res */
$cat = self::daoToCategory($res ?? []);
if (isset($cat[0])) {
return $cat[0];
$categories = self::daoToCategories($res);
if (isset($categories[self::DEFAULTCATEGORYID])) {
return $categories[self::DEFAULTCATEGORYID];
} else {
if (FreshRSS_Context::$isCli) {
fwrite(STDERR, 'FreshRSS database error: Default category not found!' . "\n");
@ -350,8 +350,7 @@ SQL;
$def_cat = $this->searchById(self::DEFAULTCATEGORYID);
if ($def_cat == null) {
$cat = new FreshRSS_Category(_t('gen.short.default_category'));
$cat->_id(self::DEFAULTCATEGORYID);
$cat = new FreshRSS_Category(_t('gen.short.default_category'), self::DEFAULTCATEGORYID);
$sql = 'INSERT INTO `_category`(id, name) VALUES(?, ?)';
if ($this->pdo->dbType() === 'pgsql') {
@ -395,55 +394,30 @@ SQL;
return isset($res[0]) ? (int)$res[0] : -1;
}
/** @param array<FreshRSS_Category> $categories */
public static function findFeed(array $categories, int $feed_id): ?FreshRSS_Feed {
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->id() === $feed_id) {
return $feed;
}
}
}
return null;
}
/**
* @param array<FreshRSS_Category> $categories
*/
public static function countUnread(array $categories, int $minPriority = 0): int {
$n = 0;
foreach ($categories as $category) {
foreach ($category->feeds() as $feed) {
if ($feed->priority() >= $minPriority) {
$n += $feed->nbNotRead();
}
}
}
return $n;
}
/**
* @param array<array{'c_name':string,'c_id':int,'c_kind':int,'c_last_update':int,'c_error':int|bool,'c_attributes'?:string,
* 'id'?:int,'name'?:string,'url'?:string,'kind'?:int,'website'?:string,'priority'?:int,
* 'error'?:int|bool,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* 'error'?:int|bool,'attributes'?:string,'cache_nbEntries'?:int,'cache_nbUnreads'?:int,'ttl'?:int}> $listDAO
* @return array<int,FreshRSS_Category>
*/
private static function daoToCategoryPrepopulated(array $listDAO) {
private static function daoToCategoriesPrepopulated(array $listDAO): array {
$list = [];
$previousLine = [];
$feedsDao = [];
$feedDao = FreshRSS_Factory::createFeedDao();
foreach ($listDAO as $line) {
FreshRSS_DatabaseDAO::pdoInt($line, ['c_id', 'c_kind', 'c_last_update', 'c_error',
'id', 'kind', 'priority', 'error', 'cache_nbEntries', 'cache_nbUnreads', 'ttl']);
if (!empty($previousLine['c_id']) && $line['c_id'] !== $previousLine['c_id']) {
// End of the current category, we add it to the $list
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
$previousLine['c_id'],
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_id($previousLine['c_id']);
$cat->_kind($previousLine['c_kind']);
$cat->_attributes('', $previousLine['c_attributes'] ?? '[]');
$list[$previousLine['c_id']] = $cat;
$cat->_attributes($previousLine['c_attributes'] ?? '[]');
$list[$cat->id()] = $cat;
$feedsDao = []; //Prepare for next category
}
@ -456,14 +430,14 @@ SQL;
if ($previousLine != null) {
$cat = new FreshRSS_Category(
$previousLine['c_name'],
$feedDao::daoToFeed($feedsDao, $previousLine['c_id'])
$previousLine['c_id'],
$feedDao::daoToFeeds($feedsDao, $previousLine['c_id'])
);
$cat->_id($previousLine['c_id']);
$cat->_kind($previousLine['c_kind']);
$cat->_lastUpdate($previousLine['c_last_update'] ?? 0);
$cat->_error($previousLine['c_error'] ?? 0);
$cat->_attributes('', $previousLine['c_attributes'] ?? []);
$list[$previousLine['c_id']] = $cat;
$cat->_attributes($previousLine['c_attributes'] ?? []);
$list[$cat->id()] = $cat;
}
return $list;
@ -471,23 +445,22 @@ SQL;
/**
* @param array<array{'name':string,'id':int,'kind':int,'lastUpdate'?:int,'error'?:int|bool,'attributes'?:string}> $listDAO
* @return array<FreshRSS_Category>
* @return array<int,FreshRSS_Category>
*/
private static function daoToCategory(array $listDAO): array {
private static function daoToCategories(array $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'lastUpdate', 'error']);
$cat = new FreshRSS_Category(
$dao['name']
$dao['name'],
$dao['id']
);
$cat->_id($dao['id']);
$cat->_kind($dao['kind']);
$cat->_lastUpdate($dao['lastUpdate'] ?? 0);
$cat->_error($dao['error'] ?? 0);
$cat->_attributes('', $dao['attributes'] ?? '');
$list[] = $cat;
$cat->_attributes($dao['attributes'] ?? '');
$list[$cat->id()] = $cat;
}
return $list;
}
}

View File

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
class FreshRSS_CategoryDAOSQLite extends FreshRSS_CategoryDAO {
/** @param array<string> $errorInfo */
/** @param array<int|string> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if ($tableInfo = $this->pdo->query("PRAGMA table_info('category')")) {
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* The context object handles the current configuration file and different
@ -6,114 +7,93 @@
*/
final class FreshRSS_Context {
/**
* @var FreshRSS_UserConfiguration|null
*/
public static $user_conf;
/**
* @var FreshRSS_SystemConfiguration|null
*/
public static $system_conf;
/**
* @var array<int,FreshRSS_Category>
*/
public static $categories = [];
private static array $categories = [];
/**
* @var array<int,FreshRSS_Tag>
*/
public static $tags = [];
/**
* @var string
*/
public static $name = '';
/**
* @var string
*/
public static $description = '';
/**
* @var int
*/
public static $total_unread = 0;
private static array $tags = [];
public static string $name = '';
public static string $description = '';
public static int $total_unread = 0;
public static int $total_important_unread = 0;
/** @var array{'all':int,'read':int,'unread':int} */
public static $total_starred = [
public static array $total_starred = [
'all' => 0,
'read' => 0,
'unread' => 0,
];
/**
* @var int
*/
public static $get_unread = 0;
public static int $get_unread = 0;
/** @var array{'all':bool,'starred':bool,'feed':int|false,'category':int|false,'tag':int|false,'tags':bool} */
public static $current_get = [
/** @var array{'all':bool,'starred':bool,'important':bool,'feed':int|false,'category':int|false,'tag':int|false,'tags':bool} */
public static array $current_get = [
'all' => false,
'starred' => false,
'important' => false,
'feed' => false,
'category' => false,
'tag' => false,
'tags' => false,
];
/**
* @var string
*/
public static $next_get = 'a';
/**
* @var int
*/
public static $state = 0;
public static string $next_get = 'a';
public static int $state = 0;
/**
* @phpstan-var 'ASC'|'DESC'
* @var string
*/
public static $order = 'DESC';
public static string $order = 'DESC';
public static int $number = 0;
public static int $offset = 0;
public static FreshRSS_BooleanSearch $search;
public static string $first_id = '';
public static string $next_id = '';
public static string $id_max = '';
public static int $sinceHours = 0;
public static bool $isCli = false;
/**
* @var int
* @deprecated Will be made `private`; use `FreshRSS_Context::systemConf()` instead.
* @internal
*/
public static $number = 0;
/** @var FreshRSS_BooleanSearch */
public static $search;
public static ?FreshRSS_SystemConfiguration $system_conf = null;
/**
* @var string
* @deprecated Will be made `private`; use `FreshRSS_Context::userConf()` instead.
* @internal
*/
public static $first_id = '';
/**
* @var string
*/
public static $next_id = '';
/**
* @var string
*/
public static $id_max = '';
/**
* @var int
*/
public static $sinceHours = 0;
/**
* @var bool
*/
public static $isCli = false;
public static ?FreshRSS_UserConfiguration $user_conf = null;
/**
* Initialize the context for the global system.
*/
public static function initSystem(bool $reload = false): FreshRSS_SystemConfiguration {
if ($reload || FreshRSS_Context::$system_conf == null) {
public static function initSystem(bool $reload = false): void {
if ($reload || FreshRSS_Context::$system_conf === null) {
//TODO: Keep in session what we need instead of always reloading from disk
FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
}
}
/**
* @throws FreshRSS_Context_Exception
*/
public static function &systemConf(): FreshRSS_SystemConfiguration {
if (FreshRSS_Context::$system_conf === null) {
throw new FreshRSS_Context_Exception('System configuration not initialised!');
}
return FreshRSS_Context::$system_conf;
}
public static function hasSystemConf(): bool {
return FreshRSS_Context::$system_conf !== null;
}
/**
* Initialize the context for the current user.
* @throws Minz_ConfigurationParamException
*/
public static function initUser(string $username = '', bool $userMustExist = true): ?FreshRSS_UserConfiguration {
public static function initUser(string $username = '', bool $userMustExist = true): void {
FreshRSS_Context::$user_conf = null;
if (!isset($_SESSION)) {
Minz_Session::init('FreshRSS');
@ -145,14 +125,16 @@ final class FreshRSS_Context {
Minz_Session::unlock();
if (FreshRSS_Context::$user_conf == null) {
return null;
return;
}
FreshRSS_Context::$search = new FreshRSS_BooleanSearch('');
//Legacy
$oldEntries = (int)FreshRSS_Context::$user_conf->param('old_entries', 0);
$keepMin = (int)FreshRSS_Context::$user_conf->param('keep_history_default', -5);
$oldEntries = FreshRSS_Context::$user_conf->param('old_entries', 0);
$oldEntries = is_numeric($oldEntries) ? (int)$oldEntries : 0;
$keepMin = FreshRSS_Context::$user_conf->param('keep_history_default', -5);
$keepMin = is_numeric($keepMin) ? (int)$keepMin : -5;
if ($oldEntries > 0 || $keepMin > -5) { //Freshrss < 1.15
$archiving = FreshRSS_Context::$user_conf->archiving;
$archiving['keep_max'] = false;
@ -172,14 +154,53 @@ final class FreshRSS_Context {
if (!in_array(FreshRSS_Context::$user_conf->display_categories, [ 'active', 'remember', 'all', 'none' ], true)) {
FreshRSS_Context::$user_conf->display_categories = FreshRSS_Context::$user_conf->display_categories === true ? 'all' : 'active';
}
}
/**
* @throws FreshRSS_Context_Exception
*/
public static function &userConf(): FreshRSS_UserConfiguration {
if (FreshRSS_Context::$user_conf === null) {
throw new FreshRSS_Context_Exception('User configuration not initialised!');
}
return FreshRSS_Context::$user_conf;
}
public static function hasUserConf(): bool {
return FreshRSS_Context::$user_conf !== null;
}
public static function clearUserConf(): void {
FreshRSS_Context::$user_conf = null;
}
/** @return array<int,FreshRSS_Category> */
public static function categories(): array {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listSortedCategories(true, false);
}
return self::$categories;
}
/** @return array<int,FreshRSS_Feed> */
public static function feeds(): array {
return FreshRSS_Category::findFeeds(self::categories());
}
/** @return array<int,FreshRSS_Tag> */
public static function labels(bool $precounts = false): array {
if (empty(self::$tags) || $precounts) {
$tagDAO = FreshRSS_Factory::createTagDao();
self::$tags = $tagDAO->listTags($precounts) ?: [];
}
return self::$tags;
}
/**
* This action updates the Context object by using request parameters.
*
* Parameters are:
* HTTP GET request parameters are:
* - state (default: conf->default_view)
* - search (default: empty string)
* - order (default: conf->sort_order)
@ -190,44 +211,41 @@ final class FreshRSS_Context {
* @throws Minz_ConfigurationNamespaceException
* @throws Minz_PDOConnectionException
*/
public static function updateUsingRequest(): void {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listSortedCategories();
public static function updateUsingRequest(bool $computeStatistics): void {
if ($computeStatistics && self::$total_unread === 0) {
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
self::$total_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_MAIN_STREAM);
self::$total_important_unread = FreshRSS_Category::countUnread(self::categories(), FreshRSS_Feed::PRIORITY_IMPORTANT);
}
// Update number of read / unread variables.
$entryDAO = FreshRSS_Factory::createEntryDao();
self::$total_starred = $entryDAO->countUnreadReadFavorites();
self::$total_unread = FreshRSS_CategoryDAO::countUnread(
self::$categories, 1
);
self::_get(Minz_Request::paramString('get') ?: 'a');
self::$state = Minz_Request::paramInt('state') ?: self::$user_conf->default_state;
self::$state = Minz_Request::paramInt('state') ?: FreshRSS_Context::userConf()->default_state;
$state_forced_by_user = Minz_Request::paramString('state') !== '';
if (!$state_forced_by_user && !self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
if (self::$user_conf->default_view === 'all') {
if (FreshRSS_Context::userConf()->default_view === 'all') {
self::$state |= FreshRSS_Entry::STATE_ALL;
} elseif (self::$user_conf->default_view === 'adaptive' && self::$get_unread <= 0) {
} elseif (FreshRSS_Context::userConf()->default_view === 'adaptive' && self::$get_unread <= 0) {
self::$state |= FreshRSS_Entry::STATE_READ;
}
if (self::$user_conf->show_fav_unread &&
if (FreshRSS_Context::userConf()->show_fav_unread &&
(self::isCurrentGet('s') || self::isCurrentGet('T') || self::isTag())) {
self::$state |= FreshRSS_Entry::STATE_READ;
}
}
self::$search = new FreshRSS_BooleanSearch(Minz_Request::paramString('search'));
$order = Minz_Request::paramString('order') ?: self::$user_conf->sort_order;
$order = Minz_Request::paramString('order') ?: FreshRSS_Context::userConf()->sort_order;
self::$order = in_array($order, ['ASC', 'DESC'], true) ? $order : 'DESC';
self::$number = Minz_Request::paramInt('nb') ?: self::$user_conf->posts_per_page;
if (self::$number > self::$user_conf->max_posts_per_rss) {
self::$number = Minz_Request::paramInt('nb') ?: FreshRSS_Context::userConf()->posts_per_page;
if (self::$number > FreshRSS_Context::userConf()->max_posts_per_rss) {
self::$number = max(
self::$user_conf->max_posts_per_rss,
self::$user_conf->posts_per_page);
FreshRSS_Context::userConf()->max_posts_per_rss,
FreshRSS_Context::userConf()->posts_per_page);
}
self::$offset = Minz_Request::paramInt('offset');
self::$first_id = Minz_Request::paramString('next');
self::$sinceHours = Minz_Request::paramInt('hours');
}
@ -253,12 +271,14 @@ final class FreshRSS_Context {
* Return the current get as a string or an array.
*
* If $array is true, the first item of the returned value is 'f' or 'c' or 't' and the second is the id.
* @phpstan-return ($asArray is true ? array{'a'|'c'|'f'|'s'|'t'|'T',bool|int} : string)
* @phpstan-return ($asArray is true ? array{'a'|'c'|'f'|'i'|'s'|'t'|'T',bool|int} : string)
* @return string|array{string,bool|int}
*/
public static function currentGet(bool $asArray = false) {
if (self::$current_get['all']) {
return $asArray ? ['a', true] : 'a';
} elseif (self::$current_get['important']) {
return $asArray ? ['i', true] : 'i';
} elseif (self::$current_get['starred']) {
return $asArray ? ['s', true] : 's';
} elseif (self::$current_get['feed']) {
@ -292,6 +312,13 @@ final class FreshRSS_Context {
return self::$current_get['all'] != false;
}
/**
* @return bool true if the current request targets important feeds, false otherwise.
*/
public static function isImportant(): bool {
return self::$current_get['important'] != false;
}
/**
* @return bool true if the current request targets a category, false otherwise.
*/
@ -323,6 +350,8 @@ final class FreshRSS_Context {
switch($type) {
case 'a':
return self::$current_get['all'];
case 'i':
return self::$current_get['important'];
case 's':
return self::$current_get['starred'];
case 'f':
@ -360,20 +389,26 @@ final class FreshRSS_Context {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listCategories();
self::$categories = $catDAO->listCategories(true);
}
switch($type) {
case 'a':
self::$current_get['all'] = true;
self::$name = _t('index.feed.title');
self::$description = self::$system_conf->meta_description;
self::$description = FreshRSS_Context::systemConf()->meta_description;
self::$get_unread = self::$total_unread;
break;
case 'i':
self::$current_get['important'] = true;
self::$name = _t('index.menu.important');
self::$description = FreshRSS_Context::systemConf()->meta_description;
self::$get_unread = self::$total_unread;
break;
case 's':
self::$current_get['starred'] = true;
self::$name = _t('index.feed.title_fav');
self::$description = self::$system_conf->meta_description;
self::$description = FreshRSS_Context::systemConf()->meta_description;
self::$get_unread = self::$total_starred['unread'];
// Update state if favorite is not yet enabled.
@ -381,7 +416,7 @@ final class FreshRSS_Context {
break;
case 'f':
// We try to find the corresponding feed. When allowing robots, always retrieve the full feed including description
$feed = FreshRSS_Context::$system_conf->allow_robots ? null : FreshRSS_CategoryDAO::findFeed(self::$categories, $id);
$feed = FreshRSS_Context::systemConf()->allow_robots ? null : FreshRSS_Category::findFeed(self::$categories, $id);
if ($feed === null) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$feed = $feedDAO->searchById($id);
@ -404,7 +439,7 @@ final class FreshRSS_Context {
if ($cat === null) {
throw new FreshRSS_Context_Exception('Invalid category: ' . $id);
}
//self::$categories[$id] = $cat;
self::$categories[$id] = $cat;
} else {
$cat = self::$categories[$id];
}
@ -420,7 +455,7 @@ final class FreshRSS_Context {
if ($tag === null) {
throw new FreshRSS_Context_Exception('Invalid tag: ' . $id);
}
//self::$tags[$id] = $tag;
self::$tags[$id] = $tag;
} else {
$tag = self::$tags[$id];
}
@ -450,10 +485,10 @@ final class FreshRSS_Context {
if (empty(self::$categories)) {
$catDAO = FreshRSS_Factory::createCategoryDao();
self::$categories = $catDAO->listCategories();
self::$categories = $catDAO->listCategories(true);
}
if (self::$user_conf->onread_jump_next && strlen($get) > 2) {
if (FreshRSS_Context::userConf()->onread_jump_next && strlen($get) > 2) {
$another_unread_id = '';
$found_current_get = false;
switch ($get[0]) {
@ -517,7 +552,7 @@ final class FreshRSS_Context {
* - the "unread" state is enable
*/
public static function isAutoRemoveAvailable(): bool {
if (!self::$user_conf->auto_remove_article) {
if (!FreshRSS_Context::userConf()->auto_remove_article) {
return false;
}
if (self::isStateEnabled(FreshRSS_Entry::STATE_READ)) {
@ -536,7 +571,7 @@ final class FreshRSS_Context {
* are read.
*/
public static function isStickyPostEnabled(): bool {
if (self::$user_conf->sticky_post) {
if (FreshRSS_Context::userConf()->sticky_post) {
return true;
}
if (self::isAutoRemoveAvailable()) {

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This class is used to test database is well-constructed.
@ -20,8 +21,8 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
public const LENGTH_INDEX_UNICODE = 191;
public function create(): string {
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
$db = FreshRSS_Context::$system_conf->db;
require_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
$db = FreshRSS_Context::systemConf()->db;
try {
$sql = sprintf($GLOBALS['SQL_CREATE_DB'], empty($db['base']) ? '' : $db['base']);
@ -173,11 +174,21 @@ class FreshRSS_DatabaseDAO extends Minz_ModelPdo {
}
public function size(bool $all = false): int {
$db = FreshRSS_Context::$system_conf->db;
$db = FreshRSS_Context::systemConf()->db;
// MariaDB does not refresh size information automatically
$sql = <<<'SQL'
ANALYZE TABLE `_category`, `_feed`, `_entry`, `_entrytmp`, `_tag`, `_entrytag`
SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$stm->fetchAll();
}
//MySQL:
$sql = <<<'SQL'
SELECT SUM(data_length + index_length)
FROM information_schema.TABLES WHERE table_schema=:table_schema
SELECT SUM(DATA_LENGTH + INDEX_LENGTH + DATA_FREE)
FROM information_schema.TABLES WHERE TABLE_SCHEMA=:table_schema
SQL;
$values = [':table_schema' => $db['base']];
if (!$all) {
@ -204,27 +215,14 @@ SQL;
return $ok;
}
public function ensureCaseInsensitiveGuids(): bool {
$ok = true;
if ($this->pdo->dbType() === 'mysql') {
include(APP_PATH . '/SQL/install.sql.mysql.php');
$ok = false;
try {
$ok = $this->pdo->exec($GLOBALS['SQL_UPDATE_GUID_LATIN1_BIN']) !== false; //FreshRSS 1.12
} catch (Exception $e) {
$ok = false;
Minz_Log::error(__METHOD__ . ' error: ' . $e->getMessage());
}
}
return $ok;
}
public function minorDbMaintenance(): void {
$catDAO = FreshRSS_Factory::createCategoryDao();
$catDAO->resetDefaultCategoryName();
$this->ensureCaseInsensitiveGuids();
include_once(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
if (!empty($GLOBALS['SQL_UPDATE_MINOR']) && $this->pdo->exec($GLOBALS['SQL_UPDATE_MINOR']) === false) {
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($this->pdo->errorInfo()));
}
}
private static function stdError(string $error): bool {
@ -395,4 +393,31 @@ SQL;
return true;
}
/**
* Ensure that some PDO columns are `int` and not `string`.
* Compatibility with PHP 7.
* @param array<string|int|null> $table
* @param array<string> $columns
*/
public static function pdoInt(array &$table, array $columns): void {
foreach ($columns as $column) {
if (isset($table[$column]) && is_string($table[$column])) {
$table[$column] = (int)$table[$column];
}
}
}
/**
* Ensure that some PDO columns are `string` and not `bigint`.
* @param array<string|int|null> $table
* @param array<string> $columns
*/
public static function pdoString(array &$table, array $columns): void {
foreach ($columns as $column) {
if (isset($table[$column])) {
$table[$column] = (string)$table[$column];
}
}
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This class is used to test database is well-constructed.
@ -10,7 +11,7 @@ class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAOSQLite {
public const UNDEFINED_TABLE = '42P01';
public function tablesAreCorrect(): bool {
$db = FreshRSS_Context::$system_conf->db;
$db = FreshRSS_Context::systemConf()->db;
$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=:tableowner';
$res = $this->fetchAssoc($sql, [':tableowner' => $db['user']]);
if ($res == null) {
@ -57,7 +58,7 @@ SQL;
public function size(bool $all = false): int {
if ($all) {
$db = FreshRSS_Context::$system_conf->db;
$db = FreshRSS_Context::systemConf()->db;
$res = $this->fetchColumn('SELECT pg_database_size(:base)', 0, [':base' => $db['base']]);
} else {
$sql = <<<SQL

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* This class is used to test database is well-constructed (SQLite).
@ -64,12 +65,12 @@ class FreshRSS_DatabaseDAOSQLite extends FreshRSS_DatabaseDAO {
$sum = 0;
if ($all) {
foreach (glob(DATA_PATH . '/users/*/db.sqlite') ?: [] as $filename) {
$sum += @filesize($filename);
$sum += (@filesize($filename) ?: 0);
}
} else {
$sum = @filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite');
$sum = (@filesize(DATA_PATH . '/users/' . $this->current_user . '/db.sqlite') ?: 0);
}
return intval($sum);
return $sum;
}
public function optimize(): bool {

View File

@ -1,5 +1,4 @@
<?php
declare(strict_types=1);
class FreshRSS_Days {

View File

@ -1,44 +1,33 @@
<?php
declare(strict_types=1);
class FreshRSS_Entry extends Minz_Model {
use FreshRSS_AttributesTrait;
public const STATE_READ = 1;
public const STATE_NOT_READ = 2;
public const STATE_ALL = 3;
public const STATE_FAVORITE = 4;
public const STATE_NOT_FAVORITE = 8;
/** @var string */
private $id = '0';
/** @var string */
private $guid;
/** @var string */
private $title;
private string $id = '0';
private string $guid;
private string $title;
/** @var array<string> */
private $authors;
/** @var string */
private $content;
/** @var string */
private $link;
/** @var int */
private $date;
/** @var int */
private $lastSeen = 0;
/** @var string In microseconds */
private $date_added = '0';
/** @var string */
private $hash = '';
/** @var bool|null */
private $is_read;
/** @var bool|null */
private $is_favorite;
/** @var int */
private $feedId;
/** @var FreshRSS_Feed|null */
private $feed;
private array $authors;
private string $content;
private string $link;
private int $date;
private int $lastSeen = 0;
/** In microseconds */
private string $date_added = '0';
private string $hash = '';
private ?bool $is_read;
private ?bool $is_favorite;
private int $feedId;
private ?FreshRSS_Feed $feed;
/** @var array<string> */
private $tags = [];
/** @var array<string,mixed> */
private $attributes = [];
private array $tags = [];
/**
* @param int|string $pubdate
@ -61,8 +50,10 @@ class FreshRSS_Entry extends Minz_Model {
}
/** @param array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int,
* 'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:string,'thumbnail'?:string,'timestamp'?:string} $dao */
* 'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string} $dao */
public static function fromArray(array $dao): FreshRSS_Entry {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id_feed', 'date', 'lastSeen', 'is_read', 'is_favorite']);
if (empty($dao['content'])) {
$dao['content'] = '';
}
@ -99,7 +90,7 @@ class FreshRSS_Entry extends Minz_Model {
$entry->_lastSeen($dao['lastSeen']);
}
if (!empty($dao['attributes'])) {
$entry->_attributes('', $dao['attributes']);
$entry->_attributes($dao['attributes']);
}
if (!empty($dao['hash'])) {
$entry->_hash($dao['hash']);
@ -107,6 +98,17 @@ class FreshRSS_Entry extends Minz_Model {
return $entry;
}
/**
* @param Traversable<array{'id'?:string,'id_feed'?:int,'guid'?:string,'title'?:string,'author'?:string,'content'?:string,'link'?:string,'date'?:int|string,'lastSeen'?:int,
* 'hash'?:string,'is_read'?:bool|int,'is_favorite'?:bool|int,'tags'?:string|array<string>,'attributes'?:?string,'thumbnail'?:string,'timestamp'?:string}> $daos
* @return Traversable<FreshRSS_Entry>
*/
public static function fromTraversable(Traversable $daos): Traversable {
foreach ($daos as $dao) {
yield FreshRSS_Entry::fromArray($dao);
}
}
public function id(): string {
return $this->id;
}
@ -154,7 +156,7 @@ class FreshRSS_Entry extends Minz_Model {
* Provides the original content without additional content potentially added by loadCompleteContent().
*/
public function originalContent(): string {
return preg_replace('#<!-- FULLCONTENT start //-->.*<!-- FULLCONTENT end //-->#s', '', $this->content);
return preg_replace('#<!-- FULLCONTENT start //-->.*<!-- FULLCONTENT end //-->#s', '', $this->content) ?? '';
}
/**
@ -169,7 +171,7 @@ class FreshRSS_Entry extends Minz_Model {
$content = $this->content;
$thumbnailAttribute = $this->attributes('thumbnail');
$thumbnailAttribute = $this->attributeArray('thumbnail') ?? [];
if (!empty($thumbnailAttribute['url'])) {
$elink = $thumbnailAttribute['url'];
if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) {
@ -183,12 +185,15 @@ HTML;
}
}
$attributeEnclosures = $this->attributes('enclosures');
$attributeEnclosures = $this->attributeArray('enclosures');
if (empty($attributeEnclosures)) {
return $content;
}
foreach ($attributeEnclosures as $enclosure) {
if (!is_array($enclosure)) {
continue;
}
$elink = $enclosure['url'] ?? '';
if ($elink == '') {
continue;
@ -197,11 +202,14 @@ HTML;
continue;
}
$credit = $enclosure['credit'] ?? '';
$description = $enclosure['description'] ?? '';
$description = nl2br($enclosure['description'] ?? '', true);
$length = $enclosure['length'] ?? 0;
$medium = $enclosure['medium'] ?? '';
$mime = $enclosure['type'] ?? '';
$thumbnails = $enclosure['thumbnails'] ?? [];
$thumbnails = $enclosure['thumbnails'] ?? null;
if (!is_array($thumbnails)) {
$thumbnails = [];
}
$etitle = $enclosure['title'] ?? '';
$content .= "\n";
@ -244,7 +252,7 @@ HTML;
/** @return Traversable<array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string,'height'?:int,'width'?:int,'thumbnails'?:array<string>}> */
public function enclosures(bool $searchBodyImages = false): Traversable {
$attributeEnclosures = $this->attributes('enclosures');
$attributeEnclosures = $this->attributeArray('enclosures');
if (is_iterable($attributeEnclosures)) {
// FreshRSS 1.20.1+: The enclosures are saved as attributes
yield from $attributeEnclosures;
@ -258,7 +266,7 @@ HTML;
$dom->loadHTML('<?xml version="1.0" encoding="UTF-8" ?>' . $this->content, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
$xpath = new DOMXPath($dom);
}
if ($searchEnclosures) {
if ($searchEnclosures && $xpath !== null) {
// Legacy code for database entries < FreshRSS 1.20.1
$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
if (!empty($enclosures)) {
@ -281,7 +289,7 @@ HTML;
}
}
}
if ($searchBodyImages) {
if ($searchBodyImages && $xpath !== null) {
$images = $xpath->query('//img');
if (!empty($images)) {
/** @var DOMElement $img */
@ -293,6 +301,7 @@ HTML;
if ($src != null) {
$result = [
'url' => $src,
'medium' => 'image',
];
yield Minz_Helper::htmlspecialchars_utf8($result);
}
@ -305,15 +314,28 @@ HTML;
}
/**
* @return array{'url':string,'type'?:string,'medium'?:string,'length'?:int,'title'?:string,'description'?:string,'credit'?:string,'height'?:int,'width'?:int,'thumbnails'?:array<string>}|null
* @return array{'url':string,'height'?:int,'width'?:int,'time'?:string}|null
*/
public function thumbnail(bool $searchEnclosures = true): ?array {
$thumbnail = $this->attributes('thumbnail');
$thumbnail = $this->attributeArray('thumbnail') ?? [];
// First, use the provided thumbnail, if any
if (!empty($thumbnail['url'])) {
return $thumbnail;
}
if ($searchEnclosures) {
foreach ($this->enclosures(true) as $enclosure) {
// Second, search each enclosures thumbnails
if (!empty($enclosure['thumbnails'][0])) {
foreach ($enclosure['thumbnails'] as $src) {
if (is_string($src)) {
return [
'url' => $src,
'medium' => 'image',
];
}
}
}
// Third, check whether each enclosure itself is an appropriate image
if (self::enclosureIsImage($enclosure)) {
return $enclosure;
}
@ -391,34 +413,6 @@ HTML;
}
}
/**
* @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>)
* @return array<string,mixed>|mixed|null
*/
public function attributes(string $key = '') {
if ($key === '') {
return $this->attributes;
} else {
return $this->attributes[$key] ?? null;
}
}
/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
public function _attributes(string $key, $value): void {
if ($key == '') {
if (is_string($value)) {
$value = json_decode($value, true);
}
if (is_array($value)) {
$this->attributes = $value;
}
} elseif ($value === null) {
unset($this->attributes[$key]);
} else {
$this->attributes[$key] = $value;
}
}
public function hash(): string {
if ($this->hash == '') {
//Do not include $this->date because it may be automatically generated when lacking
@ -435,7 +429,11 @@ HTML;
return $this->hash;
}
public function _id(string $value): void {
/** @param int|string $value String is for compatibility with 32-bit platforms */
public function _id($value): void {
if (is_int($value)) {
$value = (string)$value;
}
$this->id = $value;
if ($this->date_added == 0) {
$this->date_added = $value;
@ -577,7 +575,7 @@ HTML;
$ok &= in_array($this->feedId, $filter->getFeedIds(), true);
}
if ($ok && $filter->getNotFeedIds()) {
$ok &= !in_array($this->feedId, $filter->getFeedIds(), true);
$ok &= !in_array($this->feedId, $filter->getNotFeedIds(), true);
}
if ($ok && $filter->getAuthor()) {
foreach ($filter->getAuthor() as $author) {
@ -649,42 +647,29 @@ HTML;
return (bool)$ok;
}
/** @param array<string,bool> $titlesAsRead */
/** @param array<string,bool|int> $titlesAsRead */
public function applyFilterActions(array $titlesAsRead = []): void {
if ($this->feed != null) {
if (!$this->isRead()) {
if ($this->feed->attributes('read_upon_reception') ||
($this->feed->attributes('read_upon_reception') === null && FreshRSS_Context::$user_conf->mark_when['reception'])) {
$this->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $this, 'upon_reception');
}
if (!empty($titlesAsRead[$this->title()])) {
Minz_Log::debug('Mark title as read: ' . $this->title());
$this->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $this, 'same_title_in_feed');
}
$feed = $this->feed;
if ($feed === null) {
return;
}
if (!$this->isRead()) {
if ($feed->attributeBoolean('read_upon_reception') ||
($feed->attributeBoolean('read_upon_reception') === null && FreshRSS_Context::userConf()->mark_when['reception'])) {
$this->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $this, 'upon_reception');
}
foreach ($this->feed->filterActions() as $filterAction) {
if ($this->matches($filterAction->booleanSearch())) {
foreach ($filterAction->actions() as $action) {
switch ($action) {
case 'read':
if (!$this->isRead()) {
$this->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $this, 'filter');
}
break;
case 'star':
$this->_isFavorite(true);
break;
case 'label':
//TODO: Implement more actions
break;
}
}
}
if (!empty($titlesAsRead[$this->title()])) {
Minz_Log::debug('Mark title as read: ' . $this->title());
$this->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $this, 'same_title_in_feed');
}
}
FreshRSS_Context::userConf()->applyFilterActions($this);
if ($feed->category() !== null) {
$feed->category()->applyFilterActions($this);
}
$feed->applyFilterActions($this);
}
public function isDay(int $day, int $today): bool {
@ -706,6 +691,7 @@ HTML;
/**
* @param array<string,mixed> $attributes
* @throws Minz_Exception
*/
public static function getContentByParsing(string $url, string $path, array $attributes = [], int $maxRedirs = 3): string {
$cachePath = FreshRSS_Feed::cacheFilename($url, $attributes, FreshRSS_Feed::KIND_HTML_XPATH);
@ -717,10 +703,9 @@ HTML;
if ($maxRedirs > 0) {
//Follow any HTML redirection
$metas = $xpath->query('//meta[@content]');
/** @var array<DOMElement> $metas */
$metas = $xpath->query('//meta[@content]') ?: [];
foreach ($metas as $meta) {
if (strtolower(trim($meta->getAttribute('http-equiv'))) === 'refresh') {
if ($meta instanceof DOMElement && strtolower(trim($meta->getAttribute('http-equiv'))) === 'refresh') {
$refresh = preg_replace('/^[0-9.; ]*\s*(url\s*=)?\s*/i', '', trim($meta->getAttribute('content')));
$refresh = SimplePie_Misc::absolutize_url($refresh, $url);
if ($refresh != false && $refresh !== $url) {
@ -739,12 +724,15 @@ HTML;
}
$content = '';
$nodes = $xpath->query(new Gt\CssXPath\Translator($path));
$nodes = $xpath->query((new Gt\CssXPath\Translator($path))->asXPath());
if ($nodes != false) {
foreach ($nodes as $node) {
if (!empty($attributes['path_entries_filter'])) {
$filterednodes = $xpath->query(new Gt\CssXPath\Translator($attributes['path_entries_filter']), $node) ?: [];
$filterednodes = $xpath->query((new Gt\CssXPath\Translator($attributes['path_entries_filter']))->asXPath(), $node) ?: [];
foreach ($filterednodes as $filterednode) {
if ($filterednode->parentNode === null) {
continue;
}
$filterednode->parentNode->removeChild($filterednode);
}
}
@ -754,7 +742,7 @@ HTML;
$html = trim(sanitizeHTML($content, $base));
return $html;
} else {
throw new Exception();
throw new Minz_Exception();
}
}
@ -780,7 +768,7 @@ HTML;
if ('' !== $fullContent) {
$fullContent = "<!-- FULLCONTENT start //-->{$fullContent}<!-- FULLCONTENT end //-->";
$originalContent = $this->originalContent();
switch ($feed->attributes('content_action')) {
switch ($feed->attributeString('content_action')) {
case 'prepend':
$this->content = $fullContent . $originalContent;
break;
@ -827,6 +815,28 @@ HTML;
];
}
/**
* @return array{array<string>,array<string>} Array of first tags to show, then array of remaining tags
*/
public function tagsFormattingHelper(): array {
$firstTags = [];
$remainingTags = [];
if (FreshRSS_Context::hasUserConf() && in_array(FreshRSS_Context::userConf()->show_tags, ['b', 'f', 'h'], true)) {
$maxTagsDisplayed = (int)FreshRSS_Context::userConf()->show_tags_max;
$tags = $this->tags();
if (!empty($tags)) {
if ($maxTagsDisplayed > 0) {
$firstTags = array_slice($tags, 0, $maxTagsDisplayed);
$remainingTags = array_slice($tags, $maxTagsDisplayed);
} else {
$firstTags = $tags;
}
}
}
return [$firstTags,$remainingTags];
}
/**
* Integer format conversion for Google Reader API format
* @param string|int $dec Decimal number

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_EntryDAO extends Minz_ModelPdo {
@ -26,26 +27,6 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo {
return str_replace('INSERT INTO ', 'INSERT IGNORE INTO ', $sql);
}
//TODO: Move the database auto-updates to DatabaseDAO
protected function createEntryTempTable(): bool {
$ok = false;
$hadTransaction = $this->pdo->inTransaction();
if ($hadTransaction) {
$this->pdo->commit();
}
try {
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
Minz_Log::warning('SQL CREATE TABLE entrytmp...');
$ok = $this->pdo->exec($GLOBALS['SQL_CREATE_TABLE_ENTRYTMP'] . $GLOBALS['SQL_CREATE_INDEX_ENTRY_1']) !== false;
} catch (Exception $ex) {
Minz_Log::error(__method__ . ' error: ' . $ex->getMessage());
}
if ($hadTransaction) {
$this->pdo->beginTransaction();
}
return $ok;
}
private function updateToMediumBlob(): bool {
if ($this->pdo->dbType() !== 'mysql') {
return false;
@ -85,29 +66,22 @@ SQL;
}
//TODO: Move the database auto-updates to DatabaseDAO
/** @param array<string> $errorInfo */
/** @param array<string|int> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
foreach (['attributes'] as $column) {
if (stripos($errorLines[0], $column) !== false) {
return $this->addColumn($column);
}
}
}
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR) {
if (stripos($errorInfo[2], 'tag') !== false) {
$tagDAO = FreshRSS_Factory::createTagDao();
return $tagDAO->createTagTable(); //v1.12.0
} elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
return $this->createEntryTempTable(); //v1.7.0
}
}
}
if (isset($errorInfo[1])) {
// May be a string or an int
if ($errorInfo[1] == FreshRSS_DatabaseDAO::ER_DATA_TOO_LONG) {
if (stripos($errorInfo[2], 'content_bin') !== false) {
if (stripos((string)$errorInfo[2], 'content_bin') !== false) {
return $this->updateToMediumBlob(); //v1.15.0
}
}
@ -121,7 +95,7 @@ SQL;
private $addEntryPrepared = false;
/** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,'hash':string,
* 'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
* 'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes'?:null|string|array<string,mixed>} $valuesTmp */
public function addEntry(array $valuesTmp, bool $useTmpTable = true): bool {
if ($this->addEntryPrepared == null) {
$sql = static::sqlIgnoreConflict(
@ -137,21 +111,20 @@ SQL;
}
if ($this->addEntryPrepared) {
$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 767);
$valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
$this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
$valuesTmp['title'] = mb_strcut($valuesTmp['title'], 0, 255, 'UTF-8');
$valuesTmp['title'] = mb_strcut($valuesTmp['title'], 0, 8192, 'UTF-8');
$valuesTmp['title'] = safe_utf8($valuesTmp['title']);
$this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
$valuesTmp['author'] = mb_strcut($valuesTmp['author'], 0, 255, 'UTF-8');
$valuesTmp['author'] = mb_strcut($valuesTmp['author'], 0, 1024, 'UTF-8');
$valuesTmp['author'] = safe_utf8($valuesTmp['author']);
$this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
$valuesTmp['content'] = safe_utf8($valuesTmp['content']);
$this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
$valuesTmp['link'] = substr($valuesTmp['link'], 0, 16383);
$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
$valuesTmp['date'] = min($valuesTmp['date'], 2147483647);
$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
if (empty($valuesTmp['lastSeen'])) {
$valuesTmp['lastSeen'] = time();
@ -162,7 +135,7 @@ SQL;
$valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
$this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
$this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
$valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 1023, 'UTF-8');
$valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 2048, 'UTF-8');
$valuesTmp['tags'] = safe_utf8($valuesTmp['tags']);
$this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
if (!isset($valuesTmp['attributes'])) {
@ -218,8 +191,7 @@ SQL;
return $result;
}
/** @var PDOStatement|null */
private $updateEntryPrepared = null;
private ?PDOStatement $updateEntryPrepared = null;
/** @param array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,'hash':string,
* 'is_read':bool|int|null,'is_favorite':bool|int|null,'id_feed':int,'tags':string,'attributes':array<string,mixed>} $valuesTmp */
@ -244,21 +216,20 @@ SQL;
$this->updateEntryPrepared = $this->pdo->prepare($sql) ?: null;
}
if ($this->updateEntryPrepared) {
$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 767);
$valuesTmp['guid'] = safe_ascii($valuesTmp['guid']);
$this->updateEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
$valuesTmp['title'] = mb_strcut($valuesTmp['title'], 0, 255, 'UTF-8');
$valuesTmp['title'] = mb_strcut($valuesTmp['title'], 0, 8192, 'UTF-8');
$valuesTmp['title'] = safe_utf8($valuesTmp['title']);
$this->updateEntryPrepared->bindParam(':title', $valuesTmp['title']);
$valuesTmp['author'] = mb_strcut($valuesTmp['author'], 0, 255, 'UTF-8');
$valuesTmp['author'] = mb_strcut($valuesTmp['author'], 0, 1024, 'UTF-8');
$valuesTmp['author'] = safe_utf8($valuesTmp['author']);
$this->updateEntryPrepared->bindParam(':author', $valuesTmp['author']);
$valuesTmp['content'] = safe_utf8($valuesTmp['content']);
$this->updateEntryPrepared->bindParam(':content', $valuesTmp['content']);
$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
$valuesTmp['link'] = substr($valuesTmp['link'], 0, 16383);
$valuesTmp['link'] = safe_ascii($valuesTmp['link']);
$this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']);
$valuesTmp['date'] = min($valuesTmp['date'], 2147483647);
$this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
$this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
if ($valuesTmp['is_read'] === null) {
@ -272,7 +243,7 @@ SQL;
$this->updateEntryPrepared->bindValue(':is_favorite', $valuesTmp['is_favorite'] ? 1 : 0, PDO::PARAM_INT);
}
$this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
$valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 1023, 'UTF-8');
$valuesTmp['tags'] = mb_strcut($valuesTmp['tags'], 0, 2048, 'UTF-8');
$valuesTmp['tags'] = safe_utf8($valuesTmp['tags']);
$this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
if (!isset($valuesTmp['attributes'])) {
@ -302,6 +273,48 @@ SQL;
}
}
/**
* Count the number of new entries in the temporary table (which have not yet been committed), grouped by read / unread.
* @return array{'all':int,'unread':int,'read':int}
*/
public function countNewEntries(): array {
$sql = <<<'SQL'
SELECT is_read, COUNT(id) AS nb_entries FROM `_entrytmp`
GROUP BY is_read
SQL;
$lines = $this->fetchAssoc($sql) ?? [];
$nbRead = 0;
$nbUnread = 0;
foreach ($lines as $line) {
if (empty($line['is_read'])) {
$nbUnread = (int)($line['nb_entries'] ?? 0);
} else {
$nbRead = (int)($line['nb_entries'] ?? 0);
}
}
return ['all' => $nbRead + $nbUnread, 'unread' => $nbUnread, 'read' => $nbRead];
}
/**
* Count the number of new unread entries in the temporary table (which have not yet been committed), grouped by feed ID.
* @return array<int,int>
*/
public function newUnreadEntriesPerFeed(): array {
$sql = <<<'SQL'
SELECT id_feed, COUNT(id) AS nb_entries FROM `_entrytmp`
WHERE is_read = 0
GROUP BY id_feed
SQL;
$lines = $this->fetchAssoc($sql) ?? [];
$result = [];
foreach ($lines as $line) {
if (!empty($line['id_feed'])) {
$result[(int)$line['id_feed']] = (int)($line['nb_entries'] ?? 0);
}
}
return $result;
}
/**
* Toggle favorite marker on one or more article
*
@ -324,7 +337,7 @@ SQL;
$affected = 0;
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($idsChunks as $idsChunk) {
$affected += $this->markFavorite($idsChunk, $is_favorite);
$affected += ($this->markFavorite($idsChunk, $is_favorite) ?: 0);
}
return $affected;
}
@ -347,31 +360,29 @@ SQL;
* Update the unread article cache held on every feed details.
* Depending on the parameters, it updates the cache on one feed, on all
* feeds from one category or on all feeds.
*
* @todo It can use the query builder refactoring to build that query
*/
protected function updateCacheUnreads(?int $catId = null, ?int $feedId = null): bool {
$sql = <<<'SQL'
UPDATE `_feed` f LEFT OUTER JOIN (
SELECT e.id_feed, COUNT(*) AS nbUnreads
FROM `_entry` e
WHERE e.is_read = 0
GROUP BY e.id_feed
) x ON x.id_feed = f.id
SET f.`cache_nbUnreads` = COALESCE(x.nbUnreads, 0)
// Help MySQL/MariaDB's optimizer with the query plan:
$useIndex = $this->pdo->dbType() === 'mysql' ? 'USE INDEX (entry_feed_read_index)' : '';
$sql = <<<SQL
UPDATE `_feed`
SET `cache_nbUnreads`=(
SELECT COUNT(*) AS nbUnreads FROM `_entry` e {$useIndex}
WHERE e.id_feed=`_feed`.id AND e.is_read=0)
SQL;
$hasWhere = false;
$values = [];
if ($feedId != null) {
$sql .= ' WHERE';
$hasWhere = true;
$sql .= ' f.id=?';
$sql .= ' id=?';
$values[] = $feedId;
}
if ($catId != null) {
$sql .= $hasWhere ? ' AND' : ' WHERE';
$hasWhere = true;
$sql .= ' f.category=?';
$sql .= ' category=?';
$values[] = $catId;
}
$stm = $this->pdo->prepare($sql);
@ -388,11 +399,6 @@ SQL;
* Toggle the read marker on one or more article.
* Then the cache is updated.
*
* @todo change the way the query is build because it seems there is
* unnecessary code in here. For instance, the part with the str_repeat.
* @todo remove code duplication. It seems the code is basically the
* same if it is an array or not.
*
* @param string|array<string> $ids
* @param bool $is_read
* @return int|false affected rows
@ -403,7 +409,7 @@ SQL;
if (count($ids) < 6) { //Speed heuristics
$affected = 0;
foreach ($ids as $id) {
$affected += $this->markRead($id, $is_read);
$affected += ($this->markRead($id, $is_read) ?: 0);
}
return $affected;
} elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
@ -411,7 +417,7 @@ SQL;
$affected = 0;
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($idsChunks as $idsChunk) {
$affected += $this->markRead($idsChunk, $is_read);
$affected += ($this->markRead($idsChunk, $is_read) ?: 0);
}
return $affected;
}
@ -459,34 +465,36 @@ SQL;
*
* If $idMax equals 0, a deprecated debug message is logged
*
* @todo refactor this method along with markReadCat and markReadFeed
* since they are all doing the same thing. I think we need to build a
* tool to generate the query instead of having queries all over the
* place. It will be reused also for the filtering making every thing
* separated.
*
* @param string $idMax fail safe article ID
* @return int|false affected rows
*/
public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, ?int $priorityMin = null, ?int $prioritMax = null,
?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
FreshRSS_UserDAO::touch();
if ($idMax == 0) {
if ($idMax == '0') {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
}
$sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id '
. 'SET e.is_read=? '
. 'WHERE e.is_read <> ? AND e.id <= ?';
if ($onlyFavorites) {
$sql .= ' AND e.is_favorite=1';
} elseif ($priorityMin >= 0) {
$sql .= ' AND f.priority > ' . intval($priorityMin);
}
$sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
$values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax];
if ($onlyFavorites) {
$sql .= ' AND is_favorite=1';
}
if ($priorityMin !== null || $prioritMax !== null) {
$sql .= ' AND id_feed IN (SELECT f.id FROM `_feed` f WHERE 1=1';
if ($priorityMin !== null) {
$sql .= ' AND f.priority >= ?';
$values[] = $priorityMin;
}
if ($prioritMax !== null) {
$sql .= ' AND f.priority < ?';
$values[] = $prioritMax;
}
$sql .= ')';
}
[$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state);
[$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->pdo->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@ -519,12 +527,15 @@ SQL;
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
}
$sql = 'UPDATE `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id '
. 'SET e.is_read=? '
. 'WHERE f.category=? AND e.is_read <> ? AND e.id <= ?';
$values = [$is_read ? 1 : 0, $id, $is_read ? 1 : 0, $idMax];
$sql = <<<'SQL'
UPDATE `_entry`
SET is_read = ?
WHERE is_read <> ? AND id <= ?
AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=?)
SQL;
$values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id];
[$searchValues, $search] = $this->sqlListEntriesWhere('e.', $filters, $state);
[$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->pdo->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@ -641,7 +652,7 @@ SQL;
/**
* Remember to call updateCachedValue($id_feed) or updateCachedValues() just after.
* @param array<string,int|bool|string> $options
* @param array<string,bool|int|string> $options
* @return int|false
*/
public function cleanOldEntries(int $id_feed, array $options = []) {
@ -704,17 +715,22 @@ SQL;
}
/** @return Traversable<array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
* 'hash':string,'is_read':?bool,'is_favorite':?bool,'id_feed':int,'tags':string,'attributes':array<string,mixed>}> */
public function selectAll(): Traversable {
* 'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string}> */
public function selectAll(?int $limit = null): Traversable {
$content = static::isCompressed() ? 'UNCOMPRESS(content_bin) AS content' : 'content';
$hash = static::sqlHexEncode('hash');
$sql = <<<SQL
SELECT id, guid, title, author, {$content}, link, date, `lastSeen`, {$hash} AS hash, is_read, is_favorite, id_feed, tags, attributes
FROM `_entry`
SQL;
if (is_int($limit) && $limit >= 0) {
$sql .= ' ORDER BY id DESC LIMIT ' . $limit;
}
$stm = $this->pdo->query($sql);
if ($stm != false) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':string,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,'lastSeen':int,
* 'hash':string,'is_read':bool,'is_favorite':bool,'id_feed':int,'tags':string,'attributes':?string} $row */
yield $row;
}
} else {
@ -736,7 +752,7 @@ FROM `_entry` WHERE id_feed=:id_feed AND guid=:guid
SQL;
$res = $this->fetchAssoc($sql, [':id_feed' => $id_feed, ':guid' => $guid]);
/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string}> $res */
* 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
@ -749,7 +765,7 @@ FROM `_entry` WHERE id=:id
SQL;
$res = $this->fetchAssoc($sql, [':id' => $id]);
/** @var array<array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string}> $res */
* 'is_read':int,'is_favorite':int,'tags':string,'attributes':?string}> $res */
return isset($res[0]) ? FreshRSS_Entry::fromArray($res[0]) : null;
}
@ -788,7 +804,7 @@ SQL;
}
// Searches are combined by OR and are not recursive
$sub_search = '';
if ($filter->getEntryIds()) {
if ($filter->getEntryIds() !== null) {
$sub_search .= 'AND ' . $alias . 'id IN (';
foreach ($filter->getEntryIds() as $entry_id) {
$sub_search .= '?,';
@ -797,7 +813,7 @@ SQL;
$sub_search = rtrim($sub_search, ',');
$sub_search .= ') ';
}
if ($filter->getNotEntryIds()) {
if ($filter->getNotEntryIds() !== null) {
$sub_search .= 'AND ' . $alias . 'id NOT IN (';
foreach ($filter->getNotEntryIds() as $entry_id) {
$sub_search .= '?,';
@ -807,56 +823,56 @@ SQL;
$sub_search .= ') ';
}
if ($filter->getMinDate()) {
if ($filter->getMinDate() !== null) {
$sub_search .= 'AND ' . $alias . 'id >= ? ';
$values[] = "{$filter->getMinDate()}000000";
}
if ($filter->getMaxDate()) {
if ($filter->getMaxDate() !== null) {
$sub_search .= 'AND ' . $alias . 'id <= ? ';
$values[] = "{$filter->getMaxDate()}000000";
}
if ($filter->getMinPubdate()) {
if ($filter->getMinPubdate() !== null) {
$sub_search .= 'AND ' . $alias . 'date >= ? ';
$values[] = $filter->getMinPubdate();
}
if ($filter->getMaxPubdate()) {
if ($filter->getMaxPubdate() !== null) {
$sub_search .= 'AND ' . $alias . 'date <= ? ';
$values[] = $filter->getMaxPubdate();
}
//Negation of date intervals must be combined by OR
if ($filter->getNotMinDate() || $filter->getNotMaxDate()) {
if ($filter->getNotMinDate() !== null || $filter->getNotMaxDate() !== null) {
$sub_search .= 'AND (';
if ($filter->getNotMinDate()) {
if ($filter->getNotMinDate() !== null) {
$sub_search .= $alias . 'id < ?';
$values[] = "{$filter->getNotMinDate()}000000";
if ($filter->getNotMaxDate()) {
$sub_search .= ' OR ';
}
}
if ($filter->getNotMaxDate()) {
if ($filter->getNotMaxDate() !== null) {
$sub_search .= $alias . 'id > ?';
$values[] = "{$filter->getNotMaxDate()}000000";
}
$sub_search .= ') ';
}
if ($filter->getNotMinPubdate() || $filter->getNotMaxPubdate()) {
if ($filter->getNotMinPubdate() !== null || $filter->getNotMaxPubdate() !== null) {
$sub_search .= 'AND (';
if ($filter->getNotMinPubdate()) {
if ($filter->getNotMinPubdate() !== null) {
$sub_search .= $alias . 'date < ?';
$values[] = $filter->getNotMinPubdate();
if ($filter->getNotMaxPubdate()) {
$sub_search .= ' OR ';
}
}
if ($filter->getNotMaxPubdate()) {
if ($filter->getNotMaxPubdate() !== null) {
$sub_search .= $alias . 'date > ?';
$values[] = $filter->getNotMaxPubdate();
}
$sub_search .= ') ';
}
if ($filter->getFeedIds()) {
if ($filter->getFeedIds() !== null) {
$sub_search .= 'AND ' . $alias . 'id_feed IN (';
foreach ($filter->getFeedIds() as $feed_id) {
$sub_search .= '?,';
@ -865,7 +881,7 @@ SQL;
$sub_search = rtrim($sub_search, ',');
$sub_search .= ') ';
}
if ($filter->getNotFeedIds()) {
if ($filter->getNotFeedIds() !== null) {
$sub_search .= 'AND ' . $alias . 'id_feed NOT IN (';
foreach ($filter->getNotFeedIds() as $feed_id) {
$sub_search .= '?,';
@ -875,7 +891,7 @@ SQL;
$sub_search .= ') ';
}
if ($filter->getLabelIds()) {
if ($filter->getLabelIds() !== null) {
if ($filter->getLabelIds() === '*') {
$sub_search .= 'AND EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) ';
} else {
@ -888,7 +904,7 @@ SQL;
$sub_search .= ')) ';
}
}
if ($filter->getNotLabelIds()) {
if ($filter->getNotLabelIds() !== null) {
if ($filter->getNotLabelIds() === '*') {
$sub_search .= 'AND NOT EXISTS (SELECT et.id_tag FROM `_entrytag` et WHERE et.id_entry = ' . $alias . 'id) ';
} else {
@ -902,7 +918,7 @@ SQL;
}
}
if ($filter->getLabelNames()) {
if ($filter->getLabelNames() !== null) {
$sub_search .= 'AND ' . $alias . 'id IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (';
foreach ($filter->getLabelNames() as $label_name) {
$sub_search .= '?,';
@ -911,7 +927,7 @@ SQL;
$sub_search = rtrim($sub_search, ',');
$sub_search .= ')) ';
}
if ($filter->getNotLabelNames()) {
if ($filter->getNotLabelNames() !== null) {
$sub_search .= 'AND ' . $alias . 'id NOT IN (SELECT et.id_entry FROM `_entrytag` et, `_tag` t WHERE et.id_tag = t.id AND t.name IN (';
foreach ($filter->getNotLabelNames() as $label_name) {
$sub_search .= '?,';
@ -921,57 +937,57 @@ SQL;
$sub_search .= ')) ';
}
if ($filter->getAuthor()) {
if ($filter->getAuthor() !== null) {
foreach ($filter->getAuthor() as $author) {
$sub_search .= 'AND ' . $alias . 'author LIKE ? ';
$values[] = "%{$author}%";
}
}
if ($filter->getIntitle()) {
if ($filter->getIntitle() !== null) {
foreach ($filter->getIntitle() as $title) {
$sub_search .= 'AND ' . $alias . 'title LIKE ? ';
$values[] = "%{$title}%";
}
}
if ($filter->getTags()) {
if ($filter->getTags() !== null) {
foreach ($filter->getTags() as $tag) {
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
$values[] = "%{$tag} #%";
}
}
if ($filter->getInurl()) {
if ($filter->getInurl() !== null) {
foreach ($filter->getInurl() as $url) {
$sub_search .= 'AND ' . $alias . 'link LIKE ? ';
$values[] = "%{$url}%";
}
}
if ($filter->getNotAuthor()) {
if ($filter->getNotAuthor() !== null) {
foreach ($filter->getNotAuthor() as $author) {
$sub_search .= 'AND ' . $alias . 'author NOT LIKE ? ';
$values[] = "%{$author}%";
}
}
if ($filter->getNotIntitle()) {
if ($filter->getNotIntitle() !== null) {
foreach ($filter->getNotIntitle() as $title) {
$sub_search .= 'AND ' . $alias . 'title NOT LIKE ? ';
$values[] = "%{$title}%";
}
}
if ($filter->getNotTags()) {
if ($filter->getNotTags() !== null) {
foreach ($filter->getNotTags() as $tag) {
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
$values[] = "%{$tag} #%";
}
}
if ($filter->getNotInurl()) {
if ($filter->getNotInurl() !== null) {
foreach ($filter->getNotInurl() as $url) {
$sub_search .= 'AND ' . $alias . 'link NOT LIKE ? ';
$values[] = "%{$url}%";
}
}
if ($filter->getSearch()) {
if ($filter->getSearch() !== null) {
foreach ($filter->getSearch() as $search_value) {
if (static::isCompressed()) { // MySQL-only
$sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) LIKE ? ';
@ -983,7 +999,7 @@ SQL;
}
}
}
if ($filter->getNotSearch()) {
if ($filter->getNotSearch() !== null) {
foreach ($filter->getNotSearch() as $search_value) {
if (static::isCompressed()) { // MySQL-only
$sub_search .= 'AND CONCAT(' . $alias . 'title, UNCOMPRESS(' . $alias . 'content_bin)) NOT LIKE ? ';
@ -1013,10 +1029,11 @@ SQL;
/**
* @param 'ASC'|'DESC' $order
* @return array{0:array<int|string>,1:string}
* @throws FreshRSS_EntriesGetter_Exception
*/
protected function sqlListEntriesWhere(string $alias = '', ?FreshRSS_BooleanSearch $filters = null,
int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', string $firstId = '', int $date_min = 0) {
string $order = 'DESC', string $firstId = '', int $date_min = 0): array {
$search = ' ';
$values = [];
if ($state & FreshRSS_Entry::STATE_NOT_READ) {
@ -1061,14 +1078,15 @@ SQL;
}
/**
* @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
* @phpstan-param 'a'|'A'|'i'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
* @param int $id category/feed/tag ID
* @param 'ASC'|'DESC' $order
* @return array{0:array<int|string>,1:string}
* @throws FreshRSS_EntriesGetter_Exception
*/
private function sqlListWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0) {
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0): array {
if (!$state) {
$state = FreshRSS_Entry::STATE_ALL;
}
@ -1076,20 +1094,23 @@ SQL;
$values = [];
switch ($type) {
case 'a': //All PRIORITY_MAIN_STREAM
$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_MAIN_STREAM . ' ';
break;
case 'A': //All except PRIORITY_ARCHIVED
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_ARCHIVED . ' ';
break;
case 'i': //Priority important feeds
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_IMPORTANT . ' ';
break;
case 's': //Starred. Deprecated: use $state instead
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'f.priority > ' . FreshRSS_Feed::PRIORITY_ARCHIVED . ' ';
$where .= 'AND e.is_favorite=1 ';
break;
case 'S': //Starred
$where .= 'e.is_favorite=1 ';
break;
case 'c': //Category
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_CATEGORY . ' ';
$where .= 'AND f.category=? ';
$values[] = $id;
break;
@ -1121,19 +1142,22 @@ SQL;
. 'WHERE ' . $where
. $search
. 'ORDER BY e.id ' . $order
. ($limit > 0 ? ' LIMIT ' . intval($limit) : '')]; //TODO: See http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
. ($limit > 0 ? ' LIMIT ' . $limit : '') // http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
. ($offset > 0 ? ' OFFSET ' . $offset : '')
];
}
/**
* @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
* @phpstan-param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST' $type
* @param 'ASC'|'DESC' $order
* @param int $id category/feed/tag ID
* @return PDOStatement|false
* @throws FreshRSS_EntriesGetter_Exception
*/
private function listWhereRaw(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null,
int $date_min = 0) {
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($order !== 'DESC' && $order !== 'ASC') {
$order = 'DESC';
@ -1152,7 +1176,7 @@ SQL;
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
return $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
@ -1160,20 +1184,23 @@ SQL;
}
/**
* @phpstan-param 'a'|'A'|'s'|'S'|'c'|'f'|'t'|'T'|'ST' $type
* @phpstan-param 'a'|'A'|'s'|'S'|'i'|'c'|'f'|'t'|'T'|'ST' $type
* @param int $id category/feed/tag ID
* @param 'ASC'|'DESC' $order
* @return Traversable<FreshRSS_Entry>
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '',
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '',
?FreshRSS_BooleanSearch $filters = null, int $date_min = 0): Traversable {
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $firstId, $filters, $date_min);
$stm = $this->listWhereRaw($type, $id, $state, $order, $limit, $offset, $firstId, $filters, $date_min);
if ($stm) {
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */
yield FreshRSS_Entry::fromArray($row);
if (is_array($row)) {
/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:?string} $row */
yield FreshRSS_Entry::fromArray($row);
}
}
}
}
@ -1214,9 +1241,11 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes'?:string} $row */
yield FreshRSS_Entry::fromArray($row);
if (is_array($row)) {
/** @var array{'id':string,'id_feed':int,'guid':string,'title':string,'author':string,'content':string,'link':string,'date':int,
* 'hash':string,'is_read':int,'is_favorite':int,'tags':string,'attributes':?string} $row */
yield FreshRSS_Entry::fromArray($row);
}
}
}
@ -1225,11 +1254,12 @@ SQL;
* @param int $id category/feed/tag ID
* @param 'ASC'|'DESC' $order
* @return array<numeric-string>|null
* @throws FreshRSS_EntriesGetter_Exception
*/
public function listIdsWhere(string $type = 'a', int $id = 0, int $state = FreshRSS_Entry::STATE_ALL,
string $order = 'DESC', int $limit = 1, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
string $order = 'DESC', int $limit = 1, int $offset = 0, string $firstId = '', ?FreshRSS_BooleanSearch $filters = null): ?array {
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
[$values, $sql] = $this->sqlListWhere($type, $id, $state, $order, $limit, $offset, $firstId, $filters);
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values) && ($res = $stm->fetchAll(PDO::FETCH_COLUMN, 0)) !== false) {
/** @var array<numeric-string> $res */
@ -1291,7 +1321,7 @@ SQL;
$affected = 0;
$guidsChunks = array_chunk($guids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
foreach ($guidsChunks as $guidsChunk) {
$affected += $this->updateLastSeen($id_feed, $guidsChunk, $mtime);
$affected += ($this->updateLastSeen($id_feed, $guidsChunk, $mtime) ?: 0);
}
return $affected;
}
@ -1402,20 +1432,20 @@ SELECT c FROM (
FROM `_entry` AS e1
JOIN `_feed` AS f1 ON e1.id_feed = f1.id
WHERE e1.is_favorite = 1
AND f1.priority >= :priority_normal1
AND f1.priority >= :priority1
UNION
SELECT COUNT(e2.id) AS c, 2 AS o
FROM `_entry` AS e2
JOIN `_feed` AS f2 ON e2.id_feed = f2.id
WHERE e2.is_favorite = 1
AND e2.is_read = 0 AND f2.priority >= :priority_normal2
AND e2.is_read = 0 AND f2.priority >= :priority2
) u
ORDER BY o
SQL;
//Binding a value more than once is not standard and does not work with native prepared statements (e.g. MySQL) https://bugs.php.net/bug.php?id=40417
$res = $this->fetchColumn($sql, 0, [
':priority_normal1' => FreshRSS_Feed::PRIORITY_NORMAL,
':priority_normal2' => FreshRSS_Feed::PRIORITY_NORMAL,
':priority1' => FreshRSS_Feed::PRIORITY_CATEGORY,
':priority2' => FreshRSS_Feed::PRIORITY_CATEGORY,
]);
if ($res === null) {
return ['all' => -1, 'unread' => -1, 'read' => -1];

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
@ -18,25 +19,17 @@ class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
return rtrim($sql, ' ;') . ' ON CONFLICT DO NOTHING';
}
/** @param array<string> $errorInfo */
/** @param array<string|int> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
foreach (['attributes'] as $column) {
if (stripos($errorLines[0], $column) !== false) {
return $this->addColumn($column);
}
}
}
if ($errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
if (stripos($errorInfo[2], 'tag') !== false) {
$tagDAO = FreshRSS_Factory::createTagDao();
return $tagDAO->createTagTable(); //v1.12.0
} elseif (stripos($errorInfo[2], 'entrytmp') !== false) {
return $this->createEntryTempTable(); //v1.7.0
}
}
}
return false;
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
@ -22,7 +23,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
return str_replace('INSERT INTO ', 'INSERT OR IGNORE INTO ', $sql);
}
/** @param array<string> $errorInfo */
/** @param array<string|int> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if ($tableInfo = $this->pdo->query("PRAGMA table_info('entry')")) {
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1) ?: [];
@ -32,19 +33,6 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
}
}
}
if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
$showCreate = $tableInfo->fetchColumn();
if (is_string($showCreate) && stripos($showCreate, 'tag') === false) {
$tagDAO = FreshRSS_Factory::createTagDao();
return $tagDAO->createTagTable(); //v1.12.0
}
}
if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='entrytmp'")) {
$showCreate = $tableInfo->fetchColumn();
if (is_string($showCreate) && stripos($showCreate, 'entrytmp') === false) {
return $this->createEntryTempTable(); //v1.7.0
}
}
return false;
}
@ -78,46 +66,10 @@ SQL;
return $result;
}
protected function updateCacheUnreads(?int $catId = null, ?int $feedId = null): bool {
$sql = <<<'SQL'
UPDATE `_feed`
SET `cache_nbUnreads`=(
SELECT COUNT(*) AS nbUnreads FROM `_entry` e
WHERE e.id_feed=`_feed`.id AND e.is_read=0)
SQL;
$hasWhere = false;
$values = [];
if ($feedId != null) {
$sql .= ' WHERE';
$hasWhere = true;
$sql .= ' id=?';
$values[] = $feedId;
}
if ($catId != null) {
$sql .= $hasWhere ? ' AND' : ' WHERE';
$hasWhere = true;
$sql .= ' category=?';
$values[] = $catId;
}
$stm = $this->pdo->prepare($sql);
if ($stm !== false && $stm->execute($values)) {
return true;
} else {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
}
/**
* Toggle the read marker on one or more article.
* Then the cache is updated.
*
* @todo change the way the query is build because it seems there is
* unnecessary code in here. For instance, the part with the str_repeat.
* @todo remove code duplication. It seems the code is basically the
* same if it is an array or not.
*
* @param string|array<string> $ids
* @param bool $is_read
* @return int|false affected rows
@ -128,7 +80,7 @@ SQL;
//if (true) { //Speed heuristics //TODO: Not implemented yet for SQLite (so always call IDs one by one)
$affected = 0;
foreach ($ids as $id) {
$affected += $this->markRead($id, $is_read);
$affected += ($this->markRead($id, $is_read) ?: 0);
}
return $affected;
//}
@ -161,97 +113,6 @@ SQL;
}
}
/**
* Mark all entries as read depending on parameters.
* If $onlyFavorites is true, it is used when the user mark as read in
* the favorite pseudo-category.
* If $priorityMin is greater than 0, it is used when the user mark as
* read in the main feed pseudo-category.
* Then the cache is updated.
*
* If $idMax equals 0, a deprecated debug message is logged
*
* @todo refactor this method along with markReadCat and markReadFeed
* since they are all doing the same thing. I think we need to build a
* tool to generate the query instead of having queries all over the
* place. It will be reused also for the filtering making every thing
* separated.
*
* @param string $idMax fail safe article ID
* @param bool $onlyFavorites
* @param int $priorityMin
* @return int|false affected rows
*/
public function markReadEntries(string $idMax = '0', bool $onlyFavorites = false, int $priorityMin = 0,
?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
FreshRSS_UserDAO::touch();
if ($idMax == '0') {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadEntries(0) is deprecated!');
}
$sql = 'UPDATE `_entry` SET is_read = ? WHERE is_read <> ? AND id <= ?';
if ($onlyFavorites) {
$sql .= ' AND is_favorite=1';
} elseif ($priorityMin >= 0) {
$sql .= ' AND id_feed IN (SELECT f.id FROM `_feed` f WHERE f.priority > ' . intval($priorityMin) . ')';
}
$values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax];
[$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->pdo->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads(null, null))) {
return false;
}
return $affected;
}
/**
* Mark all the articles in a category as read.
* There is a fail safe to prevent to mark as read articles that are
* loaded during the mark as read action. Then the cache is updated.
*
* If $idMax equals 0, a deprecated debug message is logged
*
* @param int $id category ID
* @param string $idMax fail safe article ID
* @return int|false affected rows
*/
public function markReadCat(int $id, string $idMax = '0', ?FreshRSS_BooleanSearch $filters = null, int $state = 0, bool $is_read = true) {
FreshRSS_UserDAO::touch();
if ($idMax == '0') {
$idMax = time() . '000000';
Minz_Log::debug('Calling markReadCat(0) is deprecated!');
}
$sql = 'UPDATE `_entry` '
. 'SET is_read = ? '
. 'WHERE is_read <> ? AND id <= ? AND '
. 'id_feed IN (SELECT f.id FROM `_feed` f WHERE f.category=?)';
$values = [$is_read ? 1 : 0, $is_read ? 1 : 0, $idMax, $id];
[$searchValues, $search] = $this->sqlListEntriesWhere('', $filters, $state);
$stm = $this->pdo->prepare($sql . $search);
if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
$affected = $stm->rowCount();
if (($affected > 0) && (!$this->updateCacheUnreads($id, null))) {
return false;
}
return $affected;
}
/**
* Mark all the articles in a tag as read.
* @param int $id tag ID, or empty for targeting any tag

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_Factory {
@ -13,7 +14,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createCategoryDao(?string $username = null): FreshRSS_CategoryDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_CategoryDAOSQLite($username);
default:
@ -25,7 +26,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createFeedDao(?string $username = null): FreshRSS_FeedDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_FeedDAOSQLite($username);
default:
@ -37,7 +38,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createEntryDao(?string $username = null): FreshRSS_EntryDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_EntryDAOSQLite($username);
case 'pgsql':
@ -51,7 +52,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createTagDao(?string $username = null): FreshRSS_TagDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_TagDAOSQLite($username);
case 'pgsql':
@ -65,7 +66,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createStatsDAO(?string $username = null): FreshRSS_StatsDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_StatsDAOSQLite($username);
case 'pgsql':
@ -79,7 +80,7 @@ class FreshRSS_Factory {
* @throws Minz_ConfigurationNamespaceException|Minz_PDOConnectionException
*/
public static function createDatabaseDAO(?string $username = null): FreshRSS_DatabaseDAO {
switch (FreshRSS_Context::$system_conf->db['type'] ?? '') {
switch (FreshRSS_Context::systemConf()->db['type'] ?? '') {
case 'sqlite':
return new FreshRSS_DatabaseDAOSQLite($username);
case 'pgsql':

View File

@ -1,6 +1,8 @@
<?php
declare(strict_types=1);
class FreshRSS_Feed extends Minz_Model {
use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait;
/**
* Normal RSS or Atom feed
@ -28,8 +30,12 @@ class FreshRSS_Feed extends Minz_Model {
*/
public const KIND_JSON_XPATH = 20;
public const KIND_JSONFEED = 25;
public const KIND_JSON_DOTPATH = 30;
public const PRIORITY_IMPORTANT = 20;
public const PRIORITY_MAIN_STREAM = 10;
public const PRIORITY_NORMAL = 0;
public const PRIORITY_CATEGORY = 0;
public const PRIORITY_ARCHIVED = -10;
public const TTL_DEFAULT = 0;
@ -37,55 +43,31 @@ class FreshRSS_Feed extends Minz_Model {
public const ARCHIVING_RETENTION_COUNT_LIMIT = 10000;
public const ARCHIVING_RETENTION_PERIOD = 'P3M';
/** @var int */
private $id = 0;
/** @var string */
private $url = '';
/** @var int */
private $kind = 0;
/** @var int */
private $categoryId = 1;
/** @var FreshRSS_Category|null */
private $category;
/** @var int */
private $nbEntries = -1;
/** @var int */
private $nbNotRead = -1;
/** @var int */
private $nbPendingNotRead = 0;
/** @var string */
private $name = '';
/** @var string */
private $website = '';
/** @var string */
private $description = '';
/** @var int */
private $lastUpdate = 0;
/** @var int */
private $priority = self::PRIORITY_MAIN_STREAM;
/** @var string */
private $pathEntries = '';
/** @var string */
private $httpAuth = '';
/** @var bool */
private $error = false;
/** @var int */
private $ttl = self::TTL_DEFAULT;
/** @var array<string,mixed> */
private $attributes = [];
/** @var bool */
private $mute = false;
/** @var string */
private $hash = '';
/** @var string */
private $lockPath = '';
/** @var string */
private $hubUrl = '';
/** @var string */
private $selfUrl = '';
/** @var array<FreshRSS_FilterAction> $filterActions */
private $filterActions = null;
private int $id = 0;
private string $url = '';
private int $kind = 0;
private int $categoryId = 0;
private ?FreshRSS_Category $category;
private int $nbEntries = -1;
private int $nbNotRead = -1;
private string $name = '';
private string $website = '';
private string $description = '';
private int $lastUpdate = 0;
private int $priority = self::PRIORITY_MAIN_STREAM;
private string $pathEntries = '';
private string $httpAuth = '';
private bool $error = false;
private int $ttl = self::TTL_DEFAULT;
private bool $mute = false;
private string $hash = '';
private string $lockPath = '';
private string $hubUrl = '';
private string $selfUrl = '';
/**
* @throws FreshRSS_BadUrl_Exception
*/
public function __construct(string $url, bool $validate = true) {
if ($validate) {
$this->_url($url);
@ -94,7 +76,7 @@ class FreshRSS_Feed extends Minz_Model {
}
}
public static function example(): FreshRSS_Feed {
public static function default(): FreshRSS_Feed {
$f = new FreshRSS_Feed('http://example.net/', false);
$f->faviconPrepare();
return $f;
@ -106,7 +88,7 @@ class FreshRSS_Feed extends Minz_Model {
public function hash(): string {
if ($this->hash == '') {
$salt = FreshRSS_Context::$system_conf->salt;
$salt = FreshRSS_Context::systemConf()->salt;
$this->hash = hash('crc32b', $salt . $this->url);
}
return $this->hash;
@ -126,7 +108,7 @@ class FreshRSS_Feed extends Minz_Model {
}
public function category(): ?FreshRSS_Category {
if ($this->category === null) {
if ($this->category === null && $this->categoryId > 0) {
$catDAO = FreshRSS_Factory::createCategoryDao();
$this->category = $catDAO->searchById($this->categoryId);
}
@ -134,6 +116,9 @@ class FreshRSS_Feed extends Minz_Model {
}
public function categoryId(): int {
if ($this->category !== null) {
return $this->category->id() ?: $this->categoryId;
}
return $this->categoryId;
}
@ -147,7 +132,7 @@ class FreshRSS_Feed extends Minz_Model {
return $simplePie == null ? [] : iterator_to_array($this->loadEntries($simplePie));
}
public function name(bool $raw = false): string {
return $raw || $this->name != '' ? $this->name : preg_replace('%^https?://(www[.])?%i', '', $this->url);
return $raw || $this->name != '' ? $this->name : (preg_replace('%^https?://(www[.])?%i', '', $this->url) ?? '');
}
/** @return string HTML-encoded URL of the Web site of the feed */
public function website(): string {
@ -200,23 +185,14 @@ class FreshRSS_Feed extends Minz_Model {
if ($raw) {
$ttl = $this->ttl;
if ($this->mute && FreshRSS_Feed::TTL_DEFAULT === $ttl) {
$ttl = FreshRSS_Context::$user_conf ? FreshRSS_Context::$user_conf->ttl_default : 3600;
$ttl = FreshRSS_Context::userConf()->ttl_default;
}
return $ttl * ($this->mute ? -1 : 1);
}
return $this->ttl;
}
/**
* @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>)
* @return array<string,mixed>|mixed|null
*/
public function attributes(string $key = '') {
if ($key === '') {
return $this->attributes;
} else {
return $this->attributes[$key] ?? null;
if ($this->mute && $this->ttl === FreshRSS_Context::userConf()->ttl_default) {
return FreshRSS_Feed::TTL_DEFAULT;
}
return $this->ttl;
}
public function mute(): bool {
@ -231,13 +207,13 @@ class FreshRSS_Feed extends Minz_Model {
return $this->nbEntries;
}
public function nbNotRead(bool $includePending = false): int {
public function nbNotRead(): int {
if ($this->nbNotRead < 0) {
$feedDAO = FreshRSS_Factory::createFeedDao();
$this->nbNotRead = $feedDAO->countNotRead($this->id());
}
return $this->nbNotRead + ($includePending ? $this->nbPendingNotRead : 0);
return $this->nbNotRead;
}
public function faviconPrepare(): void {
@ -278,6 +254,9 @@ class FreshRSS_Feed extends Minz_Model {
$this->id = $value;
}
/**
* @throws FreshRSS_BadUrl_Exception
*/
public function _url(string $value, bool $validate = true): void {
$this->hash = '';
$url = $value;
@ -346,22 +325,6 @@ class FreshRSS_Feed extends Minz_Model {
$this->mute = $value < self::TTL_DEFAULT;
}
/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
public function _attributes(string $key, $value): void {
if ($key == '') {
if (is_string($value)) {
$value = json_decode($value, true);
}
if (is_array($value)) {
$this->attributes = $value;
}
} elseif ($value === null) {
unset($this->attributes[$key]);
} else {
$this->attributes[$key] = $value;
}
}
public function _nbNotRead(int $value): void {
$this->nbNotRead = $value;
}
@ -369,9 +332,16 @@ class FreshRSS_Feed extends Minz_Model {
$this->nbEntries = $value;
}
/**
* @throws Minz_FileNotExistException
* @throws FreshRSS_Feed_Exception
*/
public function load(bool $loadDetails = false, bool $noCache = false): ?SimplePie {
if ($this->url != '') {
// @phpstan-ignore-next-line
/**
* @phpstan-ignore-next-line
* @throws Minz_FileNotExistException
*/
if (CACHE_PATH == '') {
throw new Minz_FileNotExistException(
'CACHE_PATH',
@ -380,7 +350,7 @@ class FreshRSS_Feed extends Minz_Model {
} else {
$url = htmlspecialchars_decode($this->url, ENT_QUOTES);
if ($this->httpAuth != '') {
$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url);
$url = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $url) ?? '';
}
$simplePie = customSimplePie($this->attributes());
if (substr($url, -11) === '#force_feed') {
@ -391,7 +361,7 @@ class FreshRSS_Feed extends Minz_Model {
if (!$loadDetails) { //Only activates auto-discovery when adding a new feed
$simplePie->set_autodiscovery_level(SIMPLEPIE_LOCATOR_NONE);
}
if ($this->attributes('clear_cache')) {
if ($this->attributeBoolean('clear_cache')) {
// Do not use `$simplePie->enable_cache(false);` as it would prevent caching in multiuser context
$this->clearCache();
}
@ -419,21 +389,26 @@ class FreshRSS_Feed extends Minz_Model {
if ($loadDetails) {
// si on a utilisé lauto-discover, notre url va avoir changé
$subscribe_url = $simplePie->subscribe_url(false);
$subscribe_url = $simplePie->subscribe_url(false) ?? '';
//HTML to HTML-PRE //ENT_COMPAT except '&'
$title = strtr(html_only_entity_decode($simplePie->get_title()), ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;']);
$this->_name($title == '' ? $this->url : $title);
$this->_website(html_only_entity_decode($simplePie->get_link()));
$this->_description(html_only_entity_decode($simplePie->get_description()));
if ($this->name(true) === '') {
//HTML to HTML-PRE //ENT_COMPAT except '&'
$title = strtr(html_only_entity_decode($simplePie->get_title()), ['<' => '&lt;', '>' => '&gt;', '"' => '&quot;']);
$this->_name($title == '' ? $this->url : $title);
}
if ($this->website() === '') {
$this->_website(html_only_entity_decode($simplePie->get_link()));
}
if ($this->description() === '') {
$this->_description(html_only_entity_decode($simplePie->get_description()));
}
} else {
//The case of HTTP 301 Moved Permanently
$subscribe_url = $simplePie->subscribe_url(true);
$subscribe_url = $simplePie->subscribe_url(true) ?? '';
}
$clean_url = SimplePie_Misc::url_remove_credentials($subscribe_url);
if ($subscribe_url !== null && $subscribe_url !== $url) {
if ($subscribe_url !== '' && $subscribe_url !== $url) {
$this->_url($clean_url);
}
@ -455,7 +430,7 @@ class FreshRSS_Feed extends Minz_Model {
$testGuids = [];
$guids = [];
$links = [];
$hadBadGuids = $this->attributes('hasBadGuids');
$hadBadGuids = $this->attributeBoolean('hasBadGuids');
$items = $simplePie->get_items();
if (empty($items)) {
@ -470,7 +445,10 @@ class FreshRSS_Feed extends Minz_Model {
$hasUniqueGuids &= empty($testGuids['_' . $guid]);
$testGuids['_' . $guid] = true;
$guids[] = $guid;
$links[] = $item->get_permalink();
$permalink = $item->get_permalink();
if ($permalink != null) {
$links[] = $permalink;
}
}
if ($hadBadGuids != !$hasUniqueGuids) {
@ -488,7 +466,7 @@ class FreshRSS_Feed extends Minz_Model {
/** @return Traversable<FreshRSS_Entry> */
public function loadEntries(SimplePie $simplePie): Traversable {
$hasBadGuids = $this->attributes('hasBadGuids');
$hasBadGuids = $this->attributeBoolean('hasBadGuids');
$items = $simplePie->get_items();
if (empty($items)) {
@ -596,7 +574,7 @@ class FreshRSS_Feed extends Minz_Model {
}
}
}
$authorNames = substr($authorNames, 0, -2);
$authorNames = substr($authorNames, 0, -2) ?: '';
$entry = new FreshRSS_Entry(
$this->id(),
@ -604,15 +582,15 @@ class FreshRSS_Feed extends Minz_Model {
$title == '' ? '' : $title,
$authorNames,
$content == '' ? '' : $content,
$link == '' ? '' : $link,
$link == null ? '' : $link,
$date ?: time()
);
$entry->_tags($tags);
$entry->_feed($this);
if (!empty($attributeThumbnail['url'])) {
$entry->_attributes('thumbnail', $attributeThumbnail);
$entry->_attribute('thumbnail', $attributeThumbnail);
}
$entry->_attributes('enclosures', $attributeEnclosures);
$entry->_attribute('enclosures', $attributeEnclosures);
$entry->hash(); //Must be computed before loading full content
$entry->loadCompleteContent(); // Optionally load full content for truncated feeds
@ -621,8 +599,74 @@ class FreshRSS_Feed extends Minz_Model {
}
/**
* @throws FreshRSS_Context_Exception
* Given a feed content generated from a FreshRSS_View
* returns a SimplePie initialized already with that content
* @param string $feedContent the content of the feed, typically generated via FreshRSS_View::renderToString()
*/
private function simplePieFromContent(string $feedContent): SimplePie {
$simplePie = customSimplePie();
$simplePie->set_raw_data($feedContent);
$simplePie->init();
return $simplePie;
}
/** @return array<string,string> */
private function dotPathsForStandardJsonFeed(): array {
return [
'feedTitle' => 'title',
'item' => 'items',
'itemTitle' => 'title',
'itemContent' => 'content_text',
'itemContentHTML' => 'content_html',
'itemUri' => 'url',
'itemTimestamp' => 'date_published',
'itemTimeFormat' => DateTimeInterface::RFC3339_EXTENDED,
'itemThumbnail' => 'image',
'itemCategories' => 'tags',
'itemUid' => 'id',
'itemAttachment' => 'attachments',
'itemAttachmentUrl' => 'url',
'itemAttachmentType' => 'mime_type',
'itemAttachmentLength' => 'size_in_bytes',
];
}
public function loadJson(): ?SimplePie {
if ($this->url == '') {
return null;
}
$feedSourceUrl = htmlspecialchars_decode($this->url, ENT_QUOTES);
if ($this->httpAuth != '') {
$feedSourceUrl = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $feedSourceUrl);
}
if ($feedSourceUrl == null) {
return null;
}
$cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind());
$httpAccept = 'json';
$json = httpGet($feedSourceUrl, $cachePath, $httpAccept, $this->attributes());
if (strlen($json) <= 0) {
return null;
}
//check if the content is actual JSON
$jf = json_decode($json, true);
if (json_last_error() !== JSON_ERROR_NONE) {
return null;
}
/** @var array<string,string> $json_dotpath */
$json_dotpath = $this->attributeArray('json_dotpath') ?? [];
$dotPaths = $this->kind() === FreshRSS_Feed::KIND_JSONFEED ? $this->dotPathsForStandardJsonFeed() : $json_dotpath;
$feedContent = FreshRSS_dotpath_Util::convertJsonToRss($jf, $feedSourceUrl, $dotPaths, $this->name());
if ($feedContent == null) {
return null;
}
return $this->simplePieFromContent($feedContent);
}
public function loadHtmlXpath(): ?SimplePie {
if ($this->url == '') {
return null;
@ -631,11 +675,14 @@ class FreshRSS_Feed extends Minz_Model {
if ($this->httpAuth != '') {
$feedSourceUrl = preg_replace('#((.+)://)(.+)#', '${1}' . $this->httpAuth . '@${3}', $feedSourceUrl);
}
if ($feedSourceUrl == null) {
return null;
}
// Same naming conventions than https://rss-bridge.github.io/rss-bridge/Bridge_API/XPathAbstract.html
// https://rss-bridge.github.io/rss-bridge/Bridge_API/BridgeAbstract.html#collectdata
/** @var array<string,string> $xPathSettings */
$xPathSettings = $this->attributes('xpath');
$xPathSettings = $this->attributeArray('xpath');
$xPathFeedTitle = $xPathSettings['feedTitle'] ?? '';
$xPathItem = $xPathSettings['item'] ?? '';
$xPathItemTitle = $xPathSettings['itemTitle'] ?? '';
@ -661,7 +708,8 @@ class FreshRSS_Feed extends Minz_Model {
$view = new FreshRSS_View();
$view->_path('index/rss.phtml');
$view->internal_rendering = true;
$view->rss_url = $feedSourceUrl;
$view->rss_url = htmlspecialchars($feedSourceUrl, ENT_COMPAT, 'UTF-8');
$view->html_url = $view->rss_url;
$view->entries = [];
try {
@ -723,8 +771,10 @@ class FreshRSS_Feed extends Minz_Model {
}
$item['thumbnail'] = $xPathItemThumbnail == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemThumbnail . ')', $node);
if ($xPathItemCategories != '') {
$itemCategories = @$xpath->query($xPathItemCategories, $node);
if ($itemCategories !== false) {
$itemCategories = @$xpath->evaluate($xPathItemCategories, $node);
if (is_string($itemCategories) && $itemCategories !== '') {
$item['tags'] = [$itemCategories];
} elseif ($itemCategories instanceof DOMNodeList && $itemCategories->length > 0) {
$item['tags'] = [];
/** @var DOMNode $itemCategory */
foreach ($itemCategories as $itemCategory) {
@ -755,35 +805,34 @@ class FreshRSS_Feed extends Minz_Model {
Minz_Log::warning($ex->getMessage());
return null;
}
$simplePie = customSimplePie();
$simplePie->set_raw_data($view->renderToString());
$simplePie->init();
return $simplePie;
return $this->simplePieFromContent($view->renderToString());
}
/**
* To keep track of some new potentially unread articles since last commit+fetch from database
*/
public function incPendingUnread(int $n = 1): void {
$this->nbPendingNotRead += $n;
}
/**
* Remember to call updateCachedValue($id_feed) or updateCachedValues() just after.
* @return int|false the number of lines affected, or false if not applicable
* @throws JsonException
* @return int|null The max number of unread articles to keep, or null if disabled.
*/
public function keepMaxUnread() {
$keepMaxUnread = $this->attributes('keep_max_n_unread');
$keepMaxUnread = $this->attributeInt('keep_max_n_unread');
if ($keepMaxUnread === null) {
$keepMaxUnread = FreshRSS_Context::$user_conf->mark_when['max_n_unread'];
$keepMaxUnread = FreshRSS_Context::userConf()->mark_when['max_n_unread'];
}
$keepMaxUnread = (int)$keepMaxUnread;
if ($keepMaxUnread > 0 && $this->nbNotRead(false) + $this->nbPendingNotRead > $keepMaxUnread) {
return FreshRSS_Factory::createFeedDao()->keepMaxUnread($this->id(), max(0, $keepMaxUnread - $this->nbPendingNotRead));
return is_int($keepMaxUnread) && $keepMaxUnread >= 0 ? $keepMaxUnread : null;
}
/**
* @return int|false The number of articles marked as read, of false if error
*/
public function markAsReadMaxUnread() {
$keepMaxUnread = $this->keepMaxUnread();
if ($keepMaxUnread === null) {
return false;
}
return false;
$feedDAO = FreshRSS_Factory::createFeedDao();
$affected = $feedDAO->markAsReadMaxUnread($this->id(), $keepMaxUnread);
if ($affected > 0) {
Minz_Log::debug(__METHOD__ . " $affected items [" . $this->url(false) . ']');
}
return $affected;
}
/**
@ -792,9 +841,9 @@ class FreshRSS_Feed extends Minz_Model {
* @return int|false the number of lines affected, or false if not applicable
*/
public function markAsReadUponGone(bool $upstreamIsEmpty, int $maxTimestamp = 0) {
$readUponGone = $this->attributes('read_upon_gone');
$readUponGone = $this->attributeBoolean('read_upon_gone');
if ($readUponGone === null) {
$readUponGone = FreshRSS_Context::$user_conf->mark_when['gone'];
$readUponGone = FreshRSS_Context::userConf()->mark_when['gone'];
}
if (!$readUponGone) {
return false;
@ -820,13 +869,15 @@ class FreshRSS_Feed extends Minz_Model {
* @return int|false
*/
public function cleanOldEntries() {
$archiving = $this->attributes('archiving');
if ($archiving == null) {
/** @var array<string,bool|int|string>|null $archiving */
$archiving = $this->attributeArray('archiving');
if ($archiving === null) {
$catDAO = FreshRSS_Factory::createCategoryDao();
$category = $catDAO->searchById($this->categoryId);
$archiving = $category == null ? null : $category->attributes('archiving');
if ($archiving == null) {
$archiving = FreshRSS_Context::$user_conf->archiving;
$archiving = $category === null ? null : $category->attributeArray('archiving');
/** @var array<string,bool|int|string>|null $archiving */
if ($archiving === null) {
$archiving = FreshRSS_Context::userConf()->archiving;
}
}
if (is_array($archiving)) {
@ -840,7 +891,10 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
/** @param array<string,mixed> $attributes */
/**
* @param array<string,mixed> $attributes
* @throws FreshRSS_Context_Exception
*/
public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
$simplePie = customSimplePie($attributes);
$filename = $simplePie->get_cache_filename($url);
@ -881,119 +935,6 @@ class FreshRSS_Feed extends Minz_Model {
return @unlink($this->lockPath);
}
/**
* @return array<FreshRSS_FilterAction>
*/
public function filterActions(): array {
if (empty($this->filterActions)) {
$this->filterActions = [];
$filters = $this->attributes('filters');
if (is_array($filters)) {
foreach ($filters as $filter) {
$filterAction = FreshRSS_FilterAction::fromJSON($filter);
if ($filterAction != null) {
$this->filterActions[] = $filterAction;
}
}
}
}
return $this->filterActions;
}
/**
* @param array<FreshRSS_FilterAction>|null $filterActions
*/
private function _filterActions(?array $filterActions): void {
$this->filterActions = $filterActions;
if (is_array($this->filterActions) && !empty($this->filterActions)) {
$this->_attributes('filters', array_map(static function (?FreshRSS_FilterAction $af) {
return $af == null ? null : $af->toJSON();
}, $this->filterActions));
} else {
$this->_attributes('filters', null);
}
}
/** @return array<FreshRSS_BooleanSearch> */
public function filtersAction(string $action): array {
$action = trim($action);
if ($action == '') {
return [];
}
$filters = [];
$filterActions = $this->filterActions();
for ($i = count($filterActions) - 1; $i >= 0; $i--) {
$filterAction = $filterActions[$i];
if ($filterAction != null && $filterAction->booleanSearch() != null &&
$filterAction->actions() != null && in_array($action, $filterAction->actions(), true)) {
$filters[] = $filterAction->booleanSearch();
}
}
return $filters;
}
/**
* @param array<string> $filters
*/
public function _filtersAction(string $action, array $filters): void {
$action = trim($action);
if ($action == '') {
return;
}
$filters = array_unique(array_map('trim', $filters));
$filterActions = $this->filterActions();
//Check existing filters
for ($i = count($filterActions) - 1; $i >= 0; $i--) {
$filterAction = $filterActions[$i];
if ($filterAction == null || !is_array($filterAction->actions()) ||
$filterAction->booleanSearch() == null || trim($filterAction->booleanSearch()->getRawInput()) == '') {
array_splice($filterActions, $i, 1);
continue;
}
$actions = $filterAction->actions();
//Remove existing rules with same action
for ($j = count($actions) - 1; $j >= 0; $j--) {
if ($actions[$j] === $action) {
array_splice($actions, $j, 1);
}
}
//Update existing filter with new action
for ($k = count($filters) - 1; $k >= 0; $k --) {
$filter = $filters[$k];
if ($filter === $filterAction->booleanSearch()->getRawInput()) {
$actions[] = $action;
array_splice($filters, $k, 1);
}
}
//Save result
if (empty($actions)) {
array_splice($filterActions, $i, 1);
} else {
$filterAction->_actions($actions);
}
}
//Add new filters
for ($k = count($filters) - 1; $k >= 0; $k --) {
$filter = $filters[$k];
if ($filter != '') {
$filterAction = FreshRSS_FilterAction::fromJSON([
'search' => $filter,
'actions' => [$action],
]);
if ($filterAction != null) {
$filterActions[] = $filterAction;
}
}
}
if (empty($filterActions)) {
$filterActions = null;
}
$this->_filterActions($filterActions);
}
//<WebSub>
public function pubSubHubbubEnabled(): bool {
@ -1001,7 +942,7 @@ class FreshRSS_Feed extends Minz_Model {
$hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json';
if ($hubFile = @file_get_contents($hubFilename)) {
$hubJson = json_decode($hubFile, true);
if ($hubJson && empty($hubJson['error']) &&
if (is_array($hubJson) && empty($hubJson['error']) &&
(empty($hubJson['lease_end']) || $hubJson['lease_end'] > time())) {
return true;
}
@ -1013,8 +954,8 @@ class FreshRSS_Feed extends Minz_Model {
$url = $this->selfUrl ?: $this->url;
$hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
$hubJson = $hubFile ? json_decode($hubFile, true) : [];
if (!isset($hubJson['error']) || $hubJson['error'] !== $error) {
$hubJson = is_string($hubFile) ? json_decode($hubFile, true) : null;
if (is_array($hubJson) && !isset($hubJson['error']) || $hubJson['error'] !== $error) {
$hubJson['error'] = $error;
file_put_contents($hubFilename, json_encode($hubJson));
Minz_Log::warning('Set error to ' . ($error ? 1 : 0) . ' for ' . $url, PSHB_LOG);
@ -1027,13 +968,13 @@ class FreshRSS_Feed extends Minz_Model {
*/
public function pubSubHubbubPrepare() {
$key = '';
if (Minz_Request::serverIsPublic(FreshRSS_Context::$system_conf->base_url) &&
if (Minz_Request::serverIsPublic(FreshRSS_Context::systemConf()->base_url) &&
$this->hubUrl && $this->selfUrl && @is_dir(PSHB_PATH)) {
$path = PSHB_PATH . '/feeds/' . sha1($this->selfUrl);
$hubFilename = $path . '/!hub.json';
if ($hubFile = @file_get_contents($hubFilename)) {
$hubJson = json_decode($hubFile, true);
if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key'])) {
$text = 'Invalid JSON for WebSub: ' . $this->url;
Minz_Log::warning($text);
Minz_Log::warning($text, PSHB_LOG);
@ -1052,7 +993,7 @@ class FreshRSS_Feed extends Minz_Model {
}
} else {
@mkdir($path, 0770, true);
$key = sha1($path . FreshRSS_Context::$system_conf->salt);
$key = sha1($path . FreshRSS_Context::systemConf()->salt);
$hubJson = [
'hub' => $this->hubUrl,
'key' => $key,
@ -1064,7 +1005,7 @@ class FreshRSS_Feed extends Minz_Model {
Minz_Log::debug($text);
Minz_Log::debug($text, PSHB_LOG);
}
$currentUser = Minz_User::name();
$currentUser = Minz_User::name() ?? '';
if (FreshRSS_user_Controller::checkUsername($currentUser) && !file_exists($path . '/' . $currentUser . '.txt')) {
touch($path . '/' . $currentUser . '.txt');
}
@ -1079,7 +1020,7 @@ class FreshRSS_Feed extends Minz_Model {
} else {
$url = $this->url; //Always use current URL during unsubscribe
}
if ($url && (Minz_Request::serverIsPublic(FreshRSS_Context::$system_conf->base_url) || !$state)) {
if ($url && (Minz_Request::serverIsPublic(FreshRSS_Context::systemConf()->base_url) || !$state)) {
$hubFilename = PSHB_PATH . '/feeds/' . sha1($url) . '/!hub.json';
$hubFile = @file_get_contents($hubFilename);
if ($hubFile === false) {
@ -1087,7 +1028,7 @@ class FreshRSS_Feed extends Minz_Model {
return false;
}
$hubJson = json_decode($hubFile, true);
if (!$hubJson || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) {
if (!is_array($hubJson) || empty($hubJson['key']) || !ctype_xdigit($hubJson['key']) || empty($hubJson['hub'])) {
Minz_Log::warning('Invalid JSON for WebSub: ' . $this->url);
return false;
}
@ -1107,7 +1048,8 @@ class FreshRSS_Feed extends Minz_Model {
CURLOPT_POSTFIELDS => http_build_query([
'hub.verify' => 'sync',
'hub.mode' => $state ? 'subscribe' : 'unsubscribe',
'hub.topic' => $url, 'hub.callback' => $callbackUrl,
'hub.topic' => $url,
'hub.callback' => $callbackUrl,
]),
CURLOPT_USERAGENT => FRESHRSS_USERAGENT,
CURLOPT_MAXREDIRS => 10,

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_FeedDAO extends Minz_ModelPdo {
@ -10,8 +11,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
try {
if ($name === 'kind') { //v1.20.0
return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN kind SMALLINT DEFAULT 0') !== false;
} elseif ($name === 'attributes') { //v1.11.0
return $this->pdo->exec('ALTER TABLE `_feed` ADD COLUMN attributes TEXT') !== false;
}
} catch (Exception $e) {
Minz_Log::error(__method__ . ' error: ' . $e->getMessage());
@ -19,12 +18,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
return false;
}
/** @param array<string> $errorInfo */
/** @param array<int|string> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_FIELD_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_COLUMN) {
$errorLines = explode("\n", $errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
foreach (['attributes', 'kind'] as $column) {
$errorLines = explode("\n", (string)$errorInfo[2], 2); // The relevant column name is on the first line, other lines are noise
foreach (['kind'] as $column) {
if (stripos($errorLines[0], $column) !== false) {
return $this->addColumn($column);
}
@ -38,7 +37,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
* @param array{'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string|array<string|mixed>} $valuesTmp
* @return int|false
* @throws JsonException
*/
public function addFeed(array $valuesTmp) {
$sql = 'INSERT INTO `_feed` (url, kind, category, name, website, description, `lastUpdate`, priority, `pathEntries`, `httpAuth`, error, ttl, attributes)
@ -60,10 +58,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
$valuesTmp['category'],
mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
$valuesTmp['website'],
sanitizeHTML($valuesTmp['description'], '', 1023),
sanitizeHTML($valuesTmp['description'], ''),
$valuesTmp['lastUpdate'],
isset($valuesTmp['priority']) ? (int)$valuesTmp['priority'] : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
mb_strcut($valuesTmp['pathEntries'], 0, 511, 'UTF-8'),
mb_strcut($valuesTmp['pathEntries'], 0, 4096, 'UTF-8'),
base64_encode($valuesTmp['httpAuth']),
isset($valuesTmp['error']) ? (int)$valuesTmp['error'] : 0,
isset($valuesTmp['ttl']) ? (int)$valuesTmp['ttl'] : FreshRSS_Feed::TTL_DEFAULT,
@ -119,7 +117,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
// Merge existing and import attributes
$existingAttributes = $feed_search->attributes();
$importAttributes = $feed->attributes();
$feed->_attributes('', array_replace_recursive($existingAttributes, $importAttributes));
$feed->_attributes(array_replace_recursive($existingAttributes, $importAttributes));
// Update some values of the existing feed using the import
$values = [
@ -191,11 +189,12 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
}
/**
* @param non-empty-string $key
* @param string|array<mixed>|bool|int|null $value
* @return int|false
*/
public function updateFeedAttribute(FreshRSS_Feed $feed, string $key, $value) {
$feed->_attributes($key, $value);
$feed->_attribute($key, $value);
return $this->updateFeed(
$feed->id(),
['attributes' => $feed->attributes()]
@ -237,6 +236,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo {
if ($newCat === null) {
$newCat = $catDAO->getDefault();
}
if ($newCat === null) {
return false;
}
$sql = 'UPDATE `_feed` SET category=? WHERE category=?';
$stm = $this->pdo->prepare($sql);
@ -306,6 +308,8 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'url':string,'kind':int,'category':int,'name':string,'website':string,'description':string,'lastUpdate':int,'priority'?:int,
* 'pathEntries'?:string,'httpAuth':string,'error':int|bool,'ttl'?:int,'attributes'?:string} $row */
yield $row;
}
}
@ -318,7 +322,7 @@ SQL;
}
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
$feeds = self::daoToFeed($res);
$feeds = self::daoToFeeds($res);
return $feeds[$id] ?? null;
}
@ -327,7 +331,7 @@ SQL;
$res = $this->fetchAssoc($sql, [':url' => $url]);
/** @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res */
return empty($res[0]) ? null : (current(self::daoToFeed($res)) ?: null);
return empty($res[0]) ? null : (current(self::daoToFeeds($res)) ?: null);
}
/** @return array<int> */
@ -339,14 +343,14 @@ SQL;
}
/**
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listFeeds(): array {
$sql = 'SELECT * FROM `_feed` ORDER BY name';
$res = $this->fetchAssoc($sql);
/** @var array<array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority':int,'pathEntries':string,'httpAuth':string,'error':int,'ttl':int,'attributes':string}>|null $res */
return $res == null ? [] : self::daoToFeed($res);
return $res == null ? [] : self::daoToFeeds($res);
}
/** @return array<string,string> */
@ -371,10 +375,10 @@ SQL;
/**
* @param int $defaultCacheDuration Use -1 to return all feeds, without filtering them by TTL.
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listFeedsOrderUpdate(int $defaultCacheDuration = 3600, int $limit = 0): array {
$sql = 'SELECT id, url, kind, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes '
$sql = 'SELECT id, url, kind, category, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, ttl, attributes, `cache_nbEntries`, `cache_nbUnreads` '
. 'FROM `_feed` '
. ($defaultCacheDuration < 0 ? '' : 'WHERE ttl >= ' . FreshRSS_Feed::TTL_DEFAULT
. ' AND `lastUpdate` < (' . (time() + 60)
@ -383,7 +387,7 @@ SQL;
. ($limit < 1 ? '' : 'LIMIT ' . intval($limit));
$stm = $this->pdo->query($sql);
if ($stm !== false) {
return self::daoToFeed($stm->fetchAll(PDO::FETCH_ASSOC));
return self::daoToFeeds($stm->fetchAll(PDO::FETCH_ASSOC));
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
@ -394,18 +398,18 @@ SQL;
}
}
/** @return array<string> */
/** @return array<int,string> */
public function listTitles(int $id, int $limit = 0): array {
$sql = 'SELECT title FROM `_entry` WHERE id_feed=:id_feed ORDER BY id DESC'
. ($limit < 1 ? '' : ' LIMIT ' . intval($limit));
$res = $this->fetchColumn($sql, 0, [':id_feed' => $id]) ?? [];
/** @var array<string> $res */
/** @var array<int,string> $res */
return $res;
}
/**
* @param bool|null $muted to include only muted feeds
* @return array<FreshRSS_Feed>
* @return array<int,FreshRSS_Feed>
*/
public function listByCategory(int $cat, ?bool $muted = null): array {
$sql = 'SELECT * FROM `_feed` WHERE category=:category';
@ -421,9 +425,9 @@ SQL;
* @var array<int,array{'url':string,'kind':int,'category':int,'name':string,'website':string,'lastUpdate':int,
* 'priority'?:int,'pathEntries'?:string,'httpAuth':string,'error':int,'ttl'?:int,'attributes'?:string}> $res
*/
$feeds = self::daoToFeed($res);
$feeds = self::daoToFeeds($res);
usort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
uasort($feeds, static function (FreshRSS_Feed $a, FreshRSS_Feed $b) {
return strnatcasecmp($a->name(), $b->name());
});
@ -469,7 +473,7 @@ SQL;
* Remember to call updateCachedValues() after calling this function
* @return int|false number of lines affected or false in case of error
*/
public function keepMaxUnread(int $id, int $n) {
public function markAsReadMaxUnread(int $id, int $n) {
//Double SELECT for MySQL workaround ERROR 1093 (HY000)
$sql = <<<'SQL'
UPDATE `_entry` SET is_read=1
@ -581,10 +585,11 @@ SQL;
* 'pathEntries'?:string,'httpAuth'?:string,'error'?:int|bool,'ttl'?:int,'attributes'?:string,'cache_nbUnreads'?:int,'cache_nbEntries'?:int}> $listDAO
* @return array<int,FreshRSS_Feed>
*/
public static function daoToFeed(array $listDAO, ?int $catID = null): array {
public static function daoToFeeds(array $listDAO, ?int $catID = null): array {
$list = [];
foreach ($listDAO as $key => $dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'kind', 'category', 'lastUpdate', 'priority', 'error', 'ttl', 'cache_nbUnreads', 'cache_nbEntries']);
if (!isset($dao['name'])) {
continue;
}
@ -609,9 +614,9 @@ SQL;
$myFeed->_httpAuth(base64_decode($dao['httpAuth'] ?? '', true) ?: '');
$myFeed->_error($dao['error'] ?? 0);
$myFeed->_ttl($dao['ttl'] ?? FreshRSS_Feed::TTL_DEFAULT);
$myFeed->_attributes('', $dao['attributes'] ?? '');
$myFeed->_nbNotRead($dao['cache_nbUnreads'] ?? 0);
$myFeed->_nbEntries($dao['cache_nbEntries'] ?? 0);
$myFeed->_attributes($dao['attributes'] ?? '');
$myFeed->_nbNotRead($dao['cache_nbUnreads'] ?? -1);
$myFeed->_nbEntries($dao['cache_nbEntries'] ?? -1);
if (isset($dao['id'])) {
$myFeed->_id($dao['id']);
}
@ -628,6 +633,6 @@ SQL;
return -1;
}
$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
return $res[0] ?? 0;
return (int)($res[0] ?? 0);
}
}

View File

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
/** @param array<string> $errorInfo */
/** @param array<int|string> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if ($tableInfo = $this->pdo->query("PRAGMA table_info('feed')")) {
$columns = $tableInfo->fetchAll(PDO::FETCH_COLUMN, 1);

View File

@ -1,11 +1,11 @@
<?php
declare(strict_types=1);
class FreshRSS_FilterAction {
/** @var FreshRSS_BooleanSearch */
private $booleanSearch = null;
private FreshRSS_BooleanSearch $booleanSearch;
/** @var array<string>|null */
private $actions = null;
private ?array $actions = null;
/** @param array<string> $actions */
private function __construct(FreshRSS_BooleanSearch $booleanSearch, array $actions) {
@ -44,7 +44,7 @@ class FreshRSS_FilterAction {
/** @param array|mixed|null $json */
public static function fromJSON($json): ?FreshRSS_FilterAction {
if (!empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) {
if (is_array($json) && !empty($json['search']) && !empty($json['actions']) && is_array($json['actions'])) {
return new FreshRSS_FilterAction(new FreshRSS_BooleanSearch($json['search']), $json['actions']);
}
return null;

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
/**
* Logic to apply filter actions (for feeds, categories, user configuration...).
*/
trait FreshRSS_FilterActionsTrait {
/** @var array<FreshRSS_FilterAction>|null $filterActions */
private ?array $filterActions = null;
/**
* @return array<FreshRSS_FilterAction>
*/
private function filterActions(): array {
if (empty($this->filterActions)) {
$this->filterActions = [];
$filters = $this->attributeArray('filters') ?? [];
foreach ($filters as $filter) {
$filterAction = FreshRSS_FilterAction::fromJSON($filter);
if ($filterAction != null) {
$this->filterActions[] = $filterAction;
}
}
}
return $this->filterActions;
}
/**
* @param array<FreshRSS_FilterAction>|null $filterActions
*/
private function _filterActions(?array $filterActions): void {
$this->filterActions = $filterActions;
if ($this->filterActions !== null && !empty($this->filterActions)) {
$this->_attribute('filters', array_map(static function (?FreshRSS_FilterAction $af) {
return $af == null ? null : $af->toJSON();
}, $this->filterActions));
} else {
$this->_attribute('filters', null);
}
}
/** @return array<FreshRSS_BooleanSearch> */
public function filtersAction(string $action): array {
$action = trim($action);
if ($action == '') {
return [];
}
$filters = [];
$filterActions = $this->filterActions();
for ($i = count($filterActions) - 1; $i >= 0; $i--) {
$filterAction = $filterActions[$i];
if (in_array($action, $filterAction->actions(), true)) {
$filters[] = $filterAction->booleanSearch();
}
}
return $filters;
}
/**
* @param array<string> $filters
*/
public function _filtersAction(string $action, array $filters): void {
$action = trim($action);
if ($action === '') {
return;
}
$filters = array_unique(array_map('trim', $filters), SORT_STRING);
$filterActions = $this->filterActions();
//Check existing filters
for ($i = count($filterActions) - 1; $i >= 0; $i--) {
$filterAction = $filterActions[$i];
if ($filterAction == null || !is_array($filterAction->actions()) ||
$filterAction->booleanSearch() == null || trim($filterAction->booleanSearch()->getRawInput()) == '') {
array_splice($filterActions, $i, 1);
continue;
}
$actions = $filterAction->actions();
//Remove existing rules with same action
for ($j = count($actions) - 1; $j >= 0; $j--) {
if ($actions[$j] === $action) {
array_splice($actions, $j, 1);
}
}
//Update existing filter with new action
for ($k = count($filters) - 1; $k >= 0; $k --) {
$filter = $filters[$k];
if ($filter === $filterAction->booleanSearch()->getRawInput()) {
$actions[] = $action;
array_splice($filters, $k, 1);
}
}
//Save result
if (empty($actions)) {
array_splice($filterActions, $i, 1);
} else {
$filterAction->_actions($actions);
}
}
//Add new filters
for ($k = count($filters) - 1; $k >= 0; $k --) {
$filter = $filters[$k];
if ($filter != '') {
$filterAction = FreshRSS_FilterAction::fromJSON([
'search' => $filter,
'actions' => [$action],
]);
if ($filterAction != null) {
$filterActions[] = $filterAction;
}
}
}
if (empty($filterActions)) {
$filterActions = null;
}
$this->_filterActions($filterActions);
}
/**
* @param bool $applyLabel Parameter by reference, which will be set to true if the callers needs to apply a label to the article entry.
*/
public function applyFilterActions(FreshRSS_Entry $entry, ?bool &$applyLabel = null): void {
$applyLabel = false;
foreach ($this->filterActions() as $filterAction) {
if ($entry->matches($filterAction->booleanSearch())) {
foreach ($filterAction->actions() as $action) {
switch ($action) {
case 'read':
if (!$entry->isRead()) {
$entry->_isRead(true);
Minz_ExtensionManager::callHook('entry_auto_read', $entry, 'filter');
}
break;
case 'star':
$entry->_isFavorite(true);
break;
case 'label':
$applyLabel = true;
break;
}
}
}
}
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_FormAuth {
public static function checkCredentials(string $username, string $hash, string $nonce, string $challenge): bool {
@ -22,7 +23,7 @@ class FreshRSS_FormAuth {
$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
$mtime = @filemtime($token_file) ?: 0;
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration'];
if ($mtime + $cookie_duration < time()) {
// Token has expired (> cookie_duration) or does not exist.
@ -41,7 +42,7 @@ class FreshRSS_FormAuth {
private static function renewCookie(string $token) {
$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
if (touch($token_file)) {
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration'];
$expire = time() + $cookie_duration;
Minz_Session::setLongTermCookie('FreshRSS_login', $token, $expire);
@ -53,7 +54,7 @@ class FreshRSS_FormAuth {
/** @return string|false */
public static function makeCookie(string $username, string $password_hash) {
do {
$token = sha1(FreshRSS_Context::$system_conf->salt . $username . uniqid('' . mt_rand(), true));
$token = sha1(FreshRSS_Context::systemConf()->salt . $username . uniqid('' . mt_rand(), true));
$token_file = DATA_PATH . '/tokens/' . $token . '.txt';
} while (file_exists($token_file));
@ -77,7 +78,7 @@ class FreshRSS_FormAuth {
}
public static function purgeTokens(): void {
$limits = FreshRSS_Context::$system_conf->limits;
$limits = FreshRSS_Context::systemConf()->limits;
$cookie_duration = empty($limits['cookie_duration']) ? FreshRSS_Auth::DEFAULT_COOKIE_DURATION : $limits['cookie_duration'];
$oldest = time() - $cookie_duration;
foreach (new DirectoryIterator(DATA_PATH . '/tokens/') as $file_info) {

View File

@ -1,12 +1,11 @@
<?php
declare(strict_types=1);
class FreshRSS_Log extends Minz_Model {
/** @var string */
private $date;
/** @var string */
private $level;
/** @var string */
private $information;
private string $date;
private string $level;
private string $information;
public function date(): string {
return $this->date;

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
final class FreshRSS_LogDAO {
public static function logPath(?string $logFileName = null): string {

View File

@ -1,28 +1,17 @@
<?php
declare(strict_types=1);
/**
* Manage the reading modes in FreshRSS.
*/
class FreshRSS_ReadingMode {
/**
* @var string
*/
protected $id;
/**
* @var string
*/
protected $name;
/**
* @var string
*/
protected $title;
protected string $id;
protected string $name;
protected string $title;
/** @var array{'c':string,'a':string,'params':array<string,mixed>} */
protected $urlParams;
/**
* @var bool
*/
protected $isActive = false;
protected array $urlParams;
protected bool $isActive = false;
/**
* ReadingMode constructor.

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
require_once(LIB_PATH . '/lib_date.php');
@ -12,64 +13,63 @@ class FreshRSS_Search {
/**
* This contains the user input string
* @var string
*/
private $raw_input = '';
private string $raw_input = '';
// The following properties are extracted from the raw input
/** @var array<string>|null */
private $entry_ids;
private ?array $entry_ids = null;
/** @var array<int>|null */
private $feed_ids;
private ?array $feed_ids = null;
/** @var array<int>|'*'|null */
private $label_ids;
private $label_ids = null;
/** @var array<string>|null */
private $label_names;
private ?array $label_names = null;
/** @var array<string>|null */
private $intitle;
private ?array $intitle = null;
/** @var int|false|null */
private $min_date;
private $min_date = null;
/** @var int|false|null */
private $max_date;
private $max_date = null;
/** @var int|false|null */
private $min_pubdate;
private $min_pubdate = null;
/** @var int|false|null */
private $max_pubdate;
private $max_pubdate = null;
/** @var array<string>|null */
private $inurl;
private ?array $inurl = null;
/** @var array<string>|null */
private $author;
private ?array $author = null;
/** @var array<string>|null */
private $tags;
private ?array $tags = null;
/** @var array<string>|null */
private $search;
private ?array $search = null;
/** @var array<string>|null */
private $not_entry_ids;
private ?array $not_entry_ids = null;
/** @var array<int>|null */
private $not_feed_ids;
private ?array $not_feed_ids = null;
/** @var array<int>|'*'|null */
private $not_label_ids;
private $not_label_ids = null;
/** @var array<string>|null */
private $not_label_names;
private ?array $not_label_names = null;
/** @var array<string>|null */
private $not_intitle;
private ?array $not_intitle = null;
/** @var int|false|null */
private $not_min_date;
private $not_min_date = null;
/** @var int|false|null */
private $not_max_date;
private $not_max_date = null;
/** @var int|false|null */
private $not_min_pubdate;
private $not_min_pubdate = null;
/** @var int|false|null */
private $not_max_pubdate;
private $not_max_pubdate = null;
/** @var array<string>|null */
private $not_inurl;
private ?array $not_inurl = null;
/** @var array<string>|null */
private $not_author;
private ?array $not_author = null;
/** @var array<string>|null */
private $not_tags;
private ?array $not_tags = null;
/** @var array<string>|null */
private $not_search;
private ?array $not_search = null;
public function __construct(string $input) {
$input = self::cleanSearch($input);

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Manage the sharing options in FreshRSS.
@ -8,7 +9,7 @@ class FreshRSS_Share {
* The list of available sharing options.
* @var array<string,FreshRSS_Share>
*/
private static $list_sharing = [];
private static array $list_sharing = [];
/**
* Register a new sharing option.
@ -71,45 +72,31 @@ class FreshRSS_Share {
}
/** @var string */
private $type;
/** @var string */
private $name;
/** @var string */
private $url_transform;
private string $type;
private string $name;
private string $url_transform;
/** @var array<callable>|array<string,array<callable>> */
private $transforms;
private array $transforms;
/**
* @phpstan-var 'simple'|'advanced'
* @var string
*/
private $form_type;
/** @var string */
private $help_url;
/** @var string|null */
private $custom_name = null;
/** @var string|null */
private $base_url = null;
/** @var string|null */
private $id = null;
/** @var string|null */
private $title = null;
/** @var string|null */
private $link = null;
/** @var bool */
private $isDeprecated;
private string $form_type;
private string $help_url;
private ?string $custom_name = null;
private ?string $base_url = null;
private ?string $id = null;
private ?string $title = null;
private ?string $link = null;
private bool $isDeprecated;
/**
* @phpstan-var 'GET'|'POST'
* @var string
*/
private $method;
/** @var string|null */
private $field;
private string $method;
private ?string $field;
/**
* @phpstan-var 'button'|null
* @var string
*/
private $HTMLtag;
private ?string $HTMLtag;
/**
* Create a FreshRSS_Share object.

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_StatsDAO extends Minz_ModelPdo {
@ -48,8 +49,13 @@ WHERE e.id_feed = f.id
{$filter}
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}>|null $res */
return $res[0] ?? false;
if (!empty($res[0])) {
$dao = $res[0];
/** @var array<array{'total':int,'count_unreads':int,'count_reads':int,'count_favorites':int}> $res */
FreshRSS_DatabaseDAO::pdoInt($dao, ['total', 'count_unreads', 'count_reads', 'count_favorites']);
return $dao;
}
return false;
}
/**
@ -242,8 +248,8 @@ WHERE c.id = f.category
GROUP BY label
ORDER BY data DESC
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'label':string,'data':int}>|null @res */
$res = $this->fetchAssoc($sql);
return $res == null ? [] : $res;
}
@ -285,7 +291,13 @@ LIMIT 10
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'id':int,'name':string,'category':string,'count':int}>|null $res */
return $res == null ? [] : $res;
if (is_array($res)) {
foreach ($res as &$dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'count']);
}
return $res;
}
return [];
}
/**
@ -305,7 +317,13 @@ ORDER BY name
SQL;
$res = $this->fetchAssoc($sql);
/** @var array<array{'id':int,'name':string,'last_date':int,'nb_articles':int}>|null $res */
return $res == null ? [] : $res;
if (is_array($res)) {
foreach ($res as &$dao) {
FreshRSS_DatabaseDAO::pdoInt($dao, ['id', 'last_date', 'nb_articles']);
}
return $res;
}
return [];
}
/**

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {

View File

@ -1,10 +1,11 @@
<?php
declare(strict_types=1);
/**
* @property bool $allow_anonymous
* @property bool $allow_anonymous_refresh
* @property-read bool $allow_referrer
* @property-read bool $allow_robots
* @property bool $allow_robots
* @property bool $api_enabled
* @property string $archiving
* @property 'form'|'http_auth'|'none' $auth_type
@ -15,18 +16,21 @@
* @property bool $force_email_validation
* @property-read bool $http_auth_auto_register
* @property-read string $http_auth_auto_register_email_field
* @property-read string $language
* @property string $language
* @property array<string,int> $limits
* @property-read string $logo_html
* @property-read string $meta_description
* @property-read int $nb_parallel_refresh
* @property-read bool $pubsubhubbub_enabled
* @property-read string $salt
* @property-read bool $simplepie_syslog_enabled
* @property bool $unsafe_autologin_enabled
* @property array<string> $trusted_sources
* @property array<string,array<string,mixed>> $extensions
*/
final class FreshRSS_SystemConfiguration extends Minz_Configuration {
/** @throws Minz_ConfigurationNamespaceException */
public static function init(string $config_filename, ?string $default_filename = null): FreshRSS_SystemConfiguration {
parent::register('system', $config_filename, $default_filename);
return parent::get('system');

View File

@ -1,26 +1,13 @@
<?php
declare(strict_types=1);
class FreshRSS_Tag extends Minz_Model {
/**
* @var int
*/
private $id = 0;
/**
* @var string
*/
private $name;
/**
* @var array<string,mixed>
*/
private $attributes = [];
/**
* @var int
*/
private $nbEntries = -1;
/**
* @var int
*/
private $nbUnread = -1;
use FreshRSS_AttributesTrait, FreshRSS_FilterActionsTrait;
private int $id = 0;
private string $name;
private int $nbEntries = -1;
private int $nbUnread = -1;
public function __construct(string $name = '') {
$this->_name($name);
@ -45,34 +32,6 @@ class FreshRSS_Tag extends Minz_Model {
$this->name = trim($value);
}
/**
* @phpstan-return ($key is non-empty-string ? mixed : array<string,mixed>)
* @return array<string,mixed>|mixed|null
*/
public function attributes(string $key = '') {
if ($key === '') {
return $this->attributes;
} else {
return $this->attributes[$key] ?? null;
}
}
/** @param string|array<mixed>|bool|int|null $value Value, not HTML-encoded */
public function _attributes(string $key, $value = null): void {
if ($key == '') {
if (is_string($value)) {
$value = json_decode($value, true);
}
if (is_array($value)) {
$this->attributes = $value;
}
} elseif ($value === null) {
unset($this->attributes[$key]);
} else {
$this->attributes[$key] = $value;
}
}
public function nbEntries(): int {
if ($this->nbEntries < 0) {
$tagDAO = FreshRSS_Factory::createTagDao();

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_TagDAO extends Minz_ModelPdo {
@ -6,42 +7,6 @@ class FreshRSS_TagDAO extends Minz_ModelPdo {
return 'IGNORE';
}
public function createTagTable(): bool {
$ok = false;
$hadTransaction = $this->pdo->inTransaction();
if ($hadTransaction) {
$this->pdo->commit();
}
try {
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
Minz_Log::warning('SQL ALTER GUID case sensitivity…');
$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
$databaseDAO->ensureCaseInsensitiveGuids();
Minz_Log::warning('SQL CREATE TABLE tag…');
$ok = $this->pdo->exec($GLOBALS['SQL_CREATE_TABLE_TAGS']) !== false;
} catch (Exception $e) {
Minz_Log::error('FreshRSS_EntryDAO::createTagTable error: ' . $e->getMessage());
}
if ($hadTransaction) {
$this->pdo->beginTransaction();
}
return $ok;
}
/** @param array<string> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if (isset($errorInfo[0])) {
if ($errorInfo[0] === FreshRSS_DatabaseDAO::ER_BAD_TABLE_ERROR || $errorInfo[0] === FreshRSS_DatabaseDAOPGSQL::UNDEFINED_TABLE) {
if (stripos($errorInfo[2], 'tag') !== false) {
return $this->createTagTable(); //v1.12.0
}
}
}
return false;
}
/**
* @param array{'id'?:int,'name':string,'attributes'?:array<string,mixed>} $valuesTmp
* @return int|false
@ -56,7 +21,7 @@ WHERE NOT EXISTS (SELECT 1 FROM `_category` WHERE name = TRIM(?))
SQL;
$stm = $this->pdo->prepare($sql);
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, 63, 'UTF-8');
$valuesTmp['name'] = mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
if (!isset($valuesTmp['attributes'])) {
$valuesTmp['attributes'] = [];
}
@ -93,15 +58,16 @@ SQL;
public function updateTagName(int $id, string $name) {
// No category of the same name
$sql = <<<'SQL'
UPDATE `_tag` SET name=? WHERE id=?
AND NOT EXISTS (SELECT 1 FROM `_category` WHERE name = ?)
UPDATE `_tag` SET name = :name1 WHERE id = :id
AND NOT EXISTS (SELECT 1 FROM `_category` WHERE name = :name2)
SQL;
$name = mb_strcut(trim($name), 0, 63, 'UTF-8');
$name = mb_strcut(trim($name), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8');
$stm = $this->pdo->prepare($sql);
if ($stm !== false &&
$stm->bindValue(':id', $id, PDO::PARAM_INT) &&
$stm->bindValue(':name', $name, PDO::PARAM_STR) &&
$stm->bindValue(':name1', $name, PDO::PARAM_STR) &&
$stm->bindValue(':name2', $name, PDO::PARAM_STR) &&
$stm->execute()) {
return $stm->rowCount();
} else {
@ -130,11 +96,12 @@ SQL;
}
/**
* @param non-empty-string $key
* @param mixed $value
* @return int|false
*/
public function updateTagAttribute(FreshRSS_Tag $tag, string $key, $value) {
$tag->_attributes($key, $value);
$tag->_attribute($key, $value);
return $this->updateTagAttributes($tag->id(), $tag->attributes());
}
@ -168,6 +135,7 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
/** @var array{'id':int,'name':string,'attributes'?:array<string,mixed>} $row */
yield $row;
}
}
@ -181,6 +149,8 @@ SQL;
return;
}
while ($row = $stm->fetch(PDO::FETCH_ASSOC)) {
FreshRSS_DatabaseDAO::pdoInt($row, ['id_tag']);
FreshRSS_DatabaseDAO::pdoString($row, ['id_entry']);
yield $row;
}
}
@ -214,16 +184,16 @@ SQL;
public function searchById(int $id): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE id=:id', [':id' => $id]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
return $res === null ? null : self::daoToTag($res)[0] ?? null;
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
public function searchByName(string $name): ?FreshRSS_Tag {
$res = $this->fetchAssoc('SELECT * FROM `_tag` WHERE name=:name', [':name' => $name]);
/** @var array<array{'id':int,'name':string,'attributes'?:string}>|null $res */
return $res === null ? null : self::daoToTag($res)[0] ?? null;
return $res === null ? null : (current(self::daoToTags($res)) ?: null);
}
/** @return array<FreshRSS_Tag>|false */
/** @return array<int,FreshRSS_Tag>|false */
public function listTags(bool $precounts = false) {
if ($precounts) {
$sql = <<<'SQL'
@ -241,12 +211,9 @@ SQL;
$stm = $this->pdo->query($sql);
if ($stm !== false) {
$res = $stm->fetchAll(PDO::FETCH_ASSOC) ?: [];
return self::daoToTag($res);
return self::daoToTags($res);
} else {
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->listTags($precounts);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
@ -284,9 +251,6 @@ SQL;
return (int)$res[0]['count'];
}
$info = $this->pdo->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->count();
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return -1;
}
@ -336,6 +300,35 @@ SQL;
return false;
}
/**
* @param array<array{id_tag:int,id_entry:string}> $addLabels Labels to insert as batch
* @return int|false Number of new entries or false in case of error
*/
public function tagEntries(array $addLabels) {
$hasValues = false;
$sql = 'INSERT ' . $this->sqlIgnore() . ' INTO `_entrytag`(id_tag, id_entry) VALUES ';
foreach ($addLabels as $addLabel) {
$id_tag = (int)($addLabel['id_tag'] ?? 0);
$id_entry = $addLabel['id_entry'] ?? '';
if ($id_tag > 0 && ctype_digit($id_entry)) {
$sql .= "({$id_tag},{$id_entry}),";
$hasValues = true;
}
}
$sql = rtrim($sql, ',');
if (!$hasValues) {
return false;
}
$affected = $this->pdo->exec($sql);
if ($affected !== false) {
return $affected;
}
$info = $this->pdo->errorInfo();
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
/**
* @return array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}>|false
*/
@ -359,9 +352,6 @@ SQL;
return $lines;
}
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->getTagsForEntry($id_entry);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
@ -415,9 +405,6 @@ SQL;
return $stm->fetchAll(PDO::FETCH_ASSOC);
}
$info = $stm == null ? $this->pdo->errorInfo() : $stm->errorInfo();
if ($this->autoUpdateDb($info)) {
return $this->getTagsForEntries($entries);
}
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
return false;
}
@ -443,9 +430,9 @@ SQL;
/**
* @param iterable<array{'id':int,'name':string,'attributes'?:string}> $listDAO
* @return array<FreshRSS_Tag>
* @return array<int,FreshRSS_Tag>
*/
private static function daoToTag(iterable $listDAO): array {
private static function daoToTags(iterable $listDAO): array {
$list = [];
foreach ($listDAO as $dao) {
if (empty($dao['id']) || empty($dao['name'])) {
@ -454,12 +441,12 @@ SQL;
$tag = new FreshRSS_Tag($dao['name']);
$tag->_id($dao['id']);
if (!empty($dao['attributes'])) {
$tag->_attributes('', $dao['attributes']);
$tag->_attributes($dao['attributes']);
}
if (isset($dao['unreads'])) {
$tag->_nbUnread($dao['unreads']);
}
$list[] = $tag;
$list[$tag->id()] = $tag;
}
return $list;
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO {

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
@ -6,15 +7,4 @@ class FreshRSS_TagDAOSQLite extends FreshRSS_TagDAO {
return 'OR IGNORE';
}
/** @param array<string> $errorInfo */
protected function autoUpdateDb(array $errorInfo): bool {
if ($tableInfo = $this->pdo->query("SELECT sql FROM sqlite_master where name='tag'")) {
$showCreate = $tableInfo->fetchColumn();
if (is_string($showCreate) && stripos($showCreate, 'tag') === false) {
return $this->createTagTable(); //v1.12.0
}
}
return false;
}
}

View File

@ -1,12 +1,11 @@
<?php
declare(strict_types=1);
class FreshRSS_Themes extends Minz_Model {
/** @var string */
private static $themesUrl = '/themes/';
/** @var string */
private static $defaultIconsUrl = '/themes/icons/';
/** @var string */
public static $defaultTheme = 'Origine';
private static string $themesUrl = '/themes/';
private static string $defaultIconsUrl = '/themes/icons/';
public static string $defaultTheme = 'Origine';
/** @return array<string> */
public static function getList(): array {
@ -39,11 +38,12 @@ class FreshRSS_Themes extends Minz_Model {
if (file_exists($json_filename)) {
$content = file_get_contents($json_filename) ?: '';
$res = json_decode($content, true);
if ($res &&
if (is_array($res) &&
!empty($res['name']) &&
isset($res['files']) &&
is_array($res['files'])) {
$res['id'] = $theme_id;
/** @var array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}} */
return $res;
}
}
@ -51,10 +51,9 @@ class FreshRSS_Themes extends Minz_Model {
return false;
}
/** @var string */
private static $themeIconsUrl;
private static string $themeIconsUrl;
/** @var array<string,int> */
private static $themeIcons;
private static array $themeIcons;
/**
* @return false|array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}
@ -104,6 +103,7 @@ class FreshRSS_Themes extends Minz_Model {
'FreshRSS-logo' => '⊚',
'help' => '', //ⓘ
'icon' => '⊚',
'important' => '📌',
'key' => '🔑', //⚿
'label' => '🏷️',
'link' => '↗️', //↗
@ -156,7 +156,7 @@ class FreshRSS_Themes extends Minz_Model {
}
if ($type == self::ICON_DEFAULT) {
if ((FreshRSS_Context::$user_conf && FreshRSS_Context::$user_conf->icons_as_emojis)
if ((FreshRSS_Context::hasUserConf() && FreshRSS_Context::userConf()->icons_as_emojis)
// default to emoji alternate for some icons
) {
$type = self::ICON_EMOJI;

View File

@ -1,8 +1,9 @@
<?php
declare(strict_types=1);
/**
* @property string $apiPasswordHash
* @property array<string,mixed> $archiving
* @property array{'keep_period':string|false,'keep_max':int|false,'keep_min':int|false,'keep_favourites':bool,'keep_labels':bool,'keep_unreads':bool} $archiving
* @property bool $auto_load_more
* @property bool $auto_remove_article
* @property bool $bottomline_date
@ -11,6 +12,7 @@
* @property bool $bottomline_read
* @property bool $bottomline_sharing
* @property bool $bottomline_tags
* @property bool $bottomline_myLabels
* @property string $content_width
* @property-read int $default_state
* @property string $default_view
@ -32,14 +34,14 @@
* @property bool $lazyload
* @property string $mail_login
* @property bool $mark_updated_article_unread
* @property array<string,bool> $mark_when
* @property array<string,bool|int> $mark_when
* @property int $max_posts_per_rss
* @property-read array<string,int> $limits
* @property int|null $old_entries
* @property bool $onread_jump_next
* @property string $passwordHash
* @property int $posts_per_page
* @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}> $queries
* @property array<array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}> $queries
* @property bool $reading_confirm
* @property int $since_hours_posts_per_rss
* @property bool $show_fav_unread
@ -68,11 +70,63 @@
* @property-read bool $unsafe_autologin_enabled
* @property string $view_mode
* @property array<string,mixed> $volatile
* @property array<string,array<string,mixed>> $extensions
*/
final class FreshRSS_UserConfiguration extends Minz_Configuration {
use FreshRSS_FilterActionsTrait;
/** @throws Minz_ConfigurationNamespaceException */
public static function init(string $config_filename, ?string $default_filename = null): FreshRSS_UserConfiguration {
parent::register('user', $config_filename, $default_filename);
return parent::get('user');
}
/**
* Access the default configuration for users.
* @throws Minz_FileNotExistException
*/
public static function default(): FreshRSS_UserConfiguration {
static $default_user_conf = null;
if ($default_user_conf == null) {
$namespace = 'user_default';
FreshRSS_UserConfiguration::register($namespace, '_', FRESHRSS_PATH . '/config-user.default.php');
$default_user_conf = FreshRSS_UserConfiguration::get($namespace);
}
return $default_user_conf;
}
/**
* @param non-empty-string $key
* @return array<int|string,mixed>|null
*/
public function attributeArray(string $key): ?array {
$a = parent::param($key, null);
return is_array($a) ? $a : null;
}
/** @param non-empty-string $key */
public function attributeBool(string $key): ?bool {
$a = parent::param($key, null);
return is_bool($a) ? $a : null;
}
/** @param non-empty-string $key */
public function attributeInt(string $key): ?int {
$a = parent::param($key, null);
return is_numeric($a) ? (int)$a : null;
}
/** @param non-empty-string $key */
public function attributeString(string $key): ?string {
$a = parent::param($key, null);
return is_string($a) ? $a : null;
}
/**
* @param non-empty-string $key
* @param array<string,mixed>|mixed|null $value Value, not HTML-encoded
*/
public function _attribute(string $key, $value = null): void {
parent::_param($key, $value);
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_UserDAO extends Minz_ModelPdo {
@ -6,7 +7,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
require(APP_PATH . '/SQL/install.sql.' . $this->pdo->dbType() . '.php');
try {
$sql = $GLOBALS['SQL_CREATE_TABLES'] . $GLOBALS['SQL_CREATE_TABLE_ENTRYTMP'] . $GLOBALS['SQL_CREATE_TABLE_TAGS'];
$sql = $GLOBALS['SQL_CREATE_TABLES'];
$ok = $this->pdo->exec($sql) !== false; //Note: Only exec() can take multiple statements safely.
} catch (Exception $e) {
$ok = false;

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
/**
* Contains the description of a user query
@ -8,38 +9,43 @@
*/
class FreshRSS_UserQuery {
/** @var bool */
private $deprecated = false;
/** @var string */
private $get = '';
/** @var string */
private $get_name = '';
/** @var string */
private $get_type = '';
/** @var string */
private $name = '';
/** @var string */
private $order = '';
/** @var FreshRSS_BooleanSearch */
private $search;
/** @var int */
private $state = 0;
/** @var string */
private $url = '';
/** @var FreshRSS_FeedDAO|null */
private $feed_dao;
/** @var FreshRSS_CategoryDAO|null */
private $category_dao;
/** @var FreshRSS_TagDAO|null */
private $tag_dao;
private bool $deprecated = false;
private string $get = '';
private string $get_name = '';
private string $get_type = '';
private string $name = '';
private string $order = '';
private FreshRSS_BooleanSearch $search;
private int $state = 0;
private string $url = '';
private string $token = '';
private bool $shareRss = false;
private bool $shareOpml = false;
/** @var array<int,FreshRSS_Category> $categories */
private array $categories;
/** @var array<int,FreshRSS_Tag> $labels */
private array $labels;
public static function generateToken(string $salt): string {
if (!FreshRSS_Context::hasSystemConf()) {
return '';
}
$hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
if (function_exists('gmp_init')) {
// Shorten the hash if possible by converting from base 16 to base 62
$hash = gmp_strval(gmp_init($hash, 16), 62);
}
return $hash;
}
/**
* @param array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string} $query
* @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query
* @param array<int,FreshRSS_Category> $categories
* @param array<int,FreshRSS_Tag> $labels
*/
public function __construct(array $query, FreshRSS_FeedDAO $feed_dao = null, FreshRSS_CategoryDAO $category_dao = null, FreshRSS_TagDAO $tag_dao = null) {
$this->category_dao = $category_dao;
$this->feed_dao = $feed_dao;
$this->tag_dao = $tag_dao;
public function __construct(array $query, array $categories, array $labels) {
$this->categories = $categories;
$this->labels = $labels;
if (isset($query['get'])) {
$this->parseGet($query['get']);
}
@ -60,8 +66,18 @@ class FreshRSS_UserQuery {
if (!isset($query['search'])) {
$query['search'] = '';
}
if (!empty($query['token'])) {
$this->token = $query['token'];
}
if (isset($query['shareRss'])) {
$this->shareRss = $query['shareRss'];
}
if (isset($query['shareOpml'])) {
$this->shareOpml = $query['shareOpml'];
}
// linked too deeply with the search object, need to use dependency injection
$this->search = new FreshRSS_BooleanSearch($query['search']);
$this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', false);
if (!empty($query['state'])) {
$this->state = intval($query['state']);
}
@ -70,16 +86,19 @@ class FreshRSS_UserQuery {
/**
* Convert the current object to an array.
*
* @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string}
* @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}
*/
public function toArray(): array {
return array_filter([
'get' => $this->get,
'name' => $this->name,
'order' => $this->order,
'search' => $this->search->__toString(),
'search' => $this->search->getRawInput(),
'state' => $this->state,
'url' => $this->url,
'token' => $this->token,
'shareRss' => $this->shareRss,
'shareOpml' => $this->shareOpml,
]);
}
@ -88,98 +107,43 @@ class FreshRSS_UserQuery {
*/
private function parseGet(string $get): void {
$this->get = $get;
if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
if (preg_match('/(?P<type>[acfistT])(_(?P<id>\d+))?/', $get, $matches)) {
$id = intval($matches['id'] ?? '0');
switch ($matches['type']) {
case 'a':
$this->parseAll();
$this->get_type = 'all';
break;
case 'c':
$this->parseCategory($id);
$this->get_type = 'category';
$c = $this->categories[$id] ?? null;
$this->get_name = $c === null ? '' : $c->name();
break;
case 'f':
$this->parseFeed($id);
$this->get_type = 'feed';
$f = FreshRSS_Category::findFeed($this->categories, $id);
$this->get_name = $f === null ? '' : $f->name();
break;
case 'i':
$this->get_type = 'important';
break;
case 's':
$this->parseFavorite();
$this->get_type = 'favorite';
break;
case 't':
$this->parseTag($id);
$this->get_type = 'label';
$l = $this->labels[$id] ?? null;
$this->get_name = $l === null ? '' : $l->name();
break;
case 'T':
$this->get_type = 'all_labels';
break;
}
if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
$this->deprecated = true;
}
}
}
/**
* Parse the query string when it is an "all" query
*/
private function parseAll(): void {
$this->get_name = 'all';
$this->get_type = 'all';
}
/**
* Parse the query string when it is a "category" query
*
* @throws FreshRSS_DAO_Exception
*/
private function parseCategory(int $id): void {
if ($this->category_dao === null) {
throw new FreshRSS_DAO_Exception('Category DAO is not loaded in UserQuery');
}
$category = $this->category_dao->searchById($id);
if ($category !== null) {
$this->get_name = $category->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'category';
}
/**
* Parse the query string when it is a "feed" query
*
* @throws FreshRSS_DAO_Exception
*/
private function parseFeed(int $id): void {
if ($this->feed_dao === null) {
throw new FreshRSS_DAO_Exception('Feed DAO is not loaded in UserQuery');
}
$feed = $this->feed_dao->searchById($id);
if ($feed !== null) {
$this->get_name = $feed->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'feed';
}
/**
* Parse the query string when it is a "tag" query
*
* @throws FreshRSS_DAO_Exception
*/
private function parseTag(int $id): void {
if ($this->tag_dao == null) {
throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
}
$tag = $this->tag_dao->searchById($id);
if ($tag !== null) {
$this->get_name = $tag->name();
} else {
$this->deprecated = true;
}
$this->get_type = 'tag';
}
/**
* Parse the query string when it is a "favorite" query
*/
private function parseFavorite(): void {
$this->get_name = 'favorite';
$this->get_type = 'favorite';
}
/**
* Check if the current user query is deprecated.
* It is deprecated if the category or the feed used in the query are
@ -236,7 +200,7 @@ class FreshRSS_UserQuery {
}
public function getOrder(): string {
return $this->order;
return $this->order ?: FreshRSS_Context::userConf()->sort_order;
}
public function getSearch(): FreshRSS_BooleanSearch {
@ -244,11 +208,74 @@ class FreshRSS_UserQuery {
}
public function getState(): int {
return $this->state;
$state = $this->state;
if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
$state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
}
if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
$state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
}
return $state;
}
public function getUrl(): string {
return $this->url;
}
public function getToken(): string {
return $this->token;
}
public function setToken(string $token): void {
$this->token = $token;
}
public function setShareRss(bool $shareRss): void {
$this->shareRss = $shareRss;
}
public function shareRss(): bool {
return $this->shareRss;
}
public function setShareOpml(bool $shareOpml): void {
$this->shareOpml = $shareOpml;
}
public function shareOpml(): bool {
return $this->shareOpml;
}
protected function sharedUrl(bool $xmlEscaped = true): string {
$currentUser = Minz_User::name() ?? '';
return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
}
public function sharedUrlRss(bool $xmlEscaped = true): string {
if ($this->shareRss && $this->token !== '') {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
}
return '';
}
public function sharedUrlHtml(bool $xmlEscaped = true): string {
if ($this->shareRss && $this->token !== '') {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
}
return '';
}
/**
* OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
*/
public function safeForOpml(): bool {
return in_array($this->get_type, ['all', 'category', 'feed'], true);
}
public function sharedUrlOpml(bool $xmlEscaped = true): string {
if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
}
return '';
}
}

View File

@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
class FreshRSS_View extends Minz_View {
@ -9,167 +10,122 @@ class FreshRSS_View extends Minz_View {
public $callbackBeforeFeeds;
/** @var callable */
public $callbackBeforePagination;
/** @var array<FreshRSS_Category> */
public $categories;
/** @var FreshRSS_Category|null */
public $category;
/** @var string */
public $current_user;
/** @var array<int,FreshRSS_Category> */
public array $categories;
public ?FreshRSS_Category $category;
public ?FreshRSS_Tag $tag;
public string $current_user;
/** @var iterable<FreshRSS_Entry> */
public $entries;
/** @var FreshRSS_Entry */
public $entry;
/** @var FreshRSS_Feed|null */
public $feed;
/** @var array<FreshRSS_Feed> */
public $feeds;
/** @var int */
public $nbUnreadTags;
/** @var array<FreshRSS_Tag> */
public $tags;
public FreshRSS_Entry $entry;
public FreshRSS_Feed $feed;
/** @var array<int,FreshRSS_Feed> */
public array $feeds;
public int $nbUnreadTags;
/** @var array<int,FreshRSS_Tag> */
public array $tags;
/** @var array<int,array{'id':int,'name':string,'id_entry':string,'checked':bool}> */
public $tagsForEntry;
public array $tagsForEntry;
/** @var array<string,array<string>> */
public $tagsForEntries;
/** @var array<string,string> */
public $notification;
/** @var bool */
public $excludeMutedFeeds;
public array $tagsForEntries;
public bool $excludeMutedFeeds;
// Substriptions
/** @var FreshRSS_Category|null */
public $default_category;
/** @var bool */
public $displaySlider;
/** @var bool */
public $load_ok;
/** @var bool */
public $onlyFeedsWithError;
/** @var bool */
public $signalError;
public bool $displaySlider = false;
public bool $load_ok;
public bool $onlyFeedsWithError;
public bool $signalError;
// Manage users
/** @var array{'feed_count':int,'article_count':int,'database_size':int,'language':string,'mail_login':string,'enabled':bool,'is_admin':bool,'last_user_activity':string,'is_default':bool} */
public $details;
/** @var bool */
public $disable_aside;
/** @var bool */
public $show_email_field;
/** @var string */
public $username;
public array $details;
public bool $disable_aside;
public bool $show_email_field;
public string $username;
/** @var array<array{'language':string,'enabled':bool,'is_admin':bool,'enabled':bool,'article_count':int,'database_size':int,'last_user_activity':string,'mail_login':string,'feed_count':int,'is_default':bool}> */
public $users;
public array $users;
// Updates
/** @var string */
public $last_update_time;
public string $last_update_time;
/** @var array<string,bool> */
public $status_files;
public array $status_files;
/** @var array<string,bool> */
public $status_php;
/** @var bool */
public $update_to_apply;
public array $status_php;
public bool $update_to_apply;
/** @var array<string,bool> */
public $status_database;
/** @var bool */
public $is_release_channel_stable;
public array $status_database;
public bool $is_release_channel_stable;
// Archiving
/** @var int */
public $nb_total;
/** @var int */
public $size_total;
/** @var int */
public $size_user;
public int $nb_total;
public int $size_total;
public int $size_user;
// Display
/** @var array<string,array{'id':string,'name':string,'author':string,'description':string,'version':float|string,'files':array<string>,'theme-color'?:string|array{'dark'?:string,'light'?:string,'default'?:string}}> */
public $themes;
public array $themes;
// Shortcuts
/** @var array<int, string> */
public $list_keys;
public array $list_keys;
// User queries
/** @var array<int,FreshRSS_UserQuery> */
public $queries;
public array $queries;
/** @var FreshRSS_UserQuery|null */
public $query;
public ?FreshRSS_UserQuery $query = null;
// Export / Import
/** @var string */
public $content;
public string $content;
/** @var array<string,array<string>> */
public $entryIdsTagNames;
/** @var string */
public $list_title;
/** @var int */
public $queryId;
/** @var string */
public $type;
public array $entryIdsTagNames;
public string $list_title;
public int $queryId;
public string $type;
// Form login
/** @var int */
public $cookie_days;
public int $cookie_days;
// Registration
/** @var bool */
public $can_register;
/** @var string */
public $preferred_language;
/** @var bool */
public $show_tos_checkbox;
/** @var string */
public $terms_of_service;
/** @var string */
public $site_title;
/** @var string */
public $validation_url;
public bool $can_register;
public string $preferred_language;
public bool $show_tos_checkbox;
public string $terms_of_service;
public string $site_title;
public string $validation_url;
// Logs
/** @var int */
public $currentPage;
/** @var Minz_Paginator */
public $logsPaginator;
/** @var int */
public $nbPage;
public int $currentPage;
public Minz_Paginator $logsPaginator;
public int $nbPage;
// RSS view
/** @var string */
public $rss_title = '';
/** @var string */
public $rss_url = '';
/** @var string */
public $rss_base = '';
/** @var bool */
public $internal_rendering = false;
public FreshRSS_UserQuery $userQuery;
public string $html_url = '';
public string $rss_title = '';
public string $rss_url = '';
public string $rss_base = '';
public bool $internal_rendering = false;
// Content preview
/** @var string */
public $fatalError;
/** @var string */
public $htmlContent;
/** @var bool */
public $selectorSuccess;
public string $fatalError;
public string $htmlContent;
public bool $selectorSuccess;
// Extensions
/** @var array<string,array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> */
public $available_extensions;
/** @var ?Minz_Extension */
public $ext_details;
/** @var array<array{'name':string,'author':string,'description':string,'version':string,'entrypoint':string,'type':'system'|'user','url':string,'method':string,'directory':string}> */
public array $available_extensions;
public ?Minz_Extension $ext_details;
/** @var array{'system':array<Minz_Extension>,'user':array<Minz_Extension>} */
public $extension_list;
/** @var ?Minz_Extension */
public $extension;
public array $extension_list;
public ?Minz_Extension $extension;
/** @var array<string,string> */
public $extensions_installed;
public array $extensions_installed;
// Errors
/** @var string */
public $code;
/** @var string */
public $errorMessage;
public string $code;
public string $errorMessage;
/** @var array<string,string> */
public $message;
public array $message;
}

View File

@ -1,16 +1,15 @@
<?php
declare(strict_types=1);
final class FreshRSS_ViewJavascript extends FreshRSS_View {
/** @var array<FreshRSS_Category> */
public $categories;
/** @var array<FreshRSS_Feed> */
public $feeds;
/** @var array<FreshRSS_Tag> */
public $tags;
/** @var array<int,FreshRSS_Category> */
public array $categories;
/** @var array<int,FreshRSS_Feed> */
public array $feeds;
/** @var array<int,FreshRSS_Tag> */
public array $tags;
/** @var string */
public $nonce;
/** @var string */
public $salt1;
public string $nonce;
public string $salt1;
}

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