Ver Fonte

Merge pull request #2451 from FreshRSS/dev

FreshRSS 1.14.3
Alexandre Alapetite há 6 anos atrás
pai
commit
82611c9622
79 ficheiros alterados com 646 adições e 157 exclusões
  1. 5 0
      .hadolint.yaml
  2. 5 0
      .travis.yml
  3. 32 0
      CHANGELOG.md
  4. 12 5
      CREDITS.md
  5. 27 6
      Docker/Dockerfile
  6. 22 2
      Docker/Dockerfile-Alpine
  7. 73 0
      Docker/Dockerfile-QEMU-ARM
  8. 10 2
      Docker/README.md
  9. 7 4
      Docker/entrypoint.sh
  10. 21 0
      Docker/hooks/build
  11. 4 0
      Docker/hooks/post_checkout
  12. 11 0
      Docker/hooks/pre_build
  13. 10 4
      app/Controllers/feedController.php
  14. 1 1
      app/Controllers/indexController.php
  15. 43 3
      app/Controllers/subscriptionController.php
  16. 1 1
      app/Controllers/userController.php
  17. 6 3
      app/FreshRSS.php
  18. 7 0
      app/Models/Category.php
  19. 1 0
      app/Models/CategoryDAO.php
  20. 3 1
      app/i18n/cz/sub.php
  21. 3 1
      app/i18n/de/sub.php
  22. 3 1
      app/i18n/en/sub.php
  23. 3 1
      app/i18n/es/sub.php
  24. 3 1
      app/i18n/fr/sub.php
  25. 3 1
      app/i18n/he/sub.php
  26. 3 1
      app/i18n/it/sub.php
  27. 3 1
      app/i18n/kr/sub.php
  28. 3 1
      app/i18n/nl/sub.php
  29. 3 1
      app/i18n/oc/sub.php
  30. 3 1
      app/i18n/pt-br/sub.php
  31. 3 1
      app/i18n/ru/sub.php
  32. 3 1
      app/i18n/tr/sub.php
  33. 3 1
      app/i18n/zh-cn/sub.php
  34. 10 7
      app/install.php
  35. 8 1
      app/layout/nav_menu.phtml
  36. 10 10
      app/views/configure/shortcut.phtml
  37. 1 1
      app/views/feed/add.phtml
  38. 34 0
      app/views/helpers/category/update.phtml
  39. 2 2
      app/views/helpers/feed/update.phtml
  40. 0 5
      app/views/index/global.phtml
  41. 0 5
      app/views/index/normal.phtml
  42. 0 5
      app/views/index/reader.phtml
  43. 5 0
      app/views/subscription/category.phtml
  44. 5 44
      app/views/subscription/index.phtml
  45. 16 0
      cli/README.md
  46. 1 1
      constants.php
  47. 1 0
      docs/en/admins/01_Index.md
  48. 1 1
      docs/en/admins/02_Installation.md
  49. 28 0
      docs/en/admins/04_Frequently_Asked_Questions.md
  50. 17 0
      docs/en/users/03_Main_view.md
  51. 1 1
      docs/en/users/06_Fever_API.md
  52. 2 0
      docs/en/users/06_Mobile_access.md
  53. 1 1
      docs/fr/users/01_Installation.md
  54. 2 0
      docs/fr/users/06_Mobile_access.md
  55. 1 1
      p/api/.htaccess
  56. 2 2
      p/api/greader.php
  57. 0 1
      p/scripts/jquery.min.js
  58. 4 8
      p/scripts/main.js
  59. 29 1
      p/themes/Ansum/_components.scss
  60. 20 1
      p/themes/Ansum/ansum.css
  61. 0 0
      p/themes/Ansum/ansum.css.map
  62. 1 0
      p/themes/Ansum/sass.sh
  63. 2 0
      p/themes/BlueLagoon/BlueLagoon.css
  64. 2 0
      p/themes/Dark/dark.css
  65. 2 0
      p/themes/Flat/flat.css
  66. 27 0
      p/themes/Mapco/_components.scss
  67. 19 0
      p/themes/Mapco/mapco.css
  68. 0 0
      p/themes/Mapco/mapco.css.map
  69. 1 0
      p/themes/Mapco/sass.sh
  70. 2 0
      p/themes/Origine-compact/origine-compact.css
  71. 2 0
      p/themes/Origine/origine.css
  72. 2 0
      p/themes/Pafat/pafat.css
  73. 2 0
      p/themes/Screwdriver/screwdriver.css
  74. 1 2
      p/themes/Swage/icons/refresh.svg
  75. 3 1
      p/themes/Swage/swage.css
  76. 3 2
      p/themes/Swage/swage.scss
  77. 3 9
      p/themes/base-theme/template.css
  78. 4 0
      tests/README.md
  79. 29 0
      tests/shellchecks.sh

+ 5 - 0
.hadolint.yaml

@@ -0,0 +1,5 @@
+ignored:
+  # ignore apt version pinning
+  - DL3008
+  # ignore apk version pinning
+  - DL3018

+ 5 - 0
.travis.yml

@@ -42,10 +42,15 @@ matrix:
         - "node"
       php:
         # none
+      env:
+        - HADOLINT="$HOME/hadolint"
       install:
         - npm install jshint
