diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..a2c1a2a2b --- /dev/null +++ b/.devcontainer/Dockerfile @@ -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 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..a30fdf5c7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -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" +} diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh new file mode 100755 index 000000000..f5398efb9 --- /dev/null +++ b/.devcontainer/postCreateCommand.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +ln -s "$(pwd)" /var/www/FreshRSS + +cp ./Docker/*.Apache.conf /etc/apache2/conf.d/ + +cat <./constants.local.php +> $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 ; diff --git a/.markdownlintignore b/.markdownlintignore index 6e1cfb9c4..fa771b056 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,4 +1,5 @@ .git/ +lib/marienfressinaud/ lib/phpgt/ lib/phpmailer/ node_modules/ diff --git a/.typos.toml b/.typos.toml index f4b7d5f5a..2170f5e85 100644 --- a/.typos.toml +++ b/.typos.toml @@ -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/", diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b14a835e..5916d1299 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,92 @@ # FreshRSS changelog +## 2023-03-04 FreshRSS 1.21.0 + +* Features + * New *XML+XPath* mode for fetching XML documents when there is no RSS/ATOM feed [#5076](https://github.com/FreshRSS/FreshRSS/pull/5076) + * Better support of feed enclosures (image / audio / video attachments) [#4944](https://github.com/FreshRSS/FreshRSS/pull/4944) + * User-defined time-zone [#4906](https://github.com/FreshRSS/FreshRSS/pull/4906) + * Improve HTML+XPath mode by allowing HTML content [#4878](https://github.com/FreshRSS/FreshRSS/pull/4878) + * Search only on full tag names and not on parts of tag names [#4882](https://github.com/FreshRSS/FreshRSS/pull/4882) + * Allows searching for parentheses with `\(` or `\)` [#4989](https://github.com/FreshRSS/FreshRSS/pull/4989) + * Firefox-compatible sharing service for `mailto:` links for webmail services [#4680](https://github.com/FreshRSS/FreshRSS/pull/4680) + * Add sharing to [archive.org](https://archive.org/) [#5096](https://github.com/FreshRSS/FreshRSS/pull/5096) + * Increase max HTTP timeout to 15 minutes [#5074](https://github.com/FreshRSS/FreshRSS/pull/5074) +* Compatibility + * Require PHP 7.2+ (drop support for PHP 7.0 and 7.1) [#4848](https://github.com/FreshRSS/FreshRSS/pull/4848) + * Workaround disabled `openlog()` or `syslog()` [#5054](https://github.com/FreshRSS/FreshRSS/pull/5054) +* Deployment + * Docker default image (Debian 11 Bullseye) updated to PHP 7.4.33 + * Docker: alternative image updated to Alpine 3.17 with PHP 8.1.16 and Apache 2.4.55 [#4886](https://github.com/FreshRSS/FreshRSS/pull/4886) + * More uniform time-zone behaviour [#4903](https://github.com/FreshRSS/FreshRSS/pull/4903), [#4905](https://github.com/FreshRSS/FreshRSS/pull/4905) + * New CLI script `cli/sensitive-log.sh` to help e.g. Apache clear logs for sensitive information such as credentials [#5001](https://github.com/FreshRSS/FreshRSS/pull/5001) + * New CLI script `cli/access-permissions.sh` to help apply file permissions correctly [#5062](https://github.com/FreshRSS/FreshRSS/pull/5062) + * Improve file permissions on `./extensions/` [#4956](https://github.com/FreshRSS/FreshRSS/pull/4956) + * Update Apache mime type `font/woff` [#4894](https://github.com/FreshRSS/FreshRSS/pull/4894) + * Re-added a git `latest` branch (instead of a tag) to track the latest FreshRSS stable releases [#5148](https://github.com/FreshRSS/FreshRSS/pull/5148) +* Bug fixing + * Fix allow disabling curl proxy for specific feed, when proxy is defined globally [#5082](https://github.com/FreshRSS/FreshRSS/pull/5082) + * NFS-friendly `is_writable()` checks [#4780](https://github.com/FreshRSS/FreshRSS/pull/4780) + * Fix error handling when updating feed URL [#5039](https://github.com/FreshRSS/FreshRSS/pull/5039) + * Fix feed favicon after editing feed URL [#4975](https://github.com/FreshRSS/FreshRSS/pull/4975) + * Fix allow Ctrl+Click 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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 32da43cc8..76db326a6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ Did you want to fix a bug? To keep a great coordination between collaborators, y 3. [Create a new branch](https://help.github.com/articles/creating-and-deleting-branches-within-your-repository/). The name of the branch must be explicit and being prefixed by the related ticket id. For instance, `783-contributing-file` to fix [ticket #783](https://github.com/FreshRSS/FreshRSS/issues/783). 4. Make your changes to your fork and [send a pull request](https://help.github.com/articles/using-pull-requests/) on the **edge branch**. Don’t forget to add your name to `CREDITS.md` if you’re contributing to FreshRSS for the very first time. -If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/01_First_steps.html). +If you have to write code, please follow [our coding style recommendations](https://freshrss.github.io/FreshRSS/en/developers/02_First_steps.html). **Tip:** if you are searching for bugs easy to fix, have a look at the « [Good first issue](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22good+first+issue+%3Ababy%3A%22) » and/or « [Help wanted](https://github.com/FreshRSS/FreshRSS/issues?q=label%3A%22help+wanted+%3Aoctocat%3A%22) » ticket labels. diff --git a/CREDITS.md b/CREDITS.md index d5af0ba54..c60b9531b 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -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) diff --git a/Docker/Dockerfile b/Docker/Dockerfile index b462263fb..08e6dce43 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -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 \ diff --git a/Docker/Dockerfile-Alpine b/Docker/Dockerfile-Alpine index 1e5a95125..65c2de6be 100644 --- a/Docker/Dockerfile-Alpine +++ b/Docker/Dockerfile-Alpine @@ -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 \ diff --git a/Docker/Dockerfile-Newest b/Docker/Dockerfile-Newest index d5942c77e..37783494b 100644 --- a/Docker/Dockerfile-Newest +++ b/Docker/Dockerfile-Newest @@ -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 \ diff --git a/Docker/Dockerfile-Oldest b/Docker/Dockerfile-Oldest index d0b27cc72..ede1ba643 100644 --- a/Docker/Dockerfile-Oldest +++ b/Docker/Dockerfile-Oldest @@ -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 \ diff --git a/Docker/Dockerfile-QEMU-ARM b/Docker/Dockerfile-QEMU-ARM index 042597dec..892cbc955 100644 --- a/Docker/Dockerfile-QEMU-ARM +++ b/Docker/Dockerfile-QEMU-ARM @@ -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 \ diff --git a/Docker/FreshRSS.Apache.conf b/Docker/FreshRSS.Apache.conf index 2cfb9cbf9..6281e59e5 100644 --- a/Docker/FreshRSS.Apache.conf +++ b/Docker/FreshRSS.Apache.conf @@ -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 diff --git a/Docker/README.md b/Docker/README.md index 44963ba0c..a4c0e8289 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -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 diff --git a/Docker/entrypoint.sh b/Docker/entrypoint.sh index 82c2e358e..cbc2443d6 100755 --- a/Docker/entrypoint.sh +++ b/Docker/entrypoint.sh @@ -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 "$@" diff --git a/Makefile b/Makefile index 6216dfcec..d94fd704a 100644 --- a/Makefile +++ b/Makefile @@ -60,40 +60,37 @@ stop: ## Stop FreshRSS container if any ## Tests and linter ## ###################### .PHONY: test -test: bin/phpunit ## Run the test suite - $(PHP) ./bin/phpunit --bootstrap ./tests/bootstrap.php ./tests +test: vendor/bin/phpunit ## Run the test suite + $(PHP) vendor/bin/phpunit --bootstrap ./tests/bootstrap.php ./tests .PHONY: lint -lint: bin/phpcs ## Run the linter on the PHP files - $(PHP) ./bin/phpcs . -p -s +lint: vendor/bin/phpcs ## Run the linter on the PHP files + $(PHP) vendor/bin/phpcs . -p -s .PHONY: lint-fix -lint-fix: bin/phpcbf ## Fix the errors detected by the linter - $(PHP) ./bin/phpcbf . -p -s +lint-fix: vendor/bin/phpcbf ## Fix the errors detected by the linter + $(PHP) vendor/bin/phpcbf . -p -s bin/composer: mkdir -p bin/ - wget 'https://raw.githubusercontent.com/composer/getcomposer.org/76a7060ccb93902cd7576b67264ad91c8a2700e2/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer' + wget 'https://raw.githubusercontent.com/composer/getcomposer.org/b5dbe5ebdec95ce71b3128b359bd5a85cb0a722d/web/installer' -O - -q | php -- --quiet --install-dir='./bin/' --filename='composer' -bin/phpunit: - mkdir -p bin/ - wget -O bin/phpunit 'https://phar.phpunit.de/phpunit-9.5.20.phar' - echo '6becad2da5c37f5ad101cc665ef05a2f1a6a45d2427c8edcc74f72c92fb1e05a bin/phpunit' | sha256sum -c - || rm bin/phpunit +vendor/bin/phpunit: bin/composer + bin/composer install --prefer-dist --no-progress + ln -s ../vendor/bin/phpunit bin/phpunit -bin/phpcs: - mkdir -p bin/ - wget -O bin/phpcs 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcs.phar' - echo '7a14323a14af9f58302d15442492ee1076a8cd72c018a816cb44965bf3a9b015 bin/phpcs' | sha256sum -c - || rm bin/phpcs +vendor/bin/phpcs: bin/composer + bin/composer install --prefer-dist --no-progress + ln -s ../vendor/bin/phpcs bin/phpcs -bin/phpcbf: - mkdir -p bin/ - wget -O bin/phpcbf 'https://github.com/squizlabs/PHP_CodeSniffer/releases/download/3.7.1/phpcbf.phar' - echo 'c93c0e83cbda21c21f849ccf0f4b42979d20004a5a6172ed0ea270eca7ae6fa8 bin/phpcbf' | sha256sum -c - || rm bin/phpcbf +vendor/bin/phpcbf: bin/composer + bin/composer install --prefer-dist --no-progress + ln -s ../vendor/bin/phpcbf bin/phpcbf bin/typos: mkdir -p bin/ cd bin ; \ - wget -q 'https://github.com/crate-ci/typos/releases/download/v1.10.1/typos-v1.10.1-x86_64-unknown-linux-musl.tar.gz' && \ + wget -q 'https://github.com/crate-ci/typos/releases/download/v1.13.6/typos-v1.13.6-x86_64-unknown-linux-musl.tar.gz' && \ tar -xvf *.tar.gz './typos' && \ chmod +x typos && \ rm *.tar.gz ; \ @@ -102,6 +99,9 @@ bin/typos: node_modules/.bin/eslint: npm install +node_modules/.bin/rtlcss: + npm install + vendor/bin/phpstan: bin/composer bin/composer install --prefer-dist --no-progress @@ -181,8 +181,8 @@ endif ## TOOLS ## ########### .PHONY: rtl -rtl: ## Generate RTL CSS files - rtlcss -d p/themes/ && find p/themes/ -type f -name '*.rtl.rtl.css' -delete +rtl: node_modules/.bin/rtlcss ## Generate RTL CSS files + npm run-script rtlcss .PHONY: pot pot: ## Generate POT templates for docs diff --git a/README.fr.md b/README.fr.md index d5119de4c..559a57745 100644 --- a/README.fr.md +++ b/README.fr.md @@ -5,7 +5,7 @@ # FreshRSS -FreshRSS est un agrégateur de flux RSS à auto-héberger à l’image de [Leed](https://github.com/LeedRSS/Leed) ou de [Kriss Feed](https://tontof.net/kriss/feed/). +FreshRSS est un agrégateur de flux RSS à auto-héberger. Il se veut léger et facile à prendre en main tout en étant un outil puissant et paramétrable. @@ -19,21 +19,29 @@ FreshRSS supporte nativement le moissonnage du Web (Web Scraping) basique, basé Enfin, il permet l’ajout d’[extensions](#extensions) pour encore plus de personnalisation. -Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues). -Nous sommes une communauté amicale. - * Site officiel : * Démo : * 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 n’est fourni avec aucune garantie. +Les demandes de fonctionnalités, rapports de bugs, et autres contributions sont les bienvenues. Privilégiez pour cela des [demandes sur GitHub](https://github.com/FreshRSS/FreshRSS/issues). +Nous sommes une communauté amicale. + +Pour faciliter les contributions, l’option suivante est disponible : + +[![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 n’est fourni avec aucune garantie. + # [Documentation](https://freshrss.github.io/FreshRSS/fr/) * La [documentation utilisateurs](https://freshrss.github.io/FreshRSS/fr/users/02_First_steps.html) pour découvrir les fonctionnalités de FreshRSS. @@ -41,28 +49,24 @@ FreshRSS n’est fourni avec aucune garantie. * La [documentation développeurs](https://freshrss.github.io/FreshRSS/fr/developers/01_First_steps.html) pour savoir comment contribuer et mieux comprendre le code source de FreshRSS. * Le [guide de contribution](https://freshrss.github.io/FreshRSS/fr/contributing.html) pour nous aider à développer FreshRSS. -# Prérequis +## Prérequis * Un navigateur Web récent tel que Firefox / IceCat, Edge, Chromium / Chrome, Opera, Safari. * Fonctionne aussi sur mobile (sauf certaines fonctionnalités) * Serveur modeste, par exemple sous Linux ou Windows * Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles) * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres) -* PHP 7.0+ +* PHP 7.2+ * Requis : [cURL](https://www.php.net/curl), [DOM](https://www.php.net/dom), [JSON](https://www.php.net/json), [XML](https://www.php.net/xml), [session](https://www.php.net/session), [ctype](https://www.php.net/ctype), et [PDO_MySQL](https://www.php.net/pdo-mysql) ou [PDO_SQLite](https://www.php.net/pdo-sqlite) ou [PDO_PGSQL](https://www.php.net/pdo-pgsql) * Recommandés : [GMP](https://www.php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](https://www.php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](https://www.php.net/mbstring) (pour le texte Unicode), [iconv](https://www.php.net/iconv) (pour conversion d’encodages), [ZIP](https://www.php.net/zip) (pour import/export), [zlib](https://www.php.net/zlib) (pour les flux compressés) * MySQL 5.5.3+ ou équivalent MariaDB, ou SQLite 3.7.4+, ou PostgreSQL 9.5+ - -# Téléchargement +# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html) Si vous préférez que votre FreshRSS soit stable, vous devriez télécharger la dernière version. De nouvelles versions sont publiées tous les 2 ou 3 mois. Voir la [liste des versions](https://github.com/FreshRSS/FreshRSS/releases). Si vous voulez une publication continue (rolling release) avec les dernières nouveautés, ou bien aider à tester ou développer la future version stable, vous pouvez utiliser [la branche edge](https://github.com/FreshRSS/FreshRSS/tree/edge/). - -# [Installation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html) - ## Installation automatisée * [Docker](./Docker/) @@ -83,7 +87,7 @@ Si vous voulez une publication continue (rolling release) avec les dernières no Plus d’informations sur l’installation et la configuration serveur peuvent être trouvées dans [notre documentation](https://freshrss.github.io/FreshRSS/fr/users/01_Installation.html). -### Exemple d’installation complète sur Linux Debian/Ubuntu +## Exemple d’installation complète sur Linux Debian/Ubuntu ```sh # Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web) @@ -105,11 +109,12 @@ sudo apt-get install git sudo git clone https://github.com/FreshRSS/FreshRSS.git cd FreshRSS -# Si vous souhaitez utiliser la dernière version stable de FreshRSS -sudo git checkout $(git describe --tags --abbrev=0) +# La branche par défault “edge” est la celle de la publication continue, +# mais vous pouvez changer de branche pour “latest” si vous préférez les versions stables de FreshRSS +sudo git checkout latest # Mettre les droits d’accès pour le serveur Web -sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/ +sudo cli/access-permissions.sh # Si vous souhaitez permettre les mises à jour par l’interface Web sudo chmod -R g+w . @@ -122,7 +127,7 @@ sudo ln -s /usr/share/FreshRSS/p /var/www/html/FreshRSS # Mettre à jour FreshRSS vers une nouvelle version par git cd /usr/share/FreshRSS sudo git pull -sudo chown -R :www-data . && sudo chmod -R g+r . && sudo chmod -R g+w ./data/ +sudo cli/access-permissions.sh ``` Voir la [documentation de la ligne de commande](cli/README.md) pour plus de détails. @@ -155,7 +160,7 @@ Créer `/etc/cron.d/FreshRSS` avec : 7,37 * * * * www-data php -f /usr/share/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1 ``` -## Conseils +# Conseils * Pour une meilleure sécurité, faites en sorte que seul le répertoire `./p/` soit accessible depuis le Web, par exemple en faisant pointer un sous-domaine sur le répertoire `./p/`. * En particulier, les données personnelles se trouvent dans le répertoire `./data/`. @@ -175,19 +180,7 @@ Créer `/etc/cron.d/FreshRSS` avec : * Il faut conserver vos fichiers `./data/config.php` ainsi que `./data/users/*/config.php` * Vous pouvez exporter votre liste de flux au format OPML soit depuis l’interface Web, soit [en ligne de commande](cli/README.md) -Pour sauvegarder les articles eux-mêmes : - -## Dans le cas où vous utilisez MySQL - -Vous pouvez utiliser [phpMyAdmin](https://www.phpmyadmin.net) ou les outils de MySQL : - -```sh -mysqldump --skip-comments --disable-keys --user= --password --host --result-file=freshrss.dump.sql --databases -``` - -## 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 --filename @@ -250,7 +243,7 @@ et [l’API Fever](https://freshrss.github.io/FreshRSS/fr/users/06_Fever_API.htm * [SimplePie](https://simplepie.org/) * [MINZ](https://framagit.org/marienfressinaud/MINZ) * [php-http-304](https://alexandre.alapetite.fr/doc-alex/php-http-304/) -* [lib_opml](https://github.com/marienfressinaud/lib_opml) +* [lib_opml](https://framagit.org/marienfressinaud/lib_opml) * [PhpGt/CssXPath](https://github.com/PhpGt/CssXPath) * [PHPMailer](https://github.com/PHPMailer/PHPMailer) * [Chart.js](https://www.chartjs.org) diff --git a/README.md b/README.md index 71b04431d..b0581aae4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # FreshRSS -FreshRSS is a self-hosted RSS feed aggregator like [Leed](https://github.com/LeedRSS/Leed) or [Kriss Feed](https://tontof.net/kriss/feed/). +FreshRSS is a self-hosted RSS feed aggregator. It is lightweight, easy to work with, powerful, and customizable. @@ -19,21 +19,29 @@ FreshRSS natively supports basic Web scraping, based on [XPath](https://www.w3.o Finally, it supports [extensions](#extensions) for further tuning. -Feature requests, bug reports, and other contributions are welcome. The best way to contribute is to [open an issue on GitHub](https://github.com/FreshRSS/FreshRSS/issues). -We are a friendly community. - * Official website: * Demo: * 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 * [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) diff --git a/app/Controllers/configureController.php b/app/Controllers/configureController.php old mode 100755 new mode 100644 index 613bacade..791d58d6d --- a/app/Controllers/configureController.php +++ b/app/Controllers/configureController.php @@ -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(); diff --git a/app/Controllers/entryController.php b/app/Controllers/entryController.php old mode 100755 new mode 100644 diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php old mode 100755 new mode 100644 index 319faece8..84f38fe5e --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -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'); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index a1e1106c1..6c4b684e9 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -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(); } diff --git a/app/Controllers/indexController.php b/app/Controllers/indexController.php old mode 100755 new mode 100644 index 7fced48af..968518e3f --- a/app/Controllers/indexController.php +++ b/app/Controllers/indexController.php @@ -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'); diff --git a/app/Controllers/javascriptController.php b/app/Controllers/javascriptController.php old mode 100755 new mode 100644 index c2a5cb872..b4e769738 --- a/app/Controllers/javascriptController.php +++ b/app/Controllers/javascriptController.php @@ -1,11 +1,11 @@ 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'); diff --git a/app/Controllers/statsController.php b/app/Controllers/statsController.php index 1798ee3cf..16b09d702 100644 --- a/app/Controllers/statsController.php +++ b/app/Controllers/statsController.php @@ -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(); diff --git a/app/Controllers/subscriptionController.php b/app/Controllers/subscriptionController.php index 4a63d1ee4..f0355a82a 100644 --- a/app/Controllers/subscriptionController.php +++ b/app/Controllers/subscriptionController.php @@ -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); } } diff --git a/app/Controllers/updateController.php b/app/Controllers/updateController.php index 675bd7def..f638ce96c 100644 --- a/app/Controllers/updateController.php +++ b/app/Controllers/updateController.php @@ -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); } diff --git a/app/Controllers/userController.php b/app/Controllers/userController.php index 06dbab9fa..ac8f3be82 100644 --- a/app/Controllers/userController.php +++ b/app/Controllers/userController.php @@ -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, " 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, )); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 602c46658..76ced841c 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -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') || diff --git a/app/Models/BooleanSearch.php b/app/Models/BooleanSearch.php index b1c7bbd3b..279040a5a 100644 --- a/app/Models/BooleanSearch.php +++ b/app/Models/BooleanSearch.php @@ -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 diff --git a/app/Models/Category.php b/app/Models/Category.php index c4ca12fd3..b23e8da0a 100644 --- a/app/Models/Category.php +++ b/app/Models/Category.php @@ -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()); + }); + } } diff --git a/app/Models/CategoryDAO.php b/app/Models/CategoryDAO.php index 20a92d52a..c855f1495 100644 --- a/app/Models/CategoryDAO.php +++ b/app/Models/CategoryDAO.php @@ -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 */ - 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 $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 $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) { diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index c822bcf4d..258c2ad58 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -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); } diff --git a/app/Models/Context.php b/app/Models/Context.php index fed2a6767..734458d7f 100644 --- a/app/Models/Context.php +++ b/app/Models/Context.php @@ -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'; + } } diff --git a/app/Models/Days.php b/app/Models/Days.php index 2d770c30b..d3f1ba075 100644 --- a/app/Models/Days.php +++ b/app/Models/Days.php @@ -1,7 +1,9 @@

