mirror of
https://github.com/FreshRSS/FreshRSS.git
synced 2026-05-14 18:23:52 -04:00
Merge branch 'edge' into latest
This commit is contained in:
32
.devcontainer/Dockerfile
Normal file
32
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM alpine:3.17
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apk add --no-cache \
|
||||
tzdata \
|
||||
apache2 php-apache2 \
|
||||
php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
|
||||
php-pdo_sqlite php-pdo_mysql php-pdo_pgsql \
|
||||
bash composer curl docker-cli-buildx git gpg make nodejs npm shellcheck shfmt sudo
|
||||
|
||||
RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
|
||||
/etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \
|
||||
sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|remoteip|setenvif).so$/s/^\s*#//" \
|
||||
/etc/apache2/httpd.conf && \
|
||||
sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
|
||||
/etc/apache2/httpd.conf
|
||||
|
||||
RUN adduser --ingroup www-data --disabled-password developer && \
|
||||
echo "developer ALL=(ALL) NOPASSWD: ALL" > /etc/sudoers.d/developer
|
||||
|
||||
ENV COPY_LOG_TO_SYSLOG On
|
||||
ENV COPY_SYSLOG_TO_STDERR On
|
||||
ENV CRON_MIN ''
|
||||
ENV FRESHRSS_ENV 'development'
|
||||
ENV LISTEN '0.0.0.0:8080'
|
||||
|
||||
EXPOSE 8080
|
||||
35
.devcontainer/devcontainer.json
Normal file
35
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,35 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json
|
||||
{
|
||||
"name": "FreshRSS-dev-Alpine",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"DavidAnson.vscode-markdownlint",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"eamodio.gitlens",
|
||||
"EditorConfig.EditorConfig",
|
||||
"foxundermoon.shell-format",
|
||||
"mrmlnc.vscode-apache",
|
||||
"ms-azuretools.vscode-docker",
|
||||
"redhat.vscode-yaml",
|
||||
"timonwong.shellcheck",
|
||||
"ValeryanM.vscode-phpsab"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [
|
||||
8080
|
||||
],
|
||||
"portsAttributes": {
|
||||
"8080": {
|
||||
"label": "FreshRSS Apache",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
"remoteUser": "developer",
|
||||
"postCreateCommand": "sudo .devcontainer/postCreateCommand.sh"
|
||||
}
|
||||
17
.devcontainer/postCreateCommand.sh
Executable file
17
.devcontainer/postCreateCommand.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
ln -s "$(pwd)" /var/www/FreshRSS
|
||||
|
||||
cp ./Docker/*.Apache.conf /etc/apache2/conf.d/
|
||||
|
||||
cat <<EOT >./constants.local.php
|
||||
<?php
|
||||
define('DATA_PATH', '/home/developer/freshrss-data');
|
||||
EOT
|
||||
|
||||
./Docker/entrypoint.sh
|
||||
|
||||
chown -R developer:www-data /home/developer/freshrss-data
|
||||
chmod -R g+w /home/developer/freshrss-data
|
||||
|
||||
httpd
|
||||
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@@ -55,7 +55,7 @@ jobs:
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
# https://nodejs.org/en/about/releases/
|
||||
node-version: '16'
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
|
||||
- run: npm ci
|
||||
@@ -79,14 +79,14 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: bin
|
||||
key: ${{ runner.os }}-bin-shfmt@v3.5.1-hadolint@v2.10.0-typos@v1.10.1
|
||||
key: ${{ runner.os }}-bin-shfmt@v3.6.0-hadolint@v2.12.0-typos@v1.13.6
|
||||
|
||||
- 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.5.1
|
||||
run: GOBIN=${PWD}/bin/ go install mvdan.cc/sh/v3/cmd/shfmt@v3.6.0
|
||||
|
||||
- name: Check shell script syntax
|
||||
# shellcheck is pre-installed https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2204-Readme.md
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Install hadolint
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.10.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
|
||||
run: curl -sL -o ./bin/hadolint "https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-$(uname -s)-$(uname -m)" && chmod 700 ./bin/hadolint
|
||||
|
||||
- name: Check Dockerfile syntax
|
||||
run: find . -name 'Dockerfile*' -print0 | xargs -0 -n1 ./bin/hadolint --failure-threshold warning
|
||||
@@ -103,7 +103,7 @@ jobs:
|
||||
if: steps.shell-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd bin ;
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.10.1/typos-v1.10.1-x86_64-unknown-linux-musl.tar.gz' &&
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' &&
|
||||
tar -xvf *.tar.gz './typos' &&
|
||||
chmod +x typos &&
|
||||
rm *.tar.gz ;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.git/
|
||||
lib/marienfressinaud/
|
||||
lib/phpgt/
|
||||
lib/phpmailer/
|
||||
node_modules/
|
||||
|
||||
@@ -3,7 +3,7 @@ ot = "ot"
|
||||
Ths2 = "Ths2"
|
||||
|
||||
[default.extend-words]
|
||||
ba = "ba"
|
||||
referer = "referer"
|
||||
|
||||
[files]
|
||||
extend-exclude = [
|
||||
@@ -33,8 +33,10 @@ extend-exclude = [
|
||||
"app/i18n/zh-cn/",
|
||||
"bin/",
|
||||
"CHANGELOG-old.md",
|
||||
"composer.lock",
|
||||
"data/",
|
||||
"docs/fr/",
|
||||
"lib/marienfressinaud/",
|
||||
"lib/phpgt/",
|
||||
"lib/phpmailer/",
|
||||
"lib/SimplePie/",
|
||||
|
||||
85
CHANGELOG.md
85
CHANGELOG.md
@@ -1,9 +1,92 @@
|
||||
# FreshRSS changelog
|
||||
|
||||
## 2023-03-04 FreshRSS 1.21.0
|
||||
|
||||
* Features
|
||||
* New *XML+XPath* mode for fetching XML documents when there is no RSS/ATOM feed [#5076](https://github.com/FreshRSS/FreshRSS/pull/5076)
|
||||
* Better support of feed enclosures (image / audio / video attachments) [#4944](https://github.com/FreshRSS/FreshRSS/pull/4944)
|
||||
* User-defined time-zone [#4906](https://github.com/FreshRSS/FreshRSS/pull/4906)
|
||||
* Improve HTML+XPath mode by allowing HTML content [#4878](https://github.com/FreshRSS/FreshRSS/pull/4878)
|
||||
* Search only on full tag names and not on parts of tag names [#4882](https://github.com/FreshRSS/FreshRSS/pull/4882)
|
||||
* Allows searching for parentheses with `\(` or `\)` [#4989](https://github.com/FreshRSS/FreshRSS/pull/4989)
|
||||
* Firefox-compatible sharing service for `mailto:` links for webmail services [#4680](https://github.com/FreshRSS/FreshRSS/pull/4680)
|
||||
* Add sharing to [archive.org](https://archive.org/) [#5096](https://github.com/FreshRSS/FreshRSS/pull/5096)
|
||||
* Increase max HTTP timeout to 15 minutes [#5074](https://github.com/FreshRSS/FreshRSS/pull/5074)
|
||||
* Compatibility
|
||||
* Require PHP 7.2+ (drop support for PHP 7.0 and 7.1) [#4848](https://github.com/FreshRSS/FreshRSS/pull/4848)
|
||||
* Workaround disabled `openlog()` or `syslog()` [#5054](https://github.com/FreshRSS/FreshRSS/pull/5054)
|
||||
* Deployment
|
||||
* Docker default image (Debian 11 Bullseye) updated to PHP 7.4.33
|
||||
* Docker: alternative image updated to Alpine 3.17 with PHP 8.1.16 and Apache 2.4.55 [#4886](https://github.com/FreshRSS/FreshRSS/pull/4886)
|
||||
* More uniform time-zone behaviour [#4903](https://github.com/FreshRSS/FreshRSS/pull/4903), [#4905](https://github.com/FreshRSS/FreshRSS/pull/4905)
|
||||
* New CLI script `cli/sensitive-log.sh` to help e.g. Apache clear logs for sensitive information such as credentials [#5001](https://github.com/FreshRSS/FreshRSS/pull/5001)
|
||||
* New CLI script `cli/access-permissions.sh` to help apply file permissions correctly [#5062](https://github.com/FreshRSS/FreshRSS/pull/5062)
|
||||
* Improve file permissions on `./extensions/` [#4956](https://github.com/FreshRSS/FreshRSS/pull/4956)
|
||||
* Update Apache mime type `font/woff` [#4894](https://github.com/FreshRSS/FreshRSS/pull/4894)
|
||||
* Re-added a git `latest` branch (instead of a tag) to track the latest FreshRSS stable releases [#5148](https://github.com/FreshRSS/FreshRSS/pull/5148)
|
||||
* Bug fixing
|
||||
* Fix allow disabling curl proxy for specific feed, when proxy is defined globally [#5082](https://github.com/FreshRSS/FreshRSS/pull/5082)
|
||||
* NFS-friendly `is_writable()` checks [#4780](https://github.com/FreshRSS/FreshRSS/pull/4780)
|
||||
* Fix error handling when updating feed URL [#5039](https://github.com/FreshRSS/FreshRSS/pull/5039)
|
||||
* Fix feed favicon after editing feed URL [#4975](https://github.com/FreshRSS/FreshRSS/pull/4975)
|
||||
* Fix allow <kbd>Ctrl</kbd>+<kbd>Click</kbd> to open *Manage feeds* in new tab [#4980](https://github.com/FreshRSS/FreshRSS/pull/4980)
|
||||
* Fix empty window opened when pressing space after page load [#5146](https://github.com/FreshRSS/FreshRSS/pull/5146)
|
||||
* Fix keep current view when searching [#4981](https://github.com/FreshRSS/FreshRSS/pull/4981)
|
||||
* Fix mobile view: scroll main area again after closing slider [#5092](https://github.com/FreshRSS/FreshRSS/pull/5092)
|
||||
* Fix change confirmation when leaving sharing service config [#5098](https://github.com/FreshRSS/FreshRSS/pull/5098)
|
||||
* Fix sharing to Lemmy [#5020](https://github.com/FreshRSS/FreshRSS/pull/5020)
|
||||
* Security
|
||||
* API avoid logging passwords [CVE-2023-22481](https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-8vvv-jxg6-8578)
|
||||
* Remove execution rights on some files not needing it [#5065](https://github.com/FreshRSS/FreshRSS/pull/5065)
|
||||
* More robust application of file access permissions [#5062](https://github.com/FreshRSS/FreshRSS/pull/5062)
|
||||
* UI
|
||||
* Improve search box [#4994](https://github.com/FreshRSS/FreshRSS/pull/4994)
|
||||
* Improve navigation menu structure [#4937](https://github.com/FreshRSS/FreshRSS/pull/4937)
|
||||
* More consistent sorting of feeds alphabetically [#4841](https://github.com/FreshRSS/FreshRSS/pull/4841)
|
||||
* Improve reader view on mobile screen [#4868](https://github.com/FreshRSS/FreshRSS/pull/4868)
|
||||
* Various UI and style improvements [#4681](https://github.com/FreshRSS/FreshRSS/pull/4681), [#4794](https://github.com/FreshRSS/FreshRSS/pull/4794)
|
||||
[#4800](https://github.com/FreshRSS/FreshRSS/pull/4800), [#4850](https://github.com/FreshRSS/FreshRSS/pull/4850), [#4865](https://github.com/FreshRSS/FreshRSS/pull/4865),
|
||||
[#4872](https://github.com/FreshRSS/FreshRSS/pull/4872), [#4874](https://github.com/FreshRSS/FreshRSS/pull/4874), [#4889](https://github.com/FreshRSS/FreshRSS/pull/4889),
|
||||
[#4890](https://github.com/FreshRSS/FreshRSS/pull/4890), [#4891](https://github.com/FreshRSS/FreshRSS/pull/4891), [#4897](https://github.com/FreshRSS/FreshRSS/pull/4897),
|
||||
[#4899](https://github.com/FreshRSS/FreshRSS/pull/4899), [#4910](https://github.com/FreshRSS/FreshRSS/pull/4910), [#4923](https://github.com/FreshRSS/FreshRSS/pull/4923),
|
||||
[#4927](https://github.com/FreshRSS/FreshRSS/pull/4927), [#4960](https://github.com/FreshRSS/FreshRSS/pull/4960), [#4985](https://github.com/FreshRSS/FreshRSS/pull/4985),
|
||||
[#4998](https://github.com/FreshRSS/FreshRSS/pull/4998), [#5034](https://github.com/FreshRSS/FreshRSS/pull/5034), [#5040](https://github.com/FreshRSS/FreshRSS/pull/5040),
|
||||
[#5055](https://github.com/FreshRSS/FreshRSS/pull/5055), [#5058](https://github.com/FreshRSS/FreshRSS/pull/5058), [#5097](https://github.com/FreshRSS/FreshRSS/pull/5097),
|
||||
[#5100](https://github.com/FreshRSS/FreshRSS/pull/5100)
|
||||
* Themes
|
||||
* Dark mode for *Origine* and *Origine compact* themes [#4843](https://github.com/FreshRSS/FreshRSS/pull/4843)
|
||||
* Improve *Ansum* and *Mapco* [#4938](https://github.com/FreshRSS/FreshRSS/pull/4938), [#4959](https://github.com/FreshRSS/FreshRSS/pull/4959), [#4967](https://github.com/FreshRSS/FreshRSS/pull/4967),
|
||||
[#4983](https://github.com/FreshRSS/FreshRSS/pull/4983), [#4995](https://github.com/FreshRSS/FreshRSS/pull/4995)
|
||||
* Improve *Dark pink* [#4881](https://github.com/FreshRSS/FreshRSS/pull/4881)
|
||||
* Improve *Nord theme* [#4892](https://github.com/FreshRSS/FreshRSS/pull/4892), [#4979](https://github.com/FreshRSS/FreshRSS/pull/4979)
|
||||
* Improve *Origine* [#4893](https://github.com/FreshRSS/FreshRSS/pull/4893)
|
||||
* Improve *Origine compact* [#4873](https://github.com/FreshRSS/FreshRSS/pull/4873)
|
||||
* Improve *Pafat* [#4909](https://github.com/FreshRSS/FreshRSS/pull/4909)
|
||||
* Improve *Swage* [#4875](https://github.com/FreshRSS/FreshRSS/pull/4875), [#4922](https://github.com/FreshRSS/FreshRSS/pull/4922), [#4936](https://github.com/FreshRSS/FreshRSS/pull/4936),
|
||||
[#5029](https://github.com/FreshRSS/FreshRSS/pull/5029)
|
||||
* Mark some themes as tentatively deprecated: *BlueLagoon*, *Flat*, *Screwdriver* [#4807](https://github.com/FreshRSS/FreshRSS/pull/4807)
|
||||
* i18n
|
||||
* Improve Chinese [#4853](https://github.com/FreshRSS/FreshRSS/pull/4853), [#4856](https://github.com/FreshRSS/FreshRSS/pull/4856)
|
||||
* SimplePie
|
||||
* No URL Decode for enclosure links [#768](https://github.com/simplepie/simplepie/pull/768)
|
||||
* Fix case of multiple RSS2.0 enclosures [#769](https://github.com/simplepie/simplepie/pull/769)
|
||||
* Sanitize thumbnail URL [#770](https://github.com/simplepie/simplepie/pull/770)
|
||||
* Use single constant for default HTTP Accept header [#784](https://github.com/simplepie/simplepie/pull/784)
|
||||
* Misc.
|
||||
* Increase max feed URL length and drop unicity in database [#5038](https://github.com/FreshRSS/FreshRSS/pull/5038)
|
||||
* New support of [Development Containers](https://containers.dev) / [GitHub Codespaces](https://github.com/features/codespaces) to ease development [#4859](https://github.com/FreshRSS/FreshRSS/pull/4859)
|
||||
* Update library `lib_opml` [#4403](https://github.com/FreshRSS/FreshRSS/pull/4403)
|
||||
* Code improvements [#4232](https://github.com/FreshRSS/FreshRSS/pull/4232), [#4651](https://github.com/FreshRSS/FreshRSS/pull/4651),
|
||||
[#5024](https://github.com/FreshRSS/FreshRSS/pull/5024), [#5025](https://github.com/FreshRSS/FreshRSS/pull/5025), [#5028](https://github.com/FreshRSS/FreshRSS/pull/5028),
|
||||
[#5032](https://github.com/FreshRSS/FreshRSS/pull/5032), [#5158](https://github.com/FreshRSS/FreshRSS/pull/5158), [#5045](https://github.com/FreshRSS/FreshRSS/pull/5045),
|
||||
[#5049](https://github.com/FreshRSS/FreshRSS/pull/5049), [#5063](https://github.com/FreshRSS/FreshRSS/pull/5063), [#5084](https://github.com/FreshRSS/FreshRSS/pull/5084)
|
||||
* Update dev dependencies [#4993](https://github.com/FreshRSS/FreshRSS/pull/4993), [#5006](https://github.com/FreshRSS/FreshRSS/pull/5006), [#5109](https://github.com/FreshRSS/FreshRSS/pull/5109)
|
||||
|
||||
|
||||
## 2022-12-08 FreshRSS 1.20.2
|
||||
|
||||
* Security fixes
|
||||
* Fix security vulnerability in `ext.php` [#4928](https://github.com/FreshRSS/FreshRSS/pull/4928)
|
||||
* [CVE-2022-23497](https://github.com/FreshRSS/FreshRSS/security/advisories/GHSA-hvrj-5fwj-p7v6) Fix security vulnerability in `ext.php` [#4928](https://github.com/FreshRSS/FreshRSS/pull/4928)
|
||||
* Apache `TraceEnable Off` [#4863](https://github.com/FreshRSS/FreshRSS/pull/4863)
|
||||
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ Did you want to fix a bug? To keep a great coordination between collaborators, y
|
||||
3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783).
|
||||
4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **edge branch**. Don’t forget to add your name to `CREDITS.md` if you’re contributing to FreshRSS for the very first time.
|
||||
|
||||
If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/01_First_steps.html).
|
||||
If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/02_First_steps.html).
|
||||
|
||||
**Tip:** if you are searching for bugs easy to fix, have a look at the « [Good first issue](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22good+first+issue+%3Ababy%3A%22) » and/or « [Help wanted](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22help+wanted+%3Aoctocat%3A%22) » ticket labels.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ People are sorted by name so please keep this order.
|
||||
* [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/)
|
||||
* [Bartosz Taudul](https://github.com/wolfpld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:wolfpld), [Web](https://wolf.nereid.pl/)
|
||||
* [Benjamin Bouvier](https://github.com/bnjbvr): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:bnjbvr), [Web](https://benj.me/)
|
||||
@@ -74,6 +75,7 @@ People are sorted by name so please keep this order.
|
||||
* [happymacarts](https://github.com/happymacarts): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:happymacarts)
|
||||
* [Harshad Hirapara](https://github.com/harshad389): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:harshad389)
|
||||
* [hesch](https://github.com/hesch): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hesch)
|
||||
* [Hippolyte Thomas](https://github.com/hippothomas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hippothomas), [Web](https://hippolyte-thomas.fr/)
|
||||
* [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
|
||||
* [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)
|
||||
@@ -121,6 +123,7 @@ People are sorted by name so please keep this order.
|
||||
* [Miika Launiainen](https://gitlab.com/miicat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:miicat), [Web](https://miicat.eu/)
|
||||
* [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)
|
||||
* [MSZ](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:mszkb)
|
||||
* [Myuki](https://github.com/Myuki): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Myuki)
|
||||
* [Nainor](https://github.com/Nainor): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Nainor)
|
||||
@@ -170,6 +173,7 @@ People are sorted by name so please keep this order.
|
||||
* [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)
|
||||
* [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)
|
||||
* [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/)
|
||||
@@ -202,3 +206,4 @@ People are sorted by name so please keep this order.
|
||||
* [xnaas](https://github.com/xnaas): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:xnaas), [Web](https://xnaas.info/)
|
||||
* [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/)
|
||||
* [Zhiyuan Zheng](https://github.com/zhzy0077): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:zhzy0077)
|
||||
|
||||
@@ -2,8 +2,8 @@ FROM debian:11-slim
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
ca-certificates cron \
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM alpine:3.16
|
||||
FROM alpine:3.17
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
RUN apk add --no-cache \
|
||||
tzdata \
|
||||
apache2 php-apache2 \
|
||||
php php-curl php-gmp php-intl php-mbstring php-xml php-zip \
|
||||
php-ctype php-dom php-fileinfo php-iconv php-json php-opcache php-openssl php-phar php-session php-simplexml php-xmlreader php-xmlwriter php-xml php-tokenizer php-zlib \
|
||||
|
||||
@@ -4,6 +4,7 @@ ENV TZ UTC
|
||||
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 \
|
||||
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 \
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
FROM alpine:3.5
|
||||
FROM alpine:3.8
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
|
||||
|
||||
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 \
|
||||
|
||||
@@ -8,8 +8,8 @@ COPY ./Docker/qemu-arm-* /usr/bin/
|
||||
|
||||
ENV TZ UTC
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -y \
|
||||
ca-certificates cron \
|
||||
|
||||
@@ -4,7 +4,7 @@ DocumentRoot /var/www/FreshRSS/p/
|
||||
RemoteIPHeader X-Forwarded-For
|
||||
RemoteIPTrustedProxy 10.0.0.1/8 172.16.0.1/12 192.168.0.1/16
|
||||
LogFormat "%a %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined_proxy
|
||||
CustomLog /dev/stdout combined_proxy
|
||||
CustomLog "|/var/www/FreshRSS/cli/sensitive-log.sh" combined_proxy
|
||||
ErrorLog /dev/stderr
|
||||
AllowEncodedSlashes On
|
||||
ServerTokens OS
|
||||
|
||||
@@ -81,7 +81,7 @@ and with newer packages in general (Apache, PHP).
|
||||
|
||||
## Environment variables
|
||||
|
||||
* `TZ`: (default is `UTC`) A [server timezone](http://php.net/timezones) (default is `UTC`)
|
||||
* `TZ`: (default is `UTC`) A [server timezone](http://php.net/timezones)
|
||||
* `CRON_MIN`: (default is disabled) Define minutes for the built-in cron job to automatically refresh feeds (see below for more advanced options)
|
||||
* `FRESHRSS_ENV`: (default is `production`) Enables additional development information if set to `development` (increases the level of logging and ensures that errors are displayed) (see below for more development options)
|
||||
* `COPY_LOG_TO_SYSLOG`: (default is `On`) Copy all the logs to syslog
|
||||
@@ -303,6 +303,7 @@ services:
|
||||
options:
|
||||
max-size: 10m
|
||||
volumes:
|
||||
# Recommended volume for FreshRSS persistent data such as configuration and SQLite databases
|
||||
- data:/var/www/FreshRSS/data
|
||||
# Optional volume for storing third-party extensions
|
||||
- extensions:/var/www/FreshRSS/extensions
|
||||
@@ -314,8 +315,11 @@ services:
|
||||
# If you want to open a port 8080 on the local machine:
|
||||
- "8080:80"
|
||||
environment:
|
||||
# A timezone http://php.net/timezones (default is UTC)
|
||||
TZ: Europe/Paris
|
||||
# Cron job to refresh feeds at specified minutes
|
||||
CRON_MIN: '2,32'
|
||||
# 'development' for additional logs; default is 'production'
|
||||
FRESHRSS_ENV: development
|
||||
# Optional advanced parameter controlling the internal Apache listening port
|
||||
LISTEN: 0.0.0.0:80
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
php -f ./cli/prepare.php >/dev/null
|
||||
ln -snf "/usr/share/zoneinfo/$TZ" /etc/localtime
|
||||
echo "$TZ" >/etc/timezone
|
||||
|
||||
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
|
||||
find /etc/php*/ -type f -name php.ini -exec sed -r -i "\\#^;?post_max_size#s#^.*#post_max_size = 32M#" {} \;
|
||||
@@ -21,6 +22,10 @@ if [ -n "$CRON_MIN" ]; then
|
||||
-r "s#^[^ ]+ #$CRON_MIN #" | crontab -
|
||||
fi
|
||||
|
||||
./cli/access-permissions.sh
|
||||
|
||||
php -f ./cli/prepare.php >/dev/null
|
||||
|
||||
if [ -n "$FRESHRSS_INSTALL" ]; then
|
||||
# shellcheck disable=SC2046
|
||||
php -f ./cli/do-install.php -- \
|
||||
@@ -54,7 +59,6 @@ if [ -n "$FRESHRSS_USER" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
chown -R :www-data .
|
||||
chmod -R g+r . && chmod -R g+w ./data/
|
||||
./cli/access-permissions.sh
|
||||
|
||||
exec "$@"
|
||||
|
||||
44
Makefile
44
Makefile
@@ -60,40 +60,37 @@ stop: ## Stop FreshRSS container if any
|
||||
## Tests and linter ##
|
||||
######################
|
||||
.PHONY: test
|
||||
test: bin/phpunit ## Run the test suite
|
||||
$(PHP) ./bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
|
||||
test: vendor/bin/phpunit ## Run the test suite
|
||||
$(PHP) vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests
|
||||
|
||||
.PHONY: lint
|
||||
lint: bin/phpcs ## Run the linter on the PHP files
|
||||
$(PHP) ./bin/phpcs . -p -s
|
||||
lint: vendor/bin/phpcs ## Run the linter on the PHP files
|
||||
$(PHP) vendor/bin/phpcs . -p -s
|
||||
|
||||
.PHONY: lint-fix
|
||||
lint-fix: bin/phpcbf ## Fix the errors detected by the linter
|
||||
$(PHP) ./bin/phpcbf . -p -s
|
||||
lint-fix: vendor/bin/phpcbf ## Fix the errors detected by the linter
|
||||
$(PHP) vendor/bin/phpcbf . -p -s
|
||||
|
||||
bin/composer:
|
||||
mkdir -p bin/
|
||||
wget 'https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
|
||||
wget 'https://raw.githubusercontent.com/composer/getcomposer.org/b5dbe5ebdec95ce71b3128b359bd5a85cb0a722d/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer'
|
||||
|
||||
bin/phpunit:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpunit 'https://phar.phpunit.de/phpunit-9.5.20.phar'
|
||||
echo '6becad2da5c37f5ad101cc665ef05a2f1a6a45d2427c8edcc74f72c92fb1e05a bin/phpunit' | sha256sum -c - || rm bin/phpunit
|
||||
vendor/bin/phpunit: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpunit bin/phpunit
|
||||
|
||||
bin/phpcs:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpcs 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcs.phar'
|
||||
echo '7a14323a14af9f58302d15442492ee1076a8cd72c018a816cb44965bf3a9b015 bin/phpcs' | sha256sum -c - || rm bin/phpcs
|
||||
vendor/bin/phpcs: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpcs bin/phpcs
|
||||
|
||||
bin/phpcbf:
|
||||
mkdir -p bin/
|
||||
wget -O bin/phpcbf 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcbf.phar'
|
||||
echo 'c93c0e83cbda21c21f849ccf0f4b42979d20004a5a6172ed0ea270eca7ae6fa8 bin/phpcbf' | sha256sum -c - || rm bin/phpcbf
|
||||
vendor/bin/phpcbf: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
ln -s ../vendor/bin/phpcbf bin/phpcbf
|
||||
|
||||
bin/typos:
|
||||
mkdir -p bin/
|
||||
cd bin ; \
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.10.1/typos-v1.10.1-x86_64-unknown-linux-musl.tar.gz' && \
|
||||
wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' && \
|
||||
tar -xvf *.tar.gz './typos' && \
|
||||
chmod +x typos && \
|
||||
rm *.tar.gz ; \
|
||||
@@ -102,6 +99,9 @@ bin/typos:
|
||||
node_modules/.bin/eslint:
|
||||
npm install
|
||||
|
||||
node_modules/.bin/rtlcss:
|
||||
npm install
|
||||
|
||||
vendor/bin/phpstan: bin/composer
|
||||
bin/composer install --prefer-dist --no-progress
|
||||
|
||||
@@ -181,8 +181,8 @@ endif
|
||||
## TOOLS ##
|
||||
###########
|
||||
.PHONY: rtl
|
||||
rtl: ## Generate RTL CSS files
|
||||
rtlcss -d p/themes/ && find p/themes/ -type f -name '*.rtl.rtl.css' -delete
|
||||
rtl: node_modules/.bin/rtlcss ## Generate RTL CSS files
|
||||
npm run-script rtlcss
|
||||
|
||||
.PHONY: pot
|
||||
pot: ## Generate POT templates for docs
|
||||
|
||||
59
README.fr.md
59
README.fr.md
@@ -5,7 +5,7 @@
|
||||
|
||||
# FreshRSS
|
||||
|
||||
FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](https://github.com/LeedRSS/Leed) ou de [Kriss Feed](https://tontof.net/kriss/feed/).
|
||||
FreshRSS est un agrégateur de flux RSS à auto-héberger.
|
||||
|
||||
Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable.
|
||||
|
||||
@@ -19,21 +19,29 @@ FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé
|
||||
|
||||
Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation.
|
||||
|
||||
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
Nous sommes une communauté amicale.
|
||||
|
||||
* Site officiel : <https://freshrss.org>
|
||||
* Démo : <http://demo.freshrss.org/>
|
||||
* Licence : [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.fr.html)
|
||||
|
||||

|
||||
|
||||
# Avertissements
|
||||
## Contributions
|
||||
|
||||
FreshRSS n’est fourni avec aucune garantie.
|
||||
Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
Nous sommes une communauté amicale.
|
||||
|
||||
Pour faciliter les contributions, l’option suivante est disponible :
|
||||
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
|
||||
|
||||
## Capture d’écran
|
||||
|
||||

|
||||
|
||||
## Avertissements
|
||||
|
||||
FreshRSS n’est fourni avec aucune garantie.
|
||||
|
||||
# [Documentation](https://freshrss.github.io/FreshRSS/fr/)
|
||||
|
||||
* La [documentation utilisateurs](https://freshrss.github.io/FreshRSS/fr/users/02_First_steps.html) pour découvrir les fonctionnalités de FreshRSS.
|
||||
@@ -41,28 +49,24 @@ FreshRSS n’est fourni avec aucune garantie.
|
||||
* La [documentation développeurs](https://freshrss.github.io/FreshRSS/fr/developers/01_First_steps.html) pour savoir comment contribuer et mieux comprendre le code source de FreshRSS.
|
||||
* Le [guide de contribution](https://freshrss.github.io/FreshRSS/fr/contributing.html) pour nous aider à développer FreshRSS.
|
||||
|
||||
# Prérequis
|
||||
## Prérequis
|
||||
|
||||
* Un navigateur Web récent tel que Firefox / IceCat, Edge, Chromium / Chrome, Opera, Safari.
|
||||
* 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.0+
|
||||
* 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+
|
||||
|
||||
|
||||
# Téléchargement
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
|
||||
|
||||
Si vous préférez que votre FreshRSS soit stable, vous devriez télécharger la dernière version. De nouvelles versions sont publiées tous les 2 ou 3 mois. Voir la [liste des versions](https://github.com/FreshRSS/FreshRSS/releases).
|
||||
|
||||
Si vous voulez une publication continue (rolling release) avec les dernières nouveautés, ou bien aider à tester ou développer la future version stable, vous pouvez utiliser [la branche edge](https://github.com/FreshRSS/FreshRSS/tree/edge/).
|
||||
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html)
|
||||
|
||||
## Installation automatisée
|
||||
|
||||
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
|
||||
@@ -83,7 +87,7 @@ Si vous voulez une publication continue (rolling release) avec les dernières no
|
||||
|
||||
Plus d’informations sur l’installation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html).
|
||||
|
||||
### Exemple d’installation complète sur Linux Debian/Ubuntu
|
||||
## Exemple d’installation complète sur Linux Debian/Ubuntu
|
||||
|
||||
```sh
|
||||
# Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
|
||||
@@ -105,11 +109,12 @@ sudo apt-get install git
|
||||
sudo git clone https://github.com/FreshRSS/FreshRSS.git
|
||||
cd FreshRSS
|
||||
|
||||
# Si vous souhaitez utiliser la dernière version stable de FreshRSS
|
||||
sudo git checkout $(git describe --tags --abbrev=0)
|
||||
# La branche par défault “edge” est la celle de la publication continue,
|
||||
# mais vous pouvez changer de branche pour “latest” si vous préférez les versions stables de FreshRSS
|
||||
sudo git checkout latest
|
||||
|
||||
# Mettre les droits d’accès pour le serveur Web
|
||||
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
|
||||
sudo cli/access-permissions.sh
|
||||
# Si vous souhaitez permettre les mises à jour par l’interface Web
|
||||
sudo chmod -R g+w .
|
||||
|
||||
@@ -122,7 +127,7 @@ sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS
|
||||
# Mettre à jour FreshRSS vers une nouvelle version par git
|
||||
cd /usr/share/FreshRSS
|
||||
sudo git pull
|
||||
sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/
|
||||
sudo cli/access-permissions.sh
|
||||
```
|
||||
|
||||
Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails.
|
||||
@@ -155,7 +160,7 @@ Créer `/etc/cron.d/FreshRSS` avec :
|
||||
7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
|
||||
```
|
||||
|
||||
## Conseils
|
||||
# Conseils
|
||||
|
||||
* Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`.
|
||||
* En particulier, les données personnelles se trouvent dans le répertoire `./data/`.
|
||||
@@ -175,19 +180,7 @@ Créer `/etc/cron.d/FreshRSS` avec :
|
||||
* Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/users/*/config.php`
|
||||
* Vous pouvez exporter votre liste de flux au format OPML soit depuis l’interface Web, soit [en ligne de commande](cli/README.md)
|
||||
|
||||
Pour sauvegarder les articles eux-mêmes :
|
||||
|
||||
## Dans le cas où vous utilisez MySQL
|
||||
|
||||
Vous pouvez utiliser [phpMyAdmin](https://www.phpmyadmin.net) ou les outils de MySQL :
|
||||
|
||||
```sh
|
||||
mysqldump --skip-comments --disable-keys --user=<db_user> --password --host <db_host> --result-file=freshrss.dump.sql --databases <freshrss_db>
|
||||
```
|
||||
|
||||
## Pour toutes les bases supportées
|
||||
|
||||
Vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
|
||||
Pour sauvegarder les articles eux-mêmes, vous pouvez utiliser la [ligne de commande](cli/README.md) pour exporter votre base de données vers une base de données au format SQLite :
|
||||
|
||||
```sh
|
||||
./cli/export-sqlite-for-user.php --user <username> --filename </path/to/db.sqlite>
|
||||
@@ -250,7 +243,7 @@ et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm
|
||||
* [SimplePie](https://simplepie.org/)
|
||||
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
|
||||
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
|
||||
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
|
||||
* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
|
||||
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
|
||||
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
|
||||
* [Chart.js](https://www.chartjs.org)
|
||||
|
||||
32
README.md
32
README.md
@@ -5,7 +5,7 @@
|
||||
|
||||
# FreshRSS
|
||||
|
||||
FreshRSS is a self-hosted RSS feed aggregator like [Leed](https://github.com/LeedRSS/Leed) or [Kriss Feed](https://tontof.net/kriss/feed/).
|
||||
FreshRSS is a self-hosted RSS feed aggregator.
|
||||
|
||||
It is lightweight, easy to work with, powerful, and customizable.
|
||||
|
||||
@@ -19,21 +19,29 @@ FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.o
|
||||
|
||||
Finally, it supports [extensions](#extensions) for further tuning.
|
||||
|
||||
Feature requests, bug reports, and other contributions are welcome. The best way to contribute is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
We are a friendly community.
|
||||
|
||||
* Official website: <https://freshrss.org>
|
||||
* Demo: <https://demo.freshrss.org/>
|
||||
* License: [GNU AGPL 3](https://www.gnu.org/licenses/agpl-3.0.html)
|
||||
|
||||

|
||||
|
||||
# Disclaimer
|
||||
## Feedback and contributions
|
||||
|
||||
FreshRSS comes with absolutely no warranty.
|
||||
Feature requests, bug reports, and other contributions are welcome. The best way is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues).
|
||||
We are a friendly community.
|
||||
|
||||
To facilitate contributions, the following option is available:
|
||||
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||
|
||||
## Disclaimer
|
||||
|
||||
FreshRSS comes with absolutely no warranty.
|
||||
|
||||
# [Documentation](https://freshrss.github.io/FreshRSS/en/)
|
||||
|
||||
* [User documentation](https://freshrss.github.io/FreshRSS/en/users/02_First_steps.html), where you can discover all the possibilities offered by FreshRSS
|
||||
@@ -48,21 +56,17 @@ FreshRSS comes with absolutely no warranty.
|
||||
* 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.0+
|
||||
* 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+
|
||||
|
||||
|
||||
# Releases
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
|
||||
|
||||
The latest stable release can be found [here](https://github.com/FreshRSS/FreshRSS/releases/latest). New versions are released every two to three months.
|
||||
|
||||
If you want a rolling release with the newest features, or want to help testing or developing the next stable version, you can use [the `edge` branch](https://github.com/FreshRSS/FreshRSS/tree/edge/).
|
||||
|
||||
|
||||
# [Installation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html)
|
||||
|
||||
## Automated install
|
||||
|
||||
* [<img src="https://www.docker.com/wp-content/uploads/2022/03/horizontal-logo-monochromatic-white.png" width="200" alt="Docker" />](./Docker/)
|
||||
@@ -83,7 +87,7 @@ If you want a rolling release with the newest features, or want to help testing
|
||||
|
||||
More detailed information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/03_Installation.html).
|
||||
|
||||
## Advice
|
||||
# Advice
|
||||
|
||||
* For better security, expose only the `./p/` folder to the Web.
|
||||
* Be aware that the `./data/` folder contains all personal data, so it is a bad idea to expose it.
|
||||
@@ -138,7 +142,7 @@ and [Fever API](https://freshrss.github.io/FreshRSS/en/users/06_Fever_API.html)
|
||||
* [SimplePie](https://simplepie.org/)
|
||||
* [MINZ](https://framagit.org/marienfressinaud/MINZ)
|
||||
* [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/)
|
||||
* [lib_opml](https://github.com/marienfressinaud/lib_opml)
|
||||
* [lib_opml](https://framagit.org/marienfressinaud/lib_opml)
|
||||
* [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath)
|
||||
* [PHPMailer](https://github.com/PHPMailer/PHPMailer)
|
||||
* [Chart.js](https://www.chartjs.org)
|
||||
|
||||
35
app/Controllers/configureController.php
Executable file → Normal file
35
app/Controllers/configureController.php
Executable file → Normal file
@@ -25,6 +25,7 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
|
||||
* The options available on the page are:
|
||||
* - language (default: en)
|
||||
* - theme (default: Origin)
|
||||
* - darkMode (default: no)
|
||||
* - content width (default: thin)
|
||||
* - display of read action in header
|
||||
* - display of favorite action in header
|
||||
@@ -42,7 +43,9 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
|
||||
public function displayAction() {
|
||||
if (Minz_Request::isPost()) {
|
||||
FreshRSS_Context::$user_conf->language = Minz_Request::param('language', 'en');
|
||||
FreshRSS_Context::$user_conf->timezone = Minz_Request::param('timezone', '');
|
||||
FreshRSS_Context::$user_conf->theme = Minz_Request::param('theme', FreshRSS_Themes::$defaultTheme);
|
||||
FreshRSS_Context::$user_conf->darkMode = Minz_Request::param('darkMode', 'no');
|
||||
FreshRSS_Context::$user_conf->content_width = Minz_Request::param('content_width', 'thin');
|
||||
FreshRSS_Context::$user_conf->topline_read = Minz_Request::param('topline_read', false);
|
||||
FreshRSS_Context::$user_conf->topline_favorite = Minz_Request::param('topline_favorite', false);
|
||||
@@ -106,32 +109,32 @@ class FreshRSS_configure_Controller extends FreshRSS_ActionController {
|
||||
FreshRSS_Context::$user_conf->posts_per_page = Minz_Request::param('posts_per_page', 10);
|
||||
FreshRSS_Context::$user_conf->view_mode = Minz_Request::param('view_mode', 'normal');
|
||||
FreshRSS_Context::$user_conf->default_view = Minz_Request::param('default_view', 'adaptive');
|
||||
FreshRSS_Context::$user_conf->show_fav_unread = Minz_Request::param('show_fav_unread', false);
|
||||
FreshRSS_Context::$user_conf->auto_load_more = Minz_Request::param('auto_load_more', false);
|
||||
FreshRSS_Context::$user_conf->display_posts = Minz_Request::param('display_posts', false);
|
||||
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::param('display_categories', 'active');
|
||||
FreshRSS_Context::$user_conf->show_tags = Minz_Request::param('show_tags', '0');
|
||||
FreshRSS_Context::$user_conf->show_tags_max = Minz_Request::param('show_tags_max', '0');
|
||||
FreshRSS_Context::$user_conf->show_author_date = Minz_Request::param('show_author_date', '0');
|
||||
FreshRSS_Context::$user_conf->show_feed_name = Minz_Request::param('show_feed_name', 't');
|
||||
FreshRSS_Context::$user_conf->hide_read_feeds = Minz_Request::param('hide_read_feeds', false);
|
||||
FreshRSS_Context::$user_conf->onread_jump_next = Minz_Request::param('onread_jump_next', false);
|
||||
FreshRSS_Context::$user_conf->lazyload = Minz_Request::param('lazyload', false);
|
||||
FreshRSS_Context::$user_conf->sides_close_article = Minz_Request::param('sides_close_article', false);
|
||||
FreshRSS_Context::$user_conf->sticky_post = Minz_Request::param('sticky_post', false);
|
||||
FreshRSS_Context::$user_conf->reading_confirm = Minz_Request::param('reading_confirm', false);
|
||||
FreshRSS_Context::$user_conf->auto_remove_article = Minz_Request::param('auto_remove_article', false);
|
||||
FreshRSS_Context::$user_conf->mark_updated_article_unread = Minz_Request::param('mark_updated_article_unread', false);
|
||||
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::param('sort_order', 'DESC');
|
||||
FreshRSS_Context::$user_conf->mark_when = array(
|
||||
'article' => Minz_Request::param('mark_open_article', false),
|
||||
'gone' => Minz_Request::param('read_upon_gone', false),
|
||||
'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::param('keep_max_n_unread', false) : false,
|
||||
'reception' => Minz_Request::param('mark_upon_reception', false),
|
||||
'reception' => Minz_Request::paramBoolean('mark_upon_reception'),
|
||||
'same_title_in_feed' => Minz_Request::paramBoolean('enable_read_when_same_title_in_feed') ?
|
||||
Minz_Request::param('read_when_same_title_in_feed', false) : false,
|
||||
'scroll' => Minz_Request::param('mark_scroll', false),
|
||||
'site' => Minz_Request::param('mark_open_site', false),
|
||||
'scroll' => Minz_Request::paramBoolean('mark_scroll'),
|
||||
'site' => Minz_Request::paramBoolean('mark_open_site'),
|
||||
);
|
||||
FreshRSS_Context::$user_conf->save();
|
||||
invalidateHttpCache();
|
||||
|
||||
0
app/Controllers/entryController.php
Executable file → Normal file
0
app/Controllers/entryController.php
Executable file → Normal file
18
app/Controllers/feedController.php
Executable file → Normal file
18
app/Controllers/feedController.php
Executable file → Normal file
@@ -81,6 +81,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
||||
$feed->load(true); //Throws FreshRSS_Feed_Exception, Minz_FileNotExistException
|
||||
break;
|
||||
case FreshRSS_Feed::KIND_HTML_XPATH:
|
||||
case FreshRSS_Feed::KIND_XML_XPATH:
|
||||
$feed->_website($url);
|
||||
break;
|
||||
}
|
||||
@@ -172,7 +173,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
||||
$proxy_address = Minz_Request::param('curl_params', '');
|
||||
$proxy_type = Minz_Request::param('proxy_type', '');
|
||||
$opts = [];
|
||||
if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
|
||||
if ($proxy_type !== '') {
|
||||
$opts[CURLOPT_PROXY] = $proxy_address;
|
||||
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
|
||||
}
|
||||
@@ -201,8 +202,8 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
||||
$timeout = intval(Minz_Request::param('timeout', 0));
|
||||
$attributes['timeout'] = $timeout > 0 ? $timeout : null;
|
||||
|
||||
$feed_kind = Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
|
||||
if ($feed_kind == FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
$feed_kind = (int)Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS);
|
||||
if ($feed_kind === FreshRSS_Feed::KIND_HTML_XPATH || $feed_kind === FreshRSS_Feed::KIND_XML_XPATH) {
|
||||
$xPathSettings = [];
|
||||
if (Minz_Request::param('xPathFeedTitle', '') != '') $xPathSettings['feedTitle'] = Minz_Request::param('xPathFeedTitle', '', true);
|
||||
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
|
||||
@@ -385,10 +386,15 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
||||
if ($simplePiePush) {
|
||||
$simplePie = $simplePiePush; //Used by WebSub
|
||||
} elseif ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
$simplePie = $feed->loadHtmlXpath(false, $isNewFeed);
|
||||
if ($simplePie == null) {
|
||||
$simplePie = $feed->loadHtmlXpath();
|
||||
if ($simplePie === null) {
|
||||
throw new FreshRSS_Feed_Exception('HTML+XPath Web scraping failed for [' . $feed->url(false) . ']');
|
||||
}
|
||||
} elseif ($feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
|
||||
$simplePie = $feed->loadHtmlXpath();
|
||||
if ($simplePie === null) {
|
||||
throw new FreshRSS_Feed_Exception('XML+XPath parsing failed for [' . $feed->url(false) . ']');
|
||||
}
|
||||
} else {
|
||||
$simplePie = $feed->load(false, $isNewFeed);
|
||||
}
|
||||
@@ -949,7 +955,7 @@ class FreshRSS_feed_Controller extends FreshRSS_ActionController {
|
||||
$this->view->htmlContent = $fullContent;
|
||||
} else {
|
||||
$this->view->selectorSuccess = false;
|
||||
$this->view->htmlContent = $entry->content();
|
||||
$this->view->htmlContent = $entry->content(false);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$this->view->fatalError = _t('feedback.sub.feed.selector_preview.http_error');
|
||||
|
||||
@@ -21,8 +21,6 @@ class FreshRSS_importExport_Controller extends FreshRSS_ActionController {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
$this->entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
$this->feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
}
|
||||
|
||||
2
app/Controllers/indexController.php
Executable file → Normal file
2
app/Controllers/indexController.php
Executable file → Normal file
@@ -237,8 +237,6 @@ class FreshRSS_index_Controller extends FreshRSS_ActionController {
|
||||
return;
|
||||
}
|
||||
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
// No layout for OPML output.
|
||||
$this->view->_layout(false);
|
||||
header('Content-Type: application/xml; charset=utf-8');
|
||||
|
||||
8
app/Controllers/javascriptController.php
Executable file → Normal file
8
app/Controllers/javascriptController.php
Executable file → Normal file
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
|
||||
class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
|
||||
public function firstAction() {
|
||||
public function firstAction(): void {
|
||||
$this->view->_layout(false);
|
||||
}
|
||||
|
||||
public function actualizeAction() {
|
||||
public function actualizeAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
Minz_Session::_param('actualize_feeds', false);
|
||||
|
||||
@@ -16,7 +16,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
|
||||
$this->view->feeds = $feedDAO->listFeedsOrderUpdate(FreshRSS_Context::$user_conf->ttl_default);
|
||||
}
|
||||
|
||||
public function nbUnreadsPerFeedAction() {
|
||||
public function nbUnreadsPerFeedAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
$catDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$this->view->categories = $catDAO->listCategories(true, false);
|
||||
@@ -25,7 +25,7 @@ class FreshRSS_javascript_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
|
||||
//For Web-form login
|
||||
public function nonceAction() {
|
||||
public function nonceAction(): void {
|
||||
header('Content-Type: application/json; charset=UTF-8');
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s \G\M\T'));
|
||||
header('Expires: 0');
|
||||
|
||||
@@ -10,7 +10,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
* the common boiler plate for every action. It is triggered by the
|
||||
* underlying framework.
|
||||
*/
|
||||
public function firstAction() {
|
||||
public function firstAction(): void {
|
||||
if (!FreshRSS_Auth::hasAccess()) {
|
||||
Minz_Error::error(403);
|
||||
}
|
||||
@@ -32,27 +32,6 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
FreshRSS_View::prependTitle(_t('admin.stats.title') . ' · ');
|
||||
}
|
||||
|
||||
private function convertToSeries($data) {
|
||||
$series = array();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$series[] = array($key, $value);
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
private function convertToPieSeries($data) {
|
||||
$series = array();
|
||||
|
||||
foreach ($data as $value) {
|
||||
$value['data'] = array(array(0, (int) $value['data']));
|
||||
$series[] = $value;
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
/**
|
||||
* This action handles the statistic main page.
|
||||
*
|
||||
@@ -64,7 +43,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
* - number of article by category (entryByCategory)
|
||||
* - list of most prolific feed (topFeed)
|
||||
*/
|
||||
public function indexAction() {
|
||||
public function indexAction(): void {
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/vendor/chart.min.js?' . @filemtime(PUBLIC_PATH . '/scripts/vendor/chart.min.js')));
|
||||
|
||||
@@ -94,7 +73,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
|
||||
$last30DaysLabels = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days'));
|
||||
$last30DaysLabels[$i] = date('d.m.Y', strtotime((-30 + $i) . ' days') ?: null);
|
||||
}
|
||||
|
||||
$this->view->last30DaysLabels = $last30DaysLabels;
|
||||
@@ -106,9 +85,9 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
* to use the subscription controller to save it,
|
||||
* but shows the stats idle page
|
||||
*/
|
||||
public function feedAction() {
|
||||
$id = Minz_Request::param('id');
|
||||
$ajax = Minz_Request::param('ajax');
|
||||
public function feedAction(): void {
|
||||
$id = '' . Minz_Request::param('id', '');
|
||||
$ajax = '' . Minz_Request::param('ajax', '');
|
||||
if ($ajax) {
|
||||
$url_redirect = array('c' => 'subscription', 'a' => 'feed', 'params' => array('id' => $id, 'from' => 'stats', 'ajax' => $ajax));
|
||||
} else {
|
||||
@@ -131,7 +110,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
* - last month
|
||||
* - last week
|
||||
*/
|
||||
public function idleAction() {
|
||||
public function idleAction(): void {
|
||||
FreshRSS_View::appendScript(Minz_Url::display('/scripts/feed.js?' . @filemtime(PUBLIC_PATH . '/scripts/feed.js')));
|
||||
$feed_dao = FreshRSS_Factory::createFeedDao();
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
@@ -216,7 +195,7 @@ class FreshRSS_stats_Controller extends FreshRSS_ActionController {
|
||||
* @todo verify that the metrics used here make some sense. Especially
|
||||
* for the average.
|
||||
*/
|
||||
public function repartitionAction() {
|
||||
public function repartitionAction(): void {
|
||||
$statsDAO = FreshRSS_Factory::createStatsDAO();
|
||||
$categoryDAO = FreshRSS_Factory::createCategoryDao();
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
@@ -118,8 +118,6 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
$httpAuth = $user . ':' . $pass;
|
||||
}
|
||||
|
||||
$cat = intval(Minz_Request::param('category', 0));
|
||||
|
||||
$feed->_ttl(intval(Minz_Request::param('ttl', FreshRSS_Feed::TTL_DEFAULT)));
|
||||
$feed->_mute(boolval(Minz_Request::param('mute', false)));
|
||||
|
||||
@@ -149,7 +147,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
$proxy_address = Minz_Request::param('curl_params', '');
|
||||
$proxy_type = Minz_Request::param('proxy_type', '');
|
||||
$opts = [];
|
||||
if ($proxy_address !== '' && $proxy_type !== '' && in_array($proxy_type, [0, 2, 4, 5, 6, 7])) {
|
||||
if ($proxy_type !== '') {
|
||||
$opts[CURLOPT_PROXY] = $proxy_address;
|
||||
$opts[CURLOPT_PROXYTYPE] = intval($proxy_type);
|
||||
}
|
||||
@@ -205,7 +203,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
$feed->_filtersAction('read', preg_split('/[\n\r]+/', Minz_Request::param('filteractions_read', '')));
|
||||
|
||||
$feed->_kind(intval(Minz_Request::param('feed_kind', FreshRSS_Feed::KIND_RSS)));
|
||||
if ($feed->kind() == FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
if ($feed->kind() === FreshRSS_Feed::KIND_HTML_XPATH || $feed->kind() === FreshRSS_Feed::KIND_XML_XPATH) {
|
||||
$xPathSettings = [];
|
||||
if (Minz_Request::param('xPathItem', '') != '') $xPathSettings['item'] = Minz_Request::param('xPathItem', '', true);
|
||||
if (Minz_Request::param('xPathItemTitle', '') != '') $xPathSettings['itemTitle'] = Minz_Request::param('xPathItemTitle', '', true);
|
||||
@@ -230,7 +228,7 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
'description' => sanitizeHTML(Minz_Request::param('description', '', true)),
|
||||
'website' => checkUrl(Minz_Request::param('website', '')),
|
||||
'url' => checkUrl(Minz_Request::param('url', '')),
|
||||
'category' => $cat,
|
||||
'category' => intval(Minz_Request::param('category', 0)),
|
||||
'pathEntries' => Minz_Request::param('path_entries', ''),
|
||||
'priority' => intval(Minz_Request::param('priority', FreshRSS_Feed::PRIORITY_MAIN_STREAM)),
|
||||
'httpAuth' => $httpAuth,
|
||||
@@ -258,12 +256,18 @@ class FreshRSS_subscription_Controller extends FreshRSS_ActionController {
|
||||
$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id));
|
||||
}
|
||||
|
||||
if ($feedDAO->updateFeed($id, $values) !== false) {
|
||||
$feed->_categoryId($cat);
|
||||
if ($values['url'] != '' && $feedDAO->updateFeed($id, $values) !== false) {
|
||||
$feed->_categoryId($values['category']);
|
||||
// update url and website values for faviconPrepare
|
||||
$feed->_url($values['url'], false);
|
||||
$feed->_website($values['website'], false);
|
||||
$feed->faviconPrepare();
|
||||
|
||||
Minz_Request::good(_t('feedback.sub.feed.updated'), $url_redirect);
|
||||
} else {
|
||||
if ($values['url'] == '') {
|
||||
Minz_Log::warning('Invalid feed URL!');
|
||||
}
|
||||
Minz_Request::bad(_t('feedback.sub.feed.error'), $url_redirect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
public static function migrateToGitEdge() {
|
||||
$errorMessage = 'Error during git checkout to edge branch. Please change branch manually!';
|
||||
|
||||
if (!is_writable(FRESHRSS_PATH . '/.git/')) {
|
||||
if (!is_writable(FRESHRSS_PATH . '/.git/config')) {
|
||||
throw new Exception($errorMessage);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
if ($return != 0) {
|
||||
throw new Exception($errorMessage);
|
||||
}
|
||||
$line = is_array($output) ? implode('', $output) : $output;
|
||||
$line = implode('', $output);
|
||||
if ($line !== 'master' && $line !== 'dev') {
|
||||
return true; // not on master or dev, nothing to do
|
||||
}
|
||||
@@ -54,14 +54,14 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
$output = [];
|
||||
exec('git status -sb --porcelain remote', $output, $return);
|
||||
} else {
|
||||
$line = is_array($output) ? implode('; ', $output) : $output;
|
||||
$line = implode('; ', $output);
|
||||
Minz_Log::warning('git fetch warning: ' . $line);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
Minz_Log::warning('git fetch error: ' . $e->getMessage());
|
||||
}
|
||||
chdir($cwd);
|
||||
$line = is_array($output) ? implode('; ', $output) : $output;
|
||||
$line = implode('; ', $output);
|
||||
return $line == '' ||
|
||||
strpos($line, '[behind') !== false || strpos($line, '[ahead') !== false || strpos($line, '[gone') !== false;
|
||||
}
|
||||
@@ -118,7 +118,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
if ($version == '') {
|
||||
$version = 'unknown';
|
||||
}
|
||||
if (is_writable(FRESHRSS_PATH)) {
|
||||
if (touch(FRESHRSS_PATH . '/index.html')) {
|
||||
$this->view->update_to_apply = true;
|
||||
$this->view->message = array(
|
||||
'status' => 'good',
|
||||
@@ -217,7 +217,7 @@ class FreshRSS_update_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
|
||||
public function applyAction() {
|
||||
if (!file_exists(UPDATE_FILENAME) || !is_writable(FRESHRSS_PATH) || Minz_Configuration::get('system')->disable_update) {
|
||||
if (FreshRSS_Context::$system_conf->disable_update || !file_exists(UPDATE_FILENAME) || !touch(FRESHRSS_PATH . '/index.html')) {
|
||||
Minz_Request::forward(array('c' => 'update'), true);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,7 +242,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
|
||||
}
|
||||
if ($ok) {
|
||||
if (!is_dir($homeDir)) {
|
||||
mkdir($homeDir);
|
||||
mkdir($homeDir, 0770, true);
|
||||
}
|
||||
$ok &= (file_put_contents($configPath, "<?php\n return " . var_export($userConfig, true) . ';') !== false);
|
||||
}
|
||||
@@ -344,6 +344,7 @@ class FreshRSS_user_Controller extends FreshRSS_ActionController {
|
||||
|
||||
$ok = self::createUser($new_user_name, $email, $passwordPlain, array(
|
||||
'language' => Minz_Request::param('new_user_language', FreshRSS_Context::$user_conf->language),
|
||||
'timezone' => Minz_Request::param('new_user_timezone', ''),
|
||||
'is_admin' => Minz_Request::paramBoolean('new_user_is_admin'),
|
||||
'enabled' => true,
|
||||
));
|
||||
|
||||
@@ -18,7 +18,7 @@ class FreshRSS extends Minz_FrontController {
|
||||
* - Init notifications
|
||||
* - Enable user extensions (need all the other initializations)
|
||||
*/
|
||||
public function init() {
|
||||
public function init(): void {
|
||||
if (!isset($_SESSION)) {
|
||||
Minz_Session::init('FreshRSS');
|
||||
}
|
||||
@@ -71,10 +71,10 @@ class FreshRSS extends Minz_FrontController {
|
||||
Minz_ExtensionManager::callHook('freshrss_init');
|
||||
}
|
||||
|
||||
private static function initAuth() {
|
||||
private static function initAuth(): void {
|
||||
FreshRSS_Auth::init();
|
||||
if (Minz_Request::isPost()) {
|
||||
if (!(FreshRSS_Auth::isCsrfOk() ||
|
||||
if (FreshRSS_Context::$system_conf == null || !(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'
|
||||
@@ -92,21 +92,30 @@ class FreshRSS extends Minz_FrontController {
|
||||
}
|
||||
}
|
||||
|
||||
private static function initI18n() {
|
||||
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;
|
||||
$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 : '';
|
||||
if ($timezone == '') {
|
||||
$timezone = FreshRSS_Context::defaultTimeZone();
|
||||
}
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
private static function getThemeFileUrl($theme_id, $filename) {
|
||||
private static function getThemeFileUrl(string $theme_id, string $filename): string {
|
||||
$filetime = @filemtime(PUBLIC_PATH . '/themes/' . $theme_id . '/' . $filename);
|
||||
return '/themes/' . $theme_id . '/' . $filename . '?' . $filetime;
|
||||
}
|
||||
|
||||
public static function loadStylesAndScripts() {
|
||||
public static function loadStylesAndScripts(): void {
|
||||
if (FreshRSS_Context::$user_conf == null) {
|
||||
return;
|
||||
}
|
||||
$theme = FreshRSS_Themes::load(FreshRSS_Context::$user_conf->theme);
|
||||
if ($theme) {
|
||||
foreach(array_reverse($theme['files']) as $file) {
|
||||
@@ -140,22 +149,23 @@ class FreshRSS extends Minz_FrontController {
|
||||
FreshRSS_View::prependScript(Minz_Url::display('/scripts/main.js?' . @filemtime(PUBLIC_PATH . '/scripts/main.js')));
|
||||
}
|
||||
|
||||
private static function loadNotifications() {
|
||||
private static function loadNotifications(): void {
|
||||
$notif = Minz_Request::getNotification();
|
||||
if ($notif) {
|
||||
FreshRSS_View::_param('notification', $notif);
|
||||
}
|
||||
}
|
||||
|
||||
public static function preLayout() {
|
||||
public static function preLayout(): void {
|
||||
header("X-Content-Type-Options: nosniff");
|
||||
|
||||
FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
|
||||
self::loadStylesAndScripts();
|
||||
}
|
||||
|
||||
private static function checkEmailValidated() {
|
||||
$email_not_verified = FreshRSS_Auth::hasAccess() && FreshRSS_Context::$user_conf->email_validation_token !== '';
|
||||
private static function checkEmailValidated(): void {
|
||||
$email_not_verified = FreshRSS_Auth::hasAccess() &&
|
||||
FreshRSS_Context::$user_conf !== null && FreshRSS_Context::$user_conf->email_validation_token !== '';
|
||||
$action_is_allowed = (
|
||||
Minz_Request::is('user', 'validateEmail') ||
|
||||
Minz_Request::is('user', 'sendValidationEmail') ||
|
||||
|
||||
@@ -118,8 +118,9 @@ class FreshRSS_BooleanSearch {
|
||||
$nextOperator = 'AND';
|
||||
while ($i < $length) {
|
||||
$c = $input[$i];
|
||||
$backslashed = $i >= 1 ? $input[$i - 1] === '\\' : false;
|
||||
|
||||
if ($c === '(') {
|
||||
if ($c === '(' && !$backslashed) {
|
||||
$hasParenthesis = true;
|
||||
|
||||
$before = trim($before);
|
||||
@@ -164,11 +165,12 @@ class FreshRSS_BooleanSearch {
|
||||
$i++;
|
||||
while ($i < $length) {
|
||||
$c = $input[$i];
|
||||
if ($c === '(') {
|
||||
$backslashed = $input[$i - 1] === '\\';
|
||||
if ($c === '(' && !$backslashed) {
|
||||
// One nested level deeper
|
||||
$parentheses++;
|
||||
$sub .= $c;
|
||||
} elseif ($c === ')') {
|
||||
} elseif ($c === ')' && !$backslashed) {
|
||||
$parentheses--;
|
||||
if ($parentheses === 0) {
|
||||
// Found the matching closing parenthesis
|
||||
|
||||
@@ -103,9 +103,7 @@ class FreshRSS_Category extends Minz_Model {
|
||||
$this->hasFeedsWithError |= $feed->inError();
|
||||
}
|
||||
|
||||
usort($this->feeds, function ($a, $b) {
|
||||
return strnatcasecmp($a->name(), $b->name());
|
||||
});
|
||||
$this->sortFeeds();
|
||||
}
|
||||
|
||||
return $this->feeds;
|
||||
@@ -144,6 +142,7 @@ class FreshRSS_Category extends Minz_Model {
|
||||
}
|
||||
|
||||
$this->feeds = $values;
|
||||
$this->sortFeeds();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,6 +154,8 @@ class FreshRSS_Category extends Minz_Model {
|
||||
$this->feeds = [];
|
||||
}
|
||||
$this->feeds[] = $feed;
|
||||
|
||||
$this->sortFeeds();
|
||||
}
|
||||
|
||||
public function _attributes($key, $value) {
|
||||
@@ -194,7 +195,7 @@ class FreshRSS_Category extends Minz_Model {
|
||||
} else {
|
||||
$dryRunCategory = new FreshRSS_Category();
|
||||
$importService = new FreshRSS_Import_Service();
|
||||
$importService->importOpml($opml, $dryRunCategory, true, true);
|
||||
$importService->importOpml($opml, $dryRunCategory, true);
|
||||
if ($importService->lastStatus()) {
|
||||
$feedDAO = FreshRSS_Factory::createFeedDao();
|
||||
|
||||
@@ -245,4 +246,10 @@ class FreshRSS_Category extends Minz_Model {
|
||||
|
||||
return $ok;
|
||||
}
|
||||
|
||||
private function sortFeeds() {
|
||||
usort($this->feeds, static function ($a, $b) {
|
||||
return strnatcasecmp($a->name(), $b->name());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ SQL;
|
||||
return $categories;
|
||||
}
|
||||
|
||||
uasort($categories, function ($a, $b) {
|
||||
uasort($categories, static function ($a, $b) {
|
||||
$aPosition = $a->attributes('position');
|
||||
$bPosition = $b->attributes('position');
|
||||
if ($aPosition === $bPosition) {
|
||||
@@ -310,9 +310,9 @@ SQL;
|
||||
}
|
||||
|
||||
/** @return array<FreshRSS_Category> */
|
||||
public function listCategoriesOrderUpdate(int $defaultCacheDuration = 86400, int $limit = 0) {
|
||||
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 ' . intval($limit));
|
||||
. ($limit < 1 ? '' : ' LIMIT ' . $limit);
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($stm &&
|
||||
$stm->bindValue(':kind', FreshRSS_Category::KIND_DYNAMIC_OPML, PDO::PARAM_INT) &&
|
||||
@@ -387,7 +387,7 @@ SQL;
|
||||
return $res[0]['count'];
|
||||
}
|
||||
|
||||
public function countFeed($id) {
|
||||
public function countFeed(int $id) {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_feed` WHERE category=:id';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
@@ -396,7 +396,7 @@ SQL;
|
||||
return $res[0]['count'];
|
||||
}
|
||||
|
||||
public function countNotRead($id) {
|
||||
public function countNotRead(int $id) {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_entry` e INNER JOIN `_feed` f ON e.id_feed=f.id WHERE category=:id AND e.is_read=0';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
@@ -409,7 +409,7 @@ SQL;
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
* @param int $feed_id
|
||||
*/
|
||||
public static function findFeed($categories, $feed_id) {
|
||||
public static function findFeed(array $categories, int $feed_id) {
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
if ($feed->id() === $feed_id) {
|
||||
@@ -422,9 +422,8 @@ SQL;
|
||||
|
||||
/**
|
||||
* @param array<FreshRSS_Category> $categories
|
||||
* @param int $minPriority
|
||||
*/
|
||||
public static function CountUnreads($categories, $minPriority = 0) {
|
||||
public static function countUnread(array $categories, int $minPriority = 0): int {
|
||||
$n = 0;
|
||||
foreach ($categories as $category) {
|
||||
foreach ($category->feeds() as $feed) {
|
||||
|
||||
@@ -234,6 +234,13 @@ class FreshRSS_ConfigurationSetter {
|
||||
$data['sticky_post'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
private function _darkMode(&$data, $value) {
|
||||
if (!in_array($value, [ 'no', 'auto'], true)) {
|
||||
$value = 'no';
|
||||
}
|
||||
$data['darkMode'] = $value;
|
||||
}
|
||||
|
||||
private function _bottomline_date(&$data, $value) {
|
||||
$data['bottomline_date'] = $this->handleBool($value);
|
||||
}
|
||||
|
||||
@@ -58,12 +58,7 @@ class FreshRSS_Context {
|
||||
public static function initSystem($reload = false) {
|
||||
if ($reload || FreshRSS_Context::$system_conf == null) {
|
||||
//TODO: Keep in session what we need instead of always reloading from disk
|
||||
Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
|
||||
/**
|
||||
* @var FreshRSS_SystemConfiguration $system_conf
|
||||
*/
|
||||
$system_conf = Minz_Configuration::get('system');
|
||||
FreshRSS_Context::$system_conf = $system_conf;
|
||||
FreshRSS_Context::$system_conf = FreshRSS_SystemConfiguration::init(DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
|
||||
// Register the configuration setter for the system configuration
|
||||
$configurationSetter = new FreshRSS_ConfigurationSetter();
|
||||
FreshRSS_Context::$system_conf->_configurationSetter($configurationSetter);
|
||||
@@ -88,17 +83,12 @@ class FreshRSS_Context {
|
||||
(!$userMustExist || FreshRSS_user_Controller::userExists($username))) {
|
||||
try {
|
||||
//TODO: Keep in session what we need instead of always reloading from disk
|
||||
Minz_Configuration::register('user',
|
||||
FreshRSS_Context::$user_conf = FreshRSS_UserConfiguration::init(
|
||||
USERS_PATH . '/' . $username . '/config.php',
|
||||
FRESHRSS_PATH . '/config-user.default.php',
|
||||
FreshRSS_Context::$system_conf->configurationSetter());
|
||||
|
||||
Minz_Session::_param('currentUser', $username);
|
||||
/**
|
||||
* @var FreshRSS_UserConfiguration $user_conf
|
||||
*/
|
||||
$user_conf = Minz_Configuration::get('user');
|
||||
FreshRSS_Context::$user_conf = $user_conf;
|
||||
} catch (Exception $ex) {
|
||||
Minz_Log::warning($ex->getMessage(), USERS_PATH . '/_/' . LOG_FILENAME);
|
||||
}
|
||||
@@ -163,7 +153,7 @@ class FreshRSS_Context {
|
||||
// Update number of read / unread variables.
|
||||
$entryDAO = FreshRSS_Factory::createEntryDao();
|
||||
self::$total_starred = $entryDAO->countUnreadReadFavorites();
|
||||
self::$total_unread = FreshRSS_CategoryDAO::CountUnreads(
|
||||
self::$total_unread = FreshRSS_CategoryDAO::countUnread(
|
||||
self::$categories, 1
|
||||
);
|
||||
|
||||
@@ -510,4 +500,8 @@ class FreshRSS_Context {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function defaultTimeZone(): string {
|
||||
$timezone = ini_get('date.timezone');
|
||||
return $timezone != '' ? $timezone : 'UTC';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class FreshRSS_Days {
|
||||
const TODAY = 0;
|
||||
const YESTERDAY = 1;
|
||||
const BEFORE_YESTERDAY = 2;
|
||||
public const TODAY = 0;
|
||||
public const YESTERDAY = 1;
|
||||
public const BEFORE_YESTERDAY = 2;
|
||||
}
|
||||
|
||||
@@ -17,10 +17,14 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
*/
|
||||
private $guid;
|
||||
|
||||
/** @var string */
|
||||
private $title;
|
||||
private $authors;
|
||||
/** @var string */
|
||||
private $content;
|
||||
/** @var string */
|
||||
private $link;
|
||||
/** @var int */
|
||||
private $date;
|
||||
private $date_added = 0; //In microseconds
|
||||
/**
|
||||
@@ -67,14 +71,16 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
$dao['content'] = '';
|
||||
}
|
||||
if (!empty($dao['thumbnail'])) {
|
||||
$dao['content'] .= '<p class="enclosure-content"><img src="' . $dao['thumbnail'] . '" alt="" /></p>';
|
||||
$dao['attributes']['thumbnail'] = [
|
||||
'url' => $dao['thumbnail'],
|
||||
];
|
||||
}
|
||||
$entry = new FreshRSS_Entry(
|
||||
$dao['id_feed'] ?? 0,
|
||||
$dao['guid'] ?? '',
|
||||
$dao['title'] ?? '',
|
||||
$dao['author'] ?? '',
|
||||
$dao['content'] ?? '',
|
||||
$dao['content'],
|
||||
$dao['link'] ?? '',
|
||||
$dao['date'] ?? 0,
|
||||
$dao['is_read'] ?? false,
|
||||
@@ -116,15 +122,117 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
return $this->authors;
|
||||
}
|
||||
}
|
||||
public function content(): string {
|
||||
return $this->content;
|
||||
|
||||
/**
|
||||
* Basic test without ambition to catch all cases such as unquoted addresses, variants of entities, HTML comments, etc.
|
||||
*/
|
||||
private static function containsLink(string $html, string $link): bool {
|
||||
return preg_match('/(?P<delim>[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1;
|
||||
}
|
||||
|
||||
/** @return array<array<string,string>> */
|
||||
public function enclosures(bool $searchBodyImages = false): array {
|
||||
$results = [];
|
||||
private static function enclosureIsImage(array $enclosure): bool {
|
||||
$elink = $enclosure['url'] ?? '';
|
||||
$length = $enclosure['length'] ?? 0;
|
||||
$medium = $enclosure['medium'] ?? '';
|
||||
$mime = $enclosure['type'] ?? '';
|
||||
|
||||
return $elink != '' && $medium === 'image' || strpos($mime, 'image') === 0 ||
|
||||
($mime == '' && $length == 0 && preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $withEnclosures Set to true to include the enclosures in the returned HTML, false otherwise.
|
||||
* @param bool $allowDuplicateEnclosures Set to false to remove obvious enclosure duplicates (based on simple string comparison), true otherwise.
|
||||
* @return string HTML content
|
||||
*/
|
||||
public function content(bool $withEnclosures = true, bool $allowDuplicateEnclosures = false): string {
|
||||
if (!$withEnclosures) {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
$content = $this->content;
|
||||
|
||||
$thumbnail = $this->attributes('thumbnail');
|
||||
if (!empty($thumbnail['url'])) {
|
||||
$elink = $thumbnail['url'];
|
||||
if ($allowDuplicateEnclosures || !self::containsLink($content, $elink)) {
|
||||
$content .= <<<HTML
|
||||
<figure class="enclosure">
|
||||
<p class="enclosure-content">
|
||||
<img class="enclosure-thumbnail" src="{$elink}" alt="" />
|
||||
</p>
|
||||
</figure>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
$attributeEnclosures = $this->attributes('enclosures');
|
||||
if (empty($attributeEnclosures)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
foreach ($attributeEnclosures as $enclosure) {
|
||||
$elink = $enclosure['url'] ?? '';
|
||||
if ($elink == '') {
|
||||
continue;
|
||||
}
|
||||
if (!$allowDuplicateEnclosures && self::containsLink($content, $elink)) {
|
||||
continue;
|
||||
}
|
||||
$credit = $enclosure['credit'] ?? '';
|
||||
$description = $enclosure['description'] ?? '';
|
||||
$length = $enclosure['length'] ?? 0;
|
||||
$medium = $enclosure['medium'] ?? '';
|
||||
$mime = $enclosure['type'] ?? '';
|
||||
$thumbnails = $enclosure['thumbnails'] ?? [];
|
||||
$etitle = $enclosure['title'] ?? '';
|
||||
|
||||
$content .= '<figure class="enclosure">';
|
||||
|
||||
foreach ($thumbnails as $thumbnail) {
|
||||
$content .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" title="' . $etitle . '" /></p>';
|
||||
}
|
||||
|
||||
if (self::enclosureIsImage($enclosure)) {
|
||||
$content .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" title="' . $etitle . '" /></p>';
|
||||
} elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
|
||||
$content .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
|
||||
. ($length == null ? '' : '" data-length="' . intval($length))
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. '" controls="controls" title="' . $etitle . '"></audio> <a download="" href="' . $elink . '">💾</a></p>';
|
||||
} elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
|
||||
$content .= '<p class="enclosure-content"><video preload="none" src="' . $elink
|
||||
. ($length == null ? '' : '" data-length="' . intval($length))
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. '" controls="controls" title="' . $etitle . '"></video> <a download="" href="' . $elink . '">💾</a></p>';
|
||||
} else { //e.g. application, text, unknown
|
||||
$content .= '<p class="enclosure-content"><a download="" href="' . $elink
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
|
||||
. '" title="' . $etitle . '">💾</a></p>';
|
||||
}
|
||||
|
||||
if ($credit != '') {
|
||||
$content .= '<p class="enclosure-credits">© ' . $credit . '</p>';
|
||||
}
|
||||
if ($description != '') {
|
||||
$content .= '<figcaption class="enclosure-description">' . $description . '</figcaption>';
|
||||
}
|
||||
$content .= "</figure>\n";
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/** @return iterable<array<string,string>> */
|
||||
public function enclosures(bool $searchBodyImages = false) {
|
||||
$attributeEnclosures = $this->attributes('enclosures');
|
||||
if (is_array($attributeEnclosures)) {
|
||||
// FreshRSS 1.20.1+: The enclosures are saved as attributes
|
||||
yield from $attributeEnclosures;
|
||||
}
|
||||
try {
|
||||
$searchEnclosures = strpos($this->content, '<p class="enclosure-content') !== false;
|
||||
$searchEnclosures = !is_array($attributeEnclosures) && (strpos($this->content, '<p class="enclosure-content') !== false);
|
||||
$searchBodyImages &= (stripos($this->content, '<img') !== false);
|
||||
$xpath = null;
|
||||
if ($searchEnclosures || $searchBodyImages) {
|
||||
@@ -133,6 +241,7 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
$xpath = new DOMXpath($dom);
|
||||
}
|
||||
if ($searchEnclosures) {
|
||||
// Legacy code for database entries < FreshRSS 1.20.1
|
||||
$enclosures = $xpath->query('//div[@class="enclosure"]/p[@class="enclosure-content"]/*[@src]');
|
||||
foreach ($enclosures as $enclosure) {
|
||||
$result = [
|
||||
@@ -148,7 +257,7 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
case 'audio': $result['medium'] = 'audio'; break;
|
||||
}
|
||||
}
|
||||
$results[] = $result;
|
||||
yield Minz_Helper::htmlspecialchars_utf8($result);
|
||||
}
|
||||
}
|
||||
if ($searchBodyImages) {
|
||||
@@ -159,26 +268,31 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
$src = $img->getAttribute('data-src');
|
||||
}
|
||||
if ($src != null) {
|
||||
$results[] = [
|
||||
$result = [
|
||||
'url' => $src,
|
||||
'alt' => $img->getAttribute('alt'),
|
||||
];
|
||||
yield Minz_Helper::htmlspecialchars_utf8($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $results;
|
||||
} catch (Exception $ex) {
|
||||
return $results;
|
||||
Minz_Log::debug(__METHOD__ . ' ' . $ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>|null
|
||||
*/
|
||||
public function thumbnail() {
|
||||
foreach ($this->enclosures(true) as $enclosure) {
|
||||
if (!empty($enclosure['url']) && empty($enclosure['type'])) {
|
||||
return $enclosure;
|
||||
public function thumbnail(bool $searchEnclosures = true) {
|
||||
$thumbnail = $this->attributes('thumbnail');
|
||||
if (!empty($thumbnail['url'])) {
|
||||
return $thumbnail;
|
||||
}
|
||||
if ($searchEnclosures) {
|
||||
foreach ($this->enclosures(true) as $enclosure) {
|
||||
if (self::enclosureIsImage($enclosure)) {
|
||||
return $enclosure;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@@ -188,6 +302,7 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
public function link(): string {
|
||||
return $this->link;
|
||||
}
|
||||
/** @return string|int */
|
||||
public function date(bool $raw = false) {
|
||||
if ($raw) {
|
||||
return $this->date;
|
||||
@@ -587,7 +702,7 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
|
||||
if ($entry) {
|
||||
// l’article existe déjà en BDD, en se contente de recharger ce contenu
|
||||
$this->content = $entry->content();
|
||||
$this->content = $entry->content(false);
|
||||
} else {
|
||||
try {
|
||||
// The article is not yet in the database, so let’s fetch it
|
||||
@@ -629,7 +744,7 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
'guid' => $this->guid(),
|
||||
'title' => $this->title(),
|
||||
'author' => $this->authors(true),
|
||||
'content' => $this->content(),
|
||||
'content' => $this->content(false),
|
||||
'link' => $this->link(),
|
||||
'date' => $this->date(true),
|
||||
'hash' => $this->hash(),
|
||||
@@ -677,7 +792,6 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
'published' => $this->date(true),
|
||||
// 'updated' => $this->date(true),
|
||||
'title' => $this->title(),
|
||||
'summary' => ['content' => $this->content()],
|
||||
'canonical' => [
|
||||
['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)],
|
||||
],
|
||||
@@ -697,13 +811,16 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
if ($mode === 'compat') {
|
||||
$item['title'] = escapeToUnicodeAlternative($this->title(), false);
|
||||
unset($item['alternate'][0]['type']);
|
||||
if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) {
|
||||
$item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8');
|
||||
}
|
||||
} elseif ($mode === 'freshrss') {
|
||||
$item['summary'] = [
|
||||
'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'),
|
||||
];
|
||||
} else {
|
||||
$item['content'] = [
|
||||
'content' => $this->content(false),
|
||||
];
|
||||
}
|
||||
if ($mode === 'freshrss') {
|
||||
$item['guid'] = $this->guid();
|
||||
unset($item['summary']);
|
||||
$item['content'] = ['content' => $this->content()];
|
||||
}
|
||||
if ($category != null && $mode !== 'freshrss') {
|
||||
$item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES);
|
||||
@@ -718,10 +835,11 @@ class FreshRSS_Entry extends Minz_Model {
|
||||
}
|
||||
}
|
||||
foreach ($this->enclosures() as $enclosure) {
|
||||
if (!empty($enclosure['url']) && !empty($enclosure['type'])) {
|
||||
if (!empty($enclosure['url'])) {
|
||||
$media = [
|
||||
'href' => $enclosure['url'],
|
||||
'type' => $enclosure['type'],
|
||||
'type' => $enclosure['type'] ?? $enclosure['medium'] ??
|
||||
(self::enclosureIsImage($enclosure) ? 'image' : ''),
|
||||
];
|
||||
if (!empty($enclosure['length'])) {
|
||||
$media['length'] = intval($enclosure['length']);
|
||||
|
||||
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function sqlConcat($s1, $s2) {
|
||||
return 'CONCAT(' . $s1 . ',' . $s2 . ')'; //MySQL
|
||||
}
|
||||
|
||||
public static function sqlHexDecode(string $x): string {
|
||||
return 'unhex(' . $x . ')';
|
||||
}
|
||||
@@ -943,8 +947,8 @@ SQL;
|
||||
}
|
||||
if ($filter->getTags()) {
|
||||
foreach ($filter->getTags() as $tag) {
|
||||
$sub_search .= 'AND ' . $alias . 'tags LIKE ? ';
|
||||
$values[] = "%{$tag}%";
|
||||
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' LIKE ? ';
|
||||
$values[] = "%{$tag} #%";
|
||||
}
|
||||
}
|
||||
if ($filter->getInurl()) {
|
||||
@@ -968,8 +972,8 @@ SQL;
|
||||
}
|
||||
if ($filter->getNotTags()) {
|
||||
foreach ($filter->getNotTags() as $tag) {
|
||||
$sub_search .= 'AND ' . $alias . 'tags NOT LIKE ? ';
|
||||
$values[] = "%{$tag}%";
|
||||
$sub_search .= 'AND ' . static::sqlConcat('TRIM(' . $alias . 'tags) ', " ' #'") . ' NOT LIKE ? ';
|
||||
$values[] = "%{$tag} #%";
|
||||
}
|
||||
}
|
||||
if ($filter->getNotInurl()) {
|
||||
@@ -1161,10 +1165,12 @@ SQL;
|
||||
}
|
||||
}
|
||||
|
||||
public function listByIds($ids, $order = 'DESC') {
|
||||
/** @param array<string> $ids */
|
||||
public function listByIds(array $ids, string $order = 'DESC') {
|
||||
if (count($ids) < 1) {
|
||||
yield false;
|
||||
} elseif (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
|
||||
return;
|
||||
}
|
||||
if (count($ids) > FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER) {
|
||||
// Split a query with too many variables parameters
|
||||
$idsChunks = array_chunk($ids, FreshRSS_DatabaseDAO::MAX_VARIABLE_NUMBER);
|
||||
foreach ($idsChunks as $idsChunk) {
|
||||
@@ -1191,15 +1197,16 @@ SQL;
|
||||
|
||||
/**
|
||||
* For API
|
||||
* @return array<string>
|
||||
*/
|
||||
public function listIdsWhere($type = 'a', $id = '', $state = FreshRSS_Entry::STATE_ALL,
|
||||
$order = 'DESC', $limit = 1, $firstId = '', $filters = null) {
|
||||
$order = 'DESC', $limit = 1, $firstId = '', $filters = null): array {
|
||||
list($values, $sql) = $this->sqlListWhere($type, $id, $state, $order, $limit, $firstId, $filters);
|
||||
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$stm->execute($values);
|
||||
|
||||
return $stm->fetchAll(PDO::FETCH_COLUMN, 0);
|
||||
return $stm->fetchAll(PDO::FETCH_COLUMN, 0) ?: [];
|
||||
}
|
||||
|
||||
public function listHashForFeedGuids($id_feed, $guids) {
|
||||
|
||||
@@ -10,6 +10,10 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected static function sqlConcat($s1, $s2) {
|
||||
return $s1 . '||' . $s2;
|
||||
}
|
||||
|
||||
public static function sqlHexDecode(string $x): string {
|
||||
return $x;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,11 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
* @var int
|
||||
*/
|
||||
const KIND_HTML_XPATH = 10;
|
||||
/**
|
||||
* Normal XML with XPath scraping
|
||||
* @var int
|
||||
*/
|
||||
const KIND_XML_XPATH = 15;
|
||||
/**
|
||||
* Normal JSON with XPath scraping
|
||||
* @var int
|
||||
@@ -259,13 +264,14 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
}
|
||||
public function _url(string $value, bool $validate = true) {
|
||||
$this->hash = '';
|
||||
$url = $value;
|
||||
if ($validate) {
|
||||
$value = checkUrl($value);
|
||||
$url = checkUrl($url);
|
||||
}
|
||||
if ($value == '') {
|
||||
if ($url == '') {
|
||||
throw new FreshRSS_BadUrl_Exception($value);
|
||||
}
|
||||
$this->url = $value;
|
||||
$this->url = $url;
|
||||
}
|
||||
public function _kind(int $value) {
|
||||
$this->kind = $value;
|
||||
@@ -502,61 +508,46 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
|
||||
$content = html_only_entity_decode($item->get_content());
|
||||
|
||||
if ($item->get_enclosures() != null) {
|
||||
$elinks = array();
|
||||
$attributeThumbnail = $item->get_thumbnail() ?? [];
|
||||
if (empty($attributeThumbnail['url'])) {
|
||||
$attributeThumbnail['url'] = '';
|
||||
}
|
||||
|
||||
$attributeEnclosures = [];
|
||||
if (!empty($item->get_enclosures())) {
|
||||
foreach ($item->get_enclosures() as $enclosure) {
|
||||
$elink = $enclosure->get_link();
|
||||
if ($elink != '' && empty($elinks[$elink])) {
|
||||
$content .= '<div class="enclosure">';
|
||||
|
||||
if ($enclosure->get_title() != '') {
|
||||
$content .= '<p class="enclosure-title">' . $enclosure->get_title() . '</p>';
|
||||
}
|
||||
|
||||
$enclosureContent = '';
|
||||
$elinks[$elink] = true;
|
||||
if ($elink != '') {
|
||||
$etitle = $enclosure->get_title() ?? '';
|
||||
$credit = $enclosure->get_credit() ?? null;
|
||||
$description = $enclosure->get_description() ?? '';
|
||||
$mime = strtolower($enclosure->get_type() ?? '');
|
||||
$medium = strtolower($enclosure->get_medium() ?? '');
|
||||
$height = $enclosure->get_height();
|
||||
$width = $enclosure->get_width();
|
||||
$length = $enclosure->get_length();
|
||||
if ($medium === 'image' || strpos($mime, 'image') === 0 ||
|
||||
($mime == '' && $length == null && ($width != 0 || $height != 0 || preg_match('/[.](avif|gif|jpe?g|png|svg|webp)$/i', $elink)))) {
|
||||
$enclosureContent .= '<p class="enclosure-content"><img src="' . $elink . '" alt="" /></p>';
|
||||
} elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) {
|
||||
$enclosureContent .= '<p class="enclosure-content"><audio preload="none" src="' . $elink
|
||||
. ($length == null ? '' : '" data-length="' . intval($length))
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. '" controls="controls"></audio> <a download="" href="' . $elink . '">💾</a></p>';
|
||||
} elseif ($medium === 'video' || strpos($mime, 'video') === 0) {
|
||||
$enclosureContent .= '<p class="enclosure-content"><video preload="none" src="' . $elink
|
||||
. ($length == null ? '' : '" data-length="' . intval($length))
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. '" controls="controls"></video> <a download="" href="' . $elink . '">💾</a></p>';
|
||||
} else { //e.g. application, text, unknown
|
||||
$enclosureContent .= '<p class="enclosure-content"><a download="" href="' . $elink
|
||||
. ($mime == '' ? '' : '" data-type="' . htmlspecialchars($mime, ENT_COMPAT, 'UTF-8'))
|
||||
. ($medium == '' ? '' : '" data-medium="' . htmlspecialchars($medium, ENT_COMPAT, 'UTF-8'))
|
||||
. '">💾</a></p>';
|
||||
}
|
||||
|
||||
$thumbnailContent = '';
|
||||
if ($enclosure->get_thumbnails() != null) {
|
||||
$attributeEnclosure = [
|
||||
'url' => $elink,
|
||||
];
|
||||
if ($etitle != '') $attributeEnclosure['title'] = $etitle;
|
||||
if ($credit != null) $attributeEnclosure['credit'] = $credit->get_name();
|
||||
if ($description != '') $attributeEnclosure['description'] = $description;
|
||||
if ($mime != '') $attributeEnclosure['type'] = $mime;
|
||||
if ($medium != '') $attributeEnclosure['medium'] = $medium;
|
||||
if ($length != '') $attributeEnclosure['length'] = intval($length);
|
||||
if ($height != '') $attributeEnclosure['height'] = intval($height);
|
||||
if ($width != '') $attributeEnclosure['width'] = intval($width);
|
||||
|
||||
if (!empty($enclosure->get_thumbnails())) {
|
||||
foreach ($enclosure->get_thumbnails() as $thumbnail) {
|
||||
if (empty($elinks[$thumbnail])) {
|
||||
$elinks[$thumbnail] = true;
|
||||
$thumbnailContent .= '<p><img class="enclosure-thumbnail" src="' . $thumbnail . '" alt="" /></p>';
|
||||
if ($thumbnail !== $attributeThumbnail['url']) {
|
||||
$attributeEnclosure['thumbnails'][] = $thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$content .= $thumbnailContent;
|
||||
$content .= $enclosureContent;
|
||||
|
||||
if ($enclosure->get_description() != '') {
|
||||
$content .= '<p class="enclosure-description">' . $enclosure->get_description() . '</p>';
|
||||
}
|
||||
$content .= "</div>\n";
|
||||
$attributeEnclosures[] = $attributeEnclosure;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -586,6 +577,10 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
);
|
||||
$entry->_tags($tags);
|
||||
$entry->_feed($this);
|
||||
if (!empty($attributeThumbnail['url'])) {
|
||||
$entry->_attributes('thumbnail', $attributeThumbnail);
|
||||
}
|
||||
$entry->_attributes('enclosures', $attributeEnclosures);
|
||||
$entry->hash(); //Must be computed before loading full content
|
||||
$entry->loadCompleteContent(); // Optionally load full content for truncated feeds
|
||||
|
||||
@@ -596,7 +591,7 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
/**
|
||||
* @return SimplePie|null
|
||||
*/
|
||||
public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false) {
|
||||
public function loadHtmlXpath() {
|
||||
if ($this->url == '') {
|
||||
return null;
|
||||
}
|
||||
@@ -624,8 +619,9 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), FreshRSS_Feed::KIND_HTML_XPATH);
|
||||
$html = httpGet($feedSourceUrl, $cachePath, 'html', $this->attributes());
|
||||
$cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind());
|
||||
$html = httpGet($feedSourceUrl, $cachePath,
|
||||
$this->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'xml' : 'html', $this->attributes());
|
||||
if (strlen($html) <= 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -640,7 +636,18 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
$doc = new DOMDocument();
|
||||
$doc->recover = true;
|
||||
$doc->strictErrorChecking = false;
|
||||
$doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
|
||||
switch ($this->kind()) {
|
||||
case FreshRSS_Feed::KIND_HTML_XPATH:
|
||||
$doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
break;
|
||||
case FreshRSS_Feed::KIND_XML_XPATH:
|
||||
$doc->loadXML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
$xpath = new DOMXPath($doc);
|
||||
$view->rss_title = $xPathFeedTitle == '' ? $this->name() :
|
||||
htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8');
|
||||
@@ -653,7 +660,23 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
foreach ($nodes as $node) {
|
||||
$item = [];
|
||||
$item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node);
|
||||
$item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node);
|
||||
|
||||
$item['content'] = '';
|
||||
if ($xPathItemContent != '') {
|
||||
$result = @$xpath->evaluate($xPathItemContent, $node);
|
||||
if ($result instanceof DOMNodeList) {
|
||||
// List of nodes, save as HTML
|
||||
$content = '';
|
||||
foreach ($result as $child) {
|
||||
$content .= $doc->saveHTML($child) . "\n";
|
||||
}
|
||||
$item['content'] = $content;
|
||||
} else {
|
||||
// Typed expression, save as-is
|
||||
$item['content'] = strval($result);
|
||||
}
|
||||
}
|
||||
|
||||
$item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node);
|
||||
$item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node);
|
||||
$item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node);
|
||||
@@ -679,8 +702,15 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
$item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']);
|
||||
}
|
||||
|
||||
if ($item['title'] . $item['content'] . $item['link'] != '') {
|
||||
$item = Minz_Helper::htmlspecialchars_utf8($item);
|
||||
if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') {
|
||||
// HTML-encoding/escaping of the relevant fields (all except 'content')
|
||||
foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) {
|
||||
if (!empty($item[$key])) {
|
||||
$item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]);
|
||||
}
|
||||
}
|
||||
// CDATA protection
|
||||
$item['content'] = str_replace(']]>', ']]>', $item['content']);
|
||||
$view->entries[] = FreshRSS_Entry::fromArray($item);
|
||||
}
|
||||
}
|
||||
@@ -763,8 +793,10 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string {
|
||||
$simplePie = customSimplePie($attributes);
|
||||
$filename = $simplePie->get_cache_filename($url);
|
||||
if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
if ($kind === FreshRSS_Feed::KIND_HTML_XPATH) {
|
||||
return CACHE_PATH . '/' . $filename . '.html';
|
||||
} elseif ($kind === FreshRSS_Feed::KIND_XML_XPATH) {
|
||||
return CACHE_PATH . '/' . $filename . '.xml';
|
||||
} else {
|
||||
return CACHE_PATH . '/' . $filename . '.spc';
|
||||
}
|
||||
@@ -966,14 +998,14 @@ class FreshRSS_Feed extends Minz_Model {
|
||||
$key = $hubJson['key']; //To renew our lease
|
||||
}
|
||||
} else {
|
||||
@mkdir($path, 0777, true);
|
||||
@mkdir($path, 0770, true);
|
||||
$key = sha1($path . FreshRSS_Context::$system_conf->salt);
|
||||
$hubJson = array(
|
||||
'hub' => $this->hubUrl,
|
||||
'key' => $key,
|
||||
);
|
||||
file_put_contents($hubFilename, json_encode($hubJson));
|
||||
@mkdir(PSHB_PATH . '/keys/');
|
||||
@mkdir(PSHB_PATH . '/keys/', 0770, true);
|
||||
file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', $this->selfUrl);
|
||||
$text = 'WebSub prepared for ' . $this->url;
|
||||
Minz_Log::debug($text);
|
||||
|
||||
@@ -49,11 +49,11 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
|
||||
}
|
||||
|
||||
$values = array(
|
||||
substr($valuesTmp['url'], 0, 511),
|
||||
$valuesTmp['url'],
|
||||
$valuesTmp['kind'] ?? FreshRSS_Feed::KIND_RSS,
|
||||
$valuesTmp['category'],
|
||||
mb_strcut(trim($valuesTmp['name']), 0, FreshRSS_DatabaseDAO::LENGTH_INDEX_UNICODE, 'UTF-8'),
|
||||
substr($valuesTmp['website'], 0, 255),
|
||||
$valuesTmp['website'],
|
||||
sanitizeHTML($valuesTmp['description'], '', 1023),
|
||||
$valuesTmp['lastUpdate'],
|
||||
isset($valuesTmp['priority']) ? intval($valuesTmp['priority']) : FreshRSS_Feed::PRIORITY_MAIN_STREAM,
|
||||
@@ -434,7 +434,7 @@ SQL;
|
||||
. '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `_entry` e2 WHERE e2.id_feed=`_feed`.id AND e2.is_read=0)'
|
||||
. ($id != 0 ? ' WHERE id=:id' : '');
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
if ($id != 0) {
|
||||
if ($stm && $id != 0) {
|
||||
$stm->bindParam(':id', $id, PDO::PARAM_INT);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,9 @@
|
||||
|
||||
interface FreshRSS_Searchable {
|
||||
|
||||
/**
|
||||
* @param int|string $id
|
||||
* @return Minz_Model
|
||||
*/
|
||||
public function searchById($id);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
* @property string $unsafe_autologin_enabled
|
||||
* @property-read array<string> $trusted_sources
|
||||
*/
|
||||
class FreshRSS_SystemConfiguration extends Minz_Configuration {
|
||||
final class FreshRSS_SystemConfiguration extends Minz_Configuration {
|
||||
|
||||
public static function init($config_filename, $default_filename = null): FreshRSS_SystemConfiguration {
|
||||
parent::register('system', $config_filename, $default_filename);
|
||||
return parent::get('system');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,40 +5,61 @@ 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;
|
||||
|
||||
public function __construct($name = '') {
|
||||
public function __construct(string $name = '') {
|
||||
$this->_name($name);
|
||||
}
|
||||
|
||||
public function id() {
|
||||
public function id(): int {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function _id($value) {
|
||||
/**
|
||||
* @param int|string $value
|
||||
*/
|
||||
public function _id($value): void {
|
||||
$this->id = (int)$value;
|
||||
}
|
||||
|
||||
public function name() {
|
||||
public function name(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function _name($value) {
|
||||
public function _name(string $value): void {
|
||||
$this->name = trim($value);
|
||||
}
|
||||
|
||||
public function attributes($key = '') {
|
||||
/**
|
||||
* @return mixed|string|array<string,mixed>|null
|
||||
*/
|
||||
public function attributes(string $key = '') {
|
||||
if ($key == '') {
|
||||
return $this->attributes;
|
||||
} else {
|
||||
return isset($this->attributes[$key]) ? $this->attributes[$key] : null;
|
||||
return $this->attributes[$key] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
public function _attributes($key, $value) {
|
||||
/**
|
||||
* @param mixed|string|array<string,mixed>|null $value
|
||||
*/
|
||||
public function _attributes(string $key, $value = null): void {
|
||||
if ($key == '') {
|
||||
if (is_string($value)) {
|
||||
$value = json_decode($value, true);
|
||||
@@ -53,27 +74,33 @@ class FreshRSS_Tag extends Minz_Model {
|
||||
}
|
||||
}
|
||||
|
||||
public function nbEntries() {
|
||||
public function nbEntries(): int {
|
||||
if ($this->nbEntries < 0) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->nbEntries = $tagDAO->countEntries($this->id());
|
||||
$this->nbEntries = $tagDAO->countEntries($this->id()) ?: 0;
|
||||
}
|
||||
return $this->nbEntries;
|
||||
}
|
||||
|
||||
public function _nbEntries($value) {
|
||||
/**
|
||||
* @param string|int $value
|
||||
*/
|
||||
public function _nbEntries($value): void {
|
||||
$this->nbEntries = (int)$value;
|
||||
}
|
||||
|
||||
public function nbUnread() {
|
||||
public function nbUnread(): int {
|
||||
if ($this->nbUnread < 0) {
|
||||
$tagDAO = FreshRSS_Factory::createTagDao();
|
||||
$this->nbUnread = $tagDAO->countNotRead($this->id());
|
||||
$this->nbUnread = $tagDAO->countNotRead($this->id()) ?: 0;
|
||||
}
|
||||
return $this->nbUnread;
|
||||
}
|
||||
|
||||
public function _nbUnread($value) {
|
||||
/**
|
||||
* @param string|int$value
|
||||
*/
|
||||
public function _nbUnread($value): void {
|
||||
$this->nbUnread = (int)$value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,12 +267,13 @@ SQL;
|
||||
return $newestItemUsec;
|
||||
}
|
||||
|
||||
/** @return int|false */
|
||||
public function count() {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_tag`';
|
||||
$stm = $this->pdo->query($sql);
|
||||
if ($stm !== false) {
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
return (int)$res[0]['count'];
|
||||
} else {
|
||||
$info = $this->pdo->errorInfo();
|
||||
if ($this->autoUpdateDb($info)) {
|
||||
@@ -283,16 +284,27 @@ SQL;
|
||||
}
|
||||
}
|
||||
|
||||
public function countEntries($id) {
|
||||
/**
|
||||
* @return int|false
|
||||
*/
|
||||
public function countEntries(int $id) {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` WHERE id_tag=?';
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
$values = array($id);
|
||||
$stm->execute($values);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
if (($stm = $this->pdo->prepare($sql)) !== false &&
|
||||
$stm->execute($values) &&
|
||||
($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
|
||||
return (int)$res[0]['count'];
|
||||
} else {
|
||||
$info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function countNotRead($id = null) {
|
||||
/**
|
||||
* @return int|false
|
||||
*/
|
||||
public function countNotRead(?int $id = null) {
|
||||
$sql = 'SELECT COUNT(*) AS count FROM `_entrytag` et '
|
||||
. 'INNER JOIN `_entry` e ON et.id_entry=e.id '
|
||||
. 'WHERE e.is_read=0';
|
||||
@@ -303,11 +315,15 @@ SQL;
|
||||
$values = [$id];
|
||||
}
|
||||
|
||||
$stm = $this->pdo->prepare($sql);
|
||||
|
||||
$stm->execute($values);
|
||||
$res = $stm->fetchAll(PDO::FETCH_ASSOC);
|
||||
return $res[0]['count'];
|
||||
if (($stm = $this->pdo->prepare($sql)) !== false &&
|
||||
$stm->execute($values) &&
|
||||
($res = $stm->fetchAll(PDO::FETCH_ASSOC)) !== false) {
|
||||
return (int)$res[0]['count'];
|
||||
} else {
|
||||
$info = is_object($stm) ? $stm->errorInfo() : $this->pdo->errorInfo();
|
||||
Minz_Log::error('SQL error ' . __METHOD__ . json_encode($info));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function tagEntry($id_tag, $id_entry, $checked = true) {
|
||||
|
||||
@@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model {
|
||||
static $alts = array(
|
||||
'add' => '➕', //✚
|
||||
'all' => '☰',
|
||||
'bookmark' => '✨', //★
|
||||
'bookmark-add' => '➕', //✚
|
||||
'bookmark-tag' => '📑',
|
||||
'category' => '🗂️', //☷
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
* @property-read string $is_admin
|
||||
* @property int|null $keep_history_default
|
||||
* @property string $language
|
||||
* @property string $timezone
|
||||
* @property bool $lazyload
|
||||
* @property string $mail_login
|
||||
* @property bool $mark_updated_article_unread
|
||||
@@ -52,6 +53,7 @@
|
||||
* @property bool $sides_close_article
|
||||
* @property bool $sticky_post
|
||||
* @property string $theme
|
||||
* @property string $darkMode
|
||||
* @property string $token
|
||||
* @property bool $topline_date
|
||||
* @property bool $topline_display_authors
|
||||
@@ -66,6 +68,10 @@
|
||||
* @property string $view_mode
|
||||
* @property array<string,mixed> $volatile
|
||||
*/
|
||||
class FreshRSS_UserConfiguration extends Minz_Configuration {
|
||||
final class FreshRSS_UserConfiguration extends Minz_Configuration {
|
||||
|
||||
public static function init($config_filename, $default_filename = null, $configuration_setter = null): FreshRSS_UserConfiguration {
|
||||
parent::register('user', $config_filename, $default_filename, $configuration_setter);
|
||||
return parent::get('user');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,26 +8,35 @@
|
||||
*/
|
||||
class FreshRSS_UserQuery {
|
||||
|
||||
/** @var bool */
|
||||
private $deprecated = false;
|
||||
private $get;
|
||||
private $get_name;
|
||||
private $get_type;
|
||||
private $name;
|
||||
private $order;
|
||||
/** @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;
|
||||
private $state;
|
||||
private $url;
|
||||
/** @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;
|
||||
|
||||
/**
|
||||
* @param array<string,string> $query
|
||||
* @param FreshRSS_Searchable $feed_dao
|
||||
* @param FreshRSS_Searchable $category_dao
|
||||
*/
|
||||
public function __construct($query, FreshRSS_Searchable $feed_dao = null, FreshRSS_Searchable $category_dao = null, FreshRSS_Searchable $tag_dao = null) {
|
||||
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;
|
||||
@@ -53,17 +62,17 @@ class FreshRSS_UserQuery {
|
||||
}
|
||||
// linked too deeply with the search object, need to use dependency injection
|
||||
$this->search = new FreshRSS_BooleanSearch($query['search']);
|
||||
if (isset($query['state'])) {
|
||||
$this->state = $query['state'];
|
||||
if (!empty($query['state'])) {
|
||||
$this->state = intval($query['state']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the current object to an array.
|
||||
*
|
||||
* @return array<string,string>
|
||||
* @return array<string,string|int>
|
||||
*/
|
||||
public function toArray() {
|
||||
public function toArray(): array {
|
||||
return array_filter(array(
|
||||
'get' => $this->get,
|
||||
'name' => $this->name,
|
||||
@@ -75,29 +84,27 @@ class FreshRSS_UserQuery {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the get parameter in the query string to extract its name and
|
||||
* type
|
||||
*
|
||||
* @param string $get
|
||||
* Parse the get parameter in the query string to extract its name and type
|
||||
*/
|
||||
private function parseGet($get) {
|
||||
private function parseGet(string $get): void {
|
||||
$this->get = $get;
|
||||
if (preg_match('/(?P<type>[acfst])(_(?P<id>\d+))?/', $get, $matches)) {
|
||||
$id = intval($matches['id'] ?? '0');
|
||||
switch ($matches['type']) {
|
||||
case 'a':
|
||||
$this->parseAll();
|
||||
break;
|
||||
case 'c':
|
||||
$this->parseCategory($matches['id']);
|
||||
$this->parseCategory($id);
|
||||
break;
|
||||
case 'f':
|
||||
$this->parseFeed($matches['id']);
|
||||
$this->parseFeed($id);
|
||||
break;
|
||||
case 's':
|
||||
$this->parseFavorite();
|
||||
break;
|
||||
case 't':
|
||||
$this->parseTag($matches['id']);
|
||||
$this->parseTag($id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +113,7 @@ class FreshRSS_UserQuery {
|
||||
/**
|
||||
* Parse the query string when it is an "all" query
|
||||
*/
|
||||
private function parseAll() {
|
||||
private function parseAll(): void {
|
||||
$this->get_name = 'all';
|
||||
$this->get_type = 'all';
|
||||
}
|
||||
@@ -114,11 +121,10 @@ class FreshRSS_UserQuery {
|
||||
/**
|
||||
* Parse the query string when it is a "category" query
|
||||
*
|
||||
* @param integer $id
|
||||
* @throws FreshRSS_DAO_Exception
|
||||
*/
|
||||
private function parseCategory($id) {
|
||||
if (is_null($this->category_dao)) {
|
||||
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);
|
||||
@@ -133,11 +139,10 @@ class FreshRSS_UserQuery {
|
||||
/**
|
||||
* Parse the query string when it is a "feed" query
|
||||
*
|
||||
* @param integer $id
|
||||
* @throws FreshRSS_DAO_Exception
|
||||
*/
|
||||
private function parseFeed($id) {
|
||||
if (is_null($this->feed_dao)) {
|
||||
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);
|
||||
@@ -152,10 +157,9 @@ class FreshRSS_UserQuery {
|
||||
/**
|
||||
* Parse the query string when it is a "tag" query
|
||||
*
|
||||
* @param integer $id
|
||||
* @throws FreshRSS_DAO_Exception
|
||||
*/
|
||||
private function parseTag($id) {
|
||||
private function parseTag(int $id): void {
|
||||
if ($this->tag_dao == null) {
|
||||
throw new FreshRSS_DAO_Exception('Tag DAO is not loaded in UserQuery');
|
||||
}
|
||||
@@ -171,7 +175,7 @@ class FreshRSS_UserQuery {
|
||||
/**
|
||||
* Parse the query string when it is a "favorite" query
|
||||
*/
|
||||
private function parseFavorite() {
|
||||
private function parseFavorite(): void {
|
||||
$this->get_name = 'favorite';
|
||||
$this->get_type = 'favorite';
|
||||
}
|
||||
@@ -180,20 +184,16 @@ class FreshRSS_UserQuery {
|
||||
* Check if the current user query is deprecated.
|
||||
* It is deprecated if the category or the feed used in the query are
|
||||
* not existing.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function isDeprecated() {
|
||||
public function isDeprecated(): bool {
|
||||
return $this->deprecated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user query has parameters.
|
||||
* If the type is 'all', it is considered equal to no parameters
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasParameters() {
|
||||
public function hasParameters(): bool {
|
||||
if ($this->get_type === 'all') {
|
||||
return false;
|
||||
}
|
||||
@@ -214,42 +214,40 @@ class FreshRSS_UserQuery {
|
||||
|
||||
/**
|
||||
* Check if there is a search in the search object
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function hasSearch() {
|
||||
return $this->search->getRawInput() != "";
|
||||
public function hasSearch(): bool {
|
||||
return $this->search->getRawInput() !== '';
|
||||
}
|
||||
|
||||
public function getGet() {
|
||||
public function getGet(): string {
|
||||
return $this->get;
|
||||
}
|
||||
|
||||
public function getGetName() {
|
||||
public function getGetName(): string {
|
||||
return $this->get_name;
|
||||
}
|
||||
|
||||
public function getGetType() {
|
||||
public function getGetType(): string {
|
||||
return $this->get_type;
|
||||
}
|
||||
|
||||
public function getName() {
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getOrder() {
|
||||
public function getOrder(): string {
|
||||
return $this->order;
|
||||
}
|
||||
|
||||
public function getSearch() {
|
||||
public function getSearch(): FreshRSS_BooleanSearch {
|
||||
return $this->search;
|
||||
}
|
||||
|
||||
public function getState() {
|
||||
public function getState(): int {
|
||||
return $this->state;
|
||||
}
|
||||
|
||||
public function getUrl() {
|
||||
public function getUrl(): string {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ class FreshRSS_View extends Minz_View {
|
||||
public $details;
|
||||
public $disable_aside;
|
||||
public $show_email_field;
|
||||
/** @var string */
|
||||
public $username;
|
||||
public $users;
|
||||
|
||||
|
||||
@@ -18,11 +18,11 @@ ENGINE = INNODB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `_feed` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT, -- v0.7
|
||||
`url` VARCHAR(511) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
|
||||
`url` VARCHAR(32768) CHARACTER SET latin1 COLLATE latin1_bin NOT NULL,
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`category` INT DEFAULT 0, -- 1.20.0
|
||||
`name` VARCHAR(191) NOT NULL,
|
||||
`website` VARCHAR(255) CHARACTER SET latin1 COLLATE latin1_bin,
|
||||
`website` TEXT CHARACTER SET latin1 COLLATE latin1_bin,
|
||||
`description` TEXT,
|
||||
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
|
||||
`priority` TINYINT(2) NOT NULL DEFAULT 10,
|
||||
@@ -35,7 +35,6 @@ CREATE TABLE IF NOT EXISTS `_feed` (
|
||||
`cache_nbUnreads` INT DEFAULT 0, -- v0.7
|
||||
PRIMARY KEY (`id`),
|
||||
FOREIGN KEY (`category`) REFERENCES `_category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
UNIQUE KEY (`url`), -- v0.7
|
||||
INDEX (`name`), -- v0.7
|
||||
INDEX (`priority`) -- v0.7
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
|
||||
|
||||
@@ -15,11 +15,11 @@ CREATE TABLE IF NOT EXISTS `_category` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `_feed` (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"url" VARCHAR(511) UNIQUE NOT NULL,
|
||||
"url" VARCHAR(32768) NOT NULL,
|
||||
"kind" SMALLINT DEFAULT 0, -- 1.20.0
|
||||
"category" INT DEFAULT 0, -- 1.20.0
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"website" VARCHAR(255),
|
||||
"website" VARCHAR(32768),
|
||||
"description" TEXT,
|
||||
"lastUpdate" INT DEFAULT 0,
|
||||
"priority" SMALLINT NOT NULL DEFAULT 10,
|
||||
|
||||
@@ -16,11 +16,11 @@ CREATE TABLE IF NOT EXISTS `category` (
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `feed` (
|
||||
`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`url` VARCHAR(511) NOT NULL,
|
||||
`url` VARCHAR(32768) NOT NULL,
|
||||
`kind` SMALLINT DEFAULT 0, -- 1.20.0
|
||||
`category` INTEGER DEFAULT 0, -- 1.20.0
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`website` VARCHAR(255),
|
||||
`website` VARCHAR(32768),
|
||||
`description` TEXT,
|
||||
`lastUpdate` INT(11) DEFAULT 0, -- Until year 2038
|
||||
`priority` TINYINT(2) NOT NULL DEFAULT 10,
|
||||
@@ -31,8 +31,7 @@ CREATE TABLE IF NOT EXISTS `feed` (
|
||||
`attributes` TEXT, -- v1.11.0
|
||||
`cache_nbEntries` INT DEFAULT 0,
|
||||
`cache_nbUnreads` INT DEFAULT 0,
|
||||
FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
UNIQUE (`url`)
|
||||
FOREIGN KEY (`category`) REFERENCES `category`(`id`) ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS feed_name_index ON `feed`(`name`);
|
||||
CREATE INDEX IF NOT EXISTS feed_priority_index ON `feed`(`priority`);
|
||||
|
||||
@@ -21,6 +21,7 @@ class FreshRSS_Export_Service {
|
||||
|
||||
const FRSS_NAMESPACE = 'https://freshrss.org/opml';
|
||||
const TYPE_HTML_XPATH = 'HTML+XPath';
|
||||
const TYPE_XML_XPATH = 'XML+XPath';
|
||||
const TYPE_RSS_ATOM = 'rss';
|
||||
|
||||
/**
|
||||
@@ -43,8 +44,6 @@ class FreshRSS_Export_Service {
|
||||
* @return array First item is the filename, second item is the content
|
||||
*/
|
||||
public function generateOpml() {
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
$view = new FreshRSS_View();
|
||||
$day = date('Y-m-d');
|
||||
$view->categories = $this->category_dao->listCategories(true, true);
|
||||
|
||||
@@ -19,8 +19,6 @@ class FreshRSS_Import_Service {
|
||||
* @param string $username
|
||||
*/
|
||||
public function __construct($username = null) {
|
||||
require_once(LIB_PATH . '/lib_opml.php');
|
||||
|
||||
$this->catDAO = FreshRSS_Factory::createCategoryDao($username);
|
||||
$this->feedDAO = FreshRSS_Factory::createFeedDao($username);
|
||||
}
|
||||
@@ -34,153 +32,194 @@ class FreshRSS_Import_Service {
|
||||
* This method parses and imports an OPML file.
|
||||
*
|
||||
* @param string $opml_file the OPML file content.
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return array<FreshRSS_Category>|false an array of categories containing some feeds, or false if an error occurred.
|
||||
* @param FreshRSS_Category|null $forced_category force the feeds to be associated to this category.
|
||||
* @param boolean $dry_run true to not create categories and feeds in database.
|
||||
*/
|
||||
public function importOpml(string $opml_file, $parent_cat = null, $flatten = false, $dryRun = false) {
|
||||
public function importOpml(string $opml_file, $forced_category = null, $dry_run = false) {
|
||||
$this->lastStatus = true;
|
||||
$opml_array = array();
|
||||
try {
|
||||
$opml_array = libopml_parse_string($opml_file, false);
|
||||
} catch (LibOPML_Exception $e) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML parsing: ' . $e->getMessage() . "\n");
|
||||
} else {
|
||||
Minz_Log::warning($e->getMessage());
|
||||
}
|
||||
$libopml = new \marienfressinaud\LibOpml\LibOpml(false);
|
||||
$opml_array = $libopml->parseString($opml_file);
|
||||
} catch (\marienfressinaud\LibOpml\Exception $e) {
|
||||
self::log($e->getMessage());
|
||||
$this->lastStatus = false;
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
return $this->addOpmlElements($opml_array['body'], $parent_cat, $flatten, $dryRun);
|
||||
}
|
||||
$this->catDAO->checkDefault();
|
||||
$default_category = $this->catDAO->getDefault();
|
||||
if (!$default_category) {
|
||||
self::log('Cannot get the default category');
|
||||
$this->lastStatus = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML file based on its body.
|
||||
*
|
||||
* @param array $opml_elements an OPML element (body or outline).
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return array<FreshRSS_Category> an array of categories containing some feeds
|
||||
*/
|
||||
private function addOpmlElements($opml_elements, $parent_cat = null, $flatten = false, $dryRun = false) {
|
||||
// Get the categories by names so we can use this array to retrieve
|
||||
// existing categories later.
|
||||
$categories = $this->catDAO->listCategories(false);
|
||||
$categories_by_names = [];
|
||||
foreach ($categories as $category) {
|
||||
$categories_by_names[$category->name()] = $category;
|
||||
}
|
||||
|
||||
// Get current numbers of categories and feeds, and the limits to
|
||||
// verify the user can import its categories/feeds.
|
||||
$nb_categories = count($categories);
|
||||
$nb_feeds = count($this->feedDAO->listFeeds());
|
||||
$nb_cats = count($this->catDAO->listCategories(false));
|
||||
$limits = FreshRSS_Context::$system_conf->limits;
|
||||
|
||||
//Sort with categories first
|
||||
usort($opml_elements, static function ($a, $b) {
|
||||
return strcmp(
|
||||
(isset($a['xmlUrl']) ? 'Z' : 'A') . (isset($a['text']) ? $a['text'] : ''),
|
||||
(isset($b['xmlUrl']) ? 'Z' : 'A') . (isset($b['text']) ? $b['text'] : ''));
|
||||
});
|
||||
// Process the OPML outlines to get a list of categories and a list of
|
||||
// feeds elements indexed by their categories names.
|
||||
list (
|
||||
$categories_elements,
|
||||
$categories_to_feeds,
|
||||
) = $this->loadFromOutlines($opml_array['body'], '');
|
||||
|
||||
$categories = [];
|
||||
foreach ($categories_to_feeds as $category_name => $feeds_elements) {
|
||||
$category_element = $categories_elements[$category_name] ?? null;
|
||||
|
||||
foreach ($opml_elements as $elt) {
|
||||
if (isset($elt['xmlUrl'])) {
|
||||
// If xmlUrl exists, it means it is a feed
|
||||
if (FreshRSS_Context::$isCli && $nb_feeds >= $limits['max_feeds']) {
|
||||
Minz_Log::warning(_t('feedback.sub.feed.over_max',
|
||||
$limits['max_feeds']));
|
||||
$category = null;
|
||||
if ($forced_category) {
|
||||
// If the category is forced, ignore the actual category name
|
||||
$category = $forced_category;
|
||||
} elseif (isset($categories_by_names[$category_name])) {
|
||||
// If the category already exists, get it from $categories_by_names
|
||||
$category = $categories_by_names[$category_name];
|
||||
} elseif ($category_element) {
|
||||
// Otherwise, create the category (if possible)
|
||||
$limit_reached = $nb_categories >= $limits['max_categories'];
|
||||
$can_create_category = FreshRSS_Context::$isCli || !$limit_reached;
|
||||
|
||||
if ($can_create_category) {
|
||||
$category = $this->createCategory($category_element, $dry_run);
|
||||
if ($category) {
|
||||
$categories_by_names[$category->name()] = $category;
|
||||
$nb_categories++;
|
||||
}
|
||||
} else {
|
||||
Minz_Log::warning(
|
||||
_t('feedback.sub.category.over_max', $limits['max_categories'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$category) {
|
||||
// Category can be null if the feeds weren't in a category
|
||||
// outline, or if we weren't able to create the category.
|
||||
$category = $default_category;
|
||||
}
|
||||
|
||||
// Then, create the feeds one by one and attach them to the
|
||||
// category we just got.
|
||||
foreach ($feeds_elements as $feed_element) {
|
||||
$limit_reached = $nb_feeds >= $limits['max_feeds'];
|
||||
$can_create_feed = FreshRSS_Context::$isCli || !$limit_reached;
|
||||
if (!$can_create_feed) {
|
||||
Minz_Log::warning(
|
||||
_t('feedback.sub.feed.over_max', $limits['max_feeds'])
|
||||
);
|
||||
$this->lastStatus = false;
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($this->addFeedOpml($elt, $parent_cat, $dryRun)) {
|
||||
if ($this->createFeed($feed_element, $category, $dry_run)) {
|
||||
// TODO what if the feed already exists in the database?
|
||||
$nb_feeds++;
|
||||
} else {
|
||||
$this->lastStatus = false;
|
||||
}
|
||||
} elseif (!empty($elt['text'])) {
|
||||
// No xmlUrl? It should be a category!
|
||||
$limit_reached = !$flatten && ($nb_cats >= $limits['max_categories']);
|
||||
if (!FreshRSS_Context::$isCli && $limit_reached) {
|
||||
Minz_Log::warning(_t('feedback.sub.category.over_max',
|
||||
$limits['max_categories']));
|
||||
$this->lastStatus = false;
|
||||
$flatten = true;
|
||||
}
|
||||
|
||||
$category = $this->addCategoryOpml($elt, $parent_cat, $flatten, $dryRun);
|
||||
|
||||
if ($category) {
|
||||
$nb_cats++;
|
||||
$categories[] = $category;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $categories;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML feed element.
|
||||
* Create a feed from a feed element (i.e. OPML outline).
|
||||
*
|
||||
* @param array $feed_elt an OPML element (must be a feed element).
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @return FreshRSS_Feed|null a feed.
|
||||
* @param array<string, string> $feed_elt An OPML element (must be a feed element).
|
||||
* @param FreshRSS_Category $category The category to associate to the feed.
|
||||
* @param boolean $dry_run true to not create the feed in database.
|
||||
*
|
||||
* @return FreshRSS_Feed|null The created feed, or null if it failed.
|
||||
*/
|
||||
private function addFeedOpml($feed_elt, $parent_cat, $dryRun = false) {
|
||||
if (empty($feed_elt['xmlUrl'])) {
|
||||
return null;
|
||||
}
|
||||
if ($parent_cat == null) {
|
||||
// This feed has no parent category so we get the default one
|
||||
$this->catDAO->checkDefault();
|
||||
$parent_cat = $this->catDAO->getDefault();
|
||||
if ($parent_cat == null) {
|
||||
$this->lastStatus = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// We get different useful information
|
||||
private function createFeed($feed_elt, $category, $dry_run) {
|
||||
$url = Minz_Helper::htmlspecialchars_utf8($feed_elt['xmlUrl']);
|
||||
$name = Minz_Helper::htmlspecialchars_utf8($feed_elt['text'] ?? '');
|
||||
$name = $feed_elt['text'] ?? $feed_elt['title'] ?? '';
|
||||
$name = Minz_Helper::htmlspecialchars_utf8($name);
|
||||
$website = Minz_Helper::htmlspecialchars_utf8($feed_elt['htmlUrl'] ?? '');
|
||||
$description = Minz_Helper::htmlspecialchars_utf8($feed_elt['description'] ?? '');
|
||||
|
||||
try {
|
||||
// Create a Feed object and add it in DB
|
||||
$feed = new FreshRSS_Feed($url);
|
||||
$feed->_categoryId($parent_cat->id());
|
||||
$parent_cat->addFeed($feed);
|
||||
$feed->_categoryId($category->id());
|
||||
$category->addFeed($feed);
|
||||
$feed->_name($name);
|
||||
$feed->_website($website);
|
||||
$feed->_description($description);
|
||||
|
||||
switch ($feed_elt['type'] ?? '') {
|
||||
case FreshRSS_Export_Service::TYPE_HTML_XPATH:
|
||||
switch (strtolower($feed_elt['type'] ?? '')) {
|
||||
case strtolower(FreshRSS_Export_Service::TYPE_HTML_XPATH):
|
||||
$feed->_kind(FreshRSS_Feed::KIND_HTML_XPATH);
|
||||
break;
|
||||
case FreshRSS_Export_Service::TYPE_RSS_ATOM:
|
||||
case strtolower(FreshRSS_Export_Service::TYPE_XML_XPATH):
|
||||
$feed->_kind(FreshRSS_Feed::KIND_XML_XPATH);
|
||||
break;
|
||||
case strtolower(FreshRSS_Export_Service::TYPE_RSS_ATOM):
|
||||
default:
|
||||
$feed->_kind(FreshRSS_Feed::KIND_RSS);
|
||||
break;
|
||||
}
|
||||
|
||||
$xPathSettings = [];
|
||||
foreach ($feed_elt as $key => $value) {
|
||||
if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
|
||||
switch ($key) {
|
||||
case 'cssFullContent': $feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($value['value'])); break;
|
||||
case 'cssFullContentFilter': $feed->_attributes('path_entries_filter', $value['value']); break;
|
||||
case 'filtersActionRead': $feed->_filtersAction('read', preg_split('/[\n\r]+/', $value['value'])); break;
|
||||
case 'xPathItem': $xPathSettings['item'] = $value['value']; break;
|
||||
case 'xPathItemTitle': $xPathSettings['itemTitle'] = $value['value']; break;
|
||||
case 'xPathItemContent': $xPathSettings['itemContent'] = $value['value']; break;
|
||||
case 'xPathItemUri': $xPathSettings['itemUri'] = $value['value']; break;
|
||||
case 'xPathItemAuthor': $xPathSettings['itemAuthor'] = $value['value']; break;
|
||||
case 'xPathItemTimestamp': $xPathSettings['itemTimestamp'] = $value['value']; break;
|
||||
case 'xPathItemTimeFormat': $xPathSettings['itemTimeFormat'] = $value['value']; break;
|
||||
case 'xPathItemThumbnail': $xPathSettings['itemThumbnail'] = $value['value']; break;
|
||||
case 'xPathItemCategories': $xPathSettings['itemCategories'] = $value['value']; break;
|
||||
case 'xPathItemUid': $xPathSettings['itemUid'] = $value['value']; break;
|
||||
}
|
||||
}
|
||||
if (isset($feed_elt['frss:cssFullContent'])) {
|
||||
$feed->_pathEntries(Minz_Helper::htmlspecialchars_utf8($feed_elt['frss:cssFullContent']));
|
||||
}
|
||||
|
||||
if (isset($feed_elt['frss:cssFullContentFilter'])) {
|
||||
$feed->_attributes('path_entries_filter', $feed_elt['frss:cssFullContentFilter']);
|
||||
}
|
||||
|
||||
if (isset($feed_elt['frss:filtersActionRead'])) {
|
||||
$feed->_filtersAction(
|
||||
'read',
|
||||
preg_split('/[\n\r]+/', $feed_elt['frss:filtersActionRead'])
|
||||
);
|
||||
}
|
||||
|
||||
$xPathSettings = [];
|
||||
if (isset($feed_elt['frss:xPathItem'])) {
|
||||
$xPathSettings['item'] = $feed_elt['frss:xPathItem'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemTitle'])) {
|
||||
$xPathSettings['itemTitle'] = $feed_elt['frss:xPathItemTitle'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemContent'])) {
|
||||
$xPathSettings['itemContent'] = $feed_elt['frss:xPathItemContent'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemUri'])) {
|
||||
$xPathSettings['itemUri'] = $feed_elt['frss:xPathItemUri'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemAuthor'])) {
|
||||
$xPathSettings['itemAuthor'] = $feed_elt['frss:xPathItemAuthor'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemTimestamp'])) {
|
||||
$xPathSettings['itemTimestamp'] = $feed_elt['frss:xPathItemTimestamp'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemTimeFormat'])) {
|
||||
$xPathSettings['itemTimeFormat'] = $feed_elt['frss:xPathItemTimeFormat'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemThumbnail'])) {
|
||||
$xPathSettings['itemThumbnail'] = $feed_elt['frss:xPathItemThumbnail'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemCategories'])) {
|
||||
$xPathSettings['itemCategories'] = $feed_elt['frss:xPathItemCategories'];
|
||||
}
|
||||
if (isset($feed_elt['frss:xPathItemUid'])) {
|
||||
$xPathSettings['itemUid'] = $feed_elt['frss:xPathItemUid'];
|
||||
}
|
||||
|
||||
if (!empty($xPathSettings)) {
|
||||
$feed->_attributes('xpath', $xPathSettings);
|
||||
}
|
||||
@@ -188,9 +227,11 @@ class FreshRSS_Import_Service {
|
||||
// Call the extension hook
|
||||
/** @var FreshRSS_Feed|null */
|
||||
$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
|
||||
if ($dryRun) {
|
||||
|
||||
if ($dry_run) {
|
||||
return $feed;
|
||||
}
|
||||
|
||||
if ($feed != null) {
|
||||
// addFeedObject checks if feed is already in DB
|
||||
$id = $this->feedDAO->addFeedObject($feed);
|
||||
@@ -202,81 +243,163 @@ class FreshRSS_Import_Service {
|
||||
}
|
||||
}
|
||||
} catch (FreshRSS_Feed_Exception $e) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML feed import: ' . $e->getMessage() . "\n");
|
||||
} else {
|
||||
Minz_Log::warning($e->getMessage());
|
||||
}
|
||||
self::log($e->getMessage());
|
||||
$this->lastStatus = false;
|
||||
}
|
||||
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML feed import from URL: ' .
|
||||
SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id() . "\n");
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML feed import from URL: ' .
|
||||
SimplePie_Misc::url_remove_credentials($url) . ' in category ' . $parent_cat->id());
|
||||
}
|
||||
|
||||
$clean_url = SimplePie_Misc::url_remove_credentials($url);
|
||||
self::log("Cannot create {$clean_url} feed in category {$category->name()}");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method imports an OPML category element.
|
||||
* Create and return a category.
|
||||
*
|
||||
* @param array $cat_elt an OPML element (must be a category element).
|
||||
* @param FreshRSS_Category|null $parent_cat the name of the parent category.
|
||||
* @param boolean $flatten true to disable categories, false otherwise.
|
||||
* @return FreshRSS_Category|null a new category containing some feeds, or null if no category was created, or false if an error occurred.
|
||||
* @param array<string, string> $category_element An OPML element (must be a category element).
|
||||
* @param boolean $dry_run true to not create the category in database.
|
||||
*
|
||||
* @return FreshRSS_Category|null The created category, or null if it failed.
|
||||
*/
|
||||
private function addCategoryOpml($cat_elt, $parent_cat, $flatten = false, $dryRun = false) {
|
||||
$error = false;
|
||||
$cat = null;
|
||||
if (!$flatten) {
|
||||
$catName = Minz_Helper::htmlspecialchars_utf8($cat_elt['text']);
|
||||
$cat = new FreshRSS_Category($catName);
|
||||
private function createCategory($category_element, $dry_run) {
|
||||
$name = $category_element['text'] ?? $category_element['title'] ?? '';
|
||||
$name = Minz_Helper::htmlspecialchars_utf8($name);
|
||||
$category = new FreshRSS_Category($name);
|
||||
|
||||
foreach ($cat_elt as $key => $value) {
|
||||
if (is_array($value) && !empty($value['value']) && ($value['namespace'] ?? '') === FreshRSS_Export_Service::FRSS_NAMESPACE) {
|
||||
switch ($key) {
|
||||
case 'opmlUrl':
|
||||
$opml_url = checkUrl($value['value']);
|
||||
if ($opml_url != '') {
|
||||
$cat->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$cat->_attributes('opml_url', $opml_url);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isset($category_element['frss:opmlUrl'])) {
|
||||
$opml_url = checkUrl($category_element['frss:opmlUrl']);
|
||||
if ($opml_url != '') {
|
||||
$category->_kind(FreshRSS_Category::KIND_DYNAMIC_OPML);
|
||||
$category->_attributes('opml_url', $opml_url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun) {
|
||||
$id = $this->catDAO->addCategoryObject($cat);
|
||||
if ($id == false) {
|
||||
$this->lastStatus = false;
|
||||
$error = true;
|
||||
} else {
|
||||
$cat->_id($id);
|
||||
if ($dry_run) {
|
||||
return $category;
|
||||
}
|
||||
|
||||
$id = $this->catDAO->addCategoryObject($category);
|
||||
if ($id !== false) {
|
||||
$category->_id($id);
|
||||
return $category;
|
||||
} else {
|
||||
self::log("Cannot create category {$category->name()}");
|
||||
$this->lastStatus = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of category and feed outlines by categories names.
|
||||
*
|
||||
* This method is applied to a list of outlines. It merges the different
|
||||
* list of feeds from several outlines into one array.
|
||||
*
|
||||
* @param array $outlines
|
||||
* The outlines from which to extract the outlines.
|
||||
* @param string $parent_category_name
|
||||
* The name of the parent category of the current outlines.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
private function loadFromOutlines($outlines, $parent_category_name) {
|
||||
$categories_elements = [];
|
||||
$categories_to_feeds = [];
|
||||
|
||||
foreach ($outlines as $outline) {
|
||||
// Get the categories and feeds from the child outline (it may
|
||||
// return several categories and feeds if the outline is a category).
|
||||
list (
|
||||
$outline_categories,
|
||||
$outline_categories_to_feeds,
|
||||
) = $this->loadFromOutline($outline, $parent_category_name);
|
||||
|
||||
// Then, we merge the initial arrays with the arrays returned by
|
||||
// the outline.
|
||||
$categories_elements = array_merge($categories_elements, $outline_categories);
|
||||
|
||||
foreach ($outline_categories_to_feeds as $category_name => $feeds) {
|
||||
if (!isset($categories_to_feeds[$category_name])) {
|
||||
$categories_to_feeds[$category_name] = [];
|
||||
}
|
||||
|
||||
$categories_to_feeds[$category_name] = array_merge(
|
||||
$categories_to_feeds[$category_name],
|
||||
$feeds
|
||||
);
|
||||
}
|
||||
if ($error) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, 'FreshRSS error during OPML category import from URL: ' . $catName . "\n");
|
||||
} else {
|
||||
Minz_Log::warning('Error during OPML category import from URL: ' . $catName);
|
||||
}
|
||||
}
|
||||
|
||||
return [$categories_elements, $categories_to_feeds];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of category and feed outlines by categories names.
|
||||
*
|
||||
* This method is applied to a specific outline. If the outline represents
|
||||
* a category (i.e. @outlines key exists), it will reapply loadFromOutlines()
|
||||
* to its children. If the outline represents a feed (i.e. xmlUrl key
|
||||
* exists), it will add the outline to an array accessible by its category
|
||||
* name.
|
||||
*
|
||||
* @param array $outline
|
||||
* The outline from which to extract the categories and feeds outlines.
|
||||
* @param string $parent_category_name
|
||||
* The name of the parent category of the current outline.
|
||||
*
|
||||
* @return array[]
|
||||
*/
|
||||
private function loadFromOutline($outline, $parent_category_name) {
|
||||
$categories_elements = [];
|
||||
$categories_to_feeds = [];
|
||||
|
||||
if ($parent_category_name === '' && isset($outline['category'])) {
|
||||
// The outline has no parent category, but its OPML category
|
||||
// attribute is set, so we use it as the category name.
|
||||
// lib_opml parses this attribute as an array of strings, so we
|
||||
// rebuild a string here.
|
||||
$parent_category_name = implode(', ', $outline['category']);
|
||||
$categories_elements[$parent_category_name] = [
|
||||
'text' => $parent_category_name,
|
||||
];
|
||||
}
|
||||
|
||||
if (isset($outline['@outlines'])) {
|
||||
// The outline has children, it's probably a category
|
||||
if (!empty($outline['text'])) {
|
||||
$category_name = $outline['text'];
|
||||
} elseif (!empty($outline['title'])) {
|
||||
$category_name = $outline['title'];
|
||||
} else {
|
||||
$parent_cat = $cat;
|
||||
$category_name = $parent_category_name;
|
||||
}
|
||||
|
||||
list (
|
||||
$categories_elements,
|
||||
$categories_to_feeds,
|
||||
) = $this->loadFromOutlines($outline['@outlines'], $category_name);
|
||||
|
||||
unset($outline['@outlines']);
|
||||
$categories_elements[$category_name] = $outline;
|
||||
}
|
||||
|
||||
if (isset($cat_elt['@outlines'])) {
|
||||
// Our cat_elt contains more categories or more feeds, so we
|
||||
// add them recursively.
|
||||
// Note: FreshRSS does not support yet category arborescence, so always flatten from here
|
||||
$this->addOpmlElements($cat_elt['@outlines'], $parent_cat, true, $dryRun);
|
||||
// The xmlUrl means it's a feed URL: add the outline to the array if it
|
||||
// exists.
|
||||
if (isset($outline['xmlUrl'])) {
|
||||
if (!isset($categories_to_feeds[$parent_category_name])) {
|
||||
$categories_to_feeds[$parent_category_name] = [];
|
||||
}
|
||||
|
||||
$categories_to_feeds[$parent_category_name][] = $outline;
|
||||
}
|
||||
|
||||
return $cat;
|
||||
return [$categories_elements, $categories_to_feeds];
|
||||
}
|
||||
|
||||
private static function log($message) {
|
||||
if (FreshRSS_Context::$isCli) {
|
||||
fwrite(STDERR, "FreshRSS error during OPML import: {$message}\n");
|
||||
} else {
|
||||
Minz_Log::warning("Error during OPML import: {$message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<?php
|
||||
|
||||
class FreshRSS_fever_Util {
|
||||
const FEVER_PATH = DATA_PATH . '/fever';
|
||||
private const FEVER_PATH = DATA_PATH . '/fever';
|
||||
|
||||
/**
|
||||
* Make sure the fever path exists and is writable.
|
||||
*
|
||||
* @return boolean true if the path is writable, else false.
|
||||
* @return bool true if the path is writable, false otherwise.
|
||||
*/
|
||||
public static function checkFeverPath() {
|
||||
public static function checkFeverPath(): bool {
|
||||
if (!file_exists(self::FEVER_PATH)) {
|
||||
@mkdir(self::FEVER_PATH, 0770, true);
|
||||
}
|
||||
|
||||
$ok = is_writable(self::FEVER_PATH);
|
||||
$ok = touch(self::FEVER_PATH . '/index.html'); // is_writable() is not reliable for a folder on NFS
|
||||
if (!$ok) {
|
||||
Minz_Log::error("Could not save Fever API credentials. The directory does not have write access.");
|
||||
}
|
||||
@@ -22,25 +22,21 @@ class FreshRSS_fever_Util {
|
||||
|
||||
/**
|
||||
* Return the corresponding path for a fever key.
|
||||
*
|
||||
* @param string $feverKey
|
||||
* @return string
|
||||
*/
|
||||
public static function getKeyPath($feverKey) {
|
||||
public static function getKeyPath(string $feverKey): string {
|
||||
if (FreshRSS_Context::$system_conf === null) {
|
||||
throw new FreshRSS_Context_Exception('System configuration not initialised!');
|
||||
}
|
||||
$salt = sha1(FreshRSS_Context::$system_conf->salt);
|
||||
return self::FEVER_PATH . '/.key-' . $salt . '-' . $feverKey . '.txt';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the fever key of a user.
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $passwordPlain
|
||||
* @return string|false the Fever key, or false if the update failed
|
||||
*/
|
||||
public static function updateKey($username, $passwordPlain) {
|
||||
$ok = self::checkFeverPath();
|
||||
if (!$ok) {
|
||||
public static function updateKey(string $username, string $passwordPlain) {
|
||||
if (!self::checkFeverPath()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,22 +44,20 @@ class FreshRSS_fever_Util {
|
||||
|
||||
$feverKey = strtolower(md5("{$username}:{$passwordPlain}"));
|
||||
$feverKeyPath = self::getKeyPath($feverKey);
|
||||
$res = file_put_contents($feverKeyPath, $username);
|
||||
if ($res !== false) {
|
||||
$result = file_put_contents($feverKeyPath, $username);
|
||||
if (is_int($result) && $result > 0) {
|
||||
return $feverKey;
|
||||
} else {
|
||||
Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
|
||||
return false;
|
||||
}
|
||||
Minz_Log::warning('Could not save Fever API credentials. Unknown error.', ADMIN_LOG);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the Fever key of a user.
|
||||
*
|
||||
* @param string $username
|
||||
* @return boolean true if the deletion succeeded, else false.
|
||||
* @return bool true if the deletion succeeded, else false.
|
||||
*/
|
||||
public static function deleteKey($username) {
|
||||
public static function deleteKey(string $username) {
|
||||
$userConfig = get_user_configuration($username);
|
||||
if ($userConfig === null) {
|
||||
return false;
|
||||
|
||||
@@ -3,26 +3,25 @@
|
||||
class FreshRSS_password_Util {
|
||||
// Will also have to be computed client side on mobile devices,
|
||||
// so do not use a too high cost
|
||||
const BCRYPT_COST = 9;
|
||||
public const BCRYPT_COST = 9;
|
||||
|
||||
/**
|
||||
* Return a hash of a plain password, using BCRYPT
|
||||
*
|
||||
* @param string $passwordPlain
|
||||
* @return string
|
||||
*/
|
||||
public static function hash($passwordPlain) {
|
||||
public static function hash(string $passwordPlain): string {
|
||||
$passwordHash = password_hash(
|
||||
$passwordPlain,
|
||||
PASSWORD_BCRYPT,
|
||||
array('cost' => self::BCRYPT_COST)
|
||||
);
|
||||
$passwordPlain = '';
|
||||
|
||||
// Compatibility with bcrypt.js
|
||||
$passwordHash = preg_replace('/^\$2[xy]\$/', '\$2a\$', $passwordHash);
|
||||
|
||||
return $passwordHash == '' ? '' : $passwordHash;
|
||||
if ($passwordHash === '' || $passwordHash === null) {
|
||||
return '';
|
||||
}
|
||||
return $passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -30,11 +29,9 @@ class FreshRSS_password_Util {
|
||||
*
|
||||
* A valid password is a string of at least 7 characters.
|
||||
*
|
||||
* @param string $password
|
||||
*
|
||||
* @return boolean True if the password is valid, false otherwise
|
||||
* @return bool True if the password is valid, false otherwise
|
||||
*/
|
||||
public static function check($password) {
|
||||
public static function check(string $password): bool {
|
||||
return strlen($password) >= 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Zobrazení',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Spodní řádek',
|
||||
'display_authors' => 'Autoři',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'Časový limit HTML5 oznámení',
|
||||
),
|
||||
'show_nav_buttons' => 'Zobrazit navigační tlačítka',
|
||||
'theme' => 'Motiv',
|
||||
'theme' => array(
|
||||
'_' => 'Motiv',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'Motiv „%s“ již není dostupný. Zvolte jiný motiv, prosím.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Náhled',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Na výšku',
|
||||
'square' => 'Čtverec',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Zobrazení',
|
||||
'width' => array(
|
||||
'content' => 'Šířka obsahu',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Uživatelské dotazy',
|
||||
'reading' => 'Čtení',
|
||||
'search' => 'Hledat slova nebo #štítky',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Sdílení',
|
||||
'shortcuts' => 'Zkratky',
|
||||
'stats' => 'Statistika',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Známé základní stránky',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Schránka',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'E-mail',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath pro:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (výchozí)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Vymazat mezipaměť',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Anzeige',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Fußzeile',
|
||||
'display_authors' => 'Autoren',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'Zeitüberschreitung für HTML5-Benachrichtigung',
|
||||
),
|
||||
'show_nav_buttons' => 'Zeige Navigations-Buttons',
|
||||
'theme' => 'Erscheinungsbild',
|
||||
'theme' => array(
|
||||
'_' => 'Layout',
|
||||
'deprecated' => array(
|
||||
'_' => 'Veraltet',
|
||||
'description' => 'Diese Layout wird nicht mehr länger aktualisiert und wir in einer <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">zukünftigen Version von FreshRSS</a> entfernt sein.',
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'Das Erscheinungsbild „%s“ ist nicht mehr verfügbar. Bitte ein anderes auswählen.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Vorschaubild',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Hochformat',
|
||||
'square' => 'Quadrat',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Anzeige',
|
||||
'width' => array(
|
||||
'content' => 'Inhaltsbreite',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Benutzerabfragen',
|
||||
'reading' => 'Lesen',
|
||||
'search' => 'Suche Worte oder #Tags',
|
||||
'search_help' => 'Siehe Dokumentation zu den <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">Suchparametern</a>',
|
||||
'sharing' => 'Teilen',
|
||||
'shortcuts' => 'Tastaturkürzel',
|
||||
'stats' => 'Statistiken',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known-Seite (https://withknown.com)',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Zwischenablage',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'E-Mail',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath für:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (Standard)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Zwischenspeicher leeren',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Display', // TODO
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Bottom line', // TODO
|
||||
'display_authors' => 'Authors', // TODO
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 notification timeout', // TODO
|
||||
),
|
||||
'show_nav_buttons' => 'Show the navigation buttons', // TODO
|
||||
'theme' => 'Theme', // TODO
|
||||
'theme' => array(
|
||||
'_' => 'Theme', // TODO
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
|
||||
'thumbnail' => array(
|
||||
'label' => 'Thumbnail', // TODO
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait', // TODO
|
||||
'square' => 'Square', // TODO
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Display', // TODO
|
||||
'width' => array(
|
||||
'content' => 'Content width', // TODO
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'User queries', // TODO
|
||||
'reading' => 'Reading', // TODO
|
||||
'search' => 'Search words or #tags', // TODO
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Sharing', // TODO
|
||||
'shortcuts' => 'Shortcuts', // TODO
|
||||
'stats' => 'Statistics', // TODO
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites', // TODO
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // TODO
|
||||
'blogotext' => 'Blogotext', // TODO
|
||||
'clipboard' => 'Clipboard', // TODO
|
||||
'diaspora' => 'Diaspora*', // TODO
|
||||
'email' => 'Email', // TODO
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // TODO
|
||||
'gnusocial' => 'GNU social', // TODO
|
||||
'jdh' => 'Journal du hacker', // TODO
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath for:', // TODO
|
||||
),
|
||||
'rss' => 'RSS / Atom (default)', // TODO
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // TODO
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Display', // IGNORE
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Bottom line', // IGNORE
|
||||
'display_authors' => 'Authors', // IGNORE
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 notification timeout', // IGNORE
|
||||
),
|
||||
'show_nav_buttons' => 'Show the navigation buttons', // IGNORE
|
||||
'theme' => 'Theme', // IGNORE
|
||||
'theme' => array(
|
||||
'_' => 'Theme', // IGNORE
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // IGNORE
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // IGNORE
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // IGNORE
|
||||
'thumbnail' => array(
|
||||
'label' => 'Thumbnail', // IGNORE
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait', // IGNORE
|
||||
'square' => 'Square', // IGNORE
|
||||
),
|
||||
'timezone' => 'Time zone', // IGNORE
|
||||
'title' => 'Display', // IGNORE
|
||||
'width' => array(
|
||||
'content' => 'Content width', // IGNORE
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'User queries', // IGNORE
|
||||
'reading' => 'Reading', // IGNORE
|
||||
'search' => 'Search words or #tags', // IGNORE
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // IGNORE
|
||||
'sharing' => 'Sharing', // IGNORE
|
||||
'shortcuts' => 'Shortcuts', // IGNORE
|
||||
'stats' => 'Statistics', // IGNORE
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites', // IGNORE
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Clipboard', // IGNORE
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Email', // IGNORE
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath for:', // IGNORE
|
||||
),
|
||||
'rss' => 'RSS / Atom (default)', // IGNORE
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // IGNORE
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Display',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Bottom line',
|
||||
'display_authors' => 'Authors',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 notification timeout',
|
||||
),
|
||||
'show_nav_buttons' => 'Show the navigation buttons',
|
||||
'theme' => 'Theme',
|
||||
'theme' => array(
|
||||
'_' => 'Theme',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated',
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>',
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Thumbnail',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait',
|
||||
'square' => 'Square',
|
||||
),
|
||||
'timezone' => 'Time zone',
|
||||
'title' => 'Display',
|
||||
'width' => array(
|
||||
'content' => 'Content width',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'User queries',
|
||||
'reading' => 'Reading',
|
||||
'search' => 'Search words or #tags',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Sharing',
|
||||
'shortcuts' => 'Shortcuts',
|
||||
'stats' => 'Statistics',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites',
|
||||
'archiveORG' => 'archive.org',
|
||||
'archivePH' => 'archive.ph',
|
||||
'blogotext' => 'Blogotext',
|
||||
'clipboard' => 'Clipboard',
|
||||
'diaspora' => 'Diaspora*',
|
||||
'email' => 'Email',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook',
|
||||
'gnusocial' => 'GNU social',
|
||||
'jdh' => 'Journal du hacker',
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath for:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (default)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache',
|
||||
|
||||
0
app/i18n/es/admin.php
Executable file → Normal file
0
app/i18n/es/admin.php
Executable file → Normal file
10
app/i18n/es/conf.php
Executable file → Normal file
10
app/i18n/es/conf.php
Executable file → Normal file
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Visualización',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Línea inferior',
|
||||
'display_authors' => 'Autores/Autoras',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'Notificación de fin de espera HTML5',
|
||||
),
|
||||
'show_nav_buttons' => 'Mostrar los botones de navegación',
|
||||
'theme' => 'Tema',
|
||||
'theme' => array(
|
||||
'_' => 'Tema',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'El tema “%s” ya no está disponible. Por favor, elija otro tema.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Miniatura',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Retrato',
|
||||
'square' => 'Cuadrado',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Visualización',
|
||||
'width' => array(
|
||||
'content' => 'Ancho de contenido',
|
||||
|
||||
0
app/i18n/es/feedback.php
Executable file → Normal file
0
app/i18n/es/feedback.php
Executable file → Normal file
3
app/i18n/es/gen.php
Executable file → Normal file
3
app/i18n/es/gen.php
Executable file → Normal file
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Peticiones de usuario',
|
||||
'reading' => 'Lectura',
|
||||
'search' => 'Buscar palabras o #etiquetas',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Compartir',
|
||||
'shortcuts' => 'Atajos',
|
||||
'stats' => 'Estadísticas',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Sitios basados en conocidos',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Portapapeles',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Email', // IGNORE
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
0
app/i18n/es/index.php
Executable file → Normal file
0
app/i18n/es/index.php
Executable file → Normal file
0
app/i18n/es/install.php
Executable file → Normal file
0
app/i18n/es/install.php
Executable file → Normal file
1
app/i18n/es/sub.php
Executable file → Normal file
1
app/i18n/es/sub.php
Executable file → Normal file
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath para:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (por defecto)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Borrar caché',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Affichage',
|
||||
'darkMode' => 'Mode sombre automatique (bêta)',
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Ligne du bas',
|
||||
'display_authors' => 'Auteurs',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'Temps d’affichage de la notification HTML5',
|
||||
),
|
||||
'show_nav_buttons' => 'Afficher les boutons de navigation',
|
||||
'theme' => 'Thème',
|
||||
'theme' => array(
|
||||
'_' => 'Thème',
|
||||
'deprecated' => array(
|
||||
'_' => 'Obsolète',
|
||||
'description' => 'Ce thème est obsolète et sera supprimé dans une <a href="https://freshrss.github.io/FreshRSS/fr/users/05_Configuration.html#th%C3%A8me" target="_blank">future version de FreshRSS</a>',
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'Le thème <em>%s</em> n’est plus disponible. Veuillez choisir un autre thème.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Miniature',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait', // IGNORE
|
||||
'square' => 'Carrée',
|
||||
),
|
||||
'timezone' => 'Fuseau horaire',
|
||||
'title' => 'Affichage',
|
||||
'width' => array(
|
||||
'content' => 'Largeur du contenu',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Filtres utilisateurs',
|
||||
'reading' => 'Lecture',
|
||||
'search' => 'Rechercher des mots ou des #tags',
|
||||
'search_help' => 'Voir <a href="https://freshrss.github.io/FreshRSS/fr/users/03_Main_view.html#gr%C3%A2ce-au-champ-de-recherche" target="_blank">la documentation pour la syntaxe des recherches avancées</a>',
|
||||
'sharing' => 'Partage',
|
||||
'shortcuts' => 'Raccourcis',
|
||||
'stats' => 'Statistiques',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Sites basés sur Known',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Presse-papier',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Courriel',
|
||||
'email-webmail-firefox-fix' => 'Courriel (pour Webmail avec Firefox)',
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath pour :',
|
||||
),
|
||||
'rss' => 'RSS / Atom (par défaut)',
|
||||
'xml_xpath' => 'XML + XPath', // IGNORE
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Vider le cache',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'תצוגה',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'שורה תחתונה',
|
||||
'display_authors' => 'Authors', // TODO
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 התראה פג תוקף',
|
||||
),
|
||||
'show_nav_buttons' => 'Show the navigation buttons', // TODO
|
||||
'theme' => 'ערכת נושא',
|
||||
'theme' => array(
|
||||
'_' => 'ערכת נושא',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
|
||||
'thumbnail' => array(
|
||||
'label' => 'Thumbnail', // TODO
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait', // TODO
|
||||
'square' => 'Square', // TODO
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'תצוגה',
|
||||
'width' => array(
|
||||
'content' => 'רוחב התוכן',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'שאילתות',
|
||||
'reading' => 'קריאה',
|
||||
'search' => 'חיפוש מילים או #תגים',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'שיתוף',
|
||||
'shortcuts' => 'קיצורי דרך',
|
||||
'stats' => 'סטטיסטיקות',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites', // TODO
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Clipboard', // TODO
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'דואר אלקטרוני',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath for:', // TODO
|
||||
),
|
||||
'rss' => 'RSS / Atom (default)', // TODO
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // TODO
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Display', // TODO
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Bottom line', // TODO
|
||||
'display_authors' => 'Authors', // TODO
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 notification timeout', // TODO
|
||||
),
|
||||
'show_nav_buttons' => 'Show the navigation buttons', // TODO
|
||||
'theme' => 'Theme', // TODO
|
||||
'theme' => array(
|
||||
'_' => 'Theme', // TODO
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'The “%s” theme is not available anymore. Please choose another theme.', // TODO
|
||||
'thumbnail' => array(
|
||||
'label' => 'Thumbnail', // TODO
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Portrait', // TODO
|
||||
'square' => 'Square', // TODO
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Display', // TODO
|
||||
'width' => array(
|
||||
'content' => 'Content width', // TODO
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'User queries', // TODO
|
||||
'reading' => 'Reading', // TODO
|
||||
'search' => 'Search words or #tags', // TODO
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Sharing', // TODO
|
||||
'shortcuts' => 'Shortcuts', // TODO
|
||||
'stats' => 'Statistics', // TODO
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites', // TODO
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // TODO
|
||||
'blogotext' => 'Blogotext', // TODO
|
||||
'clipboard' => 'Clipboard', // TODO
|
||||
'diaspora' => 'Diaspora*', // TODO
|
||||
'email' => 'Email', // TODO
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // TODO
|
||||
'gnusocial' => 'GNU social', // TODO
|
||||
'jdh' => 'Journal du hacker', // TODO
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath for:', // TODO
|
||||
),
|
||||
'rss' => 'RSS / Atom (default)', // TODO
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Clear cache', // TODO
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Visualizzazione',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Barra in fondo',
|
||||
'display_authors' => 'Autori',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'Notifica timeout HTML5',
|
||||
),
|
||||
'show_nav_buttons' => 'Mostra i pulsanti di navigazione',
|
||||
'theme' => 'Tema',
|
||||
'theme' => array(
|
||||
'_' => 'Tema',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'Il tema “%s” non è più disponibile. Si prega di selezionarne un altro.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Miniatura',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Ritratto',
|
||||
'square' => 'Squadrata',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'Visualizzazione',
|
||||
'width' => array(
|
||||
'content' => 'Larghezza contenuto',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Ricerche personali',
|
||||
'reading' => 'Lettura',
|
||||
'search' => 'Ricerca parole o #tags',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Condivisione',
|
||||
'shortcuts' => 'Comandi tastiera',
|
||||
'stats' => 'Statistiche',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Siti basati su Known',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Appunti',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Email', // IGNORE
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath per:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (predefinito)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Svuota cache',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => '表示',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => '行の下部',
|
||||
'display_authors' => '著者',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 の通知タイムアウト時間',
|
||||
),
|
||||
'show_nav_buttons' => 'ナビゲーションボタンを表示する',
|
||||
'theme' => 'テーマ',
|
||||
'theme' => array(
|
||||
'_' => 'テーマ',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => '“%s”テーマはご利用いただけません。他のテーマをお選びください。',
|
||||
'thumbnail' => array(
|
||||
'label' => 'サムネイル',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'ポートレート',
|
||||
'square' => '四角',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => 'ディスプレイ',
|
||||
'width' => array(
|
||||
'content' => 'コンテンツ幅',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'ユーザークエリ',
|
||||
'reading' => 'リーディング',
|
||||
'search' => '単語で検索するかハッシュタグで検索する',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => '共有',
|
||||
'shortcuts' => 'ショートカット',
|
||||
'stats' => '統計',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'よく使われるサイト',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'クリップボード',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Eメール',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPathは:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (標準)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'キャッシュのクリア',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => '표시',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => '하단',
|
||||
'display_authors' => '저자',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 알림 타임아웃',
|
||||
),
|
||||
'show_nav_buttons' => '내비게이션 버튼 보이기',
|
||||
'theme' => '테마',
|
||||
'theme' => array(
|
||||
'_' => '테마',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => '“%s” 테마는 더이상 사용할 수 없습니다. 다른 테마를 선택해 주세요.',
|
||||
'thumbnail' => array(
|
||||
'label' => '섬네일',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => '세로 방향',
|
||||
'square' => '정사각형',
|
||||
),
|
||||
'timezone' => 'Time zone', // TODO
|
||||
'title' => '표시',
|
||||
'width' => array(
|
||||
'content' => '내용 표시 너비',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => '사용자 쿼리',
|
||||
'reading' => '읽기',
|
||||
'search' => '단어 또는 #태그 검색',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => '공유',
|
||||
'shortcuts' => '단축키',
|
||||
'stats' => '통계',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known based sites', // IGNORE
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => '클립보드',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => '메일',
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => '다음의 XPath:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (기본값)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => '캐쉬 지우기',
|
||||
|
||||
@@ -32,6 +32,7 @@ return array(
|
||||
),
|
||||
'display' => array(
|
||||
'_' => 'Opmaak',
|
||||
'darkMode' => 'Automatic dark mode (beta)', // TODO
|
||||
'icon' => array(
|
||||
'bottom_line' => 'Onderaan',
|
||||
'display_authors' => 'Auteurs',
|
||||
@@ -48,7 +49,13 @@ return array(
|
||||
'timeout' => 'HTML5 notificatie stop',
|
||||
),
|
||||
'show_nav_buttons' => 'Toon navigatieknoppen',
|
||||
'theme' => 'Thema',
|
||||
'theme' => array(
|
||||
'_' => 'Thema',
|
||||
'deprecated' => array(
|
||||
'_' => 'Deprecated', // TODO
|
||||
'description' => 'This theme is no longer supported and will be not available anymore in a <a href="https://freshrss.github.io/FreshRSS/en/users/05_Configuration.html#theme" target="_blank">future release of FreshRSS</a>', // TODO
|
||||
),
|
||||
),
|
||||
'theme_not_available' => 'Het „%s” thema is niet meer beschikbaar. Kies een ander thema.',
|
||||
'thumbnail' => array(
|
||||
'label' => 'Miniatuur',
|
||||
@@ -57,6 +64,7 @@ return array(
|
||||
'portrait' => 'Staand',
|
||||
'square' => 'Vierkant',
|
||||
),
|
||||
'timezone' => 'Tijdzone',
|
||||
'title' => 'Opmaak',
|
||||
'width' => array(
|
||||
'content' => 'Inhoud breedte',
|
||||
|
||||
@@ -174,6 +174,7 @@ return array(
|
||||
'queries' => 'Gebruikers informatie',
|
||||
'reading' => 'Lezen',
|
||||
'search' => 'Zoek woorden of #labels',
|
||||
'search_help' => 'See documentation for advanced <a href="https://freshrss.github.io/FreshRSS/en/users/10_filter.html#with-the-search-field" target="_blank">search parameters</a>', // TODO
|
||||
'sharing' => 'Delen',
|
||||
'shortcuts' => 'Snelle toegang',
|
||||
'stats' => 'Statistieken',
|
||||
@@ -191,11 +192,13 @@ return array(
|
||||
),
|
||||
'share' => array(
|
||||
'Known' => 'Known-gebaseerde sites',
|
||||
'archiveORG' => 'archive.org', // IGNORE
|
||||
'archivePH' => 'archive.ph', // IGNORE
|
||||
'blogotext' => 'Blogotext', // IGNORE
|
||||
'clipboard' => 'Klembord',
|
||||
'diaspora' => 'Diaspora*', // IGNORE
|
||||
'email' => 'Email', // IGNORE
|
||||
'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO
|
||||
'facebook' => 'Facebook', // IGNORE
|
||||
'gnusocial' => 'GNU social', // IGNORE
|
||||
'jdh' => 'Journal du hacker', // IGNORE
|
||||
|
||||
@@ -122,6 +122,7 @@ return array(
|
||||
'xpath' => 'XPath voor:',
|
||||
),
|
||||
'rss' => 'RSS / Atom (standaard)',
|
||||
'xml_xpath' => 'XML + XPath', // TODO
|
||||
),
|
||||
'maintenance' => array(
|
||||
'clear_cache' => 'Cache leegmaken',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user