+        - curl -sLo "$HADOLINT" $(curl -s https://api.github.com/repos/hadolint/hadolint/releases/latest?access_token="$GITHUB_TOKEN" | jq -r '.assets | .[] | select(.name=="hadolint-Linux-x86_64") | .browser_download_url') && chmod 700 ${HADOLINT}
       script:
         - node_modules/jshint/bin/jshint .
+        - bash tests/shellchecks.sh
+        - git ls-files --exclude='*Dockerfile*' --ignored | xargs --max-lines=1 "$HADOLINT"
   allow_failures:
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - dist: precise

+ 32 - 0
CHANGELOG.md

@@ -1,5 +1,37 @@
 # FreshRSS changelog
 
+## 2019-07-25 FreshRSS 1.14.3
+
+* UI
+	* New configuration page for each category [#2369](https://github.com/FreshRSS/FreshRSS/issues/2369)
+	* Update shortcut configuration page [#2405](https://github.com/FreshRSS/FreshRSS/issues/2405)
+	* CSS style for printing [#2149](https://github.com/FreshRSS/FreshRSS/issues/2149)
+	* Do not hide multiple `<br />` tags [#2437](https://github.com/FreshRSS/FreshRSS/issues/2437)
+	* Updated to jQuery 3.4.1 (only for statistics page) [#2424](https://github.com/FreshRSS/FreshRSS/pull/2424)
+* Bug fixing
+	* Fix wrong mark-as-read limit [#2429](https://github.com/FreshRSS/FreshRSS/issues/2429)
+	* Fix API call for removing a category [#2411](https://github.com/FreshRSS/FreshRSS/issues/2411)
+	* Fix user self-registration [#2381](https://github.com/FreshRSS/FreshRSS/issues/2381)
+	* Make CGI Authorization configuration for API more compatible [#2446](https://github.com/FreshRSS/FreshRSS/issues/2446)
+	* Fix refresh icon in Swage theme [#2375](https://github.com/FreshRSS/FreshRSS/issues/2375)
+	* Fix message banner in Swage theme [#2379](https://github.com/FreshRSS/FreshRSS/issues/2379)
+	* Docker: Add `php-gmp` for API support in Ubuntu 32-bit [#2450](https://github.com/FreshRSS/FreshRSS/pull/2450)
+* Deployment
+	* Docker: Add automatic health check [#2438](https://github.com/FreshRSS/FreshRSS/pull/2438), [#2455](https://github.com/FreshRSS/FreshRSS/pull/2455)
+	* Docker: Add a version for ARM architecture such as for Raspberry Pi [#2436](https://github.com/FreshRSS/FreshRSS/pull/2436)
+	* Docker: Ubuntu image updated to 19.04 with PHP 7.2.19 and Apache 2.4.38 [#2422](https://github.com/FreshRSS/FreshRSS/pull/2422)
+	* Docker: Alpine image updated to 3.10 with PHP 7.3.7 and Apache 2.4.39 [#2238](https://github.com/FreshRSS/FreshRSS/pull/2238)
+	* Add `hadolint` automatic check of Docker files in Travis [#2456](https://github.com/FreshRSS/FreshRSS/pull/2456)
+* Security
+	* Allow `@-` as valid characters in usernames (i.e. allow most e-mails) [#2391](https://github.com/FreshRSS/FreshRSS/issues/2391)
+* I18n
+	* Improve Occitan [#2358](https://github.com/FreshRSS/FreshRSS/pull/2358)
+* Misc.
+	* New parameter `?maxFeeds=10` to control the max number of feeds to refresh manually [#2388](https://github.com/FreshRSS/FreshRSS/pull/2388)
+	* Default to SQLite during install [#2443](https://github.com/FreshRSS/FreshRSS/pull/2443)
+	* Add automatic check of shell scripts in Travis with `shellcheck` and `shfmt` [#2454](https://github.com/FreshRSS/FreshRSS/pull/2454)
+
+
 ## 2019-04-08 FreshRSS 1.14.2
 
 * Bug fixing (regressions introduced in 1.14.X)

+ 12 - 5
CREDITS.md

@@ -7,6 +7,7 @@ People are sorted by name so please keep this order.
 ---
 
 * [Adrien Dorsaz](https://github.com/Trim): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Trim), [Web](https://adorsaz.ch/)
+* [Alexander Steinhöfer](https://github.com/lx-s): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:lx-s), [Web](https://lx-s.de/)
 * [Alexandre Alapetite](https://github.com/Alkarex): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alkarex), [Web](https://alexandre.alapetite.fr/)
 * [Alexis Degrugillier](https://github.com/aledeg): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=aledeg)
 * [Alwaysin](https://github.com/Alwaysin): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Alwaysin)
@@ -20,14 +21,15 @@ People are sorted by name so please keep this order.
 * [Damstre](https://github.com/Damstre): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Damstre)
 * [danc](https://github.com/danc): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=danc), [Web](http://tintouli.free.fr/)
 * [David Souza](https://github.com/araujo0205): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:araujo0205), [Web](http://davidsouza.tech/)
-* [dswd](https://github.com/dswd): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:dswd)
 * [Django Janny](https://github.com/keltroth): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:keltroth)
+* [dswd](https://github.com/dswd): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:dswd)
 * [ealdraed](https://github.com/ealdraed): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ealdraed)
+* [Fake4d](https://github.com/Fake4d): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Fake4d)
 * [Frans de Jonge](https://github.com/Frenzie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Frenzie), [Web](http://fransdejonge.com/)
+* [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
 * [gsongsong](https://github.com/gsongsong): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:gsongsong)
 * [Guillaume Fillon](https://github.com/kokaz): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:kokaz), [Web](http://www.guillaume-fillon.com/)
 * [Guillaume Hayot](https://github.com/postblue): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:postblue), [Web](https://postblue.info/)
-* [Gregor Nathanael Meyer](https://github.com/spackmat): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:spackmat), [Web](https://der-meyer.de)
 * [hckweb](https://github.com/hckweb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=hckweb)
 * [hoilc](https://github.com/hoilc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:hoilc)
 * [Jan van den Berg](https://github.com/jan-vandenberg): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:jan-vandenberg), [Web](https://j11g.com/)
@@ -36,6 +38,7 @@ People are sorted by name so please keep this order.
 * [Jonas Östanbäck](https://github.com/cez81): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=cez81)
 * [Julien Reichardt](https://github.com/j8r): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=j8r), [Web](https://blog.jrei.ch/)
 * [Kevin Papst](https://github.com/kevinpapst): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=kevinpapst), [Web](http://www.kevinpapst.de/)
+* [Leepic](https://github.com/Leepic): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Leepic)
 * [Luc Didry](https://github.com/ldidry): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ldidry), [Web](https://www.fiat-tux.fr/)
 * [Luc Sanchez](https://github.com/ColonelMoutarde): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=ColonelMoutarde), (https://www.luc-sanchez.fr/)
 * [marcomrc](https://github.com/marcomrc): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marcomrc)
@@ -43,29 +46,33 @@ People are sorted by name so please keep this order.
 * [Marien Fressinaud](https://github.com/marienfressinaud): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=marienfressinaud), [Web](https://marienfressinaud.fr/)
 * [Melvyn Laïly](https://github.com/yaurthek): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=yaurthek), [Web](http://x2a.yt/)
 * [MSZ](https://github.com/mszkb): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=mszkb)
+* [Nick Cross](https://github.com/rnc): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rnc)
 * [Nico B](https://github.com/youknow0): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:youknow0)
+* [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Nicolas Elie](https://github.com/nicolaselie): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicolaselie)
 * [Nicolas Frandeboeuf](https://github.com/nicofrand): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=nicofrand), [Web](https://nicofrand.ey)
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
-* [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
 * [Patrick Crandol](https://github.com/pattems): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:pattems)
+* [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
-* [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [primaeval](https://github.com/primaeval): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:primaeval)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
 * [Quentin Dufour](https://github.com/superboum): [contributions](https://github.com/FreshRSS/documentation/commits?author=superboum), [Web](http://quentin.dufour.io/)
 * [Quentin Pagès](https://github.com/Quenty31): [contributions](https://github.com/FreshRSS/documentation/commits?author=Quenty31)
 * [Ramón Cutanda](https://github.com/rcutanda): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rcutanda)
 * [romibi](https://github.com/romibi): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=romibi)
+* [Rosemary Le Faive](https://github.com/rosiel): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:rosiel)
+* [Sandro Jäckel](https://github.com/SuperSandro2000): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:SuperSandro2000), [Web](https://supersandro.de/)
 * [sirideain](https://github.com/sirideain): [contributions](https://github.com/FreshRSS/FreshRSS/commits/dev?author=sirideain)
+* [Sp3r4z](https://github.com/Sp3r4z): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Sp3r4z)
 * [subic](https://github.com/subic): [contributions](https://github.com/FreshRSS/documentation/commits?author=subic)
 * [Tets42](https://github.com/Tets42): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Tets42)
 * [Thomas Citharel](https://github.com/tcitworld): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:tomgue), [Web](https://www.tcit.fr/)
+* [Thomas Guesnon](https://github.com/patjennings): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:patjennings), [Web](http://www.thomasguesnon.fr/)
 * [thomas-gt](https://github.com/thomas-gt): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:thomas-gt)
 * [tomgue](https://github.com/tomgue): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=tomgue)
-* [Thomas Guesnon](https://github.com/patjennings): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:patjennings), [Web](http://www.thomasguesnon.fr/)
 * [Twilek-de](https://github.com/Twilek-de): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:Twilek-de)
 * [Uncovery](https://github.com/uncovery): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:uncovery)
 * [Wanabo](https://github.com/Wanabo): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=Wanabo)

+ 27 - 6
Docker/Dockerfile

@@ -1,22 +1,39 @@
-FROM ubuntu:18.10
+FROM ubuntu:19.04
 
 ENV TZ UTC
+SHELL ["/bin/bash", "-o", "pipefail", "-c"]
 RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
 
-RUN apt update && \
-	apt install --no-install-recommends -y \
+RUN apt-get update && \
+	apt-get install --no-install-recommends -y \
 	ca-certificates cron \
 	apache2 libapache2-mod-php \
-	php-curl php-intl php-mbstring php-xml php-zip \
+	php-curl php-gmp php-intl php-mbstring php-xml php-zip \
 	php-sqlite3 php-mysql php-pgsql && \
-	rm -rf /var/lib/apt/lists/
+	rm -rf /var/lib/apt/lists/*
 
-RUN mkdir -p /var/www/FreshRSS /run/apache2/
+RUN mkdir -p /var/www/FreshRSS/ /run/apache2/
 WORKDIR /var/www/FreshRSS
 
 COPY . /var/www/FreshRSS
 COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
 
+ARG FRESHRSS_VERSION
+ARG SOURCE_BRANCH
+ARG SOURCE_COMMIT
+
+LABEL \
+	org.opencontainers.image.authors="Alkarex" \
+	org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
+	org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
+	org.opencontainers.image.licenses="AGPL-3.0" \
+	org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
+	org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
+	org.opencontainers.image.title="FreshRSS" \
+	org.opencontainers.image.url="https://freshrss.org/" \
+	org.opencontainers.image.vendor="FreshRSS" \
+	org.opencontainers.image.version="$FRESHRSS_VERSION"
+
 RUN a2dismod -f alias autoindex negotiation status && \
 	a2enmod deflate expires headers mime setenvif && \
 	a2disconf '*' && \
@@ -35,6 +52,10 @@ ENV CRON_MIN ''
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 
 EXPOSE 80
+# hadolint ignore=DL3025
 CMD ([ -z "$CRON_MIN" ] || cron) && \
 	. /etc/apache2/envvars && \
 	exec apache2 -D FOREGROUND
+
+HEALTHCHECK --start-period=20s --interval=37s --timeout=5s --retries=3 \
+	CMD (php -r "readfile('http://localhost/i/');" | grep -q 'jsonVars') || exit 1

+ 22 - 2
Docker/Dockerfile-Alpine

@@ -1,7 +1,7 @@
-FROM alpine:3.9
+FROM alpine:3.10
 
 ENV TZ UTC
-
+SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
 RUN apk add --no-cache \
 	apache2 php7-apache2 \
 	php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
@@ -14,6 +14,22 @@ WORKDIR /var/www/FreshRSS
 COPY . /var/www/FreshRSS
 COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
 
+ARG FRESHRSS_VERSION
+ARG SOURCE_BRANCH
+ARG SOURCE_COMMIT
+
+LABEL \
+	org.opencontainers.image.authors="Alkarex" \
+	org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
+	org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
+	org.opencontainers.image.licenses="AGPL-3.0" \
+	org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
+	org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
+	org.opencontainers.image.title="FreshRSS" \
+	org.opencontainers.image.url="https://freshrss.org/" \
+	org.opencontainers.image.vendor="FreshRSS" \
+	org.opencontainers.image.version="$FRESHRSS_VERSION"
+
 RUN 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/^/#/" \
@@ -32,5 +48,9 @@ ENV CRON_MIN ''
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 
 EXPOSE 80
+# hadolint ignore=DL3025
 CMD ([ -z "$CRON_MIN" ] || crond -d 6) && \
 	exec httpd -D FOREGROUND
+
+HEALTHCHECK --start-period=20s --interval=37s --timeout=5s --retries=3 \
+	CMD (php -r "readfile('http://localhost/i/');" | grep -q 'jsonVars') || exit 1

+ 73 - 0
Docker/Dockerfile-QEMU-ARM

@@ -0,0 +1,73 @@
+# Only relevant for Docker Hub or QEMU multi-architecture builds.
+# Prefer the normal `Dockerfile` if you are building manually on the targeted architecture.
+
+FROM arm32v7/ubuntu:19.04
+
+# Requires ./hooks/*
+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
+
+RUN apt-get update && \
+	apt-get install --no-install-recommends -y \
+	ca-certificates cron \
+	apache2 libapache2-mod-php \
+	php-curl php-gmp php-intl php-mbstring php-xml php-zip \
+	php-sqlite3 php-mysql php-pgsql && \
+	rm -rf /var/lib/apt/lists/*
+
+RUN mkdir -p /var/www/FreshRSS/ /run/apache2/
+WORKDIR /var/www/FreshRSS
+
+COPY . /var/www/FreshRSS
+COPY ./Docker/*.Apache.conf /etc/apache2/sites-available/
+
+ARG FRESHRSS_VERSION
+ARG SOURCE_BRANCH
+ARG SOURCE_COMMIT
+
+LABEL \
+	org.opencontainers.image.authors="Alkarex" \
+	org.opencontainers.image.description="A self-hosted RSS feed aggregator" \
+	org.opencontainers.image.documentation="https://freshrss.github.io/FreshRSS/" \
+	org.opencontainers.image.licenses="AGPL-3.0" \
+	org.opencontainers.image.revision="${SOURCE_BRANCH}.${SOURCE_COMMIT}" \
+	org.opencontainers.image.source="https://github.com/FreshRSS/FreshRSS" \
+	org.opencontainers.image.title="FreshRSS" \
+	org.opencontainers.image.url="https://freshrss.org/" \
+	org.opencontainers.image.vendor="FreshRSS" \
+	org.opencontainers.image.version="$FRESHRSS_VERSION"
+
+RUN a2dismod -f alias autoindex negotiation status && \
+	a2enmod deflate expires headers mime setenvif && \
+	a2disconf '*' && \
+	a2dissite '*' && \
+	a2ensite 'FreshRSS*'
+
+RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
+	sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
+	touch /var/www/FreshRSS/Docker/env.txt && \
+	echo "17,47 * * * * . /var/www/FreshRSS/Docker/env.txt; \
+		su www-data -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
+		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
+
+# Seems needed for arm32v7/ubuntu on Docker Hub
+RUN update-ca-certificates -f
+
+# Useful with the `--squash` build option
+RUN rm /usr/bin/qemu-* /var/www/FreshRSS/Docker/qemu-*
+
+ENV COPY_SYSLOG_TO_STDERR On
+ENV CRON_MIN ''
+ENTRYPOINT ["./Docker/entrypoint.sh"]
+
+EXPOSE 80
+# hadolint ignore=DL3025
+CMD ([ -z "$CRON_MIN" ] || cron) && \
+	. /etc/apache2/envvars && \
+	exec apache2 -D FOREGROUND
+
+HEALTHCHECK --start-period=20s --interval=37s --timeout=5s --retries=3 \
+	CMD (php -r "readfile('http://localhost/i/');" | grep -q 'jsonVars') || exit 1

+ 10 - 2
Docker/README.md

@@ -1,3 +1,8 @@
+![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/freshrss/freshrss.svg)
+![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/freshrss/freshrss.svg)
+![MicroBadger Size](https://img.shields.io/microbadger/image-size/freshrss/freshrss.svg)
+![Docker Pulls](https://img.shields.io/docker/pulls/freshrss/freshrss.svg)
+
 # Deploy FreshRSS with Docker
 * See also https://hub.docker.com/r/freshrss/freshrss/
 
@@ -32,6 +37,7 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   -p 80:80 \
   -p 443:443 \
   --name traefik traefik --docker \
+  --loglevel=info \
   --entryPoints='Name:http Address::80 Compress:true Redirect.EntryPoint:https' \
   --entryPoints='Name:https Address::443 Compress:true TLS TLS.MinVersion:VersionTLS12 TLS.SniStrict:true TLS.CipherSuites:TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA' \
   --defaultentrypoints=http,https --keeptrailingslash=true \
@@ -46,7 +52,7 @@ See [more information about Docker and Let’s Encrypt in Træfik](https://docs.
 Example using the built-in refresh cron job (see further below for alternatives).
 You must first chose a domain (DNS) or sub-domain, e.g. `freshrss.example.net`.
 
-> **N.B.:** For platforms other than x64 (Intel, AMD), such as ARM (e.g. Raspberry Pi), see the section *Build Docker image* further below.
+> **N.B.:** Default images are for x64 (Intel, AMD) platforms. For ARM (e.g. Raspberry Pi), use the `*-arm` tags. For other platforms, see the section *Build Docker image* further below.
 
 ```sh
 sudo docker volume create freshrss-data
@@ -69,6 +75,7 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 	`--label traefik.frontend.rule='Host:freshrss.example.net;PathPrefixStrip:/FreshRSS/' \`
 * You may remove the `--label traefik.*` lines if you do not use Træfik.
 * Add `-p 8080:80 \` if you want to expose FreshRSS locally, e.g. on port `8080`.
+* Replace `freshrss/freshrss` by a more specific tag (see below) such as `freshrss/freshrss:dev` for the development version, or `freshrss/freshrss:arm` for a Raspberry Pi version.
 
 This already works with a built-in **SQLite** database (easiest), but more powerful databases are supported:
 
@@ -130,9 +137,10 @@ The tags correspond to FreshRSS branches and versions:
 * `:latest` (default) is the `master` branch, more stable
 * `:dev` is the `dev` branch, rolling release
 * `:x.y.z` are specific FreshRSS releases
+* `:arm` or `:*-arm` are the ARM versions (e.g. for Raspberry Pi)
 
 ### Linux: Ubuntu vs. Alpine
-Our default image is based on [Ubuntu](https://www.ubuntu.com/server). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `-alpine` tag suffix).
+Our default image is based on [Ubuntu](https://www.ubuntu.com/server). We offer an alternative based on [Alpine](https://alpinelinux.org/) (with the `*-alpine` tag suffix).
 In [our tests](https://github.com/FreshRSS/FreshRSS/pull/2205), Ubuntu is ~3 times faster,
 while Alpine is ~2.5 times [smaller on disk](https://hub.docker.com/r/freshrss/freshrss/tags) (and much faster to build).
 

+ 7 - 4
Docker/entrypoint.sh

@@ -1,15 +1,18 @@
 #!/bin/sh
 
-php -f ./cli/prepare.php > /dev/null
+php -f ./cli/prepare.php >/dev/null
 
 chown -R :www-data .
 chmod -R g+r . && chmod -R g+w ./data/
 
-find /etc/php*/ -name php.ini -exec sed -r -i "\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
+find /etc/php*/ -name php.ini -exec sed -r -i "\\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
 
 if [ -n "$CRON_MIN" ]; then
-	(echo "export TZ=$TZ" ; echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR") > /var/www/FreshRSS/Docker/env.txt
-	crontab -l | sed -r "\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" | crontab -
+	(
+		echo "export TZ=$TZ"
+		echo "export COPY_SYSLOG_TO_STDERR=$COPY_SYSLOG_TO_STDERR"
+	) >/var/www/FreshRSS/Docker/env.txt
+	crontab -l | sed -r "\\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" | crontab -
 fi
 
 exec "$@"

+ 21 - 0
Docker/hooks/build

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

+ 4 - 0
Docker/hooks/post_checkout

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

+ 11 - 0
Docker/hooks/pre_build

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

+ 10 - 4
app/Controllers/feedController.php

@@ -243,7 +243,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		}
 	}
 
-	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false, $noCommit = false) {
+	public static function actualizeFeed($feed_id, $feed_url, $force, $simplePiePush = null, $isNewFeed = false, $noCommit = false, $maxFeeds = 10) {
 		@set_time_limit(300);
 
 		$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -262,6 +262,11 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$feeds = $feedDAO->listFeedsOrderUpdate(-1);
 		}
 
+		// Set maxFeeds to a minimum of 10
+		if (!is_int($maxFeeds) || $maxFeeds < 10) {
+			$maxFeeds = 10;
+		}
+
 		// Calculate date of oldest entries we accept in DB.
 		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
@@ -459,9 +464,9 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$updated_feeds++;
 			unset($feed);
 
-			// No more than 10 feeds unless $force is true to avoid overloading
+			// No more than $maxFeeds feeds unless $force is true to avoid overloading
 			// the server.
-			if ($updated_feeds >= 10 && !$force) {
+			if ($updated_feeds >= $maxFeeds && !$force) {
 				break;
 			}
 		}
@@ -497,6 +502,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 		$id = Minz_Request::param('id');
 		$url = Minz_Request::param('url');
 		$force = Minz_Request::param('force');
+		$maxFeeds = (int)Minz_Request::param('maxFeeds');
 		$noCommit = Minz_Request::fetchPOST('noCommit', 0) == 1;
 
 		if ($id == -1 && !$noCommit) {	//Special request only to commit & refresh DB cache
@@ -511,7 +517,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 			$databaseDAO = FreshRSS_Factory::createDatabaseDAO();
 			$databaseDAO->minorDbMaintenance();
 		} else {
-			list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit);
+			list($updated_feeds, $feed, $nb_new_articles) = self::actualizeFeed($id, $url, $force, null, false, $noCommit, $maxFeeds);
 		}
 
 		if (Minz_Request::param('ajax')) {

+ 1 - 1
app/Controllers/indexController.php

@@ -54,7 +54,7 @@ class FreshRSS_index_Controller extends Minz_ActionController {
 			}
 		};
 
-		$this->view->callbackBeforeEntries = function ($view) {
+		$this->view->callbackBeforePagination = function ($view) {
 			try {
 				FreshRSS_Context::$number++;	//+1 for pagination
 				$entries = FreshRSS_index_Controller::listEntriesByContext();

+ 43 - 3
app/Controllers/subscriptionController.php

@@ -35,9 +35,20 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		$this->view->onlyFeedsWithError = Minz_Request::paramTernary('error');
 
 		$id = Minz_Request::param('id');
-		if ($id !== false) {
-			$feedDAO = FreshRSS_Factory::createFeedDao();
-			$this->view->feed = $feedDAO->searchById($id);
+		$this->view->displaySlider = false;
+		if (false !== $id) {
+			$type = Minz_Request::param('type');
+			$this->view->displaySlider = true;
+			switch ($type) {
+				case 'category':
+					$categoryDAO = FreshRSS_Factory::createCategoryDao();
+					$this->view->category = $categoryDAO->searchById($id);
+					break;
+				default:
+					$feedDAO = FreshRSS_Factory::createFeedDao();
+					$this->view->feed = $feedDAO->searchById($id);
+					break;
+			}
 		}
 	}
 
@@ -140,6 +151,35 @@ class FreshRSS_subscription_Controller extends Minz_ActionController {
 		}
 	}
 
+	public function categoryAction() {
+		$this->view->_useLayout(false);
+
+		$categoryDAO = FreshRSS_Factory::createCategoryDao();
+
+		$id = Minz_Request::param('id');
+		$category = $categoryDAO->searchById($id);
+		if ($id === false || null === $category) {
+			Minz_Error::error(404);
+			return;
+		}
+		$this->view->category = $category;
+
+		if (Minz_Request::isPost()) {
+			$values = array(
+				'name' => Minz_Request::param('name', ''),
+			);
+
+			invalidateHttpCache();
+
+			$url_redirect = array('c' => 'subscription', 'params' => array('id' => $id, 'type' => 'category'));
+			if (false !== $categoryDAO->updateCategory($id, $values)) {
+				Minz_Request::good(_t('feedback.sub.category.updated'), $url_redirect);
+			} else {
+				Minz_Request::bad(_t('feedback.sub.category.error'), $url_redirect);
+			}
+		}
+	}
+
 	/**
 	 * This action displays the bookmarklet page.
 	 */

+ 1 - 1
app/Controllers/userController.php

@@ -38,7 +38,7 @@ class FreshRSS_user_Controller extends Minz_ActionController {
 	 * The username is also used as folder name, file name, and part of SQL table name.
 	 * '_' is a reserved internal username.
 	 */
-	const USERNAME_PATTERN = '[0-9a-zA-Z_][0-9a-zA-Z_.]{1,38}|[0-9a-zA-Z]';
+	const USERNAME_PATTERN = '([0-9a-zA-Z_][0-9a-zA-Z_.@-]{1,38}|[0-9a-zA-Z])';
 
 	public static function checkUsername($username) {
 		return preg_match('/^' . self::USERNAME_PATTERN . '$/', $username) === 1;

+ 6 - 3
app/FreshRSS.php

@@ -68,9 +68,12 @@ class FreshRSS extends Minz_FrontController {
 						' [HTTP_REFERER=' . htmlspecialchars($http_referer, ENT_NOQUOTES, 'UTF-8') . ']'
 					)));
 			}
-			if ((!FreshRSS_Auth::isCsrfOk()) &&
-				(Minz_Request::controllerName() !== 'auth' || Minz_Request::actionName() !== 'login')) {
-				// Token-based protection against XSRF attacks, except for the login form itself
+			if (!(FreshRSS_Auth::isCsrfOk() ||
+				(Minz_Request::controllerName() === 'auth' && Minz_Request::actionName() === 'login') ||
+				(Minz_Request::controllerName() === 'user' && Minz_Request::actionName() === 'create' &&
+					!FreshRSS_Auth::hasAccess('admin'))
+				)) {
+				// Token-based protection against XSRF attacks, except for the login or self-create user forms
 				Minz_Translate::init('en');	//TODO: Better choice of fallback language
 				Minz_Error::error(403, array('error' => array(
 						_t('feedback.access.denied'),

+ 7 - 0
app/Models/Category.php

@@ -7,6 +7,7 @@ class FreshRSS_Category extends Minz_Model {
 	private $nbNotRead = -1;
 	private $feeds = null;
 	private $hasFeedsWithError = false;
+	private $isDefault = false;
 
 	public function __construct($name = '', $feeds = null) {
 		$this->_name($name);
@@ -28,6 +29,9 @@ class FreshRSS_Category extends Minz_Model {
 	public function name() {
 		return $this->name;
 	}
+	public function isDefault() {
+		return $this->isDefault;
+	}
 	public function nbFeed() {
 		if ($this->nbFeed < 0) {
 			$catDAO = FreshRSS_Factory::createCategoryDao();
@@ -70,6 +74,9 @@ class FreshRSS_Category extends Minz_Model {
 	public function _name($value) {
 		$this->name = trim($value);
 	}
+	public function _isDefault($value) {
+		$this->isDefault = $value;
+	}
 	public function _feeds($values) {
 		if (!is_array($values)) {
 			$values = array($values);

+ 1 - 0
app/Models/CategoryDAO.php

@@ -282,6 +282,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 				$dao['name']
 			);
 			$cat->_id($dao['id']);
+			$cat->_isDefault(static::DEFAULTCATEGORYID === intval($dao['id']));
 			$list[$key] = $cat;
 		}
 

+ 3 - 1
app/i18n/cz/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Kategorie',
 		'add' => 'Přidat kategorii',
 		'empty' => 'Vyprázdit kategorii',
+		'information' => 'Informace',
 		'new' => 'Nová kategorie',
+		'title' => 'Název',
 	),
 	'feed' => array(
 		'add' => 'Přidat RSS kanál',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Informace',
+		'information' => 'Informace',
 		'keep_history' => 'Zachovat tento minimální počet článků',
 		'moved_category_deleted' => 'Po smazání kategorie budou v ní obsažené kanály automaticky přesunuty do <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/de/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Kategorie',
 		'add' => 'Eine Kategorie hinzufügen',
 		'empty' => 'Leere Kategorie',
+		'information' => 'Information',
 		'new' => 'Neue Kategorie',
+		'title' => 'Titel',
 	),
 	'feed' => array(
 		'add' => 'Einen RSS-Feed hinzufügen',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Information',
+		'information' => 'Information',
 		'keep_history' => 'Minimale Anzahl an Artikeln, die behalten wird',
 		'moved_category_deleted' => 'Wenn Sie eine Kategorie entfernen, werden deren Feeds automatisch in die Kategorie <em>%s</em> eingefügt.',
 		'mute' => 'Stumm schalten',

+ 3 - 1
app/i18n/en/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Category',
 		'add' => 'Add a category',
 		'empty' => 'Empty category',
+		'information' => 'Information',
 		'new' => 'New category',
+		'title' => 'Title',
 	),
 	'feed' => array(
 		'add' => 'Add a RSS feed',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',
 			'help' => 'Write one search filter per line.',
 		),
-		'informations' => 'Information',
+		'information' => 'Information',
 		'keep_history' => 'Minimum number of articles to keep',
 		'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',
 		'mute' => 'mute',

+ 3 - 1
app/i18n/es/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Categoría',
 		'add' => 'Añadir a la categoría',
 		'empty' => 'Vaciar categoría',
+		'information' => 'Información',
 		'new' => 'Nueva categoría',
+		'title' => 'Título',
 	),
 	'feed' => array(
 		'add' => 'Añadir fuente RSS',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Información',
+		'information' => 'Información',
 		'keep_history' => 'Número mínimo de artículos a conservar',
 		'moved_category_deleted' => 'Al borrar una categoría todas sus fuentes pasan automáticamente a la categoría <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/fr/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Catégorie',
 		'add' => 'Ajouter une catégorie',
 		'empty' => 'Catégorie vide',
+		'information' => 'Informations',
 		'new' => 'Nouvelle catégorie',
+		'title' => 'Titre',
 	),
 	'feed' => array(
 		'add' => 'Ajouter un flux RSS',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filtres d’action',
 			'help' => 'Écrivez une recherche par ligne.',
 		),
-		'informations' => 'Informations',
+		'information' => 'Informations',
 		'keep_history' => 'Nombre minimum d’articles à conserver',
 		'moved_category_deleted' => 'Lors de la suppression d’une catégorie, ses flux seront automatiquement classés dans <em>%s</em>.',
 		'mute' => 'muet',

+ 3 - 1
app/i18n/he/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'קטגוריה',
 		'add' => 'הוספת קטגוריה',
 		'empty' => 'Empty category',	//TODO - Translation
+		'information' => 'מידע',
 		'new' => 'קטגוריה חדשה',
+		'title' => 'כותרת',
 	),
 	'feed' => array(
 		'add' => 'הוספת הזנה',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'מידע',
+		'information' => 'מידע',
 		'keep_history' => 'מסםר מינימלי של מאמרים לשמור',
 		'moved_category_deleted' => 'כאשר הקטגוריה נמחקת ההזנות שבתוכה אוטומטית מקוטלגות תחת  <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/it/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Categoria',
 		'add' => 'Aggiungi una categoria',
 		'empty' => 'Categoria vuota',
+		'information' => 'Informazioni',
 		'new' => 'Nuova categoria',
+		'title' => 'Titolo',
 	),
 	'feed' => array(
 		'add' => 'Aggiungi un Feed RSS',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Informazioni',
+		'information' => 'Informazioni',
 		'keep_history' => 'Numero minimo di articoli da mantenere',
 		'moved_category_deleted' => 'Cancellando una categoria i feed al suo interno verranno classificati automaticamente come <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/kr/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => '카테고리',
 		'add' => '카테고리 추가',
 		'empty' => '빈 카테고리',
+		'information' => '정보',
 		'new' => '새 카테고리',
+		'title' => '제목',
 	),
 	'feed' => array(
 		'add' => 'RSS 피드 추가',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => '정보',
+		'information' => '정보',
 		'keep_history' => '최소 유지 글 개수',
 		'moved_category_deleted' => '카테고리를 삭제하면, 해당 카테고리 아래에 있던 피드들은 자동적으로 <em>%s</em> 아래로 분류됩니다.',
 		'mute' => '무기한 새로고침 금지',

+ 3 - 1
app/i18n/nl/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Categorie',
 		'add' => 'Voeg categorie toe',
 		'empty' => 'Lege categorie',
+		'information' => 'Informatie',
 		'new' => 'Nieuwe categorie',
+		'title' => 'Titel',
 	),
 	'feed' => array(
 		'add' => 'Voeg een RSS feed toe',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Informatie',
+		'information' => 'Informatie',
 		'keep_history' => 'Minimum aantal artikelen om te houden',
 		'moved_category_deleted' => 'Als u een categorie verwijderd, worden de feeds automatisch geclassificeerd onder <em>%s</em>.',
 		'mute' => 'demp',

+ 3 - 1
app/i18n/oc/sub.php

@@ -13,7 +13,9 @@ return array(
 		'_' => 'Categoria',
 		'add' => 'Ajustar una categoria',
 		'empty' => 'Categoria voida',
+		'information' => 'Informacions',
 		'new' => 'Nòva categoria',
+		'title' => 'Títol',
 	),
 	'feed' => array(
 		'add' => 'Ajustar un flux RSS',
@@ -36,7 +38,7 @@ return array(
 			'_' => 'Filtre d’accion',
 			'help' => 'Escrivètz una recèrca per linha.',
 		),
-		'informations' => 'Informacions',
+		'information' => 'Informacions',
 		'keep_history' => 'Nombre minimum d’articles de servar',
 		'moved_category_deleted' => 'Quand escafatz una categoria, sos fluxes son automaticament classats dins <em>%s</em>.',
 		'mute' => 'mut',

+ 3 - 1
app/i18n/pt-br/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Categoria',
 		'add' => 'Adicionar uma categoria',
 		'empty' => 'Categoria vazia',
+		'information' => 'Informações',
 		'new' => 'Nova categoria',
+		'title' => 'Título',
 	),
 	'feed' => array(
 		'add' => 'Adicionar um RSS feed',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Informações',
+		'information' => 'Informações',
 		'keep_history' => 'Número mínimo de artigos para manter',
 		'moved_category_deleted' => 'Quando você deleta uma categoria, seus feeds são automaticamente classificados como <em>%s</em>.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/ru/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Category',	//TODO - Translation
 		'add' => 'Add a category',	//TODO - Translation
 		'empty' => 'Empty category',	//TODO - Translation
+		'information' => 'Information',	//TODO - Translation
 		'new' => 'New category',	//TODO - Translation
+		'title' => 'Title',	//TODO - Translation
 	),
 	'feed' => array(
 		'add' => 'Add a RSS feed',	//TODO - Translation
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Information',	//TODO - Translation
+		'information' => 'Information',	//TODO - Translation
 		'keep_history' => 'Minimum number of articles to keep',	//TODO - Translation
 		'moved_category_deleted' => 'When you delete a category, its feeds are automatically classified under <em>%s</em>.',	//TODO - Translation
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/tr/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => 'Kategori',
 		'add' => 'Kategori ekle',
 		'empty' => 'Boş kategori',
+		'information' => 'Bilgi',
 		'new' => 'Yeni kategori',
+		'title' => 'Başlık',
 	),
 	'feed' => array(
 		'add' => 'RSS akışı ekle',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => 'Bilgi',
+		'information' => 'Bilgi',
 		'keep_history' => 'En az tutulacak makale sayısı',
 		'moved_category_deleted' => 'Bir kategoriyi silerseniz, içerisindeki akışlar <em>%s</em> içerisine yerleşir.',
 		'mute' => 'mute',	//TODO - Translation

+ 3 - 1
app/i18n/zh-cn/sub.php

@@ -14,7 +14,9 @@ return array(
 		'_' => '分类',
 		'add' => '添加分类',
 		'empty' => '空分类',
+		'information' => '信息',
 		'new' => '新分类',
+		'title' => '标题',
 	),
 	'feed' => array(
 		'add' => '添加 RSS 源',
@@ -37,7 +39,7 @@ return array(
 			'_' => 'Filter actions',	//TODO - Translation
 			'help' => 'Write one search filter per line.',	//TODO - Translation
 		),
-		'informations' => '信息',
+		'information' => '信息',
 		'keep_history' => '至少保存的文章数',
 		'moved_category_deleted' => '删除分类时,其中的 RSS 源会自动归类到 <em>%s</em>',
 		'mute' => '暂停',

+ 10 - 7
app/install.php

@@ -125,7 +125,9 @@ function saveStep2() {
 		$_SESSION['title'] = $system_default_config->title;
 		$_SESSION['old_entries'] = param('old_entries', $user_default_config->old_entries);
 		$_SESSION['auth_type'] = param('auth_type', 'form');
-		$_SESSION['default_user'] = substr(preg_replace('/[^0-9a-zA-Z_]/', '', param('default_user', '')), 0, 38);
+		if (FreshRSS_user_Controller::checkUsername(param('default_user', ''))) {
+			$_SESSION['default_user'] = param('default_user', '');
+		}
 
 		$password_plain = param('passwordPlain', false);
 		if ($password_plain !== false && cryptAvailable()) {
@@ -605,18 +607,18 @@ function printStep3() {
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
 			<div class="group-controls">
 				<select name="type" id="type" tabindex="1">
-				<?php if (extension_loaded('pdo_mysql')) {?>
-				<option value="mysql"
-					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
-					MySQL
-				</option>
-				<?php }?>
 				<?php if (extension_loaded('pdo_sqlite')) {?>
 				<option value="sqlite"
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'sqlite') ? 'selected="selected"' : ''; ?>>
 					SQLite
 				</option>
 				<?php }?>
+				<?php if (extension_loaded('pdo_mysql')) {?>
+				<option value="mysql"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'mysql') ? 'selected="selected"' : ''; ?>>
+					MySQL
+				</option>
+				<?php }?>
 				<?php if (extension_loaded('pdo_pgsql')) {?>
 				<option value="pgsql"
 					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
@@ -722,6 +724,7 @@ case 5:
 	<head>
 		<meta charset="UTF-8" />
 		<meta name="viewport" content="initial-scale=1.0" />
+		<script id="jsonVars" type="application/json">{}</script>
 		<title><?php echo _t('install.title'); ?></title>
 		<link rel="stylesheet" href="../themes/base-theme/template.css?<?php echo @filemtime(PUBLIC_PATH . '/themes/base-theme/template.css'); ?>" />
 		<link rel="stylesheet" href="../themes/Origine/origine.css?<?php echo @filemtime(PUBLIC_PATH . '/themes/Origine/origine.css'); ?>" />

+ 8 - 1
app/layout/nav_menu.phtml

@@ -1,4 +1,11 @@
-<?php $actual_view = Minz_Request::actionName(); ?>
+<?php
+	$actual_view = Minz_Request::actionName();
+
+	flush();
+	if (isset($this->callbackBeforePagination)) {
+		call_user_func($this->callbackBeforePagination, $this);
+	}
+?>
 
 <div class="nav_menu">
 	<?php if ($actual_view === 'normal' || $actual_view === 'reader' ) { ?>

+ 10 - 10
app/views/configure/shortcut.phtml

@@ -65,33 +65,33 @@
 			</div>
 		</div>
 
-		<p class="alert alert-warn"><?php echo _t('conf.shortcut.navigation_no_mod_help');?></p>
-
 		<div class="form-group">
-			<label class="group-name" for="skip_next_entry"><?php echo _t('conf.shortcut.skip_next_article'); ?></label>
+			<label class="group-name" for="first_entry"><?php echo _t('conf.shortcut.first_article'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="skip_next_entry" name="shortcuts[skip_next_entry]" list="keys" value="<?php echo $s['skip_next_entry']; ?>" data-leave-validation="<?php echo $s['skip_next_entry']; ?>"/>
+				<input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" data-leave-validation="<?php echo $s['first_entry']; ?>"/>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="skip_prev_entry"><?php echo _t('conf.shortcut.skip_previous_article'); ?></label>
+			<label class="group-name" for="last_entry"><?php echo _t('conf.shortcut.last_article'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="skip_prev_entry" name="shortcuts[skip_prev_entry]" list="keys" value="<?php echo $s['skip_prev_entry']; ?>" data-leave-validation="<?php echo $s['skip_prev_entry']; ?>"/>
+				<input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" data-leave-validation="<?php echo $s['last_entry']; ?>"/>
 			</div>
 		</div>
 
+		<p class="alert alert-warn"><?php echo _t('conf.shortcut.navigation_no_mod_help');?></p>
+
 		<div class="form-group">
-			<label class="group-name" for="first_entry"><?php echo _t('conf.shortcut.first_article'); ?></label>
+			<label class="group-name" for="skip_next_entry"><?php echo _t('conf.shortcut.skip_next_article'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="first_entry" name="shortcuts[first_entry]" list="keys" value="<?php echo $s['first_entry']; ?>" data-leave-validation="<?php echo $s['first_entry']; ?>"/>
+				<input type="text" id="skip_next_entry" name="shortcuts[skip_next_entry]" list="keys" value="<?php echo $s['skip_next_entry']; ?>" data-leave-validation="<?php echo $s['skip_next_entry']; ?>"/>
 			</div>
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="last_entry"><?php echo _t('conf.shortcut.last_article'); ?></label>
+			<label class="group-name" for="skip_prev_entry"><?php echo _t('conf.shortcut.skip_previous_article'); ?></label>
 			<div class="group-controls">
-				<input type="text" id="last_entry" name="shortcuts[last_entry]" list="keys" value="<?php echo $s['last_entry']; ?>" data-leave-validation="<?php echo $s['last_entry']; ?>"/>
+				<input type="text" id="skip_prev_entry" name="shortcuts[skip_prev_entry]" list="keys" value="<?php echo $s['skip_prev_entry']; ?>" data-leave-validation="<?php echo $s['skip_prev_entry']; ?>"/>
 			</div>
 		</div>
 

+ 1 - 1
app/views/feed/add.phtml

@@ -8,7 +8,7 @@
 
 	<form method="post" action="<?php echo _url('feed', 'add'); ?>" autocomplete="off">
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
-		<legend><?php echo _t('sub.feed.informations'); ?></legend>
+		<legend><?php echo _t('sub.feed.information'); ?></legend>
 		<?php if ($this->load_ok) { ?>
 		<div class="form-group">
 			<label class="group-name"><?php echo _t('sub.feed.title'); ?></label>

+ 34 - 0
app/views/helpers/category/update.phtml

@@ -0,0 +1,34 @@
+<div class="post">
+	<h1><?php echo $this->category->name(); ?></h1>
+
+	<div>
+		<a href="<?php echo _url('index', 'index', 'get', 'c_' . $this->category->id()); ?>"><?php echo _i('link'); ?> <?php echo _t('gen.action.filter'); ?></a>
+	</div>
+
+	<form method="post" action="<?php echo _url('subscription', 'category', 'id', $this->category->id()); ?>" autocomplete="off">
+		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
+		<legend><?php echo _t('sub.category.information'); ?></legend>
+		<div class="form-group">
+			<label class="group-name" for="name"><?php echo _t('sub.category.title'); ?></label>
+			<div class="group-controls">
+				<input type="text" name="name" id="name" class="extend" value="<?php echo $this->category->name() ; ?>" />
+			</div>
+		</div>
+
+		<div class="form-group form-actions">
+			<div class="group-controls">
+				<button class="btn btn-important"><?php echo _t('gen.action.submit'); ?></button>
+				<button class="btn btn-attention confirm"
+					data-str-confirm="<?php echo _t('gen.js.confirm_action_feed_cat'); ?>"
+					formaction="<?php echo _url('category', 'empty', 'id', $this->category->id()); ?>"
+					formmethod="post"><?php echo _t('gen.action.empty'); ?></button>
+				<?php if (!$this->category->isDefault()): ?>
+				<button class="btn btn-attention confirm"
+					data-str-confirm="<?php echo _t('gen.js.confirm_action_feed_cat'); ?>"
+					formaction="<?php echo _url('category', 'delete', 'id', $this->category->id()); ?>"
+					formmethod="post"><?php echo _t('gen.action.remove'); ?></button>
+				<?php endif;?>
+			</div>
+		</div>
+	</form>
+</div>

+ 2 - 2
app/views/helpers/feed/update.phtml

@@ -19,7 +19,7 @@
 
 	<form method="post" action="<?php echo _url('subscription', 'feed', 'id', $this->feed->id()); ?>" autocomplete="off">
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
-		<legend><?php echo _t('sub.feed.informations'); ?></legend>
+		<legend><?php echo _t('sub.feed.information'); ?></legend>
 		<div class="form-group">
 			<label class="group-name" for="name"><?php echo _t('sub.feed.title'); ?></label>
 			<div class="group-controls">
@@ -240,7 +240,7 @@
 			<div class="group-controls">
 				<textarea name="filteractions_read" id="filteractions_read"><?php
 					foreach ($this->feed->filtersAction('read') as $filterRead) {
-						echo htmlspecialchars($filterRead->getRawInput(), ENT_NOQUOTES, 'UTF-8'), "\n\n";
+						echo htmlspecialchars($filterRead->getRawInput(), ENT_NOQUOTES, 'UTF-8'), PHP_EOL;
 					}
 				?></textarea>
 				<?php echo _i('help'); ?> <?php echo _t('sub.feed.filteractions.help'); ?>

+ 0 - 5
app/views/index/global.phtml

@@ -1,11 +1,6 @@
 <?php
 	$this->partial('nav_menu');
 
-	flush();
-	if (isset($this->callbackBeforeEntries)) {
-		call_user_func($this->callbackBeforeEntries, $this);
-	}
-
 	$class = '';
 	if (FreshRSS_Context::$user_conf->hide_read_feeds &&
 			FreshRSS_Context::isStateEnabled(FreshRSS_Entry::STATE_NOT_READ) &&

+ 0 - 5
app/views/index/normal.phtml

@@ -3,11 +3,6 @@
 $this->partial('aside_feed');
 $this->partial('nav_menu');
 
-flush();
-if (isset($this->callbackBeforeEntries)) {
-	call_user_func($this->callbackBeforeEntries, $this);
-}
-
 if (!empty($this->entries)) {
 	$display_today = true;
 	$display_yesterday = true;

+ 0 - 5
app/views/index/reader.phtml

@@ -2,11 +2,6 @@
 $this->partial('aside_feed');
 $this->partial('nav_menu');
 
-flush();
-if (isset($this->callbackBeforeEntries)) {
-	call_user_func($this->callbackBeforeEntries, $this);
-}
-
 if (!empty($this->entries)) {
 	$lazyload = FreshRSS_Context::$user_conf->lazyload;
 	$content_width = FreshRSS_Context::$user_conf->content_width;

+ 5 - 0
app/views/subscription/category.phtml

@@ -0,0 +1,5 @@
+<?php
+
+if ($this->category) {
+	$this->renderHelper('category/update');
+}

+ 5 - 44
app/views/subscription/index.phtml

@@ -80,50 +80,9 @@
 	?>
 	<div class="box">
 		<div class="box-title">
-			<form action="<?php echo _url('category', 'update', 'id', $cat->id()); ?>" method="post">
-				<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
-				<input type="text" name="name" value="<?php echo $cat->name(); ?>" />
-
-				<div class="dropdown">
-					<div id="dropdown-cat-<?php echo $cat->id(); ?>" class="dropdown-target"></div>
-
-					<a class="dropdown-toggle btn" href="#dropdown-cat-<?php echo $cat->id(); ?>"><?php echo _i('down'); ?></a>
-					<ul class="dropdown-menu">
-						<li class="dropdown-close"><a href="#close">❌</a></li>
-
-						<li class="item"><a href="<?php echo _url('index', 'index', 'get', 'c_' . $cat->id()); ?>"><?php echo _t('gen.action.filter'); ?></a></li>
-
-						<?php
-							$no_feed = empty($feeds);
-							$is_default = ($cat->id() === $this->default_category->id());
-
-							if (!$no_feed || !$is_default) {
-						?>
-						<li class="separator"></li>
-						<?php } if (!$no_feed) { ?>
-						<li class="item">
-							<button class="as-link confirm"
-							        data-str-confirm="<?php echo _t('gen.js.confirm_action_feed_cat'); ?>"
-							        type="submit"
-							        form="controller-category"
-							        formaction="<?php echo _url('category', 'empty', 'id', $cat->id()); ?>">
-							        <?php echo _t('gen.action.empty'); ?></button>
-						</li>
-						<?php } if (!$is_default) { ?>
-						<li class="item">
-							<button class="as-link confirm"
-							        data-str-confirm="<?php echo _t('gen.js.confirm_action_feed_cat'); ?>"
-							        type="submit"
-							        form="controller-category"
-							        formaction="<?php echo _url('category', 'delete', 'id', $cat->id()); ?>">
-							        <?php echo _t('gen.action.remove'); ?></button>
-						</li>
-						<?php } ?>
-					</ul>
-				</div>
-			</form>
+			<a class="configure open-slider" href="<?php echo _url('subscription', 'category', 'id', $cat->id()); ?>"><?php echo _i('configure'); ?></a>
+			<?php echo $cat->name(); ?>
 		</div>
-
 		<ul class="box-content" data-cat-id="<?php echo $cat->id(); ?>">
 			<?php if (!empty($feeds)) { ?>
 			<?php
@@ -159,12 +118,14 @@
 	</ul>
 </div>
 
-<?php $class = isset($this->feed) ? ' class="active"' : ''; ?>
+<?php $class = $this->displaySlider ? ' class="active"' : ''; ?>
 <a href="#" id="close-slider"<?php echo $class; ?>></a>
 <div id="slider"<?php echo $class; ?>>
 <?php
 	if (isset($this->feed)) {
 		$this->renderHelper('feed/update');
+	} elseif (isset($this->category)) {
+		$this->renderHelper('category/update');
 	}
 ?>
 </div>

+ 16 - 0
cli/README.md

@@ -78,6 +78,22 @@ cd /usr/share/FreshRSS
 # Optimize database (reduces the size) for a given user (perform `OPTIMIZE TABLE` in MySQL, `VACUUM` in SQLite)
 ```
 
+#### Note about cron
+
+Some commands display informations on standard error, cron will send an email with thoses informations every time the command will be executed (exited zero or non-zero).
+
+To avoid cron sending email on success:
+```sh
+@daily /usr/local/bin/my-command > /var/log/cron-freshrss-stdout.log 2>/var/log/cron-freshrss-stderr.log || cat /var/log/cron-freshrss-stderr.log
+```
+
+Explanations:
+- _/usr/local/bin/my-command > /var/log/cron-freshrss-stdout.log_ : redirect the standard output to a log file
+- _/usr/local/bin/my-command 2> /var/log/cron-freshrss-stderr.log_ : redirect the standard error to a log file
+- _|| cat /var/log/cron-freshrss-stderr.log_ : if the exit code of _/usr/local/bin/my-command_ is non-zero, then it send by mail the content error file.
+
+Now, cron will send you an email only if the exit code is non-zero and with the content of the file containing the errors.
+
 
 ## Unix piping
 

+ 1 - 1
constants.php

@@ -2,7 +2,7 @@
 //NB: Do not edit; use ./constants.local.php instead.
 
 //<Not customisable>
-define('FRESHRSS_VERSION', '1.14.2');
+define('FRESHRSS_VERSION', '1.14.3');
 define('FRESHRSS_WEBSITE', 'https://freshrss.org');
 define('FRESHRSS_WIKI', 'https://freshrss.github.io/FreshRSS/');
 

+ 1 - 0
docs/en/admins/01_Index.md

@@ -6,3 +6,4 @@ Learn how to install, update and backup FreshRSS and how to use the command line
 * [Update your installation](03_Updating.md) to the latest stable or dev version
 * [The command line interface](https://github.com/FreshRSS/FreshRSS/tree/master/cli) can be used to administrate feeds and users
 * [Automatic feed updates](https://github.com/FreshRSS/FreshRSS#automatic-feed-update) using cron is the preferred way to get the latest feeds entries   
+* [Frequently asked questions](04_Frequently_Asked_Questions.md)

+ 1 - 1
docs/en/admins/02_Installation.md

@@ -8,7 +8,7 @@ You need to verify that your server can run FreshRSS before installing it. If yo
 | ----------- | ---------------- | ----------------------------- |
 | Web server  | **Apache 2**     | Nginx                         |
 | PHP         | **PHP 5.5+**     | PHP 5.3.8+                    |
-| PHP modules | Required: libxml, cURL, PDO_MySQL, PCRE and ctype. \\ Required (32-bit only): GMP \\Recommanded: JSON, Zlib, mbstring, iconv, ZipArchive | |
+| PHP modules | Required: libxml, cURL, PDO_MySQL, PCRE and ctype. <br>Required (32-bit only): GMP <br> Recommanded: JSON, Zlib, mbstring, iconv, ZipArchive <br> *For the whole modules list see [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/744a9e8cf00aef7dec0acfa5f90f0dcfa2ef8837/Docker/Dockerfile-Alpine#L7-L9)* | |
 | Database    | **MySQL 5.5.3+** | SQLite 3.7.4+                 |
 | Browser     | **Firefox**      | Chrome, Opera, Safari, or IE11+ |
 

+ 28 - 0
docs/en/admins/04_Frequently_Asked_Questions.md

@@ -0,0 +1,28 @@
+We may not have answered all of your questions in the previous sections. The FAQ contains some questions that have not been answered elsewhere.
+
+## Promoting a user to admin
+
+At the moment, there can be only one *admin* user for the system.
+Thus promoting one user to *admin* demotes the current *admin* user.
+
+The recommended way of promoting a user is with the help of the CLI tool.
+You only have to do is to run the following command:
+```sh
+./cli/reconfigure.php --default_user <username>
+```
+
+Alternatively, you can edit configuration files manually.
+To do so, you need to change the *default_user* value in the file *./data/config.php*.
+As the file is a PHP file, you have to make sure that it's still valid after the update by running the following command:
+```sh
+php -l ./data/config.php
+```
+
+## Disabling self-registration
+
+Users can register directly on the login screen only if the configuration allows them.
+Under *Administration* > *System configuration*, you have access to *Max number of accounts*.
+As stated on that page, there is no limitation if you input **0**, thus allowing any number of user to self-register.
+If you input any other number, you will create a limitation on self-registering users.
+That means that as soon as the limit is reached, users cannot self-register but they can still be registered by the *admin* user.
+Using the value **1**, disables the self-registration since the spot is used by the *admin* user.

+ 17 - 0
docs/en/users/03_Main_view.md

@@ -30,6 +30,23 @@ Here is an example to trigger article update every hour.
 0 * * * * php /path/to/FreshRSS/app/actualize_script.php > /tmp/FreshRSS.log 2>&1
 ```
 
+Special parameters to configure the script - all parameters can be combined:
+
+- Parameter "force"  
+https://freshrss.example.net/i/?c=feed&a=actualize&force=1  
+If *force* is set to 1 all feeds will be refreshed at once.
+
+- Parameter "ajax"  
+https://freshrss.example.net/i/?c=feed&a=actualize&ajax=1  
+Only a status site is returned and not a complete website. Example: "OK"
+
+- Parameter "maxFeeds"  
+https://freshrss.example.net/i/?c=feed&a=actualize&maxFeeds=30  
+If *maxFeeds* is set the configured amount of feeds is refreshed at once. The default setting is "10".
+
+- Parameter "token"  
+https://freshrss.example.net/i/?c=feed&a=actualize&token=542345872345734  
+Security parameter to prevent unauthorized refreshes. For detailed Documentation see "Form authentication".
 
 ### Online cron
 

+ 1 - 1
docs/en/users/06_Fever_API.md

@@ -26,7 +26,7 @@ Tested with:
 - iOS
   - [Fiery Feeds](https://itunes.apple.com/app/fiery-feeds-rss-reader/id1158763303)
   - [Unread](https://itunes.apple.com/app/unread-rss-reader/id1252376153)
-  - [Reeder-3](https://itunes.apple.com/app/reeder-3/id697846300)
+  - [Reeder-4](https://itunes.apple.com/app/reeder-4/id1449412357)
 
 - MacOS
   - [Readkit](https://itunes.apple.com/app/readkit/id588726889)

+ 2 - 0
docs/en/users/06_Mobile_access.md

@@ -55,6 +55,8 @@ See the [page about the Fever compatible API](06_Fever_API.md) for another possi
 		* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Open source)
 	* MacOS
 		* [Vienna RSS](http://www.vienna-rss.com/) (Open source)
+	* Firefox
+		* [FreshRSS-Notify](https://addons.mozilla.org/firefox/addon/freshrss-notify-webextension/) (Open source)
 
 
 # Google Reader compatible API

+ 1 - 1
docs/fr/users/01_Installation.md

@@ -8,7 +8,7 @@ Il est toutefois de votre responsabilité de vérifier que votre hébergement pe
  | --------         | -----------                                                                                                    | ---------------------          |
  | Serveur web      | **Apache 2**                                                                                                   | Nginx                          |
  | PHP              | **PHP 5.5+**                                                                                                   | PHP 5.3.8+                     |
- | Modules PHP      | Requis : libxml, cURL, PDO_MySQL, PCRE et ctype \\ Requis (32 bits seulement) : GMP \\ Recommandé : JSON, Zlib, mbstring et iconv, ZipArchive |                                |
+ | Modules PHP      | Requis : libxml, cURL, PDO_MySQL, PCRE et ctype<br>Requis (32 bits seulement) : GMP<br>Recommandé : JSON, Zlib, mbstring et iconv, ZipArchive<br>*Pour une liste complète des modules nécessaires voir le [Dockerfile](https://github.com/FreshRSS/FreshRSS/blob/744a9e8cf00aef7dec0acfa5f90f0dcfa2ef8837/Docker/Dockerfile-Alpine#L7-L9)* |                                |
  | Base de données  | **MySQL 5.5.3+**                                                                                               | SQLite 3.7.4+                  |
  | Navigateur       | **Firefox**                                                                                                    | Chrome, Opera, Safari, or IE 11+ |
 

+ 2 - 0
docs/fr/users/06_Mobile_access.md

@@ -69,6 +69,8 @@ Tout client supportant une API de type Google Reader. Sélection :
 	* [FeedReader 2.0+](https://jangernert.github.io/FeedReader/) (Libre)
 * MacOS
 	* [Vienna RSS](http://www.vienna-rss.com/) (Libre)
+* Firefox
+	* [FreshRSS-Notify](https://addons.mozilla.org/fr/firefox/addon/freshrss-notify-webextension/) (Libre)
 
 # API compatible Google Reader
 

+ 1 - 1
p/api/.htaccess

@@ -1,5 +1,5 @@
 <IfModule mod_setenvif.c>
-	SetEnvIfNoCase "^Authorization$" "(.*)" HTTP_AUTHORIZATION=$1
+	SetEnvIfNoCase "Authorization" "(.*)" HTTP_AUTHORIZATION=$1
 </IfModule>
 <IfModule !mod_setenvif.c>
 	<IfModule mod_rewrite.c>

+ 2 - 2
p/api/greader.php

@@ -347,7 +347,7 @@ function subscriptionEdit($streamNames, $titles, $action, $add = '', $remove = '
 		$c_name = htmlspecialchars($c_name, ENT_COMPAT, 'UTF-8');
 		$cat = $categoryDAO->searchByName($c_name);
 		$addCatId = $cat == null ? 0 : $cat->id();
-	} else if ($remove != '' && strpos($remove, 'user/-/label/')) {
+	} elseif ($remove != '' && strpos($remove, 'user/-/label/') === 0) {
 		$addCatId = 1;	//Default category
 	}
 	$feedDAO = FreshRSS_Factory::createFeedDao();
@@ -998,7 +998,7 @@ if ($pathInfos[1] === 'accounts') {
 					 * (more efficient from a backend perspective than multiple requests). */
 					$streamId = $_GET['s'];
 					streamContentsItemsIds($streamId, $start_time, $stop_time, $count, $order, $filter_target, $exclude_target, $continuation);
-				} else if ($pathInfos[6] === 'contents' && isset($_POST['i'])) {	//FeedMe
+				} elseif ($pathInfos[6] === 'contents' && isset($_POST['i'])) {	//FeedMe
 					$e_ids = multiplePosts('i');	//item IDs
 					streamContentsItems($e_ids, $order);
 				}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 1
p/scripts/jquery.min.js


+ 4 - 8
p/scripts/main.js

@@ -859,15 +859,11 @@ function init_stream(stream) {
 
 		el = ev.target.closest('.item.share > a[href="#"]');
 		if (el) {	//Print
-			const content = '<html><head><style>' +
-				'body { font-family: Serif; text-align: justify; }' +
-				'a { color: #000; text-decoration: none; }' +
-				'a:after { content: " [" attr(href) "]"}' +
-				'</style></head><body>' +
-				el.closest('.flux_content').querySelector('.content').innerHTML +
-				'</body></html>';
 			const tmp_window = window.open();
-			tmp_window.document.writeln(content);
+			for (var i = 0; i < document.styleSheets.length; i++) {
+				tmp_window.document.writeln('<link href="' + document.styleSheets[i].href + '" rel="stylesheet" type="text/css" />');
+			}
+			tmp_window.document.writeln(el.closest('.flux_content').querySelector('.content').innerHTML);
 			tmp_window.document.close();
 			tmp_window.focus();
 			tmp_window.print();

+ 29 - 1
p/themes/Ansum/_components.scss

@@ -211,12 +211,40 @@
 
     .box-title {
 	margin: 0;
-	padding: 5px 10px;
+	padding: 0.5rem 0.75rem;
 	background: $grey-light;
 	color: $main-font-color;
 	// border-bottom: 1px solid #ddd;
 	border-radius: 2px 2px 0 0;
 
+	img{
+		margin-right: 0.75rem;
+	}
+
+	&:hover{
+		.configure {
+			visibility: visible;
+			background: url("icons/cog.svg") no-repeat 4px 4px;
+			width: 1.75rem;
+			height: 1.75rem;
+			display: block;
+			border-radius: 2px;
+			float: left;
+			margin-right: 0.5rem;
+			.icon {
+				vertical-align: middle;
+				border-radius: 3px;
+				display: none;
+			}
+			&:hover {
+				background: url("icons/cog-white.svg") no-repeat 4px 4px $main-first;
+			}
+		}
+	}
+	.configure {
+		visibility: hidden;
+	}
+
 	form{
 	    input{
 		width: 85%;

+ 20 - 1
p/themes/Ansum/ansum.css

@@ -339,10 +339,29 @@ form th {
   box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.25); }
   .box .box-title {
     margin: 0;
-    padding: 5px 10px;
+    padding: 0.5rem 0.75rem;
     background: #f5f0ec;
     color: #363330;
     border-radius: 2px 2px 0 0; }
+    .box .box-title img {
+      margin-right: 0.75rem; }
+    .box .box-title:hover .configure {
+      visibility: visible;
+      background: url("icons/cog.svg") no-repeat 4px 4px;
+      width: 1.75rem;
+      height: 1.75rem;
+      display: block;
+      border-radius: 2px;
+      float: left;
+      margin-right: 0.5rem; }
+      .box .box-title:hover .configure .icon {
+        vertical-align: middle;
+        border-radius: 3px;
+        display: none; }
+      .box .box-title:hover .configure:hover {
+        background: url("icons/cog-white.svg") no-repeat 4px 4px #ca7227; }
+    .box .box-title .configure {
+      visibility: hidden; }
     .box .box-title form input {
       width: 85%; }
     .box .box-title form .dropdown {

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
p/themes/Ansum/ansum.css.map


+ 1 - 0
p/themes/Ansum/sass.sh

@@ -1 +1,2 @@
+#!/bin/sh
 sass --watch ansum.scss:ansum.css

+ 2 - 0
p/themes/BlueLagoon/BlueLagoon.css

@@ -503,9 +503,11 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 2 - 0
p/themes/Dark/dark.css

@@ -453,9 +453,11 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 2 - 0
p/themes/Flat/flat.css

@@ -455,6 +455,7 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
@@ -463,6 +464,7 @@ a.btn {
 	background-color: #95a5a6;
 	border-radius: 3px;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 27 - 0
p/themes/Mapco/_components.scss

@@ -216,6 +216,33 @@
 	color: $main-font-color;
 	// border-bottom: 1px solid #ddd;
 	border-radius: 2px 2px 0 0;
+	img{
+		margin-right: 0.75rem;
+	}
+
+	&:hover{
+		.configure {
+			visibility: visible;
+			background: url("icons/cog.svg") no-repeat 4px 4px;
+			width: 1.75rem;
+			height: 1.75rem;
+			display: block;
+			border-radius: 2px;
+			float: left;
+			margin-right: 0.5rem;
+			.icon {
+				vertical-align: middle;
+				border-radius: 3px;
+				display: none;
+			}
+			&:hover {
+				background: url("icons/cog-white.svg") no-repeat 4px 4px $main-first;
+			}
+		}
+	}
+	.configure {
+		visibility: hidden;
+	}
 
 	form{
 	    input{

+ 19 - 0
p/themes/Mapco/mapco.css

@@ -345,6 +345,25 @@ form th {
     background: #eff0f2;
     color: #303136;
     border-radius: 2px 2px 0 0; }
+    .box .box-title img {
+      margin-right: 0.75rem; }
+    .box .box-title:hover .configure {
+      visibility: visible;
+      background: url("icons/cog.svg") no-repeat 4px 4px;
+      width: 1.75rem;
+      height: 1.75rem;
+      display: block;
+      border-radius: 2px;
+      float: left;
+      margin-right: 0.5rem; }
+      .box .box-title:hover .configure .icon {
+        vertical-align: middle;
+        border-radius: 3px;
+        display: none; }
+      .box .box-title:hover .configure:hover {
+        background: url("icons/cog-white.svg") no-repeat 4px 4px #3366cc; }
+    .box .box-title .configure {
+      visibility: hidden; }
     .box .box-title form input {
       width: 85%; }
     .box .box-title form .dropdown {

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
p/themes/Mapco/mapco.css.map


+ 1 - 0
p/themes/Mapco/sass.sh

@@ -1 +1,2 @@
+#!/bin/sh
 sass --watch mapco.scss:mapco.css

+ 2 - 0
p/themes/Origine-compact/origine-compact.css

@@ -484,9 +484,11 @@ a.btn,
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 2 - 0
p/themes/Origine/origine.css

@@ -482,9 +482,11 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 2 - 0
p/themes/Pafat/pafat.css

@@ -456,9 +456,11 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 2 - 0
p/themes/Screwdriver/screwdriver.css

@@ -503,9 +503,11 @@ a.btn {
 	line-height: 2.5em;
 }
 
+.box .box-title .configure,
 .box .box-content .item .configure {
 	visibility: hidden;
 }
+.box .box-title:hover .configure,
 .box .box-content .item:hover .configure {
 	visibility: visible;
 }

+ 1 - 2
p/themes/Swage/icons/refresh.svg

@@ -1,6 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <svg enable-background="new 0 0 16 16" version="1.1" viewBox="0 0 16 16" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
 <style type="text/css">.st0{fill:#FFFFFF;}</style>
-<path class="st0" d="M13.5,2.4C12.1,1,10.1,0.1,8,0.1c-4.3,0-7.7,3.5-7.7,7.7s3.5,7.7,7.7,7.7c3.6,0,6.6-2.5,7.5-5.8h-2,c-0.8,2.3-2.9,3.9-5.5,3.9c-3.2,0-5.8-2.6-5.8-5.8S4.8,2.1,8,2.1c1.6,0,3,0.7,4.1,1.7L9,6.9h6.8V0.1L13.5,2.4z"/>
+<path class="st0" d="M13.5,2.4C12.1,1,10.1,0.1,8,0.1c-4.3,0-7.7,3.5-7.7,7.7s3.5,7.7,7.7,7.7c3.6,0,6.6-2.5,7.5-5.8h-2c-0.8,2.3-2.9,3.9-5.5,3.9c-3.2,0-5.8-2.6-5.8-5.8S4.8,2.1,8,2.1c1.6,0,3,0.7,4.1,1.7L9,6.9h6.8V0.1L13.5,2.4z"/>
 </svg>
-

+ 3 - 1
p/themes/Swage/swage.css

@@ -904,7 +904,8 @@ padding: 12px;
 
 #new-article {
 width: 100%;
-bottom: initial;
+position: sticky;
+top: 0;
 }
 
 .header {
@@ -948,6 +949,7 @@ padding: 0;
 }
 .aside:target {
 width: 78%;
+z-index: 1000;
 }
 
 .nav_menu {

+ 3 - 2
p/themes/Swage/swage.scss

@@ -1002,10 +1002,10 @@ form {
 	.dropdown-header, .dropdown-menu > .item {
 		padding: 12px;
 	}
-
 	#new-article {
 		width: 100%;
-		bottom: initial;
+		position: sticky;
+		top: 0;
 	}
 	.header {
 		display: table;
@@ -1045,6 +1045,7 @@ form {
 		}
 		&:target {
 			width: 78%;
+			z-index: 1000;
 		}
 	}
 	.nav_menu {

+ 3 - 9
p/themes/base-theme/template.css

@@ -344,10 +344,6 @@ a.btn {
 	position: relative;
 	font-size: 1.2rem;
 	font-weight: bold;
-	text-align: center;
-}
-.box .box-title a {
-	display: block;
 }
 .box .box-title form {
 	margin: 0;
@@ -626,9 +622,6 @@ a.btn {
 br {
 	line-height: 1em;
 }
-br + br + br {
-	display: none;
-}
 
 /*=== Notification and actualize notification */
 .notification {
@@ -692,10 +685,11 @@ br + br + br {
 #bigMarkAsRead {
 	display: block;
 	width: 100%;
-	padding: 1em 0 100% 0;
-	padding: 1em 0 100vh 0;
 	text-align: center;
 	font-size: 1.4em;
+	padding: 1em 0 50px 0;
+	margin: 0 0 100% 0;
+	margin: 0 0 100vh 0;
 }
 .bigTick {
 	font-size: 4em;

+ 4 - 0
tests/README.md

@@ -5,3 +5,7 @@ cd ./tests/
 wget https://phar.phpunit.de/phpunit.phar
 php phpunit.phar --bootstrap bootstrap.php
 ```
+
+The `shellchecks.sh` script is used to safeguard shell scripts from common
+shell script bugs and to ensure a consistent style.
+It requires [ShellCheck](https://www.shellcheck.net/) and [shfmt](https://github.com/mvdan/sh).

+ 29 - 0
tests/shellchecks.sh

@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+# Based on https://github.com/koreader/koreader/blob/master/.ci/helper_shellchecks.sh
+
+ANSI_RED="\\033[31;1m"
+ANSI_GREEN="\\033[32;1m"
+ANSI_RESET="\\033[0m"
+
+mapfile -t shellscript_locations < <({ git grep -lE '^#!(/usr)?/bin/(env )?(bash|sh)' && git ls-files ./*.sh; } | sort | uniq)
+
+SHELLSCRIPT_ERROR=0
+
+for shellscript in "${shellscript_locations[@]}"; do
+	echo -e "${ANSI_GREEN}Running shellcheck on ${shellscript}"
+	shellcheck "${shellscript}" || SHELLSCRIPT_ERROR=1
+	echo -e "${ANSI_GREEN}Running shfmt on ${shellscript}"
+	if ! shfmt "${shellscript}" >/dev/null 2>&1; then
+		echo -e "${ANSI_RED}Warning: ${shellscript} contains the following problem:"
+		shfmt "${shellscript}" || SHELLSCRIPT_ERROR=1
+		continue
+	fi
+	if [ "$(cat "${shellscript}")" != "$(shfmt "${shellscript}")" ]; then
+		echo -e "${ANSI_RED}Warning: ${shellscript} does not abide by coding style, diff for expected style:"
+		shfmt "${shellscript}" | diff "${shellscript}" - || SHELLSCRIPT_ERROR=1
+	fi
+done
+
+echo -ne "${ANSI_RESET}"
+
+exit "${SHELLSCRIPT_ERROR}"

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff