Przeglądaj źródła

Merge pull request #2220 from FreshRSS/dev

FreshRSS 1.13.1
Alexandre Alapetite 7 lat temu
rodzic
commit
8dcdde6251
57 zmienionych plików z 428 dodań i 171 usunięć
  1. 34 1
      CHANGELOG.md
  2. 16 9
      Docker/Dockerfile
  3. 17 17
      Docker/FreshRSS.Apache.conf
  4. 23 0
      Docker/README.md
  5. 2 0
      Docker/entrypoint.sh
  6. 5 1
      app/Controllers/authController.php
  7. 86 28
      app/Controllers/importExportController.php
  8. 5 4
      app/Models/Auth.php
  9. 1 0
      app/Models/Entry.php
  10. 6 0
      app/Models/EntryDAO.php
  11. 4 1
      app/Models/FeedDAO.php
  12. 12 2
      app/Models/TagDAO.php
  13. 5 5
      app/Models/UserDAO.php
  14. 6 2
      app/actualize_script.php
  15. 1 0
      app/i18n/cz/sub.php
  16. 1 0
      app/i18n/de/sub.php
  17. 1 0
      app/i18n/en/sub.php
  18. 1 0
      app/i18n/es/sub.php
  19. 1 0
      app/i18n/fr/sub.php
  20. 1 0
      app/i18n/he/sub.php
  21. 1 0
      app/i18n/it/sub.php
  22. 1 0
      app/i18n/kr/sub.php
  23. 2 2
      app/i18n/nl/admin.php
  24. 3 3
      app/i18n/nl/conf.php
  25. 2 2
      app/i18n/nl/index.php
  26. 5 4
      app/i18n/nl/sub.php
  27. 2 2
      app/i18n/oc/conf.php
  28. 16 16
      app/i18n/oc/gen.php
  29. 7 6
      app/i18n/oc/sub.php
  30. 1 0
      app/i18n/pt-br/sub.php
  31. 1 0
      app/i18n/ru/sub.php
  32. 1 0
      app/i18n/tr/sub.php
  33. 1 0
      app/i18n/zh-cn/sub.php
  34. 2 1
      app/install.php
  35. 1 1
      app/layout/header.phtml
  36. 1 1
      app/views/configure/display.phtml
  37. 5 6
      app/views/helpers/export/articles.phtml
  38. 5 0
      app/views/importExport/index.phtml
  39. 1 1
      cli/_cli.php
  40. 1 1
      cli/export-opml-for-user.php
  41. 1 1
      cli/export-zip-for-user.php
  42. 4 1
      constants.php
  43. 1 0
      data/.gitignore
  44. 1 1
      docs/en/users/06_Mobile_access.md
  45. 2 2
      docs/fr/users/06_Mobile_access.md
  46. 5 12
      lib/Minz/Configuration.php
  47. 5 1
      lib/Minz/Session.php
  48. 1 0
      lib/favicons.php
  49. 1 0
      lib/lib_install.php
  50. 13 7
      lib/lib_rss.php
  51. 8 3
      p/api/.htaccess
  52. 9 12
      p/api/greader.php
  53. 16 11
      p/api/index.php
  54. 1 1
      p/i/.gitignore
  55. 2 0
      p/i/index.php
  56. 62 0
      p/scripts/api.js
  57. 9 3
      p/scripts/main.js

+ 34 - 1
CHANGELOG.md

@@ -1,5 +1,38 @@
 # FreshRSS changelog
 
