Переглянути джерело

Merge pull request #2338 from FreshRSS/dev

FreshRSS 1.14.1
Alexandre Alapetite 7 роки тому
батько
коміт
d008b6c1a7

+ 4 - 0
.jshintignore

@@ -0,0 +1,4 @@
+node_modules
+p/scripts/bcrypt.min.js
+p/scripts/flotr2.min.js
+p/scripts/jquery.min.js

+ 8 - 0
.jshintrc

@@ -0,0 +1,8 @@
+{
+	"esversion" : 6,
+	"browser" : true,
+	"globals": {
+		"confirm": true,
+		"console": true
+	}
+}

+ 17 - 7
.travis.yml

@@ -1,15 +1,16 @@
 language: php
 language: php
 php:
 php:
-  - '5.4'
-  - '5.5'
-  - '5.6'
-  - '7.0'
-  - '7.1'
-  - '7.2'
+  - 5.4
+  - 5.5
+  - 5.6
+  - 7.0
+  - 7.1
+  - 7.2
+  - 7.3
 
 
 install:
 install:
   # newest version without https://github.com/squizlabs/PHP_CodeSniffer/pull/1404
   # newest version without https://github.com/squizlabs/PHP_CodeSniffer/pull/1404
-  - composer global require squizlabs/php_codesniffer "<=3.0.0RC4"
+  - composer global require squizlabs/php_codesniffer
 
 
 script:
 script:
   - phpenv rehash
   - phpenv rehash
@@ -34,6 +35,15 @@ matrix:
       dist: precise
       dist: precise
     - php: "7.2"
     - php: "7.2"
       env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
       env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
+    - language: node_js
+      node_js:
+        - "node"
+      php:
+        # none
+      install:
+        - npm install jshint
+      script:
+        - node_modules/jshint/bin/jshint .
   allow_failures:
   allow_failures:
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - dist: precise
     - dist: precise

+ 28 - 1
CHANGELOG.md

@@ -1,5 +1,32 @@
 # FreshRSS changelog
 # FreshRSS changelog
 
 
