Browse Source

Merge pull request #1810 from FreshRSS/dev

FreshRSS 1.10.1
Alexandre Alapetite 8 years ago
parent
commit
f0fd273199

+ 3 - 0
.dockerignore

@@ -0,0 +1,3 @@
+*/.git
+*/data
+*/docs

+ 20 - 0
CHANGELOG.md

@@ -1,5 +1,25 @@
 # FreshRSS changelog
 # FreshRSS changelog
 
 
+## 2018-03-04 FreshRSS 1.10.1
+
+* Deployment
+	* New Docker image, smaller (based on Alpine Linux) and newer (with PHP 7.1) [#1813](https://github.com/FreshRSS/FreshRSS/pull/1813)
+		* with [automated build](https://hub.docker.com/r/freshrss/freshrss/) for x86-64 (AMD64) architectures
+* CLI
+	* New command `./cli/prepare.php` to make the needed sub-directories of the `./data/` directory [#1813](https://github.com/FreshRSS/FreshRSS/pull/1813)
+* Bug fixing
+	* Fix API bug for EasyRSS [#1799](https://github.com/FreshRSS/FreshRSS/issues/1799)
+	* Fix login bug when using double authentication (HTTP + Web form) [#1807](https://github.com/FreshRSS/FreshRSS/issues/1807)
+	* Fix database upgrade for FreshRSS versions older than 1.1.1 [#1803](https://github.com/FreshRSS/FreshRSS/issues/1803)
+	* Fix cases of double port in FreshRSS public URL [#1815](https://github.com/FreshRSS/FreshRSS/pull/1815)
+* UI
+	* Add tooltips on share configuration buttons [#1805](https://github.com/FreshRSS/FreshRSS/pull/1805)
+* Misc.
+	* Move `./data/shares.php` to `./app/shares.php` to facilitate updates [#1812](https://github.com/FreshRSS/FreshRSS/pull/1812)
+	* Show article author email when there is no author name [#1801](https://github.com/FreshRSS/FreshRSS/pull/1801)
+	* Improve translation tools [#1808](https://github.com/FreshRSS/FreshRSS/pull/1808)
+
+
 ## 2018-02-24 FreshRSS 1.10.0
 ## 2018-02-24 FreshRSS 1.10.0
 
 
 * API
 * API

+ 1 - 0
CREDITS.md

@@ -44,6 +44,7 @@ People are sorted by name so please keep this order.
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [Nicolas Lœuillet](https://github.com/nicosomb): [contributions](https://github.com/FreshRSS/documentation/commits?author=nicosomb), [Web](http://www.loeuillet.org/)
 * [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Nicola Spanti](https://github.com/RyDroid): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:RyDroid), [Web](http://www.nicola-spanti.info/)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
 * [Olivier Dossmann](https://github.com/blankoworld): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=blankoworld), [Web](https://olivier.dossmann.net)
+* [perrinjerome](https://github.com/perrinjerome): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:perrinjerome)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [plopoyop](https://github.com/plopoyop): [contributions](https://github.com/FreshRSS/FreshRSS/commits?author=plopoyop)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [Paulius Šukys](https://github.com/psukys): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:psukys), [Web](http://sukys.eu)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)
 * [purexo](https://github.com/purexo): [contributions](https://github.com/FreshRSS/FreshRSS/pulls?q=is:pr+author:purexo), [Web](https://purexo.mom/)

+ 22 - 0
Docker/Dockerfile

@@ -0,0 +1,22 @@
+FROM alpine:3.7
+
+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-json php7-session \
+	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}
+
+COPY . ${FRESHRSS_ROOT}
+COPY ./Docker/*.Apache.conf /etc/apache2/conf.d/
+
+EXPOSE 80
+CMD php -f ./cli/prepare.php > /dev/null && \
+	chown -R :www-data ${FRESHRSS_ROOT} && \
+	chmod -R g+r ${FRESHRSS_ROOT} && chmod -R g+w ${FRESHRSS_ROOT}/data/ && \
+	exec httpd -D FOREGROUND

+ 27 - 0
Docker/FreshRSS.Apache.conf

@@ -0,0 +1,27 @@
+<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/
+ErrorLog /dev/stderr
+TransferLog /dev/stdout
+AllowEncodedSlashes On
+
+<Directory /var/www/FreshRSS/p>
+	AllowOverride AuthConfig FileInfo Indexes Limit
+	Require all granted
+</Directory>

+ 119 - 0
Docker/README.md

@@ -0,0 +1,119 @@
+# Deploy FreshRSS with Docker
+* See also:
+	* https://hub.docker.com/r/freshrss/freshrss/
+	* https://cloud.docker.com/app/freshrss/repository/docker/freshrss/freshrss
+
+## Install Docker
+
+```sh
+curl -fsSL https://get.docker.com/ -o get-docker.sh
+sh get-docker.sh
+```
+
+## Optional: Build Docker image of FreshRSS
+Optional, as a *less recent* online image can be automatically fetched during the next step (run),
+but online images are not available for as many platforms as if you build yourself.
+
+```sh
+# First time only
+git clone https://github.com/FreshRSS/FreshRSS.git
+
+cd ./FreshRSS/
+git pull
+sudo docker pull alpine:3.7
+sudo docker build --tag freshrss/freshrss -f Docker/Dockerfile .
+```
+
+## Run FreshRSS
+
+Example using SQLite, and exposing FreshRSS on port 8080. You may have to adapt the network parameters to fit your needs.
+
+```sh
+# You can optionally run from the directory containing the FreshRSS source code:
+cd ./FreshRSS/
+
+# The data will be saved on the host in `./data/`
+mkdir -p ./data/
+
+sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
+	-v $(pwd)/data:/var/www/FreshRSS/data \
+	-p 8080:80 \
+	--name freshrss freshrss/freshrss
+```
+
+### Examples with external databases
+
+You may want to use other link methods such as Docker bridges, and use Docker volumes for the data, but here are some simple examples:
+
+#### MySQL
+See https://hub.docker.com/_/mysql/
+
+```sh
+sudo docker run -d -v /path/to/mysql-data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=rootpass -e MYSQL_DATABASE=freshrss -e MYSQL_USER=freshrss -e MYSQL_PASSWORD=pass --name mysql mysql
+sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
+	-v $(pwd)/data:/var/www/FreshRSS/data \
+	--link mysql -p 8080:80 \
+	--name freshrss freshrss/freshrss
+```
+
+#### PostgreSQL
+See https://hub.docker.com/_/postgres/
+
+```sh
+sudo docker run -d -v /path/to/pgsql-data:/var/lib/postgresql/data -e POSTGRES_DB=freshrss -e POSTGRES_USER=freshrss -e POSTGRES_PASSWORD=pass --name postgres postgres
+sudo docker run -dit --restart unless-stopped --log-opt max-size=10m \
+	-v $(pwd)/data:/var/www/FreshRSS/data \
+	--link postgres -p 8080:80 \
+	--name freshrss freshrss/freshrss
+```
+
+## Update
+
+```sh
+# Rebuild an image (see build section above) or get a new online version:
+sudo docker pull freshrss/freshrss
+# And then 
+sudo docker stop freshrss
+sudo docker rename freshrss freshrss_old
+# See the run section above for the full command
+sudo docker run ...
+# If everything is working, delete the old container
+sudo docker rm freshrss_old
+```
+
+## Command line
+
+```sh
+sudo docker exec --user apache -it freshrss php ./cli/list-users.php
+```
+
+See the [CLI documentation](../cli/) for all the other commands.
+
+### Cron job to refresh feeds
+Set a cron job up on your host machine, calling the `actualize_script.php` inside the FreshRSS Docker instance.
+
+#### Example on Debian / Ubuntu
+Create `/etc/cron.d/FreshRSS` with:
+
+```
+7,37 * * * * root docker exec --user apache -it freshrss php ./app/actualize_script.php > /tmp/FreshRSS.log 2>&1
+```
+
+## Debugging
+
+```sh
+# See FreshRSS data (it is on the host)
+cd ./data/
+# See Web server logs
+sudo docker logs -f freshrss
+
+# Enter inside FreshRSS docker container
+sudo docker exec -it freshrss sh
+## See FreshRSS root inside the container
+ls /var/www/FreshRSS/
+```
+
+## Deployment in production
+
+Use a reverse proxy on your host server, such as [Træfik](https://traefik.io/) or [nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/),
+with HTTPS, for instance using [Let’s Encrypt](https://letsencrypt.org/).

+ 3 - 1
README.fr.md

@@ -55,6 +55,8 @@ Nous sommes une communauté amicale.
 7. Avec Apache, activer [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) pour une meilleure compatibilité avec les clients mobiles.
 7. Avec Apache, activer [`AllowEncodedSlashes`](https://httpd.apache.org/docs/trunk/mod/core.html#allowencodedslashes) pour une meilleure compatibilité avec les clients mobiles.
 
 
 ## Installation automatisée
 ## Installation automatisée
+* [Docker](./Docker/)
+* [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
 * [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
 * [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
 * [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 * [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 
 
@@ -172,7 +174,7 @@ Voir le [dépôt dédié à ces extensions](https://github.com/FreshRSS/Extensio
 * [password_compat](https://github.com/ircmaxell/password_compat)
 * [password_compat](https://github.com/ircmaxell/password_compat)
 
 
 
 
-# [Clients compatibles](https://freshrss.github.io/FreshRSS/en/users/06_Mobile_access.html)
+# [Clients compatibles](https://freshrss.github.io/FreshRSS/fr/users/06_Mobile_access.html)
 Tout client supportant une API de type Google Reader. Sélection :
 Tout client supportant une API de type Google Reader. Sélection :
 
 
 * Android
 * Android

+ 4 - 2
README.md

@@ -1,5 +1,6 @@
 [![Build Status][travis-badge]][travis-link]
 [![Build Status][travis-badge]][travis-link]
 
 
+* Read this document on [github.com/FreshRSS/FreshRSS/](https://github.com/FreshRSS/FreshRSS/blob/master/README.md) to get the correct links and pictures.
 * [Version française](README.fr.md)
 * [Version française](README.fr.md)
 
 
 # FreshRSS
 # FreshRSS
@@ -46,7 +47,7 @@ We are a friendly community.
 # Documentation
 # Documentation
 * https://freshrss.github.io/FreshRSS/en/
 * https://freshrss.github.io/FreshRSS/en/
 
 
-# [Installation](https://freshrss.github.io/FreshRSS/en/users/01_Installation.html)
+# [Installation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html)
 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 1. Get FreshRSS with git or [by downloading the archive](https://github.com/FreshRSS/FreshRSS/archive/master.zip)
 2. Dump the application on your server (expose only the `./p/` folder)
 2. Dump the application on your server (expose only the `./p/` folder)
 3. Add write access on `./data/` folder to the webserver user
 3. Add write access on `./data/` folder to the webserver user
@@ -59,7 +60,8 @@ We are a friendly community.
 More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html). 
 More information about installation and server configuration can be found in [our documentation](https://freshrss.github.io/FreshRSS/en/admins/02_Installation.html). 
 
 
 ## Automated install
 ## Automated install
-* [![Install on Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
+* [Docker](./Docker/)
+* [![Cloudron](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=org.freshrss.cloudronapp)
 * [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
 * [![DP deploy](https://raw.githubusercontent.com/DFabric/DPlatform-ShellCore/gh-pages/img/deploy.png)](https://dfabric.github.io/DPlatform-ShellCore)
 * [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 * [YunoHost](https://github.com/YunoHost-Apps/freshrss_ynh)
 
 

+ 2 - 1
app/Controllers/entryController.php

@@ -169,6 +169,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
 		$nb_month_old = max(FreshRSS_Context::$user_conf->old_entries, 1);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 		$date_min = time() - (3600 * 24 * 30 * $nb_month_old);
 
 
+		$entryDAO = FreshRSS_Factory::createEntryDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feedDAO = FreshRSS_Factory::createFeedDao();
 		$feeds = $feedDAO->listFeeds();
 		$feeds = $feedDAO->listFeeds();
 		$nb_total = 0;
 		$nb_total = 0;
@@ -182,7 +183,7 @@ class FreshRSS_entry_Controller extends Minz_ActionController {
 			}
 			}
 
 
 			if ($feed_history >= 0) {
 			if ($feed_history >= 0) {
-				$nb = $feedDAO->cleanOldEntries($feed->id(), $date_min, $feed_history);
+				$nb = $entryDAO->cleanOldEntries($feed->id(), $date_min, $feed_history);
 				if ($nb > 0) {
 				if ($nb > 0) {
 					$nb_total += $nb;
 					$nb_total += $nb;
 					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']');
 					Minz_Log::debug($nb . ' old entries cleaned in feed [' . $feed->url() . ']');

+ 1 - 1
app/Controllers/feedController.php

@@ -408,7 +408,7 @@ class FreshRSS_feed_Controller extends Minz_ActionController {
 					$entryDAO->beginTransaction();
 					$entryDAO->beginTransaction();
 				}
 				}
 
 
-				$nb = $feedDAO->cleanOldEntries($feed->id(),
+				$nb = $entryDAO->cleanOldEntries($feed->id(),
 				                                $date_min,
 				                                $date_min,
 				                                max($feed_history, count($entries) + 10));
 				                                max($feed_history, count($entries) + 10));
 				if ($nb > 0) {
 				if ($nb > 0) {

+ 1 - 1
app/FreshRSS.php

@@ -128,7 +128,7 @@ class FreshRSS extends Minz_FrontController {
 		}
 		}
 		header("X-Content-Type-Options: nosniff");
 		header("X-Content-Type-Options: nosniff");
 
 
-		FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php'));
+		FreshRSS_Share::load(join_path(APP_PATH, 'shares.php'));
 		self::loadStylesAndScripts();
 		self::loadStylesAndScripts();
 	}
 	}
 }
 }

+ 2 - 1
app/Models/Auth.php

@@ -63,7 +63,6 @@ class FreshRSS_Auth {
 			$login_ok = $current_user != '';
 			$login_ok = $current_user != '';
 			if ($login_ok) {
 			if ($login_ok) {
 				Minz_Session::_param('currentUser', $current_user);
 				Minz_Session::_param('currentUser', $current_user);
-				Minz_Session::_param('REMOTE_USER', $current_user);
 			}
 			}
 			return $login_ok;
 			return $login_ok;
 		case 'none':
 		case 'none':
@@ -102,6 +101,7 @@ class FreshRSS_Auth {
 		}
 		}
 
 
 		Minz_Session::_param('loginOk', self::$login_ok);
 		Minz_Session::_param('loginOk', self::$login_ok);
+		Minz_Session::_param('REMOTE_USER', httpAuthUser());
 	}
 	}
 
 
 	/**
 	/**
@@ -133,6 +133,7 @@ class FreshRSS_Auth {
 		self::$login_ok = false;
 		self::$login_ok = false;
 		Minz_Session::_param('loginOk');
 		Minz_Session::_param('loginOk');
 		Minz_Session::_param('csrf');
 		Minz_Session::_param('csrf');
+		Minz_Session::_param('REMOTE_USER');
 		$system_conf = Minz_Configuration::get('system');
 		$system_conf = Minz_Configuration::get('system');
 
 
 		$username = '';
 		$username = '';

+ 27 - 0
app/Models/EntryDAO.php

@@ -560,6 +560,33 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $affected;
 		return $affected;
 	}
 	}
 
 
+	public function cleanOldEntries($id_feed, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id_feed) or updateCachedValues() just after
+		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
+		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
+		     . 'AND is_favorite=0 '	//Do not remove favourites
+		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
+		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
+		$stm = $this->bd->prepare($sql);
+
+		if ($stm) {
+			$id_max = intval($date_min) . '000000';
+			$stm->bindParam(':id_feed', $id_feed, PDO::PARAM_INT);
+			$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
+			$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
+		}
+
+		if ($stm && $stm->execute()) {
+			return $stm->rowCount();
+		} else {
+			$info = $stm == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $stm->errorInfo();
+			if ($this->autoUpdateDb($info)) {
+				return $this->cleanOldEntries($id_feed, $date_min, $keep);
+			}
+			Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]);
+			return false;
+		}
+	}
+
 	public function searchByGuid($id_feed, $guid) {
 	public function searchByGuid($id_feed, $guid) {
 		// un guid est unique pour un flux donné
 		// un guid est unique pour un flux donné
 		$sql = 'SELECT id, guid, title, author, '
 		$sql = 'SELECT id, guid, title, author, '

+ 1 - 1
app/Models/Feed.php

@@ -355,7 +355,7 @@ class FreshRSS_Feed extends Minz_Model {
 				$this->id(),
 				$this->id(),
 				$item->get_id(false, false),
 				$item->get_id(false, false),
 				$title === null ? '' : $title,
 				$title === null ? '' : $title,
-				$author === null ? '' : html_only_entity_decode(strip_tags($author->name)),
+				$author === null ? '' : html_only_entity_decode(strip_tags($author->name == null ? $author->email : $author->name)),
 				$content === null ? '' : $content,
 				$content === null ? '' : $content,
 				$link === null ? '' : $link,
 				$link === null ? '' : $link,
 				$date ? $date : time()
 				$date ? $date : time()

+ 0 - 24
app/Models/FeedDAO.php

@@ -353,30 +353,6 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		return $affected;
 		return $affected;
 	}
 	}
 
 
-	public function cleanOldEntries($id, $date_min, $keep = 15) {	//Remember to call updateCachedValue($id) or updateCachedValues() just after
-		$sql = 'DELETE FROM `' . $this->prefix . 'entry` '
-		     . 'WHERE id_feed=:id_feed AND id<=:id_max '
-		     . 'AND is_favorite=0 '	//Do not remove favourites
-		     . 'AND `lastSeen` < (SELECT maxLastSeen FROM (SELECT (MAX(e3.`lastSeen`)-99) AS maxLastSeen FROM `' . $this->prefix . 'entry` e3 WHERE e3.id_feed=:id_feed) recent) '	//Do not remove the most newly seen articles, plus a few seconds of tolerance
-		     . 'AND id NOT IN (SELECT id FROM (SELECT e2.id FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=:id_feed ORDER BY id DESC LIMIT :keep) keep)';	//Double select: MySQL doesn't support 'LIMIT & IN/ALL/ANY/SOME subquery'
-		$stm = $this->bd->prepare($sql);
-
-		if ($stm) {
-			$id_max = intval($date_min) . '000000';
-			$stm->bindParam(':id_feed', $id, PDO::PARAM_INT);
-			$stm->bindParam(':id_max', $id_max, PDO::PARAM_STR);
-			$stm->bindParam(':keep', $keep, PDO::PARAM_INT);
-		}
-
-		if ($stm && $stm->execute()) {
-			return $stm->rowCount();
-		} else {
-			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
-			Minz_Log::error('SQL error cleanOldEntries: ' . $info[2]);
-			return false;
-		}
-	}
-
 	public static function daoToFeed($listDAO, $catID = null) {
 	public static function daoToFeed($listDAO, $catID = null) {
 		$list = array();
 		$list = array();
 
 

+ 2 - 0
app/i18n/cz/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Sdílení',
 		'_' => 'Sdílení',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Více informací',
 		'more_information' => 'Více informací',
 		'print' => 'Tisk',
 		'print' => 'Tisk',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Jméno pro zobrazení',
 		'share_name' => 'Jméno pro zobrazení',
 		'share_url' => 'Jakou URL použít pro sdílení',
 		'share_url' => 'Jakou URL použít pro sdílení',

+ 2 - 0
app/i18n/de/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Teilen',
 		'_' => 'Teilen',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'E-Mail',
 		'email' => 'E-Mail',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Weitere Informationen',
 		'more_information' => 'Weitere Informationen',
 		'print' => 'Drucken',
 		'print' => 'Drucken',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Anzuzeigender Teilen-Name',
 		'share_name' => 'Anzuzeigender Teilen-Name',
 		'share_url' => 'Zu verwendende Teilen-URL',
 		'share_url' => 'Zu verwendende Teilen-URL',

+ 2 - 0
app/i18n/en/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Sharing',
 		'_' => 'Sharing',
+		'add' => 'Add a sharing method',
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'More information',
 		'more_information' => 'More information',
 		'print' => 'Print',
 		'print' => 'Print',
+		'remove' => 'Remove sharing method',
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Share name to display',
 		'share_name' => 'Share name to display',
 		'share_url' => 'Share URL to use',
 		'share_url' => 'Share URL to use',

+ 2 - 0
app/i18n/es/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Compartir',
 		'_' => 'Compartir',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Más información',
 		'more_information' => 'Más información',
 		'print' => 'Print',
 		'print' => 'Print',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Compartir nombre a mostrar',
 		'share_name' => 'Compartir nombre a mostrar',
 		'share_url' => 'Compatir URL a usar',
 		'share_url' => 'Compatir URL a usar',

+ 2 - 0
app/i18n/fr/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Partage',
 		'_' => 'Partage',
+		'add' => 'Ajouter une méthode de partage',
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Courriel',
 		'email' => 'Courriel',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Plus d’informations',
 		'more_information' => 'Plus d’informations',
 		'print' => 'Print',
 		'print' => 'Print',
+		'remove' => 'Supprimer la méthode de partage',
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Nom du partage à afficher',
 		'share_name' => 'Nom du partage à afficher',
 		'share_url' => 'URL du partage à utiliser',
 		'share_url' => 'URL du partage à utiliser',

+ 2 - 0
app/i18n/he/conf.php

@@ -123,6 +123,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'שיתוף',
 		'_' => 'שיתוף',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'דואר אלקטרוני',
 		'email' => 'דואר אלקטרוני',
@@ -130,6 +131,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'מידע נוסף',
 		'more_information' => 'מידע נוסף',
 		'print' => 'הדפסה',
 		'print' => 'הדפסה',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'שיתוף שם לתצוגה',
 		'share_name' => 'שיתוף שם לתצוגה',
 		'share_url' => 'לשימוש שתפו URL',
 		'share_url' => 'לשימוש שתפו URL',

+ 2 - 0
app/i18n/it/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Condivisione',
 		'_' => 'Condivisione',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Ulteriori informazioni',
 		'more_information' => 'Ulteriori informazioni',
 		'print' => 'Stampa',
 		'print' => 'Stampa',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Nome condivisione',
 		'share_name' => 'Nome condivisione',
 		'share_url' => 'URL condivisione',
 		'share_url' => 'URL condivisione',

+ 2 - 0
app/i18n/kr/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => '공유',
 		'_' => '공유',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => '메일',
 		'email' => '메일',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => '자세한 정보',
 		'more_information' => '자세한 정보',
 		'print' => '인쇄',
 		'print' => '인쇄',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => '표시할 이름',
 		'share_name' => '표시할 이름',
 		'share_url' => '사용할 공유 URL',
 		'share_url' => '사용할 공유 URL',

+ 2 - 0
app/i18n/nl/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Delen',
 		'_' => 'Delen',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Meer informatie',
 		'more_information' => 'Meer informatie',
 		'print' => 'Afdrukken',
 		'print' => 'Afdrukken',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Gedeelde naam om weer te geven',
 		'share_name' => 'Gedeelde naam om weer te geven',
 		'share_url' => 'Deel URL voor gebruik',
 		'share_url' => 'Deel URL voor gebruik',

+ 2 - 0
app/i18n/pt-br/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Compartilhando',
 		'_' => 'Compartilhando',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Mais informação',
 		'more_information' => 'Mais informação',
 		'print' => 'Imprimir',
 		'print' => 'Imprimir',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Nome de visualização para compartilhar',
 		'share_name' => 'Nome de visualização para compartilhar',
 		'share_url' => 'URL utilizada para compartilhar',
 		'share_url' => 'URL utilizada para compartilhar',

+ 2 - 0
app/i18n/ru/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Sharing',
 		'_' => 'Sharing',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'More information',
 		'more_information' => 'More information',
 		'print' => 'Print',
 		'print' => 'Print',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Share name to display',
 		'share_name' => 'Share name to display',
 		'share_url' => 'Share URL to use',
 		'share_url' => 'Share URL to use',

+ 2 - 0
app/i18n/tr/conf.php

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => 'Paylaşım',
 		'_' => 'Paylaşım',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => 'Daha fazla bilgi',
 		'more_information' => 'Daha fazla bilgi',
 		'print' => 'Yazdır',
 		'print' => 'Yazdır',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => 'Paylaşım ismi',
 		'share_name' => 'Paylaşım ismi',
 		'share_url' => 'Paylaşım URL si',
 		'share_url' => 'Paylaşım URL si',

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

@@ -126,6 +126,7 @@ return array(
 	),
 	),
 	'sharing' => array(
 	'sharing' => array(
 		'_' => '分享',
 		'_' => '分享',
+		'add' => 'Add a sharing method', // TODO
 		'blogotext' => 'Blogotext',
 		'blogotext' => 'Blogotext',
 		'diaspora' => 'Diaspora*',
 		'diaspora' => 'Diaspora*',
 		'email' => 'Email',
 		'email' => 'Email',
@@ -133,6 +134,7 @@ return array(
 		'g+' => 'Google+',
 		'g+' => 'Google+',
 		'more_information' => '更多信息',
 		'more_information' => '更多信息',
 		'print' => '打印',
 		'print' => '打印',
+		'remove' => 'Remove sharing method', // TODO
 		'shaarli' => 'Shaarli',
 		'shaarli' => 'Shaarli',
 		'share_name' => '名称',
 		'share_name' => '名称',
 		'share_url' => '地址',
 		'share_url' => '地址',

+ 0 - 0
data/shares.php → app/shares.php


+ 3 - 3
app/views/configure/sharing.phtml

@@ -14,7 +14,7 @@
 			<div class="stick">
 			<div class="stick">
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" />
 			<input type="text" id="share_##key##_name" name="share[##key##][name]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_name'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" />
 			<input type="url" id="share_##key##_url" name="share[##key##][url]" class="extend" value="" placeholder="<?php echo _t('conf.sharing.share_url'); ?>" size="64" />
-			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##"><?php echo _i('close'); ?></a></div>
+			<a href="#" class="remove btn btn-attention" data-remove="group-share-##key##" title="<?php echo _t('conf.sharing.remove'); ?>"><?php echo _i('close'); ?></a></div>
 			<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
 			<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="##help##"><?php echo _i('help'); ?></a>
 			</div></div>'>
 			</div></div>'>
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
 		<input type="hidden" name="_csrf" value="<?php echo FreshRSS_Auth::csrfToken(); ?>" />
@@ -39,7 +39,7 @@
 					<?php } else { ?>
 					<?php } else { ?>
 						<input type="url" id="share_<?php echo $key; ?>_url" name="share[<?php echo $key; ?>][url]" class="extend" value="<?php echo $share->baseUrl(); ?>" placeholder="<?php echo _t('gen.short.not_applicable'); ?>" size="64" disabled/>
 						<input type="url" id="share_<?php echo $key; ?>_url" name="share[<?php echo $key; ?>][url]" class="extend" value="<?php echo $share->baseUrl(); ?>" placeholder="<?php echo _t('gen.short.not_applicable'); ?>" size="64" disabled/>
 					<?php } ?>
 					<?php } ?>
-						<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>"><?php echo _i('close'); ?></a>
+						<a href='#' class='remove btn btn-attention' data-remove="group-share-<?php echo $key; ?>" title="<?php echo _t('conf.sharing.remove'); ?>"><?php echo _i('close'); ?></a>
 					</div>
 					</div>
 					<?php if ($share->formType() === 'advanced') { ?>
 					<?php if ($share->formType() === 'advanced') { ?>
 						<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
 						<a target="_blank" rel="noreferrer" class="btn" title="<?php echo _t('conf.sharing.more_information'); ?>" href="<?php echo $share->help(); ?>"><?php echo _i('help'); ?></a>
@@ -57,7 +57,7 @@
 					</option>
 					</option>
 					<?php } ?>
 					<?php } ?>
 				</select>
 				</select>
-				<a href='#' class='share add btn'><?php echo _i('add'); ?></a>
+				<a href='#' class='share add btn' title="<?php echo _t('conf.sharing.add'); ?>"><?php echo _i('add'); ?></a>
 			</div>
 			</div>
 		</div>
 		</div>
 
 

+ 3 - 0
cli/README.md

@@ -32,6 +32,9 @@ Options in parenthesis are optional.
 ```sh
 ```sh
 cd /usr/share/FreshRSS
 cd /usr/share/FreshRSS
 
 
+./cli/prepare.php
+# Ensure the needed directories in ./data/
+
 ./cli/do-install.php --default_user admin ( --auth_type form --environment production --base_url https://rss.example.net/ --language en --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
 ./cli/do-install.php --default_user admin ( --auth_type form --environment production --base_url https://rss.example.net/ --language en --title FreshRSS --allow_anonymous --api_enabled --db-type mysql --db-host localhost:3306 --db-user freshrss --db-password dbPassword123 --db-base freshrss --db-prefix freshrss )
 # --auth_type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
 # --auth_type can be: 'form' (default), 'http_auth' (using the Web server access control), 'none' (dangerous)
 # --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)
 # --db-type can be: 'sqlite' (default), 'mysql' (MySQL or MariaDB), 'pgsql' (PostgreSQL)

+ 23 - 1
cli/i18n/I18nData.php

@@ -32,6 +32,7 @@ class I18nData {
 	 * Add a new language. It's a copy of the reference language.
 	 * Add a new language. It's a copy of the reference language.
 	 *
 	 *
 	 * @param string $language
 	 * @param string $language
+	 * @throws Exception
 	 */
 	 */
 	public function addLanguage($language) {
 	public function addLanguage($language) {
 		if (array_key_exists($language, $this->data)) {
 		if (array_key_exists($language, $this->data)) {
@@ -45,6 +46,7 @@ class I18nData {
 	 *
 	 *
 	 * @param string $key
 	 * @param string $key
 	 * @param string $value
 	 * @param string $value
+	 * @throws Exception
 	 */
 	 */
 	public function addKey($key, $value) {
 	public function addKey($key, $value) {
 		if (array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 		if (array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
@@ -53,10 +55,29 @@ class I18nData {
 		$this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key] = $value;
 		$this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)][$key] = $value;
 	}
 	}
 
 
+	/**
+	 * Add a value for a key for the selected language.
+	 *
+	 * @param string $key
+	 * @param string $value
+	 * @param string $language
+	 * @throws Exception
+	 */
+	public function addValue($key, $value, $language) {
+		if (!in_array($language, $this->getAvailableLanguages())) {
+			throw new Exception('The selected language does not exist.');
+		}
+		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
+			throw new Exception('The selected key does not exist for the selected language.');
+		}
+		$this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
+	}
+
 	/**
 	/**
 	 * Duplicate a key from the reference language to all other languages
 	 * Duplicate a key from the reference language to all other languages
 	 *
 	 *
 	 * @param string $key
 	 * @param string $key
+	 * @throws Exception
 	 */
 	 */
 	public function duplicateKey($key) {
 	public function duplicateKey($key) {
 		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
@@ -68,7 +89,7 @@ class I18nData {
 				continue;
 				continue;
 			}
 			}
 			if (array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
 			if (array_key_exists($key, $this->data[$language][$this->getFilenamePrefix($key)])) {
-				throw new Exception(sprintf('The selected key already exist in %s.', $language));
+				continue;
 			}
 			}
 			$this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
 			$this->data[$language][$this->getFilenamePrefix($key)][$key] = $value;
 		}
 		}
@@ -78,6 +99,7 @@ class I18nData {
 	 * Remove a key in all languages
 	 * Remove a key in all languages
 	 *
 	 *
 	 * @param string $key
 	 * @param string $key
+	 * @throws Exception
 	 */
 	 */
 	public function removeKey($key) {
 	public function removeKey($key) {
 		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {
 		if (!array_key_exists($key, $this->data[static::REFERENCE_LANGUAGE][$this->getFilenamePrefix($key)])) {

+ 29 - 2
cli/i18n/I18nFile.php

@@ -36,8 +36,7 @@ class i18nFile {
 			}
 			}
 			foreach ($file as $name => $content) {
 			foreach ($file as $name => $content) {
 				$filename = $dir . DIRECTORY_SEPARATOR . $name;
 				$filename = $dir . DIRECTORY_SEPARATOR . $name;
-				$fullContent = var_export($this->unflatten($content), true);
-				file_put_contents($filename, sprintf('<?php return %s;', $fullContent));
+				file_put_contents($filename, $this->format($content));
 			}
 			}
 		}
 		}
 	}
 	}
@@ -89,4 +88,32 @@ class i18nFile {
 		return $a;
 		return $a;
 	}
 	}
 
 
+	/**
+	 * Format an array of translation
+	 *
+	 * It takes an array of translation and format it to be dumped in a
+	 * translation file. The array is first converted to a string then some
+	 * formatting regexes are applied to match the original content.
+	 *
+	 * @param array $translation
+	 * @return string
+	 */
+	private function format($translation) {
+		$translation = var_export($this->unflatten($translation), true);
+		$patterns = array(
+			'/array \(/',
+			'/=>\s*array/',
+			'/ {2}/',
+		);
+		$replacements = array(
+			'array(',
+			'=> array',
+			"\t", // Double quoting is mandatory to have a tab instead of the \t string
+		);
+		$translation = preg_replace($patterns, $replacements, $translation);
+
+		// Double quoting is mandatory to have new lines instead of \n strings
+		return sprintf("<?php\n\nreturn %s;\n", $translation);
+	}
+
 }
 }

+ 17 - 2
cli/manipulate.translation.php

@@ -6,7 +6,7 @@ if (array_key_exists('h', $options)) {
 	help();
 	help();
 }
 }
 
 
-if (1 === $argc || 4 < $argc) {
+if (1 === $argc || 5 < $argc) {
 	help();
 	help();
 }
 }
 
 
@@ -25,12 +25,21 @@ switch ($argv[1]) {
 		}
 		}
 		$i18nData->addKey($argv[2], $argv[3]);
 		$i18nData->addKey($argv[2], $argv[3]);
 		break;
 		break;
+	case 'add_value':
+		if (4 === $argc) {
+			help();
+		}
+		$i18nData->addValue($argv[2], $argv[3], $argv[4]);
+		break;
 	case 'duplicate_key' :
 	case 'duplicate_key' :
 		$i18nData->duplicateKey($argv[2]);
 		$i18nData->duplicateKey($argv[2]);
 		break;
 		break;
 	case 'delete_key' :
 	case 'delete_key' :
 		$i18nData->removeKey($argv[2]);
 		$i18nData->removeKey($argv[2]);
 		break;
 		break;
+	case 'format' :
+		$i18nFile->dump($i18nData);
+		break;
 	default :
 	default :
 		help();
 		help();
 }
 }
@@ -48,7 +57,7 @@ NAME
 	%s
 	%s
 
 
 SYNOPSIS
 SYNOPSIS
-	php %s [OPTION] [OPERATION] [KEY] [VALUE]
+	php %s [OPTION] [OPERATION] [KEY] [VALUE] [LANGUAGE]
 
 
 DESCRIPTION
 DESCRIPTION
 	Manipulate translation files. Available operations are 
 	Manipulate translation files. Available operations are 
@@ -64,6 +73,10 @@ OPERATION
 	add_key	add a new key in the referential. This operation needs a KEY and
 	add_key	add a new key in the referential. This operation needs a KEY and
 		a VALUE.
 		a VALUE.
 
 
+	add_value
+		add a value in the referential. This operation needs a KEY, a
+		VALUE, and a LANGUAGE.
+
 	duplicate_key
 	duplicate_key
 		duplicate a referential key in other languages. This operation
 		duplicate a referential key in other languages. This operation
 		needs only a KEY.
 		needs only a KEY.
@@ -72,6 +85,8 @@ OPERATION
 		delete a referential key from all languages. This operation needs
 		delete a referential key from all languages. This operation needs
 		only a KEY.
 		only a KEY.
 
 
+	format  format i18n files.
+
 HELP;
 HELP;
 	$file = str_replace(__DIR__ . '/', '', __FILE__);
 	$file = str_replace(__DIR__ . '/', '', __FILE__);
 	echo sprintf($help, $file, $file);
 	echo sprintf($help, $file, $file);

+ 37 - 0
cli/prepare.php

@@ -0,0 +1,37 @@
+#!/usr/bin/php
+<?php
+require(__DIR__ . '/_cli.php');
+
+$dirs = array(
+	'/',
+	'/cache',
+	'/extensions-data',
+	'/favicons',
+	'/PubSubHubbub',
+	'/PubSubHubbub/feeds',
+	'/PubSubHubbub/keys',
+	'/tokens',
+	'/users',
+	'/users/_',
+);
+
+$ok = true;
+
+foreach ($dirs as $dir) {
+	@mkdir(DATA_PATH . $dir, 0770, true);
+	$ok &= touch(DATA_PATH . $dir . '/index.html');
+}
+
+if (!is_file(DATA_PATH . '/config.php')) {
+	$ok &= touch(DATA_PATH . '/do-install.txt');
+}
+
+file_put_contents(DATA_PATH . '/.htaccess',
+"Order	Allow,Deny\n" .
+"Deny	from all\n" .
+"Satisfy	all\n"
+);
+
+accessRights();
+
+done($ok);

+ 1 - 1
constants.php

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

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

@@ -9,7 +9,7 @@ You need to verify that your server can run FreshRSS before installing it. If yo
 | Web server  | **Apache 2**     | Nginx                         |
 | Web server  | **Apache 2**     | Nginx                         |
 | PHP         | **PHP 5.5+**     | PHP 5.3.8+                    |
 | PHP         | **PHP 5.5+**     | PHP 5.3.8+                    |
 | PHP modules | Required: libxml, cURL, PDO_MySQL, PCRE and ctype. \\ Required (32-bit only): GMP \\Recommanded: JSON, Zlib, mbstring, iconv, ZipArchive | |
 | PHP modules | Required: libxml, cURL, PDO_MySQL, PCRE and ctype. \\ Required (32-bit only): GMP \\Recommanded: JSON, Zlib, mbstring, iconv, ZipArchive | |
-| Database    | **MySQL 5.0.3+** | SQLite 3.7.4+                 |
+| Database    | **MySQL 5.5.3+** | SQLite 3.7.4+                 |
 | Browser     | **Firefox**      | Chrome, Opera, Safari, or IE11+ |
 | Browser     | **Firefox**      | Chrome, Opera, Safari, or IE11+ |
 
 
 ## Important notice
 ## Important notice

+ 11 - 0
docs/en/users/07_Frequently_Asked_Questions.md

@@ -33,3 +33,14 @@ Here is a list of feeds which don't work:
 * http://foulab.org/fr/rss/Foulab_News: is not a W3C valid feed (November 2014)
 * http://foulab.org/fr/rss/Foulab_News: is not a W3C valid feed (November 2014)
 * http://eu.battle.net/hearthstone/fr/feed/news: is not a W3C valid feed (Novembre 2014)
 * http://eu.battle.net/hearthstone/fr/feed/news: is not a W3C valid feed (Novembre 2014)
 * http://webseriesmag.blogs.liberation.fr/we/atom.xml: is not working for the user but succeed on all the described validations (November 2014)
 * http://webseriesmag.blogs.liberation.fr/we/atom.xml: is not working for the user but succeed on all the described validations (November 2014)
+
+## How to change a forgotten password?
+
+Since [1.10.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.10.0) release, admins are able to change user passwords directly from the interface. This interface is available under  ```Administration → Manage users```.
+Select a user, enter a password, and validate.
+
+Since [1.8.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.8.0) release, admins are able to change user passwords using a terminal. It worth mentioning that it must have access to PHP CLI. Open a terminal, and type the following command:
+```sh
+./cli/update_user.php --user <username> --password <password>
+```
+For more information on that matter, there is a [dedicated documentation](../../cli/README.md).

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

@@ -9,7 +9,7 @@ Il est toutefois de votre responsabilité de vérifier que votre hébergement pe
  | Serveur web      | **Apache 2**                                                                                                   | Nginx                          |
  | Serveur web      | **Apache 2**                                                                                                   | Nginx                          |
  | PHP              | **PHP 5.5+**                                                                                                   | PHP 5.3.8+                     |
  | PHP              | **PHP 5.5+**                                                                                                   | PHP 5.3.8+                     |
  | Modules PHP      | Requis : libxml, cURL, PDO_MySQL, PCRE et ctype \\ Requis (32 bits seulement) : GMP \\ Recommandé : JSON, Zlib, mbstring et iconv, ZipArchive |                                |
  | Modules PHP      | Requis : libxml, cURL, PDO_MySQL, PCRE et ctype \\ Requis (32 bits seulement) : GMP \\ Recommandé : JSON, Zlib, mbstring et iconv, ZipArchive |                                |
- | Base de données  | **MySQL 5.0.3+**                                                                                               | SQLite 3.7.4+                  |
+ | Base de données  | **MySQL 5.5.3+**                                                                                               | SQLite 3.7.4+                  |
  | Navigateur       | **Firefox**                                                                                                    | Chrome, Opera, Safari, or IE 11+ |
  | Navigateur       | **Firefox**                                                                                                    | Chrome, Opera, Safari, or IE 11+ |
 
 
 ## Note importante
 ## Note importante

+ 11 - 0
docs/fr/users/07_Frequently_Asked_Questions.md

@@ -33,3 +33,14 @@ Voici une liste des flux qui ne fonctionnent pas :
 * http://foulab.org/fr/rss/Foulab_News : ne passe pas la validation W3C (novembre 2014)
 * http://foulab.org/fr/rss/Foulab_News : ne passe pas la validation W3C (novembre 2014)
 * http://eu.battle.net/hearthstone/fr/feed/news : ne passe pas la validation W3C (novembre 2014)
 * http://eu.battle.net/hearthstone/fr/feed/news : ne passe pas la validation W3C (novembre 2014)
 * http://webseriesmag.blogs.liberation.fr/we/atom.xml : ne fonctionne pas chez l'utilisateur mais passe l'ensemble des validations ci-dessus (novembre 2014)
 * http://webseriesmag.blogs.liberation.fr/we/atom.xml : ne fonctionne pas chez l'utilisateur mais passe l'ensemble des validations ci-dessus (novembre 2014)
+
+## Comment changer un mot de passe oublié ?
+
+Depuis la version [1.10.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.10.0), l'administrateur peut modifier le mot de passe d'un utilisateur depuis l'interface. Cette interface est disponible dans le menu ```Administration → Gestion des utilisateurs```.
+Il suffit de sélectionner l'utilisateur, de saisir un mot de passe et de valider.
+
+Depuis la version [1.8.0](https://github.com/FreshRSS/FreshRSS/releases/tag/1.8.0), l'administrateur peut modifier le mot de passe d'un utilisateur depuis un terminal. Il est bon de noter que celui-ci doit avoir un accès à PHP en ligne de commande. Pour cela, il suffit d'ouvrir son terminal et de saisir la commande suivante :
+```sh
+./cli/update_user.php --user <username> --password <password>
+```
+Pour plus d'information à ce sujet, il existe la [documentation dédiée](../../cli/README.md).

+ 2 - 1
lib/Minz/Request.php

@@ -106,7 +106,8 @@ class Minz_Request {
 		$https = self::isHttps();
 		$https = self::isHttps();
 
 
 		if (!empty($_SERVER['HTTP_HOST'])) {
 		if (!empty($_SERVER['HTTP_HOST'])) {
-			$host = $_SERVER['HTTP_HOST'];
+			//Might contain a port number, and mind IPv6 addresses
+			$host = parse_url('http://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST);
 		} elseif (!empty($_SERVER['SERVER_NAME'])) {
 		} elseif (!empty($_SERVER['SERVER_NAME'])) {
 			$host = $_SERVER['SERVER_NAME'];
 			$host = $_SERVER['SERVER_NAME'];
 		} else {
 		} else {

+ 3 - 3
p/api/greader.php

@@ -445,6 +445,9 @@ function unreadCount() {	//http://blog.martindoms.com/2009/10/16/using-the-googl
 }
 }
 
 
 function entriesToArray($entries) {
 function entriesToArray($entries) {
+	$feedDAO = FreshRSS_Factory::createFeedDao();
+	$arrayFeedCategoryNames = $feedDAO->arrayFeedCategoryNames();
+
 	$items = array();
 	$items = array();
 	foreach ($entries as $entry) {
 	foreach ($entries as $entry) {
 		$f_id = $entry->feed();
 		$f_id = $entry->feed();
@@ -494,9 +497,6 @@ function streamContents($path, $include_target, $start_time, $count, $order, $ex
 //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
 //http://blog.martindoms.com/2009/10/16/using-the-google-reader-api-part-2/#feed
 	header('Content-Type: application/json; charset=UTF-8');
 	header('Content-Type: application/json; charset=UTF-8');
 
 
-	$feedDAO = FreshRSS_Factory::createFeedDao();
-	$arrayFeedCategoryNames = $feedDAO->arrayFeedCategoryNames();
-
 	switch ($path) {
 	switch ($path) {
 		case 'reading-list':
 		case 'reading-list':
 			$type = 'A';
 			$type = 'A';