Merge pull request #2338 from FreshRSS/dev

FreshRSS 1.14.1
pull/2368/head 1.14.1
Alexandre Alapetite 6 years ago committed by GitHub
commit d008b6c1a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .jshintignore
  2. 8
      .jshintrc
  3. 24
      .travis.yml
  4. 29
      CHANGELOG.md
  5. 8
      Docker/Dockerfile
  6. 10
      Docker/Dockerfile-Alpine
  7. 13
      Docker/README.md
  8. 3
      Docker/entrypoint.sh
  9. 24
      app/Controllers/configureController.php
  10. 2
      app/Models/EntryDAO.php
  11. 2
      app/i18n/cz/conf.php
  12. 2
      app/i18n/de/conf.php
  13. 2
      app/i18n/fr/conf.php
  14. 2
      app/i18n/he/conf.php
  15. 2
      app/i18n/kr/conf.php
  16. 4
      app/i18n/nl/admin.php
  17. 22
      app/i18n/nl/conf.php
  18. 16
      app/i18n/nl/feedback.php
  19. 2
      app/i18n/oc/conf.php
  20. 2
      app/i18n/pt-br/conf.php
  21. 4
      app/i18n/zh-cn/conf.php
  22. 2
      app/views/helpers/javascript_vars.phtml
  23. 2
      cli/i18n/I18nFile.php
  24. 2
      constants.php
  25. 2
      lib/SimplePie/SimplePie/Cache/File.php
  26. 39
      lib/lib_rss.php
  27. 5
      p/ext.php
  28. 14
      p/scripts/install.js
  29. 208
      p/scripts/main.js
  30. 7
      phpcs.xml

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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' => 'קפיצה לפריט הבא שלא נקרא (הזנה או קטגוריה)',

@ -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' => '다음 읽지 않은 항목으로 이동 (피드 또는 카테고리)',

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save