+## 2019-04-07 FreshRSS 1.14.1
+
+* Bug fixing (regressions introduced in 1.14.0)
+	* Fix *load more articles* when using ascending order [#2314](https://github.com/FreshRSS/FreshRSS/issues/2314)
+	* Fix cron in the Ubuntu flavour of the Docker image [#2319](https://github.com/FreshRSS/FreshRSS/issues/2319)
+	* Fix the use of arrow keyboard keys for shortcuts [#2316](https://github.com/FreshRSS/FreshRSS/issues/2316)
+	* Fix control+click or middle-click for opening articles in a background tab [#2310](https://github.com/FreshRSS/FreshRSS/issues/2310)
+	* Fix the naming of the option to unfold categories [#2307](https://github.com/FreshRSS/FreshRSS/issues/2307)
+	* Fix shortcut problem when using unfolded articles [#2328](https://github.com/FreshRSS/FreshRSS/issues/2328)
+	* Fix auto-hiding articles [#2323](https://github.com/FreshRSS/FreshRSS/issues/2323)
+	* Fix scroll functions with Edge [#2337](https://github.com/FreshRSS/FreshRSS/pull/2337)
+	* Fix drop-down menu warning [#2353](https://github.com/FreshRSS/FreshRSS/pull/2353)
+	* Fix delay for individual mark-as-read actions [#2332](https://github.com/FreshRSS/FreshRSS/issues/2332)
+	* Fix scroll functions in Edge [#2337](https://github.com/FreshRSS/FreshRSS/pull/2337)
+* Bug fixing (misc.)
+	* Fix extensions in Windows [#994](https://github.com/FreshRSS/FreshRSS/issues/994)
+	* Fix import of empty articles [#2351](https://github.com/FreshRSS/FreshRSS/pull/2351)
+	* Fix quote escaping on CLI i18n tools [#2355](https://github.com/FreshRSS/FreshRSS/pull/2355)
+* UI
+	* Better handling of bad Ajax requests and fast page unload (ask confirmation) [#2346](https://github.com/FreshRSS/FreshRSS/pull/2346)
+* I18n
+	* Improve Dutch [#2312](https://github.com/FreshRSS/FreshRSS/pull/2312)
+* Misc.
+	* Check JavaScript (jshint) in Travis continuous integration [#2315](https://github.com/FreshRSS/FreshRSS/pull/2315)
+	* Add PHP 7.3 to Travis [#2317](https://github.com/FreshRSS/FreshRSS/pull/2317)
+
+
 ## 2019-03-31 FreshRSS 1.14.0
 ## 2019-03-31 FreshRSS 1.14.0
 
 
 * Features
 * Features
@@ -31,7 +58,7 @@
 * API
 * API
 	* Supported by [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (client for Android using Fever API)
 	* Supported by [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (client for Android using Fever API)
 * I18n
 * I18n
-	* Improved Korean [#2242](https://github.com/FreshRSS/FreshRSS/pull/2242)
+	* Improve Korean [#2242](https://github.com/FreshRSS/FreshRSS/pull/2242)
 	* Improve Occitan [#2253](https://github.com/FreshRSS/FreshRSS/pull/2253)
 	* Improve Occitan [#2253](https://github.com/FreshRSS/FreshRSS/pull/2253)
 * Security
 * Security
 	* Reworked the CSRF token interaction with the session in some edge cases [#2290](https://github.com/FreshRSS/FreshRSS/pull/2290)
 	* Reworked the CSRF token interaction with the session in some edge cases [#2290](https://github.com/FreshRSS/FreshRSS/pull/2290)

+ 5 - 3
Docker/Dockerfile

@@ -11,7 +11,7 @@ RUN apt update && \
 	php-sqlite3 php-mysql php-pgsql && \
 	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/php/
+RUN mkdir -p /var/www/FreshRSS /run/apache2/
 WORKDIR /var/www/FreshRSS
 WORKDIR /var/www/FreshRSS
 
 
 COPY . /var/www/FreshRSS
 COPY . /var/www/FreshRSS
@@ -25,8 +25,10 @@ RUN a2dismod -f alias autoindex negotiation status && \
 
 
 RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
 RUN sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/apache2.conf && \
 	sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
 	sed -r -i "/^\s*Listen /s/^/#/" /etc/apache2/ports.conf && \
-	echo "17,37 su apache -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' 2>> /proc/1/fd/2 > /tmp/FreshRSS.log" >> \
-		/var/spool/cron/crontabs/root
+	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 -
 
 
 ENV COPY_SYSLOG_TO_STDERR On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENV CRON_MIN ''

+ 6 - 4
Docker/Dockerfile-Alpine

@@ -16,14 +16,16 @@ COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
 
 
 RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
 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 && \
 		/etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \
-	sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \ 
+	sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \
 		/etc/apache2/httpd.conf && \
 		/etc/apache2/httpd.conf && \
-	sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|setenvif).so$/s/^\s*#//" \ 
+	sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|setenvif).so$/s/^\s*#//" \
 		/etc/apache2/httpd.conf && \
 		/etc/apache2/httpd.conf && \
 	sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
 	sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
 		/etc/apache2/httpd.conf && \
 		/etc/apache2/httpd.conf && \
-	echo "17,37 * * * * su apache -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' 2>> /proc/1/fd/2 > /tmp/FreshRSS.log" >> \
-		/var/spool/cron/crontabs/root
+	touch /var/www/FreshRSS/Docker/env.txt && \
+	echo "27,57 * * * * . /var/www/FreshRSS/Docker/env.txt; \
+		su apache -s /bin/sh -c 'php /var/www/FreshRSS/app/actualize_script.php' \
+		2>> /proc/1/fd/2 > /tmp/FreshRSS.log" | crontab -
 
 
 ENV COPY_SYSLOG_TO_STDERR On
 ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENV CRON_MIN ''

+ 12 - 1
Docker/README.md

@@ -211,12 +211,23 @@ For advanced users. Offers good logging and monitoring with auto-restart on fail
 Watch out to use the same run parameters than in your main FreshRSS instance, for database, networking, and file system.
 Watch out to use the same run parameters than in your main FreshRSS instance, for database, networking, and file system.
 See cron option 1 for customising the cron schedule.
 See cron option 1 for customising the cron schedule.
 
 
+#### For the Ubuntu image (default)
 ```sh
 ```sh
 sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
   -v freshrss-data:/var/www/FreshRSS/data \
-  -e 'CRON_MIN=17,37' \
+  -e 'CRON_MIN=17,47' \
   --net freshrss-network \
   --net freshrss-network \
   --name freshrss_cron freshrss/freshrss \
   --name freshrss_cron freshrss/freshrss \
+  cron
+```
+
+#### For the Alpine image
+```sh
+sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
+  -v freshrss-data:/var/www/FreshRSS/data \
+  -e 'CRON_MIN=27,57' \
+  --net freshrss-network \
+  --name freshrss_cron freshrss/freshrss:alpine \
   crond -f -d 6
   crond -f -d 6
 ```
 ```
 
 

+ 2 - 1
Docker/entrypoint.sh

@@ -8,7 +8,8 @@ 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
 if [ -n "$CRON_MIN" ]; then
-	sed -r -i "\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" /var/spool/cron/crontabs/root
+	(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
 fi
 
 
 exec "$@"
 exec "$@"

+ 5 - 19
app/Controllers/configureController.php

@@ -166,30 +166,16 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * tab and up.
 	 * tab and up.
 	 */
 	 */
 	public function shortcutAction() {
 	public function shortcutAction() {
-		$list_keys = array('a', 'b', 'backspace', 'c', 'd', 'delete', 'down', 'e', 'end', 'enter',
-		                   'escape', 'f', 'g', 'h', 'home', 'i', 'insert', 'j', 'k', 'l', 'left',
-		                   'm', 'n', 'o', 'p', 'page_down', 'page_up', 'q', 'r', 'return', 'right',
-		                   's', 'space', 't', 'tab', 'u', 'up', 'v', 'w', 'x', 'y',
-		                   'z', 'f1', 'f2', 'f3', 'f4', 'f5', 'f6', 'f7', 'f8', 'f9',
-		                   'f10', 'f11', 'f12', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0');
-		$this->view->list_keys = $list_keys;
+		$this->view->list_keys = SHORTCUT_KEYS;
 
 
 		if (Minz_Request::isPost()) {
 		if (Minz_Request::isPost()) {
-			$shortcuts = Minz_Request::param('shortcuts');
-			$shortcuts_ok = array();
-
-			foreach ($shortcuts as $key => $value) {
-				if (in_array($value, $list_keys)) {
-					$shortcuts_ok[$key] = $value;
-				}
-			}
-
-			FreshRSS_Context::$user_conf->shortcuts = $shortcuts_ok;
+			FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(Minz_Request::param('shortcuts'));
 			FreshRSS_Context::$user_conf->save();
 			FreshRSS_Context::$user_conf->save();
 			invalidateHttpCache();
 			invalidateHttpCache();
 
 
-			Minz_Request::good(_t('feedback.conf.shortcuts_updated'),
-			                   array('c' => 'configure', 'a' => 'shortcut'));
+			Minz_Request::good(_t('feedback.conf.shortcuts_updated'), array('c' => 'configure', 'a' => 'shortcut'));
+		} else {
+			FreshRSS_Context::$user_conf->shortcuts = validateShortcutList(FreshRSS_Context::$user_conf->shortcuts);
 		}
 		}
 
 
 		Minz_View::prependTitle(_t('conf.shortcut.title') . ' · ');
 		Minz_View::prependTitle(_t('conf.shortcut.title') . ' · ');

+ 1 - 1
app/Models/EntryDAO.php

@@ -350,7 +350,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$sql .= $hasWhere ? ' AND' : ' WHERE';
 			$sql .= $hasWhere ? ' AND' : ' WHERE';
 			$hasWhere = true;
 			$hasWhere = true;
 			$sql .= ' f.id=?';
 			$sql .= ' f.id=?';
-			$values[] = $id;
+			$values[] = $feedId;
 		}
 		}
 		if ($catId !== false) {
 		if ($catId !== false) {
 			$sql .= $hasWhere ? ' AND' : ' WHERE';
 			$sql .= $hasWhere ? ' AND' : ' WHERE';

+ 1 - 1
app/i18n/cz/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Po přečtení články schovat',
 		'auto_remove_article' => 'Po přečtení články schovat',
 		'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”',
 		'confirm_enabled' => 'Vyžadovat potvrzení pro akci “označit vše jako přečtené”',
 		'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
 		'display_articles_unfolded' => 'Ve výchozím stavu zobrazovat články otevřené',
-		'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie zavřené',
+		'display_categories_unfolded' => 'Ve výchozím stavu zobrazovat kategorie otevřené',
 		'hide_read_feeds' => 'Schovat kategorie a kanály s nulovým počtem nepřečtených článků (nefunguje s nastavením “Zobrazit všechny články”)',
 		'hide_read_feeds' => 'Schovat kategorie a kanály s nulovým počtem nepřečtených článků (nefunguje s nastavením “Zobrazit všechny články”)',
 		'img_with_lazyload' => 'Použít "lazy load" mód pro načítaní obrázků',
 		'img_with_lazyload' => 'Použít "lazy load" mód pro načítaní obrázků',
 		'jump_next' => 'skočit na další nepřečtený (kanál nebo kategorii)',
 		'jump_next' => 'skočit na další nepřečtený (kanál nebo kategorii)',

+ 1 - 1
app/i18n/de/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Artikel nach dem Lesen verstecken',
 		'auto_remove_article' => 'Artikel nach dem Lesen verstecken',
 		'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen',
 		'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen',
 		'display_articles_unfolded' => 'Artikel standardmäßig ausgeklappt zeigen',
 		'display_articles_unfolded' => 'Artikel standardmäßig ausgeklappt zeigen',
-		'display_categories_unfolded' => 'Kategorien standardmäßig eingeklappt zeigen',
+		'display_categories_unfolded' => 'Kategorien standardmäßig ausgeklappt zeigen',
 		'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)',
 		'hide_read_feeds' => 'Kategorien & Feeds ohne ungelesene Artikel verstecken (funktioniert nicht mit der Einstellung „Alle Artikel zeigen“)',
 		'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
 		'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
 		'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)',
 		'jump_next' => 'springe zum nächsten ungelesenen Geschwisterelement (Feed oder Kategorie)',

+ 1 - 1
app/i18n/fr/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Cacher les articles après lecture',
 		'auto_remove_article' => 'Cacher les articles après lecture',
 		'confirm_enabled' => 'Afficher une confirmation lors des actions “marquer tout comme lu”',
 		'confirm_enabled' => 'Afficher une confirmation lors des actions “marquer tout comme lu”',
 		'display_articles_unfolded' => 'Afficher les articles dépliés par défaut',
 		'display_articles_unfolded' => 'Afficher les articles dépliés par défaut',
-		'display_categories_unfolded' => 'Afficher les catégories pliées par défaut',
+		'display_categories_unfolded' => 'Afficher les catégories pliées par défaut',
 		'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)',
 		'hide_read_feeds' => 'Cacher les catégories & flux sans article non-lu (ne fonctionne pas avec la configuration “Afficher tous les articles”)',
 		'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
 		'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
 		'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)',
 		'jump_next' => 'sauter au prochain voisin non lu (flux ou catégorie)',

+ 1 - 1
app/i18n/he/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Hide articles after reading',	//TODO - Translation
 		'auto_remove_article' => 'Hide articles after reading',	//TODO - Translation
 		'confirm_enabled' => 'הצגת דו-שיח לאישור “סימון הכל כנקרא” ',
 		'confirm_enabled' => 'הצגת דו-שיח לאישור “סימון הכל כנקרא” ',
 		'display_articles_unfolded' => 'הצגת מאמרים בשלמותם כברירת מחדל',
 		'display_articles_unfolded' => 'הצגת מאמרים בשלמותם כברירת מחדל',
-		'display_categories_unfolded' => 'הצגת קטגוריות מקופלות כברירת מחדל',
+		'display_categories_unfolded' => 'הצגת קטגוריות בשלמותן כברירת מחדל',
 		'hide_read_feeds' => 'הסתרת קטגוריות &amp; הזנות ללא מאמרים שלא נקראו (לא עובד יחד עם “הצגת כל המאמרים”)',
 		'hide_read_feeds' => 'הסתרת קטגוריות &amp; הזנות ללא מאמרים שלא נקראו (לא עובד יחד עם “הצגת כל המאמרים”)',
 		'img_with_lazyload' => 'שימוש ב "טעינה עצלה" על מנת לטעון תמונות',
 		'img_with_lazyload' => 'שימוש ב "טעינה עצלה" על מנת לטעון תמונות',
 		'jump_next' => 'קפיצה לפריט הבא שלא נקרא (הזנה או קטגוריה)',
 		'jump_next' => 'קפיצה לפריט הבא שלא נקרא (הזנה או קטגוריה)',

+ 1 - 1
app/i18n/kr/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => '글을 읽은 후 숨기기',
 		'auto_remove_article' => '글을 읽은 후 숨기기',
 		'confirm_enabled' => '“모두 읽음으로 표시” 실행시 확인 창 표시',
 		'confirm_enabled' => '“모두 읽음으로 표시” 실행시 확인 창 표시',
 		'display_articles_unfolded' => '글을 펼쳐진 상태로 보여주기',
 		'display_articles_unfolded' => '글을 펼쳐진 상태로 보여주기',
-		'display_categories_unfolded' => '카테고리를 접힌 상태로 보여주기',
+		'display_categories_unfolded' => '카테고리를 펼친 상태로 보여주기',
 		'hide_read_feeds' => '읽지 않은 글이 없는 카테고리와 피드 감추기 (“모든 글 표시”가 설정된 경우 동작하지 않습니다)',
 		'hide_read_feeds' => '읽지 않은 글이 없는 카테고리와 피드 감추기 (“모든 글 표시”가 설정된 경우 동작하지 않습니다)',
 		'img_with_lazyload' => '그림을 불러오는 데에 "lazy load" 모드 사용하기',
 		'img_with_lazyload' => '그림을 불러오는 데에 "lazy load" 모드 사용하기',
 		'jump_next' => '다음 읽지 않은 항목으로 이동 (피드 또는 카테고리)',
 		'jump_next' => '다음 읽지 않은 항목으로 이동 (피드 또는 카테고리)',

+ 2 - 2
app/i18n/nl/admin.php

@@ -160,8 +160,8 @@ return array(
 		'_' => 'Systeem configuratie',
 		'_' => 'Systeem configuratie',
 		'auto-update-url' => 'Automatische update server URL',
 		'auto-update-url' => 'Automatische update server URL',
 		'instance-name' => 'Voorbeeld naam',
 		'instance-name' => 'Voorbeeld naam',
-		'max-categories' => 'Categoriën limiet per gebruiker',
-		'max-feeds' => 'Feed limiet per gebruiker',
+		'max-categories' => 'Categorielimiet per gebruiker',
+		'max-feeds' => 'Feedlimiet per gebruiker',
 		'cookie-duration' => array(
 		'cookie-duration' => array(
 			'help' => 'in seconden',
 			'help' => 'in seconden',
 			'number' => 'Tijdsduur om ingelogd te blijven',
 			'number' => 'Tijdsduur om ingelogd te blijven',

+ 11 - 11
app/i18n/nl/conf.php

@@ -91,18 +91,18 @@ return array(
 		'auto_load_more' => 'Laad volgende artikel onderaan de pagina',
 		'auto_load_more' => 'Laad volgende artikel onderaan de pagina',
 		'auto_remove_article' => 'Verberg artikel na lezen',
 		'auto_remove_article' => 'Verberg artikel na lezen',
 		'confirm_enabled' => 'Toon een bevestigings dialoog op “markeer alles als gelezen” acties',
 		'confirm_enabled' => 'Toon een bevestigings dialoog op “markeer alles als gelezen” acties',
-		'display_articles_unfolded' => 'Toon artikelen uitgeklapt als standaard',
-		'display_categories_unfolded' => 'Toon categoriën ingeklapt als standaard',
-		'hide_read_feeds' => 'Verberg categoriën en feeds zonder ongelezen artikelen (werkt niet met “Toon alle artikelen” configuratie)',
+		'display_articles_unfolded' => 'Artikelen standaard uitklappen',
+		'display_categories_unfolded' => 'Categorieën standaard uitklappen',
+		'hide_read_feeds' => 'Categorieën en feeds zonder ongelezen artikelen verbergen (werkt niet met “Toon alle artikelen” configuratie)',
 		'img_with_lazyload' => 'Gebruik "lazy load" methode om afbeeldingen te laden',
 		'img_with_lazyload' => 'Gebruik "lazy load" methode om afbeeldingen te laden',
 		'jump_next' => 'Ga naar volgende ongelezen (feed of categorie)',
 		'jump_next' => 'Ga naar volgende ongelezen (feed of categorie)',
 		'mark_updated_article_unread' => 'Markeer vernieuwd artikel als ongelezen',
 		'mark_updated_article_unread' => 'Markeer vernieuwd artikel als ongelezen',
 		'number_divided_when_reader' => 'Gedeeld door 2 in de lees modus.',
 		'number_divided_when_reader' => 'Gedeeld door 2 in de lees modus.',
 		'read' => array(
 		'read' => array(
-			'article_open_on_website' => 'Als het artikel is geopend op de originele website',
-			'article_viewed' => 'Als het artikel is bekeken',
-			'scroll' => 'Tijdens scrollen',
-			'upon_reception' => 'Tijdens ontvangst van het artikel',
+			'article_open_on_website' => 'als het artikel wordt geopend op de originele website',
+			'article_viewed' => 'als het artikel wordt bekeken',
+			'scroll' => 'tijdens het scrollen',
+			'upon_reception' => 'bij ontvangst van het artikel',
 			'when' => 'Markeer artikel als gelezen…',
 			'when' => 'Markeer artikel als gelezen…',
 		),
 		),
 		'show' => array(
 		'show' => array(
@@ -145,8 +145,8 @@ return array(
 		'wallabag' => 'wallabag',
 		'wallabag' => 'wallabag',
 	),
 	),
 	'shortcut' => array(
 	'shortcut' => array(
-		'_' => 'Shortcuts',
-		'article_action' => 'Artikel acties',
+		'_' => 'Snelkoppelingen',
+		'article_action' => 'Artikelacties',
 		'auto_share' => 'Delen',
 		'auto_share' => 'Delen',
 		'auto_share_help' => 'Als er slechts één deelmethode is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',
 		'auto_share_help' => 'Als er slechts één deelmethode is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',
 		'close_dropdown' => 'Sluit menu',
 		'close_dropdown' => 'Sluit menu',
@@ -161,8 +161,8 @@ return array(
 		'mark_favorite' => 'Markeer als favoriet',
 		'mark_favorite' => 'Markeer als favoriet',
 		'mark_read' => 'Markeer als gelezen',
 		'mark_read' => 'Markeer als gelezen',
 		'navigation' => 'Navigatie',
 		'navigation' => 'Navigatie',
-		'navigation_help' => 'Met de "Shift" toets, kunt u navigatie verwijzingen voor feeds gebruiken.<br/>Met de "Alt" toets, kunt u navigatie verwijzingen voor categoriën gebruiken.',
-		'navigation_no_mod_help' => 'De volgende navigatiesnelkoppelingen ondersteunen geen besturingstoetsen.',
+		'navigation_help' => 'Met de "Shift" toets worden navigatieverwijzingen op feeds toegepast.<br/>Met de "Alt" toets worden navigatieverwijzingen op categorieën toegepast.',
+		'navigation_no_mod_help' => 'De volgende navigatiesnelkoppelingen ondersteunen geen toetsencombinaties.',
 		'next_article' => 'Spring naar volgende artikel',
 		'next_article' => 'Spring naar volgende artikel',
 		'normal_view' => 'Schakel naar gewoon aanzicht',
 		'normal_view' => 'Schakel naar gewoon aanzicht',
 		'other_action' => 'Andere acties',
 		'other_action' => 'Andere acties',

+ 8 - 8
app/i18n/nl/feedback.php

@@ -70,15 +70,15 @@ return array(
 			'no_name' => 'Categorie naam mag niet leeg zijn.',
 			'no_name' => 'Categorie naam mag niet leeg zijn.',
 			'not_delete_default' => 'U kunt de standaard categorie niet verwijderen!',
 			'not_delete_default' => 'U kunt de standaard categorie niet verwijderen!',
 			'not_exist' => 'De categorie bestaat niet!',
 			'not_exist' => 'De categorie bestaat niet!',
-			'over_max' => 'U hebt het maximale aantal categoriën bereikt (%d)',
-			'updated' => 'Categorie is vernieuwd.',
+			'over_max' => 'Maximum aantal categorieën bereikt (%d)',
+			'updated' => 'Categorie vernieuwd.',
 		),
 		),
 		'feed' => array(
 		'feed' => array(
-			'actualized' => '<em>%s</em> is vernieuwd',
-			'actualizeds' => 'RSS feeds zijn vernieuwd',
-			'added' => 'RSS feed <em>%s</em> is toegevoegd',
-			'already_subscribed' => 'U bent al geabonneerd op <em>%s</em>',
-			'deleted' => 'Feed is verwijderd',
+			'actualized' => '<em>%s</em> vernieuwd',
+			'actualizeds' => 'RSS feeds vernieuwd',
+			'added' => 'RSS feed <em>%s</em> toegevoegd',
+			'already_subscribed' => 'Al geabonneerd op <em>%s</em>',
+			'deleted' => 'Feed verwijderd',
 			'error' => 'Feed kan niet worden vernieuwd',
 			'error' => 'Feed kan niet worden vernieuwd',
 			'internal_problem' => 'De feed kon niet worden toegevoegd. <a href="%s">Controleer de FreshRSS-logbestanden</a> voor details. Toevoegen forceren kan worden geprobeerd door <code>#force_feed</code> aan de URL toe te voegen.',
 			'internal_problem' => 'De feed kon niet worden toegevoegd. <a href="%s">Controleer de FreshRSS-logbestanden</a> voor details. Toevoegen forceren kan worden geprobeerd door <code>#force_feed</code> aan de URL toe te voegen.',
 			'invalid_url' => 'URL <em>%s</em> is ongeldig',
 			'invalid_url' => 'URL <em>%s</em> is ongeldig',
@@ -86,7 +86,7 @@ return array(
 			'n_entries_deleted' => '%d artikelen zijn verwijderd',
 			'n_entries_deleted' => '%d artikelen zijn verwijderd',
 			'no_refresh' => 'Er is geen feed om te vernieuwen…',
 			'no_refresh' => 'Er is geen feed om te vernieuwen…',
 			'not_added' => '<em>%s</em> kon niet worden toegevoegd',
 			'not_added' => '<em>%s</em> kon niet worden toegevoegd',
-			'over_max' => 'U hebt het maximale aantal feeds bereikt(%d)',
+			'over_max' => 'Maximum aantal feeds bereikt (%d)',
 			'updated' => 'Feed is vernieuwd',
 			'updated' => 'Feed is vernieuwd',
 		),
 		),
 		'purge_completed' => 'Opschonen klaar (%d artikelen verwijderd)',
 		'purge_completed' => 'Opschonen klaar (%d artikelen verwijderd)',

+ 1 - 1
app/i18n/oc/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Rescondre los articles aprèp lectura',
 		'auto_remove_article' => 'Rescondre los articles aprèp lectura',
 		'confirm_enabled' => 'Mostrar una confirmacion per las accions del tipe « o marcar tot coma legit »',
 		'confirm_enabled' => 'Mostrar una confirmacion per las accions del tipe « o marcar tot coma legit »',
 		'display_articles_unfolded' => 'Mostrar los articles desplegats per defaut',
 		'display_articles_unfolded' => 'Mostrar los articles desplegats per defaut',
-		'display_categories_unfolded' => 'Mostrar las categorias plegadas per defaut',
+		'display_categories_unfolded' => 'Mostrar las categorias desplegats per defaut',
 		'hide_read_feeds' => 'Rescondre las categorias & fluxes sens articles pas legits (fonciona pas amb la configuracion « Mostrar totes los articles »)',
 		'hide_read_feeds' => 'Rescondre las categorias & fluxes sens articles pas legits (fonciona pas amb la configuracion « Mostrar totes los articles »)',
 		'img_with_lazyload' => 'Utilizar lo mòde “cargament tardiu” pels imatges',
 		'img_with_lazyload' => 'Utilizar lo mòde “cargament tardiu” pels imatges',
 		'jump_next' => 'sautar al vesin venent pas legit (flux o categoria)',
 		'jump_next' => 'sautar al vesin venent pas legit (flux o categoria)',

+ 1 - 1
app/i18n/pt-br/conf.php

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Esconder artigos depois de lidos',
 		'auto_remove_article' => 'Esconder artigos depois de lidos',
 		'confirm_enabled' => 'Exibir uma caixa de diálogo de confirmação quando acionar "marcar todos como lido"',
 		'confirm_enabled' => 'Exibir uma caixa de diálogo de confirmação quando acionar "marcar todos como lido"',
 		'display_articles_unfolded' => 'Mostrar aritogs abertos por padrão',
 		'display_articles_unfolded' => 'Mostrar aritogs abertos por padrão',
-		'display_categories_unfolded' => 'Mostrar artigos fechados por padrão',
+		'display_categories_unfolded' => 'Mostrar artigos abertos por padrão',
 		'hide_read_feeds' => 'Esconder categorias e feeds com nenhum artigo não lido (não funciona com a configuração "Mostrar todos os artigos”)',
 		'hide_read_feeds' => 'Esconder categorias e feeds com nenhum artigo não lido (não funciona com a configuração "Mostrar todos os artigos”)',
 		'img_with_lazyload' => 'Utilizar o modo "lazy load" para carregar as imagens',
 		'img_with_lazyload' => 'Utilizar o modo "lazy load" para carregar as imagens',
 		'jump_next' => 'Vá para o próximo irmão não lido (feed ou categoria)',
 		'jump_next' => 'Vá para o próximo irmão não lido (feed ou categoria)',

+ 2 - 2
app/i18n/zh-cn/conf.php

@@ -91,8 +91,8 @@ return array(
 		'auto_load_more' => '在页面底部载入下一篇文章',
 		'auto_load_more' => '在页面底部载入下一篇文章',
 		'auto_remove_article' => '阅读后隐藏文章',
 		'auto_remove_article' => '阅读后隐藏文章',
 		'confirm_enabled' => '“全部设为已读”时显示确认对话框',
 		'confirm_enabled' => '“全部设为已读”时显示确认对话框',
-		'display_articles_unfolded' => '默认展开文章',
-		'display_categories_unfolded' => '默认展开分类',
+		'display_articles_unfolded' => '默认展开显示文章',
+		'display_categories_unfolded' => '默认展开显示类别',
 		'hide_read_feeds' => '隐藏没有未读文章的分类或 RSS 源 (启用“显示所有文章”时不生效))',
 		'hide_read_feeds' => '隐藏没有未读文章的分类或 RSS 源 (启用“显示所有文章”时不生效))',
 		'img_with_lazyload' => '延迟加载图片',
 		'img_with_lazyload' => '延迟加载图片',
 		'jump_next' => '跳转到下一未读项 (RSS 源或分类)',
 		'jump_next' => '跳转到下一未读项 (RSS 源或分类)',

+ 1 - 1
app/views/helpers/javascript_vars.phtml

@@ -1,6 +1,6 @@
 <?php
 <?php
 $mark = FreshRSS_Context::$user_conf->mark_when;
 $mark = FreshRSS_Context::$user_conf->mark_when;
-$s = FreshRSS_Context::$user_conf->shortcuts;
+$s = validateShortcutList(FreshRSS_Context::$user_conf->shortcuts);
 echo htmlspecialchars(json_encode(array(
 echo htmlspecialchars(json_encode(array(
 	'context' => array(
 	'context' => array(
 		'anonymous' => !FreshRSS_Auth::hasAccess(),
 		'anonymous' => !FreshRSS_Auth::hasAccess(),

+ 1 - 1
cli/i18n/I18nFile.php

@@ -84,7 +84,7 @@ class I18nFile implements I18nFileInterface{
 		foreach ($translation as $compoundKey => $value) {
 		foreach ($translation as $compoundKey => $value) {
 			$keys = explode('.', $compoundKey);
 			$keys = explode('.', $compoundKey);
 			array_shift($keys);
 			array_shift($keys);
-			eval("\$a['" . implode("']['", $keys) . "'] = '" . $value . "';");
+			eval("\$a['" . implode("']['", $keys) . "'] = '" . addcslashes($value, "'") . "';");
 		}
 		}
 
 
 		return $a;
 		return $a;

+ 1 - 1
constants.php

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

+ 1 - 1
lib/SimplePie/SimplePie/Cache/File.php

@@ -101,7 +101,7 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base
 	 */
 	 */
 	public function save($data)
 	public function save($data)
 	{
 	{
-		if (file_exists($this->name) && is_writeable($this->name) || file_exists($this->location) && is_writeable($this->location))
+		if (file_exists($this->name) && is_writable($this->name) || file_exists($this->location) && is_writable($this->location))
 		{
 		{
 			if ($data instanceof SimplePie)
 			if ($data instanceof SimplePie)
 			{
 			{

+ 39 - 0
lib/lib_rss.php

@@ -280,6 +280,9 @@ function customSimplePie($attributes = array()) {
 }
 }
 
 
 function sanitizeHTML($data, $base = '') {
 function sanitizeHTML($data, $base = '') {
+	if (!is_string($data)) {
+		return '';
+	}
 	static $simplePie = null;
 	static $simplePie = null;
 	if ($simplePie == null) {
 	if ($simplePie == null) {
 		$simplePie = customSimplePie();
 		$simplePie = customSimplePie();
@@ -544,3 +547,39 @@ function base64url_decode($data) {
 function _i($icon, $url_only = false) {
 function _i($icon, $url_only = false) {
 	return FreshRSS_Themes::icon($icon, $url_only);
 	return FreshRSS_Themes::icon($icon, $url_only);
 }
 }
+
+
+const SHORTCUT_KEYS = array(
+			'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+			'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
+			'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
+			'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
+			'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'Backspace', 'Delete',
+			'End', 'Enter', 'Escape', 'Home', 'Insert', 'PageDown', 'PageUp', 'Space', 'Tab',
+		);
+
+function validateShortcutList($shortcuts) {
+	$legacy = array(
+			'down' => 'ArrowDown', 'left' => 'ArrowLeft', 'page_down' => 'PageDown', 'page_up' => 'PageUp',
+			'right' => 'ArrowRight', 'up' => 'ArrowUp',
+		);
+	$upper = null;
+	$shortcuts_ok = array();
+
+	foreach ($shortcuts as $key => $value) {
+		if (in_array($value, SHORTCUT_KEYS)) {
+			$shortcuts_ok[$key] = $value;
+		} elseif (isset($legacy[$value])) {
+			$shortcuts_ok[$key] = $legacy[$value];
+		} else {	//Case-insensitive search
+			if ($upper === null) {
+				$upper = array_map('strtoupper', SHORTCUT_KEYS);
+			}
+			$i = array_search(strtoupper($value), $upper);
+			if ($i !== false) {
+				$shortcuts_ok[$key] = SHORTCUT_KEYS[$i];
+			}
+		}
+	}
+	return $shortcuts_ok;
+}

+ 5 - 0
p/ext.php

@@ -20,6 +20,11 @@ require(__DIR__ . '/../constants.php');
 function is_valid_path($path) {
 function is_valid_path($path) {
 	// It must be under the extension path.
 	// It must be under the extension path.
 	$real_ext_path = realpath(EXTENSIONS_PATH);
 	$real_ext_path = realpath(EXTENSIONS_PATH);
+
+	//Windows compatibility
+	$real_ext_path = str_replace('\\', '/', $real_ext_path);
+	$path = str_replace('\\', '/', $path);
+
 	$in_ext_path = (substr($path, 0, strlen($real_ext_path)) === $real_ext_path);
 	$in_ext_path = (substr($path, 0, strlen($real_ext_path)) === $real_ext_path);
 	if (!$in_ext_path) {
 	if (!$in_ext_path) {
 		return false;
 		return false;

+ 7 - 7
p/scripts/install.js

@@ -1,15 +1,15 @@
 "use strict";
 "use strict";
 /* jshint globalstrict: true */
 /* jshint globalstrict: true */
 
 
-function show_password() {
-	var button = this;
+function show_password(ev) {
+	var button = ev.target;
 	var passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	var passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	passwordField.setAttribute('type', 'text');
 	passwordField.setAttribute('type', 'text');
 	button.className += ' active';
 	button.className += ' active';
 	return false;
 	return false;
 }
 }
-function hide_password() {
-	var button = this;
+function hide_password(ev) {
+	var button = ev.target;
 	var passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	var passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	passwordField.setAttribute('type', 'password');
 	passwordField.setAttribute('type', 'password');
 	button.className = button.className.replace(/(?:^|\s)active(?!\S)/g , '');
 	button.className = button.className.replace(/(?:^|\s)active(?!\S)/g , '');
@@ -61,10 +61,10 @@ if (bd_type) {
 	bd_type.addEventListener('change', mySqlShowHide);
 	bd_type.addEventListener('change', mySqlShowHide);
 }
 }
 
 
-function ask_confirmation(e) {
-	var str_confirmation = this.getAttribute('data-str-confirm');
+function ask_confirmation(ev) {
+	var str_confirmation = ev.target.getAttribute('data-str-confirm');
 	if (!confirm(str_confirmation)) {
 	if (!confirm(str_confirmation)) {
-		e.preventDefault();
+		ev.preventDefault();
 	}
 	}
 }
 }
 var confirms = document.getElementsByClassName('confirm');
 var confirms = document.getElementsByClassName('confirm');

+ 121 - 87
p/scripts/main.js

@@ -2,6 +2,7 @@
 /* jshint esversion:6, strict:global */
 /* jshint esversion:6, strict:global */
 
 
 //<Polyfills>
 //<Polyfills>
+if (!document.scrollingElement) document.scrollingElement = document.documentElement;
 if (!NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;
 if (!NodeList.prototype.forEach) NodeList.prototype.forEach = Array.prototype.forEach;
 if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector;
 if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.webkitMatchesSelector;
 if (!Element.prototype.closest) Element.prototype.closest = function (s) {
 if (!Element.prototype.closest) Element.prototype.closest = function (s) {
@@ -44,18 +45,20 @@ var context;
 }());
 }());
 //</Global context>
 //</Global context>
 
 
-function badAjax() {
+function badAjax(reload) {
 	openNotification(context.i18n.notif_request_failed, 'bad');
 	openNotification(context.i18n.notif_request_failed, 'bad');
-	location.reload();
+	if (reload) {
+		setTimeout(function () { location.reload(); }, 2000);
+	}
 	return true;
 	return true;
 }
 }
 
 
 function needsScroll(elem) {
 function needsScroll(elem) {
-	const winBottom = document.documentElement.scrollTop + document.documentElement.clientHeight,
+	const winBottom = document.scrollingElement.scrollTop + document.scrollingElement.clientHeight,
 		elemTop = elem.offsetParent.offsetTop + elem.offsetTop,
 		elemTop = elem.offsetParent.offsetTop + elem.offsetTop,
 		elemBottom = elemTop + elem.offsetHeight;
 		elemBottom = elemTop + elem.offsetHeight;
-	return (elemTop < document.documentElement.scrollTop || elemBottom > winBottom) ?
-		elemTop - (document.documentElement.clientHeight / 2) : 0;
+	return (elemTop < document.scrollingElement.scrollTop || elemBottom > winBottom) ?
+		elemTop - (document.scrollingElement.clientHeight / 2) : 0;
 }
 }
 
 
 function str2int(str) {
 function str2int(str) {
@@ -159,6 +162,29 @@ function incUnreadsTag(tag_id, nb) {
 	}
 	}
 }
 }
 
 
+function removeArticle(div) {
+	if (!div || div.classList.contains('not_read') || (context.auto_mark_article && div.classList.contains('active'))) {
+		return;
+	}
+	let scrollTop = box_to_follow.scrollTop;
+	let dirty = false;
+	const p = div.previousElementSibling,
+		n = div.nextElementSibling;
+	if (p && p.classList.contains('day') && n && n.classList.contains('day')) {
+		scrollTop -= p.offsetHeight;
+		dirty = true;
+		p.remove();
+	}
+	if (div.offsetHeight > 0 && div.offsetParent.offsetTop + div.offsetTop + div.offsetHeight < scrollTop) {
+		scrollTop -= div.offsetHeight;
+		dirty = true;
+	}
+	div.remove();
+	if (dirty) {
+		box_to_follow.scrollTop = scrollTop;
+	}
+}
+
 var pending_entries = {},
 var pending_entries = {},
 	mark_read_queue = [];
 	mark_read_queue = [];
 
 
@@ -167,19 +193,19 @@ function send_mark_read_queue(queue, asRead, callback) {
 	req.open('POST', '.?c=entry&a=read' + (asRead ? '' : '&is_read=0'), true);
 	req.open('POST', '.?c=entry&a=read' + (asRead ? '' : '&is_read=0'), true);
 	req.responseType = 'json';
 	req.responseType = 'json';
 	req.onerror = function (e) {
 	req.onerror = function (e) {
-			openNotification(context.i18n.notif_request_failed, 'bad');
 			for (let i = queue.length - 1; i >= 0; i--) {
 			for (let i = queue.length - 1; i >= 0; i--) {
 				delete pending_entries['flux_' + queue[i]];
 				delete pending_entries['flux_' + queue[i]];
 			}
 			}
-			if (this.status == 403) {
-				badAjax();
-			}
+			badAjax(this.status == 403);
 		};
 		};
 	req.onload = function (e) {
 	req.onload = function (e) {
 			if (this.status != 200) {
 			if (this.status != 200) {
 				return req.onerror(e);
 				return req.onerror(e);
 			}
 			}
 			const json = xmlHttpRequestJson(this);
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return req.onerror(e);
+			}
 			for (let i = queue.length - 1; i >= 0; i--) {
 			for (let i = queue.length - 1; i >= 0; i--) {
 				const div = document.getElementById('flux_' + queue[i]),
 				const div = document.getElementById('flux_' + queue[i]),
 					myIcons = context.icons;
 					myIcons = context.icons;
@@ -191,6 +217,9 @@ function send_mark_read_queue(queue, asRead, callback) {
 						});
 						});
 					div.querySelectorAll('a.read > .icon').forEach(function (img) { img.outerHTML = myIcons.read; });
 					div.querySelectorAll('a.read > .icon').forEach(function (img) { img.outerHTML = myIcons.read; });
 					inc--;
 					inc--;
+					if (context.auto_remove_article) {
+						removeArticle(div);
+					}
 				} else {
 				} else {
 					div.classList.add('not_read');
 					div.classList.add('not_read');
 					div.classList.add('keep_unread');	//Split for IE11
 					div.classList.add('keep_unread');	//Split for IE11
@@ -237,14 +266,15 @@ function send_mark_queue_tick(callback) {
 	mark_read_queue = [];
 	mark_read_queue = [];
 	send_mark_read_queue(queue, true, callback);
 	send_mark_read_queue(queue, true, callback);
 }
 }
+var delayedFunction = send_mark_queue_tick;
 
 
 function delayedClick(a) {
 function delayedClick(a) {
 	if (a) {
 	if (a) {
-		send_mark_queue_tick(function () { a.click(); });
+		delayedFunction(function () { a.click(); });
 	}
 	}
 }
 }
 
 
-function mark_read(div, only_not_read) {
+function mark_read(div, only_not_read, asBatch) {
 	if (!div || !div.id || context.anonymous ||
 	if (!div || !div.id || context.anonymous ||
 		(only_not_read && !div.classList.contains('not_read'))) {
 		(only_not_read && !div.classList.contains('not_read'))) {
 		return false;
 		return false;
@@ -256,7 +286,7 @@ function mark_read(div, only_not_read) {
 
 
 	const asRead = div.classList.contains('not_read'),
 	const asRead = div.classList.contains('not_read'),
 		entryId = div.id.replace(/^flux_/, '');
 		entryId = div.id.replace(/^flux_/, '');
-	if (asRead) {
+	if (asRead && asBatch) {
 		mark_read_queue.push(entryId);
 		mark_read_queue.push(entryId);
 		if (send_mark_read_queue_timeout == 0) {
 		if (send_mark_read_queue_timeout == 0) {
 			send_mark_read_queue_timeout = setTimeout(function () { send_mark_queue_tick(null); }, 1000);
 			send_mark_read_queue_timeout = setTimeout(function () { send_mark_queue_tick(null); }, 1000);
@@ -287,17 +317,17 @@ function mark_favorite(div) {
 	req.open('POST', url, true);
 	req.open('POST', url, true);
 	req.responseType = 'json';
 	req.responseType = 'json';
 	req.onerror = function (e) {
 	req.onerror = function (e) {
-			openNotification(context.i18n.notif_request_failed, 'bad');
 			delete pending_entries[div.id];
 			delete pending_entries[div.id];
-			if (this.status == 403) {
-				badAjax();
-			}
+			badAjax(this.status == 403);
 		};
 		};
 	req.onload = function (e) {
 	req.onload = function (e) {
 			if (this.status != 200) {
 			if (this.status != 200) {
 				return req.onerror(e);
 				return req.onerror(e);
 			}
 			}
 			const json = xmlHttpRequestJson(this);
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return req.onerror(e);
+			}
 			let inc = 0;
 			let inc = 0;
 			if (div.classList.contains('favorite')) {
 			if (div.classList.contains('favorite')) {
 				div.classList.remove('favorite');
 				div.classList.remove('favorite');
@@ -357,21 +387,23 @@ function toggleContent(new_active, old_active, skipping) {
 		if (old_active) {
 		if (old_active) {
 			old_active.classList.remove('active');
 			old_active.classList.remove('active');
 			old_active.classList.remove('current');	//Split for IE11
 			old_active.classList.remove('current');	//Split for IE11
+			if (context.auto_remove_article) {
+				removeArticle(old_active);
+			}
 		}
 		}
 	} else {
 	} else {
 		new_active.classList.toggle('active');
 		new_active.classList.toggle('active');
 	}
 	}
 
 
 	const relative_move = context.current_view === 'global',
 	const relative_move = context.current_view === 'global',
-		box_to_move = relative_move ? document.getElementById('panel') : document.documentElement;
+		box_to_move = relative_move ? document.getElementById('panel') : document.scrollingElement;
 
 
-	if (context.sticky_post) {
+	if (context.sticky_post) {	//Stick the article to the top when opened
 		let prev_article = new_active.previousElementSibling,
 		let prev_article = new_active.previousElementSibling,
-			new_pos = new_active.offsetTop + document.documentElement.scrollTop,
-			old_scroll = box_to_move.scrollTop;
+			new_pos = new_active.offsetParent.offsetTop + new_active.offsetTop;
 
 
 		if (prev_article && new_active.offsetTop - prev_article.offsetTop <= 150) {
 		if (prev_article && new_active.offsetTop - prev_article.offsetTop <= 150) {
-			new_pos = prev_article.offsetTop;
+			new_pos = prev_article.offsetParent.offsetTop + prev_article.offsetTop;
 			if (relative_move) {
 			if (relative_move) {
 				new_pos -= box_to_move.offsetTop;
 				new_pos -= box_to_move.offsetTop;
 			}
 			}
@@ -382,14 +414,14 @@ function toggleContent(new_active, old_active, skipping) {
 			new_pos -= document.body.clientHeight / 4;
 			new_pos -= document.body.clientHeight / 4;
 		}
 		}
 		if (relative_move) {
 		if (relative_move) {
-			new_pos += old_scroll;
+			new_pos += box_to_move.scrollTop;
 		}
 		}
 		box_to_move.scrollTop = new_pos;
 		box_to_move.scrollTop = new_pos;
 	}
 	}
 
 
 	if (new_active.classList.contains('active') && !skipping) {
 	if (new_active.classList.contains('active') && !skipping) {
 		if (context.auto_mark_article) {
 		if (context.auto_mark_article) {
-			mark_read(new_active, true);
+			mark_read(new_active, true, true);
 		}
 		}
 		new_active.dispatchEvent(freshrssOpenArticleEvent);
 		new_active.dispatchEvent(freshrssOpenArticleEvent);
 	}
 	}
@@ -529,7 +561,7 @@ function user_filter(key) {
 		// Force scrolling to the filter div
 		// Force scrolling to the filter div
 		const scroll = needsScroll(document.querySelector('.header'));
 		const scroll = needsScroll(document.querySelector('.header'));
 		if (scroll !== 0) {
 		if (scroll !== 0) {
-			document.documentElement.scrollTop = scroll;
+			document.scrollingElement.scrollTop = scroll;
 		}
 		}
 		// Force the key value if there is only one action, so we can trigger it automatically
 		// Force the key value if there is only one action, so we can trigger it automatically
 		if (filters.length === 1) {
 		if (filters.length === 1) {
@@ -557,7 +589,7 @@ function auto_share(key) {
 		// Force scrolling to the share div
 		// Force scrolling to the share div
 		const scrollTop = needsScroll(share.closest('.bottom'));
 		const scrollTop = needsScroll(share.closest('.bottom'));
 		if (scrollTop !== 0) {
 		if (scrollTop !== 0) {
-			document.documentElement.scrollTop = scrollTop;
+			document.scrollingElement.scrollTop = scrollTop;
 		}
 		}
 		// Force the key value if there is only one action, so we can trigger it automatically
 		// Force the key value if there is only one action, so we can trigger it automatically
 		if (shares.length === 1) {
 		if (shares.length === 1) {
@@ -585,30 +617,9 @@ function onScroll() {
 		document.querySelectorAll('.not_read:not(.keep_unread)').forEach(function (div) {
 		document.querySelectorAll('.not_read:not(.keep_unread)').forEach(function (div) {
 				if (div.offsetHeight > 0 &&
 				if (div.offsetHeight > 0 &&
 					div.offsetParent.offsetTop + div.offsetTop + div.offsetHeight < minTop) {
 					div.offsetParent.offsetTop + div.offsetTop + div.offsetHeight < minTop) {
-					mark_read(div, true);
-				}
-			});
-	}
-	if (context.auto_remove_article) {
-		let maxTop = box_to_follow.scrollTop,
-			scrollOffset = 0;
-		document.querySelectorAll('.flux:not(.active):not(.keep_unread)').forEach(function (div) {
-				if (!pending_entries[div.id] && div.offsetHeight > 0 &&
-					div.offsetParent.offsetTop + div.offsetTop + div.offsetHeight < maxTop) {
-					const p = div.previousElementSibling,
-						n = div.nextElementSibling;
-					if (p && p.classList.contains('day') && n && n.classList.contains('day')) {
-						p.remove();
-					}
-					maxTop -= div.offsetHeight;
-					scrollOffset -= div.offsetHeight;
-					div.remove();
+					mark_read(div, true, true);
 				}
 				}
 			});
 			});
-		if (scrollOffset != 0) {
-			box_to_follow.scrollTop += scrollOffset;
-			return;	//onscroll will be called again
-		}
 	}
 	}
 	if (context.auto_load_more) {
 	if (context.auto_load_more) {
 		const pagination = document.getElementById('mark-read-pagination');
 		const pagination = document.getElementById('mark-read-pagination');
@@ -621,10 +632,10 @@ function onScroll() {
 
 
 function init_posts() {
 function init_posts() {
 	if (context.auto_load_more || context.auto_mark_scroll || context.auto_remove_article) {
 	if (context.auto_load_more || context.auto_mark_scroll || context.auto_remove_article) {
-		box_to_follow = context.current_view === 'global' ? document.getElementById('panel') : document.documentElement;
+		box_to_follow = context.current_view === 'global' ? document.getElementById('panel') : document.scrollingElement;
 		let lastScroll = 0,	//Throttle
 		let lastScroll = 0,	//Throttle
 			timerId = 0;
 			timerId = 0;
-		(box_to_follow === document.documentElement ? window : box_to_follow).onscroll = function () {
+		(box_to_follow === document.scrollingElement ? window : box_to_follow).onscroll = function () {
 			clearTimeout(timerId);
 			clearTimeout(timerId);
 			if (lastScroll + 500 < Date.now()) {
 			if (lastScroll + 500 < Date.now()) {
 				lastScroll = Date.now();
 				lastScroll = Date.now();
@@ -681,7 +692,10 @@ function init_column_categories() {
 				a.href = '#dropdown-' + id;
 				a.href = '#dropdown-' + id;
 				div.querySelector('.dropdown-target').id = 'dropdown-' + id;
 				div.querySelector('.dropdown-target').id = 'dropdown-' + id;
 				div.insertAdjacentHTML('beforeend', template);
 				div.insertAdjacentHTML('beforeend', template);
-				div.querySelector('button.confirm').disabled = false;
+				const b = div.querySelector('button.confirm');
+				if (b) {
+					b.disabled = false;
+				}
 			} else if (getComputedStyle(dropdownMenu).display === 'none') {
 			} else if (getComputedStyle(dropdownMenu).display === 'none') {
 				const id2 = div.closest('.item').id.substr(2);
 				const id2 = div.closest('.item').id.substr(2);
 				a.href = '#dropdown-' + id2;
 				a.href = '#dropdown-' + id2;
@@ -745,7 +759,7 @@ function init_shortcuts() {
 				} else if (ev.shiftKey) {	// Mark everything as read
 				} else if (ev.shiftKey) {	// Mark everything as read
 					document.querySelector('.nav_menu .read_all').click();
 					document.querySelector('.nav_menu .read_all').click();
 				} else {	// Toggle the read state
 				} else {	// Toggle the read state
-					mark_read(document.querySelector('.flux.current'), false);
+					mark_read(document.querySelector('.flux.current'), false, false);
 				}
 				}
 				return false;
 				return false;
 			}
 			}
@@ -787,7 +801,7 @@ function init_shortcuts() {
 			}
 			}
 			if (k === s.go_website) {
 			if (k === s.go_website) {
 				if (context.auto_mark_site) {
 				if (context.auto_mark_site) {
-					mark_read(document.querySelector('.flux.current'), true);
+					mark_read(document.querySelector('.flux.current'), true, false);
 				}
 				}
 				window.open(document.querySelector('.flux.current a.go_website').href);
 				window.open(document.querySelector('.flux.current a.go_website').href);
 				return false;
 				return false;
@@ -813,7 +827,7 @@ function init_stream(stream) {
 	stream.onclick = function (ev) {
 	stream.onclick = function (ev) {
 		let el = ev.target.closest('.flux a.read');
 		let el = ev.target.closest('.flux a.read');
 		if (el) {
 		if (el) {
-			mark_read(el.closest('.flux'), false);
+			mark_read(el.closest('.flux'), false, false);
 			return false;
 			return false;
 		}
 		}
 
 
@@ -882,7 +896,7 @@ function init_stream(stream) {
 				new_active = el.parentNode;
 				new_active = el.parentNode;
 			if (ev.target.tagName.toUpperCase() === 'A') {	//Leave real links alone
 			if (ev.target.tagName.toUpperCase() === 'A') {	//Leave real links alone
 				if (context.auto_mark_article) {
 				if (context.auto_mark_article) {
-					mark_read(new_active, true);
+					mark_read(new_active, true, false);
 				}
 				}
 				return true;
 				return true;
 			}
 			}
@@ -891,21 +905,28 @@ function init_stream(stream) {
 		}
 		}
 	};
 	};
 
 
-	stream.onmouseup = function (ev) {	// Mouseup enables us to catch middle click
+	stream.onmouseup = function (ev) {	// Mouseup enables us to catch middle click, and control+click in IE/Edge
+		if (ev.altKey || ev.metaKey || ev.shiftKey) {
+			return;
+		}
+
 		let el = ev.target.closest('.item.title > a');
 		let el = ev.target.closest('.item.title > a');
 		if (el) {
 		if (el) {
-			if (ev.ctrlKey) {
-				return;	// CTRL+click, it will be manage by previous rule.
-			}
-			if (ev.which == 2) {
-				// If middle click, we want same behaviour as CTRL+click.
-				const evc = document.createEvent('click');
-				evc.ctrlKey = true;
-				el.dispatchEvent(evc);
-			} else if (ev.which == 1) {
-				// Normal click, just toggle article.
-				el.parentElement.click();
+			if (ev.which == 1) {
+				if (ev.ctrlKey) {	//Control+click
+					if (context.auto_mark_site) {
+						mark_read(el.closest('.flux'), true, false);
+					}
+				} else {
+					el.parentElement.click();	//Normal click, just toggle article.
+				}
+			} else if (ev.which == 2 && !ev.ctrlKey) {	//Simple middle click: same behaviour as CTRL+click
+				if (context.auto_mark_article) {
+					const new_active = el.closest('.flux');
+					mark_read(new_active, true, false);
+				}
 			}
 			}
+			return;
 		}
 		}
 
 
 		if (context.auto_mark_site) {
 		if (context.auto_mark_site) {
@@ -916,7 +937,7 @@ function init_stream(stream) {
 				if (ev.which == 3) {
 				if (ev.which == 3) {
 					return;
 					return;
 				}
 				}
-				mark_read(el.closest('.flux'), true);
+				mark_read(el.closest('.flux'), true, false);
 			}
 			}
 		}
 		}
 	};
 	};
@@ -937,9 +958,7 @@ function init_stream(stream) {
 				req.responseType = 'json';
 				req.responseType = 'json';
 				req.onerror = function (e) {
 				req.onerror = function (e) {
 						checkboxTag.checked = !isChecked;
 						checkboxTag.checked = !isChecked;
-						if (this.status == 403) {
-							badAjax();
-						}
+						badAjax(this.status == 403);
 					};
 					};
 				req.onload = function (e) {
 				req.onload = function (e) {
 						if (this.status != 200) {
 						if (this.status != 200) {
@@ -980,10 +999,10 @@ function init_nav_entries() {
 			};
 			};
 		nav_entries.querySelector('.up').onclick = function (e) {
 		nav_entries.querySelector('.up').onclick = function (e) {
 				const active_item = document.querySelector('.flux.current'),
 				const active_item = document.querySelector('.flux.current'),
-					windowTop = document.documentElement.scrollTop,
+					windowTop = document.scrollingElement.scrollTop,
 					item_top = active_item.offsetParent.offsetTop + active_item.offsetTop;
 					item_top = active_item.offsetParent.offsetTop + active_item.offsetTop;
 
 
-				document.documentElement.scrollTop = windowTop > item_top ? item_top : 0;
+				document.scrollingElement.scrollTop = windowTop > item_top ? item_top : 0;
 				return false;
 				return false;
 			};
 			};
 	}
 	}
@@ -1006,6 +1025,9 @@ function loadDynamicTags(div) {
 				return req.onerror(e);
 				return req.onerror(e);
 			}
 			}
 			const json = xmlHttpRequestJson(this);
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return req.onerror(e);
+			}
 			let html = '<li class="item"><label><input class="checkboxTag" name="t_0" type="checkbox" /> <input type="text" name="newTag" /></label></li>';
 			let html = '<li class="item"><label><input class="checkboxTag" name="t_0" type="checkbox" /> <input type="text" name="newTag" /></label></li>';
 			if (json && json.length) {
 			if (json && json.length) {
 				for (let i = 0; i < json.length; i++) {
 				for (let i = 0; i < json.length; i++) {
@@ -1031,7 +1053,7 @@ function updateFeed(feeds, feeds_count) {
 	req.open('POST', feed.url, true);
 	req.open('POST', feed.url, true);
 	req.onloadend = function (e) {
 	req.onloadend = function (e) {
 			if (this.status != 200) {
 			if (this.status != 200) {
-				return badAjax();
+				return badAjax(false);
 			}
 			}
 			feed_processed++;
 			feed_processed++;
 			const div = document.getElementById('actualizeProgress');
 			const div = document.getElementById('actualizeProgress');
@@ -1042,7 +1064,7 @@ function updateFeed(feeds, feeds_count) {
 				const req2 = new XMLHttpRequest();
 				const req2 = new XMLHttpRequest();
 				req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
 				req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
 				req2.onloadend = function (e) {
 				req2.onloadend = function (e) {
-					location.reload();
+					delayedFunction(function () { location.reload(); });
 				};
 				};
 				req2.setRequestHeader('Content-Type', 'application/json');
 				req2.setRequestHeader('Content-Type', 'application/json');
 				req2.send(JSON.stringify({
 				req2.send(JSON.stringify({
@@ -1074,9 +1096,12 @@ function init_actualize() {
 		req.responseType = 'json';
 		req.responseType = 'json';
 		req.onload = function (e) {
 		req.onload = function (e) {
 				if (this.status != 200) {
 				if (this.status != 200) {
-					return badAjax();
+					return badAjax(false);
 				}
 				}
 				const json = xmlHttpRequestJson(this);
 				const json = xmlHttpRequestJson(this);
+				if (!json) {
+					return badAjax(false);
+				}
 				if (auto && json.feeds.length < 1) {
 				if (auto && json.feeds.length < 1) {
 					auto = false;
 					auto = false;
 					context.ajax_loading = false;
 					context.ajax_loading = false;
@@ -1184,10 +1209,12 @@ function notifs_html5_show(nb) {
 	});
 	});
 
 
 	notification.onclick = function () {
 	notification.onclick = function () {
-		location.reload();
-		window.focus();
-		notification.close();
-	};
+			delayedFunction(function() {
+				location.reload();
+				window.focus();
+				notification.close();
+			});
+		};
 
 
 	if (context.html5_notif_timeout !== 0) {
 	if (context.html5_notif_timeout !== 0) {
 		setTimeout(function () {
 		setTimeout(function () {
@@ -1211,6 +1238,9 @@ function refreshUnreads() {
 	req.responseType = 'json';
 	req.responseType = 'json';
 	req.onload = function (e) {
 	req.onload = function (e) {
 			const json = xmlHttpRequestJson(this);
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return badAjax(false);
+			}
 			const isAll = document.querySelector('.category.all.active');
 			const isAll = document.querySelector('.category.all.active');
 			let new_articles = false;
 			let new_articles = false;
 
 
@@ -1286,12 +1316,11 @@ function load_more_posts() {
 				paginationNew = streamAdopted.querySelector('.pagination');
 				paginationNew = streamAdopted.querySelector('.pagination');
 			formPagination.replaceChild(paginationNew, paginationOld);
 			formPagination.replaceChild(paginationNew, paginationOld);
 
 
-			if (context.display_order === 'ASC') {
-				document.querySelector('#nav_menu_read_all .read_all').formAction =
-					document.getElementById('bigMarkAsRead').formAction;
-			} else {
-				const bigMarkAsRead = document.getElementById('bigMarkAsRead');
-				if (bigMarkAsRead) {
+			const bigMarkAsRead = document.getElementById('bigMarkAsRead');
+			if (bigMarkAsRead) {
+				if (context.display_order === 'ASC') {
+					document.querySelector('#nav_menu_read_all .read_all').formAction = bigMarkAsRead.formAction;
+				} else {
 					bigMarkAsRead.formAction = document.querySelector('#nav_menu_read_all .read_all').formAction;
 					bigMarkAsRead.formAction = document.querySelector('#nav_menu_read_all .read_all').formAction;
 				}
 				}
 			}
 			}
@@ -1305,8 +1334,7 @@ function load_more_posts() {
 
 
 			init_load_more(box_load_more);
 			init_load_more(box_load_more);
 
 
-			const bigMarkAsRead = document.getElementById('bigMarkAsRead'),
-				div_load_more = document.getElementById('load_more');
+			const div_load_more = document.getElementById('load_more');
 			if (bigMarkAsRead) {
 			if (bigMarkAsRead) {
 				bigMarkAsRead.removeAttribute('disabled');
 				bigMarkAsRead.removeAttribute('disabled');
 			}
 			}
@@ -1407,6 +1435,12 @@ function init_normal() {
 	init_shortcuts();
 	init_shortcuts();
 	init_actualize();
 	init_actualize();
 	faviconNbUnread();
 	faviconNbUnread();
+
+	window.onbeforeunload = function (e) {
+		if (mark_read_queue && mark_read_queue.length > 0) {
+			return false;
+		}
+	};
 }
 }
 
 
 function init_beforeDOM() {
 function init_beforeDOM() {

+ 4 - 3
phpcs.xml

@@ -2,7 +2,7 @@
 <ruleset name="FreshRSS Ruleset">
 <ruleset name="FreshRSS Ruleset">
 	<description>Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/</description>
 	<description>Created with the PHP Coding Standard Generator. https://edorian.github.com/php-coding-standard-generator/</description>
 	<!-- to circumvent https://github.com/squizlabs/PHP_CodeSniffer/pull/1404 -->
 	<!-- to circumvent https://github.com/squizlabs/PHP_CodeSniffer/pull/1404 -->
-	<!--<arg name="tab-width" value="10"/>-->
+	<arg name="tab-width" value="40"/>
 	<exclude-pattern>./static</exclude-pattern>
 	<exclude-pattern>./static</exclude-pattern>
 	<exclude-pattern>./vendor</exclude-pattern>
 	<exclude-pattern>./vendor</exclude-pattern>
 	<exclude-pattern>./lib/SimplePie/</exclude-pattern>
 	<exclude-pattern>./lib/SimplePie/</exclude-pattern>
@@ -33,8 +33,9 @@
 		<exclude-pattern>./app/SQL/install.sql.mysql.php</exclude-pattern>
 		<exclude-pattern>./app/SQL/install.sql.mysql.php</exclude-pattern>
 		<exclude-pattern>./app/SQL/install.sql.pgsql.php</exclude-pattern>
 		<exclude-pattern>./app/SQL/install.sql.pgsql.php</exclude-pattern>
 		<properties>
 		<properties>
-			<property name="lineLimit" value="80"/>
-			<property name="absoluteLineLimit" value="180"/>
+			<property name="lineLimit" value="100"/>
+			<!-- needs to be large to accomodate extra large tab width to circumvent https://github.com/squizlabs/PHP_CodeSniffer/pull/1404 -->
+			<property name="absoluteLineLimit" value="500"/>
 		</properties>
 		</properties>
 	</rule>
 	</rule>
 	<!-- When calling a function: -->
 	<!-- When calling a function: -->