Add Postgres database support

This commit is contained in:
Frederic Guillot 2016-12-26 15:07:18 -05:00
parent c8234cc3c3
commit fbe69b54dc
23 changed files with 322 additions and 128 deletions

View File

@ -16,4 +16,5 @@ before_script:
- composer install - composer install
script: 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

View File

@ -5,6 +5,7 @@
.PHONY: docker-run .PHONY: docker-run
.PHONY: js .PHONY: js
.PHONY: unit-test-sqlite .PHONY: unit-test-sqlite
.PHONY: unit-test-postgres
JS_FILE = assets/js/all.js JS_FILE = assets/js/all.js
CONTAINER = miniflux CONTAINER = miniflux
@ -39,4 +40,8 @@ archive:
@ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip @ git archive --format=zip --prefix=miniflux/ v${version} -o ${dst}/miniflux-${version}.zip
unit-test-sqlite: 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

View File

@ -10,30 +10,10 @@ require_once __DIR__.'/constants.php';
require_once __DIR__.'/check_setup.php'; require_once __DIR__.'/check_setup.php';
require_once __DIR__.'/functions.php'; require_once __DIR__.'/functions.php';
PicoDb\Database::setInstance('db', function() { PicoDb\Database::setInstance('db', function () {
$db = new PicoDb\Database(array( try {
'driver' => 'sqlite', return Miniflux\Database\get_connection();
'filename' => DB_FILENAME, } catch (Exception $e) {
)); die($e->getMessage());
$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, <strong>please copy and paste this message and create a bug report:</strong><hr/>';
$html .= '<pre><code>';
$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 .= '</code></pre>';
die($html);
} }
}); });

View File

@ -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_DIRECTORY') or define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons');
defined('FAVICON_URL_PATH') or define('FAVICON_URL_PATH', 'data/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_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_MODE') or define('DEBUG_MODE', false);
defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log'); defined('DEBUG_FILENAME') or define('DEBUG_FILENAME', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'debug.log');

View File

@ -197,7 +197,7 @@ Router\action('subscribe', function () {
$values['rtl'], $values['rtl'],
$values['cloak_referrer'], $values['cloak_referrer'],
$values['feed_group_ids'], $values['feed_group_ids'],
$values['groups'] $values['group_name']
); );
if ($feed_id >= 1) { if ($feed_id >= 1) {

View File

@ -222,21 +222,13 @@ Router\get_action('mark-item-removed', function () {
Response\redirect('?action='.$redirect.'&offset='.$offset.'&feed_id='.$feed_id); 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(); $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'); $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( Response\json(array(
'feeds' => $feeds, 'last_items_timestamps' => $items_timestamps,
'nbUnread' => $nb_unread_items 'nb_unread_items' => $nb_unread_items
)); ));
}); });

45
app/core/database.php Normal file
View File

@ -0,0 +1,45 @@
<?php
namespace Miniflux\Database;
use Miniflux\Schema;
use RuntimeException;
use PicoDb;
function get_connection()
{
$db = new PicoDb\Database(get_connection_parameters());
$db->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;
}

View File

@ -7,17 +7,20 @@ use Miniflux\Session\SessionStorage;
function config($parameter, $default = null) function config($parameter, $default = null)
{ {
$session = SessionStorage::getInstance();
$cache = $session->getConfig();
$value = null; $value = null;
$session = SessionStorage::getInstance();
if (empty($cache)) { if ($session->isLogged()) {
$cache = Model\Config\get_all($session->getUserId()); $cache = $session->getConfig();
$session->setConfig($cache);
}
if (array_key_exists($parameter, $cache)) { if (empty($cache)) {
$value = $cache[$parameter]; $cache = Model\Config\get_all($session->getUserId());
$session->setConfig($cache);
}
if (array_key_exists($parameter, $cache)) {
$value = $cache[$parameter];
}
} }
if ($value === null) { if ($value === null) {

View File

@ -46,26 +46,42 @@ function get_feeds($user_id)
return Database::getInstance('db') return Database::getInstance('db')
->table(TABLE) ->table(TABLE)
->eq('user_id', $user_id) ->eq('user_id', $user_id)
->desc('parsing_error')
->desc('enabled')
->asc('title') ->asc('title')
->findAll(); ->findAll();
} }
function get_feeds_with_items_count($user_id) function get_feeds_with_items_count($user_id)
{ {
return Database::getInstance('db') $feeds_count = array();
->table(TABLE) $feeds = get_feeds($user_id);
->columns( $feed_items = Database::getInstance('db')
'feeds.*', ->table(Item\TABLE)
'SUM(CASE WHEN items.status IN ("unread") THEN 1 ELSE 0 END) as "items_unread"', ->columns('feed_id', 'status', 'count(*) as nb')
'SUM(CASE WHEN items.status IN ("read", "unread") THEN 1 ELSE 0 END) as "items_total"' ->eq('user_id', $user_id)
) ->neq('status', Item\STATUS_REMOVED)
->join('items', 'feed_id', 'id') ->groupBy('feed_id', 'status')
->eq('feeds.user_id', $user_id)
->groupBy('feeds.id')
->desc('feeds.parsing_error')
->desc('feeds.enabled')
->asc('feeds.title')
->findAll(); ->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) function get_feed_ids($user_id, $limit = null)

View File

@ -297,19 +297,18 @@ function get_item_ids_by_status($user_id, $status)
->findAllByColumn('id'); ->findAllByColumn('id');
} }
function get_latest_feeds_items($user_id) function get_latest_unread_items_timestamps($user_id)
{ {
return Database::getInstance('db') return Database::getInstance('db')
->table(Feed\TABLE) ->table(TABLE)
->columns( ->columns(
'feeds.id', 'feed_id',
'MAX(items.updated) as updated', 'MAX(updated) as updated'
'items.status'
) )
->join(TABLE, 'feed_id', 'id') ->eq('user_id', $user_id)
->eq('feeds.user_id', $user_id) ->eq('status', STATUS_UNREAD)
->groupBy('feeds.id') ->groupBy('feed_id')
->orderBy('feeds.id') ->desc('updated')
->findAll(); ->findAll();
} }

133
app/schemas/postgres.php Normal file
View File

@ -0,0 +1,133 @@
<?php
namespace Miniflux\Schema;
use PDO;
use Miniflux\Helper;
const VERSION = 1;
function version_1(PDO $pdo)
{
$pdo->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),
));
}

View File

