mirror of https://github.com/FreshRSS/FreshRSS.git
Merge branch 'edge' into mention-ttrss-exporter
This commit is contained in:
commit
26e6698393
|
@ -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.:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
/.devcontainer/
|
||||
/.git/
|
||||
/.github/
|
||||
/bin/
|
||||
/data/
|
||||
/docs/
|
||||
/extensions/node_modules/
|
||||
/extensions/vendor/
|
||||
/node_modules/
|
||||
/vendor/
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
*.min.js
|
||||
.git/
|
||||
*.min.js
|
||||
extensions/
|
||||
node_modules/
|
||||
p/scripts/vendor/
|
||||
|
|
|
@ -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' }}
|
|
@ -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
|
|
@ -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
|
|
@ -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 ;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/bin/
|
||||
/extensions/node_modules/
|
||||
/extensions/vendor/
|
||||
/node_modules/
|
||||
/vendor/
|
||||
/data.back/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.git/
|
||||
extensions/
|
||||
node_modules/
|
||||
p/scripts/bcrypt.min.js
|
||||
p/scripts/vendor/
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
.git/
|
||||
extensions/
|
||||
lib/marienfressinaud/
|
||||
lib/phpgt/
|
||||
lib/phpmailer/
|
||||
|
|
|
@ -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", {
|
||||
|
|
|
@ -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/",
|
||||
|
|
257
CHANGELOG.md
257
CHANGELOG.md
|
@ -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)
|
||||
|
|
31
CREDITS.md
31
CREDITS.md
|
@ -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)
|
||||
|
|
|
@ -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 '*' && \
|
||||
|
|
|
@ -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/" \
|
||||
|
|
|
@ -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 && \
|
||||
|
|
|
@ -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/" \
|
||||
|
|
|
@ -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')
|
|
@ -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
|
||||
|
|
|
@ -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 you’re working or want to host on an ARM64 system (such as Apple Silicon (M1/M2)) you’ll 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 image’s 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 [Let’s Encrypt](https://letsencrypt.org/) HTTPS certificates and with a redirection from HTTP to HTTPS.
|
||||
|
||||
See [`docker-compose-proxy.yml`](./freshrss/docker-compose-proxy.yml)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,7 +6,7 @@ volumes:
|
|||
services:
|
||||
|
||||
freshrss-db:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
container_name: freshrss-db
|
||||
hostname: freshrss-db
|
||||
restart: unless-stopped
|
||||
|
|
|
@ -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`)
|
||||
|
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
mv ../README.md ../README.en.md
|
||||
mv README.md ../
|
|
@ -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
|
6
Makefile
6
Makefile
|
@ -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 ; \
|
||||
|
|
36
README.fr.md
36
README.fr.md
|
@ -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 d’un mode de lecture anonyme, et supporte les étiquettes personnalisées.
|
||||
Il y a une API pour les clients (mobiles), ainsi qu’une [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 d’articles 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 l’ajout 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 n’est 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 d’encodages), [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 l’export/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 d’encodages), [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 d’accès pour le serveur Web
|
||||
sudo cli/access-permissions.sh
|
||||
# Si vous souhaitez permettre les mises à jour par l’interface Web
|
||||
sudo chmod -R g+w .
|
||||
# Si vous souhaitez permettre les mises à jour par l’interface 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 [l’API 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
|
||||
|
||||
|
|
30
README.md
30
README.md
|
@ -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 | ✔️ | ❔ | ❔ | ❔ | ✔️ | ➖ | ➖ | ➖ |
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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 don’t redirect to http://.
|
||||
# https://bz.apache.org/bugzilla/show_bug.cgi?id=61355#c13
|
||||
} else {
|
||||
return _url('auth', 'logout') ?: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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++;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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) ?: [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 isn’t logged in and `username` param isn’t 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 it’s 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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_BadUrl_Exception extends FreshRSS_Feed_Exception {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
<?php
|
||||
|
||||
class FreshRSS_DAO_Exception extends Exception {
|
||||
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_EntriesGetter_Exception extends Exception {
|
||||
class FreshRSS_EntriesGetter_Exception extends Minz_Exception {
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Feed_Exception extends Exception {
|
||||
class FreshRSS_Feed_Exception extends Minz_Exception {
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_ZipMissing_Exception extends Exception {
|
||||
class FreshRSS_ZipMissing_Exception extends Minz_Exception {
|
||||
}
|
||||
|
|
|
@ -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 it’s 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') ||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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'] ?? '';
|
||||
}
|
||||
|
|
|
@ -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('/:"(.*?)"/', ':"\1"', $input);
|
||||
if (!is_string($input)) {
|
||||
return;
|
||||
}
|
||||
$input = preg_replace('/(?<=[\s!-]|^)"(.*?)"/', '"\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 BooleanSearch’s combined by implicit AND
|
||||
// or parse everything as a series of Search’s 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[] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Days {
|
||||
|
|
|
@ -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 enclosure’s 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
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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é l’auto-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()), ['<' => '<', '>' => '>', '"' => '"']);
|
||||
$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()), ['<' => '<', '>' => '>', '"' => '"']);
|
||||
$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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class FreshRSS_LogDAO {
|
||||
public static function logPath(?string $logFileName = null): string {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_TagDAOPGSQL extends FreshRSS_TagDAO {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ? '&' : '&') . 'f=rss';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public function sharedUrlHtml(bool $xmlEscaped = true): string {
|
||||
if ($this->shareRss && $this->token !== '') {
|
||||
return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&' : '&') . '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 ? '&' : '&') . 'f=opml';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue