Merge pull request #1195 from Alkarex/PostgreSQL

PostgreSQL
This commit is contained in:
Alexandre Alapetite
2016-10-01 19:35:14 +02:00
committed by GitHub
38 changed files with 538 additions and 230 deletions

View File

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

View File

@@ -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 dencore 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 dencodages), [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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']);
$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->hasNativeHex()) {
$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hash']);
} else {
$valuesTmp['hashBin'] = pack('H*', $valuesTmp['hash']); //hex2bin() is PHP5.4+
$this->addEntryPrepared->bindParam(':hash', $valuesTmp['hashBin']);
}
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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 dun des drivers supportés (pdo_mysql, pdo_sqlite).',
'ok' => 'Vous disposez de PDO et dau moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
'nok' => 'Vous ne disposez pas de PDO ou dun des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Vous disposez de PDO et dau moins un des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
),
'php' => array(
'_' => 'Installation de PHP',

View File

@@ -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 dun des drivers supportés (pdo_mysql, pdo_sqlite).',
'ok' => 'Vous disposez de PDO et dau moins un des drivers supportés (pdo_mysql, pdo_sqlite).',
'nok' => 'Vous ne disposez pas de PDO ou dun des drivers supportés (pdo_mysql, pdo_sqlite, pdo_pgsql).',
'ok' => 'Vous disposez de PDO et dau 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.',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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