+## 2019-01-26 FreshRSS 1.13.1
+
+* Features
+	* Include articles with custom labels during export [#2196](https://github.com/FreshRSS/FreshRSS/issues/2196)
+	* Export/import articles read/unread state [#2226](https://github.com/FreshRSS/FreshRSS/pull/2226)
+	* Import FeedBin, and more robust general import [#2228](https://github.com/FreshRSS/FreshRSS/pull/2228)
+* Bug fixing
+	* Fix missing HTTP `X-Forwarded-Prefix` in cookie path behind a reverse-proxy [#2201](https://github.com/FreshRSS/FreshRSS/pull/2201)
+* Deployment
+	* Docker improvements [#2202](https://github.com/FreshRSS/FreshRSS/pull/2202)
+		* Performance: Hard-include Apache .htaccess to avoid having to scan for changes in those files
+		* Performance: Disable unused Apache security check of symlinks
+		* Performance: Disable unused Apache modules
+		* Add option to mount custom `.htaccess` for HTTP authentication
+		* Docker logs gets PHP syslog messages (e.g. from cron job and when fetching external content)
+	* New environment variable `COPY_SYSLOG_TO_STDERR` or in `constants.local.php` to copy PHP syslog messages to STDERR [#2213](https://github.com/FreshRSS/FreshRSS/pull/2213)
+	* New `TZ` timezone environment variable [#2153](https://github.com/FreshRSS/FreshRSS/issues/2153)
+	* Run Docker cron job with Apache user instead of root [#2208](https://github.com/FreshRSS/FreshRSS/pull/2208)
+	* Accept HTTP header `X-WebAuth-User` for delegated HTTP Authentication [#2204](https://github.com/FreshRSS/FreshRSS/pull/2204)
+* Extensions
+	* Trigger a `freshrss:openArticle` JavaScript event [#2222](https://github.com/FreshRSS/FreshRSS/pull/2222)
+* API
+	* Automatic test of API configuration [#2207](https://github.com/FreshRSS/FreshRSS/pull/2207)
+	* Performance + compatibility: Use Apache `SetEnvIf` module if available and fall-back to `RewriteRule` [#2202](https://github.com/FreshRSS/FreshRSS/pull/2202)
+* Security
+	* Fixes when HTTP user does not exist in FreshRSS [#2204](https://github.com/FreshRSS/FreshRSS/pull/2204)
+* I18n
+	* Improve Dutch [#2221](https://github.com/FreshRSS/FreshRSS/pull/2221)
+	* Improve Occitan [#2230](https://github.com/FreshRSS/FreshRSS/pull/2230)
+* Accessibility
+	* Remove alt in logo [#2209](https://github.com/FreshRSS/FreshRSS/pull/2209)
+
+
 ## 2018-12-22 FreshRSS 1.13.0
 
 * API
@@ -24,7 +57,7 @@
 		[#1620](https://github.com/FreshRSS/FreshRSS/issues/1620), [#2089](https://github.com/FreshRSS/FreshRSS/pull/2089),
 		[#2122](https://github.com/FreshRSS/FreshRSS/pull/2122), [#2161](https://github.com/FreshRSS/FreshRSS/pull/2161)
 * Deployment
-	* Support for `HTTP_X_FORWARDED_PREFIX` to ease the use of reverse proxies [#2191](https://github.com/FreshRSS/FreshRSS/pull/2191)
+	* Support for HTTP `X-Forwarded-Prefix` to ease the use of reverse proxies [#2191](https://github.com/FreshRSS/FreshRSS/pull/2191)
 		* Updated Docker + Træfik + Let’s Encrypt deployment guide [#2189](https://github.com/FreshRSS/FreshRSS/pull/2189)
 	* Docker image updated to Alpine 3.8.2 with PHP 7.2.13 and Apache 2.4.35
 	* Fix `.dockerignore` [#2195](https://github.com/FreshRSS/FreshRSS/pull/2195)

+ 16 - 9
Docker/Dockerfile

@@ -1,24 +1,31 @@
 FROM alpine:3.8
 
+ENV TZ UTC
+
 RUN apk add --no-cache \
 	apache2 php7-apache2 \
 	php7 php7-curl php7-gmp php7-intl php7-mbstring php7-xml php7-zip \
 	php7-ctype php7-dom php7-fileinfo php7-iconv php7-json php7-session php7-simplexml php7-xmlreader php7-zlib \
-	php7-pdo_sqlite \
-	php7-pdo_mysql \
-	php7-pdo_pgsql
+	php7-pdo_sqlite php7-pdo_mysql php7-pdo_pgsql
 
-ENV FRESHRSS_ROOT /var/www/FreshRSS
-RUN mkdir -p ${FRESHRSS_ROOT} /run/apache2/
-WORKDIR ${FRESHRSS_ROOT}
+RUN mkdir -p /var/www/FreshRSS /run/apache2/
+WORKDIR /var/www/FreshRSS
 
-COPY . ${FRESHRSS_ROOT}
+COPY . /var/www/FreshRSS
 COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
 
-RUN sed -r -i "/^[ ]*(CustomLog|ErrorLog|Listen) /s/^/#/" /etc/apache2/httpd.conf && \
-	echo "17,37 * * * * php ${FRESHRSS_ROOT}/app/actualize_script.php 2>&1 | tee /tmp/FreshRSS.log" >> \
+RUN rm -f /etc/apache2/conf.d/languages.conf /etc/apache2/conf.d/info.conf \
+		/etc/apache2/conf.d/status.conf /etc/apache2/conf.d/userdir.conf && \
+	sed -r -i "/^\s*LoadModule .*mod_(alias|autoindex|negotiation|status).so$/s/^/#/" \ 
+		/etc/apache2/httpd.conf && \
+	sed -r -i "/^\s*#\s*LoadModule .*mod_(deflate|expires|headers|mime|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
 
+ENV COPY_SYSLOG_TO_STDERR On
 ENV CRON_MIN ''
 ENTRYPOINT ["./Docker/entrypoint.sh"]
 

+ 17 - 17
Docker/FreshRSS.Apache.conf

@@ -1,19 +1,3 @@
-<IfModule !deflate_module>
-	LoadModule deflate_module modules/mod_deflate.so
-</IfModule>
-<IfModule !expires_module>
-	LoadModule expires_module modules/mod_expires.so
-</IfModule>
-<IfModule !headers_module>
-	LoadModule headers_module modules/mod_headers.so
-</IfModule>
-<IfModule !mime_module>
-	LoadModule mime_module modules/mod_mime.so
-</IfModule>
-<IfModule !rewrite_module>
-	LoadModule rewrite_module modules/mod_rewrite.so
-</IfModule>
-
 ServerName freshrss.localhost
 Listen 0.0.0.0:80
 DocumentRoot /var/www/FreshRSS/p/
@@ -21,7 +5,23 @@ CustomLog /dev/stdout combined
 ErrorLog /dev/stderr
 AllowEncodedSlashes On
 
+<Directory />
+	AllowOverride None
+	Options FollowSymLinks
+	Require all denied
+</Directory>
+
 <Directory /var/www/FreshRSS/p>
-	AllowOverride AuthConfig FileInfo Indexes Limit
+	AllowOverride None
+	Include /var/www/FreshRSS/p/.htaccess
+	Options FollowSymLinks
 	Require all granted
 </Directory>
+
+<Directory /var/www/FreshRSS/p/api>
+	Include /var/www/FreshRSS/p/api/.htaccess
+</Directory>
+
+<Directory /var/www/FreshRSS/p/i>
+	IncludeOptional /var/www/FreshRSS/p/i/.htaccess
+</Directory>

+ 23 - 0
Docker/README.md

@@ -66,6 +66,7 @@ sudo docker volume create freshrss-data
 sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   -v freshrss-data:/var/www/FreshRSS/data \
   -e 'CRON_MIN=4,34' \
+  -e TZ=Europe/Paris \
   --net freshrss-network \
   --label traefik.port=80 \
   --label traefik.frontend.rule='Host:freshrss.example.net' \
@@ -74,6 +75,7 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
   --name freshrss freshrss/freshrss
 ```
 
+* Replace `TZ=Europe/Paris` by your [server timezone](http://php.net/timezones), or remove the line to use `UTC`.
 * If you cannot have FreshRSS at the root of a dedicated domain, update the command above according to the following model:
 	`--label traefik.frontend.rule='Host:freshrss.example.net;PathPrefixStrip:/FreshRSS/' \`
 * You may remove the `--label traefik.*` lines if you do not use Træfik.
@@ -205,6 +207,27 @@ sudo docker run -d --restart unless-stopped --log-opt max-size=10m \
 
 ## More deployment options
 
+### Custom Apache configuration (advanced users)
+
+Changes in Apache `.htaccess` files are applied when restarting the container.
+In particular, if you want FreshRSS to use HTTP-based login (instead of the easier Web form login), you can mount your own `./FreshRSS/p/i/.htaccess`:
+
+```
+sudo docker run ...
+  -v /your/.htaccess:/var/www/FreshRSS/p/i/.htaccess \
+  -v /your/.htpasswd:/var/www/FreshRSS/data/.htpasswd \
+  ...
+  --name freshrss freshrss/freshrss
+```
+
+Example of `/your/.htaccess` referring to `/your/.htpasswd`:
+```
+AuthUserFile /var/www/FreshRSS/data/.htpasswd
+AuthName "FreshRSS"
+AuthType Basic
+Require valid-user
+```
+
 ### Example with [docker-compose](https://docs.docker.com/compose/)
 
 A [docker-compose.yml](docker-compose.yml) file is given as an example, using PostgreSQL. In order to use it, you have to adapt:

+ 2 - 0
Docker/entrypoint.sh

@@ -5,6 +5,8 @@ php -f ./cli/prepare.php > /dev/null
 chown -R :www-data .
 chmod -R g+r . && chmod -R g+w ./data/
 
+find /etc/php*/ -name php.ini -exec sed -r -i "\#^;?date.timezone#s#^.*#date.timezone = $TZ#" {} \;
+
 if [ -n "$CRON_MIN" ]; then
 	sed -r -i "\#FreshRSS#s#^[^ ]+ #$CRON_MIN #" /var/spool/cron/crontabs/root
 fi

+ 5 - 1
app/Controllers/authController.php

@@ -79,8 +79,12 @@ class FreshRSS_auth_Controller extends Minz_ActionController {
 			Minz_Request::forward(array('c' => 'auth', 'a' => 'formLogin'));
 			break;
 		case 'http_auth':
+			Minz_Error::error(403, array('error' => array(_t('feedback.access.denied'),
+					' [HTTP Remote-User=' . htmlspecialchars(httpAuthUser(), ENT_NOQUOTES, 'UTF-8') . ']'
+				)), false);
+			break;
 		case 'none':
-			// It should not happened!
+			// It should not happen!
 			Minz_Error::error(404);
 		default:
 			// TODO load plugin instead

+ 86 - 28
app/Controllers/importExportController.php

@@ -41,7 +41,8 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$list_files = array(
 			'opml' => array(),
 			'json_starred' => array(),
-			'json_feed' => array()
+			'json_feed' => array(),
+			'ttrss_starred' => array(),
 		);
 
 		// We try to list all files according to their type
@@ -434,10 +435,9 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			}
 			return false;
 		}
+		$items = isset($article_object['items']) ? $article_object['items'] : $article_object;
 
-		$is_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
-
-		$google_compliant = strpos($article_object['id'], 'com.google') !== false;
+		$mark_as_read = FreshRSS_Context::$user_conf->mark_when['reception'] ? 1 : 0;
 
 		$error = false;
 		$article_to_feed = array();
@@ -447,9 +447,23 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		$limits = FreshRSS_Context::$system_conf->limits;
 
 		// First, we check feeds of articles are in DB (and add them if needed).
-		foreach ($article_object['items'] as $item) {
-			$key = $google_compliant ? 'htmlUrl' : 'feedUrl';
-			$feed = new FreshRSS_Feed($item['origin'][$key]);
+		foreach ($items as $item) {
+			if (!isset($item['origin'])) {
+				$item['origin'] = array('title' => 'Import');
+			}
+			if (!empty($item['origin']['feedUrl'])) {
+				$feedUrl = $item['origin']['feedUrl'];
+			} elseif (!empty($item['origin']['streamId']) && strpos($item['origin']['streamId'], 'feed/') === 0) {
+				$feedUrl = substr($item['origin']['streamId'], 5);	//Google Reader
+				$item['origin']['feedUrl'] = $feedUrl;
+			} elseif (!empty($item['origin']['htmlUrl'])) {
+				$feedUrl = $item['origin']['htmlUrl'];
+			} else {
+				$feedUrl = 'http://import.localhost/import.xml';
+				$item['origin']['feedUrl'] = $feedUrl;
+				$item['origin']['disable'] = true;
+			}
+			$feed = new FreshRSS_Feed($feedUrl);
 			$feed = $this->feedDAO->searchByUrl($feed->url());
 
 			if ($feed == null) {
@@ -497,7 +511,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		// Then, articles are imported.
 		$newGuids = array();
 		$this->entryDAO->beginTransaction();
-		foreach ($article_object['items'] as $item) {
+		foreach ($items as $item) {
 			if (empty($article_to_feed[$item['id']])) {
 				// Related feed does not exist for this entry, do nothing.
 				continue;
@@ -505,14 +519,19 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 
 			$feed_id = $article_to_feed[$item['id']];
 			$author = isset($item['author']) ? $item['author'] : '';
-			$is_starred = $starred;
-			$tags = $item['categories'];
+			$is_starred = false;
+			$is_read = null;
+			$tags = empty($item['categories']) ? array() : $item['categories'];
 			$labels = array();
 			for ($i = count($tags) - 1; $i >= 0; $i --) {
 				$tag = trim($tags[$i]);
 				if (strpos($tag, 'user/-/') !== false) {
 					if ($tag === 'user/-/state/com.google/starred') {
 						$is_starred = true;
+					} elseif ($tag === 'user/-/state/com.google/read') {
+						$is_read = true;
+					} elseif ($tag === 'user/-/state/com.google/unread') {
+						$is_read = false;
 					} elseif (strpos($tag, 'user/-/label/') === 0) {
 						$tag = trim(substr($tag, 13));
 						if ($tag != '') {
@@ -522,19 +541,49 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 					unset($tags[$i]);
 				}
 			}
+			if ($starred && !$is_starred) {
+				//If the article has no label, mark it as starred (old format)
+				$is_starred = empty($labels);
+			}
+			if ($is_read === null) {
+				$is_read = $mark_as_read;
+			}
+
+			if (isset($item['alternate'][0]['href'])) {
+				$url = $item['alternate'][0]['href'];
+			} elseif (isset($item['url'])) {
+				$url = $item['url'];	//FeedBin
+			} else {
+				$url = '';
+			}
 
-			$url = $item['alternate'][0]['href'];
 			if (!empty($item['content']['content'])) {
 				$content = $item['content']['content'];
 			} elseif (!empty($item['summary']['content'])) {
 				$content = $item['summary']['content'];
+			} elseif (!empty($item['content'])) {
+				$content = $item['content'];	//FeedBin
+			} else {
+				$content = '';
 			}
 			$content = sanitizeHTML($content, $url);
 
+			if (!empty($item['published'])) {
+				$published = $item['published'];
+			} elseif (!empty($item['timestampUsec'])) {
+				$published = substr($item['timestampUsec'], 0, -6);
+			} elseif (!empty($item['updated'])) {
+				$published = $item['updated'];
+			} else {
+				$published = 0;
+			}
+			if (!ctype_digit('' . $published)) {
+				$published = strtotime($published);
+			}
+
 			$entry = new FreshRSS_Entry(
 				$feed_id, $item['id'], $item['title'], $author,
-				$content, $url,
-				$item['published'], $is_read, $is_starred
+				$content, $url, $published, $is_read, $is_starred
 			);
 			$entry->_id(min(time(), $entry->date(true)) . uSecString());
 			$entry->_tags($tags);
@@ -626,6 +675,9 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$feed->_category(FreshRSS_CategoryDAO::DEFAULTCATEGORYID);
 			$feed->_name($name);
 			$feed->_website($website);
+			if (!empty($origin['disable'])) {
+				$feed->_ttl(-1 * FreshRSS_Context::$user_conf->ttl_default);
+			}
 
 			// Call the extension hook
 			$feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed);
@@ -650,7 +702,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 		return $return;
 	}
 
-	public function exportFile($export_opml = true, $export_starred = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
+	public function exportFile($export_opml = true, $export_starred = false, $export_labelled = false, $export_feeds = array(), $maxFeedEntries = 50, $username = null) {
 		require_once(LIB_PATH . '/lib_opml.php');
 
 		$this->catDAO = new FreshRSS_CategoryDAO($username);
@@ -674,8 +726,11 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$export_files["feeds_${day}.opml.xml"] = $this->generateOpml();
 		}
 
-		if ($export_starred) {
-			$export_files["starred_${day}.json"] = $this->generateEntries('starred');
+		if ($export_starred || $export_labelled) {
+			$export_files["starred_${day}.json"] = $this->generateEntries(
+				($export_starred ? 'S' : '') .
+				($export_labelled ? 'T' : '')
+			);
 		}
 
 		foreach ($export_feeds as $feed_id) {
@@ -683,7 +738,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			if ($feed) {
 				$filename = "feed_${day}_" . $feed->category() . '_'
 				          . $feed->id() . '.json';
-				$export_files[$filename] = $this->generateEntries('feed', $feed, $maxFeedEntries);
+				$export_files[$filename] = $this->generateEntries('f', $feed, $maxFeedEntries);
 			}
 		}
 
@@ -725,6 +780,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 			$nb_files = $this->exportFile(
 					Minz_Request::param('export_opml', false),
 					Minz_Request::param('export_starred', false),
+					Minz_Request::param('export_labelled', false),
 					Minz_Request::param('export_feeds', array())
 				);
 		} catch (FreshRSS_ZipMissing_Exception $zme) {
@@ -758,27 +814,29 @@ class FreshRSS_importExport_Controller extends Minz_ActionController {
 	/**
 	 * This method returns a JSON file content.
 	 *
-	 * @param string $type must be "starred" or "feed"
+	 * @param string $type must be one of:
+	 * 	'S' (starred/favourite), 'f' (feed), 'T' (taggued/labelled), 'ST' (starred or labelled)
 	 * @param FreshRSS_Feed $feed feed of which we want to get entries.
 	 * @return string the JSON file content.
 	 */
 	private function generateEntries($type, $feed = null, $maxFeedEntries = 50) {
 		$this->view->categories = $this->catDAO->listCategories();
+		$tagDAO = FreshRSS_Factory::createTagDao();
 
-		if ($type == 'starred') {
+		if ($type === 's' || $type === 'S' || $type === 'T' || $type === 'ST') {
 			$this->view->list_title = _t('sub.import_export.starred_list');
 			$this->view->type = 'starred';
-			$unread_fav = $this->entryDAO->countUnreadReadFavorites();
-			$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
-				's', '', FreshRSS_Entry::STATE_ALL, 'ASC', $unread_fav['all']
-			);
-		} elseif ($type === 'feed' && $feed != null) {
+			$this->view->entriesId = $this->entryDAO->listIdsWhere($type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1);
+			$this->view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($this->view->entriesId);
+			//The following is a streamable query, i.e. must be last
+			$this->view->entriesRaw = $this->entryDAO->listWhereRaw($type, '', FreshRSS_Entry::STATE_ALL, 'ASC', -1);
+		} elseif ($type === 'f' && $feed != null) {
 			$this->view->list_title = _t('sub.import_export.feed_list', $feed->name());
 			$this->view->type = 'feed/' . $feed->id();
-			$this->view->entriesRaw = $this->entryDAO->listWhereRaw(
-				'f', $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC',
-				$maxFeedEntries
-			);
+			$this->view->entriesId = $this->entryDAO->listIdsWhere($type, $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $maxFeedEntries);
+			$this->view->entryIdsTagNames = $tagDAO->getEntryIdsTagNames($this->view->entriesId);
+			//The following is a streamable query, i.e. must be last
+			$this->view->entriesRaw = $this->entryDAO->listWhereRaw($type, $feed->id(), FreshRSS_Entry::STATE_ALL, 'ASC', $maxFeedEntries);
 			$this->view->feed = $feed;
 		}
 

+ 5 - 4
app/Models/Auth.php

@@ -28,13 +28,13 @@ class FreshRSS_Auth {
 
 		if (self::$login_ok) {
 			self::giveAccess();
-		} elseif (self::accessControl()) {
-			self::giveAccess();
+		} elseif (self::accessControl() && self::giveAccess()) {
 			FreshRSS_UserDAO::touch();
 		} else {
 			// Be sure all accesses are removed!
 			self::removeAccess();
 		}
+		return self::$login_ok;
 	}
 
 	/**
@@ -60,7 +60,7 @@ class FreshRSS_Auth {
 			return $current_user != '';
 		case 'http_auth':
 			$current_user = httpAuthUser();
-			$login_ok = $current_user != '';
+			$login_ok = $current_user != '' && FreshRSS_UserDAO::exists($current_user);
 			if ($login_ok) {
 				Minz_Session::_param('currentUser', $current_user);
 			}
@@ -81,7 +81,7 @@ class FreshRSS_Auth {
 		$user_conf = get_user_configuration($current_user);
 		if ($user_conf == null) {
 			self::$login_ok = false;
-			return;
+			return false;
 		}
 		$system_conf = Minz_Configuration::get('system');
 
@@ -102,6 +102,7 @@ class FreshRSS_Auth {
 
 		Minz_Session::_param('loginOk', self::$login_ok);
 		Minz_Session::_param('REMOTE_USER', httpAuthUser());
+		return self::$login_ok;
 	}
 
 	/**

+ 1 - 0
app/Models/Entry.php

@@ -209,6 +209,7 @@ class FreshRSS_Entry extends Minz_Model {
 		$feed_timeout = empty($attributes['timeout']) ? 0 : intval($attributes['timeout']);
 
 		if ($system_conf->simplepie_syslog_enabled) {
+			prepareSyslog();
 			syslog(LOG_INFO, 'FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url));
 		}
 

+ 6 - 0
app/Models/EntryDAO.php

@@ -839,6 +839,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'AND e.is_favorite=1 ';
 			break;
+		case 'S':	//Starred
+			$where .= 'e.is_favorite=1 ';
+			break;
 		case 'c':	//Category
 			$where .= 'f.priority >= ' . FreshRSS_Feed::PRIORITY_NORMAL . ' ';
 			$where .= 'AND f.category=? ';
@@ -855,6 +858,9 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		case 'T':	//Any tag
 			$where .= '1=1 ';
 			break;
+		case 'ST':	//Starred or tagged
+			$where .= 'e.is_favorite=1 OR EXISTS (SELECT et2.id_tag FROM `' . $this->prefix . 'entrytag` et2 WHERE et2.id_entry = e.id) ';
+			break;
 		default:
 			throw new FreshRSS_EntriesGetter_Exception('Bad type in Entry->listByType: [' . $type . ']!');
 		}

+ 4 - 1
app/Models/FeedDAO.php

@@ -61,7 +61,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$valuesTmp['lastUpdate'],
 			base64_encode($valuesTmp['httpAuth']),
 			FreshRSS_Feed::KEEP_HISTORY_DEFAULT,
-			FreshRSS_Feed::TTL_DEFAULT,
+			isset($valuesTmp['ttl']) ? intval($valuesTmp['ttl']) : FreshRSS_Feed::TTL_DEFAULT,
 			isset($valuesTmp['attributes']) ? json_encode($valuesTmp['attributes']) : '',
 		);
 
@@ -95,6 +95,9 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 				'httpAuth' => $feed->httpAuth(),
 				'attributes' => $feed->attributes(),
 			);
+			if ($feed->mute() || $feed->ttl() != FreshRSS_Context::$user_conf->ttl_default) {
+				$values['ttl'] = $feed->ttl() * ($feed->mute() ? -1 : 1);
+			}
 
 			$id = $this->addFeed($values);
 			if ($id) {

+ 12 - 2
app/Models/TagDAO.php

@@ -265,8 +265,18 @@ class FreshRSS_TagDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$values = array();
 		if (is_array($entries) && count($entries) > 0) {
 			$sql .= ' AND et.id_entry IN (' . str_repeat('?,', count($entries) - 1). '?)';
-			foreach ($entries as $entry) {
-				$values[] = is_array($entry) ? $entry['id'] : $entry->id();
+			if (is_array($entries[0])) {
+				foreach ($entries as $entry) {
+					$values[] = $entry['id'];
+				}
+			} elseif (is_object($entries[0])) {
+				foreach ($entries as $entry) {
+					$values[] = $entry->id();
+				}
+			} else {
+				foreach ($entries as $entry) {
+					$values[] = $entry;
+				}
 			}
 		}
 		$stm = $this->bd->prepare($sql);

+ 5 - 5
app/Models/UserDAO.php

@@ -65,7 +65,7 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		require_once(APP_PATH . '/SQL/install.sql.' . $db['type'] . '.php');
 
 		if ($db['type'] === 'sqlite') {
-			return unlink(join_path(DATA_PATH, 'users', $username, 'db.sqlite'));
+			return unlink(USERS_PATH . '/' . $username . '/db.sqlite');
 		} else {
 			$userPDO = new Minz_ModelPdo($username);
 
@@ -81,18 +81,18 @@ class FreshRSS_UserDAO extends Minz_ModelPdo {
 		}
 	}
 
-	public static function exist($username) {
-		return is_dir(join_path(DATA_PATH, 'users', $username));
+	public static function exists($username) {
+		return is_dir(USERS_PATH . '/' . $username);
 	}
 
 	public static function touch($username = '') {
 		if (!FreshRSS_user_Controller::checkUsername($username)) {
 			$username = Minz_Session::param('currentUser', '_');
 		}
-		return touch(join_path(DATA_PATH, 'users', $username, 'config.php'));
+		return touch(USERS_PATH . '/' . $username . '/config.php');
 	}
 
 	public static function mtime($username) {
-		return @filemtime(join_path(DATA_PATH, 'users', $username, 'config.php'));
+		return @filemtime(USERS_PATH . '/' . $username . '/config.php');
 	}
 }

+ 6 - 2
app/actualize_script.php

@@ -12,6 +12,9 @@ if (defined('STDOUT')) {
 	fwrite(STDOUT, 'Starting feed actualization at ' . $begin_date->format('c') . "\n");	//Unbuffered
 }
 
+prepareSyslog();
+syslog(LOG_INFO, 'FreshRSS Start feeds actualization...');
+
 // Set the header params ($_GET) to call the FRSS application.
 $_GET['c'] = 'feed';
 $_GET['a'] = 'actualize';
@@ -64,7 +67,7 @@ foreach ($users as $user) {
 	if (!invalidateHttpCache()) {
 		Minz_Log::warning('FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt'), ADMIN_LOG);
 		if (defined('STDERR')) {
-			fwrite(STDERR, 'Write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n");
+			fwrite(STDERR, 'FreshRSS write access problem in ' . join_path(USERS_PATH, $user, 'log.txt') . "\n");
 		}
 	}
 }
@@ -75,7 +78,8 @@ if (defined('STDOUT')) {
 	$end_date = date_create('now');
 	$duration = date_diff($end_date, $begin_date);
 	fwrite(STDOUT, 'Ending feed actualization at ' . $end_date->format('c') . "\n");	//Unbuffered
-	fwrite(STDOUT, 'Feed actualizations took ' . $duration->format('%a day(s), %h hour(s),  %i minute(s) and %s seconds') . ' for ' . count($users) . " users\n");	//Unbuffered
+	fwrite(STDOUT, 'Feed actualizations took ' . $duration->format('%a day(s), %h hour(s), %i minute(s) and %s seconds') . ' for ' . count($users) . " users\n");	//Unbuffered
 }
 echo 'End.', "\n";
 ob_end_flush();
+syslog(LOG_INFO, 'FreshRSS feeds actualization done.');

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Export',
 		'export_opml' => 'Exportovat seznam kanálů (OPML)',
 		'export_starred' => 'Exportovat oblíbené',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'Seznam %s článků',
 		'file_to_import' => 'Soubor k importu<br />(OPML, JSON nebo ZIP)',
 		'file_to_import_no_zip' => 'Soubor k importu<br />(OPML nebo JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Exportieren',
 		'export_opml' => 'Liste der Feeds exportieren (OPML)',
 		'export_starred' => 'Ihre Favoriten exportieren',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'Liste von %s Artikeln',
 		'file_to_import' => 'Zu importierende Datei<br />(OPML, JSON oder ZIP)',
 		'file_to_import_no_zip' => 'Zu importierende Datei<br />(OPML oder JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Export',
 		'export_opml' => 'Export list of feeds (OPML)',
 		'export_starred' => 'Export your favourites',
+		'export_labelled' => 'Export your labelled articles',
 		'feed_list' => 'List of %s articles',
 		'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',
 		'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Exportar',
 		'export_opml' => 'Exportar la lista de fuentes (OPML)',
 		'export_starred' => 'Exportar tus favoritos',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'Lista de %s artículos',
 		'file_to_import' => 'Archivo a importar<br />(OPML, JSON o ZIP)',
 		'file_to_import_no_zip' => 'Archivo a importar<br />(OPML o JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Exporter',
 		'export_opml' => 'Exporter la liste des flux (OPML)',
 		'export_starred' => 'Exporter les favoris',
+		'export_labelled' => 'Exporter les articles étiquetés',
 		'feed_list' => 'Liste des articles de %s',
 		'file_to_import' => 'Fichier à importer<br />(OPML, JSON ou ZIP)',
 		'file_to_import_no_zip' => 'Fichier à importer<br />(OPML ou JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'ייצוא',
 		'export_opml' => 'ייצוא רשימת הזנות (OPML)',
 		'export_starred' => 'ייצוא מועדפים',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'רשימה של %s מאמרים',
 		'file_to_import' => 'קובץ לייבוא<br />(OPML, Json or Zip)',
 		'file_to_import_no_zip' => 'קובץ לייבוא<br />(OPML or Json)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Esporta',
 		'export_opml' => 'Esporta tutta la lista dei feed (OPML)',
 		'export_starred' => 'Esporta i tuoi preferiti',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'Elenco di %s articoli',
 		'file_to_import' => 'File da importare<br />(OPML, JSON o ZIP)',
 		'file_to_import_no_zip' => 'File da importare<br />(OPML o JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => '내보내기',
 		'export_opml' => '피드 목록 내보내기 (OPML)',
 		'export_starred' => '즐겨찾기 내보내기',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => '%s 개의 글 목록',
 		'file_to_import' => '불러올 파일<br />(OPML, JSON 또는 ZIP)',
 		'file_to_import_no_zip' => '불러올 파일<br />(OPML 또는 JSON)',

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

@@ -163,8 +163,8 @@ return array(
 		'max-categories' => 'Categoriën limiet per gebruiker',
 		'max-feeds' => 'Feed limiet per gebruiker',
 		'cookie-duration' => array(
-			'help' => 'in seconds', // @todo translate
-			'number' => 'Duration to keep logged in', // @todo translate
+			'help' => 'in seconden',
+			'number' => 'Tijdsduur om ingelogd te blijven',
 		),
 		'registration' => array(
 			'help' => '0 betekent geen account limiet',

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

@@ -162,7 +162,7 @@ return array(
 		'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' => 'The following navigation shortcuts do not support modifiers.',	//TODO - Translation
+		'navigation_no_mod_help' => 'De volgende navigatiesnelkoppelingen ondersteunen geen besturingstoetsen.',
 		'next_article' => 'Spring naar volgende artikel',
 		'normal_view' => 'Schakel naar gewoon aanzicht',
 		'other_action' => 'Andere acties',
@@ -171,8 +171,8 @@ return array(
 		'rss_view' => 'Open RSS-aanzicht in een nieuwe tab',
 		'see_on_website' => 'Bekijk op originale website',
 		'shift_for_all_read' => '+ <code>shift</code> om alle artikelen als gelezen te markeren',
-		'skip_next_article' => 'Focus next without opening',	//TODO - Translation
-		'skip_previous_article' => 'Focus previous without opening',	//TODO - Translation
+		'skip_next_article' => 'Volgend artikel focusen zonder openen',
+		'skip_previous_article' => 'Vorig artikel focusen zonder openen',
 		'title' => 'Verwijzingen',
 		'user_filter' => 'Toegang gebruikers filters',
 		'user_filter_help' => 'Als er slechts één gebruikersfilter is, dan wordt die gebruikt. Anders zijn ze toegankelijk met hun nummer.',

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

@@ -53,11 +53,11 @@ return array(
 		'starred' => 'Laat alleen favorieten zien',
 		'stats' => 'Statistieken',
 		'subscription' => 'Abonnementen beheer',
-		'tags' => 'My labels',	//TODO - Translation
+		'tags' => 'Mijn labels',
 		'unread' => 'Laat alleen ongelezen zien',
 	),
 	'share' => 'Delen',
 	'tag' => array(
-		'related' => 'Verwante labels',	//TODO - Translation
+		'related' => 'Verwante labels',
 	),
 );

+ 5 - 4
app/i18n/nl/sub.php

@@ -27,7 +27,7 @@ return array(
 			'password' => 'HTTP wachtwoord',
 			'username' => 'HTTP gebruikers naam',
 		),
-		'clear_cache' => 'Always clear cache',	//TODO - Translation
+		'clear_cache' => 'Cache altijd leegmaken',
 		'css_help' => 'Haalt verstoorde RSS feeds op (attentie, heeft meer tijd nodig!)',
 		'css_path' => 'Artikelen CSS pad op originele website',
 		'description' => 'Omschrijving',
@@ -47,11 +47,11 @@ return array(
 		),
 		'websub' => 'Directe notificaties met WebSub',
 		'show' => array(
-			'all' => 'Show all feeds',	//TODO - Translation
-			'error' => 'Show only feeds with error',	//TODO - Translation
+			'all' => 'Alle feeds tonen',
+			'error' => 'Alleen feeds met een foutmelding tonen',
 		),
 		'showing' => array(
-			'error' => 'Showing only feeds with error',	//TODO - Translation
+			'error' => 'Alleen feeds met een foutmelding worden getoond',
 		),
 		'ssl_verify' => 'SSL-veiligheid controleren',
 		'stats' => 'Statistieken',
@@ -72,6 +72,7 @@ return array(
 		'export' => 'Exporteer',
 		'export_opml' => 'Exporteer lijst van feeds (OPML)',
 		'export_starred' => 'Exporteer je favorieten',
+		'export_labelled' => 'Exporteer gelabelde artikels',
 		'feed_list' => 'Lijst van %s artikelen',
 		'file_to_import' => 'Bestand om te importeren<br />(OPML, JSON of ZIP)',
 		'file_to_import_no_zip' => 'Bestand om te importeren<br />(OPML of JSON)',

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

@@ -94,14 +94,14 @@ return array(
 		'display_articles_unfolded' => 'Mostrar los articles desplegats per defaut',
 		'display_categories_unfolded' => 'Mostrar las categorias plegadas 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” per las imatges',
+		'img_with_lazyload' => 'Utilizar lo mòde “cargament tardiu” pels imatges',
 		'jump_next' => 'sautar al vesin venent pas legit (flux o categoria)',
 		'mark_updated_article_unread' => 'Marcar los articles actualizats coma pas legits',
 		'number_divided_when_reader' => 'Devisat per 2 dins la vista de lectura.',
 		'read' => array(
 			'article_open_on_website' => 'quand l’article es dobèrt sul site d’origina',
 			'article_viewed' => 'quand l’article es mostrat',
-			'scroll' => 'en davalant la pagina',
+			'scroll' => 'en davalar la pagina',
 			'upon_reception' => 'en recebre un article novèl',
 			'when' => 'Marcar un article coma legit…',
 		),

+ 16 - 16
app/i18n/oc/gen.php

@@ -43,29 +43,29 @@ return array(
 		),
 	),
 	'date' => array(
-		'Apr' => 'a\b\r\i\a\l',
+		'Apr' => '\\a\\b\\r\\i\\a\\l',
 		'apr' => 'abr.',
 		'april' => 'abrial',
-		'Aug' => 'a\g\o\s\t',
+		'Aug' => '\\a\\g\\o\\s\\t',
 		'aug' => 'agost',
 		'august' => 'agost',
 		'before_yesterday' => 'Abans ièr',
-		'Dec' => '\d\e\c\e\m\b\r\e',
+		'Dec' => '\\d\\e\\c\\e\\m\\b\\r\\e',
 		'dec' => 'dec.',
 		'december' => 'decembre',
-		'Feb' => 'f\e\b\r\i\è\r',
+		'Feb' => '\\f\\e\\b\\r\\i\\è\\r',
 		'feb' => 'feb.',
 		'february' => 'febrièr',
-		'format_date' => 'j %s \de\ Y',
-		'format_date_hour' => 'j %s \de\ Y \a H\:i',
+		'format_date' => 'j \\d\\e %s \\d\\e Y',
+		'format_date_hour' => 'j \\d\\e %s \\d\\e Y \\a H\:i',
 		'fri' => 'dv',
-		'Jan' => 'g\e\n\i\è\r',
+		'Jan' => '\\g\\e\\n\\i\\\r',
 		'jan' => 'gen.',
 		'january' => 'genièr',
-		'Jul' => 'j\u\l\h\e\t',
+		'Jul' => '\\j\\u\\l\\h\\e\\t',
 		'jul' => 'julh',
 		'july' => 'julhet',
-		'Jun' => 'j\u\n\h',
+		'Jun' => '\\j\\u\\n\\h',
 		'jun' => 'junh',
 		'june' => 'junh',
 		'last_3_month' => 'Dempuèi los tres darrièrs meses',
@@ -73,22 +73,22 @@ return array(
 		'last_month' => 'Dempuèi lo mes passat',
 		'last_week' => 'Dempuèi la setmana passada',
 		'last_year' => 'Dempuèi l’annada passada',
-		'Mar' => 'm\a\r\ç',
+		'Mar' => '\\m\\a\\r\\ç',
 		'mar' => 'març',
 		'march' => 'març',
-		'May' => '\m\a\i',
+		'May' => '\\m\\a\\i',
 		'may' => 'mai',
 		'may_' => 'mai',
 		'mon' => 'dl',
 		'month' => 'meses',
-		'Nov' => '\n\o\v\e\m\b\r\e',
+		'Nov' => '\\n\\o\\v\\e\\m\\b\\r\\e',
 		'nov' => 'nov.',
 		'november' => 'novembre',
-		'Oct' => '\o\c\t\ò\b\r\e',
+		'Oct' => '\\o\\c\\t\\ò\\b\\r\\e',
 		'oct' => 'oct.',
 		'october' => 'octòbre',
 		'sat' => 'ds',
-		'Sep' => '\s\e\t\e\m\b\r\e',
+		'Sep' => '\\s\\e\\t\\e\\m\\b\\r\\e',
 		'sep' => 'set.',
 		'september' => 'setembre',
 		'sun' => 'dg',
@@ -111,7 +111,7 @@ return array(
 			'request_failed' => 'Una requèsta a fach meuca, aquò pòt venir d’un problèma de connexion Internet.',
 			'title_new_articles' => 'FreshRSS : nòus articles !',
 		),
-		'new_article' => 'I a d’articles nòus disponibles, clicatz per actualizar la página.',
+		'new_article' => 'I a d’articles nòus disponibles, clicatz per actualizar la pagina.',
 		'should_be_activated' => 'JavaScript deu èsser activat',
 	),
 	'lang' => array(
@@ -183,7 +183,7 @@ return array(
 	'short' => array(
 		'attention' => 'Atencion !',
 		'blank_to_disable' => 'Daissar void per desactivar',
-		'by_author' => 'Per <em>%s</em>',
+		'by_author' => 'Per : ',
 		'by_default' => 'Per defaut',
 		'damn' => 'Zut !',
 		'default_category' => 'Pas triat',

+ 7 - 6
app/i18n/oc/sub.php

@@ -1,7 +1,7 @@
 <?php
 return array(
 	'api' => array(
-		'documentation' => 'Copiar l’URL seguenta per l’utilizaire dins d’una aisina extèrna.',
+		'documentation' => 'Copiatz l’URL seguenta per l’utilizaire dins d’una aisina extèrna.',
 		'title' => 'API',
 	),
 	'bookmarklet' => array(
@@ -46,11 +46,11 @@ return array(
 		),
 		'websub' => 'Notificaciones instantáneas amb WebSub',
 		'show' => array(
-			'all' => 'Show all feeds',	//TODO - Translation
-			'error' => 'Show only feeds with error',	//TODO - Translation
+			'all' => 'Mostrar totes los fluxes',
+			'error' => 'Mostrar pas que los fluxes amb errors',
 		),
 		'showing' => array(
-			'error' => 'Showing only feeds with error',	//TODO - Translation
+			'error' => 'Afichatge dels articles amb errors solament',
 		),
 		'ssl_verify' => 'Verificacion de la seguretat SSL',
 		'stats' => 'Estatisticas',
@@ -64,13 +64,14 @@ return array(
 		'website' => 'URL del site',
 	),
 	'firefox' => array(
-		'documentation' => 'Seguir las etapas descrichas <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">aquí</a> per ajustar FreshRSS a la lista dels lectors de flux de Firefox.',
+		'documentation' => 'Seguissètz las etapas descrichas <a href="https://developer.mozilla.org/en-US/Firefox/Releases/2/Adding_feed_readers_to_Firefox#Adding_a_new_feed_reader_manually">aquí</a> per ajustar FreshRSS a la lista dels lectors de flux de Firefox.',
 		'title' => 'Lector de flux de Firefox',
 	),
 	'import_export' => array(
 		'export' => 'Exportar',
 		'export_opml' => 'Exportar la lista de fluxes (OPML)',
 		'export_starred' => 'Exportar los favorits',
+		'export_labelled' => 'Exportar los articles etiquetats',
 		'feed_list' => 'Lista dels %s articles',
 		'file_to_import' => 'Fichièr d’importar<br />(OPML, JSON o ZIP)',
 		'file_to_import_no_zip' => 'Fichièr d’importar<br />(OPML o JSON)',
@@ -86,7 +87,7 @@ return array(
 		'subscription_tools' => 'Aisinas d’abonament',
 	),
 	'title' => array(
-		'_' => 'Gestión dels abonaments',
+		'_' => 'Gestion dels abonaments',
 		'feed_management' => 'Gestion dels fluxes RSS',
 		'subscription_tools' => 'Aisinas d’abonament',
 	),

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

@@ -68,6 +68,7 @@ return array(
 		'export' => 'Exportar',
 		'export_opml' => 'Exporta a lista dos feeds (OPML)',
 		'export_starred' => 'Exportar seus favoritos',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'Lista dos %s artigos',
 		'file_to_import' => 'Arquivo para importar<br />(OPML, JSON or ZIP)',
 		'file_to_import_no_zip' => 'Arquivo para importar<br />(OPML or JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Export',	//TODO - Translation
 		'export_opml' => 'Export list of feeds (OPML)',	//TODO - Translation
 		'export_starred' => 'Export your favourites',	//TODO - Translation
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => 'List of %s articles',	//TODO - Translation
 		'file_to_import' => 'File to import<br />(OPML, JSON or ZIP)',	//TODO - Translation
 		'file_to_import_no_zip' => 'File to import<br />(OPML or JSON)',	//TODO - Translation

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

@@ -72,6 +72,7 @@ return array(
 		'export' => 'Dışa aktar',
 		'export_opml' => 'Akış listesini dışarı aktar (OPML)',
 		'export_starred' => 'Favorileri dışarı aktar',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => '%s makalenin listesi',
 		'file_to_import' => 'Dosyadan içe aktar<br />(OPML, JSON or ZIP)',
 		'file_to_import_no_zip' => 'Dosyadan içe aktar<br />(OPML or JSON)',

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

@@ -72,6 +72,7 @@ return array(
 		'export' => '导出',
 		'export_opml' => '导出 RSS 源列表 (OPML)',
 		'export_starred' => '导出你的收藏',
+		'export_labelled' => 'Export your labelled articles',	//TODO
 		'feed_list' => '%s 文章列表',
 		'file_to_import' => '需要导入的文件<br />(OPML, JSON 或 ZIP)',
 		'file_to_import_no_zip' => '需要导入的文件<br />(OPML 或 JSON)',

+ 2 - 1
app/install.php

@@ -7,7 +7,8 @@ header("Content-Security-Policy: default-src 'self'");
 require(LIB_PATH . '/lib_install.php');
 
 session_name('FreshRSS');
-session_set_cookie_params(0, dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
+$forwardedPrefix = empty($_SERVER['HTTP_X_FORWARDED_PREFIX']) ? '' : rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ ');
+session_set_cookie_params(0, $forwardedPrefix . dirname(empty($_SERVER['REQUEST_URI']) ? '/' : dirname($_SERVER['REQUEST_URI'])), null, false, true);
 session_start();
 
 if (isset($_GET['step'])) {

+ 1 - 1
app/layout/header.phtml

@@ -16,7 +16,7 @@ if (FreshRSS_Auth::accessNeedsAction()) {
 	<div class="item title">
 		<h1>
 			<a href="<?php echo _url('index', 'index'); ?>">
-				<img class="logo" src="<?php echo _i('icon', true); ?>" alt="" />
+				<img class="logo" src="<?php echo _i('icon', true); ?>" alt="" />
 				<?php echo FreshRSS_Context::$system_conf->title; ?>
 			</a>
 		</h1>

+ 1 - 1
app/views/configure/display.phtml

@@ -72,7 +72,7 @@
 		</div>
 
 		<div class="form-group">
-			<label class="group-name" for="theme"><?php echo _t('conf.display.icon.entry'); ?></label>
+			<label class="group-name"><?php echo _t('conf.display.icon.entry'); ?></label>
 			<table>
 				<thead>
 					<tr>

+ 5 - 6
app/views/helpers/export/articles.phtml

@@ -16,14 +16,12 @@ $articles = array(
 echo rtrim(json_encode($articles, $options), " ]}\n\r\t"), "\n";
 $first = true;
 
-$tagDAO = FreshRSS_Factory::createTagDao();
-$entryIdsTagNames = $tagDAO->getEntryIdsTagNames($this->entriesRaw);
-if ($entryIdsTagNames == false) {
-	$entryIdsTagNames = array();
+if (empty($this->entryIdsTagNames)) {
+	$this->entryIdsTagNames = array();
 }
 
 foreach ($this->entriesRaw as $entryRaw) {
-	if (empty($entryRaw)) {
+	if ($entryRaw == null) {
 		continue;
 	}
 	$entry = FreshRSS_EntryDAO::daoToEntry($entryRaw);
@@ -58,10 +56,11 @@ foreach ($this->entriesRaw as $entryRaw) {
 			'feedUrl' => $feed == null ? '' : $feed->url(),
 		)
 	);
+	$article['categories'][] = $entry->isRead() ? 'user/-/state/com.google/read' : 'user/-/state/com.google/unread';
 	if ($entry->isFavorite()) {
 		$article['categories'][] = 'user/-/state/com.google/starred';
 	}
-	$tagNames = isset($entryIdsTagNames['e_' . $entry->id()]) ? $entryIdsTagNames['e_' . $entry->id()] : array();
+	$tagNames = isset($this->entryIdsTagNames['e_' . $entry->id()]) ? $this->entryIdsTagNames['e_' . $entry->id()] : array();
 	foreach ($tagNames as $tagName) {
 		$article['categories'][] = 'user/-/label/' . $tagName;
 	}

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

@@ -33,6 +33,11 @@
 					<?php echo _t('sub.import_export.export_opml'); ?>
 				</label>
 
+				<label class="checkbox" for="export_labelled">
+					<input type="checkbox" name="export_labelled" id="export_labelled" value="1" <?php echo extension_loaded('zip') ? 'checked="checked"' : ''; ?> />
+					<?php echo _t('sub.import_export.export_labelled'); ?>
+				</label>
+
 				<label class="checkbox" for="export_starred">
 					<input type="checkbox" name="export_starred" id="export_starred" value="1" <?php echo extension_loaded('zip') ? 'checked="checked"' : ''; ?> />
 					<?php echo _t('sub.import_export.export_starred'); ?>

+ 1 - 1
cli/_cli.php

@@ -4,7 +4,7 @@ if (php_sapi_name() !== 'cli') {
 }
 
 require(__DIR__ . '/../constants.php');
-require(LIB_PATH . '/lib_rss.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
 require(LIB_PATH . '/lib_install.php');
 
 Minz_Configuration::register('system',

+ 1 - 1
cli/export-opml-for-user.php

@@ -17,7 +17,7 @@ fwrite(STDERR, 'FreshRSS exporting OPML for user “' . $username . "”…\n");
 $importController = new FreshRSS_importExport_Controller();
 
 $ok = false;
-$ok = $importController->exportFile(true, false, array(), 0, $username);
+$ok = $importController->exportFile(true, false, false, array(), 0, $username);
 
 invalidateHttpCache($username);
 

+ 1 - 1
cli/export-zip-for-user.php

@@ -19,7 +19,7 @@ $importController = new FreshRSS_importExport_Controller();
 
 $ok = false;
 try {
-	$ok = $importController->exportFile(true, true, true,
+	$ok = $importController->exportFile(true, true, true, true,
 		empty($options['max-feed-entries']) ? 100 : intval($options['max-feed-entries']),
 		$username);
 } catch (FreshRSS_ZipMissing_Exception $zme) {

+ 4 - 1
constants.php

@@ -2,7 +2,7 @@
 //NB: Do not edit; use ./constants.local.php instead.
 
 //<Not customisable>
-define('FRESHRSS_VERSION', '1.13.0');
+define('FRESHRSS_VERSION', '1.13.1');
 define('FRESHRSS_WEBSITE', 'https://freshrss.org');
 define('FRESHRSS_WIKI', 'https://freshrss.github.io/FreshRSS/');
 
@@ -32,6 +32,9 @@ safe_define('FRESHRSS_USERAGENT', 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS
 // PHP text output compression http://php.net/ob_gzhandler (better to do it at Web server level)
 safe_define('PHP_COMPRESSION', false);
 
+// For cases when syslog is not available
+safe_define('COPY_SYSLOG_TO_STDERR', isset($_SERVER['COPY_SYSLOG_TO_STDERR']) ? filter_var($_SERVER['COPY_SYSLOG_TO_STDERR'], FILTER_VALIDATE_BOOLEAN) : false);
+
 // Maximum log file size in Bytes, before it will be divided by two
 safe_define('MAX_LOG_SIZE', 1048576);
 

+ 1 - 0
data/.gitignore

@@ -1,3 +1,4 @@
+.htpasswd
 config.php
 config.php.bak.php
 force-https.txt

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

@@ -29,7 +29,7 @@ See the [page about the Fever compatible API](06_Fever_API.md) for another possi
 	* If you get *Service Unavailable!*, then check from step 1 again.
 	* With __Apache__:
 		* If you get *FAIL getallheaders!*, the combination of your PHP version and your Web server does not provide access to [`getallheaders`](http://php.net/getallheaders)
-			* Update to PHP 5.4+, or use PHP as module instead of CGI. Otherwise turn on Apache `mod_rewrite`:
+			* Update to PHP 5.4+, or use PHP as module instead of CGI. Otherwise turn on Apache `mod_setenvif` (often enabled by default), or `mod_rewrite` with the following procedure:
 				* Allow [`FileInfo` in `.htaccess`](http://httpd.apache.org/docs/trunk/mod/core.html#allowoverride): see the [server setup](../admins/02_Installation.md) again.
 				* Enable [`mod_rewrite`](http://httpd.apache.org/docs/trunk/mod/mod_rewrite.html):
 					* With Debian / Ubuntu: `sudo a2enmod rewrite`

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

@@ -29,7 +29,7 @@ Voir la [page sur l’API compatible Fever](06_Fever_API.md) pour une autre poss
 	* Si vous obtenez *Service Unavailable!*, retourner à l’étape 6.
 	* Avec __Apache__:
 		* Si vous obtenez *FAIL getallheaders!*, alors la combinaison de votre version de PHP et de votre serveur Web ne permet pas l’accès à [`getallheaders`](http://php.net/getallheaders)
-			* Utilisez au moins PHP 5.4+, ou utilisez PHP en tant que module plutôt que CGI. Sinon, activer Apache `mod_rewrite` :
+			* Utilisez au moins PHP 5.4+, ou utilisez PHP en tant que module plutôt que CGI. Sinon, activer Apache `mod_setenvif` (souvent activé par défault), ou `mod_rewrite` avec la procédure suivante :
 				* Autoriser [`FileInfo` dans `.htaccess`](http://httpd.apache.org/docs/trunk/mod/core.html#allowoverride) : revoir [l’installation du serveur](01_Installation.md).
 				* Activer [`mod_rewrite`](http://httpd.apache.org/docs/trunk/mod/mod_rewrite.html) :
 					* Sur Debian / Ubuntu : `sudo a2enmod rewrite`
@@ -72,7 +72,7 @@ Tout client supportant une API de type Google Reader. Sélection :
 
 # API compatible Google Reader
 
-Exemples de requêtes simples:
+Exemples de requêtes simples :
 
 ```sh
 # Authentification utilisant le mot de passe API (Email et Passwd peuvent être passés en GET, ou POST - mieux)

+ 5 - 12
lib/Minz/Configuration.php

@@ -27,23 +27,16 @@ class Minz_Configuration {
 	/**
 	 * Parse a file and return its data.
 	 *
-	 * If the file does not contain a valid PHP code returning an array, an
-	 * empty array is returned anyway.
-	 *
 	 * @param $filename the name of the file to parse.
 	 * @return an array of values
-	 * @throws Minz_FileNotExistException if the file does not exist.
+	 * @throws Minz_FileNotExistException if the file does not exist or is invalid.
 	 */
 	public static function load($filename) {
-		if (!file_exists($filename)) {
-			throw new Minz_FileNotExistException($filename);
-		}
-
-		$data = include($filename);
+		$data = @include($filename);
 		if (is_array($data)) {
 			return $data;
 		} else {
-			return array();
+			throw new Minz_FileNotExistException($filename);
 		}
 	}
 
@@ -117,7 +110,7 @@ class Minz_Configuration {
 		$this->default_filename = $default_filename;
 		$this->_configurationSetter($configuration_setter);
 
-		if (!is_null($this->default_filename)) {
+		if ($this->default_filename != null) {
 			$this->data = self::load($this->default_filename);
 		}
 
@@ -126,7 +119,7 @@ class Minz_Configuration {
 				$this->data, self::load($this->config_filename)
 			);
 		} catch (Minz_FileNotExistException $e) {
-			if (is_null($this->default_filename)) {
+			if ($this->default_filename == null) {
 				throw $e;
 			}
 		}

+ 5 - 1
lib/Minz/Session.php

@@ -61,7 +61,11 @@ class Minz_Session {
 
 	public static function getCookieDir() {
 		// Get the script_name (e.g. /p/i/index.php) and keep only the path.
-		$cookie_dir = empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI'];
+		$cookie_dir = '';
+		if (!empty($_SERVER['HTTP_X_FORWARDED_PREFIX'])) {
+			$cookie_dir .= rtrim($_SERVER['HTTP_X_FORWARDED_PREFIX'], '/ ');
+		}
+		$cookie_dir .= empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI'];
 		if (substr($cookie_dir, -1) !== '/') {
 			$cookie_dir = dirname($cookie_dir) . '/';
 		}

+ 1 - 0
lib/favicons.php

@@ -22,6 +22,7 @@ function isImgMime($content) {
 }
 
 function downloadHttp(&$url, $curlOptions = array()) {
+	prepareSyslog();
 	syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $url);
 	if (substr($url, 0, 2) === '//') {
 		$url = 'https:' . $url;

+ 1 - 0
lib/lib_install.php

@@ -81,6 +81,7 @@ function generateSalt() {
 function checkDb(&$dbOptions) {
 	$dsn = '';
 	$driver_options = null;
+	prepareSyslog();
 	try {
 		switch ($dbOptions['type']) {
 		case 'mysql':

+ 13 - 7
lib/lib_rss.php

@@ -202,12 +202,19 @@ function html_only_entity_decode($text) {
 	return strtr($text, $htmlEntitiesOnly);
 }
 
+function prepareSyslog() {
+	return COPY_SYSLOG_TO_STDERR ? openlog("FreshRSS", LOG_PERROR | LOG_PID, LOG_USER) : false;
+}
+
 function customSimplePie($attributes = array()) {
 	$system_conf = Minz_Configuration::get('system');
 	$limits = $system_conf->limits;
 	$simplePie = new SimplePie();
 	$simplePie->set_useragent(FRESHRSS_USERAGENT);
 	$simplePie->set_syslog($system_conf->simplepie_syslog_enabled);
+	if ($system_conf->simplepie_syslog_enabled) {
+		prepareSyslog();
+	}
 	$simplePie->set_cache_location(CACHE_PATH);
 	$simplePie->set_cache_duration($limits['cache_duration']);
 
@@ -364,9 +371,9 @@ function get_user_configuration($username) {
 		                             join_path(FRESHRSS_PATH, 'config-user.default.php'));
 	} catch (Minz_ConfigurationNamespaceException $e) {
 		// namespace already exists, do nothing.
-		Minz_Log::warning($e->getMessage());
+		Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
 	} catch (Minz_FileNotExistException $e) {
-		Minz_Log::warning($e->getMessage());
+		Minz_Log::warning($e->getMessage(), USERS_PATH . '/_/log.txt');
 		return null;
 	}
 
@@ -375,14 +382,13 @@ function get_user_configuration($username) {
 
 
 function httpAuthUser() {
-	if (isset($_SERVER['REMOTE_USER'])) {
+	if (!empty($_SERVER['REMOTE_USER'])) {
 		return $_SERVER['REMOTE_USER'];
-	}
-
-	if (isset($_SERVER['REDIRECT_REMOTE_USER'])) {
+	} elseif (!empty($_SERVER['REDIRECT_REMOTE_USER'])) {
 		return $_SERVER['REDIRECT_REMOTE_USER'];
+	} elseif (!empty($_SERVER['HTTP_X_WEBAUTH_USER'])) {
+		return $_SERVER['HTTP_X_WEBAUTH_USER'];
 	}
-
 	return '';
 }
 

+ 8 - 3
p/api/.htaccess

@@ -1,4 +1,9 @@
-<IfModule mod_rewrite.c>
-	RewriteEngine on
-	RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+<IfModule mod_setenvif.c>
+	SetEnvIf "^Authorization$" "(.*)" HTTP_AUTHORIZATION=$1
+</IfModule>
+<IfModule !mod_setenvif.c>
+	<IfModule mod_rewrite.c>
+		RewriteEngine on
+		RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+	</IfModule>
 </IfModule>

+ 9 - 12
p/api/greader.php

@@ -143,14 +143,11 @@ function checkCompatibility() {
 	Minz_Log::warning('checkCompatibility() ' . debugInfo(), API_LOG);
 	header('Content-Type: text/plain; charset=UTF-8');
 	if (PHP_INT_SIZE < 8 && !function_exists('gmp_init')) {
-		die('FAIL 64-bit or GMP extension!');
+		die('FAIL 64-bit or GMP extension! Wrong PHP configuration.');
 	}
-	if ((!array_key_exists('HTTP_AUTHORIZATION', $_SERVER)) &&	//Apache mod_rewrite trick should be fine
-		(!array_key_exists('REDIRECT_HTTP_AUTHORIZATION', $_SERVER)) &&	//Apache mod_rewrite with FCGI
-		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'nginx') === false)) &&	//nginx should be fine
-		(empty($_SERVER['SERVER_SOFTWARE']) || (stripos($_SERVER['SERVER_SOFTWARE'], 'lighttpd') === false)) &&	//lighttpd should be fine
-		((!function_exists('getallheaders')) || (stripos(php_sapi_name(), 'cgi') !== false))) {	//Main problem is Apache/CGI mode
-		die('FAIL getallheaders! (probably)');
+	$headerAuth = headerVariable('Authorization', 'GoogleLogin_auth');
+	if ($headerAuth == '') {
+		die('FAIL get HTTP Authorization header! Wrong Web server configuration.');
 	}
 	echo 'PASS';
 	exit();
@@ -913,6 +910,10 @@ FreshRSS_Context::$system_conf = Minz_Configuration::get('system');
 
 if (!FreshRSS_Context::$system_conf->api_enabled) {
 	serviceUnavailable();
+} elseif (count($pathInfos) < 3) {
+	badRequest();
+} elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
+	checkCompatibility();
 }
 
 ini_set('session.use_cookies', '0');
@@ -927,9 +928,7 @@ if ($user !== '') {
 
 Minz_Session::_param('currentUser', $user);
 
-if (count($pathInfos) < 3) {
-	badRequest();
-} elseif ($pathInfos[1] === 'accounts') {
+if ($pathInfos[1] === 'accounts') {
 	if (($pathInfos[2] === 'ClientLogin') && isset($_REQUEST['Email']) && isset($_REQUEST['Passwd'])) {
 		clientLogin($_REQUEST['Email'], $_REQUEST['Passwd']);
 	}
@@ -1088,8 +1087,6 @@ if (count($pathInfos) < 3) {
 			userInfo();
 			break;
 	}
-} elseif ($pathInfos[1] === 'check' && $pathInfos[2] === 'compatibility') {
-	checkCompatibility();
 }
 
 badRequest();

+ 16 - 11
p/api/index.php

@@ -5,6 +5,18 @@
 <title>FreshRSS API endpoints</title>
 <meta name="robots" content="noindex" />
 <link rel="start" href="../i/" />
+<script src="../scripts/api.js" defer="defer"></script>
+<script id="jsonVars" type="application/json">
+<?php
+require(__DIR__ . '/../../constants.php');
+require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
+Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
+echo json_encode(array(
+		'greader' => Minz_Url::display('/api/greader.php', 'php', true),
+		'fever' => Minz_Url::display('/api/fever.php', 'php', true),
+	));
+?>
+</script>
 </head>
 
 <body>
@@ -14,17 +26,11 @@
 <dl>
 <dt>Your API address:</dt>
 <dd><?php
-require(__DIR__ . '/../../constants.php');
-require(LIB_PATH . '/lib_rss.php');	//Includes class autoloader
-Minz_Configuration::register('system', DATA_PATH . '/config.php', FRESHRSS_PATH . '/config.default.php');
 echo Minz_Url::display('/api/greader.php', 'html', true);
 ?></dd>
+<dt>Google Reader API configuration test:</dt>
+<dd id="greaderOutput">?</dd>
 </dl>
-<ul>
-<li><a href="greader.php/check%2Fcompatibility" rel="nofollow">Check full server configuration</a></li>
-<li><a href="greader.php/check/compatibility" rel="nofollow">Check partial server
-configuration (without <code>%2F</code> support)</a></li>
-</ul>
 
 <h2>Fever compatible API</h2>
 <dl>
@@ -32,10 +38,9 @@ configuration (without <code>%2F</code> support)</a></li>
 <dd><?php
 echo Minz_Url::display('/api/fever.php', 'html', true);
 ?></dd>
+<dt>Fever API configuration test:</dt>
+<dd id="feverOutput">?</dd>
 </dl>
-<ul>
-<li><a href="fever.php?api" rel="nofollow">Test</a></li>
-</ul>
 
 </body>
 </html>

+ 1 - 1
p/i/.gitignore

@@ -1 +1 @@
-.htaccess
+.ht*

+ 2 - 0
p/i/index.php

@@ -50,5 +50,7 @@ if (file_exists(DATA_PATH . '/do-install.txt')) {
 		echo '### Fatal error! ###<br />', "\n";
 		Minz_Log::error($e->getMessage());
 		echo 'See logs files.';
+		prepareSyslog();
+		syslog(LOG_INFO, 'FreshRSS Fatal error! ' . $e->getMessage());
 	}
 }

+ 62 - 0
p/scripts/api.js

@@ -0,0 +1,62 @@
+"use strict";
+/* jshint esversion:6, strict:global */
+
+function check(url, next) {
+	if (!url || !next) {
+		return;
+	}
+	const req = new XMLHttpRequest();
+	req.open('GET', url, true);
+	req.setRequestHeader('Authorization', 'GoogleLogin auth=test/1');
+	req.onerror = function (e) {
+			next('FAIL: HTTP ' + e);
+		};
+	req.onload = function () {
+		if (this.status == 200) {
+			next(this.response);
+		} else {
+			next('FAIL: HTTP error ' + this.status + ' ' + this.statusText);
+		}
+	};
+	req.send();
+}
+
+const jsonVars = JSON.parse(document.getElementById('jsonVars').innerHTML);
+
+check(jsonVars.greader + '/check/compatibility', function next(result1) {
+		const greaderOutput = document.getElementById('greaderOutput');
+		if (result1 === 'PASS') {
+			greaderOutput.innerHTML = '✔️ ' + result1;
+		} else {
+			check(jsonVars.greader + '/check%2Fcompatibility', function next(result2) {
+				if (result2 === 'PASS') {
+					greaderOutput.innerHTML = '⚠️ WARN: no <code>%2F</code> support, so some clients will not work!';
+				} else {
+					check('./greader.php/check/compatibility', function next(result3) {
+						if (result3 === 'PASS') {
+							greaderOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
+						} else {
+							greaderOutput.innerHTML = '❌ ' + result1;
+						}
+					});
+				}
+			});
+		}
+	});
+
+check(jsonVars.fever + '?api', function next(result1) {
+		const feverOutput = document.getElementById('feverOutput');
+		try {
+			JSON.parse(result1);
+			feverOutput.innerHTML = '✔️ PASS';
+		} catch (ex) {
+			check('./fever.php?api', function next(result2) {
+					try {
+						JSON.parse(result2);
+						feverOutput.innerHTML = '⚠️ WARN: Probable invalid base URL in ./data/config.php';
+					} catch (ex) {
+						feverOutput.innerHTML = '❌ ' + result1;
+					}
+				});
+		}
+	});

+ 9 - 3
p/scripts/main.js

@@ -237,6 +237,9 @@ function mark_favorite(active) {
 	});
 }
 
+var freshrssOpenArticleEvent = document.createEvent('Event');
+freshrssOpenArticleEvent.initEvent('freshrss:openArticle', true, true);
+
 function toggleContent(new_active, old_active, skipping) {
 	// If skipping, move current without activating or marking as read
 	if (new_active.length === 0) {
@@ -299,8 +302,11 @@ function toggleContent(new_active, old_active, skipping) {
 		}
 	}
 
-	if (context.auto_mark_article && new_active.hasClass('active') && !skipping) {
-		mark_read(new_active, true);
+	if (new_active.hasClass('active') && !skipping) {
+		if (context.auto_mark_article) {
+			mark_read(new_active, true);
+		}
+		new_active[0].dispatchEvent(freshrssOpenArticleEvent);
 	}
 }
 
@@ -543,7 +549,7 @@ function init_column_categories() {
 			}
 		});
 		$(this).parent().next(".tree-folder-items").slideToggle(300, function () {
-			//Workaround for Gecko bug in Firefox 64-65(+?):
+			//Workaround for Gecko bug 1514498 in Firefox 64
 			var sidebar = document.getElementById('sidebar');
 			if (sidebar && sidebar.scrollHeight > sidebar.clientHeight &&	//if needs scrollbar
 				sidebar.scrollWidth >= sidebar.offsetWidth) {	//but no scrollbar