@ -11,7 +11,7 @@ function version_1(PDO $pdo)
{ {
$pdo->exec('CREATE TABLE users ( $pdo->exec('CREATE TABLE users (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
username TEXT UNIQUE, username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL, password TEXT NOT NULL,
is_admin INTEGER DEFAULT 0, is_admin INTEGER DEFAULT 0,
last_login INTEGER, last_login INTEGER,
@ -36,7 +36,7 @@ function version_1(PDO $pdo)
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
feed_url TEXT NOT NULL, feed_url TEXT NOT NULL,
site_url TEXT, site_url TEXT,
title TEXT COLLATE NOCASE, title TEXT COLLATE NOCASE NOT NULL,
last_checked INTEGER DEFAULT 0, last_checked INTEGER DEFAULT 0,
last_modified TEXT, last_modified TEXT,
etag TEXT, etag TEXT,
@ -54,10 +54,10 @@ function version_1(PDO $pdo)
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
feed_id INTEGER NOT NULL, feed_id INTEGER NOT NULL,
checksum TEXT NOT NULL, checksum TEXT NOT NULL,
status TEXT, status TEXT NOT NULL,
bookmark INTEGER DEFAULT 0, bookmark INTEGER DEFAULT 0,
url TEXT, url TEXT NOT NULL,
title TEXT COLLATE NOCASE, title TEXT COLLATE NOCASE NOT NULL,
author TEXT, author TEXT,
content TEXT, content TEXT,
updated INTEGER, updated INTEGER,
@ -73,13 +73,13 @@ function version_1(PDO $pdo)
$pdo->exec('CREATE TABLE "groups" ( $pdo->exec('CREATE TABLE "groups" (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL, 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, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(user_id, title) UNIQUE(user_id, title)
)'); )');
$pdo->exec('CREATE TABLE "feeds_groups" ( $pdo->exec('CREATE TABLE "feeds_groups" (
feed_id INTEGER NOT NULL, feed_id INTEGER NOT NULL,
group_id INTEGER NOT NULL, group_id INTEGER NOT NULL,
PRIMARY KEY(feed_id, group_id), PRIMARY KEY(feed_id, group_id),
FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE, FOREIGN KEY(group_id) REFERENCES groups(id) ON DELETE CASCADE,

View File

@ -113,7 +113,7 @@ Miniflux.Feed = (function() {
Miniflux.Item = (function() { Miniflux.Item = (function() {
// timestamp of the latest item per feed ever seen // timestamp of the latest item per feed ever seen
var latest_feeds_items = []; var latest_feeds_items = {};
// indicator for new unread items // indicator for new unread items
var unreadItems = false; var unreadItems = false;
@ -474,27 +474,24 @@ Miniflux.Item = (function() {
var first_run = (latest_feeds_items.length === 0); var first_run = (latest_feeds_items.length === 0);
var current_unread = false; var current_unread = false;
var response = JSON.parse(this.responseText); var response = JSON.parse(this.responseText);
var last_items_timestamps = response['last_items_timestamps'];
for (var feed_id in response['feeds']) { for (var i = 0; i < last_items_timestamps.length; i++) {
var current_feed = response['feeds'][feed_id]; 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]) { if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.updated > latest_feeds_items[feed_id]) {
Miniflux.App.Log('feed ' + feed_id + ': New item(s)'); latest_feeds_items[feed_id] = current_feed.updated;
latest_feeds_items[feed_id] = current_feed.time; current_unread = true;
if (current_feed.status === 'unread') {
Miniflux.App.Log('feed ' + feed_id + ': New unread item(s)');
current_unread = true;
}
} }
} }
Miniflux.App.Log('first_run: ' + first_run + ', current_unread: ' + current_unread + ', response.nbUnread: ' + response['nbUnread'] + ', nbUnreadItems: ' + nbUnreadItems); 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.'); Miniflux.App.Log('Counter changed! Updating unread counter.');
unreadItems = false; unreadItems = false;
nbUnreadItems = response['nbUnread']; nbUnreadItems = response['nb_unread_items'];
updateCounters(); updateCounters();
} }
else if (document.hidden && ! first_run && current_unread) { else if (document.hidden && ! first_run && current_unread) {
@ -509,7 +506,7 @@ Miniflux.Item = (function() {
Miniflux.App.Log('unreadItems: ' + unreadItems); Miniflux.App.Log('unreadItems: ' + unreadItems);
}; };
request.open("POST", "?action=latest-feeds-items", true); request.open("GET", "?action=latest-feeds-items", true);
request.send(); request.send();
} }
}; };

View File

@ -1,7 +1,7 @@
Miniflux.Item = (function() { Miniflux.Item = (function() {
// timestamp of the latest item per feed ever seen // timestamp of the latest item per feed ever seen
var latest_feeds_items = []; var latest_feeds_items = {};
// indicator for new unread items // indicator for new unread items
var unreadItems = false; var unreadItems = false;
@ -362,27 +362,24 @@ Miniflux.Item = (function() {
var first_run = (latest_feeds_items.length === 0); var first_run = (latest_feeds_items.length === 0);
var current_unread = false; var current_unread = false;
var response = JSON.parse(this.responseText); var response = JSON.parse(this.responseText);
var last_items_timestamps = response['last_items_timestamps'];
for (var feed_id in response['feeds']) { for (var i = 0; i < last_items_timestamps.length; i++) {
var current_feed = response['feeds'][feed_id]; 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]) { if (! latest_feeds_items.hasOwnProperty(feed_id) || current_feed.updated > latest_feeds_items[feed_id]) {
Miniflux.App.Log('feed ' + feed_id + ': New item(s)'); latest_feeds_items[feed_id] = current_feed.updated;
latest_feeds_items[feed_id] = current_feed.time; current_unread = true;
if (current_feed.status === 'unread') {
Miniflux.App.Log('feed ' + feed_id + ': New unread item(s)');
current_unread = true;
}
} }
} }
Miniflux.App.Log('first_run: ' + first_run + ', current_unread: ' + current_unread + ', response.nbUnread: ' + response['nbUnread'] + ', nbUnreadItems: ' + nbUnreadItems); 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.'); Miniflux.App.Log('Counter changed! Updating unread counter.');
unreadItems = false; unreadItems = false;
nbUnreadItems = response['nbUnread']; nbUnreadItems = response['nb_unread_items'];
updateCounters(); updateCounters();
} }
else if (document.hidden && ! first_run && current_unread) { else if (document.hidden && ! first_run && current_unread) {
@ -397,7 +394,7 @@ Miniflux.Item = (function() {
Miniflux.App.Log('unreadItems: ' + unreadItems); Miniflux.App.Log('unreadItems: ' + unreadItems);
}; };
request.open("POST", "?action=latest-feeds-items", true); request.open("GET", "?action=latest-feeds-items", true);
request.send(); request.send();
} }
}; };

View File

@ -26,13 +26,13 @@
}, },
"autoload": { "autoload": {
"files": [ "files": [
"app/schemas/sqlite.php",
"app/helpers/app.php", "app/helpers/app.php",
"app/helpers/config.php", "app/helpers/config.php",
"app/helpers/csrf.php", "app/helpers/csrf.php",
"app/helpers/favicon.php", "app/helpers/favicon.php",
"app/helpers/form.php", "app/helpers/form.php",
"app/helpers/template.php", "app/helpers/template.php",
"app/core/database.php",
"app/core/translator.php", "app/core/translator.php",
"app/core/request.php", "app/core/request.php",
"app/core/response.php", "app/core/response.php",

View File

@ -7,7 +7,7 @@ define('HTTP_TIMEOUT', '20');
define('HTTP_MAX_RESPONSE_SIZE', 2097152); define('HTTP_MAX_RESPONSE_SIZE', 2097152);
// DATA_DIRECTORY => default is data (writable directory) // DATA_DIRECTORY => default is data (writable directory)
define('DATA_DIRECTORY', __DIR__.'/data'); define('DATA_DIRECTORY', 'data');
// FAVICON_DIRECTORY => default is favicons (writable directory) // FAVICON_DIRECTORY => default is favicons (writable directory)
define('FAVICON_DIRECTORY', DATA_DIRECTORY.DIRECTORY_SEPARATOR.'favicons'); 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/ // FAVICON_URL_PATH => default is data/favicons/
define('FAVICON_URL_PATH', 'data/favicons'); define('FAVICON_URL_PATH', 'data/favicons');
// DB_FILENAME => default value is db.sqlite (default database filename) // Database driver: "sqlite" or "postgres", default is sqlite
define('DB_FILENAME', 'db.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 // Enable/disable debug mode
define('DEBUG_MODE', false); define('DEBUG_MODE', false);

View File

@ -0,0 +1,13 @@
<phpunit stopOnError="true" stopOnFailure="true" colors="true">
<testsuites>
<testsuite name="Miniflux Postgres Unit Tests">
<directory>unit</directory>
</testsuite>
</testsuites>
<php>
<const name="DB_DRIVER" value="postgres" />
<const name="DB_USERNAME" value="postgres" />
<const name="DB_PASSWORD" value="" />
<const name="DB_NAME" value="miniflux_unit_test" />
</php>
</phpunit>

View File

@ -1,6 +1,6 @@
<phpunit stopOnError="true" stopOnFailure="true" colors="true"> <phpunit stopOnError="true" stopOnFailure="true" colors="true">
<testsuites> <testsuites>
<testsuite name="Miniflux Unit Tests"> <testsuite name="Miniflux Sqlite Unit Tests">
<directory>unit</directory> <directory>unit</directory>
</testsuite> </testsuite>
</testsuites> </testsuites>

View File

@ -12,20 +12,18 @@ abstract class BaseTest extends PHPUnit_Framework_TestCase
{ {
public function setUp() public function setUp()
{ {
SessionStorage::getInstance()->flush(); if (DB_DRIVER === 'postgres') {
$pdo = new PDO('pgsql:host='.DB_HOSTNAME, DB_USERNAME, DB_PASSWORD);
$pdo->exec('DROP DATABASE '.DB_NAME);
$pdo->exec('CREATE DATABASE '.DB_NAME.' WITH OWNER '.DB_USERNAME);
$pdo = null;
}
PicoDb\Database::setInstance('db', function () { PicoDb\Database::setInstance('db', function () {
$db = new PicoDb\Database(array( return Miniflux\Database\get_connection();
'driver' => 'sqlite',
'filename' => DB_FILENAME,
));
$db->getStatementHandler()->withLogging();
if (! $db->schema('\Miniflux\Schema')->check(Miniflux\Schema\VERSION)) {
var_dump($db->getLogMessages());
}
return $db;
}); });
SessionStorage::getInstance()->flush();
} }
public function tearDown() public function tearDown()

View File

@ -26,10 +26,10 @@ class FeedModelTest extends BaseTest
$this->assertEquals('feed url', $subscription['feed_url']); $this->assertEquals('feed url', $subscription['feed_url']);
$this->assertEquals('etag', $subscription['etag']); $this->assertEquals('etag', $subscription['etag']);
$this->assertEquals('last modified', $subscription['last_modified']); $this->assertEquals('last modified', $subscription['last_modified']);
$this->assertEquals('0', $subscription['download_content']); $this->assertFalse((bool) $subscription['download_content']);
$this->assertEquals('0', $subscription['rtl']); $this->assertFalse((bool) $subscription['rtl']);
$this->assertEquals('0', $subscription['cloak_referrer']); $this->assertFalse((bool) $subscription['cloak_referrer']);
$this->assertEquals('1', $subscription['enabled']); $this->assertTrue((bool) $subscription['enabled']);
} }
public function testGetAll() public function testGetAll()

View File

@ -27,7 +27,7 @@ class ItemModelTest extends BaseTest
$this->assertEquals('', $item['enclosure_type']); $this->assertEquals('', $item['enclosure_type']);
$this->assertEquals('', $item['language']); $this->assertEquals('', $item['language']);
$item = Model\Item\get_item(2, 'ID 1'); $item = Model\Item\get_item(2, 1);
$this->assertNull($item); $this->assertNull($item);
} }

View File

@ -7,13 +7,13 @@ $baseDir = dirname($vendorDir);
return array( return array(
'e40631d46120a9c38ea139981f8dab26' => $vendorDir . '/ircmaxell/password-compat/lib/password.php', 'e40631d46120a9c38ea139981f8dab26' => $vendorDir . '/ircmaxell/password-compat/lib/password.php',
'1c6478a893aa3a9ae898668d7af7b441' => $baseDir . '/app/schemas/sqlite.php',
'f0aaf41fd213d5a97cf56c060c89445c' => $baseDir . '/app/helpers/app.php', 'f0aaf41fd213d5a97cf56c060c89445c' => $baseDir . '/app/helpers/app.php',
'640b6eca1c41bc50addc30eb8c8e3b5b' => $baseDir . '/app/helpers/config.php', '640b6eca1c41bc50addc30eb8c8e3b5b' => $baseDir . '/app/helpers/config.php',
'5d8e3de7e01942090f1b7b4dbdfb5577' => $baseDir . '/app/helpers/csrf.php', '5d8e3de7e01942090f1b7b4dbdfb5577' => $baseDir . '/app/helpers/csrf.php',
'3f7c6586b45b98746d18450089390313' => $baseDir . '/app/helpers/favicon.php', '3f7c6586b45b98746d18450089390313' => $baseDir . '/app/helpers/favicon.php',
'e96e91c85d691b966391981f72a31834' => $baseDir . '/app/helpers/form.php', 'e96e91c85d691b966391981f72a31834' => $baseDir . '/app/helpers/form.php',
'b8d3001d29a919647064eeaec3f6551e' => $baseDir . '/app/helpers/template.php', 'b8d3001d29a919647064eeaec3f6551e' => $baseDir . '/app/helpers/template.php',
'a79069799c6e73d978adffa1caf9894d' => $baseDir . '/app/core/database.php',
'7793918f03299c5c8c900e412e166b4f' => $baseDir . '/app/core/translator.php', '7793918f03299c5c8c900e412e166b4f' => $baseDir . '/app/core/translator.php',
'69d59b59e8b15a53cc782e283523018d' => $baseDir . '/app/core/request.php', '69d59b59e8b15a53cc782e283523018d' => $baseDir . '/app/core/request.php',
'3fd802226a33a97ae29d1f8315ff66a9' => $baseDir . '/app/core/response.php', '3fd802226a33a97ae29d1f8315ff66a9' => $baseDir . '/app/core/response.php',

View File

@ -8,13 +8,13 @@ class ComposerStaticInitfd7e8d436e1dc450edc3153ac8bc31b4
{ {
public static $files = array ( public static $files = array (
'e40631d46120a9c38ea139981f8dab26' => __DIR__ . '/..' . '/ircmaxell/password-compat/lib/password.php', 'e40631d46120a9c38ea139981f8dab26' => __DIR__ . '/..' . '/ircmaxell/password-compat/lib/password.php',
'1c6478a893aa3a9ae898668d7af7b441' => __DIR__ . '/../..' . '/app/schemas/sqlite.php',
'f0aaf41fd213d5a97cf56c060c89445c' => __DIR__ . '/../..' . '/app/helpers/app.php', 'f0aaf41fd213d5a97cf56c060c89445c' => __DIR__ . '/../..' . '/app/helpers/app.php',
'640b6eca1c41bc50addc30eb8c8e3b5b' => __DIR__ . '/../..' . '/app/helpers/config.php', '640b6eca1c41bc50addc30eb8c8e3b5b' => __DIR__ . '/../..' . '/app/helpers/config.php',
'5d8e3de7e01942090f1b7b4dbdfb5577' => __DIR__ . '/../..' . '/app/helpers/csrf.php', '5d8e3de7e01942090f1b7b4dbdfb5577' => __DIR__ . '/../..' . '/app/helpers/csrf.php',
'3f7c6586b45b98746d18450089390313' => __DIR__ . '/../..' . '/app/helpers/favicon.php', '3f7c6586b45b98746d18450089390313' => __DIR__ . '/../..' . '/app/helpers/favicon.php',
'e96e91c85d691b966391981f72a31834' => __DIR__ . '/../..' . '/app/helpers/form.php', 'e96e91c85d691b966391981f72a31834' => __DIR__ . '/../..' . '/app/helpers/form.php',
'b8d3001d29a919647064eeaec3f6551e' => __DIR__ . '/../..' . '/app/helpers/template.php', 'b8d3001d29a919647064eeaec3f6551e' => __DIR__ . '/../..' . '/app/helpers/template.php',
'a79069799c6e73d978adffa1caf9894d' => __DIR__ . '/../..' . '/app/core/database.php',
'7793918f03299c5c8c900e412e166b4f' => __DIR__ . '/../..' . '/app/core/translator.php', '7793918f03299c5c8c900e412e166b4f' => __DIR__ . '/../..' . '/app/core/translator.php',
'69d59b59e8b15a53cc782e283523018d' => __DIR__ . '/../..' . '/app/core/request.php', '69d59b59e8b15a53cc782e283523018d' => __DIR__ . '/../..' . '/app/core/request.php',
'3fd802226a33a97ae29d1f8315ff66a9' => __DIR__ . '/../..' . '/app/core/response.php', '3fd802226a33a97ae29d1f8315ff66a9' => __DIR__ . '/../..' . '/app/core/response.php',