Browse Source

Merge pull request #2338 from FreshRSS/dev

FreshRSS 1.14.1
Alexandre Alapetite 7 years ago
parent
commit
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
 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:
   # 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:
   - phpenv rehash
@@ -34,6 +35,15 @@ matrix:
       dist: precise
     - php: "7.2"
       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:
     - env: CHECK_TRANSLATION=yes VALIDATE_STANDARD=no
     - dist: precise

+ 28 - 1
CHANGELOG.md

@@ -1,5 +1,32 @@
 # 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
 
 * Features
@@ -31,7 +58,7 @@
 * API
 	* Supported by [Readably](https://play.google.com/store/apps/details?id=com.isaiasmatewos.readably) (client for Android using Fever API)
 * 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)
 * Security
 	* 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 && \
 	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
 
 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 && \
 	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 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 \
 		/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 && \
-	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 && \
 	sed -r -i "/^\s*(CustomLog|ErrorLog|Listen) /s/^/#/" \
 		/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 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.
 See cron option 1 for customising the cron schedule.
 
+#### For the Ubuntu image (default)
 ```sh
 sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
-  -e 'CRON_MIN=17,37' \
+  -e 'CRON_MIN=17,47' \
   --net freshrss-network \
   --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
 ```
 

+ 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#" {} \;
 
 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
 
 exec "$@"

+ 5 - 19
app/Controllers/configureController.php

@@ -166,30 +166,16 @@ class FreshRSS_configure_Controller extends Minz_ActionController {
 	 * tab and up.
 	 */
 	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()) {
-			$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();
 			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') . ' · ');

+ 1 - 1
app/Models/EntryDAO.php

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

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

@@ -92,7 +92,7 @@ return array(
 		'auto_remove_article' => 'Artikel nach dem Lesen verstecken',
 		'confirm_enabled' => 'Bei der Aktion „Alle als gelesen markieren“ einen Bestätigungsdialog anzeigen',
 		'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“)',
 		'img_with_lazyload' => 'Verwende die "träges Laden"-Methode zum Laden von Bildern',
 		'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',
 		'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_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”)',
 		'img_with_lazyload' => 'Utiliser le mode “chargement différé” pour les images',
 		'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
 		'confirm_enabled' => 'הצגת דו-שיח לאישור “סימון הכל כנקרא” ',
 		'display_articles_unfolded' => 'הצגת מאמרים בשלמותם כברירת מחדל',
-		'display_categories_unfolded' => 'הצגת קטגוריות מקופלות כברירת מחדל',
+		'display_categories_unfolded' => 'הצגת קטגוריות בשלמותן כברירת מחדל',
 		'hide_read_feeds' => 'הסתרת קטגוריות &amp; הזנות ללא מאמרים שלא נקראו (לא עובד יחד עם “הצגת כל המאמרים”)',
 		'img_with_lazyload' => 'שימוש ב "טעינה עצלה" על מנת לטעון תמונות',
 		'jump_next' => 'קפיצה לפריט הבא שלא נקרא (הזנה או קטגוריה)',

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

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

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

@@ -160,8 +160,8 @@ return array(
 		'_' => 'Systeem configuratie',
 		'auto-update-url' => 'Automatische update server URL',
 		'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(
 			'help' => 'in seconden',
 			'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_remove_article' => 'Verberg artikel na lezen',
 		'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',
 		'jump_next' => 'Ga naar volgende ongelezen (feed of categorie)',
 		'mark_updated_article_unread' => 'Markeer vernieuwd artikel als ongelezen',
 		'number_divided_when_reader' => 'Gedeeld door 2 in de lees modus.',
 		'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…',
 		),
 		'show' => array(
@@ -145,8 +145,8 @@ return array(
 		'wallabag' => 'wallabag',
 	),
 	'shortcut' => array(
-		'_' => 'Shortcuts',
-		'article_action' => 'Artikel acties',
+		'_' => 'Snelkoppelingen',
+		'article_action' => 'Artikelacties',
 		'auto_share' => 'Delen',
 		'auto_share_help' => 'Als er slechts één deelmethode is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',
 		'close_dropdown' => 'Sluit menu',
@@ -161,8 +161,8 @@ return array(
 		'mark_favorite' => 'Markeer als favoriet',
 		'mark_read' => 'Markeer als gelezen',
 		'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',
 		'normal_view' => 'Schakel naar gewoon aanzicht',
 		'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.',
 			'not_delete_default' => 'U kunt de standaard categorie niet verwijderen!',
 			'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(
-			'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',
 			'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',
@@ -86,7 +86,7 @@ return array(
 			'n_entries_deleted' => '%d artikelen zijn verwijderd',
 			'no_refresh' => 'Er is geen feed om te vernieuwen…',
 			'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',
 		),
 		'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',
 		'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_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 »)',
 		'img_with_lazyload' => 'Utilizar lo mòde “cargament tardiu” pels imatges',
 		'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',
 		'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_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”)',
 		'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)',

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

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

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

@@ -1,6 +1,6 @@
 <?php
 $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(
 	'context' => array(
 		'anonymous' => !FreshRSS_Auth::hasAccess(),

+ 1 - 1
cli/i18n/I18nFile.php

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

+ 1 - 1
constants.php

@@ -2,7 +2,7 @@
 //NB: Do not edit; use ./constants.local.php instead.
 
 //<Not customisable>
-define('FRESHRSS_VERSION', '1.14.0');
+define('FRESHRSS_VERSION', '1.14.1');
 define('FRESHRSS_WEBSITE', 'https://freshrss.org');
 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)
 	{
-		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)
 			{

+ 39 - 0
lib/lib_rss.php

@@ -280,6 +280,9 @@ function customSimplePie($attributes = array()) {
 }
 
 function sanitizeHTML($data, $base = '') {
+	if (!is_string($data)) {
+		return '';
+	}
 	static $simplePie = null;
 	if ($simplePie == null) {
 		$simplePie = customSimplePie();
@@ -544,3 +547,39 @@ function base64url_decode($data) {
 function _i($icon, $url_only = false) {
 	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) {
 	// It must be under the extension 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);
 	if (!$in_ext_path) {
 		return false;

+ 7 - 7
p/scripts/install.js

@@ -1,15 +1,15 @@
 "use strict";
 /* 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'));
 	passwordField.setAttribute('type', 'text');
 	button.className += ' active';
 	return false;
 }
-function hide_password() {
-	var button = this;
+function hide_password(ev) {
+	var button = ev.target;
 	var passwordField = document.getElementById(button.getAttribute('data-toggle'));
 	passwordField.setAttribute('type', 'password');
 	button.className = button.className.replace(/(?:^|\s)active(?!\S)/g , '');
@@ -61,10 +61,10 @@ if (bd_type) {
 	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)) {
-		e.preventDefault();
+		ev.preventDefault();
 	}
 }
 var confirms = document.getElementsByClassName('confirm');

+ 121 - 87
p/scripts/main.js

@@ -2,6 +2,7 @@
 /* jshint esversion:6, strict:global */
 
 //<Polyfills>
+if (!document.scrollingElement) document.scrollingElement = document.documentElement;
 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.closest) Element.prototype.closest = function (s) {
@@ -44,18 +45,20 @@ var context;
 }());
 //</Global context>
 
-function badAjax() {
+function badAjax(reload) {
 	openNotification(context.i18n.notif_request_failed, 'bad');
-	location.reload();
+	if (reload) {
+		setTimeout(function () { location.reload(); }, 2000);
+	}
 	return true;
 }
 
 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,
 		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) {
@@ -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 = {},
 	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.responseType = 'json';
 	req.onerror = function (e) {
-			openNotification(context.i18n.notif_request_failed, 'bad');
 			for (let i = queue.length - 1; i >= 0; i--) {
 				delete pending_entries['flux_' + queue[i]];
 			}
-			if (this.status == 403) {
-				badAjax();
-			}
+			badAjax(this.status == 403);
 		};
 	req.onload = function (e) {
 			if (this.status != 200) {
 				return req.onerror(e);
 			}
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return req.onerror(e);
+			}
 			for (let i = queue.length - 1; i >= 0; i--) {
 				const div = document.getElementById('flux_' + queue[i]),
 					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; });
 					inc--;
+					if (context.auto_remove_article) {
+						removeArticle(div);
+					}
 				} else {
 					div.classList.add('not_read');
 					div.classList.add('keep_unread');	//Split for IE11
@@ -237,14 +266,15 @@ function send_mark_queue_tick(callback) {
 	mark_read_queue = [];
 	send_mark_read_queue(queue, true, callback);
 }
+var delayedFunction = send_mark_queue_tick;
 
 function delayedClick(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 ||
 		(only_not_read && !div.classList.contains('not_read'))) {
 		return false;
@@ -256,7 +286,7 @@ function mark_read(div, only_not_read) {
 
 	const asRead = div.classList.contains('not_read'),
 		entryId = div.id.replace(/^flux_/, '');
-	if (asRead) {
+	if (asRead && asBatch) {
 		mark_read_queue.push(entryId);
 		if (send_mark_read_queue_timeout == 0) {
 			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.responseType = 'json';
 	req.onerror = function (e) {
-			openNotification(context.i18n.notif_request_failed, 'bad');
 			delete pending_entries[div.id];
-			if (this.status == 403) {
-				badAjax();
-			}
+			badAjax(this.status == 403);
 		};
 	req.onload = function (e) {
 			if (this.status != 200) {
 				return req.onerror(e);
 			}
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return req.onerror(e);
+			}
 			let inc = 0;
 			if (div.classList.contains('favorite')) {
 				div.classList.remove('favorite');
@@ -357,21 +387,23 @@ function toggleContent(new_active, old_active, skipping) {
 		if (old_active) {
 			old_active.classList.remove('active');
 			old_active.classList.remove('current');	//Split for IE11
+			if (context.auto_remove_article) {
+				removeArticle(old_active);
+			}
 		}
 	} else {
 		new_active.classList.toggle('active');
 	}
 
 	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,
-			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) {
-			new_pos = prev_article.offsetTop;
+			new_pos = prev_article.offsetParent.offsetTop + prev_article.offsetTop;
 			if (relative_move) {
 				new_pos -= box_to_move.offsetTop;
 			}
@@ -382,14 +414,14 @@ function toggleContent(new_active, old_active, skipping) {
 			new_pos -= document.body.clientHeight / 4;
 		}
 		if (relative_move) {
-			new_pos += old_scroll;
+			new_pos += box_to_move.scrollTop;
 		}
 		box_to_move.scrollTop = new_pos;
 	}
 
 	if (new_active.classList.contains('active') && !skipping) {
 		if (context.auto_mark_article) {
-			mark_read(new_active, true);
+			mark_read(new_active, true, true);
 		}
 		new_active.dispatchEvent(freshrssOpenArticleEvent);
 	}
@@ -529,7 +561,7 @@ function user_filter(key) {
 		// Force scrolling to the filter div
 		const scroll = needsScroll(document.querySelector('.header'));
 		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
 		if (filters.length === 1) {
@@ -557,7 +589,7 @@ function auto_share(key) {
 		// Force scrolling to the share div
 		const scrollTop = needsScroll(share.closest('.bottom'));
 		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
 		if (shares.length === 1) {
@@ -585,30 +617,9 @@ function onScroll() {
 		document.querySelectorAll('.not_read:not(.keep_unread)').forEach(function (div) {
 				if (div.offsetHeight > 0 &&
 					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) {
 		const pagination = document.getElementById('mark-read-pagination');
@@ -621,10 +632,10 @@ function onScroll() {
 
 function init_posts() {
 	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
 			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);
 			if (lastScroll + 500 < Date.now()) {
 				lastScroll = Date.now();
@@ -681,7 +692,10 @@ function init_column_categories() {
 				a.href = '#dropdown-' + id;
 				div.querySelector('.dropdown-target').id = 'dropdown-' + id;
 				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') {
 				const id2 = div.closest('.item').id.substr(2);
 				a.href = '#dropdown-' + id2;
@@ -745,7 +759,7 @@ function init_shortcuts() {
 				} else if (ev.shiftKey) {	// Mark everything as read
 					document.querySelector('.nav_menu .read_all').click();
 				} else {	// Toggle the read state
-					mark_read(document.querySelector('.flux.current'), false);
+					mark_read(document.querySelector('.flux.current'), false, false);
 				}
 				return false;
 			}
@@ -787,7 +801,7 @@ function init_shortcuts() {
 			}
 			if (k === s.go_website) {
 				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);
 				return false;
@@ -813,7 +827,7 @@ function init_stream(stream) {
 	stream.onclick = function (ev) {
 		let el = ev.target.closest('.flux a.read');
 		if (el) {
-			mark_read(el.closest('.flux'), false);
+			mark_read(el.closest('.flux'), false, false);
 			return false;
 		}
 
@@ -882,7 +896,7 @@ function init_stream(stream) {
 				new_active = el.parentNode;
 			if (ev.target.tagName.toUpperCase() === 'A') {	//Leave real links alone
 				if (context.auto_mark_article) {
-					mark_read(new_active, true);
+					mark_read(new_active, true, false);
 				}
 				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');
 		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) {
@@ -916,7 +937,7 @@ function init_stream(stream) {
 				if (ev.which == 3) {
 					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.onerror = function (e) {
 						checkboxTag.checked = !isChecked;
-						if (this.status == 403) {
-							badAjax();
-						}
+						badAjax(this.status == 403);
 					};
 				req.onload = function (e) {
 						if (this.status != 200) {
@@ -980,10 +999,10 @@ function init_nav_entries() {
 			};
 		nav_entries.querySelector('.up').onclick = function (e) {
 				const active_item = document.querySelector('.flux.current'),
-					windowTop = document.documentElement.scrollTop,
+					windowTop = document.scrollingElement.scrollTop,
 					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;
 			};
 	}
@@ -1006,6 +1025,9 @@ function loadDynamicTags(div) {
 				return req.onerror(e);
 			}
 			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>';
 			if (json && json.length) {
 				for (let i = 0; i < json.length; i++) {
@@ -1031,7 +1053,7 @@ function updateFeed(feeds, feeds_count) {
 	req.open('POST', feed.url, true);
 	req.onloadend = function (e) {
 			if (this.status != 200) {
-				return badAjax();
+				return badAjax(false);
 			}
 			feed_processed++;
 			const div = document.getElementById('actualizeProgress');
@@ -1042,7 +1064,7 @@ function updateFeed(feeds, feeds_count) {
 				const req2 = new XMLHttpRequest();
 				req2.open('POST', './?c=feed&a=actualize&id=-1&ajax=1', true);
 				req2.onloadend = function (e) {
-					location.reload();
+					delayedFunction(function () { location.reload(); });
 				};
 				req2.setRequestHeader('Content-Type', 'application/json');
 				req2.send(JSON.stringify({
@@ -1074,9 +1096,12 @@ function init_actualize() {
 		req.responseType = 'json';
 		req.onload = function (e) {
 				if (this.status != 200) {
-					return badAjax();
+					return badAjax(false);
 				}
 				const json = xmlHttpRequestJson(this);
+				if (!json) {
+					return badAjax(false);
+				}
 				if (auto && json.feeds.length < 1) {
 					auto = false;
 					context.ajax_loading = false;
@@ -1184,10 +1209,12 @@ function notifs_html5_show(nb) {
 	});
 
 	notification.onclick = function () {
-		location.reload();
-		window.focus();
-		notification.close();
-	};
+			delayedFunction(function() {
+				location.reload();
+				window.focus();
+				notification.close();
+			});
+		};
 
 	if (context.html5_notif_timeout !== 0) {
 		setTimeout(function () {
@@ -1211,6 +1238,9 @@ function refreshUnreads() {
 	req.responseType = 'json';
 	req.onload = function (e) {
 			const json = xmlHttpRequestJson(this);
+			if (!json) {
+				return badAjax(false);
+			}
 			const isAll = document.querySelector('.category.all.active');
 			let new_articles = false;
 
@@ -1286,12 +1316,11 @@ function load_more_posts() {
 				paginationNew = streamAdopted.querySelector('.pagination');
 			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;
 				}
 			}
@@ -1305,8 +1334,7 @@ function load_more_posts() {
 
 			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) {
 				bigMarkAsRead.removeAttribute('disabled');
 			}
@@ -1407,6 +1435,12 @@ function init_normal() {
 	init_shortcuts();
 	init_actualize();
 	faviconNbUnread();
+
+	window.onbeforeunload = function (e) {
+		if (mark_read_queue && mark_read_queue.length > 0) {
+			return false;
+		}
+	};
 }
 
 function init_beforeDOM() {

+ 4 - 3
phpcs.xml

@@ -2,7 +2,7 @@
 <ruleset name="FreshRSS Ruleset">
 	<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 -->
-	<!--<arg name="tab-width" value="10"/>-->
+	<arg name="tab-width" value="40"/>
 	<exclude-pattern>./static</exclude-pattern>
 	<exclude-pattern>./vendor</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.pgsql.php</exclude-pattern>
 		<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>
 	</rule>
 	<!-- When calling a function: -->