Merge branch 'edge' into latest

This commit is contained in:
Alexandre Alapetite
2023-03-04 13:30:45 +01:00
293 changed files with 9528 additions and 7606 deletions

32
.devcontainer/Dockerfile Normal file
View 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

View 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"
}

View 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

View File

@@ -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 ;

View File

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

View File

@@ -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/",

View File

@@ -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)

View File

@@ -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**. Dont forget to add your name to `CREDITS.md` if youre 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.

View File

@@ -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)

View File

@@ -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 \

View File

@@ -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 \

View File

@@ -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 \

View File

@@ -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 \

View File

@@ -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 \

View File

@@ -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

View File

@@ -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

View File

@@ -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 "$@"

View File

@@ -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

View File

@@ -5,7 +5,7 @@
# FreshRSS
FreshRSS est un agrégateur de flux RSS à auto-héberger à limage 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 lajout 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)
![Logo de FreshRSS](docs/img/FreshRSS-logo.png)
# Avertissements
## Contributions
FreshRSS nest 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, loption suivante est disponible :
[![Ouvrir dans GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
## Capture décran
![Capture décran de FreshRSS](docs/img/FreshRSS-screenshot.png)
## Avertissements
FreshRSS nest 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 nest 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 dencodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés)
* MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+
# 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 dinformations sur linstallation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html).
### Exemple dinstallation complète sur Linux Debian/Ubuntu
## Exemple dinstallation 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 daccè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 linterface 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 linterface 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 [lAPI 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)

View File

@@ -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)
![FreshRSS logo](docs/img/FreshRSS-logo.png)
# 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:
[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=edge&repo=6322699)
## Screenshot
![FreshRSS screenshot](docs/img/FreshRSS-screenshot.png)
## 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
View 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
View File

18
app/Controllers/feedController.php Executable file → Normal file
View 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');

View File

@@ -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
View 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
View 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');

View File

@@ -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();

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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,
));

View File

@@ -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') ||

View File

@@ -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

View File

@@ -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());
});
}
}

View File

@@ -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) {

View File

@@ -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);
}

View File

@@ -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';
}
}

View File

@@ -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;
}

View File

@@ -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) {
// larticle 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 lets 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']);

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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(']]>', ']]&gt;', $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);

View File

@@ -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);
}

View File

@@ -2,5 +2,9 @@
interface FreshRSS_Searchable {
/**
* @param int|string $id
* @return Minz_Model
*/
public function searchById($id);
}

View File

@@ -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');
}
}

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model {
static $alts = array(
'add' => '', //✚
'all' => '☰',
'bookmark' => '✨', //★
'bookmark-add' => '', //✚
'bookmark-tag' => '📑',
'category' => '🗂️', //☷

View File

@@ -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');
}
}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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`);

View File

@@ -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);

View File

@@ -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}");
}
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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',

View File

@@ -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

View File

@@ -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ěť',

View File

@@ -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',

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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',

View File

@@ -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
View File

10
app/i18n/es/conf.php Executable file → Normal file
View 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
View File

3
app/i18n/es/gen.php Executable file → Normal file
View 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
View File

0
app/i18n/es/install.php Executable file → Normal file
View File

1
app/i18n/es/sub.php Executable file → Normal file
View 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é',

View File

@@ -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 daffichage 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> nest 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',

View File

@@ -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

View File

@@ -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',

View File

@@ -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' => 'רוחב התוכן',

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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',

View File

@@ -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

View File

@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPath per:',
),
'rss' => 'RSS / Atom (predefinito)',
'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'Svuota cache',

View File

@@ -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' => 'コンテンツ幅',

View File

@@ -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

View File

@@ -122,6 +122,7 @@ return array(
'xpath' => 'XPathは:',
),
'rss' => 'RSS / Atom (標準)',
'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => 'キャッシュのクリア',

View File

@@ -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' => '내용 표시 너비',

View File

@@ -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

View File

@@ -122,6 +122,7 @@ return array(
'xpath' => '다음의 XPath:',
),
'rss' => 'RSS / Atom (기본값)',
'xml_xpath' => 'XML + XPath', // TODO
),
'maintenance' => array(
'clear_cache' => '캐쉬 지우기',

View File

@@ -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',

View File

@@ -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

View File

@@ -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