diff --git a/.travis.yml b/.travis.yml index 91d6ddd..36b4c38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,5 @@ before_script: - composer install script: - - ./vendor/bin/phpunit -c tests/phpunit.unit.xml + - ./vendor/bin/phpunit -c tests/phpunit.unit.sqlite.xml + - ./vendor/bin/phpunit -c tests/phpunit.unit.postgres.xml diff --git a/Makefile b/Makefile index 3b14703..7006a0c 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ .PHONY: docker-run .PHONY: js .PHONY: unit-test-sqlite +.PHONY: unit-test-postgres JS_FILE = assets/js/all.js CONTAINER = miniflux @@ -39,4 +40,8 @@ archive: @ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip unit-test-sqlite: - @ ./vendor/bin/phpunit -c tests/phpunit.unit.xml + @ ./vendor/bin/phpunit -c tests/phpunit.unit.sqlite.xml + +unit-test-postgres: + @ ./vendor/bin/phpunit -c tests/phpunit.unit.postgres.xml + diff --git a/app/common.php b/app/common.php index c5f36aa..9d04a14 100644 --- a/app/common.php +++ b/app/common.php @@ -10,30 +10,10 @@ require_once __DIR__.'/constants.php'; require_once __DIR__.'/check_setup.php'; require_once __DIR__.'/functions.php'; -PicoDb\Database::setInstance('db', function() { - $db = new PicoDb\Database(array( - 'driver' => 'sqlite', - 'filename' => DB_FILENAME, - )); - - $db->getStatementHandler()->withLogging(); - - if ($db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) { - return $db; - } else { - $errors = $db->getLogMessages(); - $nb_errors = count($errors); - - $html = 'Unable to migrate the database schema, please copy and paste this message and create a bug report:
';
- $html .= (isset($errors[$nb_errors - 1]) ? $errors[$nb_errors - 1] : 'Unknown SQL error').PHP_EOL.PHP_EOL;
- $html .= '- PHP version: '.phpversion().PHP_EOL;
- $html .= '- SAPI: '.php_sapi_name().PHP_EOL;
- $html .= '- PDO Sqlite version: '.phpversion('pdo_sqlite').PHP_EOL;
- $html .= '- Sqlite version: '.$db->getDriver()->getDatabaseVersion().PHP_EOL;
- $html .= '- OS: '.php_uname();
- $html .= '
';
-
- die($html);
+PicoDb\Database::setInstance('db', function () {
+ try {
+ return Miniflux\Database\get_connection();
+ } catch (Exception $e) {
+ die($e->getMessage());
}
});
diff --git a/app/constants.php b/app/constants.php
index 0fd5dba..c711576 100644
--- a/app/constants.php
+++ b/app/constants.php
@@ -13,7 +13,13 @@ defined('DATA_DIRECTORY') or define('DATA_DIRECTORY', ROOT_DIRECTORY.DIRECTORY_S
defined('FAVICON_DIRECTORY') or define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
defined('FAVICON_URL_PATH') or define('FAVICON_URL_PATH', 'data/favicons');
+defined('DB_DRIVER') or define('DB_DRIVER', 'sqlite');
defined('DB_FILENAME') or define('DB_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'db.sqlite');
+defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost');
+defined('DB_PORT') or define('DB_PORT', null);
+defined('DB_NAME') or define('DB_NAME', 'miniflux');
+defined('DB_USERNAME') or define('DB_USERNAME', '');
+defined('DB_PASSWORD') or define('DB_PASSWORD', '');
defined('DEBUG_MODE') or define('DEBUG_MODE', false);
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');
diff --git a/app/controllers/feed.php b/app/controllers/feed.php
index 9dff1d7..6a426a9 100644
--- a/app/controllers/feed.php
+++ b/app/controllers/feed.php
@@ -197,7 +197,7 @@ Router\action('subscribe', function () {
$values['rtl'],
$values['cloak_referrer'],
$values['feed_group_ids'],
- $values['groups']
+ $values['group_name']
);
if ($feed_id >= 1) {
diff --git a/app/controllers/item.php b/app/controllers/item.php
index 53c2010..8a3f6eb 100644
--- a/app/controllers/item.php
+++ b/app/controllers/item.php
@@ -222,21 +222,13 @@ Router\get_action('mark-item-removed', function () {
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id);
});
-Router\post_action('latest-feeds-items', function () {
+Router\get_action('latest-feeds-items', function () {
$user_id = SessionStorage::getInstance()->getUserId();
- $items = Model\Item\get_latest_feeds_items($user_id);
+ $items_timestamps = Model\Item\get_latest_unread_items_timestamps($user_id);
$nb_unread_items = Model\Item\count_by_status($user_id, 'unread');
- $feeds = array_reduce($items, function ($result, $item) {
- $result[$item['id']] = array(
- 'time' => $item['updated'] ?: 0,
- 'status' => $item['status']
- );
- return $result;
- }, array());
-
Response\json(array(
- 'feeds' => $feeds,
- 'nbUnread' => $nb_unread_items
+ 'last_items_timestamps' => $items_timestamps,
+ 'nb_unread_items' => $nb_unread_items
));
});
diff --git a/app/core/database.php b/app/core/database.php
new file mode 100644
index 0000000..08b135a
--- /dev/null
+++ b/app/core/database.php
@@ -0,0 +1,45 @@
+getStatementHandler()->withLogging();
+
+ if ($db->schema('\Miniflux\Schema')->check(Schema\VERSION)) {
+ return $db;
+ } else {
+ $errors = $db->getLogMessages();
+ $nb_errors = count($errors);
+ $last_error = isset($errors[$nb_errors - 1]) ? $errors[$nb_errors - 1] : 'Unknown SQL error';
+ throw new RuntimeException('Unable to migrate the database schema: '.$last_error);
+ }
+}
+
+function get_connection_parameters()
+{
+ if (DB_DRIVER === 'postgres') {
+ require_once __DIR__.'/../schemas/postgres.php';
+ $params = array(
+ 'driver' => 'postgres',
+ 'hostname' => DB_HOSTNAME,
+ 'username' => DB_USERNAME,
+ 'password' => DB_PASSWORD,
+ 'database' => DB_NAME,
+ 'port' => DB_PORT,
+ );
+ } else {
+ require_once __DIR__.'/../schemas/sqlite.php';
+ $params = array(
+ 'driver' => 'sqlite',
+ 'filename' => DB_FILENAME,
+ );
+ }
+
+ return $params;
+}
diff --git a/app/helpers/config.php b/app/helpers/config.php
index e4f4ceb..cf56c05 100644
--- a/app/helpers/config.php
+++ b/app/helpers/config.php
@@ -7,17 +7,20 @@ use Miniflux\Session\SessionStorage;
function config($parameter, $default = null)
{
- $session = SessionStorage::getInstance();
- $cache = $session->getConfig();
$value = null;
+ $session = SessionStorage::getInstance();
- if (empty($cache)) {
- $cache = Model\Config\get_all($session->getUserId());
- $session->setConfig($cache);
- }
+ if ($session->isLogged()) {
+ $cache = $session->getConfig();
- if (array_key_exists($parameter, $cache)) {
- $value = $cache[$parameter];
+ if (empty($cache)) {
+ $cache = Model\Config\get_all($session->getUserId());
+ $session->setConfig($cache);
+ }
+
+ if (array_key_exists($parameter, $cache)) {
+ $value = $cache[$parameter];
+ }
}
if ($value === null) {
diff --git a/app/models/feed.php b/app/models/feed.php
index 78a7420..f1c7b26 100644
--- a/app/models/feed.php
+++ b/app/models/feed.php
@@ -46,26 +46,42 @@ function get_feeds($user_id)
return Database::getInstance('db')
->table(TABLE)
->eq('user_id', $user_id)
+ ->desc('parsing_error')
+ ->desc('enabled')
->asc('title')
->findAll();
}
function get_feeds_with_items_count($user_id)
{
- return Database::getInstance('db')
- ->table(TABLE)
- ->columns(
- 'feeds.*',
- 'SUM(CASE WHEN items.status IN ("unread") THEN 1 ELSE 0 END) as "items_unread"',
- 'SUM(CASE WHEN items.status IN ("read", "unread") THEN 1 ELSE 0 END) as "items_total"'
- )
- ->join('items', 'feed_id', 'id')
- ->eq('feeds.user_id', $user_id)
- ->groupBy('feeds.id')
- ->desc('feeds.parsing_error')
- ->desc('feeds.enabled')
- ->asc('feeds.title')
+ $feeds_count = array();
+ $feeds = get_feeds($user_id);
+ $feed_items = Database::getInstance('db')
+ ->table(Item\TABLE)
+ ->columns('feed_id', 'status', 'count(*) as nb')
+ ->eq('user_id', $user_id)
+ ->neq('status', Item\STATUS_REMOVED)
+ ->groupBy('feed_id', 'status')
->findAll();
+
+ foreach ($feed_items as $row) {
+ if (! isset($feeds_count[$row['feed_id']])) {
+ $feeds_count[$row['feed_id']] = array('unread' => 0, 'total' => 0);
+ }
+
+ if ($row['status'] === 'unread') {
+ $feeds_count[$row['feed_id']]['unread'] = $row['nb'];
+ }
+
+ $feeds_count[$row['feed_id']]['total'] += $row['nb'];
+ }
+
+ foreach ($feeds as &$feed) {
+ $feed['items_unread'] = isset($feeds_count[$feed['id']]) ? $feeds_count[$feed['id']]['unread'] : 0;
+ $feed['items_total'] = isset($feeds_count[$feed['id']]) ? $feeds_count[$feed['id']]['total'] : 0;
+ }
+
+ return $feeds;
}
function get_feed_ids($user_id, $limit = null)
diff --git a/app/models/item.php b/app/models/item.php
index 5d8a90a..94ba28b 100644
--- a/app/models/item.php
+++ b/app/models/item.php
@@ -297,19 +297,18 @@ function get_item_ids_by_status($user_id, $status)
->findAllByColumn('id');
}
-function get_latest_feeds_items($user_id)
+function get_latest_unread_items_timestamps($user_id)
{
return Database::getInstance('db')
- ->table(Feed\TABLE)
+ ->table(TABLE)
->columns(
- 'feeds.id',
- 'MAX(items.updated) as updated',
- 'items.status'
+ 'feed_id',
+ 'MAX(updated) as updated'
)
- ->join(TABLE, 'feed_id', 'id')
- ->eq('feeds.user_id', $user_id)
- ->groupBy('feeds.id')
- ->orderBy('feeds.id')
+ ->eq('user_id', $user_id)
+ ->eq('status', STATUS_UNREAD)
+ ->groupBy('feed_id')
+ ->desc('updated')
->findAll();
}
diff --git a/app/schemas/postgres.php b/app/schemas/postgres.php
new file mode 100644
index 0000000..a83890b
--- /dev/null
+++ b/app/schemas/postgres.php
@@ -0,0 +1,133 @@
+exec("CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ username VARCHAR(50) NOT NULL UNIQUE,
+ password VARCHAR(255) NOT NULL,
+ is_admin BOOLEAN DEFAULT FALSE,
+ last_login BIGINT,
+ api_token VARCHAR(255) NOT NULL UNIQUE,
+ bookmarklet_token VARCHAR(255) NOT NULL UNIQUE,
+ cronjob_token VARCHAR(255) NOT NULL UNIQUE,
+ feed_token VARCHAR(255) NOT NULL UNIQUE,
+ fever_token VARCHAR(255) NOT NULL UNIQUE,
+ fever_api_key VARCHAR(255) NOT NULL UNIQUE
+ )");
+
+ $pdo->exec('CREATE TABLE user_settings (
+ "user_id" INTEGER NOT NULL,
+ "key" VARCHAR(255) NOT NULL,
+ "value" TEXT NOT NULL,
+ PRIMARY KEY("user_id", "key"),
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE feeds (
+ id BIGSERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ feed_url VARCHAR(255) NOT NULL,
+ site_url VARCHAR(255),
+ title VARCHAR(255) NOT NULL,
+ last_checked BIGINT DEFAULT 0,
+ last_modified VARCHAR(255),
+ etag VARCHAR(255),
+ enabled BOOLEAN DEFAULT TRUE,
+ download_content BOOLEAN DEFAULT FALSE,
+ parsing_error BOOLEAN DEFAULT FALSE,
+ rtl BOOLEAN DEFAULT FALSE,
+ cloak_referrer BOOLEAN DEFAULT FALSE,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(user_id, feed_url)
+ )');
+
+ $pdo->exec('CREATE TABLE items (
+ id BIGSERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ feed_id BIGINT NOT NULL,
+ checksum VARCHAR(255) NOT NULL,
+ status VARCHAR(10) NOT NULL,
+ bookmark INTEGER DEFAULT 0,
+ url VARCHAR(255) NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ author VARCHAR(255),
+ content TEXT,
+ updated BIGINT,
+ enclosure_url VARCHAR(255),
+ enclosure_type VARCHAR(50),
+ language VARCHAR(50),
+ rtl INTEGER DEFAULT 0,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE,
+ UNIQUE(feed_id, checksum)
+ )');
+
+ $pdo->exec('CREATE TABLE "groups" (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
+ UNIQUE(user_id, title)
+ )');
+
+ $pdo->exec('CREATE TABLE "feeds_groups" (
+ feed_id BIGINT NOT NULL,
+ group_id INTEGER NOT NULL,
+ PRIMARY KEY(feed_id, group_id),
+ FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
+ FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE favicons (
+ id SERIAL PRIMARY KEY,
+ hash VARCHAR(255) UNIQUE,
+ type VARCHAR(50)
+ )');
+
+ $pdo->exec('CREATE TABLE "favicons_feeds" (
+ feed_id BIGINT NOT NULL,
+ favicon_id INTEGER NOT NULL,
+ PRIMARY KEY(feed_id, favicon_id),
+ FOREIGN KEY(favicon_id) REFERENCES favicons(id) ON DELETE CASCADE,
+ FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
+ )');
+
+ $pdo->exec('CREATE TABLE remember_me (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ ip VARCHAR(255),
+ user_agent VARCHAR(255),
+ token VARCHAR(255),
+ sequence VARCHAR(255),
+ expiration BIGINT,
+ date_creation BIGINT,
+ FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
+ )');
+
+ $fever_token = Helper\generate_token();
+ $rq = $pdo->prepare('
+ INSERT INTO users
+ (username, password, is_admin, api_token, bookmarklet_token, cronjob_token, feed_token, fever_token, fever_api_key)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ');
+
+ $rq->execute(array(
+ 'admin',
+ password_hash('admin', PASSWORD_BCRYPT),
+ '1',
+ Helper\generate_token(),
+ Helper\generate_token(),
+ Helper\generate_token(),
+ Helper\generate_token(),
+ $fever_token,
+ md5('admin:'.$fever_token),
+ ));
+}
diff --git a/app/schemas/sqlite.php b/app/schemas/sqlite.php
index 611cdf5..3a5c929 100644
--- a/app/schemas/sqlite.php
+++ b/app/schemas/sqlite.php
@@ -11,7 +11,7 @@ function version_1(PDO $pdo)
{
$pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY,
- username TEXT UNIQUE,
+ username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
is_admin INTEGER DEFAULT 0,
last_login INTEGER,
@@ -36,7 +36,7 @@ function version_1(PDO $pdo)
user_id INTEGER NOT NULL,
feed_url TEXT NOT NULL,
site_url TEXT,
- title TEXT COLLATE NOCASE,
+ title TEXT COLLATE NOCASE NOT NULL,
last_checked INTEGER DEFAULT 0,
last_modified TEXT,
etag TEXT,
@@ -54,10 +54,10 @@ function version_1(PDO $pdo)
user_id INTEGER NOT NULL,
feed_id INTEGER NOT NULL,
checksum TEXT NOT NULL,
- status TEXT,
+ status TEXT NOT NULL,
bookmark INTEGER DEFAULT 0,
- url TEXT,
- title TEXT COLLATE NOCASE,
+ url TEXT NOT NULL,
+ title TEXT COLLATE NOCASE NOT NULL,
author TEXT,
content TEXT,
updated INTEGER,
@@ -73,13 +73,13 @@ function version_1(PDO $pdo)
$pdo->exec('CREATE TABLE "groups" (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
- title TEXT COLLATE NOCASE,
+ title TEXT COLLATE NOCASE NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, title)
)');
$pdo->exec('CREATE TABLE "feeds_groups" (
- feed_id INTEGER NOT NULL,
+ feed_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
PRIMARY KEY(feed_id, group_id),
FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,
diff --git a/assets/js/all.js b/assets/js/all.js
index 286627a..09ccc60 100644
--- a/assets/js/all.js
+++ b/assets/js/all.js
@@ -113,7 +113,7 @@ Miniflux.Feed = (function() {
Miniflux.Item = (function() {
// timestamp of the latest item per feed ever seen
- var latest_feeds_items = [];
+ var latest_feeds_items = {};
// indicator for new unread items
var unreadItems = false;
@@ -474,27 +474,24 @@ Miniflux.Item = (function() {
var first_run = (latest_feeds_items.length === 0);
var current_unread = false;
var response = JSON.parse(this.responseText);
+ var last_items_timestamps = response['last_items_timestamps'];
- for (var feed_id in response['feeds']) {
- var current_feed = response['feeds'][feed_id];
+ for (var i = 0; i < last_items_timestamps.length; i++) {
+ var current_feed = last_items_timestamps[i];
+ var feed_id = current_feed.feed_id;
- if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.time > latest_feeds_items[feed_id]) {
- Miniflux.App.Log('feed ' + feed_id + ': New item(s)');
- latest_feeds_items[feed_id] = current_feed.time;
-
- if (current_feed.status === 'unread') {
- Miniflux.App.Log('feed ' + feed_id + ': New unread item(s)');
- current_unread = true;
- }
+ if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.updated > latest_feeds_items[feed_id]) {
+ latest_feeds_items[feed_id] = current_feed.updated;
+ current_unread = true;
}
}
Miniflux.App.Log('first_run: ' + first_run + ', current_unread: ' + current_unread + ', response.nbUnread: ' + response['nbUnread'] + ', nbUnreadItems: ' + nbUnreadItems);
- if (! document.hidden && (response['nbUnread'] !== nbUnreadItems || unreadItems)) {
+ if (! document.hidden && (response['nb_unread_items'] !== nbUnreadItems || unreadItems)) {
Miniflux.App.Log('Counter changed! Updating unread counter.');
unreadItems = false;
- nbUnreadItems = response['nbUnread'];
+ nbUnreadItems = response['nb_unread_items'];
updateCounters();
}
else if (document.hidden && ! first_run && current_unread) {
@@ -509,7 +506,7 @@ Miniflux.Item = (function() {
Miniflux.App.Log('unreadItems: ' + unreadItems);
};
- request.open("POST", "?action=latest-feeds-items", true);
+ request.open("GET", "?action=latest-feeds-items", true);
request.send();
}
};
diff --git a/assets/js/item.js b/assets/js/item.js
index d21b612..b3c5285 100644
--- a/assets/js/item.js
+++ b/assets/js/item.js
@@ -1,7 +1,7 @@
Miniflux.Item = (function() {
// timestamp of the latest item per feed ever seen
- var latest_feeds_items = [];
+ var latest_feeds_items = {};
// indicator for new unread items
var unreadItems = false;
@@ -362,27 +362,24 @@ Miniflux.Item = (function() {
var first_run = (latest_feeds_items.length === 0);
var current_unread = false;
var response = JSON.parse(this.responseText);
+ var last_items_timestamps = response['last_items_timestamps'];
- for (var feed_id in response['feeds']) {
- var current_feed = response['feeds'][feed_id];
+ for (var i = 0; i < last_items_timestamps.length; i++) {
+ var current_feed = last_items_timestamps[i];
+ var feed_id = current_feed.feed_id;
- if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.time > latest_feeds_items[feed_id]) {
- Miniflux.App.Log('feed ' + feed_id + ': New item(s)');
- latest_feeds_items[feed_id] = current_feed.time;
-
- if (current_feed.status === 'unread') {
- Miniflux.App.Log('feed ' + feed_id + ': New unread item(s)');
- current_unread = true;
- }
+ if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.updated > latest_feeds_items[feed_id]) {
+ latest_feeds_items[feed_id] = current_feed.updated;
+ current_unread = true;
}
}
Miniflux.App.Log('first_run: ' + first_run + ', current_unread: ' + current_unread + ', response.nbUnread: ' + response['nbUnread'] + ', nbUnreadItems: ' + nbUnreadItems);
- if (! document.hidden && (response['nbUnread'] !== nbUnreadItems || unreadItems)) {
+ if (! document.hidden && (response['nb_unread_items'] !== nbUnreadItems || unreadItems)) {
Miniflux.App.Log('Counter changed! Updating unread counter.');
unreadItems = false;
- nbUnreadItems = response['nbUnread'];
+ nbUnreadItems = response['nb_unread_items'];
updateCounters();
}
else if (document.hidden && ! first_run && current_unread) {
@@ -397,7 +394,7 @@ Miniflux.Item = (function() {
Miniflux.App.Log('unreadItems: ' + unreadItems);
};
- request.open("POST", "?action=latest-feeds-items", true);
+ request.open("GET", "?action=latest-feeds-items", true);
request.send();
}
};
diff --git a/composer.json b/composer.json
index 2f14600..605f731 100644
--- a/composer.json
+++ b/composer.json
@@ -26,13 +26,13 @@
},
"autoload": {
"files": [
- "app/schemas/sqlite.php",
"app/helpers/app.php",
"app/helpers/config.php",
"app/helpers/csrf.php",
"app/helpers/favicon.php",
"app/helpers/form.php",
"app/helpers/template.php",
+ "app/core/database.php",
"app/core/translator.php",
"app/core/request.php",
"app/core/response.php",
diff --git a/config.default.php b/config.default.php
index fea66a8..7234341 100644
--- a/config.default.php
+++ b/config.default.php
@@ -7,7 +7,7 @@ define('HTTP_TIMEOUT', '20');
define('HTTP_MAX_RESPONSE_SIZE', 2097152);
// DATA_DIRECTORY => default is data (writable directory)
-define('DATA_DIRECTORY', __DIR__.'/data');
+define('DATA_DIRECTORY', 'data');
// FAVICON_DIRECTORY => default is favicons (writable directory)
define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
@@ -15,8 +15,17 @@ define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
// FAVICON_URL_PATH => default is data/favicons/
define('FAVICON_URL_PATH', 'data/favicons');
-// DB_FILENAME => default value is db.sqlite (default database filename)
-define('DB_FILENAME', 'db.sqlite');
+// Database driver: "sqlite" or "postgres", default is sqlite
+define('DB_DRIVER', 'sqlite');
+
+// Database connection parameters when Postgres is used
+define('DB_HOSTNAME', 'localhost');
+define('DB_NAME', 'miniflux');
+define('DB_USERNAME', 'postgres');
+define('DB_PASSWORD', '');
+
+// DB_FILENAME => database file when Sqlite is used
+define('DB_FILENAME', DATA_DIRECTORY.'/db.sqlite');
// Enable/disable debug mode
define('DEBUG_MODE', false);
diff --git a/tests/phpunit.unit.postgres.xml b/tests/phpunit.unit.postgres.xml
new file mode 100644
index 0000000..880ccea
--- /dev/null
+++ b/tests/phpunit.unit.postgres.xml
@@ -0,0 +1,13 @@
+