Parcourir la source

Merge pull request #1195 from Alkarex/PostgreSQL

PostgreSQL
Alexandre Alapetite il y a 9 ans
Parent
commit
0ccf100568

+ 2 - 0
CHANGELOG.md

@@ -4,6 +4,8 @@
 
 * API
 	* Support for editing feeds and categories from client applications [#1254](https://github.com/FreshRSS/FreshRSS/issues/1254)
+* Compatibility:
+	* Experimental support for PostgreSQL [#1195](https://github.com/FreshRSS/FreshRSS/pull/1195)
 * Features
 	* Better control of number of entries per page or RSS feed [#1249](https://github.com/FreshRSS/FreshRSS/issues/1249)
 		* Since X hours: `https://freshrss.example/i/?a=rss&hours=3`

+ 15 - 8
README.fr.md

@@ -33,9 +33,9 @@ Nous sommes une communauté amicale.
 	* Fonctionne même sur un Raspberry Pi 1 avec des temps de réponse < 1s (testé sur 150 flux, 22k articles)
 * Serveur Web Apache2 (recommandé), ou nginx, lighttpd (non testé sur les autres)
 * PHP 5.3.3+ (PHP 5.4+ recommandé, et PHP 5.5+ pour les performances, et PHP 7+ pour d’encore meilleures performances)
-	* Requis : [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
+	* Requis : [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) ou [PDO_SQLite](http://php.net/pdo-sqlite) ou [PDO_PGSQL](http://php.net/pdo-pgsql), [cURL](http://php.net/curl)
 	* Recommandés : [JSON](http://php.net/json), [GMP](http://php.net/gmp) (pour accès API sur plateformes < 64 bits), [IDN](http://php.net/intl.idn) (pour les noms de domaines internationalisés), [mbstring](http://php.net/mbstring) et/ou [iconv](http://php.net/iconv) (pour conversion d’encodages), [Zip](http://php.net/zip) (pour import/export), [zlib](http://php.net/zlib) (pour les flux compressés)
-* MySQL 5.5.3+ (recommandé) ou SQLite 3.7.4+
+* MySQL 5.5.3+ (recommandé), ou SQLite 3.7.4+, ou PostgreSQL (experimental)
 * Un navigateur Web récent tel Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Fonctionne aussi sur mobile
 
@@ -56,13 +56,20 @@ Nous sommes une communauté amicale.
 ```sh
 # Si vous utilisez le serveur Web Apache (sinon il faut un autre serveur Web)
 sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (optionnel) Si vous voulez un serveur de base de données MySQL
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Composants principaux (pour Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl	#Modules Apache
+
+# Pour Ubuntu <= 15.10, Debian <= 8 Jessie
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Composants principaux (pour Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt-get install libapache2-mod-php5	#Pour Apache
+sudo apt-get install mysql-server mysql-client php5-mysql	#Base de données MySQL optionnelle
+sudo apt-get install postgresql php5-pgsql	#Base de données PostgreSQL optionnelle
+
+# Pour Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php	#Pour Apache
+sudo apt install mysql-server mysql-client php-mysql	#Base de données MySQL optionnelle
+sudo apt install postgresql php-pgsql	#Base de données PostgreSQL optionnelle
+
 # Redémarrage du serveur Web
 sudo service apache2 restart
 

+ 15 - 8
README.md

@@ -33,9 +33,9 @@ We are a friendly community.
 	* It even works on Raspberry Pi 1 with response time under a second (tested with 150 feeds, 22k articles)
 * A web server: Apache2 (recommended), nginx, lighttpd (not tested on others)
 * PHP 5.3.3+ (PHP 5.4+ recommended, and PHP 5.5+ for performance, and PHP 7 for even higher performance)
-	* Required extensions: [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite), [cURL](http://php.net/curl)
+	* Required extensions: [DOM](http://php.net/dom), [XML](http://php.net/xml), [PDO_MySQL](http://php.net/pdo-mysql) or [PDO_SQLite](http://php.net/pdo-sqlite) or [PDO_PGSQL](http://php.net/pdo-pgsql), [cURL](http://php.net/curl)
 	* Recommended extensions: [JSON](http://php.net/json), [GMP](http://php.net/gmp) (for API access on platforms < 64 bits), [IDN](http://php.net/intl.idn) (for Internationalized Domain Names), [mbstring](http://php.net/mbstring) and/or [iconv](http://php.net/iconv) (for charset conversion), [Zip](http://php.net/zip) (for import/export), [zlib](http://php.net/zlib) (for compressed feeds)
-* MySQL 5.5.3+ (recommended) or SQLite 3.7.4+
+* MySQL 5.5.3+ (recommended), or SQLite 3.7.4+, or PostgreSQL (experimental)
 * A recent browser like Firefox, Internet Explorer 11 / Edge, Chrome, Opera, Safari.
 	* Works on mobile
 
@@ -56,13 +56,20 @@ We are a friendly community.
 ```sh
 # If you use an Apache Web server (otherwise you need another Web server)
 sudo apt-get install apache2
-sudo a2enmod headers expires rewrite ssl
-# (Optional) If you want a MySQL database server
-sudo apt-get install mysql-server mysql-client php5-mysql
-# Main components (for Ubuntu <= 15.10, Debian <= 8 Jessie)
+sudo a2enmod headers expires rewrite ssl	#Apache modules
+
+# For Ubuntu <= 15.10, Debian <= 8 Jessie
 sudo apt-get install php5 php5-curl php5-gmp php5-intl php5-json php5-sqlite
-# Main components (for Ubuntu >= 16.04, Debian >= 9 Stretch)
-sudo apt install php libapache2-mod-php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt-get install libapache2-mod-php5	#For Apache
+sudo apt-get install mysql-server mysql-client php5-mysql	#Optional MySQL database
+sudo apt-get install postgresql php5-pgsql	#Optional PostgreSQL database
+
+# For Ubuntu >= 16.04, Debian >= 9 Stretch
+sudo apt install php php-curl php-gmp php-intl php-mbstring php-sqlite3 php-xml php-zip
+sudo apt install libapache2-mod-php	#For Apache
+sudo apt install mysql-server mysql-client php-mysql	#Optional MySQL database
+sudo apt install postgresql php-pgsql	#Optional PostgreSQL database
+
 # Restart Web server
 sudo service apache2 restart
 

+ 5 - 4
app/Models/CategoryDAO.php

@@ -10,7 +10,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		);
 
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . $this->prefix . 'category_id_seq"');
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addCategory: ' . $info[2]);
@@ -103,7 +103,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 	public function listCategories($prePopulateFeeds = true, $details = false) {
 		if ($prePopulateFeeds) {
 			$sql = 'SELECT c.id AS c_id, c.name AS c_name, '
-			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.cache_nbEntries, f.cache_nbUnreads ')
+			     . ($details ? 'f.* ' : 'f.id, f.name, f.url, f.website, f.priority, f.error, f.`cache_nbEntries`, f.`cache_nbUnreads` ')
 			     . 'FROM `' . $this->prefix . 'category` c '
 			     . 'LEFT OUTER JOIN `' . $this->prefix . 'feed` f ON f.category=c.id '
 			     . 'GROUP BY f.id, c_id '
@@ -210,12 +210,13 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 
 		$previousLine = null;
 		$feedsDao = array();
+		$feedDao = FreshRSS_Factory::createFeedDAO();
 		foreach ($listDAO as $line) {
 			if ($previousLine['c_id'] != null && $line['c_id'] !== $previousLine['c_id']) {
 				// End of the current category, we add it to the $list
 				$cat = new FreshRSS_Category(
 					$previousLine['c_name'],
-					FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+					$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 				);
 				$cat->_id($previousLine['c_id']);
 				$list[$previousLine['c_id']] = $cat;
@@ -231,7 +232,7 @@ class FreshRSS_CategoryDAO extends Minz_ModelPdo implements FreshRSS_Searchable
 		if ($previousLine != null) {
 			$cat = new FreshRSS_Category(
 				$previousLine['c_name'],
-				FreshRSS_FeedDAO::daoToFeed($feedsDao, $previousLine['c_id'])
+				$feedDao->daoToFeed($feedsDao, $previousLine['c_id'])
 			);
 			$cat->_id($previousLine['c_id']);
 			$list[$previousLine['c_id']] = $cat;

+ 1 - 0
app/Models/ConfigurationSetter.php

@@ -282,6 +282,7 @@ class FreshRSS_ConfigurationSetter {
 
 		switch ($value['type']) {
 		case 'mysql':
+		case 'pgsql':
 			if (empty($value['host']) ||
 					empty($value['user']) ||
 					empty($value['base']) ||

+ 43 - 0
app/Models/DatabaseDAOPGSQL.php

@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * This class is used to test database is well-constructed.
+ */
+class FreshRSS_DatabaseDAOPGSQL extends FreshRSS_DatabaseDAO {
+	public function tablesAreCorrect() {
+		$db = FreshRSS_Context::$system_conf->db;
+		$dbowner = $db['user'];
+		$sql = 'SELECT * FROM pg_catalog.pg_tables where tableowner=?';
+		$stm = $this->bd->prepare($sql);
+		$values = array($dbowner);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_ASSOC);
+		
+		$tables = array(
+			$this->prefix . 'category' => false,
+			$this->prefix . 'feed' => false,
+			$this->prefix . 'entry' => false,
+		);
+		foreach ($res as $value) {
+			$tables[array_pop($value)] = true;
+		}
+
+		return count(array_keys($tables, true, true)) == count($tables);
+	}
+
+	public function getSchema($table) {
+		$sql = 'select column_name as field, data_type as type, column_default as default, is_nullable as null from INFORMATION_SCHEMA.COLUMNS where table_name = ?';
+		$stm = $this->bd->prepare($sql);
+		$stm->execute(array($this->prefix . $table));
+		return $this->listDaoToSchema($stm->fetchAll(PDO::FETCH_ASSOC));
+	}
+
+	public function daoToSchema($dao) {
+		return array(
+			'name' => $dao['field'],
+			'type' => strtolower($dao['type']),
+			'notnull' => (bool)$dao['null'],
+			'default' => $dao['default'],
+		);
+	}
+}

+ 84 - 57
app/Models/EntryDAO.php

@@ -3,13 +3,21 @@
 class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 	public function isCompressed() {
-		return parent::$sharedDbType !== 'sqlite';
+		return parent::$sharedDbType === 'mysql';
 	}
 
 	public function hasNativeHex() {
 		return parent::$sharedDbType !== 'sqlite';
 	}
 
+	public function sqlHexDecode($x) {
+		return 'unhex(' . $x . ')';
+	}
+
+	public function sqlHexEncode($x) {
+		return 'hex(' . $x . ')';
+	}
+
 	protected function addColumn($name) {
 		Minz_Log::warning('FreshRSS_EntryDAO::addColumn: ' . $name);
 		$hasTransaction = false;
@@ -20,7 +28,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 					$this->bd->beginTransaction();
 					$hasTransaction = true;
 				}
-				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN lastSeen INT(11) DEFAULT 0');
+				$stm = $this->bd->prepare('ALTER TABLE `' . $this->prefix . 'entry` ADD COLUMN `lastSeen` INT(11) DEFAULT 0');
 				if ($stm && $stm->execute()) {
 					$stm = $this->bd->prepare('CREATE INDEX entry_lastSeen_index ON `' . $this->prefix . 'entry`(`lastSeen`);');	//"IF NOT EXISTS" does not exist in MySQL 5.7
 					if ($stm && $stm->execute()) {
@@ -105,32 +113,43 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($this->addEntryPrepared === null) {
 			$sql = 'INSERT INTO `' . $this->prefix . 'entry` (id, guid, title, author, '
 			     . ($this->isCompressed() ? 'content_bin' : 'content')
-			     . ', link, date, lastSeen, hash, is_read, is_favorite, id_feed, tags) '
-			     . 'VALUES(?, ?, ?, ?, '
-			     . ($this->isCompressed() ? 'COMPRESS(?)' : '?')
-			     . ', ?, ?, ?, '
-			     . ($this->hasNativeHex() ? 'X?' : '?')
-			     . ', ?, ?, ?, ?)';
+			     . ', link, date, `lastSeen`, hash, is_read, is_favorite, id_feed, tags) '
+			     . 'VALUES(:id, :guid, :title, :author, '
+			     . ($this->isCompressed() ? 'COMPRESS(:content)' : ':content')
+			     . ', :link, :date, :last_seen, '
+			     . $this->sqlHexDecode(':hash')
+			     . ', :is_read, :is_favorite, :id_feed, :tags)';
 			$this->addEntryPrepared = $this->bd->prepare($sql);
 		}
+		$this->addEntryPrepared->bindParam(':id', $valuesTmp['id']);
+		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+		$this->addEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+		$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+		$this->addEntryPrepared->bindParam(':title', $valuesTmp['title']);
+		$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+		$this->addEntryPrepared->bindParam(':author', $valuesTmp['author']);
+		$this->addEntryPrepared->bindParam(':content', $valuesTmp['content']);
+		$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+		$this->addEntryPrepared->bindParam(':link', $valuesTmp['link']);
+		$this->addEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+		$valuesTmp['lastSeen'] = time();
+		$this->addEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
+		$valuesTmp['is_read'] = $valuesTmp['is_read'] ? 1 : 0;
+		$this->addEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'], PDO::PARAM_INT);
+		$valuesTmp['is_favorite'] = $valuesTmp['is_favorite'] ? 1 : 0;
+		$this->addEntryPrepared->bindParam(':is_favorite', $valuesTmp['is_favorite'], PDO::PARAM_INT);
+		$this->addEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+		$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+		$this->addEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+		if ($this->hasNativeHex()) {
+			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+		} else {
+			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+			$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
+		}
 
-		$values = array(
-			$valuesTmp['id'],
-			substr($valuesTmp['guid'], 0, 760),
-			substr($valuesTmp['title'], 0, 255),
-			substr($valuesTmp['author'], 0, 255),
-			$valuesTmp['content'],
-			substr($valuesTmp['link'], 0, 1023),
-			$valuesTmp['date'],
-			time(),
-			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),	// X'09AF' hexadecimal literals do not work with SQLite/PDO	//hex2bin() is PHP5.4+
-			$valuesTmp['is_read'] ? 1 : 0,
-			$valuesTmp['is_favorite'] ? 1 : 0,
-			$valuesTmp['id_feed'],
-			substr($valuesTmp['tags'], 0, 1023),
-		);
-
-		if ($this->addEntryPrepared && $this->addEntryPrepared->execute($values)) {
+		if ($this->addEntryPrepared && $this->addEntryPrepared->execute()) {
 			return $this->bd->lastInsertId();
 		} else {
 			$info = $this->addEntryPrepared == null ? array(0 => '', 1 => '', 2 => 'syntax error') : $this->addEntryPrepared->errorInfo();
@@ -153,33 +172,41 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 		if ($this->updateEntryPrepared === null) {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` '
-			     . 'SET title=?, author=?, '
-			     . ($this->isCompressed() ? 'content_bin=COMPRESS(?)' : 'content=?')
-			     . ', link=?, date=?, lastSeen=?, hash='
-			     . ($this->hasNativeHex() ? 'X?' : '?')
-			     . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=?, ')
-			     . 'tags=? '
-			     . 'WHERE id_feed=? AND guid=?';
+			     . 'SET title=:title, author=:author, '
+			     . ($this->isCompressed() ? 'content_bin=COMPRESS(:content)' : 'content=:content')
+			     . ', link=:link, date=:date, `lastSeen`=:last_seen, '
+			     . 'hash=' . $this->sqlHexDecode(':hash')
+			     . ', ' . ($valuesTmp['is_read'] === null ? '' : 'is_read=:is_read, ')
+			     . 'tags=:tags '
+			     . 'WHERE id_feed=:id_feed AND guid=:guid';
 			$this->updateEntryPrepared = $this->bd->prepare($sql);
 		}
 
-		$values = array(
-			substr($valuesTmp['title'], 0, 255),
-			substr($valuesTmp['author'], 0, 255),
-			$valuesTmp['content'],
-			substr($valuesTmp['link'], 0, 1023),
-			$valuesTmp['date'],
-			time(),
-			$this->hasNativeHex() ? $valuesTmp['hash'] : pack('H*', $valuesTmp['hash']),
-		);
+		$valuesTmp['guid'] = substr($valuesTmp['guid'], 0, 760);
+		$this->updateEntryPrepared->bindParam(':guid', $valuesTmp['guid']);
+		$valuesTmp['title'] = substr($valuesTmp['title'], 0, 255);
+		$this->updateEntryPrepared->bindParam(':title', $valuesTmp['title']);
+		$valuesTmp['author'] = substr($valuesTmp['author'], 0, 255);
+		$this->updateEntryPrepared->bindParam(':author', $valuesTmp['author']);
+		$this->updateEntryPrepared->bindParam(':content', $valuesTmp['content']);
+		$valuesTmp['link'] = substr($valuesTmp['link'], 0, 1023);
+		$this->updateEntryPrepared->bindParam(':link', $valuesTmp['link']);
+		$this->updateEntryPrepared->bindParam(':date', $valuesTmp['date'], PDO::PARAM_INT);
+		$valuesTmp['lastSeen'] = time();
+		$this->updateEntryPrepared->bindParam(':last_seen', $valuesTmp['lastSeen'], PDO::PARAM_INT);
 		if ($valuesTmp['is_read'] !== null) {
-			$values[] = $valuesTmp['is_read'] ? 1 : 0;
+			$this->updateEntryPrepared->bindParam(':is_read', $valuesTmp['is_read'] ? 1 : 0, PDO::PARAM_INT);
+		}
+		$this->updateEntryPrepared->bindParam(':id_feed', $valuesTmp['id_feed'], PDO::PARAM_INT);
+		$valuesTmp['tags'] = substr($valuesTmp['tags'], 0, 1023);
+		$this->updateEntryPrepared->bindParam(':tags', $valuesTmp['tags']);
+
+		if ($this->hasNativeHex()) {
+			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
+		} else {
+			$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']);	//hex2bin() is PHP5.4+
+			$this->updateEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
 		}
-		$values = array_merge($values, array(
-			substr($valuesTmp['tags'], 0, 1023),
-			$valuesTmp['id_feed'],
-			substr($valuesTmp['guid'], 0, 760),
-		));
 
 		if ($this->updateEntryPrepared && $this->updateEntryPrepared->execute($values)) {
 			return $this->bd->lastInsertId();
@@ -246,7 +273,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		 .	'WHERE e.is_read=0 '
 		 .	'GROUP BY e.id_feed'
 		 . ') x ON x.id_feed=f.id '
-		 . 'SET f.cache_nbUnreads=COALESCE(x.nbUnreads, 0) '
+		 . 'SET f.`cache_nbUnreads`=COALESCE(x.nbUnreads, 0) '
 		 . 'WHERE 1';
 		$values = array();
 		if ($feedId !== false) {
@@ -309,7 +336,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		} else {
 			$sql = 'UPDATE `' . $this->prefix . 'entry` e INNER JOIN `' . $this->prefix . 'feed` f ON e.id_feed=f.id '
 				 . 'SET e.is_read=?,'
-				 . 'f.cache_nbUnreads=f.cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				 . 'f.`cache_nbUnreads`=f.`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
 				 . 'WHERE e.id=? AND e.is_read=?';
 			$values = array($is_read ? 1 : 0, $ids, $is_read ? 0 : 1);
 			$stm = $this->bd->prepare($sql);
@@ -430,12 +457,12 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		}
 		$this->bd->beginTransaction();
 
-		$sql = 'UPDATE `' . $this->prefix . 'entry` e '
-			 . 'SET e.is_read=1 '
-			 . 'WHERE e.id_feed=? AND e.is_read=0 AND e.id <= ?';
+		$sql = 'UPDATE `' . $this->prefix . 'entry` '
+			 . 'SET is_read=1 '
+			 . 'WHERE id_feed=? AND is_read=0 AND id <= ?';
 		$values = array($id_feed, $idMax);
 
-		list($searchValues, $search) = $this->sqlListEntriesWhere('e.', $filter, $state);
+		list($searchValues, $search) = $this->sqlListEntriesWhere('', $filter, $state);
 
 		$stm = $this->bd->prepare($sql . $search);
 		if (!($stm && $stm->execute(array_merge($values, $searchValues)))) {
@@ -448,7 +475,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 
 		if ($affected > 0) {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
-				 . 'SET cache_nbUnreads=cache_nbUnreads-' . $affected
+				 . 'SET `cache_nbUnreads`=`cache_nbUnreads`-' . $affected
 				 . ' WHERE id=?';
 			$values = array($id_feed);
 			$stm = $this->bd->prepare($sql);
@@ -658,7 +685,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if (count($guids) < 1) {
 			return array();
 		}
-		$sql = 'SELECT guid, hex(hash) AS hexHash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$sql = 'SELECT guid, ' . $this->sqlHexEncode('hash') . ' AS hex_hash FROM `' . $this->prefix . 'entry` WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
 		$stm = $this->bd->prepare($sql);
 		$values = array($id_feed);
 		$values = array_merge($values, $guids);
@@ -666,7 +693,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 			$result = array();
 			$rows = $stm->fetchAll(PDO::FETCH_ASSOC);
 			foreach ($rows as $row) {
-				$result[$row['guid']] = $row['hexHash'];
+				$result[$row['guid']] = $row['hex_hash'];
 			}
 			return $result;
 		} else {
@@ -684,7 +711,7 @@ class FreshRSS_EntryDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if (count($guids) < 1) {
 			return 0;
 		}
-		$sql = 'UPDATE `' . $this->prefix . 'entry` SET lastSeen=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
+		$sql = 'UPDATE `' . $this->prefix . 'entry` SET `lastSeen`=? WHERE id_feed=? AND guid IN (' . str_repeat('?,', count($guids) - 1). '?)';
 		$stm = $this->bd->prepare($sql);
 		$values = array(time(), $id_feed);
 		$values = array_merge($values, $guids);

+ 31 - 0
app/Models/EntryDAOPGSQL.php

@@ -0,0 +1,31 @@
+<?php
+
+class FreshRSS_EntryDAOPGSQL extends FreshRSS_EntryDAOSQLite {
+
+	public function sqlHexDecode($x) {
+		return 'decode(' . $x . ", 'hex')";
+	}
+
+	public function sqlHexEncode($x) {
+		return 'encode(' . $x . ", 'hex')";
+	}
+
+	protected function autoUpdateDb($errorInfo) {
+		return false;
+	}
+
+	protected function addColumn($name) {
+		return false;
+	}
+
+	public function size($all = true) {
+		$db = FreshRSS_Context::$system_conf->db;
+		$sql = 'SELECT pg_size_pretty(pg_database_size(?))';
+		$values = array($db['base']);
+		$stm = $this->bd->prepare($sql);
+		$stm->execute($values);
+		$res = $stm->fetchAll(PDO::FETCH_COLUMN, 0);
+		return $res[0];
+	}
+
+}

+ 6 - 2
app/Models/EntryDAOSQLite.php

@@ -2,6 +2,10 @@
 
 class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
+	public function sqlHexDecode($x) {
+		return $x;
+	}
+
 	protected function autoUpdateDb($errorInfo) {
 		if (empty($errorInfo[0]) || $errorInfo[0] == '42S22') {	//ER_BAD_FIELD_ERROR
 			//autoAddColumn
@@ -24,7 +28,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 
 	protected function updateCacheUnreads($catId = false, $feedId = false) {
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		 . 'SET cache_nbUnreads=('
+		 . 'SET `cache_nbUnreads`=('
 		 .	'SELECT COUNT(*) AS nbUnreads FROM `' . $this->prefix . 'entry` e '
 		 .	'WHERE e.id_feed=`' . $this->prefix . 'feed`.id AND e.is_read=0) '
 		 . 'WHERE 1';
@@ -82,7 +86,7 @@ class FreshRSS_EntryDAOSQLite extends FreshRSS_EntryDAO {
 			}
 			$affected = $stm->rowCount();
 			if ($affected > 0) {
-				$sql = 'UPDATE `' . $this->prefix . 'feed` SET cache_nbUnreads=cache_nbUnreads' . ($is_read ? '-' : '+') . '1 '
+				$sql = 'UPDATE `' . $this->prefix . 'feed` SET `cache_nbUnreads`=`cache_nbUnreads`' . ($is_read ? '-' : '+') . '1 '
 				 . 'WHERE id=(SELECT e.id_feed FROM `' . $this->prefix . 'entry` e WHERE e.id=?)';
 				$values = array($ids);
 				$stm = $this->bd->prepare($sql);

+ 26 - 16
app/Models/Factory.php

@@ -4,37 +4,47 @@ class FreshRSS_Factory {
 
 	public static function createFeedDao($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_FeedDAOSQLite($username);
-		} else {
-			return new FreshRSS_FeedDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_FeedDAOSQLite($username);
+			default:
+				return new FreshRSS_FeedDAO($username);
 		}
 	}
 
 	public static function createEntryDao($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_EntryDAOSQLite($username);
-		} else {
-			return new FreshRSS_EntryDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_EntryDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_EntryDAOPGSQL($username);
+			default:
+				return new FreshRSS_EntryDAO($username);
 		}
 	}
 
 	public static function createStatsDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_StatsDAOSQLite($username);
-		} else {
-			return new FreshRSS_StatsDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_StatsDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_StatsDAOPGSQL($username);
+			default:
+				return new FreshRSS_StatsDAO($username);
 		}
 	}
 
 	public static function createDatabaseDAO($username = null) {
 		$conf = Minz_Configuration::get('system');
-		if ($conf->db['type'] === 'sqlite') {
-			return new FreshRSS_DatabaseDAOSQLite($username);
-		} else {
-			return new FreshRSS_DatabaseDAO($username);
+		switch ($conf->db['type']) {
+			case 'sqlite':
+				return new FreshRSS_DatabaseDAOSQLite($username);
+			case 'pgsql':
+				return new FreshRSS_DatabaseDAOPGSQL($username);
+			default:
+				return new FreshRSS_DatabaseDAO($username);
 		}
 	}
 

+ 12 - 12
app/Models/FeedDAO.php

@@ -2,7 +2,7 @@
 
 class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	public function addFeed($valuesTmp) {
-		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, lastUpdate, priority, httpAuth, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
+		$sql = 'INSERT INTO `' . $this->prefix . 'feed` (url, category, name, website, description, `lastUpdate`, priority, `httpAuth`, error, keep_history, ttl) VALUES(?, ?, ?, ?, ?, ?, 10, ?, 0, -2, -2)';
 		$stm = $this->bd->prepare($sql);
 
 		$values = array(
@@ -16,7 +16,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		);
 
 		if ($stm && $stm->execute($values)) {
-			return $this->bd->lastInsertId();
+			return $this->bd->lastInsertId('"' . $this->prefix . 'feed_id_seq"');
 		} else {
 			$info = $stm == null ? array(2 => 'syntax error') : $stm->errorInfo();
 			Minz_Log::error('SQL error addFeed: ' . $info[2]);
@@ -85,13 +85,13 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 	public function updateLastUpdate($id, $inError = 0, $updateCache = true) {
 		if ($updateCache) {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '	//2 sub-requests with FOREIGN KEY(e.id_feed), INDEX(e.is_read) faster than 1 request with GROUP BY or CASE
-			     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-			     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
-			     . 'lastUpdate=?, error=? '
+			     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+			     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0),'
+			     . '`lastUpdate`=?, error=? '
 			     . 'WHERE id=?';
 		} else {
 			$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			     . 'SET lastUpdate=?, error=? '
+			     . 'SET `lastUpdate`=?, error=? '
 			     . 'WHERE id=?';
 		}
 
@@ -226,10 +226,10 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		if ($defaultCacheDuration < 0) {
 			$defaultCacheDuration = 2147483647;
 		}
-		$sql = 'SELECT id, url, name, website, lastUpdate, pathEntries, httpAuth, keep_history, ttl '
+		$sql = 'SELECT id, url, name, website, `lastUpdate`, `pathEntries`, `httpAuth`, keep_history, ttl '
 		     . 'FROM `' . $this->prefix . 'feed` '
-		     . 'WHERE ttl <> -1 AND lastUpdate < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
-		     . 'ORDER BY lastUpdate';
+		     . 'WHERE ttl <> -1 AND `lastUpdate` < (' . (time() + 60) . '-(CASE WHEN ttl=-2 THEN ' . intval($defaultCacheDuration) . ' ELSE ttl END)) '
+		     . 'ORDER BY `lastUpdate`';
 		$stm = $this->bd->prepare($sql);
 		if (!($stm && $stm->execute())) {
 			$sql2 = 'ALTER TABLE `' . $this->prefix . 'feed` ADD COLUMN ttl INT NOT NULL DEFAULT -2';	//v0.7.3
@@ -282,7 +282,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		     .	'FROM `' . $this->prefix . 'entry` e '
 		     .	'GROUP BY e.id_feed'
 		     . ') x ON x.id_feed=f.id '
-		     . 'SET f.cache_nbEntries=x.nbEntries, f.cache_nbUnreads=x.nbUnreads';
+		     . 'SET f.`cache_nbEntries`=x.nbEntries, f.`cache_nbUnreads`=x.nbUnreads';
 		$stm = $this->bd->prepare($sql);
 
 		if ($stm && $stm->execute()) {
@@ -308,7 +308,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$affected = $stm->rowCount();
 
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-			 . 'SET cache_nbEntries=0, cache_nbUnreads=0 WHERE id=?';
+			 . 'SET `cache_nbEntries`=0, `cache_nbUnreads`=0 WHERE id=?';
 		$values = array($id);
 		$stm = $this->bd->prepare($sql);
 		if (!($stm && $stm->execute($values))) {
@@ -326,7 +326,7 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo implements FreshRSS_Searchable {
 		$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 `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);
 

+ 2 - 2
app/Models/FeedDAOSQLite.php

@@ -4,8 +4,8 @@ class FreshRSS_FeedDAOSQLite extends FreshRSS_FeedDAO {
 
 	public function updateCachedValues() {	//For one single feed, call updateLastUpdate($id)
 		$sql = 'UPDATE `' . $this->prefix . 'feed` '
-		     . 'SET cache_nbEntries=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
-		     . 'cache_nbUnreads=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
+		     . 'SET `cache_nbEntries`=(SELECT COUNT(e1.id) FROM `' . $this->prefix . 'entry` e1 WHERE e1.id_feed=`' . $this->prefix . 'feed`.id),'
+		     . '`cache_nbUnreads`=(SELECT COUNT(e2.id) FROM `' . $this->prefix . 'entry` e2 WHERE e2.id_feed=`' . $this->prefix . 'feed`.id AND e2.is_read=0)';
 		$stm = $this->bd->prepare($sql);
 		if ($stm && $stm->execute()) {
 			return $stm->rowCount();

+ 25 - 20
app/Models/StatsDAO.php

@@ -4,6 +4,10 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 
 	const ENTRY_COUNT_PERIOD = 30;
 
+	protected function sqlFloor($s) {
+		return "FLOOR($s)";
+	}
+
 	/**
 	 * Calculates entry repartition for all feeds and for main stream.
 	 *
@@ -37,12 +41,12 @@ class FreshRSS_StatsDAO extends Minz_ModelPdo {
 			$filter .= "AND e.id_feed = {$feed}";
 		}
 		$sql = <<<SQL
-SELECT COUNT(1) AS `total`,
-COUNT(1) - SUM(e.is_read) AS `unread`,
-SUM(e.is_read) AS `read`,
-SUM(e.is_favorite) AS `favorite`
-FROM {$this->prefix}entry AS e
-, {$this->prefix}feed AS f
+SELECT COUNT(1) AS total,
+COUNT(1) - SUM(e.is_read) AS count_unreads,
+SUM(e.is_read) AS count_reads,
+SUM(e.is_favorite) AS count_favorites
+FROM `{$this->prefix}entry` AS e
+, `{$this->prefix}feed` AS f
 WHERE e.id_feed = f.id
 {$filter}
 SQL;
@@ -65,10 +69,11 @@ SQL;
 		$oldest = $midnight - (self::ENTRY_COUNT_PERIOD * 86400);
 
 		// Get stats per day for the last 30 days
+		$sqlDay = $this->sqlFloor("(date - $midnight) / 86400");
 		$sql = <<<SQL
-SELECT FLOOR((date - {$midnight}) / 86400) AS day,
+SELECT {$sqlDay} AS day,
 COUNT(*) as count
-FROM {$this->prefix}entry
+FROM `{$this->prefix}entry`
 WHERE date >= {$oldest} AND date < {$midnight}
 GROUP BY day
 ORDER BY day ASC
@@ -138,7 +143,7 @@ SQL;
 		$sql = <<<SQL
 SELECT DATE_FORMAT(FROM_UNIXTIME(e.date), '{$period}') AS period
 , COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 GROUP BY period
 ORDER BY period ASC
@@ -202,7 +207,7 @@ SQL;
 SELECT COUNT(1) AS count
 , MIN(date) AS date_min
 , MAX(date) AS date_max
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 SQL;
 		$stm = $this->bd->prepare($sql);
@@ -246,8 +251,8 @@ SQL;
 		$sql = <<<SQL
 SELECT c.name AS label
 , COUNT(f.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f
 WHERE c.id = f.category
 GROUP BY label
 ORDER BY data DESC
@@ -269,9 +274,9 @@ SQL;
 		$sql = <<<SQL
 SELECT c.name AS label
 , COUNT(e.id) AS data
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE c.id = f.category
 AND f.id = e.id_feed
 GROUP BY label
@@ -295,9 +300,9 @@ SELECT f.id AS id
 , MAX(f.name) AS name
 , MAX(c.name) AS category
 , COUNT(e.id) AS count
-FROM {$this->prefix}category AS c,
-{$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}category` AS c,
+`{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE c.id = f.category
 AND f.id = e.id_feed
 GROUP BY f.id
@@ -320,8 +325,8 @@ SELECT MAX(f.id) as id
 , MAX(f.name) AS name
 , MAX(date) AS last_date
 , COUNT(*) AS nb_articles
-FROM {$this->prefix}feed AS f,
-{$this->prefix}entry AS e
+FROM `{$this->prefix}feed` AS f,
+`{$this->prefix}entry` AS e
 WHERE f.id = e.id_feed
 GROUP BY f.id
 ORDER BY name

+ 67 - 0
app/Models/StatsDAOPGSQL.php

@@ -0,0 +1,67 @@
+<?php
+
+class FreshRSS_StatsDAOPGSQL extends FreshRSS_StatsDAO {
+
+	/**
+	 * Calculates the number of article per hour of the day per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerHour($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('hour', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per day of week per feed
+	 *
+	 * @param integer $feed id
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerDayOfWeek($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('day', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per month per feed
+	 *
+	 * @param integer $feed
+	 * @return string
+	 */
+	public function calculateEntryRepartitionPerFeedPerMonth($feed = null) {
+		return $this->calculateEntryRepartitionPerFeedPerPeriod('month', $feed);
+	}
+
+	/**
+	 * Calculates the number of article per period per feed
+	 *
+	 * @param string $period format string to use for grouping
+	 * @param integer $feed id
+	 * @return string
+	 */
+	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
+		$restrict = '';
+		if ($feed) {
+			$restrict = "WHERE e.id_feed = {$feed}";
+		}
+		$sql = <<<SQL
+SELECT extract( {$period} from to_timestamp(e.date)) AS period
+, COUNT(1) AS count
+FROM "{$this->prefix}entry" AS e
+{$restrict}
+GROUP BY period
+ORDER BY period ASC
+SQL;
+
+		$stm = $this->bd->prepare($sql);
+		$stm->execute();
+		$res = $stm->fetchAll(PDO::FETCH_NAMED);
+
+		foreach ($res as $value) {
+			$repartition[(int) $value['period']] = (int) $value['count'];
+		}
+
+		return $repartition;
+	}
+
+}

+ 6 - 2
app/Models/StatsDAOSQLite.php

@@ -2,6 +2,10 @@
 
 class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
 
+	protected function sqlFloor($s) {
+		return "CAST(($s) AS INT)";
+	}
+
 	protected function calculateEntryRepartitionPerFeedPerPeriod($period, $feed = null) {
 		if ($feed) {
 			$restrict = "WHERE e.id_feed = {$feed}";
@@ -11,7 +15,7 @@ class FreshRSS_StatsDAOSQLite extends FreshRSS_StatsDAO {
 		$sql = <<<SQL
 SELECT strftime('{$period}', e.date, 'unixepoch') AS period
 , COUNT(1) AS count
-FROM {$this->prefix}entry AS e
+FROM `{$this->prefix}entry` AS e
 {$restrict}
 GROUP BY period
 ORDER BY period ASC
@@ -26,7 +30,7 @@ SQL;
 			$repartition[(int) $value['period']] = (int) $value['count'];
 		}
 
-		return $this->convertToSerie($repartition);
+		return $repartition;
 	}
 
 }

+ 2 - 0
app/SQL/install.sql.mysql.php

@@ -1,4 +1,6 @@
 <?php
+define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
+
 define('SQL_CREATE_TABLES', '
 CREATE TABLE IF NOT EXISTS `%1$scategory` (
 	`id` SMALLINT NOT NULL AUTO_INCREMENT,	-- v0.7

+ 59 - 0
app/SQL/install.sql.pgsql.php

@@ -0,0 +1,59 @@
+<?php
+define('SQL_CREATE_DB', 'CREATE DATABASE %1$s ENCODING \'UTF8\';');
+
+global $SQL_CREATE_TABLES;
+$SQL_CREATE_TABLES = array(
+'CREATE TABLE IF NOT EXISTS "%1$scategory" (
+	"id" SERIAL PRIMARY KEY,
+	"name" VARCHAR(255) UNIQUE NOT NULL
+);',
+
+'CREATE TABLE IF NOT EXISTS "%1$sfeed" (
+	"id" SERIAL PRIMARY KEY,
+	"url" varchar(511) UNIQUE NOT NULL,
+	"category" SMALLINT DEFAULT 0,
+	"name" VARCHAR(255) NOT NULL,
+	"website" VARCHAR(255),
+	"description" text,
+	"lastUpdate" INT DEFAULT 0,
+	"priority" SMALLINT NOT NULL DEFAULT 10,
+	"pathEntries" VARCHAR(511) DEFAULT NULL,
+	"httpAuth" VARCHAR(511) DEFAULT NULL,
+	"error" smallint DEFAULT 0,
+	"keep_history" INT NOT NULL DEFAULT -2,
+	"ttl" INT NOT NULL DEFAULT -2,
+	"cache_nbEntries" INT DEFAULT 0,
+	"cache_nbUnreads" INT DEFAULT 0,
+	FOREIGN KEY ("category") REFERENCES "%1$scategory" ("id") ON DELETE SET NULL ON UPDATE CASCADE
+);',
+'CREATE INDEX name_index ON "%1$sfeed" ("name");',
+'CREATE INDEX priority_index ON "%1$sfeed" ("priority");',
+'CREATE INDEX keep_history_index ON "%1$sfeed" ("keep_history");',
+
+'CREATE TABLE IF NOT EXISTS "%1$sentry" (
+	"id" BIGINT NOT NULL PRIMARY KEY,
+	"guid" VARCHAR(760) UNIQUE NOT NULL,
+	"title" VARCHAR(255) NOT NULL,
+	"author" VARCHAR(255),
+	"content" TEXT,
+	"link" VARCHAR(1023) NOT NULL,
+	"date" INT,
+	"lastSeen" INT DEFAULT 0,
+	"hash" BYTEA,
+	"is_read" SMALLINT NOT NULL DEFAULT 0,
+	"is_favorite" SMALLINT NOT NULL DEFAULT 0,
+	"id_feed" SMALLINT,
+	"tags" VARCHAR(1023),
+	FOREIGN KEY ("id_feed") REFERENCES "%1$sfeed" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
+	UNIQUE ("id_feed","guid")
+);',
+'CREATE INDEX is_favorite_index ON "%1$sentry" ("is_favorite");',
+'CREATE INDEX is_read_index ON "%1$sentry" ("is_read");',
+'CREATE INDEX entry_lastSeen_index ON "%1$sentry" ("lastSeen");',
+
+'INSERT INTO "%1$scategory" (id, name) VALUES(1, \'%2$s\');',
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) VALUES(\'http://freshrss.org/feeds/all.atom.xml\', 1, \'FreshRSS.org\', \'http://freshrss.org/\', \'FreshRSS, a free, self-hostable aggregator…\', 86400);',
+'INSERT INTO "%1$sfeed" (url, category, name, website, description, ttl) VALUES(\'https://github.com/FreshRSS/FreshRSS/releases.atom\', 1, \'FreshRSS @ GitHub\', \'https://github.com/FreshRSS/FreshRSS/\', \'FreshRSS releases @ GitHub\', 86400);',
+);
+
+define('SQL_DROP_TABLES', 'DROP TABLES "%1$sentry", "%1$sfeed", "%1$scategory"');

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP instalace',

+ 2 - 2
app/i18n/cz/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Máte požadovanou knihovnu pro regulární výrazy (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Nemáte PDO nebo některý z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Máte PDO a alespoň jeden z podporovaných ovladačů (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'Vaše verze PHP je %s, ale FreshRSS vyžaduje alespoň verzi %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP-Installation',

+ 2 - 2
app/i18n/de/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Sie haben die benötigte Bibliothek für reguläre Ausdrücke (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Ihnen fehlt PDO oder einer der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Sie haben PDO und mindestens einen der unterstützten Treiber (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'Ihre PHP-Version ist %s aber FreshRSS benötigt mindestens Version %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP installation',

+ 2 - 2
app/i18n/en/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'You have the required library for regular expressions (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'You lack PDO or one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'You have PDO and at least one of the supported drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'Your PHP version is %s but FreshRSS requires at least version %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'Installation de PHP',

+ 2 - 2
app/i18n/fr/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Vous disposez du nécessaire pour les expressions régulières (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite).',
-			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Vous ne disposez pas de PDO ou d’un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'Vous disposez de PDO et d’au moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'Votre version de PHP est la %s mais FreshRSS requiert au moins la version %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'Installazione PHP',

+ 2 - 2
app/i18n/it/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Libreria richiesta per le regular expressions presente (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite).',
+			'nok' => 'Manca PDO o uno degli altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO e altri driver supportati (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'Installazione PHP',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
-			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'U mist PDO of een van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP installatie',

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

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'U hebt de benodigde bibliotheek voor regular expressions (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite).',
-			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite).',
+			'nok' => 'U mist PDO of één van de ondersteunde (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'U hebt PDO en ten minste één van de ondersteunde drivers (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'Uw PHP versie is %s maar FreshRSS benodigd tenminste versie %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
-			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP installation',

+ 2 - 2
app/i18n/ru/install.php

@@ -69,8 +69,8 @@ return array(
 			'ok' => 'У вас установлена необходимая библиотека для работы с регулярными выражениями (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite).',
-			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite).',
+			'nok' => 'У вас не установлен PDO или один из необходимых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'У вас установлен PDO и как минимум один из поддерживаемых драйверов (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'У вас установлен PHP версии %s, но FreshRSS необходима версия не ниже %s.',

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

@@ -71,8 +71,8 @@ return array(
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'_' => 'PHP kurulumu',

+ 2 - 2
app/i18n/tr/install.php

@@ -73,8 +73,8 @@ return array(
 			'ok' => 'Düzenli ifadeler kütüphanesi sorunsuz (PCRE).',
 		),
 		'pdo' => array(
-			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite).',
-			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite).',
+			'nok' => 'PDO veya PDO destekli bir sürücü eksik (pdo_mysql, pdo_sqlite, pdo_pgsql).',
+			'ok' => 'PDO sorunsuz (pdo_mysql, pdo_sqlite, pdo_pgsql).',
 		),
 		'php' => array(
 			'nok' => 'PHP versiyonunuz %s fakat FreshRSS için gerekli olan en düşük sürüm %s.',

+ 34 - 25
app/install.php

@@ -19,8 +19,6 @@ if (isset($_GET['step'])) {
 	define('STEP', 0);
 }
 
-define('SQL_CREATE_DB', 'CREATE DATABASE IF NOT EXISTS %1$s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;');
-
 if (STEP === 3 && isset($_POST['type'])) {
 	$_SESSION['bd_type'] = $_POST['type'];
 }
@@ -33,6 +31,9 @@ if (isset($_SESSION['bd_type'])) {
 	case 'sqlite':
 		include(APP_PATH . '/SQL/install.sql.sqlite.php');
 		break;
+	case 'pgsql':
+		include(APP_PATH . '/SQL/install.sql.pgsql.php');
+		break;
 	}
 }
 
@@ -199,6 +200,9 @@ function saveStep3() {
 			$_SESSION['bd_prefix'] = substr($_POST['prefix'], 0, 16);
 			$_SESSION['bd_prefix_user'] = $_SESSION['bd_prefix'] . (empty($_SESSION['default_user']) ? '' : ($_SESSION['default_user'] . '_'));
 		}
+		if ($_SESSION['bd_type'] === 'pgsql') {
+			$_SESSION['bd_base'] = strtolower($_SESSION['bd_base']);
+		}
 
 		// We use dirname to remove the /i part
 		$base_url = dirname(Minz_Request::guessBaseUrl());
@@ -235,26 +239,6 @@ function saveStep3() {
 	invalidateHttpCache();
 }
 
-function newPdo() {
-	switch ($_SESSION['bd_type']) {
-	case 'mysql':
-		$str = 'mysql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
-		$driver_options = array(
-			PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4',
-		);
-		break;
-	case 'sqlite':
-		$str = 'sqlite:' . join_path(USERS_PATH, $_SESSION['default_user'], 'db.sqlite');
-		$driver_options = array(
-			PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
-		);
-		break;
-	default:
-		return false;
-	}
-	return new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
-}
-
 function deleteInstall() {
 	$res = unlink(join_path(DATA_PATH, 'do-install.txt'));
 
@@ -301,7 +285,8 @@ function checkStep1() {
 	$curl = extension_loaded('curl');
 	$pdo_mysql = extension_loaded('pdo_mysql');
 	$pdo_sqlite = extension_loaded('pdo_sqlite');
-	$pdo = $pdo_mysql || $pdo_sqlite;
+	$pdo_pgsql = extension_loaded('pdo_pgsql');
+	$pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql;
 	$pcre = extension_loaded('pcre');
 	$ctype = extension_loaded('ctype');
 	$dom = class_exists('DOMDocument');
@@ -319,6 +304,7 @@ function checkStep1() {
 		'curl' => $curl ? 'ok' : 'ko',
 		'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko',
 		'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko',
+		'pdo-pgsql' => $pdo_pgsql ? 'ok' : 'ko',
 		'pdo' => $pdo ? 'ok' : 'ko',
 		'pcre' => $pcre ? 'ok' : 'ko',
 		'ctype' => $ctype ? 'ok' : 'ko',
@@ -435,6 +421,23 @@ function checkBD() {
 				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
 			);
 			break;
+		case 'pgsql':
+			$driver_options = array(
+				PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
+			);
+
+			try {	// on ouvre une connexion juste pour créer la base si elle n'existe pas
+				$str = 'pgsql:host=' . $_SESSION['bd_host'] . ';dbname=postgres';
+				$c = new PDO($str, $_SESSION['bd_user'], $_SESSION['bd_password'], $driver_options);
+				$sql = sprintf(SQL_CREATE_DB, $_SESSION['bd_base']);
+				$res = $c->query($sql);
+			} catch (PDOException $e) {
+				syslog(LOG_DEBUG, 'pgsql ' . $e->getMessage());
+			}
+
+			// on écrase la précédente connexion en sélectionnant la nouvelle BDD
+			$str = 'pgsql:host=' . $_SESSION['bd_host'] . ';dbname=' . $_SESSION['bd_base'];
+			break;
 		default:
 			return false;
 		}
@@ -690,7 +693,7 @@ function printStep3() {
 	<p class="alert alert-error"><span class="alert-head"><?php echo _t('gen.short.damn'); ?></span> <?php echo _t('install.bdd.conf.ko'),(empty($_SESSION['bd_error']) ? '' : ' : ' . $_SESSION['bd_error']); ?></p>
 	<?php } ?>
 
-	<form action="index.php?step=3" method="post">
+	<form action="index.php?step=3" method="post" autocomplete="off">
 		<legend><?php echo _t('install.bdd.conf'); ?></legend>
 		<div class="form-group">
 			<label class="group-name" for="type"><?php echo _t('install.bdd.type'); ?></label>
@@ -708,6 +711,12 @@ function printStep3() {
 					SQLite
 				</option>
 				<?php }?>
+				<?php if (extension_loaded('pdo_pgsql')) {?>
+				<option value="pgsql"
+					<?php echo(isset($_SESSION['bd_type']) && $_SESSION['bd_type'] === 'pgsql') ? 'selected="selected"' : ''; ?>>
+					PostgreSQL (⚠️ experimental)
+				</option>
+				<?php }?>
 				</select>
 			</div>
 		</div>
@@ -730,7 +739,7 @@ function printStep3() {
 		<div class="form-group">
 			<label class="group-name" for="pass"><?php echo _t('install.bdd.password'); ?></label>
 			<div class="group-controls">
-				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" />
+				<input type="password" id="pass" name="pass" value="<?php echo isset($_SESSION['bd_password']) ? $_SESSION['bd_password'] : ''; ?>" tabindex="4" autocomplete="off" />
 			</div>
 		</div>
 

+ 6 - 6
app/views/stats/index.phtml

@@ -23,18 +23,18 @@
 				</tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_read'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['read']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['read']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_reads']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_reads']); ?></td>
 				</tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_unread'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['unread']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['unread']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_unreads']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_unreads']); ?></td>
 				</tr>
 				<tr>
 					<th><?php echo _t('admin.stats.status_favorites'); ?></th>
-					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['favorite']); ?></td>
-					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['favorite']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['main_stream']['count_favorites']); ?></td>
+					<td class="numeric"><?php echo format_number($this->repartition['all_feeds']['count_favorites']); ?></td>
 				</tr>
 			</tbody>
 		</table>

+ 4 - 4
app/views/stats/repartition.phtml

@@ -12,7 +12,7 @@
 		if (!empty($feeds)) {
 			echo '<optgroup label="', $category->name(), '">';
 			foreach ($feeds as $feed) {
-				if ($this->feed && $feed->id() == $this->feed->id()){
+				if ($this->feed && $feed->id() == $this->feed->id()) {
 					echo '<option value="', $feed->id(), '" selected="selected" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
 				} else {
 					echo '<option value="', $feed->id(), '" data-url="', _url('stats', 'repartition', 'id', $feed->id()), '">', $feed->name(), '</option>';
@@ -39,9 +39,9 @@
 		</tr>
 		<tr>
 			<td class="numeric"><?php echo $this->repartition['total']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['read']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['unread']; ?></td>
-			<td class="numeric"><?php echo $this->repartition['favorite']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_reads']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_unreads']; ?></td>
+			<td class="numeric"><?php echo $this->repartition['count_favorites']; ?></td>
 		</tr>
 		</table>
 	</div>

+ 58 - 31
lib/Minz/ModelPdo.php

@@ -56,39 +56,41 @@ class Minz_ModelPdo {
 		$dbServer = parse_url('db://' . $db['host']);
 
 		try {
-			$type = $db['type'];
-			if ($type === 'mysql') {
-				$string = 'mysql:host=' . $dbServer['host']
-				        . ';dbname=' . $db['base']
-				        . ';charset=utf8mb4';
-				if (!empty($dbServer['port'])) {
-					$string .= ';port=' . $dbServer['port'];
-				}
-				$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
-				$this->prefix = $db['prefix'] . $currentUser . '_';
-			} elseif ($type === 'sqlite') {
-				$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
-				//$driver_options[PDO::ATTR_ERRMODE] = PDO::ERRMODE_EXCEPTION;
-				$this->prefix = '';
-			} else {
-				throw new Minz_PDOConnectionException(
-					'Invalid database type!',
-					$db['user'], Minz_Exception::ERROR
-				);
-			}
-			self::$sharedDbType = $type;
-			self::$sharedPrefix = $this->prefix;
-
-			$this->bd = new MinzPDO(
-				$string,
-				$db['user'],
-				$db['password'],
-				$driver_options
-			);
-			if ($type === 'sqlite') {
-				$this->bd->exec('PRAGMA foreign_keys = ON;');
+			switch ($db['type']) {
+				case 'mysql':
+					$string = 'mysql:host=' . $dbServer['host'] . ';dbname=' . $db['base'] . ';charset=utf8mb4';
+					if (!empty($dbServer['port'])) {
+						$string .= ';port=' . $dbServer['port'];
+					}
+					$driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4';
+					$this->prefix = $db['prefix'] . $currentUser . '_';
+					$this->bd = new MinzPDOMySql($string, $db['user'], $db['password'], $driver_options);
+					break;
+				case 'sqlite':
+					$string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite');
+					$this->prefix = '';
+					$this->bd = new MinzPDOMSQLite($string, $db['user'], $db['password'], $driver_options);
+					$this->bd->exec('PRAGMA foreign_keys = ON;');
+					break;
+				case 'pgsql':
+					$string = 'pgsql:host=' . $dbServer['host'] . ';dbname=' . $db['base'];
+					if (!empty($dbServer['port'])) {
+						$string .= ';port=' . $dbServer['port'];
+					}
+					$this->prefix = $db['prefix'] . $currentUser . '_';
+					$this->bd = new MinzPDOPGSQL($string, $db['user'], $db['password'], $driver_options);
+					$this->bd->exec("SET NAMES 'UTF8';");
+					break;
+				default:
+					throw new Minz_PDOConnectionException(
+						'Invalid database type!',
+						$db['user'], Minz_Exception::ERROR
+					);
+					break;
 			}
 			self::$sharedBd = $this->bd;
+			self::$sharedDbType = $db['type'];
+			self::$sharedPrefix = $this->prefix;
 		} catch (Exception $e) {
 			throw new Minz_PDOConnectionException(
 				$string,
@@ -123,18 +125,43 @@ class MinzPDO extends PDO {
 		}
 	}
 
+	protected function compatibility($statement) {
+		return $statement;
+	}
+
 	public function prepare($statement, $driver_options = array()) {
 		MinzPDO::check($statement);
+		$statement = $this->compatibility($statement);
 		return parent::prepare($statement, $driver_options);
 	}
 
 	public function exec($statement) {
 		MinzPDO::check($statement);
+		$statement = $this->compatibility($statement);
 		return parent::exec($statement);
 	}
 
 	public function query($statement) {
 		MinzPDO::check($statement);
+		$statement = $this->compatibility($statement);
 		return parent::query($statement);
 	}
 }
+
+class MinzPDOMySql extends MinzPDO {
+	public function lastInsertId($name = null) {
+		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL
+	}
+}
+
+class MinzPDOMSQLite extends MinzPDO {
+	public function lastInsertId($name = null) {
+		return parent::lastInsertId();	//We discard the name, only used by PostgreSQL
+	}
+}
+
+class MinzPDOPGSQL extends MinzPDO {
+	protected function compatibility($statement) {
+		return str_replace(array('`', ' LIKE '), array('"', ' ILIKE '), $statement);
+	}
+}

+ 4 - 2
p/scripts/install.js

@@ -43,13 +43,15 @@ if (auth_type) {
 function mySqlShowHide() {
 	var mysql = document.getElementById('mysql');
 	if (mysql) {
-		mysql.style.display = document.getElementById('type').value === 'mysql' ? 'block' : 'none';
-		if (document.getElementById('type').value !== 'mysql') {
+		if (document.getElementById('type').value === 'sqlite') {
 			document.getElementById('host').value = '';
 			document.getElementById('user').value = '';
 			document.getElementById('pass').value = '';
 			document.getElementById('base').value = '';
 			document.getElementById('prefix').value = '';
+			mysql.style.display = 'none';
+		} else {
+			mysql.style.display = 'block';
 		}
 	}
 }