'; + $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[\'"])' . preg_quote($link, '/') . '(?P=delim)/', $html) == 1; } - /** @return array> */ - 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; + } + } + + $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 .= '
'; + + foreach ($thumbnails as $thumbnail) { + $content .= '

'; + } + + if (self::enclosureIsImage($enclosure)) { + $content .= '

'; + } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) { + $content .= '

💾

'; + } elseif ($medium === 'video' || strpos($mime, 'video') === 0) { + $content .= '

💾

'; + } else { //e.g. application, text, unknown + $content .= '

💾

'; + } + + if ($credit != '') { + $content .= '

© ' . $credit . '

'; + } + if ($description != '') { + $content .= '
' . $description . '
'; + } + $content .= "
\n"; + } + + return $content; + } + + /** @return iterable> */ + 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, '

content, '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|null */ - public function thumbnail() { - foreach ($this->enclosures(true) as $enclosure) { - if (!empty($enclosure['url']) && empty($enclosure['type'])) { - return $enclosure; + public function thumbnail(bool $searchEnclosures = true) { + $thumbnail = $this->attributes('thumbnail'); + if (!empty($thumbnail['url'])) { + return $thumbnail; + } + if ($searchEnclosures) { + foreach ($this->enclosures(true) as $enclosure) { + if (self::enclosureIsImage($enclosure)) { + return $enclosure; + } } } return null; @@ -188,6 +302,7 @@ class FreshRSS_Entry extends Minz_Model { public function link(): string { return $this->link; } + /** @return string|int */ public function date(bool $raw = false) { if ($raw) { return $this->date; @@ -587,7 +702,7 @@ class FreshRSS_Entry extends Minz_Model { if ($entry) { // l’article existe déjà en BDD, en se contente de recharger ce contenu - $this->content = $entry->content(); + $this->content = $entry->content(false); } else { try { // The article is not yet in the database, so let’s fetch it @@ -629,7 +744,7 @@ class FreshRSS_Entry extends Minz_Model { 'guid' => $this->guid(), 'title' => $this->title(), 'author' => $this->authors(true), - 'content' => $this->content(), + 'content' => $this->content(false), 'link' => $this->link(), 'date' => $this->date(true), 'hash' => $this->hash(), @@ -677,7 +792,6 @@ class FreshRSS_Entry extends Minz_Model { 'published' => $this->date(true), // 'updated' => $this->date(true), 'title' => $this->title(), - 'summary' => ['content' => $this->content()], 'canonical' => [ ['href' => htmlspecialchars_decode($this->link(), ENT_QUOTES)], ], @@ -697,13 +811,16 @@ class FreshRSS_Entry extends Minz_Model { if ($mode === 'compat') { $item['title'] = escapeToUnicodeAlternative($this->title(), false); unset($item['alternate'][0]['type']); - if (mb_strlen($this->content(), 'UTF-8') > self::API_MAX_COMPAT_CONTENT_LENGTH) { - $item['summary']['content'] = mb_strcut($this->content(), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'); - } - } elseif ($mode === 'freshrss') { + $item['summary'] = [ + 'content' => mb_strcut($this->content(true), 0, self::API_MAX_COMPAT_CONTENT_LENGTH, 'UTF-8'), + ]; + } else { + $item['content'] = [ + 'content' => $this->content(false), + ]; + } + if ($mode === 'freshrss') { $item['guid'] = $this->guid(); - unset($item['summary']); - $item['content'] = ['content' => $this->content()]; } if ($category != null && $mode !== 'freshrss') { $item['categories'][] = 'user/-/label/' . htmlspecialchars_decode($category->name(), ENT_QUOTES); @@ -718,10 +835,11 @@ class FreshRSS_Entry extends Minz_Model { } } foreach ($this->enclosures() as $enclosure) { - if (!empty($enclosure['url']) && !empty($enclosure['type'])) { + if (!empty($enclosure['url'])) { $media = [ 'href' => $enclosure['url'], - 'type' => $enclosure['type'], + 'type' => $enclosure['type'] ?? $enclosure['medium'] ?? + (self::enclosureIsImage($enclosure) ? 'image' : ''), ]; if (!empty($enclosure['length'])) { $media['length'] = intval($enclosure['length']); diff --git a/app/Models/EntryDAO.php b/app/Models/EntryDAO.php index b63515223..3b7c1ac3f 100644 --- a/app/Models/EntryDAO.php +++ b/app/Models/EntryDAO.php @@ -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 $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 */ 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) { diff --git a/app/Models/EntryDAOSQLite.php b/app/Models/EntryDAOSQLite.php index 8039581e6..35f3ef676 100644 --- a/app/Models/EntryDAOSQLite.php +++ b/app/Models/EntryDAOSQLite.php @@ -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; } diff --git a/app/Models/Feed.php b/app/Models/Feed.php index f24ec1884..7c46199a5 100644 --- a/app/Models/Feed.php +++ b/app/Models/Feed.php @@ -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 .= '

'; - - if ($enclosure->get_title() != '') { - $content .= '

' . $enclosure->get_title() . '

'; - } - - $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 .= '

'; - } elseif ($medium === 'audio' || strpos($mime, 'audio') === 0) { - $enclosureContent .= '

💾

'; - } elseif ($medium === 'video' || strpos($mime, 'video') === 0) { - $enclosureContent .= '

💾

'; - } else { //e.g. application, text, unknown - $enclosureContent .= '

💾

'; - } - $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 .= '

'; + if ($thumbnail !== $attributeThumbnail['url']) { + $attributeEnclosure['thumbnails'][] = $thumbnail; } } } - $content .= $thumbnailContent; - $content .= $enclosureContent; - - if ($enclosure->get_description() != '') { - $content .= '

' . $enclosure->get_description() . '

'; - } - $content .= "
\n"; + $attributeEnclosures[] = $attributeEnclosure; } } } @@ -586,6 +577,10 @@ class FreshRSS_Feed extends Minz_Model { ); $entry->_tags($tags); $entry->_feed($this); + if (!empty($attributeThumbnail['url'])) { + $entry->_attributes('thumbnail', $attributeThumbnail); + } + $entry->_attributes('enclosures', $attributeEnclosures); $entry->hash(); //Must be computed before loading full content $entry->loadCompleteContent(); // Optionally load full content for truncated feeds @@ -596,7 +591,7 @@ class FreshRSS_Feed extends Minz_Model { /** * @return SimplePie|null */ - public function loadHtmlXpath(bool $loadDetails = false, bool $noCache = false) { + public function loadHtmlXpath() { if ($this->url == '') { return null; } @@ -624,8 +619,9 @@ class FreshRSS_Feed extends Minz_Model { return null; } - $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), FreshRSS_Feed::KIND_HTML_XPATH); - $html = httpGet($feedSourceUrl, $cachePath, 'html', $this->attributes()); + $cachePath = FreshRSS_Feed::cacheFilename($feedSourceUrl, $this->attributes(), $this->kind()); + $html = httpGet($feedSourceUrl, $cachePath, + $this->kind() === FreshRSS_Feed::KIND_XML_XPATH ? 'xml' : 'html', $this->attributes()); if (strlen($html) <= 0) { return null; } @@ -640,7 +636,18 @@ class FreshRSS_Feed extends Minz_Model { $doc = new DOMDocument(); $doc->recover = true; $doc->strictErrorChecking = false; - $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + + switch ($this->kind()) { + case FreshRSS_Feed::KIND_HTML_XPATH: + $doc->loadHTML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + break; + case FreshRSS_Feed::KIND_XML_XPATH: + $doc->loadXML($html, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING); + break; + default: + return null; + } + $xpath = new DOMXPath($doc); $view->rss_title = $xPathFeedTitle == '' ? $this->name() : htmlspecialchars(@$xpath->evaluate('normalize-space(' . $xPathFeedTitle . ')'), ENT_COMPAT, 'UTF-8'); @@ -653,7 +660,23 @@ class FreshRSS_Feed extends Minz_Model { foreach ($nodes as $node) { $item = []; $item['title'] = $xPathItemTitle == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTitle . ')', $node); - $item['content'] = $xPathItemContent == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemContent . ')', $node); + + $item['content'] = ''; + if ($xPathItemContent != '') { + $result = @$xpath->evaluate($xPathItemContent, $node); + if ($result instanceof DOMNodeList) { + // List of nodes, save as HTML + $content = ''; + foreach ($result as $child) { + $content .= $doc->saveHTML($child) . "\n"; + } + $item['content'] = $content; + } else { + // Typed expression, save as-is + $item['content'] = strval($result); + } + } + $item['link'] = $xPathItemUri == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemUri . ')', $node); $item['author'] = $xPathItemAuthor == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemAuthor . ')', $node); $item['timestamp'] = $xPathItemTimestamp == '' ? '' : @$xpath->evaluate('normalize-space(' . $xPathItemTimestamp . ')', $node); @@ -679,8 +702,15 @@ class FreshRSS_Feed extends Minz_Model { $item['guid'] = 'urn:sha1:' . sha1($item['title'] . $item['content'] . $item['link']); } - if ($item['title'] . $item['content'] . $item['link'] != '') { - $item = Minz_Helper::htmlspecialchars_utf8($item); + if ($item['title'] != '' || $item['content'] != '' || $item['link'] != '') { + // HTML-encoding/escaping of the relevant fields (all except 'content') + foreach (['author', 'categories', 'guid', 'link', 'thumbnail', 'timestamp', 'title'] as $key) { + if (!empty($item[$key])) { + $item[$key] = Minz_Helper::htmlspecialchars_utf8($item[$key]); + } + } + // CDATA protection + $item['content'] = str_replace(']]>', ']]>', $item['content']); $view->entries[] = FreshRSS_Entry::fromArray($item); } } @@ -763,8 +793,10 @@ class FreshRSS_Feed extends Minz_Model { public static function cacheFilename(string $url, array $attributes, int $kind = FreshRSS_Feed::KIND_RSS): string { $simplePie = customSimplePie($attributes); $filename = $simplePie->get_cache_filename($url); - if ($kind == FreshRSS_Feed::KIND_HTML_XPATH) { + if ($kind === FreshRSS_Feed::KIND_HTML_XPATH) { return CACHE_PATH . '/' . $filename . '.html'; + } elseif ($kind === FreshRSS_Feed::KIND_XML_XPATH) { + return CACHE_PATH . '/' . $filename . '.xml'; } else { return CACHE_PATH . '/' . $filename . '.spc'; } @@ -966,14 +998,14 @@ class FreshRSS_Feed extends Minz_Model { $key = $hubJson['key']; //To renew our lease } } else { - @mkdir($path, 0777, true); + @mkdir($path, 0770, true); $key = sha1($path . FreshRSS_Context::$system_conf->salt); $hubJson = array( 'hub' => $this->hubUrl, 'key' => $key, ); file_put_contents($hubFilename, json_encode($hubJson)); - @mkdir(PSHB_PATH . '/keys/'); + @mkdir(PSHB_PATH . '/keys/', 0770, true); file_put_contents(PSHB_PATH . '/keys/' . $key . '.txt', $this->selfUrl); $text = 'WebSub prepared for ' . $this->url; Minz_Log::debug($text); diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index 5993f50dc..1aae5fee5 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -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); } diff --git a/app/Models/Searchable.php b/app/Models/Searchable.php index d5bcea49d..a15a44ed7 100644 --- a/app/Models/Searchable.php +++ b/app/Models/Searchable.php @@ -2,5 +2,9 @@ interface FreshRSS_Searchable { + /** + * @param int|string $id + * @return Minz_Model + */ public function searchById($id); } diff --git a/app/Models/SystemConfiguration.php b/app/Models/SystemConfiguration.php index ec5960c0e..9fc79969d 100644 --- a/app/Models/SystemConfiguration.php +++ b/app/Models/SystemConfiguration.php @@ -25,6 +25,10 @@ * @property string $unsafe_autologin_enabled * @property-read array $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'); + } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 589648e26..c1290d192 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -5,40 +5,61 @@ class FreshRSS_Tag extends Minz_Model { * @var int */ private $id = 0; + /** + * @var string + */ private $name; + /** + * @var array + */ 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|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|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; } } diff --git a/app/Models/TagDAO.php b/app/Models/TagDAO.php index f232b2f9f..35123606b 100644 --- a/app/Models/TagDAO.php +++ b/app/Models/TagDAO.php @@ -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) { diff --git a/app/Models/Themes.php b/app/Models/Themes.php index d652ada5b..86125c5f5 100644 --- a/app/Models/Themes.php +++ b/app/Models/Themes.php @@ -79,7 +79,6 @@ class FreshRSS_Themes extends Minz_Model { static $alts = array( 'add' => '➕', //✚ 'all' => '☰', - 'bookmark' => '✨', //★ 'bookmark-add' => '➕', //✚ 'bookmark-tag' => '📑', 'category' => '🗂️', //☷ diff --git a/app/Models/UserConfiguration.php b/app/Models/UserConfiguration.php index 05c3c08ac..53b12cc2e 100644 --- a/app/Models/UserConfiguration.php +++ b/app/Models/UserConfiguration.php @@ -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 $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'); + } } diff --git a/app/Models/UserQuery.php b/app/Models/UserQuery.php index 964324bf7..278074362 100644 --- a/app/Models/UserQuery.php +++ b/app/Models/UserQuery.php @@ -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 $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 + * @return array */ - 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[acfst])(_(?P\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; } diff --git a/app/Models/View.php b/app/Models/View.php index ab1780405..309773c93 100644 --- a/app/Models/View.php +++ b/app/Models/View.php @@ -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; diff --git a/app/SQL/install.sql.mysql.php b/app/SQL/install.sql.mysql.php index d85bd3dc3..b8fff170a 100644 --- a/app/SQL/install.sql.mysql.php +++ b/app/SQL/install.sql.mysql.php @@ -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 diff --git a/app/SQL/install.sql.pgsql.php b/app/SQL/install.sql.pgsql.php index c4da2afad..00a30a8c7 100644 --- a/app/SQL/install.sql.pgsql.php +++ b/app/SQL/install.sql.pgsql.php @@ -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, diff --git a/app/SQL/install.sql.sqlite.php b/app/SQL/install.sql.sqlite.php index ccf256d6a..8762b33eb 100644 --- a/app/SQL/install.sql.sqlite.php +++ b/app/SQL/install.sql.sqlite.php @@ -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`); diff --git a/app/Services/ExportService.php b/app/Services/ExportService.php index ad0f5f5a8..6b0a3f178 100644 --- a/app/Services/ExportService.php +++ b/app/Services/ExportService.php @@ -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); diff --git a/app/Services/ImportService.php b/app/Services/ImportService.php index 28286a753..55aa28679 100644 --- a/app/Services/ImportService.php +++ b/app/Services/ImportService.php @@ -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|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 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 $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 $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}"); + } } } diff --git a/app/Utils/feverUtil.php b/app/Utils/feverUtil.php index a7d21dacb..0e4b712ce 100644 --- a/app/Utils/feverUtil.php +++ b/app/Utils/feverUtil.php @@ -1,19 +1,19 @@ 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; diff --git a/app/Utils/passwordUtil.php b/app/Utils/passwordUtil.php index cff97d2bc..0edead213 100644 --- a/app/Utils/passwordUtil.php +++ b/app/Utils/passwordUtil.php @@ -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; } } diff --git a/app/i18n/cz/conf.php b/app/i18n/cz/conf.php index 7b603555c..4411b5047 100644 --- a/app/i18n/cz/conf.php +++ b/app/i18n/cz/conf.php @@ -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 future release of FreshRSS', // 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', diff --git a/app/i18n/cz/gen.php b/app/i18n/cz/gen.php index 55586b24c..1f75033fa 100644 --- a/app/i18n/cz/gen.php +++ b/app/i18n/cz/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Uživatelské dotazy', 'reading' => 'Čtení', 'search' => 'Hledat slova nebo #štítky', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/cz/sub.php b/app/i18n/cz/sub.php index a11a9359d..3d08c315b 100644 --- a/app/i18n/cz/sub.php +++ b/app/i18n/cz/sub.php @@ -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ěť', diff --git a/app/i18n/de/conf.php b/app/i18n/de/conf.php index 5e9cdb36e..8962123f4 100644 --- a/app/i18n/de/conf.php +++ b/app/i18n/de/conf.php @@ -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 zukünftigen Version von FreshRSS 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', diff --git a/app/i18n/de/gen.php b/app/i18n/de/gen.php index 59f532c74..fb35bc41c 100644 --- a/app/i18n/de/gen.php +++ b/app/i18n/de/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Benutzerabfragen', 'reading' => 'Lesen', 'search' => 'Suche Worte oder #Tags', + 'search_help' => 'Siehe Dokumentation zu den Suchparametern', '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 diff --git a/app/i18n/de/sub.php b/app/i18n/de/sub.php index 580f7d348..b265c1b98 100644 --- a/app/i18n/de/sub.php +++ b/app/i18n/de/sub.php @@ -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', diff --git a/app/i18n/el/conf.php b/app/i18n/el/conf.php index 98f559d18..daacfe684 100644 --- a/app/i18n/el/conf.php +++ b/app/i18n/el/conf.php @@ -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 future release of FreshRSS', // 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 diff --git a/app/i18n/el/gen.php b/app/i18n/el/gen.php index a0c95ab39..03852a0c6 100644 --- a/app/i18n/el/gen.php +++ b/app/i18n/el/gen.php @@ -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 search parameters', // 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 diff --git a/app/i18n/el/sub.php b/app/i18n/el/sub.php index 424fafc7b..aae9ae412 100644 --- a/app/i18n/el/sub.php +++ b/app/i18n/el/sub.php @@ -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 diff --git a/app/i18n/en-us/conf.php b/app/i18n/en-us/conf.php index 8330e4970..afea0299a 100644 --- a/app/i18n/en-us/conf.php +++ b/app/i18n/en-us/conf.php @@ -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 future release of FreshRSS', // 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 diff --git a/app/i18n/en-us/gen.php b/app/i18n/en-us/gen.php index c5f92ad40..ca08ed27f 100644 --- a/app/i18n/en-us/gen.php +++ b/app/i18n/en-us/gen.php @@ -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 search parameters', // 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 diff --git a/app/i18n/en-us/sub.php b/app/i18n/en-us/sub.php index a6b311084..92d75b81e 100644 --- a/app/i18n/en-us/sub.php +++ b/app/i18n/en-us/sub.php @@ -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 diff --git a/app/i18n/en/conf.php b/app/i18n/en/conf.php index fe03499ea..9899cf897 100644 --- a/app/i18n/en/conf.php +++ b/app/i18n/en/conf.php @@ -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 future release of FreshRSS', + ), + ), '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', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 8f7065a83..d3a36995f 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'User queries', 'reading' => 'Reading', 'search' => 'Search words or #tags', + 'search_help' => 'See documentation for advanced search parameters', // 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', diff --git a/app/i18n/en/sub.php b/app/i18n/en/sub.php index c7e100c25..04caaff05 100644 --- a/app/i18n/en/sub.php +++ b/app/i18n/en/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath for:', ), 'rss' => 'RSS / Atom (default)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Clear cache', diff --git a/app/i18n/es/admin.php b/app/i18n/es/admin.php old mode 100755 new mode 100644 diff --git a/app/i18n/es/conf.php b/app/i18n/es/conf.php old mode 100755 new mode 100644 index c91b0205c..5137ff987 --- a/app/i18n/es/conf.php +++ b/app/i18n/es/conf.php @@ -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 future release of FreshRSS', // 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', diff --git a/app/i18n/es/feedback.php b/app/i18n/es/feedback.php old mode 100755 new mode 100644 diff --git a/app/i18n/es/gen.php b/app/i18n/es/gen.php old mode 100755 new mode 100644 index 209a40dac..5ea2fce23 --- a/app/i18n/es/gen.php +++ b/app/i18n/es/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Peticiones de usuario', 'reading' => 'Lectura', 'search' => 'Buscar palabras o #etiquetas', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/es/index.php b/app/i18n/es/index.php old mode 100755 new mode 100644 diff --git a/app/i18n/es/install.php b/app/i18n/es/install.php old mode 100755 new mode 100644 diff --git a/app/i18n/es/sub.php b/app/i18n/es/sub.php old mode 100755 new mode 100644 index 52d681067..4fd2fa393 --- a/app/i18n/es/sub.php +++ b/app/i18n/es/sub.php @@ -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é', diff --git a/app/i18n/fr/conf.php b/app/i18n/fr/conf.php index 61306289c..3122e3be5 100644 --- a/app/i18n/fr/conf.php +++ b/app/i18n/fr/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Affichage', + 'darkMode' => 'Mode sombre automatique (bêta)', 'icon' => array( 'bottom_line' => 'Ligne du bas', 'display_authors' => 'Auteurs', @@ -48,7 +49,13 @@ return array( 'timeout' => 'Temps d’affichage de la notification HTML5', ), 'show_nav_buttons' => 'Afficher les boutons de navigation', - 'theme' => 'Thème', + 'theme' => array( + '_' => 'Thème', + 'deprecated' => array( + '_' => 'Obsolète', + 'description' => 'Ce thème est obsolète et sera supprimé dans une future version de FreshRSS', + ), + ), 'theme_not_available' => 'Le thème %s n’est plus disponible. Veuillez choisir un autre thème.', 'thumbnail' => array( 'label' => 'Miniature', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Portrait', // IGNORE 'square' => 'Carrée', ), + 'timezone' => 'Fuseau horaire', 'title' => 'Affichage', 'width' => array( 'content' => 'Largeur du contenu', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 69d260063..53e7160a2 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Filtres utilisateurs', 'reading' => 'Lecture', 'search' => 'Rechercher des mots ou des #tags', + 'search_help' => 'Voir la documentation pour la syntaxe des recherches avancées', '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 diff --git a/app/i18n/fr/sub.php b/app/i18n/fr/sub.php index f9df0dbcc..be6dc094d 100644 --- a/app/i18n/fr/sub.php +++ b/app/i18n/fr/sub.php @@ -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', diff --git a/app/i18n/he/conf.php b/app/i18n/he/conf.php index ad479db44..c4a490a2d 100644 --- a/app/i18n/he/conf.php +++ b/app/i18n/he/conf.php @@ -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 future release of FreshRSS', // 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' => 'רוחב התוכן', diff --git a/app/i18n/he/gen.php b/app/i18n/he/gen.php index a8df3db6b..6345e66e9 100644 --- a/app/i18n/he/gen.php +++ b/app/i18n/he/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'שאילתות', 'reading' => 'קריאה', 'search' => 'חיפוש מילים או #תגים', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/he/sub.php b/app/i18n/he/sub.php index 25552ffa1..bae5f5177 100644 --- a/app/i18n/he/sub.php +++ b/app/i18n/he/sub.php @@ -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 diff --git a/app/i18n/id/conf.php b/app/i18n/id/conf.php index b8a5b4fc1..8b1fa8dc6 100644 --- a/app/i18n/id/conf.php +++ b/app/i18n/id/conf.php @@ -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 future release of FreshRSS', // 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 diff --git a/app/i18n/id/gen.php b/app/i18n/id/gen.php index 93f8b0afe..1fc2fa155 100644 --- a/app/i18n/id/gen.php +++ b/app/i18n/id/gen.php @@ -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 search parameters', // 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 diff --git a/app/i18n/id/sub.php b/app/i18n/id/sub.php index 7fdf5c024..3f9a4916a 100644 --- a/app/i18n/id/sub.php +++ b/app/i18n/id/sub.php @@ -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 diff --git a/app/i18n/it/conf.php b/app/i18n/it/conf.php index 4597687cc..6f3540322 100644 --- a/app/i18n/it/conf.php +++ b/app/i18n/it/conf.php @@ -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 future release of FreshRSS', // 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', diff --git a/app/i18n/it/gen.php b/app/i18n/it/gen.php index e5458866c..f3edb57aa 100644 --- a/app/i18n/it/gen.php +++ b/app/i18n/it/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Ricerche personali', 'reading' => 'Lettura', 'search' => 'Ricerca parole o #tags', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/it/sub.php b/app/i18n/it/sub.php index 8614caca7..7ab83cf07 100644 --- a/app/i18n/it/sub.php +++ b/app/i18n/it/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath per:', ), 'rss' => 'RSS / Atom (predefinito)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Svuota cache', diff --git a/app/i18n/ja/conf.php b/app/i18n/ja/conf.php index 5e9aabfa2..4dd939760 100644 --- a/app/i18n/ja/conf.php +++ b/app/i18n/ja/conf.php @@ -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 future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => '“%s”テーマはご利用いただけません。他のテーマをお選びください。', 'thumbnail' => array( 'label' => 'サムネイル', @@ -57,6 +64,7 @@ return array( 'portrait' => 'ポートレート', 'square' => '四角', ), + 'timezone' => 'Time zone', // TODO 'title' => 'ディスプレイ', 'width' => array( 'content' => 'コンテンツ幅', diff --git a/app/i18n/ja/gen.php b/app/i18n/ja/gen.php index 69fc8f9c9..f6cf2dcdf 100644 --- a/app/i18n/ja/gen.php +++ b/app/i18n/ja/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'ユーザークエリ', 'reading' => 'リーディング', 'search' => '単語で検索するかハッシュタグで検索する', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/ja/sub.php b/app/i18n/ja/sub.php index 80548c025..2425b21f3 100644 --- a/app/i18n/ja/sub.php +++ b/app/i18n/ja/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPathは:', ), 'rss' => 'RSS / Atom (標準)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'キャッシュのクリア', diff --git a/app/i18n/ko/conf.php b/app/i18n/ko/conf.php index 279f2f4ad..a88fcf9e0 100644 --- a/app/i18n/ko/conf.php +++ b/app/i18n/ko/conf.php @@ -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 future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => '“%s” 테마는 더이상 사용할 수 없습니다. 다른 테마를 선택해 주세요.', 'thumbnail' => array( 'label' => '섬네일', @@ -57,6 +64,7 @@ return array( 'portrait' => '세로 방향', 'square' => '정사각형', ), + 'timezone' => 'Time zone', // TODO 'title' => '표시', 'width' => array( 'content' => '내용 표시 너비', diff --git a/app/i18n/ko/gen.php b/app/i18n/ko/gen.php index 4f6b6a228..da1f57e9c 100644 --- a/app/i18n/ko/gen.php +++ b/app/i18n/ko/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => '사용자 쿼리', 'reading' => '읽기', 'search' => '단어 또는 #태그 검색', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/ko/sub.php b/app/i18n/ko/sub.php index e0ef5990b..f376247d5 100644 --- a/app/i18n/ko/sub.php +++ b/app/i18n/ko/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => '다음의 XPath:', ), 'rss' => 'RSS / Atom (기본값)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => '캐쉬 지우기', diff --git a/app/i18n/nl/conf.php b/app/i18n/nl/conf.php index 8b3d597b1..e02ca81cc 100644 --- a/app/i18n/nl/conf.php +++ b/app/i18n/nl/conf.php @@ -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 future release of FreshRSS', // 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', diff --git a/app/i18n/nl/gen.php b/app/i18n/nl/gen.php index abd21f460..ad3379ece 100644 --- a/app/i18n/nl/gen.php +++ b/app/i18n/nl/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Gebruikers informatie', 'reading' => 'Lezen', 'search' => 'Zoek woorden of #labels', + 'search_help' => 'See documentation for advanced search parameters', // 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 diff --git a/app/i18n/nl/sub.php b/app/i18n/nl/sub.php index 0fa767171..631da9477 100644 --- a/app/i18n/nl/sub.php +++ b/app/i18n/nl/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath voor:', ), 'rss' => 'RSS / Atom (standaard)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Cache leegmaken', diff --git a/app/i18n/oc/conf.php b/app/i18n/oc/conf.php index c1834e9aa..4a3b483e7 100644 --- a/app/i18n/oc/conf.php +++ b/app/i18n/oc/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Afichatge', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => 'Linha enbàs', 'display_authors' => 'Autors', @@ -48,7 +49,13 @@ return array( 'timeout' => 'Temps d’afichatge de las notificacions HTML5', ), 'show_nav_buttons' => 'Mostrar los botons de navigacion', - 'theme' => 'Tèma', + 'theme' => array( + '_' => 'Tèma', + 'deprecated' => array( + '_' => 'Deprecated', // TODO + 'description' => 'This theme is no longer supported and will be not available anymore in a future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => 'Lo tèma « %s » es pas pus disponible. Causissètz un autre tèma.', 'thumbnail' => array( 'label' => 'Vinheta', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Retrach', 'square' => 'Carrat', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Afichatge', 'width' => array( 'content' => 'Largor del contengut', diff --git a/app/i18n/oc/gen.php b/app/i18n/oc/gen.php index 41f2c1499..8e852a810 100644 --- a/app/i18n/oc/gen.php +++ b/app/i18n/oc/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Filtres utilizaire', 'reading' => 'Lectura', 'search' => 'Recercar de mots o d’#etiquetas', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Partatge', 'shortcuts' => 'Acorchis', 'stats' => 'Estatisticas', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Sites basats sus Known', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => 'Quicha-papiers.', 'diaspora' => 'Diaspora*', // IGNORE 'email' => 'Corrièl', + 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO 'facebook' => 'Facebook', // IGNORE 'gnusocial' => 'GNU social', // IGNORE 'jdh' => 'Journal du hacker', // IGNORE diff --git a/app/i18n/oc/sub.php b/app/i18n/oc/sub.php index 92a73057c..008b4964d 100644 --- a/app/i18n/oc/sub.php +++ b/app/i18n/oc/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath per :', ), 'rss' => 'RSS / Atom (defaut)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Escafar lo cache', diff --git a/app/i18n/pl/conf.php b/app/i18n/pl/conf.php index 31b0d238c..8700a1c13 100644 --- a/app/i18n/pl/conf.php +++ b/app/i18n/pl/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Wyświetlanie', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => 'Dolny margines', 'display_authors' => 'Autorzy', @@ -48,7 +49,13 @@ return array( 'timeout' => 'Czas wyświetlania powiadomienia HTML5', ), 'show_nav_buttons' => 'Pokaż przyciski nawigacyjne', - 'theme' => 'Motyw', + 'theme' => array( + '_' => 'Motyw', + 'deprecated' => array( + '_' => 'Deprecated', // TODO + 'description' => 'This theme is no longer supported and will be not available anymore in a future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => 'Motyw “%s” nie jest już dostępny. Wybierz inny motyw.', 'thumbnail' => array( 'label' => 'Miniaturka', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Portret', 'square' => 'Kwadrat', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Wyświetlanie', 'width' => array( 'content' => 'Rozmiar treści', diff --git a/app/i18n/pl/gen.php b/app/i18n/pl/gen.php index 1a7bd69a5..fc91d8bca 100644 --- a/app/i18n/pl/gen.php +++ b/app/i18n/pl/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Zapisane zapytania', 'reading' => 'Czytanie', 'search' => 'Wyszukaj wyrazy lub #tagi', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Podawanie dalej', 'shortcuts' => 'Skróty klawiszowe', 'stats' => 'Statystyki', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Strony bazujące na usłudze Known', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => 'Schowek', '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 diff --git a/app/i18n/pl/sub.php b/app/i18n/pl/sub.php index b6121fcb7..565401982 100644 --- a/app/i18n/pl/sub.php +++ b/app/i18n/pl/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath dla:', ), 'rss' => 'RSS / Atom (domyślne)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Wyczyść pamięć podręczną', diff --git a/app/i18n/pt-br/conf.php b/app/i18n/pt-br/conf.php index b925aee21..f8ad55f14 100644 --- a/app/i18n/pt-br/conf.php +++ b/app/i18n/pt-br/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Exibição', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => 'Linha inferior', 'display_authors' => 'Autores', @@ -48,7 +49,13 @@ return array( 'timeout' => 'Notificação em HTML5 de timeout', ), 'show_nav_buttons' => 'Mostrar botões de navegação', - 'theme' => 'Tema', + 'theme' => array( + '_' => 'Tema', + 'deprecated' => array( + '_' => 'Deprecated', // TODO + 'description' => 'This theme is no longer supported and will be not available anymore in a future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => 'O tema “%s” não está mais disponível. Por favor escolha outro tema.', 'thumbnail' => array( 'label' => 'Miniatura', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Modo retrato', 'square' => 'Modo quadrado', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Exibição', 'width' => array( 'content' => 'Largura do conteúdo', diff --git a/app/i18n/pt-br/gen.php b/app/i18n/pt-br/gen.php index 969056969..51c1eb327 100644 --- a/app/i18n/pt-br/gen.php +++ b/app/i18n/pt-br/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Queries de usuário', 'reading' => 'Leitura', 'search' => 'Procurar por palavras ou #tags', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Compartilhamento', 'shortcuts' => 'Atalhos', 'stats' => 'Estatísticas', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Sites no Known', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => 'Área de transferência', '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 diff --git a/app/i18n/pt-br/sub.php b/app/i18n/pt-br/sub.php index c9755755e..4cdee8681 100644 --- a/app/i18n/pt-br/sub.php +++ b/app/i18n/pt-br/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath para:', ), 'rss' => 'RSS / Atom (padrão)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Limpar o cache', diff --git a/app/i18n/ru/conf.php b/app/i18n/ru/conf.php index c0d25aec1..2c5dda544 100644 --- a/app/i18n/ru/conf.php +++ b/app/i18n/ru/conf.php @@ -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 future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => 'Тема “%s” больше не доступна. Пожалуйста выберите другю тему.', 'thumbnail' => array( 'label' => 'Эскиз', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Вертикальный', 'square' => 'Квадратный', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Отображение', 'width' => array( 'content' => 'Ширина содержимого', diff --git a/app/i18n/ru/gen.php b/app/i18n/ru/gen.php index 3ed1ab1ac..ddfea7ca4 100644 --- a/app/i18n/ru/gen.php +++ b/app/i18n/ru/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Пользовательские запросы', 'reading' => 'Чтение', 'search' => 'Искать слова или #теги', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Обмен', 'shortcuts' => 'Горячие клавиши', 'stats' => 'Статистика', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Сайты на Known', + '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 diff --git a/app/i18n/ru/sub.php b/app/i18n/ru/sub.php index 5704b53b1..d13c4c4f0 100644 --- a/app/i18n/ru/sub.php +++ b/app/i18n/ru/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath для:', ), 'rss' => 'RSS / Atom (по умолчанию)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Очистить кэш', diff --git a/app/i18n/sk/conf.php b/app/i18n/sk/conf.php index 7efc3a75d..d4714b506 100644 --- a/app/i18n/sk/conf.php +++ b/app/i18n/sk/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Zobrazenie', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => 'Spodný riadok', 'display_authors' => 'Autori', @@ -48,7 +49,13 @@ return array( 'timeout' => 'Limit HTML5 oznámenia', ), 'show_nav_buttons' => 'Zobraziť tlačidlá oznámenia', - 'theme' => 'Vzhľad', + 'theme' => array( + '_' => 'Vzhľad', + 'deprecated' => array( + '_' => 'Deprecated', // TODO + 'description' => 'This theme is no longer supported and will be not available anymore in a future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => 'Vzhľad “%s” už nie je dostupný. Prosím, vyberte si iný vzhľad.', 'thumbnail' => array( 'label' => 'Miniatúra', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Nastojato', 'square' => 'Štvorec', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Zobraziť', 'width' => array( 'content' => 'Šírka obsahu', diff --git a/app/i18n/sk/gen.php b/app/i18n/sk/gen.php index 6bb5e4161..d591266e4 100644 --- a/app/i18n/sk/gen.php +++ b/app/i18n/sk/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Používateľské dopyty', 'reading' => 'Čítanie', 'search' => 'Hľadajte slová alebo #značky', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Zdieľanie', 'shortcuts' => 'Skratky', 'stats' => 'Štatistiky', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Stránky založené na Known', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => 'Schránka', 'diaspora' => 'Diaspora*', // IGNORE 'email' => 'E-mail', // IGNORE + 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO 'facebook' => 'Facebook', // IGNORE 'gnusocial' => 'GNU social', // IGNORE 'jdh' => 'Journal du hacker', // IGNORE diff --git a/app/i18n/sk/sub.php b/app/i18n/sk/sub.php index f583f6ca0..3c980d202 100644 --- a/app/i18n/sk/sub.php +++ b/app/i18n/sk/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath pre:', ), 'rss' => 'RSS / Atom (prednastavené)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Vymazať vyrovnáciu pamäť', diff --git a/app/i18n/tr/conf.php b/app/i18n/tr/conf.php index 7220d6670..41f658879 100644 --- a/app/i18n/tr/conf.php +++ b/app/i18n/tr/conf.php @@ -32,6 +32,7 @@ return array( ), 'display' => array( '_' => 'Görünüm', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => 'Alt çizgi', 'display_authors' => 'Yazarlar', @@ -48,7 +49,13 @@ return array( 'timeout' => 'HTML5 bildirim zaman aşımı', ), 'show_nav_buttons' => 'Gezinti düğmelerini göster', - 'theme' => 'Tema', + 'theme' => array( + '_' => 'Tema', + 'deprecated' => array( + '_' => 'Deprecated', // TODO + 'description' => 'This theme is no longer supported and will be not available anymore in a future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => '“%s” teması şuan uygun değilç Lütfen başka bir tema seçin.', 'thumbnail' => array( 'label' => 'Önizleme', @@ -57,6 +64,7 @@ return array( 'portrait' => 'Portre', 'square' => 'Kare', ), + 'timezone' => 'Time zone', // TODO 'title' => 'Görünüm', 'width' => array( 'content' => 'İçerik genişliği', diff --git a/app/i18n/tr/gen.php b/app/i18n/tr/gen.php index 8839023e6..4b84d6c40 100644 --- a/app/i18n/tr/gen.php +++ b/app/i18n/tr/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => 'Kullanıcı sorguları', 'reading' => 'Okuma', 'search' => 'Kelime veya #etiket ara', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => 'Paylaşım', 'shortcuts' => 'Kısayollar', 'stats' => 'İstatistikler', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => 'Bilinen siteler', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => 'Kopyala', '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 diff --git a/app/i18n/tr/sub.php b/app/i18n/tr/sub.php index 056c059ac..3e03f667c 100644 --- a/app/i18n/tr/sub.php +++ b/app/i18n/tr/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath:', ), 'rss' => 'RSS / Atom (varsayılan)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => 'Önbelleği temizle', diff --git a/app/i18n/zh-cn/admin.php b/app/i18n/zh-cn/admin.php index 46b4d190a..79bcde2e1 100644 --- a/app/i18n/zh-cn/admin.php +++ b/app/i18n/zh-cn/admin.php @@ -17,7 +17,7 @@ return array( 'api_enabled' => '允许 API 访问 (用于手机应用)', 'form' => '网页表单(传统方式, 需要 JavaScript)', 'http' => 'HTTP(面向启用 HTTPS 的高级用户)', - 'none' => '无认证(危险)', + 'none' => '无(危险)', 'title' => '认证', 'token' => '认证口令', 'token_help' => '用于不经认证访问默认用户的 RSS 输出:', @@ -26,7 +26,7 @@ return array( ), 'check_install' => array( 'cache' => array( - 'nok' => '请检查 ./data/cache 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data/cache 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'cache 目录权限正常', ), 'categories' => array( @@ -39,27 +39,27 @@ return array( ), 'ctype' => array( 'nok' => '找不到字符类型检测库(php-ctype)', - 'ok' => '已找到字符类型检测库 (php-ctype)', + 'ok' => '已找到字符类型检测库(ctype)', ), 'curl' => array( - 'nok' => '找不到 cURL 库(php-cURL)', - 'ok' => '已找到 cURL 库(php-cURL)', + 'nok' => '找不到 cURL 库(php-curl 包)', + 'ok' => '已找到 cURL 库', ), 'data' => array( - 'nok' => '请检查 ./data 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'data 目录权限正常', ), - 'database' => '数据库相关', + 'database' => '数据库安装', 'dom' => array( - 'nok' => '找不到用于浏览 DOM 的库(php-xml)', - 'ok' => '已找到用于浏览 DOM 的库(php-xml)', + 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)', + 'ok' => '已找到用于浏览 DOM 的库', ), 'entries' => array( 'nok' => 'Entry 表配置错误', - 'ok' => 'Entry 表正常', + 'ok' => 'Entry 表配置正常', ), 'favicons' => array( - 'nok' => '请检查 ./data/favicons 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data/favicons 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'favicons 目录权限正常', ), 'feeds' => array( @@ -67,46 +67,46 @@ return array( 'ok' => 'Feed 表正常', ), 'fileinfo' => array( - 'nok' => '找不到 fileinfo 库(php-fileinfo)', - 'ok' => '已找到 fileinfo 库(php-fileinfo)', + 'nok' => '找不到 PHP fileinfo 库(php-fileinfo 包)', + 'ok' => '已找到 fileinfo 库', ), 'files' => '文件相关', 'json' => array( - 'nok' => '找不到 JSON 扩展(php-json )', - 'ok' => '已找到 JSON 扩展(php-json)', + 'nok' => '找不到 JSON 扩展(php-json 包)', + 'ok' => '已找到 JSON 扩展', ), 'mbstring' => array( - 'nok' => '找不到推荐的 Unicode 解析库(mbstring)', - 'ok' => '已找到推荐的 Unicode 解析库(mbstring)', + 'nok' => '找不到推荐用于 Unicode 的 mbstring 库', + 'ok' => '已找到推荐用于 Unicode 的 mbstring 库', ), 'pcre' => array( 'nok' => '找不到正则表达式解析库(php-pcre)', - 'ok' => '已找到正则表达式解析库(php-pcre)', + 'ok' => '已找到正则表达式解析库(PCRE)', ), 'pdo' => array( - 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', - 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', + 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', + 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', ), 'php' => array( - '_' => 'PHP 相关', + '_' => 'PHP 安装', 'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s', 'ok' => '你的 PHP 版本为 %s,与 FreshRSS 兼容', ), 'tables' => array( 'nok' => '数据库中缺少一个或多个表', - 'ok' => '数据库中相关表存在', + 'ok' => '数据库中存在正确的表', ), 'title' => '环境检查', 'tokens' => array( - 'nok' => '请检查 ./data/tokens 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data/tokens 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'tokens 目录权限正常', ), 'users' => array( - 'nok' => '请检查 ./data/users 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data/users 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'users 目录权限正常', ), 'zip' => array( - 'nok' => '找不到 ZIP 扩展(php-zip)', + 'nok' => '找不到 ZIP 扩展(php-zip 包)', 'ok' => '已找到 ZIP 扩展', ), ), @@ -119,10 +119,10 @@ return array( 'enabled' => '已启用', 'latest' => '已安装', 'name' => '名称', - 'no_configure_view' => '此扩展不能配置。', + 'no_configure_view' => '此扩展无法配置。', 'system' => array( '_' => '系统扩展', - 'no_rights' => '系统扩展(你无权修改)', + 'no_rights' => '系统扩展(你没有所需权限)', ), 'title' => '扩展', 'update' => '更新可用', @@ -130,20 +130,20 @@ return array( 'version' => '版本', ), 'stats' => array( - '_' => '统计', + '_' => '统计数据', 'all_feeds' => '所有订阅源', 'category' => '分类', 'entry_count' => '文章数', 'entry_per_category' => '各分类文章数', - 'entry_per_day' => '近三十日每日文章数', - 'entry_per_day_of_week' => '一周各日(平均:%.2f 条消息)', - 'entry_per_hour' => '各小时(平均:%.2f 条消息)', - 'entry_per_month' => '各月(平均:%.2f 条消息)', + 'entry_per_day' => '每日文章数(近三十日)', + 'entry_per_day_of_week' => '一周中(平均:%.2f 条消息)', + 'entry_per_hour' => '各小时(平均:%.2f 条消息)', + 'entry_per_month' => '各月(平均:%.2f 条消息)', 'entry_repartition' => '文章分布', 'feed' => '订阅源', 'feed_per_category' => '各分类订阅源数', 'idle' => '长期无更新订阅源', - 'main' => '主要统计', + 'main' => '主要统计数据', 'main_stream' => '首页', 'no_idle' => '订阅源近期皆有更新!', 'number_entries' => '%d 篇文章', @@ -158,9 +158,9 @@ return array( ), 'system' => array( '_' => '系统配置', - 'auto-update-url' => '自动升级服务器地址', + 'auto-update-url' => '自动更新服务器 URL', 'cookie-duration' => array( - 'help' => '单位(秒)', + 'help' => '单位:秒', 'number' => '保持登录的时长', ), 'force_email_validation' => '强制验证邮箱地址', @@ -178,8 +178,8 @@ return array( ), ), 'status' => array( - 'disabled' => '注册表单禁用', - 'enabled' => '注册表单启用', + 'disabled' => '注册表单已禁用', + 'enabled' => '注册表单已启用', ), 'title' => '用户注册表单', ), @@ -191,7 +191,7 @@ return array( 'current_version' => '当前 FreshRSS 版本为 %s。', 'last' => '上次检查:%s', 'none' => '没有可用更新', - 'title' => '系统更新', + 'title' => '更新系统', ), 'user' => array( 'admin' => '管理员', diff --git a/app/i18n/zh-cn/conf.php b/app/i18n/zh-cn/conf.php index 8f8ef09ad..0be182cfb 100644 --- a/app/i18n/zh-cn/conf.php +++ b/app/i18n/zh-cn/conf.php @@ -13,17 +13,17 @@ return array( 'archiving' => array( '_' => '归档', - 'exception' => '高级清理策略', - 'help' => '具体选项位于各订阅源的设置', - 'keep_favourites' => '不清理已收藏的文章', - 'keep_labels' => '不清理标签', + 'exception' => '清理例外', + 'help' => '更多可用选项位于各订阅源的设置', + 'keep_favourites' => '永不删除已收藏的文章', + 'keep_labels' => '永不删除标签', 'keep_max' => '最多保留的文章数', 'keep_min_by_feed' => '至少保留的文章数', 'keep_period' => '文章最多保留', - 'keep_unreads' => '不清理未读文章', - 'maintenance' => '优化', + 'keep_unreads' => '永不删除未读文章', + 'maintenance' => '维护', 'optimize' => '优化数据库', - 'optimize_help' => '偶尔执行优化可以减少数据库大小', + 'optimize_help' => '偶尔执行可以减少数据库大小', 'policy' => '清理策略', 'policy_warning' => '如果未选择清理策略,则将保留全部文章。', 'purge_now' => '立即清除', @@ -32,12 +32,13 @@ return array( ), 'display' => array( '_' => '显示', + 'darkMode' => 'Automatic dark mode (beta)', // TODO 'icon' => array( 'bottom_line' => '底栏', 'display_authors' => '作者', 'entry' => '文章图标', 'publication_date' => '更新日期', - 'related_tags' => '相关标签', + 'related_tags' => '文章标签', 'sharing' => '分享', 'summary' => '摘要', 'top_line' => '顶栏', @@ -48,15 +49,22 @@ 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 future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => '“%s” 主题不再可用,请选择其他主题。', 'thumbnail' => array( 'label' => '缩略图', - 'landscape' => '风景', + 'landscape' => '横向', 'none' => '无', - 'portrait' => '肖像', - 'square' => '方块', + 'portrait' => '纵向', + 'square' => '方形', ), + 'timezone' => 'Time zone', // TODO 'title' => '显示', 'width' => array( 'content' => '内容宽度', @@ -80,17 +88,17 @@ return array( ), ), 'profile' => array( - '_' => '用户管理', + '_' => '账户管理', 'api' => 'API 管理', 'delete' => array( '_' => '账户删除', - 'warn' => '将删除你的帐户以及所有相关数据!', + 'warn' => '你的帐户以及所有相关数据将被删除。', ), 'email' => '邮箱地址', 'password_api' => 'API 密码
(例如用于手机应用)', 'password_form' => '密码
(用于 Web-form 登录方式)', 'password_format' => '至少 7 个字符', - 'title' => '用户帐户', + 'title' => '账户', ), 'query' => array( '_' => '自定义查询', @@ -135,67 +143,67 @@ return array( ), 'reading' => array( '_' => '阅读', - 'after_onread' => '「全部标记为已读」后', + 'after_onread' => '“全部标记为已读”后', 'always_show_favorites' => '默认显示收藏夹中所有的文章', 'article' => array( 'authors_date' => array( '_' => '作者和日期', - 'both' => '两者都显示', - 'footer' => '仅页脚显示', - 'header' => '仅页眉显示', + 'both' => '页脚与页眉', + 'footer' => '页脚', + 'header' => '页眉', 'none' => '不显示', ), 'feed_name' => array( - 'above_title' => '在文章标题和标签上方', + 'above_title' => '在标题/标签上方', 'none' => '不显示', 'with_authors' => '与作者和日期一行', ), 'feed_title' => '订阅源标题', 'tags' => array( '_' => '文章标签', - 'both' => '两者都显示', - 'footer' => '仅页脚显示', - 'header' => '仅页眉显示', + 'both' => '页脚与页眉', + 'footer' => '页脚', + 'header' => '页眉', 'none' => '不显示', ), 'tags_max' => array( '_' => '标签最多显示个数', - 'help' => '0 标识显示所有标签', + 'help' => '0 表示:显示所有标签且不折叠', ), ), 'articles_per_page' => '每页文章数', 'auto_load_more' => '在页面底部载入更多文章', 'auto_remove_article' => '阅读后隐藏文章', - 'confirm_enabled' => '「全部标记为已读」时显示确认对话框', + 'confirm_enabled' => '“全部标记为已读”时显示确认对话框', 'display_articles_unfolded' => '默认展开显示文章', 'display_categories_unfolded' => '展开的分类', 'headline' => array( 'articles' => '文章:打开/关闭', 'articles_header_footer' => '文章: 页眉/页脚', - 'categories' => '左侧导航:分类', + 'categories' => '左侧导航栏:分类', 'mark_as_read' => '标为已读选项', 'misc' => '其它', 'view' => '浏览', ), - 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源 (启用「显示所有文章」后不生效)', + 'hide_read_feeds' => '隐藏没有未读文章的分类和订阅源(启用“显示所有文章”后不生效)', 'img_with_lazyload' => '延迟加载图片', 'jump_next' => '跳转到下一未读项(订阅源或分类)', - 'mark_updated_article_unread' => '将更新的文章设为未读', + 'mark_updated_article_unread' => '将有更新的文章设为未读', 'number_divided_when_reader' => '阅读视图中显示一半', 'read' => array( 'article_open_on_website' => '在打开原文章后', 'article_viewed' => '在文章被浏览后', 'keep_max_n_unread' => '未读最多保留 n 条', 'scroll' => '在滚动浏览后', - 'upon_gone' => '在被原订阅源移除后', + 'upon_gone' => '在被原订阅源被移除后', 'upon_reception' => '在接收文章后', 'when' => '何时将文章标记为已读', 'when_same_title' => '已存在 n 条相同标题文章', ), 'show' => array( '_' => '文章显示', - 'active_category' => '激活的分类', - 'adaptive' => '智能显示', + 'active_category' => '活跃的分类', + 'adaptive' => '自适应显示', 'all_articles' => '显示所有', 'all_categories' => '所有分类', 'no_category' => '无分类', @@ -203,13 +211,13 @@ return array( 'unread' => '只显示未读', ), 'show_fav_unread_help' => '同样适用于标签', - 'sides_close_article' => '点击文章区域外以关闭', + 'sides_close_article' => '点击文章文本区域外关闭文章', 'sort' => array( '_' => '排列顺序', 'newer_first' => '由新至旧', 'older_first' => '由旧至新', ), - 'sticky_post' => '打开文章时将其置于页首', + 'sticky_post' => '打开文章时将其置顶', 'title' => '阅读', 'view' => array( 'default' => '默认视图', @@ -222,20 +230,20 @@ return array( '_' => '分享', 'add' => '添加分享方式', 'blogotext' => 'Blogotext', // IGNORE - 'deprecated' => '这项功能已废弃并在将来版本的 FreshRSS 中移除,详情请见 说明文档.', + 'deprecated' => '此功能已被废弃并会在未来的 FreshRSS 版本中移除,详情见 说明文档.', 'diaspora' => 'Diaspora*', // IGNORE - 'email' => '邮箱', // IGNORE - 'facebook' => '脸书', // IGNORE + 'email' => 'Email', // IGNORE + 'facebook' => 'Facebook', // IGNORE 'more_information' => '更多信息', 'print' => '打印', 'raindrop' => 'Raindrop.io', // IGNORE 'remove' => '删除分享方式', 'shaarli' => 'Shaarli', // IGNORE - 'share_name' => '名称', - 'share_url' => '地址', + 'share_name' => '显示名称', + 'share_url' => '用于分享的 URL', 'title' => '分享', - 'twitter' => '推特', // IGNORE - 'wallabag' => 'Wallabag', // IGNORE + 'twitter' => 'Twitter', // IGNORE + 'wallabag' => 'wallabag', // IGNORE ), 'shortcut' => array( '_' => '快捷键', @@ -243,9 +251,9 @@ return array( 'auto_share' => '分享', 'auto_share_help' => '如果有多种分享方式,则会按照它们的序号依次访问。', 'close_dropdown' => '关闭菜单', - 'collapse_article' => '收起文章', + 'collapse_article' => '折叠文章', 'first_article' => '打开第一篇文章', - 'focus_search' => '聚焦到搜索框', + 'focus_search' => '访问搜索框', 'global_view' => '切换到全屏视图', 'help' => '显示帮助文档', 'javascript' => '若要使用快捷键,必须启用 JavaScript', @@ -254,18 +262,18 @@ return array( 'mark_favorite' => '加入收藏', 'mark_read' => '设为已读', 'navigation' => '浏览', - 'navigation_help' => '组合 ⇧ Shift 键,浏览快捷键将生效于订阅源。
组合 Alt ⎇ 键,浏览快捷键将生效于分类。', + 'navigation_help' => '组合 ⇧ Shift 键,导航快捷键将应用于订阅源。
组合 Alt ⎇ 键,导航快捷键将应用于分类。', 'navigation_no_mod_help' => '以下快捷键不支持组合键(Shift 或 Alt)', 'next_article' => '打开下一篇文章', 'next_unread_article' => '打开下一篇未读文章', - 'non_standard' => '这些键 (%s) 可能不能作为快捷键', + 'non_standard' => '这些键(%s)可能不能作为快捷键', 'normal_view' => '切换到普通视图', 'other_action' => '其他操作', 'previous_article' => '打开上一篇文章', 'reading_view' => '切换到阅读视图', 'rss_view' => '切换到 RSS 视图', 'see_on_website' => '在原网站中查看', - 'shift_for_all_read' => '组合 Alt ⎇键 将上方的文章标记为已读
组合 ⇧ Shift按键 可以将全部文章设为已读', + 'shift_for_all_read' => '+ Alt ⎇ 键将上方的文章标记为已读
+ ⇧ Shift 键将所有文章设为已读', 'skip_next_article' => '跳转到下一篇文章而不打开', 'skip_previous_article' => '跳转到上一篇文章而不打开', 'title' => '快捷键', @@ -275,7 +283,7 @@ return array( 'views' => '视图', ), 'user' => array( - 'articles_and_size' => '%s 篇文章 (%s)', + 'articles_and_size' => '%s 篇文章(%s)', 'current' => '当前用户', 'is_admin' => '该用户为管理员', 'users' => '用户', diff --git a/app/i18n/zh-cn/feedback.php b/app/i18n/zh-cn/feedback.php index 020e70918..701471f4e 100644 --- a/app/i18n/zh-cn/feedback.php +++ b/app/i18n/zh-cn/feedback.php @@ -20,8 +20,8 @@ return array( ), 'api' => array( 'password' => array( - 'failed' => '您的密码无法修改', - 'updated' => '您的密码已修改', + 'failed' => '你的密码无法修改', + 'updated' => '你的密码已修改', ), ), 'auth' => array( @@ -43,7 +43,7 @@ return array( 'already_enabled' => '%s 已启用', 'cannot_remove' => '无法删除 %s', 'disable' => array( - 'ko' => '禁用 %s 失败。检查 FreshRSS 日志 查看详情。', + 'ko' => '无法禁用 %s。检查 FreshRSS 日志 查看详情。', 'ok' => '%s 现已禁用', ), 'enable' => array( @@ -56,15 +56,15 @@ return array( 'removed' => '%s 已删除', ), 'import_export' => array( - 'export_no_zip_extension' => '服务器未启用 ZIP 扩展。请尝试逐个导出文件。', - 'feeds_imported' => '你的订阅已导入,即将刷新', + 'export_no_zip_extension' => '服务器未启用 ZIP 扩展,请尝试逐个导出文件。', + 'feeds_imported' => '你的订阅源已导入,即将刷新', 'feeds_imported_with_errors' => '你的订阅源已导入,但发生错误', 'file_cannot_be_uploaded' => '文件未能上传!', 'no_zip_extension' => '服务器未启用 ZIP 扩展。', 'zip_error' => '导入 ZIP 文件时出错', ), 'profile' => array( - 'error' => '你的帐户修改失败', + 'error' => '你的帐户无法修改', 'updated' => '你的帐户已修改', ), 'sub' => array( @@ -79,7 +79,7 @@ return array( 'emptied' => '已清空分类', 'error' => '更新分类失败', 'name_exists' => '分类名已存在', - 'no_id' => '你必须明确分类编号', + 'no_id' => '你必须指定分类 ID', 'no_name' => '分类名不能为空', 'not_delete_default' => '你不能删除默认分类!', 'not_exist' => '分类不存在!', @@ -94,21 +94,21 @@ return array( 'cache_cleared' => '%s 缓存已清理', 'deleted' => '已删除订阅源', 'error' => '订阅源更新失败', - 'internal_problem' => '订阅源添加失败。检查 FreshRSS 日志 查看详情。你可以在地址链接后附加 #force_feed 从而尝试强制添加。', - 'invalid_url' => '地址链接 %s 无效', + 'internal_problem' => '订阅源添加失败,检查 FreshRSS 日志 查看详情。你可以在 URL 后添加 #force_feed 尝试强制添加。', + 'invalid_url' => 'URL %s 无效', 'n_actualized' => '已更新 %d 个订阅源', 'n_entries_deleted' => '已删除 %d 篇文章', - 'no_refresh' => '没有可刷新的订阅源…', + 'no_refresh' => '没有可刷新的订阅源', 'not_added' => '%s 添加失败', 'not_found' => '无法找到订阅', 'over_max' => '你已达到订阅源数上限(%d)', - 'reloaded' => '%s 已重置', + 'reloaded' => '%s 已重新加载', 'selector_preview' => array( 'http_error' => '无法加载网站内容。', - 'no_entries' => '您的订阅中没有任何条目。您至少需要一个条目来创建一个预览。', + 'no_entries' => '你的订阅中没有任何条目,你至少需要一个条目来创建一个预览。', 'no_feed' => '网络错误(订阅源不存在)', - 'no_result' => '选择器没有匹配到任何东西。作为备用,原始的feed文本将被显示出来。', - 'selector_empty' => '选择器是空的。你需要一个来创建预览。', + 'no_result' => '选择器没有匹配到任何东西,回退显示原始的订阅源文本。', + 'selector_empty' => '选择器是空的,你需要一个来创建预览。', ), 'updated' => '已更新订阅源', ), @@ -122,10 +122,10 @@ return array( 'update' => array( 'can_apply' => 'FreshRSS 将更新到 版本 %s。', 'error' => '更新出错:%s', - 'file_is_nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'file_is_nok' => '请检查 %s 目录权限。HTTP 服务器必须拥有写入权限。', 'finished' => '更新完成!', 'none' => '没有可用更新', - 'server_not_found' => '找不到更新服务器 [%s]', + 'server_not_found' => '找不到更新服务器。 [%s]', ), 'user' => array( 'created' => array( diff --git a/app/i18n/zh-cn/gen.php b/app/i18n/zh-cn/gen.php index 2b2249db5..d4999e5b0 100644 --- a/app/i18n/zh-cn/gen.php +++ b/app/i18n/zh-cn/gen.php @@ -12,7 +12,7 @@ return array( 'action' => array( - 'actualize' => '更新提要', + 'actualize' => '更新订阅源', 'add' => '添加', 'back' => '← 返回', 'back_to_rss_feeds' => '← 返回订阅源', @@ -26,7 +26,7 @@ return array( 'export' => '导出', 'filter' => '过滤', 'import' => '导入', - 'load_default_shortcuts' => '重置快捷键', + 'load_default_shortcuts' => '加载默认快捷键', 'manage' => '管理', 'mark_read' => '标记已读', 'open_url' => '打开链接', @@ -38,7 +38,7 @@ return array( 'see_website' => '网站中查看', 'submit' => '提交', 'truncate' => '删除所有文章', - 'update' => '更新订阅', + 'update' => '更新', ), 'auth' => array( 'accept_tos' => '我接受 服务条款', @@ -127,7 +127,7 @@ return array( 'js' => array( 'category_empty' => '清空分类', 'confirm_action' => '你确定要执行此操作吗?这将不可撤销!', - 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询。这将不可撤销!', + 'confirm_action_feed_cat' => '你确定要执行此操作吗?你将丢失相关的收藏和自定义查询,这将不可撤销!', 'feedback' => array( 'body_new_articles' => 'FreshRSS 中有 %%d 篇文章等待阅读。', 'body_unread_articles' => '(未读: %%d)', @@ -174,13 +174,14 @@ return array( 'queries' => '自定义查询', 'reading' => '阅读', 'search' => '搜索内容或#标签', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => '分享', 'shortcuts' => '快捷键', 'stats' => '统计', 'system' => '系统配置', 'update' => '更新', 'user_management' => '用户管理', - 'user_profile' => '用户帐户', + 'user_profile' => '帐户', ), 'period' => array( 'days' => '天', @@ -191,12 +192,14 @@ return array( ), 'share' => array( 'Known' => '基于 Known 的站点', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => '剪贴板', 'diaspora' => 'Diaspora*', // IGNORE - 'email' => '邮箱', // IGNORE - 'facebook' => '脸书', // IGNORE + 'email' => 'Email', // IGNORE + 'email-webmail-firefox-fix' => 'Email (webmail - 兼容 Firefox)', + 'facebook' => 'Facebook', // IGNORE 'gnusocial' => 'GNU social', // IGNORE 'jdh' => 'Journal du hacker', // IGNORE 'lemmy' => 'Lemmy', // IGNORE @@ -211,10 +214,10 @@ return array( 'raindrop' => 'Raindrop.io', // IGNORE 'reddit' => 'Reddit', // IGNORE 'shaarli' => 'Shaarli', // IGNORE - 'twitter' => '推特', // IGNORE + 'twitter' => 'Twitter', // IGNORE 'wallabag' => 'Wallabag v1', // IGNORE 'wallabagv2' => 'Wallabag v2', // IGNORE - 'web-sharing-api' => 'Web分享', + 'web-sharing-api' => '系统分享', 'whatsapp' => 'Whatsapp', // IGNORE 'xing' => 'Xing', // IGNORE ), diff --git a/app/i18n/zh-cn/index.php b/app/i18n/zh-cn/index.php index 916140107..59d9ffb87 100644 --- a/app/i18n/zh-cn/index.php +++ b/app/i18n/zh-cn/index.php @@ -17,7 +17,7 @@ return array( 'bugs_reports' => '报告错误', 'credits' => '致谢', 'credits_content' => '某些设计元素来自于 Bootstrap ,尽管 FreshRSS 并没有使用此框架。图标 来自于 GNOME 项目Open Sans 字体出自 Steve Matteson 之手。FreshRSS 基于 PHP 框架 Minz。', - 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 Kriss FeedLeed。 它不仅轻快又易用,而且强大又易于配置。', + 'freshrss_description' => 'FreshRSS 是一个自托管的 RSS 聚合服务,类似于 Kriss FeedLeed。 它不仅轻快易用,并且强大又易于配置。', 'github' => 'Github Issues', 'license' => '授权', 'project_website' => '项目网站', @@ -25,8 +25,8 @@ return array( 'version' => '版本', ), 'feed' => array( - 'add' => '你可以添加一些订阅源。', - 'empty' => '暂时没有文章可显示。', + 'add' => '请添加一些订阅源。', + 'empty' => '没有文章可以显示。', 'rss_of' => '%s 的订阅源', 'title' => '首页', 'title_fav' => '收藏', diff --git a/app/i18n/zh-cn/install.php b/app/i18n/zh-cn/install.php index 8927674d2..1d9d61e38 100644 --- a/app/i18n/zh-cn/install.php +++ b/app/i18n/zh-cn/install.php @@ -21,7 +21,7 @@ return array( 'auth' => array( 'form' => '网页表单(传统方式, 依赖 JavaScript)', 'http' => 'HTTP(面向启用 HTTPS 的高级用户)', - 'none' => '无认证(危险)', + 'none' => '无(危险)', 'password_form' => '密码
(用于网页表单登录方式)', 'password_format' => '至少 7 个字符', 'type' => '认证方式', @@ -30,61 +30,61 @@ return array( '_' => '数据库', 'conf' => array( '_' => '数据库配置', - 'ko' => '请验证你的数据库信息', + 'ko' => '验证你的数据库信息', 'ok' => '数据库配置已保存', ), 'host' => '主机', - 'password' => '密码', + 'password' => '数据库密码', 'prefix' => '表前缀', 'type' => '数据库类型', - 'username' => '用户名', + 'username' => '数据库用户名', ), 'check' => array( '_' => '检查', 'already_installed' => '我们检测到 FreshRSS 已经安装!', 'cache' => array( - 'nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 %s 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'cache 目录权限正常', ), 'ctype' => array( 'nok' => '找不到字符类型检测库(php-ctype)', - 'ok' => '已找到字符类型检测库', + 'ok' => '已找到字符类型检测库(ctype)', ), 'curl' => array( - 'nok' => '找不到 cURL 库(php-curl)', + 'nok' => '找不到 cURL 库(php-curl 包)', 'ok' => '已找到 cURL 库', ), 'data' => array( - 'nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 %s 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'data 目录权限正常', ), 'dom' => array( - 'nok' => '找不到用于浏览 DOM 的库(php-xml)', + 'nok' => '找不到用于浏览 DOM 的库(php-xml 包)', 'ok' => '已找到用于浏览 DOM 的库', ), 'favicons' => array( - 'nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 ./data/favicons 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'favicons 目录权限正常', ), 'fileinfo' => array( - 'nok' => '找不到 PHP fileinfo 库(php-fileinfo)', + 'nok' => '找不到 PHP fileinfo 库(fileinfo 包)', 'ok' => '已找到 fileinfo 库', ), 'json' => array( - 'nok' => '找不到推荐的 JSON 解析库', - 'ok' => '已找到推荐的 JSON 解析库', + 'nok' => '找不到 JSON 扩展(php-json 包)', + 'ok' => '已找到 JSON 扩展', ), 'mbstring' => array( - 'nok' => '找不到推荐的 Unicode 解析库(mbstring)', - 'ok' => '已找到推荐的 Unicode 解析库', + 'nok' => '找不到推荐用于 Unicode 的 mbstring 库', + 'ok' => '已找到推荐用于 Unicode 的 mbstring 库', ), 'pcre' => array( 'nok' => '找不到正则表达式解析库(php-pcre)', - 'ok' => '已找到正则表达式解析库', + 'ok' => '已找到正则表达式解析库(PCRE)', ), 'pdo' => array( - 'nok' => '找不到 PDO 或支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', - 'ok' => '已找到 PDO 和支持的至少一种驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', + 'nok' => '找不到 PDO 或其中一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', + 'ok' => '已找到 PDO 和至少一种支持的驱动(pdo_mysql、pdo_sqlite、pdo_pgsql)', ), 'php' => array( 'nok' => '你的 PHP 版本为 %s,但 FreshRSS 最低需要 %s', @@ -92,12 +92,12 @@ return array( ), 'reload' => '再检查一遍', 'tmp' => array( - 'nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 %s 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => '缓存目录权限正常。', ), 'unknown_process_username' => '未知', 'users' => array( - 'nok' => '请检查 %s 目录权限。HTTP 服务器必须有其写入权限。', + 'nok' => '请检查 %s 目录权限,HTTP 服务器必须拥有写入权限。', 'ok' => 'users 目录权限正常', ), 'xml' => array( @@ -114,7 +114,7 @@ return array( 'fix_errors_before' => '请在继续下一步前修复错误', 'javascript_is_better' => '启用 JavaScript 会使 FreshRSS 工作得更好', 'js' => array( - 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置。你确定要继续吗?', + 'confirm_reinstall' => '重新安装 FreshRSS 将会重置之前的配置,你确定要继续吗?', ), 'language' => array( '_' => '语言', diff --git a/app/i18n/zh-cn/sub.php b/app/i18n/zh-cn/sub.php index 4ad401329..5e6e570a9 100644 --- a/app/i18n/zh-cn/sub.php +++ b/app/i18n/zh-cn/sub.php @@ -16,9 +16,9 @@ return array( 'title' => 'API', // IGNORE ), 'bookmarklet' => array( - 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮', + 'documentation' => '拖动此书签到你的书签栏或者右键选择「收藏此链接」,然后在你想要订阅的页面上点击「订阅」按钮。', 'label' => '订阅', - 'title' => '书签应用', + 'title' => '书签', ), 'category' => array( '_' => '分类', @@ -26,18 +26,18 @@ return array( 'archiving' => '归档', 'dynamic_opml' => array( '_' => '动态订阅', - 'help' => '使用地址上的 OPML 文件 中的订阅源填充这一分类', + 'help' => '使用 URL 上的 OPML 文件 中的订阅源填充这一分类', ), 'empty' => '空分类', 'information' => '信息', - 'opml_url' => 'OPML 地址', + 'opml_url' => 'OPML URL', // IGNORE 'position' => '显示位置', 'position_help' => '控制分类排列顺序', 'title' => '标题', ), 'feed' => array( 'accept_cookies' => '接受 Cookies', - 'accept_cookies_help' => '允许提要服务器设置 Cookies(仅在请求期间存储在内存中)', + 'accept_cookies_help' => '允许订阅源服务器设置 Cookies(仅在请求期间存储在内存中)', 'add' => '添加订阅源', 'advanced' => '高级', 'archiving' => '归档', @@ -77,8 +77,8 @@ return array( 'html_xpath' => array( '_' => 'HTML + XPath (Web 抓取)', 'feed_title' => array( - '_' => '提要标题', - 'help' => '如 //title 或是静态字符串如 "My custom feed"', + '_' => '订阅源标题', + 'help' => '如 //title 或是静态字符串如: "My custom feed"', ), 'help' => 'XPath 1.0 是为资深用户准备的标准查询语言,FreshRSS 用以实现 Web 抓取.', 'item' => array( @@ -99,8 +99,8 @@ return array( 'help' => '例如 descendant::img/@src', ), 'item_timeFormat' => array( - '_' => 'Custom date/time format', // TODO - 'help' => 'Optional. A format supported by DateTime::createFromFormat() such as d-m-Y H:i:s', // TODO + '_' => '自定义日期/时间格式', + 'help' => '可选项, 格式参见 DateTime::createFromFormat() 例如 d-m-Y H:i:s', ), 'item_timestamp' => array( '_' => '文章日期:', @@ -111,7 +111,7 @@ return array( 'help' => '注意使用 XPath 轴 descendant::,例如 descendant::h2', ), 'item_uid' => array( - '_' => '文章唯一标识', + '_' => '文章唯一 ID', 'help' => '可选,例如: descendant::div/@data-uri', ), 'item_uri' => array( @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath 定位:', ), 'rss' => 'RSS / Atom (默认)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => '清理缓存', @@ -175,8 +176,8 @@ return array( 'export_opml' => '导出订阅源列表(OPML)', 'export_starred' => '导出你的收藏', 'feed_list' => '%s 文章列表', - 'file_to_import' => '需要导入的文件
(OPML、JSON 或 ZIP)', - 'file_to_import_no_zip' => '需要导入的文件
(OPML 或 JSON)', + 'file_to_import' => '需要导入的文件
(OPML、JSON 或 ZIP)', + 'file_to_import_no_zip' => '需要导入的文件
(OPML 或 JSON)', 'import' => '导入', 'starred_list' => '收藏文章列表', 'title' => '导入/导出', diff --git a/app/i18n/zh-cn/user.php b/app/i18n/zh-cn/user.php index 8b4d35a7f..8a096b985 100644 --- a/app/i18n/zh-cn/user.php +++ b/app/i18n/zh-cn/user.php @@ -13,21 +13,21 @@ return array( 'email' => array( 'feedback' => array( - 'invalid' => '电子邮箱地址无效', + 'invalid' => '邮箱地址无效', 'required' => '必须填写邮箱地址', ), 'validation' => array( - 'change_email' => '您可以在 用户管理 中变更您的邮箱地址', - 'email_sent_to' => '我们已通过 %s 发送验证邮件给您,请按其中指示来验证邮箱地址。', + 'change_email' => '你可以在 用户管理 中变更你的邮箱地址', + 'email_sent_to' => '我们已通过 %s 发送验证邮件给你,请按其中指示来验证邮箱地址。', 'feedback' => array( - 'email_failed' => '由于服务器配置错误,我们无法向您发送邮件。', - 'email_sent' => '邮件已发送到您的邮箱中', + 'email_failed' => '由于服务器配置错误,我们无法向你发送邮件。', + 'email_sent' => '邮件已发送到你的邮箱中', 'error' => '邮箱地址无法通过验证', 'ok' => '邮箱地址已成功通过验证', 'unnecessary' => '该邮箱地址已被验证', 'wrong_token' => '由于令牌错误,邮箱地址无法通过验证。', ), - 'need_to' => '您需要先验证邮箱地址才能使用 %s', + 'need_to' => '你需要先验证邮箱地址才能使用 %s', 'resend_email' => '重发邮件', 'title' => '验证邮箱地址', ), @@ -35,8 +35,8 @@ return array( 'mailer' => array( 'email_need_validation' => array( 'body' => '%s,欢迎', - 'title' => '您需要验证您的帐户', - 'welcome' => '您已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:', + 'title' => '你需要验证你的帐户', + 'welcome' => '你已注册 %s 现在只需点击下方链接通过邮箱验证即可完成注册:', ), ), 'password' => array( @@ -44,7 +44,7 @@ return array( ), 'tos' => array( 'feedback' => array( - 'invalid' => '您必须接受服务条款才能注册', + 'invalid' => '你必须接受服务条款才能注册', ), ), 'username' => array( diff --git a/app/i18n/zh-tw/conf.php b/app/i18n/zh-tw/conf.php index 15fabaa40..34439c01b 100644 --- a/app/i18n/zh-tw/conf.php +++ b/app/i18n/zh-tw/conf.php @@ -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 future release of FreshRSS', // TODO + ), + ), 'theme_not_available' => '“%s” 主題不再可用,請選擇其他主題。', 'thumbnail' => array( 'label' => '縮圖', @@ -57,6 +64,7 @@ return array( 'portrait' => '肖像', 'square' => '方塊', ), + 'timezone' => 'Time zone', // TODO 'title' => '顯示', 'width' => array( 'content' => '內容寬度', diff --git a/app/i18n/zh-tw/gen.php b/app/i18n/zh-tw/gen.php index 1dcd94eeb..3ef8bca44 100644 --- a/app/i18n/zh-tw/gen.php +++ b/app/i18n/zh-tw/gen.php @@ -174,6 +174,7 @@ return array( 'queries' => '自定義查詢', 'reading' => '閱讀', 'search' => '搜尋內容或#標簽', + 'search_help' => 'See documentation for advanced search parameters', // TODO 'sharing' => '分享', 'shortcuts' => '快捷鍵', 'stats' => '統計', @@ -191,11 +192,13 @@ return array( ), 'share' => array( 'Known' => '基於 Known 的站點', + 'archiveORG' => 'archive.org', // IGNORE 'archivePH' => 'archive.ph', // IGNORE 'blogotext' => 'Blogotext', // IGNORE 'clipboard' => '剪貼板', 'diaspora' => 'Diaspora*', // IGNORE 'email' => '郵箱', // IGNORE + 'email-webmail-firefox-fix' => 'Email (webmail - fix for Firefox)', // TODO 'facebook' => '臉書', // IGNORE 'gnusocial' => 'GNU social', // IGNORE 'jdh' => 'Journal du hacker', // IGNORE diff --git a/app/i18n/zh-tw/sub.php b/app/i18n/zh-tw/sub.php index dddcb2661..8a255645d 100644 --- a/app/i18n/zh-tw/sub.php +++ b/app/i18n/zh-tw/sub.php @@ -122,6 +122,7 @@ return array( 'xpath' => 'XPath 定位:', ), 'rss' => 'RSS / Atom (默認)', + 'xml_xpath' => 'XML + XPath', // TODO ), 'maintenance' => array( 'clear_cache' => '清理暫存', diff --git a/app/install.php b/app/install.php index 9d0d855b8..3163367f4 100644 --- a/app/install.php +++ b/app/install.php @@ -283,11 +283,7 @@ function freshrss_already_installed() { // A configuration file already exists, we try to load it. $system_conf = null; try { - Minz_Configuration::register('system', $conf_path); - /** - * @var FreshRSS_SystemConfiguration $system_conf - */ - $system_conf = Minz_Configuration::get('system'); + $system_conf = FreshRSS_SystemConfiguration::init($conf_path); } catch (Minz_FileNotExistException $e) { return false; } @@ -295,7 +291,7 @@ function freshrss_already_installed() { // ok, the global conf exists… but what about default user conf? $current_user = $system_conf->default_user; try { - Minz_Configuration::register('user', join_path(USERS_PATH, $current_user, 'config.php')); + FreshRSS_UserConfiguration::init(USERS_PATH . '/' . $current_user . '/config.php'); } catch (Minz_FileNotExistException $e) { return false; } @@ -449,7 +445,7 @@ function printStep1() {

- 🔃 + 🔃 diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 5f1762834..03b8108f7 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -1,72 +1,90 @@ diff --git a/app/layout/aside_feed.phtml b/app/layout/aside_feed.phtml index 3c4f1ec2e..bb9d678dc 100644 --- a/app/layout/aside_feed.phtml +++ b/app/layout/aside_feed.phtml @@ -37,7 +37,7 @@
  • - + diff --git a/app/layout/aside_subscription.phtml b/app/layout/aside_subscription.phtml index aa7857f74..e1f520f34 100644 --- a/app/layout/aside_subscription.phtml +++ b/app/layout/aside_subscription.phtml @@ -1,38 +1,45 @@ diff --git a/app/layout/header.phtml b/app/layout/header.phtml index f8e54c7ce..0a49d5992 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -2,7 +2,7 @@
    logo_html == '') { ?> - + logo_html; @@ -15,10 +15,15 @@ allow_anonymous) { ?>
    - + + + + + @@ -48,38 +53,59 @@
    diff --git a/app/layout/layout.phtml b/app/layout/layout.phtml index 48ada7183..1e9ce6905 100644 --- a/app/layout/layout.phtml +++ b/app/layout/layout.phtml @@ -52,7 +52,7 @@ if (_t('gen.dir') === 'rtl') { - + "> +
    + - - logo_html == '') { ?> - + logo_html; diff --git a/app/shares.php b/app/shares.php index 8685dba5d..117cd4dce 100644 --- a/app/shares.php +++ b/app/shares.php @@ -26,6 +26,13 @@ */ return array( + 'archiveORG' => array( + 'url' => 'https://web.archive.org/save/~LINK~', + 'transform' => array(), + 'help' => 'https://web.archive.org', + 'form' => 'simple', + 'method' => 'GET', + ), 'archivePH' => array( 'url' => 'https://archive.ph/submit/?url=~LINK~', 'transform' => array(), @@ -61,6 +68,12 @@ return array( 'form' => 'simple', 'method' => 'GET', ), + 'email-webmail-firefox-fix' => array( // see https://github.com/FreshRSS/FreshRSS/issues/2666 + 'url' => 'mailto:?subject=~TITLE~&body=~LINK~', + 'transform' => array('rawurlencode'), + 'form' => 'simple', + 'method' => 'GET', + ), 'facebook' => array( 'url' => 'https://www.facebook.com/sharer.php?u=~LINK~&t=~TITLE~', 'transform' => array('rawurlencode'), @@ -88,7 +101,7 @@ return array( 'method' => 'GET', ), 'lemmy' => array( - 'url' => '~URL~/create_post?url=~LINK~&name=~TITLE~', + 'url' => '~URL~/create_post?url=~LINK~&title=~TITLE~', 'transform' => array('rawurlencode'), 'help' => 'https://join-lemmy.org/', 'form' => 'advanced', diff --git a/app/views/auth/register.phtml b/app/views/auth/register.phtml index a56eff3ee..999b2406a 100644 --- a/app/views/auth/register.phtml +++ b/app/views/auth/register.phtml @@ -15,6 +15,18 @@
    +
    + + +
    +
    +
    + +
    + +
    +
    +
    @@ -41,7 +60,7 @@ data-leave-validation="theme === $theme['id']) ? 1 : 0 ?>" />
  • - +
    -
    -
    +
    + + : + + +
    +
    + +
    + + +
  • @@ -76,6 +105,16 @@ +
    + +
    + +
    +
    + content_width; ?>
    @@ -126,7 +165,7 @@   - + diff --git a/app/views/configure/integration.phtml b/app/views/configure/integration.phtml index c078ae709..34c10b3c3 100644 --- a/app/views/configure/integration.phtml +++ b/app/views/configure/integration.phtml @@ -15,7 +15,7 @@